Files
BFM-decomp/tools/bfm_extract/extract_exe.py
T
Drew T 3207d52491 feat(phase-3.5): prototype research spike — go/no-go (protos don't accelerate Gen1 matching)
- extract_proto_exe.py: subdir-aware proto main-EXE extraction (reuses iso9660); extracted
  sep8 SLUS_007.26 (413,696 B) + aug31 MUSASHI/USA_DEMO.EXE (415,744 B, base 0x80018000)
- ghidra_import.sh: reusable headless import; both protos imported into the bfm project
  (sep8 1726 funcs, aug31 1737 funcs, both PsyQ 4.0.0), R9-verified
- DumpFunctionSignatures.java + match_protos.py: 3-tier (exact/norm/seq) cross-binary
  correspondence; MIPS normalizer validated by anchors
- FINDINGS: (T2) no debug symbols in either proto — Hidden Palace "no symbols" VERIFIED;
  recon "Sep-8 less-stripped" REFUTED by per-file byte compare. (T3) Sep-8 99.6% byte-identical
  to retail (only 3 funcs differ: DebugMenuHandler, CdReadSectorReadyCB, SaveLoadRoutine);
  Aug-31 66% norm-identical, 862 1:1 correspondences. (T4) demo shares retail's 18-entry
  game-mode dispatch architecture but the handler code diverged — Q#10 resolved
- VERDICT (docs/proto-correspondence.md): NO-GO as a Gen1 label/symbol accelerator; GO to retain
  Sep-8 (Phase-6 compiler corroborant) + both (Gen2 assets); proto-side labels in
  config/symbols.proto-{sep8,demo}.txt (R13-tagged, never feed symbols.us.txt)
- reorg extracted/ into per-ROM subfolders (extracted/retail/, extracted/proto/); nested
  gitignore allowlist; extractor defaults updated; manifest --verify PASS
- memory-map.md: Phase-3.5 block + demo dispatch anchors; Q#10 RESOLVED; §5 proto notes VERIFIED
- ghidra_mcp_start.sh: PROG arg to serve a prototype
- rules R13 (proto-provenance/confidence tagging), R14 (verify recon counts vs bytes)
- bumps project version 1.3.0 -> 1.3.1
2026-06-14 03:19:23 -06:00

260 lines
8.1 KiB
Python

"""Phase 1 CLI: extract and validate the BFM main executable (SLUS_007.26).
Walks the ISO9660 file system on the MODE2/2352 data track (it does NOT use a
hardcoded byte offset), extracts SLUS_007.26 verbatim -- including its 2048-byte
PS-EXE header, which ghidra_psx_ldr needs -- writes it to a gitignored output
path, then validates the PS-EXE header fields and the exact file size and prints
a PASS/FAIL report plus the SHA1 of the extracted file.
Run from the repo root (use ``python3`` on Linux/WSL)::
python tools/bfm_extract/extract_exe.py
python tools/bfm_extract/extract_exe.py --list
python tools/bfm_extract/extract_exe.py --verify-disc
python tools/bfm_extract/extract_exe.py --bin path/to/track.bin --out path/out
Output under ``extracted/`` is ROM-derived and is gitignored on purpose; it must
never be committed.
"""
from __future__ import annotations
import argparse
import hashlib
import struct
import sys
import zlib
from pathlib import Path
# Allow running as a plain script (``python tools/bfm_extract/extract_exe.py``)
# as well as a module; fall back to a path-relative import in the script case.
try:
from .iso9660 import Iso9660Image, Iso9660Error
except ImportError: # pragma: no cover - exercised only as a loose script
sys.path.insert(0, str(Path(__file__).resolve().parent))
from iso9660 import Iso9660Image, Iso9660Error # type: ignore
# --- Known-good facts for the BFM USA dump ----------------------------------
DEFAULT_BIN = (
Path("Brave Fencer Musashi (USA)")
/ "Brave Fencer Musashi (USA) (Track 1).bin"
)
DEFAULT_OUT = Path("extracted") / "retail" / "SLUS_007.26"
EXE_NAME = "SLUS_007.26"
EXPECTED_SIZE = 0x65000 # 413,696 bytes, header included
# SHA1 of the extracted SLUS_007.26 (stable, independently reproduced from raw
# sectors in Phase 1). Not publicly documented; used as the full-disc round-trip
# anchor by extract.py.
EXPECTED_EXE_SHA1 = "143dbb89f34491258bbc27810d0a12ec8b43a8dd"
PS_EXE_MAGIC = b"PS-X EXE"
# Redump-published Track-1 hashes for SLUS-00726 (USA). The canonical retail
# disc has build date 1998-08-25. Matching these proves the local dump is the
# canonical redump dump, which transitively validates the extracted EXE (the
# EXE's own SHA1 is not publicly documented).
REDUMP_TRACK1_SHA1 = "b44f0f0a19936f23b26188b658e13201a6a9c211"
REDUMP_TRACK1_CRC32 = "c238191b"
HASH_CHUNK = 1 << 20 # 1 MiB streaming reads; never load the ~348 MB BIN whole
# (label, header byte offset, expected little-endian u32 value)
EXPECTED_HEADER_FIELDS = [
("pc0", 0x10, 0x80010000),
("gp0", 0x14, 0x00000000),
("t_addr", 0x18, 0x80010000),
("t_size", 0x1C, 0x00064800),
("sp_base", 0x30, 0x801FFFF0),
]
def _u32(buf: bytes, off: int) -> int:
return struct.unpack_from("<I", buf, off)[0]
def validate_ps_exe(data: bytes) -> list[tuple[str, bool, str]]:
"""Check magic, the five header fields, and the total size.
Returns a list of (check name, passed, detail) tuples.
"""
checks: list[tuple[str, bool, str]] = []
magic = data[: len(PS_EXE_MAGIC)]
checks.append(
(
"magic 'PS-X EXE'",
magic == PS_EXE_MAGIC,
f"found {magic!r}",
)
)
for label, off, expected in EXPECTED_HEADER_FIELDS:
actual = _u32(data, off) if len(data) >= off + 4 else None
ok = actual == expected
detail = (
f"@0x{off:02X} = 0x{actual:08X} (expected 0x{expected:08X})"
if actual is not None
else f"@0x{off:02X} = <truncated>"
)
checks.append((label, ok, detail))
checks.append(
(
"size == 0x65000",
len(data) == EXPECTED_SIZE,
f"{len(data)} bytes (expected {EXPECTED_SIZE})",
)
)
return checks
def parse_args(argv: list[str] | None = None) -> argparse.Namespace:
p = argparse.ArgumentParser(
description="Extract and validate the BFM main executable (SLUS_007.26).",
)
p.add_argument(
"--bin",
type=Path,
default=DEFAULT_BIN,
help=f"Path to the MODE2/2352 data-track BIN (default: {DEFAULT_BIN}).",
)
p.add_argument(
"--out",
type=Path,
default=DEFAULT_OUT,
help=f"Output path for the extracted executable (default: {DEFAULT_OUT}).",
)
p.add_argument(
"--list",
action="store_true",
help="List the root directory and exit (no extraction).",
)
p.add_argument(
"--verify-disc",
action="store_true",
help=(
"Hash the entire Track-1 BIN and compare against redump's "
"published SHA1/CRC32, then exit (no extraction)."
),
)
return p.parse_args(argv)
def cmd_list(img: Iso9660Image) -> int:
files = img.list_files()
print(f"Volume: {img.volume_id!r} ({len(files)} root entries)")
print(f"{'LBA':>7} {'SIZE':>11} {'DIR':>3} NAME")
for rec in files:
print(
f"{rec.lba:>7} {rec.size:>11} "
f"{'yes' if rec.is_dir else ' -':>3} {rec.raw_name}"
)
return 0
def cmd_extract(img: Iso9660Image, out_path: Path) -> int:
rec = img.find(EXE_NAME)
if rec is None:
print(f"FAIL: {EXE_NAME} not found in the ISO root directory", file=sys.stderr)
return 1
print(f"Located {rec.raw_name} at LBA {rec.lba}, size {rec.size} bytes")
data = img.read_extent(rec.lba, rec.size)
out_path.parent.mkdir(parents=True, exist_ok=True)
out_path.write_bytes(data)
print(f"Wrote {len(data)} bytes -> {out_path}")
# --- validation report ---
print("\nPS-EXE validation:")
checks = validate_ps_exe(data)
all_ok = True
for name, ok, detail in checks:
status = "PASS" if ok else "FAIL"
all_ok = all_ok and ok
print(f" [{status}] {name:<16} {detail}")
sha1 = hashlib.sha1(data).hexdigest()
print(f"\nSHA1: {sha1}")
if all_ok:
print("\nRESULT: PASS - all PS-EXE header and size checks passed.")
return 0
print("\nRESULT: FAIL - one or more validation checks failed.", file=sys.stderr)
return 1
def cmd_verify_disc(bin_path: Path) -> int:
"""Hash the entire Track-1 BIN and compare against redump's published values.
Streams the ~348 MB file in fixed-size chunks (never loaded whole) and
computes both SHA1 and CRC32 in one pass. A PASS means the dump is the
canonical redump dump (build date 1998-08-25), which transitively validates
the extracted EXE.
"""
if not bin_path.is_file():
print(f"ERROR: BIN data track not found: {bin_path}", file=sys.stderr)
return 2
sha1 = hashlib.sha1()
crc32 = 0
total = 0
with open(bin_path, "rb") as fh:
while True:
chunk = fh.read(HASH_CHUNK)
if not chunk:
break
sha1.update(chunk)
crc32 = zlib.crc32(chunk, crc32)
total += len(chunk)
actual_sha1 = sha1.hexdigest()
actual_crc32 = f"{crc32 & 0xFFFFFFFF:08x}"
print(f"Track 1: {bin_path}")
print(f" size : {total} bytes")
sha1_ok = actual_sha1 == REDUMP_TRACK1_SHA1
crc32_ok = actual_crc32 == REDUMP_TRACK1_CRC32
print(
f" [{'PASS' if sha1_ok else 'FAIL'}] SHA1 {actual_sha1} "
f"(redump {REDUMP_TRACK1_SHA1})"
)
print(
f" [{'PASS' if crc32_ok else 'FAIL'}] CRC32 {actual_crc32} "
f"(redump {REDUMP_TRACK1_CRC32})"
)
if sha1_ok and crc32_ok:
print(
"\nRESULT: PASS - Track 1 matches the canonical redump dump "
"(SLUS-00726 USA, build 1998-08-25)."
)
return 0
print(
"\nRESULT: FAIL - Track 1 does NOT match the redump dump; "
"this is not the canonical disc image.",
file=sys.stderr,
)
return 1
def main(argv: list[str] | None = None) -> int:
args = parse_args(argv)
if args.verify_disc:
return cmd_verify_disc(args.bin)
try:
with Iso9660Image(args.bin) as img:
if args.list:
return cmd_list(img)
return cmd_extract(img, args.out)
except Iso9660Error as e:
print(f"ERROR: {e}", file=sys.stderr)
return 2
if __name__ == "__main__":
raise SystemExit(main())