/// 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"); } }