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