feat: add export database and import record decoding (M7)
- New src/exports.rs: embeds doc/xbox360_exports.md via include_str! and lazily parses it into a lookup table (OnceLock). Covers 2,913 exports across xboxkrnl.exe, xam.xex, and xbdm.xex. - New src/imports.rs: decodes import records from extracted PE images by reading the u32 at each import address, extracting record type (variable/thunk) and ordinal, and resolving against the export DB. - inspect command now shows a full resolved imports table with names. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,6 +1,8 @@
|
||||
use xex2tractor::crypto;
|
||||
use xex2tractor::exports;
|
||||
use xex2tractor::extract;
|
||||
use xex2tractor::header::{ModuleFlags, XEX2_MAGIC};
|
||||
use xex2tractor::imports::{self, ImportRecordType};
|
||||
use xex2tractor::optional::{CompressionInfo, CompressionType, EncryptionType, SystemFlags};
|
||||
use xex2tractor::security::{ImageFlags, MediaFlags, RegionFlags};
|
||||
|
||||
@@ -440,3 +442,137 @@ fn test_cli_extract_shows_format_info() {
|
||||
|
||||
let _ = std::fs::remove_file(&output_path);
|
||||
}
|
||||
|
||||
// ── Export database tests ────────────────────────────────────────────────────
|
||||
|
||||
#[test]
|
||||
fn test_export_db_total_count() {
|
||||
assert_eq!(exports::total_count(), 2913);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_export_db_module_counts() {
|
||||
assert_eq!(exports::module_count("xboxkrnl.exe"), 922);
|
||||
assert_eq!(exports::module_count("xam.xex"), 1736);
|
||||
assert_eq!(exports::module_count("xbdm.xex"), 255);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_export_db_lookup_known() {
|
||||
let info = exports::lookup("xboxkrnl.exe", 0x009).unwrap();
|
||||
assert_eq!(info.name, "ExAllocatePool");
|
||||
assert!(info.is_function);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_export_db_lookup_unknown_ordinal() {
|
||||
assert!(exports::lookup("xboxkrnl.exe", 0xFFFF).is_none());
|
||||
}
|
||||
|
||||
// ── Import record decoding tests ─────────────────────────────────────────────
|
||||
|
||||
#[test]
|
||||
fn test_decode_import_records() {
|
||||
let data = sample_data();
|
||||
let xex = xex2tractor::parse(&data).unwrap();
|
||||
let pe_image = extract::extract_pe_image(&data, &xex).unwrap();
|
||||
|
||||
let resolved = imports::decode_import_records(&pe_image, &xex).unwrap();
|
||||
|
||||
// Sample has 104 xam.xex + 294 xboxkrnl.exe = 398 total import records
|
||||
assert_eq!(resolved.len(), 398);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_decode_import_record_types() {
|
||||
let data = sample_data();
|
||||
let xex = xex2tractor::parse(&data).unwrap();
|
||||
let pe_image = extract::extract_pe_image(&data, &xex).unwrap();
|
||||
|
||||
let resolved = imports::decode_import_records(&pe_image, &xex).unwrap();
|
||||
|
||||
let variables = resolved
|
||||
.iter()
|
||||
.filter(|r| r.record.record_type == ImportRecordType::Variable)
|
||||
.count();
|
||||
let thunks = resolved
|
||||
.iter()
|
||||
.filter(|r| r.record.record_type == ImportRecordType::Thunk)
|
||||
.count();
|
||||
|
||||
// Every import should be either variable or thunk
|
||||
assert_eq!(variables + thunks, resolved.len());
|
||||
// Both types should be present
|
||||
assert!(variables > 0);
|
||||
assert!(thunks > 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_decode_import_alternating_pattern() {
|
||||
let data = sample_data();
|
||||
let xex = xex2tractor::parse(&data).unwrap();
|
||||
let pe_image = extract::extract_pe_image(&data, &xex).unwrap();
|
||||
|
||||
let resolved = imports::decode_import_records(&pe_image, &xex).unwrap();
|
||||
|
||||
// Check first few xam.xex entries: variable, thunk, variable, thunk, ...
|
||||
let xam: Vec<_> = resolved.iter().filter(|r| r.library == "xam.xex").collect();
|
||||
assert!(xam.len() >= 4);
|
||||
assert_eq!(xam[0].record.record_type, ImportRecordType::Variable);
|
||||
assert_eq!(xam[1].record.record_type, ImportRecordType::Thunk);
|
||||
assert_eq!(xam[2].record.record_type, ImportRecordType::Variable);
|
||||
assert_eq!(xam[3].record.record_type, ImportRecordType::Thunk);
|
||||
|
||||
// Variable and thunk for same function share the same ordinal
|
||||
assert_eq!(xam[0].record.ordinal, xam[1].record.ordinal);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_decode_import_names_resolved() {
|
||||
let data = sample_data();
|
||||
let xex = xex2tractor::parse(&data).unwrap();
|
||||
let pe_image = extract::extract_pe_image(&data, &xex).unwrap();
|
||||
|
||||
let resolved = imports::decode_import_records(&pe_image, &xex).unwrap();
|
||||
|
||||
// Most imports should resolve to known names
|
||||
let named = resolved.iter().filter(|r| r.export.is_some()).count();
|
||||
assert!(
|
||||
named > resolved.len() / 2,
|
||||
"expected most imports to resolve, got {named}/{}",
|
||||
resolved.len()
|
||||
);
|
||||
|
||||
// Check a specific known import is present
|
||||
let has_dbg_break = resolved.iter().any(|r| {
|
||||
r.export
|
||||
.as_ref()
|
||||
.is_some_and(|e| e.name == "DbgBreakPoint")
|
||||
});
|
||||
assert!(has_dbg_break, "should find DbgBreakPoint import");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_cli_inspect_shows_resolved_imports() {
|
||||
let path = format!("{}/tests/data/default.xex", env!("CARGO_MANIFEST_DIR"));
|
||||
|
||||
let output = std::process::Command::new(env!("CARGO_BIN_EXE_xex2tractor"))
|
||||
.args(["inspect", &path])
|
||||
.output()
|
||||
.expect("failed to run xex2tractor");
|
||||
|
||||
assert!(output.status.success());
|
||||
let stdout = String::from_utf8_lossy(&output.stdout);
|
||||
assert!(
|
||||
stdout.contains("Resolved Imports"),
|
||||
"inspect should show resolved imports section"
|
||||
);
|
||||
assert!(
|
||||
stdout.contains("xboxkrnl.exe"),
|
||||
"inspect should show xboxkrnl.exe imports"
|
||||
);
|
||||
assert!(
|
||||
stdout.contains("xam.xex"),
|
||||
"inspect should show xam.xex imports"
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user