Files
troupe-standup/wizard-work/beam/sha256_manifest.py
T

111 lines
3.4 KiB
Python

#!/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, skip=None):
out = []
skip = set(skip or ())
for dirpath, _, files in os.walk(root):
for name in files:
full = os.path.join(dirpath, name)
rel = os.path.relpath(full, root)
if rel in skip:
continue
# Never follow the manifest file itself when it lives inside root.
out.append((rel, sha256_file(full)))
out.sort(key=lambda x: x[0])
return out
def cmd_gen(root, out_path):
skip = set()
if out_path:
try:
skip.add(os.path.relpath(os.path.abspath(out_path), os.path.abspath(root)))
except ValueError:
pass
rows = collect(root, skip=skip)
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))
# The manifest itself, when stored inside root, is not an "extra" file.
try:
manifest_rel = os.path.relpath(os.path.abspath(manifest_path), os.path.abspath(root))
except ValueError:
manifest_rel = None
skip = {manifest_rel} if manifest_rel and not manifest_rel.startswith("..") else set()
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, skip=skip):
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())