goldforge 0.8.1

Library for handling file formats used by GoldSrc and related engines.
Documentation
// Copyright 2025-2026 Gabriel Bjørnager Jensen.
//
// This Source Code Form is subject to the terms of
// the Mozilla Public License, v. 2.0. If a copy of
// the MPL was not distributed with this file, you
// can obtain one at:
// <https://mozilla.org/MPL/2.0/>.

//! The [`Wad`] type.

mod test;

mod validate;

use crate::wad::{
	DecodeError,
	Lump,
	Lumps,
	RawHeader,
	RawLumpSlice,
	Tag,
};
use crate::wad::ffi::{doom, halflife, quake};

use core::slice;
use oct::{FromOcts, IntoOcts};

#[cfg(feature = "alloc")]
use alloc::borrow::ToOwned;

#[cfg(feature = "alloc")]
use alloc::boxed::Box;

#[cfg(feature = "alloc")]
use core::mem::transmute;

/// A WAD file.
///
/// Objects of this type represent WAD files of any
/// supported version, which currently includes
/// DOOM, WAD2, and WAD3.
///
/// # Examples
///
/// Reading a raw WAD file:
///
/// ```rust
/// use goldforge::wad::Wad;
///
/// static BYTES: &[u8] = &[
/// #    0x50, 0x57, 0x41, 0x44, 0x01, 0x00, 0x00, 0x00,
/// #    0x11, 0x00, 0x00, 0x00, 0x49, 0x4C, 0x55, 0x56,
/// #    0x55, 0x0C, 0x00, 0x00, 0x00, 0x05, 0x00, 0x00,
/// #    0x00, 0x47, 0x4C, 0x44, 0x46, 0x52, 0x47, 0x00,
/// #    0x00,
///     // ..
/// ];
///
/// let wad = Wad::from_bytes(BYTES).unwrap();
///
/// assert!(!wad.is_iwad());
/// assert!(wad.is_pwad());
/// assert!(!wad.is_wad2());
/// assert!(!wad.is_wad3());
///
/// assert_eq!(wad.lump_count(), 1);
///
/// let lump = wad.lumps().next().unwrap();
///
/// assert_eq!(lump.name(), Some("GLDFRG"));
/// assert_eq!(lump.data(), b"ILUVU");
/// assert_eq!(lump.lump_type(), None);
/// assert_eq!(lump.compression(), None);
///  ```
#[repr(transparent)]
#[derive(Debug)]
pub struct Wad {
	/// The raw byte data.
	///
	/// We have a hard guarantee that this data contains
	/// a valid WAD file and that its contents do not
	/// change. For our own constructors, this promise
	/// may be postponed if [`validate`] is correctly
	/// used.
	///
	/// [`validate`]: Self::validate
	data: [u8],
}

impl Wad {
	/// Constructs a WAD file from raw bytes.
	///
	/// The entire file is validated at once. Thus,
	/// users can expect to be able to read all fields
	/// without issues
	///
	/// # Errors
	///
	/// For the WAD header, this function will yield an
	/// [`Err`] instance if any of the following apply:
	/// * The `identification` field is neither `IWAD`,
	///   `DAWI`, `PWAD`, `DAWP`, `WAD2`, `DAW2`, nor
	///   `WAD3`
	/// * Either of the `numlumps` or `infotableofs`
	///   fields cannot be represented by [`usize`] after
	///   signed-to-unsigned wrap *and* `numlumps` is
	///   non-zero
	///
	/// For each lump, this function will yield an
	/// `Err` instance if any of the following apply:
	/// * Any of the `filepos`, `disksize`, or `size`
	///   fields cannot be represented by `usize` after
	///   signed-to-unsigned *and* `size` (DOOM only) or
	///   `disksize` is non-zero
	///
	/// Generally, this function is quite conservative
	/// with regard to what is considered a read error.
	/// Users should handle stricter requirements
	/// themselves when necessitated.
	pub fn from_bytes(bytes: &[u8]) -> Result<&Self, DecodeError> {
		// SAFETY: `validate` handles the file.
		let this = unsafe { Self::from_bytes_unchecked(bytes) };

		this.validate().map(|()| this)
	}

