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