Files
Sylpheed/crates/sylpheed-xexdb/src/sql_views.rs
Fabian Hamm 13c895abff feat(xexdb): port import-thunk naming + schema-wide foreign keys
Carries `xenia-rs` `harvest/import-thunk-naming` (b4f19f1) across into the
lifted crate. That work was found UNCOMMITTED in the retired repo's working
tree on 2026-09-14 and exists nowhere else: absent from `iterate-4A`, absent
from this crate as lifted. CONSOLIDATION.md Phase 5 ends with "delete the local
clone" and Phase 7 drops the emulator, so it had a deletion scheduled against
it.

Not a cherry-pick. The lift's base is exactly the harvest's parent (8401d4d),
so each file was merged three-way -- lifted vs base vs harvest -- which is what
made the port reviewable: 10 conflicts, 9 of them pure rustfmt reflow from the
lift's formatting pass, and 1 semantic.

The semantic one is insertion ORDER. `xdbf_achievements.image_id` is now a
foreign key onto `xdbf_images(id)`, so the image rows must be inserted before
the achievement rows. The merge moved that block; the conflict was the stale
copy left at the old position.

What arrives:

* **Import-thunk recognition** (`imports.rs`, 338 lines). An XEX import is not
  a PLT jump: the linker emits a four-word thunk whose first two words are
  import RECORDS that the loader rewrites at module load. On disc they are
  still records, so a PowerPC-only decoder prints two meaningless `.long`s in
  front of an indirect branch. This maps every word of every thunk, and every
  direct branch into one, back to its `imports` row. Shape-validated rather
  than trusted: an entry is indexed only when the four words it points at
  actually have the thunk shape.

  Adds `instructions.import_address` (FK onto `imports.address`) and
  `import_role` (`'record'` | `'thunk'` | `'call'`, NULL iff import_address is
  NULL), plus an index. `tools/zq.py` gains `imp` and `impcalls`, and `dis`
  now names imports instead of printing `.long 0x01010194`.

* **Sixteen schema-wide foreign keys**, declared wherever a column is derived
  from another table. CREATE TABLE and insertion order become load-bearing.

  `functions` is deliberately NOT an FK parent and the golden test now asserts
  zero inbound FKs onto it: DuckDB implements UPDATE as delete+insert, so one
  inbound FK would make `functions.name` un-updatable and break
  `apply_re_symbols.sql`, which re-applies RE symbol names after every
  regeneration.

The database path needed no change in the binary: `DbWriter` builds its own
index inside `ingest_instructions` from `info.import_libraries`. Only the two
output paths (`enrich_section` for JSONL, `write_asm`) take it as an argument.

Verified: `cargo test -p sylpheed-xexdb` = 10 passed / 0 failed, including
`db_schema_golden` (41s, builds a real DuckDB) which locks the 16-FK set and
the no-FK-onto-functions rule. `cargo clippy -p sylpheed-xexdb --all-targets
-- -D warnings` clean; `cargo fmt --all --check` clean.

Stacked on #32 -- it ports into a crate that only exists there.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-14 20:43:16 +02:00

302 lines
12 KiB
Rust

