Files
xex2tractor/tests/integration.rs
MechaCat02 475e1d555c feat: add PE verification and complete extraction pipeline (M6)
Add PE header verification (MZ signature, PE signature, POWERPCBE
machine type) to the extraction pipeline. Implement master key trial
with validation for encrypted files. Update CLI extract command to
show encryption/compression info. Update README with extract usage.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-28 21:51:52 +01:00

443 lines
16 KiB
Rust

use xex2tractor::crypto;
use xex2tractor::extract;
use xex2tractor::header::{ModuleFlags, XEX2_MAGIC};
use xex2tractor::optional::{CompressionInfo, CompressionType, EncryptionType, SystemFlags};
use xex2tractor::security::{ImageFlags, MediaFlags, RegionFlags};
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());
}
// ── Security info tests ───────────────────────────────────────────────────────
#[test]
fn test_security_info_parsed() {
let data = sample_data();
let xex = xex2tractor::parse(&data).unwrap();
let sec = &xex.security_info;
assert_eq!(sec.header_size, 0x00000F34);
assert_eq!(sec.image_size, 0x00920000);
assert_eq!(sec.unk_108, 0x00000174);
assert_eq!(sec.load_address, 0x82000000);
assert_eq!(sec.import_table_count, 2);
assert_eq!(sec.export_table, 0);
}
#[test]
fn test_security_flags() {
let data = sample_data();
let xex = xex2tractor::parse(&data).unwrap();
let sec = &xex.security_info;
assert_eq!(sec.image_flags, ImageFlags(0x00000008));
assert_eq!(sec.region, RegionFlags(0xFFFFFFFF));
assert_eq!(sec.allowed_media_types, MediaFlags(0x00000004));
}
#[test]
fn test_security_page_descriptors() {
let data = sample_data();
let xex = xex2tractor::parse(&data).unwrap();
let sec = &xex.security_info;
assert_eq!(sec.page_descriptor_count, 146);
assert_eq!(sec.page_descriptors.len(), 146);
// First descriptor has page_count = 19
assert_eq!(sec.page_descriptors[0].page_count, 19);
// Page size should be 64KB (4KB flag is not set)
assert_eq!(sec.image_flags.page_size(), 0x10000);
// Each page descriptor should have a valid page_count
for desc in &sec.page_descriptors {
assert!(desc.page_count > 0, "page_count should be positive");
}
}
#[test]
fn test_security_crypto_fields() {
let data = sample_data();
let xex = xex2tractor::parse(&data).unwrap();
let sec = &xex.security_info;
// RSA signature starts with 2C94EBE6
assert_eq!(sec.rsa_signature[0..4], [0x2C, 0x94, 0xEB, 0xE6]);
// XGD2 media ID starts with 3351
assert_eq!(sec.xgd2_media_id[0..2], [0x33, 0x51]);
// AES key starts with EACB
assert_eq!(sec.aes_key[0..2], [0xEA, 0xCB]);
}
// ── Compression info tests ────────────────────────────────────────────────────
#[test]
fn test_compression_info_normal() {
let data = sample_data();
let xex = xex2tractor::parse(&data).unwrap();
let fmt = xex.optional_headers.file_format_info.as_ref().unwrap();
match &fmt.compression_info {
CompressionInfo::Normal {
window_size,
first_block,
} => {
// Window size should be a power of 2
assert!(window_size.is_power_of_two(), "window_size should be power of 2");
assert!(*window_size > 0);
// First block should have non-zero size
assert!(first_block.block_size > 0);
// Block hash should not be all zeros
assert!(!first_block.block_hash.iter().all(|&b| b == 0));
}
other => panic!("expected Normal compression info, got {other:?}"),
}
}
// ── Crypto tests ─────────────────────────────────────────────────────────────
#[test]
fn test_session_key_derivation() {
let data = sample_data();
let xex = xex2tractor::parse(&data).unwrap();
let session_key = crypto::derive_session_key(&xex.security_info.aes_key);
// Session key should be non-zero
assert!(!session_key.iter().all(|&b| b == 0));
// Session key should differ from encrypted key
assert_ne!(&session_key[..], &xex.security_info.aes_key[..]);
}
#[test]
fn test_payload_decryption() {
let data = sample_data();
let xex = xex2tractor::parse(&data).unwrap();
let session_key = crypto::derive_session_key(&xex.security_info.aes_key);
// Decrypt the first 256 bytes of payload
let payload_start = xex.header.header_size as usize;
let mut payload_head = data[payload_start..payload_start + 256].to_vec();
let original = payload_head.clone();
crypto::decrypt_in_place(&session_key, &mut payload_head);
// Decrypted data should differ from encrypted
assert_ne!(payload_head, original);
}
// ── CLI tests ─────────────────────────────────────────────────────────────────
#[test]
fn test_cli_inspect_with_sample() {
let path = format!("{}/tests/data/default.xex", env!("CARGO_MANIFEST_DIR"));
let output = std::process::Command::new(env!("CARGO_BIN_EXE_xex2tractor"))
.args(["inspect", &path])
.output()
.expect("failed to run xex2tractor");
assert!(output.status.success(), "CLI inspect 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("Window Size:")); // new compression info
assert!(stdout.contains("First Block:")); // new compression info
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
// Security info section
assert!(stdout.contains("Security Info"));
assert!(stdout.contains("0x00000F34")); // header size
assert!(stdout.contains("0x00920000")); // image size
assert!(stdout.contains("XGD2_MEDIA_ONLY")); // image flags
assert!(stdout.contains("ALL REGIONS")); // region
assert!(stdout.contains("DVD_CD")); // media type
assert!(stdout.contains("Page Descriptors")); // page descriptors section
}
#[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_inspect_missing_file() {
let output = std::process::Command::new(env!("CARGO_BIN_EXE_xex2tractor"))
.args(["inspect", "/nonexistent/file.xex"])
.output()
.expect("failed to run xex2tractor");
assert!(!output.status.success());
}
// ── Extraction tests ─────────────────────────────────────────────────────────
#[test]
fn test_extract_pe_image() {
let data = sample_data();
let xex = xex2tractor::parse(&data).unwrap();
let pe_image = extract::extract_pe_image(&data, &xex).unwrap();
// Output size should match security_info.image_size
assert_eq!(pe_image.len(), xex.security_info.image_size as usize);
}
#[test]
fn test_extract_pe_starts_with_mz() {
let data = sample_data();
let xex = xex2tractor::parse(&data).unwrap();
let pe_image = extract::extract_pe_image(&data, &xex).unwrap();
// PE image must start with MZ signature (0x4D5A)
assert_eq!(pe_image[0], 0x4D, "first byte should be 'M'");
assert_eq!(pe_image[1], 0x5A, "second byte should be 'Z'");
}
#[test]
fn test_extract_pe_has_valid_pe_header() {
let data = sample_data();
let xex = xex2tractor::parse(&data).unwrap();
let pe_image = extract::extract_pe_image(&data, &xex).unwrap();
// Read e_lfanew from DOS header (offset 0x3C, little-endian per PE spec)
let e_lfanew = u32::from_le_bytes([
pe_image[0x3C],
pe_image[0x3D],
pe_image[0x3E],
pe_image[0x3F],
]) as usize;
// PE signature at e_lfanew: "PE\0\0"
assert_eq!(&pe_image[e_lfanew..e_lfanew + 4], b"PE\0\0");
// Machine type at e_lfanew + 4: 0x01F2 (IMAGE_FILE_MACHINE_POWERPCBE, little-endian)
let machine = u16::from_le_bytes([pe_image[e_lfanew + 4], pe_image[e_lfanew + 5]]);
assert_eq!(machine, 0x01F2, "machine should be POWERPCBE");
}
#[test]
fn test_cli_extract_writes_file() {
let path = format!("{}/tests/data/default.xex", env!("CARGO_MANIFEST_DIR"));
let output_path = format!("{}/target/test_extract_output.exe", env!("CARGO_MANIFEST_DIR"));
// Clean up any previous test output
let _ = std::fs::remove_file(&output_path);
let output = std::process::Command::new(env!("CARGO_BIN_EXE_xex2tractor"))
.args(["extract", &path, &output_path])
.output()
.expect("failed to run xex2tractor");
assert!(output.status.success(), "CLI extract should succeed");
// Verify output file exists and starts with MZ
let extracted = std::fs::read(&output_path).expect("should be able to read extracted file");
assert!(extracted.len() > 2);
assert_eq!(extracted[0], 0x4D); // 'M'
assert_eq!(extracted[1], 0x5A); // 'Z'
// Clean up
let _ = std::fs::remove_file(&output_path);
}
#[test]
fn test_cli_extract_default_output_path() {
let path = format!("{}/tests/data/default.xex", env!("CARGO_MANIFEST_DIR"));
let expected_output = format!("{}/tests/data/default.exe", env!("CARGO_MANIFEST_DIR"));
// Clean up any previous test output
let _ = std::fs::remove_file(&expected_output);
let output = std::process::Command::new(env!("CARGO_BIN_EXE_xex2tractor"))
.args(["extract", &path])
.output()
.expect("failed to run xex2tractor");
assert!(output.status.success(), "CLI extract should succeed");
// Verify default output path was used
assert!(
std::fs::metadata(&expected_output).is_ok(),
"default output file should exist"
);
// Clean up
let _ = std::fs::remove_file(&expected_output);
}
#[test]
fn test_extract_pe_verification_runs() {
let data = sample_data();
let xex = xex2tractor::parse(&data).unwrap();
let pe_image = extract::extract_pe_image(&data, &xex).unwrap();
// verify_pe_image should succeed on the extracted image
assert!(extract::verify_pe_image(&pe_image).is_ok());
}
#[test]
fn test_cli_extract_shows_format_info() {
let path = format!("{}/tests/data/default.xex", env!("CARGO_MANIFEST_DIR"));
let output_path = format!("{}/target/test_extract_info.exe", env!("CARGO_MANIFEST_DIR"));
let _ = std::fs::remove_file(&output_path);
let output = std::process::Command::new(env!("CARGO_BIN_EXE_xex2tractor"))
.args(["extract", &path, &output_path])
.output()
.expect("failed to run xex2tractor");
assert!(output.status.success());
let stdout = String::from_utf8_lossy(&output.stdout);
assert!(stdout.contains("Encryption:"));
assert!(stdout.contains("Compression:"));
assert!(stdout.contains("Extracted PE image"));
let _ = std::fs::remove_file(&output_path);
}