git mirror - github.com/owenewans/holy - branch master
clone: https://src.holypkg.eu/holy/

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)