	/// Constructs a boxed WAD file from raw, boxed bytes.
	///
	/// # Errors
	///
	/// See [`from_bytes`].
	///
	/// [`from_bytes`]: Self::from_bytes
	#[cfg(feature = "alloc")]
	pub fn from_boxed_bytes(bytes: Box<[u8]>) -> Result<Box<Self>, DecodeError> {
		// SAFETY: `validate` handles the file.
		let this = unsafe { Self::from_boxed_bytes_unchecked(bytes) };

		this.validate().map(|()| this)
	}

	/// Constructs a WAD file from raw bytes without
	/// validating.
	///
	/// # Safety
	///
	/// The raw bytes must denote a valid WAD file. See
	/// [`from_bytes`] for more information.
	///
	/// [`from_bytes`]: Self::from_bytes
	#[inline]
	#[must_use]
	pub const unsafe fn from_bytes_unchecked(bytes: &[u8]) -> &Self {
		// SAFETY: `Wad` is transparent to `[u8]`.
		let p = &raw const *bytes as *const Self;
		unsafe { &*p }
	}

	/// Constructs a WAD file from raw, boxed bytes
	/// without validating.
	///
	/// # Safety
	///
	/// The raw bytes must denote a valid WAD file. See
	/// [`from_bytes`] for more information.
	///
	/// [`from_bytes`]: Self::from_bytes
	#[cfg(feature = "alloc")]
	#[inline]
	#[must_use]
	pub const unsafe fn from_boxed_bytes_unchecked(bytes: Box<[u8]>) -> Box<Self> {
		// SAFETY: `Wad` is transparent to `[u8]`.
		unsafe { transmute::<Box<[u8]>, Box<Self>>(bytes) }
	}

	/// Constructs a WAD file from raw parts.
	///
	/// # Safety
	///
	/// The object at `ptr` must be representable as a
	/// byte array of at least `len` units. Furthermore,
	/// this array must have a lifetime that fully
	/// contains `'a`. Finally, these, underlying bytes
	/// must denote a valid WAD file (of any supported
	/// version).
	#[inline]
	#[must_use]
	pub const unsafe fn from_raw_parts<'a>(ptr: *const u8, len: usize) -> &'a Self {
		let bytes = unsafe { slice::from_raw_parts(ptr, len) };

		// SAFETY: Caller guarantees validity.
		unsafe { Self::from_bytes_unchecked(bytes) }
	}

	/// Retrieves the (computed) WAD tag.
	#[inline(always)]
	#[must_use]
	pub(super) fn tag(&self) -> Tag {
		let bytes = self.as_bytes();

		let magic = <[i8; 4]>::read_from_octs(bytes);

		unsafe { Tag::from_magic_unchecked(magic) }
	}

	/// Retrieves the WAD header.
	#[inline]
	#[must_use]
	pub(super) fn header(&self) -> RawHeader<'_> {
		let bytes = self.as_bytes();

