7 Commits

Author SHA1 Message Date
MechaCat02
b6ee119824 Merge feature/m8-resolve-imports: Xenia-style import resolution 2026-03-29 17:36:03 +02:00
MechaCat02
2425e8177e 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>
2026-03-29 17:35:57 +02:00
MechaCat02
956f5e8ba8 Merge feature/m7-import-decode: export database and import record decoding 2026-03-29 17:32:54 +02:00
MechaCat02
0d42fc1b06 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>
2026-03-29 17:32:49 +02:00
MechaCat02
dee636c09f docs: add Xbox 360 system exports reference
Auto-generated from Xenia Canary source. Covers 2,913 exports
across xboxkrnl.exe, xam.xex, and xbdm.xex with ordinals, names,
types, and implementation status.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-29 17:26:43 +02:00
MechaCat02
4ed8fadd4c feat: show decrypted session key in inspect output
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-28 22:24:23 +01:00
MechaCat02
ee5e0b60f8 fix: include Cargo.lock changes from v0.6.0 version bump
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-28 22:00:13 +01:00
9 changed files with 3721 additions and 5 deletions

2
Cargo.lock generated
View File

@@ -288,7 +288,7 @@ dependencies = [
[[package]]
name = "xex2tractor"
version = "0.5.0"
version = "0.8.0"
dependencies = [
"aes",
"cbc",

View File

@@ -1,6 +1,6 @@
[package]
name = "xex2tractor"
version = "0.6.0"
version = "0.8.0"
edition = "2024"
description = "A tool for extracting and inspecting Xbox 360 XEX2 executable files"
license = "MIT"

2947
doc/xbox360_exports.md Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -1,5 +1,7 @@
/// Pretty-print formatting for parsed XEX2 structures.
use crate::crypto;
use crate::header::Xex2Header;
use crate::imports::ResolvedImport;
use crate::optional::{
format_hex_bytes, format_rating, format_timestamp, CompressionInfo, HeaderKey, OptionalHeaders,
};
@@ -266,6 +268,11 @@ pub fn display_security_info(security: &SecurityInfo) {
"AES Key (encrypted): {}",
format_hex_bytes(&security.aes_key)
);
let session_key = crypto::derive_session_key(&security.aes_key);
println!(
"AES Key (decrypted): {}",
format_hex_bytes(&session_key)
);
if security.export_table == 0 {
println!("Export Table: 0x00000000 (none)");
@@ -308,3 +315,44 @@ pub fn display_security_info(security: &SecurityInfo) {
" Total mapped size: 0x{address_offset:X} ({address_offset} bytes)"
);
}
/// Prints resolved import records grouped by library.
pub fn display_resolved_imports(imports: &[ResolvedImport]) {
// Count per library
let mut lib_counts: Vec<(String, usize)> = Vec::new();
for imp in imports {
if let Some(entry) = lib_counts.iter_mut().find(|(name, _)| *name == imp.library) {
entry.1 += 1;
} else {
lib_counts.push((imp.library.clone(), 1));
}
}
let summary: Vec<String> = lib_counts
.iter()
.map(|(name, count)| format!("{name}: {count}"))
.collect();
println!();
println!(
"=== Resolved Imports ({}) ===",
summary.join(", ")
);
let mut current_lib = "";
for imp in imports {
if imp.library != current_lib {
println!();
println!(" [{}]", imp.library);
current_lib = &imp.library;
}
let name = imp
.export
.as_ref()
.map_or("<unknown>", |e| &e.name);
println!(
" 0x{:08X} {:>8} ordinal 0x{:04X} {}",
imp.record.address, imp.record.record_type, imp.record.ordinal, name
);
}
}

194
src/exports.rs Normal file
View File

@@ -0,0 +1,194 @@
/// Xbox 360 system export database.
///
/// Parses the embedded `doc/xbox360_exports.md` at first access and provides
/// ordinal-to-name lookups for xboxkrnl.exe, xam.xex, and xbdm.xex.
use std::collections::HashMap;
use std::sync::OnceLock;
static EXPORT_DB: OnceLock<ExportDatabase> = OnceLock::new();
const EXPORTS_MD: &str = include_str!("../doc/xbox360_exports.md");
/// Information about a single Xbox 360 system export.
#[derive(Debug, Clone)]
pub struct ExportInfo {
pub ordinal: u16,
pub name: String,
pub is_function: bool,
}
/// Lookup table mapping `(library_filename, ordinal)` to export info.
struct ExportDatabase {
/// Key: lowercase library filename (e.g. "xboxkrnl.exe"), value: ordinal -> ExportInfo
modules: HashMap<String, HashMap<u16, ExportInfo>>,
}
/// Returns the total number of exports in the database.
pub fn total_count() -> usize {
let db = get_db();
db.modules.values().map(|m| m.len()).sum()
}
/// Returns the number of exports for a given library.
pub fn module_count(library: &str) -> usize {
let db = get_db();
db.modules
.get(&library.to_ascii_lowercase())
.map_or(0, |m| m.len())
}
/// Looks up an export by library filename and ordinal.
pub fn lookup(library: &str, ordinal: u16) -> Option<&'static ExportInfo> {
let db = get_db();
db.modules
.get(&library.to_ascii_lowercase())?
.get(&ordinal)
}
fn get_db() -> &'static ExportDatabase {
EXPORT_DB.get_or_init(|| parse_exports_md(EXPORTS_MD))
}
/// Parses the markdown export database into a lookup structure.
///
/// Expected format per module section:
/// ```text
/// ## module_name (filename.exe)
/// ...
/// | 0xNNN | FunctionName | function | status | ... |
/// ```
fn parse_exports_md(md: &str) -> ExportDatabase {
let mut modules: HashMap<String, HashMap<u16, ExportInfo>> = HashMap::new();
let mut current_file: Option<String> = None;
for line in md.lines() {
// Detect section headers: "## xboxkrnl (xboxkrnl.exe)"
if let Some(rest) = line.strip_prefix("## ") {
if let (Some(paren_start), Some(paren_end)) =
(rest.find('('), rest.find(')'))
{
let filename = rest[paren_start + 1..paren_end].trim().to_ascii_lowercase();
current_file = Some(filename);
} else {
current_file = None;
}
continue;
}
// Parse table rows: "| 0xNNN | Name | function/variable | ... |"
let Some(ref file) = current_file else {
continue;
};
if !line.starts_with("| 0x") {
continue;
}
let cols: Vec<&str> = line.split('|').collect();
// cols[0] is empty (before first |), cols[1] = ordinal, cols[2] = name, cols[3] = type
if cols.len() < 4 {
continue;
}
let ordinal_str = cols[1].trim();
let name = cols[2].trim();
let type_str = cols[3].trim();
let Some(ordinal) = parse_hex_ordinal(ordinal_str) else {
continue;
};
let entry = ExportInfo {
ordinal,
name: name.to_string(),
is_function: type_str == "function",
};
modules
.entry(file.clone())
.or_default()
.insert(ordinal, entry);
}
ExportDatabase { modules }
}
fn parse_hex_ordinal(s: &str) -> Option<u16> {
let hex = s.strip_prefix("0x").or_else(|| s.strip_prefix("0X"))?;
u16::from_str_radix(hex, 16).ok()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_total_count() {
assert_eq!(total_count(), 2913);
}
#[test]
fn test_module_counts() {
assert_eq!(module_count("xboxkrnl.exe"), 922);
assert_eq!(module_count("xam.xex"), 1736);
assert_eq!(module_count("xbdm.xex"), 255);
}
#[test]
fn test_module_count_case_insensitive() {
assert_eq!(module_count("XBOXKRNL.EXE"), 922);
assert_eq!(module_count("Xam.Xex"), 1736);
}
#[test]
fn test_module_count_unknown() {
assert_eq!(module_count("unknown.dll"), 0);
}
#[test]
fn test_lookup_xboxkrnl_known() {
let info = lookup("xboxkrnl.exe", 0x009).unwrap();
assert_eq!(info.name, "ExAllocatePool");
assert!(info.is_function);
assert_eq!(info.ordinal, 0x009);
}
#[test]
fn test_lookup_xboxkrnl_variable() {
let info = lookup("xboxkrnl.exe", 0x00C).unwrap();
assert_eq!(info.name, "ExConsoleGameRegion");
assert!(!info.is_function);
}
#[test]
fn test_lookup_xam_known() {
let info = lookup("xam.xex", 0x001).unwrap();
assert_eq!(info.name, "NetDll_WSAStartup");
assert!(info.is_function);
}
#[test]
fn test_lookup_xbdm_known() {
let info = lookup("xbdm.xex", 0x001).unwrap();
assert_eq!(info.name, "DmAllocatePool");
assert!(info.is_function);
}
#[test]
fn test_lookup_unknown_ordinal() {
assert!(lookup("xboxkrnl.exe", 0xFFFF).is_none());
}
#[test]
fn test_lookup_unknown_library() {
assert!(lookup("nonexistent.dll", 0x001).is_none());
}
#[test]
fn test_parse_hex_ordinal() {
assert_eq!(parse_hex_ordinal("0x001"), Some(1));
assert_eq!(parse_hex_ordinal("0x3A3"), Some(0x3A3));
assert_eq!(parse_hex_ordinal("0xFFFF"), Some(0xFFFF));
assert_eq!(parse_hex_ordinal("invalid"), None);
assert_eq!(parse_hex_ordinal(""), None);
}
}

