#![allow(clippy::indexing_slicing)]
use alloc::vec::Vec;
use core::fmt;
pub struct Wad<'a> {
data: &'a [u8],
lumps: Vec<LumpInfo>,
}
struct LumpInfo {
name: [u8; 8],
offset: usize,
size: usize,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum WadKind {
Iwad,
Pwad,
}
#[derive(Debug)]
pub enum WadError {
TooSmall,
BadMagic,
DirectoryOutOfBounds,
LumpOutOfBounds { index: usize },
}
impl fmt::Display for WadError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::TooSmall => write!(f, "WAD data too small for header"),
Self::BadMagic => write!(f, "invalid WAD magic (expected IWAD or PWAD)"),
Self::DirectoryOutOfBounds => write!(f, "WAD directory offset out of bounds"),
Self::LumpOutOfBounds { index } => write!(f, "lump {index} data out of bounds"),
}
}
}
impl core::error::Error for WadError {}
fn read_i32_le(data: &[u8], offset: usize) -> i32 {
let Some(slice) = data.get(offset..offset + 4) else { return 0 };
let mut bytes = [0u8; 4];
bytes.copy_from_slice(slice);
i32::from_le_bytes(bytes)
}
fn normalize_name(raw: &[u8; 8]) -> [u8; 8] {
let mut out = [0u8; 8];
for (i, &b) in raw.iter().enumerate() {
if b == 0 {
break;
}
out[i] = b.to_ascii_uppercase();
}
out
}
fn str_to_name(s: &str) -> [u8; 8] {
let mut name = [0u8; 8];
for (i, &b) in s.as_bytes().iter().take(8).enumerate() {
name[i] = b.to_ascii_uppercase();
}
name
}
impl<'a> Wad<'a> {
pub fn parse(data: &'a [u8]) -> Result<Self, WadError> {
if data.len() < 12 {
return Err(WadError::TooSmall);
}
let magic = &data[0..4];
if magic != b"IWAD" && magic != b"PWAD" {
return Err(WadError::BadMagic);
}
let num_lumps = read_i32_le(data, 4) as usize;
let dir_offset = read_i32_le(data, 8) as usize;
let dir_end = dir_offset
.checked_add(num_lumps.checked_mul(16).ok_or(WadError::DirectoryOutOfBounds)?)
.ok_or(WadError::DirectoryOutOfBounds)?;
if dir_end > data.len() {
return Err(WadError::DirectoryOutOfBounds);
}
let mut lumps = Vec::with_capacity(num_lumps);
for i in 0..num_lumps {
let entry = dir_offset + i * 16;
let offset = read_i32_le(data, entry) as usize;
let size = read_i32_le(data, entry + 4) as usize;
let mut name = [0u8; 8];
name.copy_from_slice(&data[entry + 8..entry + 16]);
if size > 0 && offset.checked_add(size).is_none_or(|end| end > data.len()) {
return Err(WadError::LumpOutOfBounds { index: i });
}
lumps.push(LumpInfo { name, offset, size });
}
Ok(Self { data, lumps })
}
pub fn kind(&self) -> WadKind {
if &self.data[0..4] == b"IWAD" {
WadKind::Iwad
} else {
WadKind::Pwad
}
}
pub fn num_lumps(&self) -> usize {
self.lumps.len()
}
pub fn find_lump(&self, name: &str) -> Option<usize> {
let target = str_to_name(name);
self.lumps
.iter()
.position(|l| normalize_name(&l.name) == target)
}
pub fn find_lump_after(&self, name: &str, after: usize) -> Option<usize> {
let target = str_to_name(name);
self.lumps[after..]
.iter()
.position(|l| normalize_name(&l.name) == target)
.map(|i| i + after)
}
pub fn lump_data(&self, index: usize) -> &'a [u8] {
let info = &self.lumps[index];
&self.data[info.offset..info.offset + info.size]
}
pub fn lump_size(&self, index: usize) -> usize {
self.lumps[index].size
}
pub fn lump_name(&self, index: usize) -> &[u8; 8] {
&self.lumps[index].name
}
pub fn lump_by_name(&self, name: &str) -> Option<&'a [u8]> {
self.find_lump(name).map(|i| self.lump_data(i))
}
}
#[cfg(test)]
#[allow(clippy::unwrap_used)] mod tests {
use super::*;
#[test]
fn parse_minimal_iwad() {
let mut data = Vec::new();
data.extend_from_slice(b"IWAD");
data.extend_from_slice(&0i32.to_le_bytes()); data.extend_from_slice(&12i32.to_le_bytes()); let wad = Wad::parse(&data).unwrap();
assert_eq!(wad.kind(), WadKind::Iwad);
assert_eq!(wad.num_lumps(), 0);
}
#[test]
fn parse_single_lump() {
let mut data = Vec::new();
data.extend_from_slice(b"PWAD");
data.extend_from_slice(&1i32.to_le_bytes()); data.extend_from_slice(&16i32.to_le_bytes());
data.extend_from_slice(b"TEST");
data.extend_from_slice(&12i32.to_le_bytes()); data.extend_from_slice(&4i32.to_le_bytes()); data.extend_from_slice(b"TESTLUMP");
let wad = Wad::parse(&data).unwrap();
assert_eq!(wad.kind(), WadKind::Pwad);
assert_eq!(wad.num_lumps(), 1);
assert_eq!(wad.find_lump("TESTLUMP"), Some(0));
assert_eq!(wad.find_lump("testlump"), Some(0)); assert_eq!(wad.lump_data(0), b"TEST");
assert_eq!(wad.lump_size(0), 4);
}
#[test]
fn bad_magic() {
let data = b"XWAD\x00\x00\x00\x00\x0c\x00\x00\x00";
assert!(matches!(Wad::parse(data), Err(WadError::BadMagic)));
}
#[test]
fn too_small() {
assert!(matches!(Wad::parse(b"IWA"), Err(WadError::TooSmall)));
}
}