quake_util/wad/
parser.rs

1use crate::{lump, wad, BinParseError, BinParseResult, Palette};
2use io::{Read, Seek, SeekFrom};
3use lump::Lump;
4use std::boxed::Box;
5use std::collections::hash_map::Entry as MapEntry;
6use std::collections::HashMap;
7use std::io;
8use std::mem::size_of;
9use std::mem::size_of_val;
10use std::string::{String, ToString};
11use std::vec::Vec;
12use wad::repr::Head;
13
14/// WAD parser.  Wraps a mutable reference to a Read + Seek cursor to provide
15/// random read access.
16#[derive(Debug)]
17pub struct Parser<'a, Reader: Seek + Read> {
18    cursor: &'a mut Reader,
19    start: u64,
20    directory: HashMap<String, wad::Entry>,
21}
22
23impl<'a, Reader: Seek + Read> Parser<'a, Reader> {
24    /// Constructs a new wad parser starting at the provided cursor.  May
25    /// produce a list of warnings for duplicate entriess (entries sharing the
26    /// same name).
27    pub fn new(cursor: &'a mut Reader) -> BinParseResult<(Self, Vec<String>)> {
28        let start = cursor.stream_position().map_err(BinParseError::Io)?;
29        let (directory, warnings) = parse_directory(cursor, start)?;
30
31        Ok((
32            Self {
33                cursor,
34                start,
35                directory,
36            },
37            warnings,
38        ))
39    }
40
41    /// Clones WAD entries into a hash map.  Entries are used to access lumps
42    /// within the WAD.
43    pub fn directory(&self) -> HashMap<String, wad::Entry> {
44        self.directory.clone()
45    }
46
47    /// Attempts to parse a mip-mapped texture at the offset provided by the
48    /// entry
49    pub fn parse_mip_texture(
50        &mut self,
51        entry: &wad::Entry,
52    ) -> BinParseResult<lump::MipTexture> {
53        self.seek_to_entry(entry)?;
54        lump::parse_mip_texture(self.cursor)
55    }
56
57    /// Attempts to parse a 2D at the offset provided by the entry
58    pub fn parse_image(
59        &mut self,
60        entry: &wad::Entry,
61    ) -> BinParseResult<lump::Image> {
62        self.seek_to_entry(entry)?;
63        lump::parse_image(self.cursor)
64    }
65
66    /// Attempts to parse a 768 byte palette at the offset provided by the entry
67    pub fn parse_palette(
68        &mut self,
69        entry: &wad::Entry,
70    ) -> BinParseResult<Box<Palette>> {
71        self.seek_to_entry(entry)?;
72        lump::parse_palette(self.cursor)
73    }
74
75    /// Attempts to read a number of bytes using the provided entry's length and
76    /// offset
77    pub fn read_raw(
78        &mut self,
79        entry: &wad::Entry,
80    ) -> BinParseResult<Box<[u8]>> {
81        self.seek_to_entry(entry)?;
82        let length = usize::try_from(entry.length()).map_err(|_| {
83            BinParseError::Parse("Length too large".to_string())
84        })?;
85        lump::read_raw(self.cursor, length)
86    }
87
88    /// Attempts to read a lump based on the provided entry's name and lump
89    /// kind.  All known kinds of lump are attempted based on the entry.  E.g.
90    /// there is a special case where Quake's gfx.wad has a flat lump named
91    /// CONCHARS which is erroneously tagged as miptex.
92    pub fn parse_inferred(
93        &mut self,
94        entry: &wad::Entry,
95    ) -> BinParseResult<Lump> {
96        const CONCHARS_NAME: &[u8; 9] = b"CONCHARS\0";
97
98        let mut attempt_order = [
99            lump::kind::MIPTEX,
100            lump::kind::SBAR,
101            lump::kind::PALETTE,
102            lump::kind::FLAT,
103        ];
104
105        // Some paranoid nonsense because not even Id can be trusted to tag
106        // their lumps correctly
107        let mut prioritize = |first_kind| {
108            let mut index = 0;
109
110            for (i, kind) in attempt_order.into_iter().enumerate() {
111                if kind == first_kind {
112                    index = i;
113                }
114            }
115
116            while index > 0 {
117                attempt_order[index] = attempt_order[index - 1];
118                attempt_order[index - 1] = first_kind;
119                index -= 1;
120            }
121        };
122
123        prioritize(entry.kind());
124
125        let length = usize::try_from(entry.length()).map_err(|_| {
126            BinParseError::Parse("Length too large".to_string())
127        })?;
128
129        // It's *improbable* that a palette-sized lump could be a valid
130        // status bar image OR miptex, though it's possibly just 768
131        // rando bytes.  So if the explicit type is FLAT and it's 768 bytes,
132        // we can't know for sure that it
133        if length == size_of::<Palette>() && entry.kind() != lump::kind::FLAT {
134            prioritize(lump::kind::PALETTE);
135        }
136
137        // Quake's gfx.wad has CONCHARS's type set explicitly to MIPTEX,
138        // even though it's a FLAT (128x128 pixels)
139        if entry.name()[..size_of_val(CONCHARS_NAME)] == CONCHARS_NAME[..] {
140            prioritize(lump::kind::FLAT);
141        }
142
143        let mut last_error = BinParseError::Parse("Unreachable".to_string());
144
145        for attempt_kind in attempt_order {
146            match attempt_kind {
147                lump::kind::MIPTEX => match self.parse_mip_texture(entry) {
148                    Ok(miptex) => {
149                        return Ok(Lump::MipTexture(miptex));
150                    }
151                    Err(e) => {
152                        last_error = e;
153                    }
154                },
155                lump::kind::SBAR => match self.parse_image(entry) {
156                    Ok(img) => {
157                        return Ok(Lump::StatusBar(img));
158                    }
159                    Err(e) => {
160                        last_error = e;
161                    }
162                },
163                lump::kind::PALETTE => match self.parse_palette(entry) {
164                    Ok(pal) => {
165                        return Ok(Lump::Palette(pal));
166                    }
167                    Err(e) => {
168                        last_error = e;
169                    }
170                },
171                lump::kind::FLAT => match self.read_raw(entry) {
172                    Ok(bytes) => {
173                        return Ok(Lump::Flat(bytes));
174                    }
175                    Err(e) => {
176                        last_error = e;
177                    }
178                },
179                _ => unreachable!(),
180            }
181        }
182
183        Err(last_error)
184    }
185
186    fn seek_to_entry(&mut self, entry: &wad::Entry) -> BinParseResult<()> {
187        let offset = self
188            .start
189            .checked_add(entry.offset().into())
190            .ok_or(BinParseError::Parse("Offset too large".to_string()))?;
191
192        self.cursor.seek(SeekFrom::Start(offset))?;
193        Ok(())
194    }
195}
196
197fn parse_directory(
198    cursor: &mut (impl Seek + Read),
199    start: u64,
200) -> BinParseResult<(HashMap<String, wad::Entry>, Vec<String>)> {
201    let mut header_bytes = [0u8; size_of::<Head>()];
202    cursor.read_exact(&mut header_bytes[..])?;
203    let header: Head = header_bytes.try_into()?;
204    let entry_ct = header.entry_count();
205    let dir_offset = header.directory_offset();
206
207    let dir_pos = start
208        .checked_add(dir_offset.into())
209        .ok_or(BinParseError::Parse("Offset too large".to_string()))?;
210
211    cursor
212        .seek(SeekFrom::Start(dir_pos))
213        .map_err(BinParseError::Io)?;
214
215    let mut entries = HashMap::<String, wad::Entry>::with_capacity(
216        entry_ct.try_into().unwrap(),
217    );
218
219    let mut warnings = Vec::new();
220
221    for _ in 0..entry_ct {
222        const WAD_ENTRY_SIZE: usize = size_of::<wad::Entry>();
223        let mut entry_bytes = [0u8; WAD_ENTRY_SIZE];
224        cursor.read_exact(&mut entry_bytes[0..WAD_ENTRY_SIZE])?;
225        let entry: wad::Entry = entry_bytes.try_into()?;
226
227        let entry_name = entry
228            .name_to_string()
229            .map_err(|e| BinParseError::Parse(e.to_string()))?;
230
231        if let MapEntry::Vacant(map_entry) = entries.entry(entry_name.clone()) {
232            map_entry.insert(entry);
233        } else {
234            warnings
235                .push(format!("Skipping duplicate entry for `{entry_name}`"));
236        }
237    }
238
239    Ok((entries, warnings))
240}