mirror of
https://github.com/Druthulu/BFM-decomp
synced 2026-09-26 13:33:34 -04:00
b6ea3a2747
- extract.py: one-command full-disc extraction (27 files: 24 Track-1 verbatim
+ 3 .DA as raw CD-DA audio from tracks 2-4) over the frozen iso9660.py
- cd_archive.py + pac.py: .CD TOC walker + PAC splitter, {index}.{type} naming
(no collisions; 447 sub-files, 1189 entries), SQV detection (98)
- lzss.py: game-semantics decoder (pos==0 terminator, -1 bias) + 5 unit tests;
138 type-4 decoded clean (0 warnings)
- manifest.py: deterministic JSON-Lines manifest (1801 artifacts) + --verify
- crosscheck.py + tools/brave-CUE/posix_shim.h: build CUE's brave from source
(GPL untouched), byte-compare; 1484 raw identical, 138/138 type-4 F6
- docs/formats.md: F5 counts (SC02=43), F2 refuted, F6 resolved, F1 type-7=139
- docs/effort-map.md + CLAUDE.md: Effort-map check rule (R7), Ultracode fix
- .gitignore: /extracted/* + !manifest/EXE exceptions; brave _build/
- install gcc 13.3.0 for the oracle
- milestone: one command extracts the full disc; MAIN.CD=49 sub-files; decoder
agrees byte-for-byte with CUE up to the game pos==0 terminator; zero ROM
staged; project version 1.1.0 -> 1.2.0
101 lines
3.2 KiB
Python
101 lines
3.2 KiB
Python
"""Brave Fencer Musashi ``.CD`` container reader (TOC walker).
|
|
|
|
A ``.CD`` archive (MAIN.CD, SC01-SC07.CD) begins with a TOC in sector 0::
|
|
|
|
+0x00 u32 LE count number of sub-files (MAIN.CD = 49)
|
|
+0x04 u32 LE zero
|
|
+0x08 count * { u32 LE rel_sector, u32 LE length }
|
|
|
|
``rel_sector`` is relative to the ``.CD`` start (x 2048). Sub-files begin at
|
|
sector 1, are sector-aligned (0x800), and are zero-padded. All 8 archives share
|
|
this format.
|
|
|
|
LIST.CD is NOT an archive (it is the concatenated trimmed TOCs of the 8
|
|
archives, formats.md §2.3) and must never be parsed here -- the caller excludes
|
|
it by name.
|
|
|
|
See docs/formats.md §2. Pure stdlib; operates on in-memory buffers, so it is
|
|
unit-testable without a disc.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import struct
|
|
from dataclasses import dataclass
|
|
from typing import Iterator
|
|
|
|
SECTOR = 0x800
|
|
PAC_MAGIC = b"PAC\x00"
|
|
SQV_MAGIC = b".sqv"
|
|
|
|
|
|
class CdArchiveError(Exception):
|
|
"""Raised on a malformed or out-of-bounds ``.CD`` TOC."""
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class CdEntry:
|
|
index: int # 0-based TOC ordinal (-> FILE_%03d)
|
|
rel_sector: int # u32 LE start sector, relative to the .CD file
|
|
byte_offset: int # rel_sector * SECTOR
|
|
length: int # u32 LE byte length (unpadded)
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class CdToc:
|
|
count: int
|
|
entries: tuple[CdEntry, ...]
|
|
|
|
|
|
def toc_byte_span(count: int) -> int:
|
|
"""Bytes the TOC header occupies: ``8 + 8*count`` (for the LIST.CD F4 check)."""
|
|
return 8 + 8 * count
|
|
|
|
|
|
def parse_toc(buffer: bytes) -> CdToc:
|
|
"""Parse the ``.CD`` TOC from the start of ``buffer`` (the whole ``.CD`` or
|
|
at least its header). Validates the count and that the header fits.
|
|
"""
|
|
if len(buffer) < 8:
|
|
raise CdArchiveError(f".CD too small for a TOC header ({len(buffer)} bytes)")
|
|
count = struct.unpack_from("<I", buffer, 0)[0]
|
|
if count == 0 or count > 0xFFFF:
|
|
raise CdArchiveError(f"implausible .CD sub-file count {count}")
|
|
need = toc_byte_span(count)
|
|
if need > len(buffer):
|
|
raise CdArchiveError(f"TOC needs {need} bytes but buffer is {len(buffer)}")
|
|
entries = []
|
|
for i in range(count):
|
|
rel_sector, length = struct.unpack_from("<II", buffer, 8 + 8 * i)
|
|
entries.append(
|
|
CdEntry(
|
|
index=i,
|
|
rel_sector=rel_sector,
|
|
byte_offset=rel_sector * SECTOR,
|
|
length=length,
|
|
)
|
|
)
|
|
return CdToc(count=count, entries=tuple(entries))
|
|
|
|
|
|
def iter_subfiles(cd_buffer: bytes, toc: CdToc) -> Iterator[tuple[CdEntry, bytes]]:
|
|
"""Yield ``(entry, raw sub-file bytes)`` for each TOC entry, in order."""
|
|
for e in toc.entries:
|
|
end = e.byte_offset + e.length
|
|
if e.byte_offset > len(cd_buffer) or end > len(cd_buffer):
|
|
raise CdArchiveError(
|
|
f"sub-file {e.index} [{e.byte_offset}:{end}] out of bounds "
|
|
f"(.CD is {len(cd_buffer)} bytes)"
|
|
)
|
|
yield e, cd_buffer[e.byte_offset:end]
|
|
|
|
|
|
def classify_subfile(buffer: bytes) -> str:
|
|
"""Return ``'pac'`` | ``'sqv'`` | ``'raw'`` from the leading magic (no parsing)."""
|
|
head = buffer[:4]
|
|
if head == PAC_MAGIC:
|
|
return "pac"
|
|
if head == SQV_MAGIC:
|
|
return "sqv"
|
|
return "raw"
|