Add the disc pipeline: identify, unpack and verify

Phase 1 of the roadmap. The disc is now turned into verified asset files, and
the result is checked against a committed hash table rather than against
whatever happens to be on disk.

  gc-extract identify disc.iso        SCUS_972.68 v1.01 NTSC, ELF SHA-1 matches
  gc-extract unpack   disc.iso out/   5135 files, 4,500,106,960 bytes, 10.8s
  gc-extract verify   out/ table.tsv  OK: 5135 files match

All 92 G/*.WAD containers parse. MPEG1 and MPEG2 are raw video and pass
through whole; the other 90 are indexed containers whose extents are extracted
individually.

Formats recovered and written up in docs/formats/:

- iso9660.rs. R&C2 lists its assets in the filesystem, unlike R&C1, so a
  directory walk is the primary asset index. Three details that all fail
  quietly rather than loudly: directory records store every 32-bit field twice
  (little-endian then big-endian), the CD001 signature is at sector offset 1
  rather than 0x8001, and the directory flag at record offset 25 is
  authoritative where the identifier's trailing slash is not.

- global_wad.rs. Container headers hold a table of extents. All 92 indexed
  containers use {offset in sectors, size in bytes}. Measured, not assumed: the
  sector-count reading places every entry of all 27 LEVEL<n> and all 27
  AUDIO<n> containers out of range, while the byte reading places all of them
  correctly. SCENE<n> is the one exception, a flat sector list whose extents
  are sized by the next value.

- wad.rs. Insomniac's LZ77 stream: four flag-selected packet classes, with 0-3
  trailing literal bytes per packet carried in unused low bits. This is not a
  bit stream, which is why it resists the usual LZ77 reading.

- buf.rs. A bounds-checked reader, so untrusted 2003 disc data produces typed
  errors rather than panics.

gc-extract carries the three stages and a SHA-1 implementation with FIPS 180-4
test vectors, so identification works without pulling in a crypto dependency.

Verification: 54 unit tests, plus four integration tests that run only when
GC_DISC names a disc. Those four check identification against the recorded
values, cross-check the Rust identifier against tools/identify-disc.py, parse
all 92 containers, and confirm a full unpack matches the committed table. The
committed table is 316 KB of sizes and SHA-1s for all 5,135 files: hashes only,
no game content, so it is safe to version and it turns "matches retail" into
something a test can assert.

docs/plan/disc-pipeline.md records two wrong turns, because both are easy to
repeat and both failed by producing plausible wrong output rather than an
error: the container table was read backwards, and the LZ77 stream was modelled
as a bit stream. The first survived because it happened to be correct on the
one container used for manual checking.
This commit is contained in:
2026-10-09 16:08:30 +02:00
parent a02f79a6d1
commit 027373e65b
21 changed files with 8372 additions and 97 deletions
+18 -19
View File
@@ -6,24 +6,23 @@
//!
//! # What belongs here
//!
//! * `hero` - Ratchet's controller. Movement on a **spherical** planet,
//! which is what distinguishes this game: reaching for the
//! camera and the collision code changes gravity handling
//! across the whole world, and is the main reason the first
//! game's movement code does not transfer.
//! * `chargeboots`- hold and release timing, chained jumps and the dash.
//! * `weapons` - the wrench and the weapon set, plus the weapon modification
//! system (per-weapon bitmasks: lock-on, shock, acid, ...).
//! * `gadgets` - the Pyrocitor, Bomb Glove, Glove of Doom and friends.
//! * `moby_update`- the per-class update dispatch, driven by each level's
//! `lvl.vtbl` class table. Classes that are identical across
//! levels are shared; the rest are resolved through the
//! overlay relocator in `gc-formats`.
//! * `collision` - queries against the collision world.
//! * `progression`- bolts, the Gadgetron vendor, RPG upgrades and unlocks.
//! * `save_game` - the IFF chunk save format, and importing a Ratchet & Clank
//! (2002) save to unlock the legacy weapons for free.
//! * `memcard` - the 25-state memory-card finite state machine and its UI.
//! * `hero` - Ratchet's controller. Movement on a **spherical** planet, which is
//! what distinguishes this game: the camera and collision code change gravity
//! handling across the whole world, and is the main reason the first game's
//! movement code does not transfer.
//! * `chargeboots` - hold and release timing, chained jumps and the dash.
//! * `weapons` - the wrench and the weapon set, plus the weapon modification
//! system (per-weapon bitmasks: lock-on, shock, acid, ...).
//! * `gadgets` - the Pyrocitor, Bomb Glove, Glove of Doom and friends.
//! * `moby_update` - the per-class update dispatch, driven by each level's
//! `lvl.vtbl` class table. Classes that are identical across levels are
//! shared; the rest are resolved through the overlay relocator in
//! `gc-formats`.
//! * `collision` - queries against the collision world.
//! * `progression` - bolts, the Gadgetron vendor, RPG upgrades and unlocks.
//! * `save_game` - the IFF chunk save format, and importing a Ratchet & Clank
//! (2002) save to unlock the legacy weapons for free.
//! * `memcard` - the 25-state memory-card finite state machine and its UI.
//!
//! # The float model
//!
@@ -35,4 +34,4 @@
//! # Timing
//!
//! The game is 60 Hz (this disc is NTSC). Constants are expressed per tick at
//! that rate. PAL discs run 50 Hz and would need different timing throughout.
//! that rate. PAL discs run 50 Hz and would need different timing throughout.