#!/usr/bin/env python3 """ Verify OCI layout emitted by make-thin-v1.sh when OCI=1. Checks: 1) oci-layout exists and version is 1.0.0 2) index.json manifest digest/size match manifest.json hash/size 3) manifest.json references config/layers present in blobs with matching sha256 and size Usage: python scripts/mirror/verify_oci_layout.py out/mirror/thin/oci Exit 0 on success, non-zero on failure with message. """ import hashlib, json, pathlib, sys def sha256(path: pathlib.Path) -> str: h = hashlib.sha256() with path.open('rb') as f: for chunk in iter(lambda: f.read(8192), b''): h.update(chunk) return h.hexdigest() def main(): if len(sys.argv) != 2: print(__doc__) sys.exit(2) root = pathlib.Path(sys.argv[1]) layout = root / "oci-layout" index = root / "index.json" manifest = root / "manifest.json" if not layout.exists() or not index.exists() or not manifest.exists(): raise SystemExit("missing oci-layout/index.json/manifest.json") layout_obj = json.loads(layout.read_text()) if layout_obj.get("imageLayoutVersion") != "1.0.0": raise SystemExit("oci-layout version not 1.0.0") idx_obj = json.loads(index.read_text()) if not idx_obj.get("manifests"): raise SystemExit("index.json manifests empty") man_digest = idx_obj["manifests"][0]["digest"] man_size = idx_obj["manifests"][0]["size"] actual_man_sha = sha256(manifest) if man_digest != f"sha256:{actual_man_sha}": raise SystemExit(f"manifest digest mismatch: {man_digest} vs sha256:{actual_man_sha}") if man_size != manifest.stat().st_size: raise SystemExit("manifest size mismatch") man_obj = json.loads(manifest.read_text()) blobs = root / "blobs" / "sha256" # config cfg_digest = man_obj["config"]["digest"].split(":",1)[1] cfg_size = man_obj["config"]["size"] cfg_path = blobs / cfg_digest if not cfg_path.exists(): raise SystemExit(f"config blob missing: {cfg_path}") if cfg_path.stat().st_size != cfg_size: raise SystemExit("config size mismatch") if sha256(cfg_path) != cfg_digest: raise SystemExit("config digest mismatch") for layer in man_obj.get("layers", []): ldigest = layer["digest"].split(":",1)[1] lsize = layer["size"] lpath = blobs / ldigest if not lpath.exists(): raise SystemExit(f"layer blob missing: {lpath}") if lpath.stat().st_size != lsize: raise SystemExit("layer size mismatch") if sha256(lpath) != ldigest: raise SystemExit("layer digest mismatch") print("OK: OCI layout verified") if __name__ == "__main__": main()