DecompressedStream: fix decompression without flush
[~helmut/debian-dedup.git] / dedup / debpkg.py
1 import sys
2 import tarfile
3
4 from debian import deb822
5
6 from dedup.arreader import ArReader
7 from dedup.compression import decompress
8 from dedup.hashing import hash_file
9
10 class MultiHash(object):
11     def __init__(self, *hashes):
12         self.hashes = hashes
13
14     def update(self, data):
15         for hasher in self.hashes:
16             hasher.update(data)
17
18 def get_tar_hashes(tar, hash_functions):
19     """Given a TarFile read all regular files and compute all of the given hash
20     functions on each file.
21     @type tar: tarfile.TarFile
22     @param hash_functions: a sequence of parameter-less functions each creating a
23             new hashlib-like object
24     @rtype: gen((str, int, {str: str}}
25     @returns: an iterable of (filename, filesize, hashes) tuples where
26             hashes is a dict mapping hash function names to hash values
27     """
28
29     for elem in tar:
30         if not elem.isreg(): # excludes hard links as well
31             continue
32         hasher = MultiHash(*[func() for func in hash_functions])
33         hasher = hash_file(hasher, tar.extractfile(elem))
34         hashes = {}
35         for hashobj in hasher.hashes:
36             hashvalue = hashobj.hexdigest()
37             if hashvalue:
38                 hashes[hashobj.name] = hashvalue
39         yield (elem.name, elem.size, hashes)
40
41 if sys.version_info.major >= 3:
42     def opentar(filelike):
43         return tarfile.open(fileobj=filelike, mode="r|", encoding="utf8",
44                             errors="surrogateescape")
45
46     def decodetarname(name):
47         """Decoded name of a tarinfo.
48         @raises UnicodeDecodeError:
49         """
50         try:
51             name.encode("utf8", "strict")
52         except UnicodeEncodeError as e:
53             if e.reason == "surrogates not allowed":
54                 name.encode("utf8", "surrogateescape").decode("utf8", "strict")
55         return name
56 else:
57     def opentar(filelike):
58         return tarfile.open(fileobj=filelike, mode="r|")
59
60     def decodetarname(name):
61         """Decoded name of a tarinfo.
62         @raises UnicodeDecodeError:
63         """
64         return name.decode("utf8")
65
66 class DebExtractor(object):
67     "Base class for extracting desired features from a Debian package."
68
69     def __init__(self):
70         self.arstate = "start"
71
72     def process(self, filelike):
73         """Process a Debian package.
74         @param filelike: is a file-like object containing the contents of the
75                          Debian packge and can be read once without seeks.
76         """
77         af = ArReader(filelike)
78         af.read_magic()
79         while True:
80             try:
81                 name = af.read_entry()
82             except EOFError:
83                 break
84             else:
85                 self.handle_ar_member(name, af)
86         self.handle_ar_end()
87
88     def handle_ar_member(self, name, filelike):
89         """Handle an ar archive member of the Debian package.
90         If you replace this method, you must also replace handle_ar_end and
91         none of the methods handle_debversion, handle_control_tar or
92         handle_data_tar are called.
93         @type name: bytes
94         @param name: is the name of the member
95         @param filelike: is a file-like object containing the contents of the
96                          member and can be read once without seeks.
97         """
98         if self.arstate == "start":
99             if name != b"debian-binary":
100                 raise ValueError("debian-binary not found")
101             version = filelike.read()
102             self.handle_debversion(version)
103             if not version.startswith(b"2."):
104                 raise ValueError("debian version not recognized")
105             self.arstate = "version"
106         elif self.arstate == "version":
107             if name.startswith(b"control.tar"):
108                 filelike = decompress(filelike, name[11:].decode("ascii"))
109                 self.handle_control_tar(opentar(filelike))
110                 self.arstate = "control"
111             elif not name.startswith(b"_"):
112                 raise ValueError("unexpected ar member %r" % name)
113         elif self.arstate == "control":
114             if name.startswith(b"data.tar"):
115                 filelike = decompress(filelike, name[8:].decode("ascii"))
116                 self.handle_data_tar(opentar(filelike))
117                 self.arstate = "data"
118             elif not name.startswith(b"_"):
119                 raise ValueError("unexpected ar member %r" % name)
120         else:
121             assert self.arstate == "data"
122
123     def handle_ar_end(self):
124         "Handle the end of the ar archive of the Debian package."
125         if self.arstate != "data":
126             raise ValueError("data.tar not found")
127
128     def handle_debversion(self, version):
129         """Handle the debian-binary member of the Debian package.
130         @type version: bytes
131         @param version: The full contents of the ar member.
132         """
133
134     def handle_control_tar(self, tarfileobj):
135         """Handle the control.tar member of the Debian package.
136         If you replace this method, none of handle_control_member,
137         handle_control_info or handle_control_end are called.
138         @type tarfileobj: tarfile.TarFile
139         @param tarfile: is opened for streaming reads
140         """
141         controlseen = False
142         for elem in tarfileobj:
143             if elem.isreg():
144                 name = elem.name
145                 if name.startswith("./"):
146                     name = name[2:]
147                 content = tarfileobj.extractfile(elem).read()
148                 self.handle_control_member(name, content)
149                 if name == "control":
150                     self.handle_control_info(deb822.Packages(content))
151                     controlseen = True
152             elif not (elem.isdir() and elem.name == "."):
153                 raise ValueError("invalid non-file %r found in control.tar" %
154                                  elem.name)
155         if not controlseen:
156             raise ValueError("control missing from control.tar")
157         self.handle_control_end()
158
159     def handle_control_member(self, name, content):
160         """Handle a file member of the control.tar member of the Debian package.
161         @type name: str
162         @param name: is the plain member name
163         @type content: bytes
164         """
165
166     def handle_control_info(self, info):
167         """Handle the control member of the control.tar member of the Debian
168         package.
169         @type info: deb822.Packages
170         """
171
172     def handle_control_end(self):
173         "Handle the end of the control.tar member of the Debian package."
174
175     def handle_data_tar(self, tarfileobj):
176         """Handle the data.tar member of the Debian package.
177         @type tarfileobj: tarfile.TarFile
178         @param tarfile: is opened for streaming reads
179         """