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).
This commit is contained in:
119
crates/sylpheed-formats/examples/ratc_optless_children.rs
Normal file
119
crates/sylpheed-formats/examples/ratc_optless_children.rs
Normal file
@@ -0,0 +1,119 @@
|
||||
//! The 60 RATC children that carry no `opt ` block — do they lack it, or is our
|
||||
//! window too small?
|
||||
//!
|
||||
//! [`ratc::parse`] now prefers the name a child's own `opt ` block states, and
|
||||
//! falls back to the old backwards printable-run scan when there is no block
|
||||
//! within 128 bytes. That fallback fires for 60 of the disc's 18 002 children
|
||||
//! (0.3 %), and the reach of the finding in `docs/re/structures/ratc-child-names.md`
|
||||
//! stops there: "whether they genuinely lack the block or sit past the search
|
||||
//! window is not established".
|
||||
//!
|
||||
//! This settles that. For every child with no accepted block it reports
|
||||
//!
|
||||
//! * whether an `opt ` tag exists at all further back, and how far;
|
||||
//! * which guard rejected a tag that WAS in the window (length, gap, charset);
|
||||
//! * the child's position in its bundle and its magic, in case the opt-less
|
||||
//! ones are structurally distinct (e.g. always the first child);
|
||||
//! * the raw bytes before the magic, so the fallback's answer can be judged.
|
||||
//!
|
||||
//! cargo run -p sylpheed-formats --example ratc_optless_children -- <pak>...
|
||||
use sylpheed_formats::{pak, ratc};
|
||||
|
||||
/// Why a child has no accepted `opt ` name. Mirrors `ratc::opt_name`'s guards
|
||||
/// one for one, so a rejection here is the same rejection the parser made.
|
||||
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Clone)]
|
||||
enum Why {
|
||||
/// No `opt ` tag in the 128-byte window, and none anywhere before it either.
|
||||
NoTagAtAll,
|
||||
/// No tag in the window, but one exists further back, this many bytes away.
|
||||
TagBeyondWindow(usize),
|
||||
/// Tag found, but its BE32 length is 0 or > 64.
|
||||
BadLength(usize),
|
||||
/// Tag found, name ends more than 8 bytes before the magic — a neighbour's.
|
||||
GapTooBig(usize),
|
||||
/// Tag found, the named bytes are not all printable ASCII.
|
||||
NotGraphic,
|
||||
}
|
||||
|
||||
/// The parser's own window.
|
||||
const WINDOW: usize = 128;
|
||||
|
||||
fn classify(buf: &[u8], at: usize) -> Option<Why> {
|
||||
let lo = at.saturating_sub(WINDOW);
|
||||
let pos = match buf[lo..at].windows(4).rposition(|w| w == b"opt ") {
|
||||
Some(p) => lo + p,
|
||||
None => {
|
||||
// Widen to the whole buffer before the child: is it merely far away?
|
||||
return Some(match buf[..at].windows(4).rposition(|w| w == b"opt ") {
|
||||
Some(p) => Why::TagBeyondWindow(at - p),
|
||||
None => Why::NoTagAtAll,
|
||||
});
|
||||
}
|
||||
};
|
||||
let len = u32::from_be_bytes(buf.get(pos + 4..pos + 8)?.try_into().ok()?) as usize;
|
||||
if len == 0 || len > 64 || pos + 8 + len > at {
|
||||
return Some(Why::BadLength(len));
|
||||
}
|
||||
let gap = at - (pos + 8 + len);
|
||||
if gap > 8 {
|
||||
return Some(Why::GapTooBig(gap));
|
||||
}
|
||||
let s = String::from_utf8_lossy(&buf[pos + 8..pos + 8 + len]);
|
||||
if s.is_empty() || !s.chars().all(|c| c.is_ascii_graphic()) {
|
||||
return Some(Why::NotGraphic);
|
||||
}
|
||||
None // accepted — this child is not one of the 60
|
||||
}
|
||||
|
||||
fn main() {
|
||||
let mut total = 0usize;
|
||||
let mut rows: Vec<(String, usize, usize, String, String, Why)> = Vec::new();
|
||||
let mut first_child_of_bundle = 0usize;
|
||||
for path in std::env::args().skip(1) {
|
||||
let Ok(ar) = pak::PakArchive::open(&path) else { continue };
|
||||
let short = path.rsplit('/').next().unwrap_or(&path).to_string();
|
||||
let entries: Vec<_> = ar.entries().to_vec();
|
||||
for (ei, e) in entries.iter().enumerate() {
|
||||
let Ok(bytes) = ar.read(e) else { continue };
|
||||
let Some(kids) = ratc::parse(&bytes) else { continue };
|
||||
for (ci, c) in kids.iter().enumerate() {
|
||||
total += 1;
|
||||
let Some(why) = classify(&bytes, c.offset) else { continue };
|
||||
if ci == 0 {
|
||||
first_child_of_bundle += 1;
|
||||
}
|
||||
let lo = c.offset.saturating_sub(24);
|
||||
let hex = bytes[lo..c.offset]
|
||||
.iter()
|
||||
.map(|b| format!("{b:02x}"))
|
||||
.collect::<Vec<_>>()
|
||||
.join(" ");
|
||||
rows.push((short.clone(), ei, ci, c.name.clone(), hex, why));
|
||||
}
|
||||
}
|
||||
}
|
||||
println!("RATC children scanned : {total}");
|
||||
println!(" with NO accepted `opt ` block: {}", rows.len());
|
||||
println!(" ... of which are child #0 : {first_child_of_bundle}");
|
||||
|
||||
let mut by_why: std::collections::BTreeMap<String, usize> = Default::default();
|
||||
for r in &rows {
|
||||
let k = match &r.5 {
|
||||
Why::TagBeyondWindow(_) => "TagBeyondWindow".to_string(),
|
||||
Why::BadLength(_) => "BadLength".to_string(),
|
||||
Why::GapTooBig(_) => "GapTooBig".to_string(),
|
||||
other => format!("{other:?}"),
|
||||
};
|
||||
*by_why.entry(k).or_default() += 1;
|
||||
}
|
||||
println!("\nwhy, by cause:");
|
||||
for (k, n) in &by_why {
|
||||
println!(" {k:20} x{n}");
|
||||
}
|
||||
|
||||
println!("\nevery occurrence (name is what the FALLBACK scan returned):");
|
||||
for (p, ei, ci, name, hex, why) in &rows {
|
||||
println!(" {p:28} entry {ei:4} child {ci:3} {name:24} {why:?}");
|
||||
println!(" 24 bytes before the magic: {hex}");
|
||||
}
|
||||
}
|
||||
@@ -110,7 +110,12 @@ fn opt_name(bytes: &[u8], off: usize) -> Option<String> {
|
||||
/// 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.
|
||||
/// 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];
|
||||
|
||||
Reference in New Issue
Block a user