Initial commit: xenia-rs workspace for Xbox 360 RE

Rust reimplementation of the xenia Xbox 360 emulator targeting reverse-
engineering and preservation, initially scoped to Project Sylpheed.

Includes:
- XEX2 loader (LZX decompression, AES decryption, PE parsing)
- XISO / XGD2 disc image VFS
- PPC interpreter with 200+ opcodes and VMX128 decoding
- Static analyzer: functions, cross-references, labels, asm + SQLite output
- HLE kernel covering the xboxkrnl/xam subset used by Sylpheed init
- Debugger with in-memory and SQLite-backed execution tracing
- `xenia-rs` CLI with extract/dis/exec commands that produce cumulative,
  superset SQLite databases and opt-in instruction/import/branch traces

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
MechaCat02
2026-04-16 23:11:49 +02:00
commit c694bb3f43
63 changed files with 13456 additions and 0 deletions

View File

@@ -0,0 +1,17 @@
[package]
name = "xenia-xex"
version.workspace = true
edition.workspace = true
license.workspace = true
[dependencies]
xenia-types = { workspace = true }
xenia-memory = { workspace = true }
tracing = { workspace = true }
byteorder = { workspace = true }
thiserror = { workspace = true }
anyhow = { workspace = true }
aes = { workspace = true }
serde = { workspace = true }
serde_json = { workspace = true }

View File

@@ -0,0 +1 @@
fn main() {}

View File

@@ -0,0 +1,128 @@
use serde::Serialize;
/// XEX2 file header. Parsed from the beginning of an Xbox 360 executable.
#[derive(Debug, Serialize)]
pub struct Xex2Header {
pub magic: u32,
pub module_flags: u32,
pub header_size: u32,
pub security_offset: u32,
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 (addresses only until resolve_imports is called).
pub import_libraries: Vec<ImportLibrary>,
/// Execution info (title ID, media ID, etc.).
pub execution_info: Option<ExecutionInfo>,
/// Original PE name from the XEX header.
pub original_pe_name: Option<String>,
}
#[derive(Debug, Serialize)]
pub struct Xex2OptionalHeader {
pub key: u32,
pub value: u32,
}
#[derive(Debug, Serialize)]
pub struct Xex2SecurityInfo {
pub image_size: u32,
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>,
}
#[derive(Debug, Clone, Copy, Serialize)]
pub struct Xex2PageDescriptor {
pub size_and_info: u32,
}
impl Xex2PageDescriptor {
pub fn page_count(&self) -> u32 {
self.size_and_info >> 4
}
pub fn info(&self) -> u32 {
self.size_and_info & 0xF
}
}
/// File format info (compression and encryption types).
#[derive(Debug, Clone, Serialize)]
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, Serialize)]
pub struct BasicCompressionBlock {
pub data_size: u32,
pub zero_size: u32,
}
/// An imported library with its resolved imports.
#[derive(Debug, Clone, Serialize)]
pub struct ImportLibrary {
pub name: String,
pub id: u32,
pub version_min: u32,
pub version_cur: u32,
/// Import entries. Before `resolve_imports`, these contain addresses but no ordinals.
/// After `resolve_imports`, ordinals and record types are filled in from the PE image.
pub imports: Vec<ImportEntry>,
}
/// A single import entry within an import library.
#[derive(Debug, Clone, Serialize)]
pub struct ImportEntry {
pub ordinal: u16,
pub record_type: u8, // 0 = variable, 1 = thunk
pub address: u32,
}
/// Execution info parsed from the XEX header.
#[derive(Debug, Clone, Serialize)]
pub struct ExecutionInfo {
pub media_id: u32,
pub title_id: u32,
pub disc_number: u8,
pub disc_count: u8,
}
/// 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;
pub const IMAGE_BASE_ADDRESS: u32 = 0x00010201;
pub const IMPORT_LIBRARIES: u32 = 0x000103FF;
pub const TLS_INFO: u32 = 0x00020200;
pub const EXECUTION_INFO: u32 = 0x00040006;
pub const DEFAULT_STACK_SIZE: u32 = 0x00020104;
pub const ORIGINAL_PE_NAME: u32 = 0x000183FF;
pub const FILE_FORMAT_INFO: u32 = 0x000003FF;
}

View File

@@ -0,0 +1,6 @@
pub mod header;
pub mod loader;
pub mod lzx;
pub mod pe;
pub use header::Xex2Header;

View File

