blacklist content rather than hashes
[~helmut/debian-dedup.git] / dedup / hashing.py
1 import itertools
2
3 class HashBlacklist(object):
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(object):
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,), itertools.imap(len, blacklist)))
54         self.maxlen = maxlen
55         self.stored = ""
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         return HashBlacklistContent(self.hashobj.copy(), self.blacklist,
80                                     self.maxlen)
81
82 class DecompressedHash(object):
83     """Apply a decompression function before the hash. This class provides the
84     hashlib interface (update, hexdigest, copy) excluding digest and name."""
85     def __init__(self, decompressor, hashobj):
86         """
87         @param decompressor: a decompression object like bz2.BZ2Decompressor or
88             lzma.LZMADecompressor. It has to provide methods decompress and
89             copy as well as an unused_data attribute. It may provide a flush
90             method.
91         @param hashobj: a hashlib-like obj providing methods update, hexdigest
92             and copy
93         """
94         self.decompressor = decompressor
95         self.hashobj = hashobj
96
97     def update(self, data):
98         self.hashobj.update(self.decompressor.decompress(data))
99
100     def hexdigest(self):
101         if not hasattr(self.decompressor, "flush"):
102             if self.decompressor.unused_data:
103                 raise ValueError("decompressor did not consume all data")
104             return self.hashobj.hexdigest()
105         tmpdecomp = self.decompressor.copy()
106         data = tmpdecomp.flush()
107         if tmpdecomp.unused_data:
108             raise ValueError("decompressor did not consume all data")
109         tmphash = self.hashobj.copy()
110         tmphash.update(data)
111         return tmphash.hexdigest()
112
113     def copy(self):
114         return DecompressedHash(self.decompressor.copy(), self.hashobj.copy())
115
116 class SuppressingHash(object):
117     """A hash that silences exceptions from the update and hexdigest methods of
118     a hashlib-like object. If an exception has occured, hexdigest always
119     returns None."""
120     def __init__(self, hashobj, exceptions=()):
121         """
122         @param hashobj: a hashlib-like object providing methods update, copy
123             and hexdigest. If a name attribute is present, it is mirrored as
124             well.
125         @type exceptions: tuple
126         @param exceptions: exception classes to be suppressed
127         """
128         self.hashobj = hashobj
129         self.exceptions = exceptions
130         if hasattr(hashobj, "name"):
131             self.name = hashobj.name
132
133     def update(self, data):
134         if self.hashobj:
135             try:
136                 self.hashobj.update(data)
137             except self.exceptions:
138                 self.hashobj = None
139
140     def hexdigest(self):
141         if self.hashobj:
142             try:
143                 return self.hashobj.hexdigest()
144             except self.exceptions:
145                 self.hashobj = None
146         return None
147
148     def copy(self):
149         if self.hashobj:
150             return SuppressingHash(self.hashobj.copy(), self.exceptions)
151         return SuppressingHash(None, self.exceptions)
152
153 def hash_file(hashobj, filelike, blocksize=65536):
154     """Feed the entire contents from the given filelike to the given hashobj.
155     @param hashobj: hashlib-like object providing an update method
156     @param filelike: file-like object providing read(size)
157     """
158     data = filelike.read(blocksize)
159     while data:
160         hashobj.update(data)
161         data = filelike.read(blocksize)
162     return hashobj
163
164 class HashedStream(object):
165     """A file-like object, that supports sequential reading and hashes the
166     contents on the fly."""
167     def __init__(self, filelike, hashobj):
168         """
169         @param filelike: a file-like object, that must support the read method
170         @param hashobj: a hashlib-like object providing update and hexdigest
171         """
172         self.filelike = filelike
173         self.hashobj = hashobj
174
175     def read(self, length):
176         data = self.filelike.read(length)
177         self.hashobj.update(data)
178         return data
179
180     def hexdigest(self):
181         return self.hashobj.hexdigest()