msg_tool\scripts\escude/
archive.rs

1//! Escu:de Archive File (.bin)
2use super::crypto::*;
3use crate::ext::io::*;
4use crate::scripts::base::*;
5use crate::types::*;
6use crate::utils::encoding::{decode_to_string, encode_string};
7use anyhow::Result;
8use std::collections::HashMap;
9use std::ffi::CString;
10use std::io::{Read, Seek, SeekFrom, Write};
11use std::sync::{Arc, Mutex};
12
13#[derive(Debug)]
14/// Escu:de Archive Builder
15pub struct EscudeBinArchiveBuilder {}
16
17impl EscudeBinArchiveBuilder {
18    /// Creates a new instance of `EscudeBinArchiveBuilder`
19    pub const fn new() -> Self {
20        EscudeBinArchiveBuilder {}
21    }
22}
23
24impl ScriptBuilder for EscudeBinArchiveBuilder {
25    fn default_encoding(&self) -> Encoding {
26        Encoding::Cp932
27    }
28
29    fn default_archive_encoding(&self) -> Option<Encoding> {
30        Some(Encoding::Cp932)
31    }
32
33    fn build_script(
34        &self,
35        data: Vec<u8>,
36        _filename: &str,
37        _encoding: Encoding,
38        archive_encoding: Encoding,
39        config: &ExtraConfig,
40        _archive: Option<&Box<dyn Script>>,
41    ) -> Result<Box<dyn Script + Send + Sync>> {
42        Ok(Box::new(EscudeBinArchive::new(
43            MemReader::new(data),
44            archive_encoding,
45            config,
46        )?))
47    }
48
49    fn build_script_from_file(
50        &self,
51        filename: &str,
52        _encoding: Encoding,
53        archive_encoding: Encoding,
54        config: &ExtraConfig,
55        _archive: Option<&Box<dyn Script>>,
56    ) -> Result<Box<dyn Script + Send + Sync>> {
57        if filename == "-" {
58            let data = crate::utils::files::read_file(filename)?;
59            Ok(Box::new(EscudeBinArchive::new(
60                MemReader::new(data),
61                archive_encoding,
62                config,
63            )?))
64        } else {
65            let f = std::fs::File::open(filename)?;
66            let reader = std::io::BufReader::new(f);
67            Ok(Box::new(EscudeBinArchive::new(
68                reader,
69                archive_encoding,
70                config,
71            )?))
72        }
73    }
74
75    fn build_script_from_reader<'a>(
76        &self,
77        reader: Box<dyn ReadSeek + Send + Sync + 'a>,
78        _filename: &str,
79        _encoding: Encoding,
80        archive_encoding: Encoding,
81        config: &ExtraConfig,
82        _archive: Option<&Box<dyn Script>>,
83    ) -> Result<Box<dyn Script + Send + Sync + 'a>> {
84        Ok(Box::new(EscudeBinArchive::new(
85            reader,
86            archive_encoding,
87            config,
88        )?))
89    }
90
91    fn extensions(&self) -> &'static [&'static str] {
92        &["bin"]
93    }
94
95    fn script_type(&self) -> &'static ScriptType {
96        &ScriptType::EscudeArc
97    }
98
99    fn is_this_format(&self, _filename: &str, buf: &[u8], buf_len: usize) -> Option<u8> {
100        if buf_len > 8 && buf.starts_with(b"ESC-ARC2") {
101            return Some(255);
102        }
103        None
104    }
105
106    fn is_archive(&self) -> bool {
107        true
108    }
109
110    fn create_archive(
111        &self,
112        filename: &str,
113        files: &[&str],
114        encoding: Encoding,
115        config: &ExtraConfig,
116    ) -> Result<Box<dyn Archive>> {
117        let f = std::fs::File::create(filename)?;
118        let writer = std::io::BufWriter::new(f);
119        let archive = EscudeBinArchiveWriter::new(writer, files, encoding, config)?;
120        Ok(Box::new(archive))
121    }
122}
123
124#[derive(Debug)]
125struct BinEntry {
126    name_offset: u32,
127    data_offset: u32,
128    length: u32,
129}
130
131struct Entry {
132    name: String,
133    data: MemReader,
134}
135
136impl Read for Entry {
137    fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
138        self.data.read(buf)
139    }
140}
141
142impl ArchiveContent for Entry {
143    fn name(&self) -> &str {
144        &self.name
145    }
146
147    fn size(&self) -> Option<u64> {
148        Some(self.data.data.len() as u64)
149    }
150
151    fn script_type(&self) -> Option<&ScriptType> {
152        if self.data.data.starts_with(b"ESCR1_00") {
153            Some(&ScriptType::Escude)
154        } else if self.data.data.starts_with(b"LIST") {
155            Some(&ScriptType::EscudeList)
156        } else {
157            None
158        }
159    }
160
161    fn data(&mut self) -> Result<Vec<u8>> {
162        Ok(self.data.data.clone())
163    }
164
165    fn to_data<'a>(&'a mut self) -> Result<Box<dyn ReadSeek + Send + Sync + 'a>> {
166        Ok(Box::new(&mut self.data))
167    }
168}
169
170#[derive(Debug)]
171/// Escu:de Binary Archive
172pub struct EscudeBinArchive<'b, T: Read + Seek + std::fmt::Debug + 'b> {
173    reader: Arc<Mutex<T>>,
174    file_count: u32,
175    entries: Vec<BinEntry>,
176    archive_encoding: Encoding,
177    _mark: std::marker::PhantomData<&'b ()>,
178}
179
180impl<'b, T: Read + Seek + std::fmt::Debug + 'b> EscudeBinArchive<'b, T> {
181    /// Creates a new `EscudeBinArchive` from a reader
182    ///
183    /// * `reader` - The reader to read the archive from
184    /// * `archive_encoding` - The encoding used for the archive filenames
185    /// * `config` - Extra configuration options
186    pub fn new(mut reader: T, archive_encoding: Encoding, _config: &ExtraConfig) -> Result<Self> {
187        let mut header = [0u8; 8];
188        reader.read_exact(&mut header)?;
189        if &header != b"ESC-ARC2" {
190            return Err(anyhow::anyhow!("Invalid Escude binary script header"));
191        }
192        reader.seek(SeekFrom::Start(0xC))?;
193        let mut crypto_reader = CryptoReader::new(&mut reader)?;
194        let file_count = crypto_reader.read_u32()?;
195        let _name_tbl_len = crypto_reader.read_u32()?;
196        let mut entries = Vec::with_capacity(file_count as usize);
197        for _ in 0..file_count {
198            let name_offset = crypto_reader.read_u32()?;
199            let data_offset = crypto_reader.read_u32()?;
200            let length = crypto_reader.read_u32()?;
201            entries.push(BinEntry {
202                name_offset,
203                data_offset,
204                length,
205            });
206        }
207        Ok(EscudeBinArchive {
208            reader: Arc::new(Mutex::new(reader)),
209            file_count,
210            entries,
211            archive_encoding,
212            _mark: std::marker::PhantomData,
213        })
214    }
215}
216
217impl<'b, T: Read + Seek + std::fmt::Debug + 'b> Script for EscudeBinArchive<'b, T> {
218    fn default_output_script_type(&self) -> OutputScriptType {
219        OutputScriptType::Json
220    }
221
222    fn default_format_type(&self) -> FormatOptions {
223        FormatOptions::None
224    }
225
226    fn is_archive(&self) -> bool {
227        true
228    }
229
230    fn iter_archive_filename<'a>(
231        &'a self,
232    ) -> Result<Box<dyn Iterator<Item = Result<String>> + 'a>> {
233        Ok(Box::new(EscudeBinArchiveIter {
234            entries: self.entries.iter(),
235            reader: self.reader.clone(),
236            file_count: self.file_count,
237            archive_encoding: self.archive_encoding,
238        }))
239    }
240
241    fn iter_archive_offset<'a>(&'a self) -> Result<Box<dyn Iterator<Item = Result<u64>> + 'a>> {
242        Ok(Box::new(
243            self.entries.iter().map(|e| Ok(e.data_offset as u64)),
244        ))
245    }
246
247    fn open_file<'a>(&'a self, index: usize) -> Result<Box<dyn ArchiveContent + Send + Sync + 'a>> {
248        if index >= self.entries.len() {
249            return Err(anyhow::anyhow!(
250                "Index out of bounds: {} (max: {})",
251                index,
252                self.entries.len()
253            ));
254        }
255        let entry = &self.entries[index];
256        let name = self
257            .reader
258            .cpeek_cstring_at(entry.name_offset as u64 + self.file_count as u64 * 12 + 0x14)?;
259        let name = decode_to_string(self.archive_encoding, name.as_bytes(), true)?;
260        let mut data = self
261            .reader
262            .cpeek_at_vec(entry.data_offset as u64, entry.length as usize)?;
263        if data.starts_with(b"acp") {
264            let mut decoder = match super::lzw::LZWDecoder::new(&data) {
265                Ok(decoder) => decoder,
266                Err(e) => return Err(anyhow::anyhow!("Failed to create LZW decoder: {}", e)),
267            };
268            data = decoder.unpack()?;
269        }
270        Ok(Box::new(Entry {
271            name,
272            data: MemReader::new(data),
273        }))
274    }
275}
276
277struct EscudeBinArchiveIter<'a, T: Iterator<Item = &'a BinEntry>, R: Read + Seek> {
278    entries: T,
279    reader: Arc<Mutex<R>>,
280    file_count: u32,
281    archive_encoding: Encoding,
282}
283
284impl<'a, T: Iterator<Item = &'a BinEntry>, R: Read + Seek> Iterator
285    for EscudeBinArchiveIter<'a, T, R>
286{
287    type Item = Result<String>;
288
289    fn next(&mut self) -> Option<Self::Item> {
290        let entry = match self.entries.next() {
291            Some(entry) => entry,
292            None => return None,
293        };
294        let name_offset = entry.name_offset as u64 + self.file_count as u64 * 12 + 0x14;
295        let name = match self.reader.cpeek_cstring_at(name_offset) {
296            Ok(name) => name,
297            Err(e) => return Some(Err(e.into())),
298        };
299        let name = match decode_to_string(self.archive_encoding, name.as_bytes(), true) {
300            Ok(name) => name,
301            Err(e) => return Some(Err(e.into())),
302        };
303        Some(Ok(name))
304    }
305}
306
307/// Escu:de Binary Archive Writer
308pub struct EscudeBinArchiveWriter<T: Write + Seek> {
309    writer: T,
310    headers: HashMap<String, BinEntry>,
311    name_tbl_len: u32,
312    fake: bool,
313}
314
315impl<T: Write + Seek> EscudeBinArchiveWriter<T> {
316    /// Creates a new `EscudeBinArchiveWriter`
317    ///
318    /// * `writer` - The writer to write the archive to
319    /// * `files` - The list of files to include in the archive
320    /// * `encoding` - The encoding used for the archive filenames
321    /// * `config` - Extra configuration options
322    pub fn new(
323        mut writer: T,
324        files: &[&str],
325        encoding: Encoding,
326        config: &ExtraConfig,
327    ) -> Result<Self> {
328        writer.write_all(b"ESC-ARC2")?;
329        let header_len = 0xC + 0xC * files.len();
330        let header = vec![0u8; header_len];
331        writer.write_all(&header)?;
332        let mut headers = HashMap::new();
333        for file in files {
334            let f = file.to_string();
335            let encoded = encode_string(encoding, file, false)?;
336            let encoded = CString::new(encoded)?;
337            let name_offset = writer.stream_position()? as u32;
338            writer.write_all(encoded.as_bytes_with_nul())?;
339            headers.insert(
340                f,
341                BinEntry {
342                    name_offset,
343                    data_offset: 0,
344                    length: 0,
345                },
346            );
347        }
348        let name_tbl_len = writer.stream_position()? as u32 - header_len as u32 - 0x8;
349        Ok(EscudeBinArchiveWriter {
350            writer,
351            headers,
352            name_tbl_len,
353            fake: config.escude_fake_compress,
354        })
355    }
356}
357
358impl<T: Write + Seek> Archive for EscudeBinArchiveWriter<T> {
359    fn new_file<'a>(
360        &'a mut self,
361        name: &str,
362        _size: Option<u64>,
363    ) -> Result<Box<dyn WriteSeek + 'a>> {
364        let entry = self
365            .headers
366            .get_mut(name)
367            .ok_or_else(|| anyhow::anyhow!("File '{}' not found in archive", name))?;
368        if entry.data_offset != 0 {
369            return Err(anyhow::anyhow!("File '{}' already exists in archive", name));
370        }
371        entry.data_offset = self.writer.stream_position()? as u32;
372        Ok(Box::new(EscudeBinArchiveFileWithLzw::new(
373            entry,
374            &mut self.writer,
375            self.fake,
376        )?))
377    }
378
379    fn write_header(&mut self) -> Result<()> {
380        self.writer.seek(SeekFrom::Start(0x8))?;
381        let mut crypto = CryptoWriter::new(&mut self.writer)?;
382        let file_count = self.headers.len() as u32;
383        crypto.write_u32(file_count)?;
384        crypto.write_u32(self.name_tbl_len)?;
385        let mut entries: Vec<_> = self.headers.values().collect();
386        entries.sort_by(|a, b| a.name_offset.cmp(&b.name_offset));
387        for entry in entries {
388            let name_offset = entry.name_offset - file_count * 12 - 0x14;
389            crypto.write_u32(name_offset)?;
390            crypto.write_u32(entry.data_offset)?;
391            crypto.write_u32(entry.length)?;
392        }
393        Ok(())
394    }
395}
396
397/// Escu:de Binary Archive File with LZW Compression
398pub struct EscudeBinArchiveFileWithLzw<'a, T: Write + Seek> {
399    writer: EscudeBinArchiveFile<'a, T>,
400    buf: MemWriter,
401    fake: bool,
402}
403
404impl<'a, T: Write + Seek> EscudeBinArchiveFileWithLzw<'a, T> {
405    fn new(header: &'a mut BinEntry, writer: &'a mut T, fake: bool) -> Result<Self> {
406        let writer = EscudeBinArchiveFile {
407            header,
408            writer,
409            pos: 0,
410        };
411        Ok(EscudeBinArchiveFileWithLzw {
412            writer,
413            buf: MemWriter::new(),
414            fake,
415        })
416    }
417}
418
419impl<'a, T: Write + Seek> Write for EscudeBinArchiveFileWithLzw<'a, T> {
420    fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
421        self.buf.write(buf)
422    }
423
424    fn flush(&mut self) -> std::io::Result<()> {
425        self.buf.flush()
426    }
427}
428
429impl<'a, T: Write + Seek> Seek for EscudeBinArchiveFileWithLzw<'a, T> {
430    fn seek(&mut self, pos: SeekFrom) -> std::io::Result<u64> {
431        self.buf.seek(pos)
432    }
433
434    fn stream_position(&mut self) -> std::io::Result<u64> {
435        self.buf.stream_position()
436    }
437
438    fn rewind(&mut self) -> std::io::Result<()> {
439        self.buf.rewind()
440    }
441}
442
443impl<'a, T: Write + Seek> Drop for EscudeBinArchiveFileWithLzw<'a, T> {
444    fn drop(&mut self) {
445        let buf = self.buf.as_slice();
446        let encoder = super::lzw::LZWEncoder::new();
447        let data = match encoder.encode(buf, self.fake) {
448            Ok(data) => data,
449            Err(e) => {
450                eprintln!("Failed to encode LZW data: {}", e);
451                crate::COUNTER.inc_error();
452                return;
453            }
454        };
455        match self.writer.write_all(&data) {
456            Ok(_) => {}
457            Err(e) => {
458                eprintln!("Failed to write LZW data: {}", e);
459                crate::COUNTER.inc_error();
460            }
461        }
462    }
463}
464
465/// Escu:de Binary Archive File
466pub struct EscudeBinArchiveFile<'a, T: Write + Seek> {
467    header: &'a mut BinEntry,
468    writer: &'a mut T,
469    pos: usize,
470}
471
472impl<'a, T: Write + Seek> Write for EscudeBinArchiveFile<'a, T> {
473    fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
474        self.writer.seek(SeekFrom::Start(
475            self.header.data_offset as u64 + self.pos as u64,
476        ))?;
477        let written = self.writer.write(buf)?;
478        self.pos += written;
479        self.header.length = self.header.length.max(self.pos as u32);
480        Ok(written)
481    }
482
483    fn flush(&mut self) -> std::io::Result<()> {
484        self.writer.flush()
485    }
486}
487
488impl<'a, T: Write + Seek> Seek for EscudeBinArchiveFile<'a, T> {
489    fn seek(&mut self, pos: SeekFrom) -> std::io::Result<u64> {
490        let new_pos = match pos {
491            SeekFrom::Start(offset) => offset as usize,
492            SeekFrom::End(offset) => {
493                if offset < 0 {
494                    if (-offset) as usize > self.header.length as usize {
495                        return Err(std::io::Error::new(
496                            std::io::ErrorKind::InvalidInput,
497                            "Seek from end exceeds file length",
498                        ));
499                    }
500                    self.header.length as usize - (-offset) as usize
501                } else {
502                    self.header.length as usize + offset as usize
503                }
504            }
505            SeekFrom::Current(offset) => {
506                if offset < 0 {
507                    if (-offset) as usize > self.pos {
508                        return Err(std::io::Error::new(
509                            std::io::ErrorKind::InvalidInput,
510                            "Seek from current exceeds current position",
511                        ));
512                    }
513                    self.pos.saturating_sub((-offset) as usize)
514                } else {
515                    self.pos + offset as usize
516                }
517            }
518        };
519        self.pos = new_pos;
520        Ok(self.pos as u64)
521    }
522}