@@ -0,0 +1,571 @@
use crate::header::*;
use aes::cipher::{BlockDecrypt, KeyInit};
use aes::Aes128;
use byteorder::{BigEndian, ReadBytesExt};
use std::io::{self, Cursor, Read, Seek, SeekFrom};
/// Parse a XEX2 header from raw file data.
pub fn parse_xex2_header(data: &[u8]) -> io::Result<Xex2Header> {
let mut cursor = Cursor::new(data);
let magic = cursor.read_u32::<BigEndian>()?;
if magic != XEX2_MAGIC {
return Err(io::Error::new(
io::ErrorKind::InvalidData,
format!("Invalid XEX2 magic: {:#010x} (expected {:#010x})", magic, XEX2_MAGIC),
));
}
let module_flags = cursor.read_u32::<BigEndian>()?;
let header_size = cursor.read_u32::<BigEndian>()?;
let _reserved = cursor.read_u32::<BigEndian>()?;
let security_offset = cursor.read_u32::<BigEndian>()?;
let header_count = cursor.read_u32::<BigEndian>()?;
let mut optional_headers = Vec::new();
for _ in 0..header_count {
let key = cursor.read_u32::<BigEndian>()?;
let value = cursor.read_u32::<BigEndian>()?;
optional_headers.push(Xex2OptionalHeader { key, value });
}
// Parse security info
let security_info = if (security_offset as usize) < data.len() {
cursor.seek(SeekFrom::Start(security_offset as u64))?;
Some(parse_security_info(&mut cursor)?)
} else {
None
};
// Parse file format info
let file_format_info = parse_file_format_info(data, &optional_headers);
// Parse import libraries (addresses only; call resolve_imports after decompression)
let import_libraries = parse_import_libraries(data, &optional_headers);
// Parse execution info
let execution_info = parse_execution_info(data, &optional_headers);
// Parse original PE name
let original_pe_name = parse_original_pe_name(data, &optional_headers);
Ok(Xex2Header {
magic,
module_flags,
header_size,
security_offset,
header_count,
optional_headers,
security_info,
file_format_info,
import_libraries,
execution_info,
original_pe_name,
})
}
fn parse_security_info(cursor: &mut Cursor<&[u8]>) -> io::Result<Xex2SecurityInfo> {
// 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)
let _header_size = cursor.read_u32::<BigEndian>()?; // 0x000
let image_size = cursor.read_u32::<BigEndian>()?; // 0x004
// Skip RSA signature (0x100 bytes)
let mut rsa_sig = [0u8; 0x100];
cursor.read_exact(&mut rsa_sig)?; // 0x008
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
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 });
}
Ok(Xex2SecurityInfo {
image_size,
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.
/// At this stage, only record addresses are read; ordinals and record types
/// are resolved later by `resolve_imports` once the PE image is decompressed.
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 + 12 > data.len() {
return Vec::new();
}
fn be_u32(data: &[u8], off: usize) -> u32 {
u32::from_be_bytes([data[off], data[off+1], data[off+2], data[off+3]])
}
fn be_u16(data: &[u8], off: usize) -> u16 {
u16::from_be_bytes([data[off], data[off+1]])
}
let total_size = be_u32(data, offset) as usize;
let string_table_size = be_u32(data, offset + 4) as usize;
let string_count = be_u32(data, offset + 8) as usize;
// Parse string table (null-terminated, 4-byte aligned)
let string_data_start = offset + 12;
let mut strings = Vec::new();
let mut spos = 0usize;
for _ in 0..string_count {
let start = string_data_start + spos;
let mut end = start;
while end < data.len() && data[end] != 0 { end += 1; }
let name = std::str::from_utf8(&data[start..end]).unwrap_or("???").to_string();
spos += name.len() + 1;
// 4-byte alignment
if spos % 4 != 0 { spos += 4 - (spos % 4); }
strings.push(name);
}
// Parse libraries
let mut libs = Vec::new();
let mut lib_off = offset + 12 + string_table_size;
while lib_off + 0x28 <= data.len() && lib_off < offset + total_size {
let lib_size = be_u32(data, lib_off) as usize;
if lib_size == 0 { break; }
let id = be_u32(data, lib_off + 0x18);
let version_cur = be_u32(data, lib_off + 0x1C);
let version_min = be_u32(data, lib_off + 0x20);
let name_index = (be_u16(data, lib_off + 0x24) & 0xFF) as usize;
let count = be_u16(data, lib_off + 0x26) as usize;
let lib_name = strings.get(name_index).cloned().unwrap_or_else(|| format!("lib_{name_index}"));
let mut imports = Vec::new();
for i in 0..count {
let record_addr = be_u32(data, lib_off + 0x28 + i * 4);
imports.push(ImportEntry {
ordinal: 0,
record_type: 0xFF,
address: record_addr,
});
}
libs.push(ImportLibrary {
name: lib_name,
id,
version_min,
version_cur,
imports,
});
lib_off += lib_size;
}
libs
}
/// Resolve import ordinals and record types from the decompressed PE image.
/// Must be called after `load_image` provides the PE data.
pub fn resolve_imports(header: &mut Xex2Header, pe_image: &[u8]) {
let image_base = get_image_base(header).unwrap_or(0);
for lib in &mut header.import_libraries {
for imp in &mut lib.imports {
let pe_off = imp.address.wrapping_sub(image_base) as usize;
if pe_off + 4 <= pe_image.len() {
// PE image values are big-endian (Xbox 360 native)
let val = u32::from_be_bytes([
pe_image[pe_off], pe_image[pe_off+1],
pe_image[pe_off+2], pe_image[pe_off+3],
]);
imp.record_type = ((val >> 24) & 0xFF) as u8;
imp.ordinal = (val & 0xFFFF) as u16;
}
}
}
}
/// Parse execution info from optional header data.
fn parse_execution_info(data: &[u8], headers: &[Xex2OptionalHeader]) -> Option<ExecutionInfo> {
// EXECUTION_INFO key is 0x00040006 — the low byte 0x06 means the value
// is an inline struct of 6 u32 words (24 bytes total).
// Layout: media_id(4), version(4), base_version(4), title_id(4),
// platform(1), exec_type(1), disc_number(1), disc_count(1)
let header = headers.iter().find(|h| h.key == header_keys::EXECUTION_INFO)?;
let off = header.value as usize;
if off + 20 > data.len() {
return None;
}
let media_id = u32::from_be_bytes([data[off], data[off+1], data[off+2], data[off+3]]);
let title_id = u32::from_be_bytes([data[off+12], data[off+13], data[off+14], data[off+15]]);
let disc_number = data[off + 18];
let disc_count = data[off + 19];
Some(ExecutionInfo {
media_id,
title_id,
disc_number,
disc_count,
})
}
/// Parse original PE name from optional header data.
fn parse_original_pe_name(data: &[u8], headers: &[Xex2OptionalHeader]) -> Option<String> {
let header = headers.iter().find(|h| h.key == header_keys::ORIGINAL_PE_NAME)?;
let off = header.value as usize;
if off + 4 > data.len() {
return None;
}
let size = u32::from_be_bytes([data[off], data[off+1], data[off+2], data[off+3]]) as usize;
if off + size > data.len() || size <= 4 {
return None;
}
let name_bytes = &data[off + 4..off + size];
Some(String::from_utf8_lossy(name_bytes).trim_end_matches('\0').to_string())
}
/// Get an optional header value by key.
pub fn get_opt_header(header: &Xex2Header, key: u32) -> Option<u32> {
header.optional_headers.iter()
.find(|h| h.key == key)
.map(|h| h.value)
}
/// Get the entry point address from the XEX2 header.
pub fn get_entry_point(header: &Xex2Header) -> Option<u32> {
get_opt_header(header, header_keys::ENTRY_POINT)
}
/// Get the image base address.
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 (pure Rust)
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 pure Rust decoder
let window_bits = match info.normal_window_size {
s if s == 0 => 15, // default
s => (s as f64).log2() as u32,
};
let mut decoder = crate::lzx::LzxDecoder::new(window_bits);
let output = decoder.decompress(&deblocked, uncompressed_size)
.map_err(|e| io::Error::new(io::ErrorKind::InvalidData, format!("LZX decompression failed: {e}")))?;
tracing::info!("LZX decompressed: {} -> {} bytes", deblocked.len(), uncompressed_size);
Ok(output)
}

