#!/usr/bin/env python from __future__ import annotations import argparse import hashlib import json import sys import zipfile from pathlib import PurePosixPath from typing import Any ROOT_NAME = "VacatureRadar_Project" MANIFEST_PATH = f"{ROOT_NAME}/PROJECT_MANIFEST.json" REQUIRED_PATHS = { f"{ROOT_NAME}/README.md", f"{ROOT_NAME}/CODEX_START_HERE.md", f"{ROOT_NAME}/AGENTS.md", f"{ROOT_NAME}/docs/ai/BACKLOG.yaml", f"{ROOT_NAME}/docs/ai/PROJECT_STATE.md", f"{ROOT_NAME}/scripts/codex_bootstrap.sh", f"{ROOT_NAME}/scripts/codex_verify.sh", f"{ROOT_NAME}/uv.lock", MANIFEST_PATH, } FORBIDDEN_PARTS = { ".git", ".venv", ".pytest_cache", ".ruff_cache", "__pycache__", "backups", "logs", "media", "staticfiles", } FORBIDDEN_NAMES = {".env", ".coverage", "db.sqlite3", "celerybeat-schedule"} class PackageError(ValueError): pass def digest(data: bytes) -> str: return hashlib.sha256(data).hexdigest() def load_manifest(archive: zipfile.ZipFile) -> dict[str, Any]: try: payload = json.loads(archive.read(MANIFEST_PATH)) except KeyError as exc: raise PackageError("PROJECT_MANIFEST.json ontbreekt") from exc except json.JSONDecodeError as exc: raise PackageError(f"PROJECT_MANIFEST.json is ongeldig: {exc}") from exc if not isinstance(payload, dict) or not isinstance(payload.get("files"), list): raise PackageError("PROJECT_MANIFEST.json heeft een ongeldig schema") return payload def validate_names(names: list[str]) -> None: if len(names) != len(set(names)): raise PackageError("ZIP bevat dubbele padnamen") missing = sorted(REQUIRED_PATHS - set(names)) if missing: raise PackageError(f"Verplichte paden ontbreken: {missing}") for name in names: path = PurePosixPath(name) if path.is_absolute() or ".." in path.parts: raise PackageError(f"Onveilig ZIP-pad: {name}") if not path.parts or path.parts[0] != ROOT_NAME: raise PackageError(f"Bestand staat buiten projectroot: {name}") if any(part in FORBIDDEN_PARTS for part in path.parts): raise PackageError(f"Runtime/cachepad hoort niet in ZIP: {name}") if path.name in FORBIDDEN_NAMES: raise PackageError(f"Runtime/secretbestand hoort niet in ZIP: {name}") if path.suffix in {".pyc", ".pyo"}: raise PackageError(f"Bytecode hoort niet in ZIP: {name}") def validate_manifest(archive: zipfile.ZipFile, payload: dict[str, Any]) -> None: entries = payload["files"] expected_paths: set[str] = set() total_bytes = 0 for entry in entries: if not isinstance(entry, dict): raise PackageError("Manifestentry is geen mapping") relative = entry.get("path") expected_size = entry.get("bytes") expected_hash = entry.get("sha256") if not isinstance(relative, str) or not relative: raise PackageError("Manifestentry mist path") archive_path = f"{ROOT_NAME}/{relative}" if archive_path in expected_paths: raise PackageError(f"Dubbele manifestentry: {relative}") expected_paths.add(archive_path) try: data = archive.read(archive_path) except KeyError as exc: raise PackageError(f"Manifestbestand ontbreekt in ZIP: {relative}") from exc if len(data) != expected_size: raise PackageError(f"Grootte wijkt af voor {relative}") if digest(data) != expected_hash: raise PackageError(f"SHA-256 wijkt af voor {relative}") total_bytes += len(data) actual_paths = { name for name in archive.namelist() if not name.endswith("/") and name != MANIFEST_PATH } if expected_paths != actual_paths: extra = sorted(actual_paths - expected_paths) missing = sorted(expected_paths - actual_paths) raise PackageError(f"Manifest/ZIP-paden verschillen; extra={extra}, missing={missing}") if payload.get("file_count_excluding_manifest") != len(entries): raise PackageError("Manifest file_count klopt niet") if payload.get("total_bytes_excluding_manifest") != total_bytes: raise PackageError("Manifest total_bytes klopt niet") def main() -> int: parser = argparse.ArgumentParser(description="Verifieer een VacatureRadar-project-ZIP") parser.add_argument("zip_path") args = parser.parse_args() try: with zipfile.ZipFile(args.zip_path) as archive: corrupt = archive.testzip() if corrupt: raise PackageError(f"CRC-fout in {corrupt}") names = [name for name in archive.namelist() if not name.endswith("/")] validate_names(names) validate_manifest(archive, load_manifest(archive)) except (OSError, zipfile.BadZipFile, PackageError) as exc: print(f"Packagevalidatiefout: {exc}", file=sys.stderr) return 1 print(f"Project-ZIP geldig: {args.zip_path}") return 0 if __name__ == "__main__": raise SystemExit(main())