248
src/imports.rs Normal file
View File

@@ -0,0 +1,248 @@
/// Import record decoding and resolution for XEX2 PE images.
///
/// Reads import records from the extracted PE image, decodes their type and
/// ordinal, and resolves them against the Xbox 360 export database.
use std::fmt;
use crate::error::{Result, Xex2Error};
use crate::exports::{self, ExportInfo};
use crate::util::read_u32_be;
use crate::Xex2File;
/// The type of an import record.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ImportRecordType {
/// A 4-byte variable slot (record_type 0x00).
Variable,
/// A 16-byte function thunk stub (record_type 0x01).
Thunk,
}
impl fmt::Display for ImportRecordType {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
ImportRecordType::Variable => write!(f, "variable"),
ImportRecordType::Thunk => write!(f, "thunk"),
}
}
}
/// A decoded import record from the PE image.
#[derive(Debug, Clone)]
pub struct ImportRecord {
/// The original memory address from the XEX2 import table.
pub address: u32,
/// Offset within the PE image (address - load_address).
pub pe_offset: usize,
/// Whether this is a variable slot or function thunk.
pub record_type: ImportRecordType,
/// The ordinal number identifying the imported function/variable.
pub ordinal: u16,
}
/// An import record resolved against the export database.
#[derive(Debug, Clone)]
pub struct ResolvedImport {
/// The decoded import record.
pub record: ImportRecord,
/// The library filename (e.g. "xboxkrnl.exe").
pub library: String,
/// The export info if the ordinal was found in the database.
pub export: Option<ExportInfo>,
}
/// Decodes all import records from the PE image and resolves them against
/// the export database.
///
/// Iterates over each import library's addresses, reads the u32 value at
/// the corresponding PE offset, extracts record_type and ordinal, then
/// looks up the ordinal in the export database.
pub fn decode_import_records(pe_image: &[u8], xex: &Xex2File) -> Result<Vec<ResolvedImport>> {
let imports = xex
.optional_headers
.import_libraries
.as_ref()
.ok_or_else(|| Xex2Error::InvalidPeImage("no import libraries header".into()))?;
let load_address = xex.security_info.load_address;
let mut resolved = Vec::new();
for lib in &imports.libraries {
for &addr in &lib.import_addresses {
let pe_offset = (addr.wrapping_sub(load_address)) as usize;
let raw = read_u32_be(pe_image, pe_offset).map_err(|_| {
Xex2Error::InvalidPeImage(format!(
"import address 0x{addr:08X} (offset 0x{pe_offset:08X}) out of bounds"
))
})?;
let record_type_byte = (raw >> 24) & 0xFF;
let ordinal = (raw & 0xFFFF) as u16;
let record_type = match record_type_byte {
0x00 => ImportRecordType::Variable,
0x01 => ImportRecordType::Thunk,
_ => {
// Unknown record types — treat as variable (best effort)
ImportRecordType::Variable
}
};
let record = ImportRecord {
address: addr,
pe_offset,
record_type,
ordinal,
};
let export = exports::lookup(&lib.name, ordinal).cloned();
resolved.push(ResolvedImport {
record,
library: lib.name.clone(),
export,
});
}
}
Ok(resolved)
}
/// Summary of import resolution results.
#[derive(Debug)]
pub struct ImportResolutionSummary {
/// Total number of import records processed.
pub total: usize,
/// Number of variable slots written.
pub variables_written: usize,
/// Number of thunk stubs written.
pub thunks_written: usize,
/// Per-library breakdown: (library_name, count).
pub per_library: Vec<(String, usize)>,
}
impl fmt::Display for ImportResolutionSummary {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"Resolved {} imports ({} variables, {} thunks)",
self.total, self.variables_written, self.thunks_written
)?;
for (lib, count) in &self.per_library {
write!(f, "\n {lib}: {count}")?;
}
Ok(())
}
}
/// Resolves imports in the PE image by writing Xenia-style values.
///
/// For variable imports (type 0): writes `0xD000BEEF | (ordinal & 0xFFF) << 16`.
/// For thunk imports (type 1): rewrites the first 8 bytes to valid PowerPC:
/// - word 0: `li r3, 0` (0x38600000)
/// - word 1: `li r4, <ordinal>` (0x38800000 | ordinal)
/// - words 2-3 are left unchanged (already mtspr CTR, r11 + bctr)
pub fn resolve_imports(
pe_image: &mut [u8],
xex: &Xex2File,
) -> Result<ImportResolutionSummary> {
let imports = xex
.optional_headers
.import_libraries
.as_ref()
.ok_or_else(|| Xex2Error::InvalidPeImage("no import libraries header".into()))?;
let load_address = xex.security_info.load_address;
let mut total = 0;
let mut variables_written = 0;
let mut thunks_written = 0;
let mut per_library: Vec<(String, usize)> = Vec::new();
for lib in &imports.libraries {
let mut lib_count = 0;
for &addr in &lib.import_addresses {
let pe_offset = (addr.wrapping_sub(load_address)) as usize;
let raw = read_u32_be(pe_image, pe_offset).map_err(|_| {
Xex2Error::InvalidPeImage(format!(
"import address 0x{addr:08X} (offset 0x{pe_offset:08X}) out of bounds"
))
})?;
let record_type_byte = (raw >> 24) & 0xFF;
let ordinal = (raw & 0xFFFF) as u16;
match record_type_byte {
0x00 => {
// Variable: write 0xD000BEEF | (ordinal & 0xFFF) << 16
let resolved_val: u32 =
0xD000BEEF | ((ordinal as u32 & 0xFFF) << 16);
pe_image[pe_offset..pe_offset + 4]
.copy_from_slice(&resolved_val.to_be_bytes());
variables_written += 1;
}
0x01 => {
// Thunk: rewrite first 8 bytes to valid PPC
if pe_offset + 16 > pe_image.len() {
return Err(Xex2Error::InvalidPeImage(format!(
"thunk at 0x{addr:08X} (offset 0x{pe_offset:08X}) extends past PE image"
)));
}
// li r3, 0
let word0: u32 = 0x38600000;
// li r4, <ordinal>
let word1: u32 = 0x38800000 | ordinal as u32;
pe_image[pe_offset..pe_offset + 4]
.copy_from_slice(&word0.to_be_bytes());
pe_image[pe_offset + 4..pe_offset + 8]
.copy_from_slice(&word1.to_be_bytes());
// words 2-3 (mtspr CTR, r11 + bctr) left unchanged
thunks_written += 1;
}
_ => {
// Unknown record type — skip
}
}
lib_count += 1;
total += 1;
}
per_library.push((lib.name.clone(), lib_count));
}
Ok(ImportResolutionSummary {
total,
variables_written,
thunks_written,
per_library,
})
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_import_record_type_display() {
assert_eq!(ImportRecordType::Variable.to_string(), "variable");
assert_eq!(ImportRecordType::Thunk.to_string(), "thunk");
}
#[test]
fn test_resolution_summary_display() {
let summary = ImportResolutionSummary {
total: 10,
variables_written: 5,
thunks_written: 5,
per_library: vec![("xboxkrnl.exe".into(), 10)],
};
let s = summary.to_string();
assert!(s.contains("10 imports"));
assert!(s.contains("5 variables"));
assert!(s.contains("5 thunks"));
assert!(s.contains("xboxkrnl.exe: 10"));
}
}

