treat Pre-Depends like regular Depends
[~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         @type tarfileobj: tarfile.TarFile
159         @param tarfile: is opened for streaming reads
160         """
161
162     def handle_data_tar(self, tarfileobj):
163         """Handle the data.tar member of the Debian package.
164         @type tarfileobj: tarfile.TarFile
165         @param tarfile: is opened for streaming reads
166         """