diff --git a/Cargo.lock b/Cargo.lock index 548f74c..f1bacd8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -106,6 +106,15 @@ version = "2.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1b8e56985ec62d17e9c1001dc89c88ecd7dc08e47eba5ec7c29c7b5eeecde967" +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + [[package]] name = "blowfish" version = "0.9.1" @@ -183,6 +192,15 @@ version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5b63caa9aa9397e2d9480a9b13673856c78d8ac123288526c37d7839f2a86990" +[[package]] +name = "cpufeatures" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +dependencies = [ + "libc", +] + [[package]] name = "crc32fast" version = "1.4.2" @@ -223,6 +241,16 @@ dependencies = [ "memchr", ] +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "crypto-common", +] + [[package]] name = "displaydoc" version = "0.2.5" @@ -613,6 +641,7 @@ dependencies = [ "rand", "serde", "serde_json", + "sha1", "unicode-segmentation", "url", "utf16string", @@ -801,6 +830,17 @@ dependencies = [ "serde", ] +[[package]] +name = "sha1" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + [[package]] name = "simd-adler32" version = "0.3.7" diff --git a/Cargo.toml b/Cargo.toml index 3d0be02..1644998 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -22,12 +22,15 @@ png = { version = "0.17", optional = true } rand = { version = "0.9", optional = true } serde = { version = "1", features = ["derive"] } serde_json = "1" +sha1 = { version = "0.10", optional = true } unicode-segmentation = "1.12" url = { version = "2.5", optional = true } utf16string = "0.2" [features] -default = ["bgi", "bgi-arc", "bgi-img", "cat-system", "cat-system-arc", "cat-system-img", "circus", "escude", "escude-arc", "kirikiri", "kirikiri-img", "will-plus", "yaneurao", "yaneurao-itufuru"] +default = ["artemis", "artemis-arc", "bgi", "bgi-arc", "bgi-img", "cat-system", "cat-system-arc", "cat-system-img", "circus", "escude", "escude-arc", "kirikiri", "kirikiri-img", "will-plus", "yaneurao", "yaneurao-itufuru"] +artemis = [] +artemis-arc = ["artemis", "msg_tool_macro/artemis-arc", "sha1"] bgi = [] bgi-arc = ["bgi", "rand", "utils-bit-stream"] bgi-img = ["bgi", "image", "utils-bit-stream"] diff --git a/msg_tool_macro/Cargo.toml b/msg_tool_macro/Cargo.toml index d37332e..cb6cb77 100644 --- a/msg_tool_macro/Cargo.toml +++ b/msg_tool_macro/Cargo.toml @@ -6,6 +6,9 @@ edition = "2024" [lib] proc-macro = true +[features] +artemis-arc = [] + [dependencies] syn = { version = "2", features = ["full"] } quote = "1" diff --git a/msg_tool_macro/src/lib.rs b/msg_tool_macro/src/lib.rs index 3f7255b..68c56b5 100644 --- a/msg_tool_macro/src/lib.rs +++ b/msg_tool_macro/src/lib.rs @@ -72,7 +72,9 @@ pub fn struct_unpack_impl_for_num(item: TokenStream) -> TokenStream { /// * `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. -#[proc_macro_derive(StructPack, attributes(skip_pack, fstring, fstring_pad, fvec))] +/// * `pstring()` attribute can be used to specify a packed string length for String fields, where `` can be `u8`, `u16`, `u32`, or `u64`. +/// Length is read as a prefix before the string data. +#[proc_macro_derive(StructPack, attributes(skip_pack, fstring, fstring_pad, fvec, pstring))] pub fn struct_pack_derive(input: TokenStream) -> TokenStream { let a = syn::parse_macro_input!(input as PackStruct); match a { @@ -84,6 +86,7 @@ pub fn struct_pack_derive(input: TokenStream) -> TokenStream { let mut fixed_string: Option = None; let mut fixed_vec: Option = None; let mut fstring_pad = 0u8; // Default padding byte + let mut pstring_type: Option = None; for attr in &field.attrs { let path = attr.path(); if path.is_ident("skip_pack") { @@ -112,6 +115,23 @@ pub fn struct_pack_derive(input: TokenStream) -> TokenStream { } } } + } else if path.is_ident("pstring") { + if let syn::Meta::List(list) = &attr.meta { + list.parse_nested_meta(|meta| { + if meta.path.is_ident("u8") { + pstring_type = Some(syn::Ident::new("u8", meta.path.span())); + } else if meta.path.is_ident("u16") { + pstring_type = Some(syn::Ident::new("u16", meta.path.span())); + } else if meta.path.is_ident("u32") { + pstring_type = Some(syn::Ident::new("u32", meta.path.span())); + } else if meta.path.is_ident("u64") { + pstring_type = Some(syn::Ident::new("u64", meta.path.span())); + } else { + return Err(meta.error("Expected u8, u16, or u32 for pstring")); + } + Ok(()) + }).unwrap(); + } } } if skipped { @@ -146,6 +166,14 @@ pub fn struct_pack_derive(input: TokenStream) -> TokenStream { } }; } + if let Some(pstring_type) = pstring_type { + let write_fn = syn::Ident::new(format!("write_{}", pstring_type).as_str(), pstring_type.span()); + return quote::quote! { + let encoded = crate::utils::encoding::encode_string(encoding, &self.#field_name, true)?; + writer.#write_fn(encoded.len() as #pstring_type)?; + writer.write_all(&encoded)?; + }; + } } } if let Some(segment) = type_path.path.segments.first() { @@ -321,7 +349,9 @@ pub fn struct_pack_derive(input: TokenStream) -> TokenStream { /// * `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. -#[proc_macro_derive(StructUnpack, attributes(skip_unpack, fstring, fstring_no_trim, fvec))] +/// * `pstring()` attribute can be used to specify a packed string length for String fields, where `` can be `u8`, `u16`, `u32` or `u64`. +/// length is read as a prefix before the string data. +#[proc_macro_derive(StructUnpack, attributes(skip_unpack, fstring, fstring_no_trim, fvec, pstring))] pub fn struct_unpack_derive(input: TokenStream) -> TokenStream { let sut = syn::parse_macro_input!(input as syn::ItemStruct); let name = sut.ident; @@ -333,6 +363,7 @@ pub fn struct_unpack_derive(input: TokenStream) -> TokenStream { let mut fixed_string: Option = None; let mut fstring_no_trim = false; let mut fixed_vec: Option = None; + let mut pstring_type: Option = None; for attr in &field.attrs { let path = attr.path(); if path.is_ident("skip_unpack") { @@ -355,6 +386,23 @@ pub fn struct_unpack_derive(input: TokenStream) -> TokenStream { } } } + } else if path.is_ident("pstring") { + if let syn::Meta::List(list) = &attr.meta { + list.parse_nested_meta(|meta| { + if meta.path.is_ident("u8") { + pstring_type = Some(syn::Ident::new("u8", meta.path.span())); + } else if meta.path.is_ident("u16") { + pstring_type = Some(syn::Ident::new("u16", meta.path.span())); + } else if meta.path.is_ident("u32") { + pstring_type = Some(syn::Ident::new("u32", meta.path.span())); + } else if meta.path.is_ident("u64") { + pstring_type = Some(syn::Ident::new("u64", meta.path.span())); + } else { + return Err(meta.error("Expected u8, u16, or u32 for pstring")); + } + Ok(()) + }).unwrap(); + } } } let field_name = match &field.ident { @@ -382,6 +430,14 @@ pub fn struct_unpack_derive(input: TokenStream) -> TokenStream { let #field_name = reader.read_fstring(#fixed_string, encoding, #trim)?; }; } + if let Some(pstring_type) = pstring_type { + let read_fn = syn::Ident::new(format!("read_{}", pstring_type).as_str(), pstring_type.span()); + return quote::quote! { + let len = reader.#read_fn()? as usize; + let #field_name = reader.read_exact_vec(len)?; + let #field_name = crate::utils::encoding::decode_to_string(encoding, &#field_name, true)?; + } + } } } if let Some(segment) = type_path.path.segments.first() { @@ -413,3 +469,20 @@ pub fn struct_unpack_derive(input: TokenStream) -> TokenStream { }; output.into() } + +#[cfg(feature = "artemis-arc")] +#[proc_macro] +pub fn gen_artemis_arc_ext(_: TokenStream) -> TokenStream { + let mut exts = Vec::new(); + exts.push(quote::quote! { "pfs" }); + for i in 0..=999 { + let ext = format!("pfs.{:03}", i); + exts.push(quote::quote! { #ext }); + } + let output = quote::quote! { + &[ + #(#exts),* + ] + }; + output.into() +} diff --git a/src/scripts/artemis/archive/mod.rs b/src/scripts/artemis/archive/mod.rs new file mode 100644 index 0000000..b92ef74 --- /dev/null +++ b/src/scripts/artemis/archive/mod.rs @@ -0,0 +1 @@ +pub mod pfs; diff --git a/src/scripts/artemis/archive/pfs.rs b/src/scripts/artemis/archive/pfs.rs new file mode 100644 index 0000000..b225718 --- /dev/null +++ b/src/scripts/artemis/archive/pfs.rs @@ -0,0 +1,280 @@ +use crate::ext::io::*; +use crate::scripts::base::*; +use crate::types::*; +use crate::utils::struct_pack::*; +use anyhow::Result; +use msg_tool_macro::*; +use sha1::Digest; +use std::io::{Read, Seek, SeekFrom, Write}; +use std::sync::{Arc, Mutex}; + +#[derive(Debug)] +pub struct ArtemisArcBuilder {} + +impl ArtemisArcBuilder { + pub fn new() -> Self { + ArtemisArcBuilder {} + } +} + +impl ScriptBuilder for ArtemisArcBuilder { + fn default_encoding(&self) -> Encoding { + Encoding::Utf8 + } + + fn default_archive_encoding(&self) -> Option { + Some(Encoding::Utf8) + } + + fn build_script( + &self, + buf: Vec, + _filename: &str, + _encoding: Encoding, + archive_encoding: Encoding, + config: &ExtraConfig, + ) -> Result> { + Ok(Box::new(ArtemisArc::new( + MemReader::new(buf), + archive_encoding, + config, + )?)) + } + + fn build_script_from_file( + &self, + filename: &str, + _encoding: Encoding, + archive_encoding: Encoding, + config: &ExtraConfig, + ) -> Result> { + let f = std::fs::File::open(filename)?; + let f = std::io::BufReader::new(f); + Ok(Box::new(ArtemisArc::new(f, archive_encoding, config)?)) + } + + fn build_script_from_reader( + &self, + reader: Box, + _filename: &str, + _encoding: Encoding, + archive_encoding: Encoding, + config: &ExtraConfig, + ) -> Result> { + Ok(Box::new(ArtemisArc::new(reader, archive_encoding, config)?)) + } + + fn extensions(&self) -> &'static [&'static str] { + gen_artemis_arc_ext!() + } + + fn is_archive(&self) -> bool { + true + } + + fn script_type(&self) -> &'static ScriptType { + &ScriptType::ArtemisArc + } +} + +#[derive(Debug, Clone, StructPack, StructUnpack)] +struct PfsEntryHeader { + #[pstring(u32)] + name: String, + _unk: u32, + offset: u32, + size: u32, +} + +#[derive(Debug)] +pub struct ArtemisArc { + reader: Arc>, + entries: Vec, + xor_key: Option<[u8; 20]>, +} + +impl ArtemisArc { + pub fn new(mut reader: T, archive_encoding: Encoding, _config: &ExtraConfig) -> Result { + let mut magic = [0; 2]; + reader.read_exact(&mut magic)?; + if &magic != b"pf" { + return Err(anyhow::anyhow!( + "Invalid Artemis archive magic: {:?}", + magic + )); + } + let version = reader.read_u8()?; + if version != b'2' && version != b'6' && version != b'8' { + return Err(anyhow::anyhow!( + "Unsupported Artemis archive version: {}", + version + )); + } + let index_size = reader.read_u32()?; + let file_count = reader.read_u32()?; + let mut entries = Vec::with_capacity(file_count as usize); + for _ in 0..file_count { + let header = reader.read_struct(false, archive_encoding)?; + entries.push(header); + } + let xor_key = if version == b'8' { + reader.seek(SeekFrom::Start(7))?; + let mut sha = sha1::Sha1::default(); + let ra = &mut reader; + let mut r = ra.take(index_size as u64); + std::io::copy(&mut r, &mut sha)?; + sha.flush()?; + let result = sha.finalize(); + let mut xor_key = [0u8; 20]; + xor_key.copy_from_slice(&result); + Some(xor_key) + } else { + None + }; + Ok(ArtemisArc { + reader: Arc::new(Mutex::new(reader)), + entries, + xor_key, + }) + } +} + +impl Script for ArtemisArc { + 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<'a>(&'a mut self) -> Result> + 'a>> { + Ok(Box::new( + self.entries.iter().map(|header| Ok(header.name.clone())), + )) + } + + fn iter_archive_mut<'a>( + &'a mut self, + ) -> Result>> + 'a>> { + Ok(Box::new(ArtemisArcIter { + entries: self.entries.iter(), + reader: self.reader.clone(), + xor_key: self.xor_key.clone(), + })) + } +} + +struct Entry { + header: PfsEntryHeader, + reader: Arc>, + pos: u64, + script_type: Option, + xor_key: Option<[u8; 20]>, +} + +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))?; + let bytes_read = buf.len().min(self.header.size as usize - self.pos as usize); + if bytes_read == 0 { + return Ok(0); + } + let bytes_read = reader.read(&mut buf[..bytes_read])?; + if let Some(xor_key) = &self.xor_key { + for i in 0..bytes_read { + let l = (self.pos + i as u64) % 20; + buf[i] ^= xor_key[l as usize]; + } + } + self.pos += bytes_read as u64; + 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, + SeekFrom::End(offset) => { + if offset < 0 { + if (-offset) as u64 > self.header.size as u64 { + return Err(std::io::Error::new( + std::io::ErrorKind::InvalidInput, + "Seek from end exceeds file length", + )); + } + self.header.size as u64 - (-offset) as u64 + } else { + self.header.size as u64 + 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 from current exceeds current position", + )); + } + self.pos.saturating_sub((-offset) as u64) + } else { + self.pos + offset as u64 + } + } + }; + self.pos = new_pos; + Ok(self.pos) + } + + fn stream_position(&mut self) -> std::io::Result { + Ok(self.pos) + } +} + +struct ArtemisArcIter<'a, T: Iterator, R: Read + Seek + 'static> { + entries: T, + reader: Arc>, + xor_key: Option<[u8; 20]>, +} + +impl<'a, T: Iterator, R: Read + Seek + 'static> Iterator + for ArtemisArcIter<'a, T, R> +{ + type Item = Result>; + + fn next(&mut self) -> Option { + if let Some(header) = self.entries.next() { + let entry = Entry { + header: header.clone(), + reader: self.reader.clone(), + pos: 0, + script_type: None, + xor_key: self.xor_key.clone(), + }; + Some(Ok(Box::new(entry))) + } else { + None + } + } +} diff --git a/src/scripts/artemis/mod.rs b/src/scripts/artemis/mod.rs new file mode 100644 index 0000000..46b8941 --- /dev/null +++ b/src/scripts/artemis/mod.rs @@ -0,0 +1,2 @@ +#[cfg(feature = "artemis-arc")] +pub mod archive; diff --git a/src/scripts/mod.rs b/src/scripts/mod.rs index 32d9f4e..b2da791 100644 --- a/src/scripts/mod.rs +++ b/src/scripts/mod.rs @@ -1,3 +1,5 @@ +#[cfg(feature = "artemis")] +pub mod artemis; pub mod base; #[cfg(feature = "bgi")] pub mod bgi; @@ -68,6 +70,8 @@ lazy_static::lazy_static! { Box::new(will_plus::ws2::Ws2ScriptBuilder::new()), #[cfg(feature = "cat-system")] Box::new(cat_system::cst::CstScriptBuilder::new()), + #[cfg(feature = "artemis-arc")] + Box::new(artemis::archive::pfs::ArtemisArcBuilder::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 e65a0c0..6d73bad 100644 --- a/src/types.rs +++ b/src/types.rs @@ -228,6 +228,10 @@ pub struct ExtraConfig { #[derive(Clone, Copy, Debug, ValueEnum, PartialEq, Eq, PartialOrd, Ord)] /// Script type pub enum ScriptType { + #[cfg(feature = "artemis-arc")] + #[value(alias("pfs"))] + /// Artemis archive (pfs) + ArtemisArc, #[cfg(feature = "bgi")] #[value(alias("ethornell"))] /// Buriko General Interpreter/Ethornell Script