-
Notifications
You must be signed in to change notification settings - Fork 4
Expand file tree
/
Copy pathhashutils.py
More file actions
287 lines (251 loc) · 8.52 KB
/
Copy pathhashutils.py
File metadata and controls
287 lines (251 loc) · 8.52 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
#!/usr/bin/python
''' Convenience hashing facilities.
This predefines classes for various hash algorithms:
`BLAKE3` (if we can import `blake3`), `MD5`, `SHA1`, `SHA224`,
`SHA256`, `SHA384`, `SHA512`, and a `BaseHashCode` base class
as a common ancestor with a `.hashclass()` factory method for
creating new subclasses for other algorithms.
All `BaseHashCode` classes have a variety of convenience factories for making instances:
* `from_hashbytes`: from an existing digest `bytes`
* `from_hashbytes_hex`: from a hex string of an existing digest
* `from_named_hashbytes_hex`: from a hashname (eg `'blake3'`) and a hex string
* `from_named_hashbytes_hex`: from a *hashname*`:`*hex* string
* `from_data`: from `bytes` content
* `from_buffer`: from a `CornuCopyBuffer` or anything which can be promoted to one
* `from_fspath`: from a filesystem path
Hashing some content returns instances of these classes, which
are subclasses of `bytes` i.e. the digest. All `BaseHashCode` classes various methods:
* `str(hashcode)`: returns a *hashname*`:`*hex* string
* `.hashname`: the hash function name
* `.hex()`: the digest as a hex string
The common example:
# obtain the contwnt hash from the file `fspath`
hashcode = hashclass.from_fspath(fspath)
where `hashclass` is whatever `BaseHashCode` subclass is in use.
'''
from binascii import hexlify, unhexlify
import hashlib
import mmap
import os
from typing import Optional
from cs.buffer import CornuCopyBuffer
from cs.deco import promote
from cs.lex import r
__version__ = '20250414.1-post'
DISTINFO = {
'keywords': ["python3"],
'classifiers': [
"Programming Language :: Python",
"Programming Language :: Python :: 3",
],
'install_requires': [
'cs.buffer',
'cs.deco',
'cs.lex',
],
}
class BaseHashCode(bytes):
''' Base class for hashcodes, subclassed by `SHA1`, `SHA256` et al.
You can obtain the class for a particular hasher by name, example:
SHA256 = BaseHashCode.hashclass('sha256')
'''
__slots__ = ()
# registry of classes
by_hashname = {}
@staticmethod
def get_hashfunc(hashname: str):
''' Fetch the hash function implied by `hashname`.
'''
try:
hashfunc = getattr(hashlib, hashname)
except AttributeError as hashlib_e:
if hashname == 'blake3':
# see if the blake3 module is around
# pylint:disable=import-outside-toplevel
try:
from blake3 import blake3 as hashfunc
except ImportError as import_e:
import sys
raise ValueError(
f'unknown {hashname=}: {import_e}; {sys.path=}'
) from import_e
else:
raise ValueError(f'unknown {hashname=}: {hashlib_e}')
return hashfunc
@classmethod
def hashclass(cls, hashname: str, hashfunc=None, **kw):
''' Return the class for the hash function named `hashname`.
Parameters:
* `hashname`: the name of the hash function
* `hashfunc`: optional hash function for the class
'''
try:
hashcls = cls.by_hashname[hashname]
except KeyError:
if hashfunc is None:
hashfunc = cls.get_hashfunc(hashname)
class hashcls(
cls,
hashfunc=hashfunc,
hashname=hashname,
**kw,
):
''' Hash class implementation.
'''
__slots__ = ()
hashcls.__name__ = hashname.upper()
hashcls.__doc__ = f'Hash class for the {hashname!r} algorithm.'
else:
if hashfunc is not None:
if hashfunc is not hashcls.hashfunc:
raise ValueError(
f'class {hashcls.__name__} already exists with a different hash function {hashcls.hashfunc} from supplied {hashfunc=}'
)
return hashcls
@classmethod
def __init_subclass__(
cls, *, hashname: Optional[str] = None, hashfunc=None, **kw
):
super().__init_subclass__(**kw)
if hashname is None and hashfunc is None:
# we're a superclass of another base class
return
if hashfunc is None:
hashfunc = cls.get_hashfunc(hashname)
by_hashname = cls.by_hashname
try:
hashcls = by_hashname[hashname]
except KeyError:
# new hash class, register it
by_hashname[hashname] = cls
else:
raise ValueError(
f'hashname {hashname!r}: class {hashcls} already exists for hashname'
)
cls.hashname = hashname
cls.hashfunc = hashfunc
cls.hashlen = len(hashfunc(b'').digest())
if not cls.__doc__:
cls.__doc__ = f'{hashfunc.__name__} hashcode class, subclass of `bytes`.'
hashfunc = lambda bs=None: None # pylint: disable=unnecessary-lambda-assignment
def __str__(self):
''' Return `f'{self.hashname}:{self.hex()}'`.
'''
return f'{self.hashname}:{self.hex()}'
@property
def hashname(self):
''' The hash code type name, derived from the class name.
'''
return self.__class__.__name__.lower()
def hex(self) -> str:
''' Return the hashcode bytes transcribes as a hexadecimal ASCII `str`.
'''
return hexlify(self).decode('ascii')
@classmethod
def from_hashbytes(cls, hashbytes):
''' Factory function returning a `BaseHashCode` object from the hash bytes.
'''
assert len(hashbytes) == cls.hashlen, (
"expected %d bytes, received %d: %r" %
(cls.hashlen, len(hashbytes), hashbytes)
)
return cls(hashbytes)
@classmethod
def from_hashbytes_hex(cls, hashhex: str):
''' Factory function returning a `BaseHashCode` object
from the hash bytes hex text.
'''
bs = unhexlify(hashhex)
return cls.from_hashbytes(bs)
@classmethod
def from_named_hashbytes_hex(cls, hashname, hashhex):
''' Factory function to return a `BaseHashCode` object
from the hash type name and the hash bytes hex text.
'''
hashclass = cls.hashclass(hashname)
if not issubclass(hashclass, cls):
raise ValueError(
f'{cls.__name__}.from_named_hashbytes_hex({hashname!r},{hashhex!r}):'
f' inferred hash class {hashclass.__name__}:{hashclass!r}'
f' is not a subclass of {cls.__name__}:{cls!r}'
)
bs = unhexlify(hashhex)
return hashclass.from_hashbytes(bs)
@classmethod
def from_prefixed_hashbytes_hex(cls, hashtext: str):
''' Factory function returning a `BaseHashCode` object
from the hash bytes hex text prefixed by the hashname.
This is the reverse of `__str__`.
'''
hashname, hashhex = hashtext.split(':')
return cls.from_named_hashbytes_hex(hashname, hashhex)
@classmethod
def from_data(cls, bs):
''' Compute hashcode from the data `bs`.
'''
return cls(cls.hashfunc(bs).digest())
@classmethod
@promote
def from_buffer(cls, bfr: CornuCopyBuffer):
''' Compute hashcode from the contents of the `CornuCopyBuffer` `bfr`.
'''
h = cls.hashfunc()
for bs in bfr:
h.update(bs)
return cls(h.digest())
@classmethod
def from_fspath(cls, fspath, **kw):
''' Compute hashcode from the contents of the file `fspath`.
'''
# try to mmap the file and hash the whole thing in one go
fd = None
try:
fd = os.open(fspath, os.O_RDONLY)
except OSError:
pass
else:
S = os.fstat(fd)
if S.st_size == 0:
return cls.from_data(b'')
try:
with mmap.mmap(fd, 0, flags=mmap.MAP_PRIVATE,
prot=mmap.PROT_READ) as mmapped:
return cls.from_data(mmapped)
except OSError:
pass
finally:
if fd is not None:
os.close(fd)
# mmap fails, try plain open of file
return cls.from_buffer(CornuCopyBuffer.from_filename(fspath, **kw))
@classmethod
def promote(cls, obj):
''' Promote to a `BaseHashCode` instance.
'''
if isinstance(obj, cls):
return obj
if isinstance(obj, bytes):
return cls.from_hashbytes(obj)
if isinstance(obj, str):
return cls.from_prefixed_hashbytes_hex(obj)
try:
hashname, hashhex = obj
except (TypeError, ValueError):
pass
else:
return cls.from_named_hashbytes_hex(hashname, hashhex)
raise TypeError(f'{cls.__name__}.promote({r(obj)}): cannot promote')
# convenience predefined hash classes
MD5 = BaseHashCode.hashclass('md5')
SHA1 = BaseHashCode.hashclass('sha1')
SHA224 = BaseHashCode.hashclass('sha224')
SHA256 = BaseHashCode.hashclass('sha256')
SHA384 = BaseHashCode.hashclass('sha384')
SHA512 = BaseHashCode.hashclass('sha512')
# define BLAKE3 if available
try:
from blake3 import blake3
except ImportError:
pass
else:
BLAKE3 = BaseHashCode.hashclass('blake3')