Files
Going-Commando/crates/gc-formats/src/chunks.rs
T
ReaperOfVeriod 79914c420e Show the chunk record's leading byte is a per-chunk phase counter; every chunk
has a uniform record length

Follows the alignment result and settles what the leading byte is.

Within one chunk the leading byte steps by a constant and wraps:

  offsets: 1420 1748 2072 2392 2716 3044 3372 3700 4028 4352 ...
  byte0:      0  16   32   48   64   80   96  112  128  144 ...

Sweeping every (step, phase) over all 27 levels:

  fully explained: 1,262 of 1,509 chunks
  by step: 8 -> 51 chunks, 16 -> 554, 32 -> 520, 64 -> 137
  unexplained:  247 chunks across 11 chunk types

So on 1,262 chunks the leading byte carries no per-record information: it is a
deterministic function of the record's index and the chunk's phase, which is a
position marker rather than data. That is why the earlier "16 discrete values,
uniform counts" pattern appeared at all.

ResolvedChunk::bucket_rule returns the (step, phase) when the whole chunk fits and
None otherwise, so the 247 unexplained chunks stay distinguishable and nothing
skips a leading byte on the strength of a rule that does not apply to it.

Every chunk has a uniform record length: 1,509 of 1,509. Length varies BETWEEN
chunks of a type -- one type runs 76 to 104 bytes -- but never within one. This
withdraws the previous commit's "variable-length type", which came from pooling
minlen/maxlen across all of a type's records at once. That is the pooled-claim
error rule 3a exists to prevent, and it is the third time this session a pooled
measurement produced a claim that partitioning reversed. The type is not
variable-length; the chunk is fixed-length and different chunks differ.
2026-10-12 04:21:32 +02:00

407 lines
16 KiB
Rust

