#!/usr/bin/env python3 """Snapshot names that exist only inside the IDA databases into fw/cgnames/.json. Three databases have come back corrupted during this work, and rebuilding one discards every name that was derived by fw/ta_cgname.py rather than by porting -- GCP silently fell from 83% to 74% that way. fw/ta_cgname.py now persists as it goes, but the names it applied BEFORE that change exist only in the .i64 files, and they cannot be re-derived cheaply: once a callee is named it stops being a sub_* candidate, so re-running the tool finds nothing to record. This recovers them from the artifacts instead. fw/decomp/*.index.json records name, address and tier for every function of every build, so the names whose tier is not one of the porting tiers are exactly the ones that live only in the database. Which tiers are snapshotted, and why: IDA taken from the decompiler's own signature or applied by ta_cgname -- NOT reproducible from the reference alone, so these are the ones that matter. THUNK Hex-Rays thunk resolution. Reproducible on a rebuild, but harmless to record and it keeps the map self-sufficient if thunk resolution ever differs. DISPATCH command-handler names recovered from the dispatcher switch. DISPATCH was excluded at first on the grounds that fw/ta_re.py re-derives it every run. That reasoning was wrong: it is re-derived, but NOT deterministically. A full clean regeneration lost xgmi_cmd00, xgmi_cmd04, xgmi_cmd0B and fp_cmd04 -- every one of them tier DISPATCH -- because dispatcher recovery depends on which function Hex-Rays decompiles cleanly on a given analysis, and IDA's auto-analysis is not identical across fresh databases. Persisting them is still safe against masking a regression: the dispatcher step runs regardless, so where recovery works the DISPATCH tier wins and where it fails the persisted name is restored as tier IDA. The tier difference is then itself the signal that recovery failed on that build. EXACT, MNEM and FUZZY remain excluded: those depend only on the reference bytes and are stable. usage: ./ta_snapshot_names.py [--tiers IDA] [--dry-run] """ from __future__ import annotations import argparse import glob import json import os from pathlib import Path HERE = Path(__file__).resolve().parent def main() -> int: ap = argparse.ArgumentParser() ap.add_argument("--tiers", default="IDA,DISPATCH", help="comma-separated tiers to snapshot (default: IDA)") ap.add_argument("--dry-run", action="store_true") a = ap.parse_args() tiers = {t.strip() for t in a.tiers.split(",") if t.strip()} outdir = HERE / "cgnames" outdir.mkdir(exist_ok=True) per_ta: dict[str, dict] = {} for ip in sorted(glob.glob(str(HERE / "decomp" / "*.index.json"))): base = os.path.basename(ip).replace(".index.json", "") ta = base.split("_")[0] d = json.loads(Path(ip).read_text()) for f in d["functions"]: if f.get("name") and f.get("tier") in tiers: per_ta.setdefault(ta, {}).setdefault(base, {})[f["addr"]] = f["name"] total = 0 for ta, builds in sorted(per_ta.items()): mapfile = outdir / f"{ta}.json" existing = json.loads(mapfile.read_text()) if mapfile.exists() else {} added = 0 for b, names in builds.items(): tgt = existing.setdefault(b, {}) for ad, nm in names.items(): if tgt.get(ad) != nm: tgt[ad] = nm added += 1 n = sum(len(v) for v in existing.values()) total += added print(" %-10s +%-5d new -> %d names across %d builds%s" % (ta, added, n, len(existing), " (dry-run)" if a.dry_run else "")) if not a.dry_run: mapfile.write_text(json.dumps(existing, indent=1, sort_keys=True)) print("snapshotted %d names in tiers %s" % (total, sorted(tiers))) return 0 if __name__ == "__main__": raise SystemExit(main())