cdragon_wad/
lib.rs

1//! Support of Riot WAD archive files
2//!
3//! # Example: list files in wad
4//! ```no_run
5//! use cdragon_wad::{WadFile, WadHashMapper};
6//! let wad = WadFile::open("Global.wad.client").expect("failed to open WAD file");
7//! let hmapper = WadHashMapper::from_path("hashes.game.txt").expect("failed to load hashes");
8//! for entry in wad.iter_entries() {
9//!     let entry = entry.expect("failed to read entry");
10//!     println!("{}", hmapper.get(entry.path.hash).unwrap_or("?"));
11//! }
12//! ```
13//!
14//! [cdragon_hashes::HashKind] can be used to use the appropriate hash file (assuming CDragon's
15//! files are used).
16//! ```
17//! # use cdragon_hashes::HashKind;
18//! # use cdragon_wad::WadHashMapper;
19//! if let Some(kind) = HashKind::from_wad_path("Global.wad.client") {
20//!   let mapper = WadHashMapper::from_path(kind.mapping_path());
21//! }
22//! ```
23
24use std::fs::File;
25use std::io::{Read, Seek, SeekFrom, BufReader};
26use std::path::Path;
27use nom::{
28    number::complete::{le_u8, le_u16, le_u32, le_u64},
29    bytes::complete::tag,
30    combinator::{map, map_res},
31    sequence::tuple,
32};
33use thiserror::Error;
34use cdragon_hashes::{
35    define_hash_type,
36    wad::compute_wad_hash,
37};
38use cdragon_utils::{
39    GuardedFile,
40    parsing::{ParseError, ReadArray},
41    parse_buf,
42};
43pub use cdragon_hashes::wad::WadHashMapper;
44
45
46/// Result type for WAD errors
47type Result<T, E = WadError> = std::result::Result<T, E>;
48
49
50/// Riot WAD archive file
51///
52/// Store information from the header and list of entries.
53/// To read a WAD file, use [WadFile] or [WadReader].
54pub struct Wad {
55    /// WAD version (`(major, minor)`)
56    pub version: (u8, u8),
57    entry_count: u32,
58    entry_data: Vec<u8>,
59}
60
61impl std::fmt::Debug for Wad {
62    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
63        f.debug_struct("Wad")
64            .field("version", &self.version)
65            .field("entry_count", &self.entry_count)
66            .finish()
67    }
68}
69
70impl Wad {
71    const ENTRY_LEN: usize = 32;
72
73    /// Read a WAD file, check header, read entry headers
74    pub fn read<R: Read + Seek>(reader: &mut R) -> Result<Self> {
75        let (version, entry_count, entry_offset) = Self::parse_header(reader)?;
76
77        let data_size = Self::ENTRY_LEN * entry_count as usize;
78        let mut entry_data = Vec::with_capacity(data_size);
79        reader.seek(SeekFrom::Start(entry_offset))?;
80        if reader.take(data_size as u64).read_to_end(&mut entry_data)? != data_size {
81            return Err(ParseError::NotEnoughData.into());
82        }
83
84        Ok(Self { version, entry_count, entry_data })
85    }
86
87    /// Parse header, advance to the beginning of the body
88    fn parse_header<R: Read + Seek>(reader: &mut R) -> Result<((u8, u8), u32, u64)> {
89        const MAGIC_VERSION_LEN: usize = 2 + 2;
90
91        let version = {
92            let buf = reader.read_array::<MAGIC_VERSION_LEN>()?;
93            let (_, major, minor) = parse_buf!(buf, tuple((tag("RW"), le_u8, le_u8)));
94            (major, minor)
95        };
96
97        let (entry_count, entry_offset) = match version.0 {
98            2 => {
99                // Skip "useless" fields
100                reader.seek(SeekFrom::Current(84 + 8))?;
101                let buf = reader.read_array::<{2 + 2 + 4}>()?;
102                let (entry_offset, entry_size, entry_count) = parse_buf!(buf, tuple((le_u16, le_u16, le_u32)));
103                // Not supported because it's not needed, but could be
104                if entry_size != 32 {
105                    return Err(WadError::UnsupportedV2EntrySize(entry_size));
106                }
107                (entry_count, entry_offset as u64)
108            }
109            3 => {
110                // Skip "useless" fields
111                reader.seek(SeekFrom::Current(264))?;
112                let buf = reader.read_array::<4>()?;
113                let entry_count = parse_buf!(buf, le_u32);
114                let entry_offset = reader.stream_position()?;
115                (entry_count, entry_offset)
116            }
117            // Note: version 1 could be supported
118            _ => return Err(WadError::UnsupportedVersion(version.0, version.1)),
119        };
120
121        Ok((version, entry_count, entry_offset))
122    }
123
124    /// Iterate on file entries
125    pub fn iter_entries(&self) -> impl Iterator<Item=Result<WadEntry>> + '_ {
126        (0..self.entry_count as usize).map(move |i| self.parse_entry(i))
127    }
128
129    /// Parse entry at given index
130    fn parse_entry(&self, index: usize) -> Result<WadEntry> {
131        let offset = index * Self::ENTRY_LEN;
132        let buf = &self.entry_data[offset .. offset + Self::ENTRY_LEN];
133
134        let (path, offset, size, target_size, data_format, duplicate, first_subchunk_index, data_hash) =
135            parse_buf!(buf, tuple((
136                        map(le_u64, WadEntryHash::from), le_u32, le_u32, le_u32,
137                        map_res(le_u8, WadDataFormat::try_from),
138                        map(le_u8, |v| v != 0), le_u16, le_u64,
139            )));
140        Ok(WadEntry { path, offset, size, target_size, data_format, duplicate, first_subchunk_index, data_hash })
141    }
142
143    /// Find '.subchunktoc' file, if one exists
144    fn find_subchunk_toc(&self, hmapper: &WadHashMapper) -> Option<WadEntry> {
145        for entry in self.iter_entries().flatten() {
146            if let Some(path) = hmapper.get(entry.path.hash) {
147                if path.ends_with(".subchunktoc") {
148                    return Some(entry)
149                }
150            }
151        }
152        None
153    }
154}
155
156/// Read WAD archive files and their entries
157///
158/// This should be the prefered way to read a WAD file.
159#[derive(Debug)]
160pub struct WadReader<R: Read + Seek> {
161    reader: R,
162    wad: Wad,
163    subchunk_toc: Vec<WadSubchunkTocEntry>,
164}
165
166impl<R: Read + Seek> WadReader<R> {
167    /// Load subchunks data from a '.subchunktoc' file
168    ///
169    /// Return whether data has been found, and loaded
170    pub fn load_subchunk_toc(&mut self, hmapper: &WadHashMapper) -> Result<bool> {
171        if let Some(entry) = self.wad.find_subchunk_toc(hmapper) {
172            const TOC_ITEM_LEN: usize = 4 + 4 + 8;
173            let nitems = entry.target_size as usize / TOC_ITEM_LEN;
174            self.subchunk_toc.clear();
175            self.subchunk_toc.reserve_exact(nitems);
176
177            let mut subchunk_toc = Vec::new();
178            {
179                let mut reader = self.read_entry(&entry)?;
180                for _ in 0..nitems {
181                    let buf = reader.read_array::<TOC_ITEM_LEN>()?;
182                    let (size, target_size, data_hash) = parse_buf!(buf, tuple((le_u32, le_u32, le_u64)));
183                    subchunk_toc.push(WadSubchunkTocEntry { size, target_size, data_hash });
184                }
185            }
186            self.subchunk_toc = subchunk_toc;
187            Ok(true)
188        } else {
189            Ok(false)
190        }
191    }
192
193    /// Read an entry data
194    ///
195    /// The entry must not be a redirection.
196    pub fn read_entry(&mut self, entry: &WadEntry) -> Result<Box<dyn Read + '_>, WadError> {
197        self.reader.seek(SeekFrom::Start(entry.offset as u64))?;
198        let mut reader = Read::take(&mut self.reader, entry.size as u64);
199        match entry.data_format {
200            WadDataFormat::Uncompressed => {
201                Ok(Box::new(reader))
202            }
203            WadDataFormat::Gzip => Err(WadError::UnsupportedDataFormat(entry.data_format)),
204            WadDataFormat::Redirection => Err(WadError::UnsupportedDataFormat(entry.data_format)),
205            WadDataFormat::Zstd => {
206                let decoder = zstd::stream::read::Decoder::new(reader)?;
207                Ok(Box::new(decoder))
208            }
209            WadDataFormat::Chunked(subchunk_count) => {
210                if self.subchunk_toc.is_empty() {
211                    Err(WadError::MissingSubchunkToc)
212                } else {
213                    // Allocate the whole final buffer and read everything right no
214                    // It would be possible to implement a custom reader but that's not worth the
215                    // complexity
216                    let mut result = Vec::with_capacity(entry.target_size as usize);
217                    for i in 0..subchunk_count {
218                        let subchunk_entry = &self.subchunk_toc[(entry.first_subchunk_index + i as u16) as usize];
219                        let mut subchunk_reader = Read::take(&mut reader, subchunk_entry.size as u64);
220                        if subchunk_entry.size == subchunk_entry.target_size {
221                            // Assume no compression
222                            subchunk_reader.read_to_end(&mut result)?;
223                        } else {
224                            zstd::stream::read::Decoder::new(subchunk_reader)?.read_to_end(&mut result)?;
225                        }
226                    }
227                    Ok(Box::new(std::io::Cursor::new(result)))
228                }
229            }
230        }
231    }
232
233    /// Extract an entry to the given path
234    pub fn extract_entry(&mut self, entry: &WadEntry, path: &Path) -> Result<()> {
235        let mut reader = self.read_entry(entry)?;
236        GuardedFile::for_scope(path, |file| {
237            std::io::copy(&mut *reader, file)
238        })?;
239        Ok(())
240    }
241
242    /// Guess the extension of an entry
243    pub fn guess_entry_extension(&mut self, entry: &WadEntry) -> Option<&'static str> {
244        if entry.target_size == 0 {
245            return None;
246        }
247        let mut reader = self.read_entry(entry).ok()?;
248        guess_extension(&mut reader)
249    }
250
251    /// Iterate on entries
252    pub fn iter_entries(&self) -> impl Iterator<Item=Result<WadEntry>> + '_ {
253        self.wad.iter_entries()
254    }
255}
256
257/// Read WAD from a file
258pub type WadFile = WadReader<BufReader<File>>;
259
260impl WadFile {
261    /// Open a WAD from its path
262    pub fn open<P: AsRef<Path>>(path: P) -> Result<Self> {
263        let file = File::open(path.as_ref())?;
264        let mut reader = BufReader::new(file);
265        let wad = Wad::read(&mut reader)?;
266        Ok(Self { reader, wad, subchunk_toc: Vec::new(), })
267    }
268}
269
270
271/// Subchunk TOC item data
272#[derive(Debug)]
273struct WadSubchunkTocEntry {
274    /// Subchunk size, compressed
275    size: u32,
276    /// Subchunk size, uncompressed
277    target_size: u32,
278    /// First 8 bytes of sha256 hash of data
279    #[allow(dead_code)]
280    data_hash: u64,
281}
282
283
284/// Information on a single file in a WAD
285#[allow(dead_code)]
286#[derive(Debug)]
287pub struct WadEntry {
288    /// File path of the entry, hashed
289    pub path: WadEntryHash,
290    /// Data offset in the WAD
291    offset: u32,
292    /// Size in the WAD (possibly compressed)
293    size: u32,
294    /// Uncompressed size
295    target_size: u32,
296    /// Format of the entry data in the WAD file
297    data_format: WadDataFormat,
298    /// True for duplicate entries
299    duplicate: bool,
300    /// Index of the first subchunk (only relevant for chunked data)
301    first_subchunk_index: u16,
302    /// First 8 bytes of sha256 hash of data
303    data_hash: u64,
304}
305
306impl WadEntry {
307    /// Return `true` for a redirection entry
308    pub fn is_redirection(&self) -> bool {
309        self.data_format == WadDataFormat::Redirection
310    }
311}
312
313
314define_hash_type! {
315    /// Hash used by WAD entries
316    WadEntryHash(u64) => compute_wad_hash
317}
318
319
320#[allow(dead_code)]
321#[derive(Copy, Clone, Eq, PartialEq, Debug)]
322/// Type of a WAD entry
323pub enum WadDataFormat {
324    /// Uncompressed entry
325    Uncompressed,
326    /// Entry compressed with gzip
327    Gzip,
328    /// Entry redirection
329    Redirection,
330    /// Entry compressed with zstd
331    Zstd,
332    /// Entry split into *n* individual zstd-compressed chunks
333    ///
334    /// A "subchunk TOC" is required for such entries.
335    Chunked(u8),
336}
337
338impl TryFrom<u8> for WadDataFormat {
339    type Error = WadError;
340
341    fn try_from(value: u8) -> Result<Self, Self::Error> {
342        match value {
343            0 => Ok(Self::Uncompressed),
344            1 => Ok(Self::Gzip),
345            2 => Ok(Self::Redirection),
346            3 => Ok(Self::Zstd),
347            b if b & 0xf == 4 => Ok(Self::Chunked(b >> 4)),
348            _ => Err(WadError::InvalidDataFormat(value)),
349        }
350    }
351}
352
353
354/// Guess file extension from a reader
355fn guess_extension(reader: &mut dyn Read) -> Option<&'static str> {
356    const PREFIX_TO_EXT: &[(&[u8], &str)] = &[
357        (b"\xff\xd8\xff", "jpg"),
358        (b"\x89PNG\x0d\x0a\x1a\x0a", "png"),
359        (b"OggS", "ogg"),
360        (b"\x00\x01\x00\x00", "ttf"),
361        (b"\x1a\x45\xdf\xa3", "webm"),
362        (b"true", "ttf"),
363        (b"OTTO\0", "otf"),
364        (b"\"use strict\";", "min.js"),
365        (b"<template ", "template.html"),
366        (b"<!-- Elements -->", "template.html"),
367        (b"DDS ", "dds"),
368        (b"<svg", "svg"),
369        (b"PROP", "bin"),
370        (b"PTCH", "bin"),
371        (b"BKHD", "bnk"),
372        (b"r3d2Mesh", "scb"),
373        (b"r3d2anmd", "anm"),
374        (b"r3d2canm", "anm"),
375        (b"r3d2sklt", "skl"),
376        (b"r3d2", "wpk"),
377        (b"\x33\x22\x11\x00", "skn"),
378        (b"PreLoadBuildingBlocks = {", "preload"),
379        (b"\x1bLuaQ\x00\x01\x04\x04", "luabin"),
380        (b"\x1bLuaQ\x00\x01\x04\x08", "luabin64"),
381        (b"\x02\x3d\x00\x28", "troybin"),
382        (b"[ObjectBegin]", "sco"),
383        (b"OEGM", "mapgeo"),
384        (b"TEX\0", "tex"),
385    ];
386
387    // Use a sufficient length for all extensions (this is not checked)
388    let mut buf: [u8; 32] = [0; 32];
389    let n = reader.read(&mut buf).ok()?;
390    let buf = &buf[..n];
391    PREFIX_TO_EXT
392        .iter()
393        .find(|(prefix, _)| buf.starts_with(prefix))
394        .map(|(_, ext)| *ext)
395        // Try to parse as JSON
396        // Note: it won't detected JSON files that start with a BOM
397        .or_else(|| if match serde_json::from_slice::<serde_json::Value>(buf) {
398            Ok(_) => true,
399            Err(e) if e.is_eof() => true,
400            _ => false,
401        } {
402            Some("json")
403        } else {
404            None
405        })
406}
407
408
409/// Error in a WAD file
410#[allow(missing_docs)]
411#[derive(Error, Debug)]
412pub enum WadError {
413    #[error(transparent)]
414    Io(#[from] std::io::Error),
415    #[error("parsing error")]
416    Parsing(#[from] ParseError),
417    #[error("invalid WAD entry data format: {0}")]
418    InvalidDataFormat(u8),
419    #[error("WAD version not supported: {0}.{1}")]
420    UnsupportedVersion(u8, u8),
421    #[error("WAD entry data format not supported for reading: {0:?}")]
422    UnsupportedDataFormat(WadDataFormat),
423    #[error("WAD V2 entry size not supported: {0}")]
424    UnsupportedV2EntrySize(u16),
425    #[error("missing subchunk TOC to read chunked entry")]
426    MissingSubchunkToc,
427}
428