Files
xex2tractor/src/optional.rs
MechaCat02 a9436a3a7a feat: parse and display all optional headers (M2)
Implement parsing for all 15 optional header types found in XEX2 files:
inline values (entry point, base address, stack size, system flags),
fixed-size structures (execution info, file format, TLS, game ratings,
LAN key, checksum/timestamp), and variable-size structures (static
libraries, import libraries, resource info, original PE name, Xbox 360
logo). Add comprehensive unit and integration tests.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-28 18:59:41 +01:00

1243 lines
42 KiB
Rust

/// 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<u32>,
}
/// Parsed import libraries container.
#[derive(Debug, Clone)]
pub struct ImportLibrariesInfo {
pub string_table: Vec<String>,
pub libraries: Vec<ImportLibrary>,
}
/// 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<T>` 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<OptionalHeaderEntry>,
// Inline u32 values
pub entry_point: Option<u32>,
pub original_base_address: Option<u32>,
pub image_base_address: Option<u32>,
pub default_stack_size: Option<u32>,
pub default_filesystem_cache_size: Option<u32>,
pub default_heap_size: Option<u32>,
pub enabled_for_fastcap: Option<u32>,
pub title_workspace_size: Option<u32>,
pub additional_title_memory: Option<u32>,
// Bitmask types
pub system_flags: Option<SystemFlags>,
// Fixed-size structures
pub execution_info: Option<ExecutionInfo>,
pub file_format_info: Option<FileFormatInfo>,
pub checksum_timestamp: Option<ChecksumTimestamp>,
pub tls_info: Option<TlsInfo>,
pub game_ratings: Option<GameRatings>,
pub lan_key: Option<[u8; 16]>,
pub enabled_for_callcap: Option<CallcapImports>,
pub exports_by_name: Option<DataDirectory>,
// Variable-size structures
pub original_pe_name: Option<String>,
pub bounding_path: Option<String>,
pub static_libraries: Option<Vec<StaticLibrary>>,
pub import_libraries: Option<ImportLibrariesInfo>,
pub resource_info: Option<Vec<ResourceEntry>>,
// Data blobs (stored as size only for display)
pub xbox360_logo_size: Option<u32>,
}
// ── Parsing ───────────────────────────────────────────────────────────────────
/// Parses all optional header entries and their associated data structures.
pub fn parse_optional_headers(data: &[u8], header: &Xex2Header) -> Result<OptionalHeaders> {
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<ExecutionInfo> {
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<FileFormatInfo> {
// 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<ChecksumTimestamp> {
Ok(ChecksumTimestamp {
checksum: read_u32_be(data, offset)?,
timestamp: read_u32_be(data, offset + 0x04)?,
})
}
fn parse_tls_info(data: &[u8], offset: usize) -> Result<TlsInfo> {
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<GameRatings> {
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<CallcapImports> {
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<DataDirectory> {
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<String> {
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<Vec<StaticLibrary>> {
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<ImportLibrariesInfo> {
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!("<unknown:{name_index}>")
};
// 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<Vec<ResourceEntry>> {
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::<Vec<_>>()
.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<u8> {
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");
}
}