DecompressedHash should fail on trailing input
[~helmut/debian-dedup.git] / dedup / hashing.py
1 class HashBlacklist(object):
2     """Turn a hashlib-like object into a hash that returns None for some
3     blacklisted hashes instead of the real hash value.
4
5     We only work with hexdigests here, so diget() disappears. The methods
6     copy and update as well as the name attribute keep working as expected.
7     """
8     def __init__(self, hashobj, blacklist=()):
9         """
10         @param hashobj: a hashlib-like object
11         @param blacklist: an object providing __contains__.
12             hexdigest values which are contained in the blacklist
13             are turned into None values
14         """
15         self.hashobj = hashobj
16         self.blacklist = blacklist
17         self.update = self.hashobj.update
18
19     @property
20     def name(self):
21         return self.hashobj.name
22
23     def hexdigest(self):
24         digest = self.hashobj.hexdigest()
25         if digest in self.blacklist:
26             return None
27         return digest
28
29     def copy(self):
30         return HashBlacklist(self.hashobj.copy(), self.blacklist)
31
32 class DecompressedHash(object):
33     """Apply a decompression function before the hash. This class provides the
34     hashlib interface (update, hexdigest, copy) excluding digest and name."""
35     def __init__(self, decompressor, hashobj):
36         """
37         @param decompressor: a decompression object like bz2.BZ2Decompressor or
38             lzma.LZMADecompressor. It has to provide methods decompress and
39             copy as well as an unused_data attribute. It may provide a flush
40             method.
41         @param hashobj: a hashlib-like obj providing methods update, hexdigest
42             and copy
43         """
44         self.decompressor = decompressor
45         self.hashobj = hashobj
46
47     def update(self, data):
48         self.hashobj.update(self.decompressor.decompress(data))
49
50     def hexdigest(self):
51         if not hasattr(self.decompressor, "flush"):
52             if self.decompressor.unused_data:
53                 raise ValueError("decompressor did not consume all data")
54             return self.hashobj.hexdigest()
55         tmpdecomp = self.decompressor.copy()
56         data = tmpdecomp.flush()
57         if tmpdecomp.unused_data:
58             raise ValueError("decompressor did not consume all data")
59         tmphash = self.hashobj.copy()
60         tmphash.update(data)
61         return tmphash.hexdigest()
62
63     def copy(self):
64         return DecompressedHash(self.decompressor.copy(), self.hashobj.copy())
65
66 class SuppressingHash(object):
67     """A hash that silences exceptions from the update and hexdigest methods of
68     a hashlib-like object. If an exception has occured, hexdigest always
69     returns None."""
70     def __init__(self, hashobj, exceptions=()):
71         """
72         @param hashobj: a hashlib-like object providing methods update, copy
73             and hexdigest. If a name attribute is present, it is mirrored as
74             well.
75         @type exceptions: tuple
76         @param exceptions: exception classes to be suppressed
77         """
78         self.hashobj = hashobj
79         self.exceptions = exceptions
80         if hasattr(hashobj, "name"):
81             self.name = hashobj.name
82
83     def update(self, data):
84         if self.hashobj:
85             try:
86                 self.hashobj.update(data)
87             except self.exceptions:
88                 self.hashobj = None
89
90     def hexdigest(self):
91         if self.hashobj:
92             try:
93                 return self.hashobj.hexdigest()
94             except self.exceptions:
95                 self.hashobj = None
96         return None
97
98     def copy(self):
99         if self.hashobj:
100             return SuppressingHash(self.hashobj.copy(), self.exceptions)
101         return SuppressingHash(None, self.exceptions)
102
103 def hash_file(hashobj, filelike, blocksize=65536):
104     """Feed the entire contents from the given filelike to the given hashobj.
105     @param hashobj: hashlib-like object providing an update method
106     @param filelike: file-like object providing read(size)
107     """
108     data = filelike.read(blocksize)
109     while data:
110         hashobj.update(data)
111         data = filelike.read(blocksize)
112     return hashobj
113
114 class HashedStream(object):
115     """A file-like object, that supports sequential reading and hashes the
116     contents on the fly."""
117     def __init__(self, filelike, hashobj):
118         """
119         @param filelike: a file-like object, that must support the read method
120         @param hashobj: a hashlib-like object providing update and hexdigest
121         """
122         self.filelike = filelike
123         self.hashobj = hashobj
124
125     def read(self, length):
126         data = self.filelike.read(length)
127         self.hashobj.update(data)
128         return data
129
130     def hexdigest(self):
131         return self.hashobj.hexdigest()