Compare commits
3 Commits
956f5e8ba8
...
v0.8.1
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a951027aeb | ||
|
|
b6ee119824 | ||
|
|
2425e8177e |
65
Cargo.lock
generated
65
Cargo.lock
generated
@@ -194,6 +194,12 @@ version = "1.70.2"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695"
|
checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "itoa"
|
||||||
|
version = "1.0.18"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "libc"
|
name = "libc"
|
||||||
version = "0.2.183"
|
version = "0.2.183"
|
||||||
@@ -206,6 +212,12 @@ version = "0.2.6"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "7b29dffab797218e12e4df08ef5d15ab9efca2504038b1b32b9b32fc844b39c9"
|
checksum = "7b29dffab797218e12e4df08ef5d15ab9efca2504038b1b32b9b32fc844b39c9"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "memchr"
|
||||||
|
version = "2.8.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "once_cell_polyfill"
|
name = "once_cell_polyfill"
|
||||||
version = "1.70.2"
|
version = "1.70.2"
|
||||||
@@ -230,6 +242,49 @@ dependencies = [
|
|||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "serde"
|
||||||
|
version = "1.0.228"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e"
|
||||||
|
dependencies = [
|
||||||
|
"serde_core",
|
||||||
|
"serde_derive",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "serde_core"
|
||||||
|
version = "1.0.228"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad"
|
||||||
|
dependencies = [
|
||||||
|
"serde_derive",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "serde_derive"
|
||||||
|
version = "1.0.228"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "serde_json"
|
||||||
|
version = "1.0.149"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86"
|
||||||
|
dependencies = [
|
||||||
|
"itoa",
|
||||||
|
"memchr",
|
||||||
|
"serde",
|
||||||
|
"serde_core",
|
||||||
|
"zmij",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "strsim"
|
name = "strsim"
|
||||||
version = "0.11.1"
|
version = "0.11.1"
|
||||||
@@ -288,10 +343,18 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "xex2tractor"
|
name = "xex2tractor"
|
||||||
version = "0.7.0"
|
version = "0.8.1"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"aes",
|
"aes",
|
||||||
"cbc",
|
"cbc",
|
||||||
"clap",
|
"clap",
|
||||||
"lzxd",
|
"lzxd",
|
||||||
|
"serde",
|
||||||
|
"serde_json",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "zmij"
|
||||||
|
version = "1.0.21"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa"
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "xex2tractor"
|
name = "xex2tractor"
|
||||||
version = "0.7.0"
|
version = "0.8.1"
|
||||||
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"
|
||||||
@@ -10,3 +10,5 @@ aes = "0.8.4"
|
|||||||
cbc = "0.1.2"
|
cbc = "0.1.2"
|
||||||
clap = { version = "4.6.0", features = ["derive"] }
|
clap = { version = "4.6.0", features = ["derive"] }
|
||||||
lzxd = "0.2.6"
|
lzxd = "0.2.6"
|
||||||
|
serde = { version = "1", features = ["derive"] }
|
||||||
|
serde_json = "1"
|
||||||
|
|||||||
33562
doc/xbox360_exports.json
Normal file
33562
doc/xbox360_exports.json
Normal file
File diff suppressed because it is too large
Load Diff
110
src/exports.rs
110
src/exports.rs
@@ -1,13 +1,15 @@
|
|||||||
/// Xbox 360 system export database.
|
/// Xbox 360 system export database.
|
||||||
///
|
///
|
||||||
/// Parses the embedded `doc/xbox360_exports.md` at first access and provides
|
/// Parses the embedded `doc/xbox360_exports.json` at first access and provides
|
||||||
/// ordinal-to-name lookups for xboxkrnl.exe, xam.xex, and xbdm.xex.
|
/// ordinal-to-name lookups for xboxkrnl.exe, xam.xex, and xbdm.xex.
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use std::sync::OnceLock;
|
use std::sync::OnceLock;
|
||||||
|
|
||||||
|
use serde::Deserialize;
|
||||||
|
|
||||||
static EXPORT_DB: OnceLock<ExportDatabase> = OnceLock::new();
|
static EXPORT_DB: OnceLock<ExportDatabase> = OnceLock::new();
|
||||||
|
|
||||||
const EXPORTS_MD: &str = include_str!("../doc/xbox360_exports.md");
|
const EXPORTS_JSON: &str = include_str!("../doc/xbox360_exports.json");
|
||||||
|
|
||||||
/// Information about a single Xbox 360 system export.
|
/// Information about a single Xbox 360 system export.
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
@@ -46,77 +48,52 @@ pub fn lookup(library: &str, ordinal: u16) -> Option<&'static ExportInfo> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn get_db() -> &'static ExportDatabase {
|
fn get_db() -> &'static ExportDatabase {
|
||||||
EXPORT_DB.get_or_init(|| parse_exports_md(EXPORTS_MD))
|
EXPORT_DB.get_or_init(|| parse_exports_json(EXPORTS_JSON))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Parses the markdown export database into a lookup structure.
|
#[derive(Deserialize)]
|
||||||
///
|
struct JsonRoot {
|
||||||
/// Expected format per module section:
|
modules: HashMap<String, JsonModule>,
|
||||||
/// ```text
|
}
|
||||||
/// ## module_name (filename.exe)
|
|
||||||
/// ...
|
#[derive(Deserialize)]
|
||||||
/// | 0xNNN | FunctionName | function | status | ... |
|
struct JsonModule {
|
||||||
/// ```
|
file: String,
|
||||||
fn parse_exports_md(md: &str) -> ExportDatabase {
|
exports: Vec<JsonExport>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
struct JsonExport {
|
||||||
|
ordinal: u32,
|
||||||
|
name: String,
|
||||||
|
#[serde(rename = "type")]
|
||||||
|
export_type: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_exports_json(json: &str) -> ExportDatabase {
|
||||||
|
let root: JsonRoot = serde_json::from_str(json).expect("invalid exports JSON");
|
||||||
let mut modules: HashMap<String, HashMap<u16, ExportInfo>> = HashMap::new();
|
let mut modules: HashMap<String, HashMap<u16, ExportInfo>> = HashMap::new();
|
||||||
let mut current_file: Option<String> = None;
|
|
||||||
|
|
||||||
for line in md.lines() {
|
for module in root.modules.values() {
|
||||||
// Detect section headers: "## xboxkrnl (xboxkrnl.exe)"
|
let file_key = module.file.to_ascii_lowercase();
|
||||||
if let Some(rest) = line.strip_prefix("## ") {
|
let entries = modules.entry(file_key).or_default();
|
||||||
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 | ... |"
|
for export in &module.exports {
|
||||||
let Some(ref file) = current_file else {
|
let ordinal = export.ordinal as u16;
|
||||||
continue;
|
entries.insert(
|
||||||
};
|
|
||||||
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,
|
ordinal,
|
||||||
name: name.to_string(),
|
ExportInfo {
|
||||||
is_function: type_str == "function",
|
ordinal,
|
||||||
};
|
name: export.name.clone(),
|
||||||
|
is_function: export.export_type == "function",
|
||||||
modules
|
},
|
||||||
.entry(file.clone())
|
);
|
||||||
.or_default()
|
}
|
||||||
.insert(ordinal, entry);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
ExportDatabase { modules }
|
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)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
@@ -182,13 +159,4 @@ mod tests {
|
|||||||
fn test_lookup_unknown_library() {
|
fn test_lookup_unknown_library() {
|
||||||
assert!(lookup("nonexistent.dll", 0x001).is_none());
|
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);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
127
src/imports.rs
127
src/imports.rs
@@ -109,6 +109,118 @@ pub fn decode_import_records(pe_image: &[u8], xex: &Xex2File) -> Result<Vec<Reso
|
|||||||
Ok(resolved)
|
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)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
@@ -118,4 +230,19 @@ mod tests {
|
|||||||
assert_eq!(ImportRecordType::Variable.to_string(), "variable");
|
assert_eq!(ImportRecordType::Variable.to_string(), "variable");
|
||||||
assert_eq!(ImportRecordType::Thunk.to_string(), "thunk");
|
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"));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
23
src/main.rs
23
src/main.rs
@@ -23,6 +23,9 @@ enum Command {
|
|||||||
file: PathBuf,
|
file: PathBuf,
|
||||||
/// Output path for the extracted PE file (default: same name with .exe extension)
|
/// Output path for the extracted PE file (default: same name with .exe extension)
|
||||||
output: Option<PathBuf>,
|
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 {
|
match cli.command {
|
||||||
Command::Inspect { file } => cmd_inspect(&file),
|
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),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -61,7 +68,7 @@ fn cmd_inspect(path: &PathBuf) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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 output_path = output.unwrap_or_else(|| path.with_extension("exe"));
|
||||||
|
|
||||||
let data = read_file(path);
|
let data = read_file(path);
|
||||||
@@ -73,7 +80,7 @@ fn cmd_extract(path: &PathBuf, output: Option<PathBuf>) {
|
|||||||
println!("Compression: {}", fmt.compression_type);
|
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,
|
Ok(img) => img,
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
eprintln!("Error extracting PE image: {e}");
|
eprintln!("Error extracting PE image: {e}");
|
||||||
@@ -81,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) {
|
if let Err(e) = std::fs::write(&output_path, &pe_image) {
|
||||||
eprintln!("Error writing {}: {e}", output_path.display());
|
eprintln!("Error writing {}: {e}", output_path.display());
|
||||||
process::exit(1);
|
process::exit(1);
|
||||||
|
|||||||
@@ -552,6 +552,113 @@ fn test_decode_import_names_resolved() {
|
|||||||
assert!(has_dbg_break, "should find DbgBreakPoint import");
|
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]
|
#[test]
|
||||||
fn test_cli_inspect_shows_resolved_imports() {
|
fn test_cli_inspect_shows_resolved_imports() {
|
||||||
let path = format!("{}/tests/data/default.xex", env!("CARGO_MANIFEST_DIR"));
|
let path = format!("{}/tests/data/default.xex", env!("CARGO_MANIFEST_DIR"));
|
||||||
|
|||||||
Reference in New Issue
Block a user