use crate::Result;
pub(crate) const HEADER_SIZE: usize = 0x2e;
pub(crate) const MAGIC: &[u8; 16] = b"Master of Magic\0";
#[derive(Debug, Clone)]
pub struct Header {
pub encrypted_header: bool,
pub table_offset: u32,
pub seed: u32,
pub filecount: u32,
pub version: u32,
}
impl Header {
pub fn decode(buf: &[u8]) -> Result<Self> {
if buf.len() < HEADER_SIZE {
return Err(crate::Error::InvalidImage(format!(
"grf: header buffer is {} bytes, need {}",
buf.len(),
HEADER_SIZE
)));
}
if &buf[0..16] != MAGIC {
return Err(crate::Error::InvalidImage(
"grf: bad header magic (expected \"Master of Magic\")".into(),
));
}
let key = &buf[16..30];
let encrypted_header = !key.iter().all(|&b| b == 0);
let table_offset = u32::from_le_bytes(buf[30..34].try_into().unwrap());
let seed = u32::from_le_bytes(buf[34..38].try_into().unwrap());
let filecount_enc = u32::from_le_bytes(buf[38..42].try_into().unwrap());
let version = u32::from_le_bytes(buf[42..46].try_into().unwrap());
let filecount = filecount_enc.wrapping_sub(seed).wrapping_sub(7);
match version {
0x102 | 0x103 | 0x200 => {}
other => {
return Err(crate::Error::Unsupported(format!(
"grf: unsupported version {other:#x}"
)));
}
}
Ok(Self {
encrypted_header,
table_offset,
seed,
filecount,
version,
})
}
pub fn encode(&self) -> [u8; HEADER_SIZE] {
let mut buf = [0u8; HEADER_SIZE];
buf[0..16].copy_from_slice(MAGIC);
if self.encrypted_header {
for (i, b) in buf[16..30].iter_mut().enumerate() {
*b = (i + 1) as u8;
}
}
buf[30..34].copy_from_slice(&self.table_offset.to_le_bytes());
buf[34..38].copy_from_slice(&self.seed.to_le_bytes());
let filecount_enc = self.filecount.wrapping_add(self.seed).wrapping_add(7);
buf[38..42].copy_from_slice(&filecount_enc.to_le_bytes());
buf[42..46].copy_from_slice(&self.version.to_le_bytes());
buf
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn round_trip() {
let h = Header {
encrypted_header: false,
table_offset: 0xdead_beef,
seed: 0,
filecount: 42,
version: 0x200,
};
let bytes = h.encode();
let back = Header::decode(&bytes).unwrap();
assert_eq!(back.table_offset, 0xdead_beef);
assert_eq!(back.seed, 0);
assert_eq!(back.filecount, 42);
assert_eq!(back.version, 0x200);
assert!(!back.encrypted_header);
}
#[test]
fn encrypted_header_marker() {
let h = Header {
encrypted_header: true,
table_offset: 0,
seed: 0,
filecount: 0,
version: 0x103,
};
let bytes = h.encode();
assert_eq!(
&bytes[16..30],
&[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14]
);
let back = Header::decode(&bytes).unwrap();
assert!(back.encrypted_header);
}
#[test]
fn filecount_with_seed() {
let h = Header {
encrypted_header: false,
table_offset: 0,
seed: 100,
filecount: 50,
version: 0x200,
};
let bytes = h.encode();
assert_eq!(u32::from_le_bytes(bytes[38..42].try_into().unwrap()), 157);
let back = Header::decode(&bytes).unwrap();
assert_eq!(back.filecount, 50);
}
#[test]
fn rejects_bad_magic() {
let mut bytes = [0u8; HEADER_SIZE];
bytes[42..46].copy_from_slice(&0x200u32.to_le_bytes());
let err = Header::decode(&bytes).unwrap_err();
assert!(matches!(err, crate::Error::InvalidImage(_)));
}
#[test]
fn rejects_unknown_version() {
let h = Header {
encrypted_header: false,
table_offset: 0,
seed: 0,
filecount: 0,
version: 0x100,
};
let bytes = h.encode();
let err = Header::decode(&bytes).unwrap_err();
assert!(matches!(err, crate::Error::Unsupported(_)));
}
}