#!/usr/bin/env python3 """Provenance manifest of the 40250 client assets the tests and the runtime read (docs/PORT-PLAN.md 2D step 3). Records path (relative to the Client directory) + size + sha256 for pack/Index, every .eix/.epk in pack/ and the loose Eternexus/root/*.msm race specs. `verify` fails on a missing, changed or untracked file, so a strict run proves it read exactly the bytes the manifest names. tools/asset_manifest.py generate [-o audit/assets/40250-client.json] tools/asset_manifest.py verify [-m audit/assets/40250-client.json] """ import argparse import hashlib import json import os import sys DEFAULT = os.path.join(os.path.dirname(__file__), "..", "audit", "assets", "40250-client.json") def sha256(path): h = hashlib.sha256() with open(path, "rb") as f: for block in iter(lambda: f.read(1 << 20), b""): h.update(block) return h.hexdigest() def tracked(client): files = ["pack/Index"] pack = os.path.join(client, "pack") files += sorted("pack/" + n for n in os.listdir(pack) if n.lower().endswith((".eix", ".epk"))) root = os.path.join(client, "Eternexus", "root") if os.path.isdir(root): files += sorted("Eternexus/root/" + n for n in os.listdir(root) if n.lower().endswith(".msm")) return files def entry(client, rel): p = os.path.join(client, rel) return {"path": rel, "size": os.path.getsize(p), "sha256": sha256(p)} def generate(args): files = [entry(args.client, rel) for rel in tracked(args.client)] out = { "schema": 1, "source": "40250 Server Client TMP4/Client", "note": "path relative to the Client directory; regenerate with tools/asset_manifest.py generate", "files": files, } os.makedirs(os.path.dirname(os.path.abspath(args.output)), exist_ok=True) with open(args.output, "w") as f: json.dump(out, f, indent=1) f.write("\n") print("%d files, %d bytes -> %s" % (len(files), sum(e["size"] for e in files), args.output)) return 0 def verify(args): try: with open(args.manifest) as f: want = json.load(f)["files"] except (OSError, ValueError, KeyError) as e: print("asset_manifest: cannot read manifest %s: %s" % (args.manifest, e), file=sys.stderr) return 1 if not os.path.isfile(os.path.join(args.client, "pack", "Index")): print("asset_manifest: no pack/Index under %s" % args.client, file=sys.stderr) return 1 bad = 0 for e in want: p = os.path.join(args.client, e["path"]) if not os.path.isfile(p): print("missing %s" % e["path"], file=sys.stderr) bad += 1 elif os.path.getsize(p) != e["size"] or sha256(p) != e["sha256"]: print("changed %s" % e["path"], file=sys.stderr) bad += 1 extra = sorted(set(tracked(args.client)) - {e["path"] for e in want}) for rel in extra: print("untracked %s" % rel, file=sys.stderr) bad += len(extra) print("%d files checked, %d mismatch(es)" % (len(want), bad)) return 1 if bad else 0 def main(): ap = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter) sub = ap.add_subparsers(dest="cmd", required=True) g = sub.add_parser("generate") g.add_argument("client") g.add_argument("-o", "--output", default=DEFAULT) v = sub.add_parser("verify") v.add_argument("client") v.add_argument("-m", "--manifest", default=DEFAULT) args = ap.parse_args() return generate(args) if args.cmd == "generate" else verify(args) if __name__ == "__main__": sys.exit(main())