Files
Sylpheed/crates/sylpheed-formats/src/ratc.rs
Sylpheed RE agent f817dd5939 re(ui): the 60 nameless RATC children are frames, not children -- .tan decoded
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).
2026-08-29 07:39:09 +00:00

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());
}
}