feat: add import resolution with Xenia-style thunk stubs (M8)
- resolve_imports() rewrites PE import records in-place: - Variable slots: 0xD000BEEF | (ordinal & 0xFFF) << 16 - Thunk stubs: li r3, 0 / li r4, <ordinal> / mtspr CTR, r11 / bctr - New -r/--resolve-imports flag on the extract command - Without the flag, extraction is unchanged (byte-for-byte identical) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -552,6 +552,113 @@ fn test_decode_import_names_resolved() {
|
||||
assert!(has_dbg_break, "should find DbgBreakPoint import");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_resolve_imports_variables() {
|
||||
let data = sample_data();
|
||||
let xex = xex2tractor::parse(&data).unwrap();
|
||||
let mut pe_image = extract::extract_pe_image(&data, &xex).unwrap();
|
||||
|
||||
let summary = imports::resolve_imports(&mut pe_image, &xex).unwrap();
|
||||
assert!(summary.variables_written > 0);
|
||||
assert!(summary.thunks_written > 0);
|
||||
assert_eq!(
|
||||
summary.total,
|
||||
summary.variables_written + summary.thunks_written
|
||||
);
|
||||
|
||||
// Check first xam.xex variable at PE offset 0x600 (ordinal 0x028C)
|
||||
// Should be 0xD000BEEF | (0x28C & 0xFFF) << 16 = 0xD28CBEEF
|
||||
let val = u32::from_be_bytes([
|
||||
pe_image[0x600],
|
||||
pe_image[0x601],
|
||||
pe_image[0x602],
|
||||
pe_image[0x603],
|
||||
]);
|
||||
assert_eq!(val, 0xD28CBEEF, "variable slot should have 0xD000BEEF pattern");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_resolve_imports_thunks() {
|
||||
let data = sample_data();
|
||||
let xex = xex2tractor::parse(&data).unwrap();
|
||||
let mut pe_image = extract::extract_pe_image(&data, &xex).unwrap();
|
||||
|
||||
imports::resolve_imports(&mut pe_image, &xex).unwrap();
|
||||
|
||||
// Check first xam.xex thunk at PE offset 0x84DA7C (ordinal 0x028C)
|
||||
let off = 0x0084DA7C;
|
||||
let w0 = u32::from_be_bytes([pe_image[off], pe_image[off + 1], pe_image[off + 2], pe_image[off + 3]]);
|
||||
let w1 = u32::from_be_bytes([pe_image[off + 4], pe_image[off + 5], pe_image[off + 6], pe_image[off + 7]]);
|
||||
let w2 = u32::from_be_bytes([pe_image[off + 8], pe_image[off + 9], pe_image[off + 10], pe_image[off + 11]]);
|
||||
let w3 = u32::from_be_bytes([pe_image[off + 12], pe_image[off + 13], pe_image[off + 14], pe_image[off + 15]]);
|
||||
|
||||
assert_eq!(w0, 0x38600000, "thunk word 0 should be li r3, 0");
|
||||
assert_eq!(w1, 0x3880028C, "thunk word 1 should be li r4, 0x028C");
|
||||
assert_eq!(w2, 0x7D6903A6, "thunk word 2 should be mtspr CTR, r11");
|
||||
assert_eq!(w3, 0x4E800420, "thunk word 3 should be bctr");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_extract_without_resolve_unchanged() {
|
||||
let data = sample_data();
|
||||
let xex = xex2tractor::parse(&data).unwrap();
|
||||
let pe_unresolved = extract::extract_pe_image(&data, &xex).unwrap();
|
||||
|
||||
// Without resolve, variable at 0x600 should be the raw descriptor
|
||||
let val = u32::from_be_bytes([
|
||||
pe_unresolved[0x600],
|
||||
pe_unresolved[0x601],
|
||||
pe_unresolved[0x602],
|
||||
pe_unresolved[0x603],
|
||||
]);
|
||||
assert_eq!(val, 0x0000028C, "unresolved variable should be raw descriptor");
|
||||
|
||||
// Thunk word 0 should be the record marker, not a PPC instruction
|
||||
let off = 0x0084DA7C;
|
||||
let w0 = u32::from_be_bytes([
|
||||
pe_unresolved[off],
|
||||
pe_unresolved[off + 1],
|
||||
pe_unresolved[off + 2],
|
||||
pe_unresolved[off + 3],
|
||||
]);
|
||||
assert_eq!(w0, 0x0100028C, "unresolved thunk word 0 should be record marker");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_resolve_imports_pe_still_valid() {
|
||||
let data = sample_data();
|
||||
let xex = xex2tractor::parse(&data).unwrap();
|
||||
let mut pe_image = extract::extract_pe_image(&data, &xex).unwrap();
|
||||
|
||||
imports::resolve_imports(&mut pe_image, &xex).unwrap();
|
||||
|
||||
// PE verification should still pass (MZ + PE headers untouched)
|
||||
assert!(extract::verify_pe_image(&pe_image).is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_cli_extract_resolve_imports() {
|
||||
let path = format!("{}/tests/data/default.xex", env!("CARGO_MANIFEST_DIR"));
|
||||
let output_path = format!(
|
||||
"{}/target/test_resolve_output.exe",
|
||||
env!("CARGO_MANIFEST_DIR")
|
||||
);
|
||||
let _ = std::fs::remove_file(&output_path);
|
||||
|
||||
let output = std::process::Command::new(env!("CARGO_BIN_EXE_xex2tractor"))
|
||||
.args(["extract", "-r", &path, &output_path])
|
||||
.output()
|
||||
.expect("failed to run xex2tractor");
|
||||
|
||||
assert!(output.status.success(), "CLI extract -r should succeed");
|
||||
let stdout = String::from_utf8_lossy(&output.stdout);
|
||||
assert!(stdout.contains("Resolved"), "should print resolution summary");
|
||||
assert!(stdout.contains("variables"), "should mention variables");
|
||||
assert!(stdout.contains("thunks"), "should mention thunks");
|
||||
|
||||
let _ = std::fs::remove_file(&output_path);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_cli_inspect_shows_resolved_imports() {
|
||||
let path = format!("{}/tests/data/default.xex", env!("CARGO_MANIFEST_DIR"));
|
||||
|
||||
Reference in New Issue
Block a user