From df0e4fb6cf094641bc5cb0934ca23f28e26a4295 Mon Sep 17 00:00:00 2001 From: lifegpc Date: Fri, 8 Aug 2025 23:18:16 +0800 Subject: [PATCH] Add CRXD support --- src/ext/io.rs | 74 ++++++++++++++ src/scripts/circus/archive/crm.rs | 4 + src/scripts/circus/image/crx.rs | 98 +++++++++++++++++- src/scripts/circus/image/crxd.rs | 163 ++++++++++++++++++++++++++++++ src/scripts/circus/image/mod.rs | 1 + src/scripts/mod.rs | 2 + src/types.rs | 3 + 7 files changed, 341 insertions(+), 4 deletions(-) create mode 100644 src/scripts/circus/image/crxd.rs diff --git a/src/ext/io.rs b/src/ext/io.rs index 32595e8..f1662fb 100644 --- a/src/ext/io.rs +++ b/src/ext/io.rs @@ -1268,3 +1268,77 @@ impl CPeek for MemWriter { self.to_ref().cpeek_cstring() } } + +pub struct StreamRegion { + stream: T, + start_pos: u64, + end_pos: u64, + cur_pos: u64, +} + +impl StreamRegion { + pub fn new(stream: T, start_pos: u64, end_pos: u64) -> Result { + if start_pos > end_pos { + return Err(std::io::Error::new( + std::io::ErrorKind::InvalidInput, + "Start position cannot be greater than end position", + )); + } + Ok(Self { + stream, + start_pos, + end_pos, + cur_pos: 0, + }) + } + + pub fn with_start_pos(mut stream: T, start_pos: u64) -> Result { + let end_pos = stream.stream_length()?; + Self::new(stream, start_pos, end_pos) + } +} + +impl Read for StreamRegion { + fn read(&mut self, buf: &mut [u8]) -> Result { + if self.cur_pos + self.start_pos >= self.end_pos { + return Ok(0); // EOF + } + self.stream + .seek(SeekFrom::Start(self.start_pos + self.cur_pos))?; + let bytes_to_read = (self.end_pos - self.start_pos - self.cur_pos) as usize; + let m = buf.len().min(bytes_to_read); + let readed = self.stream.read(&mut buf[..m])?; + self.cur_pos += readed as u64; + Ok(readed) + } +} + +impl Seek for StreamRegion { + fn seek(&mut self, pos: SeekFrom) -> Result { + let new_pos = match pos { + SeekFrom::Start(offset) => self.start_pos + offset, + SeekFrom::End(offset) => (self.end_pos as i64 + offset as i64) as u64, + SeekFrom::Current(offset) => { + (self.start_pos as i64 + self.cur_pos as i64 + offset as i64) as u64 + } + }; + if new_pos < self.start_pos || new_pos > self.end_pos { + return Err(std::io::Error::new( + std::io::ErrorKind::InvalidInput, + "Seek position out of bounds", + )); + } + self.cur_pos = new_pos - self.start_pos; + self.stream.seek(SeekFrom::Start(new_pos)) + } + + fn stream_position(&mut self) -> Result { + Ok(self.cur_pos) + } + + fn rewind(&mut self) -> Result<()> { + self.cur_pos = 0; + self.stream.seek(SeekFrom::Start(self.start_pos))?; + Ok(()) + } +} diff --git a/src/scripts/circus/archive/crm.rs b/src/scripts/circus/archive/crm.rs index 99bdbc9..d8d69bc 100644 --- a/src/scripts/circus/archive/crm.rs +++ b/src/scripts/circus/archive/crm.rs @@ -282,5 +282,9 @@ fn detect_script_type(_buf: &[u8], _buf_len: usize, _filename: &str) -> Option= 4 && _buf.starts_with(b"CRXG") { return Some(ScriptType::CircusCrx); } + #[cfg(feature = "circus-img")] + if _buf_len >= 4 && _buf.starts_with(b"CRXD") { + return Some(ScriptType::CircusCrxd); + } None } diff --git a/src/scripts/circus/image/crx.rs b/src/scripts/circus/image/crx.rs index 2315973..2960295 100644 --- a/src/scripts/circus/image/crx.rs +++ b/src/scripts/circus/image/crx.rs @@ -7,6 +7,7 @@ use anyhow::Result; use clap::ValueEnum; use clap::builder::PossibleValue; use msg_tool_macro::*; +use overf::wrapping; use std::io::{Read, Seek, Write}; #[derive(Debug, Clone, Copy, PartialEq, Eq)] @@ -135,12 +136,12 @@ impl ScriptBuilder for CrxImageBuilder { #[derive(Clone, Debug, StructPack, StructUnpack)] struct Clip { field_0: u32, + img_width: u16, + img_height: u16, + clip_offset_x: u16, + clip_offset_y: u16, clip_width: u16, clip_height: u16, - field_8: u16, - field_a: u16, - width: u16, - height: u16, } #[derive(Clone, Debug, StructPack, StructUnpack)] @@ -232,6 +233,43 @@ impl CrxImage { }) } + pub fn draw_diff(&self, diff: &Self) -> Result { + let base_header = &self.header; + let diff_header = &diff.header; + let (img_width, img_height) = + if base_header.clips.is_empty() && diff_header.clips.is_empty() { + ( + (base_header.width + base_header.inner_x) + .max(diff_header.width + diff_header.inner_x), + (base_header.height + base_header.inner_y) + .max(diff_header.height + diff_header.inner_y), + ) + } else { + if base_header.clips.is_empty() { + let clip = &diff_header.clips[0]; + (clip.img_width, clip.img_height) + } else { + let clip = &base_header.clips[0]; + (clip.img_width, clip.img_height) + } + }; + let base = self.export_image()?; + let mut nw = draw_on_canvas( + base, + img_width as u32, + img_height as u32, + base_header.inner_x as u32, + base_header.inner_y as u32, + )?; + draw_on_img( + &mut nw, + &diff.export_image()?, + diff_header.inner_x as u32, + diff_header.inner_y as u32, + )?; + Ok(nw) + } + fn decode_row0( dst: &mut Vec, mut dst_p: usize, @@ -907,3 +945,55 @@ impl Script for CrxImage { Ok(()) } } + +fn draw_on_img(base: &mut ImageData, diff: &ImageData, left: u32, top: u32) -> Result<()> { + if base.color_type != diff.color_type { + return Err(anyhow::anyhow!( + "Color types do not match: {:?} vs {:?}", + base.color_type, + diff.color_type + )); + } + let bpp = base.color_type.bpp(1) as usize; + let base_stride = base.width as usize * bpp; + let diff_stride = diff.width as usize * bpp; + + for y in 0..diff.height { + let base_y = top + y; + if base_y >= base.height { + continue; // Skip if the base image is not tall enough + } + + for x in 0..diff.width { + let base_x = left + x; + if base_x >= base.width { + continue; // Skip if the base image is not wide enough + } + + let base_index = (base_y as usize * base_stride) + (base_x as usize * bpp); + let diff_index = (y as usize * diff_stride) + (x as usize * bpp); + + let diff_pixel = &diff.data[diff_index..diff_index + bpp]; + let base_pixel_orig = base.data[base_index..base_index + bpp].to_vec(); + let mut b = base_pixel_orig[0]; + let mut g = base_pixel_orig[1]; + let mut r = base_pixel_orig[2]; + wrapping! { + b += diff_pixel[0]; + g += diff_pixel[1]; + r += diff_pixel[2]; + } + base.data[base_index] = b; + base.data[base_index + 1] = g; + base.data[base_index + 2] = r; + if bpp == 4 { + let mut a = base_pixel_orig[3]; + wrapping! { + a -= diff_pixel[3]; + } + base.data[base_index + 3] = a; + } + } + } + Ok(()) +} diff --git a/src/scripts/circus/image/crxd.rs b/src/scripts/circus/image/crxd.rs new file mode 100644 index 0000000..d1df912 --- /dev/null +++ b/src/scripts/circus/image/crxd.rs @@ -0,0 +1,163 @@ +use super::crx::CrxImage; +use crate::ext::io::*; +use crate::scripts::base::*; +use crate::types::*; +use anyhow::Result; +use std::io::{Read, Seek}; + +#[derive(Debug)] +pub struct CrxdImageBuilder {} + +impl CrxdImageBuilder { + pub fn new() -> Self { + Self {} + } +} + +impl ScriptBuilder for CrxdImageBuilder { + fn default_encoding(&self) -> Encoding { + Encoding::Cp932 + } + + fn build_script( + &self, + data: Vec, + filename: &str, + encoding: Encoding, + _archive_encoding: Encoding, + config: &ExtraConfig, + archive: Option<&Box>, + ) -> Result> { + Ok(Box::new(CrxdImage::new( + MemReader::new(data), + filename, + encoding, + config, + archive, + )?)) + } + + fn extensions(&self) -> &'static [&'static str] { + &["crx"] + } + + fn script_type(&self) -> &'static ScriptType { + &ScriptType::CircusCrxd + } + + fn is_image(&self) -> bool { + true + } + + fn is_this_format(&self, _filename: &str, buf: &[u8], buf_len: usize) -> Option { + if buf_len >= 4 && buf.starts_with(b"CRXD") { + return Some(255); + } + None + } +} + +#[derive(Debug)] +pub struct CrxdImage { + base: CrxImage, + diff: CrxImage, +} + +impl CrxdImage { + pub fn new( + data: T, + filename: &str, + encoding: Encoding, + config: &ExtraConfig, + archive: Option<&Box>, + ) -> Result { + let mut reader = data; + let mut magic = [0; 4]; + reader.read_exact(&mut magic)?; + if magic != *b"CRXD" { + return Err(anyhow::anyhow!("Invalid CRXD magic")); + } + reader.seek_relative(4)?; + let offset = reader.read_u32()?; + let name = reader.read_fstring(0x14, encoding, true)?; + let base = if let Some(archive) = archive { + CrxImage::new( + archive.open_file_by_offset(offset as u64)?.to_data()?, + config, + )? + } else { + let mut nf = std::path::PathBuf::from(filename); + nf.set_file_name(name); + let f = std::fs::File::open(nf)?; + CrxImage::new(std::io::BufReader::new(f), config)? + }; + let mut typ = [0; 4]; + reader.read_exact(&mut typ)?; + if typ == *b"CRXJ" { + reader.seek_relative(4)?; + let offset = reader.read_u32()?; + let diff = Self::read_diff( + archive + .ok_or(anyhow::anyhow!("No archive provided"))? + .open_file_by_offset(offset as u64)? + .to_data()?, + archive.clone(), + config, + )?; + return Ok(Self { base, diff }); + } else if typ == *b"CRXG" { + let reader = StreamRegion::with_start_pos(reader, 0x20)?; + let diff = CrxImage::new(reader, config)?; + return Ok(Self { base, diff }); + } + Err(anyhow::anyhow!("Unsupported diff CRXD type: {:?}", typ)) + } + + fn read_diff( + mut reader: T, + archive: Option<&Box>, + config: &ExtraConfig, + ) -> Result { + let mut magic = [0; 4]; + reader.read_exact(&mut magic)?; + if magic != *b"CRXD" { + return Err(anyhow::anyhow!("Invalid CRXD magic")); + } + reader.seek_relative(0x1C)?; + let mut typ = [0; 4]; + reader.read_exact(&mut typ)?; + if typ == *b"CRXJ" { + reader.seek_relative(4)?; + let offset = reader.read_u32()?; + return Ok(CrxImage::new( + archive + .ok_or(anyhow::anyhow!("No archive provided"))? + .open_file_by_offset(offset as u64)? + .to_data()?, + config, + )?); + } else if typ == *b"CRXG" { + let reader = StreamRegion::with_start_pos(reader, 0x20)?; + return Ok(CrxImage::new(reader, config)?); + } + Err(anyhow::anyhow!("Unsupported diff CRXD type: {:?}", typ)) + } +} + +impl Script for CrxdImage { + fn default_output_script_type(&self) -> OutputScriptType { + OutputScriptType::Json + } + + fn default_format_type(&self) -> FormatOptions { + FormatOptions::None + } + + fn is_image(&self) -> bool { + true + } + + fn export_image(&self) -> Result { + self.base.draw_diff(&self.diff) + } +} diff --git a/src/scripts/circus/image/mod.rs b/src/scripts/circus/image/mod.rs index 93d9133..292923c 100644 --- a/src/scripts/circus/image/mod.rs +++ b/src/scripts/circus/image/mod.rs @@ -1 +1,2 @@ pub mod crx; +pub mod crxd; diff --git a/src/scripts/mod.rs b/src/scripts/mod.rs index b7003ad..4636d31 100644 --- a/src/scripts/mod.rs +++ b/src/scripts/mod.rs @@ -96,6 +96,8 @@ lazy_static::lazy_static! { Box::new(circus::archive::dat::DatArchiveBuilder::new()), #[cfg(feature = "circus-arc")] Box::new(circus::archive::crm::CrmArchiveBuilder::new()), + #[cfg(feature = "circus-img")] + Box::new(circus::image::crxd::CrxdImageBuilder::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 93abc04..0a0d716 100644 --- a/src/types.rs +++ b/src/types.rs @@ -340,6 +340,9 @@ pub enum ScriptType { #[cfg(feature = "circus-img")] /// Circus CRX Image CircusCrx, + #[cfg(feature = "circus-img")] + /// Circus Differential Image + CircusCrxd, #[cfg(feature = "escude-arc")] /// Escude bin archive EscudeArc,