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