692
crates/xenia-xex/src/lzx.rs Normal file
View File

@@ -0,0 +1,692 @@
//! LZX decompressor for Xbox 360 XEX2 "normal compression".
//! Ported from libmspack lzxd.c (C) 2003-2013 Stuart Caie, LGPL 2.1.
use std::fmt;
// ── LZX constants ───────────────────────────────────────────────────────────
const LZX_MIN_MATCH: usize = 2;
const LZX_NUM_CHARS: usize = 256;
const LZX_BLOCKTYPE_VERBATIM: u8 = 1;
const LZX_BLOCKTYPE_ALIGNED: u8 = 2;
const LZX_BLOCKTYPE_UNCOMPRESSED: u8 = 3;
const LZX_NUM_PRIMARY_LENGTHS: usize = 7;
const LZX_NUM_SECONDARY_LENGTHS: usize = 249;
const LZX_FRAME_SIZE: usize = 32768;
const HUFF_MAXBITS: usize = 16;
const PRETREE_MAXSYMS: usize = 20;
const PRETREE_TABLEBITS: usize = 6;
const MAINTREE_MAXSYMS: usize = LZX_NUM_CHARS + 290 * 8; // 2576
const MAINTREE_TABLEBITS: usize = 12;
const LENGTH_MAXSYMS: usize = LZX_NUM_SECONDARY_LENGTHS + 1; // 250
const LENGTH_TABLEBITS: usize = 12;
const ALIGNED_MAXSYMS: usize = 8;
const ALIGNED_TABLEBITS: usize = 7;
const LENTABLE_SAFETY: usize = 64;
const BITBUF_WIDTH: u32 = 32;
// ── Static tables ───────────────────────────────────────────────────────────
static POSITION_SLOTS: [u32; 11] = [30, 32, 34, 36, 38, 42, 50, 66, 98, 162, 290];
static EXTRA_BITS: [u8; 36] = [
0, 0, 0, 0, 1, 1, 2, 2, 3, 3, 4, 4, 5, 5, 6, 6,
7, 7, 8, 8, 9, 9, 10, 10, 11, 11, 12, 12, 13, 13, 14, 14,
15, 15, 16, 16,
];
#[rustfmt::skip]
static POSITION_BASE: [u32; 290] = [
0, 1, 2, 3, 4, 6, 8, 12, 16, 24, 32, 48, 64, 96, 128, 192, 256, 384, 512,
768, 1024, 1536, 2048, 3072, 4096, 6144, 8192, 12288, 16384, 24576, 32768,
49152, 65536, 98304, 131072, 196608, 262144, 393216, 524288, 655360,
786432, 917504, 1048576, 1179648, 1310720, 1441792, 1572864, 1703936,
1835008, 1966080, 2097152, 2228224, 2359296, 2490368, 2621440, 2752512,
2883584, 3014656, 3145728, 3276800, 3407872, 3538944, 3670016, 3801088,
3932160, 4063232, 4194304, 4325376, 4456448, 4587520, 4718592, 4849664,
4980736, 5111808, 5242880, 5373952, 5505024, 5636096, 5767168, 5898240,
6029312, 6160384, 6291456, 6422528, 6553600, 6684672, 6815744, 6946816,
7077888, 7208960, 7340032, 7471104, 7602176, 7733248, 7864320, 7995392,
8126464, 8257536, 8388608, 8519680, 8650752, 8781824, 8912896, 9043968,
9175040, 9306112, 9437184, 9568256, 9699328, 9830400, 9961472, 10092544,
10223616, 10354688, 10485760, 10616832, 10747904, 10878976, 11010048,
11141120, 11272192, 11403264, 11534336, 11665408, 11796480, 11927552,
12058624, 12189696, 12320768, 12451840, 12582912, 12713984, 12845056,
12976128, 13107200, 13238272, 13369344, 13500416, 13631488, 13762560,
13893632, 14024704, 14155776, 14286848, 14417920, 14548992, 14680064,
14811136, 14942208, 15073280, 15204352, 15335424, 15466496, 15597568,
15728640, 15859712, 15990784, 16121856, 16252928, 16384000, 16515072,
16646144, 16777216, 16908288, 17039360, 17170432, 17301504, 17432576,
17563648, 17694720, 17825792, 17956864, 18087936, 18219008, 18350080,
18481152, 18612224, 18743296, 18874368, 19005440, 19136512, 19267584,
19398656, 19529728, 19660800, 19791872, 19922944, 20054016, 20185088,
20316160, 20447232, 20578304, 20709376, 20840448, 20971520, 21102592,
21233664, 21364736, 21495808, 21626880, 21757952, 21889024, 22020096,
22151168, 22282240, 22413312, 22544384, 22675456, 22806528, 22937600,
23068672, 23199744, 23330816, 23461888, 23592960, 23724032, 23855104,
23986176, 24117248, 24248320, 24379392, 24510464, 24641536, 24772608,
24903680, 25034752, 25165824, 25296896, 25427968, 25559040, 25690112,
25821184, 25952256, 26083328, 26214400, 26345472, 26476544, 26607616,
26738688, 26869760, 27000832, 27131904, 27262976, 27394048, 27525120,
27656192, 27787264, 27918336, 28049408, 28180480, 28311552, 28442624,
28573696, 28704768, 28835840, 28966912, 29097984, 29229056, 29360128,
29491200, 29622272, 29753344, 29884416, 30015488, 30146560, 30277632,
30408704, 30539776, 30670848, 30801920, 30932992, 31064064, 31195136,
31326208, 31457280, 31588352, 31719424, 31850496, 31981568, 32112640,
32243712, 32374784, 32505856, 32636928, 32768000, 32899072, 33030144,
33161216, 33292288, 33423360,
];
// ── Error type ──────────────────────────────────────────────────────────────
#[derive(Debug)]
pub enum LzxError {
BadHuffmanTable,
Decrunch(String),
}
impl fmt::Display for LzxError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::BadHuffmanTable => write!(f, "failed to build Huffman table"),
Self::Decrunch(msg) => write!(f, "LZX decrunch error: {msg}"),
}
}
}
impl std::error::Error for LzxError {}
// ── Bit reader (MSB order, 16-bit LE pairs) ────────────────────────────────
struct BitReader<'a> {
data: &'a [u8],
pos: usize,
buf: u32,
left: i32,
}
impl<'a> BitReader<'a> {
fn new(data: &'a [u8]) -> Self {
Self { data, pos: 0, buf: 0, left: 0 }
}
/// Inject one 16-bit little-endian pair into MSB bit buffer.
fn fill(&mut self) {
let b0 = if self.pos < self.data.len() {
let b = self.data[self.pos]; self.pos += 1; b as u32
} else { 0 };
let b1 = if self.pos < self.data.len() {
let b = self.data[self.pos]; self.pos += 1; b as u32
} else { 0 };
let word = (b1 << 8) | b0;
self.buf |= word << (16 - self.left as u32);
self.left += 16;
}
#[inline]
fn ensure(&mut self, n: i32) {
while self.left < n { self.fill(); }
}
#[inline]
fn peek(&self, n: u32) -> u32 {
self.buf >> (BITBUF_WIDTH - n)
}
#[inline]
fn remove(&mut self, n: u32) {
self.buf <<= n;
self.left -= n as i32;
}
#[inline]
fn read(&mut self, n: u32) -> u32 {
self.ensure(n as i32);
let v = self.peek(n);
self.remove(n);
v
}
/// Read a raw byte directly (for UNCOMPRESSED blocks).
fn raw_byte(&mut self) -> u8 {
if self.pos < self.data.len() {
let b = self.data[self.pos]; self.pos += 1; b
} else { 0 }
}
/// Re-align the bitstream at a frame boundary.
fn align_frame(&mut self) {
if self.left > 0 { self.ensure(16); }
let r = self.left & 15;
if r != 0 { self.remove(r as u32); }
}
}
// ── Huffman table builder (MSB order) ───────────────────────────────────────
fn make_decode_table(
nsyms: usize,
nbits: usize,
length: &[u8],
table: &mut [u16],
) -> bool {
let mut pos: usize = 0;
let table_mask = 1usize << nbits;
let mut bit_mask = table_mask >> 1;
// Short codes: direct mapping
for bit_num in 1..=nbits {
for sym in 0..nsyms {
if length[sym] as usize != bit_num { continue; }
let leaf = pos;
pos += bit_mask;
if pos > table_mask { return true; }
for i in leaf..leaf + bit_mask {
table[i] = sym as u16;
}
}
bit_mask >>= 1;
}
if pos == table_mask { return false; }
// Mark remaining entries as unused
for i in pos..table_mask {
table[i] = 0xFFFF;
}
let mut next_symbol = if (table_mask >> 1) < nsyms { nsyms } else { table_mask >> 1 };
let mut pos32 = (pos as u32) << 16;
let table_mask32 = (table_mask as u32) << 16;
let mut bit_mask32: u32 = 1 << 15;
// Long codes: tree traversal
for bit_num in (nbits + 1)..=HUFF_MAXBITS {
for sym in 0..nsyms {
if length[sym] as usize != bit_num { continue; }
if pos32 >= table_mask32 { return true; }
let mut leaf = (pos32 >> 16) as usize;
for fill in 0..(bit_num - nbits) {
if table[leaf] == 0xFFFF {
table[next_symbol << 1] = 0xFFFF;
table[(next_symbol << 1) + 1] = 0xFFFF;
table[leaf] = next_symbol as u16;
next_symbol += 1;
}
leaf = (table[leaf] as usize) << 1;
if (pos32 >> (15 - fill as u32)) & 1 != 0 {
leaf += 1;
}
}
table[leaf] = sym as u16;
pos32 += bit_mask32;
}
bit_mask32 >>= 1;
}
pos32 != table_mask32
}
// ── Huffman symbol decoder ──────────────────────────────────────────────────
fn read_huffsym(
br: &mut BitReader,
table: &[u16],
lens: &[u8],
tablebits: usize,
maxsyms: usize,
) -> Result<usize, LzxError> {
br.ensure(HUFF_MAXBITS as i32);
let mut sym = table[br.peek(tablebits as u32) as usize] as usize;
if sym >= maxsyms {
let mut i: u32 = 1 << (BITBUF_WIDTH - tablebits as u32);
loop {
i >>= 1;
if i == 0 { return Err(LzxError::BadHuffmanTable); }
sym = table[(sym << 1) | if br.buf & i != 0 { 1 } else { 0 }] as usize;
if sym < maxsyms { break; }
}
}
br.remove(lens[sym] as u32);
Ok(sym)
}
// ── LZX decoder state ───────────────────────────────────────────────────────
pub struct LzxDecoder {
window: Vec<u8>,
window_size: usize,
window_posn: usize,
frame_posn: usize,
frame: usize,
num_offsets: usize,
r0: u32,
r1: u32,
r2: u32,
block_type: u8,
block_length: usize,
block_remaining: usize,
header_read: bool,
intel_filesize: i32,
intel_curpos: i32,
intel_started: bool,
// Huffman code lengths
pretree_len: Vec<u8>,
maintree_len: Vec<u8>,
length_len: Vec<u8>,
aligned_len: Vec<u8>,
// Huffman decode tables
pretree_table: Vec<u16>,
maintree_table: Vec<u16>,
length_table: Vec<u16>,
aligned_table: Vec<u16>,
length_empty: bool,
}
impl LzxDecoder {
pub fn new(window_bits: u32) -> Self {
assert!((15..=21).contains(&window_bits));
let window_size = 1usize << window_bits;
let num_offsets = (POSITION_SLOTS[(window_bits - 15) as usize] as usize) << 3;
Self {
window: vec![0u8; window_size],
window_size,
window_posn: 0,
frame_posn: 0,
frame: 0,
num_offsets,
r0: 1, r1: 1, r2: 1,
block_type: 0,
block_length: 0,
block_remaining: 0,
header_read: false,
intel_filesize: 0,
intel_curpos: 0,
intel_started: false,
pretree_len: vec![0u8; PRETREE_MAXSYMS + LENTABLE_SAFETY],
maintree_len: vec![0u8; MAINTREE_MAXSYMS + LENTABLE_SAFETY],
length_len: vec![0u8; LENGTH_MAXSYMS + LENTABLE_SAFETY],
aligned_len: vec![0u8; ALIGNED_MAXSYMS + LENTABLE_SAFETY],
pretree_table: vec![0u16; (1 << PRETREE_TABLEBITS) + PRETREE_MAXSYMS * 2],
maintree_table: vec![0u16; (1 << MAINTREE_TABLEBITS) + MAINTREE_MAXSYMS * 2],
length_table: vec![0u16; (1 << LENGTH_TABLEBITS) + LENGTH_MAXSYMS * 2],
aligned_table: vec![0u16; (1 << ALIGNED_TABLEBITS) + ALIGNED_MAXSYMS * 2],
length_empty: false,
}
}
fn build_table(
lens: &[u8], table: &mut [u16], maxsyms: usize, tablebits: usize,
) -> Result<(), LzxError> {
if make_decode_table(maxsyms, tablebits, lens, table) {
Err(LzxError::BadHuffmanTable)
} else {
Ok(())
}
}
fn build_table_maybe_empty(
lens: &[u8], table: &mut [u16], maxsyms: usize, tablebits: usize,
) -> Result<bool, LzxError> {
if make_decode_table(maxsyms, tablebits, lens, table) {
// Check if table is simply empty (all lengths zero)
for i in 0..maxsyms {
if lens[i] > 0 {
return Err(LzxError::BadHuffmanTable);
}
}
Ok(true) // empty
} else {
Ok(false) // not empty
}
}
/// Read Huffman code lengths using the pretree (lzxd_read_lens).
fn read_lens(
br: &mut BitReader,
lens: &mut [u8],
pretree_len: &mut [u8],
pretree_table: &mut [u16],
first: usize,
last: usize,
) -> Result<(), LzxError> {
// Build pretree: 20 symbols, 4 bits each
for i in 0..20 {
pretree_len[i] = br.read(4) as u8;
}
Self::build_table(pretree_len, pretree_table, PRETREE_MAXSYMS, PRETREE_TABLEBITS)?;
let mut x = first;
while x < last {
let z = read_huffsym(br, pretree_table, pretree_len, PRETREE_TABLEBITS, PRETREE_MAXSYMS)?;
if z == 17 {
// Run of zeros: [read 4 bits] + 4
let mut y = br.read(4) as usize + 4;
while y > 0 && x < last { lens[x] = 0; x += 1; y -= 1; }
} else if z == 18 {
// Run of zeros: [read 5 bits] + 20
let mut y = br.read(5) as usize + 20;
while y > 0 && x < last { lens[x] = 0; x += 1; y -= 1; }
} else if z == 19 {
// Run of same: [read 1 bit] + 4, then read symbol
let mut y = br.read(1) as usize + 4;
let z2 = read_huffsym(br, pretree_table, pretree_len, PRETREE_TABLEBITS, PRETREE_MAXSYMS)?;
let mut val = lens[x] as i32 - z2 as i32;
if val < 0 { val += 17; }
while y > 0 && x < last { lens[x] = val as u8; x += 1; y -= 1; }
} else {
// Delta: code 0..16
let mut val = lens[x] as i32 - z as i32;
if val < 0 { val += 17; }
lens[x] = val as u8;
x += 1;
}
}
Ok(())
}
/// Decompress the full LZX stream into the output buffer.
pub fn decompress(&mut self, input: &[u8], output_len: usize) -> Result<Vec<u8>, LzxError> {
let mut br = BitReader::new(input);
let mut output = Vec::with_capacity(output_len);
let mut offset: usize = 0;
let end_frame = (output_len / LZX_FRAME_SIZE) + 1;
while self.frame < end_frame {
// Read header once
if !self.header_read {
let i_bit = br.read(1);
let (hi, lo) = if i_bit != 0 {
(br.read(16), br.read(16))
} else {
(0, 0)
};
self.intel_filesize = ((hi << 16) | lo) as i32;
self.header_read = true;
}
// Frame size
let frame_size = if output_len > 0 && (output_len - offset) < LZX_FRAME_SIZE {
output_len - offset
} else {
LZX_FRAME_SIZE
};
let mut bytes_todo = (self.frame_posn + frame_size).wrapping_sub(self.window_posn) as i32;
while bytes_todo > 0 {
// New block?
if self.block_remaining == 0 {
// Realign after odd UNCOMPRESSED block
if self.block_type == LZX_BLOCKTYPE_UNCOMPRESSED && (self.block_length & 1) != 0 {
br.raw_byte();
}
// Read block type (3 bits) and length (24 bits)
self.block_type = br.read(3) as u8;
let hi = br.read(16) as usize;
let lo = br.read(8) as usize;
self.block_length = (hi << 8) | lo;
self.block_remaining = self.block_length;
match self.block_type {
LZX_BLOCKTYPE_ALIGNED => {
for i in 0..8 { self.aligned_len[i] = br.read(3) as u8; }
Self::build_table(&self.aligned_len, &mut self.aligned_table, ALIGNED_MAXSYMS, ALIGNED_TABLEBITS)?;
// Fall through to verbatim tree reading
Self::read_lens(&mut br, &mut self.maintree_len, &mut self.pretree_len, &mut self.pretree_table, 0, 256)?;
Self::read_lens(&mut br, &mut self.maintree_len, &mut self.pretree_len, &mut self.pretree_table, 256, LZX_NUM_CHARS + self.num_offsets)?;
Self::build_table(&self.maintree_len, &mut self.maintree_table, MAINTREE_MAXSYMS, MAINTREE_TABLEBITS)?;
if self.maintree_len[0xE8] != 0 { self.intel_started = true; }
Self::read_lens(&mut br, &mut self.length_len, &mut self.pretree_len, &mut self.pretree_table, 0, LZX_NUM_SECONDARY_LENGTHS)?;
self.length_empty = Self::build_table_maybe_empty(&self.length_len, &mut self.length_table, LENGTH_MAXSYMS, LENGTH_TABLEBITS)?;
}
LZX_BLOCKTYPE_VERBATIM => {
Self::read_lens(&mut br, &mut self.maintree_len, &mut self.pretree_len, &mut self.pretree_table, 0, 256)?;
Self::read_lens(&mut br, &mut self.maintree_len, &mut self.pretree_len, &mut self.pretree_table, 256, LZX_NUM_CHARS + self.num_offsets)?;
Self::build_table(&self.maintree_len, &mut self.maintree_table, MAINTREE_MAXSYMS, MAINTREE_TABLEBITS)?;
if self.maintree_len[0xE8] != 0 { self.intel_started = true; }
Self::read_lens(&mut br, &mut self.length_len, &mut self.pretree_len, &mut self.pretree_table, 0, LZX_NUM_SECONDARY_LENGTHS)?;
self.length_empty = Self::build_table_maybe_empty(&self.length_len, &mut self.length_table, LENGTH_MAXSYMS, LENGTH_TABLEBITS)?;
}
LZX_BLOCKTYPE_UNCOMPRESSED => {
self.intel_started = true;
// Align to byte boundary
if br.left == 0 { br.ensure(16); }
br.left = 0;
br.buf = 0;
// Read R0, R1, R2 (12 bytes, little-endian u32s)
let mut buf = [0u8; 12];
for b in &mut buf { *b = br.raw_byte(); }
self.r0 = u32::from_le_bytes([buf[0], buf[1], buf[2], buf[3]]);
self.r1 = u32::from_le_bytes([buf[4], buf[5], buf[6], buf[7]]);
self.r2 = u32::from_le_bytes([buf[8], buf[9], buf[10], buf[11]]);
}
_ => return Err(LzxError::Decrunch("bad block type".into())),
}
}
let mut this_run = self.block_remaining as i32;
if this_run > bytes_todo { this_run = bytes_todo; }
bytes_todo -= this_run;
self.block_remaining -= this_run as usize;
let window_size = self.window_size;
match self.block_type {
LZX_BLOCKTYPE_VERBATIM => {
while this_run > 0 {
let main_element = read_huffsym(&mut br, &self.maintree_table, &self.maintree_len, MAINTREE_TABLEBITS, MAINTREE_MAXSYMS)?;
if main_element < LZX_NUM_CHARS {
self.window[self.window_posn] = main_element as u8;
self.window_posn += 1;
this_run -= 1;
} else {
let me = main_element - LZX_NUM_CHARS;
let mut match_length = me & LZX_NUM_PRIMARY_LENGTHS;
if match_length == LZX_NUM_PRIMARY_LENGTHS {
if self.length_empty { return Err(LzxError::Decrunch("LENGTH tree empty".into())); }
let footer = read_huffsym(&mut br, &self.length_table, &self.length_len, LENGTH_TABLEBITS, LENGTH_MAXSYMS)?;
match_length += footer;
}
match_length += LZX_MIN_MATCH;
let mut match_offset = (me >> 3) as u32;
match match_offset {
0 => match_offset = self.r0,
1 => { match_offset = self.r1; self.r1 = self.r0; self.r0 = match_offset; }
2 => { match_offset = self.r2; self.r2 = self.r0; self.r0 = match_offset; }
3 => { match_offset = 1; self.r2 = self.r1; self.r1 = self.r0; self.r0 = match_offset; }
_ => {
let extra = if match_offset >= 36 { 17 } else { EXTRA_BITS[match_offset as usize] as u32 };
let verbatim_bits = br.read(extra);
match_offset = POSITION_BASE[match_offset as usize] - 2 + verbatim_bits;
self.r2 = self.r1; self.r1 = self.r0; self.r0 = match_offset;
}
}
if self.window_posn + match_length > window_size {
return Err(LzxError::Decrunch("match overrun".into()));
}
self.copy_match(match_offset as usize, match_length);
this_run -= match_length as i32;
}
}
}
LZX_BLOCKTYPE_ALIGNED => {
while this_run > 0 {
let main_element = read_huffsym(&mut br, &self.maintree_table, &self.maintree_len, MAINTREE_TABLEBITS, MAINTREE_MAXSYMS)?;
if main_element < LZX_NUM_CHARS {
self.window[self.window_posn] = main_element as u8;
self.window_posn += 1;
this_run -= 1;
} else {
let me = main_element - LZX_NUM_CHARS;
let mut match_length = me & LZX_NUM_PRIMARY_LENGTHS;
if match_length == LZX_NUM_PRIMARY_LENGTHS {
if self.length_empty { return Err(LzxError::Decrunch("LENGTH tree empty".into())); }
let footer = read_huffsym(&mut br, &self.length_table, &self.length_len, LENGTH_TABLEBITS, LENGTH_MAXSYMS)?;
match_length += footer;
}
match_length += LZX_MIN_MATCH;
let mut match_offset = (me >> 3) as u32;
match match_offset {
0 => match_offset = self.r0,
1 => { match_offset = self.r1; self.r1 = self.r0; self.r0 = match_offset; }
2 => { match_offset = self.r2; self.r2 = self.r0; self.r0 = match_offset; }
_ => {
let extra = if match_offset >= 36 { 17 } else { EXTRA_BITS[match_offset as usize] as u32 };
match_offset = POSITION_BASE[match_offset as usize] - 2;
if extra > 3 {
let verbatim_bits = br.read(extra - 3);
match_offset += verbatim_bits << 3;
let aligned = read_huffsym(&mut br, &self.aligned_table, &self.aligned_len, ALIGNED_TABLEBITS, ALIGNED_MAXSYMS)?;
match_offset += aligned as u32;
} else if extra == 3 {
let aligned = read_huffsym(&mut br, &self.aligned_table, &self.aligned_len, ALIGNED_TABLEBITS, ALIGNED_MAXSYMS)?;
match_offset += aligned as u32;
} else if extra > 0 {
let verbatim_bits = br.read(extra);
match_offset += verbatim_bits;
} else {
match_offset = 1;
}
self.r2 = self.r1; self.r1 = self.r0; self.r0 = match_offset;
}
}
if self.window_posn + match_length > window_size {
return Err(LzxError::Decrunch("match overrun".into()));
}
self.copy_match(match_offset as usize, match_length);
this_run -= match_length as i32;
}
}
}
LZX_BLOCKTYPE_UNCOMPRESSED => {
let run = this_run as usize;
for _ in 0..run {
self.window[self.window_posn] = br.raw_byte();
self.window_posn += 1;
}
}
_ => return Err(LzxError::Decrunch("bad block type in decode".into())),
}
// Overrun accounting
if this_run < 0 {
let overrun = (-this_run) as usize;
if overrun > self.block_remaining {
return Err(LzxError::Decrunch("overrun past block end".into()));
}
self.block_remaining -= overrun;
}
}
// Frame boundary check
if (self.window_posn.wrapping_sub(self.frame_posn)) != frame_size {
return Err(LzxError::Decrunch(format!(
"decode beyond frame: {} != {}", self.window_posn - self.frame_posn, frame_size
)));
}
// Re-align bitstream
br.align_frame();
// Intel E8 postprocessing
if self.intel_started && self.intel_filesize != 0
&& self.frame <= 32768 && frame_size > 10
{
let mut e8_buf = vec![0u8; frame_size];
e8_buf.copy_from_slice(&self.window[self.frame_posn..self.frame_posn + frame_size]);
let mut i = 0usize;
let limit = frame_size - 10;
let mut curpos = self.intel_curpos;
let filesize = self.intel_filesize;
while i < limit {
if e8_buf[i] != 0xE8 { i += 1; curpos += 1; continue; }
let abs_off = e8_buf[i+1] as i32
| (e8_buf[i+2] as i32) << 8
| (e8_buf[i+3] as i32) << 16
| (e8_buf[i+4] as i32) << 24;
if abs_off >= -curpos && abs_off < filesize {
let rel_off = if abs_off >= 0 { abs_off - curpos } else { abs_off + filesize };
e8_buf[i+1] = rel_off as u8;
e8_buf[i+2] = (rel_off >> 8) as u8;
e8_buf[i+3] = (rel_off >> 16) as u8;
e8_buf[i+4] = (rel_off >> 24) as u8;
}
i += 5;
curpos += 5;
}
self.intel_curpos += frame_size as i32;
let to_write = frame_size.min(output_len - offset);
output.extend_from_slice(&e8_buf[..to_write]);
offset += to_write;
} else {
if self.intel_filesize != 0 { self.intel_curpos += frame_size as i32; }
let to_write = frame_size.min(output_len - offset);
output.extend_from_slice(&self.window[self.frame_posn..self.frame_posn + to_write]);
offset += to_write;
}
// Advance frame
self.frame_posn += frame_size;
self.frame += 1;
if self.window_posn == self.window_size { self.window_posn = 0; }
if self.frame_posn == self.window_size { self.frame_posn = 0; }
}
Ok(output)
}
/// Copy a match from the window (handles wrap-around).
fn copy_match(&mut self, match_offset: usize, match_length: usize) {
let window_size = self.window_size;
let mut remaining = match_length;
if match_offset > self.window_posn {
// Source wraps around window end
let j = match_offset - self.window_posn;
let mut src = window_size - j;
if j < remaining {
remaining -= j;
for _ in 0..j {
self.window[self.window_posn] = self.window[src];
self.window_posn += 1;
src += 1;
}
src = 0; // wrap to start
}
for _ in 0..remaining {
self.window[self.window_posn] = self.window[src];
self.window_posn += 1;
src += 1;
}
} else {
let mut src = self.window_posn - match_offset;
for _ in 0..remaining {
self.window[self.window_posn] = self.window[src];
self.window_posn += 1;
src += 1;
}
}
}
}