//! Additive SQL views over the Phase-3 ingest tables.
//!
//! These views are created when `--analyze=sql` or `--analyze=both` is set.
//! They are *not* a replacement for the Rust passes ([`crate::xref`],
//! [`crate::func`]) — those still own data-ref resolution and prologue
//! pattern matching. The views cover the cleanly-relational parts:
//!
//! - branch xrefs (self-join on `instructions.target_hex`)
//! - call graph + reachability (recursive CTE over `xrefs`)
//! - convenience joins (function-first-instruction, imports-called)
//!
//! All views are read-only and stable across re-creation: dropping and
//! recreating the database via [`crate::db::DbWriter::open_fresh`] re-runs
//! these definitions.
//!
//! ## Cross-check semantics
//!
//! `v_branch_xrefs` is intended to produce *exactly* the same `(source,
//! target, kind)` tuples as the Rust `xref.rs` first pass — given the same
//! input image. [`crate::db::DbWriter::cross_check_branch_xrefs`] queries
//! the symmetric difference and returns the row counts; both should be
//! zero. A non-zero count means the formatter's `mnemonic` column or the
//! kind-classification CASE drifted out of agreement with `xref.rs`, and
//! is worth a one-line warning at log time.
/// Every XDBF string side-by-side across the languages the title ships, so a
/// piece of UI text can be looked up once and read in all locales.
const V_XDBF_TEXT: &str = "
CREATE OR REPLACE VIEW v_xdbf_text AS
SELECT
s.string_id,
MAX(CASE WHEN s.language = 1 THEN s.value END) AS english,
MAX(CASE WHEN s.language = 2 THEN s.value END) AS japanese,
MAX(CASE WHEN s.language = 3 THEN s.value END) AS german,
MAX(CASE WHEN s.language = 4 THEN s.value END) AS french,
MAX(CASE WHEN s.language = 5 THEN s.value END) AS spanish,
MAX(CASE WHEN s.language = 6 THEN s.value END) AS italian
FROM xdbf_strings s
GROUP BY s.string_id;
";
/// Achievements joined to their three strings in every shipped language.
const V_XDBF_ACHIEVEMENTS: &str = "
CREATE OR REPLACE VIEW v_xdbf_achievements AS
SELECT
a.id,
a.gamerscore,
s.language,
s.language_name,
n.value AS name,
u.value AS unlocked_desc,
l.value AS locked_desc,
a.image_id
FROM xdbf_achievements a
JOIN (SELECT DISTINCT language, language_name FROM xdbf_strings) s ON TRUE
LEFT JOIN xdbf_strings n ON n.language = s.language AND n.string_id = a.label_id
LEFT JOIN xdbf_strings u ON u.language = s.language AND u.string_id = a.description_id
LEFT JOIN xdbf_strings l ON l.language = s.language AND l.string_id = a.unachieved_id;
";
/// `(view_name, CREATE VIEW … SQL)` pairs in the order they must run.
/// Later views may depend on earlier ones (e.g. `v_call_graph` reads
/// `xrefs`, which is the Rust-pass table; `v_branch_xrefs` is independent).
pub const ALL_VIEWS: &[(&str, &str)] = &[
("v_branch_xrefs", V_BRANCH_XREFS),
("v_call_graph", V_CALL_GRAPH),
("v_reachability_from_entry", V_REACHABILITY_FROM_ENTRY),
(
"v_indirect_reachability_from_entry",
V_INDIRECT_REACHABILITY_FROM_ENTRY,
),
("v_function_first_instruction", V_FUNCTION_FIRST_INSTRUCTION),
("v_imports_called", V_IMPORTS_CALLED),
("v_xdbf_text", V_XDBF_TEXT),
("v_xdbf_achievements", V_XDBF_ACHIEVEMENTS),
("v_switch_cases", V_SWITCH_CASES),
("v_class_hierarchy", V_CLASS_HIERARCHY),
("v_class_methods", V_CLASS_METHODS),
("v_function_strings", V_FUNCTION_STRINGS),
];
/// Branch cross-references derived purely from `instructions.target_hex`.
///
/// Mirrors the kind classification in [`crate::xref::collect_branch_target`]
/// and the short tags returned by [`crate::xref::XrefKind::tag`] (which are
/// what `xrefs.kind` actually stores):
/// - I-form (`b`/`bl`/`ba`/`bla`): `bl`/`bla` → `"call"`, `b`/`ba` → `"j"`
/// - B-form (`bc`/`bcl`/`bca`/`bcla`): always → `"br"`
///
/// Indirect branches (`bclr`/`bcctr`) leave `target_hex` NULL and are
/// excluded from this view by design.
const V_BRANCH_XREFS: &str = "
CREATE OR REPLACE VIEW v_branch_xrefs AS
SELECT
address AS source,
target_hex AS target,
CASE
WHEN mnemonic IN ('bl', 'bla') THEN 'call'
WHEN mnemonic IN ('b', 'ba') THEN 'j'
WHEN mnemonic IN ('bc', 'bcl', 'bca', 'bcla') THEN 'br'
ELSE 'br'
END AS kind,
mnemonic AS instruction,
function AS source_func
FROM instructions
WHERE target_hex IS NOT NULL;
";
/// Call-graph edges resolved against function names.
///
/// Reads from `xrefs` (the Rust-pass table) — this is the canonical source
/// for *all* edge kinds, including indirect/data; SQL can't reconstruct the
/// data-ref edges cleanly because they require register tracking. For pure
/// branch edges, `v_branch_xrefs` produces equivalent rows directly from
/// `instructions`.
const V_CALL_GRAPH: &str = "
CREATE OR REPLACE VIEW v_call_graph AS
SELECT
x.source AS caller_addr,
cf.name AS caller_name,
x.target AS callee_addr,
tf.name AS callee_name,
x.kind AS edge_kind
FROM xrefs x
LEFT JOIN functions cf ON cf.address = x.source_func
LEFT JOIN functions tf ON tf.address = x.target
WHERE x.kind = 'call';
";
/// Transitive function-level reachability from the entry point over
/// call/jump/branch edges. Useful for finding dead code
/// (`SELECT address FROM functions
/// WHERE address NOT IN (SELECT addr FROM v_reachability_from_entry)`)
/// and for scoping analysis to the live subset.
///
/// Seeds from the function containing the `entry_point` label and walks
/// the recursive closure: a reachable function's instructions branch into
/// the functions enclosing the branch targets, which are then reachable
/// in turn. `UNION` (not `UNION ALL`) deduplicates to handle call-graph
/// cycles (recursive functions, mutually-recursive pairs).
const V_REACHABILITY_FROM_ENTRY: &str = "
CREATE OR REPLACE VIEW v_reachability_from_entry AS
WITH RECURSIVE reach(fn) AS (
SELECT i.function FROM instructions i
JOIN labels l ON l.address = i.address
WHERE l.name = 'entry_point' AND i.function IS NOT NULL
UNION
SELECT tgt.function FROM xrefs x
JOIN instructions src ON src.address = x.source
JOIN instructions tgt ON tgt.address = x.target
JOIN reach r ON src.function = r.fn
WHERE x.kind IN ('call', 'j', 'br', 'jt')
AND tgt.function IS NOT NULL
)
SELECT fn AS addr FROM reach;
";
/// Reachability extended over `kind='ind_call'` edges from M5. Strict
/// superset of `v_reachability_from_entry` — every fn there is also here,
/// plus any function reached only via a vtable bcctrl whose vtable+slot
/// the M5 dataflow could resolve. Sample 5 newly-reachable PCs in canary
/// before trusting widely; the analysis intentionally leaves out alias-
/// dependent indirect calls (vtable loaded from a `this` field).
const V_INDIRECT_REACHABILITY_FROM_ENTRY: &str = "
CREATE OR REPLACE VIEW v_indirect_reachability_from_entry AS
WITH RECURSIVE reach(fn) AS (
SELECT i.function FROM instructions i
JOIN labels l ON l.address = i.address
WHERE l.name = 'entry_point' AND i.function IS NOT NULL
UNION
SELECT tgt.function FROM xrefs x
JOIN instructions src ON src.address = x.source
JOIN instructions tgt ON tgt.address = x.target
JOIN reach r ON src.function = r.fn
WHERE x.kind IN ('call', 'ind_call', 'j', 'br', 'jt')
AND tgt.function IS NOT NULL
)
SELECT fn AS addr FROM reach;
";
/// Convenience join: each function's first decoded instruction. Useful for
/// quickly inspecting prologue patterns without computing offsets manually.
const V_FUNCTION_FIRST_INSTRUCTION: &str = "
CREATE OR REPLACE VIEW v_function_first_instruction AS
SELECT
f.address AS function_addr,
f.name AS function_name,
i.raw AS first_raw,
i.disasm AS first_disasm,
i.ext_disasm AS first_ext_disasm
FROM functions f
JOIN instructions i ON i.address = f.address;
";
/// Every call site of a kernel/library import, one row per branch.
///
/// Driven by `instructions.import_address` — the foreign key onto `imports`
/// laid down by [`crate::imports::ImportSites`] — rather than by matching
/// `xrefs` against import-kind labels. That change fixes two things:
///
/// - a tail-call `b` into a thunk is a call to that import, but `xrefs` tags
/// it `'j'`, so the label-matching version silently dropped all of them
/// (64 sites on the reference title, against 3,217 `bl`);
/// - the library and ordinal are now available without re-parsing the name,
/// so `WHERE library = 'xboxkrnl.exe'` works directly.
///
/// `call_site` is the branch instruction; `import_addr` is the thunk head.
const V_IMPORTS_CALLED: &str = "
CREATE OR REPLACE VIEW v_imports_called AS
SELECT
i.address AS call_site,
i.function AS function_addr,
f.name AS function_name,
i.mnemonic AS instruction,
i.import_address AS import_addr,
im.library AS library,
im.name AS import_name,
im.ordinal AS ordinal
FROM instructions i
JOIN imports im ON im.address = i.import_address
LEFT JOIN functions f ON f.address = i.function
WHERE i.import_role = 'call';
";
/// Every recovered `switch` case, joined to the dispatching function and to
/// the label on the case body. One row per case *value* — several rows can
/// share a `target_address` when case values fall through to one body.
const V_SWITCH_CASES: &str = "
CREATE OR REPLACE VIEW v_switch_cases AS
SELECT
jt.bctr_pc AS dispatch_pc,
jt.function AS function_addr,
f.name AS function_name,
jt.kind AS table_kind,
jt.table_address AS table_address,
e.case_index AS case_index,
e.target_address AS target_address,
l.name AS target_label
FROM jump_tables jt
JOIN jump_table_entries e ON e.bctr_pc = jt.bctr_pc
LEFT JOIN functions f ON f.address = jt.function
LEFT JOIN labels l ON l.address = e.target_address;
";
/// The C++ inheritance graph as recovered from RTTI. Index 0 of a base-class
/// array is the class itself and is excluded, so every row is a genuine
/// `derived -> base` edge carrying the displacement triple needed to find the
/// base subobject inside an instance.
const V_CLASS_HIERARCHY: &str = "
CREATE OR REPLACE VIEW v_class_hierarchy AS
SELECT DISTINCT
dtd.demangled_name AS derived_class,
b.name AS base_class,
b.base_index AS base_index,
b.mdisp AS mdisp,
b.pdisp AS pdisp,
b.vdisp AS vdisp,
c.vtable_address AS derived_vtable
FROM rtti_base_classes b
JOIN rtti_locators c ON c.class_hierarchy = b.class_hierarchy
JOIN rtti_type_descriptors dtd ON dtd.address = c.type_descriptor
WHERE b.base_index > 0;
";
/// Virtual methods per class, resolved through the RTTI-named vtable. The
/// authoritative counterpart to querying `methods` by an `ANON_Class_*` name.
const V_CLASS_METHODS: &str = "
CREATE OR REPLACE VIEW v_class_methods AS
SELECT
td.demangled_name AS class_name,
c.subobject_offset AS subobject_offset,
v.address AS vtable_address,
m.slot AS slot,
m.function_address AS method_addr,
f.name AS method_name,
f.has_eh AS method_has_eh
FROM rtti_locators c
JOIN rtti_type_descriptors td ON td.address = c.type_descriptor
JOIN vtables v ON v.address = c.vtable_address
JOIN methods m ON m.vtable_address = v.address
LEFT JOIN functions f ON f.address = m.function_address;
";
/// Which function references which string literal. The single most useful
/// orientation query in a stripped binary: it is how you find the code behind
/// a message you can see on screen.
const V_FUNCTION_STRINGS: &str = "
CREATE OR REPLACE VIEW v_function_strings AS
SELECT
x.source_func AS function_addr,
f.name AS function_name,
x.source AS reference_pc,
x.kind AS reference_kind,
s.address AS string_addr,
s.encoding AS encoding,
s.content AS content
FROM xrefs x
JOIN strings s ON s.address = x.target
LEFT JOIN functions f ON f.address = x.source_func
WHERE x.kind IN ('ref', 'read');
";