//! Disc-wide guards for the 2D surface formats, locking in what was measured on //! 2026-08-11. Skipped (as no-ops) when the extracted disc is absent. //! //! These assert *disc-wide invariants* rather than one hand-picked file, because //! each of the findings they guard was originally missed by reasoning from a //! sample: T8aD's "~15 % unsupported variants" were a wrong model, and LSTA's //! "a few entries disagree with the count" was a miscount. use std::path::{Path, PathBuf}; use sylpheed_formats::{lsta, pak::PakArchive, ratc, t8ad}; fn disc_root() -> Option { if let Ok(p) = std::env::var("SYLPHEED_DISC") { let p = PathBuf::from(p); if p.join("dat").is_dir() { return Some(p); } } let default = Path::new( "/home/fabi/RE - Project Sylpheed/Project Sylpheed - Arc of Deception (USA, Europe) (En,Ja)", ); if default.join("dat").is_dir() { return Some(default.to_path_buf()); } None } macro_rules! skip_without_disc { ($root:ident) => { let Some($root) = disc_root() else { eprintln!("SKIP: extracted disc not found (set SYLPHEED_DISC to enable)"); return; }; }; } /// Every entry of every pak, plus every RATC child, as raw bytes. fn for_each_blob(root: &Path, mut f: impl FnMut(&str, &str, &[u8])) { let mut paks: Vec = std::fs::read_dir(root.join("dat")) .expect("dat/") .flatten() .map(|e| e.path()) .filter(|p| p.extension().and_then(|s| s.to_str()) == Some("pak")) .collect(); paks.sort(); for p in &paks { let pak_name = p.file_name().unwrap().to_string_lossy().to_string(); let Ok(arc) = PakArchive::open(p) else { continue }; for e in arc.entries() { let Ok(bytes) = arc.read(e) else { continue }; f(&pak_name, &format!("{:08x}", e.name_hash), &bytes); if ratc::is_ratc(&bytes) { if let Some(kids) = ratc::parse(&bytes) { for k in &kids { if k.offset + k.size <= bytes.len() { f(&pak_name, &k.name, &bytes[k.offset..k.offset + k.size]); } } } } } } } /// A T8aD surface is a list of sub-rectangles, and on this disc **every** one /// decodes. Regressing the rectangle model would show up here as a decode gap, /// which is exactly how the old 256-grid reading looked (96 %, not 100 %). #[test] fn every_t8ad_on_the_disc_decodes() { skip_without_disc!(root); let (mut total, mut ok) = (0usize, 0usize); let mut first_failure = None; for_each_blob(&root, |pak, name, b| { if !t8ad::is_t8ad(b) || b.len() < 0x40 { return; } total += 1; if t8ad::parse(b).is_some() { ok += 1; } else if first_failure.is_none() { first_failure = Some(format!("{pak}:{name}")); } }); assert!(total > 19_000, "expected the disc's ~19 216 surfaces, saw {total}"); assert_eq!(ok, total, "first failure: {first_failure:?}"); } /// An LSTA's header count is exact and counts **both** kinds of element: T8aD /// sprites and `PRMD` primitives. (It was long read as unreliable because the /// comparison ignored primitives.) #[test] fn lsta_count_equals_sprites_plus_primitives() { skip_without_disc!(root); let count_magic = |b: &[u8], magic: &[u8; 4]| { let (mut n, mut i) = (0usize, 4usize); while i + 4 <= b.len() { if &b[i..i + 4] == magic { n += 1; i += 4; } else { i += 1; } } n }; let (mut lists, mut exact) = (0usize, 0usize); let mut bad = Vec::new(); for_each_blob(&root, |pak, name, b| { if !lsta::is_lsta(b) || b.len() < 8 { return; } lists += 1; let declared = u32::from_be_bytes([b[4], b[5], b[6], b[7]]) as usize; let sprites = count_magic(b, b"T8aD"); let prims = count_magic(b, b"PRMD"); if declared == sprites + prims { exact += 1; } else if bad.len() < 4 { bad.push(format!("{pak}:{name} declared {declared} != {sprites}+{prims}")); } }); assert!(lists >= 60, "expected the disc's 64 LSTA lists, saw {lists}"); assert_eq!(exact, lists, "mismatches: {bad:?}"); } /// Nested RATC records are **leaves**: they carry no child list of their own. /// "One level deep" describes the data, not a parser limit. #[test] fn ratc_nesting_is_exactly_one_level() { skip_without_disc!(root); let mut grandchildren = 0usize; let mut bundles = 0usize; for_each_blob(&root, |_, _, b| { if !ratc::is_ratc(b) { return; } let Some(kids) = ratc::parse(b) else { return }; bundles += 1; for k in &kids { if k.offset + k.size > b.len() { continue; } let sub = &b[k.offset..k.offset + k.size]; if ratc::is_ratc(sub) { grandchildren += ratc::parse(sub).map(|g| g.len()).unwrap_or(0); } } }); assert!(bundles > 2_000, "expected thousands of RATC bundles, saw {bundles}"); assert_eq!(grandchildren, 0, "a nested RATC record listed children"); }