"""Extract a main executable from a Brave Fencer Musashi PROTOTYPE disc.
Companion to ``extract_exe.py``, which is deliberately retail-locked: it hard-
validates ``SLUS_007.26``'s exact size, SHA1, and PS-EXE header field *values*.
The prototypes differ from retail in all three, and one of them hides its real
executable in a subdirectory, so a separate tool is cleaner than loosening the
retail validator:
* Sep-8 1998 "Musashi Master" -> root ``SLUS_007.26`` (~413,696 B, near-retail)
* Aug-31 1998 "Musashi Demo" -> ``MUSASHI/USA_DEMO.EXE`` (415,744 B); the root
``LAUNCH.EXE`` is only a bootloader.
This tool reuses the general ``iso9660`` reader (which knows nothing about any
specific file), supports an optional ``--dir`` subdirectory path, and validates
only the ``PS-X EXE`` magic -- the header fields and SHA1 are printed for the
record (each header field is compared against retail's value purely as
information, so the diff is visible at a glance).
ROM-derived output under ``extracted/`` is gitignored; never commit the proto
binaries (the discs in ``disks/`` are gitignored and reproduce them).
Examples (run from the repo root)::
# Sep-8 Master (root SLUS_007.26)
python3 tools/bfm_extract/extract_proto_exe.py \\
--bin "disks/Brave Fencer Musashi (Sep 8, 1998 prototype)/PSX - Musashi Master.img" \\
--exe-name SLUS_007.26 --out extracted/proto/sep8_SLUS_007.26
# Aug-31 Demo (MUSASHI/USA_DEMO.EXE)
python3 tools/bfm_extract/extract_proto_exe.py \\
--bin "disks/Brave Fencer Musashi (Aug 31, 1998 prototype)/PSX - Musashi Demo.img" \\
--dir MUSASHI --exe-name USA_DEMO.EXE --out extracted/proto/aug31_USA_DEMO.EXE
# List a directory (root, or a subdir via --dir) and exit
python3 tools/bfm_extract/extract_proto_exe.py --bin
--list
python3 tools/bfm_extract/extract_proto_exe.py --bin
--dir MUSASHI --list
"""
from __future__ import annotations
import argparse
import hashlib
import sys
from pathlib import Path
# Allow running as a plain script as well as a module (mirrors extract_exe.py).
try:
from .iso9660 import Iso9660Image, Iso9660Error
from .extract_exe import PS_EXE_MAGIC, EXPECTED_HEADER_FIELDS, _u32
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
from extract_exe import PS_EXE_MAGIC, EXPECTED_HEADER_FIELDS, _u32 # type: ignore
def resolve_dir(img: Iso9660Image, dirname: str | None) -> "FileRecord | None": # noqa: F821
"""Return the FileRecord for a named subdirectory, or None for the root."""
if not dirname:
return None
rec = img.find(dirname)
if rec is None or not rec.is_dir:
raise Iso9660Error(f"subdirectory not found: {dirname!r}")
return rec
def cmd_list(img: Iso9660Image, directory) -> int:
where = "" if directory is None else directory.name
files = img.list_files(directory)
print(f"Volume: {img.volume_id!r} dir={where} ({len(files)} 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 report_header(data: bytes) -> bool:
"""Print the PS-EXE magic + header fields. Returns True iff the magic is valid.
Header field values are shown against retail's for context ('same as retail'
vs 'differs') -- this is informational only; a proto is expected to differ.
"""
print("\nPS-EXE header (field values compared to retail SLUS_007.26 for context):")
magic = data[: len(PS_EXE_MAGIC)]
magic_ok = magic == PS_EXE_MAGIC
print(f" [{'PASS' if magic_ok else 'FAIL'}] magic 'PS-X EXE' found {magic!r}")
for label, off, retail in EXPECTED_HEADER_FIELDS:
actual = _u32(data, off) if len(data) >= off + 4 else None
if actual is None:
print(f" [ -- ] {label:<8} @0x{off:02X} = ")
continue
tag = "same as retail" if actual == retail else f"differs (retail 0x{retail:08X})"
print(f" [info] {label:<8} @0x{off:02X} = 0x{actual:08X} ({tag})")
return magic_ok
def cmd_extract(img: Iso9660Image, directory, exe_name: str, out_path: Path) -> int:
rec = img.find(exe_name, directory)
where = "" if directory is None else directory.name
if rec is None:
print(f"FAIL: {exe_name!r} not found in {where}", file=sys.stderr)
return 1
if rec.is_dir:
print(f"FAIL: {exe_name!r} is a directory, not a file", file=sys.stderr)
return 1
print(f"Located {rec.raw_name} in {where} 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}")
magic_ok = report_header(data)
sha1 = hashlib.sha1(data).hexdigest()
print(f"\nsize: {len(data)} bytes")
print(f"SHA1: {sha1}")
if magic_ok:
print("\nRESULT: PASS - PS-X EXE extracted (magic verified).")
return 0
print("\nRESULT: FAIL - extracted file is not a PS-X EXE.", file=sys.stderr)
return 1
def parse_args(argv: list[str] | None = None) -> argparse.Namespace:
p = argparse.ArgumentParser(
description="Extract a main EXE from a BFM prototype disc (magic-only validation).",
)
p.add_argument("--bin", type=Path, required=True, help="Path to the proto MODE2/2352 data track (.img/.bin).")
p.add_argument("--dir", dest="subdir", default=None, help="Subdirectory holding the EXE (e.g. MUSASHI). Default: root.")
p.add_argument("--exe-name", default=None, help="EXE file name to extract (e.g. SLUS_007.26, USA_DEMO.EXE).")
p.add_argument("--out", type=Path, default=None, help="Output path for the extracted executable.")
p.add_argument("--list", action="store_true", help="List the directory (root, or --dir) and exit (no extraction).")
return p.parse_args(argv)
def main(argv: list[str] | None = None) -> int:
args = parse_args(argv)
try:
with Iso9660Image(args.bin) as img:
directory = resolve_dir(img, args.subdir)
if args.list:
return cmd_list(img, directory)
if not args.exe_name or args.out is None:
print("ERROR: --exe-name and --out are required (or use --list).", file=sys.stderr)
return 2
return cmd_extract(img, directory, args.exe_name, args.out)
except Iso9660Error as e:
print(f"ERROR: {e}", file=sys.stderr)
return 2
if __name__ == "__main__":
raise SystemExit(main())