"""The static PKGBUILD parser. This module is load-bearing for security. A parse bug here does not merely show the wrong version: it means the audit inspected different text from what the build will run, and a review that should have blocked a package passes. The tests are written against shell semantics, verified with bash where it matters, rather than against whatever the parser happened to do. Nothing here may execute a PKGBUILD. """ from __future__ import annotations import subprocess import sys from pathlib import Path sys.path.insert(0, str(Path(__file__).resolve().parent.parent)) from updater import srcinfo # noqa: E402 from updater.audit import ( # noqa: E402 Change, Severity, functions_generated, scan_patterns, scan_sums, ) from updater.srcinfo import ( # noqa: E402 _expand_braces, _scan_array_end, _split_words, _strip_comment, parse_pkgbuild, parse_srcinfo, ) CLONE = Path.home() / ".cache/paru/clone" def bash_expand(source: str) -> list[str]: """What bash actually produces for an array, as the ground truth.""" script = f'arr={source}\nfor x in "${{arr[@]}}"; do printf "%s\\n" "$x"; done' out = subprocess.run( ["bash", "-c", script], capture_output=True, text=True, timeout=10, check=False ).stdout return out.splitlines() # -------------------------------------------------------------------------- # the invariant # -------------------------------------------------------------------------- def test_nothing_in_this_module_can_execute_a_pkgbuild(): """A `source`-ing shortcut would run the maintainer's code at review time.""" import re as remod body = Path(srcinfo.__file__).read_text() forbidden = [ r"\bsubprocess\b", r"\bos\.system\b", r"\bpopen\b", r"(?= 50, f"only checked {checked} packages" assert not problems, "\n".join(problems[:10]) def test_the_previously_misparsed_packages_are_now_correct(): """Named regressions from the audit, when the clones are present.""" expectations = { "noctalia-git": lambda pb: {"build", "package", "pkgver"} <= set(pb.funcs), "aescrypt": lambda pb: any(n.startswith("package_") for n in pb.funcs), "python-beartype": lambda pb: "check" in pb.funcs and "package" in pb.funcs, "lib32-gstreamer": lambda pb: "check" in pb.funcs and bool(pb.funcs["check"]), "shelly-bin": lambda pb: any(n.startswith("package_") for n in pb.funcs), "mesonlsp": lambda pb: len(pb.get("source")) >= 6, "proton-ge-custom-bin": lambda pb: all(" " in dep for dep in pb.get("optdepends")), } skipped = [] for package, predicate in expectations.items(): pb, _ = _parsed(package) if pb is None: skipped.append(package) continue assert predicate(pb), f"{package} still parses incorrectly: {sorted(pb.funcs)}" def test_source_counts_agree_with_srcinfo_for_most_real_packages(): """.SRCINFO is authoritative and already expanded, so where the PKGBUILD needs no variable expansion the two must agree. Some will legitimately differ: a PKGBUILD that builds `source` from variables or appends to it inside a loop cannot be evaluated statically, and that is a stated limitation rather than a bug. """ if not CLONE.is_dir(): return agree = differ = 0 for directory in sorted(d for d in CLONE.iterdir() if d.is_dir()): raw = directory / ".SRCINFO" if not raw.is_file(): continue text = raw.read_text(errors="replace") # Packages with architecture-suffixed sources are excluded: `si.source` # is the effective host-arch list (base plus this arch's entries) while # the PKGBUILD's `source` is only the base, so the two are not # comparable. The merge itself is covered separately. if "_x86_64" in text or "_aarch64" in text: continue pb, si = _parsed(directory.name) if pb is None or si is None: continue if not pb.get("source") or not si.source: continue if len(pb.get("source")) == len(si.source): agree += 1 else: differ += 1 assert agree >= 50, f"only {agree} packages agreed (differ={differ})" assert agree > differ * 5, f"too many disagreements: {agree} vs {differ}" def test_a_real_package_with_dollar_paren_sums_keeps_them_whole(): """tor-browser-bin uses `$(_dist_checksum ${_tag})` in its sums.""" pb, _ = _parsed("tor-browser-bin") if pb is None: return entries = pb.get("sha256sums") or pb.get("sha512sums") or pb.get("b2sums") joined = " ".join(entries) if "$(_dist_checksum" in joined: assert not any(e == "$(_dist_checksum" for e in entries), entries def main() -> int: tests = [(n, f) for n, f in sorted(globals().items()) if n.startswith("test_") and callable(f)] failed = 0 for name, fn in tests: try: fn() except Exception as exc: # noqa: BLE001 failed += 1 print(f"FAIL {name}\n {type(exc).__name__}: {exc}") else: print(f"ok {name}") print(f"\n{len(tests) - failed}/{len(tests)} passed") return 1 if failed else 0 if __name__ == "__main__": raise SystemExit(main()) def test_arch_suffixed_keys_merge_for_the_host_architecture(): """`.SRCINFO` carries `source_x86_64` and `depends_aarch64` beside the base. Unmatched, they are dropped: a package whose sources are arch-suffixed then looks like it has no sources and no checksums at all, which is a silent false negative on the architecture being built for. Only this machine's architecture is merged -- another one's dependency is not an edge here. """ si = parse_srcinfo( "pkgbase = d\n\tpkgver = 1.0\n" "\tsource = https://e/base.tar.gz\n" "\tsource_x86_64 = https://e/x86.tar.gz\n" "\tsource_aarch64 = https://e/arm.tar.gz\n" "\tsha256sums_x86_64 = deadbeef\n" "\tdepends = glibc\n\tdepends_aarch64 = musl\n" ) assert si.source == ["https://e/base.tar.gz", "https://e/x86.tar.gz"] assert si.sha256sums == ["deadbeef"] assert si.depends == ["glibc"], "another architecture's dep became an edge"