#!/usr/bin/env python3 """Verify ZIP safety, embedded checksums and extracted build-pack validation.""" from __future__ import annotations import argparse import hashlib import stat import subprocess import sys import tempfile import zipfile from pathlib import Path, PurePosixPath def parse_manifest(data: bytes) -> dict[str, str]: result: dict[str, str] = {} for number, line in enumerate(data.decode("utf-8").splitlines(), 1): if not line.strip(): continue try: digest, relative = line.split(" ", 1) except ValueError as exc: raise ValueError(f"Invalid checksum line {number}") from exc if len(digest) != 64 or any(c not in "0123456789abcdef" for c in digest): raise ValueError(f"Invalid SHA-256 at line {number}") if relative in result: raise ValueError(f"Duplicate checksum path: {relative}") result[relative] = digest return result def main() -> int: parser = argparse.ArgumentParser() parser.add_argument("archive", type=Path) args = parser.parse_args() archive_path = args.archive.resolve() if not archive_path.is_file(): raise SystemExit(f"Archive not found: {archive_path}") with zipfile.ZipFile(archive_path) as archive: infos = archive.infolist() names = [info.filename for info in infos] if len(names) != len(set(names)): raise SystemExit("Archive contains duplicate paths") for info in infos: path = PurePosixPath(info.filename) if path.is_absolute() or ".." in path.parts or not path.parts: raise SystemExit(f"Unsafe archive path: {info.filename}") mode = (info.external_attr >> 16) & 0xFFFF if stat.S_ISLNK(mode): raise SystemExit(f"Archive contains symlink: {info.filename}") bad = archive.testzip() if bad: raise SystemExit(f"CRC failure: {bad}") manifest_names = [n for n in names if n.endswith("/PACK_MANIFEST.sha256")] index_names = [n for n in names if n.endswith("/FILE_INDEX.txt")] if len(manifest_names) != 1 or len(index_names) != 1: raise SystemExit("Archive must contain one checksum manifest and one file index") manifest_name = manifest_names[0] prefix = manifest_name[: -len("PACK_MANIFEST.sha256")] if index_names[0] != prefix + "FILE_INDEX.txt": raise SystemExit("Checksum manifest and file index do not share one root") manifest = parse_manifest(archive.read(manifest_name)) index = {line for line in archive.read(index_names[0]).decode("utf-8").splitlines() if line} archived_relative = {n[len(prefix):] for n in names if n.startswith(prefix) and not n.endswith("/")} if index != archived_relative: missing = sorted(index - archived_relative) extra = sorted(archived_relative - index) raise SystemExit(f"FILE_INDEX mismatch; missing={missing}, extra={extra}") expected_manifest_paths = archived_relative - {"PACK_MANIFEST.sha256"} if set(manifest) != expected_manifest_paths: raise SystemExit("Checksum manifest path set does not match archive") for relative, expected in manifest.items(): actual = hashlib.sha256(archive.read(prefix + relative)).hexdigest() if actual != expected: raise SystemExit(f"Checksum mismatch: {relative}") with tempfile.TemporaryDirectory(prefix="devrunbook-verify-") as temp: archive.extractall(temp) root = Path(temp) / PurePosixPath(prefix).parts[0] subprocess.run([sys.executable, str(root / "scripts/validate_pack.py")], cwd=root, check=True) print("DevRunbook archive verification PASSED") print(f"- Entries: {len(names)}") print("- Safe paths, no duplicates, no symlinks and valid CRCs") print("- Embedded file index and SHA-256 manifest verified") print("- Extracted build-pack validator passed") return 0 if __name__ == "__main__": raise SystemExit(main())