150 lines
7.1 KiB
Python
150 lines
7.1 KiB
Python
"""Compare captured input maps to exact named Git trees; no builds or checkouts."""
|
|
from pathlib import Path, PurePosixPath
|
|
import argparse
|
|
import hashlib
|
|
import json
|
|
import os
|
|
import subprocess
|
|
import zipfile
|
|
|
|
HERE = Path(__file__).resolve().parent
|
|
COMMITS = {"desktop-4": "12a7352ddef5f69a144ead8219a50916233d63a9",
|
|
"desktop-5": "f6a6010baed9e6164a1fcc7ecd58256a562827d7"}
|
|
COMPARISON = "Exact committed Git blobs equal every captured input; same complete filtered inventory"
|
|
FILES = {".gitattributes", "verify.py", "README.md", "verify-committed-inputs.py",
|
|
"committed-desktop-4.json", "committed-desktop-5.json"}
|
|
|
|
|
|
def need(ok, message):
|
|
if not ok:
|
|
raise ValueError(message)
|
|
|
|
|
|
def pin(data):
|
|
return {"bytes": len(data), "sha256": hashlib.sha256(data).hexdigest()}
|
|
|
|
|
|
def parsed(data):
|
|
def unique(items):
|
|
result = {}
|
|
for key, value in items:
|
|
need(key not in result, "duplicate JSON key: " + key)
|
|
result[key] = value
|
|
return result
|
|
return json.loads(data, object_pairs_hook=unique)
|
|
|
|
|
|
def safe(name):
|
|
return (isinstance(name, str) and bool(name) and "\\" not in name and ":" not in name
|
|
and not PurePosixPath(name).is_absolute()
|
|
and all(part not in ("", ".", "..") for part in name.split("/")))
|
|
|
|
|
|
def relevant(path):
|
|
return path in ("Cargo.toml", "Cargo.lock", "rust-toolchain.toml") or path.startswith(
|
|
("crates/", "build/", ".cargo/", "scripts/", "docs/", "examples/", "harness/fixtures/", "harness/prototypes/"))
|
|
|
|
|
|
def git(repo, *arguments, data=None):
|
|
environment = dict(os.environ, GIT_NO_LAZY_FETCH="1")
|
|
return subprocess.run(["git", *arguments], cwd=repo, input=data, stdout=subprocess.PIPE,
|
|
stderr=subprocess.PIPE, check=True, env=environment).stdout
|
|
|
|
|
|
def compare_commit(repo, commit, inputs):
|
|
need(git(repo, "rev-parse", "--verify", commit + "^{commit}").decode().strip() == commit, "named commit unavailable")
|
|
entries = git(repo, "ls-tree", "-r", "--full-tree", "-z", commit).split(b"\0")
|
|
blobs = {}
|
|
for entry in filter(None, entries):
|
|
header, path_bytes = entry.split(b"\t", 1)
|
|
path = path_bytes.decode("utf-8")
|
|
if relevant(path):
|
|
_mode, kind, oid = header.split()
|
|
need(kind == b"blob", "relevant non-blob tree entry: " + path)
|
|
need(path not in blobs, "duplicate tree path")
|
|
blobs[path] = oid
|
|
need(set(blobs) == set(inputs), "complete filtered Git inventory differs; missing="
|
|
+ repr(sorted(set(inputs) - set(blobs))) + "; additional=" + repr(sorted(set(blobs) - set(inputs))))
|
|
# Object IDs avoid path quoting and commit:path interpretation in batch input.
|
|
paths = sorted(blobs)
|
|
data = git(repo, "cat-file", "--batch", data=b"".join(blobs[path] + b"\n" for path in paths))
|
|
offset, actual = 0, {}
|
|
for path in paths:
|
|
end = data.index(b"\n", offset)
|
|
oid, kind, size_bytes = data[offset:end].split()
|
|
size = int(size_bytes)
|
|
need(oid == blobs[path] and kind == b"blob" and size >= 0, "Git batch object relationship")
|
|
content = data[end + 1:end + 1 + size]
|
|
need(len(content) == size and data[end + 1 + size:end + 2 + size] == b"\n", "Git batch framing")
|
|
actual[path] = pin(content)
|
|
offset = end + 2 + size
|
|
need(offset == len(data), "trailing Git batch bytes")
|
|
different = [path for path in paths if actual[path] != inputs[path]]
|
|
need(not different, "captured bytes differ from named commit: " + repr(different))
|
|
|
|
|
|
def packaged(checks, runs):
|
|
files = parsed((checks / "files.json").read_bytes())
|
|
need(set(files) == {"prepare.py", "verify.py", "README.md", "manifest.json", "evidence.zip"}, "checks outer inventory")
|
|
for name, expected in files.items():
|
|
need(pin((checks / name).read_bytes()) == expected, "checks outer pin: " + name)
|
|
manifest = parsed((checks / "manifest.json").read_bytes())
|
|
need(manifest["schema"] == 1 and manifest["kind"] == "multiselect-cargo-gates", "checks manifest kind")
|
|
captured = {}
|
|
with zipfile.ZipFile(checks / "evidence.zip") as archive:
|
|
names = archive.namelist()
|
|
need(len(names) == len(set(names)) and set(names) == set(manifest["members"])
|
|
and all(safe(name) for name in names), "checks exact safe members")
|
|
def raw(logical):
|
|
member = manifest["logicalFiles"][logical]
|
|
data = archive.read(member)
|
|
need(pin(data) == manifest["members"][member], "checks member pin: " + logical)
|
|
return data
|
|
for run in runs:
|
|
prefix = "cargo/" + run + "/"
|
|
result = parsed(raw(prefix + "result.json"))
|
|
spec = manifest["runs"][run]
|
|
need(all(result[key] == spec[key] for key in ("command", "head", "exitCode", "inputCount")), run + ": checks run relationships")
|
|
captured[run] = (raw(prefix + "inputs.json"), result)
|
|
return captured
|
|
|
|
|
|
def verify(repo, captured):
|
|
outer = parsed((HERE / "files.json").read_bytes())
|
|
need(set(outer) == FILES, "source evidence file inventory")
|
|
for name, expected in outer.items():
|
|
need(pin((HERE / name).read_bytes()) == expected, "source evidence pin: " + name)
|
|
for run, commit in COMMITS.items():
|
|
receipt = parsed((HERE / ("committed-" + run + ".json")).read_bytes())
|
|
need(receipt["gate"] == run and receipt["commit"] == commit
|
|
and receipt["comparison"] == COMPARISON, run + ": historical identity relationship")
|
|
map_bytes, result = captured[run]
|
|
inputs = parsed(map_bytes)
|
|
need(pin(map_bytes) == receipt["inputMap"] == result["inputs"], run + ": exact captured input map")
|
|
need(result["exitCode"] == 0 and result["inputsUnchanged"] is True, run + ": accepted unchanged gate")
|
|
need(len(inputs) == receipt["inputCount"] == result["inputCount"], run + ": input counts")
|
|
need(all(safe(path) and relevant(path) for path in inputs), run + ": input scope")
|
|
need(sum(path.startswith("crates/vendor/iced_widget/") for path in inputs) == 71, run + ": vendor count")
|
|
compare_commit(repo, commit, inputs)
|
|
print("PASS exact Git tree/blobs:", run, commit, len(inputs), "captured inputs")
|
|
|
|
|
|
def main():
|
|
parser = argparse.ArgumentParser(description=__doc__)
|
|
parser.add_argument("--repo", type=Path, required=True, help="repository with both named commits already present")
|
|
source = parser.add_mutually_exclusive_group(required=True)
|
|
source.add_argument("--checks", type=Path, help="portable checks package directory")
|
|
source.add_argument("--raw-gates", type=Path, help="optional preparation mode: raw gate directory, not a packaged-evidence verification")
|
|
args = parser.parse_args()
|
|
if args.checks:
|
|
captured = packaged(args.checks, COMMITS)
|
|
else:
|
|
captured = {run: ((args.raw_gates / run / "inputs.json").read_bytes(),
|
|
parsed((args.raw_gates / run / "result.json").read_bytes())) for run in COMMITS}
|
|
verify(args.repo, captured)
|
|
print("Source identity only; no build, checkout, helper execution or native acceptance claim.")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|