mirror of
https://github.com/Druthulu/BFM-decomp
synced 2026-10-01 15:43:06 -04:00
936d7d741c
- ImportAnnotations.java: the S86 OSGi-bundle blocker was 3 javac errors (Long->int unboxing x2, a nonexistent LocalVariableImpl ctor -> VariableStorage); "/undefined" resolves to DataType.DEFAULT (it lives in neither type manager — main's first proof passed the cmp with failed=13 because the plate-comment rows had set the same function comments); ghidra_rebuild.sh now dies unless the import printed failed=0 (R49), writes .proof markers - ghidra_annotations_delta.py: analysis drift measured and encoded as three counted classes — Error/Analysis bookmarks; auto-named DEFAULT functions the rebuild did not create (29 in main's LINKED regions); auto-named rows lagging the curated symbol file (10 sep8 + 5 aug31, R15). Result: main 38 hand-authored rows (13 annotated fns incl. 3 the ELF does not define, 22 comments, 3 labels); resident/overlays/protos container rows only; the DB holds no hand-authored types - controls (R39): mutated block row -> PROOF FAIL; synthetic comment/bookmark/label/signature round-trip -> PROOF PASS twice (idempotent); the filter keeps the synthetic rows and a hand-renamed name-only diff; fake failed=2 refused, resident re-proven; roster --check controls both ways - proofs, all PASS failed=0: resident 65s, ov_SC01_077 169s, ov_SC06_018 173s, SLUS_007.26 210s, sep8 202s, aug31 206s - tools/ghidra_roster.py -> config/ghidra/ROSTER.md (--check in tools-health, ignores the per-machine proof column) - .claude/settings.json hooks $CLAUDE_PROJECT_DIR-relative; ghidra_mcp_start.sh is a silent exit 0 without Ghidra or the project (both controlled); SETUP P33 B5 section + 5 inventory rows + §2.8 (R21); CURRENT_PHASE log + checkpoint
124 lines
5.6 KiB
Python
124 lines
5.6 KiB
Python
#!/usr/bin/env python3
|
||
"""ghidra_roster.py — config/ghidra/ROSTER.md from the committed Ghidra annotation files (P33 B5).
|
||
|
||
tools/ghidra_roster.py # writes config/ghidra/ROSTER.md
|
||
tools/ghidra_roster.py --check # exit 1 if ROSTER.md is stale (for tools-health)
|
||
|
||
One row per config/ghidra/<program>.jsonl: the program's kind (PS-X EXE via the PSX loader, or a raw
|
||
blob at its vram), its payload and vram as the build registers them (`make -s print-EXE / print-VRAM_BASE`
|
||
— the same source tools/ghidra_rebuild.sh imports from, R33), the memory blocks the file pins, the
|
||
hand-authored row census (everything that is not a `program`/`block`/`archive` container row), and the
|
||
last `--proof` verdict recorded by tools/ghidra_rebuild.sh in .run/ghidra_rebuild/<program>.proof
|
||
("unproven here" when that scratch file is absent — a fresh clone has not run the proof yet).
|
||
The 123 other programs of the live project carry no hand-authored rows and are regenerated on demand
|
||
(tools/prefetch_fleet.py); they are not in the roster on purpose.
|
||
"""
|
||
import collections
|
||
import datetime as dt
|
||
import json
|
||
import pathlib
|
||
import subprocess
|
||
import sys
|
||
|
||
REPO = pathlib.Path(__file__).resolve().parent.parent
|
||
CONF = REPO / "config" / "ghidra"
|
||
SCR = REPO / ".run" / "ghidra_rebuild"
|
||
CONTAINER = {"program", "block", "archive"}
|
||
# The PS-X EXE programs (the PSX loader, no vram argument) — mirrors the case table in ghidra_rebuild.sh.
|
||
EXES = {
|
||
"SLUS_007.26": ("main", "extracted/retail/SLUS_007.26"),
|
||
"sep8_SLUS_007.26": ("proto-sep8", "extracted/proto/sep8_SLUS_007.26 (prototype disc)"),
|
||
"aug31_USA_DEMO.EXE": ("proto-demo", "extracted/proto/aug31_USA_DEMO.EXE (prototype disc)"),
|
||
}
|
||
|
||
|
||
def make_print(var, alias):
|
||
r = subprocess.run(["make", "-s", f"print-{var}", f"BINARY={alias}"], cwd=REPO, capture_output=True, text=True)
|
||
return r.stdout.strip() if r.returncode == 0 else "?"
|
||
|
||
|
||
def row_for(path):
|
||
prog = path.stem
|
||
kinds = collections.Counter()
|
||
blocks = []
|
||
image_base = fmt = "?"
|
||
for ln in path.read_text(encoding="utf-8").splitlines():
|
||
if not ln.strip():
|
||
continue
|
||
o = json.loads(ln)
|
||
kinds[o["k"]] += 1
|
||
if o["k"] == "program":
|
||
image_base, fmt = o.get("image_base", "?"), o.get("format", "?")
|
||
elif o["k"] == "block":
|
||
blocks.append(o)
|
||
if prog in EXES:
|
||
kind, payload = "PS-X EXE", EXES[prog][1]
|
||
vram = "header"
|
||
else:
|
||
kind, payload, vram = "raw blob", make_print("EXE", prog), make_print("VRAM_BASE", prog)
|
||
hand = {k: v for k, v in kinds.items() if k not in CONTAINER}
|
||
census = ", ".join(f"{k} {v}" for k, v in sorted(hand.items())) or "none (container rows only)"
|
||
proof = SCR / f"{prog}.proof"
|
||
verdict = proof.read_text(encoding="utf-8").strip() if proof.exists() else "unproven here"
|
||
# the loaded (initialized) blocks by name+range; the loader's hardware/uninitialized blocks only as a count
|
||
loaded = [f"{b['name']} {b['start']}–{b['end']}" for b in blocks if b.get("init")]
|
||
other = len(blocks) - len(loaded)
|
||
blk = "; ".join(loaded) + (f" (+{other} uninit/hardware)" if other else "")
|
||
return f"| `{prog}` | {kind} ({fmt}) | `{payload}` | {vram} | {blk} | {sum(hand.values())} — {census} | {verdict} |"
|
||
|
||
|
||
def render():
|
||
files = sorted(CONF.glob("*.jsonl"))
|
||
lines = [
|
||
"# config/ghidra/ROSTER.md — the Ghidra programs whose hand-authored RE work is tracked as text",
|
||
"",
|
||
"> Generated by `tools/ghidra_roster.py` — never edit by hand. One row per `config/ghidra/<program>.jsonl`",
|
||
"> (the delta format of `tools/ghidra_scripts/ExportAnnotations.java`). Rebuild any row's program from the",
|
||
"> disc + this file with `tools/ghidra_rebuild.sh <program>`; prove it with `--proof` (P33 B5). The binary",
|
||
"> Ghidra project embeds the game's bytes and left git at P33; these files are what R20 backs up instead.",
|
||
"",
|
||
"| program | kind | payload | vram | blocks | hand-authored rows | last proof (this machine) |",
|
||
"|---|---|---|---|---|---|---|",
|
||
]
|
||
lines += [row_for(p) for p in files]
|
||
lines += [
|
||
"",
|
||
f"{len(files)} programs tracked. The live project's other programs (the location overlays and `md_*` modules,",
|
||
"129 on disk at P33) hold no hand-authored rows and are regenerated on demand by `tools/prefetch_fleet.py`.",
|
||
"Proof verdicts come from `.run/ghidra_rebuild/<program>.proof` (scratch; absent on a fresh clone).",
|
||
"",
|
||
f"_Generated {dt.date.today().isoformat()}._",
|
||
"",
|
||
]
|
||
return "\n".join(lines)
|
||
|
||
|
||
def main(argv):
|
||
out = CONF / "ROSTER.md"
|
||
text = render()
|
||
if "--check" in argv:
|
||
cur = out.read_text(encoding="utf-8") if out.exists() else ""
|
||
# the proof column and the date are per-machine; --check compares everything else (a fresh clone
|
||
# has no .run/ghidra_rebuild/*.proof and must not read as stale)
|
||
def strip(t):
|
||
out = []
|
||
for l in t.splitlines():
|
||
if l.startswith("_Generated"):
|
||
continue
|
||
if l.startswith("| `"):
|
||
l = l.rsplit("|", 2)[0]
|
||
out.append(l)
|
||
return "\n".join(out)
|
||
if strip(cur) != strip(text):
|
||
print(f"ghidra_roster: {out} is STALE — run tools/ghidra_roster.py", file=sys.stderr)
|
||
return 1
|
||
print(f"ghidra_roster: OK ({len(list(CONF.glob('*.jsonl')))} programs)")
|
||
return 0
|
||
out.write_text(text, encoding="utf-8")
|
||
print(f"ghidra_roster: wrote {out} ({len(list(CONF.glob('*.jsonl')))} programs)")
|
||
return 0
|
||
|
||
|
||
if __name__ == "__main__":
|
||
sys.exit(main(sys.argv[1:]))
|