feat: parse and display all optional headers (M2)
Implement parsing for all 15 optional header types found in XEX2 files: inline values (entry point, base address, stack size, system flags), fixed-size structures (execution info, file format, TLS, game ratings, LAN key, checksum/timestamp), and variable-size structures (static libraries, import libraries, resource info, original PE name, Xbox 360 logo). Add comprehensive unit and integration tests. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2
Cargo.lock
generated
2
Cargo.lock
generated
@@ -4,4 +4,4 @@ version = 4
|
||||
|
||||
[[package]]
|
||||
name = "xex2tractor"
|
||||
version = "0.1.0"
|
||||
version = "0.2.0"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "xex2tractor"
|
||||
version = "0.1.0"
|
||||
version = "0.2.0"
|
||||
edition = "2024"
|
||||
description = "A tool for extracting and inspecting Xbox 360 XEX2 executable files"
|
||||
license = "MIT"
|
||||
|
||||
26
README.md
26
README.md
@@ -18,6 +18,32 @@ Header Size: 0x00003000 (12288 bytes)
|
||||
Reserved: 0x00000000
|
||||
Security Offset: 0x00000090
|
||||
Header Count: 15
|
||||
|
||||
=== Optional Headers (15 entries) ===
|
||||
|
||||
[ENTRY_POINT] 0x824AB748
|
||||
[IMAGE_BASE_ADDRESS] 0x82000000
|
||||
[DEFAULT_STACK_SIZE] 0x00080000 (524288 bytes)
|
||||
[SYSTEM_FLAGS] 0x00000400 [PAL50_INCOMPATIBLE]
|
||||
|
||||
[EXECUTION_INFO]
|
||||
Media ID: 0x2D2E2EEB
|
||||
Title ID: 0x535107D4
|
||||
Version: 0.0.0.2
|
||||
...
|
||||
|
||||
[FILE_FORMAT_INFO]
|
||||
Encryption: Normal (AES-128-CBC)
|
||||
Compression: Normal (LZX)
|
||||
|
||||
[STATIC_LIBRARIES] (12 libraries)
|
||||
XAPILIB 2.0.3215.0 (Unknown(64))
|
||||
D3D9 2.0.3215.1 (Unknown(64))
|
||||
...
|
||||
|
||||
[IMPORT_LIBRARIES] (2 libraries)
|
||||
xam.xex v2.0.4552.0 (min v2.0.4552.0) - 104 imports
|
||||
xboxkrnl.exe v2.0.4552.0 (min v2.0.4552.0) - 294 imports
|
||||
```
|
||||
|
||||
## Building
|
||||
|
||||
186
src/display.rs
186
src/display.rs
@@ -1,5 +1,8 @@
|
||||
/// Pretty-print formatting for parsed XEX2 structures.
|
||||
use crate::header::Xex2Header;
|
||||
use crate::optional::{
|
||||
format_hex_bytes, format_rating, format_timestamp, HeaderKey, OptionalHeaders,
|
||||
};
|
||||
|
||||
/// Prints the XEX2 main header in a human-readable format.
|
||||
pub fn display_header(header: &Xex2Header) {
|
||||
@@ -14,3 +17,186 @@ pub fn display_header(header: &Xex2Header) {
|
||||
println!("Security Offset: 0x{:08X}", header.security_offset);
|
||||
println!("Header Count: {}", header.header_count);
|
||||
}
|
||||
|
||||
/// Prints all parsed optional headers in a human-readable format.
|
||||
pub fn display_optional_headers(headers: &OptionalHeaders) {
|
||||
println!();
|
||||
println!("=== Optional Headers ({} entries) ===", headers.entries.len());
|
||||
|
||||
// Display inline u32 values first
|
||||
if let Some(v) = headers.entry_point {
|
||||
println!();
|
||||
println!("[ENTRY_POINT] 0x{v:08X}");
|
||||
}
|
||||
if let Some(v) = headers.original_base_address {
|
||||
println!("[ORIGINAL_BASE_ADDRESS] 0x{v:08X}");
|
||||
}
|
||||
if let Some(v) = headers.image_base_address {
|
||||
println!("[IMAGE_BASE_ADDRESS] 0x{v:08X}");
|
||||
}
|
||||
if let Some(v) = headers.default_stack_size {
|
||||
println!("[DEFAULT_STACK_SIZE] 0x{v:08X} ({v} bytes)");
|
||||
}
|
||||
if let Some(v) = headers.default_filesystem_cache_size {
|
||||
println!("[DEFAULT_FILESYSTEM_CACHE_SIZE] 0x{v:08X} ({v} bytes)");
|
||||
}
|
||||
if let Some(v) = headers.default_heap_size {
|
||||
println!("[DEFAULT_HEAP_SIZE] 0x{v:08X} ({v} bytes)");
|
||||
}
|
||||
if let Some(v) = headers.title_workspace_size {
|
||||
println!("[TITLE_WORKSPACE_SIZE] 0x{v:08X} ({v} bytes)");
|
||||
}
|
||||
if let Some(v) = headers.additional_title_memory {
|
||||
println!("[ADDITIONAL_TITLE_MEMORY] 0x{v:08X} ({v} bytes)");
|
||||
}
|
||||
if let Some(v) = headers.enabled_for_fastcap {
|
||||
println!("[ENABLED_FOR_FASTCAP] 0x{v:08X}");
|
||||
}
|
||||
|
||||
// System flags
|
||||
if let Some(ref flags) = headers.system_flags {
|
||||
println!("[SYSTEM_FLAGS] {flags}");
|
||||
}
|
||||
|
||||
// Execution info
|
||||
if let Some(ref exec) = headers.execution_info {
|
||||
println!();
|
||||
println!("[EXECUTION_INFO]");
|
||||
println!(" Media ID: 0x{:08X}", exec.media_id);
|
||||
println!(" Title ID: 0x{:08X}", exec.title_id);
|
||||
println!(" Version: {}", exec.version);
|
||||
println!(" Base Version: {}", exec.base_version);
|
||||
println!(" Platform: {}", exec.platform);
|
||||
println!(" Executable Type: {}", exec.executable_type);
|
||||
println!(" Disc: {}/{}", exec.disc_number, exec.disc_count);
|
||||
println!(" Savegame ID: 0x{:08X}", exec.savegame_id);
|
||||
}
|
||||
|
||||
// File format info
|
||||
if let Some(ref fmt) = headers.file_format_info {
|
||||
println!();
|
||||
println!("[FILE_FORMAT_INFO]");
|
||||
println!(" Encryption: {}", fmt.encryption_type);
|
||||
println!(" Compression: {}", fmt.compression_type);
|
||||
}
|
||||
|
||||
// Checksum + timestamp
|
||||
if let Some(ref ct) = headers.checksum_timestamp {
|
||||
println!();
|
||||
println!("[CHECKSUM_TIMESTAMP]");
|
||||
println!(" Checksum: 0x{:08X}", ct.checksum);
|
||||
println!(
|
||||
" Timestamp: 0x{:08X} ({})",
|
||||
ct.timestamp,
|
||||
format_timestamp(ct.timestamp)
|
||||
);
|
||||
}
|
||||
|
||||
// Original PE name
|
||||
if let Some(ref name) = headers.original_pe_name {
|
||||
println!();
|
||||
println!("[ORIGINAL_PE_NAME] \"{name}\"");
|
||||
}
|
||||
|
||||
// Bounding path
|
||||
if let Some(ref path) = headers.bounding_path {
|
||||
println!("[BOUNDING_PATH] \"{path}\"");
|
||||
}
|
||||
|
||||
// TLS info
|
||||
if let Some(ref tls) = headers.tls_info {
|
||||
println!();
|
||||
println!("[TLS_INFO]");
|
||||
println!(" Slot Count: {}", tls.slot_count);
|
||||
println!(" Raw Data Address: 0x{:08X}", tls.raw_data_address);
|
||||
println!(" Data Size: {} bytes", tls.data_size);
|
||||
println!(" Raw Data Size: {} bytes", tls.raw_data_size);
|
||||
}
|
||||
|
||||
// Static libraries
|
||||
if let Some(ref libs) = headers.static_libraries {
|
||||
println!();
|
||||
println!("[STATIC_LIBRARIES] ({} libraries)", libs.len());
|
||||
for lib in libs {
|
||||
println!(" {lib}");
|
||||
}
|
||||
}
|
||||
|
||||
// Import libraries
|
||||
if let Some(ref imports) = headers.import_libraries {
|
||||
println!();
|
||||
println!("[IMPORT_LIBRARIES] ({} libraries)", imports.libraries.len());
|
||||
for lib in &imports.libraries {
|
||||
println!(
|
||||
" {} v{} (min v{}) - {} imports",
|
||||
lib.name, lib.version, lib.version_min, lib.record_count
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Resource info
|
||||
if let Some(ref resources) = headers.resource_info {
|
||||
println!();
|
||||
println!("[RESOURCE_INFO] ({} entries)", resources.len());
|
||||
for res in resources {
|
||||
println!(
|
||||
" \"{}\" @ 0x{:08X}, size: {} bytes",
|
||||
res.name, res.address, res.size
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Game ratings
|
||||
if let Some(ref ratings) = headers.game_ratings {
|
||||
println!();
|
||||
println!("[GAME_RATINGS]");
|
||||
println!(" ESRB: {} | PEGI: {} | PEGI-FI: {} | PEGI-PT: {}",
|
||||
format_rating(ratings.esrb), format_rating(ratings.pegi),
|
||||
format_rating(ratings.pegi_fi), format_rating(ratings.pegi_pt));
|
||||
println!(" BBFC: {} | CERO: {} | USK: {} | OFLC-AU: {}",
|
||||
format_rating(ratings.bbfc), format_rating(ratings.cero),
|
||||
format_rating(ratings.usk), format_rating(ratings.oflc_au));
|
||||
println!(" OFLC-NZ: {} | KMRB: {} | Brazil: {} | FPB: {}",
|
||||
format_rating(ratings.oflc_nz), format_rating(ratings.kmrb),
|
||||
format_rating(ratings.brazil), format_rating(ratings.fpb));
|
||||
}
|
||||
|
||||
// LAN key
|
||||
if let Some(ref key) = headers.lan_key {
|
||||
println!();
|
||||
println!("[LAN_KEY] {}", format_hex_bytes(key));
|
||||
}
|
||||
|
||||
// Callcap imports
|
||||
if let Some(ref callcap) = headers.enabled_for_callcap {
|
||||
println!();
|
||||
println!("[ENABLED_FOR_CALLCAP]");
|
||||
println!(" Start Thunk: 0x{:08X}", callcap.start_func_thunk_addr);
|
||||
println!(" End Thunk: 0x{:08X}", callcap.end_func_thunk_addr);
|
||||
}
|
||||
|
||||
// Exports by name
|
||||
if let Some(ref dir) = headers.exports_by_name {
|
||||
println!();
|
||||
println!("[EXPORTS_BY_NAME]");
|
||||
println!(" Offset: 0x{:08X}", dir.offset);
|
||||
println!(" Size: {} bytes", dir.size);
|
||||
}
|
||||
|
||||
// Xbox 360 logo
|
||||
if let Some(size) = headers.xbox360_logo_size {
|
||||
println!();
|
||||
println!("[XBOX360_LOGO] {size} bytes");
|
||||
}
|
||||
|
||||
// Unknown headers
|
||||
for entry in &headers.entries {
|
||||
if let HeaderKey::Unknown(raw) = entry.key {
|
||||
println!();
|
||||
println!(
|
||||
"[UNKNOWN(0x{raw:08X})] value/offset: 0x{:08X}",
|
||||
entry.value
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
10
src/lib.rs
10
src/lib.rs
@@ -9,16 +9,20 @@
|
||||
pub mod display;
|
||||
pub mod error;
|
||||
pub mod header;
|
||||
pub mod optional;
|
||||
pub mod util;
|
||||
|
||||
use error::Result;
|
||||
use header::Xex2Header;
|
||||
use optional::OptionalHeaders;
|
||||
|
||||
/// A parsed XEX2 file containing all extracted structures.
|
||||
#[derive(Debug)]
|
||||
pub struct Xex2File {
|
||||
/// The main XEX2 header (magic, flags, sizes, offsets).
|
||||
pub header: Xex2Header,
|
||||
/// All parsed optional headers.
|
||||
pub optional_headers: OptionalHeaders,
|
||||
}
|
||||
|
||||
/// Parses an XEX2 file from a byte slice.
|
||||
@@ -27,6 +31,10 @@ pub struct Xex2File {
|
||||
/// Returns a [`Xex2File`] with all successfully parsed structures.
|
||||
pub fn parse(data: &[u8]) -> Result<Xex2File> {
|
||||
let header = header::parse_header(data)?;
|
||||
let optional_headers = optional::parse_optional_headers(data, &header)?;
|
||||
|
||||
Ok(Xex2File { header })
|
||||
Ok(Xex2File {
|
||||
header,
|
||||
optional_headers,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -26,4 +26,5 @@ fn main() {
|
||||
};
|
||||
|
||||
xex2tractor::display::display_header(&xex.header);
|
||||
xex2tractor::display::display_optional_headers(&xex.optional_headers);
|
||||
}
|
||||
|
||||
1242
src/optional.rs
Normal file
1242
src/optional.rs
Normal file
File diff suppressed because it is too large
Load Diff
@@ -1,10 +1,13 @@
|
||||
use xex2tractor::header::{ModuleFlags, XEX2_MAGIC};
|
||||
use xex2tractor::optional::{CompressionType, EncryptionType, SystemFlags};
|
||||
|
||||
fn sample_data() -> Vec<u8> {
|
||||
let path = format!("{}/tests/data/default.xex", env!("CARGO_MANIFEST_DIR"));
|
||||
std::fs::read(&path).expect("sample file should exist at tests/data/default.xex")
|
||||
}
|
||||
|
||||
// ── Header tests ──────────────────────────────────────────────────────────────
|
||||
|
||||
#[test]
|
||||
fn test_full_parse() {
|
||||
let data = sample_data();
|
||||
@@ -27,11 +30,98 @@ fn test_parse_empty_file() {
|
||||
#[test]
|
||||
fn test_parse_invalid_magic() {
|
||||
let mut data = sample_data();
|
||||
// Corrupt the magic bytes
|
||||
data[0] = 0x00;
|
||||
assert!(xex2tractor::parse(&data).is_err());
|
||||
}
|
||||
|
||||
// ── Optional header tests ─────────────────────────────────────────────────────
|
||||
|
||||
#[test]
|
||||
fn test_optional_headers_all_present() {
|
||||
let data = sample_data();
|
||||
let xex = xex2tractor::parse(&data).unwrap();
|
||||
let opt = &xex.optional_headers;
|
||||
|
||||
// All 15 entries should be parsed
|
||||
assert_eq!(opt.entries.len(), 15);
|
||||
|
||||
// Verify presence of all expected headers
|
||||
assert!(opt.entry_point.is_some());
|
||||
assert!(opt.image_base_address.is_some());
|
||||
assert!(opt.default_stack_size.is_some());
|
||||
assert!(opt.system_flags.is_some());
|
||||
assert!(opt.execution_info.is_some());
|
||||
assert!(opt.file_format_info.is_some());
|
||||
assert!(opt.checksum_timestamp.is_some());
|
||||
assert!(opt.original_pe_name.is_some());
|
||||
assert!(opt.tls_info.is_some());
|
||||
assert!(opt.static_libraries.is_some());
|
||||
assert!(opt.import_libraries.is_some());
|
||||
assert!(opt.resource_info.is_some());
|
||||
assert!(opt.game_ratings.is_some());
|
||||
assert!(opt.lan_key.is_some());
|
||||
assert!(opt.xbox360_logo_size.is_some());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_optional_inline_values() {
|
||||
let data = sample_data();
|
||||
let xex = xex2tractor::parse(&data).unwrap();
|
||||
let opt = &xex.optional_headers;
|
||||
|
||||
assert_eq!(opt.entry_point.unwrap(), 0x824AB748);
|
||||
assert_eq!(opt.image_base_address.unwrap(), 0x82000000);
|
||||
assert_eq!(opt.default_stack_size.unwrap(), 0x00080000);
|
||||
assert_eq!(opt.system_flags.unwrap(), SystemFlags(0x00000400));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_optional_execution_info() {
|
||||
let data = sample_data();
|
||||
let xex = xex2tractor::parse(&data).unwrap();
|
||||
let exec = xex.optional_headers.execution_info.as_ref().unwrap();
|
||||
|
||||
assert_eq!(exec.title_id, 0x535107D4);
|
||||
assert_eq!(exec.media_id, 0x2D2E2EEB);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_optional_file_format() {
|
||||
let data = sample_data();
|
||||
let xex = xex2tractor::parse(&data).unwrap();
|
||||
let fmt = xex.optional_headers.file_format_info.as_ref().unwrap();
|
||||
|
||||
assert_eq!(fmt.encryption_type, EncryptionType::Normal);
|
||||
assert_eq!(fmt.compression_type, CompressionType::Normal);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_optional_static_libraries() {
|
||||
let data = sample_data();
|
||||
let xex = xex2tractor::parse(&data).unwrap();
|
||||
let libs = xex.optional_headers.static_libraries.as_ref().unwrap();
|
||||
|
||||
assert_eq!(libs.len(), 12);
|
||||
// Verify first and a few known libraries
|
||||
assert_eq!(libs[0].name, "XAPILIB");
|
||||
assert_eq!(libs[1].name, "D3D9");
|
||||
assert_eq!(libs[3].name, "XBOXKRNL");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_optional_import_libraries() {
|
||||
let data = sample_data();
|
||||
let xex = xex2tractor::parse(&data).unwrap();
|
||||
let imports = xex.optional_headers.import_libraries.as_ref().unwrap();
|
||||
|
||||
assert_eq!(imports.string_table.len(), 2);
|
||||
assert_eq!(imports.string_table[0], "xam.xex");
|
||||
assert_eq!(imports.string_table[1], "xboxkrnl.exe");
|
||||
assert!(!imports.libraries.is_empty());
|
||||
}
|
||||
|
||||
// ── CLI tests ─────────────────────────────────────────────────────────────────
|
||||
|
||||
#[test]
|
||||
fn test_cli_runs_with_sample() {
|
||||
let path = format!("{}/tests/data/default.xex", env!("CARGO_MANIFEST_DIR"));
|
||||
@@ -42,10 +132,28 @@ fn test_cli_runs_with_sample() {
|
||||
|
||||
assert!(output.status.success(), "CLI should exit successfully");
|
||||
let stdout = String::from_utf8_lossy(&output.stdout);
|
||||
assert!(stdout.contains("XEX2 Header"), "should display header section");
|
||||
assert!(stdout.contains("0x58455832"), "should display magic value");
|
||||
assert!(stdout.contains("TITLE"), "should display module flag name");
|
||||
assert!(stdout.contains("Header Count: 15"), "should show 15 headers");
|
||||
|
||||
// Header section
|
||||
assert!(stdout.contains("XEX2 Header"));
|
||||
assert!(stdout.contains("0x58455832"));
|
||||
assert!(stdout.contains("TITLE"));
|
||||
assert!(stdout.contains("Header Count: 15"));
|
||||
|
||||
// Optional headers section
|
||||
assert!(stdout.contains("Optional Headers (15 entries)"));
|
||||
assert!(stdout.contains("[ENTRY_POINT] 0x824AB748"));
|
||||
assert!(stdout.contains("[IMAGE_BASE_ADDRESS] 0x82000000"));
|
||||
assert!(stdout.contains("EXECUTION_INFO"));
|
||||
assert!(stdout.contains("0x535107D4")); // title ID
|
||||
assert!(stdout.contains("FILE_FORMAT_INFO"));
|
||||
assert!(stdout.contains("Normal (AES-128-CBC)"));
|
||||
assert!(stdout.contains("Normal (LZX)"));
|
||||
assert!(stdout.contains("STATIC_LIBRARIES"));
|
||||
assert!(stdout.contains("XAPILIB"));
|
||||
assert!(stdout.contains("IMPORT_LIBRARIES"));
|
||||
assert!(stdout.contains("xboxkrnl.exe"));
|
||||
assert!(stdout.contains("default.pe")); // original PE name
|
||||
assert!(stdout.contains("PAL50_INCOMPATIBLE")); // system flags
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -54,9 +162,9 @@ fn test_cli_no_args() {
|
||||
.output()
|
||||
.expect("failed to run xex2tractor");
|
||||
|
||||
assert!(!output.status.success(), "CLI should fail without args");
|
||||
assert!(!output.status.success());
|
||||
let stderr = String::from_utf8_lossy(&output.stderr);
|
||||
assert!(stderr.contains("Usage"), "should print usage message");
|
||||
assert!(stderr.contains("Usage"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -66,5 +174,5 @@ fn test_cli_missing_file() {
|
||||
.output()
|
||||
.expect("failed to run xex2tractor");
|
||||
|
||||
assert!(!output.status.success(), "CLI should fail with missing file");
|
||||
assert!(!output.status.success());
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user