Files
Sylpheed/crates/sylpheed-xexdb/tests/db_schema_golden.rs
MechaCat02 c9dd2cb705 fix(xexdb): clear the lint gate on the imported crates
rustfmt, then clippy -D warnings across the three new crates. Mechanical,
except three decisions that are stated rather than silently allowed:

  * lzx.rs gets file-scoped needless_range_loop/explicit_counter_loop allows.
    Index arithmetic IS the algorithm -- LZX is defined over symbol indices,
    Huffman slots and window positions, and a decompressor that is merely
    idiomatic is worth nothing if it is not bit-exact.
  * sylpheed-xexdb gets crate-scoped allows for needless_range_loop (nine
    sites index reg[r] where r is the PowerPC register number -- the index is
    the meaning), too_many_arguments and type_complexity. This code arrived
    whole from a retired repository; a refactor here would be an unreviewed
    edit dressed as a lint fix.
  * Everything else clippy asked for is FIXED, including all 14 doc-indent
    sites, the let-else, and a Prepared type alias in the binary.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-13 20:25:44 +02:00

569 lines
17 KiB
Rust

//! DB schema golden — locks the column layout (names + types) of every
//! table written by `DbWriter`. A schema change here without a fixture
//! update fails the test, forcing a conscious decision before downstream
//! query consumers break.
//!
//! The fixture is constructed in-process (no XEX/ISO needed): a small
//! synthetic PE-shaped byte slice with one `.text` section of 4
//! instructions, plus an empty import-library list and one detected
//! function.
use std::collections::{BTreeMap, BTreeSet, HashMap};
use std::io::Write;
use duckdb::Connection;
use sylpheed_xex::pe::PeSection;
use sylpheed_xexdb::DbWriter;
use sylpheed_xexdb::formatter::DisasmInfo;
use sylpheed_xexdb::func::{FuncAnalysis, FuncInfo};
use sylpheed_xexdb::rtti::RttiResult;
use sylpheed_xexdb::xref::XrefMap;
/// Build a 16-byte `.text` section: 4 instructions (mflr / nop / blr / nop).
fn synthetic_pe() -> (
Vec<u8>,
Vec<PeSection>,
Vec<sylpheed_xex::header::ImportLibrary>,
) {
// VA layout: image_base + 0x1000 = .text start (so RVA = 0x1000).
// The DB writer expects pe[rva] to hold the byte at that RVA, so the
// buffer must be at least 0x1000 + section_size bytes long.
const RVA: usize = 0x1000;
const TEXT: [u32; 4] = [
// mfspr r12, LR (a.k.a. mflr r12) — opcode 31, xo 339, spr 8 (LR).
// Encoded with spr halves swapped per the ISA: spr_field = (8<<5).
(31u32 << 26) | (12 << 21) | ((8 << 5) << 11) | (339 << 1),
0x60000000, // nop (ori r0, r0, 0)
(19u32 << 26) | (20 << 21) | (16 << 1), // blr (bclr 20, 0)
0x60000000, // nop
];
let mut pe = vec![0u8; RVA + 16];
for (i, &word) in TEXT.iter().enumerate() {
pe[RVA + i * 4..RVA + i * 4 + 4].copy_from_slice(&word.to_be_bytes());
}
let sections = vec![PeSection {
name: ".text".to_string(),
virtual_address: 0x1000,
virtual_size: 16,
raw_offset: 0x1000,
raw_size: 16,
flags: 0x60000020, // CODE | EXECUTE | READ
}];
let import_libraries = vec![]; // No imports in the fixture.
(pe, sections, import_libraries)
}
fn synthetic_func_analysis(image_base: u32) -> FuncAnalysis {
// Single function covering all four .text instructions.
let entry = image_base + 0x1000;
let mut functions = BTreeMap::new();
functions.insert(
entry,
FuncInfo {
start: entry,
end: entry + 16,
frame_size: 0,
saved_gprs: 0,
is_leaf: true,
is_saverestore: false,
pdata_validated: false,
pdata_length: None,
pdata_prolog_length: None,
has_eh: false,
},
);
FuncAnalysis {
functions,
save_gpr_base: None,
restore_gpr_base: None,
pdata_entries: Vec::new(),
}
}
#[test]
fn db_schema_matches_expected_columns() {
let (pe, sections, libs) = synthetic_pe();
let image_base = 0x82000000u32;
let entry = image_base + 0x1000;
let info = DisasmInfo {
image_base,
entry_point: entry,
original_pe_name: Some("synthetic.exe"),
title_id: Some(0xDEADBEEF),
media_id: Some(0xCAFEF00D),
sections: &sections,
import_libraries: &libs,
xex_header: None,
};
let func_analysis = synthetic_func_analysis(image_base);
let mut labels: HashMap<u32, String> = HashMap::new();
labels.insert(entry, "entry_point".to_string());
let xrefs: XrefMap = XrefMap::new();
let tmp = std::env::temp_dir().join("sylpheed_xexdb_schema_golden.duckdb");
let _ = std::fs::remove_file(&tmp);
{
let mut w = DbWriter::open_fresh(&tmp).expect("open fresh DB");
w.write_base(&info).expect("write_base");
w.ingest_instructions(&pe, &info, &func_analysis, &labels, &BTreeSet::new())
.expect("ingest_instructions");
w.write_analysis_results(
&pe,
&info,
&func_analysis,
&labels,
&xrefs,
&[],
&[],
&[],
None,
&[],
&[],
&RttiResult::default(),
None,
)
.expect("write_analysis_results");
w.create_sql_views().expect("create_sql_views");
}
let conn = Connection::open(&tmp).expect("reopen DB");
// Lock the column layout per table. Pairs are (name, type).
let expected: &[(&str, &[(&str, &str)])] = &[
("metadata", &[("key", "VARCHAR"), ("value", "VARCHAR")]),
(
"sections",
&[
("name", "VARCHAR"),
("virtual_address", "BIGINT"),
("virtual_size", "BIGINT"),
("raw_offset", "BIGINT"),
("raw_size", "BIGINT"),
("flags", "BIGINT"),
("is_code", "BOOLEAN"),
],
),
(
"imports",
&[
("library", "VARCHAR"),
("ordinal", "BIGINT"),
("name", "VARCHAR"),
("record_type", "BIGINT"),
("address", "BIGINT"),
],
),
(
"instructions",
&[
("address", "BIGINT"),
("raw", "BIGINT"),
("mnemonic", "VARCHAR"),
("operands", "VARCHAR"),
("disasm", "VARCHAR"),
("ext_mnemonic", "VARCHAR"),
("ext_operands", "VARCHAR"),
("ext_disasm", "VARCHAR"),
("target_hex", "BIGINT"),
("section", "VARCHAR"),
("function", "BIGINT"),
("label", "VARCHAR"),
("is_data", "BOOLEAN"),
],
),
(
"functions",
&[
("address", "BIGINT"),
("name", "VARCHAR"),
("end_address", "BIGINT"),
("frame_size", "BIGINT"),
("saved_gprs", "BIGINT"),
("is_leaf", "BOOLEAN"),
("is_saverestore", "BOOLEAN"),
("pdata_validated", "BOOLEAN"),
("pdata_length", "BIGINT"),
("prolog_length", "BIGINT"),
("has_eh", "BOOLEAN"),
],
),
(
"jump_tables",
&[
("bctr_pc", "BIGINT"),
("function", "BIGINT"),
("table_address", "BIGINT"),
("entry_count", "BIGINT"),
("table_slots", "BIGINT"),
("index_map_address", "BIGINT"),
("index_map_count", "BIGINT"),
("case_bound", "BIGINT"),
("kind", "VARCHAR"),
],
),
(
"jump_table_entries",
&[
("bctr_pc", "BIGINT"),
("case_index", "BIGINT"),
("target_address", "BIGINT"),
],
),
(
"data_in_code",
&[
("address", "BIGINT"),
("length", "BIGINT"),
("kind", "VARCHAR"),
],
),
(
"rtti_type_descriptors",
&[
("address", "BIGINT"),
("mangled_name", "VARCHAR"),
("demangled_name", "VARCHAR"),
],
),
(
"rtti_locators",
&[
("address", "BIGINT"),
("subobject_offset", "BIGINT"),
("cd_offset", "BIGINT"),
("type_descriptor", "BIGINT"),
("class_hierarchy", "BIGINT"),
("vtable_address", "BIGINT"),
],
),
(
"rtti_base_classes",
&[
("class_hierarchy", "BIGINT"),
("base_index", "BIGINT"),
("type_descriptor", "BIGINT"),
("name", "VARCHAR"),
("num_contained_bases", "BIGINT"),
("mdisp", "BIGINT"),
("pdisp", "BIGINT"),
("vdisp", "BIGINT"),
("attributes", "BIGINT"),
],
),
(
"pdata_entries",
&[
("begin_address", "BIGINT"),
("end_address", "BIGINT"),
("function_length", "BIGINT"),
("prolog_length", "BIGINT"),
("flags", "BIGINT"),
],
),
(
"labels",
&[
("address", "BIGINT"),
("name", "VARCHAR"),
("kind", "VARCHAR"),
],
),
(
"xdbf_entries",
&[
("namespace", "BIGINT"),
("namespace_name", "VARCHAR"),
("id", "BIGINT"),
("body_offset", "BIGINT"),
("size", "BIGINT"),
("magic", "VARCHAR"),
],
),
(
"xdbf_achievements",
&[
("id", "BIGINT"),
("name", "VARCHAR"),
("unlocked_desc", "VARCHAR"),
("locked_desc", "VARCHAR"),
("label_id", "BIGINT"),
("description_id", "BIGINT"),
("unachieved_id", "BIGINT"),
("image_id", "BIGINT"),
("gamerscore", "BIGINT"),
("flags", "BIGINT"),
],
),
(
"xdbf_strings",
&[
("language", "BIGINT"),
("language_name", "VARCHAR"),
("string_id", "BIGINT"),
("value", "VARCHAR"),
],
),
(
"xdbf_images",
&[
("id", "BIGINT"),
("is_title_icon", "BOOLEAN"),
("body_offset", "BIGINT"),
("size", "BIGINT"),
("format", "VARCHAR"),
],
),
(
"demangled_names",
&[
("address", "BIGINT"),
("mangled", "VARCHAR"),
("raw_demangled", "VARCHAR"),
("namespace_path", "VARCHAR"),
("class_name", "VARCHAR"),
("method_name", "VARCHAR"),
("params_signature", "VARCHAR"),
],
),
(
"vtables",
&[
("address", "BIGINT"),
("length", "BIGINT"),
("col_address", "BIGINT"),
("class_name", "VARCHAR"),
("rtti_present", "BOOLEAN"),
("base_classes_json", "VARCHAR"),
],
),
(
"methods",
&[
("vtable_address", "BIGINT"),
("slot", "BIGINT"),
("function_address", "BIGINT"),
("mangled_name", "VARCHAR"),
("demangled_name", "VARCHAR"),
],
),
(
"classes",
&[
("name", "VARCHAR"),
("vtable_address", "BIGINT"),
("rtti_present", "BOOLEAN"),
("base_classes_json", "VARCHAR"),
],
),
(
"strings",
&[
("address", "BIGINT"),
("encoding", "VARCHAR"),
("length", "BIGINT"),
("content", "VARCHAR"),
("section", "VARCHAR"),
],
),
(
"tls_info",
&[
("raw_data_start", "BIGINT"),
("raw_data_end", "BIGINT"),
("index_address", "BIGINT"),
("callback_array", "BIGINT"),
("zero_fill_size", "BIGINT"),
("characteristics", "BIGINT"),
],
),
(
"tls_callbacks",
&[("slot", "BIGINT"), ("address", "BIGINT")],
),
(
"function_pointer_arrays",
&[
("address", "BIGINT"),
("length", "BIGINT"),
("kind", "VARCHAR"),
],
),
(
"function_pointer_array_entries",
&[
("array_address", "BIGINT"),
("slot", "BIGINT"),
("function_address", "BIGINT"),
],
),
(
"indirect_dispatch_sites",
&[
("dispatch_pc", "BIGINT"),
("vptr_offset", "BIGINT"),
("slot", "BIGINT"),
("candidate_count", "BIGINT"),
("truncated", "BOOLEAN"),
],
),
(
"indirect_dispatch_candidates",
&[
("dispatch_pc", "BIGINT"),
("vtable_address", "BIGINT"),
("method_address", "BIGINT"),
],
),
(
"vptr_writes",
&[
("writer_pc", "BIGINT"),
("vtable_address", "BIGINT"),
("vptr_offset", "BIGINT"),
("writer_function", "BIGINT"),
],
),
(
"eh_funcinfo",
&[
("address", "BIGINT"),
("magic", "BIGINT"),
("max_state", "BIGINT"),
("p_unwind_map", "BIGINT"),
("n_try_blocks", "BIGINT"),
("p_try_block_map", "BIGINT"),
("n_ip_map_entries", "BIGINT"),
("p_ip_to_state_map", "BIGINT"),
("p_es_type_list", "BIGINT"),
("eh_flags", "BIGINT"),
],
),
(
"eh_unwind_map",
&[
("funcinfo_address", "BIGINT"),
("state_index", "BIGINT"),
("to_state", "BIGINT"),
("action_pc", "BIGINT"),
],
),
(
"eh_try_blocks",
&[
("funcinfo_address", "BIGINT"),
("try_index", "BIGINT"),
("try_low", "BIGINT"),
("try_high", "BIGINT"),
("catch_high", "BIGINT"),
("n_catches", "BIGINT"),
("p_handler_array", "BIGINT"),
],
),
(
"xrefs",
&[
("source", "BIGINT"),
("target", "BIGINT"),
("kind", "VARCHAR"),
("addr_mode", "VARCHAR"),
("instruction", "VARCHAR"),
("source_func", "BIGINT"),
("source_label", "VARCHAR"),
("target_label", "VARCHAR"),
],
),
];
let mut errs: Vec<String> = Vec::new();
for (table, cols) in expected {
let mut stmt = conn
.prepare(&format!("PRAGMA table_info('{}')", table))
.unwrap_or_else(|e| panic!("prepare PRAGMA for {table}: {e}"));
let rows: Vec<(String, String)> = stmt
.query_map([], |row| {
let name: String = row.get(1)?;
let ty: String = row.get(2)?;
Ok((name, ty))
})
.expect("query")
.map(|r| r.unwrap())
.collect();
if rows.len() != cols.len() {
writeln!(
std::io::stderr(),
"{table}: column count mismatch (got {}, expected {})",
rows.len(),
cols.len()
)
.ok();
errs.push(format!("{table}: count {} vs {}", rows.len(), cols.len()));
}
for (i, (got, expected_col)) in rows.iter().zip(cols.iter()).enumerate() {
if got.0 != expected_col.0 || got.1 != expected_col.1 {
errs.push(format!(
"{table} col {i}: got ({}, {}) expected ({}, {})",
got.0, got.1, expected_col.0, expected_col.1
));
}
}
}
assert!(
errs.is_empty(),
"schema drift detected:\n {}",
errs.join("\n ")
);
// Verify row counts in the populated tables.
let n_instr: i64 = conn
.query_row("SELECT COUNT(*) FROM instructions", [], |r| r.get(0))
.unwrap();
assert_eq!(
n_instr, 4,
"expected 4 instruction rows from the synthetic PE"
);
// The synthetic mflr should produce target_hex = NULL, blr likewise (indirect).
let n_with_target: i64 = conn
.query_row("SELECT COUNT(target_hex) FROM instructions", [], |r| {
r.get(0)
})
.unwrap();
assert_eq!(
n_with_target, 0,
"indirect-only fixture should have no direct branch targets"
);
// SQL views must be queryable. The `_` in SQL LIKE is a single-char
// wildcard, so we list the names explicitly rather than `LIKE 'v_%'`
// (which also matches DuckDB's built-in `views` system view).
let expected_views = [
"v_branch_xrefs",
"v_call_graph",
"v_function_first_instruction",
"v_imports_called",
"v_indirect_reachability_from_entry",
"v_reachability_from_entry",
];
for v in expected_views {
let exists: i64 = conn
.query_row(
"SELECT COUNT(*) FROM duckdb_views() WHERE view_name = ?",
[v],
|r| r.get(0),
)
.unwrap();
assert_eq!(exists, 1, "missing SQL view: {v}");
}
let _ = std::fs::remove_file(&tmp);
}