diff --git a/Cargo.lock b/Cargo.lock index 2d95537..9a9e3c9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1336,6 +1336,7 @@ dependencies = [ "base64", "byteorder", "clap 4.5.48", + "crc32fast", "csv", "ctrlc", "emote-psb", @@ -1343,6 +1344,7 @@ dependencies = [ "fancy-regex", "fastcdc", "flate2", + "include-flate", "int-enum", "jieba-rs", "json", @@ -1382,8 +1384,6 @@ dependencies = [ [[package]] name = "msg_tool_macro" version = "0.2.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1501aeba3591ee077f92da216cb3fa9a34677f3610c0eb322f8d5b364585fc46" dependencies = [ "quote", "syn 2.0.106", diff --git a/Cargo.toml b/Cargo.toml index 78e1153..867c043 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -13,6 +13,7 @@ anyhow = "1" base64 = { version = "0.22", optional = true } byteorder = { version = "1.5", default-features = false, optional = true} clap = { version = "4.5", features = ["derive"] } +crc32fast = { version = "1.5", optional = true } csv = "1.3" ctrlc = "3.4" emote-psb = { version = "0.5", optional = true , features = ["serde"] } @@ -20,6 +21,7 @@ encoding = "0.2" fancy-regex = { version = "0.16", optional = true } fastcdc = { version = "3.2", optional = true } flate2 = { version = "1.1", optional = true } +include-flate = { version = "0.3", optional = true } int-enum = { version = "1.2", optional = true } jieba-rs = { version = "0.8", optional = true } json = { version = "0.12", optional = true } @@ -32,7 +34,7 @@ markup5ever = { version = "0.35", optional = true } markup5ever_rcdom = { version = "0.35", optional = true } memchr = { version = "2.7", optional = true } mozjpeg = { version = "0.10", optional = true } -msg_tool_macro = { version = "0.2.9" } +msg_tool_macro = { path = "./msg_tool_macro" } num_cpus = "1.17" overf = "0.1" parse-size = { version = "1.1", optional = true } @@ -59,7 +61,7 @@ default = ["all-fmt", "image-jpg", "image-jxl", "image-webp", "audio-flac", "jie all-fmt = ["all-script", "all-img", "all-arc", "all-audio"] all-script = ["artemis", "artemis-panmimisoft", "bgi", "cat-system", "circus", "entis-gls", "escude", "ex-hibit", "favorite", "hexen-haus", "kirikiri", "musica", "silky", "softpal", "will-plus", "yaneurao", "yaneurao-itufuru"] all-img = ["bgi-img", "cat-system-img", "circus-img", "emote-img", "hexen-haus-img", "kirikiri-img", "softpal-img", "will-plus-img"] -all-arc = ["artemis-arc", "bgi-arc", "cat-system-arc", "circus-arc", "escude-arc", "ex-hibit-arc", "hexen-haus-arc", "kirikiri-arc", "softpal-arc"] +all-arc = ["artemis-arc", "bgi-arc", "cat-system-arc", "circus-arc", "escude-arc", "ex-hibit-arc", "hexen-haus-arc", "kirikiri-arc", "musica-arc", "softpal-arc"] all-audio = ["bgi-audio", "circus-audio"] artemis = ["stylua", "utils-escape"] artemis-panmimisoft = ["artemis", "rust-ini"] @@ -89,6 +91,7 @@ kirikiri = ["emote-psb", "fancy-regex", "flate2", "json", "lz4", "utils-escape"] kirikiri-arc = ["kirikiri", "adler", "fastcdc", "flate2", "parse-size", "sha2", "xp3", "zstd"] kirikiri-img = ["kirikiri", "image", "libtlg-rs"] musica = [] +musica-arc = ["musica", "crc32fast", "include-flate", "utils-blowfish", "utils-rc4", "utils-serde-base64bytes", "utils-xored-stream"] silky = [] softpal = ["int-enum"] softpal-arc = ["softpal"] @@ -96,7 +99,7 @@ softpal-img = ["softpal", "image"] will-plus = ["utils-str"] will-plus-img = ["will-plus", "image"] yaneurao = [] -yaneurao-itufuru = ["yaneurao"] +yaneurao-itufuru = ["yaneurao", "utils-xored-stream"] # basic feature image = ["png"] image-jpg = ["mozjpeg"] @@ -112,7 +115,10 @@ utils-blowfish = ["byteorder"] utils-crc32 = [] utils-escape = ["fancy-regex"] utils-pcm = [] +utils-rc4 = [] +utils-serde-base64bytes = ["base64"] utils-str = [] +utils-xored-stream = [] [target.'cfg(windows)'.dependencies] windows-sys = { version = "0.61", features = ["Win32_Globalization", "Win32_System_Diagnostics_Debug"] } diff --git a/README.md b/README.md index 5985fd6..f6a0f79 100644 --- a/README.md +++ b/README.md @@ -197,7 +197,11 @@ msg-tool create -t ### Musica | Script Type | Feature Name | Name | Export | Import | Export Multiple | Import Multiple | Custom Export | Custom Import | Create | Remarks | |---|---|---|---|---|---|---|---|---|---|---| -| `musica-sc` | `musica` | Musica Script File (.sc) | ✔️ | ✔️ | ❌ | ❌ | ❌ | ❌ | ❌ | | +| `musica` | `musica` | Musica Script File (.sc) | ✔️ | ✔️ | ❌ | ❌ | ❌ | ❌ | ❌ | | + +| Archive Type | Feature Name | Name | Unpack | Pack | Remarks | +|---|---|---|---|---|---| +| `musica-arc` | `musica-arc` | Musica Archive Resource File (.paz) | ✔️ | ❌ | | ### Silky Engine | Script Type | Feature Name | Name | Export | Import | Export Multiple | Import Multiple | Custom Export | Custom Import | Create | Remarks | |---|---|---|---|---|---|---|---|---|---|---| diff --git a/msg_tool_macro/src/lib.rs b/msg_tool_macro/src/lib.rs index 592c772..c9fb42a 100644 --- a/msg_tool_macro/src/lib.rs +++ b/msg_tool_macro/src/lib.rs @@ -71,6 +71,7 @@ pub fn struct_unpack_impl_for_num(item: TokenStream) -> TokenStream { /// ``` /// /// * `skip_pack` attribute can be used to skip fields from packing. +/// * `cstring` attribute can be used to specify that a String field is a C-style string (null-terminated). /// * `fstring = ` attribute can be used to specify a fixed string length for String fields. /// * `fstring_pad = ` attribute can be used to specify a padding byte for fixed strings. (Default is 0) /// * `fvec = ` attribute can be used to specify a fixed vector length for Vec<_> fields. @@ -81,7 +82,16 @@ pub fn struct_unpack_impl_for_num(item: TokenStream) -> TokenStream { /// * `skip_pack_if()` attribute can be used to skip packing a field if the expression evaluates to true. The expression must be a valid Rust expression that evaluates to a boolean. #[proc_macro_derive( StructPack, - attributes(skip_pack, fstring, fstring_pad, fvec, pstring, pvec, skip_pack_if) + attributes( + skip_pack, + cstring, + fstring, + fstring_pad, + fvec, + pstring, + pvec, + skip_pack_if + ) )] pub fn struct_pack_derive(input: TokenStream) -> TokenStream { let a = syn::parse_macro_input!(input as PackStruct); @@ -98,10 +108,13 @@ pub fn struct_pack_derive(input: TokenStream) -> TokenStream { let mut pvec_type: Option = None; let mut cur = None; let mut skip_if = None; + let mut is_cstring = false; for attr in &field.attrs { let path = attr.path(); if path.is_ident("skip_pack") { skipped = true; + } else if path.is_ident("cstring") { + is_cstring = true; } else if path.is_ident("fstring") { if let syn::Meta::NameValue(nv) = &attr.meta { if let syn::Expr::Lit(lit) = &nv.value { @@ -181,7 +194,13 @@ pub fn struct_pack_derive(input: TokenStream) -> TokenStream { if let syn::Type::Path(type_path) = field_type { if let Some(segment) = type_path.path.segments.last() { if segment.ident == "String" { - if let Some(fixed_string) = fixed_string { + if is_cstring { + cur = Some(quote::quote! { + let s = encode_string(encoding, &self.#field_name, true)?; + writer.write_all(&s)?; + writer.write_all(&[0])?; + }); + } else if let Some(fixed_string) = fixed_string { cur = Some(quote::quote! { let s = encode_string(encoding, &self.#field_name, true)?; let mut slen = s.len(); @@ -280,10 +299,13 @@ pub fn struct_pack_derive(input: TokenStream) -> TokenStream { let mut pvec_type: Option = None; let mut cur = None; let mut skip_if = None; + let mut is_cstring = false; for attr in &field.attrs { let path = attr.path(); if path.is_ident("skip_pack") { skipped = true; + } else if path.is_ident("cstring") { + is_cstring = true; } else if path.is_ident("fstring") { if let syn::Meta::NameValue(nv) = &attr.meta { if let syn::Expr::Lit(lit) = &nv.value { @@ -364,7 +386,13 @@ pub fn struct_pack_derive(input: TokenStream) -> TokenStream { if let syn::Type::Path(type_path) = field_type { if let Some(segment) = type_path.path.segments.last() { if segment.ident == "String" { - if let Some(fixed_string) = fixed_string { + if is_cstring { + cur = Some(quote::quote! { + let s = encode_string(encoding, &#field_name, true)?; + writer.write_all(&s)?; + writer.write_all(&[0])?; + }); + } else if let Some(fixed_string) = fixed_string { cur = Some(quote::quote! { let s = encode_string(encoding, &#field_name, true)?; let mut slen = s.len(); @@ -464,6 +492,7 @@ pub fn struct_pack_derive(input: TokenStream) -> TokenStream { /// ``` /// /// * `skip_unpack` attribute can be used to skip fields from unpacking. +/// * `cstring` attribute can be used to specify that a String field is a C-style string (null-terminated). /// * `fstring = ` attribute can be used to specify a fixed string length for String fields. /// * `fstring_no_trim` attribute can be used to disable trimming of fixed strings. /// * `fvec = ` attribute can be used to specify a fixed vector length for Vec<_> fields. @@ -476,6 +505,7 @@ pub fn struct_pack_derive(input: TokenStream) -> TokenStream { StructUnpack, attributes( skip_unpack, + cstring, fstring, fstring_no_trim, fvec, @@ -499,10 +529,13 @@ pub fn struct_unpack_derive(input: TokenStream) -> TokenStream { let mut pvec_type: Option = None; let mut cur = None; let mut skip_if: Option = None; + let mut is_cstring = false; for attr in &field.attrs { let path = attr.path(); if path.is_ident("skip_unpack") { skipped = true; + } else if path.is_ident("cstring") { + is_cstring = true; } else if path.is_ident("fstring") { if let syn::Meta::NameValue(nv) = &attr.meta { if let syn::Expr::Lit(lit) = &nv.value { @@ -580,7 +613,12 @@ pub fn struct_unpack_derive(input: TokenStream) -> TokenStream { if let syn::Type::Path(type_path) = field_type { if let Some(segment) = type_path.path.segments.last() { if segment.ident == "String" { - if let Some(fixed_string) = fixed_string { + if is_cstring { + cur = Some(quote::quote! { + let #field_name = reader.read_cstring()?; + let #field_name = decode_to_string(encoding, (#field_name).as_bytes(), true)?; + }); + } else if let Some(fixed_string) = fixed_string { let trim = syn::LitBool::new(!fstring_no_trim, field.span()); cur = Some(quote::quote! { let #field_name = reader.read_fstring(#fixed_string, encoding, #trim)?; diff --git a/src/args.rs b/src/args.rs index 7b7b097..a590d61 100644 --- a/src/args.rs +++ b/src/args.rs @@ -71,6 +71,14 @@ fn parse_jxl_distance(s: &str) -> Result { number_range(s, 0.0, 25.0) } +#[cfg(feature = "musica-arc")] +pub fn get_musica_game_title_value_parser() -> Vec { + crate::scripts::musica::archive::paz::get_supported_games() + .iter() + .map(|s| clap::builder::PossibleValue::new(s)) + .collect() +} + /// Tools for export and import scripts #[derive(Parser, Debug, Clone)] #[clap( @@ -529,6 +537,10 @@ pub struct Arg { #[arg(long, global = true)] /// Insert new language at the specified index in Kirikiri SCN script. If index is out of bounds, this flags will be ignored. pub kirikiri_language_insert: bool, + #[cfg(feature = "musica-arc")] + #[arg(long, global = true, value_parser = get_musica_game_title_value_parser())] + /// Musica game title for paz archive. + pub musica_game_title: Option, #[command(subcommand)] /// Command pub command: Command, diff --git a/src/ext/io.rs b/src/ext/io.rs index 0b13c7b..0c1f1e9 100644 --- a/src/ext/io.rs +++ b/src/ext/io.rs @@ -1,4 +1,5 @@ //!Extensions for IO operations. +use crate::scripts::base::ReadSeek; use crate::types::Encoding; use crate::utils::encoding::decode_to_string; use crate::utils::struct_pack::{StructPack, StructUnpack}; @@ -1792,7 +1793,8 @@ impl Seek for StreamRegion { )); } self.cur_pos = new_pos - self.start_pos; - self.stream.seek(SeekFrom::Start(new_pos)) + self.stream.seek(SeekFrom::Start(new_pos))?; + Ok(self.cur_pos) } fn stream_position(&mut self) -> Result { @@ -2166,3 +2168,117 @@ impl Seek for PrefixStream { Ok(()) } } + +/// A readable stream that concatenates multiple streams. +#[derive(Debug)] +pub struct MultipleReadStream { + streams: Vec>, + stream_lengths: Vec, + total_length: u64, + pos: u64, +} + +impl MultipleReadStream { + /// Creates a new `MultipleReadStream`. + pub fn new() -> Self { + MultipleReadStream { + streams: Vec::new(), + stream_lengths: Vec::new(), + total_length: 0, + pos: 0, + } + } + + /// Adds a new stream to the end of the concatenated streams. + pub fn add_stream(&mut self, mut stream: T) -> Result<()> { + let length = stream.stream_length()?; + self.streams.push(Box::new(stream)); + self.stream_lengths.push(self.total_length); + self.total_length += length; + Ok(()) + } + + /// Adds a new boxed stream to the end of the concatenated streams. + pub fn add_stream_boxed(&mut self, mut stream: Box) -> Result<()> { + let length = stream.stream_length()?; + self.streams.push(stream); + self.stream_lengths.push(self.total_length); + self.total_length += length; + Ok(()) + } +} + +impl Read for MultipleReadStream { + fn read(&mut self, buf: &mut [u8]) -> Result { + if self.pos >= self.total_length { + return Ok(0); + } + let (stream_index, stream_offset) = match self.stream_lengths.binary_search_by(|&len| { + if len > self.pos { + std::cmp::Ordering::Greater + } else { + std::cmp::Ordering::Less + } + }) { + Ok(index) => (index, 0), + Err(0) => (0, self.pos), + Err(index) => (index - 1, self.pos - self.stream_lengths[index - 1]), + }; + let stream = &mut self.streams[stream_index]; + stream.seek(SeekFrom::Start(stream_offset))?; + let bytes_read = stream.read(buf)?; + self.pos += bytes_read as u64; + Ok(bytes_read) + } +} + +impl Seek for MultipleReadStream { + fn seek(&mut self, pos: SeekFrom) -> Result { + let new_pos = match pos { + SeekFrom::Start(offset) => offset, + SeekFrom::End(offset) => { + if offset < 0 { + if (-offset) as u64 > self.total_length { + return Err(std::io::Error::new( + std::io::ErrorKind::InvalidInput, + "Seek position is before the start of the stream", + )); + } + self.total_length - (-offset) as u64 + } else { + self.total_length + offset as u64 + } + } + SeekFrom::Current(offset) => { + if offset < 0 { + if (-offset) as u64 > self.pos { + return Err(std::io::Error::new( + std::io::ErrorKind::InvalidInput, + "Seek position is before the start of the stream", + )); + } + self.pos - (-offset) as u64 + } else { + self.pos + offset as u64 + } + } + }; + if new_pos > self.total_length { + return Err(std::io::Error::new( + std::io::ErrorKind::InvalidInput, + "Seek position is beyond the end of the stream", + )); + } + self.pos = new_pos; + Ok(self.pos) + } + + fn stream_position(&mut self) -> Result { + Ok(self.pos) + } + + fn rewind(&mut self) -> Result<()> { + self.pos = 0; + Ok(()) + } +} diff --git a/src/main.rs b/src/main.rs index 0c5ca8e..04c627c 100644 --- a/src/main.rs +++ b/src/main.rs @@ -2894,6 +2894,8 @@ fn main() { xp3_pack_workers: arg.xp3_pack_workers, #[cfg(feature = "kirikiri")] kirikiri_language_insert: arg.kirikiri_language_insert, + #[cfg(feature = "musica-arc")] + musica_game_title: arg.musica_game_title.clone(), }); match &arg.command { args::Command::Export { input, output } => { diff --git a/src/scripts/mod.rs b/src/scripts/mod.rs index 16902e0..48bd477 100644 --- a/src/scripts/mod.rs +++ b/src/scripts/mod.rs @@ -160,6 +160,8 @@ lazy_static::lazy_static! { Box::new(kirikiri::archive::xp3::Xp3ArchiveBuilder::new()), #[cfg(feature = "musica")] Box::new(musica::sc::MusicaBuilder::new()), + #[cfg(feature = "musica-arc")] + Box::new(musica::archive::paz::PazArcBuilder::new()), ]; /// A list of all script extensions. pub static ref ALL_EXTS: Vec = diff --git a/src/scripts/musica/archive/mod.rs b/src/scripts/musica/archive/mod.rs new file mode 100644 index 0000000..907e471 --- /dev/null +++ b/src/scripts/musica/archive/mod.rs @@ -0,0 +1 @@ +pub mod paz; diff --git a/src/scripts/musica/archive/paz.json b/src/scripts/musica/archive/paz.json new file mode 100644 index 0000000..1271199 --- /dev/null +++ b/src/scripts/musica/archive/paz.json @@ -0,0 +1,46 @@ +{ + "Trinoline: Genesis": { + "Version": 2, + "ArcKeys": { + "bg": { + "IndexKey": "gsBKy6b+2Q7TsfxB5Ku602R+GUmr5nVdcrgx9UcN/eg=", + "DataKey": "PtoqotLwtZL+mXJXaFvLUfpqHtaG140tuUMxNnpew2Y=" + }, + "bgm": { + "IndexKey": "OL7DoI/A0h1v1f4mfHRVhyfN0TMLbtUFcWkIc5VBMqM=", + "DataKey": "lq5NKuGdlmtrvXMsOWrQyw7L7JPk+aR4PWBKEJ7VSV0=" + }, + "mov": { + "IndexKey": "2vKxRqrmrS7UW5UgPps4M1V6lknfQlxSmybdA+Xfkjs=", + "DataKey": null + }, + "scr": { + "IndexKey": "5xrr9exVXAsdiAhQngTm7I7vyZEcqDV7AgZ5uDOohwA=", + "DataKey": "pR8d01HN8urgXbCZ1aA/vXRcT9PBCe3DEYeQqaW6oYo=" + }, + "se": { + "IndexKey": "zzEKw7ssKqW/od1ivGVwIQVQllFFaKF4sRg1rD0GOSk=", + "DataKey": "712To4p5wVuxPoW87ObKjYN5/0IEi4Fj0R4IInhZ1+A=" + }, + "st": { + "IndexKey": "RsLMdkbToHJpnk9HnFAIXr8kmTY3yxHX9P8RLjcao+4=", + "DataKey": "VR4vmT01xzL3c/xkikKkMcFN0A+ARJ7VkFnkbjzodSU=" + }, + "sys": { + "IndexKey": "zode7k5uiB6gVYBYTfgOlIyVOJaZ44HjHPCeSMqO6nI=", + "DataKey": "B63q2sRUCogFGzncYBRuVAG4bHsdd/JZzYcJTiF+j5E=" + }, + "voice": { + "IndexKey": "GMjUQl418C+WfOQUtzmiVb4zB1ll/iZu3QzLTZYX2Pk=", + "DataKey": "Rs4OdRr3usmf0WQctaCuGjqK0Yv8fihNZwltRbXbZMs=" + } + }, + "TypeKeys": { + "png": "1jTdPWvv", + "ogg": "GPxb5R68", + "sc": "WdLwefCN", + "avi": "wdiz7GQH" + }, + "Signature": 2223998086 + } +} \ No newline at end of file diff --git a/src/scripts/musica/archive/paz.rs b/src/scripts/musica/archive/paz.rs new file mode 100644 index 0000000..1d0f733 --- /dev/null +++ b/src/scripts/musica/archive/paz.rs @@ -0,0 +1,404 @@ +use crate::ext::io::*; +use crate::scripts::base::*; +use crate::types::*; +use crate::utils::blowfish::*; +use crate::utils::encoding::*; +use crate::utils::rc4::*; +use crate::utils::serde_base64bytes::Base64Bytes; +use crate::utils::struct_pack::*; +use crate::utils::xored_stream::XoredStream; +use anyhow::Result; +use msg_tool_macro::{StructPack, StructUnpack}; +use serde::Deserialize; +use std::collections::{BTreeMap, HashMap}; +use std::io::{Read, Seek, SeekFrom, Write}; +use std::sync::{Arc, Mutex}; + +include_flate::flate!(static PAZ_DATA: str from "src/scripts/musica/archive/paz.json" with zstd); + +#[derive(Clone, Debug, Deserialize)] +#[serde(rename_all = "PascalCase")] +struct ArcKey { + index_key: Base64Bytes, + data_key: Option, +} + +#[derive(Clone, Debug, Deserialize)] +#[serde(rename_all = "PascalCase")] +struct Schema { + version: u32, + arc_keys: HashMap, + type_keys: HashMap, + /// PAZ file signature + signature: u32, +} + +impl Schema { + pub fn get_type_key(&self, entry: &PazEntry) -> Option<&str> { + let name = std::path::Path::new(&entry.name) + .extension()? + .to_string_lossy() + .to_lowercase(); + self.type_keys.get(&name).map(|s| s.as_str()) + } +} + +lazy_static::lazy_static! { + static ref PAZ_SCHEMA: BTreeMap = { + serde_json::from_str(&PAZ_DATA).expect("Failed to parse paz.json") + }; +} + +/// Get the supported game titles for PAZ archives. +pub fn get_supported_games() -> Vec<&'static str> { + PAZ_SCHEMA.keys().map(|s| s.as_str()).collect() +} + +fn query_paz_schema(game: &str) -> Option<&'static Schema> { + PAZ_SCHEMA.get(game) +} + +fn query_paz_schema_by_signature(signature: u32) -> Option<(&'static str, &'static Schema)> { + for (game, schema) in PAZ_SCHEMA.iter() { + if schema.signature == signature { + return Some((game.as_str(), schema)); + } + } + None +} + +#[derive(Debug)] +pub struct PazArcBuilder {} + +impl PazArcBuilder { + pub fn new() -> Self { + PazArcBuilder {} + } +} + +impl ScriptBuilder for PazArcBuilder { + fn default_encoding(&self) -> Encoding { + Encoding::Cp932 + } + + fn default_archive_encoding(&self) -> Option { + Some(Encoding::Cp932) + } + + fn build_script( + &self, + buf: Vec, + filename: &str, + _encoding: Encoding, + archive_encoding: Encoding, + config: &ExtraConfig, + _archive: Option<&Box>, + ) -> Result> { + Ok(Box::new(PazArc::new( + MemReader::new(buf), + filename, + archive_encoding, + config, + )?)) + } + + fn build_script_from_file( + &self, + filename: &str, + _encoding: Encoding, + archive_encoding: Encoding, + config: &ExtraConfig, + _archive: Option<&Box>, + ) -> Result> { + let f = std::fs::File::open(filename)?; + let f = std::io::BufReader::new(f); + Ok(Box::new(PazArc::new( + f, + filename, + 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(PazArc::new( + reader, + filename, + archive_encoding, + config, + )?)) + } + + fn extensions(&self) -> &'static [&'static str] { + &["paz"] + } + + fn is_archive(&self) -> bool { + true + } + + fn script_type(&self) -> &'static ScriptType { + &ScriptType::MusicaPaz + } + + fn is_this_format(&self, _filename: &str, buf: &[u8], buf_len: usize) -> Option { + if buf_len >= 4 { + let sign = u32::from_le_bytes([buf[0], buf[1], buf[2], buf[3]]); + if let Some(_) = query_paz_schema_by_signature(sign) { + return Some(10); + } + } + None + } +} + +#[derive(Debug, StructPack, StructUnpack, Clone)] +struct PazEntry { + #[cstring] + name: String, + offset: u64, + unpacked_size: u32, + size: u32, + aligned_size: u32, + flags: u32, +} + +#[derive(Debug)] +pub struct PazArc { + stream: Arc>, + schema: Schema, + arc_key: ArcKey, + entries: Vec, + archive_encoding: Encoding, + xor_key: u8, +} + +impl PazArc { + pub fn new( + reader: T, + filename: &str, + archive_encoding: Encoding, + config: &ExtraConfig, + ) -> Result { + let mut stream = MultipleReadStream::new(); + stream.add_stream(reader)?; + for suffix in b'A'..=b'Z' { + let arc_filename = format!("{}{}", filename, suffix as char); + if let Ok(f) = std::fs::File::open(&arc_filename) { + let f = std::io::BufReader::new(f); + stream.add_stream_boxed(Box::new(f))?; + } else { + break; + } + } + let schema = if let Some(title) = &config.musica_game_title { + let schema = query_paz_schema(title).ok_or_else(|| { + anyhow::anyhow!("Unsupported game title '{}' for PAZ archive", title) + })?; + let sig = stream.read_u32()?; + if schema.signature != 0 && schema.signature != sig { + eprintln!( + "Warning: PAZ signature {:08X} does not match expected signature {:08X} for game '{}'", + sig, schema.signature, title + ); + crate::COUNTER.inc_warning(); + } + schema + } else { + let sig = stream.read_u32()?; + let (game, schema) = query_paz_schema_by_signature(sig).ok_or_else(|| { + anyhow::anyhow!( + "Unknown PAZ signature {:08X}. Please specify the game title in the config.", + sig + ) + })?; + eprintln!("Detected PAZ archive for game '{}'", game); + schema + }; + let arc_name = std::path::Path::new(filename) + .file_stem() + .and_then(|s| s.to_str()) + .ok_or_else(|| anyhow::anyhow!("Invalid filename"))? + .to_lowercase(); + let arc_key = schema.arc_keys.get(&arc_name).ok_or_else(|| { + anyhow::anyhow!( + "No ARC key found for archive name '{}' in game schema", + arc_name + ) + })?; + let mut start_offset = if schema.version > 0 { 0x20 } else { 0 }; + stream.seek(SeekFrom::Start(start_offset))?; + let mut index_size = stream.read_u32()?; + start_offset += 4; + let xor_key = (index_size >> 24) as u8; + if xor_key != 0 { + let t = xor_key as u32; + index_size ^= t << 24 | t << 16 | t << 8 | t; + } + if index_size & 7 != 0 { + return Err(anyhow::anyhow!("Invalid PAZ index size")); + } + let entries = { + let blowfish: Blowfish = Blowfish::new(&arc_key.index_key)?; + let mut index_stream: Box = Box::new(StreamRegion::new( + &mut stream, + start_offset, + start_offset + index_size as u64, + )?); + if xor_key != 0 { + index_stream = Box::new(XoredStream::new(index_stream, xor_key)); + } + let mut index_stream = BlowfishDecryptor::new(blowfish.clone(), index_stream); + let count = index_stream.read_u32()?; + let mut entries = Vec::with_capacity(count as usize); + for _ in 0..count { + let entry: PazEntry = index_stream.read_struct(false, archive_encoding)?; + entries.push(entry); + } + entries + }; + Ok(PazArc { + stream: Arc::new(Mutex::new(stream)), + schema: schema.clone(), + arc_key: arc_key.clone(), + entries, + archive_encoding, + xor_key, + }) + } +} + +impl Script for PazArc { + 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(|entry| Ok(entry.name.clone())), + )) + } + + fn iter_archive_offset<'a>(&'a self) -> Result> + 'a>> { + Ok(Box::new(self.entries.iter().map(|entry| Ok(entry.offset)))) + } + + fn open_file<'a>(&'a self, index: usize) -> Result> { + if index >= self.entries.len() { + return Err(anyhow::anyhow!("Index out of bounds")); + } + let entry = self.entries[index].clone(); + let stream = XoredStream::new( + StreamRegion::new( + MutexWrapper::new(self.stream.clone(), entry.offset), + entry.offset, + entry.offset + entry.aligned_size as u64, + )?, + self.xor_key, + ); + if let Some(data_key) = &self.arc_key.data_key { + let blowfish: Blowfish = Blowfish::new(&data_key.bytes)?; + let stream = StreamRegion::new( + BlowfishDecryptor::new(blowfish, stream), + 0, + entry.size as u64, + )?; + if let Some(type_key) = self.schema.get_type_key(&entry) { + let key = format!( + "{} {:08X} {}", + entry.name.to_ascii_lowercase(), + entry.unpacked_size, + type_key + ); + let key = encode_string(self.archive_encoding, &key, false)?; + let mut rc4 = Rc4::new(&key); + if self.schema.version >= 2 { + let crc = crc32fast::hash(&key); + let skip = ((crc >> 12) as i32) & 0xFF; + rc4.skip_bytes(skip as usize); + } + let stream = Rc4Stream::new(stream, rc4); + return Ok(Box::new(PazFileEntry::new(entry, stream))); + } + return Ok(Box::new(PazFileEntry::new(entry, stream))); + } + Err(anyhow::anyhow!("Data decryption key not found.")) + } +} + +#[derive(Debug)] +struct PazFileEntry { + entry: PazEntry, + stream: T, +} + +impl PazFileEntry { + pub fn new(entry: PazEntry, stream: T) -> Self { + PazFileEntry { entry, stream } + } +} + +impl Read for PazFileEntry { + fn read(&mut self, buf: &mut [u8]) -> std::io::Result { + self.stream.read(buf) + } +} + +impl Seek for PazFileEntry { + fn seek(&mut self, pos: std::io::SeekFrom) -> std::io::Result { + self.stream.seek(pos) + } + + fn rewind(&mut self) -> std::io::Result<()> { + self.stream.rewind() + } + + fn stream_position(&mut self) -> std::io::Result { + self.stream.stream_position() + } +} + +impl ArchiveContent for PazFileEntry { + fn name(&self) -> &str { + &self.entry.name + } +} + +#[test] +fn test_deserialize_paz() { + for (game, schema) in PAZ_SCHEMA.iter() { + println!("Game: {}", game); + println!("Version: {}", schema.version); + for (arc_name, arc_key) in schema.arc_keys.iter() { + println!(" Arc Name: {}", arc_name); + println!(" Index Key: {:02X?}", arc_key.index_key.bytes); + if let Some(data_key) = &arc_key.data_key { + println!(" Data Key: {:02X?}", data_key.bytes); + } else { + println!(" Data Key: None"); + } + } + for (type_name, type_key) in schema.type_keys.iter() { + println!(" Type Name: {}, Type Key: {}", type_name, type_key); + } + println!("Signature: {:08X}", schema.signature); + } +} diff --git a/src/scripts/musica/mod.rs b/src/scripts/musica/mod.rs index 039babb..d455d2e 100644 --- a/src/scripts/musica/mod.rs +++ b/src/scripts/musica/mod.rs @@ -1,2 +1,4 @@ //! Musica scripts +#[cfg(feature = "musica-arc")] +pub mod archive; pub mod sc; diff --git a/src/scripts/yaneurao/itufuru/archive.rs b/src/scripts/yaneurao/itufuru/archive.rs index 566e015..32cec27 100644 --- a/src/scripts/yaneurao/itufuru/archive.rs +++ b/src/scripts/yaneurao/itufuru/archive.rs @@ -1,10 +1,10 @@ //! Yaneurao Itufuru Archive File (.scd) -use super::crypto::*; use crate::ext::io::*; use crate::scripts::base::*; use crate::types::*; use crate::utils::encoding::encode_string; use crate::utils::struct_pack::*; +use crate::utils::xored_stream::XoredStream as Crypto; use anyhow::Result; use msg_tool_macro::*; use std::collections::HashMap; diff --git a/src/scripts/yaneurao/itufuru/mod.rs b/src/scripts/yaneurao/itufuru/mod.rs index 53fd370..a2c7881 100644 --- a/src/scripts/yaneurao/itufuru/mod.rs +++ b/src/scripts/yaneurao/itufuru/mod.rs @@ -1,4 +1,3 @@ //! Yaneurao Itufuru Scripts pub mod archive; -mod crypto; pub mod script; diff --git a/src/types.rs b/src/types.rs index 4d4fd47..1084d74 100644 --- a/src/types.rs +++ b/src/types.rs @@ -506,6 +506,9 @@ pub struct ExtraConfig { #[cfg(feature = "kirikiri")] /// Insert new language at the specified index in Kirikiri SCN script. If index is out of bounds, this flags will be ignored. pub kirikiri_language_insert: bool, + #[cfg(feature = "musica-arc")] + /// Musica game title for paz archive. + pub musica_game_title: Option, } #[derive(Clone, Copy, Debug, ValueEnum, PartialEq, Eq, PartialOrd, Ord)] @@ -683,6 +686,9 @@ pub enum ScriptType { #[cfg(feature = "musica")] /// Musica Script (.sc) Musica, + #[cfg(feature = "musica-arc")] + /// Musica Engine Resource Archive (.paz) + MusicaPaz, #[cfg(feature = "silky")] /// Silky Engine Mes script Silky, diff --git a/src/utils/blowfish.rs b/src/utils/blowfish.rs index 07e0bb9..6b8909d 100644 --- a/src/utils/blowfish.rs +++ b/src/utils/blowfish.rs @@ -168,6 +168,7 @@ mod consts { use anyhow::Result; use byteorder::{BE, ByteOrder, LE}; +use std::io::{Read, Seek}; use std::marker::PhantomData; /// Blowfish variant which uses Little Endian byte order read/writes.s. @@ -181,15 +182,9 @@ pub struct Blowfish { _pd: PhantomData, } -impl std::fmt::Debug for Blowfish { +impl std::fmt::Debug for Blowfish { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.write_str("Blowfish { ... }") - } -} - -impl std::fmt::Debug for Blowfish { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.write_str("Blowfish { ... }") + f.write_str("Blowfish { ... }") } } @@ -290,3 +285,106 @@ impl Blowfish { Ok(blowfish) } } + +#[derive(Debug)] +/// Blowfish decryption stream wrapper. +pub struct BlowfishDecryptor { + cipher: Blowfish, + buffer: [u8; 8], + buffer_len: usize, + buffer_is_decrypted: bool, + stream: T, +} + +impl BlowfishDecryptor { + pub fn new(cipher: Blowfish, stream: T) -> Self { + Self { + cipher, + buffer: [0u8; 8], + buffer_len: 0, + buffer_is_decrypted: false, + stream, + } + } + + pub fn new_with_key(key: &[u8], stream: T) -> Result { + let cipher = Blowfish::new(key)?; + Ok(Self::new(cipher, stream)) + } +} + +impl Read for BlowfishDecryptor { + fn read(&mut self, buf: &mut [u8]) -> std::io::Result { + if self.buffer_is_decrypted && self.buffer_len > 0 { + let to_copy = std::cmp::min(buf.len(), self.buffer_len); + buf[..to_copy].copy_from_slice(&self.buffer[..to_copy]); + if to_copy < self.buffer_len { + for i in 0..(self.buffer_len - to_copy) { + self.buffer[i] = self.buffer[to_copy + i]; + } + } + self.buffer_len -= to_copy; + self.buffer_is_decrypted = self.buffer_len > 0; + return Ok(to_copy); + } + if buf.len() < 8 { + if self.buffer_len < 8 { + self.stream + .read_exact(&mut self.buffer[self.buffer_len..])?; + } + self.cipher.decrypt_block(&mut self.buffer); + self.buffer_len = 8; + self.buffer_is_decrypted = true; + let to_copy = std::cmp::min(buf.len(), self.buffer_len); + buf[..to_copy].copy_from_slice(&self.buffer[..to_copy]); + if to_copy < self.buffer_len { + for i in 0..(self.buffer_len - to_copy) { + self.buffer[i] = self.buffer[to_copy + i]; + } + } + self.buffer_len -= to_copy; + self.buffer_is_decrypted = self.buffer_len > 0; + return Ok(to_copy); + } + let mut start_index = 0; + if self.buffer_len > 0 { + buf[..self.buffer_len].copy_from_slice(&self.buffer[..self.buffer_len]); + start_index += self.buffer_len; + } + let readed = self.stream.read(&mut buf[start_index..])?; + let total_readed = start_index + readed; + let total_decrypted = total_readed - (total_readed % 8); + self.cipher.decrypt_block(&mut buf[..total_decrypted]); + if total_readed != total_decrypted { + self.buffer_len = total_readed - total_decrypted; + self.buffer[..self.buffer_len].copy_from_slice(&buf[total_decrypted..total_readed]); + } else { + self.buffer_len = 0; + } + self.buffer_is_decrypted = false; + Ok(total_decrypted) + } +} + +impl Seek for BlowfishDecryptor { + fn seek(&mut self, pos: std::io::SeekFrom) -> std::io::Result { + if matches!(pos, std::io::SeekFrom::End(0)) { + let newpos = self.stream.seek(pos)?; + self.buffer_len = 0; + self.buffer_is_decrypted = false; + return Ok(newpos); + } + let newpos = self.stream.seek(pos)?; + if newpos % 8 != 0 { + let block_start = newpos - (newpos % 8); + self.stream.seek(std::io::SeekFrom::Start(block_start))?; + self.buffer_len = (newpos - block_start) as usize; + self.stream + .read_exact(&mut self.buffer[..self.buffer_len])?; + } else { + self.buffer_len = 0; + } + self.buffer_is_decrypted = false; + Ok(newpos) + } +} diff --git a/src/utils/mod.rs b/src/utils/mod.rs index 0381bfc..ce7afd9 100644 --- a/src/utils/mod.rs +++ b/src/utils/mod.rs @@ -25,10 +25,16 @@ pub mod name_replacement; pub mod num_range; #[cfg(feature = "utils-pcm")] pub mod pcm; +#[cfg(feature = "utils-rc4")] +pub mod rc4; +#[cfg(feature = "utils-serde-base64bytes")] +pub mod serde_base64bytes; #[cfg(feature = "utils-str")] pub mod str; pub mod struct_pack; pub mod threadpool; +#[cfg(feature = "utils-xored-stream")] +pub mod xored_stream; #[cfg(windows)] pub use encoding_win::WinError; diff --git a/src/utils/rc4.rs b/src/utils/rc4.rs new file mode 100644 index 0000000..68622c7 --- /dev/null +++ b/src/utils/rc4.rs @@ -0,0 +1,87 @@ +use std::io::{Read, Write}; + +pub struct Rc4 { + state: [u8; 256], + i: u8, + j: u8, +} + +impl Rc4 { + pub fn new(key: &[u8]) -> Self { + let mut state = [0u8; 256]; + for i in 0..256 { + state[i] = i as u8; + } + + let mut j: u8 = 0; + for i in 0..256 { + j = j.wrapping_add(state[i]).wrapping_add(key[i % key.len()]); + state.swap(i, j as usize); + } + + Rc4 { state, i: 0, j: 0 } + } + + pub fn next_byte(&mut self) -> u8 { + self.i = self.i.wrapping_add(1); + self.j = self.j.wrapping_add(self.state[self.i as usize]); + self.state.swap(self.i as usize, self.j as usize); + let k = self.state + [(self.state[self.i as usize].wrapping_add(self.state[self.j as usize])) as usize]; + k + } + + pub fn skip_bytes(&mut self, n: usize) { + for _ in 0..n { + self.next_byte(); + } + } + + pub fn generate_block(&mut self, len: usize) -> Vec { + (0..len).map(|_| self.next_byte()).collect() + } + + pub fn process_block(&mut self, data: &mut [u8]) { + for byte in data.iter_mut() { + *byte ^= self.next_byte(); + } + } +} + +pub struct Rc4Stream { + inner: T, + rc4: Rc4, +} + +impl Rc4Stream { + pub fn new(inner: T, rc4: Rc4) -> Self { + Rc4Stream { inner, rc4 } + } + + pub fn new_with_key(inner: T, key: &[u8]) -> Self { + Rc4Stream { + inner, + rc4: Rc4::new(key), + } + } +} + +impl Read for Rc4Stream { + fn read(&mut self, buf: &mut [u8]) -> std::io::Result { + let n = self.inner.read(buf)?; + self.rc4.process_block(&mut buf[..n]); + Ok(n) + } +} + +impl Write for Rc4Stream { + fn write(&mut self, buf: &[u8]) -> std::io::Result { + let mut data = buf.to_vec(); + self.rc4.process_block(&mut data); + self.inner.write(&data) + } + + fn flush(&mut self) -> std::io::Result<()> { + self.inner.flush() + } +} diff --git a/src/utils/serde_base64bytes.rs b/src/utils/serde_base64bytes.rs new file mode 100644 index 0000000..d1f8557 --- /dev/null +++ b/src/utils/serde_base64bytes.rs @@ -0,0 +1,45 @@ +use base64::Engine; +use serde::{Deserialize, Serialize}; +use std::ops::{Deref, DerefMut}; + +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct Base64Bytes { + pub bytes: Vec, +} + +impl Deref for Base64Bytes { + type Target = Vec; + + fn deref(&self) -> &Self::Target { + &self.bytes + } +} + +impl DerefMut for Base64Bytes { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.bytes + } +} + +impl Serialize for Base64Bytes { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + let encoded = base64::engine::general_purpose::STANDARD.encode(&self.bytes); + serializer.serialize_str(&encoded) + } +} + +impl<'de> Deserialize<'de> for Base64Bytes { + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + let s: &str = Deserialize::deserialize(deserializer)?; + let decoded = base64::engine::general_purpose::STANDARD + .decode(s) + .map_err(serde::de::Error::custom)?; + Ok(Base64Bytes { bytes: decoded }) + } +} diff --git a/src/scripts/yaneurao/itufuru/crypto.rs b/src/utils/xored_stream.rs similarity index 78% rename from src/scripts/yaneurao/itufuru/crypto.rs rename to src/utils/xored_stream.rs index 9f46bbd..1585ac0 100644 --- a/src/scripts/yaneurao/itufuru/crypto.rs +++ b/src/utils/xored_stream.rs @@ -1,17 +1,17 @@ use std::io::{Read, Seek, Write}; -pub struct Crypto { +pub struct XoredStream { reader: T, key: u8, } -impl Crypto { +impl XoredStream { pub fn new(reader: T, key: u8) -> Self { - Crypto { reader, key } + XoredStream { reader, key } } } -impl Read for Crypto { +impl Read for XoredStream { fn read(&mut self, buf: &mut [u8]) -> std::io::Result { let read_bytes = self.reader.read(buf)?; for byte in &mut buf[..read_bytes] { @@ -21,7 +21,7 @@ impl Read for Crypto { } } -impl Seek for Crypto { +impl Seek for XoredStream { fn seek(&mut self, pos: std::io::SeekFrom) -> std::io::Result { self.reader.seek(pos) } @@ -35,7 +35,7 @@ impl Seek for Crypto { } } -impl Write for Crypto { +impl Write for XoredStream { fn write(&mut self, buf: &[u8]) -> std::io::Result { let mut encrypted_buf = buf.to_vec(); for byte in &mut encrypted_buf { @@ -49,9 +49,9 @@ impl Write for Crypto { } } -impl std::fmt::Debug for Crypto { +impl std::fmt::Debug for XoredStream { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.debug_struct("Crypto") + f.debug_struct("XoredStream") .field("reader", &self.reader) .field("key", &self.key) .finish()