1use 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)]
16pub struct BgiArchiveBuilder {}
18
19impl BgiArchiveBuilder {
20 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)]
253pub 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 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
526pub 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 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
609pub 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
667pub 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 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}