msg_tool\scripts\bgi\archive/
v2.rs

1//! Buriko General Interpreter/Ethornell Archive File Version 2 (.arc)
2use super::bse::*;
3use super::dsc::*;
4use crate::ext::io::*;
5use crate::scripts::base::*;
6use crate::types::*;
7use crate::utils::encoding::encode_string;
8use crate::utils::struct_pack::*;
9use anyhow::Result;
10use msg_tool_macro::*;
11use std::collections::HashMap;
12use std::io::{Read, Seek, SeekFrom, Write};
13use std::sync::{Arc, Mutex};
14
15#[derive(Debug)]
16/// Builder for BGI Archive Version 2
17pub struct BgiArchiveBuilder {}
18
19impl BgiArchiveBuilder {
20    /// Creates a new instance of `BgiArchiveBuilder`.
21    pub const fn new() -> Self {
22        BgiArchiveBuilder {}
23    }
24}
25
26impl ScriptBuilder for BgiArchiveBuilder {
27    fn default_encoding(&self) -> Encoding {
28        Encoding::Cp932
29    }
30
31    fn default_archive_encoding(&self) -> Option<Encoding> {
32        Some(Encoding::Cp932)
33    }
34
35    fn build_script(
36        &self,
37        data: Vec<u8>,
38        filename: &str,
39        _encoding: Encoding,
40        archive_encoding: Encoding,
41        config: &ExtraConfig,
42        _archive: Option<&Box<dyn Script>>,
43    ) -> Result<Box<dyn Script>> {
44        Ok(Box::new(BgiArchive::new(
45            MemReader::new(data),
46            archive_encoding,
47            config,
48            filename,
49        )?))
50    }
51
52    fn build_script_from_file(
53        &self,
54        filename: &str,
55        _encoding: Encoding,
56        archive_encoding: Encoding,
57        config: &ExtraConfig,
58        _archive: Option<&Box<dyn Script>>,
59    ) -> Result<Box<dyn Script>> {
60        if filename == "-" {
61            let data = crate::utils::files::read_file(filename)?;
62            Ok(Box::new(BgiArchive::new(
63                MemReader::new(data),
64                archive_encoding,
65                config,
66                filename,
67            )?))
68        } else {
69            let f = std::fs::File::open(filename)?;
70            let reader = std::io::BufReader::new(f);
71            Ok(Box::new(BgiArchive::new(
72                reader,
73                archive_encoding,
74                config,
75                filename,
76            )?))
77        }
78    }
79
80    fn build_script_from_reader(
81        &self,
82        reader: Box<dyn ReadSeek>,
83        filename: &str,
84        _encoding: Encoding,
85        archive_encoding: Encoding,
86        config: &ExtraConfig,
87        _archive: Option<&Box<dyn Script>>,
88    ) -> Result<Box<dyn Script>> {
89        Ok(Box::new(BgiArchive::new(
90            reader,
91            archive_encoding,
92            config,
93            filename,
94        )?))
95    }
96
97    fn extensions(&self) -> &'static [&'static str] {
98        &["arc"]
99    }
100
101    fn script_type(&self) -> &'static ScriptType {
102        &ScriptType::BGIArcV2
103    }
104
105    fn is_this_format(&self, _filename: &str, buf: &[u8], buf_len: usize) -> Option<u8> {
106        if buf_len >= 12 && buf.starts_with(b"BURIKO ARC20") {
107            return Some(255);
108        }
109        None
110    }
111
112    fn is_archive(&self) -> bool {
113        true
114    }
115
116    fn create_archive(
117        &self,
118        filename: &str,
119        files: &[&str],
120        encoding: Encoding,
121        config: &ExtraConfig,
122    ) -> Result<Box<dyn Archive>> {
123        let f = std::fs::File::create(filename)?;
124        let writer = std::io::BufWriter::new(f);
125        Ok(Box::new(BgiArchiveWriter::new(
126            writer, files, encoding, config,
127        )?))
128    }
129}
130
131#[derive(Clone, Debug, StructPack, StructUnpack)]
132struct BgiFileHeader {
133    #[fstring = 0x60]
134    filename: String,
135    offset: u32,
136    size: u32,
137    #[fvec = 8]
138    _unk: Vec<u8>,
139    #[fvec = 16]
140    _padding: Vec<u8>,
141}
142
143struct Entry<T: Read + Seek> {
144    header: BgiFileHeader,
145    reader: Arc<Mutex<T>>,
146    pos: usize,
147    base_offset: u64,
148    script_type: Option<ScriptType>,
149}
150
151impl<T: Read + Seek> ArchiveContent for Entry<T> {
152    fn name(&self) -> &str {
153        &self.header.filename
154    }
155
156    fn script_type(&self) -> Option<&ScriptType> {
157        self.script_type.as_ref()
158    }
159}
160
161impl<T: Read + Seek> Read for Entry<T> {
162    fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
163        let mut reader = self.reader.lock().map_err(|e| {
164            std::io::Error::new(
165                std::io::ErrorKind::Other,
166                format!("Failed to lock mutex: {}", e),
167            )
168        })?;
169        reader.seek(SeekFrom::Start(
170            self.base_offset + self.header.offset as u64 + self.pos as u64,
171        ))?;
172        let bytes_read = buf.len().min(self.header.size as usize - self.pos);
173        if bytes_read == 0 {
174            return Ok(0);
175        }
176        let bytes_read = reader.read(&mut buf[..bytes_read])?;
177        self.pos += bytes_read;
178        Ok(bytes_read)
179    }
180}
181
182impl<T: Read + Seek> Seek for Entry<T> {
183    fn seek(&mut self, pos: SeekFrom) -> std::io::Result<u64> {
184        let new_pos = match pos {
185            SeekFrom::Start(offset) => offset as usize,
186            SeekFrom::End(offset) => {
187                if offset < 0 {
188                    if (-offset) as usize > self.header.size as usize {
189                        return Err(std::io::Error::new(
190                            std::io::ErrorKind::InvalidInput,
191                            "Seek from end exceeds file length",
192                        ));
193                    }
194                    self.header.size as usize - (-offset) as usize
195                } else {
196                    self.header.size as usize + offset as usize
197                }
198            }
199            SeekFrom::Current(offset) => {
200                if offset < 0 {
201                    if (-offset) as usize > self.pos {
202                        return Err(std::io::Error::new(
203                            std::io::ErrorKind::InvalidInput,
204                            "Seek from current exceeds current position",
205                        ));
206                    }
207                    self.pos.saturating_sub((-offset) as usize)
208                } else {
209                    self.pos + offset as usize
210                }
211            }
212        };
213        self.pos = new_pos;
214        Ok(self.pos as u64)
215    }
216
217    fn stream_position(&mut self) -> std::io::Result<u64> {
218        Ok(self.pos as u64)
219    }
220}
221
222struct MemEntry<F: Fn(&[u8], usize, &str) -> Option<&'static ScriptType>> {
223    name: String,
224    data: MemReader,
225    detect: F,
226}
227
228impl<F: Fn(&[u8], usize, &str) -> Option<&'static ScriptType>> Read for MemEntry<F> {
229    fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
230        self.data.read(buf)
231    }
232}
233
234impl<F: Fn(&[u8], usize, &str) -> Option<&'static ScriptType>> ArchiveContent for MemEntry<F> {
235    fn name(&self) -> &str {
236        &self.name
237    }
238
239    fn script_type(&self) -> Option<&ScriptType> {
240        (self.detect)(&self.data.data, self.data.data.len(), &self.name)
241    }
242
243    fn data(&mut self) -> Result<Vec<u8>> {
244        Ok(self.data.data.clone())
245    }
246
247    fn to_data<'a>(&'a mut self) -> Result<Box<dyn ReadSeek + 'a>> {
248        Ok(Box::new(&mut self.data))
249    }
250}
251
252#[derive(Debug)]
253/// BGI Archive Version 2
254pub struct BgiArchive<T: Read + Seek + std::fmt::Debug> {
255    reader: Arc<Mutex<T>>,
256    entries: Vec<BgiFileHeader>,
257    base_offset: u64,
258    #[cfg(feature = "bgi-img")]
259    is_sysgrp_arc: bool,
260}
261
262impl<T: Read + Seek + std::fmt::Debug> BgiArchive<T> {
263    /// Creates a new BGI Archive from a reader.
264    ///
265    /// * `reader` - The reader to read the archive from.
266    /// * `archive_encoding` - The encoding used for the archive.
267    /// * `config` - Extra configuration options.
268    /// * `filename` - The name of the archive file (used for detecting sysgrp.arc).
269    pub fn new(
270        mut reader: T,
271        archive_encoding: Encoding,
272        _config: &ExtraConfig,
273        _filename: &str,
274    ) -> Result<Self> {
275        let mut header = [0u8; 12];
276        reader.read_exact(&mut header)?;
277        if !header.starts_with(b"BURIKO ARC20") {
278            return Err(anyhow::anyhow!("Invalid BGI archive header"));
279        }
280
281        let file_count = reader.read_u32()?;
282        let mut entries = Vec::with_capacity(file_count as usize);
283        for _ in 0..file_count {
284            let entry = BgiFileHeader::unpack(&mut reader, false, archive_encoding)?;
285            entries.push(entry);
286        }
287
288        #[cfg(feature = "bgi-img")]
289        let is_sysgrp_arc = _config.bgi_is_sysgrp_arc.unwrap_or_else(|| {
290            std::path::Path::new(&_filename.to_lowercase())
291                .file_name()
292                .map(|f| f == "sysgrp.arc")
293                .unwrap_or(false)
294        });
295
296        Ok(BgiArchive {
297            reader: Arc::new(Mutex::new(reader)),
298            entries,
299            base_offset: 16 + (file_count as u64 * 0x80),
300            #[cfg(feature = "bgi-img")]
301            is_sysgrp_arc,
302        })
303    }
304}
305
306impl<T: Read + Seek + std::fmt::Debug + 'static> Script for BgiArchive<T> {
307    fn default_output_script_type(&self) -> OutputScriptType {
308        OutputScriptType::Json
309    }
310
311    fn default_format_type(&self) -> FormatOptions {
312        FormatOptions::None
313    }
314
315    fn is_archive(&self) -> bool {
316        true
317    }
318
319    fn iter_archive_filename<'a>(
320        &'a self,
321    ) -> Result<Box<dyn Iterator<Item = Result<String>> + 'a>> {
322        Ok(Box::new(
323            self.entries.iter().map(|e| Ok(e.filename.clone())),
324        ))
325    }
326
327    fn iter_archive_offset<'a>(&'a self) -> Result<Box<dyn Iterator<Item = Result<u64>> + 'a>> {
328        Ok(Box::new(self.entries.iter().map(|e| Ok(e.offset as u64))))
329    }
330
331    fn open_file<'a>(&'a self, index: usize) -> Result<Box<dyn ArchiveContent + 'a>> {
332        if index >= self.entries.len() {
333            return Err(anyhow::anyhow!(
334                "Index out of bounds: {} (max: {})",
335                index,
336                self.entries.len()
337            ));
338        }
339        let entry = &self.entries[index];
340        let mut entry = Entry {
341            header: entry.clone(),
342            reader: self.reader.clone(),
343            pos: 0,
344            base_offset: self.base_offset,
345            script_type: None,
346        };
347        let mut buf = [0u8; 32];
348        match entry.read(&mut buf) {
349            Ok(_) => {}
350            Err(e) => {
351                return Err(anyhow::anyhow!(
352                    "Failed to read entry '{}': {}",
353                    entry.header.filename,
354                    e
355                ));
356            }
357        }
358        entry.pos = 0;
359        if buf.starts_with(b"DSC FORMAT 1.00") {
360            let data = match entry.data() {
361                Ok(data) => data,
362                Err(e) => {
363                    return Err(anyhow::anyhow!(
364                        "Failed to read DSC data for '{}': {}",
365                        entry.header.filename,
366                        e
367                    ));
368                }
369            };
370            entry.pos = 0;
371            let dsc = match DscDecoder::new(&data) {
372                Ok(dsc) => dsc,
373                Err(e) => {
374                    return Err(anyhow::anyhow!(
375                        "Failed to create DSC decoder for '{}': {}",
376                        entry.header.filename,
377                        e
378                    ));
379                }
380            };
381            let decoded = match dsc.unpack() {
382                Ok(decoded) => decoded,
383                Err(e) => {
384                    return Err(anyhow::anyhow!(
385                        "Failed to unpack DSC data for '{}': {}",
386                        entry.header.filename,
387                        e
388                    ));
389                }
390            };
391            let reader = MemReader::new(decoded);
392            if reader.data.starts_with(b"BSE 1.") {
393                match BseReader::new(reader, detect_script_type, &entry.header.filename) {
394                    Ok(bse_reader) => {
395                        return Ok(Box::new(bse_reader));
396                    }
397                    Err(e) => {
398                        return Err(anyhow::anyhow!(
399                            "Failed to create BSE reader for '{}': {}",
400                            entry.header.filename,
401                            e
402                        ));
403                    }
404                };
405            }
406            return Ok(Box::new(MemEntry {
407                name: entry.header.filename.clone(),
408                data: reader,
409                #[cfg(feature = "bgi-img")]
410                detect: if self.is_sysgrp_arc {
411                    detect_script_type_sysgrp
412                } else {
413                    detect_script_type
414                },
415                #[cfg(not(feature = "bgi-img"))]
416                detect: detect_script_type,
417            }));
418        }
419        if buf.starts_with(b"BSE 1.") {
420            let filename = entry.header.filename.clone();
421            #[cfg(feature = "bgi-img")]
422            let detect = if self.is_sysgrp_arc {
423                detect_script_type_sysgrp
424            } else {
425                detect_script_type
426            };
427            #[cfg(not(feature = "bgi-img"))]
428            let detect = detect_script_type;
429            match BseReader::new(entry, detect, &filename) {
430                Ok(mut bse_reader) => {
431                    if bse_reader.is_dsc() {
432                        let data = match bse_reader.data() {
433                            Ok(data) => data,
434                            Err(e) => {
435                                return Err(anyhow::anyhow!(
436                                    "Failed to read BSE data for '{}': {}",
437                                    &filename,
438                                    e
439                                ));
440                            }
441                        };
442                        let dsc = match DscDecoder::new(&data) {
443                            Ok(dsc) => dsc,
444                            Err(e) => {
445                                return Err(anyhow::anyhow!(
446                                    "Failed to create DSC decoder for '{}': {}",
447                                    &filename,
448                                    e
449                                ));
450                            }
451                        };
452                        let decoded = match dsc.unpack() {
453                            Ok(decoded) => decoded,
454                            Err(e) => {
455                                return Err(anyhow::anyhow!(
456                                    "Failed to unpack DSC data for '{}': {}",
457                                    &filename,
458                                    e
459                                ));
460                            }
461                        };
462                        let reader = MemReader::new(decoded);
463                        return Ok(Box::new(MemEntry {
464                            name: filename,
465                            data: reader,
466                            detect,
467                        }));
468                    }
469                    return Ok(Box::new(bse_reader));
470                }
471                Err(e) => {
472                    return Err(anyhow::anyhow!(
473                        "Failed to create BSE reader for '{}': {}",
474                        &filename,
475                        e
476                    ));
477                }
478            };
479        }
480        #[cfg(feature = "bgi-img")]
481        if self.is_sysgrp_arc {
482            entry.script_type = Some(ScriptType::BGIImg);
483        } else {
484            entry.script_type =
485                detect_script_type(&buf, buf.len(), &entry.header.filename).cloned();
486        }
487        #[cfg(not(feature = "bgi-img"))]
488        {
489            entry.script_type =
490                detect_script_type(&buf, buf.len(), &entry.header.filename).cloned();
491        }
492        Ok(Box::new(entry))
493    }
494}
495
496fn detect_script_type(buf: &[u8], buf_len: usize, filename: &str) -> Option<&'static ScriptType> {
497    if buf_len >= 28 && buf.starts_with(b"BurikoCompiledScriptVer1.00\0") {
498        return Some(&ScriptType::BGI);
499    }
500    #[cfg(feature = "bgi-img")]
501    if buf_len >= 16 && buf.starts_with(b"CompressedBG___") {
502        return Some(&ScriptType::BGICbg);
503    }
504    #[cfg(feature = "bgi-audio")]
505    if buf_len >= 8 && buf[4..].starts_with(b"bw  ") {
506        return Some(&ScriptType::BGIAudio);
507    }
508    let filename = filename.to_lowercase();
509    if filename.ends_with("._bp") {
510        return Some(&ScriptType::BGIBp);
511    } else if filename.ends_with("._bsi") {
512        return Some(&ScriptType::BGIBsi);
513    }
514    None
515}
516
517#[cfg(feature = "bgi-img")]
518fn detect_script_type_sysgrp(
519    _buf: &[u8],
520    _buf_len: usize,
521    _filename: &str,
522) -> Option<&'static ScriptType> {
523    Some(&ScriptType::BGIImg)
524}
525
526/// BGI Archive Writer for Version 2
527pub struct BgiArchiveWriter<T: Write + Seek> {
528    writer: T,
529    headers: HashMap<String, BgiFileHeader>,
530    compress_file: bool,
531    encoding: Encoding,
532    min_len: usize,
533}
534
535impl<T: Write + Seek> BgiArchiveWriter<T> {
536    /// Creates a new BGI Archive Writer.
537    ///
538    /// * `writer` - The writer to write the archive to.
539    /// * `files` - The list of files to include in the archive.
540    /// * `encoding` - The encoding used for the archive.
541    /// * `config` - Extra configuration options.
542    pub fn new(
543        mut writer: T,
544        files: &[&str],
545        encoding: Encoding,
546        config: &ExtraConfig,
547    ) -> Result<Self> {
548        writer.write_all(b"BURIKO ARC20")?;
549        let file_count = files.len();
550        writer.write_u32(file_count as u32)?;
551        let mut headers = HashMap::new();
552        for file in files {
553            let header = BgiFileHeader {
554                filename: file.to_string(),
555                offset: 0,
556                size: 0,
557                _unk: vec![0; 8],
558                _padding: vec![0; 16],
559            };
560            header.pack(&mut writer, false, encoding)?;
561            headers.insert(file.to_string(), header);
562        }
563        Ok(BgiArchiveWriter {
564            writer,
565            headers,
566            compress_file: config.bgi_compress_file,
567            encoding,
568            min_len: config.bgi_compress_min_len,
569        })
570    }
571}
572
573impl<T: Write + Seek> Archive for BgiArchiveWriter<T> {
574    fn new_file<'a>(&'a mut self, name: &str) -> Result<Box<dyn WriteSeek + 'a>> {
575        let entry = self
576            .headers
577            .get_mut(name)
578            .ok_or_else(|| anyhow::anyhow!("File '{}' not found in archive", name))?;
579        if entry.offset != 0 || entry.size != 0 {
580            return Err(anyhow::anyhow!("File '{}' already exists in archive", name));
581        }
582        self.writer.seek(SeekFrom::End(0))?;
583        entry.offset = self.writer.stream_position()? as u32;
584        let file = BgiArchiveFile {
585            header: entry,
586            writer: &mut self.writer,
587            pos: 0,
588        };
589        Ok(if self.compress_file {
590            Box::new(BgiArchiveFileWithDsc::new(file, self.min_len))
591        } else {
592            Box::new(file)
593        })
594    }
595
596    fn write_header(&mut self) -> Result<()> {
597        self.writer.seek(SeekFrom::Start(0x10))?;
598        let base_offset = self.headers.len() as u32 * 0x80 + 16;
599        let mut files = self.headers.iter_mut().map(|(_, d)| d).collect::<Vec<_>>();
600        files.sort_by_key(|f| f.offset);
601        for file in files {
602            file.offset -= base_offset;
603            file.pack(&mut self.writer, false, self.encoding)?;
604        }
605        Ok(())
606    }
607}
608
609/// BGI Archive File Writer (Not compressed)
610pub struct BgiArchiveFile<'a, T: Write + Seek> {
611    header: &'a mut BgiFileHeader,
612    writer: &'a mut T,
613    pos: usize,
614}
615
616impl<'a, T: Write + Seek> Write for BgiArchiveFile<'a, T> {
617    fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
618        self.writer
619            .seek(SeekFrom::Start(self.header.offset as u64 + self.pos as u64))?;
620        let bytes_written = self.writer.write(buf)?;
621        self.pos += bytes_written;
622        self.header.size = self.header.size.max(self.pos as u32);
623        Ok(bytes_written)
624    }
625
626    fn flush(&mut self) -> std::io::Result<()> {
627        self.writer.flush()
628    }
629}
630
631impl<'a, T: Write + Seek> Seek for BgiArchiveFile<'a, T> {
632    fn seek(&mut self, pos: SeekFrom) -> std::io::Result<u64> {
633        let new_pos = match pos {
634            SeekFrom::Start(offset) => offset as usize,
635            SeekFrom::End(offset) => {
636                if offset < 0 {
637                    if (-offset) as usize > self.header.size as usize {
638                        return Err(std::io::Error::new(
639                            std::io::ErrorKind::InvalidInput,
640                            "Seek from end exceeds file length",
641                        ));
642                    }
643                    self.header.size as usize - (-offset) as usize
644                } else {
645                    self.header.size as usize + offset as usize
646                }
647            }
648            SeekFrom::Current(offset) => {
649                if offset < 0 {
650                    if (-offset) as usize > self.pos {
651                        return Err(std::io::Error::new(
652                            std::io::ErrorKind::InvalidInput,
653                            "Seek from current exceeds current position",
654                        ));
655                    }
656                    self.pos.saturating_sub((-offset) as usize)
657                } else {
658                    self.pos + offset as usize
659                }
660            }
661        };
662        self.pos = new_pos;
663        Ok(self.pos as u64)
664    }
665}
666
667/// BGI Archive File Writer with DSC compression
668pub struct BgiArchiveFileWithDsc<'a, T: Write + Seek> {
669    writer: BgiArchiveFile<'a, T>,
670    buf: MemWriter,
671    min_len: usize,
672}
673
674impl<'a, T: Write + Seek> BgiArchiveFileWithDsc<'a, T> {
675    /// Creates a new BGI Archive File Writer with DSC compression.
676    ///
677    /// * `writer` - The writer to write the archive file to.
678    /// * `min_len` - The minimum length for LZSS compression.
679    pub fn new(writer: BgiArchiveFile<'a, T>, min_len: usize) -> Self {
680        BgiArchiveFileWithDsc {
681            writer,
682            buf: MemWriter::new(),
683            min_len,
684        }
685    }
686}
687
688impl<'a, T: Write + Seek> Write for BgiArchiveFileWithDsc<'a, T> {
689    fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
690        self.buf.write(buf)
691    }
692
693    fn flush(&mut self) -> std::io::Result<()> {
694        self.buf.flush()
695    }
696}
697
698impl<'a, T: Write + Seek> Seek for BgiArchiveFileWithDsc<'a, T> {
699    fn seek(&mut self, pos: SeekFrom) -> std::io::Result<u64> {
700        self.buf.seek(pos)
701    }
702
703    fn stream_position(&mut self) -> std::io::Result<u64> {
704        self.buf.stream_position()
705    }
706
707    fn rewind(&mut self) -> std::io::Result<()> {
708        self.buf.rewind()
709    }
710}
711
712impl<'a, T: Write + Seek> Drop for BgiArchiveFileWithDsc<'a, T> {
713    fn drop(&mut self) {
714        let buf = self.buf.as_slice();
715        let encoder = DscEncoder::new(&mut self.writer, self.min_len);
716        if let Err(e) = encoder.pack(&buf) {
717            eprintln!("Failed to write DSC data: {}", e);
718            crate::COUNTER.inc_error();
719        }
720    }
721}