//! The post-shrub chunk container.
//!
//! Spec: `docs/formats/core-data-regions.md`.
//!
//! This is the container that fills the region after the last shrub class body
//! and before the constant trailer. It is *not* established to be moby_anim;
//! a 49 x 368-byte table inside it turned out to be byte-identical on all 27
//! levels, which is evidence against that identification rather than for it.
//!
//! The framing is established. Each chunk is:
//!
//! ```text
//! +0x00 16 bytes, last byte 0xff (opaque, differs per chunk)
//! +0x10 10 zero bytes
//! +0x1a N tagged offsets, top byte 0xf0
//! ```
//!
//! and each tagged offset is `0xF0000000 | low24`. Resolving a record at
//! `chunk_start + low` yields records that share a field layout: 17-21% of
//! byte positions are constant across a chunk's own records, on every level.
//! That is what distinguishes this from a coincidental byte pattern.
//!
//! The stride between records is a property of the chunk, not of the region.
//! Level 0 alone contains chunks at 148, 340, 368, 288, 320, 236, 272, 384,
//! 116, 160, 250, 580 and 576. Because the strides differ, a region-wide
//! autocorrelation averages them to the noise floor -- which is why an
//! earlier claim of a "368-byte period" for the region as a whole was wrong.
//! 368 is the stride of one chunk.
//!
//! Fields are *not* named. The 16-byte header is opaque, the `0xf0` tag is
//! unexplained, and it is not known which fields are an index, a length and
//! an offset. What this parser provides is bounds-checked chunk and record
//! extraction, which is what the format analysis actually established.
use crate::buf::{Buf, ErrorKind, Result};
/// Bytes in a chunk's opaque leading header.
pub const CHUNK_HEADER_BYTES: usize = 16;
/// Zero bytes between the header and the offset table.
pub const CHUNK_GAP_BYTES: usize = 10;
/// The value every tagged offset's top byte carries, on all 27 levels.
pub const CHUNK_TAG: u8 = 0xf0;
/// The two bytes immediately preceding every chunk region on the disc.
///
/// The last class body in a level -- shrub where the level has shrub, tie
/// where it does not -- ends with this constant pair before the chunk
/// container begins. It is *not* a unique pattern: the pair occurs 25 to 103
/// times in a 5 to 17 MB lump, so on its own it means nothing. What locates
/// the boundary is the pair sitting exactly in front of a chunk signature
/// (see [`chunk_region_start`]).
///
/// Not interpreted. Whether it is a terminator, an end count or a tag is not
/// established; what is established is that it is constant across all 27
/// levels and sits at this one place.
/// Alignment of the first byte of a chunk record.
///
/// The leading byte is not decoded -- what it indexes is not established, and
/// naming it would be a guess. What *is* established is that it is a small
/// bucket index, not data: within a chunk type it takes 16 discrete values
/// (0x00, 0x10, ... 0xf0) with near-uniform counts, so it reads as an index into
/// groups rather than a running offset.
///
/// **The alignment step is per type, not global.** Across all 27 levels, over
/// 1,509 chunks and 34,491 records:
///
/// - the leading byte is a multiple of **8** on 34,432 of 34,491;
/// - a multiple of **16** on only 33,349 -- the 1,142-record difference falls in
/// **seven** types, four of which step their buckets by 8 (0x00, 0x08, 0x10,
/// ...) instead of 16;
/// - and 59 records in three types break even the 8-byte rule, which is where the
/// variable-length type lives (0x00f2ef4275, records 76 to 104 bytes).
///
/// So [`record_is_aligned`] is checked at 8, not 16, and a type that steps by 16
/// is not distinguishable from one that steps by 8 without looking at the type.
pub const CHUNK_RECORD_ALIGNMENT: usize = 8;
/// True when a record's leading byte is a multiple of [`CHUNK_RECORD_ALIGNMENT`].
///
/// Measured over all 27 levels: 34,432 of 34,491 records, with the exceptions
/// confined to the seven types above.
pub fn record_is_aligned(record: &[u8]) -> bool {
match record.first() {
Some(&b) => (b as usize) % CHUNK_RECORD_ALIGNMENT == 0,
None => false,
}
}
pub const CHUNK_REGION_MARKER: [u8; 2] = [0xf6, 0x7c];
/// A chunk header plus the records its offsets address.
///
/// `stride` is the distance between the first two resolved offsets. Every
/// offset after the first is `stride` from its predecessor within a few bytes
/// on the measured levels, but the parser takes each offset literally rather
/// than assuming a uniform stride, because the deltas are not exactly constant
/// (e.g. 340, 340, 336, 340 on level 0).
pub struct Chunk {
/// The 16 opaque header bytes.
pub header: [u8; CHUNK_HEADER_BYTES],
/// Offset of this chunk within the region it was parsed from.
pub offset: usize,
/// Record count, from header byte 14.
///
/// Measured: equals `offsets.len()` on **1,123 of 1,123** chunks across
/// all 27 levels, zero mismatches. This is the one header field that is
/// decoded rather than merely measured.
pub count: u8,
/// The low 24 bits of each tagged offset, in file order.
pub offsets: Vec<u32>,
/// Record stride, from the first two offsets. Zero if fewer than two.
pub stride: usize,
}
/// One chunk, with its records already bounds-checked.
pub struct ResolvedChunk<'a> {
/// The offset table.
pub chunk: Chunk,
/// Record bytes, one per entry in `chunk.offsets`.
pub records: Vec<&'a [u8]>,
}
impl<'a> ResolvedChunk<'a> {
/// The per-chunk phase and step that explain every record's leading byte, or
/// `None` when no `(step, phase)` in that form fits.
///
/// **Measured: 1,262 of 1,509 chunks** across all 27 levels are fully explained
/// by `record[i].leading == step * ((i + phase) mod (256 / step))`, with
/// `step` taking 8 on 51 chunks, 16 on 554, 32 on 520 and 64 on 137. The other
/// 247 chunks, across 11 chunk types, are not explained by it.
///
/// Where this returns `Some`, the leading byte carries **no per-record
/// information**: it is a deterministic function of the record's index and the
/// chunk's phase, so it is a position marker rather than data. The 247 chunks
/// where it returns `None` are the ones where that has *not* been shown, and
/// nothing should skip their leading byte on the strength of this.
pub fn bucket_rule(&self) -> Option<(u8, usize)> {
use std::collections::BTreeSet;
if self.records.is_empty() {
return None;
}
let n = self.records.len();
for step in [8u16, 16, 32, 64] {
let m = (256 / step) as usize;
for phase in 0..m {
let mut ok = true;
for (i, r) in self.records.iter().enumerate() {
let Some(&b) = r.first() else {
ok = false;
break;
};
if b != ((((i + phase) % m) * step as usize % 256) as u8) {
ok = false;
break;
}
}
if ok {
return Some((step as u8, phase));
}
}
}
let _ = n;
let _: Option<BTreeSet<u8>> = None;
None
}
/// True when every record in this chunk has the same length.
///
/// **Measured: true for 1,509 of 1,509 chunks** across all 27 levels. Record
/// length varies *between* chunks of the same type -- one type runs 76 to 104
/// bytes -- but never within one. An earlier claim that some type had
/// variable-length records was pooling across chunks; it is withdrawn.
pub fn has_uniform_record_length(&self) -> bool {
let mut it = self.records.iter().filter(|r| !r.is_empty());
let Some(first) = it.next().map(|r| r.len()) else {
return true;
};
it.all(|r| r.len() == first)
}
}
/// True if `offset` looks like the start of a chunk.
///
/// The signature is the header's last byte being `0xff`, the 10-byte gap
/// being zero, and at least `min_offsets` following words carrying the `0xf0`
/// tag.
pub fn is_chunk_start(region: &[u8], offset: usize, min_offsets: usize) -> bool {
let hdr_end = offset + CHUNK_HEADER_BYTES;
if hdr_end + CHUNK_GAP_BYTES + min_offsets * 4 > region.len() {
return false;
}
if region[hdr_end - 1] != 0xff {
return false;
}
if region[hdr_end..hdr_end + CHUNK_GAP_BYTES] != [0u8; CHUNK_GAP_BYTES] {
return false;
}
(0..min_offsets).all(|k| {
let at = hdr_end + CHUNK_GAP_BYTES + k * 4;
read_u32(region, at) >> 24 == CHUNK_TAG as u32
})
}
/// The start of the chunk container: the first offset at or after `from`
/// carrying a chunk signature immediately preceded by [`CHUNK_REGION_MARKER`].
///
/// This is what closes the last class region (shrub, or tie on the levels
/// without shrub). The two parts of the signature are each weak alone -- the
/// marker pair occurs by chance dozens of times per lump, and the chunk shape
/// matches random data often enough to need four tagged offsets to be believed
/// -- and together they are exact: on all 27 levels the marker sits exactly
/// two bytes before the first chunk header, with no further marker-paired
/// signature after the region start.
///
/// Returns `None` when the signature is not found, so an unlocated boundary
/// stays visible rather than being closed against a guess.
pub fn chunk_region_start(core: &[u8], from: usize) -> Option<usize> {
if from < 2 {
return None;
}
// The marker's first byte is the cheapest rejection, so test it before
// comparing the pair or the signature behind it.
let mut at = from - 2;
while at + 2 <= core.len() {
if core[at] == CHUNK_REGION_MARKER[0]
&& core[at + 1] == CHUNK_REGION_MARKER[1]
&& is_chunk_start(core, at + 2, 4)
{
return Some(at + 2);
}
at += 1;
}
None
}
/// Little-endian, matching `Buf::u32` and the PS2.
fn read_u32(bytes: &[u8], at: usize) -> u32 {
u32::from_le_bytes([bytes[at], bytes[at + 1], bytes[at + 2], bytes[at + 3]])
}
/// Parse the chunk at `offset`, without resolving records.
///
/// Returns `None` if `offset` does not carry the chunk signature.
pub fn parse_chunk(region: &[u8], offset: usize) -> Result<Option<Chunk>> {
if !is_chunk_start(region, offset, 4) {
return Ok(None);
}
let mut b = Buf::new(region);
b.seek(offset);
let header_bytes = b.read_bytes(CHUNK_HEADER_BYTES, "chunk header")?;
let mut header = [0u8; CHUNK_HEADER_BYTES];
header.copy_from_slice(header_bytes);
b.skip(CHUNK_GAP_BYTES, "chunk gap")?;
// The offset table runs until the first word without the tag.
let table_start = b.pos();
let mut offsets = Vec::new();
while b.pos() + 4 <= region.len() {
let v = b.u32("chunk offset")?;
if v >> 24 != CHUNK_TAG as u32 {
b.seek(table_start);
break;
}
offsets.push(v & 0x00ff_ffff);
}
// The offset table runs to the first untagged word, but the header carries
// its own count. Truncating to that count stops a stray tagged word in the
// record data from extending the table.
offsets.truncate(header[14] as usize);
let stride = if offsets.len() >= 2 {
(offsets[1] - offsets[0]) as usize
} else {
0
};
Ok(Some(Chunk {
count: header[14],
header,
offset,
offsets,
stride,
}))
}
/// Resolve every record a chunk addresses, bounds-checked against `region`.
///
/// Each record is `region[chunk.offset + low .. + stride]`. A record that
/// would run past the end of the region is dropped rather than clamped, so the
/// returned length may be shorter than `chunk.offsets` -- on the measured
/// levels it is not, but a parser that silently clamps would hide a malformed
/// chunk rather than report it.
pub fn resolve<'r>(chunk: &Chunk, region: &'r [u8]) -> Result<ResolvedChunk<'r>> {
let mut records = Vec::with_capacity(chunk.offsets.len());
for &low in &chunk.offsets {
let start = chunk
.offset
.checked_add(low as usize)
.ok_or(crate::buf::Error {
offset: chunk.offset,
context: "chunk record",
kind: ErrorKind::Invalid("chunk offset overflows usize"),
})?;
if start + chunk.stride > region.len() {
continue;
}
records.push(&region[start..start + chunk.stride]);
}
Ok(ResolvedChunk {
chunk: Chunk {
count: chunk.count,
header: chunk.header,
offset: chunk.offset,
offsets: chunk.offsets.clone(),
stride: chunk.stride,
},
records,
})
}
/// Walk every chunk in `region`, resolving records.
///
/// Returns chunks that carried the signature but whose records did not all
/// resolve, alongside the ones that did. Callers that care about coverage
/// should check both.
pub fn parse_all_chunks(region: &[u8]) -> Result<Vec<ResolvedChunk<'_>>> {
let mut out = Vec::new();
let mut at = 0usize;
while at + CHUNK_HEADER_BYTES + CHUNK_GAP_BYTES + 16 <= region.len() {
let Some(chunk) = parse_chunk(region, at)? else {
at += 1;
continue;
};
// Resume past this chunk's header and offset table so a chunk's own
// record data is not rescanned as further chunk headers.
let consumed = CHUNK_HEADER_BYTES + CHUNK_GAP_BYTES + chunk.offsets.len() * 4;
let resolved = resolve(&chunk, region)?;
out.push(resolved);
at += consumed.max(1);
}
Ok(out)
}
#[cfg(test)]
mod tests {
use super::*;
/// A chunk signature at `at`, preceded by the marker: the marker pair, the
/// 16-byte header whose last byte is `0xff`, the 10 zero bytes, then four
/// tagged offsets.
fn plant_chunk(core: &mut [u8], at: usize) {
let base = at - 2;
core[base..base + 2].copy_from_slice(&CHUNK_REGION_MARKER);
// Header: byte 14 is the record count, matching the four tagged words.
core[at + 14] = 4;
core[at + 15] = 0xff;
// The 10 zero bytes of the gap at +0x10. Their low bits resolve to a
// zero stride, which is fine: only the signature is under test.
core[at + CHUNK_HEADER_BYTES..at + CHUNK_HEADER_BYTES + CHUNK_GAP_BYTES].fill(0);
for k in 0..4 {
let word = (CHUNK_TAG as u32) << 24;
core[at + CHUNK_HEADER_BYTES + CHUNK_GAP_BYTES + k * 4
..at + CHUNK_HEADER_BYTES + CHUNK_GAP_BYTES + k * 4 + 4]
.copy_from_slice(&word.to_le_bytes());
}
}
#[test]
fn finds_the_markered_signature() {
let mut core = vec![0x5au8; 256];
plant_chunk(&mut core, 128);
assert_eq!(chunk_region_start(&core, 64), Some(128));
// Scanning from earlier still finds it: the signature is what locates
// the boundary, not the search start.
assert_eq!(chunk_region_start(&core, 2), Some(128));
}
#[test]
fn a_marker_without_a_signature_is_not_a_boundary() {
let mut core = vec![0x5au8; 256];
core[10..12].copy_from_slice(&CHUNK_REGION_MARKER);
assert_eq!(chunk_region_start(&core, 2), None);
}
#[test]
fn a_signature_without_the_marker_is_not_a_boundary() {
let mut core = vec![0x5au8; 256];
// Plant the signature, then break the marker in front of it.
plant_chunk(&mut core, 128);
core[126] = 0x5a;
assert_eq!(chunk_region_start(&core, 2), None);
}
#[test]
fn an_unlocated_boundary_stays_none() {
assert_eq!(chunk_region_start(&[], 2), None);
// `from` below the marker's own minimum is refused rather than
// underflowing.
assert_eq!(chunk_region_start(&[0xf6], 0), None);
}
}