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>> {
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>> {
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(
76        &self,
77        reader: Box<dyn ReadSeek>,
78        _filename: &str,
79        _encoding: Encoding,
80        archive_encoding: Encoding,
81        config: &ExtraConfig,
82        _archive: Option<&Box<dyn Script>>,
83    ) -> Result<Box<dyn Script>> {
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 script_type(&self) -> Option<&ScriptType> {
148        if self.data.data.starts_with(b"ESCR1_00") {
149            Some(&ScriptType::Escude)
150        } else if self.data.data.starts_with(b"LIST") {
151            Some(&ScriptType::EscudeList)
152        } else {
153            None
154        }
155    }
156
157    fn data(&mut self) -> Result<Vec<u8>> {
158        Ok(self.data.data.clone())
159    }
160
161    fn to_data<'a>(&'a mut self) -> Result<Box<dyn ReadSeek + 'a>> {
162        Ok(Box::new(&mut self.data))
163    }
164}
165
166#[derive(Debug)]
167/// Escu:de Binary Archive
168pub struct EscudeBinArchive<T: Read + Seek + std::fmt::Debug> {
169    reader: Arc<Mutex<T>>,
170    file_count: u32,
171    entries: Vec<BinEntry>,
172    archive_encoding: Encoding,
173}
174
175impl<T: Read + Seek + std::fmt::Debug> EscudeBinArchive<T> {
176    /// Creates a new `EscudeBinArchive` from a reader
177    ///
178    /// * `reader` - The reader to read the archive from
179    /// * `archive_encoding` - The encoding used for the archive filenames
180    /// * `config` - Extra configuration options
181    pub fn new(mut reader: T, archive_encoding: Encoding, _config: &ExtraConfig) -> Result<Self> {
182        let mut header = [0u8; 8];
183        reader.read_exact(&mut header)?;
184        if &header != b"ESC-ARC2" {
185            return Err(anyhow::anyhow!("Invalid Escude binary script header"));
186        }
187        reader.seek(SeekFrom::Start(0xC))?;
188        let mut crypto_reader = CryptoReader::new(&mut reader)?;
189        let file_count = crypto_reader.read_u32()?;
190        let _name_tbl_len = crypto_reader.read_u32()?;
191        let mut entries = Vec::with_capacity(file_count as usize);
192        for _ in 0..file_count {
193            let name_offset = crypto_reader.read_u32()?;
194            let data_offset = crypto_reader.read_u32()?;
195            let length = crypto_reader.read_u32()?;
196            entries.push(BinEntry {
197                name_offset,
198                data_offset,
199                length,
200            });
201        }
202        Ok(EscudeBinArchive {
203            reader: Arc::new(Mutex::new(reader)),
204            file_count,
205            entries,
206            archive_encoding,
207        })
208    }
209}
210
211impl<T: Read + Seek + std::fmt::Debug + std::any::Any> Script for EscudeBinArchive<T> {
212    fn default_output_script_type(&self) -> OutputScriptType {
213        OutputScriptType::Json
214    }
215
216    fn default_format_type(&self) -> FormatOptions {
217        FormatOptions::None
218    }
219
220    fn is_archive(&self) -> bool {
221        true
222    }
223
224    fn iter_archive_filename<'a>(
225        &'a self,
226    ) -> Result<Box<dyn Iterator<Item = Result<String>> + 'a>> {
227        Ok(Box::new(EscudeBinArchiveIter {
228            entries: self.entries.iter(),
229            reader: self.reader.clone(),
230            file_count: self.file_count,
231            archive_encoding: self.archive_encoding,
232        }))
233    }
234
235    fn iter_archive_offset<'a>(&'a self) -> Result<Box<dyn Iterator<Item = Result<u64>> + 'a>> {
236        Ok(Box::new(
237            self.entries.iter().map(|e| Ok(e.data_offset as u64)),
238        ))
239    }
240
241    fn open_file<'a>(&'a self, index: usize) -> Result<Box<dyn ArchiveContent + 'a>> {
242        if index >= self.entries.len() {
243            return Err(anyhow::anyhow!(
244                "Index out of bounds: {} (max: {})",
245                index,
246                self.entries.len()
247            ));
248        }
249        let entry = &self.entries[index];
250        let name = self
251            .reader
252            .cpeek_cstring_at(entry.name_offset as u64 + self.file_count as u64 * 12 + 0x14)?;
253        let name = decode_to_string(self.archive_encoding, name.as_bytes(), true)?;
254        let mut data = self
255            .reader
256            .cpeek_at_vec(entry.data_offset as u64, entry.length as usize)?;
257        if data.starts_with(b"acp") {
258            let mut decoder = match super::lzw::LZWDecoder::new(&data) {
259                Ok(decoder) => decoder,
260                Err(e) => return Err(anyhow::anyhow!("Failed to create LZW decoder: {}", e)),
261            };
262            data = decoder.unpack()?;
263        }
264        Ok(Box::new(Entry {
265            name,
266            data: MemReader::new(data),
267        }))
268    }
269}
270
271struct EscudeBinArchiveIter<'a, T: Iterator<Item = &'a BinEntry>, R: Read + Seek> {
272    entries: T,
273    reader: Arc<Mutex<R>>,
274    file_count: u32,
275    archive_encoding: Encoding,
276}
277
278impl<'a, T: Iterator<Item = &'a BinEntry>, R: Read + Seek> Iterator
279    for EscudeBinArchiveIter<'a, T, R>
280{
281    type Item = Result<String>;
282
283    fn next(&mut self) -> Option<Self::Item> {
284        let entry = match self.entries.next() {
285            Some(entry) => entry,
286            None => return None,
287        };
288        let name_offset = entry.name_offset as u64 + self.file_count as u64 * 12 + 0x14;
289        let name = match self.reader.cpeek_cstring_at(name_offset) {
290            Ok(name) => name,
291            Err(e) => return Some(Err(e.into())),
292        };
293        let name = match decode_to_string(self.archive_encoding, name.as_bytes(), true) {
294            Ok(name) => name,
295            Err(e) => return Some(Err(e.into())),
296        };
297        Some(Ok(name))
298    }
299}
300
301/// Escu:de Binary Archive Writer
302pub struct EscudeBinArchiveWriter<T: Write + Seek> {
303    writer: T,
304    headers: HashMap<String, BinEntry>,
305    name_tbl_len: u32,
306    fake: bool,
307}
308
309impl<T: Write + Seek> EscudeBinArchiveWriter<T> {
310    /// Creates a new `EscudeBinArchiveWriter`
311    ///
312    /// * `writer` - The writer to write the archive to
313    /// * `files` - The list of files to include in the archive
314    /// * `encoding` - The encoding used for the archive filenames
315    /// * `config` - Extra configuration options
316    pub fn new(
317        mut writer: T,
318        files: &[&str],
319        encoding: Encoding,
320        config: &ExtraConfig,
321    ) -> Result<Self> {
322        writer.write_all(b"ESC-ARC2")?;
323        let header_len = 0xC + 0xC * files.len();
324        let header = vec![0u8; header_len];
325        writer.write_all(&header)?;
326        let mut headers = HashMap::new();
327        for file in files {
328            let f = file.to_string();
329            let encoded = encode_string(encoding, file, false)?;
330            let encoded = CString::new(encoded)?;
331            let name_offset = writer.stream_position()? as u32;
332            writer.write_all(encoded.as_bytes_with_nul())?;
333            headers.insert(
334                f,
335                BinEntry {
336                    name_offset,
337                    data_offset: 0,
338                    length: 0,
339                },
340            );
341        }
342        let name_tbl_len = writer.stream_position()? as u32 - header_len as u32 - 0x8;
343        Ok(EscudeBinArchiveWriter {
344            writer,
345            headers,
346            name_tbl_len,
347            fake: config.escude_fake_compress,
348        })
349    }
350}
351
352impl<T: Write + Seek> Archive for EscudeBinArchiveWriter<T> {
353    fn new_file<'a>(&'a mut self, name: &str) -> Result<Box<dyn WriteSeek + 'a>> {
354        let entry = self
355            .headers
356            .get_mut(name)
357            .ok_or_else(|| anyhow::anyhow!("File '{}' not found in archive", name))?;
358        if entry.data_offset != 0 {
359            return Err(anyhow::anyhow!("File '{}' already exists in archive", name));
360        }
361        entry.data_offset = self.writer.stream_position()? as u32;
362        Ok(Box::new(EscudeBinArchiveFileWithLzw::new(
363            entry,
364            &mut self.writer,
365            self.fake,
366        )?))
367    }
368
369    fn write_header(&mut self) -> Result<()> {
370        self.writer.seek(SeekFrom::Start(0x8))?;
371        let mut crypto = CryptoWriter::new(&mut self.writer)?;
372        let file_count = self.headers.len() as u32;
373        crypto.write_u32(file_count)?;
374        crypto.write_u32(self.name_tbl_len)?;
375        let mut entries: Vec<_> = self.headers.values().collect();
376        entries.sort_by(|a, b| a.name_offset.cmp(&b.name_offset));
377        for entry in entries {
378            let name_offset = entry.name_offset - file_count * 12 - 0x14;
379            crypto.write_u32(name_offset)?;
380            crypto.write_u32(entry.data_offset)?;
381            crypto.write_u32(entry.length)?;
382        }
383        Ok(())
384    }
385}
386
387/// Escu:de Binary Archive File with LZW Compression
388pub struct EscudeBinArchiveFileWithLzw<'a, T: Write + Seek> {
389    writer: EscudeBinArchiveFile<'a, T>,
390    buf: MemWriter,
391    fake: bool,
392}
393
394impl<'a, T: Write + Seek> EscudeBinArchiveFileWithLzw<'a, T> {
395    fn new(header: &'a mut BinEntry, writer: &'a mut T, fake: bool) -> Result<Self> {
396        let writer = EscudeBinArchiveFile {
397            header,
398            writer,
399            pos: 0,
400        };
401        Ok(EscudeBinArchiveFileWithLzw {
402            writer,
403            buf: MemWriter::new(),
404            fake,
405        })
406    }
407}
408
409impl<'a, T: Write + Seek> Write for EscudeBinArchiveFileWithLzw<'a, T> {
410    fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
411        self.buf.write(buf)
412    }
413
414    fn flush(&mut self) -> std::io::Result<()> {
415        self.buf.flush()
416    }
417}
418
419impl<'a, T: Write + Seek> Seek for EscudeBinArchiveFileWithLzw<'a, T> {
420    fn seek(&mut self, pos: SeekFrom) -> std::io::Result<u64> {
421        self.buf.seek(pos)
422    }
423
424    fn stream_position(&mut self) -> std::io::Result<u64> {
425        self.buf.stream_position()
426    }
427
428    fn rewind(&mut self) -> std::io::Result<()> {
429        self.buf.rewind()
430    }
431}
432
433impl<'a, T: Write + Seek> Drop for EscudeBinArchiveFileWithLzw<'a, T> {
434    fn drop(&mut self) {
435        let buf = self.buf.as_slice();
436        let encoder = super::lzw::LZWEncoder::new();
437        let data = match encoder.encode(buf, self.fake) {
438            Ok(data) => data,
439            Err(e) => {
440                eprintln!("Failed to encode LZW data: {}", e);
441                crate::COUNTER.inc_error();
442                return;
443            }
444        };
445        match self.writer.write_all(&data) {
446            Ok(_) => {}
447            Err(e) => {
448                eprintln!("Failed to write LZW data: {}", e);
449                crate::COUNTER.inc_error();
450            }
451        }
452    }
453}
454
455/// Escu:de Binary Archive File
456pub struct EscudeBinArchiveFile<'a, T: Write + Seek> {
457    header: &'a mut BinEntry,
458    writer: &'a mut T,
459    pos: usize,
460}
461
462impl<'a, T: Write + Seek> Write for EscudeBinArchiveFile<'a, T> {
463    fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
464        self.writer.seek(SeekFrom::Start(
465            self.header.data_offset as u64 + self.pos as u64,
466        ))?;
467        let written = self.writer.write(buf)?;
468        self.pos += written;
469        self.header.length = self.header.length.max(self.pos as u32);
470        Ok(written)
471    }
472
473    fn flush(&mut self) -> std::io::Result<()> {
474        self.writer.flush()
475    }
476}
477
478impl<'a, T: Write + Seek> Seek for EscudeBinArchiveFile<'a, T> {
479    fn seek(&mut self, pos: SeekFrom) -> std::io::Result<u64> {
480        let new_pos = match pos {
481            SeekFrom::Start(offset) => offset as usize,
482            SeekFrom::End(offset) => {
483                if offset < 0 {
484                    if (-offset) as usize > self.header.length as usize {
485                        return Err(std::io::Error::new(
486                            std::io::ErrorKind::InvalidInput,
487                            "Seek from end exceeds file length",
488                        ));
489                    }
490                    self.header.length as usize - (-offset) as usize
491                } else {
492                    self.header.length as usize + offset as usize
493                }
494            }
495            SeekFrom::Current(offset) => {
496                if offset < 0 {
497                    if (-offset) as usize > self.pos {
498                        return Err(std::io::Error::new(
499                            std::io::ErrorKind::InvalidInput,
500                            "Seek from current exceeds current position",
501                        ));
502                    }
503                    self.pos.saturating_sub((-offset) as usize)
504                } else {
505                    self.pos + offset as usize
506                }
507            }
508        };
509        self.pos = new_pos;
510        Ok(self.pos as u64)
511    }
512}