wip: extract the xexdb tool closure
This commit is contained in:
15
crates/sylpheed-xex/Cargo.toml
Normal file
15
crates/sylpheed-xex/Cargo.toml
Normal file
@@ -0,0 +1,15 @@
|
||||
[package]
|
||||
name = "sylpheed-xex"
|
||||
version = "0.1.0"
|
||||
edition = "2024"
|
||||
description = "XEX2 container: decrypt, LZX, PE image, resources — and the disc image it lives in"
|
||||
|
||||
[dependencies]
|
||||
tracing = "0.1"
|
||||
byteorder = "1"
|
||||
thiserror = "1"
|
||||
anyhow = "1"
|
||||
aes = "0.8"
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
serde_json = "1"
|
||||
metrics = "0.23"
|
||||
139
crates/sylpheed-xex/src/header.rs
Normal file
139
crates/sylpheed-xex/src/header.rs
Normal file
@@ -0,0 +1,139 @@
|
||||
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;
|
||||
// These two were swapped. `0x00020104` is TLS_INFO and `0x00020200` is
|
||||
// DEFAULT_STACK_SIZE — confirmed against the reference implementation
|
||||
// (xenia-canary `kernel/util/xex2_info.h`) and against this title, whose
|
||||
// `0x00020104` header points at a TLS descriptor (slot_count 64) while
|
||||
// `0x00020200` carries the inline value 0x80000 (512 KiB), a sane stack.
|
||||
// Swapped, `get_stack_size` returned the TLS descriptor's file offset.
|
||||
pub const TLS_INFO: u32 = 0x00020104;
|
||||
pub const EXECUTION_INFO: u32 = 0x00040006;
|
||||
pub const DEFAULT_STACK_SIZE: u32 = 0x00020200;
|
||||
pub const ORIGINAL_PE_NAME: u32 = 0x000183FF;
|
||||
pub const FILE_FORMAT_INFO: u32 = 0x000003FF;
|
||||
pub const SYSTEM_FLAGS: u32 = 0x00030000;
|
||||
pub const RESOURCE_INFO: u32 = 0x000002FF;
|
||||
pub const STATIC_LIBRARIES: u32 = 0x000200FF;
|
||||
pub const CHECKSUM_TIMESTAMP: u32 = 0x00018002;
|
||||
pub const GAME_RATINGS: u32 = 0x00040310;
|
||||
}
|
||||
16
crates/sylpheed-xex/src/lib.rs
Normal file
16
crates/sylpheed-xex/src/lib.rs
Normal file
@@ -0,0 +1,16 @@
|
||||
//! XEX2 container: header, decrypt, LZX, PE image, resources — and the disc
|
||||
//! image it may live inside (`vfs`).
|
||||
//!
|
||||
//! From `xenia-rs`'s `xenia-xex` + `xenia-vfs` when that emulator was retired.
|
||||
//! `docs/agents/CONSOLIDATION.md` Phase 3.
|
||||
|
||||
pub mod vfs;
|
||||
pub mod header;
|
||||
pub mod loader;
|
||||
pub mod lzx;
|
||||
pub mod pe;
|
||||
pub mod pdata;
|
||||
pub mod resources;
|
||||
pub mod tls;
|
||||
|
||||
pub use header::Xex2Header;
|
||||
591
crates/sylpheed-xex/src/loader.rs
Normal file
591
crates/sylpheed-xex/src/loader.rs
Normal file
@@ -0,0 +1,591 @@
|
||||
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.is_multiple_of(4) { 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
|
||||
}
|
||||
|
||||
/// XEX `XEX_HEADER_SYSTEM_FLAGS` (key `0x00030000`) — the privilege bitmap
|
||||
/// queried by `XexCheckExecutablePrivilege`. Low byte 0x00 means the inline
|
||||
/// `value` field is the u32 itself (canary `xex_module.cc:103-108`). Returns
|
||||
/// 0 when the header is absent (matches canary's `GetOptHeader` zero-init).
|
||||
pub fn get_system_flags(header: &Xex2Header) -> u32 {
|
||||
get_opt_header(header, header_keys::SYSTEM_FLAGS).unwrap_or(0)
|
||||
}
|
||||
|
||||
/// Load the XEX image data into a flat buffer (decompressing if needed).
|
||||
/// Returns the decompressed image bytes ready to map into guest memory.
|
||||
#[tracing::instrument(skip_all, fields(bytes = data.len()))]
|
||||
pub fn load_image(data: &[u8], header: &Xex2Header) -> io::Result<Vec<u8>> {
|
||||
let started = std::time::Instant::now();
|
||||
let source = &data[header.header_size as usize..];
|
||||
let bytes_in = source.len();
|
||||
|
||||
let output = match &header.file_format_info {
|
||||
Some(info) if info.compression_type == COMPRESSION_BASIC => {
|
||||
tracing::debug!(compression = "basic", "decompressing");
|
||||
load_basic_compressed(source, info)?
|
||||
}
|
||||
Some(info) if info.compression_type == COMPRESSION_NORMAL => {
|
||||
tracing::debug!(compression = "normal/LZX", "decompressing");
|
||||
load_normal_compressed(source, info, header)?
|
||||
}
|
||||
_ => source.to_vec(),
|
||||
};
|
||||
|
||||
let elapsed_ms = started.elapsed().as_millis() as f64;
|
||||
metrics::histogram!("xex.load_image_ms").record(elapsed_ms);
|
||||
metrics::counter!("xex.bytes_in").increment(bytes_in as u64);
|
||||
metrics::counter!("xex.bytes_out").increment(output.len() as u64);
|
||||
let ratio = if bytes_in == 0 { 0.0 } else { output.len() as f64 / bytes_in as f64 };
|
||||
tracing::info!(bytes_in, bytes_out = output.len(), ratio, elapsed_ms, "image loaded");
|
||||
Ok(output)
|
||||
}
|
||||
|
||||
/// 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).
|
||||
#[tracing::instrument(skip_all, fields(bytes = input.len()))]
|
||||
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)
|
||||
#[tracing::instrument(skip_all, fields(bytes_in = source.len()))]
|
||||
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/sylpheed-xex/src/lzx.rs
Normal file
692
crates/sylpheed-xex/src/lzx.rs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
219
crates/sylpheed-xex/src/pdata.rs
Normal file
219
crates/sylpheed-xex/src/pdata.rs
Normal file
@@ -0,0 +1,219 @@
|
||||
//! PE32 `.pdata` exception data parser for PowerPC Xbox 360 binaries.
|
||||
//!
|
||||
//! Each `RUNTIME_FUNCTION` entry is 8 bytes, big-endian on disk:
|
||||
//! ```text
|
||||
//! word 0: BeginAddress (absolute VA, not RVA — Xbox 360 convention)
|
||||
//! word 1: packed metadata (read as a single big-endian u32; MSVC
|
||||
//! bit-field layout packs LSB-first):
|
||||
//! bits 0.. 7 (low 8) : prolog_length (instruction count, dwords)
|
||||
//! bits 8..29 (mid 22): function_length (instruction count, dwords)
|
||||
//! bit 30 : 32-bit code flag (always 1 on PPC)
|
||||
//! bit 31 : exception-handler-present flag
|
||||
//! ```
|
||||
//!
|
||||
//! Reference: Microsoft PE32+ exception data spec (PowerPC RUNTIME_FUNCTION);
|
||||
//! xenia-canary `src/xenia/cpu/xex_module.cc:1570-1587` (canary only reads
|
||||
//! `BeginAddress`; the metadata layout above is the authoritative spec).
|
||||
//!
|
||||
//! `BeginAddress = 0` terminates the table early in some images (canary breaks
|
||||
//! on this; we mirror).
|
||||
|
||||
use crate::pe::PeSection;
|
||||
|
||||
/// One parsed `RUNTIME_FUNCTION` entry.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub struct PdataEntry {
|
||||
/// Absolute VA of the function's first instruction.
|
||||
pub begin_address: u32,
|
||||
/// Function size in bytes (function_length_dwords * 4).
|
||||
pub function_length: u32,
|
||||
/// Prolog size in bytes (prolog_length_dwords * 4).
|
||||
pub prolog_length: u32,
|
||||
/// Raw 2-bit flags lifted from the packed word's top two bits, i.e.
|
||||
/// `(meta >> 30) & 3`. So **bit 0 mirrors packed bit 30 (32-bit-code, set
|
||||
/// on essentially every PPC entry) and bit 1 mirrors packed bit 31
|
||||
/// (exception handler registered)** — test `flags & 2` for "has EH".
|
||||
pub flags: u8,
|
||||
}
|
||||
|
||||
impl PdataEntry {
|
||||
/// One-past-the-last instruction (exclusive).
|
||||
pub fn end_address(&self) -> u32 {
|
||||
self.begin_address.wrapping_add(self.function_length)
|
||||
}
|
||||
}
|
||||
|
||||
/// Parse the `.pdata` section out of a decompressed PE image.
|
||||
///
|
||||
/// `pe` is the full image buffer (image_base-relative); `image_base` and the
|
||||
/// `.pdata` section descriptor come from `sylpheed_xex::pe::parse_sections`.
|
||||
/// Returns an empty vec if no `.pdata` section is present or it falls outside
|
||||
/// the buffer — never an error (the caller already validated the section list).
|
||||
pub fn parse_pdata(pe: &[u8], image_base: u32, sections: &[PeSection]) -> Vec<PdataEntry> {
|
||||
let pdata = match sections.iter().find(|s| s.name == ".pdata") {
|
||||
Some(s) => s,
|
||||
None => return Vec::new(),
|
||||
};
|
||||
|
||||
let off = pdata.virtual_address as usize;
|
||||
let len = pdata.virtual_size as usize;
|
||||
if off.saturating_add(len) > pe.len() {
|
||||
return Vec::new();
|
||||
}
|
||||
|
||||
// Each entry is 8 bytes; truncate any partial trailing entry.
|
||||
let n_entries = len / 8;
|
||||
let mut out = Vec::with_capacity(n_entries);
|
||||
|
||||
for i in 0..n_entries {
|
||||
let p = off + i * 8;
|
||||
let begin = u32::from_be_bytes([pe[p], pe[p + 1], pe[p + 2], pe[p + 3]]);
|
||||
let meta = u32::from_be_bytes([pe[p + 4], pe[p + 5], pe[p + 6], pe[p + 7]]);
|
||||
|
||||
// Sentinel: BeginAddress=0 marks early termination (canary `xex_module.cc:1583`).
|
||||
if begin == 0 {
|
||||
break;
|
||||
}
|
||||
|
||||
let prolog_dwords = meta & 0xFF;
|
||||
let function_dwords = (meta >> 8) & 0x003F_FFFF;
|
||||
let flags = ((meta >> 30) & 0x3) as u8;
|
||||
|
||||
out.push(PdataEntry {
|
||||
begin_address: begin,
|
||||
function_length: function_dwords * 4,
|
||||
prolog_length: prolog_dwords * 4,
|
||||
flags,
|
||||
});
|
||||
}
|
||||
|
||||
// Sanity: drop any entry whose begin_address falls outside the image bounds.
|
||||
// Image high water = image_base + the largest virtual_address+virtual_size.
|
||||
let high = sections
|
||||
.iter()
|
||||
.map(|s| image_base.wrapping_add(s.virtual_address).wrapping_add(s.virtual_size))
|
||||
.max()
|
||||
.unwrap_or(u32::MAX);
|
||||
out.retain(|e| e.begin_address >= image_base && e.begin_address < high);
|
||||
|
||||
out
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::pe::PeSection;
|
||||
|
||||
fn mk_pe(image_base: u32, text_va: u32, text_size: u32, pdata: &[(u32, u32)]) -> (Vec<u8>, Vec<PeSection>) {
|
||||
// Build a synthetic PE image with .text and .pdata.
|
||||
// Layout: pdata at RVA 0x1000, .text at RVA 0x2000.
|
||||
let pdata_rva = 0x1000u32;
|
||||
let pdata_size = (pdata.len() * 8) as u32;
|
||||
let total = (text_va + text_size).max(pdata_rva + pdata_size) as usize;
|
||||
let mut buf = vec![0u8; total];
|
||||
|
||||
for (i, &(begin, packed)) in pdata.iter().enumerate() {
|
||||
let p = pdata_rva as usize + i * 8;
|
||||
buf[p..p + 4].copy_from_slice(&begin.to_be_bytes());
|
||||
buf[p + 4..p + 8].copy_from_slice(&packed.to_be_bytes());
|
||||
}
|
||||
|
||||
let sections = vec![
|
||||
PeSection {
|
||||
name: ".pdata".into(),
|
||||
virtual_address: pdata_rva,
|
||||
virtual_size: pdata_size,
|
||||
raw_offset: pdata_rva,
|
||||
raw_size: pdata_size,
|
||||
flags: 0x4000_0040, // INITIALIZED_DATA | READ
|
||||
},
|
||||
PeSection {
|
||||
name: ".text".into(),
|
||||
virtual_address: text_va,
|
||||
virtual_size: text_size,
|
||||
raw_offset: text_va,
|
||||
raw_size: text_size,
|
||||
flags: 0x6000_0020, // CODE | EXECUTE | READ
|
||||
},
|
||||
];
|
||||
let _ = image_base; // image_base only matters for high-water bound
|
||||
(buf, sections)
|
||||
}
|
||||
|
||||
/// Pack metadata in the on-disk layout: prolog in low 8 bits, function
|
||||
/// in next 22, flags in top 2.
|
||||
fn pack(prolog_dwords: u32, function_dwords: u32, flags: u32) -> u32 {
|
||||
((flags & 0x3) << 30) | ((function_dwords & 0x3F_FFFF) << 8) | (prolog_dwords & 0xFF)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parses_simple_pdata() {
|
||||
// function at 0x82001000, 32 bytes long (8 dwords), 8-dword prolog (32 bytes).
|
||||
let packed = pack(8, 8, 0b01); // 32-bit-code flag set
|
||||
let (pe, sections) = mk_pe(0x8200_0000, 0x2000, 0x100, &[(0x8200_1000, packed)]);
|
||||
let entries = parse_pdata(&pe, 0x8200_0000, §ions);
|
||||
|
||||
assert_eq!(entries.len(), 1);
|
||||
assert_eq!(entries[0].begin_address, 0x8200_1000);
|
||||
assert_eq!(entries[0].prolog_length, 32);
|
||||
assert_eq!(entries[0].function_length, 32);
|
||||
assert_eq!(entries[0].flags, 0b01);
|
||||
assert_eq!(entries[0].end_address(), 0x8200_1020);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn stops_on_zero_sentinel() {
|
||||
let packed = pack(4, 4, 0b01);
|
||||
let entries = vec![
|
||||
(0x8200_1000, packed),
|
||||
(0u32, 0u32), // sentinel
|
||||
(0x8200_2000, packed),
|
||||
];
|
||||
let (pe, sections) = mk_pe(0x8200_0000, 0x2000, 0x4000, &entries);
|
||||
let parsed = parse_pdata(&pe, 0x8200_0000, §ions);
|
||||
assert_eq!(parsed.len(), 1);
|
||||
assert_eq!(parsed[0].begin_address, 0x8200_1000);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn drops_out_of_range_entries() {
|
||||
let packed = pack(4, 4, 0b01);
|
||||
let entries = vec![
|
||||
(0x8200_1000, packed),
|
||||
(0x4000_0000, packed), // outside image — drop
|
||||
];
|
||||
let (pe, sections) = mk_pe(0x8200_0000, 0x2000, 0x100, &entries);
|
||||
let parsed = parse_pdata(&pe, 0x8200_0000, §ions);
|
||||
assert_eq!(parsed.len(), 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn decodes_real_world_layout() {
|
||||
// Mimics a real-world entry: function_length 306 dwords (1224 bytes),
|
||||
// 0 prolog dwords, 32-bit-code flag set. Verify the bit-packed value
|
||||
// round-trips correctly through parse_pdata.
|
||||
let packed = pack(0, 306, 0b01);
|
||||
let begin = 0x8200_2000u32; // inside the synthetic .text region
|
||||
let (pe, sections) = mk_pe(0x8200_0000, 0x2000, 0x1000, &[(begin, packed)]);
|
||||
let entries = parse_pdata(&pe, 0x8200_0000, §ions);
|
||||
assert_eq!(entries.len(), 1);
|
||||
assert_eq!(entries[0].function_length, 306 * 4);
|
||||
assert_eq!(entries[0].prolog_length, 0);
|
||||
assert_eq!(entries[0].flags, 0b01);
|
||||
assert_eq!(entries[0].end_address(), begin + 1224);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn returns_empty_when_no_pdata_section() {
|
||||
let sections = vec![PeSection {
|
||||
name: ".text".into(),
|
||||
virtual_address: 0x1000,
|
||||
virtual_size: 0x100,
|
||||
raw_offset: 0x1000,
|
||||
raw_size: 0x100,
|
||||
flags: 0x6000_0020,
|
||||
}];
|
||||
let pe = vec![0u8; 0x2000];
|
||||
assert!(parse_pdata(&pe, 0x8200_0000, §ions).is_empty());
|
||||
}
|
||||
}
|
||||
68
crates/sylpheed-xex/src/pe.rs
Normal file
68
crates/sylpheed-xex/src/pe.rs
Normal 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)
|
||||
}
|
||||
127
crates/sylpheed-xex/src/resources.rs
Normal file
127
crates/sylpheed-xex/src/resources.rs
Normal file
@@ -0,0 +1,127 @@
|
||||
//! XEX `XEX_HEADER_RESOURCE_INFO` (key `0x000002FF`) — the embedded resource table.
|
||||
//!
|
||||
//! The header points at a length-prefixed table of fixed 16-byte records:
|
||||
//!
|
||||
//! ```text
|
||||
//! u32 size total table size in bytes, including this field
|
||||
//! record[] entries (size - 4) / 16 of:
|
||||
//! char[8] name resource name, NUL-padded (the title's is its
|
||||
//! title id in uppercase hex, e.g. "535107D4")
|
||||
//! u32 address absolute VA of the resource inside the loaded image
|
||||
//! u32 size resource length in bytes
|
||||
//! ```
|
||||
//!
|
||||
//! For a title the named resource is its **XDBF/SPA package** — achievements,
|
||||
//! localized strings, and images. See `sylpheed_xexdb::xdbf`.
|
||||
//!
|
||||
//! Reference: xenia-canary `kernel/util/xex2_info.h` (`xex2_resource`).
|
||||
|
||||
use crate::header::{Xex2Header, header_keys};
|
||||
|
||||
/// One entry of the XEX resource table.
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct XexResource {
|
||||
/// Resource name from the table, trailing NULs stripped.
|
||||
pub name: String,
|
||||
/// Absolute VA of the resource within the loaded image.
|
||||
pub address: u32,
|
||||
/// Resource length in bytes.
|
||||
pub size: u32,
|
||||
}
|
||||
|
||||
impl XexResource {
|
||||
/// Offset of this resource within an image-base-relative buffer.
|
||||
pub fn image_offset(&self, image_base: u32) -> Option<usize> {
|
||||
self.address.checked_sub(image_base).map(|o| o as usize)
|
||||
}
|
||||
}
|
||||
|
||||
/// Parse the resource table out of the raw XEX bytes.
|
||||
///
|
||||
/// `data` is the whole XEX file (the optional-header value is a file offset
|
||||
/// into it, not a VA). Returns an empty vec when the header is absent or the
|
||||
/// table is truncated — never an error.
|
||||
pub fn parse_resources(data: &[u8], header: &Xex2Header) -> Vec<XexResource> {
|
||||
let Some(off) = header
|
||||
.optional_headers
|
||||
.iter()
|
||||
.find(|h| h.key == header_keys::RESOURCE_INFO)
|
||||
.map(|h| h.value as usize)
|
||||
else {
|
||||
return Vec::new();
|
||||
};
|
||||
if off + 4 > data.len() {
|
||||
return Vec::new();
|
||||
}
|
||||
let size = u32::from_be_bytes([data[off], data[off + 1], data[off + 2], data[off + 3]]) as usize;
|
||||
// The size field counts itself; anything smaller than one record is junk.
|
||||
if size < 4 + 16 || off + size > data.len() {
|
||||
return Vec::new();
|
||||
}
|
||||
let count = (size - 4) / 16;
|
||||
let mut out = Vec::with_capacity(count);
|
||||
for i in 0..count {
|
||||
let p = off + 4 + i * 16;
|
||||
let name = String::from_utf8_lossy(&data[p..p + 8])
|
||||
.trim_end_matches('\0')
|
||||
.to_string();
|
||||
let address = u32::from_be_bytes([data[p + 8], data[p + 9], data[p + 10], data[p + 11]]);
|
||||
let rsize = u32::from_be_bytes([data[p + 12], data[p + 13], data[p + 14], data[p + 15]]);
|
||||
out.push(XexResource { name, address, size: rsize });
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::header::{Xex2Header, Xex2OptionalHeader};
|
||||
|
||||
fn mk_header(opt: Vec<Xex2OptionalHeader>) -> Xex2Header {
|
||||
Xex2Header {
|
||||
magic: crate::header::XEX2_MAGIC,
|
||||
module_flags: 0,
|
||||
header_size: 0,
|
||||
security_offset: 0,
|
||||
header_count: opt.len() as u32,
|
||||
optional_headers: opt,
|
||||
security_info: None,
|
||||
file_format_info: None,
|
||||
import_libraries: Vec::new(),
|
||||
execution_info: None,
|
||||
original_pe_name: None,
|
||||
}
|
||||
}
|
||||
|
||||
fn with_resource(value: u32) -> Xex2Header {
|
||||
mk_header(vec![Xex2OptionalHeader { key: header_keys::RESOURCE_INFO, value }])
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parses_one_resource() {
|
||||
let mut data = vec![0u8; 0x100];
|
||||
let off = 0x40usize;
|
||||
data[off..off + 4].copy_from_slice(&(4u32 + 16).to_be_bytes());
|
||||
data[off + 4..off + 12].copy_from_slice(b"535107D4");
|
||||
data[off + 12..off + 16].copy_from_slice(&0x828F_B900u32.to_be_bytes());
|
||||
data[off + 16..off + 20].copy_from_slice(&0x0002_1FCFu32.to_be_bytes());
|
||||
let r = parse_resources(&data, &with_resource(off as u32));
|
||||
assert_eq!(r.len(), 1);
|
||||
assert_eq!(r[0].name, "535107D4");
|
||||
assert_eq!(r[0].address, 0x828F_B900);
|
||||
assert_eq!(r[0].size, 0x0002_1FCF);
|
||||
assert_eq!(r[0].image_offset(0x8200_0000), Some(0x8F_B900));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn absent_header_yields_nothing() {
|
||||
assert!(parse_resources(&[0u8; 0x100], &mk_header(Vec::new())).is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn truncated_table_yields_nothing() {
|
||||
let mut data = vec![0u8; 0x20];
|
||||
data[0..4].copy_from_slice(&0xFFFF_FFFFu32.to_be_bytes());
|
||||
assert!(parse_resources(&data, &with_resource(0)).is_empty());
|
||||
}
|
||||
}
|
||||
172
crates/sylpheed-xex/src/tls.rs
Normal file
172
crates/sylpheed-xex/src/tls.rs
Normal file
@@ -0,0 +1,172 @@
|
||||
//! `.tls` section parser for PE32 PowerPC.
|
||||
//!
|
||||
//! When MSVC links a binary that uses `__declspec(thread)` storage, it emits
|
||||
//! a `.tls` section plus an IMAGE_TLS_DIRECTORY32 inside `.rdata`. The
|
||||
//! directory points at:
|
||||
//! - the raw initialised TLS data range (start, end VAs)
|
||||
//! - the address of the index field (a u32 written at runtime by the
|
||||
//! loader to identify which TLS slot was assigned)
|
||||
//! - an array of TLS callback function pointers (NUL-terminated)
|
||||
//! - the size of the zero-fill area appended after raw data
|
||||
//!
|
||||
//! Xbox 360 binaries follow the standard PE layout. Sylpheed has no `.tls`
|
||||
//! section and no TLS directory — the parser simply returns `None` and
|
||||
//! callers emit zero rows.
|
||||
//!
|
||||
//! Reference: Microsoft PE/COFF spec, IMAGE_TLS_DIRECTORY32 layout.
|
||||
|
||||
use crate::pe::PeSection;
|
||||
|
||||
/// One TLS callback function pointer extracted from the directory's
|
||||
/// callback array.
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub struct TlsCallback {
|
||||
pub address: u32,
|
||||
}
|
||||
|
||||
/// Parsed `.tls` directory information. All fields are absolute VAs.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct TlsInfo {
|
||||
/// VA of the start of the initialised raw TLS data (template).
|
||||
pub raw_data_start: u32,
|
||||
/// VA of one-past-end of the raw TLS data.
|
||||
pub raw_data_end: u32,
|
||||
/// VA of the u32 the loader writes the assigned slot index into.
|
||||
pub index_address: u32,
|
||||
/// VA of the zero-terminated callback array; 0 when no callbacks.
|
||||
pub callback_array: u32,
|
||||
/// Bytes of zero-fill appended after the raw template at thread init.
|
||||
pub zero_fill_size: u32,
|
||||
/// Characteristics flags (alignment / etc).
|
||||
pub characteristics: u32,
|
||||
/// Resolved TLS callbacks (parsed from `callback_array`).
|
||||
pub callbacks: Vec<TlsCallback>,
|
||||
}
|
||||
|
||||
/// Parse the `.tls` section. Returns `None` if the binary has no `.tls`
|
||||
/// section or the directory is malformed.
|
||||
pub fn parse_tls(pe: &[u8], image_base: u32, sections: &[PeSection]) -> Option<TlsInfo> {
|
||||
// Find the `.tls` section. The IMAGE_TLS_DIRECTORY32 lives somewhere
|
||||
// in `.rdata`; rather than hunt the IMAGE_DATA_DIRECTORY entry through
|
||||
// the optional header, we accept any 24-byte struct at the start of
|
||||
// `.tls` if the section's raw data looks like a valid directory.
|
||||
//
|
||||
// Per MS docs, IMAGE_TLS_DIRECTORY32 layout (24 bytes):
|
||||
// +0x00 StartAddressOfRawData (VA, 4)
|
||||
// +0x04 EndAddressOfRawData (VA, 4)
|
||||
// +0x08 AddressOfIndex (VA, 4)
|
||||
// +0x0C AddressOfCallBacks (VA, 4 — array of FN ptrs, NUL-terminated)
|
||||
// +0x10 SizeOfZeroFill (4)
|
||||
// +0x14 Characteristics (4)
|
||||
let tls_section = sections.iter().find(|s| s.name == ".tls")?;
|
||||
let off = tls_section.virtual_address as usize;
|
||||
if off + 24 > pe.len() { return None; }
|
||||
|
||||
// Xbox 360 PE bodies are big-endian; this is consistent with how we
|
||||
// parse the PE elsewhere (e.g. xref scanning reads BE u32 from PE).
|
||||
let read_u32 = |start: usize| -> u32 {
|
||||
u32::from_be_bytes([pe[start], pe[start + 1], pe[start + 2], pe[start + 3]])
|
||||
};
|
||||
|
||||
let raw_data_start = read_u32(off);
|
||||
let raw_data_end = read_u32(off + 4);
|
||||
let index_address = read_u32(off + 8);
|
||||
let callback_array = read_u32(off + 12);
|
||||
let zero_fill_size = read_u32(off + 16);
|
||||
let characteristics = read_u32(off + 20);
|
||||
|
||||
// Sanity: raw_data_start should land somewhere inside the image.
|
||||
if raw_data_start == 0 && raw_data_end == 0 && index_address == 0 {
|
||||
return None;
|
||||
}
|
||||
|
||||
// Walk the callback array (zero-terminated array of u32 VAs).
|
||||
let mut callbacks = Vec::new();
|
||||
if callback_array != 0 {
|
||||
let mut p = callback_array.wrapping_sub(image_base) as usize;
|
||||
while p + 4 <= pe.len() {
|
||||
let v = read_u32(p);
|
||||
if v == 0 { break; }
|
||||
callbacks.push(TlsCallback { address: v });
|
||||
p += 4;
|
||||
if callbacks.len() >= 64 { break; } // sanity cap
|
||||
}
|
||||
}
|
||||
|
||||
Some(TlsInfo {
|
||||
raw_data_start,
|
||||
raw_data_end,
|
||||
index_address,
|
||||
callback_array,
|
||||
zero_fill_size,
|
||||
characteristics,
|
||||
callbacks,
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::pe::PeSection;
|
||||
|
||||
fn mk_section(name: &str, va: u32, size: u32) -> PeSection {
|
||||
PeSection {
|
||||
name: name.into(),
|
||||
virtual_address: va,
|
||||
virtual_size: size,
|
||||
raw_offset: va,
|
||||
raw_size: size,
|
||||
flags: 0x4000_0040,
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn returns_none_when_no_tls_section() {
|
||||
let pe = vec![0u8; 0x100];
|
||||
let sections = vec![mk_section(".text", 0x10, 0x40)];
|
||||
assert!(parse_tls(&pe, 0x82000000, §ions).is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parses_directory_and_callback_array() {
|
||||
let image_base = 0x82000000u32;
|
||||
let mut pe = vec![0u8; 0x4000];
|
||||
|
||||
// Place the .tls section at RVA 0x100 with the directory.
|
||||
let tls_va: u32 = 0x100;
|
||||
let cb_va: u32 = 0x200;
|
||||
// Directory fields:
|
||||
let raw_start = 0x800u32;
|
||||
let raw_end = 0x900u32;
|
||||
let idx = 0x1000u32;
|
||||
let zero_fill = 0x40u32;
|
||||
let chars = 0x0u32;
|
||||
let cb_array = image_base + cb_va;
|
||||
for (i, v) in [
|
||||
image_base + raw_start, image_base + raw_end,
|
||||
image_base + idx, cb_array, zero_fill, chars,
|
||||
].iter().enumerate() {
|
||||
pe[tls_va as usize + i * 4..tls_va as usize + i * 4 + 4]
|
||||
.copy_from_slice(&v.to_be_bytes());
|
||||
}
|
||||
|
||||
// Two callbacks + NUL terminator at cb_va.
|
||||
let cb1 = image_base + 0x500;
|
||||
let cb2 = image_base + 0x600;
|
||||
pe[cb_va as usize..cb_va as usize + 4].copy_from_slice(&cb1.to_be_bytes());
|
||||
pe[cb_va as usize + 4..cb_va as usize + 8].copy_from_slice(&cb2.to_be_bytes());
|
||||
// pe[cb_va + 8..cb_va + 12] already zero (terminator).
|
||||
|
||||
let sections = vec![mk_section(".tls", tls_va, 0x100)];
|
||||
let info = parse_tls(&pe, image_base, §ions).expect("parses");
|
||||
|
||||
assert_eq!(info.raw_data_start, image_base + raw_start);
|
||||
assert_eq!(info.raw_data_end, image_base + raw_end);
|
||||
assert_eq!(info.index_address, image_base + idx);
|
||||
assert_eq!(info.callback_array, cb_array);
|
||||
assert_eq!(info.zero_fill_size, zero_fill);
|
||||
assert_eq!(info.callbacks.len(), 2);
|
||||
assert_eq!(info.callbacks[0].address, cb1);
|
||||
assert_eq!(info.callbacks[1].address, cb2);
|
||||
}
|
||||
}
|
||||
58
crates/sylpheed-xex/src/vfs/device.rs
Normal file
58
crates/sylpheed-xex/src/vfs/device.rs
Normal file
@@ -0,0 +1,58 @@
|
||||
use super::{VfsDevice, VfsEntry, VfsError};
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
/// Host filesystem pass-through device.
|
||||
pub struct HostPathDevice {
|
||||
name: String,
|
||||
root: PathBuf,
|
||||
}
|
||||
|
||||
impl HostPathDevice {
|
||||
pub fn new(name: impl Into<String>, root: impl AsRef<Path>) -> Self {
|
||||
Self {
|
||||
name: name.into(),
|
||||
root: root.as_ref().to_path_buf(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl VfsDevice for HostPathDevice {
|
||||
fn name(&self) -> &str {
|
||||
&self.name
|
||||
}
|
||||
|
||||
fn list_root(&self) -> Result<Vec<VfsEntry>, VfsError> {
|
||||
let mut entries = Vec::new();
|
||||
for entry in std::fs::read_dir(&self.root)? {
|
||||
let entry = entry?;
|
||||
let metadata = entry.metadata()?;
|
||||
entries.push(VfsEntry {
|
||||
name: entry.file_name().to_string_lossy().into_owned(),
|
||||
is_directory: metadata.is_dir(),
|
||||
size: metadata.len(),
|
||||
offset: 0,
|
||||
// Host FS carries no Xbox attribute byte; synthesise the
|
||||
// DIRECTORY/NORMAL split like canary's HostPathDevice.
|
||||
attributes: if metadata.is_dir() { 0x10 } else { 0x80 },
|
||||
});
|
||||
}
|
||||
Ok(entries)
|
||||
}
|
||||
|
||||
fn read_file(&self, path: &str) -> Result<Vec<u8>, VfsError> {
|
||||
let full_path = self.root.join(path);
|
||||
std::fs::read(&full_path).map_err(VfsError::from)
|
||||
}
|
||||
|
||||
fn stat(&self, path: &str) -> Result<VfsEntry, VfsError> {
|
||||
let full_path = self.root.join(path);
|
||||
let metadata = std::fs::metadata(&full_path)?;
|
||||
Ok(VfsEntry {
|
||||
name: path.to_string(),
|
||||
is_directory: metadata.is_dir(),
|
||||
size: metadata.len(),
|
||||
offset: 0,
|
||||
attributes: if metadata.is_dir() { 0x10 } else { 0x80 },
|
||||
})
|
||||
}
|
||||
}
|
||||
343
crates/sylpheed-xex/src/vfs/disc_image.rs
Normal file
343
crates/sylpheed-xex/src/vfs/disc_image.rs
Normal file
@@ -0,0 +1,343 @@
|
||||
use super::{VfsDevice, VfsEntry, VfsError};
|
||||
use std::io::{Read, Seek, SeekFrom};
|
||||
|
||||
/// XISO disc image device. Parses Xbox 360 disc images (GDFX/XISO format).
|
||||
///
|
||||
/// Caches the fully-resolved entry list at open() — GDFX is a directory
|
||||
/// tree, and resolving any nested path (`dat/tables.pak`, `media/x.wav`)
|
||||
/// requires descending into subdirectories. A prior version only scanned
|
||||
/// the root buffer, so any file under a subdirectory was reported as
|
||||
/// missing. We read each directory's buffer from disk once at open time
|
||||
/// and emit full paths into `entries`.
|
||||
pub struct DiscImageDevice {
|
||||
name: String,
|
||||
path: std::path::PathBuf,
|
||||
game_offset: u64,
|
||||
/// Flattened file + directory tree, each with its full path relative
|
||||
/// to the partition root ("dat/tables.pak", etc.). Populated once at
|
||||
/// `open()` so lookups are O(n) over a cached vec instead of rereading
|
||||
/// the tree on every NtCreateFile.
|
||||
entries: Vec<VfsEntry>,
|
||||
}
|
||||
|
||||
/// XISO sector size
|
||||
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;
|
||||
|
||||
/// File attribute: read-only. Canary OR's this into every GDFX entry's
|
||||
/// attribute byte because a pressed disc is inherently read-only
|
||||
/// (`disc_image_device.cc:154`: `attributes | kFileAttributeReadOnly`).
|
||||
const FILE_ATTRIBUTE_READONLY: u8 = 0x01;
|
||||
|
||||
/// 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 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)?;
|
||||
|
||||
let mut dev = Self {
|
||||
name: name.into(),
|
||||
path: path.to_path_buf(),
|
||||
game_offset,
|
||||
entries: Vec::new(),
|
||||
};
|
||||
dev.collect_entries(&mut file, &root_buffer, 0, "")?;
|
||||
Ok(dev)
|
||||
}
|
||||
|
||||
/// Walk one directory's B-tree buffer, emit each file/directory into
|
||||
/// `out` with its full relative path, and recurse into subdirectory
|
||||
/// buffers on disk.
|
||||
///
|
||||
/// `prefix` is the current parent path (empty at the root). Names
|
||||
/// concatenate as `<prefix>/<name>` so the final path matches what
|
||||
/// guest callers like `NtCreateFile("dat/tables.pak")` expect.
|
||||
///
|
||||
/// `file` is the already-open disc image handle, reused for every
|
||||
/// subdirectory read so we don't pay a fresh open per directory on
|
||||
/// deep trees.
|
||||
fn collect_entries(
|
||||
&mut self,
|
||||
file: &mut std::fs::File,
|
||||
buffer: &[u8],
|
||||
ordinal: u16,
|
||||
prefix: &str,
|
||||
) -> Result<(), VfsError> {
|
||||
let p = ordinal as usize * 4;
|
||||
if p + 14 > buffer.len() {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
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 Ok(());
|
||||
}
|
||||
|
||||
if node_l != 0 && node_l != 0xFFFF {
|
||||
self.collect_entries(file, buffer, node_l, prefix)?;
|
||||
}
|
||||
|
||||
let name = String::from_utf8_lossy(&buffer[p + 14..p + 14 + name_length]).to_string();
|
||||
let is_directory = (attributes & FILE_ATTRIBUTE_DIRECTORY) != 0;
|
||||
// Match canary: the on-disc attribute byte (DIRECTORY/HIDDEN/SYSTEM/
|
||||
// ARCHIVE/NORMAL bits as authored) OR the implicit READONLY bit for
|
||||
// pressed media. We forward the FULL byte, not a path-shape guess, so
|
||||
// attribute queries report exactly what the disc records.
|
||||
let attributes = (attributes | FILE_ATTRIBUTE_READONLY) as u32;
|
||||
let file_offset = self.game_offset + sector * SECTOR_SIZE;
|
||||
let full_path = if prefix.is_empty() {
|
||||
name.clone()
|
||||
} else {
|
||||
format!("{}/{}", prefix, name)
|
||||
};
|
||||
|
||||
self.entries.push(VfsEntry {
|
||||
name: full_path.clone(),
|
||||
is_directory,
|
||||
size: length,
|
||||
offset: file_offset,
|
||||
attributes,
|
||||
});
|
||||
|
||||
// Descend into subdirectories. Zero-length directory entries exist
|
||||
// (empty dirs) and must be skipped to avoid `read_exact` on 0 bytes.
|
||||
if is_directory && length > 0 {
|
||||
file.seek(SeekFrom::Start(file_offset))?;
|
||||
let mut sub_buffer = vec![0u8; length as usize];
|
||||
file.read_exact(&mut sub_buffer)?;
|
||||
self.collect_entries(file, &sub_buffer, 0, &full_path)?;
|
||||
}
|
||||
|
||||
if node_r != 0 && node_r != 0xFFFF {
|
||||
self.collect_entries(file, buffer, node_r, prefix)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl VfsDevice for DiscImageDevice {
|
||||
fn name(&self) -> &str {
|
||||
&self.name
|
||||
}
|
||||
|
||||
fn list_root(&self) -> Result<Vec<VfsEntry>, VfsError> {
|
||||
// Return the full flattened tree. Callers of this method are
|
||||
// dump/debug paths (see `xenia-rs dumpxiso`), which want to see
|
||||
// every file — root-only was the old flat-enumeration bug.
|
||||
Ok(self.entries.clone())
|
||||
}
|
||||
|
||||
fn read_file(&self, path: &str) -> Result<Vec<u8>, VfsError> {
|
||||
let entry = self
|
||||
.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> {
|
||||
self.entries
|
||||
.iter()
|
||||
.find(|e| e.name.eq_ignore_ascii_case(path))
|
||||
.cloned()
|
||||
.ok_or_else(|| VfsError::NotFound(path.to_string()))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
/// Regression: the XISO reader used to only enumerate the root directory,
|
||||
/// so any nested path (`dat/tables.pak`, `media/stream.xma`) failed to
|
||||
/// open. Verified end-to-end by `browse` on the Sylpheed disc which
|
||||
/// now lists 358 entries including `dat/*` files.
|
||||
///
|
||||
/// This test runs only if an XISO is available in the parent of the repo
|
||||
/// root — matches the developer's local layout for the real disc. CI
|
||||
/// machines without the disc simply skip the test (early-return Ok).
|
||||
#[test]
|
||||
fn nested_file_resolves_when_disc_present() {
|
||||
let disc_path = std::path::Path::new(
|
||||
"../../../Project Sylpheed - Arc of Deception (USA, Europe) (En,Ja).iso",
|
||||
);
|
||||
if !disc_path.exists() {
|
||||
eprintln!("skipping: disc image not present at {:?}", disc_path);
|
||||
return;
|
||||
}
|
||||
let dev = DiscImageDevice::open("disc", disc_path).expect("open xiso");
|
||||
// Both a top-level and a nested file must be visible.
|
||||
assert!(
|
||||
dev.entries.iter().any(|e| e.name == "default.xex"),
|
||||
"default.xex must be at the root"
|
||||
);
|
||||
assert!(
|
||||
dev.entries
|
||||
.iter()
|
||||
.any(|e| e.name.eq_ignore_ascii_case("dat/tables.pak")),
|
||||
"nested entry dat/tables.pak missing — subdirectory enumeration broken",
|
||||
);
|
||||
// And read_file must be able to fetch the nested bytes.
|
||||
let bytes = dev
|
||||
.read_file("dat/tables.pak")
|
||||
.expect("read_file on nested path");
|
||||
assert!(!bytes.is_empty(), "nested read returned empty buffer");
|
||||
}
|
||||
|
||||
/// Build a one-node GDFX directory buffer in memory and parse it with
|
||||
/// `collect_entries`, asserting the real on-disc attribute byte is
|
||||
/// forwarded into `VfsEntry.attributes` (with READONLY OR'd in, matching
|
||||
/// canary `disc_image_device.cc:154`) rather than synthesised from the
|
||||
/// path shape.
|
||||
fn parse_single_entry(name: &str, on_disc_attr: u8) -> VfsEntry {
|
||||
// GDFX dirent: node_l(u16) node_r(u16) sector(u32) length(u32)
|
||||
// attributes(u8) name_length(u8) name(bytes). The directory bit
|
||||
// gates subdirectory descent; use length=0 so a "directory" entry
|
||||
// is treated as an empty leaf and we don't recurse off the buffer.
|
||||
let mut buf = Vec::new();
|
||||
buf.extend_from_slice(&0u16.to_le_bytes()); // node_l
|
||||
buf.extend_from_slice(&0u16.to_le_bytes()); // node_r
|
||||
buf.extend_from_slice(&0u32.to_le_bytes()); // sector
|
||||
buf.extend_from_slice(&0u32.to_le_bytes()); // length (0 => leaf)
|
||||
buf.push(on_disc_attr); // attributes
|
||||
buf.push(name.len() as u8); // name_length
|
||||
buf.extend_from_slice(name.as_bytes());
|
||||
|
||||
let mut dev = DiscImageDevice {
|
||||
name: "test".into(),
|
||||
path: std::path::PathBuf::new(),
|
||||
game_offset: 0,
|
||||
entries: Vec::new(),
|
||||
};
|
||||
// `file` is only touched when descending into a non-empty directory;
|
||||
// our length=0 entries never recurse, so a dummy handle is fine.
|
||||
let mut file = std::fs::File::open("/dev/null").expect("open /dev/null");
|
||||
dev.collect_entries(&mut file, &buf, 0, "").expect("parse");
|
||||
assert_eq!(dev.entries.len(), 1);
|
||||
dev.entries.into_iter().next().unwrap()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn directory_entry_reports_directory_attribute() {
|
||||
// On-disc 0x10 (DIRECTORY) -> attributes carries 0x10 and READONLY.
|
||||
let e = parse_single_entry("dat", FILE_ATTRIBUTE_DIRECTORY);
|
||||
assert!(e.is_directory, "directory bit not decoded");
|
||||
assert_ne!(
|
||||
e.attributes & 0x10,
|
||||
0,
|
||||
"FILE_ATTRIBUTE_DIRECTORY must be set for a directory entry"
|
||||
);
|
||||
assert_ne!(e.attributes & 0x01, 0, "READONLY must be OR'd in (canary)");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn file_entry_has_no_directory_attribute() {
|
||||
// On-disc 0x80 (NORMAL) -> not a directory; READONLY still OR'd in.
|
||||
let e = parse_single_entry("default.xex", 0x80);
|
||||
assert!(!e.is_directory, "non-directory misdecoded as directory");
|
||||
assert_eq!(
|
||||
e.attributes & 0x10,
|
||||
0,
|
||||
"FILE_ATTRIBUTE_DIRECTORY must be clear for a file entry"
|
||||
);
|
||||
assert_ne!(e.attributes & 0x80, 0, "NORMAL bit must be preserved");
|
||||
assert_ne!(e.attributes & 0x01, 0, "READONLY must be OR'd in (canary)");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn archive_and_hidden_bits_are_preserved() {
|
||||
// ARCHIVE(0x20) | HIDDEN(0x02) authored on disc must survive intact.
|
||||
let e = parse_single_entry("save.dat", 0x20 | 0x02);
|
||||
assert_eq!(e.attributes & 0x20, 0x20, "ARCHIVE bit dropped");
|
||||
assert_eq!(e.attributes & 0x02, 0x02, "HIDDEN bit dropped");
|
||||
assert_eq!(e.attributes & 0x10, 0, "spurious DIRECTORY bit");
|
||||
}
|
||||
}
|
||||
43
crates/sylpheed-xex/src/vfs/mod.rs
Normal file
43
crates/sylpheed-xex/src/vfs/mod.rs
Normal file
@@ -0,0 +1,43 @@
|
||||
pub mod device;
|
||||
pub mod disc_image;
|
||||
|
||||
use thiserror::Error;
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum VfsError {
|
||||
#[error("I/O error: {0}")]
|
||||
Io(#[from] std::io::Error),
|
||||
|
||||
#[error("Invalid format: {0}")]
|
||||
InvalidFormat(String),
|
||||
|
||||
#[error("File not found: {0}")]
|
||||
NotFound(String),
|
||||
}
|
||||
|
||||
/// A virtual filesystem entry (file or directory).
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct VfsEntry {
|
||||
pub name: String,
|
||||
pub is_directory: bool,
|
||||
pub size: u64,
|
||||
pub offset: u64,
|
||||
/// Xbox `FILE_ATTRIBUTE_*` bitmask for this entry, sourced from the
|
||||
/// backing device's real on-disc metadata rather than inferred from
|
||||
/// the path shape. For GDFX disc images this is the on-disc attribute
|
||||
/// byte at dirent offset +12 OR'd with `FILE_ATTRIBUTE_READONLY`
|
||||
/// (matches xenia-canary `disc_image_device.cc:154`:
|
||||
/// `entry->attributes_ = attributes | kFileAttributeReadOnly`).
|
||||
///
|
||||
/// Bit layout (canary `vfs/entry.h:66-76`): READONLY=0x01, HIDDEN=0x02,
|
||||
/// SYSTEM=0x04, DIRECTORY=0x10, ARCHIVE=0x20, NORMAL=0x80.
|
||||
pub attributes: u32,
|
||||
}
|
||||
|
||||
/// Trait for VFS device implementations (XISO, STFS, host path, etc.)
|
||||
pub trait VfsDevice: Send + Sync {
|
||||
fn name(&self) -> &str;
|
||||
fn list_root(&self) -> Result<Vec<VfsEntry>, VfsError>;
|
||||
fn read_file(&self, path: &str) -> Result<Vec<u8>, VfsError>;
|
||||
fn stat(&self, path: &str) -> Result<VfsEntry, VfsError>;
|
||||
}
|
||||
Reference in New Issue
Block a user