Vili/scripts/test_unpack_debs.py
Meta Vens f344cef3b9 Tests for unpack-debs.py: only libraries, flat, sibling links, no overwrite
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-24 21:20:52 +02:00

115 lines
4.9 KiB
Python

"""deployment/idunn/unpack-debs.py takes a Debian package's shared libraries and nothing else."""
import importlib.util
import io
import pathlib
import tarfile
import tempfile
import unittest
SOURCE = pathlib.Path(__file__).resolve().parents[1] / "deployment" / "idunn" / "unpack-debs.py"
spec = importlib.util.spec_from_file_location("unpack_debs", SOURCE)
unpack_debs = importlib.util.module_from_spec(spec)
spec.loader.exec_module(unpack_debs)
LIB = "./usr/lib/x86_64-linux-gnu/"
def deb(path, members, compression="xz", marker=b"2.0\n"):
"""A .deb: an ar archive of a debian-binary marker and a data tarball holding `members` (name, kind, payload)."""
data = io.BytesIO()
with tarfile.open(fileobj=data, mode=f"w:{compression}") as archive:
for name, kind, payload in members:
info = tarfile.TarInfo(name)
if kind == "file":
info.size = len(payload)
archive.addfile(info, io.BytesIO(payload))
elif kind in ("symlink", "hardlink"):
info.type = tarfile.SYMTYPE if kind == "symlink" else tarfile.LNKTYPE
info.linkname = payload
archive.addfile(info)
else:
info.type = tarfile.DIRTYPE
archive.addfile(info)
body = data.getvalue()
def member(name, content):
header = f"{name + '/':<16}{0:<12}{0:<6}{0:<6}{'100644':<8}{len(content):<10}`\n".encode("ascii")
return header + content + (b"\n" if len(content) % 2 else b"")
path.write_bytes(b"!<arch>\n" + member("debian-binary", marker) + member(f"data.tar.{compression}", body))
class UnpackDebs(unittest.TestCase):
def setUp(self):
self.tmp = tempfile.TemporaryDirectory()
self.dir = pathlib.Path(self.tmp.name)
self.out = self.dir / "lib"
self.out.mkdir()
def tearDown(self):
self.tmp.cleanup()
def test_takes_only_shared_libraries_flat_and_keeps_sibling_links(self):
package = self.dir / "a.deb"
deb(package, [
(LIB, "dir", b""),
(LIB + "libfoo.so.1.2", "file", b"ELF"),
(LIB + "libfoo.so.1", "symlink", "libfoo.so.1.2"),
(LIB + "libfoo.a", "file", b"static"),
(LIB + "pkgconfig/foo.pc", "file", b"pc"),
("./usr/share/doc/foo/copyright", "file", b"text"),
("./etc/foo.so.conf", "file", b"conf"),
])
self.assertEqual(sorted(unpack_debs.unpack(package, self.out)), ["libfoo.so.1", "libfoo.so.1.2"])
self.assertEqual(sorted(entry.name for entry in self.out.iterdir()), ["libfoo.so.1", "libfoo.so.1.2"])
self.assertEqual((self.out / "libfoo.so.1.2").read_bytes(), b"ELF")
self.assertEqual((self.out / "libfoo.so.1").readlink().as_posix(), "libfoo.so.1.2")
self.assertEqual((self.out / "libfoo.so.1.2").stat().st_mode & 0o777, 0o644)
def test_a_gzip_data_archive_is_read_too(self):
package = self.dir / "g.deb"
deb(package, [(LIB + "libbar.so.0", "file", b"ELF")], compression="gz")
self.assertEqual(unpack_debs.unpack(package, self.out), ["libbar.so.0"])
def test_refuses_to_overwrite(self):
(self.out / "libfoo.so.1").write_bytes(b"already here")
package = self.dir / "a.deb"
deb(package, [(LIB + "libfoo.so.1", "file", b"ELF")])
with self.assertRaises(SystemExit):
unpack_debs.unpack(package, self.out)
self.assertEqual((self.out / "libfoo.so.1").read_bytes(), b"already here")
def test_refuses_a_link_that_leaves_the_directory(self):
for target in ("/etc/passwd", "../../etc/passwd", "sub/libfoo.so.1", ".."):
package = self.dir / "l.deb"
deb(package, [(LIB + "libfoo.so.1", "symlink", target)])
with self.assertRaises(SystemExit, msg=target):
unpack_debs.unpack(package, self.out)
self.assertEqual(list(self.out.iterdir()), [], target)
def test_a_package_with_no_library_is_an_error(self):
package = self.dir / "n.deb"
deb(package, [("./usr/share/doc/foo/copyright", "file", b"text")])
with self.assertRaises(SystemExit):
unpack_debs.unpack(package, self.out)
def test_an_odd_sized_member_before_the_data_is_padded_over(self):
package = self.dir / "o.deb"
deb(package, [(LIB + "libodd.so.0", "file", b"ELF")], marker=b"2.0 x")
self.assertEqual(unpack_debs.unpack(package, self.out), ["libodd.so.0"])
def test_refuses_a_member_that_is_neither_a_file_nor_a_symlink(self):
package = self.dir / "h.deb"
deb(package, [(LIB + "libhard.so.0", "hardlink", "libother.so.0")])
with self.assertRaises(SystemExit):
unpack_debs.unpack(package, self.out)
def test_not_an_ar_archive_is_an_error(self):
package = self.dir / "x.deb"
package.write_bytes(b"PK\x03\x04 not a deb")
with self.assertRaises(SystemExit):
unpack_debs.unpack(package, self.out)
if __name__ == "__main__":
unittest.main()