//! 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, /// 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 { 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> { 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> { 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>> { 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); } }