[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:
@@ -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