Add session key derivation and payload decryption using AES-128-CBC with well-known XEX2 master keys. Refactor CLI to use clap with inspect/extract subcommands. Extend FileFormatInfo to parse compression metadata (basic blocks, LZX window size/block chain). Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
311 lines
10 KiB
Rust
311 lines
10 KiB
Rust
/// Pretty-print formatting for parsed XEX2 structures.
|
|
use crate::header::Xex2Header;
|
|
use crate::optional::{
|
|
format_hex_bytes, format_rating, format_timestamp, CompressionInfo, HeaderKey, OptionalHeaders,
|
|
};
|
|
use crate::security::SecurityInfo;
|
|
|
|
/// Prints the XEX2 main header in a human-readable format.
|
|
pub fn display_header(header: &Xex2Header) {
|
|
println!("=== XEX2 Header ===");
|
|
println!("Magic: XEX2 (0x{:08X})", header.magic);
|
|
println!("Module Flags: {}", header.module_flags);
|
|
println!(
|
|
"Header Size: 0x{:08X} ({} bytes)",
|
|
header.header_size, header.header_size
|
|
);
|
|
println!("Reserved: 0x{:08X}", header.reserved);
|
|
println!("Security Offset: 0x{:08X}", header.security_offset);
|
|
println!("Header Count: {}", header.header_count);
|
|
}
|
|
|
|
/// Prints all parsed optional headers in a human-readable format.
|
|
pub fn display_optional_headers(headers: &OptionalHeaders) {
|
|
println!();
|
|
println!("=== Optional Headers ({} entries) ===", headers.entries.len());
|
|
|
|
// Display inline u32 values first
|
|
if let Some(v) = headers.entry_point {
|
|
println!();
|
|
println!("[ENTRY_POINT] 0x{v:08X}");
|
|
}
|
|
if let Some(v) = headers.original_base_address {
|
|
println!("[ORIGINAL_BASE_ADDRESS] 0x{v:08X}");
|
|
}
|
|
if let Some(v) = headers.image_base_address {
|
|
println!("[IMAGE_BASE_ADDRESS] 0x{v:08X}");
|
|
}
|
|
if let Some(v) = headers.default_stack_size {
|
|
println!("[DEFAULT_STACK_SIZE] 0x{v:08X} ({v} bytes)");
|
|
}
|
|
if let Some(v) = headers.default_filesystem_cache_size {
|
|
println!("[DEFAULT_FILESYSTEM_CACHE_SIZE] 0x{v:08X} ({v} bytes)");
|
|
}
|
|
if let Some(v) = headers.default_heap_size {
|
|
println!("[DEFAULT_HEAP_SIZE] 0x{v:08X} ({v} bytes)");
|
|
}
|
|
if let Some(v) = headers.title_workspace_size {
|
|
println!("[TITLE_WORKSPACE_SIZE] 0x{v:08X} ({v} bytes)");
|
|
}
|
|
if let Some(v) = headers.additional_title_memory {
|
|
println!("[ADDITIONAL_TITLE_MEMORY] 0x{v:08X} ({v} bytes)");
|
|
}
|
|
if let Some(v) = headers.enabled_for_fastcap {
|
|
println!("[ENABLED_FOR_FASTCAP] 0x{v:08X}");
|
|
}
|
|
|
|
// System flags
|
|
if let Some(ref flags) = headers.system_flags {
|
|
println!("[SYSTEM_FLAGS] {flags}");
|
|
}
|
|
|
|
// Execution info
|
|
if let Some(ref exec) = headers.execution_info {
|
|
println!();
|
|
println!("[EXECUTION_INFO]");
|
|
println!(" Media ID: 0x{:08X}", exec.media_id);
|
|
println!(" Title ID: 0x{:08X}", exec.title_id);
|
|
println!(" Version: {}", exec.version);
|
|
println!(" Base Version: {}", exec.base_version);
|
|
println!(" Platform: {}", exec.platform);
|
|
println!(" Executable Type: {}", exec.executable_type);
|
|
println!(" Disc: {}/{}", exec.disc_number, exec.disc_count);
|
|
println!(" Savegame ID: 0x{:08X}", exec.savegame_id);
|
|
}
|
|
|
|
// File format info
|
|
if let Some(ref fmt) = headers.file_format_info {
|
|
println!();
|
|
println!("[FILE_FORMAT_INFO]");
|
|
println!(" Encryption: {}", fmt.encryption_type);
|
|
println!(" Compression: {}", fmt.compression_type);
|
|
match &fmt.compression_info {
|
|
CompressionInfo::Basic { blocks } => {
|
|
println!(" Blocks: {} basic compression blocks", blocks.len());
|
|
}
|
|
CompressionInfo::Normal {
|
|
window_size,
|
|
first_block,
|
|
} => {
|
|
println!(
|
|
" Window Size: 0x{window_size:X} ({} KB)",
|
|
window_size / 1024
|
|
);
|
|
println!(
|
|
" First Block: {} bytes",
|
|
first_block.block_size
|
|
);
|
|
}
|
|
_ => {}
|
|
}
|
|
}
|
|
|
|
// 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)"
|
|
);
|
|
}
|