formats: expose loop_length_units -- the port was reading +0x08 itself
sylpheed-port reports that a record's loop length is on no public ref at all (example, test and docs/re/ only), so their screen.rs parses the four bytes with its own RATC guard. That is my field to publish. One function serves both levels, since a nested .rat leaf is itself a RATC bundle with the same header shape. Returns None for a non-RATC or short slice so callers need no guard of their own. Verified against the disc, controls first: rejects a non-RATC slice, rejects one too short for the field, reads big-endian at +0x08 -- then reproduces every published value (ptbtn00f 120, ptloop01 600, ptloop02 720) over 65 GP_TITLE records with 0 violations of +0x08 >= largest keyframe time. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Wuu56cE8vJGTBtn1ppsk8v
This commit is contained in:
63
crates/sylpheed-formats/examples/record_loop_length_api.rs
Normal file
63
crates/sylpheed-formats/examples/record_loop_length_api.rs
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
//! Verify the newly-public `ui_layout::loop_length_units` against the disc.
|
||||||
|
//!
|
||||||
|
//! `sylpheed-port` reads a record's `+0x08` itself, guarded on the RATC magic,
|
||||||
|
//! because the field was exposed on no public ref at all — example, test and
|
||||||
|
//! `docs/re/` only. This checks the public function reproduces the numbers the
|
||||||
|
//! finding was written from before the port depends on it.
|
||||||
|
//!
|
||||||
|
//! CONTROL FIRST: the function must return `None` for a non-RATC slice and for a
|
||||||
|
//! slice too short to hold the field. An accessor that returns a number for
|
||||||
|
//! anything cannot be trusted to return the right one.
|
||||||
|
//!
|
||||||
|
//! cargo run -p sylpheed-formats --example record_loop_length_api
|
||||||
|
use sylpheed_formats::{pak::PakArchive, ui_layout};
|
||||||
|
use std::path::PathBuf;
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
// ---- controls -------------------------------------------------------
|
||||||
|
assert_eq!(ui_layout::loop_length_units(b"NOTR\x00\x00\x00\x00\x00\x00\x00\x78"), None,
|
||||||
|
"control FAILED: accepted a non-RATC slice");
|
||||||
|
assert_eq!(ui_layout::loop_length_units(b"RATC\x00\x00"), None,
|
||||||
|
"control FAILED: accepted a slice too short for +0x08");
|
||||||
|
assert_eq!(ui_layout::loop_length_units(b"RATC\x00\x00\x00\x00\x00\x00\x00\x78"), Some(120),
|
||||||
|
"control FAILED: did not read +0x08 big-endian");
|
||||||
|
println!("controls pass: rejects non-RATC, rejects short, reads BE at +0x08");
|
||||||
|
|
||||||
|
let root = PathBuf::from(std::env::var("SYLPHEED_DISC").expect("SYLPHEED_DISC"));
|
||||||
|
let ar = PakArchive::open(root.join("dat/GP_TITLE.pak")).expect("GP_TITLE.pak");
|
||||||
|
|
||||||
|
// The records the finding names, with their published values.
|
||||||
|
let expect: &[(&str, u32)] = &[("ptbtn00f.rat", 120), ("ptloop01.rat", 600),
|
||||||
|
("ptloop02.rat", 720)];
|
||||||
|
let mut seen = 0usize;
|
||||||
|
let (mut recs, mut viol) = (0usize, 0usize);
|
||||||
|
|
||||||
|
for (ei, e) in ar.entries().iter().enumerate() {
|
||||||
|
let Ok(by) = ar.read(e) else { continue };
|
||||||
|
let Some(b) = ui_layout::parse_build(&by) else { continue };
|
||||||
|
for (name, (off, size)) in &b.records {
|
||||||
|
let rec = &by[*off..(*off + *size).min(by.len())];
|
||||||
|
let Some(len) = ui_layout::loop_length_units(rec) else { continue };
|
||||||
|
recs += 1;
|
||||||
|
// the disc-wide invariant the finding rests on
|
||||||
|
let largest = ui_layout::parse_build(rec)
|
||||||
|
.map(|l| l.elements.iter()
|
||||||
|
.flat_map(|el| el.keyframes.iter().filter_map(|k| k.time))
|
||||||
|
.max().unwrap_or(0))
|
||||||
|
.unwrap_or(0);
|
||||||
|
if len < largest { viol += 1; }
|
||||||
|
for (want_name, want) in expect {
|
||||||
|
if name == want_name && seen < 16 {
|
||||||
|
seen += 1;
|
||||||
|
let ok = if len == *want { "OK" } else { "MISMATCH" };
|
||||||
|
println!(" entry {ei:2} {name:16} +0x08 = {len:4} \
|
||||||
|
(published {want}) largest kf {largest:4} {ok}");
|
||||||
|
assert_eq!(len, *want, "{name} disagrees with the published value");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
println!("\n{recs} records read through the public fn; \
|
||||||
|
{viol} violate +0x08 >= largest keyframe time");
|
||||||
|
assert!(seen > 0, "found none of the named records — the check proved nothing");
|
||||||
|
}
|
||||||
@@ -498,6 +498,30 @@ fn opt_link(rec: &[u8]) -> Option<String> {
|
|||||||
(!s.is_empty()).then_some(s)
|
(!s.is_empty()).then_some(s)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// A RATC record's animation **loop length** in keyframe units — its `+0x08`.
|
||||||
|
///
|
||||||
|
/// Works at either level: a nested `.rat` leaf is itself a RATC bundle with the
|
||||||
|
/// same header shape as the one containing it, so this reads a whole screen
|
||||||
|
/// build's length and a single record's length through one path.
|
||||||
|
///
|
||||||
|
/// **Why it is public.** The loop length is not the largest keyframe time —
|
||||||
|
/// `ptbtn00f.rat`, the `PRESS Ⓐ` plate glow, declares **120** while its last
|
||||||
|
/// keyframe is at **105**, and that 15-unit slack is the plate holding dark
|
||||||
|
/// between cycles. A consumer that infers the period from the keyframes gets
|
||||||
|
/// 105 (1.750 s) against a real pulse measured four times at 2.12–2.34 s.
|
||||||
|
/// Decoded disc-wide: 1 781 records, **0** violations of
|
||||||
|
/// `+0x08 >= largest keyframe time` — see
|
||||||
|
/// `docs/re/structures/ui-record-loop-length.md`.
|
||||||
|
///
|
||||||
|
/// Returns `None` for anything that is not a RATC record, so it is safe to call
|
||||||
|
/// on an arbitrary slice; callers do not need their own magic guard.
|
||||||
|
pub fn loop_length_units(rec: &[u8]) -> Option<u32> {
|
||||||
|
if rec.len() < 0x0c || rec[0..4] != *b"RATC" {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
Some(be32(rec, 0x08))
|
||||||
|
}
|
||||||
|
|
||||||
/// The sprite a `.rat` record places: a NUL-terminated name at `0x20`.
|
/// The sprite a `.rat` record places: a NUL-terminated name at `0x20`.
|
||||||
///
|
///
|
||||||
/// The field is **not** 16 bytes. Capping it there truncates every longer name —
|
/// The field is **not** 16 bytes. Capping it there truncates every longer name —
|
||||||
|
|||||||
Reference in New Issue
Block a user