From e9c2131372f09a55ff6c7167240ba5941f82e355 Mon Sep 17 00:00:00 2001 From: lifegpc Date: Fri, 8 Aug 2025 21:42:16 +0800 Subject: [PATCH] Add CIRCUS CRM archive support --- src/scripts/circus/archive/crm.rs | 286 ++++++++++++++++++++++++++++++ src/scripts/circus/archive/mod.rs | 1 + src/scripts/mod.rs | 2 + src/types.rs | 3 + 4 files changed, 292 insertions(+) create mode 100644 src/scripts/circus/archive/crm.rs diff --git a/src/scripts/circus/archive/crm.rs b/src/scripts/circus/archive/crm.rs new file mode 100644 index 0000000..99bdbc9 --- /dev/null +++ b/src/scripts/circus/archive/crm.rs @@ -0,0 +1,286 @@ +use crate::ext::io::*; +use crate::scripts::base::*; +use crate::types::*; +use anyhow::Result; +use std::collections::BTreeMap; +use std::io::{Read, Seek, SeekFrom}; +use std::sync::{Arc, Mutex}; + +#[derive(Debug)] +pub struct CrmArchiveBuilder {} + +impl CrmArchiveBuilder { + pub fn new() -> Self { + Self {} + } +} + +impl ScriptBuilder for CrmArchiveBuilder { + fn default_encoding(&self) -> Encoding { + Encoding::Cp932 + } + + fn default_archive_encoding(&self) -> Option { + Some(Encoding::Cp932) + } + + fn build_script( + &self, + data: Vec, + _filename: &str, + _encoding: Encoding, + archive_encoding: Encoding, + config: &ExtraConfig, + _archive: Option<&Box>, + ) -> Result> { + Ok(Box::new(CrmArchive::new( + MemReader::new(data), + archive_encoding, + config, + )?)) + } + + fn build_script_from_file( + &self, + filename: &str, + _encoding: Encoding, + archive_encoding: Encoding, + config: &ExtraConfig, + _archive: Option<&Box>, + ) -> Result> { + if filename == "-" { + let data = crate::utils::files::read_file(filename)?; + Ok(Box::new(CrmArchive::new( + MemReader::new(data), + archive_encoding, + config, + )?)) + } else { + let f = std::fs::File::open(filename)?; + let reader = std::io::BufReader::new(f); + Ok(Box::new(CrmArchive::new(reader, archive_encoding, config)?)) + } + } + + fn build_script_from_reader( + &self, + reader: Box, + _filename: &str, + _encoding: Encoding, + archive_encoding: Encoding, + config: &ExtraConfig, + _archive: Option<&Box>, + ) -> Result> { + Ok(Box::new(CrmArchive::new(reader, archive_encoding, config)?)) + } + + fn extensions(&self) -> &'static [&'static str] { + &["crm"] + } + + fn script_type(&self) -> &'static ScriptType { + &ScriptType::CircusCrm + } + + fn is_archive(&self) -> bool { + true + } + + fn is_this_format(&self, _filename: &str, buf: &[u8], buf_len: usize) -> Option { + if buf_len >= 4 && buf.starts_with(b"CRXB") { + return Some(10); + } + None + } +} + +#[derive(Debug, Clone)] +struct CrmFileHeader { + offset: u32, + size: u32, + name: String, +} + +struct Entry { + header: CrmFileHeader, + reader: Arc>, + pos: usize, + script_type: Option, +} + +impl ArchiveContent for Entry { + fn name(&self) -> &str { + &self.header.name + } + + fn script_type(&self) -> Option<&ScriptType> { + self.script_type.as_ref() + } +} + +impl Read for Entry { + fn read(&mut self, buf: &mut [u8]) -> std::io::Result { + let mut reader = self.reader.lock().map_err(|e| { + std::io::Error::new( + std::io::ErrorKind::Other, + format!("Failed to lock mutex: {}", e), + ) + })?; + reader.seek(SeekFrom::Start(self.header.offset as u64 + self.pos as u64))?; + let bytes_read = buf.len().min(self.header.size as usize - self.pos); + if bytes_read == 0 { + return Ok(0); + } + let bytes_read = reader.read(&mut buf[..bytes_read])?; + self.pos += bytes_read; + Ok(bytes_read) + } +} + +impl Seek for Entry { + fn seek(&mut self, pos: SeekFrom) -> std::io::Result { + let new_pos = match pos { + SeekFrom::Start(offset) => offset as usize, + SeekFrom::End(offset) => { + if offset < 0 { + if (-offset) as usize > self.header.size as usize { + return Err(std::io::Error::new( + std::io::ErrorKind::InvalidInput, + "Seek from end exceeds file length", + )); + } + self.header.size as usize - (-offset) as usize + } else { + self.header.size as usize + offset as usize + } + } + SeekFrom::Current(offset) => { + if offset < 0 { + if (-offset) as usize > self.pos { + return Err(std::io::Error::new( + std::io::ErrorKind::InvalidInput, + "Seek from current exceeds current position", + )); + } + self.pos.saturating_sub((-offset) as usize) + } else { + self.pos + offset as usize + } + } + }; + self.pos = new_pos; + Ok(self.pos as u64) + } + + fn stream_position(&mut self) -> std::io::Result { + Ok(self.pos as u64) + } +} + +#[derive(Debug)] +pub struct CrmArchive { + reader: Arc>, + entries: Vec, +} + +impl CrmArchive { + pub fn new(mut reader: T, encoding: Encoding, _config: &ExtraConfig) -> Result { + let mut magic = [0u8; 4]; + reader.read_exact(&mut magic)?; + if &magic != b"CRXB" { + return Err(anyhow::anyhow!("Invalid CRM archive magic: {:?}", magic)); + } + reader.seek_relative(4)?; + let count = reader.read_u32()? as usize; + reader.seek_relative(4)?; + let mut entries = Vec::with_capacity(count); + let file_len = reader.stream_length()?; + let mut offset_map = BTreeMap::new(); + for _ in 0..count { + let offset = reader.read_u32()?; + reader.seek_relative(4)?; + let name = reader.read_fstring(0x18, encoding, true)?; + offset_map.insert(offset, name); + } + let mut next_iter = offset_map.keys().skip(1); + for (offset, name) in &offset_map { + let size = if let Some(next) = next_iter.next() { + *next + } else { + file_len as u32 + } - offset; + entries.push(CrmFileHeader { + offset: *offset, + size, + name: name.clone(), + }); + } + Ok(Self { + reader: Arc::new(Mutex::new(reader)), + entries, + }) + } +} + +impl Script for CrmArchive { + fn default_output_script_type(&self) -> OutputScriptType { + OutputScriptType::Json + } + + fn default_format_type(&self) -> FormatOptions { + FormatOptions::None + } + + fn is_archive(&self) -> bool { + true + } + + fn iter_archive_filename<'a>( + &'a self, + ) -> Result> + 'a>> { + Ok(Box::new(self.entries.iter().map(|e| Ok(e.name.clone())))) + } + + fn iter_archive_offset<'a>(&'a self) -> Result> + 'a>> { + Ok(Box::new(self.entries.iter().map(|e| Ok(e.offset as u64)))) + } + + fn open_file<'a>(&'a self, index: usize) -> Result> { + if index >= self.entries.len() { + return Err(anyhow::anyhow!( + "Index out of bounds: {} (max: {})", + index, + self.entries.len() + )); + } + let entry = &self.entries[index]; + let mut entry = Entry { + header: entry.clone(), + reader: self.reader.clone(), + pos: 0, + script_type: None, + }; + let mut buf = [0; 32]; + let readed = match entry.read(&mut buf) { + Ok(readed) => readed, + Err(e) => { + return Err(anyhow::anyhow!( + "Failed to read entry '{}': {}", + entry.header.name, + e + )); + } + }; + entry.pos = 0; + entry.script_type = detect_script_type(&buf, readed, &entry.header.name); + Ok(Box::new(entry)) + } +} + +fn detect_script_type(_buf: &[u8], _buf_len: usize, _filename: &str) -> Option { + #[cfg(feature = "circus-img")] + if _buf_len >= 4 && _buf.starts_with(b"CRXG") { + return Some(ScriptType::CircusCrx); + } + None +} diff --git a/src/scripts/circus/archive/mod.rs b/src/scripts/circus/archive/mod.rs index db93d1a..6af54e9 100644 --- a/src/scripts/circus/archive/mod.rs +++ b/src/scripts/circus/archive/mod.rs @@ -1,2 +1,3 @@ +pub mod crm; pub mod dat; pub mod pck; diff --git a/src/scripts/mod.rs b/src/scripts/mod.rs index 653015d..b7003ad 100644 --- a/src/scripts/mod.rs +++ b/src/scripts/mod.rs @@ -94,6 +94,8 @@ lazy_static::lazy_static! { Box::new(ex_hibit::rld::RldScriptBuilder::new()), #[cfg(feature = "circus-arc")] Box::new(circus::archive::dat::DatArchiveBuilder::new()), + #[cfg(feature = "circus-arc")] + Box::new(circus::archive::crm::CrmArchiveBuilder::new()), ]; pub static ref ALL_EXTS: Vec = BUILDER.iter().flat_map(|b| b.extensions()).map(|s| s.to_string()).collect(); diff --git a/src/types.rs b/src/types.rs index 08ee079..93abc04 100644 --- a/src/types.rs +++ b/src/types.rs @@ -326,6 +326,9 @@ pub enum ScriptType { /// Circus MES script Circus, #[cfg(feature = "circus-arc")] + /// Circus Image archive + CircusCrm, + #[cfg(feature = "circus-arc")] /// Circus DAT archive CircusDat, #[cfg(feature = "circus-arc")]