_ _
| |_ ___| |_ _
| | . | | | |
|_|_|___|_|_ |
|___|
git mirror - github.com/owenewans/holy - branch master
file tests/import-deb.py
#!/usr/bin/env python3
import hashlib
import io
import lzma
import os
import pathlib
import platform
import subprocess
import sys
import tarfile
import tempfile
binary = str(pathlib.Path(sys.argv[1]).resolve())
def tar(members, mode="w:gz"):
output = io.BytesIO()
with tarfile.open(fileobj=output, mode=mode) as archive:
for name, body, kind in members:
entry = tarfile.TarInfo(name)
entry.uid, entry.gid = os.getuid(), os.getgid()
entry.mode = 0o755 if kind == "dir" else 0o644
if kind == "dir":
entry.type = tarfile.DIRTYPE
elif kind == "hardlink":
entry.type = tarfile.LNKTYPE
entry.linkname = body.decode()
else:
entry.size = len(body)
archive.addfile(entry, io.BytesIO(body) if kind == "file" else None)
return output.getvalue()
def ar(members):
output = bytearray(b"!<arch>\n")
for name, body in members:
assert len(name) <= 15
output.extend(f"{name + '/':<16}{0:<12}{0:<6}{0:<6}{0o100644:<8}{len(body):<10}`\n".encode())
output.extend(body)
if len(body) % 2:
output.extend(b"\n")
return bytes(output)
with tempfile.TemporaryDirectory() as scratch:
tmp = pathlib.Path(scratch)
def run(*args, status=0):
result = subprocess.run([binary, *map(str, args)], capture_output=True)
assert result.returncode == status, (args, result.returncode, result.stdout, result.stderr)
return result.stdout.decode()
def foreign(name, control=None, data=None, order=None):
control = control or b"Package: debfixture\nVersion: 1:2.0-3\nArchitecture: all\nDescription: fixture\n"
data = data or [("usr/", b"", "dir"), ("usr/share/", b"", "dir"),
("usr/share/debfixture", b"deb import\n", "file")]
members = order or [("debian-binary", b"2.0\n"),
("control.tar.gz", tar([("./control", control, "file")])),
("data.tar.gz", tar(data))]
path = tmp / (name + ".deb")
path.write_bytes(ar(members))
return path
def convert(path, name, status=0):
output = tmp / name
run("import", path, "--source", "fixture", "--format", "deb", "--output", output, status=status)
assert (output / "original").read_bytes() == path.read_bytes()
artifacts = list(output.glob("*.holy"))
if status:
assert not artifacts and not (output / "conversion").exists()
else:
assert len(artifacts) == 1
assert "family deb" in (output / "conversion").read_text()
assert hashlib.sha256(path.read_bytes()).hexdigest() in (output / "conversion").read_text()
run("verify", "local:" + str(artifacts[0]))
run("scan", "local:" + str(artifacts[0]))
return artifacts
artifact = convert(foreign("basic"), "basic-output")[0]
control_part = tar([("control", b"Package: debfixture\nVersion: 1\nArchitecture: all\n", "file")])
data_part = tar([("item", b"x", "file")])
convert(foreign("extended-format", order=[("debian-binary", b"2.1\nfuture field\n"),
("_optional", b"ignored"), ("control.tar.gz", control_part),
("data.tar.gz", data_part), ("future", b"ignored")]), "extended-format-output")
convert(foreign("bad-minor", order=[("debian-binary", b"2.x\n"),
("control.tar.gz", control_part), ("data.tar.gz", data_part)]),
"bad-minor-output", status=2)
convert(foreign("misnamed-codec", order=[("debian-binary", b"2.0\n"),
("control.tar.xz", control_part), ("data.tar.gz", data_part)]),
"misnamed-codec-output", status=2)
convert(foreign("misnamed-plain", order=[("debian-binary", b"2.0\n"),
("control.tar.gz", control_part), ("data.tar", data_part)]),
"misnamed-plain-output", status=2)
convert(foreign("unexpected-before-data", order=[("debian-binary", b"2.0\n"),
("control.tar.gz", control_part), ("future", b"ignored"),
("data.tar.gz", data_part)]), "unexpected-before-data-output", status=2)
convert(foreign("missing-data", order=[("debian-binary", b"2.0\n"),
("control.tar.gz", control_part)]), "missing-data-output", status=2)
zstd_control = subprocess.run(["zstd", "-q", "-c"], input=tar(
[("control", b"Package: debfixture\nVersion: 1\nArchitecture: all\n", "file")], "w"),
capture_output=True, check=True).stdout
zstd_data = subprocess.run(["zstd", "-q", "-c"], input=tar([("item", b"x", "file")], "w"),
capture_output=True, check=True).stdout
convert(foreign("zstd", order=[("debian-binary", b"2.0\n"),
("control.tar.zst", zstd_control), ("data.tar.zst", zstd_data)]), "zstd-output")
convert(foreign("misnamed-zstd", order=[("debian-binary", b"2.0\n"),
("control.tar.gz", zstd_control), ("data.tar.zst", zstd_data)]),
"misnamed-zstd-output", status=2)
lz4_data = subprocess.run(["lz4", "-q", "-c"], input=tar([("item", b"x", "file")], "w"),
capture_output=True, check=True).stdout
convert(foreign("unsupported-lz4", order=[("debian-binary", b"2.0\n"),
("control.tar.gz", control_part), ("data.tar", lz4_data)]),
"unsupported-lz4-output", status=2)
payload = b"deb import\n"
md5_line = hashlib.md5(payload).hexdigest().encode() + b" usr/share/debfixture\n"
def with_md5(name, checksums, body=payload):
return foreign(name, order=[("debian-binary", b"2.0\n"),
("control.tar.gz", tar([("control", b"Package: debfixture\nVersion: 1\nArchitecture: all\n", "file"),
("md5sums", checksums, "file")])),
("data.tar.gz", tar([("usr/", b"", "dir"), ("usr/share/", b"", "dir"),
("usr/share/debfixture", body, "file")]))])
convert(with_md5("md5-valid", md5_line), "md5-valid-output")
convert(with_md5("md5-mismatch", md5_line, b"changed\n"), "md5-mismatch-output", status=2)
convert(with_md5("md5-missing", b"0" * 32 + b" usr/share/missing\n"),
"md5-missing-output", status=2)
convert(with_md5("md5-duplicate", md5_line + md5_line), "md5-duplicate-output", status=2)
convert(with_md5("md5-invalid", b"0" * 32 + b" ../outside\n"),
"md5-invalid-output", status=2)
hardlink_checksums = md5_line + hashlib.md5(payload).hexdigest().encode() + b" usr/share/alias\n"
convert(foreign("md5-hardlink", order=[("debian-binary", b"2.0\n"),
("control.tar.gz", tar([("control", b"Package: debfixture\nVersion: 1\nArchitecture: all\n", "file"),
("md5sums", hardlink_checksums, "file")])),
("data.tar.gz", tar([("usr/", b"", "dir"), ("usr/share/", b"", "dir"),
("usr/share/debfixture", payload, "file"),
("usr/share/alias", b"usr/share/debfixture", "hardlink")]))]),
"md5-hardlink-output")
for codec, mode in (("plain", "w"), ("gzip", "w:gz"), ("xz", "w:xz"), ("bzip2", "w:bz2")):
control_part = tar([("control", b"Package: debfixture\nVersion: 1\nArchitecture: all\n", "file")],
mode if codec != "bzip2" else "w")
data_part = tar([("item", b"x", "file")], mode)
extension = {"plain": "", "gzip": ".gz", "xz": ".xz", "bzip2": ".bz2"}[codec]
convert(foreign(codec, order=[("debian-binary", b"2.0\n"),
("control.tar" + (extension if codec != "bzip2" else ""), control_part),
("data.tar" + extension, data_part)]), codec + "-output")
lzma_data = lzma.compress(tar([("item", b"x", "file")], "w"), format=lzma.FORMAT_ALONE)
convert(foreign("lzma", order=[("debian-binary", b"2.0\n"),
("control.tar", tar([("control", b"Package: debfixture\nVersion: 1\nArchitecture: all\n", "file")], "w")),
("data.tar.lzma", lzma_data)]), "lzma-output")
run("fetch", "local:" + str(artifact), "--extract", "--output", tmp / "extracted")
assert (tmp / "extracted/HOLY/foreign/deb/control").read_bytes().startswith(b"Package: debfixture")
root = tmp / "root"
root.mkdir()
run("db", "init", "--root", root)
run("cache", "stage", "local:" + str(artifact), "--root", root)
digest = hashlib.sha256(artifact.read_bytes()).hexdigest()
plan = run("db", "plan-set", digest, "--root", root).split(" sha256 ")[1].split()[0]
run("db", "apply-set", plan, digest, "--root", root)
run("db", "check", "--all", "--root", root)
assert (root / "usr/share/debfixture").read_bytes() == b"deb import\n"
run("db", "rm", digest, "--root", root)
convert(foreign("depends", control=b"Package: debfixture\nVersion: 1\nArchitecture: all\nDepends: other (>= 2) | else\n"), "depends-output")
dependency = next((tmp / "depends-output").glob("*.holy"))
assert '"package-or" "other@ge@2|else@any@-"' in run(
"requirements", "local:" + str(dependency))
run("solve", "local:" + str(dependency), status=4)
old_other = convert(foreign("old-other", control=b"Package: other\nVersion: 1\nArchitecture: all\n",
data=[("usr/share/old-other", b"old\n", "file")]), "old-other-output")[0]
new_other = convert(foreign("new-other", control=b"Package: other\nVersion: 2\nArchitecture: all\n",
data=[("usr/share/new-other", b"new\n", "file")]), "new-other-output")[0]
alternative = convert(foreign("else", control=b"Package: else\nVersion: 1\nArchitecture: all\n",
data=[("usr/share/else", b"else\n", "file")]), "else-output")[0]
run("solve", "local:" + str(dependency), "local:" + str(old_other), status=4)
assert hashlib.sha256(new_other.read_bytes()).hexdigest() in run(
"solve", "local:" + str(dependency), "local:" + str(old_other),
"local:" + str(new_other))
assert hashlib.sha256(alternative.read_bytes()).hexdigest() in run(
"solve", "local:" + str(dependency), "local:" + str(old_other),
"local:" + str(alternative))
run("solve", "local:" + str(dependency), "local:" + str(new_other),
"local:" + str(alternative), status=3)
alternative_hash = hashlib.sha256(alternative.read_bytes()).hexdigest()
dependency_hash = hashlib.sha256(dependency.read_bytes()).hexdigest()
for package, digest in ((alternative, alternative_hash), (dependency, dependency_hash)):
run("cache", "stage", "local:" + str(package), "--root", root)
selected_plan = run("db", "plan-set", digest, "--root", root).split(" sha256 ")[1].split()[0]
run("db", "apply-set", selected_plan, digest, "--root", root)
graph = (root / "var/lib/holypkg/installed" / dependency_hash / "graph").read_text()
assert alternative_hash in graph and '"else"' in graph
run("db", "rm", alternative_hash, "--root", root, status=3)
run("db", "rm", dependency_hash, "--root", root)
run("db", "rm", alternative_hash, "--root", root)
old_library = convert(foreign("old-library", control=b"Package: library\nVersion: 1.0~rc1-1\nArchitecture: all\n"),
"old-library-output")[0]
new_library = convert(foreign("new-library", control=b"Package: library\nVersion: 1.0-1\nArchitecture: all\n"),
"new-library-output")[0]
app = convert(foreign("app", control=b"Package: app\nVersion: 1\nArchitecture: all\nSource: app-src\nDepends: library (>= 1.0), helper\n"),
"app-output")[0]
edges = run("requirements", "local:" + str(app))
assert '"package" "library"' in edges and '"ge" "1.0"' in edges
assert '"package" "helper"' in edges and "foreign" not in edges
helper = convert(foreign("helper", control=b"Package: helper\nVersion: 1\nArchitecture: all\n"),
"helper-output")[0]
selected = run("solve", "local:" + str(app), "local:" + str(old_library),
"local:" + str(new_library), "local:" + str(helper))
assert hashlib.sha256(new_library.read_bytes()).hexdigest() in selected
run("solve", "local:" + str(app), "local:" + str(old_library), "local:" + str(helper), status=4)
virtual = convert(foreign("virtual", control=b"Package: virtual-impl\nVersion: 5\nArchitecture: all\nProvides: library (= 1.0), helper\n"),
"virtual-output")[0]
claims = run("provides", "local:" + str(virtual))
assert '"package" "library"' in claims and '"1.0"' in claims
assert hashlib.sha256(virtual.read_bytes()).hexdigest() in run(
"solve", "local:" + str(app), "local:" + str(virtual))
virtual_hash = hashlib.sha256(virtual.read_bytes()).hexdigest()
run("cache", "stage", "local:" + str(virtual), "--root", root)
virtual_plan = run("db", "plan-set", virtual_hash, "--root", root).split(" sha256 ")[1].split()[0]
run("db", "apply-set", virtual_plan, virtual_hash, "--root", root)
installed_app = convert(foreign("installed-app", control=b"Package: installed-app\nVersion: 1\nArchitecture: all\nDepends: library (>= 1.0), helper\n",
data=[("usr/", b"", "dir"), ("usr/share/", b"", "dir"),
("usr/share/app-marker", b"app\n", "file")]),
"installed-app-output")[0]
app_hash = hashlib.sha256(installed_app.read_bytes()).hexdigest()
run("cache", "stage", "local:" + str(installed_app), "--root", root)
app_plan = run("db", "plan-set", app_hash, "--root", root).split(" sha256 ")[1].split()[0]
run("db", "apply-set", app_plan, app_hash, "--root", root)
run("db", "check", "--all", "--root", root)
assert virtual_hash in (root / "var/lib/holypkg/installed" / app_hash / "graph").read_text()
run("db", "rm", virtual_hash, "--root", root, status=3)
run("db", "rm", app_hash, "--root", root)
run("db", "rm", virtual_hash, "--root", root)
unversioned = convert(foreign("unversioned", control=b"Package: unversioned-impl\nVersion: 1\nArchitecture: all\nProvides: library, helper\n"),
"unversioned-output")[0]
run("solve", "local:" + str(app), "local:" + str(unversioned), status=4)
bad_claim = convert(foreign("bad-claim", control=b"Package: bad-impl\nVersion: 1\nArchitecture: all\nProvides: library (>= 1.0)\n"),
"bad-claim-output")[0]
assert "foreign" in run("requirements", "local:" + str(bad_claim))
run("solve", "local:" + str(bad_claim), status=3)
duplicate_claim = convert(foreign("duplicate-claim", control=b"Package: duplicate-impl\nVersion: 1\nArchitecture: all\nProvides: library, library\n"),
"duplicate-claim-output")[0]
assert "foreign" in run("requirements", "local:" + str(duplicate_claim))
run("solve", "local:" + str(duplicate_claim), status=3)
compact = convert(foreign("compact", control=b"Package: compact\nVersion: 1\nArchitecture: all\nDepends: library(>=1.0)\n"),
"compact-output")[0]
assert '"package" "library"' in run("requirements", "local:" + str(compact))
unsupported = convert(foreign("predepends", control=b"Package: predepends\nVersion: 1\nArchitecture: all\nPre-Depends: library\n"),
"predepends-output")[0]
assert "foreign" in run("requirements", "local:" + str(unsupported))
sentinel = tmp / "hook-ran"
hook = ("#!/bin/sh\necho unexpected > " + str(sentinel) + "\n").encode()
hook_control = tar([("control", b"Package: debfixture\nVersion: 1\nArchitecture: all\n", "file"),
("postinst", hook, "file")])
hook_artifact = convert(foreign("hook", order=[("debian-binary", b"2.0\n"),
("control.tar.gz", hook_control), ("data.tar.gz", tar([("value", b"x", "file")]))]),
"hook-output")[0]
run("fetch", "local:" + str(hook_artifact), "--extract", "--output", tmp / "hook-extracted")
assert (tmp / "hook-extracted/HOLY/foreign/deb/postinst").read_bytes() == hook
assert not sentinel.exists()
config_artifact = convert(foreign("conffiles", order=[("debian-binary", b"2.0\n"),
("control.tar.gz", tar([("control", b"Package: debfixture\nVersion: 1\nArchitecture: all\n", "file"),
("conffiles", b"/etc/example\n", "file")])),
("data.tar.gz", tar([("etc/", b"", "dir"), ("etc/example", b"x", "file")]))]),
"conffiles-output")[0]
assert 'etc/example mode=0644' in run("manifest", "local:" + str(config_artifact))
assert ' config' in run("manifest", "local:" + str(config_artifact))
for label, conffile in (("missing", b"/etc/absent\n"),
("duplicate-conffile", b"/etc/example\n/etc/example\n"),
("unsafe-conffile", b"/etc/../example\n")):
convert(foreign(label, order=[("debian-binary", b"2.0\n"),
("control.tar.gz", tar([("control", b"Package: debfixture\nVersion: 1\nArchitecture: all\n", "file"),
("conffiles", conffile, "file")])),
("data.tar.gz", tar([("etc/", b"", "dir"), ("etc/example", b"x", "file")]))]),
label + "-output", status=2)
convert(foreign("duplicate", data=[("usr/", b"", "dir"), ("usr/share/a", b"x", "file"),
("usr/share/a", b"y", "file")]), "duplicate-output", status=2)
convert(foreign("traversal", data=[("../outside", b"x", "file")]), "traversal-output", status=2)
convert(foreign("bad-version", order=[("debian-binary", b"3.0\n"),
("control.tar.gz", tar([("control", b"Package: x\nVersion: 1\nArchitecture: all\n", "file")])),
("data.tar.gz", tar([("file", b"x", "file")]))]), "bad-version-output", status=2)
convert(foreign("bad-order", order=[("debian-binary", b"2.0\n"),
("data.tar.gz", tar([("file", b"x", "file")]))]), "bad-order-output", status=2)
convert(foreign("duplicate-field", control=b"Package: debfixture\nVersion: 1\nVersion: 2\nArchitecture: all\n"),
"duplicate-field-output", status=2)
if platform.machine() == "x86_64":
source = tmp / "hello.c"
source.write_text("int main(void) { return 0; }\n")
subprocess.run(["gcc", "-o", str(tmp / "hello"), str(source)], check=True)
elf_data = [("usr/", b"", "dir"), ("usr/bin/", b"", "dir"),
("usr/bin/hello", (tmp / "hello").read_bytes(), "file")]
convert(foreign("wrong-arch", data=elf_data), "wrong-arch-output", status=3)
convert(foreign("elf", control=b"Package: debfixture\nVersion: 1\nArchitecture: amd64\n",
data=elf_data), "elf-output")
thin = tmp / "thin.deb"
thin.write_bytes(b"!<thin>\n")
convert(thin, "thin-output", status=2)