feat: parse and display XEX2 main header (M1)

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>
This commit is contained in:
MechaCat02
2026-03-28 18:52:15 +01:00
parent abbd264e4c
commit b5f2abe09a
9 changed files with 572 additions and 1 deletions

117
src/util.rs Normal file
View File

@@ -0,0 +1,117 @@
/// Big-endian binary read helpers with bounds checking.
use crate::error::{Result, Xex2Error};
/// Reads a big-endian `u32` from `data` at the given byte `offset`.
pub fn read_u32_be(data: &[u8], offset: usize) -> Result<u32> {
let end = offset + 4;
if end > data.len() {
return Err(Xex2Error::FileTooSmall {
expected: end,
actual: data.len(),
});
}
Ok(u32::from_be_bytes([
data[offset],
data[offset + 1],
data[offset + 2],
data[offset + 3],
]))
}
/// Reads a big-endian `u16` from `data` at the given byte `offset`.
pub fn read_u16_be(data: &[u8], offset: usize) -> Result<u16> {
let end = offset + 2;
if end > data.len() {
return Err(Xex2Error::FileTooSmall {
expected: end,
actual: data.len(),
});
}
Ok(u16::from_be_bytes([data[offset], data[offset + 1]]))
}
/// Reads a single byte from `data` at the given `offset`.
pub fn read_u8(data: &[u8], offset: usize) -> Result<u8> {
if offset >= data.len() {
return Err(Xex2Error::FileTooSmall {
expected: offset + 1,
actual: data.len(),
});
}
Ok(data[offset])
}
/// Returns a byte slice of `len` bytes from `data` starting at `offset`.
pub fn read_bytes(data: &[u8], offset: usize, len: usize) -> Result<&[u8]> {
let end = offset + len;
if end > data.len() {
return Err(Xex2Error::FileTooSmall {
expected: end,
actual: data.len(),
});
}
Ok(&data[offset..end])
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_read_u32_be() {
let data = [0x58, 0x45, 0x58, 0x32];
assert_eq!(read_u32_be(&data, 0).unwrap(), 0x58455832);
}
#[test]
fn test_read_u32_be_with_offset() {
let data = [0x00, 0x00, 0x58, 0x45, 0x58, 0x32];
assert_eq!(read_u32_be(&data, 2).unwrap(), 0x58455832);
}
#[test]
fn test_read_u16_be() {
let data = [0x12, 0x34];
assert_eq!(read_u16_be(&data, 0).unwrap(), 0x1234);
}
#[test]
fn test_read_u8() {
let data = [0xAB, 0xCD];
assert_eq!(read_u8(&data, 1).unwrap(), 0xCD);
}
#[test]
fn test_read_bytes() {
let data = [0x01, 0x02, 0x03, 0x04, 0x05];
assert_eq!(read_bytes(&data, 1, 3).unwrap(), &[0x02, 0x03, 0x04]);
}
#[test]
fn test_read_u32_be_out_of_bounds() {
let data = [0x00, 0x01];
let err = read_u32_be(&data, 0).unwrap_err();
assert!(matches!(err, Xex2Error::FileTooSmall { expected: 4, actual: 2 }));
}
#[test]
fn test_read_u16_be_out_of_bounds() {
let data = [0x00];
let err = read_u16_be(&data, 0).unwrap_err();
assert!(matches!(err, Xex2Error::FileTooSmall { expected: 2, actual: 1 }));
}
#[test]
fn test_read_u8_out_of_bounds() {
let data: [u8; 0] = [];
let err = read_u8(&data, 0).unwrap_err();
assert!(matches!(err, Xex2Error::FileTooSmall { expected: 1, actual: 0 }));
}
#[test]
fn test_read_bytes_out_of_bounds() {
let data = [0x01, 0x02];
let err = read_bytes(&data, 1, 3).unwrap_err();
assert!(matches!(err, Xex2Error::FileTooSmall { expected: 4, actual: 2 }));
}
}