View File

@@ -10,8 +10,10 @@ pub mod crypto;
pub mod decompress;
pub mod display;
pub mod error;
pub mod exports;
pub mod extract;
pub mod header;
pub mod imports;
pub mod optional;
pub mod security;
pub mod util;

View File

@@ -23,6 +23,9 @@ enum Command {
file: PathBuf,
/// Output path for the extracted PE file (default: same name with .exe extension)
output: Option<PathBuf>,
/// Resolve imports by writing Xenia-style thunk stubs and variable slots
#[arg(short = 'r', long = "resolve-imports")]
resolve_imports: bool,
},
}
@@ -31,7 +34,11 @@ fn main() {
match cli.command {
Command::Inspect { file } => cmd_inspect(&file),
Command::Extract { file, output } => cmd_extract(&file, output),
Command::Extract {
file,
output,
resolve_imports,
} => cmd_extract(&file, output, resolve_imports),
}
}
@@ -42,9 +49,26 @@ fn cmd_inspect(path: &PathBuf) {
xex2tractor::display::display_header(&xex.header);
xex2tractor::display::display_optional_headers(&xex.optional_headers);
xex2tractor::display::display_security_info(&xex.security_info);
// Decode and display resolved imports (requires extracting the PE image)
if xex.optional_headers.import_libraries.is_some() {
match xex2tractor::extract::extract_pe_image(&data, &xex) {
Ok(pe_image) => match xex2tractor::imports::decode_import_records(&pe_image, &xex) {
Ok(resolved) => {
xex2tractor::display::display_resolved_imports(&resolved);
}
Err(e) => {
eprintln!("Warning: could not decode imports: {e}");
}
},
Err(e) => {
eprintln!("Warning: could not extract PE for import resolution: {e}");
}
}
}
}
fn cmd_extract(path: &PathBuf, output: Option<PathBuf>) {
fn cmd_extract(path: &PathBuf, output: Option<PathBuf>, resolve_imports: bool) {
let output_path = output.unwrap_or_else(|| path.with_extension("exe"));
let data = read_file(path);
@@ -56,7 +80,7 @@ fn cmd_extract(path: &PathBuf, output: Option<PathBuf>) {
println!("Compression: {}", fmt.compression_type);
}
let pe_image = match xex2tractor::extract::extract_pe_image(&data, &xex) {
let mut pe_image = match xex2tractor::extract::extract_pe_image(&data, &xex) {
Ok(img) => img,
Err(e) => {
eprintln!("Error extracting PE image: {e}");
@@ -64,6 +88,16 @@ fn cmd_extract(path: &PathBuf, output: Option<PathBuf>) {
}
};
if resolve_imports {
match xex2tractor::imports::resolve_imports(&mut pe_image, &xex) {
Ok(summary) => println!("{summary}"),
Err(e) => {
eprintln!("Error resolving imports: {e}");
process::exit(1);
}
}
}
if let Err(e) = std::fs::write(&output_path, &pe_image) {
eprintln!("Error writing {}: {e}", output_path.display());
process::exit(1);

View File

@@ -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,244 @@ 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_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"));
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"
);
}