Implement XEX2 main header parsing with module flag decoding. Add error handling, big-endian read utilities, CLI entry point, and comprehensive unit + integration tests against a sample file. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
209 lines
6.7 KiB
Rust
209 lines
6.7 KiB
Rust
/// XEX2 main header parsing.
|
|
///
|
|
/// The main header is located at the very beginning of the XEX2 file (offset 0x00)
|
|
/// and contains the magic bytes, module flags, header size, security info offset,
|
|
/// and the count of optional header entries that follow.
|
|
use std::fmt;
|
|
|
|
use crate::error::{Result, Xex2Error};
|
|
use crate::util::read_u32_be;
|
|
|
|
/// Expected magic value at offset 0x00: ASCII "XEX2" = 0x58455832.
|
|
pub const XEX2_MAGIC: u32 = 0x58455832;
|
|
|
|
/// Size of the fixed portion of the main header (before optional header entries).
|
|
pub const HEADER_SIZE: usize = 0x18;
|
|
|
|
/// The parsed XEX2 main header.
|
|
#[derive(Debug, Clone)]
|
|
pub struct Xex2Header {
|
|
/// Magic bytes — must be `XEX2_MAGIC` (0x58455832).
|
|
pub magic: u32,
|
|
/// Bitfield indicating the module type (title, DLL, patch, etc.).
|
|
pub module_flags: ModuleFlags,
|
|
/// Total size of all headers in bytes. The PE image data starts at this offset.
|
|
pub header_size: u32,
|
|
/// Reserved field (typically 0).
|
|
pub reserved: u32,
|
|
/// File offset to the `xex2_security_info` structure.
|
|
pub security_offset: u32,
|
|
/// Number of optional header entries following the main header.
|
|
pub header_count: u32,
|
|
}
|
|
|
|
/// Parses the XEX2 main header from the beginning of `data`.
|
|
///
|
|
/// Validates that the magic bytes match `XEX2_MAGIC` and that the buffer is
|
|
/// large enough to contain the fixed header fields.
|
|
pub fn parse_header(data: &[u8]) -> Result<Xex2Header> {
|
|
if data.len() < HEADER_SIZE {
|
|
return Err(Xex2Error::FileTooSmall {
|
|
expected: HEADER_SIZE,
|
|
actual: data.len(),
|
|
});
|
|
}
|
|
|
|
let magic = read_u32_be(data, 0x00)?;
|
|
if magic != XEX2_MAGIC {
|
|
return Err(Xex2Error::InvalidMagic(magic));
|
|
}
|
|
|
|
Ok(Xex2Header {
|
|
magic,
|
|
module_flags: ModuleFlags(read_u32_be(data, 0x04)?),
|
|
header_size: read_u32_be(data, 0x08)?,
|
|
reserved: read_u32_be(data, 0x0C)?,
|
|
security_offset: read_u32_be(data, 0x10)?,
|
|
header_count: read_u32_be(data, 0x14)?,
|
|
})
|
|
}
|
|
|
|
/// Wrapper around the module flags bitmask from the XEX2 header.
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
pub struct ModuleFlags(pub u32);
|
|
|
|
impl ModuleFlags {
|
|
pub const TITLE: u32 = 0x00000001;
|
|
pub const EXPORTS_TO_TITLE: u32 = 0x00000002;
|
|
pub const SYSTEM_DEBUGGER: u32 = 0x00000004;
|
|
pub const DLL_MODULE: u32 = 0x00000008;
|
|
pub const MODULE_PATCH: u32 = 0x00000010;
|
|
pub const PATCH_FULL: u32 = 0x00000020;
|
|
pub const PATCH_DELTA: u32 = 0x00000040;
|
|
pub const USER_MODE: u32 = 0x00000080;
|
|
|
|
/// All known flags paired with their display names, in bit order.
|
|
const FLAGS: &[(u32, &str)] = &[
|
|
(Self::TITLE, "TITLE"),
|
|
(Self::EXPORTS_TO_TITLE, "EXPORTS_TO_TITLE"),
|
|
(Self::SYSTEM_DEBUGGER, "SYSTEM_DEBUGGER"),
|
|
(Self::DLL_MODULE, "DLL_MODULE"),
|
|
(Self::MODULE_PATCH, "MODULE_PATCH"),
|
|
(Self::PATCH_FULL, "PATCH_FULL"),
|
|
(Self::PATCH_DELTA, "PATCH_DELTA"),
|
|
(Self::USER_MODE, "USER_MODE"),
|
|
];
|
|
|
|
/// Returns the raw `u32` value.
|
|
pub fn bits(self) -> u32 {
|
|
self.0
|
|
}
|
|
|
|
/// 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 ModuleFlags {
|
|
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(", "))
|
|
}
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
/// Builds a minimal valid XEX2 header buffer with the given field values.
|
|
fn make_header(
|
|
magic: u32,
|
|
module_flags: u32,
|
|
header_size: u32,
|
|
reserved: u32,
|
|
security_offset: u32,
|
|
header_count: u32,
|
|
) -> Vec<u8> {
|
|
let mut buf = Vec::with_capacity(HEADER_SIZE);
|
|
buf.extend_from_slice(&magic.to_be_bytes());
|
|
buf.extend_from_slice(&module_flags.to_be_bytes());
|
|
buf.extend_from_slice(&header_size.to_be_bytes());
|
|
buf.extend_from_slice(&reserved.to_be_bytes());
|
|
buf.extend_from_slice(&security_offset.to_be_bytes());
|
|
buf.extend_from_slice(&header_count.to_be_bytes());
|
|
buf
|
|
}
|
|
|
|
#[test]
|
|
fn test_parse_valid_header() {
|
|
let data = make_header(XEX2_MAGIC, 0x01, 0x3000, 0, 0x90, 15);
|
|
let header = parse_header(&data).unwrap();
|
|
assert_eq!(header.magic, XEX2_MAGIC);
|
|
assert_eq!(header.module_flags, ModuleFlags(0x01));
|
|
assert_eq!(header.header_size, 0x3000);
|
|
assert_eq!(header.reserved, 0);
|
|
assert_eq!(header.security_offset, 0x90);
|
|
assert_eq!(header.header_count, 15);
|
|
}
|
|
|
|
#[test]
|
|
fn test_invalid_magic() {
|
|
let data = make_header(0xDEADBEEF, 0, 0, 0, 0, 0);
|
|
let err = parse_header(&data).unwrap_err();
|
|
assert!(matches!(err, Xex2Error::InvalidMagic(0xDEADBEEF)));
|
|
}
|
|
|
|
#[test]
|
|
fn test_file_too_small() {
|
|
let data = [0u8; 10];
|
|
let err = parse_header(&data).unwrap_err();
|
|
assert!(matches!(
|
|
err,
|
|
Xex2Error::FileTooSmall {
|
|
expected: HEADER_SIZE,
|
|
..
|
|
}
|
|
));
|
|
}
|
|
|
|
#[test]
|
|
fn test_module_flags_display_title() {
|
|
let flags = ModuleFlags(0x01);
|
|
assert_eq!(flags.to_string(), "0x00000001 [TITLE]");
|
|
}
|
|
|
|
#[test]
|
|
fn test_module_flags_display_multiple() {
|
|
let flags = ModuleFlags(0x09); // TITLE | DLL_MODULE
|
|
assert_eq!(flags.to_string(), "0x00000009 [TITLE, DLL_MODULE]");
|
|
}
|
|
|
|
#[test]
|
|
fn test_module_flags_display_none() {
|
|
let flags = ModuleFlags(0);
|
|
assert_eq!(flags.to_string(), "0x00000000");
|
|
}
|
|
|
|
#[test]
|
|
fn test_module_flags_display_all() {
|
|
let flags = ModuleFlags(0xFF);
|
|
let names = flags.flag_names();
|
|
assert_eq!(names.len(), 8);
|
|
assert_eq!(names[0], "TITLE");
|
|
assert_eq!(names[7], "USER_MODE");
|
|
}
|
|
|
|
/// Test against the actual default.xex sample file.
|
|
#[test]
|
|
fn test_parse_sample_header() {
|
|
let path = format!("{}/tests/data/default.xex", env!("CARGO_MANIFEST_DIR"));
|
|
let data = std::fs::read(&path).expect("sample file should exist at tests/data/default.xex");
|
|
let header = parse_header(&data).unwrap();
|
|
assert_eq!(header.magic, XEX2_MAGIC);
|
|
assert_eq!(header.module_flags, ModuleFlags(0x00000001));
|
|
assert_eq!(header.header_size, 0x00003000);
|
|
assert_eq!(header.reserved, 0x00000000);
|
|
assert_eq!(header.security_offset, 0x00000090);
|
|
assert_eq!(header.header_count, 15);
|
|
}
|
|
}
|