/// 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 { 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 { 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); } }