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>
179 lines
6.2 KiB
Rust
179 lines
6.2 KiB
Rust
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();
|
|
let xex = xex2tractor::parse(&data).unwrap();
|
|
|
|
assert_eq!(xex.header.magic, XEX2_MAGIC);
|
|
assert_eq!(xex.header.module_flags, ModuleFlags(0x00000001));
|
|
assert_eq!(xex.header.header_size, 0x00003000);
|
|
assert_eq!(xex.header.reserved, 0x00000000);
|
|
assert_eq!(xex.header.security_offset, 0x00000090);
|
|
assert_eq!(xex.header.header_count, 15);
|
|
}
|
|
|
|
#[test]
|
|
fn test_parse_empty_file() {
|
|
let data = vec![];
|
|
assert!(xex2tractor::parse(&data).is_err());
|
|
}
|
|
|
|
#[test]
|
|
fn test_parse_invalid_magic() {
|
|
let mut data = sample_data();
|
|
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"));
|
|
let output = std::process::Command::new(env!("CARGO_BIN_EXE_xex2tractor"))
|
|
.arg(&path)
|
|
.output()
|
|
.expect("failed to run xex2tractor");
|
|
|
|
assert!(output.status.success(), "CLI should exit successfully");
|
|
let stdout = String::from_utf8_lossy(&output.stdout);
|
|
|
|
// 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]
|
|
fn test_cli_no_args() {
|
|
let output = std::process::Command::new(env!("CARGO_BIN_EXE_xex2tractor"))
|
|
.output()
|
|
.expect("failed to run xex2tractor");
|
|
|
|
assert!(!output.status.success());
|
|
let stderr = String::from_utf8_lossy(&output.stderr);
|
|
assert!(stderr.contains("Usage"));
|
|
}
|
|
|
|
#[test]
|
|
fn test_cli_missing_file() {
|
|
let output = std::process::Command::new(env!("CARGO_BIN_EXE_xex2tractor"))
|
|
.arg("/nonexistent/file.xex")
|
|
.output()
|
|
.expect("failed to run xex2tractor");
|
|
|
|
assert!(!output.status.success());
|
|
}
|