Compare commits
4 Commits
v0.1.0
...
38d1cc1b6d
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
38d1cc1b6d | ||
|
|
66e078363c | ||
|
|
b1f90a55b6 | ||
|
|
a9436a3a7a |
2
Cargo.lock
generated
2
Cargo.lock
generated
@@ -4,4 +4,4 @@ version = 4
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "xex2tractor"
|
name = "xex2tractor"
|
||||||
version = "0.1.0"
|
version = "0.3.0"
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "xex2tractor"
|
name = "xex2tractor"
|
||||||
version = "0.1.0"
|
version = "0.3.0"
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
description = "A tool for extracting and inspecting Xbox 360 XEX2 executable files"
|
description = "A tool for extracting and inspecting Xbox 360 XEX2 executable files"
|
||||||
license = "MIT"
|
license = "MIT"
|
||||||
|
|||||||
40
README.md
40
README.md
@@ -18,6 +18,46 @@ Header Size: 0x00003000 (12288 bytes)
|
|||||||
Reserved: 0x00000000
|
Reserved: 0x00000000
|
||||||
Security Offset: 0x00000090
|
Security Offset: 0x00000090
|
||||||
Header Count: 15
|
Header Count: 15
|
||||||
|
|
||||||
|
=== Optional Headers (15 entries) ===
|
||||||
|
|
||||||
|
[ENTRY_POINT] 0x824AB748
|
||||||
|
[IMAGE_BASE_ADDRESS] 0x82000000
|
||||||
|
[DEFAULT_STACK_SIZE] 0x00080000 (524288 bytes)
|
||||||
|
[SYSTEM_FLAGS] 0x00000400 [PAL50_INCOMPATIBLE]
|
||||||
|
|
||||||
|
[EXECUTION_INFO]
|
||||||
|
Media ID: 0x2D2E2EEB
|
||||||
|
Title ID: 0x535107D4
|
||||||
|
Version: 0.0.0.2
|
||||||
|
...
|
||||||
|
|
||||||
|
[FILE_FORMAT_INFO]
|
||||||
|
Encryption: Normal (AES-128-CBC)
|
||||||
|
Compression: Normal (LZX)
|
||||||
|
|
||||||
|
[STATIC_LIBRARIES] (12 libraries)
|
||||||
|
XAPILIB 2.0.3215.0 (Unknown(64))
|
||||||
|
D3D9 2.0.3215.1 (Unknown(64))
|
||||||
|
...
|
||||||
|
|
||||||
|
[IMPORT_LIBRARIES] (2 libraries)
|
||||||
|
xam.xex v2.0.4552.0 (min v2.0.4552.0) - 104 imports
|
||||||
|
xboxkrnl.exe v2.0.4552.0 (min v2.0.4552.0) - 294 imports
|
||||||
|
|
||||||
|
=== Security Info ===
|
||||||
|
Header Size: 0x00000F34 (3892 bytes)
|
||||||
|
Image Size: 0x00920000 (9568256 bytes)
|
||||||
|
RSA Signature: 2C94EBE6...11A6E8AA (256 bytes)
|
||||||
|
Image Flags: 0x00000008 [XGD2_MEDIA_ONLY]
|
||||||
|
Load Address: 0x82000000
|
||||||
|
Region: 0xFFFFFFFF [ALL REGIONS]
|
||||||
|
Allowed Media Types: 0x00000004 [DVD_CD]
|
||||||
|
...
|
||||||
|
|
||||||
|
Page Descriptors (146 entries, 64KB pages):
|
||||||
|
#0 Unknown(0) 19 pages ( 1245184 bytes) offset +0x00000000 SHA1: B136058FBBAD...
|
||||||
|
...
|
||||||
```
|
```
|
||||||
|
|
||||||
## Building
|
## Building
|
||||||
|
|||||||
275
src/display.rs
275
src/display.rs
@@ -1,5 +1,9 @@
|
|||||||
/// Pretty-print formatting for parsed XEX2 structures.
|
/// Pretty-print formatting for parsed XEX2 structures.
|
||||||
use crate::header::Xex2Header;
|
use crate::header::Xex2Header;
|
||||||
|
use crate::optional::{
|
||||||
|
format_hex_bytes, format_rating, format_timestamp, HeaderKey, OptionalHeaders,
|
||||||
|
};
|
||||||
|
use crate::security::SecurityInfo;
|
||||||
|
|
||||||
/// Prints the XEX2 main header in a human-readable format.
|
/// Prints the XEX2 main header in a human-readable format.
|
||||||
pub fn display_header(header: &Xex2Header) {
|
pub fn display_header(header: &Xex2Header) {
|
||||||
@@ -14,3 +18,274 @@ pub fn display_header(header: &Xex2Header) {
|
|||||||
println!("Security Offset: 0x{:08X}", header.security_offset);
|
println!("Security Offset: 0x{:08X}", header.security_offset);
|
||||||
println!("Header Count: {}", header.header_count);
|
println!("Header Count: {}", header.header_count);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Prints all parsed optional headers in a human-readable format.
|
||||||
|
pub fn display_optional_headers(headers: &OptionalHeaders) {
|
||||||
|
println!();
|
||||||
|
println!("=== Optional Headers ({} entries) ===", headers.entries.len());
|
||||||
|
|
||||||
|
// Display inline u32 values first
|
||||||
|
if let Some(v) = headers.entry_point {
|
||||||
|
println!();
|
||||||
|
println!("[ENTRY_POINT] 0x{v:08X}");
|
||||||
|
}
|
||||||
|
if let Some(v) = headers.original_base_address {
|
||||||
|
println!("[ORIGINAL_BASE_ADDRESS] 0x{v:08X}");
|
||||||
|
}
|
||||||
|
if let Some(v) = headers.image_base_address {
|
||||||
|
println!("[IMAGE_BASE_ADDRESS] 0x{v:08X}");
|
||||||
|
}
|
||||||
|
if let Some(v) = headers.default_stack_size {
|
||||||
|
println!("[DEFAULT_STACK_SIZE] 0x{v:08X} ({v} bytes)");
|
||||||
|
}
|
||||||
|
if let Some(v) = headers.default_filesystem_cache_size {
|
||||||
|
println!("[DEFAULT_FILESYSTEM_CACHE_SIZE] 0x{v:08X} ({v} bytes)");
|
||||||
|
}
|
||||||
|
if let Some(v) = headers.default_heap_size {
|
||||||
|
println!("[DEFAULT_HEAP_SIZE] 0x{v:08X} ({v} bytes)");
|
||||||
|
}
|
||||||
|
if let Some(v) = headers.title_workspace_size {
|
||||||
|
println!("[TITLE_WORKSPACE_SIZE] 0x{v:08X} ({v} bytes)");
|
||||||
|
}
|
||||||
|
if let Some(v) = headers.additional_title_memory {
|
||||||
|
println!("[ADDITIONAL_TITLE_MEMORY] 0x{v:08X} ({v} bytes)");
|
||||||
|
}
|
||||||
|
if let Some(v) = headers.enabled_for_fastcap {
|
||||||
|
println!("[ENABLED_FOR_FASTCAP] 0x{v:08X}");
|
||||||
|
}
|
||||||
|
|
||||||
|
// System flags
|
||||||
|
if let Some(ref flags) = headers.system_flags {
|
||||||
|
println!("[SYSTEM_FLAGS] {flags}");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Execution info
|
||||||
|
if let Some(ref exec) = headers.execution_info {
|
||||||
|
println!();
|
||||||
|
println!("[EXECUTION_INFO]");
|
||||||
|
println!(" Media ID: 0x{:08X}", exec.media_id);
|
||||||
|
println!(" Title ID: 0x{:08X}", exec.title_id);
|
||||||
|
println!(" Version: {}", exec.version);
|
||||||
|
println!(" Base Version: {}", exec.base_version);
|
||||||
|
println!(" Platform: {}", exec.platform);
|
||||||
|
println!(" Executable Type: {}", exec.executable_type);
|
||||||
|
println!(" Disc: {}/{}", exec.disc_number, exec.disc_count);
|
||||||
|
println!(" Savegame ID: 0x{:08X}", exec.savegame_id);
|
||||||
|
}
|
||||||
|
|
||||||
|
// File format info
|
||||||
|
if let Some(ref fmt) = headers.file_format_info {
|
||||||
|
println!();
|
||||||
|
println!("[FILE_FORMAT_INFO]");
|
||||||
|
println!(" Encryption: {}", fmt.encryption_type);
|
||||||
|
println!(" Compression: {}", fmt.compression_type);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Checksum + timestamp
|
||||||
|
if let Some(ref ct) = headers.checksum_timestamp {
|
||||||
|
println!();
|
||||||
|
println!("[CHECKSUM_TIMESTAMP]");
|
||||||
|
println!(" Checksum: 0x{:08X}", ct.checksum);
|
||||||
|
println!(
|
||||||
|
" Timestamp: 0x{:08X} ({})",
|
||||||
|
ct.timestamp,
|
||||||
|
format_timestamp(ct.timestamp)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Original PE name
|
||||||
|
if let Some(ref name) = headers.original_pe_name {
|
||||||
|
println!();
|
||||||
|
println!("[ORIGINAL_PE_NAME] \"{name}\"");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Bounding path
|
||||||
|
if let Some(ref path) = headers.bounding_path {
|
||||||
|
println!("[BOUNDING_PATH] \"{path}\"");
|
||||||
|
}
|
||||||
|
|
||||||
|
// TLS info
|
||||||
|
if let Some(ref tls) = headers.tls_info {
|
||||||
|
println!();
|
||||||
|
println!("[TLS_INFO]");
|
||||||
|
println!(" Slot Count: {}", tls.slot_count);
|
||||||
|
println!(" Raw Data Address: 0x{:08X}", tls.raw_data_address);
|
||||||
|
println!(" Data Size: {} bytes", tls.data_size);
|
||||||
|
println!(" Raw Data Size: {} bytes", tls.raw_data_size);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Static libraries
|
||||||
|
if let Some(ref libs) = headers.static_libraries {
|
||||||
|
println!();
|
||||||
|
println!("[STATIC_LIBRARIES] ({} libraries)", libs.len());
|
||||||
|
for lib in libs {
|
||||||
|
println!(" {lib}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Import libraries
|
||||||
|
if let Some(ref imports) = headers.import_libraries {
|
||||||
|
println!();
|
||||||
|
println!("[IMPORT_LIBRARIES] ({} libraries)", imports.libraries.len());
|
||||||
|
for lib in &imports.libraries {
|
||||||
|
println!(
|
||||||
|
" {} v{} (min v{}) - {} imports",
|
||||||
|
lib.name, lib.version, lib.version_min, lib.record_count
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Resource info
|
||||||
|
if let Some(ref resources) = headers.resource_info {
|
||||||
|
println!();
|
||||||
|
println!("[RESOURCE_INFO] ({} entries)", resources.len());
|
||||||
|
for res in resources {
|
||||||
|
println!(
|
||||||
|
" \"{}\" @ 0x{:08X}, size: {} bytes",
|
||||||
|
res.name, res.address, res.size
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Game ratings
|
||||||
|
if let Some(ref ratings) = headers.game_ratings {
|
||||||
|
println!();
|
||||||
|
println!("[GAME_RATINGS]");
|
||||||
|
println!(" ESRB: {} | PEGI: {} | PEGI-FI: {} | PEGI-PT: {}",
|
||||||
|
format_rating(ratings.esrb), format_rating(ratings.pegi),
|
||||||
|
format_rating(ratings.pegi_fi), format_rating(ratings.pegi_pt));
|
||||||
|
println!(" BBFC: {} | CERO: {} | USK: {} | OFLC-AU: {}",
|
||||||
|
format_rating(ratings.bbfc), format_rating(ratings.cero),
|
||||||
|
format_rating(ratings.usk), format_rating(ratings.oflc_au));
|
||||||
|
println!(" OFLC-NZ: {} | KMRB: {} | Brazil: {} | FPB: {}",
|
||||||
|
format_rating(ratings.oflc_nz), format_rating(ratings.kmrb),
|
||||||
|
format_rating(ratings.brazil), format_rating(ratings.fpb));
|
||||||
|
}
|
||||||
|
|
||||||
|
// LAN key
|
||||||
|
if let Some(ref key) = headers.lan_key {
|
||||||
|
println!();
|
||||||
|
println!("[LAN_KEY] {}", format_hex_bytes(key));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Callcap imports
|
||||||
|
if let Some(ref callcap) = headers.enabled_for_callcap {
|
||||||
|
println!();
|
||||||
|
println!("[ENABLED_FOR_CALLCAP]");
|
||||||
|
println!(" Start Thunk: 0x{:08X}", callcap.start_func_thunk_addr);
|
||||||
|
println!(" End Thunk: 0x{:08X}", callcap.end_func_thunk_addr);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Exports by name
|
||||||
|
if let Some(ref dir) = headers.exports_by_name {
|
||||||
|
println!();
|
||||||
|
println!("[EXPORTS_BY_NAME]");
|
||||||
|
println!(" Offset: 0x{:08X}", dir.offset);
|
||||||
|
println!(" Size: {} bytes", dir.size);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Xbox 360 logo
|
||||||
|
if let Some(size) = headers.xbox360_logo_size {
|
||||||
|
println!();
|
||||||
|
println!("[XBOX360_LOGO] {size} bytes");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Unknown headers
|
||||||
|
for entry in &headers.entries {
|
||||||
|
if let HeaderKey::Unknown(raw) = entry.key {
|
||||||
|
println!();
|
||||||
|
println!(
|
||||||
|
"[UNKNOWN(0x{raw:08X})] value/offset: 0x{:08X}",
|
||||||
|
entry.value
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Prints the security info in a human-readable format.
|
||||||
|
pub fn display_security_info(security: &SecurityInfo) {
|
||||||
|
println!();
|
||||||
|
println!("=== Security Info ===");
|
||||||
|
println!(
|
||||||
|
"Header Size: 0x{:08X} ({} bytes)",
|
||||||
|
security.header_size, security.header_size
|
||||||
|
);
|
||||||
|
println!(
|
||||||
|
"Image Size: 0x{:08X} ({} bytes)",
|
||||||
|
security.image_size, security.image_size
|
||||||
|
);
|
||||||
|
|
||||||
|
// RSA signature — show first 8 and last 8 bytes
|
||||||
|
let sig = &security.rsa_signature;
|
||||||
|
println!(
|
||||||
|
"RSA Signature: {}...{} (256 bytes)",
|
||||||
|
sig[..4].iter().map(|b| format!("{b:02X}")).collect::<String>(),
|
||||||
|
sig[252..].iter().map(|b| format!("{b:02X}")).collect::<String>()
|
||||||
|
);
|
||||||
|
|
||||||
|
println!("Unknown (0x108): 0x{:08X}", security.unk_108);
|
||||||
|
println!("Image Flags: {}", security.image_flags);
|
||||||
|
println!("Load Address: 0x{:08X}", security.load_address);
|
||||||
|
println!(
|
||||||
|
"Section Digest: {}",
|
||||||
|
format_hex_bytes(&security.section_digest)
|
||||||
|
);
|
||||||
|
println!("Import Table Count: {}", security.import_table_count);
|
||||||
|
println!(
|
||||||
|
"Import Table Digest: {}",
|
||||||
|
format_hex_bytes(&security.import_table_digest)
|
||||||
|
);
|
||||||
|
println!(
|
||||||
|
"XGD2 Media ID: {}",
|
||||||
|
security
|
||||||
|
.xgd2_media_id
|
||||||
|
.iter()
|
||||||
|
.map(|b| format!("{b:02X}"))
|
||||||
|
.collect::<String>()
|
||||||
|
);
|
||||||
|
println!(
|
||||||
|
"AES Key (encrypted): {}",
|
||||||
|
format_hex_bytes(&security.aes_key)
|
||||||
|
);
|
||||||
|
|
||||||
|
if security.export_table == 0 {
|
||||||
|
println!("Export Table: 0x00000000 (none)");
|
||||||
|
} else {
|
||||||
|
println!("Export Table: 0x{:08X}", security.export_table);
|
||||||
|
}
|
||||||
|
|
||||||
|
println!(
|
||||||
|
"Header Digest: {}",
|
||||||
|
format_hex_bytes(&security.header_digest)
|
||||||
|
);
|
||||||
|
println!("Region: {}", security.region);
|
||||||
|
println!("Allowed Media Types: {}", security.allowed_media_types);
|
||||||
|
|
||||||
|
// Page descriptors
|
||||||
|
println!();
|
||||||
|
let page_size = security.image_flags.page_size();
|
||||||
|
let page_size_label = if page_size == 0x1000 { "4KB" } else { "64KB" };
|
||||||
|
println!(
|
||||||
|
"Page Descriptors ({} entries, {} pages):",
|
||||||
|
security.page_descriptor_count, page_size_label
|
||||||
|
);
|
||||||
|
|
||||||
|
let mut address_offset: u64 = 0;
|
||||||
|
for (i, desc) in security.page_descriptors.iter().enumerate() {
|
||||||
|
let digest_preview: String = desc.data_digest[..6]
|
||||||
|
.iter()
|
||||||
|
.map(|b| format!("{b:02X}"))
|
||||||
|
.collect();
|
||||||
|
let size = desc.page_count as u64 * page_size as u64;
|
||||||
|
println!(
|
||||||
|
" #{i:<4} {:<10} {:<4} pages ({:>8} bytes) offset +0x{address_offset:08X} SHA1: {digest_preview}...",
|
||||||
|
desc.section_type.to_string(),
|
||||||
|
desc.page_count,
|
||||||
|
size
|
||||||
|
);
|
||||||
|
address_offset += size;
|
||||||
|
}
|
||||||
|
println!(
|
||||||
|
" Total mapped size: 0x{address_offset:X} ({address_offset} bytes)"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|||||||
16
src/lib.rs
16
src/lib.rs
@@ -9,16 +9,24 @@
|
|||||||
pub mod display;
|
pub mod display;
|
||||||
pub mod error;
|
pub mod error;
|
||||||
pub mod header;
|
pub mod header;
|
||||||
|
pub mod optional;
|
||||||
|
pub mod security;
|
||||||
pub mod util;
|
pub mod util;
|
||||||
|
|
||||||
use error::Result;
|
use error::Result;
|
||||||
use header::Xex2Header;
|
use header::Xex2Header;
|
||||||
|
use optional::OptionalHeaders;
|
||||||
|
use security::SecurityInfo;
|
||||||
|
|
||||||
/// A parsed XEX2 file containing all extracted structures.
|
/// A parsed XEX2 file containing all extracted structures.
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub struct Xex2File {
|
pub struct Xex2File {
|
||||||
/// The main XEX2 header (magic, flags, sizes, offsets).
|
/// The main XEX2 header (magic, flags, sizes, offsets).
|
||||||
pub header: Xex2Header,
|
pub header: Xex2Header,
|
||||||
|
/// All parsed optional headers.
|
||||||
|
pub optional_headers: OptionalHeaders,
|
||||||
|
/// Security info (signatures, keys, page descriptors).
|
||||||
|
pub security_info: SecurityInfo,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Parses an XEX2 file from a byte slice.
|
/// Parses an XEX2 file from a byte slice.
|
||||||
@@ -27,6 +35,12 @@ pub struct Xex2File {
|
|||||||
/// Returns a [`Xex2File`] with all successfully parsed structures.
|
/// Returns a [`Xex2File`] with all successfully parsed structures.
|
||||||
pub fn parse(data: &[u8]) -> Result<Xex2File> {
|
pub fn parse(data: &[u8]) -> Result<Xex2File> {
|
||||||
let header = header::parse_header(data)?;
|
let header = header::parse_header(data)?;
|
||||||
|
let optional_headers = optional::parse_optional_headers(data, &header)?;
|
||||||
|
let security_info = security::parse_security_info(data, header.security_offset)?;
|
||||||
|
|
||||||
Ok(Xex2File { header })
|
Ok(Xex2File {
|
||||||
|
header,
|
||||||
|
optional_headers,
|
||||||
|
security_info,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -26,4 +26,6 @@ fn main() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
xex2tractor::display::display_header(&xex.header);
|
xex2tractor::display::display_header(&xex.header);
|
||||||
|
xex2tractor::display::display_optional_headers(&xex.optional_headers);
|
||||||
|
xex2tractor::display::display_security_info(&xex.security_info);
|
||||||
}
|
}
|
||||||
|
|||||||
1242
src/optional.rs
Normal file
1242
src/optional.rs
Normal file
File diff suppressed because it is too large
Load Diff
510
src/security.rs
Normal file
510
src/security.rs
Normal file
@@ -0,0 +1,510 @@
|
|||||||
|
/// XEX2 security info parsing.
|
||||||
|
///
|
||||||
|
/// The security info structure is located at the file offset specified by
|
||||||
|
/// `xex2_header.security_offset` and contains cryptographic signatures,
|
||||||
|
/// encryption keys, memory layout information, and per-page integrity digests.
|
||||||
|
use std::fmt;
|
||||||
|
|
||||||
|
use crate::error::{Result, Xex2Error};
|
||||||
|
use crate::util::{read_bytes, read_u32_be};
|
||||||
|
|
||||||
|
// ── Security Info ─────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/// The parsed XEX2 security info structure.
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct SecurityInfo {
|
||||||
|
/// Size of this security info structure in bytes.
|
||||||
|
pub header_size: u32,
|
||||||
|
/// Size of the decompressed PE image in bytes.
|
||||||
|
pub image_size: u32,
|
||||||
|
/// RSA-2048 signature over the header (256 bytes).
|
||||||
|
pub rsa_signature: [u8; 256],
|
||||||
|
/// Unknown field at offset 0x108 (often a length value).
|
||||||
|
pub unk_108: u32,
|
||||||
|
/// Image flags bitmask.
|
||||||
|
pub image_flags: ImageFlags,
|
||||||
|
/// Virtual memory address where the PE image is loaded.
|
||||||
|
pub load_address: u32,
|
||||||
|
/// SHA-1 digest of section data (20 bytes).
|
||||||
|
pub section_digest: [u8; 20],
|
||||||
|
/// Number of import table entries.
|
||||||
|
pub import_table_count: u32,
|
||||||
|
/// SHA-1 digest of the import table (20 bytes).
|
||||||
|
pub import_table_digest: [u8; 20],
|
||||||
|
/// XGD2 media identifier (16 bytes).
|
||||||
|
pub xgd2_media_id: [u8; 16],
|
||||||
|
/// Encrypted AES-128 session key (16 bytes).
|
||||||
|
pub aes_key: [u8; 16],
|
||||||
|
/// Memory address of the XEX export table (0 if none).
|
||||||
|
pub export_table: u32,
|
||||||
|
/// SHA-1 digest of header data (20 bytes).
|
||||||
|
pub header_digest: [u8; 20],
|
||||||
|
/// Allowed regions bitmask.
|
||||||
|
pub region: RegionFlags,
|
||||||
|
/// Allowed media types bitmask.
|
||||||
|
pub allowed_media_types: MediaFlags,
|
||||||
|
/// Number of page descriptors following.
|
||||||
|
pub page_descriptor_count: u32,
|
||||||
|
/// Per-page descriptors with section types and integrity digests.
|
||||||
|
pub page_descriptors: Vec<PageDescriptor>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Parses the security info structure from `data` at the given file `offset`.
|
||||||
|
pub fn parse_security_info(data: &[u8], offset: u32) -> Result<SecurityInfo> {
|
||||||
|
let off = offset as usize;
|
||||||
|
|
||||||
|
// Minimum size: fixed fields up to page_descriptor_count (0x184 bytes)
|
||||||
|
let min_size = off + 0x184;
|
||||||
|
if min_size > data.len() {
|
||||||
|
return Err(Xex2Error::InvalidOffset {
|
||||||
|
name: "security_info",
|
||||||
|
offset,
|
||||||
|
file_size: data.len(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
let header_size = read_u32_be(data, off)?;
|
||||||
|
let image_size = read_u32_be(data, off + 0x004)?;
|
||||||
|
|
||||||
|
let mut rsa_signature = [0u8; 256];
|
||||||
|
rsa_signature.copy_from_slice(read_bytes(data, off + 0x008, 256)?);
|
||||||
|
|
||||||
|
let unk_108 = read_u32_be(data, off + 0x108)?;
|
||||||
|
let image_flags = ImageFlags(read_u32_be(data, off + 0x10C)?);
|
||||||
|
let load_address = read_u32_be(data, off + 0x110)?;
|
||||||
|
|
||||||
|
let mut section_digest = [0u8; 20];
|
||||||
|
section_digest.copy_from_slice(read_bytes(data, off + 0x114, 20)?);
|
||||||
|
|
||||||
|
let import_table_count = read_u32_be(data, off + 0x128)?;
|
||||||
|
|
||||||
|
let mut import_table_digest = [0u8; 20];
|
||||||
|
import_table_digest.copy_from_slice(read_bytes(data, off + 0x12C, 20)?);
|
||||||
|
|
||||||
|
let mut xgd2_media_id = [0u8; 16];
|
||||||
|
xgd2_media_id.copy_from_slice(read_bytes(data, off + 0x140, 16)?);
|
||||||
|
|
||||||
|
let mut aes_key = [0u8; 16];
|
||||||
|
aes_key.copy_from_slice(read_bytes(data, off + 0x150, 16)?);
|
||||||
|
|
||||||
|
let export_table = read_u32_be(data, off + 0x160)?;
|
||||||
|
|
||||||
|
let mut header_digest = [0u8; 20];
|
||||||
|
header_digest.copy_from_slice(read_bytes(data, off + 0x164, 20)?);
|
||||||
|
|
||||||
|
let region = RegionFlags(read_u32_be(data, off + 0x178)?);
|
||||||
|
let allowed_media_types = MediaFlags(read_u32_be(data, off + 0x17C)?);
|
||||||
|
let page_descriptor_count = read_u32_be(data, off + 0x180)?;
|
||||||
|
|
||||||
|
// Parse page descriptors (24 bytes each, starting at offset + 0x184)
|
||||||
|
let desc_start = off + 0x184;
|
||||||
|
let desc_total = page_descriptor_count as usize * 24;
|
||||||
|
if desc_start + desc_total > data.len() {
|
||||||
|
return Err(Xex2Error::FileTooSmall {
|
||||||
|
expected: desc_start + desc_total,
|
||||||
|
actual: data.len(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut page_descriptors = Vec::with_capacity(page_descriptor_count as usize);
|
||||||
|
for i in 0..page_descriptor_count as usize {
|
||||||
|
let base = desc_start + i * 24;
|
||||||
|
let value = read_u32_be(data, base)?;
|
||||||
|
let info = ((value >> 28) & 0xF) as u8;
|
||||||
|
let page_count = value & 0x0FFFFFFF;
|
||||||
|
|
||||||
|
let mut data_digest = [0u8; 20];
|
||||||
|
data_digest.copy_from_slice(read_bytes(data, base + 4, 20)?);
|
||||||
|
|
||||||
|
page_descriptors.push(PageDescriptor {
|
||||||
|
section_type: SectionType::from_raw(info),
|
||||||
|
page_count,
|
||||||
|
data_digest,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(SecurityInfo {
|
||||||
|
header_size,
|
||||||
|
image_size,
|
||||||
|
rsa_signature,
|
||||||
|
unk_108,
|
||||||
|
image_flags,
|
||||||
|
load_address,
|
||||||
|
section_digest,
|
||||||
|
import_table_count,
|
||||||
|
import_table_digest,
|
||||||
|
xgd2_media_id,
|
||||||
|
aes_key,
|
||||||
|
export_table,
|
||||||
|
header_digest,
|
||||||
|
region,
|
||||||
|
allowed_media_types,
|
||||||
|
page_descriptor_count,
|
||||||
|
page_descriptors,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Image Flags ───────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/// Image flags bitmask from the security info.
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
|
pub struct ImageFlags(pub u32);
|
||||||
|
|
||||||
|
impl ImageFlags {
|
||||||
|
const FLAGS: &[(u32, &str)] = &[
|
||||||
|
(0x00000002, "MANUFACTURING_UTILITY"),
|
||||||
|
(0x00000004, "MANUFACTURING_SUPPORT_TOOLS"),
|
||||||
|
(0x00000008, "XGD2_MEDIA_ONLY"),
|
||||||
|
(0x00000100, "CARDEA_KEY"),
|
||||||
|
(0x00000200, "XEIKA_KEY"),
|
||||||
|
(0x00000400, "USERMODE_TITLE"),
|
||||||
|
(0x00000800, "USERMODE_SYSTEM"),
|
||||||
|
(0x10000000, "PAGE_SIZE_4KB"),
|
||||||
|
(0x20000000, "REGION_FREE"),
|
||||||
|
(0x40000000, "REVOCATION_CHECK_OPTIONAL"),
|
||||||
|
(0x80000000, "REVOCATION_CHECK_REQUIRED"),
|
||||||
|
];
|
||||||
|
|
||||||
|
/// 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()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns `true` if the 4KB page size flag is set (otherwise 64KB).
|
||||||
|
pub fn is_4kb_pages(self) -> bool {
|
||||||
|
self.0 & 0x10000000 != 0
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the page size in bytes based on the page size flag.
|
||||||
|
pub fn page_size(self) -> u32 {
|
||||||
|
if self.is_4kb_pages() {
|
||||||
|
0x1000
|
||||||
|
} else {
|
||||||
|
0x10000
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl fmt::Display for ImageFlags {
|
||||||
|
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(", "))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Region Flags ──────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/// Allowed region flags bitmask.
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
|
pub struct RegionFlags(pub u32);
|
||||||
|
|
||||||
|
impl RegionFlags {
|
||||||
|
const NTSCU: u32 = 0x000000FF;
|
||||||
|
const NTSCJ: u32 = 0x0000FF00;
|
||||||
|
const NTSCJ_JAPAN: u32 = 0x00000100;
|
||||||
|
const NTSCJ_CHINA: u32 = 0x00000200;
|
||||||
|
const PAL: u32 = 0x00FF0000;
|
||||||
|
const PAL_AU_NZ: u32 = 0x00010000;
|
||||||
|
const OTHER: u32 = 0xFF000000;
|
||||||
|
const ALL: u32 = 0xFFFFFFFF;
|
||||||
|
|
||||||
|
/// Returns a human-readable description of the active region flags.
|
||||||
|
pub fn description(self) -> String {
|
||||||
|
if self.0 == Self::ALL {
|
||||||
|
return "ALL REGIONS".to_string();
|
||||||
|
}
|
||||||
|
if self.0 == 0 {
|
||||||
|
return "NONE".to_string();
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut regions = Vec::new();
|
||||||
|
if self.0 & Self::NTSCU != 0 {
|
||||||
|
regions.push("NTSC/U");
|
||||||
|
}
|
||||||
|
if self.0 & Self::NTSCJ_JAPAN != 0 {
|
||||||
|
regions.push("NTSC/J-Japan");
|
||||||
|
}
|
||||||
|
if self.0 & Self::NTSCJ_CHINA != 0 {
|
||||||
|
regions.push("NTSC/J-China");
|
||||||
|
}
|
||||||
|
// Only show generic NTSC/J if specific bits aren't set but region is
|
||||||
|
if self.0 & Self::NTSCJ != 0
|
||||||
|
&& self.0 & Self::NTSCJ_JAPAN == 0
|
||||||
|
&& self.0 & Self::NTSCJ_CHINA == 0
|
||||||
|
{
|
||||||
|
regions.push("NTSC/J");
|
||||||
|
}
|
||||||
|
if self.0 & Self::PAL_AU_NZ != 0 {
|
||||||
|
regions.push("PAL-AU/NZ");
|
||||||
|
}
|
||||||
|
// Only show generic PAL if specific bits aren't set but region is
|
||||||
|
if self.0 & Self::PAL != 0 && self.0 & Self::PAL_AU_NZ == 0 {
|
||||||
|
regions.push("PAL");
|
||||||
|
}
|
||||||
|
if self.0 & Self::OTHER != 0 {
|
||||||
|
regions.push("Other");
|
||||||
|
}
|
||||||
|
regions.join(", ")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl fmt::Display for RegionFlags {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
|
write!(f, "0x{:08X} [{}]", self.0, self.description())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Media Flags ───────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/// Allowed media types bitmask.
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
|
pub struct MediaFlags(pub u32);
|
||||||
|
|
||||||
|
impl MediaFlags {
|
||||||
|
const FLAGS: &[(u32, &str)] = &[
|
||||||
|
(0x00000001, "HARD_DISK"),
|
||||||
|
(0x00000002, "DVD_X2"),
|
||||||
|
(0x00000004, "DVD_CD"),
|
||||||
|
(0x00000008, "DVD_5"),
|
||||||
|
(0x00000010, "DVD_9"),
|
||||||
|
(0x00000020, "SYSTEM_FLASH"),
|
||||||
|
(0x00000080, "MEMORY_UNIT"),
|
||||||
|
(0x00000100, "USB_MASS_STORAGE"),
|
||||||
|
(0x00000200, "NETWORK"),
|
||||||
|
(0x00000400, "DIRECT_FROM_MEMORY"),
|
||||||
|
(0x00000800, "RAM_DRIVE"),
|
||||||
|
(0x00001000, "SVOD"),
|
||||||
|
(0x01000000, "INSECURE_PACKAGE"),
|
||||||
|
(0x02000000, "SAVEGAME_PACKAGE"),
|
||||||
|
(0x04000000, "LOCALLY_SIGNED_PACKAGE"),
|
||||||
|
(0x08000000, "LIVE_SIGNED_PACKAGE"),
|
||||||
|
(0x10000000, "XBOX_PACKAGE"),
|
||||||
|
];
|
||||||
|
|
||||||
|
/// 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 MediaFlags {
|
||||||
|
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(", "))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Page Descriptor ───────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/// A single page descriptor with section type, page count, and SHA-1 digest.
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct PageDescriptor {
|
||||||
|
/// The section type (code, data, read-only data, or unknown).
|
||||||
|
pub section_type: SectionType,
|
||||||
|
/// Number of pages in this section.
|
||||||
|
pub page_count: u32,
|
||||||
|
/// SHA-1 hash of the page data (20 bytes).
|
||||||
|
pub data_digest: [u8; 20],
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Section type from the page descriptor info bits.
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
|
pub enum SectionType {
|
||||||
|
Code,
|
||||||
|
Data,
|
||||||
|
ReadOnlyData,
|
||||||
|
Unknown(u8),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SectionType {
|
||||||
|
/// Creates a `SectionType` from the raw 4-bit info field.
|
||||||
|
pub fn from_raw(raw: u8) -> Self {
|
||||||
|
match raw {
|
||||||
|
1 => Self::Code,
|
||||||
|
2 => Self::Data,
|
||||||
|
3 => Self::ReadOnlyData,
|
||||||
|
v => Self::Unknown(v),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl fmt::Display for SectionType {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
|
match self {
|
||||||
|
Self::Code => write!(f, "Code"),
|
||||||
|
Self::Data => write!(f, "Data"),
|
||||||
|
Self::ReadOnlyData => write!(f, "ReadOnly"),
|
||||||
|
Self::Unknown(v) => write!(f, "Unknown({v})"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[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_parse_security_info_header() {
|
||||||
|
let data = sample_data();
|
||||||
|
let sec = parse_security_info(&data, 0x90).unwrap();
|
||||||
|
assert_eq!(sec.header_size, 0x00000F34);
|
||||||
|
assert_eq!(sec.image_size, 0x00920000);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_parse_security_info_fields() {
|
||||||
|
let data = sample_data();
|
||||||
|
let sec = parse_security_info(&data, 0x90).unwrap();
|
||||||
|
assert_eq!(sec.unk_108, 0x00000174);
|
||||||
|
assert_eq!(sec.image_flags, ImageFlags(0x00000008));
|
||||||
|
assert_eq!(sec.load_address, 0x82000000);
|
||||||
|
assert_eq!(sec.import_table_count, 2);
|
||||||
|
assert_eq!(sec.export_table, 0x00000000);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_parse_security_region_flags() {
|
||||||
|
let data = sample_data();
|
||||||
|
let sec = parse_security_info(&data, 0x90).unwrap();
|
||||||
|
assert_eq!(sec.region, RegionFlags(0xFFFFFFFF));
|
||||||
|
assert_eq!(sec.region.description(), "ALL REGIONS");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_parse_security_media_flags() {
|
||||||
|
let data = sample_data();
|
||||||
|
let sec = parse_security_info(&data, 0x90).unwrap();
|
||||||
|
assert_eq!(sec.allowed_media_types, MediaFlags(0x00000004));
|
||||||
|
assert_eq!(sec.allowed_media_types.flag_names(), vec!["DVD_CD"]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_parse_page_descriptors() {
|
||||||
|
let data = sample_data();
|
||||||
|
let sec = parse_security_info(&data, 0x90).unwrap();
|
||||||
|
assert_eq!(sec.page_descriptor_count, 0x92); // 146
|
||||||
|
assert_eq!(sec.page_descriptors.len(), 146);
|
||||||
|
// First descriptor: page_count = 0x13 = 19
|
||||||
|
assert_eq!(sec.page_descriptors[0].page_count, 19);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_parse_rsa_signature() {
|
||||||
|
let data = sample_data();
|
||||||
|
let sec = parse_security_info(&data, 0x90).unwrap();
|
||||||
|
// First bytes of RSA signature from hex dump
|
||||||
|
assert_eq!(sec.rsa_signature[0], 0x2C);
|
||||||
|
assert_eq!(sec.rsa_signature[1], 0x94);
|
||||||
|
assert_eq!(sec.rsa_signature[2], 0xEB);
|
||||||
|
assert_eq!(sec.rsa_signature[3], 0xE6);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_parse_xgd2_media_id() {
|
||||||
|
let data = sample_data();
|
||||||
|
let sec = parse_security_info(&data, 0x90).unwrap();
|
||||||
|
assert_eq!(sec.xgd2_media_id[0], 0x33);
|
||||||
|
assert_eq!(sec.xgd2_media_id[1], 0x51);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_parse_aes_key() {
|
||||||
|
let data = sample_data();
|
||||||
|
let sec = parse_security_info(&data, 0x90).unwrap();
|
||||||
|
assert_eq!(sec.aes_key[0], 0xEA);
|
||||||
|
assert_eq!(sec.aes_key[1], 0xCB);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_image_flags_display() {
|
||||||
|
let f = ImageFlags(0x00000008);
|
||||||
|
assert_eq!(f.to_string(), "0x00000008 [XGD2_MEDIA_ONLY]");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_image_flags_page_size() {
|
||||||
|
assert_eq!(ImageFlags(0x10000000).page_size(), 0x1000);
|
||||||
|
assert_eq!(ImageFlags(0x00000000).page_size(), 0x10000);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_region_flags_all() {
|
||||||
|
assert_eq!(RegionFlags(0xFFFFFFFF).description(), "ALL REGIONS");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_region_flags_ntscu_only() {
|
||||||
|
assert_eq!(RegionFlags(0x000000FF).description(), "NTSC/U");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_region_flags_none() {
|
||||||
|
assert_eq!(RegionFlags(0).description(), "NONE");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_region_flags_specific() {
|
||||||
|
// NTSC/U + PAL-AU/NZ
|
||||||
|
let r = RegionFlags(0x000100FF);
|
||||||
|
let desc = r.description();
|
||||||
|
assert!(desc.contains("NTSC/U"));
|
||||||
|
assert!(desc.contains("PAL-AU/NZ"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_media_flags_display() {
|
||||||
|
let f = MediaFlags(0x00000004);
|
||||||
|
assert_eq!(f.to_string(), "0x00000004 [DVD_CD]");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_media_flags_multiple() {
|
||||||
|
let f = MediaFlags(0x00000003); // HARD_DISK | DVD_X2
|
||||||
|
let names = f.flag_names();
|
||||||
|
assert_eq!(names, vec!["HARD_DISK", "DVD_X2"]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_section_type_from_raw() {
|
||||||
|
assert_eq!(SectionType::from_raw(1), SectionType::Code);
|
||||||
|
assert_eq!(SectionType::from_raw(2), SectionType::Data);
|
||||||
|
assert_eq!(SectionType::from_raw(3), SectionType::ReadOnlyData);
|
||||||
|
assert!(matches!(SectionType::from_raw(0), SectionType::Unknown(0)));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_section_type_display() {
|
||||||
|
assert_eq!(SectionType::Code.to_string(), "Code");
|
||||||
|
assert_eq!(SectionType::Data.to_string(), "Data");
|
||||||
|
assert_eq!(SectionType::ReadOnlyData.to_string(), "ReadOnly");
|
||||||
|
assert_eq!(SectionType::Unknown(5).to_string(), "Unknown(5)");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_invalid_security_offset() {
|
||||||
|
let data = sample_data();
|
||||||
|
let err = parse_security_info(&data, 0xFFFFFFFF).unwrap_err();
|
||||||
|
assert!(matches!(err, Xex2Error::InvalidOffset { .. }));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,10 +1,14 @@
|
|||||||
use xex2tractor::header::{ModuleFlags, XEX2_MAGIC};
|
use xex2tractor::header::{ModuleFlags, XEX2_MAGIC};
|
||||||
|
use xex2tractor::optional::{CompressionType, EncryptionType, SystemFlags};
|
||||||
|
use xex2tractor::security::{ImageFlags, MediaFlags, RegionFlags};
|
||||||
|
|
||||||
fn sample_data() -> Vec<u8> {
|
fn sample_data() -> Vec<u8> {
|
||||||
let path = format!("{}/tests/data/default.xex", env!("CARGO_MANIFEST_DIR"));
|
let path = format!("{}/tests/data/default.xex", env!("CARGO_MANIFEST_DIR"));
|
||||||
std::fs::read(&path).expect("sample file should exist at tests/data/default.xex")
|
std::fs::read(&path).expect("sample file should exist at tests/data/default.xex")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── Header tests ──────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_full_parse() {
|
fn test_full_parse() {
|
||||||
let data = sample_data();
|
let data = sample_data();
|
||||||
@@ -27,11 +31,162 @@ fn test_parse_empty_file() {
|
|||||||
#[test]
|
#[test]
|
||||||
fn test_parse_invalid_magic() {
|
fn test_parse_invalid_magic() {
|
||||||
let mut data = sample_data();
|
let mut data = sample_data();
|
||||||
// Corrupt the magic bytes
|
|
||||||
data[0] = 0x00;
|
data[0] = 0x00;
|
||||||
assert!(xex2tractor::parse(&data).is_err());
|
assert!(xex2tractor::parse(&data).is_err());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── Optional header tests ─────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_optional_headers_all_present() {
|
||||||
|
let data = sample_data();
|
||||||
|
let xex = xex2tractor::parse(&data).unwrap();
|
||||||
|
let opt = &xex.optional_headers;
|
||||||
|
|
||||||
|
// All 15 entries should be parsed
|
||||||
|
assert_eq!(opt.entries.len(), 15);
|
||||||
|
|
||||||
|
// Verify presence of all expected headers
|
||||||
|
assert!(opt.entry_point.is_some());
|
||||||
|
assert!(opt.image_base_address.is_some());
|
||||||
|
assert!(opt.default_stack_size.is_some());
|
||||||
|
assert!(opt.system_flags.is_some());
|
||||||
|
assert!(opt.execution_info.is_some());
|
||||||
|
assert!(opt.file_format_info.is_some());
|
||||||
|
assert!(opt.checksum_timestamp.is_some());
|
||||||
|
assert!(opt.original_pe_name.is_some());
|
||||||
|
assert!(opt.tls_info.is_some());
|
||||||
|
assert!(opt.static_libraries.is_some());
|
||||||
|
assert!(opt.import_libraries.is_some());
|
||||||
|
assert!(opt.resource_info.is_some());
|
||||||
|
assert!(opt.game_ratings.is_some());
|
||||||
|
assert!(opt.lan_key.is_some());
|
||||||
|
assert!(opt.xbox360_logo_size.is_some());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_optional_inline_values() {
|
||||||
|
let data = sample_data();
|
||||||
|
let xex = xex2tractor::parse(&data).unwrap();
|
||||||
|
let opt = &xex.optional_headers;
|
||||||
|
|
||||||
|
assert_eq!(opt.entry_point.unwrap(), 0x824AB748);
|
||||||
|
assert_eq!(opt.image_base_address.unwrap(), 0x82000000);
|
||||||
|
assert_eq!(opt.default_stack_size.unwrap(), 0x00080000);
|
||||||
|
assert_eq!(opt.system_flags.unwrap(), SystemFlags(0x00000400));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_optional_execution_info() {
|
||||||
|
let data = sample_data();
|
||||||
|
let xex = xex2tractor::parse(&data).unwrap();
|
||||||
|
let exec = xex.optional_headers.execution_info.as_ref().unwrap();
|
||||||
|
|
||||||
|
assert_eq!(exec.title_id, 0x535107D4);
|
||||||
|
assert_eq!(exec.media_id, 0x2D2E2EEB);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_optional_file_format() {
|
||||||
|
let data = sample_data();
|
||||||
|
let xex = xex2tractor::parse(&data).unwrap();
|
||||||
|
let fmt = xex.optional_headers.file_format_info.as_ref().unwrap();
|
||||||
|
|
||||||
|
assert_eq!(fmt.encryption_type, EncryptionType::Normal);
|
||||||
|
assert_eq!(fmt.compression_type, CompressionType::Normal);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_optional_static_libraries() {
|
||||||
|
let data = sample_data();
|
||||||
|
let xex = xex2tractor::parse(&data).unwrap();
|
||||||
|
let libs = xex.optional_headers.static_libraries.as_ref().unwrap();
|
||||||
|
|
||||||
|
assert_eq!(libs.len(), 12);
|
||||||
|
// Verify first and a few known libraries
|
||||||
|
assert_eq!(libs[0].name, "XAPILIB");
|
||||||
|
assert_eq!(libs[1].name, "D3D9");
|
||||||
|
assert_eq!(libs[3].name, "XBOXKRNL");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_optional_import_libraries() {
|
||||||
|
let data = sample_data();
|
||||||
|
let xex = xex2tractor::parse(&data).unwrap();
|
||||||
|
let imports = xex.optional_headers.import_libraries.as_ref().unwrap();
|
||||||
|
|
||||||
|
assert_eq!(imports.string_table.len(), 2);
|
||||||
|
assert_eq!(imports.string_table[0], "xam.xex");
|
||||||
|
assert_eq!(imports.string_table[1], "xboxkrnl.exe");
|
||||||
|
assert!(!imports.libraries.is_empty());
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Security info tests ───────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_security_info_parsed() {
|
||||||
|
let data = sample_data();
|
||||||
|
let xex = xex2tractor::parse(&data).unwrap();
|
||||||
|
let sec = &xex.security_info;
|
||||||
|
|
||||||
|
assert_eq!(sec.header_size, 0x00000F34);
|
||||||
|
assert_eq!(sec.image_size, 0x00920000);
|
||||||
|
assert_eq!(sec.unk_108, 0x00000174);
|
||||||
|
assert_eq!(sec.load_address, 0x82000000);
|
||||||
|
assert_eq!(sec.import_table_count, 2);
|
||||||
|
assert_eq!(sec.export_table, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_security_flags() {
|
||||||
|
let data = sample_data();
|
||||||
|
let xex = xex2tractor::parse(&data).unwrap();
|
||||||
|
let sec = &xex.security_info;
|
||||||
|
|
||||||
|
assert_eq!(sec.image_flags, ImageFlags(0x00000008));
|
||||||
|
assert_eq!(sec.region, RegionFlags(0xFFFFFFFF));
|
||||||
|
assert_eq!(sec.allowed_media_types, MediaFlags(0x00000004));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_security_page_descriptors() {
|
||||||
|
let data = sample_data();
|
||||||
|
let xex = xex2tractor::parse(&data).unwrap();
|
||||||
|
let sec = &xex.security_info;
|
||||||
|
|
||||||
|
assert_eq!(sec.page_descriptor_count, 146);
|
||||||
|
assert_eq!(sec.page_descriptors.len(), 146);
|
||||||
|
|
||||||
|
// First descriptor has page_count = 19
|
||||||
|
assert_eq!(sec.page_descriptors[0].page_count, 19);
|
||||||
|
|
||||||
|
// Page size should be 64KB (4KB flag is not set)
|
||||||
|
assert_eq!(sec.image_flags.page_size(), 0x10000);
|
||||||
|
|
||||||
|
// Each page descriptor should have a valid page_count
|
||||||
|
for desc in &sec.page_descriptors {
|
||||||
|
assert!(desc.page_count > 0, "page_count should be positive");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_security_crypto_fields() {
|
||||||
|
let data = sample_data();
|
||||||
|
let xex = xex2tractor::parse(&data).unwrap();
|
||||||
|
let sec = &xex.security_info;
|
||||||
|
|
||||||
|
// RSA signature starts with 2C94EBE6
|
||||||
|
assert_eq!(sec.rsa_signature[0..4], [0x2C, 0x94, 0xEB, 0xE6]);
|
||||||
|
|
||||||
|
// XGD2 media ID starts with 3351
|
||||||
|
assert_eq!(sec.xgd2_media_id[0..2], [0x33, 0x51]);
|
||||||
|
|
||||||
|
// AES key starts with EACB
|
||||||
|
assert_eq!(sec.aes_key[0..2], [0xEA, 0xCB]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── CLI tests ─────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_cli_runs_with_sample() {
|
fn test_cli_runs_with_sample() {
|
||||||
let path = format!("{}/tests/data/default.xex", env!("CARGO_MANIFEST_DIR"));
|
let path = format!("{}/tests/data/default.xex", env!("CARGO_MANIFEST_DIR"));
|
||||||
@@ -42,10 +197,37 @@ fn test_cli_runs_with_sample() {
|
|||||||
|
|
||||||
assert!(output.status.success(), "CLI should exit successfully");
|
assert!(output.status.success(), "CLI should exit successfully");
|
||||||
let stdout = String::from_utf8_lossy(&output.stdout);
|
let stdout = String::from_utf8_lossy(&output.stdout);
|
||||||
assert!(stdout.contains("XEX2 Header"), "should display header section");
|
|
||||||
assert!(stdout.contains("0x58455832"), "should display magic value");
|
// Header section
|
||||||
assert!(stdout.contains("TITLE"), "should display module flag name");
|
assert!(stdout.contains("XEX2 Header"));
|
||||||
assert!(stdout.contains("Header Count: 15"), "should show 15 headers");
|
assert!(stdout.contains("0x58455832"));
|
||||||
|
assert!(stdout.contains("TITLE"));
|
||||||
|
assert!(stdout.contains("Header Count: 15"));
|
||||||
|
|
||||||
|
// Optional headers section
|
||||||
|
assert!(stdout.contains("Optional Headers (15 entries)"));
|
||||||
|
assert!(stdout.contains("[ENTRY_POINT] 0x824AB748"));
|
||||||
|
assert!(stdout.contains("[IMAGE_BASE_ADDRESS] 0x82000000"));
|
||||||
|
assert!(stdout.contains("EXECUTION_INFO"));
|
||||||
|
assert!(stdout.contains("0x535107D4")); // title ID
|
||||||
|
assert!(stdout.contains("FILE_FORMAT_INFO"));
|
||||||
|
assert!(stdout.contains("Normal (AES-128-CBC)"));
|
||||||
|
assert!(stdout.contains("Normal (LZX)"));
|
||||||
|
assert!(stdout.contains("STATIC_LIBRARIES"));
|
||||||
|
assert!(stdout.contains("XAPILIB"));
|
||||||
|
assert!(stdout.contains("IMPORT_LIBRARIES"));
|
||||||
|
assert!(stdout.contains("xboxkrnl.exe"));
|
||||||
|
assert!(stdout.contains("default.pe")); // original PE name
|
||||||
|
assert!(stdout.contains("PAL50_INCOMPATIBLE")); // system flags
|
||||||
|
|
||||||
|
// Security info section
|
||||||
|
assert!(stdout.contains("Security Info"));
|
||||||
|
assert!(stdout.contains("0x00000F34")); // header size
|
||||||
|
assert!(stdout.contains("0x00920000")); // image size
|
||||||
|
assert!(stdout.contains("XGD2_MEDIA_ONLY")); // image flags
|
||||||
|
assert!(stdout.contains("ALL REGIONS")); // region
|
||||||
|
assert!(stdout.contains("DVD_CD")); // media type
|
||||||
|
assert!(stdout.contains("Page Descriptors")); // page descriptors section
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -54,9 +236,9 @@ fn test_cli_no_args() {
|
|||||||
.output()
|
.output()
|
||||||
.expect("failed to run xex2tractor");
|
.expect("failed to run xex2tractor");
|
||||||
|
|
||||||
assert!(!output.status.success(), "CLI should fail without args");
|
assert!(!output.status.success());
|
||||||
let stderr = String::from_utf8_lossy(&output.stderr);
|
let stderr = String::from_utf8_lossy(&output.stderr);
|
||||||
assert!(stderr.contains("Usage"), "should print usage message");
|
assert!(stderr.contains("Usage"));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -66,5 +248,5 @@ fn test_cli_missing_file() {
|
|||||||
.output()
|
.output()
|
||||||
.expect("failed to run xex2tractor");
|
.expect("failed to run xex2tractor");
|
||||||
|
|
||||||
assert!(!output.status.success(), "CLI should fail with missing file");
|
assert!(!output.status.success());
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user