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>
1243 lines
42 KiB
Rust
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");
|
|
}
|
|
}
|