[Rust] Implement FPU/VMX128 opcodes, XEX LZX decompression, XISO browsing, and memory safety
Some checks failed
Orchestrator / Commit Message Validation (push) Has been skipped
Orchestrator / Lint (push) Successful in 2m5s
Orchestrator / Windows (x86-64) (push) Failing after 6m13s
Orchestrator / Linux (x86-64) (push) Failing after 20m59s
Orchestrator / Create Release (push) Has been skipped
Some checks failed
Orchestrator / Commit Message Validation (push) Has been skipped
Orchestrator / Lint (push) Successful in 2m5s
Orchestrator / Windows (x86-64) (push) Failing after 6m13s
Orchestrator / Linux (x86-64) (push) Failing after 20m59s
Orchestrator / Create Release (push) Has been skipped
Major additions to the xenia-rs Rust port: - CPU: ~170 new PPC opcode implementations (FPU, VMX128, 64-bit ALU, load/store variants) - XEX: Full LZX (normal) decompression pipeline with AES-128-CBC decryption via mspack FFI - XEX: Parse file format info, import libraries, and security info AES key from headers - VFS: Rewrite XISO disc image to use seek-based I/O (handles 7GB+ images without loading into memory) - App: Auto-detect ISO files and extract default.xex for all CLI commands - App: Add `info` and `browse` CLI subcommands - Kernel: Expand HLE exports from 14 to 40 stubs (memory, threading, TLS, I/O, video) - Memory: Add bounds checking on all guest memory accesses to prevent segfaults - Types: Add Vec128 array-based accessors (from_u32x4_array, from_f32x4_array, etc.) Tested against Project Sylpheed (USA) disc image - all four CLI commands (browse, info, disasm, exec) work correctly. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
3
xenia-rs/.gitignore
vendored
Normal file
3
xenia-rs/.gitignore
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
/target/
|
||||
*.iso
|
||||
*.xiso
|
||||
95
xenia-rs/Cargo.lock
generated
95
xenia-rs/Cargo.lock
generated
@@ -2,6 +2,17 @@
|
||||
# It is not intended for manual editing.
|
||||
version = 4
|
||||
|
||||
[[package]]
|
||||
name = "aes"
|
||||
version = "0.8.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b169f7a6d4742236a0a00c541b845991d0ac43e546831af1249753ab4c3aa3a0"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"cipher",
|
||||
"cpufeatures",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "aho-corasick"
|
||||
version = "1.1.4"
|
||||
@@ -79,12 +90,32 @@ version = "1.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b"
|
||||
|
||||
[[package]]
|
||||
name = "cc"
|
||||
version = "1.2.60"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "43c5703da9466b66a946814e1adf53ea2c90f10063b86290cc9eb67ce3478a20"
|
||||
dependencies = [
|
||||
"find-msvc-tools",
|
||||
"shlex",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cfg-if"
|
||||
version = "1.0.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
|
||||
|
||||
[[package]]
|
||||
name = "cipher"
|
||||
version = "0.4.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad"
|
||||
dependencies = [
|
||||
"crypto-common",
|
||||
"inout",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "clap"
|
||||
version = "4.6.0"
|
||||
@@ -131,12 +162,56 @@ version = "1.0.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570"
|
||||
|
||||
[[package]]
|
||||
name = "cpufeatures"
|
||||
version = "0.2.17"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280"
|
||||
dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "crypto-common"
|
||||
version = "0.1.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a"
|
||||
dependencies = [
|
||||
"generic-array",
|
||||
"typenum",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "find-msvc-tools"
|
||||
version = "0.1.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582"
|
||||
|
||||
[[package]]
|
||||
name = "generic-array"
|
||||
version = "0.14.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a"
|
||||
dependencies = [
|
||||
"typenum",
|
||||
"version_check",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "heck"
|
||||
version = "0.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
|
||||
|
||||
[[package]]
|
||||
name = "inout"
|
||||
version = "0.1.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "879f10e63c20629ecabbb64a8010319738c66a5cd0c29b02d63d272b03751d01"
|
||||
dependencies = [
|
||||
"generic-array",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "is_terminal_polyfill"
|
||||
version = "1.70.2"
|
||||
@@ -277,6 +352,12 @@ dependencies = [
|
||||
"lazy_static",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "shlex"
|
||||
version = "1.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
|
||||
|
||||
[[package]]
|
||||
name = "smallvec"
|
||||
version = "1.15.1"
|
||||
@@ -390,6 +471,12 @@ dependencies = [
|
||||
"tracing-log",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "typenum"
|
||||
version = "1.19.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb"
|
||||
|
||||
[[package]]
|
||||
name = "unicode-ident"
|
||||
version = "1.0.24"
|
||||
@@ -408,6 +495,12 @@ version = "0.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65"
|
||||
|
||||
[[package]]
|
||||
name = "version_check"
|
||||
version = "0.9.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
|
||||
|
||||
[[package]]
|
||||
name = "windows-link"
|
||||
version = "0.2.1"
|
||||
@@ -617,8 +710,10 @@ dependencies = [
|
||||
name = "xenia-xex"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"aes",
|
||||
"anyhow",
|
||||
"byteorder",
|
||||
"cc",
|
||||
"thiserror",
|
||||
"tracing",
|
||||
"xenia-memory",
|
||||
|
||||
@@ -40,3 +40,4 @@ byteorder = "1"
|
||||
thiserror = "2"
|
||||
anyhow = "1"
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
aes = "0.8"
|
||||
|
||||
@@ -55,8 +55,23 @@ fn main() -> Result<()> {
|
||||
}
|
||||
}
|
||||
|
||||
/// Load XEX data from a path. If the path is an ISO, extract default.xex from it.
|
||||
fn load_xex_data(path: &str) -> Result<Vec<u8>> {
|
||||
let lower = path.to_lowercase();
|
||||
if lower.ends_with(".iso") || lower.ends_with(".xiso") {
|
||||
use xenia_vfs::VfsDevice;
|
||||
println!("Detected disc image, extracting default.xex...");
|
||||
let disc = xenia_vfs::disc_image::DiscImageDevice::open("disc", std::path::Path::new(path))
|
||||
.map_err(|e| anyhow::anyhow!("Failed to open disc image: {}", e))?;
|
||||
disc.read_file("default.xex")
|
||||
.map_err(|e| anyhow::anyhow!("Failed to extract default.xex from disc image: {}", e))
|
||||
} else {
|
||||
Ok(std::fs::read(path)?)
|
||||
}
|
||||
}
|
||||
|
||||
fn cmd_info(path: &str) -> Result<()> {
|
||||
let data = std::fs::read(path)?;
|
||||
let data = load_xex_data(path)?;
|
||||
let header = xenia_xex::loader::parse_xex2_header(&data)?;
|
||||
|
||||
println!("=== XEX2 Header ===");
|
||||
@@ -85,11 +100,34 @@ fn cmd_info(path: &str) -> Result<()> {
|
||||
println!("Page Descs: {}", sec.page_descriptors.len());
|
||||
}
|
||||
|
||||
if let Some(ref ffi) = header.file_format_info {
|
||||
println!("\n=== File Format ===");
|
||||
println!("Encryption: {}", match ffi.encryption_type {
|
||||
0 => "None", 1 => "Normal (AES)", _ => "Unknown"
|
||||
});
|
||||
println!("Compression: {}", match ffi.compression_type {
|
||||
0 => "None", 1 => "Basic", 2 => "Normal (LZX)", _ => "Unknown"
|
||||
});
|
||||
if !ffi.basic_blocks.is_empty() {
|
||||
println!("Basic blocks: {}", ffi.basic_blocks.len());
|
||||
}
|
||||
if ffi.normal_window_size != 0 {
|
||||
println!("LZX Window: {:#x}", ffi.normal_window_size);
|
||||
}
|
||||
}
|
||||
|
||||
if !header.import_libraries.is_empty() {
|
||||
println!("\n=== Import Libraries ===");
|
||||
for lib in &header.import_libraries {
|
||||
println!(" {} (v{:#010x}, {} ordinals)", lib.name, lib.version_cur, lib.ordinals.len());
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn cmd_disasm(path: &str, count: usize) -> Result<()> {
|
||||
let data = std::fs::read(path)?;
|
||||
let data = load_xex_data(path)?;
|
||||
let header = xenia_xex::loader::parse_xex2_header(&data)?;
|
||||
|
||||
let entry = xenia_xex::loader::get_entry_point(&header)
|
||||
@@ -98,24 +136,27 @@ fn cmd_disasm(path: &str, count: usize) -> Result<()> {
|
||||
.ok_or_else(|| anyhow::anyhow!("No image base found in XEX2 header"))?;
|
||||
|
||||
println!("Entry point: {:#010x}, Image base: {:#010x}", entry, base);
|
||||
|
||||
// Load and decompress the image
|
||||
let image_data = xenia_xex::loader::load_image(&data, &header)?;
|
||||
println!("Image loaded: {} bytes decompressed", image_data.len());
|
||||
println!("Disassembly from entry point ({} instructions):\n", count);
|
||||
|
||||
// For now, disassemble from the raw file data at the entry offset
|
||||
let entry_offset = (entry - base) as usize + header.header_size as usize;
|
||||
if entry_offset + count * 4 <= data.len() {
|
||||
let block = xenia_cpu::disasm::disassemble_block(&data[entry_offset..], entry, count);
|
||||
let entry_offset = (entry - base) as usize;
|
||||
if entry_offset + count * 4 <= image_data.len() {
|
||||
let block = xenia_cpu::disasm::disassemble_block(&image_data[entry_offset..], entry, count);
|
||||
for (addr, text) in block {
|
||||
println!(" {:#010x}: {}", addr, text);
|
||||
}
|
||||
} else {
|
||||
println!(" (entry point offset {:#x} is outside file bounds)", entry_offset);
|
||||
println!(" (entry point offset {:#x} is outside image bounds, image is {:#x} bytes)", entry_offset, image_data.len());
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn cmd_exec(path: &str, max_instructions: u64) -> Result<()> {
|
||||
let data = std::fs::read(path)?;
|
||||
let data = load_xex_data(path)?;
|
||||
let header = xenia_xex::loader::parse_xex2_header(&data)?;
|
||||
|
||||
let entry = xenia_xex::loader::get_entry_point(&header)
|
||||
@@ -123,21 +164,38 @@ fn cmd_exec(path: &str, max_instructions: u64) -> Result<()> {
|
||||
let base = xenia_xex::loader::get_image_base(&header)
|
||||
.ok_or_else(|| anyhow::anyhow!("No image base found"))?;
|
||||
|
||||
// Print compression info
|
||||
if let Some(ref ffi) = header.file_format_info {
|
||||
println!("Compression: {} (encryption: {})",
|
||||
match ffi.compression_type {
|
||||
0 => "none", 1 => "basic", 2 => "normal (LZX)", _ => "unknown"
|
||||
},
|
||||
match ffi.encryption_type {
|
||||
0 => "none", 1 => "normal (AES)", _ => "unknown"
|
||||
});
|
||||
}
|
||||
if !header.import_libraries.is_empty() {
|
||||
println!("Import libraries:");
|
||||
for lib in &header.import_libraries {
|
||||
println!(" {} ({} ordinals)", lib.name, lib.ordinals.len());
|
||||
}
|
||||
}
|
||||
|
||||
println!("Loading XEX: entry={:#010x} base={:#010x}", entry, base);
|
||||
|
||||
// Allocate guest memory
|
||||
let mut mem = xenia_memory::GuestMemory::new()
|
||||
.map_err(|e| anyhow::anyhow!("Failed to allocate guest memory: {}", e))?;
|
||||
|
||||
// Map the XEX image into guest memory
|
||||
let image_data = &data[header.header_size as usize..];
|
||||
// Load and decompress the XEX image
|
||||
let image_data = xenia_xex::loader::load_image(&data, &header)?;
|
||||
let alloc_size = ((image_data.len() + 4095) & !4095) as u32;
|
||||
mem.alloc(
|
||||
base,
|
||||
alloc_size,
|
||||
xenia_memory::page_table::MemoryProtect::READ | xenia_memory::page_table::MemoryProtect::WRITE,
|
||||
).map_err(|e| anyhow::anyhow!("Failed to allocate guest memory region: {}", e))?;
|
||||
mem.write_bulk(base, image_data);
|
||||
mem.write_bulk(base, &image_data);
|
||||
|
||||
// Allocate stack (1MB at 0x70000000)
|
||||
let stack_base = 0x7000_0000u32;
|
||||
@@ -174,6 +232,12 @@ fn cmd_exec(path: &str, max_instructions: u64) -> Result<()> {
|
||||
break;
|
||||
}
|
||||
|
||||
// Check if PC is in mapped memory before trying to execute
|
||||
if !mem.is_mapped(ctx.pc) {
|
||||
println!("[{:>8}] FAULT: PC {:#010x} is in unmapped memory", instruction_count, ctx.pc);
|
||||
break;
|
||||
}
|
||||
|
||||
// Pre-step debugger
|
||||
debugger.pre_step(&ctx, &mem);
|
||||
|
||||
|
||||
@@ -805,7 +805,7 @@ mod tests {
|
||||
#[test]
|
||||
fn test_decode_ori_nop() {
|
||||
// ori r0, r0, 0 = NOP
|
||||
let raw: u32 = (24 << 26);
|
||||
let raw: u32 = 24 << 26;
|
||||
let instr = decode(raw, 0);
|
||||
assert_eq!(instr.opcode, PpcOpcode::ori);
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -12,10 +12,15 @@ pub fn register_exports(state: &mut KernelState) {
|
||||
state.register_export(Xboxkrnl, 0xBB, "NtAllocateVirtualMemory", nt_allocate_virtual_memory);
|
||||
state.register_export(Xboxkrnl, 0xBC, "NtFreeVirtualMemory", nt_free_virtual_memory);
|
||||
state.register_export(Xboxkrnl, 0xC4, "NtQueryVirtualMemory", nt_query_virtual_memory);
|
||||
state.register_export(Xboxkrnl, 0xB9, "MmAllocatePhysicalMemory", mm_allocate_physical_memory);
|
||||
state.register_export(Xboxkrnl, 0xBA, "MmAllocatePhysicalMemoryEx", mm_allocate_physical_memory_ex);
|
||||
|
||||
// Threading
|
||||
state.register_export(Xboxkrnl, 0x0C, "ExCreateThread", ex_create_thread);
|
||||
state.register_export(Xboxkrnl, 0x5F, "KeDelayExecutionThread", ke_delay_execution_thread);
|
||||
state.register_export(Xboxkrnl, 0x97, "KeSetAffinityThread", ke_set_affinity_thread);
|
||||
state.register_export(Xboxkrnl, 0x154, "KeTlsGetValue", ke_tls_get_value);
|
||||
state.register_export(Xboxkrnl, 0x155, "KeTlsSetValue", ke_tls_set_value);
|
||||
|
||||
// Sync
|
||||
state.register_export(Xboxkrnl, 0xC0, "NtCreateEvent", nt_create_event);
|
||||
@@ -23,24 +28,49 @@ pub fn register_exports(state: &mut KernelState) {
|
||||
state.register_export(Xboxkrnl, 0x6B, "KeWaitForSingleObject", ke_wait_for_single_object);
|
||||
state.register_export(Xboxkrnl, 0x53, "NtClose", nt_close);
|
||||
|
||||
// Spinlocks/IRQL
|
||||
state.register_export(Xboxkrnl, 0xB1, "KfAcquireSpinLock", kf_acquire_spin_lock);
|
||||
state.register_export(Xboxkrnl, 0xB4, "KfReleaseSpinLock", kf_release_spin_lock);
|
||||
state.register_export(Xboxkrnl, 0x85, "KeRaiseIrqlToDpcLevel", ke_raise_irql_to_dpc_level);
|
||||
state.register_export(Xboxkrnl, 0xB3, "KfLowerIrql", kf_lower_irql);
|
||||
|
||||
// Module
|
||||
state.register_export(Xboxkrnl, 0x195, "XexGetModuleHandle", xex_get_module_handle);
|
||||
state.register_export(Xboxkrnl, 0x197, "XexGetProcedureAddress", xex_get_procedure_address);
|
||||
|
||||
// Object
|
||||
state.register_export(Xboxkrnl, 0x110, "ObReferenceObjectByHandle", ob_reference_object_by_handle);
|
||||
|
||||
// Process/System
|
||||
state.register_export(Xboxkrnl, 0x66, "KeGetCurrentProcessType", ke_get_current_process_type);
|
||||
state.register_export(Xboxkrnl, 0x83, "KeQueryPerformanceFrequency", ke_query_performance_frequency);
|
||||
state.register_export(Xboxkrnl, 0x84, "KeQuerySystemTime", ke_query_system_time);
|
||||
state.register_export(Xboxkrnl, 0x10, "ExGetXConfigSetting", ex_get_xconfig_setting);
|
||||
|
||||
// RTL
|
||||
state.register_export(Xboxkrnl, 0x11A, "RtlInitAnsiString", rtl_init_ansi_string);
|
||||
state.register_export(Xboxkrnl, 0x12D, "RtlInitUnicodeString", rtl_init_unicode_string);
|
||||
state.register_export(Xboxkrnl, 0x127, "RtlFreeAnsiString", rtl_free_ansi_string);
|
||||
state.register_export(Xboxkrnl, 0x13B, "sprintf", stub_sprintf);
|
||||
|
||||
// I/O
|
||||
state.register_export(Xboxkrnl, 0xD2, "NtCreateFile", nt_create_file);
|
||||
state.register_export(Xboxkrnl, 0xF0, "NtReadFile", nt_read_file);
|
||||
state.register_export(Xboxkrnl, 0xE8, "NtQueryInformationFile", nt_query_information_file);
|
||||
state.register_export(Xboxkrnl, 0xE7, "NtQueryFullAttributesFile", nt_query_full_attributes_file);
|
||||
|
||||
// Video
|
||||
state.register_export(Xboxkrnl, 0x142, "VdGetCurrentDisplayGamma", vd_get_current_display_gamma);
|
||||
state.register_export(Xboxkrnl, 0x14B, "VdQueryVideoMode", vd_query_video_mode);
|
||||
state.register_export(Xboxkrnl, 0x1C2, "VdInitializeEngines", vd_initialize_engines);
|
||||
|
||||
// Debug
|
||||
state.register_export(Xboxkrnl, 0x166, "DbgPrint", dbg_print);
|
||||
}
|
||||
|
||||
// ===== Memory =====
|
||||
|
||||
fn nt_allocate_virtual_memory(ctx: &mut PpcContext, _mem: &mut GuestMemory, _state: &mut KernelState) {
|
||||
let _base_addr_ptr = ctx.gpr[3] as u32;
|
||||
let _size_ptr = ctx.gpr[4] as u32;
|
||||
// Stub: return success
|
||||
ctx.gpr[3] = 0; // STATUS_SUCCESS
|
||||
}
|
||||
|
||||
@@ -52,18 +82,48 @@ fn nt_query_virtual_memory(ctx: &mut PpcContext, _mem: &mut GuestMemory, _state:
|
||||
ctx.gpr[3] = 0;
|
||||
}
|
||||
|
||||
fn mm_allocate_physical_memory(ctx: &mut PpcContext, _mem: &mut GuestMemory, _state: &mut KernelState) {
|
||||
// r3 = region, r4 = size, r5 = protect
|
||||
// Return a fake address in physical memory range
|
||||
ctx.gpr[3] = 0xA000_0000; // Fake physical allocation
|
||||
}
|
||||
|
||||
fn mm_allocate_physical_memory_ex(ctx: &mut PpcContext, _mem: &mut GuestMemory, _state: &mut KernelState) {
|
||||
// r3 = size, r4 = protect, r5 = min_addr, r6 = max_addr, r7 = alignment
|
||||
ctx.gpr[3] = 0xA000_0000; // Fake physical allocation
|
||||
}
|
||||
|
||||
// ===== Threading =====
|
||||
|
||||
fn ex_create_thread(ctx: &mut PpcContext, _mem: &mut GuestMemory, state: &mut KernelState) {
|
||||
let handle = state.alloc_handle();
|
||||
// Write handle to output parameter (r3 = handle_ptr)
|
||||
tracing::info!("ExCreateThread: allocated handle {:#x}", handle);
|
||||
ctx.gpr[3] = 0; // STATUS_SUCCESS
|
||||
}
|
||||
|
||||
fn ke_delay_execution_thread(ctx: &mut PpcContext, _mem: &mut GuestMemory, _state: &mut KernelState) {
|
||||
// In cooperative mode, this is where we'd yield to another thread
|
||||
ctx.gpr[3] = 0;
|
||||
}
|
||||
|
||||
fn ke_set_affinity_thread(ctx: &mut PpcContext, _mem: &mut GuestMemory, _state: &mut KernelState) {
|
||||
// r3 = thread handle, r4 = affinity mask
|
||||
ctx.gpr[3] = 0; // Return previous affinity
|
||||
}
|
||||
|
||||
fn ke_tls_get_value(ctx: &mut PpcContext, _mem: &mut GuestMemory, state: &mut KernelState) {
|
||||
let index = ctx.gpr[3] as u32;
|
||||
ctx.gpr[3] = state.tls_get(index);
|
||||
}
|
||||
|
||||
fn ke_tls_set_value(ctx: &mut PpcContext, _mem: &mut GuestMemory, state: &mut KernelState) {
|
||||
let index = ctx.gpr[3] as u32;
|
||||
let value = ctx.gpr[4];
|
||||
state.tls_set(index, value);
|
||||
ctx.gpr[3] = 1; // TRUE = success
|
||||
}
|
||||
|
||||
// ===== Sync =====
|
||||
|
||||
fn nt_create_event(ctx: &mut PpcContext, _mem: &mut GuestMemory, state: &mut KernelState) {
|
||||
let _handle = state.alloc_handle();
|
||||
ctx.gpr[3] = 0;
|
||||
@@ -74,25 +134,88 @@ fn ke_set_event(ctx: &mut PpcContext, _mem: &mut GuestMemory, _state: &mut Kerne
|
||||
}
|
||||
|
||||
fn ke_wait_for_single_object(ctx: &mut PpcContext, _mem: &mut GuestMemory, _state: &mut KernelState) {
|
||||
// Stub: return immediately as if signaled
|
||||
ctx.gpr[3] = 0; // STATUS_SUCCESS
|
||||
ctx.gpr[3] = 0; // STATUS_SUCCESS (immediately signaled)
|
||||
}
|
||||
|
||||
fn nt_close(ctx: &mut PpcContext, _mem: &mut GuestMemory, _state: &mut KernelState) {
|
||||
ctx.gpr[3] = 0;
|
||||
}
|
||||
|
||||
fn xex_get_module_handle(ctx: &mut PpcContext, _mem: &mut GuestMemory, _state: &mut KernelState) {
|
||||
ctx.gpr[3] = 0; // Return NULL for now
|
||||
// ===== Spinlocks/IRQL =====
|
||||
|
||||
fn kf_acquire_spin_lock(ctx: &mut PpcContext, _mem: &mut GuestMemory, _state: &mut KernelState) {
|
||||
// Return old IRQL (simulate DISPATCH_LEVEL = 2)
|
||||
ctx.gpr[3] = 0; // Previous IRQL (PASSIVE_LEVEL)
|
||||
}
|
||||
|
||||
fn kf_release_spin_lock(ctx: &mut PpcContext, _mem: &mut GuestMemory, _state: &mut KernelState) {
|
||||
// r3 = spin lock, r4 = old IRQL
|
||||
ctx.gpr[3] = 0;
|
||||
}
|
||||
|
||||
fn ke_raise_irql_to_dpc_level(ctx: &mut PpcContext, _mem: &mut GuestMemory, _state: &mut KernelState) {
|
||||
ctx.gpr[3] = 0; // Return old IRQL
|
||||
}
|
||||
|
||||
fn kf_lower_irql(ctx: &mut PpcContext, _mem: &mut GuestMemory, _state: &mut KernelState) {
|
||||
ctx.gpr[3] = 0;
|
||||
}
|
||||
|
||||
// ===== Module =====
|
||||
|
||||
fn xex_get_module_handle(ctx: &mut PpcContext, _mem: &mut GuestMemory, _state: &mut KernelState) {
|
||||
ctx.gpr[3] = 0; // Return NULL
|
||||
}
|
||||
|
||||
fn xex_get_procedure_address(ctx: &mut PpcContext, _mem: &mut GuestMemory, _state: &mut KernelState) {
|
||||
// r3 = module_handle, r4 = ordinal, r5 = address_ptr
|
||||
let ordinal = ctx.gpr[4] as u32;
|
||||
tracing::warn!("XexGetProcedureAddress: ordinal {:#x} not found", ordinal);
|
||||
ctx.gpr[3] = 0xC000_0034; // STATUS_OBJECT_NAME_NOT_FOUND
|
||||
}
|
||||
|
||||
// ===== Object =====
|
||||
|
||||
fn ob_reference_object_by_handle(ctx: &mut PpcContext, _mem: &mut GuestMemory, _state: &mut KernelState) {
|
||||
// r3 = handle, r4 = object_type, r5 = out_object_ptr
|
||||
ctx.gpr[3] = 0; // STATUS_SUCCESS
|
||||
}
|
||||
|
||||
// ===== Process/System =====
|
||||
|
||||
fn ke_get_current_process_type(ctx: &mut PpcContext, _mem: &mut GuestMemory, _state: &mut KernelState) {
|
||||
ctx.gpr[3] = 1; // PROC_USER (user mode process)
|
||||
}
|
||||
|
||||
fn ke_query_performance_frequency(ctx: &mut PpcContext, _mem: &mut GuestMemory, _state: &mut KernelState) {
|
||||
ctx.gpr[3] = 50_000_000; // 50 MHz (Xbox 360 timebase frequency)
|
||||
}
|
||||
|
||||
fn ke_query_system_time(ctx: &mut PpcContext, mem: &mut GuestMemory, _state: &mut KernelState) {
|
||||
use xenia_memory::MemoryAccess;
|
||||
let time_ptr = ctx.gpr[3] as u32;
|
||||
if time_ptr != 0 {
|
||||
// Write a fake system time (Windows FILETIME format, 100ns intervals since 1601)
|
||||
// Use a fixed value so execution is deterministic
|
||||
let fake_time: u64 = 132_500_000_000_000_000; // ~2021
|
||||
mem.write_u32(time_ptr, (fake_time >> 32) as u32);
|
||||
mem.write_u32(time_ptr + 4, fake_time as u32);
|
||||
}
|
||||
}
|
||||
|
||||
fn ex_get_xconfig_setting(ctx: &mut PpcContext, _mem: &mut GuestMemory, _state: &mut KernelState) {
|
||||
// r3 = category, r4 = setting, r5 = buffer, r6 = buffer_size_ptr
|
||||
ctx.gpr[3] = 0; // STATUS_SUCCESS (but writes nothing)
|
||||
}
|
||||
|
||||
// ===== RTL =====
|
||||
|
||||
fn rtl_init_ansi_string(ctx: &mut PpcContext, mem: &mut GuestMemory, _state: &mut KernelState) {
|
||||
use xenia_memory::MemoryAccess;
|
||||
let dest_ptr = ctx.gpr[3] as u32;
|
||||
let src_ptr = ctx.gpr[4] as u32;
|
||||
|
||||
if src_ptr != 0 {
|
||||
// Read string length
|
||||
let mut len: u16 = 0;
|
||||
let mut addr = src_ptr;
|
||||
while mem.read_u8(addr) != 0 {
|
||||
@@ -106,6 +229,78 @@ fn rtl_init_ansi_string(ctx: &mut PpcContext, mem: &mut GuestMemory, _state: &mu
|
||||
}
|
||||
}
|
||||
|
||||
fn rtl_init_unicode_string(ctx: &mut PpcContext, mem: &mut GuestMemory, _state: &mut KernelState) {
|
||||
use xenia_memory::MemoryAccess;
|
||||
let dest_ptr = ctx.gpr[3] as u32;
|
||||
let src_ptr = ctx.gpr[4] as u32;
|
||||
|
||||
if src_ptr != 0 {
|
||||
// Count wide chars (2 bytes each, null-terminated)
|
||||
let mut len: u16 = 0;
|
||||
let mut addr = src_ptr;
|
||||
while mem.read_u16(addr) != 0 {
|
||||
len += 2;
|
||||
addr += 2;
|
||||
}
|
||||
// UNICODE_STRING: {Length, MaxLength, Buffer}
|
||||
mem.write_u16(dest_ptr, len);
|
||||
mem.write_u16(dest_ptr + 2, len + 2);
|
||||
mem.write_u32(dest_ptr + 4, src_ptr);
|
||||
} else {
|
||||
mem.write_u16(dest_ptr, 0);
|
||||
mem.write_u16(dest_ptr + 2, 0);
|
||||
mem.write_u32(dest_ptr + 4, 0);
|
||||
}
|
||||
}
|
||||
|
||||
fn rtl_free_ansi_string(ctx: &mut PpcContext, _mem: &mut GuestMemory, _state: &mut KernelState) {
|
||||
// Stub: no-op (we don't track allocations yet)
|
||||
ctx.gpr[3] = 0;
|
||||
}
|
||||
|
||||
fn stub_sprintf(ctx: &mut PpcContext, mem: &mut GuestMemory, _state: &mut KernelState) {
|
||||
use xenia_memory::MemoryAccess;
|
||||
// r3 = dest buffer, r4 = format string
|
||||
// Stub: just copy the format string as-is
|
||||
let dest = ctx.gpr[3] as u32;
|
||||
let fmt = ctx.gpr[4] as u32;
|
||||
if fmt != 0 && dest != 0 {
|
||||
let mut addr = fmt;
|
||||
let mut daddr = dest;
|
||||
loop {
|
||||
let c = mem.read_u8(addr);
|
||||
mem.write_u8(daddr, c);
|
||||
if c == 0 { break; }
|
||||
addr += 1;
|
||||
daddr += 1;
|
||||
}
|
||||
}
|
||||
ctx.gpr[3] = 0; // Return length (stub)
|
||||
}
|
||||
|
||||
// ===== I/O =====
|
||||
|
||||
fn nt_create_file(ctx: &mut PpcContext, _mem: &mut GuestMemory, state: &mut KernelState) {
|
||||
let handle = state.alloc_handle();
|
||||
tracing::info!("NtCreateFile: allocated handle {:#x}", handle);
|
||||
ctx.gpr[3] = 0; // STATUS_SUCCESS
|
||||
}
|
||||
|
||||
fn nt_read_file(ctx: &mut PpcContext, _mem: &mut GuestMemory, _state: &mut KernelState) {
|
||||
// Stub: return end of file
|
||||
ctx.gpr[3] = 0xC000_0011; // STATUS_END_OF_FILE
|
||||
}
|
||||
|
||||
fn nt_query_information_file(ctx: &mut PpcContext, _mem: &mut GuestMemory, _state: &mut KernelState) {
|
||||
ctx.gpr[3] = 0; // STATUS_SUCCESS
|
||||
}
|
||||
|
||||
fn nt_query_full_attributes_file(ctx: &mut PpcContext, _mem: &mut GuestMemory, _state: &mut KernelState) {
|
||||
ctx.gpr[3] = 0xC000_0034; // STATUS_OBJECT_NAME_NOT_FOUND
|
||||
}
|
||||
|
||||
// ===== Video =====
|
||||
|
||||
fn vd_get_current_display_gamma(ctx: &mut PpcContext, _mem: &mut GuestMemory, _state: &mut KernelState) {
|
||||
ctx.gpr[3] = 0;
|
||||
}
|
||||
@@ -114,7 +309,6 @@ fn vd_query_video_mode(ctx: &mut PpcContext, mem: &mut GuestMemory, _state: &mut
|
||||
use xenia_memory::MemoryAccess;
|
||||
let mode_ptr = ctx.gpr[3] as u32;
|
||||
if mode_ptr != 0 {
|
||||
// Write a basic video mode (1280x720)
|
||||
mem.write_u32(mode_ptr, 1280); // width
|
||||
mem.write_u32(mode_ptr + 4, 720); // height
|
||||
mem.write_u32(mode_ptr + 8, 0); // is_interlaced
|
||||
@@ -124,6 +318,13 @@ fn vd_query_video_mode(ctx: &mut PpcContext, mem: &mut GuestMemory, _state: &mut
|
||||
ctx.gpr[3] = 0;
|
||||
}
|
||||
|
||||
fn vd_initialize_engines(ctx: &mut PpcContext, _mem: &mut GuestMemory, _state: &mut KernelState) {
|
||||
tracing::info!("VdInitializeEngines called");
|
||||
ctx.gpr[3] = 0;
|
||||
}
|
||||
|
||||
// ===== Debug =====
|
||||
|
||||
fn dbg_print(ctx: &mut PpcContext, mem: &mut GuestMemory, _state: &mut KernelState) {
|
||||
use xenia_memory::MemoryAccess;
|
||||
let str_ptr = ctx.gpr[3] as u32;
|
||||
|
||||
@@ -17,6 +17,7 @@ pub enum ModuleId {
|
||||
pub struct KernelState {
|
||||
exports: HashMap<(ModuleId, u32), (&'static str, KernelExportFn)>,
|
||||
next_handle: u32,
|
||||
tls_slots: HashMap<u32, u64>,
|
||||
}
|
||||
|
||||
impl KernelState {
|
||||
@@ -24,6 +25,7 @@ impl KernelState {
|
||||
let mut state = Self {
|
||||
exports: HashMap::new(),
|
||||
next_handle: 0x1000,
|
||||
tls_slots: HashMap::new(),
|
||||
};
|
||||
crate::exports::register_exports(&mut state);
|
||||
state
|
||||
@@ -71,6 +73,14 @@ impl KernelState {
|
||||
self.next_handle += 4;
|
||||
h
|
||||
}
|
||||
|
||||
pub fn tls_get(&self, index: u32) -> u64 {
|
||||
self.tls_slots.get(&index).copied().unwrap_or(0)
|
||||
}
|
||||
|
||||
pub fn tls_set(&mut self, index: u32, value: u64) {
|
||||
self.tls_slots.insert(index, value);
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for KernelState {
|
||||
|
||||
@@ -133,6 +133,15 @@ impl GuestMemory {
|
||||
}
|
||||
}
|
||||
|
||||
/// Check if a guest address has been allocated/committed.
|
||||
pub fn is_mapped(&self, addr: u32) -> bool {
|
||||
let page = (addr / PAGE_SIZE) as usize;
|
||||
if page >= self.page_table.len() {
|
||||
return false;
|
||||
}
|
||||
self.page_table[page].state().contains(AllocationState::COMMIT)
|
||||
}
|
||||
|
||||
/// Get a page table entry for a given address.
|
||||
pub fn page_entry(&self, addr: u32) -> &PageEntry {
|
||||
let page = (addr / PAGE_SIZE) as usize;
|
||||
@@ -142,6 +151,7 @@ impl GuestMemory {
|
||||
|
||||
impl MemoryAccess for GuestMemory {
|
||||
fn read_u8(&self, addr: u32) -> u8 {
|
||||
if !self.is_mapped(addr) { return 0; }
|
||||
let ptr = self.translate_virtual(addr);
|
||||
unsafe { *ptr }
|
||||
}
|
||||
@@ -149,6 +159,8 @@ impl MemoryAccess for GuestMemory {
|
||||
fn read_u16(&self, addr: u32) -> u16 {
|
||||
if let Some(mmio) = self.find_mmio(addr) {
|
||||
(mmio.read_callback)(addr) as u16
|
||||
} else if !self.is_mapped(addr) {
|
||||
0
|
||||
} else {
|
||||
let ptr = self.translate_virtual(addr) as *const [u8; 2];
|
||||
u16::from_be_bytes(unsafe { *ptr })
|
||||
@@ -158,6 +170,8 @@ impl MemoryAccess for GuestMemory {
|
||||
fn read_u32(&self, addr: u32) -> u32 {
|
||||
if let Some(mmio) = self.find_mmio(addr) {
|
||||
(mmio.read_callback)(addr)
|
||||
} else if !self.is_mapped(addr) {
|
||||
0
|
||||
} else {
|
||||
let ptr = self.translate_virtual(addr) as *const [u8; 4];
|
||||
u32::from_be_bytes(unsafe { *ptr })
|
||||
@@ -169,6 +183,8 @@ impl MemoryAccess for GuestMemory {
|
||||
let hi = (mmio.read_callback)(addr) as u64;
|
||||
let lo = (mmio.read_callback)(addr.wrapping_add(4)) as u64;
|
||||
(hi << 32) | lo
|
||||
} else if !self.is_mapped(addr) {
|
||||
0
|
||||
} else {
|
||||
let ptr = self.translate_virtual(addr) as *const [u8; 8];
|
||||
u64::from_be_bytes(unsafe { *ptr })
|
||||
@@ -176,6 +192,7 @@ impl MemoryAccess for GuestMemory {
|
||||
}
|
||||
|
||||
fn write_u8(&mut self, addr: u32, val: u8) {
|
||||
if !self.is_mapped(addr) { return; }
|
||||
let ptr = self.translate_virtual_mut(addr);
|
||||
unsafe { *ptr = val };
|
||||
}
|
||||
@@ -183,6 +200,8 @@ impl MemoryAccess for GuestMemory {
|
||||
fn write_u16(&mut self, addr: u32, val: u16) {
|
||||
if let Some(mmio) = self.find_mmio(addr) {
|
||||
(mmio.write_callback)(addr, val as u32);
|
||||
} else if !self.is_mapped(addr) {
|
||||
return;
|
||||
} else {
|
||||
let ptr = self.translate_virtual_mut(addr);
|
||||
unsafe {
|
||||
@@ -194,6 +213,8 @@ impl MemoryAccess for GuestMemory {
|
||||
fn write_u32(&mut self, addr: u32, val: u32) {
|
||||
if let Some(mmio) = self.find_mmio(addr) {
|
||||
(mmio.write_callback)(addr, val);
|
||||
} else if !self.is_mapped(addr) {
|
||||
return;
|
||||
} else {
|
||||
let ptr = self.translate_virtual_mut(addr);
|
||||
unsafe {
|
||||
@@ -206,6 +227,8 @@ impl MemoryAccess for GuestMemory {
|
||||
if let Some(mmio) = self.find_mmio(addr) {
|
||||
(mmio.write_callback)(addr, (val >> 32) as u32);
|
||||
(mmio.write_callback)(addr.wrapping_add(4), val as u32);
|
||||
} else if !self.is_mapped(addr) {
|
||||
return;
|
||||
} else {
|
||||
let ptr = self.translate_virtual_mut(addr);
|
||||
unsafe {
|
||||
@@ -215,7 +238,7 @@ impl MemoryAccess for GuestMemory {
|
||||
}
|
||||
|
||||
fn translate(&self, addr: u32) -> Option<*const u8> {
|
||||
if self.find_mmio(addr).is_some() {
|
||||
if self.find_mmio(addr).is_some() || !self.is_mapped(addr) {
|
||||
None
|
||||
} else {
|
||||
Some(self.translate_virtual(addr))
|
||||
|
||||
@@ -93,6 +93,51 @@ impl Vec128 {
|
||||
let off = i * 8;
|
||||
self.bytes[off..off + 8].copy_from_slice(&val.to_be_bytes());
|
||||
}
|
||||
|
||||
/// Get all 4 u32 elements as an array.
|
||||
pub fn as_u32x4(&self) -> [u32; 4] {
|
||||
[self.u32x4(0), self.u32x4(1), self.u32x4(2), self.u32x4(3)]
|
||||
}
|
||||
|
||||
/// Get all 4 f32 elements as an array.
|
||||
pub fn as_f32x4(&self) -> [f32; 4] {
|
||||
[self.f32x4(0), self.f32x4(1), self.f32x4(2), self.f32x4(3)]
|
||||
}
|
||||
|
||||
/// Get all 8 u16 elements as an array.
|
||||
pub fn as_u16x8(&self) -> [u16; 8] {
|
||||
[
|
||||
self.u16x8(0), self.u16x8(1), self.u16x8(2), self.u16x8(3),
|
||||
self.u16x8(4), self.u16x8(5), self.u16x8(6), self.u16x8(7),
|
||||
]
|
||||
}
|
||||
|
||||
/// Get all 16 bytes as an array.
|
||||
pub fn as_bytes(&self) -> [u8; 16] {
|
||||
self.bytes
|
||||
}
|
||||
|
||||
/// Create from a byte array.
|
||||
pub fn from_bytes(bytes: [u8; 16]) -> Self {
|
||||
Self { bytes }
|
||||
}
|
||||
|
||||
/// Create from a u32 array (big-endian elements).
|
||||
pub fn from_u32x4_array(arr: [u32; 4]) -> Self {
|
||||
Self::from_u32x4(arr[0], arr[1], arr[2], arr[3])
|
||||
}
|
||||
|
||||
/// Create from an f32 array (big-endian elements).
|
||||
pub fn from_f32x4_array(arr: [f32; 4]) -> Self {
|
||||
Self::from_f32x4(arr[0], arr[1], arr[2], arr[3])
|
||||
}
|
||||
|
||||
/// Create from a u16 array (big-endian elements).
|
||||
pub fn from_u16x8_array(arr: [u16; 8]) -> Self {
|
||||
let mut v = Self::ZERO;
|
||||
for i in 0..8 { v.set_u16x8(i, arr[i]); }
|
||||
v
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for Vec128 {
|
||||
|
||||
@@ -1,23 +1,132 @@
|
||||
use crate::{VfsDevice, VfsEntry, VfsError};
|
||||
use std::io::{Read, Seek, SeekFrom};
|
||||
|
||||
/// XISO disc image device. Parses Xbox 360 disc images.
|
||||
/// XISO disc image device. Parses Xbox 360 disc images (GDFX/XISO format).
|
||||
pub struct DiscImageDevice {
|
||||
name: String,
|
||||
_data: Vec<u8>,
|
||||
path: std::path::PathBuf,
|
||||
game_offset: u64,
|
||||
/// Cached root directory buffer (typically small, a few KB).
|
||||
root_buffer: Vec<u8>,
|
||||
}
|
||||
|
||||
/// XISO sector size
|
||||
pub const SECTOR_SIZE: usize = 0x800;
|
||||
pub const SECTOR_SIZE: u64 = 0x800;
|
||||
|
||||
/// GDFX magic string
|
||||
const GDFX_MAGIC: &[u8; 20] = b"MICROSOFT*XBOX*MEDIA";
|
||||
|
||||
/// File attribute: directory
|
||||
const FILE_ATTRIBUTE_DIRECTORY: u8 = 0x10;
|
||||
|
||||
/// Known game partition offsets to try
|
||||
const LIKELY_OFFSETS: &[u64] = &[
|
||||
0x0000_0000,
|
||||
0x0000_FB20,
|
||||
0x0002_0600,
|
||||
0x0208_0000,
|
||||
0x0FD9_0000,
|
||||
];
|
||||
|
||||
impl DiscImageDevice {
|
||||
pub fn open(name: impl Into<String>, path: &std::path::Path) -> Result<Self, VfsError> {
|
||||
let data = std::fs::read(path)?;
|
||||
// TODO: validate XISO header
|
||||
let mut file = std::fs::File::open(path)?;
|
||||
|
||||
// Find the game partition by locating the GDFX magic at sector 32
|
||||
let mut game_offset = 0u64;
|
||||
let mut magic_found = false;
|
||||
let mut magic_buf = [0u8; 20];
|
||||
|
||||
for &offset in LIKELY_OFFSETS {
|
||||
let magic_pos = offset + 32 * SECTOR_SIZE;
|
||||
if file.seek(SeekFrom::Start(magic_pos)).is_ok()
|
||||
&& file.read_exact(&mut magic_buf).is_ok()
|
||||
&& magic_buf == *GDFX_MAGIC
|
||||
{
|
||||
game_offset = offset;
|
||||
magic_found = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if !magic_found {
|
||||
return Err(VfsError::InvalidFormat(
|
||||
"GDFX magic not found - not a valid XISO disc image".into(),
|
||||
));
|
||||
}
|
||||
|
||||
// Read root directory info from sector 32 header
|
||||
let fs_ptr = game_offset + 32 * SECTOR_SIZE;
|
||||
file.seek(SeekFrom::Start(fs_ptr + 20))?;
|
||||
let mut buf4 = [0u8; 4];
|
||||
file.read_exact(&mut buf4)?;
|
||||
let root_sector = u32::from_le_bytes(buf4) as u64;
|
||||
file.read_exact(&mut buf4)?;
|
||||
let root_size = u32::from_le_bytes(buf4) as u64;
|
||||
|
||||
let root_byte_offset = game_offset + root_sector * SECTOR_SIZE;
|
||||
|
||||
// Read the root directory buffer into memory (typically small)
|
||||
file.seek(SeekFrom::Start(root_byte_offset))?;
|
||||
let mut root_buffer = vec![0u8; root_size as usize];
|
||||
file.read_exact(&mut root_buffer)?;
|
||||
|
||||
Ok(Self {
|
||||
name: name.into(),
|
||||
_data: data,
|
||||
path: path.to_path_buf(),
|
||||
game_offset,
|
||||
root_buffer,
|
||||
})
|
||||
}
|
||||
|
||||
/// Read all directory entries from the root directory tree.
|
||||
fn read_entries(&self) -> Vec<VfsEntry> {
|
||||
let mut entries = Vec::new();
|
||||
self.read_entry(&self.root_buffer, 0, &mut entries);
|
||||
entries
|
||||
}
|
||||
|
||||
/// Recursively read a directory entry from the binary tree structure.
|
||||
fn read_entry(&self, buffer: &[u8], ordinal: u16, entries: &mut Vec<VfsEntry>) {
|
||||
let p = ordinal as usize * 4;
|
||||
if p + 14 > buffer.len() {
|
||||
return;
|
||||
}
|
||||
|
||||
let node_l = u16::from_le_bytes([buffer[p], buffer[p + 1]]);
|
||||
let node_r = u16::from_le_bytes([buffer[p + 2], buffer[p + 3]]);
|
||||
let sector = u32::from_le_bytes([buffer[p + 4], buffer[p + 5], buffer[p + 6], buffer[p + 7]]) as u64;
|
||||
let length = u32::from_le_bytes([buffer[p + 8], buffer[p + 9], buffer[p + 10], buffer[p + 11]]) as u64;
|
||||
let attributes = buffer[p + 12];
|
||||
let name_length = buffer[p + 13] as usize;
|
||||
|
||||
if p + 14 + name_length > buffer.len() {
|
||||
return;
|
||||
}
|
||||
|
||||
// Traverse left subtree first (smaller names)
|
||||
if node_l != 0 && node_l != 0xFFFF {
|
||||
self.read_entry(buffer, node_l, entries);
|
||||
}
|
||||
|
||||
// Read this entry's name
|
||||
let name = String::from_utf8_lossy(&buffer[p + 14..p + 14 + name_length]).to_string();
|
||||
let is_directory = (attributes & FILE_ATTRIBUTE_DIRECTORY) != 0;
|
||||
|
||||
let file_offset = self.game_offset + sector * SECTOR_SIZE;
|
||||
|
||||
entries.push(VfsEntry {
|
||||
name,
|
||||
is_directory,
|
||||
size: length,
|
||||
offset: file_offset,
|
||||
});
|
||||
|
||||
// Traverse right subtree (larger names)
|
||||
if node_r != 0 && node_r != 0xFFFF {
|
||||
self.read_entry(buffer, node_r, entries);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl VfsDevice for DiscImageDevice {
|
||||
@@ -26,15 +135,51 @@ impl VfsDevice for DiscImageDevice {
|
||||
}
|
||||
|
||||
fn list_root(&self) -> Result<Vec<VfsEntry>, VfsError> {
|
||||
// TODO: Parse XISO directory tree
|
||||
Ok(Vec::new())
|
||||
Ok(self.read_entries())
|
||||
}
|
||||
|
||||
fn read_file(&self, _path: &str) -> Result<Vec<u8>, VfsError> {
|
||||
Err(VfsError::NotFound("Not yet implemented".into()))
|
||||
fn read_file(&self, path: &str) -> Result<Vec<u8>, VfsError> {
|
||||
let entries = self.read_entries();
|
||||
let entry = entries.iter()
|
||||
.find(|e| e.name.eq_ignore_ascii_case(path) && !e.is_directory)
|
||||
.ok_or_else(|| VfsError::NotFound(path.to_string()))?;
|
||||
|
||||
let offset = entry.offset;
|
||||
let size = entry.size as usize;
|
||||
|
||||
// Read from file using seek
|
||||
let mut file = std::fs::File::open(&self.path)?;
|
||||
let file_len = file.seek(SeekFrom::End(0))?;
|
||||
if offset + size as u64 > file_len {
|
||||
return Err(VfsError::NotFound(format!(
|
||||
"File data extends past end of image: {} (offset={:#x}, size={:#x}, image_len={:#x})",
|
||||
path, offset, size, file_len
|
||||
)));
|
||||
}
|
||||
file.seek(SeekFrom::Start(offset))?;
|
||||
let mut buf = vec![0u8; size];
|
||||
let bytes_read = file.read(&mut buf)?;
|
||||
if bytes_read < size {
|
||||
// Try reading the rest
|
||||
let mut total = bytes_read;
|
||||
while total < size {
|
||||
let n = file.read(&mut buf[total..])?;
|
||||
if n == 0 {
|
||||
return Err(VfsError::NotFound(format!(
|
||||
"Short read: got {} of {} bytes for {}",
|
||||
total, size, path
|
||||
)));
|
||||
}
|
||||
total += n;
|
||||
}
|
||||
}
|
||||
Ok(buf)
|
||||
}
|
||||
|
||||
fn stat(&self, _path: &str) -> Result<VfsEntry, VfsError> {
|
||||
Err(VfsError::NotFound("Not yet implemented".into()))
|
||||
fn stat(&self, path: &str) -> Result<VfsEntry, VfsError> {
|
||||
let entries = self.read_entries();
|
||||
entries.into_iter()
|
||||
.find(|e| e.name.eq_ignore_ascii_case(path))
|
||||
.ok_or_else(|| VfsError::NotFound(path.to_string()))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,3 +11,7 @@ tracing = { workspace = true }
|
||||
byteorder = { workspace = true }
|
||||
thiserror = { workspace = true }
|
||||
anyhow = { workspace = true }
|
||||
aes = { workspace = true }
|
||||
|
||||
[build-dependencies]
|
||||
cc = "1"
|
||||
|
||||
18
xenia-rs/crates/xenia-xex/build.rs
Normal file
18
xenia-rs/crates/xenia-xex/build.rs
Normal file
@@ -0,0 +1,18 @@
|
||||
fn main() {
|
||||
let mspack_dir = std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
|
||||
.join("..")
|
||||
.join("..")
|
||||
.join("..")
|
||||
.join("third_party")
|
||||
.join("mspack");
|
||||
|
||||
cc::Build::new()
|
||||
.file("lzx_wrapper.c")
|
||||
.file(mspack_dir.join("lzxd.c"))
|
||||
.file(mspack_dir.join("system.c"))
|
||||
.include(&mspack_dir)
|
||||
.define("HAVE_CONFIG_H", None)
|
||||
.define("SIZEOF_OFF_T", "8")
|
||||
.warnings(false)
|
||||
.compile("mspack_lzx");
|
||||
}
|
||||
143
xenia-rs/crates/xenia-xex/lzx_wrapper.c
Normal file
143
xenia-rs/crates/xenia-xex/lzx_wrapper.c
Normal file
@@ -0,0 +1,143 @@
|
||||
/*
|
||||
* Thin C wrapper around mspack's LZX decompressor for use from Rust FFI.
|
||||
* This provides a simple buffer-to-buffer decompression function.
|
||||
*/
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <stdint.h>
|
||||
#include <stdio.h>
|
||||
|
||||
/* Stub for xenia_log (referenced by lzxd.c debug macros) */
|
||||
void xenia_log(const char *fmt, ...) {
|
||||
(void)fmt;
|
||||
}
|
||||
|
||||
/* Pull in mspack headers from xenia's third_party */
|
||||
#define HAVE_CONFIG_H
|
||||
#include "config.h"
|
||||
#include "mspack.h"
|
||||
#include "system.h"
|
||||
#include "lzx.h"
|
||||
|
||||
/* Memory-backed file for mspack I/O */
|
||||
typedef struct {
|
||||
struct mspack_system sys;
|
||||
void *buffer;
|
||||
off_t buffer_size;
|
||||
off_t offset;
|
||||
} mspack_memory_file;
|
||||
|
||||
static struct mspack_file *mem_open(struct mspack_system *self, const char *fn, int mode) {
|
||||
(void)self; (void)fn; (void)mode;
|
||||
return NULL;
|
||||
}
|
||||
static void mem_close(struct mspack_file *file) { (void)file; }
|
||||
|
||||
static int mem_read(struct mspack_file *file, void *buffer, int chars) {
|
||||
mspack_memory_file *memfile = (mspack_memory_file *)file;
|
||||
off_t remaining = memfile->buffer_size - memfile->offset;
|
||||
off_t total = (off_t)chars < remaining ? (off_t)chars : remaining;
|
||||
memcpy(buffer, (uint8_t *)memfile->buffer + memfile->offset, total);
|
||||
memfile->offset += total;
|
||||
return (int)total;
|
||||
}
|
||||
|
||||
static int mem_write(struct mspack_file *file, void *buffer, int chars) {
|
||||
mspack_memory_file *memfile = (mspack_memory_file *)file;
|
||||
off_t remaining = memfile->buffer_size - memfile->offset;
|
||||
off_t total = (off_t)chars < remaining ? (off_t)chars : remaining;
|
||||
memcpy((uint8_t *)memfile->buffer + memfile->offset, buffer, total);
|
||||
memfile->offset += total;
|
||||
return (int)total;
|
||||
}
|
||||
|
||||
static int mem_seek(struct mspack_file *file, off_t offset, int mode) {
|
||||
(void)file; (void)offset; (void)mode;
|
||||
return -1;
|
||||
}
|
||||
|
||||
static off_t mem_tell(struct mspack_file *file) {
|
||||
(void)file;
|
||||
return 0;
|
||||
}
|
||||
|
||||
static void mem_msg(struct mspack_file *file, const char *format, ...) {
|
||||
(void)file; (void)format;
|
||||
}
|
||||
|
||||
static void *mem_alloc(struct mspack_system *self, size_t bytes) {
|
||||
(void)self;
|
||||
return calloc(bytes, 1);
|
||||
}
|
||||
|
||||
static void mem_free(void *ptr) { free(ptr); }
|
||||
|
||||
static void mem_copy(void *src, void *dest, size_t bytes) {
|
||||
memcpy(dest, src, bytes);
|
||||
}
|
||||
|
||||
/*
|
||||
* Decompress LZX data from a memory buffer.
|
||||
* Returns 0 on success, non-zero on error.
|
||||
*/
|
||||
int xenia_lzx_decompress(
|
||||
const void *lzx_data, uint32_t lzx_len,
|
||||
void *dest, uint32_t dest_len,
|
||||
uint32_t window_size)
|
||||
{
|
||||
/* Calculate window_bits from window_size (find the bit position) */
|
||||
uint32_t window_bits = 0;
|
||||
uint32_t tmp = window_size;
|
||||
while (tmp > 1) {
|
||||
tmp >>= 1;
|
||||
window_bits++;
|
||||
}
|
||||
if ((1u << window_bits) != window_size || window_bits < 15 || window_bits > 21) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
/* Set up mspack memory system */
|
||||
struct mspack_system sys;
|
||||
memset(&sys, 0, sizeof(sys));
|
||||
sys.open = mem_open;
|
||||
sys.close = mem_close;
|
||||
sys.read = mem_read;
|
||||
sys.write = mem_write;
|
||||
sys.seek = mem_seek;
|
||||
sys.tell = mem_tell;
|
||||
sys.message = mem_msg;
|
||||
sys.alloc = mem_alloc;
|
||||
sys.free = mem_free;
|
||||
sys.copy = mem_copy;
|
||||
|
||||
mspack_memory_file src_file;
|
||||
memset(&src_file, 0, sizeof(src_file));
|
||||
src_file.buffer = (void *)lzx_data;
|
||||
src_file.buffer_size = (off_t)lzx_len;
|
||||
src_file.offset = 0;
|
||||
|
||||
mspack_memory_file dst_file;
|
||||
memset(&dst_file, 0, sizeof(dst_file));
|
||||
dst_file.buffer = dest;
|
||||
dst_file.buffer_size = (off_t)dest_len;
|
||||
dst_file.offset = 0;
|
||||
|
||||
struct lzxd_stream *lzxd = lzxd_init(
|
||||
&sys,
|
||||
(struct mspack_file *)&src_file,
|
||||
(struct mspack_file *)&dst_file,
|
||||
(int)window_bits,
|
||||
0, /* reset_interval: 0 = never reset */
|
||||
0x8000, /* input_buffer_size */
|
||||
(off_t)dest_len,
|
||||
0 /* is_delta */
|
||||
);
|
||||
|
||||
if (!lzxd) {
|
||||
return 2;
|
||||
}
|
||||
|
||||
int result = lzxd_decompress(lzxd, (off_t)dest_len);
|
||||
lzxd_free(lzxd);
|
||||
return result;
|
||||
}
|
||||
@@ -8,6 +8,10 @@ pub struct Xex2Header {
|
||||
pub header_count: u32,
|
||||
pub optional_headers: Vec<Xex2OptionalHeader>,
|
||||
pub security_info: Option<Xex2SecurityInfo>,
|
||||
/// Parsed file format info (if present).
|
||||
pub file_format_info: Option<FileFormatInfo>,
|
||||
/// Parsed import libraries.
|
||||
pub import_libraries: Vec<ImportLibrary>,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
@@ -22,6 +26,8 @@ pub struct Xex2SecurityInfo {
|
||||
pub load_address: u32,
|
||||
pub export_table_address: u32,
|
||||
pub image_flags: u32,
|
||||
/// Encrypted session key (decrypted with retail/devkit key to get actual session key).
|
||||
pub aes_key: [u8; 16],
|
||||
pub page_descriptors: Vec<Xex2PageDescriptor>,
|
||||
}
|
||||
|
||||
@@ -40,9 +46,49 @@ impl Xex2PageDescriptor {
|
||||
}
|
||||
}
|
||||
|
||||
/// File format info (compression and encryption types).
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct FileFormatInfo {
|
||||
pub info_size: u32,
|
||||
pub encryption_type: u16,
|
||||
pub compression_type: u16,
|
||||
/// For basic compression: list of (data_size, zero_size) block pairs.
|
||||
pub basic_blocks: Vec<BasicCompressionBlock>,
|
||||
/// For normal (LZX) compression: window size.
|
||||
pub normal_window_size: u32,
|
||||
/// For normal (LZX) compression: first block size (from header).
|
||||
pub normal_first_block_size: u32,
|
||||
/// For normal (LZX) compression: first block hash (from header).
|
||||
pub normal_first_block_hash: [u8; 20],
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub struct BasicCompressionBlock {
|
||||
pub data_size: u32,
|
||||
pub zero_size: u32,
|
||||
}
|
||||
|
||||
/// An imported library with its ordinals.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ImportLibrary {
|
||||
pub name: String,
|
||||
pub version_min: u32,
|
||||
pub version_cur: u32,
|
||||
pub ordinals: Vec<u32>,
|
||||
}
|
||||
|
||||
/// XEX2 magic: "XEX2"
|
||||
pub const XEX2_MAGIC: u32 = 0x58455832;
|
||||
|
||||
/// Compression types
|
||||
pub const COMPRESSION_NONE: u16 = 0;
|
||||
pub const COMPRESSION_BASIC: u16 = 1;
|
||||
pub const COMPRESSION_NORMAL: u16 = 2;
|
||||
|
||||
/// Encryption types
|
||||
pub const ENCRYPTION_NONE: u16 = 0;
|
||||
pub const ENCRYPTION_NORMAL: u16 = 1;
|
||||
|
||||
/// Optional header keys
|
||||
pub mod header_keys {
|
||||
pub const ENTRY_POINT: u32 = 0x00010100;
|
||||
@@ -50,6 +96,7 @@ pub mod header_keys {
|
||||
pub const IMPORT_LIBRARIES: u32 = 0x000103FF;
|
||||
pub const TLS_INFO: u32 = 0x00020200;
|
||||
pub const EXECUTION_INFO: u32 = 0x00040006;
|
||||
pub const DEFAULT_STACK_SIZE: u32 = 0x00020200;
|
||||
pub const DEFAULT_STACK_SIZE: u32 = 0x00020104;
|
||||
pub const ORIGINAL_PE_NAME: u32 = 0x000183FF;
|
||||
pub const FILE_FORMAT_INFO: u32 = 0x000003FF;
|
||||
}
|
||||
|
||||
@@ -1,7 +1,19 @@
|
||||
use crate::header::*;
|
||||
use aes::cipher::{BlockDecrypt, KeyInit};
|
||||
use aes::Aes128;
|
||||
use byteorder::{BigEndian, ReadBytesExt};
|
||||
use std::io::{self, Cursor, Read, Seek, SeekFrom};
|
||||
|
||||
unsafe extern "C" {
|
||||
fn xenia_lzx_decompress(
|
||||
lzx_data: *const std::ffi::c_void,
|
||||
lzx_len: u32,
|
||||
dest: *mut std::ffi::c_void,
|
||||
dest_len: u32,
|
||||
window_size: u32,
|
||||
) -> i32;
|
||||
}
|
||||
|
||||
/// Parse a XEX2 header from raw file data.
|
||||
pub fn parse_xex2_header(data: &[u8]) -> io::Result<Xex2Header> {
|
||||
let mut cursor = Cursor::new(data);
|
||||
@@ -35,6 +47,12 @@ pub fn parse_xex2_header(data: &[u8]) -> io::Result<Xex2Header> {
|
||||
None
|
||||
};
|
||||
|
||||
// Parse file format info
|
||||
let file_format_info = parse_file_format_info(data, &optional_headers);
|
||||
|
||||
// Parse import libraries
|
||||
let import_libraries = parse_import_libraries(data, &optional_headers);
|
||||
|
||||
Ok(Xex2Header {
|
||||
magic,
|
||||
module_flags,
|
||||
@@ -43,31 +61,74 @@ pub fn parse_xex2_header(data: &[u8]) -> io::Result<Xex2Header> {
|
||||
header_count,
|
||||
optional_headers,
|
||||
security_info,
|
||||
file_format_info,
|
||||
import_libraries,
|
||||
})
|
||||
}
|
||||
|
||||
fn parse_security_info(cursor: &mut Cursor<&[u8]>) -> io::Result<Xex2SecurityInfo> {
|
||||
let _header_size = cursor.read_u32::<BigEndian>()?;
|
||||
let image_size = cursor.read_u32::<BigEndian>()?;
|
||||
// xex2_security_info layout (from xex2_info.h):
|
||||
// 0x000: header_size (u32)
|
||||
// 0x004: image_size (u32)
|
||||
// 0x008: rsa_signature (0x100 bytes)
|
||||
// 0x108: unk_108 (u32)
|
||||
// 0x10C: image_flags (u32)
|
||||
// 0x110: load_address (u32)
|
||||
// 0x114: section_digest (0x14 bytes)
|
||||
// 0x128: import_table_count (u32)
|
||||
// 0x12C: import_table_digest (0x14 bytes)
|
||||
// 0x140: xgd2_media_id (0x10 bytes)
|
||||
// 0x150: aes_key (0x10 bytes)
|
||||
// 0x160: export_table (u32)
|
||||
// 0x164: header_digest (0x14 bytes)
|
||||
// 0x178: region (u32)
|
||||
// 0x17C: allowed_media_types (u32)
|
||||
// 0x180: page_descriptor_count (u32)
|
||||
// 0x184: page_descriptors[] (each is 0x18 bytes: u32 value + 0x14 digest)
|
||||
|
||||
// Skip RSA signature (256 bytes) and other security fields
|
||||
let mut skip_buf = [0u8; 256];
|
||||
cursor.read_exact(&mut skip_buf)?;
|
||||
let _header_size = cursor.read_u32::<BigEndian>()?; // 0x000
|
||||
let image_size = cursor.read_u32::<BigEndian>()?; // 0x004
|
||||
|
||||
// Skip image info hash (20 bytes) and import table hash (20 bytes)
|
||||
cursor.read_exact(&mut [0u8; 20])?;
|
||||
cursor.read_exact(&mut [0u8; 20])?;
|
||||
// Skip RSA signature (0x100 bytes)
|
||||
let mut rsa_sig = [0u8; 0x100];
|
||||
cursor.read_exact(&mut rsa_sig)?; // 0x008
|
||||
|
||||
let load_address = cursor.read_u32::<BigEndian>()?;
|
||||
let _load_size = cursor.read_u32::<BigEndian>()?;
|
||||
let export_table_address = cursor.read_u32::<BigEndian>()?;
|
||||
let image_flags = cursor.read_u32::<BigEndian>()?;
|
||||
let _unk_108 = cursor.read_u32::<BigEndian>()?; // 0x108
|
||||
let image_flags = cursor.read_u32::<BigEndian>()?; // 0x10C
|
||||
let load_address = cursor.read_u32::<BigEndian>()?; // 0x110
|
||||
|
||||
// Skip section_digest (0x14 bytes)
|
||||
let mut digest = [0u8; 0x14];
|
||||
cursor.read_exact(&mut digest)?; // 0x114
|
||||
|
||||
let _import_table_count = cursor.read_u32::<BigEndian>()?; // 0x128
|
||||
|
||||
// Skip import_table_digest (0x14 bytes)
|
||||
cursor.read_exact(&mut digest)?; // 0x12C
|
||||
|
||||
// Skip xgd2_media_id (0x10 bytes)
|
||||
let mut media_id = [0u8; 0x10];
|
||||
cursor.read_exact(&mut media_id)?; // 0x140
|
||||
|
||||
// Read aes_key (0x10 bytes)
|
||||
let mut aes_key = [0u8; 0x10];
|
||||
cursor.read_exact(&mut aes_key)?; // 0x150
|
||||
|
||||
let export_table_address = cursor.read_u32::<BigEndian>()?; // 0x160
|
||||
|
||||
// Skip header_digest (0x14 bytes)
|
||||
cursor.read_exact(&mut digest)?; // 0x164
|
||||
|
||||
let _region = cursor.read_u32::<BigEndian>()?; // 0x178
|
||||
let _allowed_media = cursor.read_u32::<BigEndian>()?; // 0x17C
|
||||
|
||||
let page_descriptor_count = cursor.read_u32::<BigEndian>()?; // 0x180
|
||||
|
||||
// Read page descriptor count
|
||||
let page_descriptor_count = cursor.read_u32::<BigEndian>()?;
|
||||
let mut page_descriptors = Vec::new();
|
||||
for _ in 0..page_descriptor_count {
|
||||
let size_and_info = cursor.read_u32::<BigEndian>()?;
|
||||
// Skip data_digest (0x14 bytes per descriptor)
|
||||
cursor.read_exact(&mut digest)?;
|
||||
page_descriptors.push(Xex2PageDescriptor { size_and_info });
|
||||
}
|
||||
|
||||
@@ -76,10 +137,144 @@ fn parse_security_info(cursor: &mut Cursor<&[u8]>) -> io::Result<Xex2SecurityInf
|
||||
load_address,
|
||||
export_table_address,
|
||||
image_flags,
|
||||
aes_key,
|
||||
page_descriptors,
|
||||
})
|
||||
}
|
||||
|
||||
/// Parse file format info from the optional header data.
|
||||
fn parse_file_format_info(data: &[u8], headers: &[Xex2OptionalHeader]) -> Option<FileFormatInfo> {
|
||||
// The key format: low 8 bits indicate the data size category
|
||||
// 0xFF = data offset is a pointer to variable-size data in the header area
|
||||
let header = headers.iter().find(|h| h.key == header_keys::FILE_FORMAT_INFO)?;
|
||||
let offset = header.value as usize;
|
||||
if offset + 8 > data.len() {
|
||||
return None;
|
||||
}
|
||||
|
||||
let mut cursor = Cursor::new(data);
|
||||
cursor.seek(SeekFrom::Start(offset as u64)).ok()?;
|
||||
|
||||
let info_size = cursor.read_u32::<BigEndian>().ok()?;
|
||||
let encryption_type = cursor.read_u16::<BigEndian>().ok()?;
|
||||
let compression_type = cursor.read_u16::<BigEndian>().ok()?;
|
||||
|
||||
let mut basic_blocks = Vec::new();
|
||||
let mut normal_window_size = 0u32;
|
||||
let mut normal_first_block_size = 0u32;
|
||||
let mut normal_first_block_hash = [0u8; 20];
|
||||
|
||||
match compression_type {
|
||||
COMPRESSION_BASIC => {
|
||||
// Basic compression blocks: (data_size, zero_size) pairs
|
||||
// Number of blocks = (info_size - 8) / 8
|
||||
let block_count = if info_size > 8 { (info_size - 8) / 8 } else { 0 };
|
||||
for _ in 0..block_count {
|
||||
let data_size = cursor.read_u32::<BigEndian>().ok()?;
|
||||
let zero_size = cursor.read_u32::<BigEndian>().ok()?;
|
||||
basic_blocks.push(BasicCompressionBlock { data_size, zero_size });
|
||||
}
|
||||
}
|
||||
COMPRESSION_NORMAL => {
|
||||
normal_window_size = cursor.read_u32::<BigEndian>().ok()?;
|
||||
// Read first_block: block_size (4) + block_hash (20)
|
||||
normal_first_block_size = cursor.read_u32::<BigEndian>().ok()?;
|
||||
cursor.read_exact(&mut normal_first_block_hash).ok()?;
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
Some(FileFormatInfo {
|
||||
info_size,
|
||||
encryption_type,
|
||||
compression_type,
|
||||
basic_blocks,
|
||||
normal_window_size,
|
||||
normal_first_block_size,
|
||||
normal_first_block_hash,
|
||||
})
|
||||
}
|
||||
|
||||
/// Parse import libraries from the optional header data.
|
||||
fn parse_import_libraries(data: &[u8], headers: &[Xex2OptionalHeader]) -> Vec<ImportLibrary> {
|
||||
let header = match headers.iter().find(|h| h.key == header_keys::IMPORT_LIBRARIES) {
|
||||
Some(h) => h,
|
||||
None => return Vec::new(),
|
||||
};
|
||||
|
||||
let offset = header.value as usize;
|
||||
if offset + 4 > data.len() {
|
||||
return Vec::new();
|
||||
}
|
||||
|
||||
let mut cursor = Cursor::new(data);
|
||||
if cursor.seek(SeekFrom::Start(offset as u64)).is_err() {
|
||||
return Vec::new();
|
||||
}
|
||||
|
||||
let mut libraries = Vec::new();
|
||||
|
||||
// Import libraries header: total_size (4), string_table_size (4), string_count (4)
|
||||
let _total_size = match cursor.read_u32::<BigEndian>() { Ok(v) => v, Err(_) => return libraries };
|
||||
let string_table_size = match cursor.read_u32::<BigEndian>() { Ok(v) => v, Err(_) => return libraries };
|
||||
let string_count = match cursor.read_u32::<BigEndian>() { Ok(v) => v, Err(_) => return libraries };
|
||||
|
||||
// Read string table
|
||||
let string_table_start = cursor.position() as usize;
|
||||
let mut names = Vec::new();
|
||||
for _ in 0..string_count {
|
||||
let mut name = String::new();
|
||||
loop {
|
||||
let b = match cursor.read_u8() { Ok(v) => v, Err(_) => break };
|
||||
if b == 0 { break; }
|
||||
name.push(b as char);
|
||||
}
|
||||
names.push(name);
|
||||
}
|
||||
|
||||
// Align to end of string table
|
||||
let string_table_end = string_table_start + string_table_size as usize;
|
||||
if string_table_end > data.len() {
|
||||
return libraries;
|
||||
}
|
||||
let _ = cursor.seek(SeekFrom::Start(string_table_end as u64));
|
||||
|
||||
// Read library records
|
||||
// Each record: size(4), next_import_digest(20 bytes), id(4), version(4), version_min(4),
|
||||
// name_index(2), record_count(2), ordinals(record_count * 4)
|
||||
for _ in 0..names.len() {
|
||||
let lib_size = match cursor.read_u32::<BigEndian>() { Ok(v) => v, Err(_) => break };
|
||||
if lib_size < 40 { break; }
|
||||
|
||||
// Skip digest (20 bytes)
|
||||
let mut digest = [0u8; 20];
|
||||
if cursor.read_exact(&mut digest).is_err() { break; }
|
||||
|
||||
let _id = cursor.read_u32::<BigEndian>().unwrap_or(0);
|
||||
let version_cur = cursor.read_u32::<BigEndian>().unwrap_or(0);
|
||||
let version_min = cursor.read_u32::<BigEndian>().unwrap_or(0);
|
||||
let name_index = cursor.read_u16::<BigEndian>().unwrap_or(0);
|
||||
let record_count = cursor.read_u16::<BigEndian>().unwrap_or(0);
|
||||
|
||||
let name = names.get(name_index as usize).cloned().unwrap_or_default();
|
||||
|
||||
let mut ordinals = Vec::new();
|
||||
for _ in 0..record_count {
|
||||
let ordinal = cursor.read_u32::<BigEndian>().unwrap_or(0);
|
||||
ordinals.push(ordinal);
|
||||
}
|
||||
|
||||
libraries.push(ImportLibrary {
|
||||
name,
|
||||
version_min,
|
||||
version_cur,
|
||||
ordinals,
|
||||
});
|
||||
}
|
||||
|
||||
libraries
|
||||
}
|
||||
|
||||
/// Get an optional header value by key.
|
||||
pub fn get_opt_header(header: &Xex2Header, key: u32) -> Option<u32> {
|
||||
header.optional_headers.iter()
|
||||
@@ -96,3 +291,231 @@ pub fn get_entry_point(header: &Xex2Header) -> Option<u32> {
|
||||
pub fn get_image_base(header: &Xex2Header) -> Option<u32> {
|
||||
get_opt_header(header, header_keys::IMAGE_BASE_ADDRESS)
|
||||
}
|
||||
|
||||
/// Get the default stack size.
|
||||
pub fn get_stack_size(header: &Xex2Header) -> u32 {
|
||||
get_opt_header(header, header_keys::DEFAULT_STACK_SIZE).unwrap_or(0x10_0000) // Default 1MB
|
||||
}
|
||||
|
||||
/// Load the XEX image data into a flat buffer (decompressing if needed).
|
||||
/// Returns the decompressed image bytes ready to map into guest memory.
|
||||
pub fn load_image(data: &[u8], header: &Xex2Header) -> io::Result<Vec<u8>> {
|
||||
let source = &data[header.header_size as usize..];
|
||||
|
||||
match &header.file_format_info {
|
||||
Some(info) if info.compression_type == COMPRESSION_BASIC => {
|
||||
load_basic_compressed(source, info)
|
||||
}
|
||||
Some(info) if info.compression_type == COMPRESSION_NORMAL => {
|
||||
load_normal_compressed(source, info, header)
|
||||
}
|
||||
_ => {
|
||||
// Uncompressed (or no format info = treat as uncompressed)
|
||||
Ok(source.to_vec())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Load basic compressed image data.
|
||||
fn load_basic_compressed(source: &[u8], info: &FileFormatInfo) -> io::Result<Vec<u8>> {
|
||||
// Calculate total uncompressed size
|
||||
let total_size: u64 = info.basic_blocks.iter()
|
||||
.map(|b| b.data_size as u64 + b.zero_size as u64)
|
||||
.sum();
|
||||
|
||||
let mut output = vec![0u8; total_size as usize];
|
||||
let mut src_offset = 0usize;
|
||||
let mut dst_offset = 0usize;
|
||||
|
||||
for block in &info.basic_blocks {
|
||||
let data_size = block.data_size as usize;
|
||||
let zero_size = block.zero_size as usize;
|
||||
|
||||
if src_offset + data_size > source.len() {
|
||||
return Err(io::Error::new(
|
||||
io::ErrorKind::UnexpectedEof,
|
||||
format!("Basic compression block data extends past end of file (src_offset={:#x}, data_size={:#x}, source_len={:#x})",
|
||||
src_offset, data_size, source.len()),
|
||||
));
|
||||
}
|
||||
|
||||
// Copy data block
|
||||
if dst_offset + data_size <= output.len() {
|
||||
output[dst_offset..dst_offset + data_size]
|
||||
.copy_from_slice(&source[src_offset..src_offset + data_size]);
|
||||
}
|
||||
src_offset += data_size;
|
||||
dst_offset += data_size;
|
||||
|
||||
// Zero-filled gap (already zeroed from vec initialization)
|
||||
dst_offset += zero_size;
|
||||
}
|
||||
|
||||
Ok(output)
|
||||
}
|
||||
|
||||
/// Xbox 360 retail AES key for XEX2 session key decryption.
|
||||
const XEX2_RETAIL_KEY: [u8; 16] = [
|
||||
0x20, 0xB1, 0x85, 0xA5, 0x9D, 0x28, 0xFD, 0xC3,
|
||||
0x40, 0x58, 0x3F, 0xBB, 0x08, 0x96, 0xBF, 0x91,
|
||||
];
|
||||
|
||||
/// Xbox 360 devkit AES key (all zeros).
|
||||
#[allow(dead_code)]
|
||||
const XEX2_DEVKIT_KEY: [u8; 16] = [0u8; 16];
|
||||
|
||||
/// AES-128-CBC decryption with zero IV (matching Xbox 360 XEX decryption).
|
||||
fn aes_decrypt_cbc(key: &[u8; 16], input: &[u8]) -> Vec<u8> {
|
||||
let cipher = Aes128::new(key.into());
|
||||
let mut output = vec![0u8; input.len()];
|
||||
let mut iv = [0u8; 16];
|
||||
|
||||
for (i, chunk) in input.chunks(16).enumerate() {
|
||||
if chunk.len() < 16 {
|
||||
// Partial block at end - copy as-is
|
||||
output[i * 16..i * 16 + chunk.len()].copy_from_slice(chunk);
|
||||
break;
|
||||
}
|
||||
let mut block = aes::Block::clone_from_slice(chunk);
|
||||
cipher.decrypt_block(&mut block);
|
||||
// XOR with IV (previous ciphertext block)
|
||||
for j in 0..16 {
|
||||
block[j] ^= iv[j];
|
||||
}
|
||||
iv.copy_from_slice(chunk);
|
||||
output[i * 16..(i + 1) * 16].copy_from_slice(&block);
|
||||
}
|
||||
|
||||
output
|
||||
}
|
||||
|
||||
/// Derive the session key by decrypting the XEX's aes_key field with the retail key.
|
||||
/// Falls back to devkit key if retail produces invalid results.
|
||||
fn derive_session_key(header: &Xex2Header) -> [u8; 16] {
|
||||
let sec = match &header.security_info {
|
||||
Some(s) => s,
|
||||
None => return [0u8; 16],
|
||||
};
|
||||
|
||||
let decrypted = aes_decrypt_cbc(&XEX2_RETAIL_KEY, &sec.aes_key);
|
||||
let mut session_key = [0u8; 16];
|
||||
session_key.copy_from_slice(&decrypted[..16]);
|
||||
session_key
|
||||
}
|
||||
|
||||
/// De-block compressed data: strip block headers and extract chunk payloads.
|
||||
///
|
||||
/// The first block's size comes from the file format header (first_block_size).
|
||||
/// Each block in the data starts with a block_info struct for the NEXT block:
|
||||
/// - block_size: u32 BE (size of the next block)
|
||||
/// - block_hash: [u8; 20] (SHA1 of the next block)
|
||||
/// Followed by chunks: { chunk_size: u16 BE, data: [u8; chunk_size] }, terminated by chunk_size=0
|
||||
fn deblock(input: &[u8], first_block_size: u32) -> io::Result<Vec<u8>> {
|
||||
let mut output = Vec::new();
|
||||
let mut pos = 0usize;
|
||||
let mut cur_block_size = first_block_size as usize;
|
||||
|
||||
while cur_block_size > 0 && pos < input.len() {
|
||||
let next_block_pos = pos + cur_block_size;
|
||||
|
||||
// Read next block's info from start of current block data
|
||||
let next_block_size = if pos + 4 <= input.len() {
|
||||
u32::from_be_bytes([
|
||||
input[pos], input[pos + 1], input[pos + 2], input[pos + 3],
|
||||
]) as usize
|
||||
} else {
|
||||
0
|
||||
};
|
||||
|
||||
// Skip block_info header (4 bytes size + 20 bytes hash)
|
||||
let mut p = pos + 4 + 20;
|
||||
|
||||
// Read chunks within this block
|
||||
loop {
|
||||
if p + 2 > input.len() {
|
||||
break;
|
||||
}
|
||||
let chunk_size = ((input[p] as usize) << 8) | (input[p + 1] as usize);
|
||||
p += 2;
|
||||
if chunk_size == 0 {
|
||||
break;
|
||||
}
|
||||
if p + chunk_size > input.len() {
|
||||
return Err(io::Error::new(
|
||||
io::ErrorKind::UnexpectedEof,
|
||||
format!("De-block chunk extends past input (pos={:#x}, chunk_size={:#x}, input_len={:#x})",
|
||||
p, chunk_size, input.len()),
|
||||
));
|
||||
}
|
||||
output.extend_from_slice(&input[p..p + chunk_size]);
|
||||
p += chunk_size;
|
||||
}
|
||||
|
||||
if next_block_pos <= pos {
|
||||
break; // Prevent infinite loop
|
||||
}
|
||||
pos = next_block_pos;
|
||||
cur_block_size = next_block_size;
|
||||
}
|
||||
|
||||
Ok(output)
|
||||
}
|
||||
|
||||
/// Load normal (LZX) compressed image data.
|
||||
/// Pipeline: decrypt → de-block → LZX decompress
|
||||
fn load_normal_compressed(source: &[u8], info: &FileFormatInfo, header: &Xex2Header) -> io::Result<Vec<u8>> {
|
||||
let uncompressed_size = header.security_info.as_ref()
|
||||
.map(|s| s.image_size as usize)
|
||||
.unwrap_or(0);
|
||||
|
||||
if uncompressed_size == 0 {
|
||||
return Err(io::Error::new(
|
||||
io::ErrorKind::InvalidData,
|
||||
"Cannot decompress: image_size is 0",
|
||||
));
|
||||
}
|
||||
|
||||
// Step 1: Decrypt if needed
|
||||
let decrypted;
|
||||
let input = if info.encryption_type == ENCRYPTION_NORMAL {
|
||||
let session_key = derive_session_key(header);
|
||||
decrypted = aes_decrypt_cbc(&session_key, source);
|
||||
&decrypted
|
||||
} else {
|
||||
source
|
||||
};
|
||||
|
||||
// Step 2: De-block (strip block headers, extract chunk payloads)
|
||||
let deblocked = deblock(input, info.normal_first_block_size)?;
|
||||
|
||||
if deblocked.is_empty() {
|
||||
return Err(io::Error::new(
|
||||
io::ErrorKind::InvalidData,
|
||||
"De-blocking produced no data",
|
||||
));
|
||||
}
|
||||
|
||||
// Step 3: LZX decompress using mspack C library
|
||||
let mut output = vec![0u8; uncompressed_size];
|
||||
|
||||
let result = unsafe {
|
||||
xenia_lzx_decompress(
|
||||
deblocked.as_ptr() as *const std::ffi::c_void,
|
||||
deblocked.len() as u32,
|
||||
output.as_mut_ptr() as *mut std::ffi::c_void,
|
||||
uncompressed_size as u32,
|
||||
info.normal_window_size,
|
||||
)
|
||||
};
|
||||
|
||||
if result != 0 {
|
||||
return Err(io::Error::new(
|
||||
io::ErrorKind::InvalidData,
|
||||
format!("LZX decompression failed (mspack error code {})", result),
|
||||
));
|
||||
}
|
||||
|
||||
tracing::info!("LZX decompressed: {} -> {} bytes", deblocked.len(), uncompressed_size);
|
||||
|
||||
Ok(output)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user