View File

@@ -0,0 +1,68 @@
//! Minimal PE parser for Xbox 360 executables.
//! PE headers are little-endian even on the big-endian Xbox 360.
use serde::Serialize;
#[derive(Serialize, Debug, Clone)]
pub struct PeSection {
pub name: String,
pub virtual_address: u32,
pub virtual_size: u32,
pub raw_offset: u32,
pub raw_size: u32,
pub flags: u32,
}
impl PeSection {
pub fn is_code(&self) -> bool {
self.flags & 0x20000000 != 0 // IMAGE_SCN_MEM_EXECUTE
}
}
fn le_u16(data: &[u8], off: usize) -> u16 {
u16::from_le_bytes([data[off], data[off + 1]])
}
fn le_u32(data: &[u8], off: usize) -> u32 {
u32::from_le_bytes([data[off], data[off + 1], data[off + 2], data[off + 3]])
}
pub fn parse_sections(pe: &[u8]) -> anyhow::Result<Vec<PeSection>> {
anyhow::ensure!(pe.len() >= 64, "PE too small");
anyhow::ensure!(pe[0] == b'M' && pe[1] == b'Z', "not a PE (bad MZ)");
let e_lfanew = le_u32(pe, 0x3C) as usize;
anyhow::ensure!(e_lfanew + 4 <= pe.len(), "e_lfanew out of bounds");
let nt_sig = le_u32(pe, e_lfanew);
anyhow::ensure!(nt_sig == 0x00004550, "bad PE signature: 0x{nt_sig:08X}");
let file_header_off = e_lfanew + 4;
let num_sections = le_u16(pe, file_header_off + 2) as usize;
let opt_header_size = le_u16(pe, file_header_off + 16) as usize;
let section_table_off = file_header_off + 20 + opt_header_size;
let mut sections = Vec::new();
for i in 0..num_sections {
let s = section_table_off + i * 40;
if s + 40 > pe.len() { break; }
let name_bytes = &pe[s..s + 8];
let name = std::str::from_utf8(name_bytes)
.unwrap_or("???")
.trim_end_matches('\0')
.to_string();
sections.push(PeSection {
name,
virtual_size: le_u32(pe, s + 8),
virtual_address: le_u32(pe, s + 12),
raw_size: le_u32(pe, s + 16),
raw_offset: le_u32(pe, s + 20),
flags: le_u32(pe, s + 36),
});
}
Ok(sections)
}