diff --git a/src/scripts/base.rs b/src/scripts/base.rs index b0ea92e..5630033 100644 --- a/src/scripts/base.rs +++ b/src/scripts/base.rs @@ -7,10 +7,14 @@ pub trait ReadSeek: Read + Seek + std::fmt::Debug {} pub trait WriteSeek: Write + Seek {} +pub trait AnyDebug: std::fmt::Debug + std::any::Any {} + impl ReadSeek for T {} impl WriteSeek for T {} +impl AnyDebug for T {} + pub trait ScriptBuilder: std::fmt::Debug { fn default_encoding(&self) -> Encoding; @@ -368,6 +372,10 @@ pub trait Script: std::fmt::Debug + std::any::Any { let f = std::io::BufWriter::new(f); self.import_multi_image(data, Box::new(f)) } + + fn extra_info<'a>(&'a self) -> Option> { + None + } } pub trait Archive { diff --git a/src/scripts/circus/archive/dat.rs b/src/scripts/circus/archive/dat.rs new file mode 100644 index 0000000..d6f4bcb --- /dev/null +++ b/src/scripts/circus/archive/dat.rs @@ -0,0 +1,376 @@ +use crate::ext::io::*; +use crate::scripts::base::*; +use crate::types::*; +use anyhow::Result; +use std::io::{Read, Seek, SeekFrom}; +use std::sync::{Arc, Mutex}; + +#[derive(Debug)] +pub struct DatArchiveBuilder {} + +impl DatArchiveBuilder { + pub fn new() -> Self { + Self {} + } +} + +impl ScriptBuilder for DatArchiveBuilder { + 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(DatArchive::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(DatArchive::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(DatArchive::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(DatArchive::new(reader, archive_encoding, config)?)) + } + + fn extensions(&self) -> &'static [&'static str] { + &["dat"] + } + + fn script_type(&self) -> &'static ScriptType { + &ScriptType::CircusDat + } + + fn is_archive(&self) -> bool { + true + } + + fn is_this_format(&self, _filename: &str, buf: &[u8], buf_len: usize) -> Option { + is_this_format(&buf[..buf_len]).ok() + } +} + +#[derive(Debug, Clone)] +struct DatFileHeader { + name: String, + offset: u32, + size: u32, +} + +struct Entry { + header: DatFileHeader, + 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 DatExtraInfo { + pub name_len: usize, +} + +#[derive(Debug)] +pub struct DatArchive { + reader: Arc>, + entries: Vec, + name_len: usize, +} + +const NAME_LEN: [usize; 3] = [0x24, 0x30, 0x3C]; + +impl DatArchive { + pub fn new(mut reader: T, encoding: Encoding, _config: &ExtraConfig) -> Result { + let (name_len, entries) = Self::read_all_index(&mut reader, encoding)?; + let reader = Arc::new(Mutex::new(reader)); + Ok(Self { + reader, + entries, + name_len, + }) + } + + fn read_all_index(reader: &mut T, encoding: Encoding) -> Result<(usize, Vec)> { + for &name_len in &NAME_LEN { + match Self::read_index(reader, encoding, name_len) { + Ok(entries) => return Ok((name_len, entries)), + Err(_) => continue, + } + } + Err(anyhow::anyhow!("Failed to read DAT index")) + } + + fn read_index( + reader: &mut T, + encoding: Encoding, + name_len: usize, + ) -> Result> { + reader.rewind()?; + let mut count = reader.read_u32()?; + let index_size = (name_len + 4) * count as usize; + count -= 1; + let mut entries = Vec::with_capacity(count as usize); + let mut next_offset = reader.peek_u32_at(4 + name_len)?; + if (next_offset as usize) < index_size + 4 { + return Err(anyhow::anyhow!("Invalid next_offset")); + } + let first_size = reader.peek_u32_at(name_len)?; + let second_offset = reader.peek_u32_at(8 + name_len * 2)?; + if second_offset - next_offset == first_size { + return Err(anyhow::anyhow!("Invalid second_offset")); + } + let file_len = reader.stream_length()?; + for i in 0..count { + let name = reader.read_fstring(name_len, encoding, true)?; + if name.is_empty() { + return Err(anyhow::anyhow!("Empty file name in DAT archive")); + } + let offset = next_offset; + if i + 1 == count { + next_offset = file_len as u32; + } else { + next_offset = reader.peek_u32_at((name_len + 4) * (i as usize + 2))?; + } + if next_offset < offset { + return Err(anyhow::anyhow!("Invalid offset in DAT archive")); + } + let size = next_offset - offset; + if offset < index_size as u32 || offset + size > file_len as u32 { + return Err(anyhow::anyhow!("Invalid offset or size in DAT archive")); + } + let header = DatFileHeader { name, offset, size }; + entries.push(header); + reader.seek_relative(4)?; + } + Ok(entries) + } +} + +impl Script for DatArchive { + 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 extra_info<'a>(&'a self) -> Option> { + Some(Box::new(DatExtraInfo { + name_len: self.name_len, + })) + } +} + +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); + } + #[cfg(feature = "circus-audio")] + if _buf_len >= 4 && _buf.starts_with(b"XPCM") { + return Some(ScriptType::CircusPcm); + } + None +} + +fn is_this_format_name_len(buf: &[u8], name_len: usize) -> Result { + let mut reader = MemReaderRef::new(buf); + let count = reader.read_u32()? as usize; + let index_size = (name_len + 4) * count; + let mut score = if count > 0 && count < 1000 { 5 } else { 0 }; + let mcount = ((buf.len() - 4) / (name_len + 4)).min(count - 1); + score += ((mcount / 2).min(10)) as u8; + if mcount == 0 { + return Err(anyhow::anyhow!("No entries found in DAT archive")); + } + let mut next_offset = reader.cpeek_u32_at(4 + name_len)?; + if (next_offset as usize) < index_size + 4 { + return Err(anyhow::anyhow!("Invalid next_offset in DAT archive")); + } + let first_size = reader.cpeek_u32_at(name_len)?; + let second_offset = reader.cpeek_u32_at(8 + name_len * 2)?; + if second_offset - next_offset == first_size { + return Err(anyhow::anyhow!("Invalid second_offset in DAT archive")); + } + for i in 0..mcount { + let offset = next_offset; + if i + 1 == mcount { + break; + } else { + next_offset = reader.cpeek_u32_at((name_len + 4) * (i + 2))?; + } + if next_offset < offset { + return Err(anyhow::anyhow!("Invalid offset in DAT archive")); + } + if offset < index_size as u32 { + return Err(anyhow::anyhow!( + "Offset is less than index size in DAT archive" + )); + } + } + Ok(score) +} + +pub fn is_this_format(buf: &[u8]) -> Result { + for &name_len in &NAME_LEN { + match is_this_format_name_len(buf, name_len) { + Ok(score) => return Ok(score), + Err(_) => continue, + } + } + Err(anyhow::anyhow!("Not a valid DAT archive format")) +} diff --git a/src/scripts/circus/archive/mod.rs b/src/scripts/circus/archive/mod.rs index 788b293..db93d1a 100644 --- a/src/scripts/circus/archive/mod.rs +++ b/src/scripts/circus/archive/mod.rs @@ -1 +1,2 @@ +pub mod dat; pub mod pck; diff --git a/src/scripts/mod.rs b/src/scripts/mod.rs index 93125d0..653015d 100644 --- a/src/scripts/mod.rs +++ b/src/scripts/mod.rs @@ -92,6 +92,8 @@ lazy_static::lazy_static! { Box::new(circus::audio::pcm::PcmBuilder::new()), #[cfg(feature = "ex-hibit")] Box::new(ex_hibit::rld::RldScriptBuilder::new()), + #[cfg(feature = "circus-arc")] + Box::new(circus::archive::dat::DatArchiveBuilder::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 b3709b3..08ee079 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 DAT archive + CircusDat, + #[cfg(feature = "circus-arc")] /// Circus PCK archive CircusPck, #[cfg(feature = "circus-audio")]