1use 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
46type Result<T, E = WadError> = std::result::Result<T, E>;
48
49
50pub struct Wad {
55 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 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 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 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 if entry_size != 32 {
105 return Err(WadError::UnsupportedV2EntrySize(entry_size));
106 }
107 (entry_count, entry_offset as u64)
108 }
109 3 => {
110 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 _ => return Err(WadError::UnsupportedVersion(version.0, version.1)),
119 };
120
121 Ok((version, entry_count, entry_offset))
122 }
123
124 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 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 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#[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 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 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 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 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 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 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 pub fn iter_entries(&self) -> impl Iterator<Item=Result<WadEntry>> + '_ {
253 self.wad.iter_entries()
254 }
255}
256
257pub type WadFile = WadReader<BufReader<File>>;
259
260impl WadFile {
261 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#[derive(Debug)]
273struct WadSubchunkTocEntry {
274 size: u32,
276 target_size: u32,
278 #[allow(dead_code)]
280 data_hash: u64,
281}
282
283
284#[allow(dead_code)]
286#[derive(Debug)]
287pub struct WadEntry {
288 pub path: WadEntryHash,
290 offset: u32,
292 size: u32,
294 target_size: u32,
296 data_format: WadDataFormat,
298 duplicate: bool,
300 first_subchunk_index: u16,
302 data_hash: u64,
304}
305
306impl WadEntry {
307 pub fn is_redirection(&self) -> bool {
309 self.data_format == WadDataFormat::Redirection
310 }
311}
312
313
314define_hash_type! {
315 WadEntryHash(u64) => compute_wad_hash
317}
318
319
320#[allow(dead_code)]
321#[derive(Copy, Clone, Eq, PartialEq, Debug)]
322pub enum WadDataFormat {
324 Uncompressed,
326 Gzip,
328 Redirection,
330 Zstd,
332 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
354fn 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 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 .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#[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