msg_tool\scripts\circus\archive/
pck.rs

1//! Circus Archive File (.pck/.dat)
2use crate::ext::io::*;
3use crate::scripts::base::*;
4use crate::types::*;
5use crate::utils::encoding::*;
6use crate::utils::struct_pack::*;
7use anyhow::Result;
8use msg_tool_macro::*;
9use std::collections::HashMap;
10use std::io::{Read, Seek, SeekFrom, Write};
11use std::sync::{Arc, Mutex};
12
13#[derive(Debug)]
14/// Circus PCK Archive Builder
15pub struct PckArchiveBuilder {}
16
17impl PckArchiveBuilder {
18    /// Creates a new instance of `PckArchiveBuilder`.
19    pub const fn new() -> Self {
20        Self {}
21    }
22}
23
24impl ScriptBuilder for PckArchiveBuilder {
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(PckArchive::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(PckArchive::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(PckArchive::new(reader, archive_encoding, config)?))
68        }
69    }
70
71    fn build_script_from_reader<'a>(
72        &self,
73        reader: Box<dyn ReadSeek + Send + Sync + 'a>,
74        _filename: &str,
75        _encoding: Encoding,
76        archive_encoding: Encoding,
77        config: &ExtraConfig,
78        _archive: Option<&Box<dyn Script>>,
79    ) -> Result<Box<dyn Script + Send + Sync + 'a>> {
80        Ok(Box::new(PckArchive::new(reader, archive_encoding, config)?))
81    }
82
83    fn extensions(&self) -> &'static [&'static str] {
84        &["pck", "dat"]
85    }
86
87    fn script_type(&self) -> &'static ScriptType {
88        &ScriptType::CircusPck
89    }
90
91    fn is_archive(&self) -> bool {
92        true
93    }
94
95    fn create_archive(
96        &self,
97        filename: &str,
98        files: &[&str],
99        encoding: Encoding,
100        config: &ExtraConfig,
101    ) -> Result<Box<dyn Archive>> {
102        let f = std::fs::File::create(filename)?;
103        let writer = std::io::BufWriter::new(f);
104        Ok(Box::new(PckArchiveWriter::new(
105            writer, files, encoding, config,
106        )?))
107    }
108
109    fn is_this_format(&self, _filename: &str, buf: &[u8], buf_len: usize) -> Option<u8> {
110        is_this_format(&buf[..buf_len]).ok()
111    }
112}
113
114#[derive(Debug, Clone, StructPack, StructUnpack)]
115struct PckFileHeader {
116    #[fstring = 0x38]
117    name: String,
118    offset: u32,
119    size: u32,
120}
121
122#[derive(Debug)]
123struct Entry<T: Read + Seek> {
124    header: PckFileHeader,
125    reader: Arc<Mutex<T>>,
126    pos: usize,
127    script_type: Option<ScriptType>,
128}
129
130impl<T: Read + Seek + std::fmt::Debug + Send + Sync> ArchiveContent for Entry<T> {
131    fn name(&self) -> &str {
132        &self.header.name
133    }
134
135    fn size(&self) -> Option<u64> {
136        Some(self.header.size as u64)
137    }
138
139    fn script_type(&self) -> Option<&ScriptType> {
140        self.script_type.as_ref()
141    }
142
143    fn to_data<'a>(&'a mut self) -> Result<Box<dyn ReadSeek + Send + Sync + 'a>> {
144        Ok(Box::new(self))
145    }
146}
147
148impl<T: Read + Seek> Read for Entry<T> {
149    fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
150        let mut reader = self.reader.lock().map_err(|e| {
151            std::io::Error::new(
152                std::io::ErrorKind::Other,
153                format!("Failed to lock mutex: {}", e),
154            )
155        })?;
156        reader.seek(SeekFrom::Start(self.header.offset as u64 + self.pos as u64))?;
157        let bytes_read = buf.len().min(self.header.size as usize - self.pos);
158        if bytes_read == 0 {
159            return Ok(0);
160        }
161        let bytes_read = reader.read(&mut buf[..bytes_read])?;
162        self.pos += bytes_read;
163        Ok(bytes_read)
164    }
165}
166
167impl<T: Read + Seek> Seek for Entry<T> {
168    fn seek(&mut self, pos: SeekFrom) -> std::io::Result<u64> {
169        let new_pos = match pos {
170            SeekFrom::Start(offset) => offset as usize,
171            SeekFrom::End(offset) => {
172                if offset < 0 {
173                    if (-offset) as usize > self.header.size as usize {
174                        return Err(std::io::Error::new(
175                            std::io::ErrorKind::InvalidInput,
176                            "Seek from end exceeds file length",
177                        ));
178                    }
179                    self.header.size as usize - (-offset) as usize
180                } else {
181                    self.header.size as usize + offset as usize
182                }
183            }
184            SeekFrom::Current(offset) => {
185                if offset < 0 {
186                    if (-offset) as usize > self.pos {
187                        return Err(std::io::Error::new(
188                            std::io::ErrorKind::InvalidInput,
189                            "Seek from current exceeds current position",
190                        ));
191                    }
192                    self.pos.saturating_sub((-offset) as usize)
193                } else {
194                    self.pos + offset as usize
195                }
196            }
197        };
198        self.pos = new_pos;
199        Ok(self.pos as u64)
200    }
201
202    fn stream_position(&mut self) -> std::io::Result<u64> {
203        Ok(self.pos as u64)
204    }
205}
206
207#[derive(Debug)]
208/// PCK Archive
209pub struct PckArchive<'b, T: Read + Seek + std::fmt::Debug + 'b> {
210    reader: Arc<Mutex<T>>,
211    entries: Vec<PckFileHeader>,
212    _mark: std::marker::PhantomData<&'b ()>,
213}
214
215impl<'b, T: Read + Seek + std::fmt::Debug + 'b> PckArchive<'b, T> {
216    /// Creates a new `PckArchive` from a reader.
217    ///
218    /// * `reader` - The reader to read the PCK archive from.
219    /// * `archive_encoding` - The encoding to use for string fields in the archive.
220    /// * `config` - Extra configuration options.
221    pub fn new(mut reader: T, archive_encoding: Encoding, _config: &ExtraConfig) -> Result<Self> {
222        let file_count = reader.read_u32()?;
223        // (offset, size)
224        let mut offset_list = Vec::with_capacity(file_count as usize);
225        for _ in 0..file_count {
226            let offset = reader.read_u32()?;
227            let size = reader.read_u32()?;
228            offset_list.push((offset, size));
229        }
230        for i in 1..file_count as usize {
231            let (prev_offset, prev_size) = offset_list[i - 1];
232            let offset = offset_list[i].0;
233            if prev_offset + prev_size > offset {
234                return Err(anyhow::anyhow!(
235                    "PckArchive: Overlapping entries detected at index {}: previous entry ends at {}, current entry starts at {}",
236                    i - 1,
237                    prev_offset + prev_size,
238                    offset
239                ));
240            }
241        }
242        let mut entries = Vec::with_capacity(file_count as usize);
243        for (i, (offset, size)) in offset_list.into_iter().enumerate() {
244            let header: PckFileHeader = reader.read_struct(false, archive_encoding, &None)?;
245            if header.offset != offset {
246                return Err(anyhow::anyhow!(
247                    "PckArchive: Header offset mismatch at entry {}: expected {}, got {}",
248                    i,
249                    offset,
250                    header.offset
251                ));
252            }
253            if header.size != size {
254                return Err(anyhow::anyhow!(
255                    "PckArchive: Header size mismatch at entry {}: expected {}, got {}",
256                    i,
257                    size,
258                    header.size
259                ));
260            }
261            entries.push(header);
262        }
263        Ok(Self {
264            reader: Arc::new(Mutex::new(reader)),
265            entries,
266            _mark: std::marker::PhantomData,
267        })
268    }
269}
270
271impl<'b, T: Read + Seek + std::fmt::Debug + Send + Sync + 'b> Script for PckArchive<'b, T> {
272    fn default_output_script_type(&self) -> OutputScriptType {
273        OutputScriptType::Json
274    }
275
276    fn default_format_type(&self) -> FormatOptions {
277        FormatOptions::None
278    }
279
280    fn is_archive(&self) -> bool {
281        true
282    }
283
284    fn iter_archive_filename<'a>(
285        &'a self,
286    ) -> Result<Box<dyn Iterator<Item = Result<String>> + 'a>> {
287        Ok(Box::new(self.entries.iter().map(|e| Ok(e.name.clone()))))
288    }
289
290    fn iter_archive_offset<'a>(&'a self) -> Result<Box<dyn Iterator<Item = Result<u64>> + 'a>> {
291        Ok(Box::new(self.entries.iter().map(|e| Ok(e.offset as u64))))
292    }
293
294    fn open_file<'a>(&'a self, index: usize) -> Result<Box<dyn ArchiveContent + Send + Sync + 'a>> {
295        if index >= self.entries.len() {
296            return Err(anyhow::anyhow!(
297                "Index out of bounds: {} (max: {})",
298                index,
299                self.entries.len()
300            ));
301        }
302        let entry = &self.entries[index];
303        let mut entry = Entry {
304            header: entry.clone(),
305            reader: self.reader.clone(),
306            pos: 0,
307            script_type: None,
308        };
309        let mut buf = [0; 32];
310        let readed = match entry.read(&mut buf) {
311            Ok(readed) => readed,
312            Err(e) => {
313                return Err(anyhow::anyhow!(
314                    "Failed to read entry '{}': {}",
315                    entry.header.name,
316                    e
317                ));
318            }
319        };
320        entry.pos = 0;
321        entry.script_type = detect_script_type(&buf, readed, &entry.header.name);
322        Ok(Box::new(entry))
323    }
324}
325
326fn detect_script_type(_buf: &[u8], _buf_len: usize, _filename: &str) -> Option<ScriptType> {
327    #[cfg(feature = "circus-img")]
328    if _buf_len >= 4 && _buf.starts_with(b"CRXG") {
329        return Some(ScriptType::CircusCrx);
330    }
331    #[cfg(feature = "circus-audio")]
332    if _buf_len >= 4 && _buf.starts_with(b"XPCM") {
333        return Some(ScriptType::CircusPcm);
334    }
335    None
336}
337
338/// PCK Archive Writer
339pub struct PckArchiveWriter<T: Write + Seek> {
340    writer: T,
341    headers: HashMap<String, PckFileHeader>,
342    encoding: Encoding,
343}
344
345impl<T: Write + Seek> PckArchiveWriter<T> {
346    /// Creates a new `PckArchiveWriter` for writing a PCK archive.
347    ///
348    /// * `writer` - The writer to write the PCK archive to.
349    /// * `files` - A list of file names to include in the archive.
350    /// * `encoding` - The encoding to use for string fields in the archive.
351    /// * `config` - Extra configuration options.
352    pub fn new(
353        mut writer: T,
354        files: &[&str],
355        encoding: Encoding,
356        _config: &ExtraConfig,
357    ) -> Result<Self> {
358        let file_count = files.len() as u32;
359        writer.write_u32(file_count)?;
360        let mut headers = HashMap::new();
361        for _ in 0..file_count {
362            writer.write_u32(0)?; // Placeholder for offset
363            writer.write_u32(0)?; // Placeholder for size
364        }
365        for file in files {
366            let header = PckFileHeader {
367                name: file.to_string(),
368                offset: 0,
369                size: 0,
370            };
371            header.pack(&mut writer, false, encoding, &None)?;
372            headers.insert(file.to_string(), header);
373        }
374        Ok(PckArchiveWriter {
375            writer,
376            headers,
377            encoding,
378        })
379    }
380}
381
382impl<T: Write + Seek> Archive for PckArchiveWriter<T> {
383    fn new_file<'a>(
384        &'a mut self,
385        name: &str,
386        _size: Option<u64>,
387    ) -> Result<Box<dyn WriteSeek + 'a>> {
388        let entry = self
389            .headers
390            .get_mut(name)
391            .ok_or_else(|| anyhow::anyhow!("File '{}' not found in archive", name))?;
392        if entry.offset != 0 || entry.size != 0 {
393            return Err(anyhow::anyhow!("File '{}' already exists in archive", name));
394        }
395        self.writer.seek(SeekFrom::End(0))?;
396        entry.offset = self.writer.stream_position()? as u32;
397        let file = PckArchiveFile {
398            header: entry,
399            writer: &mut self.writer,
400            pos: 0,
401        };
402        Ok(Box::new(file))
403    }
404
405    fn write_header(&mut self) -> Result<()> {
406        self.writer.seek(SeekFrom::Start(0x4))?;
407        let mut files = self.headers.iter().map(|(_, d)| d).collect::<Vec<_>>();
408        files.sort_by_key(|f| f.offset);
409        for file in files.iter() {
410            self.writer.write_u32(file.offset)?;
411            self.writer.write_u32(file.size)?;
412        }
413        for file in files {
414            file.pack(&mut self.writer, false, self.encoding, &None)?;
415        }
416        Ok(())
417    }
418}
419
420/// PCK Archive File
421pub struct PckArchiveFile<'a, T: Write + Seek> {
422    header: &'a mut PckFileHeader,
423    writer: &'a mut T,
424    pos: usize,
425}
426
427impl<'a, T: Write + Seek> Write for PckArchiveFile<'a, T> {
428    fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
429        self.writer
430            .seek(SeekFrom::Start(self.header.offset as u64 + self.pos as u64))?;
431        let bytes_written = self.writer.write(buf)?;
432        self.pos += bytes_written;
433        self.header.size = self.header.size.max(self.pos as u32);
434        Ok(bytes_written)
435    }
436
437    fn flush(&mut self) -> std::io::Result<()> {
438        self.writer.flush()
439    }
440}
441
442impl<'a, T: Write + Seek> Seek for PckArchiveFile<'a, T> {
443    fn seek(&mut self, pos: SeekFrom) -> std::io::Result<u64> {
444        let new_pos = match pos {
445            SeekFrom::Start(offset) => offset as usize,
446            SeekFrom::End(offset) => {
447                if offset < 0 {
448                    if (-offset) as usize > self.header.size as usize {
449                        return Err(std::io::Error::new(
450                            std::io::ErrorKind::InvalidInput,
451                            "Seek from end exceeds file length",
452                        ));
453                    }
454                    self.header.size as usize - (-offset) as usize
455                } else {
456                    self.header.size as usize + offset as usize
457                }
458            }
459            SeekFrom::Current(offset) => {
460                if offset < 0 {
461                    if (-offset) as usize > self.pos {
462                        return Err(std::io::Error::new(
463                            std::io::ErrorKind::InvalidInput,
464                            "Seek from current exceeds current position",
465                        ));
466                    }
467                    self.pos.saturating_sub((-offset) as usize)
468                } else {
469                    self.pos + offset as usize
470                }
471            }
472        };
473        self.pos = new_pos;
474        Ok(self.pos as u64)
475    }
476}
477
478/// Checks if the buffer is a valid PCK archive format.
479pub fn is_this_format(buf: &[u8]) -> Result<u8> {
480    let mut reader = MemReaderRef::new(buf);
481    let count = reader.read_u32()? as usize;
482    let mut score = if count > 0 && count < 0x40000 { 5 } else { 0 };
483    let avail_count = ((buf.len() - 4) / 0x8).min(count);
484    score += ((avail_count / 2).min(10)) as u8;
485    if avail_count == 0 {
486        return Err(anyhow::anyhow!("No valid entries found in PCK archive"));
487    }
488    let mut prev_off = reader.read_u32()?;
489    let mut prev_size = reader.read_u32()?;
490    let mut index = 1;
491    while index < avail_count {
492        let off = reader.read_u32()?;
493        let size = reader.read_u32()?;
494        if off < prev_off
495            || prev_off
496                .checked_add(prev_size)
497                .ok_or_else(|| anyhow::anyhow!("Overflow in offset calculation"))?
498                != off
499        {
500            return Err(anyhow::anyhow!("Invalid offset."));
501        }
502        prev_off = off;
503        prev_size = size;
504        index += 1;
505    }
506    Ok(score)
507}