webapp.py: fuse two sql queries in get_details
[~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, name="unnamed"):
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         @param name: initialized the name property
96         """
97         self.decompressor = decompressor
98         self.hashobj = hashobj
99         self.name = name
100
101     def update(self, data):
102         self.hashobj.update(self.decompressor.decompress(data))
103
104     def hexdigest(self):
105         if not hasattr(self.decompressor, "flush"):
106             if self.decompressor.unused_data:
107                 raise ValueError("decompressor did not consume all data")
108             return self.hashobj.hexdigest()
109         tmpdecomp = self.decompressor.copy()
110         data = tmpdecomp.flush()
111         if tmpdecomp.unused_data:
112             raise ValueError("decompressor did not consume all data")
113         tmphash = self.hashobj.copy()
114         tmphash.update(data)
115         return tmphash.hexdigest()
116
117     def copy(self):
118         return DecompressedHash(self.decompressor.copy(), self.hashobj.copy(),
119                                 self.name)
120
121 class SuppressingHash:
122     """A hash that silences exceptions from the update and hexdigest methods of
123     a hashlib-like object. If an exception has occurred, hexdigest always
124     returns None."""
125     def __init__(self, hashobj, exceptions=()):
126         """
127         @param hashobj: a hashlib-like object providing methods update, copy
128             and hexdigest. If a name attribute is present, it is mirrored as
129             well.
130         @type exceptions: tuple
131         @param exceptions: exception classes to be suppressed
132         """
133         self.hashobj = hashobj
134         self.exceptions = exceptions
135         if hasattr(hashobj, "name"):
136             self.name = hashobj.name
137
138     def update(self, data):
139         if self.hashobj:
140             try:
141                 self.hashobj.update(data)
142             except self.exceptions:
143                 self.hashobj = None
144
145     def hexdigest(self):
146         if self.hashobj:
147             try:
148                 return self.hashobj.hexdigest()
149             except self.exceptions:
150                 self.hashobj = None
151         return None
152
153     def copy(self):
154         if self.hashobj:
155             return SuppressingHash(self.hashobj.copy(), self.exceptions)
156         return SuppressingHash(None, self.exceptions)
157
158 def hash_file(hashobj, filelike, blocksize=65536):
159     """Feed the entire contents from the given filelike to the given hashobj.
160     @param hashobj: hashlib-like object providing an update method
161     @param filelike: file-like object providing read(size)
162     """
163     data = filelike.read(blocksize)
164     while data:
165         hashobj.update(data)
166         data = filelike.read(blocksize)
167
168 class HashedStream:
169     """A file-like object, that supports sequential reading and hashes the
170     contents on the fly."""
171     def __init__(self, filelike, hashobj):
172         """
173         @param filelike: a file-like object, that must support the read method
174         @param hashobj: a hashlib-like object providing update and hexdigest
175         """
176         self.filelike = filelike
177         self.hashobj = hashobj
178
179     def read(self, length):
180         data = self.filelike.read(length)
181         self.hashobj.update(data)
182         return data
183
184     def hexdigest(self):
185         return self.hashobj.hexdigest()
186
187     def validate(self, hexdigest):
188         """Soak up any remaining input and validate the read data using the
189         given hexdigest.
190         @raises ValueError: when the hash does not match
191         """
192         while self.read(65536):
193             pass
194         if self.hexdigest() != hexdigest:
195             raise ValueError("hash sum mismatch")