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:
2
Cargo.lock
generated
2
Cargo.lock
generated
@@ -288,7 +288,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "xex2tractor"
|
name = "xex2tractor"
|
||||||
version = "0.6.1"
|
version = "0.7.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"aes",
|
"aes",
|
||||||
"cbc",
|
"cbc",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "xex2tractor"
|
name = "xex2tractor"
|
||||||
version = "0.6.1"
|
version = "0.7.0"
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
description = "A tool for extracting and inspecting Xbox 360 XEX2 executable files"
|
description = "A tool for extracting and inspecting Xbox 360 XEX2 executable files"
|
||||||
license = "MIT"
|
license = "MIT"
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
/// Pretty-print formatting for parsed XEX2 structures.
|
/// Pretty-print formatting for parsed XEX2 structures.
|
||||||
use crate::crypto;
|
use crate::crypto;
|
||||||
use crate::header::Xex2Header;
|
use crate::header::Xex2Header;
|
||||||
|
use crate::imports::ResolvedImport;
|
||||||
use crate::optional::{
|
use crate::optional::{
|
||||||
format_hex_bytes, format_rating, format_timestamp, CompressionInfo, HeaderKey, OptionalHeaders,
|
format_hex_bytes, format_rating, format_timestamp, CompressionInfo, HeaderKey, OptionalHeaders,
|
||||||
};
|
};
|
||||||
@@ -314,3 +315,44 @@ pub fn display_security_info(security: &SecurityInfo) {
|
|||||||
" Total mapped size: 0x{address_offset:X} ({address_offset} bytes)"
|
" 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
194
src/exports.rs
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
121
src/imports.rs
Normal file
121
src/imports.rs
Normal file
@@ -0,0 +1,121 @@
|
|||||||
|
/// 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)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[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");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -10,8 +10,10 @@ pub mod crypto;
|
|||||||
pub mod decompress;
|
pub mod decompress;
|
||||||
pub mod display;
|
pub mod display;
|
||||||
pub mod error;
|
pub mod error;
|
||||||
|
pub mod exports;
|
||||||
pub mod extract;
|
pub mod extract;
|
||||||
pub mod header;
|
pub mod header;
|
||||||
|
pub mod imports;
|
||||||
pub mod optional;
|
pub mod optional;
|
||||||
pub mod security;
|
pub mod security;
|
||||||
pub mod util;
|
pub mod util;
|
||||||
|
|||||||
17
src/main.rs
17
src/main.rs
@@ -42,6 +42,23 @@ fn cmd_inspect(path: &PathBuf) {
|
|||||||
xex2tractor::display::display_header(&xex.header);
|
xex2tractor::display::display_header(&xex.header);
|
||||||
xex2tractor::display::display_optional_headers(&xex.optional_headers);
|
xex2tractor::display::display_optional_headers(&xex.optional_headers);
|
||||||
xex2tractor::display::display_security_info(&xex.security_info);
|
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>) {
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
use xex2tractor::crypto;
|
use xex2tractor::crypto;
|
||||||
|
use xex2tractor::exports;
|
||||||
use xex2tractor::extract;
|
use xex2tractor::extract;
|
||||||
use xex2tractor::header::{ModuleFlags, XEX2_MAGIC};
|
use xex2tractor::header::{ModuleFlags, XEX2_MAGIC};
|
||||||
|
use xex2tractor::imports::{self, ImportRecordType};
|
||||||
use xex2tractor::optional::{CompressionInfo, CompressionType, EncryptionType, SystemFlags};
|
use xex2tractor::optional::{CompressionInfo, CompressionType, EncryptionType, SystemFlags};
|
||||||
use xex2tractor::security::{ImageFlags, MediaFlags, RegionFlags};
|
use xex2tractor::security::{ImageFlags, MediaFlags, RegionFlags};
|
||||||
|
|
||||||
@@ -440,3 +442,137 @@ fn test_cli_extract_shows_format_info() {
|
|||||||
|
|
||||||
let _ = std::fs::remove_file(&output_path);
|
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