diff --git a/.gitignore b/.gitignore index 60f1a22..5f06b0b 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,4 @@ /target -Cargo.lock +/dump /test-res/lg-file diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..825c14d --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,188 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "adler" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" + +[[package]] +name = "ahash" +version = "0.8.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42cd52102d3df161c77a887b608d7a4897d7cc112886a9537b738a887a03aaff" +dependencies = [ + "cfg-if", + "once_cell", + "version_check", + "zerocopy", +] + +[[package]] +name = "allocator-api2" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0942ffc6dcaadf03badf6e6a2d0228460359d5e34b57ccdc720b7382dfbd5ec5" + +[[package]] +name = "benchmarking" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32842502e72b27b30b2681692d16bf47a8a375c5a2795613f0dc699bed9a48bb" + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "crc32fast" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b540bd8bc810d3885c6ea91e2018302f68baba2129ab3e88f32389ee9370880d" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "fdeflate" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4f9bfee30e4dedf0ab8b422f03af778d9612b63f502710fc500a334ebe2de645" +dependencies = [ + "simd-adler32", +] + +[[package]] +name = "flate2" +version = "1.0.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46303f565772937ffe1d394a4fac6f411c6013172fadde9dcdb1e147a086940e" +dependencies = [ + "crc32fast", + "miniz_oxide", +] + +[[package]] +name = "hashbrown" +version = "0.14.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "290f1a1d9242c78d09ce40a5e87e7554ee637af1351968159f4952f028f75604" +dependencies = [ + "ahash", + "allocator-api2", +] + +[[package]] +name = "miniz_oxide" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d811f3e15f28568be3407c8e7fdb6514c1cda3cb30683f15b6a1a1dc4ea14a7" +dependencies = [ + "adler", + "simd-adler32", +] + +[[package]] +name = "once_cell" +version = "1.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" + +[[package]] +name = "png" +version = "0.17.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f6c3c3e617595665b8ea2ff95a86066be38fb121ff920a9c0eb282abcd1da5a" +dependencies = [ + "bitflags", + "crc32fast", + "fdeflate", + "flate2", + "miniz_oxide", +] + +[[package]] +name = "proc-macro2" +version = "1.0.78" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2422ad645d89c99f8f3e6b88a9fdeca7fabeac836b1002371c4367c8f984aae" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quake-util" +version = "0.3.0" +dependencies = [ + "benchmarking", + "hashbrown", + "png", +] + +[[package]] +name = "quote" +version = "1.0.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "291ec9ab5efd934aaf503a6466c5d5251535d108ee747472c3977cc5acc868ef" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "simd-adler32" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d66dc143e6b11c1eddc06d5c423cfc97062865baf299914ab64caa38182078fe" + +[[package]] +name = "syn" +version = "2.0.48" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f3531638e407dfc0814761abb7c00a5b54992b849452a0646b7f65c9f770f3f" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "unicode-ident" +version = "1.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" + +[[package]] +name = "version_check" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" + +[[package]] +name = "zerocopy" +version = "0.7.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "74d4d3961e53fa4c9a25a8637fc2bfaf2595b3d3ae34875568a5cf64787716be" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.7.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ce1b18ccd8e73a9321186f97e46f9f04b778851177567b1975109d26a08d2a6" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] diff --git a/Cargo.toml b/Cargo.toml index a2b0f42..bf9e24a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "quake-util" -version = "0.2.0" +version = "0.3.0" authors = ["Seth "] edition = "2021" description = "A utility library for using Quake file formats" @@ -13,7 +13,8 @@ alloc_fills = ["hashbrown"] std = [] [dependencies] -hashbrown = { version = "^0.12", optional = true } +hashbrown = { version = "^0.14", optional = true } [dev-dependencies] benchmarking = "^0.4" +png = "^0.17" diff --git a/examples/bench.rs b/examples/bench.rs index 3d6ee3d..4c24042 100644 --- a/examples/bench.rs +++ b/examples/bench.rs @@ -15,8 +15,8 @@ mod main { fn parse_file(file_path: &str) { let f = File::open(file_path).unwrap(); - let reader = BufReader::new(f); - let _ = parse(reader).unwrap(); + let mut reader = BufReader::new(f); + let _ = parse(&mut reader).unwrap(); } fn measure_parse(path: &str) -> Duration { @@ -33,7 +33,7 @@ mod main { } pub fn run_benches() { - let map_names = vec!["ad_heresp2.map", "standard.map"]; + let map_names = ["ad_heresp2.map", "standard.map"]; let maps = map_names .iter() .map(|&m| (m, prepare_file(m).unwrap())) diff --git a/examples/bench_util/mod.rs b/examples/bench_util/mod.rs index 1d3f6b9..87c7daf 100644 --- a/examples/bench_util/mod.rs +++ b/examples/bench_util/mod.rs @@ -50,7 +50,7 @@ pub fn download_file(filename: &str, out_path: &str) -> Result<(), String> { pub fn prepare_file(filename: &str) -> Result { let hash_path = format!("{LARGE_FILE_HASH_PATH}/{filename}.{HASH_SUFFIX}"); let file_path = format!("{LARGE_FILE_PATH}/{filename}"); - let expected_hash = strip_hash(read_to_string(&hash_path).as_deref())?; + let expected_hash = strip_hash(read_to_string(hash_path).as_deref())?; let file_res = File::open(&file_path); let _ = create_dir_all(LARGE_FILE_PATH); diff --git a/examples/print_wad_entries.rs b/examples/print_wad_entries.rs new file mode 100644 index 0000000..d5ebbb3 --- /dev/null +++ b/examples/print_wad_entries.rs @@ -0,0 +1,67 @@ +#[cfg(feature = "std")] +use quake_util::{lump, wad}; + +#[cfg(feature = "std")] +use std::{env::args, fs::File, io::BufReader}; + +#[cfg(feature = "std")] +fn main() { + let mut arguments = args(); + + let arg1 = if let Some(arg1) = arguments.nth(1) { + arg1 + } else { + panic!("No arguments"); + }; + + let file = File::open(arg1).expect("Could not open file"); + let mut cursor = BufReader::new(file); + + let (mut parser, warnings) = wad::Parser::new(&mut cursor).unwrap(); + + for warning in warnings { + eprintln!("Warning: {warning}"); + } + + for (name, entry) in parser.directory() { + print!("Entry `{}`: ", name); + + match &parser + .parse_inferred(&entry) + .map_err(|e| format!("{}: {}", name, e)) + .unwrap() + { + lump::Lump::MipTexture(tex) => { + println!("Texture"); + for image in tex.mips() { + println!( + "\t{}x{}: {} bytes", + image.width(), + image.height(), + image.pixels().len() + ); + } + } + lump::Lump::Palette(_) => { + println!("Palette"); + println!("\t768 bytes"); + } + lump::Lump::StatusBar(img) => { + println!("Status bar image"); + println!( + "\t{}x{}: {} bytes", + img.width(), + img.height(), + img.pixels().len(), + ); + } + lump::Lump::Flat(bytes) => { + println!("Flat"); + println!("\t{} bytes", bytes.len()); + } + } + } +} + +#[cfg(not(feature = "std"))] +fn main() {} diff --git a/examples/wadump.rs b/examples/wadump.rs new file mode 100644 index 0000000..a0cd737 --- /dev/null +++ b/examples/wadump.rs @@ -0,0 +1,126 @@ +#[cfg(feature = "std")] +mod main { + use lump::Lump; + use png::{ColorType, Encoder}; + use quake_util::{lump, wad, Palette, QUAKE_PALETTE}; + use std::{ + env::args, + fs::{create_dir_all, File}, + io::{BufReader, BufWriter, Write}, + path::PathBuf, + }; + + pub fn dump_wad() { + let mut arguments = args(); + + let wad_path = if let Some(wad_path) = arguments.nth(1) { + wad_path + } else { + panic!("No arguments"); + }; + + let file = File::open(wad_path).expect("Could not open file"); + let mut reader = BufReader::new(file); + + let (mut parser, warnings) = wad::Parser::new(&mut reader).unwrap(); + + for warning in warnings { + eprintln!("Warning: {warning}"); + } + + for (name, entry) in parser.directory() { + let lump = parser + .parse_inferred(&entry) + .map_err(|e| format!("`{}`: {}", name, e)) + .unwrap(); + + match lump { + Lump::MipTexture(tex) => { + println!("Writing texture..."); + for (idx, image) in tex.mips().iter().enumerate() { + write_png( + &format!("{}.{}", &name, idx,), + image.width(), + image.pixels(), + ); + } + } + Lump::Palette(bytes) => { + println!("Writing palette..."); + write_palette(&name, &bytes); + } + Lump::StatusBar(img) => { + println!("Writing image..."); + write_png(&name, img.width(), img.pixels()); + } + Lump::Flat(bytes) => { + let dimensions = if &name == "CONCHARS" { + Some((128u32, 128u32)) + } else if &name == "CONBACK" { + Some((320u32, 200u32)) + } else { + eprintln!("Unknown lump \"{}\"", &name); + None + }; + + if let Some((width, height)) = dimensions { + if bytes.len() as u32 == width * height { + println!("Writing {} image...", name); + write_png(&name, width, &bytes); + } else { + eprintln!("Bad dimensions for \"{}\"", &name); + } + } + } + } + } + } + + fn new_writer(file_name: &str) -> impl Write { + let mut path = PathBuf::from("dump"); + create_dir_all(&path).unwrap(); + path.push(file_name); + let file = File::create(path).unwrap(); + BufWriter::new(file) + } + + fn write_png(name: &str, width: u32, pixels: &[u8]) { + let height = pixels.len() as u32 / width; + let writer = new_writer(&format!("{}.png", name)); + let mut encoder = Encoder::new(writer, width, height); + encoder.set_color(ColorType::Rgb); + let mut writer = encoder.write_header().unwrap(); + let colors = pixels_to_colors(pixels); + writer + .write_image_data( + &colors.iter().flatten().copied().collect::>(), + ) + .unwrap(); + } + + fn write_palette(name: &str, bytes: &Palette) { + let mut writer = new_writer(&format!("{}.lmp", name)); + writer + .write_all(&bytes.iter().flatten().copied().collect::>()) + .unwrap(); + } + + fn pixels_to_colors(pixels: &[u8]) -> Box<[[u8; 3]]> { + let ct = pixels.len(); + let mut colors = Box::<[[u8; 3]]>::from(vec![[0u8; 3]; ct]); + + for (idx, pixel) in pixels.iter().copied().enumerate() { + colors[idx] = QUAKE_PALETTE[usize::from(pixel)]; + } + + colors + } +} + +#[cfg(feature = "std")] +fn main() { + main::dump_wad(); +} + +#[cfg(not(feature = "std"))] +fn main() {} diff --git a/src/common/ext_traits.rs b/src/common/ext_traits.rs new file mode 100644 index 0000000..ab959fd --- /dev/null +++ b/src/common/ext_traits.rs @@ -0,0 +1,17 @@ +use core::cell::Cell; + +pub trait CellOptionExt { + fn steal(&self) -> T; + + fn into_unwrapped(self) -> T; +} + +impl CellOptionExt for Cell> { + fn steal(&self) -> T { + self.take().expect("Empty cell option") + } + + fn into_unwrapped(self) -> T { + self.into_inner().expect("Empty cell option") + } +} diff --git a/src/common/mod.rs b/src/common/mod.rs new file mode 100644 index 0000000..61331d4 --- /dev/null +++ b/src/common/mod.rs @@ -0,0 +1,59 @@ +use std::ffi::CString; + +mod ext_traits; + +pub use ext_traits::CellOptionExt; + +pub type Palette = [[u8; 3]; 256]; + +pub const QUAKE_PALETTE: Palette = include_palette(); + +const fn include_palette() -> Palette { + let bytes = *include_bytes!("palette.lmp"); + assert!(bytes.len() == std::mem::size_of::()); + unsafe { std::mem::transmute(bytes) } +} + +#[derive(Clone, Copy)] +pub struct Junk { + _value: T, +} + +impl PartialEq> for Junk { + fn eq(&self, _: &Junk) -> bool { + true + } +} + +impl Eq for Junk {} + +impl Default for Junk { + fn default() -> Self { + Self { + _value: T::default(), + } + } +} + +impl core::fmt::Debug for Junk { + fn fmt( + &self, + _formatter: &mut core::fmt::Formatter, + ) -> Result<(), core::fmt::Error> { + Ok(()) + } +} + +pub fn slice_to_cstring(slice: &[u8]) -> std::ffi::CString { + let mut len = 0; + + while len < slice.len() { + if slice[len] == 0u8 { + break; + } + + len += 1; + } + + CString::new(&slice[..len]).unwrap() +} diff --git a/src/common/palette.lmp b/src/common/palette.lmp new file mode 100644 index 0000000..7eefda1 Binary files /dev/null and b/src/common/palette.lmp differ diff --git a/src/error/mod.rs b/src/error/mod.rs new file mode 100644 index 0000000..c5b12d1 --- /dev/null +++ b/src/error/mod.rs @@ -0,0 +1,101 @@ +extern crate std; + +use std::{error, fmt, io, num::NonZeroU64, string::String}; + +#[derive(Debug)] +pub enum BinParse { + Io(io::Error), + Parse(String), +} + +impl From for BinParse { + fn from(err: io::Error) -> BinParse { + BinParse::Io(err) + } +} + +impl fmt::Display for BinParse { + fn fmt(&self, fmt: &mut fmt::Formatter) -> Result<(), fmt::Error> { + match self { + Self::Io(e) => { + fmt.write_fmt(format_args!("IO Error: {e}"))?; + } + Self::Parse(s) => { + fmt.write_fmt(format_args!("Binary Parse Error: {s}"))?; + } + } + + Ok(()) + } +} + +impl std::error::Error for BinParse {} + +pub type BinParseResult = Result; + +#[derive(Debug, Clone)] +pub struct Line { + pub message: String, + pub line_number: Option, +} + +impl fmt::Display for Line { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self.line_number { + Some(ln) => write!(f, "Line {}: {}", ln, self.message), + None => write!(f, "{}", self.message), + } + } +} + +impl error::Error for Line {} + +#[derive(Debug)] +pub enum TextParse { + Io(io::Error), + Lexer(Line), + Parser(Line), +} + +impl TextParse { + pub fn from_lexer(message: String, line_number: NonZeroU64) -> TextParse { + TextParse::Lexer(Line { + message, + line_number: Some(line_number), + }) + } + + pub fn from_parser(message: String, line_number: NonZeroU64) -> TextParse { + TextParse::Parser(Line { + message, + line_number: Some(line_number), + }) + } + + pub fn eof() -> TextParse { + TextParse::Parser(Line { + message: String::from("Unexpected end-of-file"), + line_number: None, + }) + } +} + +impl From for TextParse { + fn from(err: io::Error) -> TextParse { + TextParse::Io(err) + } +} + +impl fmt::Display for TextParse { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self { + TextParse::Io(msg) => write!(f, "{}", msg), + TextParse::Lexer(err) => write!(f, "{}", err), + TextParse::Parser(err) => write!(f, "{}", err), + } + } +} + +impl std::error::Error for TextParse {} + +pub type TextParseResult = std::result::Result; diff --git a/src/lib.rs b/src/lib.rs index de709f4..d8a40ac 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,5 +1,4 @@ #![no_std] -#![cfg_attr(feature = "std", feature(io_error_other))] #[cfg(all(not(feature = "std"), not(feature = "alloc_fills")))] compile_error!("Must use feature 'std' or 'alloc_fills'"); @@ -14,4 +13,22 @@ extern crate std; #[cfg(not(feature = "std"))] extern crate alloc; +#[cfg(feature = "std")] +mod common; + +#[cfg(feature = "std")] +pub use common::{Palette, QUAKE_PALETTE}; + +#[cfg(feature = "std")] +use common::slice_to_cstring; + +#[cfg(feature = "std")] +pub mod error; + +#[cfg(feature = "std")] +pub mod lump; + +#[cfg(feature = "std")] +pub mod wad; + pub mod qmap; diff --git a/src/lump/mod.rs b/src/lump/mod.rs new file mode 100644 index 0000000..2f901d7 --- /dev/null +++ b/src/lump/mod.rs @@ -0,0 +1,29 @@ +//! Data lumps as used in WAD archive or as loose files + +mod parse; +mod repr; + +pub use parse::{parse_image, parse_mip_texture, parse_palette, read_raw}; + +pub use repr::{Image, Lump, MipTexture, MipTextureHead}; + +/// Lump identifiers +pub mod kind { + /// 768 byte (256 packed colors) palette lump + pub const PALETTE: u8 = 0x40; + + /// 2D image lump + pub const SBAR: u8 = 0x42; + + /// Mip-mapped texture lump + pub const MIPTEX: u8 = 0x44; + + /// Raw (headerless) bytes lump + pub const FLAT: u8 = 0x45; +} + +#[cfg(test)] +mod parse_test; + +#[cfg(test)] +mod repr_test; diff --git a/src/lump/parse.rs b/src/lump/parse.rs new file mode 100644 index 0000000..5016dfa --- /dev/null +++ b/src/lump/parse.rs @@ -0,0 +1,78 @@ +use crate::common::Palette; +use crate::error; +use crate::lump::{Image, MipTexture, MipTextureHead}; +use error::BinParseResult; +use std::boxed::Box; +use std::io::{Read, Seek, SeekFrom}; +use std::mem::{size_of, transmute, MaybeUninit}; +use std::string::ToString; + +/// Attempt to parse bytes into a mip-mapped texture +pub fn parse_mip_texture( + cursor: &mut (impl Seek + Read), +) -> BinParseResult { + let mut head_bytes = [0u8; size_of::()]; + let lump_start = cursor.stream_position()?; + + cursor.read_exact(&mut head_bytes)?; + + let head: MipTextureHead = head_bytes.try_into()?; + let mip0_length = u64::from(head.width) * u64::from(head.height); + const UNINIT_IMAGE: MaybeUninit = MaybeUninit::uninit(); + let mut mips = [UNINIT_IMAGE; 4]; + + for i in 0u32..4u32 { + let pix_start: u64 = head.offsets[i as usize].into(); + let length: usize = (mip0_length >> (i * 2)).try_into().unwrap(); + + cursor.seek(SeekFrom::Start( + lump_start + .checked_add(pix_start) + .ok_or(error::BinParse::Parse("Bad offset".to_string()))?, + ))?; + + let mut pixels = vec![0u8; length].into_boxed_slice(); + cursor.read_exact(&mut pixels)?; + + mips[i as usize].write(Image::from_pixels(head.width >> i, pixels)); + } + + Ok(MipTexture::from_parts(head.name, unsafe { + mips.map(|elem| elem.assume_init()) + })) +} + +/// Attempt to parse 768 bytes into a palette +pub fn parse_palette(reader: &mut impl Read) -> BinParseResult> { + let mut bytes = [0u8; size_of::()]; + reader.read_exact(&mut bytes[..])?; + Ok(Box::from(unsafe { transmute::<_, Palette>(bytes) })) +} + +/// Attempt to parse a 2D image +pub fn parse_image(reader: &mut impl Read) -> BinParseResult { + let mut u32_buf = [0u8; size_of::()]; + reader.read_exact(&mut u32_buf[..])?; + let width = u32::from_le_bytes(u32_buf); + reader.read_exact(&mut u32_buf[..])?; + let height = u32::from_le_bytes(u32_buf); + + let pixel_ct = width + .checked_mul(height) + .ok_or(error::BinParse::Parse("Image too large".to_string()))?; + + let mut pixels = vec![0u8; pixel_ct as usize].into_boxed_slice(); + reader.read_exact(&mut pixels)?; + + Ok(Image::from_pixels(width, pixels)) +} + +/// Read `length` bytes into a boxed slice +pub fn read_raw( + reader: &mut impl Read, + length: usize, +) -> BinParseResult> { + let mut bytes = vec![0u8; length].into_boxed_slice(); + reader.read_exact(&mut bytes)?; + Ok(bytes) +} diff --git a/src/lump/parse_test.rs b/src/lump/parse_test.rs new file mode 100644 index 0000000..4828a74 --- /dev/null +++ b/src/lump/parse_test.rs @@ -0,0 +1,123 @@ +use super::{ + parse_image, parse_mip_texture, parse_palette, read_raw, MipTextureHead, +}; +use crate::error; +use std::io::Cursor; +use std::mem::size_of; +use std::vec::Vec; + +fn good_miptex_bytes() -> Vec { + let mut bytes = Vec::new(); + let mut offset: u32 = size_of::().try_into().unwrap(); + bytes.extend(b"namenamenamenam\0"); + bytes.extend((16_u32).to_le_bytes()); + bytes.extend((16_u32).to_le_bytes()); + bytes.extend((offset).to_le_bytes()); + offset += 256; + bytes.extend((offset).to_le_bytes()); + offset += 64; + bytes.extend((offset).to_le_bytes()); + offset += 16; + bytes.extend((offset).to_le_bytes()); + + bytes.extend([0u8; 256]); + bytes.extend([0u8; 64]); + bytes.extend([0u8; 16]); + bytes.extend([0u8; 4]); + + bytes +} + +#[test] +fn parse_good_mip_texture() { + let bytes = good_miptex_bytes(); + let mut cursor = Cursor::new(bytes); + let miptex = parse_mip_texture(&mut cursor).unwrap(); + assert_eq!(miptex.mip(0).width(), 16); + assert_eq!(miptex.mip(0).height(), 16); +} + +#[test] +fn parse_mip_texture_with_bad_head() { + let mut bytes = good_miptex_bytes(); + bytes[16..20].copy_from_slice(&(13u32).to_le_bytes()); + let mut cursor = Cursor::new(bytes); + let e = parse_mip_texture(&mut cursor).unwrap_err(); + assert!(matches!(e, error::BinParse::Parse(_))); +} + +#[test] +fn parse_mip_texture_short_head() { + let bytes = &good_miptex_bytes()[..27]; + let mut cursor = Cursor::new(bytes); + let e = parse_mip_texture(&mut cursor).unwrap_err(); + assert!(matches!(e, error::BinParse::Io(_))); +} + +#[test] +fn parse_mip_texture_with_bad_offset() { + let mut bytes = good_miptex_bytes(); + bytes[24..28].copy_from_slice(&(360u32).to_le_bytes()); + let mut cursor = Cursor::new(bytes); + let e = parse_mip_texture(&mut cursor).unwrap_err(); + assert!(matches!(e, error::BinParse::Io(_))); +} + +#[test] +fn parse_good_palette() { + let bytes = [0u8; 768]; + let pal = parse_palette(&mut &bytes[..]).unwrap(); + assert_eq!(pal.len(), 256); +} + +#[test] +fn parse_bad_palette() { + let bytes = [0u8; 3]; + let e = parse_palette(&mut &bytes[..]).unwrap_err(); + assert!(matches!(e, error::BinParse::Io(_))); +} + +#[test] +fn parse_good_image() { + let mut bytes = Vec::::new(); + bytes.extend((48u32).to_le_bytes()); + bytes.extend((32u32).to_le_bytes()); + bytes.extend([0u8; 32 * 48]); + let image = parse_image(&mut &bytes[..]).unwrap(); + assert_eq!(image.width(), 48); + assert_eq!(image.height(), 32); + assert_eq!(image.pixels().len(), 48 * 32); +} + +#[test] +fn parse_image_too_large() { + let mut bytes = Vec::::new(); + bytes.extend((65_536_u32).to_le_bytes()); + bytes.extend((65_536_u32).to_le_bytes()); + let e = parse_image(&mut &bytes[..]).unwrap_err(); + assert!(matches!(e, error::BinParse::Parse(_))); +} + +#[test] +fn parse_image_cutoff() { + let mut bytes = Vec::::new(); + bytes.extend((256u32).to_le_bytes()); + bytes.extend((256u32).to_le_bytes()); + bytes.extend((65_535_u32).to_le_bytes()); + let e = parse_image(&mut &bytes[..]).unwrap_err(); + assert!(matches!(e, error::BinParse::Io(_))); +} + +#[test] +fn read_raw_success() { + let bytes = [0u8; 123]; + let flat = read_raw(&mut &bytes[..], 123).unwrap(); + assert_eq!(flat.len(), 123); +} + +#[test] +fn read_raw_io_error() { + let bytes = [0u8; 123]; + let e = read_raw(&mut &bytes[..], 209).unwrap_err(); + assert!(matches!(e, error::BinParse::Io(_))); +} diff --git a/src/lump/repr.rs b/src/lump/repr.rs new file mode 100644 index 0000000..f3da456 --- /dev/null +++ b/src/lump/repr.rs @@ -0,0 +1,248 @@ +use crate::error; +use crate::lump::kind; +use crate::slice_to_cstring; +use crate::Palette; +use std::boxed::Box; +use std::ffi::{CString, IntoStringError}; +use std::mem::size_of; +use std::string::{String, ToString}; + +/// Enum w/ variants for each known lump kind +#[derive(Clone, PartialEq, Eq, Debug)] +pub enum Lump { + Palette(Box), + StatusBar(Image), + MipTexture(MipTexture), + Flat(Box<[u8]>), +} + +impl Lump { + /// Single-byte lump kind identifier as used in WAD entries + pub fn kind(&self) -> u8 { + match self { + Self::Palette(_) => kind::PALETTE, + Self::StatusBar(_) => kind::SBAR, + Self::MipTexture(_) => kind::MIPTEX, + _ => kind::FLAT, + } + } +} + +/// Image stored as palette indices (0..256) in row-major order +#[derive(Clone, PartialEq, Eq, Debug)] +pub struct Image { + width: u32, + height: u32, + pixels: Box<[u8]>, +} + +impl Image { + /// Create an image from a width and list of pixels. Height is calculated + /// from number of pixels and width, or 0 if pixels are empty. + /// + /// # Panics + /// + /// Panics if pixel count does not fit within a `u32`, pixels cannot fit + /// within an integer number of row, or there are a non-zero number of + /// pixels and width is 0. + pub fn from_pixels(width: u32, pixels: Box<[u8]>) -> Self { + let pixel_ct: u32 = pixels.len().try_into().expect("Too many pixels"); + + if pixels.len() == 0 { + return Image { + width: 0, + height: 0, + pixels, + }; + } + + if width == 0 { + panic!("Image with pixels must have width > 0"); + } + + if pixel_ct % width != 0 { + panic!("Incomplete pixel row"); + } + + Image { + width, + height: pixel_ct / width, + pixels, + } + } + + pub fn width(&self) -> u32 { + self.width + } + + pub fn height(&self) -> u32 { + self.height + } + + /// Slice of all the pixels + pub fn pixels(&self) -> &[u8] { + &self.pixels[..] + } +} + +/// Mip-mapped texture. Contains exactly 4 mips (including the full resolution +/// image). +/// +/// Textures mips are guaranteed to be valid, meaning that width and height of +/// a mip is half that of the width and height of the previous mip. +#[derive(Clone, PartialEq, Eq, Debug)] +pub struct MipTexture { + name: [u8; 16], + mips: [Image; 4], +} + +impl MipTexture { + pub const MIP_COUNT: usize = 4; + + /// Assemble a texture from provided mips with the given name. Useful if + /// name is already given by a WAD entry as a block of 16 bytes. + /// + /// # Panic + /// + /// Will panic if mips are not valid. + pub fn from_parts(name: [u8; 16], mips: [Image; Self::MIP_COUNT]) -> Self { + Self::validate_mips(&mips); + MipTexture { name, mips } + } + + /// Assemble a texture from provided mips with `name` converted to a block + /// of 16 bytes. + /// + /// @ Panic + /// + /// Will panic if mips are not valid or `name` does not fit within 16 bytes. + pub fn new(name: String, mips: [Image; Self::MIP_COUNT]) -> Self { + let mut name_field = [0u8; 16]; + let name_bytes = &name.into_bytes(); + name_field[..name_bytes.len()].copy_from_slice(name_bytes); + let name = name_field; + Self::validate_mips(&mips); + + MipTexture { name, mips } + } + + fn validate_mips(mips: &[Image; Self::MIP_COUNT]) { + for l in 0..(Self::MIP_COUNT - 1) { + let r = l + 1; + + if Some(mips[l].width) != mips[r].width.checked_mul(2) { + panic!("Bad mipmaps"); + } + + if Some(mips[l].height) != mips[r].height.checked_mul(2) { + panic!("Bad mipmaps"); + } + } + } + + /// Obtain the name as a C string. If the name is not already + /// null-terminated (in which case the entry is not well-formed) a null byte + /// is appended to make a valid C string. + pub fn name_to_cstring(&self) -> CString { + slice_to_cstring(&self.name) + } + + /// Attempt to interpret the name as UTF-8 encoded string + pub fn name_to_string(&self) -> Result { + self.name_to_cstring().into_string() + } + + pub fn name(&self) -> [u8; 16] { + self.name + } + + /// Get the texture mip as an image at the specified index. + /// + /// # Panic + /// + /// Panics if index is > 3 + pub fn mip(&self, index: usize) -> &Image { + if index < Self::MIP_COUNT { + &self.mips[index] + } else { + panic!("Outside mip bounds ([0..{}])", Self::MIP_COUNT); + } + } + + /// Get the texture mips as a slice of images + pub fn mips(&self) -> &[Image] { + &self.mips[..] + } +} + +/// Lump header for mip-mapped textures +#[derive(Clone, Copy, PartialEq, Eq, Debug)] +#[repr(C, packed)] +pub struct MipTextureHead { + pub(crate) name: [u8; 16], + pub(crate) width: u32, + pub(crate) height: u32, + pub(crate) offsets: [u32; 4], +} + +impl TryFrom<[u8; size_of::()]> for MipTextureHead { + type Error = error::BinParse; + + /// Obtain header from a block of bytes as found in a miptex WAD lump. + /// + /// # Panic + /// + /// Will panic if width or height are not each divisible by 8, in which case + /// valid mips cannot be generated. Will panic if number of pixels in mip + /// 0 cannot fit within a `u32`. + fn try_from( + bytes: [u8; size_of::()], + ) -> Result { + let name = <[u8; 16]>::try_from(&bytes[..16]).unwrap(); + + let bytes = &bytes[16..]; + + let width = + u32::from_le_bytes(<[u8; 4]>::try_from(&bytes[..4]).unwrap()); + + let bytes = &bytes[4..]; + + let height = + u32::from_le_bytes(<[u8; 4]>::try_from(&bytes[..4]).unwrap()); + + if width % 8 != 0 { + return Err(error::BinParse::Parse(format!( + "Invalid width {}", + width + ))); + } + + if height % 8 != 0 { + return Err(error::BinParse::Parse(format!( + "Invalid height {}", + height + ))); + } + + width + .checked_mul(height) + .ok_or(error::BinParse::Parse("Texture too large".to_string()))?; + + let bytes = &bytes[4..]; + + let mut offsets = [0u32; 4]; + + for i in 0..4 { + offsets[i] = u32::from_le_bytes( + <[u8; 4]>::try_from(&bytes[(4 * i)..(4 * i + 4)]).unwrap(), + ); + } + + Ok(MipTextureHead { + name, + width, + height, + offsets, + }) + } +} diff --git a/src/lump/repr_test.rs b/src/lump/repr_test.rs new file mode 100644 index 0000000..e6a0270 --- /dev/null +++ b/src/lump/repr_test.rs @@ -0,0 +1,207 @@ +use super::kind; +use super::{Image, Lump, MipTexture, MipTextureHead}; +use crate::error; +use std::ffi::CString; +use std::mem::size_of; +use std::string::String; + +use std::boxed::Box; + +#[test] +fn lump_kind() { + assert_eq!(Lump::Palette(Box::new([[0; 3]; 256])).kind(), kind::PALETTE); + assert_eq!( + Lump::StatusBar(Image::from_pixels(0, Box::new([]))).kind(), + kind::SBAR + ); + assert_eq!( + Lump::MipTexture(MipTexture::from_parts( + [0u8; 16], + [ + Image::from_pixels(16, Box::new([0u8; 256])), + Image::from_pixels(8, Box::new([0u8; 64])), + Image::from_pixels(4, Box::new([0u8; 16])), + Image::from_pixels(2, Box::new([0u8; 4])), + ] + )) + .kind(), + kind::MIPTEX + ); + assert_eq!(Lump::Flat(Box::new([1, 2, 3])).kind(), kind::FLAT); +} + +#[test] +fn image_from_pixels() { + let image = Image::from_pixels(40, Box::new([0u8; 1280])); + assert_eq!(image.width(), 40); + assert_eq!(image.height(), 32); + assert_eq!(image.pixels().len(), 1280); +} + +#[test] +#[should_panic] +fn zero_width_image() { + Image::from_pixels(0, Box::new([0u8; 1])); +} + +#[test] +#[should_panic] +fn malformed_image() { + Image::from_pixels(128, Box::new([0u8; 666])); +} + +#[test] +#[should_panic] +fn bad_mip_width() { + MipTexture::from_parts( + [0u8; 16], + [ + Image::from_pixels(16, Box::new([0u8; 256])), + Image::from_pixels(8, Box::new([0u8; 64])), + Image::from_pixels(4, Box::new([0u8; 16])), + Image::from_pixels(8, Box::new([0u8; 16])), + ], + ); +} + +#[test] +#[should_panic] +fn bad_mip_height() { + MipTexture::from_parts( + [0u8; 16], + [ + Image::from_pixels(16, Box::new([0u8; 256])), + Image::from_pixels(8, Box::new([0u8; 64])), + Image::from_pixels(4, Box::new([0u8; 16])), + Image::from_pixels(2, Box::new([0u8; 2])), + ], + ); +} + +#[test] +fn access_mip_levels() { + let images = [ + Image::from_pixels(256, Box::new([0u8; 32768])), + Image::from_pixels(128, Box::new([0u8; 8192])), + Image::from_pixels(64, Box::new([0u8; 2048])), + Image::from_pixels(32, Box::new([0u8; 512])), + ]; + let miptex = MipTexture::from_parts([0u8; 16], images.clone()); + + for i in 0..4 { + assert_eq!(miptex.mip(i), &images[i]); + } +} + +#[test] +#[should_panic] +fn access_bad_mip_level() { + let miptex = MipTexture::from_parts( + [0u8; 16], + [ + Image::from_pixels(128, Box::new([0u8; 32768])), + Image::from_pixels(64, Box::new([0u8; 8192])), + Image::from_pixels(32, Box::new([0u8; 2048])), + Image::from_pixels(16, Box::new([0u8; 512])), + ], + ); + miptex.mip(4); +} + +#[test] +fn miptex_mips() { + let images = [ + Image::from_pixels(384, Box::new([0u8; 3072 * 16])), + Image::from_pixels(192, Box::new([0u8; 768 * 16])), + Image::from_pixels(96, Box::new([0u8; 192 * 16])), + Image::from_pixels(48, Box::new([0u8; 48 * 16])), + ]; + let miptex = MipTexture::from_parts([0u8; 16], images.clone()); + + for (idx, mip) in miptex.mips().iter().enumerate() { + assert_eq!(mip, &images[idx]); + } +} + +const NAME: [u8; 16] = *b"SomeOldNameGame\0"; + +fn good_miptex_head_bytes() -> [u8; size_of::()] { + let mut bytes = [0u8; size_of::()]; + bytes[..16].copy_from_slice(&NAME); + bytes[16..20].copy_from_slice(&(384_u32).to_le_bytes()); + bytes[20..24].copy_from_slice(&(192_u32).to_le_bytes()); + bytes[24..28].copy_from_slice(&(1_111_u32).to_le_bytes()); + bytes[28..32].copy_from_slice(&(2_222_u32).to_le_bytes()); + bytes[32..36].copy_from_slice(&(3_333_u32).to_le_bytes()); + bytes[36..].copy_from_slice(&(4_444_u32).to_le_bytes()); + bytes +} + +#[test] +fn miptex_head_from_bytes() { + let bytes = good_miptex_head_bytes(); + let head: MipTextureHead = bytes.try_into().unwrap(); + let name = head.name; + let width = head.width; + let height = head.height; + let offsets = head.offsets; + + assert_eq!(name, NAME); + assert_eq!(width, 384); + assert_eq!(height, 192); + assert_eq!(offsets, [1_111, 2_222, 3_333, 4_444]); +} + +#[test] +fn miptex_head_bad_width() { + let mut bytes = good_miptex_head_bytes(); + bytes[16..20].copy_from_slice(&(69_u32).to_le_bytes()); + let e = ::try_from(bytes).unwrap_err(); + assert!(matches!(e, error::BinParse::Parse(_))); +} + +#[test] +fn miptex_head_bad_height() { + let mut bytes = good_miptex_head_bytes(); + bytes[20..24].copy_from_slice(&(60_u32).to_le_bytes()); + let e = ::try_from(bytes).unwrap_err(); + assert!(matches!(e, error::BinParse::Parse(_))); +} + +#[test] +fn miptex_head_too_large() { + let mut bytes = good_miptex_head_bytes(); + bytes[16..20].copy_from_slice(&(65_536_u32).to_le_bytes()); + bytes[20..24].copy_from_slice(&(65_536_u32).to_le_bytes()); + let e = ::try_from(bytes).unwrap_err(); + assert!(matches!(e, error::BinParse::Parse(_))); +} + +fn good_mips() -> [Image; 4] { + [ + Image::from_pixels(16, Box::new([0u8; 256])), + Image::from_pixels(8, Box::new([0u8; 64])), + Image::from_pixels(4, Box::new([0u8; 16])), + Image::from_pixels(2, Box::new([0u8; 4])), + ] +} + +#[test] +fn miptex_new_short_name() { + let miptex = MipTexture::new(String::from("hi"), good_mips()); + assert_eq!(miptex.name(), *b"hi\0\0\0\0\0\0\0\0\0\0\0\0\0\0"); + assert_eq!(miptex.name_to_string().unwrap(), String::from("hi")); + assert_eq!(miptex.name_to_cstring(), CString::new("hi").unwrap()); +} + +#[test] +fn miptex_new_long_name() { + let miptex = MipTexture::new(String::from("in_16_characters"), good_mips()); + assert_eq!(miptex.name(), *b"in_16_characters"); +} + +#[test] +#[should_panic] +fn miptex_name_too_long() { + MipTexture::new(String::from("this_string_is_too_long"), good_mips()); +} diff --git a/src/qmap/lexer.rs b/src/qmap/lexer.rs index ad698ed..fa06cdc 100644 --- a/src/qmap/lexer.rs +++ b/src/qmap/lexer.rs @@ -2,7 +2,7 @@ extern crate std; use std::{ - cell::RefCell, + cell::{Cell, RefCell}, convert::TryInto, fmt, io, num::{NonZeroU64, NonZeroU8}, @@ -10,8 +10,9 @@ use std::{ vec::Vec, }; -use crate::qmap; -use qmap::{ParseError, ParseResult}; +use crate::error; + +pub type LexResult = Result, Cell>>; const TEXT_CAPACITY: usize = 32; @@ -46,6 +47,7 @@ impl fmt::Display for Token { } } +#[derive(Debug)] pub struct TokenIterator { text: RefCell>>, state: fn(iter: &mut TokenIterator) -> Option, @@ -67,12 +69,20 @@ impl TokenIterator { } } - fn byte_read(&mut self, b: io::Result) -> ParseResult> { - let byte = b.map_err(ParseError::from_io)?; - - self.byte = Some(byte.try_into().map_err(|_| { - ParseError::from_lexer(String::from("Null byte"), self.line_number) - })?); + fn byte_read(&mut self, b: io::Result) -> LexResult { + let byte = b.map_err(|e| Cell::new(Some(e.into())))?; + + self.byte = Some( + byte.try_into() + .map_err(|_| { + error::TextParse::from_lexer( + String::from("Null byte"), + self.line_number, + ) + }) + .map_err(Some) + .map_err(Cell::new)?, + ); let maybe_token = (self.state)(self); @@ -90,16 +100,16 @@ impl TokenIterator { Ok(maybe_token) } - fn eof_read(&mut self) -> ParseResult> { + fn eof_read(&mut self) -> LexResult { if let Some(last_text) = self.text.replace(None) { if last_text[0] == NonZeroU8::new(b'"').unwrap() && (last_text.last() != NonZeroU8::new(b'"').as_ref() || last_text.len() == 1) { - Err(ParseError::from_lexer( + Err(Cell::new(Some(error::TextParse::from_lexer( String::from("Missing closing quote"), self.line_number, - )) + )))) } else { Ok(Some(Token { text: last_text, @@ -113,9 +123,9 @@ impl TokenIterator { } impl Iterator for TokenIterator { - type Item = ParseResult; + type Item = Result>>; - fn next(&mut self) -> Option> { + fn next(&mut self) -> Option { loop { if let Some(b) = self.input.next() { if let token @ Some(_) = self.byte_read(b).transpose() { diff --git a/src/qmap/lexer_test.rs b/src/qmap/lexer_test.rs index 3a7714c..c853e20 100644 --- a/src/qmap/lexer_test.rs +++ b/src/qmap/lexer_test.rs @@ -1,8 +1,9 @@ -use crate::qmap; +use crate::{common, error, qmap}; +use common::CellOptionExt; use qmap::lexer::{Token, TokenIterator}; -use qmap::ParseError; use std::io; use std::num::NonZeroU8; +use std::string::ToString; use std::vec::Vec; fn bytes_to_token_text(bytes: &[u8]) -> Vec { @@ -30,7 +31,7 @@ fn lex_all_symbols() { let input = b" // a comment \n { } x \"\"\t\" \"\n-1.23e4 {k \r\n q\""; let iter = TokenIterator::new(&input[..]); - let expected = vec![ + let expected = [ (&b"{"[..], 2u64), (&b"}"[..], 2u64), (&b"x"[..], 2u64), @@ -47,7 +48,7 @@ fn lex_all_symbols() { }); iter.zip(expected_iter).for_each(|(actual, expected)| { - assert_eq!(actual.unwrap(), expected); + assert_eq!(actual.map_err(|_| ()).unwrap(), expected); }); } @@ -59,7 +60,7 @@ fn lex_bad_quoted() { let bad_token = TokenIterator::new(&input[..]).nth(1); if let Err(qmap_error) = bad_token.unwrap() { - if let ParseError::Lexer(line_error) = qmap_error { + if let error::TextParse::Lexer(line_error) = qmap_error.steal() { assert!(line_error.message.contains("closing quote")); assert_eq!(u64::from(line_error.line_number.unwrap()), 1u64); } else { @@ -76,8 +77,8 @@ fn lex_io_error() { let bad_token = TokenIterator::new(reader).next(); if let Err(qmap_error) = bad_token.unwrap() { - if let ParseError::Io(io_error) = qmap_error { - assert!(io_error.contains("Generic test error")); + if let error::TextParse::Io(io_error) = qmap_error.steal() { + assert!(io_error.to_string().contains("Generic test error")); } else { panic!("Unexpected error type"); } diff --git a/src/qmap/mod.rs b/src/qmap/mod.rs index 6eb1d9d..64fb281 100644 --- a/src/qmap/mod.rs +++ b/src/qmap/mod.rs @@ -3,7 +3,6 @@ //! # Example //! //! ``` -//! # use quake_util::qmap; //! # use std::ffi::CString; //! # use std::io::Read; //! # @@ -11,19 +10,21 @@ //! # fn main() -> Result<(), String> { //! # #[cfg(feature="std")] //! # { -//! # let source = &b" +//! # let mut source = &b" //! # { //! # } //! # "[..]; //! # //! # let mut dest = Vec::::new(); //! # -//! use qmap::{Entity, ParseError, WriteError}; +//! use quake_util::qmap; +//! use qmap::{Entity, WriteError}; +//! use quake_util::error::TextParse as TextParseError; //! -//! let mut map = qmap::parse(source).map_err(|err| match err { -//! ParseError::Io(_) => String::from("Failed to read map"), -//! l_err@ParseError::Lexer(_) => l_err.to_string(), -//! p_err@ParseError::Parser(_) => p_err.to_string(), +//! let mut map = qmap::parse(&mut source).map_err(|err| match err { +//! TextParseError::Io(_) => String::from("Failed to read map"), +//! l_err@TextParseError::Lexer(_) => l_err.to_string(), +//! p_err@TextParseError::Parser(_) => p_err.to_string(), //! })?; //! //! let mut soldier = Entity::new(); @@ -58,9 +59,6 @@ mod lexer; #[cfg(feature = "std")] mod parser; -#[cfg(feature = "std")] -mod parse_result; - pub use repr::{ Alignment, Brush, CheckWritable, Edict, Entity, EntityKind, HalfSpace, Point, QuakeMap, Surface, ValidationResult, Vec2, Vec3, @@ -69,9 +67,6 @@ pub use repr::{ #[cfg(feature = "std")] pub use parser::parse; -#[cfg(feature = "std")] -pub use parse_result::{LineError, ParseError, ParseResult}; - #[cfg(feature = "std")] pub use repr::{WriteAttempt, WriteError}; diff --git a/src/qmap/parse_result.rs b/src/qmap/parse_result.rs deleted file mode 100644 index 26207ed..0000000 --- a/src/qmap/parse_result.rs +++ /dev/null @@ -1,72 +0,0 @@ -extern crate std; - -use std::{ - error, fmt, - num::NonZeroU64, - string::{String, ToString}, -}; - -#[derive(Debug, Clone)] -pub struct LineError { - pub message: String, - pub line_number: Option, -} - -impl fmt::Display for LineError { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - match self.line_number { - Some(ln) => write!(f, "Line {}: {}", ln, self.message), - None => write!(f, "{}", self.message), - } - } -} - -impl error::Error for LineError {} - -#[derive(Debug, Clone)] -pub enum ParseError { - Io(String), - Lexer(LineError), - Parser(LineError), -} - -impl ParseError { - pub fn from_lexer(message: String, line_number: NonZeroU64) -> ParseError { - ParseError::Lexer(LineError { - message, - line_number: Some(line_number), - }) - } - - pub fn from_parser(message: String, line_number: NonZeroU64) -> ParseError { - ParseError::Parser(LineError { - message, - line_number: Some(line_number), - }) - } - - pub fn from_io(io_error: std::io::Error) -> ParseError { - ParseError::Io(io_error.to_string()) - } - - pub fn eof() -> ParseError { - ParseError::Parser(LineError { - message: String::from("Unexpected end-of-file"), - line_number: None, - }) - } -} - -impl fmt::Display for ParseError { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - match self.clone() { - ParseError::Io(msg) => write!(f, "{}", msg), - ParseError::Lexer(err) => write!(f, "{}", err), - ParseError::Parser(err) => write!(f, "{}", err), - } - } -} - -impl error::Error for ParseError {} - -pub type ParseResult = std::result::Result; diff --git a/src/qmap/parser.rs b/src/qmap/parser.rs index 7e3ad95..cdef360 100644 --- a/src/qmap/parser.rs +++ b/src/qmap/parser.rs @@ -1,14 +1,41 @@ #[cfg(feature = "std")] extern crate std; -use std::{io::Read, iter::Peekable, num::NonZeroU8, str::FromStr, vec::Vec}; +use std::{ + cell::Cell, io::Read, iter::Peekable, num::NonZeroU8, str::FromStr, + vec::Vec, +}; -use crate::qmap; +use crate::{common, error, qmap}; +use common::CellOptionExt; use qmap::lexer::{Token, TokenIterator}; use qmap::repr::{Alignment, Brush, Edict, Entity, Point, QuakeMap, Surface}; -use qmap::ParseResult; type TokenPeekable = Peekable>; + +trait Extract { + fn extract(&mut self) -> Result, error::TextParse>; +} + +impl Extract for TokenPeekable +where + R: Read, +{ + fn extract(&mut self) -> Result, error::TextParse> { + self.next().transpose().map_err(|e| e.into_unwrapped()) + } +} + +trait Normalize { + fn normalize(&self) -> Result<&Option, error::TextParse>; +} + +impl Normalize for Result, Cell>> { + fn normalize(&self) -> Result<&Option, error::TextParse> { + self.as_ref().map_err(|e| e.steal()) + } +} + const MIN_BRUSH_SURFACES: usize = 4; /// Parses a Quake source map @@ -17,7 +44,7 @@ const MIN_BRUSH_SURFACES: usize = 4; /// `brushDef`s/`patchDef`s are not presently supported) but may have texture /// alignment in either "Valve220" format or the "legacy" predecessor (i.e. /// without texture axes) -pub fn parse(reader: R) -> ParseResult { +pub fn parse(reader: &mut R) -> error::TextParseResult { let mut entities: Vec = Vec::new(); let mut peekable_tokens = TokenIterator::new(reader).peekable(); @@ -29,26 +56,34 @@ pub fn parse(reader: R) -> ParseResult { Ok(QuakeMap { entities }) } -fn parse_entity(tokens: &mut TokenPeekable) -> ParseResult { - expect_byte(&tokens.next().transpose()?, b'{')?; +fn parse_entity( + tokens: &mut TokenPeekable, +) -> error::TextParseResult { + expect_byte(&tokens.extract()?, b'{')?; let edict = parse_edict(tokens)?; let brushes = parse_brushes(tokens)?; - expect_byte(&tokens.next().transpose()?, b'}')?; + expect_byte(&tokens.extract()?, b'}')?; Ok(Entity { edict, brushes }) } -fn parse_edict(tokens: &mut TokenPeekable) -> ParseResult { +fn parse_edict( + tokens: &mut TokenPeekable, +) -> error::TextParseResult { let mut edict = Edict::new(); while let Some(tok_res) = tokens.peek() { - if tok_res.as_ref().map_err(|e| e.clone())?.match_quoted() { - let key = strip_quoted(&tokens.next().transpose()?.unwrap().text) + if tok_res + .as_ref() + .map_err(CellOptionExt::steal)? + .match_quoted() + { + let key = strip_quoted(&tokens.extract()?.unwrap().text) .to_vec() .into(); - let maybe_value = tokens.next().transpose()?; + let maybe_value = tokens.extract()?; expect_quoted(&maybe_value)?; let value = strip_quoted(&maybe_value.unwrap().text).to_vec().into(); @@ -63,11 +98,11 @@ fn parse_edict(tokens: &mut TokenPeekable) -> ParseResult { fn parse_brushes( tokens: &mut TokenPeekable, -) -> ParseResult> { +) -> error::TextParseResult> { let mut brushes = Vec::new(); while let Some(tok_res) = tokens.peek() { - if tok_res.as_ref().map_err(|e| e.clone())?.match_byte(b'{') { + if tok_res.as_ref().map_err(|e| e.steal())?.match_byte(b'{') { brushes.push(parse_brush(tokens)?); } else { break; @@ -77,35 +112,34 @@ fn parse_brushes( Ok(brushes) } -fn parse_brush(tokens: &mut TokenPeekable) -> ParseResult { +fn parse_brush( + tokens: &mut TokenPeekable, +) -> error::TextParseResult { let mut surfaces = Vec::with_capacity(MIN_BRUSH_SURFACES); - expect_byte(&tokens.next().transpose()?, b'{')?; + expect_byte(&tokens.extract()?, b'{')?; while let Some(tok_res) = tokens.peek() { - if tok_res.as_ref().map_err(|e| e.clone())?.match_byte(b'(') { + if tok_res.as_ref().map_err(|e| e.steal())?.match_byte(b'(') { surfaces.push(parse_surface(tokens)?); } else { break; } } - expect_byte_or(&tokens.next().transpose()?, b'}', &[b'('])?; + expect_byte_or(&tokens.extract()?, b'}', &[b'('])?; Ok(surfaces) } fn parse_surface( tokens: &mut TokenPeekable, -) -> ParseResult { +) -> error::TextParseResult { let pt1 = parse_point(tokens)?; let pt2 = parse_point(tokens)?; let pt3 = parse_point(tokens)?; let half_space = [pt1, pt2, pt3]; - let texture_token = &tokens - .next() - .transpose()? - .ok_or_else(qmap::ParseError::eof)?; + let texture_token = &tokens.extract()?.ok_or_else(error::TextParse::eof)?; let texture = if b'"' == (&texture_token.text)[0].into() { strip_quoted(&texture_token.text[..]).to_vec().into() @@ -114,13 +148,13 @@ fn parse_surface( }; let alignment = if let Some(tok_res) = tokens.peek() { - if tok_res.as_ref().map_err(|e| e.clone())?.match_byte(b'[') { + if tok_res.as_ref().map_err(|e| e.steal())?.match_byte(b'[') { parse_valve_alignment(tokens)? } else { parse_legacy_alignment(tokens)? } } else { - return Err(qmap::ParseError::eof()); + return Err(error::TextParse::eof()); }; Ok(Surface { @@ -130,24 +164,26 @@ fn parse_surface( }) } -fn parse_point(tokens: &mut TokenPeekable) -> ParseResult { - expect_byte(&tokens.next().transpose()?, b'(')?; - let x = expect_float(&tokens.next().transpose()?)?; - let y = expect_float(&tokens.next().transpose()?)?; - let z = expect_float(&tokens.next().transpose()?)?; - expect_byte(&tokens.next().transpose()?, b')')?; +fn parse_point( + tokens: &mut TokenPeekable, +) -> error::TextParseResult { + expect_byte(&tokens.extract()?, b'(')?; + let x = expect_float(&tokens.extract()?)?; + let y = expect_float(&tokens.extract()?)?; + let z = expect_float(&tokens.extract()?)?; + expect_byte(&tokens.extract()?, b')')?; Ok([x, y, z]) } fn parse_legacy_alignment( tokens: &mut TokenPeekable, -) -> ParseResult { - let offset_x = expect_float(&tokens.next().transpose()?)?; - let offset_y = expect_float(&tokens.next().transpose()?)?; - let rotation = expect_float(&tokens.next().transpose()?)?; - let scale_x = expect_float(&tokens.next().transpose()?)?; - let scale_y = expect_float(&tokens.next().transpose()?)?; +) -> error::TextParseResult { + let offset_x = expect_float(&tokens.extract()?)?; + let offset_y = expect_float(&tokens.extract()?)?; + let rotation = expect_float(&tokens.extract()?)?; + let scale_x = expect_float(&tokens.extract()?)?; + let scale_y = expect_float(&tokens.extract()?)?; Ok(Alignment { offset: [offset_x, offset_y], @@ -159,24 +195,24 @@ fn parse_legacy_alignment( fn parse_valve_alignment( tokens: &mut TokenPeekable, -) -> ParseResult { - expect_byte(&tokens.next().transpose()?, b'[')?; - let u_x = expect_float(&tokens.next().transpose()?)?; - let u_y = expect_float(&tokens.next().transpose()?)?; - let u_z = expect_float(&tokens.next().transpose()?)?; - let offset_x = expect_float(&tokens.next().transpose()?)?; - expect_byte(&tokens.next().transpose()?, b']')?; - - expect_byte(&tokens.next().transpose()?, b'[')?; - let v_x = expect_float(&tokens.next().transpose()?)?; - let v_y = expect_float(&tokens.next().transpose()?)?; - let v_z = expect_float(&tokens.next().transpose()?)?; - let offset_y = expect_float(&tokens.next().transpose()?)?; - expect_byte(&tokens.next().transpose()?, b']')?; - - let rotation = expect_float(&tokens.next().transpose()?)?; - let scale_x = expect_float(&tokens.next().transpose()?)?; - let scale_y = expect_float(&tokens.next().transpose()?)?; +) -> error::TextParseResult { + expect_byte(&tokens.extract()?, b'[')?; + let u_x = expect_float(&tokens.extract()?)?; + let u_y = expect_float(&tokens.extract()?)?; + let u_z = expect_float(&tokens.extract()?)?; + let offset_x = expect_float(&tokens.extract()?)?; + expect_byte(&tokens.extract()?, b']')?; + + expect_byte(&tokens.extract()?, b'[')?; + let v_x = expect_float(&tokens.extract()?)?; + let v_y = expect_float(&tokens.extract()?)?; + let v_z = expect_float(&tokens.extract()?)?; + let offset_y = expect_float(&tokens.extract()?)?; + expect_byte(&tokens.extract()?, b']')?; + + let rotation = expect_float(&tokens.extract()?)?; + let scale_x = expect_float(&tokens.extract()?)?; + let scale_y = expect_float(&tokens.extract()?)?; Ok(Alignment { offset: [offset_x, offset_y], @@ -186,10 +222,10 @@ fn parse_valve_alignment( }) } -fn expect_byte(token: &Option, byte: u8) -> ParseResult<()> { +fn expect_byte(token: &Option, byte: u8) -> error::TextParseResult<()> { match token.as_ref() { Some(payload) if payload.match_byte(byte) => Ok(()), - Some(payload) => Err(qmap::ParseError::from_parser( + Some(payload) => Err(error::TextParse::from_parser( format!( "Expected `{}`, got `{}`", char::from(byte), @@ -197,7 +233,7 @@ fn expect_byte(token: &Option, byte: u8) -> ParseResult<()> { ), payload.line_number, )), - _ => Err(qmap::ParseError::eof()), + _ => Err(error::TextParse::eof()), } } @@ -205,18 +241,18 @@ fn expect_byte_or( token: &Option, byte: u8, rest: &[u8], -) -> ParseResult<()> { +) -> error::TextParseResult<()> { match token.as_ref() { Some(payload) if payload.match_byte(byte) => Ok(()), Some(payload) => { - let rest_str = (&rest + let rest_str = rest .iter() .copied() .map(|b| format!("`{}`", char::from(b))) - .collect::>()[..]) + .collect::>()[..] .join(", "); - Err(qmap::ParseError::from_parser( + Err(error::TextParse::from_parser( format!( "Expected {} or `{}`, got `{}`", rest_str, @@ -226,31 +262,31 @@ fn expect_byte_or( payload.line_number, )) } - _ => Err(qmap::ParseError::eof()), + _ => Err(error::TextParse::eof()), } } -fn expect_quoted(token: &Option) -> ParseResult<()> { +fn expect_quoted(token: &Option) -> error::TextParseResult<()> { match token.as_ref() { Some(payload) if payload.match_quoted() => Ok(()), - Some(payload) => Err(qmap::ParseError::from_parser( + Some(payload) => Err(error::TextParse::from_parser( format!("Expected quoted, got `{}`", payload.text_as_string()), payload.line_number, )), - _ => Err(qmap::ParseError::eof()), + _ => Err(error::TextParse::eof()), } } -fn expect_float(token: &Option) -> ParseResult { +fn expect_float(token: &Option) -> error::TextParseResult { match token.as_ref() { Some(payload) => match f64::from_str(&payload.text_as_string()) { Ok(num) => Ok(num), - Err(_) => Err(qmap::ParseError::from_parser( + Err(_) => Err(error::TextParse::from_parser( format!("Expected number, got `{}`", payload.text_as_string()), payload.line_number, )), }, - None => Err(qmap::ParseError::eof()), + None => Err(error::TextParse::eof()), } } diff --git a/src/qmap/parser_test.rs b/src/qmap/parser_test.rs index 47465c2..852d8f1 100644 --- a/src/qmap/parser_test.rs +++ b/src/qmap/parser_test.rs @@ -1,6 +1,6 @@ -use crate::qmap; +use crate::{error, qmap}; use qmap::parser::parse; -use qmap::{EntityKind, ParseError}; +use qmap::EntityKind; use std::ffi::CString; use std::io; @@ -30,13 +30,13 @@ fn panic_unexpected_variant(err: T) { #[test] fn parse_empty_map() { - let map = parse(&b""[..]).unwrap(); + let map = parse(&mut &b""[..]).unwrap(); assert_eq!(map.entities.len(), 0); } #[test] fn parse_empty_point_entity() { - let map = parse(&b"{ }"[..]).unwrap(); + let map = parse(&mut &b"{ }"[..]).unwrap(); assert_eq!(map.entities.len(), 1); let ent = &map.entities[0]; assert_eq!(ent.edict.len(), 0); @@ -48,7 +48,7 @@ fn parse_empty_point_entity() { #[test] fn parse_point_entity_with_key_value() { let map = parse( - &br#" + &mut &br#" { "classname" "light" } @@ -71,7 +71,7 @@ fn parse_point_entity_with_key_value() { #[test] fn parse_standard_brush_entity() { let map = parse( - &b" + &mut &b" { { ( 1 2 3 ) ( 4 5 6 ) ( 7 8 9 ) TEXTURE1 0 0 0 1 1 @@ -99,7 +99,7 @@ fn parse_standard_brush_entity() { #[test] fn parse_valve_brush_entity() { let map = parse( - &b" + &mut &b" { { ( 1 2 3 ) ( 4 5 6 ) ( 7 8 9 ) TEX2 [ 1 0 0 0 ] [ 0 1 0 0 ] 0 1 1 @@ -132,7 +132,7 @@ fn parse_valve_brush_entity() { #[test] fn parse_weird_numbers() { let map = parse( - &b" + &mut &b" { { ( 9E99 1E-9 -1.37e9 ) ( 12 -0 -100.7 ) ( 0e8 0E8 1.2e34 ) T 0.25 0.25 45 2.0001 2.002 @@ -162,7 +162,7 @@ fn parse_weird_numbers() { #[test] fn parse_weird_textures() { let map = parse( - &br#" + &mut &br#" { { ( 1 2 3 ) ( 4 5 6 ) ( 7 8 9 ) {FENCE @@ -193,8 +193,8 @@ fn parse_weird_textures() { #[test] fn parse_token_error() { - let err = parse(&b"\""[..]).err().unwrap(); - if let ParseError::Lexer(line_err) = err { + let err = parse(&mut &b"\""[..]).err().unwrap(); + if let error::TextParse::Lexer(line_err) = err { assert_eq!(u64::from(line_err.line_number.unwrap()), 1u64); } else { panic_unexpected_variant(err); @@ -203,9 +203,9 @@ fn parse_token_error() { #[test] fn parse_io_error() { - let reader = ErroringReader::new(); - let err = parse(reader).err().unwrap(); - if let ParseError::Io(_) = err { + let mut reader = ErroringReader::new(); + let err = parse(&mut reader).err().unwrap(); + if let error::TextParse::Io(_) = err { } else { panic_unexpected_variant(err); } @@ -213,8 +213,8 @@ fn parse_io_error() { #[test] fn parse_eof_error() { - let err = parse(&b"{"[..]).err().unwrap(); - if let ParseError::Parser(line_err) = err { + let err = parse(&mut &b"{"[..]).err().unwrap(); + if let error::TextParse::Parser(line_err) = err { assert_eq!(line_err.line_number, None); assert!(line_err.message.contains("end-of-file")); } else { @@ -224,8 +224,8 @@ fn parse_eof_error() { #[test] fn parse_closing_brace_error() { - let err = parse(&b"}"[..]).err().unwrap(); - if let ParseError::Parser(line_err) = err { + let err = parse(&mut &b"}"[..]).err().unwrap(); + if let error::TextParse::Parser(line_err) = err { assert_eq!(u64::from(line_err.line_number.unwrap()), 1u64); assert!(line_err.message.contains('}')); } else { @@ -235,8 +235,8 @@ fn parse_closing_brace_error() { #[test] fn parse_missing_value() { - let err = parse(&b"{\n \"classname\"\n }"[..]).err().unwrap(); - if let ParseError::Parser(line_err) = err { + let err = parse(&mut &b"{\n \"classname\"\n }"[..]).err().unwrap(); + if let error::TextParse::Parser(line_err) = err { assert_eq!(u64::from(line_err.line_number.unwrap()), 3u64); assert!(line_err.message.contains('}')); } else { @@ -251,8 +251,8 @@ fn parse_bad_texture_name() { ( 1 2 3 ) ( 2 3 1 ) ( 3 1 2 ) "bad"tex" 0 0 0 1 1 } } "#; - let err = parse(&map_text[..]).err().unwrap(); - if let ParseError::Parser(line_err) = err { + let err = parse(&mut &map_text[..]).err().unwrap(); + if let error::TextParse::Parser(line_err) = err { assert!(line_err.message.contains("tex\"")); } else { panic_unexpected_variant(err); @@ -268,8 +268,8 @@ fn parse_unclosed_surface() { ( 1 2 3 ) ( 2 3 1 ) ( 3 1 2 ) tex 0 0 0 1 1 { "#; - let err = parse(&map_text[..]).err().unwrap(); - if let ParseError::Parser(line_err) = err { + let err = parse(&mut &map_text[..]).err().unwrap(); + if let error::TextParse::Parser(line_err) = err { let (pfx, _) = line_err.message.split_once("got").unwrap(); assert!(pfx.contains("`}`")); assert!(pfx.contains("`(`")); diff --git a/src/qmap/repr_test.rs b/src/qmap/repr_test.rs index 4ea2a44..f76b5f3 100644 --- a/src/qmap/repr_test.rs +++ b/src/qmap/repr_test.rs @@ -149,7 +149,7 @@ fn check_simple_map() { #[test] fn check_bad_map() { - assert!(matches!(bad_map_edict().check_writable(), Err(_))); + assert!(bad_map_edict().check_writable().is_err()); } #[test] @@ -166,9 +166,7 @@ fn check_bad_entities() { .into_iter() .chain(good_edict_strings.clone()); - let value_iter = good_edict_strings - .clone() - .chain(bad_edict_strings.into_iter()); + let value_iter = good_edict_strings.chain(bad_edict_strings); let trials = bad_char_iter.zip(key_iter.zip(value_iter)); @@ -188,10 +186,9 @@ fn check_bad_entities() { #[test] fn check_bad_surface_texture() { - assert!(matches!( - entity_with_texture(&CString::new("\"").unwrap()).check_writable(), - Err(_) - )); + assert!(entity_with_texture(&CString::new("\"").unwrap()) + .check_writable() + .is_err(),); } #[test] @@ -213,7 +210,7 @@ fn check_bad_surface_alignment() { alignment: BAD_ALIGNMENT_ROTATION, }; - assert!(matches!(surf.check_writable(), Err(_))); + assert!(surf.check_writable().is_err()); } #[test] diff --git a/src/wad/mod.rs b/src/wad/mod.rs new file mode 100644 index 0000000..e8a154b --- /dev/null +++ b/src/wad/mod.rs @@ -0,0 +1,40 @@ +//! Quake WAD parsing +//! +//! # Example +//! ``` +//! # use quake_util::wad; +//! # +//! # fn main () { +//! # let bytes = Vec::new(); +//! # let mut src = std::io::Cursor::new(bytes); +//! +//! if let Ok((mut parser, _warnings)) = wad::Parser::new(&mut src) { +//! for (entry_name, entry) in parser.directory() { +//! let kind = parser.parse_inferred(&entry).map( +//! |lump| lump.kind().to_string(), +//! ).unwrap_or( +//! "".to_string() +//! ); +//! +//! println!("Entry {entry_name} has lump type {kind}"); +//! } +//! } else { +//! eprintln!("Error loading WAD"); +//! } +//! +//! # } +//! ``` + +mod repr; + +mod parser; + +pub use parser::Parser; + +pub use repr::Entry; + +#[cfg(test)] +mod repr_test; + +#[cfg(test)] +mod parser_test; diff --git a/src/wad/parser.rs b/src/wad/parser.rs new file mode 100644 index 0000000..e6361d2 --- /dev/null +++ b/src/wad/parser.rs @@ -0,0 +1,240 @@ +use crate::{error, error::BinParseResult, lump, wad, Palette}; +use io::{Read, Seek, SeekFrom}; +use lump::Lump; +use std::boxed::Box; +use std::collections::hash_map::Entry as MapEntry; +use std::collections::HashMap; +use std::io; +use std::mem::size_of; +use std::mem::size_of_val; +use std::string::{String, ToString}; +use std::vec::Vec; +use wad::repr::Head; + +/// WAD parser. Wraps a mutable reference to a Read + Seek cursor to provide +/// random read access. +#[derive(Debug)] +pub struct Parser<'a, Reader: Seek + Read> { + cursor: &'a mut Reader, + start: u64, + directory: HashMap, +} + +impl<'a, Reader: Seek + Read> Parser<'a, Reader> { + /// Constructs a new wad parser starting at the provided cursor. May + /// produce a list of warnings for duplicate entriess (entries sharing the + /// same name). + pub fn new(cursor: &'a mut Reader) -> BinParseResult<(Self, Vec)> { + let start = cursor.stream_position().map_err(error::BinParse::Io)?; + let (directory, warnings) = parse_directory(cursor, start)?; + + Ok(( + Self { + cursor, + start, + directory, + }, + warnings, + )) + } + + /// Clones WAD entries into a hash map. Entries are used to access lumps + /// within the WAD. + pub fn directory(&self) -> HashMap { + self.directory.clone() + } + + /// Attempts to parse a mip-mapped texture at the offset provided by the + /// entry + pub fn parse_mip_texture( + &mut self, + entry: &wad::Entry, + ) -> BinParseResult { + self.seek_to_entry(entry)?; + lump::parse_mip_texture(self.cursor) + } + + /// Attempts to parse a 2D at the offset provided by the entry + pub fn parse_image( + &mut self, + entry: &wad::Entry, + ) -> BinParseResult { + self.seek_to_entry(entry)?; + lump::parse_image(self.cursor) + } + + /// Attempts to parse a 768 byte palette at the offset provided by the entry + pub fn parse_palette( + &mut self, + entry: &wad::Entry, + ) -> BinParseResult> { + self.seek_to_entry(entry)?; + lump::parse_palette(self.cursor) + } + + /// Attempts to read a number of bytes using the provided entry's length and + /// offset + pub fn read_raw( + &mut self, + entry: &wad::Entry, + ) -> BinParseResult> { + self.seek_to_entry(entry)?; + let length = usize::try_from(entry.length()).map_err(|_| { + error::BinParse::Parse("Length too large".to_string()) + })?; + lump::read_raw(self.cursor, length) + } + + /// Attempts to read a lump based on the provided entry's name and lump + /// kind. All known kinds of lump are attempted based on the entry. E.g. + /// there is a special case where Quake's gfx.wad has a flat lump named + /// CONCHARS which is erroneously tagged as miptex. + pub fn parse_inferred( + &mut self, + entry: &wad::Entry, + ) -> BinParseResult { + const CONCHARS_NAME: &[u8; 9] = b"CONCHARS\0"; + + let mut attempt_order = [ + lump::kind::MIPTEX, + lump::kind::SBAR, + lump::kind::PALETTE, + lump::kind::FLAT, + ]; + + // Some paranoid nonsense because not even Id can be trusted to tag + // their lumps correctly + let mut prioritize = |first_kind| { + let mut index = 0; + + for (i, kind) in attempt_order.into_iter().enumerate() { + if kind == first_kind { + index = i; + } + } + + while index > 0 { + attempt_order[index] = attempt_order[index - 1]; + attempt_order[index - 1] = first_kind; + index -= 1; + } + }; + + prioritize(entry.kind()); + + let length = usize::try_from(entry.length()).map_err(|_| { + error::BinParse::Parse("Length too large".to_string()) + })?; + + // It's *improbable* that a palette-sized lump could be a valid + // status bar image OR miptex, though it's possibly just 768 + // rando bytes. So if the explicit type is FLAT and it's 768 bytes, + // we can't know for sure that it + if length == size_of::() && entry.kind() != lump::kind::FLAT { + prioritize(lump::kind::PALETTE); + } + + // Quake's gfx.wad has CONCHARS's type set explicitly to MIPTEX, + // even though it's a FLAT (128x128 pixels) + if entry.name()[..size_of_val(CONCHARS_NAME)] == CONCHARS_NAME[..] { + prioritize(lump::kind::FLAT); + } + + let mut last_error = error::BinParse::Parse("Unreachable".to_string()); + + for attempt_kind in attempt_order { + match attempt_kind { + lump::kind::MIPTEX => match self.parse_mip_texture(entry) { + Ok(miptex) => { + return Ok(Lump::MipTexture(miptex)); + } + Err(e) => { + last_error = e; + } + }, + lump::kind::SBAR => match self.parse_image(entry) { + Ok(img) => { + return Ok(Lump::StatusBar(img)); + } + Err(e) => { + last_error = e; + } + }, + lump::kind::PALETTE => match self.parse_palette(entry) { + Ok(pal) => { + return Ok(Lump::Palette(pal)); + } + Err(e) => { + last_error = e; + } + }, + lump::kind::FLAT => match self.read_raw(entry) { + Ok(bytes) => { + return Ok(Lump::Flat(bytes)); + } + Err(e) => { + last_error = e; + } + }, + _ => unreachable!(), + } + } + + Err(last_error) + } + + fn seek_to_entry(&mut self, entry: &wad::Entry) -> BinParseResult<()> { + let offset = self + .start + .checked_add(entry.offset().into()) + .ok_or(error::BinParse::Parse("Offset too large".to_string()))?; + + self.cursor.seek(SeekFrom::Start(offset))?; + Ok(()) + } +} + +fn parse_directory( + cursor: &mut (impl Seek + Read), + start: u64, +) -> BinParseResult<(HashMap, Vec)> { + let mut header_bytes = [0u8; size_of::()]; + cursor.read_exact(&mut header_bytes[..])?; + let header: Head = header_bytes.try_into()?; + let entry_ct = header.entry_count(); + let dir_offset = header.directory_offset(); + + let dir_pos = start + .checked_add(dir_offset.into()) + .ok_or(error::BinParse::Parse("Offset too large".to_string()))?; + + cursor + .seek(SeekFrom::Start(dir_pos)) + .map_err(error::BinParse::Io)?; + + let mut entries = HashMap::::with_capacity( + entry_ct.try_into().unwrap(), + ); + + let mut warnings = Vec::new(); + + for _ in 0..entry_ct { + const WAD_ENTRY_SIZE: usize = size_of::(); + let mut entry_bytes = [0u8; WAD_ENTRY_SIZE]; + cursor.read_exact(&mut entry_bytes[0..WAD_ENTRY_SIZE])?; + let entry: wad::Entry = entry_bytes.try_into()?; + + let entry_name = entry + .name_to_string() + .map_err(|e| error::BinParse::Parse(e.to_string()))?; + + if let MapEntry::Vacant(map_entry) = entries.entry(entry_name.clone()) { + map_entry.insert(entry); + } else { + warnings + .push(format!("Skipping duplicate entry for `{entry_name}`")); + } + } + + Ok((entries, warnings)) +} diff --git a/src/wad/parser_test.rs b/src/wad/parser_test.rs new file mode 100644 index 0000000..6ed546c --- /dev/null +++ b/src/wad/parser_test.rs @@ -0,0 +1,341 @@ +use crate::error; +use crate::lump::{kind, Lump}; +use crate::wad; +use std::io::Cursor; +use std::iter::repeat; +use std::mem::{size_of, size_of_val}; +use std::vec::Vec; +use wad::repr::Head; + +fn image_bytes() -> Vec { + let width: u32 = 64; + let height: u32 = 128; + let pix_ct: usize = (width * height).try_into().unwrap(); + + let mut image = Vec::new(); + image.extend(width.to_le_bytes()); + image.extend(height.to_le_bytes()); + image.extend(repeat(0u8).take(pix_ct)); + + image +} + +fn miptex_bytes(name: [u8; 16]) -> Vec { + let width: u32 = 512; + let height: u32 = 32; + let mip0_sz = width * height; + let mut miptex = Vec::new(); + + miptex.extend(name); + miptex.extend(width.to_le_bytes()); + miptex.extend(height.to_le_bytes()); + + let mut offset: u32 = miptex.len().try_into().unwrap(); + + for i in 0..4 { + miptex.extend(offset.to_le_bytes()); + let mip_sz = mip0_sz >> (2 * i); + offset += mip_sz; + } + + for i in 0..4 { + let mip_sz = mip0_sz >> (2 * i); + miptex.extend(repeat(0u8).take(mip_sz.try_into().unwrap())); + } + + miptex +} + +fn palette_bytes() -> Vec { + let mut palette = Vec::new(); + + for i in 0..768i32 { + palette.push((i & 0xff).try_into().unwrap()); + } + + palette +} + +fn entry_bytes(offset: u32, length: u32, kind: u8, name: [u8; 16]) -> Vec { + let mut entry = Vec::new(); + + entry.extend(offset.to_le_bytes()); + entry.extend(length.to_le_bytes()); + entry.extend(length.to_le_bytes()); + entry.push(kind); + entry.push(0u8); + entry.extend([0; 2]); + entry.extend(name); + + entry +} + +fn compressed_entry_bytes(offset: u32) -> Vec { + let mut entry = Vec::new(); + + entry.extend(offset.to_le_bytes()); + entry.extend([0; 8]); + entry.push(kind::FLAT); + entry.push(1u8); + entry.extend([0; 2]); + entry.extend(b"compressed\0\0\0\0\0\0"); + + entry +} + +fn duplicate_entry_wad_bytes() -> Vec { + let mut wad = Vec::new(); + let entry_count = 2u32; + let directory_offset = 12u32; + let name = *b"same_name\0\0\0\0\0\0\0"; + + wad.extend(b"WAD2"); + wad.extend(entry_count.to_le_bytes()); + wad.extend(directory_offset.to_le_bytes()); + + let entry1 = + entry_bytes(wad.len().try_into().unwrap(), 0, kind::FLAT, name); + wad.extend(entry1); + + let entry2 = + entry_bytes(wad.len().try_into().unwrap(), 0, kind::FLAT, name); + wad.extend(entry2); + + wad +} + +fn good_wad_bytes() -> Vec { + let mut wad = Vec::new(); + let image_name = *b"image\0\0\0\0\0\0\0\0\0\0\0"; + let miptex_name = *b"miptex\0\0\0\0\0\0\0\0\0\0"; + let palette_name = *b"palette\0\0\0\0\0\0\0\0\0"; + let flat_name = *b"flat\0\0\0\0\0\0\0\0\0\0\0\0"; + let conchars_name = *b"CONCHARS\0\0\0\0\0\0\0\0"; + let entry_count: u32 = 5; + let image = image_bytes(); + let miptex = miptex_bytes(miptex_name); + let palette = palette_bytes(); + let flat = [0u8; 123]; + let conchars = [0u8; 128 * 128]; + let directory_offset: u32 = (image.len() + + miptex.len() + + palette.len() + + flat.len() + + conchars.len() + + size_of::()) + .try_into() + .unwrap(); + + wad.extend(b"WAD2"); + wad.extend(entry_count.to_le_bytes()); + wad.extend(directory_offset.to_le_bytes()); + + let image_offset: u32 = wad.len().try_into().unwrap(); + wad.extend(&image); + + let miptex_offset: u32 = wad.len().try_into().unwrap(); + wad.extend(&miptex); + + let palette_offset: u32 = wad.len().try_into().unwrap(); + wad.extend(&palette); + + let flat_offset: u32 = wad.len().try_into().unwrap(); + wad.extend(&flat); + + let conchars_offset: u32 = wad.len().try_into().unwrap(); + wad.extend(&conchars); + + wad.extend(entry_bytes( + image_offset, + image.len().try_into().unwrap(), + kind::SBAR, + image_name, + )); + wad.extend(entry_bytes( + miptex_offset, + miptex.len().try_into().unwrap(), + kind::MIPTEX, + miptex_name, + )); + wad.extend(entry_bytes( + palette_offset, + palette.len().try_into().unwrap(), + kind::PALETTE, + palette_name, + )); + wad.extend(entry_bytes( + flat_offset, + flat.len().try_into().unwrap(), + kind::FLAT, + flat_name, + )); + wad.extend(entry_bytes( + conchars_offset, + conchars.len().try_into().unwrap(), + kind::MIPTEX, + conchars_name, + )); + + wad +} + +#[test] +fn parse_good_wad() { + let mut wad_file = Cursor::new(good_wad_bytes()); + let (mut parser, warnings) = wad::Parser::new(&mut wad_file).unwrap(); + let dir = parser.directory(); + let panic_dir = || panic!("{:?}", dir); + let image_entry = dir.get("image").unwrap_or_else(panic_dir); + let miptex_entry = dir.get("miptex").unwrap_or_else(panic_dir); + let palette_entry = dir.get("palette").unwrap_or_else(panic_dir); + let flat_entry = dir.get("flat").unwrap_or_else(panic_dir); + let conchars_entry = dir.get("CONCHARS").unwrap_or_else(panic_dir); + + assert_eq!(warnings.len(), 0); + assert_eq!(image_entry.kind(), kind::SBAR); + assert_eq!(miptex_entry.kind(), kind::MIPTEX); + assert_eq!(palette_entry.kind(), kind::PALETTE); + assert_eq!(flat_entry.kind(), kind::FLAT); + assert_eq!(conchars_entry.kind(), kind::MIPTEX); + + { + let image_lump = parser.parse_image(image_entry).unwrap(); + let (width, height) = (image_lump.width(), image_lump.height()); + assert_eq!(width, 64); + assert_eq!(height, 128); + assert_eq!( + width * height, + image_lump.pixels().len().try_into().unwrap() + ); + } + + { + let miptex_lump = parser.parse_mip_texture(miptex_entry).unwrap(); + assert_eq!(miptex_lump.mip(0).width(), 512); + assert_eq!(miptex_lump.mip(1).width(), 256); + assert_eq!(miptex_lump.mip(2).width(), 128); + assert_eq!(miptex_lump.mip(3).width(), 64); + assert_eq!(miptex_lump.mip(0).height(), 32); + assert_eq!(miptex_lump.mip(1).height(), 16); + assert_eq!(miptex_lump.mip(2).height(), 8); + assert_eq!(miptex_lump.mip(3).height(), 4); + } + + { + let palette_lump = parser.parse_palette(palette_entry).unwrap(); + let mut val = 0u8; + + for i in 0..256usize { + assert_eq!( + (*palette_lump)[i], + [val, val.wrapping_add(1), val.wrapping_add(2)] + ); + val = val.wrapping_add(3); + } + } + + { + let flat_lump = parser.read_raw(flat_entry).unwrap(); + assert_eq!(flat_lump.len(), 123); + } + + for (entry_name, entry) in dir { + assert!(match &entry_name[..] { + "image" => matches!( + parser.parse_inferred(&entry).unwrap(), + Lump::StatusBar(_) + ), + "miptex" => matches!( + parser.parse_inferred(&entry).unwrap(), + Lump::MipTexture(_) + ), + "palette" => matches!( + parser.parse_inferred(&entry).unwrap(), + Lump::Palette(_) + ), + "flat" => + matches!(parser.parse_inferred(&entry).unwrap(), Lump::Flat(_)), + "CONCHARS" => + matches!(parser.parse_inferred(&entry).unwrap(), Lump::Flat(_)), + name => panic!("Unexpected entry name `{name}`"), + }); + } +} + +#[test] +fn parse_duplicate_entry() { + let mut wad_file = Cursor::new(duplicate_entry_wad_bytes()); + let (_, warnings) = wad::Parser::new(&mut wad_file).unwrap(); + + assert_eq!(warnings.len(), 1); +} + +#[test] +fn parse_bad_magic_wad() { + let mut wad_file = Cursor::new(b"WART\0\0\0\0\0\0\0\0"); + let e = wad::Parser::new(&mut wad_file).unwrap_err(); + + assert!(matches!(e, error::BinParse::Parse(_))); +} + +#[test] +fn parse_bad_short_wad() { + let mut wad_file = Cursor::new(b"WAD2"); + let e = wad::Parser::new(&mut wad_file).unwrap_err(); + + assert!(matches!(e, error::BinParse::Io(_))); +} + +#[test] +fn parse_bad_directory() { + let mut wad_file = Cursor::new(b"WAD2\x01\0\0\0\0\0\0\0"); + let e = wad::Parser::new(&mut wad_file).unwrap_err(); + + assert!(matches!(e, error::BinParse::Io(_))); +} + +#[test] +fn parse_bad_compression_entry() { + let mut wad = Vec::::new(); + wad.extend(b"WAD2\x01\0\0\0\x0c\0\0\0"); + wad.extend(compressed_entry_bytes(12)); + let mut wad_file = Cursor::new(wad); + let e = wad::Parser::new(&mut wad_file).unwrap_err(); + + assert!(matches!(e, error::BinParse::Parse(_))); +} + +#[test] +fn parse_bad_lumps() { + for kind in [kind::SBAR, kind::MIPTEX, kind::PALETTE, kind::FLAT] { + let mut wad = Vec::::new(); + let bad_lump = b"\xBA\xDB\xAD"; + wad.extend(b"WAD2"); + wad.extend((1u32).to_le_bytes()); + + wad.extend( + ::try_from(size_of::() + size_of_val(bad_lump)) + .unwrap() + .to_le_bytes(), + ); + + wad.extend(bad_lump); + + wad.extend(entry_bytes( + wad.len().try_into().unwrap(), + 666, + kind, + *b"BAD_BAD_BAD_BAD\0", + )); + + let mut wad_file = Cursor::new(wad); + let (mut parser, _) = wad::Parser::new(&mut wad_file).unwrap(); + let dir = parser.directory(); + let entry = dir.get("BAD_BAD_BAD_BAD").unwrap(); + + assert!(matches!( + parser.parse_inferred(entry), + Err(error::BinParse::Io(_)) + )); + } +} diff --git a/src/wad/repr.rs b/src/wad/repr.rs new file mode 100644 index 0000000..16bd2ca --- /dev/null +++ b/src/wad/repr.rs @@ -0,0 +1,175 @@ +use std::ffi::{CString, IntoStringError}; +use std::mem::size_of; +use std::string::{String, ToString}; + +use crate::common::Junk; +use crate::{error, slice_to_cstring}; + +pub const MAGIC: [u8; 4] = *b"WAD2"; + +#[derive(Clone, Copy, PartialEq, Eq, Debug)] +#[repr(C, packed)] +pub struct Head { + magic: [u8; 4], + entry_count: u32, + directory_offset: u32, +} + +impl Head { + pub fn new(entry_count: u32, directory_offset: u32) -> Self { + Head { + magic: MAGIC, + entry_count, + directory_offset, + } + } + + pub fn entry_count(&self) -> u32 { + self.entry_count + } + + pub fn directory_offset(&self) -> u32 { + self.directory_offset + } +} + +impl TryFrom<[u8; size_of::()]> for Head { + type Error = error::BinParse; + + fn try_from(bytes: [u8; size_of::()]) -> Result { + let mut chunks = bytes.chunks_exact(4usize); + + if chunks.next().unwrap() != &MAGIC[..] { + let magic_str: String = + MAGIC.iter().copied().map(char::from).collect(); + + return Err(error::BinParse::Parse(format!( + "Magic number does not match `{magic_str}`" + ))); + } + + let entry_count = u32::from_le_bytes( + <[u8; 4]>::try_from(chunks.next().unwrap()).unwrap(), + ); + + let directory_offset = u32::from_le_bytes( + <[u8; 4]>::try_from(chunks.next().unwrap()).unwrap(), + ); + + Ok(Head::new(entry_count, directory_offset)) + } +} + +/// Provides the location of a lump within a WAD archive, length of the lump, +/// name (16 bytes, null-terminated), and lump kind +#[derive(Clone, Copy, PartialEq, Eq, Debug)] +#[repr(C, packed)] +pub struct Entry { + offset: u32, + length: u32, + uncompressed_length: u32, // unused? + lump_kind: u8, + compression: u8, // 0 - uncompressed, other values unused? + _padding: Junk, + name: [u8; 16], +} + +impl Entry { + pub(crate) fn from_config(config: EntryConfig) -> Entry { + Entry { + offset: config.offset, + length: config.length, + uncompressed_length: config.length, + lump_kind: config.lump_kind, + compression: 0u8, + _padding: Junk::default(), + name: config.name, + } + } + + /// Obtain the name as a C string. If the name is not already + /// null-terminated (in which case the entry is not well-formed) a null byte + /// is appended to make a valid C string. + pub fn name_to_cstring(&self) -> CString { + slice_to_cstring(&self.name) + } + + /// Attempt to interpret the name as UTF-8 encoded string + pub fn name_to_string(&self) -> Result { + self.name_to_cstring().into_string() + } + + /// Name in raw bytes + pub fn name(&self) -> [u8; 16] { + self.name + } + + /// WAD offset of lump + pub fn offset(&self) -> u32 { + self.offset + } + + /// Length of lump in bytes + pub fn length(&self) -> u32 { + self.length + } + + /// Lump kind as a byte + pub fn kind(&self) -> u8 { + self.lump_kind + } +} + +impl TryFrom<[u8; size_of::()]> for Entry { + type Error = error::BinParse; + + // Attempt to read an entry from a block of bytes. Fails if compression + // flag is on (unsupported). + fn try_from(bytes: [u8; size_of::()]) -> Result { + let (offset_bytes, rest) = bytes.split_at(4); + + let offset = + u32::from_le_bytes(<[u8; 4]>::try_from(offset_bytes).unwrap()); + + let (length_bytes, rest) = rest.split_at(4); + + let length = + u32::from_le_bytes(<[u8; 4]>::try_from(length_bytes).unwrap()); + + let (uc_length_bytes, rest) = rest.split_at(4); + + let _uc_length = + u32::from_le_bytes(<[u8; 4]>::try_from(uc_length_bytes).unwrap()); + + let (&[lump_kind], rest) = rest.split_at(1) else { + unreachable!() + }; + + let (&[compression], rest) = rest.split_at(1) else { + unreachable!() + }; + + if compression != 0 { + return Err(error::BinParse::Parse( + "Compression is unsupported".to_string(), + )); + } + + let name: [u8; 16] = rest[2..].try_into().unwrap(); + + Ok(Entry::from_config(EntryConfig { + offset, + length, + lump_kind, + name, + })) + } +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub(crate) struct EntryConfig { + pub offset: u32, + pub length: u32, + pub lump_kind: u8, + pub name: [u8; 16], +} diff --git a/src/wad/repr_test.rs b/src/wad/repr_test.rs new file mode 100644 index 0000000..0639562 --- /dev/null +++ b/src/wad/repr_test.rs @@ -0,0 +1,108 @@ +use super::repr::{Entry, EntryConfig, Head}; +use crate::error; +use std::{ffi::CString, string::String}; + +#[test] +fn construct_head() { + let expected_count = 13; + let expected_dir_offset = 43234; + let head = Head::new(expected_count, expected_dir_offset); + assert_eq!(head.entry_count(), expected_count); + assert_eq!(head.directory_offset(), expected_dir_offset); +} + +#[test] +fn parse_good_head() { + let expected_count: u32 = 37; + let expected_dir_offset: u32 = 600; + let mut bytes = [0; std::mem::size_of::()]; + bytes[0..4].copy_from_slice(b"WAD2"); + bytes[4..8].copy_from_slice(&expected_count.to_le_bytes()); + bytes[8..12].copy_from_slice(&expected_dir_offset.to_le_bytes()); + + let head: Head = bytes.try_into().unwrap(); + + assert_eq!(head.entry_count(), expected_count); + assert_eq!(head.directory_offset(), expected_dir_offset); +} + +#[test] +fn parse_bad_head() { + let mut bytes = [0; std::mem::size_of::()]; + bytes[0..4].copy_from_slice(b"BAD2"); + + let err = Head::try_from(bytes).unwrap_err(); + + match err { + error::BinParse::Parse(_) => {} + _ => panic!("Incorrect error type"), + } +} + +#[test] +fn construct_entry() { + let expected_offset = 200; + let expected_length = 111; + let expected_kind = crate::lump::kind::SBAR; + let expected_name = [ + b'h', b'e', b'l', b'l', b'o', 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + ]; + + let config = EntryConfig { + offset: expected_offset, + length: expected_length, + lump_kind: expected_kind, + name: expected_name, + }; + + let entry = Entry::from_config(config); + + assert_eq!(entry.name(), expected_name); + assert_eq!(entry.offset(), expected_offset); + assert_eq!(entry.length(), expected_length); + assert_eq!(entry.kind(), expected_kind); + assert_eq!( + entry.name_to_cstring(), + CString::new(String::from("hello")).unwrap() + ); + assert_eq!(entry.name_to_string(), Ok(String::from("hello"))); +} + +#[test] +fn parse_good_entry() { + let expected_offset: u32 = 20049; + let expected_length: u32 = 3001; + let expected_kind = crate::lump::kind::MIPTEX; + let expected_name = [ + b'h', b'o', b'w', b'd', b'y', b'_', b'p', b'a', b'r', b't', b'n', b'e', + b'r', 0, 0, 0, + ]; + + let mut bytes = [0; std::mem::size_of::()]; + bytes[0..4].copy_from_slice(&expected_offset.to_le_bytes()); + bytes[4..8].copy_from_slice(&expected_length.to_le_bytes()); + bytes[8..12].copy_from_slice(&expected_length.to_le_bytes()); + bytes[12] = expected_kind; + bytes[13] = 0u8; + bytes[16..].copy_from_slice(&expected_name); + + let entry: Entry = bytes.try_into().unwrap(); + + assert_eq!(entry.name(), expected_name); + assert_eq!(entry.offset(), expected_offset); + assert_eq!(entry.length(), expected_length); + assert_eq!(entry.kind(), expected_kind); +} + +#[test] +fn parse_entry_bad_compression() { + let mut bytes = [0; std::mem::size_of::()]; + bytes[13] = 1u8; + + let err = Entry::try_from(bytes).unwrap_err(); + + match err { + error::BinParse::Parse(_) => {} + _ => panic!("Incorrect error type"), + } +}