From a9436a3a7a8240d8677a6d9770fbe0ee92f55fb3 Mon Sep 17 00:00:00 2001 From: MechaCat02 Date: Sat, 28 Mar 2026 18:59:41 +0100 Subject: [PATCH] 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 --- Cargo.lock | 2 +- Cargo.toml | 2 +- README.md | 26 + src/display.rs | 186 +++++++ src/lib.rs | 10 +- src/main.rs | 1 + src/optional.rs | 1242 ++++++++++++++++++++++++++++++++++++++++++ tests/integration.rs | 124 ++++- 8 files changed, 1582 insertions(+), 11 deletions(-) create mode 100644 src/optional.rs diff --git a/Cargo.lock b/Cargo.lock index dc0426a..8db8de6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4,4 +4,4 @@ version = 4 [[package]] name = "xex2tractor" -version = "0.1.0" +version = "0.2.0" diff --git a/Cargo.toml b/Cargo.toml index 09e7515..4834c3f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -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" diff --git a/README.md b/README.md index b6476fd..8a85b9d 100644 --- a/README.md +++ b/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 diff --git a/src/display.rs b/src/display.rs index 72ecda6..505796e 100644 --- a/src/display.rs +++ b/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 + ); + } + } +} diff --git a/src/lib.rs b/src/lib.rs index 5b243d7..ed6877d 100644 --- a/src/lib.rs +++ b/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 { let header = header::parse_header(data)?; + let optional_headers = optional::parse_optional_headers(data, &header)?; - Ok(Xex2File { header }) + Ok(Xex2File { + header, + optional_headers, + }) } diff --git a/src/main.rs b/src/main.rs index 752a3f1..8b04f40 100644 --- a/src/main.rs +++ b/src/main.rs @@ -26,4 +26,5 @@ fn main() { }; xex2tractor::display::display_header(&xex.header); + xex2tractor::display::display_optional_headers(&xex.optional_headers); } diff --git a/src/optional.rs b/src/optional.rs new file mode 100644 index 0000000..f3ba338 --- /dev/null +++ b/src/optional.rs @@ -0,0 +1,1242 @@ +/// Optional header parsing for XEX2 files. +/// +/// Each optional header entry is 8 bytes: a 4-byte key and a 4-byte value/offset. +/// The low byte of the key determines how the value field is interpreted: +/// - `0x00` or `0x01`: the value field IS the data (inline) +/// - Any other value: the value field is a file offset to the actual data structure +use std::fmt; + +use crate::error::{Result, Xex2Error}; +use crate::header::Xex2Header; +use crate::util::{read_u16_be, read_u32_be, read_u8}; + +// ── Header Key Constants ────────────────────────────────────────────────────── + +/// Known optional header key values. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum HeaderKey { + ResourceInfo, + FileFormatInfo, + DeltaPatchDescriptor, + BaseReference, + DiscProfileId, + BoundingPath, + DeviceId, + OriginalBaseAddress, + EntryPoint, + ImageBaseAddress, + ImportLibraries, + ChecksumTimestamp, + EnabledForCallcap, + EnabledForFastcap, + OriginalPeName, + StaticLibraries, + TlsInfo, + DefaultStackSize, + DefaultFilesystemCacheSize, + DefaultHeapSize, + PageHeapSizeAndFlags, + SystemFlags, + SystemFlags32, + SystemFlags64, + ExecutionInfo, + TitleWorkspaceSize, + GameRatings, + LanKey, + Xbox360Logo, + MultidiscMediaIds, + AlternateTitleIds, + AdditionalTitleMemory, + ExportsByName, + Unknown(u32), +} + +impl HeaderKey { + /// Creates a `HeaderKey` from a raw 32-bit key value. + pub fn from_raw(raw: u32) -> Self { + match raw { + 0x000002FF => Self::ResourceInfo, + 0x000003FF => Self::FileFormatInfo, + 0x000005FF => Self::DeltaPatchDescriptor, + 0x00000405 => Self::BaseReference, + 0x00004304 => Self::DiscProfileId, + 0x000080FF => Self::BoundingPath, + 0x00008105 => Self::DeviceId, + 0x00010001 => Self::OriginalBaseAddress, + 0x00010100 => Self::EntryPoint, + 0x00010201 => Self::ImageBaseAddress, + 0x000103FF => Self::ImportLibraries, + 0x00018002 => Self::ChecksumTimestamp, + 0x00018102 => Self::EnabledForCallcap, + 0x00018200 => Self::EnabledForFastcap, + 0x000183FF => Self::OriginalPeName, + 0x000200FF => Self::StaticLibraries, + 0x00020104 => Self::TlsInfo, + 0x00020200 => Self::DefaultStackSize, + 0x00020301 => Self::DefaultFilesystemCacheSize, + 0x00020401 => Self::DefaultHeapSize, + 0x00028002 => Self::PageHeapSizeAndFlags, + 0x00030000 => Self::SystemFlags, + 0x00030100 => Self::SystemFlags32, + 0x00030200 => Self::SystemFlags64, + 0x00040006 => Self::ExecutionInfo, + 0x00040201 => Self::TitleWorkspaceSize, + 0x00040310 => Self::GameRatings, + 0x00040404 => Self::LanKey, + 0x000405FF => Self::Xbox360Logo, + 0x000406FF => Self::MultidiscMediaIds, + 0x000407FF => Self::AlternateTitleIds, + 0x00040801 => Self::AdditionalTitleMemory, + 0x00E10402 => Self::ExportsByName, + _ => Self::Unknown(raw), + } + } + + /// Returns the raw 32-bit key value. + pub fn raw_value(&self) -> u32 { + match self { + Self::ResourceInfo => 0x000002FF, + Self::FileFormatInfo => 0x000003FF, + Self::DeltaPatchDescriptor => 0x000005FF, + Self::BaseReference => 0x00000405, + Self::DiscProfileId => 0x00004304, + Self::BoundingPath => 0x000080FF, + Self::DeviceId => 0x00008105, + Self::OriginalBaseAddress => 0x00010001, + Self::EntryPoint => 0x00010100, + Self::ImageBaseAddress => 0x00010201, + Self::ImportLibraries => 0x000103FF, + Self::ChecksumTimestamp => 0x00018002, + Self::EnabledForCallcap => 0x00018102, + Self::EnabledForFastcap => 0x00018200, + Self::OriginalPeName => 0x000183FF, + Self::StaticLibraries => 0x000200FF, + Self::TlsInfo => 0x00020104, + Self::DefaultStackSize => 0x00020200, + Self::DefaultFilesystemCacheSize => 0x00020301, + Self::DefaultHeapSize => 0x00020401, + Self::PageHeapSizeAndFlags => 0x00028002, + Self::SystemFlags => 0x00030000, + Self::SystemFlags32 => 0x00030100, + Self::SystemFlags64 => 0x00030200, + Self::ExecutionInfo => 0x00040006, + Self::TitleWorkspaceSize => 0x00040201, + Self::GameRatings => 0x00040310, + Self::LanKey => 0x00040404, + Self::Xbox360Logo => 0x000405FF, + Self::MultidiscMediaIds => 0x000406FF, + Self::AlternateTitleIds => 0x000407FF, + Self::AdditionalTitleMemory => 0x00040801, + Self::ExportsByName => 0x00E10402, + Self::Unknown(raw) => *raw, + } + } +} + +impl fmt::Display for HeaderKey { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::ResourceInfo => write!(f, "RESOURCE_INFO"), + Self::FileFormatInfo => write!(f, "FILE_FORMAT_INFO"), + Self::DeltaPatchDescriptor => write!(f, "DELTA_PATCH_DESCRIPTOR"), + Self::BaseReference => write!(f, "BASE_REFERENCE"), + Self::DiscProfileId => write!(f, "DISC_PROFILE_ID"), + Self::BoundingPath => write!(f, "BOUNDING_PATH"), + Self::DeviceId => write!(f, "DEVICE_ID"), + Self::OriginalBaseAddress => write!(f, "ORIGINAL_BASE_ADDRESS"), + Self::EntryPoint => write!(f, "ENTRY_POINT"), + Self::ImageBaseAddress => write!(f, "IMAGE_BASE_ADDRESS"), + Self::ImportLibraries => write!(f, "IMPORT_LIBRARIES"), + Self::ChecksumTimestamp => write!(f, "CHECKSUM_TIMESTAMP"), + Self::EnabledForCallcap => write!(f, "ENABLED_FOR_CALLCAP"), + Self::EnabledForFastcap => write!(f, "ENABLED_FOR_FASTCAP"), + Self::OriginalPeName => write!(f, "ORIGINAL_PE_NAME"), + Self::StaticLibraries => write!(f, "STATIC_LIBRARIES"), + Self::TlsInfo => write!(f, "TLS_INFO"), + Self::DefaultStackSize => write!(f, "DEFAULT_STACK_SIZE"), + Self::DefaultFilesystemCacheSize => write!(f, "DEFAULT_FILESYSTEM_CACHE_SIZE"), + Self::DefaultHeapSize => write!(f, "DEFAULT_HEAP_SIZE"), + Self::PageHeapSizeAndFlags => write!(f, "PAGE_HEAP_SIZE_AND_FLAGS"), + Self::SystemFlags => write!(f, "SYSTEM_FLAGS"), + Self::SystemFlags32 => write!(f, "SYSTEM_FLAGS_32"), + Self::SystemFlags64 => write!(f, "SYSTEM_FLAGS_64"), + Self::ExecutionInfo => write!(f, "EXECUTION_INFO"), + Self::TitleWorkspaceSize => write!(f, "TITLE_WORKSPACE_SIZE"), + Self::GameRatings => write!(f, "GAME_RATINGS"), + Self::LanKey => write!(f, "LAN_KEY"), + Self::Xbox360Logo => write!(f, "XBOX360_LOGO"), + Self::MultidiscMediaIds => write!(f, "MULTIDISC_MEDIA_IDS"), + Self::AlternateTitleIds => write!(f, "ALTERNATE_TITLE_IDS"), + Self::AdditionalTitleMemory => write!(f, "ADDITIONAL_TITLE_MEMORY"), + Self::ExportsByName => write!(f, "EXPORTS_BY_NAME"), + Self::Unknown(raw) => write!(f, "UNKNOWN(0x{raw:08X})"), + } + } +} + +// ── Raw Optional Header Entry ───────────────────────────────────────────────── + +/// A single raw optional header entry (8 bytes). +#[derive(Debug, Clone)] +pub struct OptionalHeaderEntry { + pub key: HeaderKey, + /// Raw key value for low-byte interpretation. + pub raw_key: u32, + /// The value or offset field. + pub value: u32, +} + +impl OptionalHeaderEntry { + /// Returns `true` if this entry's value is inline data (not an offset). + pub fn is_inline(&self) -> bool { + let low = self.raw_key & 0xFF; + low == 0x00 || low == 0x01 + } +} + +// ── Parsed Data Structures ──────────────────────────────────────────────────── + +/// A packed version field: major(4b), minor(4b), build(16b), qfe(8b). +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct Version { + pub major: u8, + pub minor: u8, + pub build: u16, + pub qfe: u8, +} + +impl Version { + /// Parses a `Version` from a packed 32-bit big-endian value. + pub fn from_u32(raw: u32) -> Self { + Self { + major: ((raw >> 28) & 0xF) as u8, + minor: ((raw >> 24) & 0xF) as u8, + build: ((raw >> 8) & 0xFFFF) as u16, + qfe: (raw & 0xFF) as u8, + } + } +} + +impl fmt::Display for Version { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}.{}.{}.{}", self.major, self.minor, self.build, self.qfe) + } +} + +/// Execution info (24 bytes) — title ID, media ID, disc info, etc. +#[derive(Debug, Clone)] +pub struct ExecutionInfo { + pub media_id: u32, + pub version: Version, + pub base_version: Version, + pub title_id: u32, + pub platform: u8, + pub executable_type: u8, + pub disc_number: u8, + pub disc_count: u8, + pub savegame_id: u32, +} + +/// Encryption type for the PE image payload. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum EncryptionType { + None, + Normal, + Unknown(u16), +} + +impl fmt::Display for EncryptionType { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::None => write!(f, "None"), + Self::Normal => write!(f, "Normal (AES-128-CBC)"), + Self::Unknown(v) => write!(f, "Unknown ({v})"), + } + } +} + +/// Compression type for the PE image payload. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum CompressionType { + None, + Basic, + Normal, + Delta, + Unknown(u16), +} + +impl fmt::Display for CompressionType { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::None => write!(f, "None"), + Self::Basic => write!(f, "Basic (zero-fill)"), + Self::Normal => write!(f, "Normal (LZX)"), + Self::Delta => write!(f, "Delta"), + Self::Unknown(v) => write!(f, "Unknown ({v})"), + } + } +} + +/// File format info — encryption and compression settings. +#[derive(Debug, Clone)] +pub struct FileFormatInfo { + pub encryption_type: EncryptionType, + pub compression_type: CompressionType, +} + +/// Checksum and build timestamp. +#[derive(Debug, Clone)] +pub struct ChecksumTimestamp { + pub checksum: u32, + pub timestamp: u32, +} + +/// Thread-local storage info (16 bytes). +#[derive(Debug, Clone)] +pub struct TlsInfo { + pub slot_count: u32, + pub raw_data_address: u32, + pub data_size: u32, + pub raw_data_size: u32, +} + +/// Library approval type for static libraries. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum ApprovalType { + Unapproved, + Possible, + Approved, + Expired, + Unknown(u8), +} + +impl fmt::Display for ApprovalType { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::Unapproved => write!(f, "Unapproved"), + Self::Possible => write!(f, "Possible"), + Self::Approved => write!(f, "Approved"), + Self::Expired => write!(f, "Expired"), + Self::Unknown(v) => write!(f, "Unknown({v})"), + } + } +} + +/// A single static library entry (16 bytes). +#[derive(Debug, Clone)] +pub struct StaticLibrary { + pub name: String, + pub version_major: u16, + pub version_minor: u16, + pub version_build: u16, + pub approval_type: ApprovalType, + pub version_qfe: u8, +} + +impl fmt::Display for StaticLibrary { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!( + f, + "{:<8} {}.{}.{}.{} ({})", + self.name, + self.version_major, + self.version_minor, + self.version_build, + self.version_qfe, + self.approval_type + ) + } +} + +/// A single import library with its imported records. +#[derive(Debug, Clone)] +pub struct ImportLibrary { + pub name: String, + pub id: u32, + pub version: Version, + pub version_min: Version, + pub record_count: u16, + pub import_addresses: Vec, +} + +/// Parsed import libraries container. +#[derive(Debug, Clone)] +pub struct ImportLibrariesInfo { + pub string_table: Vec, + pub libraries: Vec, +} + +/// A single embedded resource descriptor (16 bytes). +#[derive(Debug, Clone)] +pub struct ResourceEntry { + pub name: String, + pub address: u32, + pub size: u32, +} + +/// Rating board identifiers for game ratings display. +#[derive(Debug, Clone, Copy)] +pub struct GameRatings { + pub esrb: u8, + pub pegi: u8, + pub pegi_fi: u8, + pub pegi_pt: u8, + pub bbfc: u8, + pub cero: u8, + pub usk: u8, + pub oflc_au: u8, + pub oflc_nz: u8, + pub kmrb: u8, + pub brazil: u8, + pub fpb: u8, +} + +/// Callcap import thunk addresses. +#[derive(Debug, Clone)] +pub struct CallcapImports { + pub start_func_thunk_addr: u32, + pub end_func_thunk_addr: u32, +} + +/// Data directory (for exports-by-name). +#[derive(Debug, Clone)] +pub struct DataDirectory { + pub offset: u32, + pub size: u32, +} + +/// System privilege flags bitmask. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct SystemFlags(pub u32); + +impl SystemFlags { + const FLAGS: &[(u32, &str)] = &[ + (0x00000001, "NO_FORCED_REBOOT"), + (0x00000002, "FOREGROUND_TASKS"), + (0x00000004, "NO_ODD_MAPPING"), + (0x00000008, "HANDLE_MCE_INPUT"), + (0x00000010, "RESTRICTED_HUD_FEATURES"), + (0x00000020, "HANDLE_GAMEPAD_DISCONNECT"), + (0x00000040, "INSECURE_SOCKETS"), + (0x00000080, "XBOX1_INTEROPERABILITY"), + (0x00000100, "DASH_CONTEXT"), + (0x00000200, "USES_GAME_VOICE_CHANNEL"), + (0x00000400, "PAL50_INCOMPATIBLE"), + (0x00000800, "INSECURE_UTILITY_DRIVE"), + (0x00001000, "XAM_HOOKS"), + (0x00002000, "ACCESS_PII"), + (0x00004000, "CROSS_PLATFORM_SYSTEM_LINK"), + (0x00008000, "MULTIDISC_SWAP"), + (0x00010000, "MULTIDISC_INSECURE_MEDIA"), + (0x00020000, "AP25_MEDIA"), + (0x00040000, "NO_CONFIRM_EXIT"), + (0x00080000, "ALLOW_BACKGROUND_DOWNLOAD"), + (0x00100000, "CREATE_PERSISTABLE_RAMDRIVE"), + (0x00200000, "INHERIT_PERSISTENT_RAMDRIVE"), + (0x00400000, "ALLOW_HUD_VIBRATION"), + (0x00800000, "ACCESS_UTILITY_PARTITIONS"), + (0x01000000, "IPTV_INPUT_SUPPORTED"), + (0x02000000, "PREFER_BIG_BUTTON_INPUT"), + (0x04000000, "ALLOW_EXTENDED_SYSTEM_RESERVATION"), + (0x08000000, "MULTIDISC_CROSS_TITLE"), + (0x10000000, "INSTALL_INCOMPATIBLE"), + (0x20000000, "ALLOW_AVATAR_GET_METADATA_BY_XUID"), + (0x40000000, "ALLOW_CONTROLLER_SWAPPING"), + (0x80000000, "DASH_EXTENSIBILITY_MODULE"), + ]; + + /// Returns a list of human-readable flag names that are set. + pub fn flag_names(self) -> Vec<&'static str> { + Self::FLAGS + .iter() + .filter(|(bit, _)| self.0 & bit != 0) + .map(|(_, name)| *name) + .collect() + } +} + +impl fmt::Display for SystemFlags { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let names = self.flag_names(); + if names.is_empty() { + write!(f, "0x{:08X}", self.0) + } else { + write!(f, "0x{:08X} [{}]", self.0, names.join(", ")) + } + } +} + +// ── Aggregate Parsed Headers ────────────────────────────────────────────────── + +/// All parsed optional headers from the XEX2 file. +/// +/// Each field is `Option` because any given header may or may not be present. +#[derive(Debug, Clone)] +pub struct OptionalHeaders { + /// The raw header entries (key + value/offset) in file order. + pub entries: Vec, + + // Inline u32 values + pub entry_point: Option, + pub original_base_address: Option, + pub image_base_address: Option, + pub default_stack_size: Option, + pub default_filesystem_cache_size: Option, + pub default_heap_size: Option, + pub enabled_for_fastcap: Option, + pub title_workspace_size: Option, + pub additional_title_memory: Option, + + // Bitmask types + pub system_flags: Option, + + // Fixed-size structures + pub execution_info: Option, + pub file_format_info: Option, + pub checksum_timestamp: Option, + pub tls_info: Option, + pub game_ratings: Option, + pub lan_key: Option<[u8; 16]>, + pub enabled_for_callcap: Option, + pub exports_by_name: Option, + + // Variable-size structures + pub original_pe_name: Option, + pub bounding_path: Option, + pub static_libraries: Option>, + pub import_libraries: Option, + pub resource_info: Option>, + + // Data blobs (stored as size only for display) + pub xbox360_logo_size: Option, +} + +// ── Parsing ─────────────────────────────────────────────────────────────────── + +/// Parses all optional header entries and their associated data structures. +pub fn parse_optional_headers(data: &[u8], header: &Xex2Header) -> Result { + let count = header.header_count as usize; + let entries_start = 0x18; + let entries_end = entries_start + count * 8; + + if entries_end > data.len() { + return Err(Xex2Error::FileTooSmall { + expected: entries_end, + actual: data.len(), + }); + } + + // Parse raw entries + let mut entries = Vec::with_capacity(count); + for i in 0..count { + let offset = entries_start + i * 8; + let raw_key = read_u32_be(data, offset)?; + let value = read_u32_be(data, offset + 4)?; + entries.push(OptionalHeaderEntry { + key: HeaderKey::from_raw(raw_key), + raw_key, + value, + }); + } + + let mut headers = OptionalHeaders { + entries: entries.clone(), + entry_point: None, + original_base_address: None, + image_base_address: None, + default_stack_size: None, + default_filesystem_cache_size: None, + default_heap_size: None, + enabled_for_fastcap: None, + title_workspace_size: None, + additional_title_memory: None, + system_flags: None, + execution_info: None, + file_format_info: None, + checksum_timestamp: None, + tls_info: None, + game_ratings: None, + lan_key: None, + enabled_for_callcap: None, + exports_by_name: None, + original_pe_name: None, + bounding_path: None, + static_libraries: None, + import_libraries: None, + resource_info: None, + xbox360_logo_size: None, + }; + + // Parse each entry's data + for entry in &entries { + match entry.key { + // Inline u32 values + HeaderKey::EntryPoint => { + headers.entry_point = Some(entry.value); + } + HeaderKey::OriginalBaseAddress => { + headers.original_base_address = Some(entry.value); + } + HeaderKey::ImageBaseAddress => { + headers.image_base_address = Some(entry.value); + } + HeaderKey::DefaultStackSize => { + headers.default_stack_size = Some(entry.value); + } + HeaderKey::DefaultFilesystemCacheSize => { + headers.default_filesystem_cache_size = Some(entry.value); + } + HeaderKey::DefaultHeapSize => { + headers.default_heap_size = Some(entry.value); + } + HeaderKey::EnabledForFastcap => { + headers.enabled_for_fastcap = Some(entry.value); + } + HeaderKey::TitleWorkspaceSize => { + headers.title_workspace_size = Some(entry.value); + } + HeaderKey::AdditionalTitleMemory => { + headers.additional_title_memory = Some(entry.value); + } + HeaderKey::SystemFlags => { + headers.system_flags = Some(SystemFlags(entry.value)); + } + + // Fixed-size data at offset + HeaderKey::ExecutionInfo => { + headers.execution_info = Some(parse_execution_info(data, entry.value as usize)?); + } + HeaderKey::FileFormatInfo => { + headers.file_format_info = + Some(parse_file_format_info(data, entry.value as usize)?); + } + HeaderKey::ChecksumTimestamp => { + headers.checksum_timestamp = + Some(parse_checksum_timestamp(data, entry.value as usize)?); + } + HeaderKey::TlsInfo => { + headers.tls_info = Some(parse_tls_info(data, entry.value as usize)?); + } + HeaderKey::GameRatings => { + headers.game_ratings = Some(parse_game_ratings(data, entry.value as usize)?); + } + HeaderKey::LanKey => { + headers.lan_key = Some(parse_lan_key(data, entry.value as usize)?); + } + HeaderKey::EnabledForCallcap => { + headers.enabled_for_callcap = + Some(parse_callcap_imports(data, entry.value as usize)?); + } + HeaderKey::ExportsByName => { + headers.exports_by_name = + Some(parse_data_directory(data, entry.value as usize)?); + } + + // Variable-size data at offset + HeaderKey::OriginalPeName => { + headers.original_pe_name = + Some(parse_sized_string(data, entry.value as usize)?); + } + HeaderKey::BoundingPath => { + headers.bounding_path = Some(parse_sized_string(data, entry.value as usize)?); + } + HeaderKey::StaticLibraries => { + headers.static_libraries = + Some(parse_static_libraries(data, entry.value as usize)?); + } + HeaderKey::ImportLibraries => { + headers.import_libraries = + Some(parse_import_libraries(data, entry.value as usize)?); + } + HeaderKey::ResourceInfo => { + headers.resource_info = Some(parse_resource_info(data, entry.value as usize)?); + } + HeaderKey::Xbox360Logo => { + // Just read the size field, don't parse the bitmap data + let size = read_u32_be(data, entry.value as usize)?; + headers.xbox360_logo_size = Some(size); + } + + // Other known/unknown keys — store raw entry but don't parse further + _ => {} + } + } + + Ok(headers) +} + +// ── Individual parsers ──────────────────────────────────────────────────────── + +fn parse_execution_info(data: &[u8], offset: usize) -> Result { + Ok(ExecutionInfo { + media_id: read_u32_be(data, offset)?, + version: Version::from_u32(read_u32_be(data, offset + 0x04)?), + base_version: Version::from_u32(read_u32_be(data, offset + 0x08)?), + title_id: read_u32_be(data, offset + 0x0C)?, + platform: read_u8(data, offset + 0x10)?, + executable_type: read_u8(data, offset + 0x11)?, + disc_number: read_u8(data, offset + 0x12)?, + disc_count: read_u8(data, offset + 0x13)?, + savegame_id: read_u32_be(data, offset + 0x14)?, + }) +} + +fn parse_file_format_info(data: &[u8], offset: usize) -> Result { + // Skip the 4-byte info_size field + let encryption_raw = read_u16_be(data, offset + 0x04)?; + let compression_raw = read_u16_be(data, offset + 0x06)?; + + let encryption_type = match encryption_raw { + 0 => EncryptionType::None, + 1 => EncryptionType::Normal, + v => EncryptionType::Unknown(v), + }; + + let compression_type = match compression_raw { + 0 => CompressionType::None, + 1 => CompressionType::Basic, + 2 => CompressionType::Normal, + 3 => CompressionType::Delta, + v => CompressionType::Unknown(v), + }; + + Ok(FileFormatInfo { + encryption_type, + compression_type, + }) +} + +fn parse_checksum_timestamp(data: &[u8], offset: usize) -> Result { + Ok(ChecksumTimestamp { + checksum: read_u32_be(data, offset)?, + timestamp: read_u32_be(data, offset + 0x04)?, + }) +} + +fn parse_tls_info(data: &[u8], offset: usize) -> Result { + Ok(TlsInfo { + slot_count: read_u32_be(data, offset)?, + raw_data_address: read_u32_be(data, offset + 0x04)?, + data_size: read_u32_be(data, offset + 0x08)?, + raw_data_size: read_u32_be(data, offset + 0x0C)?, + }) +} + +fn parse_game_ratings(data: &[u8], offset: usize) -> Result { + Ok(GameRatings { + esrb: read_u8(data, offset)?, + pegi: read_u8(data, offset + 1)?, + pegi_fi: read_u8(data, offset + 2)?, + pegi_pt: read_u8(data, offset + 3)?, + bbfc: read_u8(data, offset + 4)?, + cero: read_u8(data, offset + 5)?, + usk: read_u8(data, offset + 6)?, + oflc_au: read_u8(data, offset + 7)?, + oflc_nz: read_u8(data, offset + 8)?, + kmrb: read_u8(data, offset + 9)?, + brazil: read_u8(data, offset + 10)?, + fpb: read_u8(data, offset + 11)?, + }) +} + +fn parse_lan_key(data: &[u8], offset: usize) -> Result<[u8; 16]> { + let bytes = crate::util::read_bytes(data, offset, 16)?; + let mut key = [0u8; 16]; + key.copy_from_slice(bytes); + Ok(key) +} + +fn parse_callcap_imports(data: &[u8], offset: usize) -> Result { + Ok(CallcapImports { + start_func_thunk_addr: read_u32_be(data, offset)?, + end_func_thunk_addr: read_u32_be(data, offset + 0x04)?, + }) +} + +fn parse_data_directory(data: &[u8], offset: usize) -> Result { + Ok(DataDirectory { + offset: read_u32_be(data, offset)?, + size: read_u32_be(data, offset + 0x04)?, + }) +} + +/// Parses a length-prefixed string: 4-byte size, then null-terminated ASCII. +fn parse_sized_string(data: &[u8], offset: usize) -> Result { + let size = read_u32_be(data, offset)? as usize; + if size <= 4 { + return Ok(String::new()); + } + let str_bytes = crate::util::read_bytes(data, offset + 4, size - 4)?; + // Trim trailing null bytes + let end = str_bytes.iter().position(|&b| b == 0).unwrap_or(str_bytes.len()); + let s = std::str::from_utf8(&str_bytes[..end])?; + Ok(s.to_string()) +} + +fn parse_static_libraries(data: &[u8], offset: usize) -> Result> { + let size = read_u32_be(data, offset)? as usize; + if size <= 4 { + return Ok(Vec::new()); + } + let lib_count = (size - 4) / 16; + let mut libraries = Vec::with_capacity(lib_count); + + for i in 0..lib_count { + let base = offset + 4 + i * 16; + let name_bytes = crate::util::read_bytes(data, base, 8)?; + let name_end = name_bytes.iter().position(|&b| b == 0).unwrap_or(8); + let name = std::str::from_utf8(&name_bytes[..name_end])?.to_string(); + + let version_major = read_u16_be(data, base + 0x08)?; + let version_minor = read_u16_be(data, base + 0x0A)?; + let version_build = read_u16_be(data, base + 0x0C)?; + let approval_raw = read_u8(data, base + 0x0E)?; + let version_qfe = read_u8(data, base + 0x0F)?; + + let approval_type = match approval_raw { + 0 => ApprovalType::Unapproved, + 1 => ApprovalType::Possible, + 2 => ApprovalType::Approved, + 3 => ApprovalType::Expired, + v => ApprovalType::Unknown(v), + }; + + libraries.push(StaticLibrary { + name, + version_major, + version_minor, + version_build, + approval_type, + version_qfe, + }); + } + + Ok(libraries) +} + +fn parse_import_libraries(data: &[u8], offset: usize) -> Result { + let _total_size = read_u32_be(data, offset)?; + let string_table_size = read_u32_be(data, offset + 0x04)? as usize; + let string_table_count = read_u32_be(data, offset + 0x08)? as usize; + + // Parse string table: null-terminated strings, 4-byte aligned + let str_data_start = offset + 0x0C; + let str_data = crate::util::read_bytes(data, str_data_start, string_table_size)?; + let mut string_table = Vec::with_capacity(string_table_count); + let mut pos = 0; + for _ in 0..string_table_count { + let end = str_data[pos..] + .iter() + .position(|&b| b == 0) + .unwrap_or(str_data.len() - pos); + let s = std::str::from_utf8(&str_data[pos..pos + end])?.to_string(); + string_table.push(s); + // Advance past null terminator, then align to 4 bytes + pos += end + 1; + pos = (pos + 3) & !3; + } + + // Parse library entries following the string table + let mut lib_offset = str_data_start + string_table_size; + let mut libraries = Vec::new(); + + // Keep reading libraries until we've consumed the total import data area + let end_offset = offset + _total_size as usize; + while lib_offset + 0x28 <= end_offset { + let lib_size = read_u32_be(data, lib_offset)? as usize; + if lib_size < 0x28 { + break; + } + + // Skip next_import_digest (20 bytes at +0x04) + let id = read_u32_be(data, lib_offset + 0x18)?; + let version = Version::from_u32(read_u32_be(data, lib_offset + 0x1C)?); + let version_min = Version::from_u32(read_u32_be(data, lib_offset + 0x20)?); + let name_index = (read_u16_be(data, lib_offset + 0x24)? & 0xFF) as usize; + let record_count = read_u16_be(data, lib_offset + 0x26)?; + + let name = if name_index < string_table.len() { + string_table[name_index].clone() + } else { + format!("") + }; + + // Read import addresses + let addr_start = lib_offset + 0x28; + let mut import_addresses = Vec::with_capacity(record_count as usize); + for j in 0..record_count as usize { + let addr = read_u32_be(data, addr_start + j * 4)?; + import_addresses.push(addr); + } + + libraries.push(ImportLibrary { + name, + id, + version, + version_min, + record_count, + import_addresses, + }); + + lib_offset += lib_size; + } + + Ok(ImportLibrariesInfo { + string_table, + libraries, + }) +} + +fn parse_resource_info(data: &[u8], offset: usize) -> Result> { + let size = read_u32_be(data, offset)? as usize; + if size <= 4 { + return Ok(Vec::new()); + } + let count = (size - 4) / 16; + let mut resources = Vec::with_capacity(count); + + for i in 0..count { + let base = offset + 4 + i * 16; + let name_bytes = crate::util::read_bytes(data, base, 8)?; + let name_end = name_bytes.iter().position(|&b| b == 0).unwrap_or(8); + let name = std::str::from_utf8(&name_bytes[..name_end])?.to_string(); + let address = read_u32_be(data, base + 0x08)?; + let res_size = read_u32_be(data, base + 0x0C)?; + + resources.push(ResourceEntry { + name, + address, + size: res_size, + }); + } + + Ok(resources) +} + +/// Formats a Unix timestamp as a human-readable UTC date string. +/// +/// Simple implementation without external crates — handles dates from 1970 to ~2099. +pub fn format_timestamp(timestamp: u32) -> String { + let ts = timestamp as u64; + let secs_per_day: u64 = 86400; + let mut days = ts / secs_per_day; + let day_secs = ts % secs_per_day; + + let hours = day_secs / 3600; + let minutes = (day_secs % 3600) / 60; + let seconds = day_secs % 60; + + // Calculate year/month/day from days since epoch (1970-01-01) + let mut year = 1970u64; + loop { + let days_in_year = if is_leap_year(year) { 366 } else { 365 }; + if days < days_in_year { + break; + } + days -= days_in_year; + year += 1; + } + + let leap = is_leap_year(year); + let month_days = [ + 31, + if leap { 29 } else { 28 }, + 31, + 30, + 31, + 30, + 31, + 31, + 30, + 31, + 30, + 31, + ]; + + let mut month = 0; + for (i, &md) in month_days.iter().enumerate() { + if days < md { + month = i + 1; + break; + } + days -= md; + } + let day = days + 1; + + format!("{year:04}-{month:02}-{day:02} {hours:02}:{minutes:02}:{seconds:02} UTC") +} + +fn is_leap_year(year: u64) -> bool { + (year.is_multiple_of(4) && !year.is_multiple_of(100)) || year.is_multiple_of(400) +} + +/// Formats a byte slice as a hex string with spaces between bytes. +pub fn format_hex_bytes(bytes: &[u8]) -> String { + bytes + .iter() + .map(|b| format!("{b:02X}")) + .collect::>() + .join(" ") +} + +/// Formats a rating byte for display (0xFF = unrated). +pub fn format_rating(value: u8) -> String { + if value == 0xFF { + "Unrated".to_string() + } else { + format!("{value}") + } +} + +#[cfg(test)] +mod tests { + use super::*; + + fn sample_data() -> Vec { + let path = format!("{}/tests/data/default.xex", env!("CARGO_MANIFEST_DIR")); + std::fs::read(&path).expect("sample file should exist") + } + + #[test] + fn test_header_key_from_raw_known() { + assert_eq!(HeaderKey::from_raw(0x00010100), HeaderKey::EntryPoint); + assert_eq!(HeaderKey::from_raw(0x000003FF), HeaderKey::FileFormatInfo); + assert_eq!(HeaderKey::from_raw(0x00040006), HeaderKey::ExecutionInfo); + } + + #[test] + fn test_header_key_from_raw_unknown() { + assert!(matches!(HeaderKey::from_raw(0x12345678), HeaderKey::Unknown(0x12345678))); + } + + #[test] + fn test_inline_detection() { + // key & 0xFF == 0x00 → inline + let entry = OptionalHeaderEntry { + key: HeaderKey::EntryPoint, + raw_key: 0x00010100, + value: 0x824AB748, + }; + assert!(entry.is_inline()); + + // key & 0xFF == 0xFF → offset + let entry = OptionalHeaderEntry { + key: HeaderKey::FileFormatInfo, + raw_key: 0x000003FF, + value: 0x0FD8, + }; + assert!(!entry.is_inline()); + } + + #[test] + fn test_version_from_u32() { + // 0x00000002 → major=0, minor=0, build=0, qfe=2 + let v = Version::from_u32(0x00000002); + assert_eq!(v.major, 0); + assert_eq!(v.minor, 0); + assert_eq!(v.build, 0); + assert_eq!(v.qfe, 2); + + // 0x20110C80 → major=2, minor=0, build=0x110C, qfe=0x80 + let v = Version::from_u32(0x20110C80); + assert_eq!(v.major, 2); + assert_eq!(v.minor, 0); + assert_eq!(v.build, 0x110C); + assert_eq!(v.qfe, 0x80); + } + + #[test] + fn test_version_display() { + let v = Version { + major: 2, + minor: 0, + build: 4364, + qfe: 128, + }; + assert_eq!(v.to_string(), "2.0.4364.128"); + } + + #[test] + fn test_parse_optional_headers_count() { + let data = sample_data(); + let header = crate::header::parse_header(&data).unwrap(); + let opt = parse_optional_headers(&data, &header).unwrap(); + assert_eq!(opt.entries.len(), 15); + } + + #[test] + fn test_parse_entry_point() { + let data = sample_data(); + let header = crate::header::parse_header(&data).unwrap(); + let opt = parse_optional_headers(&data, &header).unwrap(); + assert_eq!(opt.entry_point, Some(0x824AB748)); + } + + #[test] + fn test_parse_image_base_address() { + let data = sample_data(); + let header = crate::header::parse_header(&data).unwrap(); + let opt = parse_optional_headers(&data, &header).unwrap(); + assert_eq!(opt.image_base_address, Some(0x82000000)); + } + + #[test] + fn test_parse_default_stack_size() { + let data = sample_data(); + let header = crate::header::parse_header(&data).unwrap(); + let opt = parse_optional_headers(&data, &header).unwrap(); + assert_eq!(opt.default_stack_size, Some(0x00080000)); + } + + #[test] + fn test_parse_system_flags() { + let data = sample_data(); + let header = crate::header::parse_header(&data).unwrap(); + let opt = parse_optional_headers(&data, &header).unwrap(); + assert_eq!(opt.system_flags, Some(SystemFlags(0x00000400))); + let flags = opt.system_flags.unwrap(); + assert_eq!(flags.flag_names(), vec!["PAL50_INCOMPATIBLE"]); + } + + #[test] + fn test_parse_execution_info() { + let data = sample_data(); + let header = crate::header::parse_header(&data).unwrap(); + let opt = parse_optional_headers(&data, &header).unwrap(); + let exec = opt.execution_info.as_ref().unwrap(); + assert_eq!(exec.title_id, 0x535107D4); + assert_eq!(exec.media_id, 0x2D2E2EEB); + assert_eq!(exec.disc_number, 1); + assert_eq!(exec.disc_count, 1); + } + + #[test] + fn test_parse_file_format_info() { + let data = sample_data(); + let header = crate::header::parse_header(&data).unwrap(); + let opt = parse_optional_headers(&data, &header).unwrap(); + let fmt = opt.file_format_info.as_ref().unwrap(); + assert_eq!(fmt.encryption_type, EncryptionType::Normal); + assert_eq!(fmt.compression_type, CompressionType::Normal); + } + + #[test] + fn test_parse_original_pe_name() { + let data = sample_data(); + let header = crate::header::parse_header(&data).unwrap(); + let opt = parse_optional_headers(&data, &header).unwrap(); + assert_eq!(opt.original_pe_name.as_deref(), Some("default.pe")); + } + + #[test] + fn test_parse_checksum_timestamp() { + let data = sample_data(); + let header = crate::header::parse_header(&data).unwrap(); + let opt = parse_optional_headers(&data, &header).unwrap(); + let ct = opt.checksum_timestamp.as_ref().unwrap(); + assert_eq!(ct.checksum, 0x00902EF1); + assert_eq!(ct.timestamp, 0x463FA3D7); + } + + #[test] + fn test_parse_tls_info() { + let data = sample_data(); + let header = crate::header::parse_header(&data).unwrap(); + let opt = parse_optional_headers(&data, &header).unwrap(); + let tls = opt.tls_info.as_ref().unwrap(); + assert_eq!(tls.slot_count, 0x40); + assert_eq!(tls.raw_data_address, 0x00000000); + assert_eq!(tls.data_size, 0x00000000); + assert_eq!(tls.raw_data_size, 0x00000000); + } + + #[test] + fn test_parse_static_libraries() { + let data = sample_data(); + let header = crate::header::parse_header(&data).unwrap(); + let opt = parse_optional_headers(&data, &header).unwrap(); + let libs = opt.static_libraries.as_ref().unwrap(); + assert_eq!(libs.len(), 12); + assert_eq!(libs[0].name, "XAPILIB"); + assert_eq!(libs[0].version_major, 2); + assert_eq!(libs[0].version_minor, 0); + assert_eq!(libs[3].name, "XBOXKRNL"); + } + + #[test] + fn test_parse_import_libraries() { + let data = sample_data(); + let header = crate::header::parse_header(&data).unwrap(); + let opt = parse_optional_headers(&data, &header).unwrap(); + let imports = opt.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()); + } + + #[test] + fn test_parse_resource_info() { + let data = sample_data(); + let header = crate::header::parse_header(&data).unwrap(); + let opt = parse_optional_headers(&data, &header).unwrap(); + let resources = opt.resource_info.as_ref().unwrap(); + assert_eq!(resources.len(), 1); + assert_eq!(resources[0].name, "535107D4"); + } + + #[test] + fn test_parse_lan_key() { + let data = sample_data(); + let header = crate::header::parse_header(&data).unwrap(); + let opt = parse_optional_headers(&data, &header).unwrap(); + assert!(opt.lan_key.is_some()); + } + + #[test] + fn test_parse_game_ratings() { + let data = sample_data(); + let header = crate::header::parse_header(&data).unwrap(); + let opt = parse_optional_headers(&data, &header).unwrap(); + let ratings = opt.game_ratings.as_ref().unwrap(); + // First byte from sample is 0x06 (ESRB) + assert_eq!(ratings.esrb, 0x06); + } + + #[test] + fn test_parse_xbox360_logo() { + let data = sample_data(); + let header = crate::header::parse_header(&data).unwrap(); + let opt = parse_optional_headers(&data, &header).unwrap(); + assert!(opt.xbox360_logo_size.is_some()); + } + + #[test] + fn test_format_timestamp() { + // 0x463FA3D7 = 1178575831 → 2007-05-07 22:10:31 UTC + let s = format_timestamp(0x463FA3D7); + assert_eq!(s, "2007-05-07 22:10:31 UTC"); + } + + #[test] + fn test_system_flags_display() { + let f = SystemFlags(0x00000400); + assert_eq!(f.to_string(), "0x00000400 [PAL50_INCOMPATIBLE]"); + } + + #[test] + fn test_system_flags_display_empty() { + let f = SystemFlags(0); + assert_eq!(f.to_string(), "0x00000000"); + } + + #[test] + fn test_format_rating_unrated() { + assert_eq!(format_rating(0xFF), "Unrated"); + } + + #[test] + fn test_format_rating_value() { + assert_eq!(format_rating(6), "6"); + } +} diff --git a/tests/integration.rs b/tests/integration.rs index 34d3cf3..1d90a1c 100644 --- a/tests/integration.rs +++ b/tests/integration.rs @@ -1,10 +1,13 @@ use xex2tractor::header::{ModuleFlags, XEX2_MAGIC}; +use xex2tractor::optional::{CompressionType, EncryptionType, SystemFlags}; fn sample_data() -> Vec { 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()); }