msg_tool\scripts\ex_hibit\arc/
grp.rs1use crate::ext::io::*;
3use crate::scripts::base::*;
4use crate::types::*;
5use anyhow::{Context, Result};
6use std::fmt::Debug;
7use std::io::{Read, Seek, SeekFrom};
8use std::path::{Path, PathBuf};
9use std::sync::{Arc, Mutex};
10
11#[derive(Debug)]
12pub struct ExHibitGrpArchiveBuilder {}
14
15impl ExHibitGrpArchiveBuilder {
16 pub const fn new() -> Self {
18 Self {}
19 }
20}
21
22impl ScriptBuilder for ExHibitGrpArchiveBuilder {
23 fn default_encoding(&self) -> Encoding {
24 Encoding::Cp932
25 }
26
27 fn default_archive_encoding(&self) -> Option<Encoding> {
28 Some(Encoding::Cp932)
29 }
30
31 fn build_script(
32 &self,
33 data: Vec<u8>,
34 filename: &str,
35 _encoding: Encoding,
36 archive_encoding: Encoding,
37 config: &ExtraConfig,
38 _archive: Option<&Box<dyn Script>>,
39 ) -> Result<Box<dyn Script + Send + Sync>> {
40 Ok(Box::new(ExHibitGrpArchive::new(
41 MemReader::new(data),
42 filename,
43 archive_encoding,
44 config,
45 )?))
46 }
47
48 fn build_script_from_file(
49 &self,
50 filename: &str,
51 _encoding: Encoding,
52 archive_encoding: Encoding,
53 config: &ExtraConfig,
54 _archive: Option<&Box<dyn Script>>,
55 ) -> Result<Box<dyn Script + Send + Sync>> {
56 if filename == "-" {
57 return Err(anyhow::anyhow!(
58 "Reading ExHibit GRP from stdin is not supported; provide a file path."
59 ));
60 }
61 let file = std::fs::File::open(filename)
62 .with_context(|| format!("Failed to open '{}'.", filename))?;
63 let reader = std::io::BufReader::new(file);
64 Ok(Box::new(ExHibitGrpArchive::new(
65 reader,
66 filename,
67 archive_encoding,
68 config,
69 )?))
70 }
71
72 fn build_script_from_reader<'a>(
73 &self,
74 reader: Box<dyn ReadSeek + Send + Sync + 'a>,
75 filename: &str,
76 _encoding: Encoding,
77 archive_encoding: Encoding,
78 config: &ExtraConfig,
79 _archive: Option<&Box<dyn Script>>,
80 ) -> Result<Box<dyn Script + Send + Sync + 'a>> {
81 Ok(Box::new(ExHibitGrpArchive::new(
82 reader,
83 filename,
84 archive_encoding,
85 config,
86 )?))
87 }
88
89 fn extensions(&self) -> &'static [&'static str] {
90 &["grp"]
91 }
92
93 fn script_type(&self) -> &'static ScriptType {
94 &ScriptType::ExHibitGrp
95 }
96
97 fn is_this_format(&self, filename: &str, buf: &[u8], buf_len: usize) -> Option<u8> {
98 if !matches_grp_name(filename) {
99 return None;
100 }
101 if buf_len >= 4 && buf.starts_with(b"AiFS") {
102 return None;
103 }
104 Some(10)
105 }
106
107 fn is_archive(&self) -> bool {
108 true
109 }
110}
111
112#[derive(Clone, Debug)]
113struct GrpFileEntry {
114 name: String,
115 offset: u64,
116 size: u64,
117}
118
119#[derive(Debug)]
120pub struct ExHibitGrpArchive<'b, T: Read + Seek + Debug + 'b> {
122 reader: Arc<Mutex<T>>,
123 entries: Vec<GrpFileEntry>,
124 _mark: std::marker::PhantomData<&'b ()>,
125}
126
127impl<'b, T: Read + Seek + Debug + 'b> ExHibitGrpArchive<'b, T> {
128 fn new(
129 mut reader: T,
130 filename: &str,
131 _archive_encoding: Encoding,
132 _config: &ExtraConfig,
133 ) -> Result<Self> {
134 let mut header = [0u8; 4];
135 reader
136 .peek_exact_at(0, &mut header)
137 .context("Failed to read GRP header.")?;
138 if &header == b"AiFS" {
139 return Err(anyhow::anyhow!(
140 "Input file is a TOC (AiFS) rather than an archive."
141 ));
142 }
143
144 let path = Path::new(filename);
145 let (toc_path, arc_index) = locate_toc_file(path).context("Failed to locate TOC file.")?;
146
147 let archive_size = (&mut reader)
148 .stream_length()
149 .context("Failed to determine archive size.")?;
150
151 let entries = parse_toc_entries(&toc_path, arc_index, archive_size)
152 .with_context(|| format!("Failed to parse TOC '{}'.", toc_path.display()))?;
153
154 Ok(Self {
155 reader: Arc::new(Mutex::new(reader)),
156 entries,
157 _mark: std::marker::PhantomData,
158 })
159 }
160}
161
162impl<'b, T: Read + Seek + Debug + Send + Sync + 'b> Script for ExHibitGrpArchive<'b, T> {
163 fn default_output_script_type(&self) -> OutputScriptType {
164 OutputScriptType::Json
165 }
166
167 fn default_format_type(&self) -> FormatOptions {
168 FormatOptions::None
169 }
170
171 fn is_archive(&self) -> bool {
172 true
173 }
174
175 fn iter_archive_filename<'a>(
176 &'a self,
177 ) -> Result<Box<dyn Iterator<Item = Result<String>> + 'a>> {
178 Ok(Box::new(
179 self.entries.iter().map(|entry| Ok(entry.name.clone())),
180 ))
181 }
182
183 fn iter_archive_offset<'a>(&'a self) -> Result<Box<dyn Iterator<Item = Result<u64>> + 'a>> {
184 Ok(Box::new(self.entries.iter().map(|entry| Ok(entry.offset))))
185 }
186
187 fn open_file<'a>(&'a self, index: usize) -> Result<Box<dyn ArchiveContent + Send + Sync + 'a>> {
188 if index >= self.entries.len() {
189 return Err(anyhow::anyhow!(
190 "Index out of bounds: {} (max: {}).",
191 index,
192 self.entries.len()
193 ));
194 }
195 let entry = self.entries[index].clone();
196 Ok(Box::new(GrpEntry::new(entry, self.reader.clone())))
197 }
198}
199
200#[derive(Debug)]
201struct GrpEntry<T: Read + Seek> {
202 info: GrpFileEntry,
203 reader: Arc<Mutex<T>>,
204 pos: u64,
205}
206
207impl<T: Read + Seek> GrpEntry<T> {
208 fn new(info: GrpFileEntry, reader: Arc<Mutex<T>>) -> Self {
209 Self {
210 info,
211 reader,
212 pos: 0,
213 }
214 }
215
216 fn remaining(&self) -> u64 {
217 self.info.size.saturating_sub(self.pos)
218 }
219}
220
221impl<T: Read + Seek + std::fmt::Debug + Send + Sync> ArchiveContent for GrpEntry<T> {
222 fn name(&self) -> &str {
223 &self.info.name
224 }
225
226 fn size(&self) -> Option<u64> {
227 Some(self.info.size)
228 }
229
230 fn to_data<'a>(&'a mut self) -> Result<Box<dyn ReadSeek + Send + Sync + 'a>> {
231 Ok(Box::new(self))
232 }
233}
234
235impl<T: Read + Seek> Read for GrpEntry<T> {
236 fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
237 if buf.is_empty() || self.pos >= self.info.size {
238 return Ok(0);
239 }
240 let remaining = self.remaining() as usize;
241 if remaining == 0 {
242 return Ok(0);
243 }
244 let to_read = buf.len().min(remaining);
245 let mut reader = self.reader.lock().map_err(|e| {
246 std::io::Error::new(
247 std::io::ErrorKind::Other,
248 format!("Failed to lock reader mutex: {}", e),
249 )
250 })?;
251 reader.seek(SeekFrom::Start(self.info.offset + self.pos))?;
252 let bytes = reader.read(&mut buf[..to_read])?;
253 self.pos = self.pos.checked_add(bytes as u64).ok_or_else(|| {
254 std::io::Error::new(std::io::ErrorKind::Other, "Read position overflow.")
255 })?;
256 Ok(bytes)
257 }
258}
259
260impl<T: Read + Seek> Seek for GrpEntry<T> {
261 fn seek(&mut self, pos: SeekFrom) -> std::io::Result<u64> {
262 let new_pos = match pos {
263 SeekFrom::Start(offset) => offset,
264 SeekFrom::End(offset) => {
265 let signed = self.info.size as i128 + offset as i128;
266 if signed < 0 {
267 return Err(std::io::Error::new(
268 std::io::ErrorKind::InvalidInput,
269 "Seek before entry start is not allowed.",
270 ));
271 }
272 signed as u64
273 }
274 SeekFrom::Current(offset) => {
275 let signed = self.pos as i128 + offset as i128;
276 if signed < 0 {
277 return Err(std::io::Error::new(
278 std::io::ErrorKind::InvalidInput,
279 "Seek before entry start is not allowed.",
280 ));
281 }
282 signed as u64
283 }
284 };
285 if new_pos > self.info.size {
286 return Err(std::io::Error::new(
287 std::io::ErrorKind::InvalidInput,
288 "Seek beyond entry size is not allowed.",
289 ));
290 }
291 self.pos = new_pos;
292 Ok(self.pos)
293 }
294}
295
296#[derive(Debug)]
297struct NameInfo {
298 digits_offset: usize,
299 digits_len: usize,
300 arc_num: u32,
301}
302
303fn matches_grp_name(filename: &str) -> bool {
304 Path::new(filename)
305 .file_name()
306 .and_then(|name| name.to_str())
307 .and_then(|name| parse_name_info(name).ok())
308 .is_some()
309}
310
311fn parse_name_info(name: &str) -> Result<NameInfo> {
312 if name.len() < 7 {
313 return Err(anyhow::anyhow!(
314 "Filename '{}' is too short for GRP pattern.",
315 name
316 ));
317 }
318 let name = name.as_bytes();
319 let prefix = &name[..3];
320 if !prefix.eq_ignore_ascii_case(b"res") {
321 return Err(anyhow::anyhow!(
322 "Filename '{:#?}' does not start with 'res'.",
323 name
324 ));
325 }
326 let suffix = &name[name.len() - 4..];
327 if !suffix.eq_ignore_ascii_case(b".grp") {
328 return Err(anyhow::anyhow!(
329 "Filename '{:#?}' does not end with '.grp'.",
330 name
331 ));
332 }
333 let digits = &name[3..name.len() - 4];
334 if digits.is_empty() || !digits.iter().all(|c| c.is_ascii_digit()) {
335 return Err(anyhow::anyhow!(
336 "Filename '{:#?}' does not contain a numeric sequence.",
337 name
338 ));
339 }
340 let arc_num = std::str::from_utf8(digits)
341 .with_context(|| {
342 format!(
343 "Failed to parse archive number from '{:#?}' (digits '{:#?}').",
344 name, digits
345 )
346 })?
347 .parse::<u32>()
348 .with_context(|| {
349 format!(
350 "Failed to parse archive number from '{:#?}' (digits '{:#?}').",
351 name, digits
352 )
353 })?;
354 Ok(NameInfo {
355 digits_offset: 3,
356 digits_len: digits.len(),
357 arc_num,
358 })
359}
360
361fn locate_toc_file(path: &Path) -> Result<(PathBuf, u32)> {
362 let file_name = path
363 .file_name()
364 .and_then(|name| name.to_str())
365 .ok_or_else(|| anyhow::anyhow!("Filename contains invalid UTF-8."))?;
366 let info = parse_name_info(file_name)?;
367 if info.arc_num == 0 {
368 return Err(anyhow::anyhow!(
369 "Archive '{}' has number 0 and therefore no preceding TOC file.",
370 file_name
371 ));
372 }
373
374 let mut toc_num = info.arc_num as i64 - 1;
375 let mut arc_index: u32 = 1;
376 while toc_num >= 0 {
377 let digits = format!("{:0width$}", toc_num, width = info.digits_len);
378 let mut candidate = String::with_capacity(file_name.len());
379 candidate.push_str(&file_name[..info.digits_offset]);
380 candidate.push_str(&digits);
381 candidate.push_str(&file_name[info.digits_offset + info.digits_len..]);
382 let candidate_path = path.with_file_name(&candidate);
383 if !candidate_path.exists() {
384 return Err(anyhow::anyhow!(
385 "TOC file '{}' does not exist.",
386 candidate_path.display()
387 ));
388 }
389 let mut file = std::fs::File::open(&candidate_path).with_context(|| {
390 format!(
391 "Failed to open TOC candidate '{}'.",
392 candidate_path.display()
393 )
394 })?;
395 let mut header = [0u8; 4];
396 file.read_exact(&mut header).with_context(|| {
397 format!("Failed to read header from '{}'.", candidate_path.display())
398 })?;
399 if &header == b"AiFS" {
400 return Ok((candidate_path, arc_index));
401 }
402 toc_num -= 1;
403 arc_index = arc_index
404 .checked_add(1)
405 .ok_or_else(|| anyhow::anyhow!("Archive index overflow while searching TOC."))?;
406 }
407
408 Err(anyhow::anyhow!(
409 "Unable to locate a TOC (AiFS) file for '{}'.",
410 file_name
411 ))
412}
413
414fn parse_toc_entries(
415 toc_path: &Path,
416 arc_index: u32,
417 archive_size: u64,
418) -> Result<Vec<GrpFileEntry>> {
419 let file = std::fs::File::open(toc_path)?;
420 let mut reader = std::io::BufReader::new(file);
421 let toc_len = reader.stream_length()?;
422 if toc_len < 0x10 {
423 return Err(anyhow::anyhow!("TOC file is too small."));
424 }
425
426 reader.seek(SeekFrom::Start(0xC))?;
427 let res_count = reader.read_i32()?;
428 if res_count <= 0 {
429 return Err(anyhow::anyhow!("TOC resource count is invalid."));
430 }
431 if arc_index as i64 > res_count as i64 {
432 return Err(anyhow::anyhow!(
433 "Archive index {} is out of range (resource count {}).",
434 arc_index,
435 res_count
436 ));
437 }
438
439 let mut index_offset = 0x10u64;
440 let mut arc_offset = None;
441 for _ in 0..res_count {
442 if index_offset + 0x10 > toc_len {
443 break;
444 }
445 reader.seek(SeekFrom::Start(index_offset))?;
446 let mut num = reader.read_i32()?;
447 if num == 0x0100_0000 {
448 index_offset = index_offset
449 .checked_add(4)
450 .ok_or_else(|| anyhow::anyhow!("Index offset overflow."))?;
451 if index_offset + 4 > toc_len {
452 break;
453 }
454 reader.seek(SeekFrom::Start(index_offset))?;
455 num = reader.read_i32()?;
456 }
457 reader.seek(SeekFrom::Start(index_offset + 0xC))?;
458 let entry_count = reader.read_u32()?;
459 if num == arc_index as i32 {
460 arc_offset = Some(index_offset);
461 break;
462 }
463 let step = (entry_count as u64)
464 .checked_mul(8)
465 .and_then(|v| v.checked_add(0x10))
466 .ok_or_else(|| anyhow::anyhow!("Index offset overflow while skipping entries."))?;
467 index_offset = index_offset
468 .checked_add(step)
469 .ok_or_else(|| anyhow::anyhow!("Index offset overflow while iterating."))?;
470 }
471
472 let arc_offset =
473 arc_offset.ok_or_else(|| anyhow::anyhow!("Archive reference not found in TOC."))?;
474
475 reader.seek(SeekFrom::Start(arc_offset + 4))?;
476 let start_index = reader.read_i32()?;
477 if start_index < 0 {
478 return Err(anyhow::anyhow!("Start index is negative."));
479 }
480 reader.seek(SeekFrom::Start(arc_offset + 0xC))?;
481 let entry_count = reader.read_i32()?;
482 if entry_count < 0 {
483 return Err(anyhow::anyhow!("Entry count is negative."));
484 }
485 let entry_count = entry_count as u32;
486
487 let data_offset = arc_offset
488 .checked_add(0x10)
489 .ok_or_else(|| anyhow::anyhow!("Entry table offset overflow."))?;
490 let table_len = (entry_count as u64)
491 .checked_mul(8)
492 .ok_or_else(|| anyhow::anyhow!("Entry table size overflow."))?;
493 if data_offset + table_len > toc_len {
494 return Err(anyhow::anyhow!("TOC entry table exceeds file size."));
495 }
496
497 let mut entries = Vec::with_capacity(entry_count as usize);
498 let mut entry_offset = data_offset;
499 for i in 0..entry_count {
500 reader.seek(SeekFrom::Start(entry_offset))?;
501 let offset = reader.read_u32()? as u64;
502 let size = reader.read_u32()? as u64;
503 if size != 0 {
504 let end = offset
505 .checked_add(size)
506 .ok_or_else(|| anyhow::anyhow!("Entry size overflow."))?;
507 if end > archive_size {
508 return Err(anyhow::anyhow!(
509 "Entry {} exceeds archive size (offset {}, size {}).",
510 i,
511 offset,
512 size
513 ));
514 }
515 let index = (start_index as u32)
516 .checked_add(i)
517 .ok_or_else(|| anyhow::anyhow!("Entry index overflow."))?;
518 entries.push(GrpFileEntry {
519 name: format!("{:05}.ogg", index),
520 offset,
521 size,
522 });
523 }
524 entry_offset += 8;
525 }
526
527 if entries.is_empty() {
528 return Err(anyhow::anyhow!("Archive contains no entries."));
529 }
530
531 Ok(entries)
532}