80 findings, not the 14 the first run showed -- clippy stops at the first failing compilation unit, so `--keep-going` is what makes the list complete. 60 were machine-applicable (`cargo clippy --fix`). The rest by hand: * five descending `sort_by` -> `sort_by_key(Reverse(..))` * `chunks_exact(4)` on both sides of four zips, so the compared items stay `[u8; 4]` rather than one array against one slice * three `type` aliases for the census maps and the captured-quad tuple * `&PathBuf` -> `&Path` in two disc tests * two range loops; one of them keeps `#[allow(needless_range_loop)]` with the reason -- the index is into a map's value, which changes each iteration * the module doc list in `invert_capture` re-indented to markdown's rules * `blit`'s eight arguments get `#[allow(too_many_arguments)]`, not a struct One dead `let off = b.len();` in a `ratc` test is dropped rather than renamed. The sibling test at :162 is the one that asserts an offset; if this one was meant to as well, that is a test change and not a lint fix. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
206 lines
7.9 KiB
Rust
206 lines
7.9 KiB
Rust
//! Refutation check on `sylpheed-port`'s kind census: *"Every sprite decoration
|
|
//! on both screens is `0x0` — `ptframe1`…`ptframe4` included — and every button
|
|
//! is `0x3002`."*
|
|
//!
|
|
//! Their exporter decodes the field independently; this reads it from the other
|
|
//! side. The check is deliberately WIDER than their claim in two ways, because a
|
|
//! census that only looks where the claim looks cannot fail:
|
|
//!
|
|
//! * it covers every element, not only `.t32` sprites, so a decoration with an
|
|
//! unexpected kind cannot hide behind the word "sprite";
|
|
//! * it covers every build of `GP_TITLE`, not the two screens they checked, so
|
|
//! the claim's *reach* gets tested and not just its instances.
|
|
//!
|
|
//! It also cross-checks `kind` against the focus/nav index at `+0x2C`, which is
|
|
//! `-1` on anything that cannot take the cursor. That turns a census into a
|
|
//! decode: if the two fields agree everywhere, the bit that separates them is
|
|
//! identified rather than guessed. Run over EVERY UI pak on the disc, not just
|
|
//! `GP_TITLE`, so the claim is disc-wide.
|
|
//!
|
|
//! cargo run -p sylpheed-formats --example kind_census_five_screens
|
|
use std::collections::BTreeMap;
|
|
use std::path::PathBuf;
|
|
use sylpheed_formats::{pak::PakArchive, ui_layout};
|
|
|
|
fn suffix(n: &str) -> &str {
|
|
match n.rfind('.') {
|
|
Some(i) => &n[i..],
|
|
None => "(none)",
|
|
}
|
|
}
|
|
|
|
fn main() {
|
|
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");
|
|
let n = ar.entries().len();
|
|
// kind -> suffix -> count, and the exceptions we care about by name
|
|
let mut table: BTreeMap<u32, BTreeMap<String, usize>> = BTreeMap::new();
|
|
let mut t32_nonzero: Vec<(usize, String, u32)> = Vec::new();
|
|
let mut btn_nonstd: Vec<(usize, String, u32)> = Vec::new();
|
|
let mut builds = 0usize;
|
|
for e in 0..n {
|
|
let by = match ar.read(&ar.entries()[e]) {
|
|
Ok(b) => b,
|
|
Err(_) => continue,
|
|
};
|
|
let b = match ui_layout::parse_build(&by) {
|
|
Some(b) => b,
|
|
None => continue,
|
|
};
|
|
builds += 1;
|
|
for el in &b.elements {
|
|
*table
|
|
.entry(el.kind)
|
|
.or_default()
|
|
.entry(suffix(&el.name).to_string())
|
|
.or_default() += 1;
|
|
if el.name.ends_with(".t32") && !el.name.contains("btn") && el.kind != 0 {
|
|
t32_nonzero.push((e, el.name.clone(), el.kind));
|
|
}
|
|
if el.name.contains("btn") && el.kind != 0x3002 {
|
|
btn_nonstd.push((e, el.name.clone(), el.kind));
|
|
}
|
|
}
|
|
}
|
|
println!("GP_TITLE: {builds} parseable builds of {n} entries\n");
|
|
println!("{:<10} elements by file suffix", "kind");
|
|
for (k, m) in &table {
|
|
let s: Vec<String> = m.iter().map(|(sfx, c)| format!("{sfx}x{c}")).collect();
|
|
println!("0x{k:<8X} {}", s.join(" "));
|
|
}
|
|
println!(
|
|
"\nNON-BUTTON .t32 elements with kind != 0: {}",
|
|
t32_nonzero.len()
|
|
);
|
|
for (e, nm, k) in t32_nonzero.iter().take(30) {
|
|
println!(" entry {e:>2} {nm:<24} kind 0x{k:X}");
|
|
}
|
|
println!("\n*btn* elements with kind != 0x3002: {}", btn_nonstd.len());
|
|
for (e, nm, k) in btn_nonstd.iter().take(30) {
|
|
println!(" entry {e:>2} {nm:<24} kind 0x{k:X}");
|
|
}
|
|
|
|
// Is `kind` a bitfield, and does bit 0x2 mean "focusable"? The focus/nav
|
|
// index at +0x2C is -1 on everything that cannot take the cursor, so the two
|
|
// fields cross-check each other. Printed rather than asserted: this is the
|
|
// evidence for the reading, not the reading itself.
|
|
println!("\nkind vs the focus index at +0x2C (-1 = not focusable), GP_TITLE:");
|
|
// +0x2C is not in `Element`, so it is read straight out of the 60-byte
|
|
// declaration entry: table at 0x20, 0x14 = count, entry stride 60.
|
|
const AT: usize = 0x20;
|
|
const STRIDE: usize = 60;
|
|
let mut cross: BTreeMap<(u32, i32), usize> = BTreeMap::new();
|
|
for e in 0..n {
|
|
let by = match ar.read(&ar.entries()[e]) {
|
|
Ok(b) => b,
|
|
Err(_) => continue,
|
|
};
|
|
if ui_layout::parse_build(&by).is_none() {
|
|
continue;
|
|
}
|
|
if by.len() < 0x18 {
|
|
continue;
|
|
}
|
|
let count = u32::from_be_bytes([by[0x14], by[0x15], by[0x16], by[0x17]]) as usize;
|
|
for i in 0..count {
|
|
let at = AT + i * STRIDE;
|
|
if at + STRIDE > by.len() {
|
|
break;
|
|
}
|
|
let kind =
|
|
u32::from_be_bytes([by[at + 0x28], by[at + 0x29], by[at + 0x2A], by[at + 0x2B]]);
|
|
let foc =
|
|
i32::from_be_bytes([by[at + 0x2C], by[at + 0x2D], by[at + 0x2E], by[at + 0x2F]]);
|
|
*cross
|
|
.entry((kind, if foc < 0 { -1 } else { 1 }))
|
|
.or_default() += 1;
|
|
}
|
|
}
|
|
for ((k, f), c) in &cross {
|
|
println!(
|
|
" kind 0x{k:<6X} focus {:<10} {c:>4} elements",
|
|
if *f < 0 { "= -1" } else { ">= 0" }
|
|
);
|
|
}
|
|
|
|
// ── the same test, every UI pak on the disc ──────────────────────────────
|
|
let mut all: BTreeMap<(u32, i32), usize> = BTreeMap::new();
|
|
let mut paks = 0usize;
|
|
let mut violations: Vec<String> = Vec::new();
|
|
let mut dir: Vec<_> = std::fs::read_dir(root.join("dat"))
|
|
.expect("dat")
|
|
.filter_map(|d| d.ok())
|
|
.map(|d| d.path())
|
|
.collect();
|
|
dir.sort();
|
|
for path in dir {
|
|
let name = path.file_name().unwrap().to_string_lossy().to_string();
|
|
if !name.ends_with(".pak") {
|
|
continue;
|
|
}
|
|
let ar = match PakArchive::open(&path) {
|
|
Ok(a) => a,
|
|
Err(_) => continue,
|
|
};
|
|
let mut used = false;
|
|
for i in 0..ar.entries().len() {
|
|
let by = match ar.read(&ar.entries()[i]) {
|
|
Ok(b) => b,
|
|
Err(_) => continue,
|
|
};
|
|
if ui_layout::parse_build(&by).is_none() {
|
|
continue;
|
|
}
|
|
if by.len() < 0x18 {
|
|
continue;
|
|
}
|
|
used = true;
|
|
let count = u32::from_be_bytes([by[0x14], by[0x15], by[0x16], by[0x17]]) as usize;
|
|
for e in 0..count {
|
|
let at = AT + e * STRIDE;
|
|
if at + STRIDE > by.len() {
|
|
break;
|
|
}
|
|
let kind = u32::from_be_bytes([
|
|
by[at + 0x28],
|
|
by[at + 0x29],
|
|
by[at + 0x2A],
|
|
by[at + 0x2B],
|
|
]);
|
|
let foc = i32::from_be_bytes([
|
|
by[at + 0x2C],
|
|
by[at + 0x2D],
|
|
by[at + 0x2E],
|
|
by[at + 0x2F],
|
|
]);
|
|
let f = if foc < 0 { -1 } else { 1 };
|
|
*all.entry((kind, f)).or_default() += 1;
|
|
if ((kind & 0x2) != 0) != (f > 0) {
|
|
violations.push(format!(
|
|
"{name} entry {i} elem {e}: kind 0x{kind:X} focus {foc}"
|
|
));
|
|
}
|
|
}
|
|
}
|
|
if used {
|
|
paks += 1
|
|
}
|
|
}
|
|
let total: usize = all.values().sum();
|
|
println!("\nDISC-WIDE — {paks} UI paks, {total} declaration entries");
|
|
println!("{:<12} {:>12} {:>12}", "kind", "focus = -1", "focus >= 0");
|
|
let kinds: std::collections::BTreeSet<u32> = all.keys().map(|(k, _)| *k).collect();
|
|
for k in kinds {
|
|
println!(
|
|
"0x{k:<10X} {:>12} {:>12}",
|
|
all.get(&(k, -1)).copied().unwrap_or(0),
|
|
all.get(&(k, 1)).copied().unwrap_or(0)
|
|
);
|
|
}
|
|
println!("\nHYPOTHESIS: bit 0x2 of kind == (focus index >= 0)");
|
|
println!("violations: {} of {total}", violations.len());
|
|
for v in violations.iter().take(20) {
|
|
println!(" {v}");
|
|
}
|
|
}
|