		match self.tag() {
			Tag::Wad  => doom::wadinfo_t::ref_from_octs(bytes).into(),
			Tag::Wad2 => quake::wadinfo_t::ref_from_octs(bytes).into(),
			Tag::Wad3 => halflife::wadinfo_t::ref_from_octs(bytes).into(),
		}
	}

	/// Retrieves the WAD directory.
	#[inline]
	#[must_use]
	pub(super) fn directory(&self) -> RawLumpSlice<'_> {
		let header = self.header();

		let bytes = {
			#[allow(clippy::cast_possible_truncation)]
			#[allow(clippy::cast_possible_wrap)]
			let start = header.infotableofs().cast_unsigned() as usize;

			unsafe { self.as_bytes().get_unchecked(start..) }
		};

		#[allow(clippy::cast_possible_truncation)]
		#[allow(clippy::cast_possible_wrap)]
		let len = header.numlumps().cast_unsigned() as usize;

		match self.tag() {
			Tag::Wad  => <[doom::filelump_t]>::ref_from_octs_with_len(bytes, len).into(),
			Tag::Wad2 => <[quake::lumpinfo_t]>::ref_from_octs_with_len(bytes, len).into(),
			Tag::Wad3 => <[halflife::lumpinfo_t]>::ref_from_octs_with_len(bytes, len).into(),
		}
	}

	/// Tests if the WAD file is an internal (DOOM)
	/// file.
	#[inline]
	#[must_use]
	pub fn is_iwad(&self) -> bool {
		matches!(
			self.header().identification().as_octs(),
			b"IWAD" | b"DAWI",
		)
	}

	/// Tests if the WAD file is a patch (DOOM) file.
	#[inline]
	#[must_use]
	pub fn is_pwad(&self) -> bool {
		matches!(
			self.header().identification().as_octs(),
			b"PWAD" | b"DAWP",
		)
	}

	/// Tests if the file is a WAD2 file.
	#[inline]
	#[must_use]
	pub fn is_wad2(&self) -> bool {
		matches!(
			self.header().identification().as_octs(),
			b"WAD2" | b"DAW2",
		)
	}

	/// Tests if the file is a WAD3 file.
	#[inline]
	#[must_use]
	pub fn is_wad3(&self) -> bool {
		self.header().identification().as_octs() == b"WAD3"
	}

	/// Tests the existance of the specified lump name.
	///
	/// If the specified lump exists, this method will
	/// return `true`.
	#[must_use]
	pub fn contains_lump(&self, name: &str) -> bool {
		self.lumps().any(|l| l.raw_name().as_octs() == name.as_bytes())
	}

	/// Searches for the specified lump name.
	///
	/// A handle for the lump is returned if the name is
	/// found. If no lump exists with that name, this
	/// method will return [`None`].
	#[must_use]
	pub fn lump(&self, name: &str) -> Option<Lump<'_>> {
		// NOTE: Lumps have first-in last-out precedence.
		self.lumps().rev().find(|l| l.raw_name().as_octs() == name.as_bytes())
	}

	/// Creates an iterator over the lumps.
	#[inline]
	pub fn lumps(&self) -> Lumps<'_> {
		Lumps::new(self)
	}

	/// Counts the amount of lumps in the WAD file.
	#[inline]
	#[must_use]
	pub fn lump_count(&self) -> usize {
		#[allow(clippy::cast_possible_truncation)]
		#[allow(clippy::cast_possible_wrap)]
		{ self.header().numlumps().cast_unsigned() as usize }
	}

	/// Counts the (bytewise) length of the WAD file.
	#[expect(clippy::len_without_is_empty)]
	#[inline]
	#[must_use]
	pub fn len(&self) -> usize {
		self.data.len()
	}

	/// Reinterprets the WAD file as raw bytes.
	#[inline]
	pub fn as_bytes(&self) -> &[u8] {
		&self.data
	}

	/// Retrieves a raw pointer to the WAD file
	/// contents.
	#[inline]
	pub fn as_ptr(&self) -> *const u8 {
		self.data.as_ptr()
	}
}

#[cfg(feature = "alloc")]
impl Clone for Box<Wad> {
	#[inline]
	fn clone(&self) -> Self {
		self.to_owned()
	}
}

#[cfg(feature = "alloc")]
impl ToOwned for Wad {
	type Owned = Box<Self>;

	fn to_owned(&self) -> Self::Owned {
		let bytes = self.data.to_owned().into_boxed_slice();

		// SAFETY: Bytes are reused.
		unsafe { Self::from_boxed_bytes_unchecked(bytes) }
	}
}