The shrub region's end was the last unlocated bound in core_data. It was not in the core index, and unlike moby->tie it had no successor boundary to chain against, so the last shrub class in every level ran out to the lump's end and the aggregates over it (the "3.5M records / 28MB of shrub body") were artifacts of that oversized span. Range measured: for each level, from the last shrub class's geometry offset to the constant trailer. Nothing outside that range is read. Method: scan forward from the last shrub class for the pair (a) the constant two bytes f6 7c sitting immediately in front of (b) a chunk signature -- a 16-byte header ending 0xff, ten zero bytes, then four or more 0xf0-tagged offsets. The two halves are each weak alone, and the halves are measured separately: the marker pair occurs 25 to 103 times in a 5 to 17 MB lump, so matching it alone picks dozens of wrong offsets; the chunk shape matches random data often enough to need four tagged offsets before it is believed. Result: on all 27 levels the marker sits exactly two bytes before the first chunk header, and the two parts of the signature are never found apart. No chunk signature occurs inside any last shrub body, on any level. The three levels without shrub (10, 25, 26) carry the same marker before the container that closes tie instead, which is what makes the locator a property of the region rather than a shrub rule. Consequences: - All 358 shrub class bodies are bounded and are whole 8-byte records. That was not assertable while the last class in each level ran to the lump's end, and it is the check the boundary earns its keep for. - 9,600 records in total, against the 8,938 previously measurable in successor-bounded spans; 662 of them sit in the 24 last-class bodies. The shrub region totals 2,033,776 bytes across the 24 levels that have one. - The chunk container is 25,775,850 bytes across 27 levels (1,236 chunks, 22,562 records). The container measurements were previously taken over "the 900,000 bytes before the trailer", a window that did not align to its real extent: on level 0 it is 690,302 bytes, and 27 of 1,123 chunks were truncated at that invented edge. chunk_containers_resolve_on_every_level now reads the region from core_layout instead of measuring backwards from the end of the lump. core_layout takes core_data now: the container's start is the one bound that cannot be derived from the index. It gains the chunks region, so every byte of level-specific core_data belongs to a named region. Where the signature is not found the end stays None -- the region is left visible rather than clamped to the tail. Still open: the marker's meaning (constant across 27 levels, at exactly this position, unnamed) and the shrub body payload's encoding.
315 lines
12 KiB
Rust
315 lines
12 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.
|
|
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]>,
|
|
}
|
|
|
|
/// 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(®ion[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);
|
|
}
|
|
}
|