drop support for Python 2.x
[~helmut/debian-dedup.git] / dedup / hashing.py
1 import itertools
2
3 class HashBlacklist:
4     """Turn a hashlib-like object into a hash that returns None for some
5     blacklisted hashes instead of the real hash value.
6
7     We only work with hexdigests here, so diget() disappears. The methods
8     copy and update as well as the name attribute keep working as expected.
9     """
10     def __init__(self, hashobj, blacklist=()):
11         """
12         @param hashobj: a hashlib-like object
13         @param blacklist: an object providing __contains__.
14             hexdigest values which are contained in the blacklist
15             are turned into None values
16         """
17         self.hashobj = hashobj
18         self.blacklist = blacklist
19         self.update = self.hashobj.update
20
21     @property
22     def name(self):
23         return self.hashobj.name
24
25     def hexdigest(self):
26         digest = self.hashobj.hexdigest()
27         if digest in self.blacklist:
28             return None
29         return digest
30
31     def copy(self):
32         return HashBlacklist(self.hashobj.copy(), self.blacklist)
33
34 class HashBlacklistContent:
35     """Turn a hashlib-like object into a hash that returns None for some
36     blacklisted content instead of the real hash value. Unlike HashBlacklist,
37     not the output of the hash is considered, but its input."""
38
39     def __init__(self, hashobj, blacklist=(), maxlen=None):
40         """
41         @param hashobj: a hashlib-like object
42         @param blacklist: an object providing __contains__.
43             hash inputs which are contained in the blacklist
44             are turned into None values
45         @param maxlen: the maximum length of a blacklisted input.
46             Defaults to max(map(len, blacklist)), so if it is absent,
47             the blacklist must support iteration.
48         """
49         self.hashobj = hashobj
50         self.blacklist = blacklist
51         if maxlen is None:
52             # the chain avoids passing the empty sequence to max
53             maxlen = max(itertools.chain((0,), map(len, blacklist)))
54         self.maxlen = maxlen
55         self.stored = b""
56
57     @property
58     def name(self):
59         return self.hashobj.name
60
61     def update(self, data):
62         if self.stored is not None:
63             self.stored += data
64             if len(self.stored) > self.maxlen:
65                 self.stored = None
66         self.hashobj.update(data)
67
68     def digest(self):
69         if self.stored is not None and self.stored in self.blacklist:
70             return None
71         return self.hashobj.digest()
72
73     def hexdigest(self):
74         if self.stored is not None and self.stored in self.blacklist:
75             return None
76         return self.hashobj.hexdigest()
77
78     def copy(self):
79         new = HashBlacklistContent(self.hashobj.copy(), self.blacklist,
80                                    self.maxlen)
81         new.stored = self.stored
82         return new
83
84 class DecompressedHash:
85     """Apply a decompression function before the hash. This class provides the
86     hashlib interface (update, hexdigest, copy) excluding digest and name."""
87     def __init__(self, decompressor, hashobj):
88         """
89         @param decompressor: a decompression object like bz2.BZ2Decompressor or
90             lzma.LZMADecompressor. It has to provide methods decompress and
91             copy as well as an unused_data attribute. It may provide a flush
92             method.
93         @param hashobj: a hashlib-like obj providing methods update, hexdigest
94             and copy
95         """
96         self.decompressor = decompressor
97         self.hashobj = hashobj
98
99     def update(self, data):
100         self.hashobj.update(self.decompressor.decompress(data))
101
102     def hexdigest(self):
103         if not hasattr(self.decompressor, "flush"):
104             if self.decompressor.unused_data:
105                 raise ValueError("decompressor did not consume all data")
106             return self.hashobj.hexdigest()
107         tmpdecomp = self.decompressor.copy()
108         data = tmpdecomp.flush()
109         if tmpdecomp.unused_data:
110             raise ValueError("decompressor did not consume all data")
111         tmphash = self.hashobj.copy()
112         tmphash.update(data)
113         return tmphash.hexdigest()
114
115     def copy(self):
116         return DecompressedHash(self.decompressor.copy(), self.hashobj.copy())
117
118 class SuppressingHash:
119     """A hash that silences exceptions from the update and hexdigest methods of
120     a hashlib-like object. If an exception has occurred, hexdigest always
121     returns None."""
122     def __init__(self, hashobj, exceptions=()):
123         """
124         @param hashobj: a hashlib-like object providing methods update, copy
125             and hexdigest. If a name attribute is present, it is mirrored as
126             well.
127         @type exceptions: tuple
128         @param exceptions: exception classes to be suppressed
129         """
130         self.hashobj = hashobj
131         self.exceptions = exceptions
132         if hasattr(hashobj, "name"):
133             self.name = hashobj.name
134
135     def update(self, data):
136         if self.hashobj:
137             try:
138                 self.hashobj.update(data)
139             except self.exceptions:
140                 self.hashobj = None
141
142     def hexdigest(self):
143         if self.hashobj:
144             try:
145                 return self.hashobj.hexdigest()
146             except self.exceptions:
147                 self.hashobj = None
148         return None
149
150     def copy(self):
151         if self.hashobj:
152             return SuppressingHash(self.hashobj.copy(), self.exceptions)
153         return SuppressingHash(None, self.exceptions)
154
155 def hash_file(hashobj, filelike, blocksize=65536):
156     """Feed the entire contents from the given filelike to the given hashobj.
157     @param hashobj: hashlib-like object providing an update method
158     @param filelike: file-like object providing read(size)
159     """
160     data = filelike.read(blocksize)
161     while data:
162         hashobj.update(data)
163         data = filelike.read(blocksize)
164     return hashobj
165
166 class HashedStream:
167     """A file-like object, that supports sequential reading and hashes the
168     contents on the fly."""
169     def __init__(self, filelike, hashobj):
170         """
171         @param filelike: a file-like object, that must support the read method
172         @param hashobj: a hashlib-like object providing update and hexdigest
173         """
174         self.filelike = filelike
175         self.hashobj = hashobj
176
177     def read(self, length):
178         data = self.filelike.read(length)
179         self.hashobj.update(data)
180         return data
181
182     def hexdigest(self):
183         return self.hashobj.hexdigest()
184
185     def validate(self, hexdigest):
186         """Soak up any remaining input and validate the read data using the
187         given hexdigest.
188         @raises ValueError: when the hash does not match
189         """
190         while self.read(65536):
191             pass
192         if self.hexdigest() != hexdigest:
193             raise ValueError("hash sum mismatch")