Merge lp:~cjwatson/launchpad/remove-deb-ar-assumption into lp:launchpad

Proposed by Colin Watson
Status: Merged
Merged at revision: 18974
Proposed branch: lp:~cjwatson/launchpad/remove-deb-ar-assumption
Merge into: lp:launchpad
Prerequisite: lp:~cjwatson/launchpad/write-file-bytes
Diff against target: 259 lines (+119/-50)
2 files modified
lib/lp/archiveuploader/nascentuploadfile.py (+20/-35)
lib/lp/archiveuploader/tests/test_nascentuploadfile.py (+99/-15)
To merge this branch: bzr merge lp:~cjwatson/launchpad/remove-deb-ar-assumption
Reviewer Review Type Date Requested Status
William Grant code Approve
Review via email: mp+367917@code.launchpad.net

Commit message

Check the .deb format using dpkg-deb rather than ar.

Description of the change

Debian has been talking about changing the outer .deb container file format to something other than ar, e.g. in order to lift size limits. It may or may not go anywhere, but it reminded me that we really shouldn't be making any particular assumptions about what the container format is: the interface we should rely on is that provided by dpkg.

To post a comment you must log in.
Revision history for this message
William Grant (wgrant) :
review: Approve (code)

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'lib/lp/archiveuploader/nascentuploadfile.py'
2--- lib/lp/archiveuploader/nascentuploadfile.py 2018-05-03 15:10:39 +0000
3+++ lib/lp/archiveuploader/nascentuploadfile.py 2019-05-24 17:19:17 +0000
4@@ -1,4 +1,4 @@
5-# Copyright 2009-2018 Canonical Ltd. This software is licensed under the
6+# Copyright 2009-2019 Canonical Ltd. This software is licensed under the
7 # GNU Affero General Public License version 3 (see the file LICENSE).
8
9 """Specific models for uploaded files"""
10@@ -697,41 +697,26 @@
11 def verifyFormat(self):
12 """Check if the DEB format is sane.
13
14- Debian packages are in fact 'ar' files. Thus we run '/usr/bin/ar'
15- to look at the contents of the deb files to confirm they make sense.
16+ We run 'dpkg-deb' to look at the contents of the deb files to
17+ confirm they make sense.
18 """
19- ar_process = subprocess.Popen(
20- ["/usr/bin/ar", "t", self.filepath],
21- stdout=subprocess.PIPE)
22- output = ar_process.stdout.read()
23- result = ar_process.wait()
24- if result != 0:
25- yield UploadError(
26- "%s: 'ar t' invocation failed." % self.filename)
27- yield UploadError(
28- prefix_multi_line_string(output, " [ar output:] "))
29-
30- chunks = output.strip().split("\n")
31- if len(chunks) != 3:
32- yield UploadError(
33- "%s: found %d chunks, expecting 3. %r" % (
34- self.filename, len(chunks), chunks))
35-
36- debian_binary, control_tar, data_tar = chunks
37- if debian_binary != "debian-binary":
38- yield UploadError(
39- "%s: first chunk is %s, expected debian-binary." % (
40- self.filename, debian_binary))
41- if control_tar not in ("control.tar.gz", "control.tar.xz"):
42- yield UploadError(
43- "%s: second chunk is %s, expected control.tar.gz or "
44- "control.tar.xz." % (self.filename, control_tar))
45- if data_tar not in ("data.tar.gz", "data.tar.bz2", "data.tar.lzma",
46- "data.tar.xz"):
47- yield UploadError(
48- "%s: third chunk is %s, expected data.tar.gz, "
49- "data.tar.bz2, data.tar.lzma or data.tar.xz." %
50- (self.filename, data_tar))
51+ try:
52+ subprocess.check_output(
53+ ["dpkg-deb", "-I", self.filepath], stderr=subprocess.STDOUT)
54+ except subprocess.CalledProcessError as e:
55+ yield UploadError(
56+ "%s: 'dpkg-deb -I' invocation failed." % self.filename)
57+ yield UploadError(
58+ prefix_multi_line_string(e.output, " [dpkg-deb output:] "))
59+
60+ try:
61+ subprocess.check_output(
62+ ["dpkg-deb", "-c", self.filepath], stderr=subprocess.STDOUT)
63+ except subprocess.CalledProcessError as e:
64+ yield UploadError(
65+ "%s: 'dpkg-deb -c' invocation failed." % self.filename)
66+ yield UploadError(
67+ prefix_multi_line_string(e.output, " [dpkg-deb output:] "))
68
69 def verifyDebTimestamp(self):
70 """Check specific DEB format timestamp checks."""
71
72=== modified file 'lib/lp/archiveuploader/tests/test_nascentuploadfile.py'
73--- lib/lp/archiveuploader/tests/test_nascentuploadfile.py 2019-05-24 17:19:17 +0000
74+++ lib/lp/archiveuploader/tests/test_nascentuploadfile.py 2019-05-24 17:19:17 +0000
75@@ -7,19 +7,29 @@
76
77 __metaclass__ = type
78
79+from functools import partial
80+import gzip
81 import hashlib
82+import io
83 import os
84 import subprocess
85+import tarfile
86
87 from debian.deb822 import (
88 Changes,
89+ Deb822,
90 Dsc,
91 )
92+try:
93+ import lzma
94+except ImportError:
95+ from backports import lzma
96 from testtools.matchers import (
97 Contains,
98 Equals,
99 MatchesAny,
100 MatchesListwise,
101+ MatchesRegex,
102 )
103
104 from lp.archiveuploader.changesfile import ChangesFile
105@@ -360,11 +370,23 @@
106 "Priority": b"optional",
107 "Homepage": b"http://samba.org/~jelmer/dulwich",
108 "Description": b"Pure-python Git library\n"
109- b"Dulwich is a Python implementation of the file formats and "
110- b"protocols",
111+ b" Dulwich is a Python implementation of the file formats and"
112+ b" protocols",
113 }
114
115- def createDeb(self, filename, control_format, data_format, members=None):
116+ def _writeCompressedFile(self, filename, data):
117+ if filename.endswith(".gz"):
118+ open_func = gzip.open
119+ elif filename.endswith(".xz"):
120+ open_func = partial(lzma.LZMAFile, format=lzma.FORMAT_XZ)
121+ else:
122+ raise ValueError(
123+ "Unhandled compression extension in '%s'" % filename)
124+ with open_func(filename, "wb") as f:
125+ f.write(data)
126+
127+ def createDeb(self, filename, control, control_format, data_format,
128+ members=None):
129 """Return the contents of a dummy .deb file."""
130 tempdir = self.makeTemporaryDirectory()
131 if members is None:
132@@ -374,7 +396,31 @@
133 "data.tar.%s" % data_format,
134 ]
135 for member in members:
136- write_file(os.path.join(tempdir, member), b"")
137+ if member == "debian-binary":
138+ write_file(os.path.join(tempdir, member), b"2.0\n")
139+ elif member.startswith("control.tar."):
140+ with io.BytesIO() as control_tar_buf:
141+ with tarfile.open(
142+ mode="w", fileobj=control_tar_buf) as control_tar:
143+ with io.BytesIO() as control_buf:
144+ Deb822(control).dump(
145+ fd=control_buf, encoding="UTF-8")
146+ control_buf.seek(0)
147+ tarinfo = tarfile.TarInfo(name="control")
148+ tarinfo.size = len(control_buf.getvalue())
149+ control_tar.addfile(tarinfo, fileobj=control_buf)
150+ control_tar_bytes = control_tar_buf.getvalue()
151+ self._writeCompressedFile(
152+ os.path.join(tempdir, member), control_tar_bytes)
153+ elif member.startswith("data.tar."):
154+ with io.BytesIO() as data_tar_buf:
155+ with tarfile.open(mode="w", fileobj=data_tar_buf):
156+ pass
157+ data_tar_bytes = data_tar_buf.getvalue()
158+ self._writeCompressedFile(
159+ os.path.join(tempdir, member), data_tar_bytes)
160+ else:
161+ raise ValueError("Unhandled .deb member '%s'" % member)
162 retcode = subprocess.call(
163 ["ar", "rc", filename] + members, cwd=tempdir)
164 self.assertEqual(0, retcode)
165@@ -383,13 +429,16 @@
166
167 def createDebBinaryUploadFile(self, filename, component_and_section,
168 priority_name, package, version, changes,
169- control_format=None, data_format=None,
170- members=None):
171+ control=None, control_format=None,
172+ data_format=None, members=None):
173 """Create a DebBinaryUploadFile."""
174- if (control_format is not None or data_format is not None or
175- members is not None):
176+ if (control is not None or control_format is not None or
177+ data_format is not None or members is not None):
178+ if control is None:
179+ control = self.getBaseControl()
180 data = self.createDeb(
181- filename, control_format, data_format, members=members)
182+ filename, control, control_format, data_format,
183+ members=members)
184 else:
185 data = "DUMMY DATA"
186 (path, md5, sha1, size) = self.writeUploadFile(filename, data)
187@@ -415,13 +464,49 @@
188 self.assertEqual("0.42", uploadfile.source_version)
189 self.assertEqual("0.42", uploadfile.control_version)
190
191+ def test_verifyFormat_missing_control(self):
192+ # verifyFormat rejects .debs with no control member.
193+ uploadfile = self.createDebBinaryUploadFile(
194+ "foo_0.42_i386.deb", "main/python", "unknown", "mypkg", "0.42",
195+ None, members=["debian-binary", "data.tar.gz"])
196+ self.assertThat(
197+ ["".join(error.args) for error in uploadfile.verifyFormat()],
198+ MatchesListwise([
199+ Equals(
200+ "%s: 'dpkg-deb -I' invocation failed." %
201+ uploadfile.filename),
202+ MatchesRegex(
203+ r"^ \[dpkg-deb output:\] .* has premature member "
204+ r"'data\.tar\.gz'"),
205+ Equals(
206+ "%s: 'dpkg-deb -c' invocation failed." %
207+ uploadfile.filename),
208+ MatchesRegex(
209+ r"^ \[dpkg-deb output:\] .* has premature member "
210+ r"'data\.tar\.gz'"),
211+ ]))
212+
213+ def test_verifyFormat_missing_data(self):
214+ # verifyFormat rejects .debs with no data member.
215+ uploadfile = self.createDebBinaryUploadFile(
216+ "foo_0.42_i386.deb", "main/python", "unknown", "mypkg", "0.42",
217+ None, members=["debian-binary", "control.tar.gz"])
218+ self.assertThat(
219+ ["".join(error.args) for error in uploadfile.verifyFormat()],
220+ MatchesListwise([
221+ Equals(
222+ "%s: 'dpkg-deb -c' invocation failed." %
223+ uploadfile.filename),
224+ MatchesRegex(
225+ r"^ \[dpkg-deb output:\] .* unexpected end of file"),
226+ ]))
227+
228 def test_verifyFormat_control_xz(self):
229 # verifyFormat accepts .debs with an xz-compressed control member.
230 uploadfile = self.createDebBinaryUploadFile(
231 "foo_0.42_i386.deb", "main/python", "unknown", "mypkg", "0.42",
232 None, control_format="xz", data_format="gz")
233- control = self.getBaseControl()
234- uploadfile.parseControl(control)
235+ uploadfile.extractAndParseControl()
236 self.assertEqual([], list(uploadfile.verifyFormat()))
237
238 def test_verifyFormat_data_xz(self):
239@@ -429,8 +514,7 @@
240 uploadfile = self.createDebBinaryUploadFile(
241 "foo_0.42_i386.deb", "main/python", "unknown", "mypkg", "0.42",
242 None, control_format="gz", data_format="xz")
243- control = self.getBaseControl()
244- uploadfile.parseControl(control)
245+ uploadfile.extractAndParseControl()
246 self.assertEqual([], list(uploadfile.verifyFormat()))
247
248 def test_verifyDebTimestamp_SystemError(self):
249@@ -456,8 +540,8 @@
250 bpr = uploadfile.storeInDatabase(build)
251 self.assertEqual(u'python (<< 2.7), python (>= 2.5)', bpr.depends)
252 self.assertEqual(
253- u"Dulwich is a Python implementation of the file formats "
254- u"and protocols", bpr.description)
255+ u" Dulwich is a Python implementation of the file formats and"
256+ u" protocols", bpr.description)
257 self.assertEqual(False, bpr.essential)
258 self.assertEqual(524, bpr.installedsize)
259 self.assertEqual(True, bpr.architecturespecific)