]> git.rkrishnan.org Git - tahoe-lafs/tahoe-lafs.git/blob - src/allmydata/uri.py
hush pyflakes-0.4.0 warnings: remove trivial unused variables. For #900.
[tahoe-lafs/tahoe-lafs.git] / src / allmydata / uri.py
1
2 import re, urllib
3 from zope.interface import implements
4 from twisted.python.components import registerAdapter
5 from allmydata.storage.server import si_a2b, si_b2a
6 from allmydata.util import base32, hashutil
7 from allmydata.interfaces import IURI, IDirnodeURI, IFileURI, IImmutableFileURI, \
8     IVerifierURI, IMutableFileURI, IDirectoryURI, IReadonlyDirectoryURI
9
10 class BadURIError(Exception):
11     pass
12
13 # the URI shall be an ascii representation of the file. It shall contain
14 # enough information to retrieve and validate the contents. It shall be
15 # expressed in a limited character set (namely [TODO]).
16
17 BASE32STR_128bits = '(%s{25}%s)' % (base32.BASE32CHAR, base32.BASE32CHAR_3bits)
18 BASE32STR_256bits = '(%s{51}%s)' % (base32.BASE32CHAR, base32.BASE32CHAR_1bits)
19
20 SEP='(?::|%3A)'
21 NUMBER='([0-9]+)'
22 NUMBER_IGNORE='(?:[0-9]+)'
23
24 # URIs (soon to be renamed "caps") are always allowed to come with a leading
25 # 'http://127.0.0.1:(8123|3456)/uri/' that will be ignored.
26 OPTIONALHTTPLEAD=r'(?:https?://(?:[^:/]+)(?::%s)?/uri/)?' % NUMBER_IGNORE
27
28
29 class _BaseURI:
30     def __hash__(self):
31         return self.to_string().__hash__()
32     def __eq__(self, them):
33         if isinstance(them, _BaseURI):
34             return self.to_string() == them.to_string()
35         else:
36             return False
37     def __ne__(self, them):
38         if isinstance(them, _BaseURI):
39             return self.to_string() != them.to_string()
40         else:
41             return True
42     def to_human_encoding(self):
43         return 'http://127.0.0.1:3456/uri/'+self.to_string()
44
45     def get_storage_index(self):
46         return self.storage_index
47
48 class CHKFileURI(_BaseURI):
49     implements(IURI, IImmutableFileURI)
50
51     BASE_STRING='URI:CHK:'
52     STRING_RE=re.compile('^URI:CHK:'+BASE32STR_128bits+':'+
53                          BASE32STR_256bits+':'+NUMBER+':'+NUMBER+':'+NUMBER+
54                          '$')
55     HUMAN_RE=re.compile('^'+OPTIONALHTTPLEAD+'URI'+SEP+'CHK'+SEP+
56                      BASE32STR_128bits+SEP+BASE32STR_256bits+SEP+NUMBER+
57                      SEP+NUMBER+SEP+NUMBER+'$')
58
59     def __init__(self, key, uri_extension_hash, needed_shares, total_shares,
60                  size):
61         self.key = key
62         self.uri_extension_hash = uri_extension_hash
63         self.needed_shares = needed_shares
64         self.total_shares = total_shares
65         self.size = size
66         self.storage_index = hashutil.storage_index_hash(self.key)
67         if not len(self.storage_index) == 16: # sha256 hash truncated to 128
68             raise BadURIError("storage index must be 16 bytes long")
69
70     @classmethod
71     def init_from_human_encoding(cls, uri):
72         mo = cls.HUMAN_RE.search(uri)
73         if not mo:
74             raise BadURIError("%s doesn't look like a cap" % (uri,))
75         return cls(base32.a2b(mo.group(1)), base32.a2b(mo.group(2)),
76                    int(mo.group(3)), int(mo.group(4)), int(mo.group(5)))
77
78     @classmethod
79     def init_from_string(cls, uri):
80         mo = cls.STRING_RE.search(uri)
81         if not mo:
82             raise BadURIError("%s doesn't look like a cap" % (uri,))
83         return cls(base32.a2b(mo.group(1)), base32.a2b(mo.group(2)),
84                    int(mo.group(3)), int(mo.group(4)), int(mo.group(5)))
85
86     def to_string(self):
87         assert isinstance(self.needed_shares, int)
88         assert isinstance(self.total_shares, int)
89         assert isinstance(self.size, (int,long))
90
91         return ('URI:CHK:%s:%s:%d:%d:%d' %
92                 (base32.b2a(self.key),
93                  base32.b2a(self.uri_extension_hash),
94                  self.needed_shares,
95                  self.total_shares,
96                  self.size))
97
98     def is_readonly(self):
99         return True
100     def is_mutable(self):
101         return False
102     def get_readonly(self):
103         return self
104
105     def get_size(self):
106         return self.size
107
108     def get_verify_cap(self):
109         return CHKFileVerifierURI(storage_index=self.storage_index,
110                                   uri_extension_hash=self.uri_extension_hash,
111                                   needed_shares=self.needed_shares,
112                                   total_shares=self.total_shares,
113                                   size=self.size)
114
115 class CHKFileVerifierURI(_BaseURI):
116     implements(IVerifierURI)
117
118     BASE_STRING='URI:CHK-Verifier:'
119     STRING_RE=re.compile('^URI:CHK-Verifier:'+BASE32STR_128bits+':'+
120                          BASE32STR_256bits+':'+NUMBER+':'+NUMBER+':'+NUMBER)
121     HUMAN_RE=re.compile('^'+OPTIONALHTTPLEAD+'URI'+SEP+'CHK-Verifier'+SEP+
122                         BASE32STR_128bits+SEP+BASE32STR_256bits+SEP+NUMBER+
123                         SEP+NUMBER+SEP+NUMBER)
124
125     def __init__(self, storage_index, uri_extension_hash,
126                  needed_shares, total_shares, size):
127         assert len(storage_index) == 16
128         self.storage_index = storage_index
129         self.uri_extension_hash = uri_extension_hash
130         self.needed_shares = needed_shares
131         self.total_shares = total_shares
132         self.size = size
133
134     @classmethod
135     def init_from_human_encoding(cls, uri):
136         mo = cls.HUMAN_RE.search(uri)
137         assert mo, uri
138         return cls(base32.a2b(mo.group(1)), base32.a2b(mo.group(2)),
139                    int(mo.group(3)), int(mo.group(4)), int(mo.group(5)))
140
141     @classmethod
142     def init_from_string(cls, uri):
143         mo = cls.STRING_RE.search(uri)
144         assert mo, (uri, cls, cls.STRING_RE)
145         return cls(si_a2b(mo.group(1)), base32.a2b(mo.group(2)),
146                    int(mo.group(3)), int(mo.group(4)), int(mo.group(5)))
147
148     def to_string(self):
149         assert isinstance(self.needed_shares, int)
150         assert isinstance(self.total_shares, int)
151         assert isinstance(self.size, (int,long))
152
153         return ('URI:CHK-Verifier:%s:%s:%d:%d:%d' %
154                 (si_b2a(self.storage_index),
155                  base32.b2a(self.uri_extension_hash),
156                  self.needed_shares,
157                  self.total_shares,
158                  self.size))
159
160
161 class LiteralFileURI(_BaseURI):
162     implements(IURI, IImmutableFileURI)
163
164     BASE_STRING='URI:LIT:'
165     STRING_RE=re.compile('^URI:LIT:'+base32.BASE32STR_anybytes+'$')
166     HUMAN_RE=re.compile('^'+OPTIONALHTTPLEAD+'URI'+SEP+'LIT'+SEP+base32.BASE32STR_anybytes+'$')
167
168     def __init__(self, data=None):
169         if data is not None:
170             assert isinstance(data, str)
171             self.data = data
172
173     @classmethod
174     def init_from_human_encoding(cls, uri):
175         mo = cls.HUMAN_RE.search(uri)
176         assert mo, uri
177         return cls(base32.a2b(mo.group(1)))
178
179     @classmethod
180     def init_from_string(cls, uri):
181         mo = cls.STRING_RE.search(uri)
182         assert mo, uri
183         return cls(base32.a2b(mo.group(1)))
184
185     def to_string(self):
186         return 'URI:LIT:%s' % base32.b2a(self.data)
187
188     def is_readonly(self):
189         return True
190     def is_mutable(self):
191         return False
192     def get_readonly(self):
193         return self
194     def get_storage_index(self):
195         return None
196
197     def get_verify_cap(self):
198         # LIT files need no verification, all the data is present in the URI
199         return None
200
201     def get_size(self):
202         return len(self.data)
203
204 class WriteableSSKFileURI(_BaseURI):
205     implements(IURI, IMutableFileURI)
206
207     BASE_STRING='URI:SSK:'
208     STRING_RE=re.compile('^'+BASE_STRING+BASE32STR_128bits+':'+
209                          BASE32STR_256bits+'$')
210     HUMAN_RE=re.compile('^'+OPTIONALHTTPLEAD+'URI'+SEP+'SSK'+SEP+
211                         BASE32STR_128bits+SEP+BASE32STR_256bits+'$')
212
213     def __init__(self, writekey, fingerprint):
214         self.writekey = writekey
215         self.readkey = hashutil.ssk_readkey_hash(writekey)
216         self.storage_index = hashutil.ssk_storage_index_hash(self.readkey)
217         assert len(self.storage_index) == 16
218         self.fingerprint = fingerprint
219
220     @classmethod
221     def init_from_human_encoding(cls, uri):
222         mo = cls.HUMAN_RE.search(uri)
223         if not mo:
224             raise BadURIError("'%s' doesn't look like a cap" % (uri,))
225         return cls(base32.a2b(mo.group(1)), base32.a2b(mo.group(2)))
226
227     @classmethod
228     def init_from_string(cls, uri):
229         mo = cls.STRING_RE.search(uri)
230         if not mo:
231             raise BadURIError("'%s' doesn't look like a %s cap" % (uri, cls))
232         return cls(base32.a2b(mo.group(1)), base32.a2b(mo.group(2)))
233
234     def to_string(self):
235         assert isinstance(self.writekey, str)
236         assert isinstance(self.fingerprint, str)
237         return 'URI:SSK:%s:%s' % (base32.b2a(self.writekey),
238                                   base32.b2a(self.fingerprint))
239
240     def __repr__(self):
241         return "<%s %s>" % (self.__class__.__name__, self.abbrev())
242
243     def abbrev(self):
244         return base32.b2a(self.writekey[:5])
245     def abbrev_si(self):
246         return base32.b2a(self.storage_index)[:5]
247
248     def is_readonly(self):
249         return False
250     def is_mutable(self):
251         return True
252     def get_readonly(self):
253         return ReadonlySSKFileURI(self.readkey, self.fingerprint)
254     def get_verify_cap(self):
255         return SSKVerifierURI(self.storage_index, self.fingerprint)
256
257 class ReadonlySSKFileURI(_BaseURI):
258     implements(IURI, IMutableFileURI)
259
260     BASE_STRING='URI:SSK-RO:'
261     STRING_RE=re.compile('^URI:SSK-RO:'+BASE32STR_128bits+':'+BASE32STR_256bits+'$')
262     HUMAN_RE=re.compile('^'+OPTIONALHTTPLEAD+'URI'+SEP+'SSK-RO'+SEP+BASE32STR_128bits+SEP+BASE32STR_256bits+'$')
263
264     def __init__(self, readkey, fingerprint):
265         self.readkey = readkey
266         self.storage_index = hashutil.ssk_storage_index_hash(self.readkey)
267         assert len(self.storage_index) == 16
268         self.fingerprint = fingerprint
269
270     @classmethod
271     def init_from_human_encoding(cls, uri):
272         mo = cls.HUMAN_RE.search(uri)
273         if not mo:
274             raise BadURIError("'%s' doesn't look like a cap" % (uri,))
275         return cls(base32.a2b(mo.group(1)), base32.a2b(mo.group(2)))
276
277     @classmethod
278     def init_from_string(cls, uri):
279         mo = cls.STRING_RE.search(uri)
280         if not mo:
281             raise BadURIError("'%s' doesn't look like a cap" % (uri,))
282         return cls(base32.a2b(mo.group(1)), base32.a2b(mo.group(2)))
283
284     def to_string(self):
285         assert isinstance(self.readkey, str)
286         assert isinstance(self.fingerprint, str)
287         return 'URI:SSK-RO:%s:%s' % (base32.b2a(self.readkey),
288                                      base32.b2a(self.fingerprint))
289
290     def __repr__(self):
291         return "<%s %s>" % (self.__class__.__name__, self.abbrev())
292
293     def abbrev(self):
294         return base32.b2a(self.readkey[:5])
295     def abbrev_si(self):
296         return base32.b2a(self.storage_index)[:5]
297
298     def is_readonly(self):
299         return True
300     def is_mutable(self):
301         return True
302     def get_readonly(self):
303         return self
304     def get_verify_cap(self):
305         return SSKVerifierURI(self.storage_index, self.fingerprint)
306
307 class SSKVerifierURI(_BaseURI):
308     implements(IVerifierURI)
309
310     BASE_STRING='URI:SSK-Verifier:'
311     STRING_RE=re.compile('^'+BASE_STRING+BASE32STR_128bits+':'+BASE32STR_256bits+'$')
312     HUMAN_RE=re.compile('^'+OPTIONALHTTPLEAD+'URI'+SEP+'SSK-Verifier'+SEP+BASE32STR_128bits+SEP+BASE32STR_256bits+'$')
313
314     def __init__(self, storage_index, fingerprint):
315         assert len(storage_index) == 16
316         self.storage_index = storage_index
317         self.fingerprint = fingerprint
318
319     @classmethod
320     def init_from_human_encoding(cls, uri):
321         mo = cls.HUMAN_RE.search(uri)
322         assert mo, uri
323         return cls(si_a2b(mo.group(1)), base32.a2b(mo.group(2)))
324
325     @classmethod
326     def init_from_string(cls, uri):
327         mo = cls.STRING_RE.search(uri)
328         assert mo, (uri, cls)
329         return cls(si_a2b(mo.group(1)), base32.a2b(mo.group(2)))
330
331     def to_string(self):
332         assert isinstance(self.storage_index, str)
333         assert isinstance(self.fingerprint, str)
334         return 'URI:SSK-Verifier:%s:%s' % (si_b2a(self.storage_index),
335                                            base32.b2a(self.fingerprint))
336
337 class _DirectoryBaseURI(_BaseURI):
338     implements(IURI, IDirnodeURI)
339     def __init__(self, filenode_uri=None):
340         self._filenode_uri = filenode_uri
341
342     def __repr__(self):
343         return "<%s %s>" % (self.__class__.__name__, self.abbrev())
344
345     @classmethod
346     def init_from_string(cls, uri):
347         mo = cls.BASE_STRING_RE.search(uri)
348         if not mo:
349             raise BadURIError("'%s' doesn't look like a %s cap" % (uri, cls))
350         bits = uri[mo.end():]
351         fn = cls.INNER_URI_CLASS.init_from_string(
352             cls.INNER_URI_CLASS.BASE_STRING+bits)
353         return cls(fn)
354
355     @classmethod
356     def init_from_human_encoding(cls, uri):
357         mo = cls.BASE_HUMAN_RE.search(uri)
358         if not mo:
359             raise BadURIError("'%s' doesn't look like a %s cap" % (uri, cls))
360         bits = uri[mo.end():]
361         while bits and bits[-1] == '/':
362             bits = bits[:-1]
363         fn = cls.INNER_URI_CLASS.init_from_string(
364             cls.INNER_URI_CLASS.BASE_STRING+urllib.unquote(bits))
365         return cls(fn)
366
367     def to_string(self):
368         fnuri = self._filenode_uri.to_string()
369         mo = re.match(self.INNER_URI_CLASS.BASE_STRING, fnuri)
370         assert mo, fnuri
371         bits = fnuri[mo.end():]
372         return self.BASE_STRING+bits
373
374     def abbrev(self):
375         return self._filenode_uri.to_string().split(':')[2][:5]
376     def abbrev_si(self):
377         return base32.b2a(self._filenode_uri.storage_index)[:5]
378
379     def get_filenode_cap(self):
380         return self._filenode_uri
381
382     def is_mutable(self):
383         return True
384
385     def get_verify_cap(self):
386         return DirectoryURIVerifier(self._filenode_uri.get_verify_cap())
387
388     def get_storage_index(self):
389         return self._filenode_uri.get_storage_index()
390
391 class DirectoryURI(_DirectoryBaseURI):
392     implements(IDirectoryURI)
393
394     BASE_STRING='URI:DIR2:'
395     BASE_STRING_RE=re.compile('^'+BASE_STRING)
396     BASE_HUMAN_RE=re.compile('^'+OPTIONALHTTPLEAD+'URI'+SEP+'DIR2'+SEP)
397     INNER_URI_CLASS=WriteableSSKFileURI
398
399     def __init__(self, filenode_uri=None):
400         if filenode_uri:
401             assert not filenode_uri.is_readonly()
402         _DirectoryBaseURI.__init__(self, filenode_uri)
403
404     def is_readonly(self):
405         return False
406
407     def get_readonly(self):
408         return ReadonlyDirectoryURI(self._filenode_uri.get_readonly())
409
410 class ReadonlyDirectoryURI(_DirectoryBaseURI):
411     implements(IReadonlyDirectoryURI)
412
413     BASE_STRING='URI:DIR2-RO:'
414     BASE_STRING_RE=re.compile('^'+BASE_STRING)
415     BASE_HUMAN_RE=re.compile('^'+OPTIONALHTTPLEAD+'URI'+SEP+'DIR2-RO'+SEP)
416     INNER_URI_CLASS=ReadonlySSKFileURI
417
418     def __init__(self, filenode_uri=None):
419         if filenode_uri:
420             assert filenode_uri.is_readonly()
421         _DirectoryBaseURI.__init__(self, filenode_uri)
422
423     def is_readonly(self):
424         return True
425
426     def get_readonly(self):
427         return self
428
429 class _ImmutableDirectoryBaseURI(_DirectoryBaseURI):
430     def __init__(self, filenode_uri=None):
431         if filenode_uri:
432             assert isinstance(filenode_uri, self.INNER_URI_CLASS), filenode_uri
433         _DirectoryBaseURI.__init__(self, filenode_uri)
434
435     def is_mutable(self):
436         return False
437
438     def is_readonly(self):
439         return True
440
441     def get_readonly(self):
442         return self
443
444 class ImmutableDirectoryURI(_ImmutableDirectoryBaseURI):
445     BASE_STRING='URI:DIR2-CHK:'
446     BASE_STRING_RE=re.compile('^'+BASE_STRING)
447     BASE_HUMAN_RE=re.compile('^'+OPTIONALHTTPLEAD+'URI'+SEP+'DIR2-CHK'+SEP)
448     INNER_URI_CLASS=CHKFileURI
449     def get_verify_cap(self):
450         vcap = self._filenode_uri.get_verify_cap()
451         return ImmutableDirectoryURIVerifier(vcap)
452
453
454 class LiteralDirectoryURI(_ImmutableDirectoryBaseURI):
455     BASE_STRING='URI:DIR2-LIT:'
456     BASE_STRING_RE=re.compile('^'+BASE_STRING)
457     BASE_HUMAN_RE=re.compile('^'+OPTIONALHTTPLEAD+'URI'+SEP+'DIR2-LIT'+SEP)
458     INNER_URI_CLASS=LiteralFileURI
459     def get_verify_cap(self):
460         # LIT caps have no verifier, since they aren't distributed
461         return None
462
463 def wrap_dirnode_cap(filecap):
464     if isinstance(filecap, WriteableSSKFileURI):
465         return DirectoryURI(filecap)
466     if isinstance(filecap, ReadonlySSKFileURI):
467         return ReadonlyDirectoryURI(filecap)
468     if isinstance(filecap, CHKFileURI):
469         return ImmutableDirectoryURI(filecap)
470     if isinstance(filecap, LiteralFileURI):
471         return LiteralDirectoryURI(filecap)
472     assert False, "cannot wrap a dirnode around %s" % filecap.__class__
473
474 class DirectoryURIVerifier(_DirectoryBaseURI):
475     implements(IVerifierURI)
476
477     BASE_STRING='URI:DIR2-Verifier:'
478     BASE_STRING_RE=re.compile('^'+BASE_STRING)
479     BASE_HUMAN_RE=re.compile('^'+OPTIONALHTTPLEAD+'URI'+SEP+'DIR2-Verifier'+SEP)
480     INNER_URI_CLASS=SSKVerifierURI
481
482     def __init__(self, filenode_uri=None):
483         if filenode_uri:
484             assert IVerifierURI.providedBy(filenode_uri)
485         self._filenode_uri = filenode_uri
486
487     def get_filenode_cap(self):
488         return self._filenode_uri
489
490 class ImmutableDirectoryURIVerifier(DirectoryURIVerifier):
491     implements(IVerifierURI)
492     BASE_STRING='URI:DIR2-CHK-Verifier:'
493     BASE_STRING_RE=re.compile('^'+BASE_STRING)
494     BASE_HUMAN_RE=re.compile('^'+OPTIONALHTTPLEAD+'URI'+SEP+'DIR2-CHK-VERIFIER'+SEP)
495     INNER_URI_CLASS=CHKFileVerifierURI
496
497 class UnknownURI:
498     def __init__(self, uri):
499         self._uri = uri
500     def to_string(self):
501         return self._uri
502
503 def from_string(s):
504     if not isinstance(s, str):
505         raise TypeError("unknown URI type: %s.." % str(s)[:100])
506     elif s.startswith('URI:CHK:'):
507         return CHKFileURI.init_from_string(s)
508     elif s.startswith('URI:CHK-Verifier:'):
509         return CHKFileVerifierURI.init_from_string(s)
510     elif s.startswith('URI:LIT:'):
511         return LiteralFileURI.init_from_string(s)
512     elif s.startswith('URI:SSK:'):
513         return WriteableSSKFileURI.init_from_string(s)
514     elif s.startswith('URI:SSK-RO:'):
515         return ReadonlySSKFileURI.init_from_string(s)
516     elif s.startswith('URI:SSK-Verifier:'):
517         return SSKVerifierURI.init_from_string(s)
518     elif s.startswith('URI:DIR2:'):
519         return DirectoryURI.init_from_string(s)
520     elif s.startswith('URI:DIR2-RO:'):
521         return ReadonlyDirectoryURI.init_from_string(s)
522     elif s.startswith('URI:DIR2-Verifier:'):
523         return DirectoryURIVerifier.init_from_string(s)
524     elif s.startswith('URI:DIR2-CHK:'):
525         return ImmutableDirectoryURI.init_from_string(s)
526     elif s.startswith('URI:DIR2-LIT:'):
527         return LiteralDirectoryURI.init_from_string(s)
528     return UnknownURI(s)
529
530 def is_uri(s):
531     try:
532         from_string(s)
533         return True
534     except (TypeError, AssertionError):
535         return False
536
537 def from_string_dirnode(s):
538     u = from_string(s)
539     assert IDirnodeURI.providedBy(u)
540     return u
541
542 registerAdapter(from_string_dirnode, str, IDirnodeURI)
543
544 def from_string_filenode(s):
545     u = from_string(s)
546     assert IFileURI.providedBy(u)
547     return u
548
549 registerAdapter(from_string_filenode, str, IFileURI)
550
551 def from_string_mutable_filenode(s):
552     u = from_string(s)
553     assert IMutableFileURI.providedBy(u)
554     return u
555 registerAdapter(from_string_mutable_filenode, str, IMutableFileURI)
556
557 def from_string_verifier(s):
558     u = from_string(s)
559     assert IVerifierURI.providedBy(u)
560     return u
561 registerAdapter(from_string_verifier, str, IVerifierURI)
562
563
564 def pack_extension(data):
565     pieces = []
566     for k in sorted(data.keys()):
567         value = data[k]
568         if isinstance(value, (int, long)):
569             value = "%d" % value
570         assert isinstance(value, str), k
571         assert re.match(r'^[a-zA-Z_\-]+$', k)
572         pieces.append(k + ':' + hashutil.netstring(value))
573     uri_extension = ''.join(pieces)
574     return uri_extension
575
576 def unpack_extension(data):
577     d = {}
578     while data:
579         colon = data.index(':')
580         key = data[:colon]
581         data = data[colon+1:]
582
583         colon = data.index(':')
584         number = data[:colon]
585         length = int(number)
586         data = data[colon+1:]
587
588         value = data[:length]
589         assert data[length] == ','
590         data = data[length+1:]
591
592         d[key] = value
593
594     # convert certain things to numbers
595     for intkey in ('size', 'segment_size', 'num_segments',
596                    'needed_shares', 'total_shares'):
597         if intkey in d:
598             d[intkey] = int(d[intkey])
599     return d
600
601
602 def unpack_extension_readable(data):
603     unpacked = unpack_extension(data)
604     unpacked["UEB_hash"] = hashutil.uri_extension_hash(data)
605     for k in sorted(unpacked.keys()):
606         if 'hash' in k:
607             unpacked[k] = base32.b2a(unpacked[k])
608     return unpacked
609