diff --git a/Cargo.lock b/Cargo.lock index ce1f306..9d60fb0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1414,6 +1414,7 @@ dependencies = [ "base64", "block_compression", "byteorder", + "bytes", "clap 4.6.0", "crc32fast", "csv", diff --git a/Cargo.toml b/Cargo.toml index a8a1b1d..9174fff 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -13,6 +13,7 @@ anyhow = "1" base64 = { version = "0.22", optional = true } block_compression = { version = "0.9", optional = true, default-features = false, features = ["bc7"] } byteorder = { version = "1.5", default-features = false, optional = true} +bytes = { version = "1.11", optional = true } clap = { version = "4.5", features = ["derive"] } crc32fast = { version = "1.5", optional = true } csv = "1.3" @@ -92,7 +93,7 @@ hexen-haus = ["memchr", "utils-str"] hexen-haus-arc = ["hexen-haus"] hexen-haus-img = ["hexen-haus", "image"] kirikiri = ["emote-psb", "fancy-regex", "flate2", "json", "lz4", "utils-escape"] -kirikiri-arc = ["kirikiri", "adler", "fastcdc", "flate2", "include-flate", "int-enum", "msg_tool_build/kirikiri-arc", "parse-size", "sha2", "utils-serde-base64bytes", "utils-simple-pack", "zopfli", "zstd"] +kirikiri-arc = ["kirikiri", "adler", "bytes", "fastcdc", "flate2", "include-flate", "int-enum", "msg_tool_build/kirikiri-arc", "parse-size", "sha2", "utils-serde-base64bytes", "utils-simple-pack", "zopfli", "zstd"] kirikiri-img = ["kirikiri", "image", "libtlg-rs"] musica = [] musica-arc = ["musica", "crc32fast", "flate2", "include-flate", "utils-blowfish", "utils-rc4", "utils-serde-base64bytes", "utils-xored-stream"] diff --git a/src/args.rs b/src/args.rs index 9050e5a..f711935 100644 --- a/src/args.rs +++ b/src/args.rs @@ -723,6 +723,11 @@ pub struct Arg { #[arg(long, global = true, value_parser = get_xp3_game_title_value_parser())] /// Game title for Kirikiri XP3 archive. This is used to decrypt file in archives. pub xp3_game_title: Option, + #[cfg(feature = "kirikiri-arc")] + #[arg(long, global = true)] + /// Print/write debug information for Kirikiri XP3 archive when extracting archive to specifiy location. + /// This is used to find correct configuration for unknown XP3 archives. + pub xp3_debug_archive: bool, #[command(subcommand)] /// Command pub command: Command, diff --git a/src/main.rs b/src/main.rs index c50a382..7b94b1e 100644 --- a/src/main.rs +++ b/src/main.rs @@ -3393,6 +3393,8 @@ fn main() { )), #[cfg(feature = "kirikiri-arc")] xp3_game_title: arg.xp3_game_title.clone(), + #[cfg(feature = "kirikiri-arc")] + xp3_debug_archive: arg.xp3_debug_archive, }); match &arg.command { args::Command::Export { input, output } => { diff --git a/src/scripts/kirikiri/archive/xp3/archive.rs b/src/scripts/kirikiri/archive/xp3/archive.rs index 914aaef..d53a61d 100644 --- a/src/scripts/kirikiri/archive/xp3/archive.rs +++ b/src/scripts/kirikiri/archive/xp3/archive.rs @@ -1,6 +1,7 @@ use super::consts::*; use super::crypt::Crypt; use crate::scripts::base::ReadSeek; +use std::ops::{Deref, DerefMut}; use std::sync::{Arc, Mutex}; /// Represents a single data segment for a file. @@ -35,6 +36,7 @@ pub struct Xp3Entry { pub file_hash: u32, pub original_size: u64, pub archived_size: u64, + pub timestamp: Option, pub segments: Vec, pub extras: Vec, } @@ -47,13 +49,50 @@ impl Xp3Entry { #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)] pub struct ExtraProp { - pub tag: [u8; 4], + pub tag: PropTag, pub data: Vec, } +#[derive(Clone, PartialEq, Eq, PartialOrd, Ord)] +pub struct PropTag { + tag: [u8; 4], +} + +impl std::fmt::Debug for PropTag { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{:?}", bytes::Bytes::copy_from_slice(&self.tag)) + } +} + +impl Deref for PropTag { + type Target = [u8; 4]; + + fn deref(&self) -> &Self::Target { + &self.tag + } +} + +impl DerefMut for PropTag { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.tag + } +} + +impl From<[u8; 4]> for PropTag { + fn from(value: [u8; 4]) -> Self { + PropTag { tag: value } + } +} + +impl PartialEq<&[u8; 4]> for PropTag { + fn eq(&self, other: &&[u8; 4]) -> bool { + &self.tag == *other + } +} + impl ExtraProp { pub fn is_filename_hash(&self) -> bool { - &self.tag == CHUNK_HNFN + self.tag == CHUNK_HNFN } } diff --git a/src/scripts/kirikiri/archive/xp3/consts.rs b/src/scripts/kirikiri/archive/xp3/consts.rs index 85c9223..04df9f7 100644 --- a/src/scripts/kirikiri/archive/xp3/consts.rs +++ b/src/scripts/kirikiri/archive/xp3/consts.rs @@ -7,6 +7,7 @@ pub const CHUNK_INFO: &[u8; 4] = b"info"; pub const CHUNK_SEGM: &[u8; 4] = b"segm"; pub const CHUNK_ADLR: &[u8; 4] = b"adlr"; pub const CHUNK_HNFN: &[u8; 4] = b"hnfn"; +pub const CHUNK_TIME: &[u8; 4] = b"time"; // Index entry flags pub const TVP_XP3_INDEX_ENCODE_METHOD_MASK: u8 = 0x07; diff --git a/src/scripts/kirikiri/archive/xp3/crypt/mod.rs b/src/scripts/kirikiri/archive/xp3/crypt/mod.rs index 4625adf..15f850b 100644 --- a/src/scripts/kirikiri/archive/xp3/crypt/mod.rs +++ b/src/scripts/kirikiri/archive/xp3/crypt/mod.rs @@ -26,6 +26,7 @@ pub fn default_init_crypt(archive: &mut Xp3Archive) -> Result<()> { filename_map.insert(hash, name); } } + archive.extras.retain(|extra| !extra.is_filename_hash()); for entry in &mut archive.entries { if let Some(name) = filename_map.get(&entry.file_hash) { entry.name = name.clone(); diff --git a/src/scripts/kirikiri/archive/xp3/mod.rs b/src/scripts/kirikiri/archive/xp3/mod.rs index 6051be2..b5179db 100644 --- a/src/scripts/kirikiri/archive/xp3/mod.rs +++ b/src/scripts/kirikiri/archive/xp3/mod.rs @@ -171,6 +171,9 @@ impl Xp3Archive { filename: &str, ) -> Result { let mut archive = archive::Xp3Archive::new(stream, config, filename)?; + if config.xp3_debug_archive { + println!("Debug info for {}:\n{:#?}", filename, archive); + } archive.entries.retain(|entry| { let i = &entry.name; !(i.find("$$$ This is a protected archive. $$$").is_some() diff --git a/src/scripts/kirikiri/archive/xp3/read.rs b/src/scripts/kirikiri/archive/xp3/read.rs index 963acb0..8345f14 100644 --- a/src/scripts/kirikiri/archive/xp3/read.rs +++ b/src/scripts/kirikiri/archive/xp3/read.rs @@ -66,6 +66,7 @@ impl Xp3Archive { let mut file_hash = None; let mut original_size = None; let mut archived_size = None; + let mut timestamp = None; let mut segments = Vec::new(); let mut seg_offset = 0; let mut entry_extras = Vec::new(); @@ -116,11 +117,16 @@ impl Xp3Archive { }); seg_offset += original_size; } + } else if &chunk_sig == CHUNK_TIME { + if chunk_size == 8 { + timestamp = Some(index_stream.read_u64()?); + chunk_size -= 8; + } } else { let data = index_stream.read_exact_vec(chunk_size as usize)?; chunk_size = 0; entry_extras.push(ExtraProp { - tag: chunk_sig, + tag: chunk_sig.into(), data, }); } @@ -140,12 +146,16 @@ impl Xp3Archive { archived_size: archived_size.ok_or_else(|| { anyhow::anyhow!("Missing archived size chunk in file entry") })?, + timestamp, segments, extras: entry_extras, }); } else { let data = index_stream.read_exact_vec(size as usize)?; - extras.push(ExtraProp { tag: sig, data }); + extras.push(ExtraProp { + tag: sig.into(), + data, + }); } } } diff --git a/src/types.rs b/src/types.rs index 9fd3723..7c5aef2 100644 --- a/src/types.rs +++ b/src/types.rs @@ -638,6 +638,10 @@ pub struct ExtraConfig { #[cfg(feature = "kirikiri-arc")] /// Game title for Kirikiri XP3 archive. This is used to decrypt file in archives. pub xp3_game_title: Option, + #[cfg(feature = "kirikiri-arc")] + /// Print debug information for Kirikiri XP3 archive when extracting archive to stdout. + /// This is used to find correct configuration for unknown XP3 archives. + pub xp3_debug_archive: bool, } #[cfg(feature = "artemis")]