msg_tool\scripts\hexen_haus\archive/
wag.rs1use 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; const SECTION_IMAGE: u32 = 0x4447_4D49; const SECTION_NAME: u32 = 0x454E_4E46; #[derive(Debug)]
18pub struct HexenHausWagArchiveBuilder;
20
21impl HexenHausWagArchiveBuilder {
22 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)]
123pub 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 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}