"""Verify the pinned Cygwin build-input archive before offline installation.""" import argparse import hashlib import json from pathlib import Path, PurePosixPath import shutil import tarfile def records(manifest): metadata = manifest["metadata"] if {item["file"] for item in metadata} != { "x86_64/setup.xz", "x86_64/setup.xz.sig", }: raise ValueError("signed setup metadata is required") entries = metadata + manifest["packages"] expected = {entry["file"]: entry for entry in entries} if len(expected) != len(entries): raise ValueError("duplicate Cygwin input") for name in expected: path = PurePosixPath(name) if ( not path.parts or path.is_absolute() or ".." in path.parts or path.as_posix() != name or "\\" in name or ":" in name ): raise ValueError(f"unsafe Cygwin input: {name}") return expected def extract(manifest, archive, destination): expected = records(manifest) pin = manifest["archive"] with archive.open("rb") as stream: if ( archive.stat().st_size != pin["bytes"] or hashlib.file_digest(stream, "sha256").hexdigest() != pin["sha256"] ): raise ValueError("Cygwin archive digest or size mismatch") stream.seek(0) with tarfile.open(fileobj=stream, mode="r:gz") as bundle: members = bundle.getmembers() if len(members) != len(expected) or {m.name for m in members} != set( expected ): raise ValueError("missing, extra, or duplicate Cygwin inputs") for member in members: if not member.isfile() or member.issparse(): raise ValueError(f"non-regular Cygwin input: {member.name}") with bundle.extractfile(member) as source: if ( member.size != expected[member.name]["bytes"] or hashlib.file_digest(source, "sha512").hexdigest() != expected[member.name]["sha512"] ): raise ValueError(f"Cygwin input mismatch: {member.name}") destination.mkdir(parents=True) try: bundle.extractall(destination, members=members, filter="data") except BaseException: shutil.rmtree(destination) raise def check_installed(manifest, inventory): installed = sorted( tuple(line.split()) for line in inventory.read_text().splitlines() if len(line.split()) == 2 and not line.startswith("Package ") ) expected = sorted((item["name"], item["version"]) for item in manifest["packages"]) if installed != expected: raise ValueError("installed Cygwin packages differ from reviewed inputs") if __name__ == "__main__": parser = argparse.ArgumentParser(description=__doc__) parser.add_argument("command", choices=("extract", "check-installed")) parser.add_argument("--archive", type=Path) parser.add_argument("--directory", type=Path) parser.add_argument("--inventory", type=Path) args = parser.parse_args() pinned = json.loads( Path(__file__).with_name("voice-cygwin-snapshot.json").read_text() ) if args.command == "extract": if args.archive is None or args.directory is None: parser.error("extract requires --archive and --directory") extract(pinned, args.archive, args.directory) else: if args.inventory is None: parser.error("check-installed requires --inventory") check_installed(pinned, args.inventory)