msg_tool\scripts\hexen_haus\archive/
wag.rs

1//! HexenHaus WAG archive (.wag)
2use crate::ext::io::*;
3use crate::scripts::base::*;
4use crate::types::*;
5use crate::utils::encoding::decode_to_string;
6use anyhow::{Result, anyhow};
7use std::convert::TryFrom;
8use std::io::{Read, Seek, SeekFrom};
9use std::sync::{Arc, Mutex};
10
11const WAG_SIGNATURE: &[u8; 4] = b"IAF_";
12const OFFSET_TABLE_START: u64 = 0x4A;
13const DATA_SIGNATURE: u32 = 0x4154_4144; // 'DATA'
14const SECTION_IMAGE: u32 = 0x4447_4D49; // 'IMGD'
15const SECTION_NAME: u32 = 0x454E_4E46; // 'FNNE'
16
17#[derive(Debug)]
18/// HexenHaus WAG archive builder
19pub struct HexenHausWagArchiveBuilder;
20
21impl HexenHausWagArchiveBuilder {
22    /// Creates a new `HexenHausWagArchiveBuilder`
23    pub const fn new() -> Self {
24        HexenHausWagArchiveBuilder
25    }
26}
27
28impl ScriptBuilder for HexenHausWagArchiveBuilder {
29    fn default_encoding(&self) -> Encoding {
30        Encoding::Cp932
31    }
32
33    fn default_archive_encoding(&self) -> Option<Encoding> {
34        Some(Encoding::Cp932)
35    }
36
37    fn build_script(
38        &self,
39        buf: Vec<u8>,
40        _filename: &str,
41        _encoding: Encoding,
42        archive_encoding: Encoding,
43        config: &ExtraConfig,
44        _archive: Option<&Box<dyn Script>>,
45    ) -> Result<Box<dyn Script + Send + Sync>> {
46        Ok(Box::new(HexenHausWagArchive::new(
47            MemReader::new(buf),
48            archive_encoding,
49            config,
50        )?))
51    }
52
53    fn build_script_from_file(
54        &self,
55        filename: &str,
56        _encoding: Encoding,
57        archive_encoding: Encoding,
58        config: &ExtraConfig,
59        _archive: Option<&Box<dyn Script>>,
60    ) -> Result<Box<dyn Script + Send + Sync>> {
61        if filename == "-" {
62            let data = crate::utils::files::read_file(filename)?;
63            return Ok(Box::new(HexenHausWagArchive::new(
64                MemReader::new(data),
65                archive_encoding,
66                config,
67            )?));
68        }
69        let file = std::fs::File::open(filename)?;
70        let reader = std::io::BufReader::new(file);
71        Ok(Box::new(HexenHausWagArchive::new(
72            reader,
73            archive_encoding,
74            config,
75        )?))
76    }
77
78    fn build_script_from_reader<'a>(
79        &self,
80        reader: Box<dyn ReadSeek + Send + Sync + 'a>,
81        _filename: &str,
82        _encoding: Encoding,
83        archive_encoding: Encoding,
84        config: &ExtraConfig,
85        _archive: Option<&Box<dyn Script>>,
86    ) -> Result<Box<dyn Script + Send + Sync + 'a>> {
87        Ok(Box::new(HexenHausWagArchive::new(
88            reader,
89            archive_encoding,
90            config,
91        )?))
92    }
93
94    fn extensions(&self) -> &'static [&'static str] {
95        &["wag"]
96    }
97
98    fn script_type(&self) -> &'static ScriptType {
99        &ScriptType::HexenHausWag
100    }
101
102    fn is_this_format(&self, _filename: &str, buf: &[u8], buf_len: usize) -> Option<u8> {
103        if buf_len >= WAG_SIGNATURE.len() && buf.starts_with(WAG_SIGNATURE) {
104            Some(10)
105        } else {
106            None
107        }
108    }
109
110    fn is_archive(&self) -> bool {
111        true
112    }
113}
114
115#[derive(Debug, Clone)]
116struct HexenHausWagEntry {
117    name: String,
118    offset: u64,
119    size: u32,
120}
121
122#[derive(Debug)]
123/// HexenHaus WAG archive reader
124pub struct HexenHausWagArchive<'b, T: Read + Seek + std::fmt::Debug + 'b> {
125    reader: Arc<Mutex<T>>,
126    file_length: u64,
127    entries: Vec<HexenHausWagEntry>,
128    _mark: std::marker::PhantomData<&'b ()>,
129}
130
131impl<'b, T: Read + Seek + std::fmt::Debug + 'b> HexenHausWagArchive<'b, T> {
132    /// Creates a new `HexenHausWagArchive`
133    pub fn new(mut reader: T, archive_encoding: Encoding, _config: &ExtraConfig) -> Result<Self> {
134        reader.seek(SeekFrom::Start(0))?;
135        let mut signature = [0u8; 4];
136        reader.read_exact(&mut signature)?;
137        if signature != *WAG_SIGNATURE {
138            return Err(anyhow!("Invalid HexenHaus WAG signature"));
139        }
140
141        reader.seek(SeekFrom::Start(6))?;
142        let file_count = reader.read_u32()?;
143        if file_count == 0 {
144            return Err(anyhow!("WAG archive contains no files"));
145        }
146
147        let file_length = reader.seek(SeekFrom::End(0))?;
148        reader.seek(SeekFrom::Start(0))?;
149
150        let reader = Arc::new(Mutex::new(reader));
151        let entry_count = file_count as usize;
152
153        let offset_table_len = entry_count
154            .checked_mul(4)
155            .ok_or_else(|| anyhow!("Offset table length overflow"))?;
156        let offset_table_len_u64 =
157            u64::try_from(offset_table_len).map_err(|_| anyhow!("Offset table length overflow"))?;
158        let offset_table_end = OFFSET_TABLE_START
159            .checked_add(offset_table_len_u64)
160            .ok_or_else(|| anyhow!("Offset table exceeds addressable range"))?;
161        if offset_table_end > file_length {
162            return Err(anyhow!("Offset table extends beyond file length"));
163        }
164
165        let mut offsets_raw = vec![0u8; offset_table_len];
166        read_decrypted_exact(&reader, OFFSET_TABLE_START, &mut offsets_raw)?;
167        if offsets_raw.len() % 4 != 0 {
168            return Err(anyhow!("Invalid offset table length"));
169        }
170
171        let mut offsets = Vec::with_capacity(entry_count);
172        let mut offsets_reader = MemReader::new(offsets_raw);
173        while !offsets_reader.is_eof() {
174            let offset = offsets_reader.read_u32()?;
175            offsets.push(offset as u64);
176        }
177
178        let mut entries = Vec::with_capacity(entry_count);
179        for offset in offsets {
180            if offset
181                .checked_add(10)
182                .map_or(true, |value| value > file_length)
183            {
184                continue;
185            }
186            let mut header_buf = [0u8; 10];
187            read_decrypted_exact(&reader, offset, &mut header_buf)?;
188            let mut header_reader = MemReaderRef::new(&header_buf);
189            let signature = header_reader.read_u32()?;
190            if signature != DATA_SIGNATURE {
191                continue;
192            }
193            let section_count = header_reader.read_u32()?;
194
195            let mut entry_name: Option<String> = None;
196            let mut data_offset = 0u64;
197            let mut data_size = 0u32;
198            let mut position = offset
199                .checked_add(10)
200                .ok_or_else(|| anyhow!("Entry position overflow"))?;
201
202            for _ in 0..section_count {
203                if position >= file_length {
204                    break;
205                }
206                let mut section_sig_buf = [0u8; 4];
207                read_decrypted_exact(&reader, position, &mut section_sig_buf)?;
208                let section_signature = u32::from_le_bytes(section_sig_buf);
209                position = position
210                    .checked_add(4)
211                    .ok_or_else(|| anyhow!("Section position overflow"))?;
212
213                match section_signature {
214                    SECTION_IMAGE => {
215                        let mut size_buf = [0u8; 4];
216                        read_decrypted_exact(&reader, position, &mut size_buf)?;
217                        let image_size = u32::from_le_bytes(size_buf);
218                        let imgd_start = position
219                            .checked_sub(4)
220                            .ok_or_else(|| anyhow!("Invalid IMGD start position"))?;
221                        data_offset = imgd_start;
222                        data_size = image_size
223                            .checked_add(0x10)
224                            .ok_or_else(|| anyhow!("IMGD section size overflow"))?;
225                        position = position
226                            .checked_add(4)
227                            .ok_or_else(|| anyhow!("Section position overflow"))?;
228                        position = position
229                            .checked_add(u64::from(image_size))
230                            .ok_or_else(|| anyhow!("Section position overflow"))?;
231                        position = position
232                            .checked_add(2)
233                            .ok_or_else(|| anyhow!("Section position overflow"))?;
234                    }
235                    SECTION_NAME => {
236                        let mut name_len_buf = [0u8; 4];
237                        read_decrypted_exact(&reader, position, &mut name_len_buf)?;
238                        let raw_name_len = u32::from_le_bytes(name_len_buf);
239                        position = position
240                            .checked_add(4)
241                            .ok_or_else(|| anyhow!("Section position overflow"))?;
242
243                        let mut skip_buf = [0u8; 2];
244                        read_decrypted_exact(&reader, position, &mut skip_buf)?;
245                        position = position
246                            .checked_add(2)
247                            .ok_or_else(|| anyhow!("Section position overflow"))?;
248
249                        let name_length = raw_name_len.saturating_sub(2) as usize;
250                        if name_length > 0 {
251                            if position > file_length {
252                                break;
253                            }
254                            let remaining = file_length - position;
255                            let name_length_u64 = u64::try_from(name_length)
256                                .map_err(|_| anyhow!("Name length overflow"))?;
257                            if name_length_u64 > remaining {
258                                break;
259                            }
260                            let mut name_buf = vec![0u8; name_length];
261                            read_decrypted_exact(&reader, position, &mut name_buf)?;
262                            position = position
263                                .checked_add(name_length_u64)
264                                .ok_or_else(|| anyhow!("Section position overflow"))?;
265                            let name = decode_to_string(archive_encoding, &name_buf, true)?;
266                            if !name.is_empty() {
267                                entry_name = Some(name);
268                            }
269                        }
270
271                        let mut skip_tail = [0u8; 2];
272                        read_decrypted_exact(&reader, position, &mut skip_tail)?;
273                        position = position
274                            .checked_add(2)
275                            .ok_or_else(|| anyhow!("Section position overflow"))?;
276                    }
277                    _ => {
278                        let mut section_size_buf = [0u8; 4];
279                        read_decrypted_exact(&reader, position, &mut section_size_buf)?;
280                        let section_size = u32::from_le_bytes(section_size_buf);
281                        position = position
282                            .checked_add(4)
283                            .ok_or_else(|| anyhow!("Section position overflow"))?;
284                        position = position
285                            .checked_add(u64::from(section_size))
286                            .ok_or_else(|| anyhow!("Section position overflow"))?;
287                        position = position
288                            .checked_add(2)
289                            .ok_or_else(|| anyhow!("Section position overflow"))?;
290                    }
291                }
292            }
293
294            if data_size == 0 {
295                continue;
296            }
297            if data_offset
298                .checked_add(u64::from(data_size))
299                .map_or(true, |end| end > file_length)
300            {
301                continue;
302            }
303            if let Some(name) = entry_name {
304                if !name.is_empty() {
305                    entries.push(HexenHausWagEntry {
306                        name,
307                        offset: data_offset,
308                        size: data_size,
309                    });
310                }
311            }
312        }
313
314        if entries.is_empty() {
315            return Err(anyhow!("WAG archive contains no readable entries"));
316        }
317
318        Ok(HexenHausWagArchive {
319            reader,
320            file_length,
321            entries,
322            _mark: std::marker::PhantomData,
323        })
324    }
325
326    fn read_decrypted_slice(&self, offset: u64, size: usize) -> Result<Vec<u8>> {
327        let requested = u64::try_from(size).map_err(|_| anyhow!("Requested size overflow"))?;
328        let length = requested.min(self.file_length.saturating_sub(offset));
329        let read_len = usize::try_from(length).map_err(|_| anyhow!("Unable to allocate buffer"))?;
330        let mut buf = vec![0u8; read_len];
331        if read_len == 0 {
332            return Ok(buf);
333        }
334        read_decrypted_exact(&self.reader, offset, &mut buf)?;
335        Ok(buf)
336    }
337}
338
339impl<'b, T: Read + Seek + std::fmt::Debug + Send + Sync + 'b> Script
340    for HexenHausWagArchive<'b, T>
341{
342    fn default_output_script_type(&self) -> OutputScriptType {
343        OutputScriptType::Json
344    }
345
346    fn default_format_type(&self) -> FormatOptions {
347        FormatOptions::None
348    }
349
350    fn is_archive(&self) -> bool {
351        true
352    }
353
354    fn iter_archive_filename<'a>(
355        &'a self,
356    ) -> Result<Box<dyn Iterator<Item = Result<String>> + 'a>> {
357        Ok(Box::new(
358            self.entries.iter().map(|entry| Ok(entry.name.clone())),
359        ))
360    }
361
362    fn iter_archive_offset<'a>(&'a self) -> Result<Box<dyn Iterator<Item = Result<u64>> + 'a>> {
363        Ok(Box::new(self.entries.iter().map(|entry| Ok(entry.offset))))
364    }
365
366    fn open_file<'a>(&'a self, index: usize) -> Result<Box<dyn ArchiveContent + Send + Sync + 'a>> {
367        if index >= self.entries.len() {
368            return Err(anyhow!(
369                "Index out of bounds: {} (total files: {})",
370                index,
371                self.entries.len()
372            ));
373        }
374        let entry = self.entries[index].clone();
375        let header =
376            self.read_decrypted_slice(entry.offset, usize::min(entry.size as usize, 16))?;
377        let typ = super::detect_script_type(&entry.name, &header);
378        Ok(Box::new(WagEntry {
379            header: entry,
380            reader: self.reader.clone(),
381            pos: 0,
382            typ,
383        }))
384    }
385}
386
387#[derive(Debug)]
388struct WagEntry<T: Read + Seek> {
389    header: HexenHausWagEntry,
390    reader: Arc<Mutex<T>>,
391    pos: u64,
392    typ: Option<ScriptType>,
393}
394
395impl<T: Read + Seek + std::fmt::Debug + Send + Sync> ArchiveContent for WagEntry<T> {
396    fn name(&self) -> &str {
397        &self.header.name
398    }
399
400    fn size(&self) -> Option<u64> {
401        Some(self.header.size as u64)
402    }
403
404    fn script_type(&self) -> Option<&ScriptType> {
405        self.typ.as_ref()
406    }
407
408    fn to_data<'a>(&'a mut self) -> Result<Box<dyn ReadSeek + Send + Sync + 'a>> {
409        Ok(Box::new(self))
410    }
411}
412
413impl<T: Read + Seek> Read for WagEntry<T> {
414    fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
415        let mut reader = self.reader.lock().map_err(|e| {
416            std::io::Error::new(
417                std::io::ErrorKind::Other,
418                format!("Failed to lock mutex: {}", e),
419            )
420        })?;
421        reader.seek(SeekFrom::Start(self.header.offset + self.pos))?;
422        let total_size = u64::from(self.header.size);
423        if self.pos >= total_size {
424            return Ok(0);
425        }
426        let remaining = total_size - self.pos;
427        let remaining_usize = match usize::try_from(remaining) {
428            Ok(value) => value,
429            Err(_) => usize::MAX,
430        };
431        let to_read = remaining_usize.min(buf.len());
432        if to_read == 0 {
433            return Ok(0);
434        }
435        let bytes_read = reader.read(&mut buf[..to_read])?;
436        drop(reader);
437        for byte in &mut buf[..bytes_read] {
438            *byte = byte.rotate_right(4);
439        }
440        self.pos = self.pos.saturating_add(bytes_read as u64);
441        Ok(bytes_read)
442    }
443}
444
445impl<T: Read + Seek> Seek for WagEntry<T> {
446    fn seek(&mut self, pos: SeekFrom) -> std::io::Result<u64> {
447        let new_pos = match pos {
448            SeekFrom::Start(offset) => offset,
449            SeekFrom::End(offset) => {
450                let size = i64::from(self.header.size);
451                let target = size.checked_add(offset).ok_or_else(|| {
452                    std::io::Error::new(
453                        std::io::ErrorKind::InvalidInput,
454                        "Seek from end exceeds file length",
455                    )
456                })?;
457                if target < 0 {
458                    return Err(std::io::Error::new(
459                        std::io::ErrorKind::InvalidInput,
460                        "Seek from end before start",
461                    ));
462                }
463                target as u64
464            }
465            SeekFrom::Current(offset) => {
466                let current = i64::try_from(self.pos).map_err(|_| {
467                    std::io::Error::new(
468                        std::io::ErrorKind::InvalidInput,
469                        "Current position overflow",
470                    )
471                })?;
472                let target = current.checked_add(offset).ok_or_else(|| {
473                    std::io::Error::new(
474                        std::io::ErrorKind::InvalidInput,
475                        "Seek from current caused overflow",
476                    )
477                })?;
478                if target < 0 {
479                    return Err(std::io::Error::new(
480                        std::io::ErrorKind::InvalidInput,
481                        "Seek from current before start",
482                    ));
483                }
484                target as u64
485            }
486        };
487        self.pos = new_pos;
488        Ok(self.pos)
489    }
490
491    fn stream_position(&mut self) -> std::io::Result<u64> {
492        Ok(self.pos)
493    }
494}
495
496fn read_decrypted_exact<T: Read + Seek>(
497    reader: &Arc<Mutex<T>>,
498    offset: u64,
499    buf: &mut [u8],
500) -> Result<()> {
501    if buf.is_empty() {
502        return Ok(());
503    }
504    let mut guard = reader
505        .lock()
506        .map_err(|e| anyhow!("Failed to lock reader: {}", e))?;
507    guard.seek(SeekFrom::Start(offset))?;
508    guard.read_exact(buf)?;
509    drop(guard);
510    for byte in buf.iter_mut() {
511        *byte = byte.rotate_right(4);
512    }
513    Ok(())
514}