xenia-analysis: unify disasm via xenia-cpu, split ingest/analyze, add sinks
The old src/ppc.rs that re-implemented PPC formatting collapses into a 30-line shim that delegates to xenia-cpu's single-source-of-truth disasm. A new disasm.rs wraps the shared iterator and feeds enriched items (analysis context: function membership, xrefs, mnemonics) into pluggable sinks. Sinks split: text.rs (objdump-like output), json.rs (JSONL stream matching the new xenia dis --json mode), duckdb.rs (the analysis DB ingest). db.rs is restructured into ingest_instructions + write_analysis_results so a run can stop after raw ingest, and a new target_hex column lands on the instructions table. sql_views.rs adds five additive views layered on top of the raw tables. Tests: assert-based JSON-fixture goldens (disasm_goldens) and a PRAGMA-table_info schema golden (db_schema_golden) covering all ingested tables and the SQL views. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
37
crates/xenia-analysis/src/sinks/duckdb.rs
Normal file
37
crates/xenia-analysis/src/sinks/duckdb.rs
Normal file
@@ -0,0 +1,37 @@
|
||||
//! DuckDB sink — appends rich disasm items to the `instructions` table.
|
||||
//!
|
||||
//! Column layout matches [`crate::db`]: address, raw, mnemonic, operands,
|
||||
//! disasm, ext_mnemonic, ext_operands, ext_disasm, section, function, label.
|
||||
|
||||
use duckdb::{Appender, params};
|
||||
|
||||
use crate::disasm::RichDisasmItem;
|
||||
|
||||
/// Append every item to the appender. Returns the number of rows written.
|
||||
/// Does NOT flush — the caller decides when to flush, since multiple
|
||||
/// section iterators typically share one appender.
|
||||
pub fn append_instructions<'a>(
|
||||
appender: &mut Appender<'_>,
|
||||
items: impl IntoIterator<Item = RichDisasmItem<'a>>,
|
||||
) -> duckdb::Result<u64> {
|
||||
let mut count: u64 = 0;
|
||||
for ri in items {
|
||||
let t = &ri.item.text;
|
||||
appender.append_row(params![
|
||||
ri.item.addr as i64,
|
||||
ri.item.raw as i64,
|
||||
t.mnemonic.as_str(),
|
||||
t.operands.as_str(),
|
||||
t.disasm.as_str(),
|
||||
t.ext_mnemonic.as_deref(),
|
||||
t.ext_operands.as_deref(),
|
||||
t.ext_disasm.as_deref(),
|
||||
t.branch_target.map(|t| t as i64),
|
||||
ri.section,
|
||||
ri.function.map(|f| f as i64),
|
||||
ri.label,
|
||||
])?;
|
||||
count += 1;
|
||||
}
|
||||
Ok(count)
|
||||
}
|
||||
63
crates/xenia-analysis/src/sinks/json.rs
Normal file
63
crates/xenia-analysis/src/sinks/json.rs
Normal file
@@ -0,0 +1,63 @@
|
||||
//! JSON Lines sink — one structured row per line, constant memory.
|
||||
//!
|
||||
//! Suited for piping into `jq`, importing into pandas / DuckDB's
|
||||
//! `read_json_auto`, or feeding downstream tooling that expects a
|
||||
//! line-delimited stream rather than a single megaobject.
|
||||
|
||||
use std::io::{self, Write};
|
||||
|
||||
use serde::Serialize;
|
||||
|
||||
use crate::disasm::RichDisasmItem;
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct JsonRow<'a> {
|
||||
addr: u32,
|
||||
raw: u32,
|
||||
mnemonic: &'a str,
|
||||
operands: &'a str,
|
||||
disasm: &'a str,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
ext_mnemonic: Option<&'a str>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
ext_operands: Option<&'a str>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
ext_disasm: Option<&'a str>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
branch_target: Option<u32>,
|
||||
section: &'a str,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
function: Option<u32>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
label: Option<&'a str>,
|
||||
}
|
||||
|
||||
/// Write each item as a single JSON object on its own line. Returns the
|
||||
/// number of rows written.
|
||||
pub fn write_jsonl<'a, W: Write>(
|
||||
out: &mut W,
|
||||
items: impl IntoIterator<Item = RichDisasmItem<'a>>,
|
||||
) -> io::Result<u64> {
|
||||
let mut count: u64 = 0;
|
||||
for ri in items {
|
||||
let t = &ri.item.text;
|
||||
let row = JsonRow {
|
||||
addr: ri.item.addr,
|
||||
raw: ri.item.raw,
|
||||
mnemonic: &t.mnemonic,
|
||||
operands: &t.operands,
|
||||
disasm: &t.disasm,
|
||||
ext_mnemonic: t.ext_mnemonic.as_deref(),
|
||||
ext_operands: t.ext_operands.as_deref(),
|
||||
ext_disasm: t.ext_disasm.as_deref(),
|
||||
branch_target: t.branch_target,
|
||||
section: ri.section,
|
||||
function: ri.function,
|
||||
label: ri.label,
|
||||
};
|
||||
serde_json::to_writer(&mut *out, &row)?;
|
||||
out.write_all(b"\n")?;
|
||||
count += 1;
|
||||
}
|
||||
Ok(count)
|
||||
}
|
||||
8
crates/xenia-analysis/src/sinks/mod.rs
Normal file
8
crates/xenia-analysis/src/sinks/mod.rs
Normal file
@@ -0,0 +1,8 @@
|
||||
//! Output sinks for [`crate::disasm::RichDisasmItem`] streams.
|
||||
//!
|
||||
//! Each sink consumes the same iterator shape and writes to a different
|
||||
//! medium: human-readable .asm text, JSON Lines, or DuckDB rows.
|
||||
|
||||
pub mod duckdb;
|
||||
pub mod json;
|
||||
pub mod text;
|
||||
58
crates/xenia-analysis/src/sinks/text.rs
Normal file
58
crates/xenia-analysis/src/sinks/text.rs
Normal file
@@ -0,0 +1,58 @@
|
||||
//! Text sink — renders one .asm instruction line with optional
|
||||
//! branch-target / data-ref annotations.
|
||||
//!
|
||||
//! The full `write_asm` orchestration (section headers, function prologue
|
||||
//! info, xref comment blocks, hex-dump of data sections) stays in
|
||||
//! [`crate::formatter`]; this sink only owns the per-instruction line.
|
||||
|
||||
use std::collections::HashMap;
|
||||
use std::io::{self, Write};
|
||||
|
||||
use xenia_xex::pe::PeSection;
|
||||
|
||||
use crate::disasm::RichDisasmItem;
|
||||
use crate::xref::{XrefKind, section_for_addr};
|
||||
|
||||
/// Render one instruction line:
|
||||
/// ` 82000000: 60000000 nop`
|
||||
/// ` 82000004: 4800FFFC bl 0x82000000 ; -> entry_point`
|
||||
/// ` 82000010: 812A0000 lwz r9, 0(r10) ; [R] 0x828A0000 (.rdata) = dat_…`
|
||||
pub fn write_instr_line<W: Write + ?Sized>(
|
||||
out: &mut W,
|
||||
item: &RichDisasmItem<'_>,
|
||||
labels: &HashMap<u32, String>,
|
||||
sections: &[PeSection],
|
||||
image_base: u32,
|
||||
data_annotation: Option<(u32, XrefKind)>,
|
||||
) -> io::Result<()> {
|
||||
let disasm_text = item.item.text.display();
|
||||
|
||||
// Branch-target → label annotation. Uses the structured `branch_target`
|
||||
// field (cleaner than the legacy "find 0x in disasm string" regex).
|
||||
let mut annotated = match item.item.text.branch_target {
|
||||
Some(target) => match labels.get(&target) {
|
||||
Some(lbl) => format!("{disasm_text:<40} ; -> {lbl}"),
|
||||
None => disasm_text.to_string(),
|
||||
},
|
||||
None => disasm_text.to_string(),
|
||||
};
|
||||
|
||||
if let Some((data_addr, kind)) = data_annotation {
|
||||
let tag = match kind {
|
||||
XrefKind::DataRead => "[R]",
|
||||
XrefKind::DataWrite => "[W]",
|
||||
_ => "[&]",
|
||||
};
|
||||
let sec = section_for_addr(data_addr, sections, image_base).unwrap_or("?");
|
||||
let data_lbl = labels.get(&data_addr)
|
||||
.map(|s| format!(" = {s}"))
|
||||
.unwrap_or_default();
|
||||
if !annotated.contains("; ->") {
|
||||
annotated = format!("{annotated:<40} ; {tag} 0x{data_addr:08X} ({sec}){data_lbl}");
|
||||
} else {
|
||||
annotated = format!("{annotated} {tag} 0x{data_addr:08X} ({sec}){data_lbl}");
|
||||
}
|
||||
}
|
||||
|
||||
writeln!(out, " {:08X}: {:08X} {}", item.item.addr, item.item.raw, annotated)
|
||||
}
|
||||
Reference in New Issue
Block a user