Closes the reach caveat the `opt ` name fix left behind: 60 of 18 002 RATC children carry no `opt ` block, and it was not established whether they lack one or sit past our 128-byte window. Neither. They are not children. `examples/ratc_optless_children.rs` re-runs `ratc::parse`'s own guards over the disc and reports which one fired: all 60 are "tag beyond the window", none is rejected by length, gap or charset, none is child #0, and all 60 live in six bundles of one archive. Within a bundle the distances back to the nearest tag are an exact arithmetic progression, step 60 600 -- ten different records finding the SAME tag, because there is only one. Reading a bundle directly: children 1..10 are equal-size T8aD blocks under a single `opt ` name, `pb_f15_eg_anm.tan`. `.tan` is a FRAME SEQUENCE. One block declares the resource; its payload is a run of T8aD frames. Disc-wide, over all 18 718 `opt ` names in all 33 paks: a RATC bundle names exactly six kinds of resource -- `.t32` 14 756, `.rat` 3 311, `.prm` 367, `.tbm` 224, `.sbo` 54, `.tan` 6. Six `.tan`, ten frames each = 60, the entire population with nothing left over. The negative is closed, not narrowed. Consequence recorded but deliberately not fixed: `ratc::parse` over-reports there, listing a `.tan`'s frames as anonymous children. Nothing in the menu milestone reads a `.tan` -- it occurs only in GP_READY_ROOM, which S1 ruled out -- so no screen the port draws changes. Also a METHOD entry for this container OOM-killing `slb_leading_segment_disc` under default test parallelism (SIGKILL, no assertion; 8/8 pass with --test-threads=1).
217 lines
8.2 KiB
Rust
217 lines
8.2 KiB
Rust
//! `RATC` — a nested resource bundle.
|
|
//!
|
|
//! After a small header, a RATC holds named children — `foo.t32` (T8aD
|
|
//! textures), `foo.rat` (nested RATC), fonts, PNG — each immediately preceded by
|
|
//! its ASCII name string. The children are self-locating by their 4-char magic,
|
|
//! so we list them by scanning for those magics and pairing each with the name
|
|
//! run that precedes it. This is reliable for *listing* (names / types / sizes
|
|
//! are literal bytes); decoding a child's pixels is delegated to that child's
|
|
//! own parser ([`crate::t8ad`]).
|
|
|
|
/// A child resource inside a RATC bundle.
|
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
|
pub struct RatcChild {
|
|
/// Name string preceding the child (e.g. `prselectbtn_g04b.t32`), or empty.
|
|
pub name: String,
|
|
/// Child magic tag: `T8aD`, `RATC`, `ttcf`, `png`, …
|
|
pub kind: String,
|
|
/// Byte offset of the child (its magic) within the RATC payload.
|
|
pub offset: usize,
|
|
/// Byte length of the child, up to the next child (or end of payload).
|
|
pub size: usize,
|
|
}
|
|
|
|
/// Magic at the start of a RATC bundle.
|
|
pub const RATC_MAGIC: [u8; 4] = *b"RATC";
|
|
|
|
/// Whether `bytes` is a RATC bundle.
|
|
pub fn is_ratc(bytes: &[u8]) -> bool {
|
|
bytes.len() >= 4 && bytes[0..4] == RATC_MAGIC
|
|
}
|
|
|
|
/// Recognized child magics and their display tag.
|
|
fn child_kind(m: &[u8]) -> Option<&'static str> {
|
|
match m {
|
|
b"T8aD" => Some("T8aD"),
|
|
b"RATC" => Some("RATC"),
|
|
b"ttcf" => Some("ttc"),
|
|
b"\x89PNG" => Some("png"),
|
|
_ => None,
|
|
}
|
|
}
|
|
|
|
/// List the children of a RATC bundle (the self-magic at offset 0 is skipped).
|
|
pub fn parse(bytes: &[u8]) -> Option<Vec<RatcChild>> {
|
|
if !is_ratc(bytes) {
|
|
return None;
|
|
}
|
|
|
|
// Offsets of every recognized child magic (skip the self RATC at 0).
|
|
let mut offs: Vec<(usize, &'static str)> = Vec::new();
|
|
let mut i = 4;
|
|
while i + 4 <= bytes.len() {
|
|
if let Some(kind) = child_kind(&bytes[i..i + 4]) {
|
|
offs.push((i, kind));
|
|
i += 4;
|
|
} else {
|
|
i += 1;
|
|
}
|
|
}
|
|
|
|
let mut children = Vec::with_capacity(offs.len());
|
|
for (idx, &(off, kind)) in offs.iter().enumerate() {
|
|
let next = offs.get(idx + 1).map(|&(o, _)| o).unwrap_or(bytes.len());
|
|
children.push(RatcChild {
|
|
name: opt_name(bytes, off).unwrap_or_else(|| name_before(bytes, off)),
|
|
kind: kind.to_string(),
|
|
offset: off,
|
|
size: next.saturating_sub(off),
|
|
});
|
|
}
|
|
Some(children)
|
|
}
|
|
|
|
/// The name a child's own `opt ` block states, if it has one.
|
|
///
|
|
/// The real format is explicit. Immediately before each child sits
|
|
///
|
|
/// ```text
|
|
/// "opt " | BE32 length | name | NUL | 3 bytes | <child magic>
|
|
/// ```
|
|
///
|
|
/// -- the same `opt ` block `ui_layout`'s focus link already reads. Prefer it,
|
|
/// because [`name_before`] is a heuristic and those 3 trailing bytes are
|
|
/// sometimes printable, in which case the heuristic reads THEM as the name.
|
|
/// Measured disc-wide: of 18 002 RATC children, 17 942 carry an `opt ` block,
|
|
/// 17 918 of which agree with the scan and **24 do not** -- every one of the 24
|
|
/// a 3-byte tail (`8AX` x22, `'OX` x2) beating a real name. On the title screens
|
|
/// that cost the whole background: `pteff05.t32` came out as `8AX`, its element
|
|
/// then resolved to no sprite, and `compose` silently dropped it. See
|
|
/// `docs/re/structures/ratc-child-names.md`.
|
|
fn opt_name(bytes: &[u8], off: usize) -> Option<String> {
|
|
let lo = off.saturating_sub(128);
|
|
let win = &bytes[lo..off];
|
|
let pos = lo + win.windows(4).rposition(|w| w == b"opt ")?;
|
|
let len = u32::from_be_bytes(bytes.get(pos + 4..pos + 8)?.try_into().ok()?) as usize;
|
|
if len == 0 || len > 64 || pos + 8 + len > off {
|
|
return None;
|
|
}
|
|
// It must be THIS child's block: the name, its NUL and a short run to the
|
|
// magic. Anything further away is a neighbour's block, so fall back.
|
|
if off - (pos + 8 + len) > 8 {
|
|
return None;
|
|
}
|
|
let s = String::from_utf8_lossy(&bytes[pos + 8..pos + 8 + len]).to_string();
|
|
(!s.is_empty() && s.chars().all(|c| c.is_ascii_graphic())).then_some(s)
|
|
}
|
|
|
|
/// The nearest name string preceding `off`: the *last* printable run (len ≥ 3)
|
|
/// in the 96 bytes before the child magic. A few record-header bytes usually sit
|
|
/// between the name and the magic, so an exact-adjacency scan isn't enough.
|
|
///
|
|
/// Fallback only -- [`opt_name`] is the stated name. 60 of the disc's 18 002
|
|
/// children have no `opt ` block and still rely on this, and all 60 are
|
|
/// accounted for: they are the ten frames of the disc's only `.tan` frame
|
|
/// sequence, in six language copies of one `GP_READY_ROOM` bundle, where a
|
|
/// single `opt ` block names the whole run. See
|
|
/// `docs/re/structures/ratc-tan-frame-sequence.md` -- and note that this means
|
|
/// `parse` OVER-reports there, listing frames as children.
|
|
fn name_before(bytes: &[u8], off: usize) -> String {
|
|
let start = off.saturating_sub(96);
|
|
let window = &bytes[start..off];
|
|
let mut best = String::new();
|
|
let mut run_start: Option<usize> = None;
|
|
let flush = |from: usize, to: usize, best: &mut String| {
|
|
if to - from >= 3 {
|
|
*best = String::from_utf8_lossy(&window[from..to]).trim().to_string();
|
|
}
|
|
};
|
|
for (i, &c) in window.iter().enumerate() {
|
|
if (0x20..=0x7e).contains(&c) {
|
|
run_start.get_or_insert(i);
|
|
} else if let Some(s) = run_start.take() {
|
|
flush(s, i, &mut best);
|
|
}
|
|
}
|
|
if let Some(s) = run_start {
|
|
flush(s, window.len(), &mut best);
|
|
}
|
|
best
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
/// The `opt ` block wins over a printable tail.
|
|
///
|
|
/// This is the `pteff05.t32` / `8AX` case, byte for byte: the name is stated
|
|
/// with an explicit length, then a NUL, then three payload bytes that happen
|
|
/// to spell `8AX` in ASCII. The old backwards printable-run scan returned
|
|
/// `8AX` here, which is what dropped the background from every menu screen.
|
|
#[test]
|
|
fn opt_block_beats_a_printable_tail() {
|
|
let mut b = RATC_MAGIC.to_vec();
|
|
b.extend_from_slice(&[0u8; 28]);
|
|
b.extend_from_slice(b"opt ");
|
|
b.extend_from_slice(&11u32.to_be_bytes()); // len("pteff05.t32")
|
|
b.extend_from_slice(b"pteff05.t32\0");
|
|
b.extend_from_slice(b"8AX"); // payload, printable by accident
|
|
let off = b.len();
|
|
b.extend_from_slice(b"T8aD");
|
|
b.extend_from_slice(&[0u8; 16]);
|
|
|
|
let kids = parse(&b).expect("parse");
|
|
assert_eq!(kids.len(), 1);
|
|
assert_eq!(kids[0].name, "pteff05.t32");
|
|
assert_eq!(kids[0].offset, off);
|
|
// And the heuristic on its own really would have said `8AX` -- so this
|
|
// test fails for the right reason if the preference is ever reversed.
|
|
assert_eq!(name_before(&b, off), "8AX");
|
|
}
|
|
|
|
/// No `opt ` block: 60 of the disc's 18 002 children are like this, and they
|
|
/// must keep working off the scan.
|
|
#[test]
|
|
fn falls_back_to_the_scan_without_an_opt_block() {
|
|
let mut b = RATC_MAGIC.to_vec();
|
|
b.extend_from_slice(&[0u8; 28]);
|
|
b.extend_from_slice(b"plain.t32");
|
|
b.extend_from_slice(&[0x0e, 0x10, 0xa4]);
|
|
let off = b.len();
|
|
b.extend_from_slice(b"T8aD");
|
|
b.extend_from_slice(&[0u8; 16]);
|
|
|
|
let kids = parse(&b).expect("parse");
|
|
assert_eq!(kids[0].name, "plain.t32");
|
|
}
|
|
|
|
#[test]
|
|
fn lists_named_children() {
|
|
let mut b = RATC_MAGIC.to_vec();
|
|
b.extend_from_slice(&[0u8; 28]); // header padding
|
|
b.extend_from_slice(b"logo.t32");
|
|
let t8_off = b.len();
|
|
b.extend_from_slice(b"T8aD");
|
|
b.extend_from_slice(&[0u8; 40]); // some child bytes
|
|
b.extend_from_slice(b"sub.rat");
|
|
let ratc_off = b.len();
|
|
b.extend_from_slice(b"RATC");
|
|
b.extend_from_slice(&[0u8; 8]);
|
|
|
|
let kids = parse(&b).unwrap();
|
|
assert_eq!(kids.len(), 2);
|
|
assert_eq!(kids[0].kind, "T8aD");
|
|
assert_eq!(kids[0].name, "logo.t32");
|
|
assert_eq!(kids[0].offset, t8_off);
|
|
assert_eq!(kids[0].size, ratc_off - t8_off);
|
|
assert_eq!(kids[1].kind, "RATC");
|
|
assert_eq!(kids[1].name, "sub.rat");
|
|
}
|
|
|
|
#[test]
|
|
fn rejects_non_ratc() {
|
|
assert!(parse(b"T8aD....").is_none());
|
|
}
|
|
}
|