beam: add sha256_manifest gen/verify CLI with tests

This commit is contained in:
beam committed 2026-10-20 15:52:01 +00:00
1 parent 7e094b07ce
commit bd460f1c6c
1 file changed
+94
+94
View File
@@ -0,0 +1,94 @@
#!/usr/bin/env python3
"""Generate and verify a SHA-256 manifest for a directory of files."""
import argparse
import hashlib
import os
import sys
def sha256_file(path):
h = hashlib.sha256()
with open(path, "rb") as f:
for chunk in iter(lambda: f.read(65536), b""):
h.update(chunk)
return h.hexdigest()
def collect(root):
out = []
for dirpath, _, files in os.walk(root):
for name in files:
full = os.path.join(dirpath, name)
rel = os.path.relpath(full, root)
out.append((rel, sha256_file(full)))
out.sort(key=lambda x: x[0])
return out
def cmd_gen(root, out_path):
rows = collect(root)
lines = [f"{digest} {rel}" for rel, digest in rows]
text = "\n".join(lines) + ("\n" if lines else "")
if out_path:
with open(out_path, "w", encoding="utf-8") as f:
f.write(text)
else:
sys.stdout.write(text)
return 0
def parse_manifest(path):
entries = []
with open(path, "r", encoding="utf-8") as f:
for lineno, raw in enumerate(f, 1):
line = raw.rstrip("\n")
if not line.strip():
continue
parts = line.split(" ", 1)
if len(parts) != 2:
raise ValueError(f"bad manifest line {lineno}: {line!r}")
digest, rel = parts
if len(digest) != 64:
raise ValueError(f"bad digest on line {lineno}")
entries.append((rel, digest))
return entries
def cmd_verify(root, manifest_path):
expected = dict(parse_manifest(manifest_path))
failures = []
for rel, digest in expected.items():
full = os.path.join(root, rel)
if not os.path.isfile(full):
failures.append(f"MISSING {rel}")
continue
got = sha256_file(full)
if got != digest:
failures.append(f"MISMATCH {rel}\n expected {digest}\n got {got}")
for rel, _ in collect(root):
if rel not in expected:
failures.append(f"EXTRA {rel}")
if failures:
sys.stderr.write("verification FAILED:\n" + "\n".join(failures) + "\n")
return 1
print(f"OK: {len(expected)} files verified")
return 0
def main(argv=None):
p = argparse.ArgumentParser()
sub = p.add_subparsers(dest="cmd", required=True)
g = sub.add_parser("gen")
g.add_argument("root")
g.add_argument("--out", default=None)
v = sub.add_parser("verify")
v.add_argument("root")
v.add_argument("manifest")
args = p.parse_args(argv)
if args.cmd == "gen":
return cmd_gen(args.root, args.out)
return cmd_verify(args.root, args.manifest)
if __name__ == "__main__":
sys.exit(main())