From 7b0de4468b89dc2fc1b47b42a20ecfe6ce778b63 Mon Sep 17 00:00:00 2001 From: lifegpc Date: Tue, 7 Apr 2026 12:01:27 +0800 Subject: [PATCH] Add support to read embbed control block (tested game: https://vndb.org/v19829 ) --- Cargo.lock | 9 ++ Cargo.toml | 4 +- build.rs | 14 +++ msg_tool_build/.gitignore | 2 + msg_tool_build/Cargo.toml | 18 ++++ msg_tool_build/src/kr_arc.rs | 38 +++++++ msg_tool_build/src/lib.rs | 4 + msg_tool_build/src/simple_pack.rs | 66 ++++++++++++ src/scripts/kirikiri/archive/xp3/archive.rs | 7 ++ src/scripts/kirikiri/archive/xp3/consts.rs | 1 + src/scripts/kirikiri/archive/xp3/crypt.json | 19 ++++ src/scripts/kirikiri/archive/xp3/crypt/cx.rs | 14 ++- .../archive/xp3/crypt/cx_cb/9nine_ep1.bin | 18 ++++ .../xp3/crypt/cx_cb/9nine_ep1_sekai.bin | 15 +++ src/scripts/kirikiri/archive/xp3/crypt/mod.rs | 49 ++++++++- src/utils/mod.rs | 2 + src/utils/simple_pack.rs | 100 ++++++++++++++++++ 17 files changed, 376 insertions(+), 4 deletions(-) create mode 100644 msg_tool_build/.gitignore create mode 100644 msg_tool_build/Cargo.toml create mode 100644 msg_tool_build/src/kr_arc.rs create mode 100644 msg_tool_build/src/lib.rs create mode 100644 msg_tool_build/src/simple_pack.rs create mode 100644 src/scripts/kirikiri/archive/xp3/crypt/cx_cb/9nine_ep1.bin create mode 100644 src/scripts/kirikiri/archive/xp3/crypt/cx_cb/9nine_ep1_sekai.bin create mode 100644 src/utils/simple_pack.rs diff --git a/Cargo.lock b/Cargo.lock index f9312df..ce1f306 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1437,6 +1437,7 @@ dependencies = [ "memchr", "mozjpeg", "msg-tool-jpegxl-sys", + "msg_tool_build", "msg_tool_macro", "num_cpus", "overf", @@ -1461,6 +1462,14 @@ dependencies = [ "zstd", ] +[[package]] +name = "msg_tool_build" +version = "0.3.1" +dependencies = [ + "json", + "zstd", +] + [[package]] name = "msg_tool_macro" version = "0.3.0" diff --git a/Cargo.toml b/Cargo.toml index a418a1c..a8a1b1d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -92,7 +92,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", "parse-size", "sha2", "utils-serde-base64bytes", "zopfli", "zstd"] +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-img = ["kirikiri", "image", "libtlg-rs"] musica = [] musica-arc = ["musica", "crc32fast", "flate2", "include-flate", "utils-blowfish", "utils-rc4", "utils-serde-base64bytes", "utils-xored-stream"] @@ -127,6 +127,7 @@ utils-pcm = [] utils-psd = ["image", "flate2", "utils-bit-stream"] utils-rc4 = [] utils-serde-base64bytes = ["base64"] +utils-simple-pack = ["zstd"] utils-str = [] utils-xored-stream = [] @@ -134,4 +135,5 @@ utils-xored-stream = [] windows-sys = { version = "0.61", features = ["Win32_Globalization", "Win32_System_Diagnostics_Debug"] } [build-dependencies] +msg_tool_build = { path = "./msg_tool_build", optional = true } parse-size = "1.1" diff --git a/build.rs b/build.rs index ff77807..4444e0a 100644 --- a/build.rs +++ b/build.rs @@ -10,4 +10,18 @@ fn main() { println!("cargo:rustc-link-arg=/STACK:{}", stack_size); #[cfg(target_env = "gnu")] println!("cargo:rustc-link-arg=-Wl,-z,stack-size={}", stack_size); + #[cfg(feature = "kirikiri-arc")] + { + let source_dir = std::path::PathBuf::from(std::env::var("CARGO_MANIFEST_DIR").unwrap()); + let crypt_json_path = source_dir.join("src/scripts/kirikiri/archive/xp3/crypt.json"); + let outdir = std::path::PathBuf::from(std::env::var("OUT_DIR").unwrap()); + let level = std::env::var("MSG_TOOL_KIRIKIRI_ARC_GEN_LEVEL").unwrap_or("22".to_string()); + println!("cargo:rerun-if-env-changed=OUT_DIR"); + println!("cargo:rerun-if-changed={}", crypt_json_path.display()); + let level = level + .parse::() + .expect("MSG_TOOL_KIRIKIRI_ARC_GEN_LEVEL must be a valid integer"); + println!("cargo:rerun-if-env-changed=MSG_TOOL_KIRIKIRI_ARC_GEN_LEVEL"); + msg_tool_build::kr_arc::gen_cx_cb(&crypt_json_path, &outdir, level).unwrap(); + } } diff --git a/msg_tool_build/.gitignore b/msg_tool_build/.gitignore new file mode 100644 index 0000000..1e7caa9 --- /dev/null +++ b/msg_tool_build/.gitignore @@ -0,0 +1,2 @@ +Cargo.lock +target/ diff --git a/msg_tool_build/Cargo.toml b/msg_tool_build/Cargo.toml new file mode 100644 index 0000000..0eea8d9 --- /dev/null +++ b/msg_tool_build/Cargo.toml @@ -0,0 +1,18 @@ +[package] +name = "msg_tool_build" +version = "0.3.1" +edition = "2024" +repository = "https://github.com/lifegpc/msg-tool" +description = "Build time library for the msg-tool project." +license = "GPL-3.0-or-later" + +[dependencies] +json = { version = "0.12", optional = true } +zstd = { version = "0.13", optional = true } + +[features] +kirikiri-arc = ["json", "simple-pack"] +simple-pack = ["zstd"] + +[package.metadata.docs.rs] +all-features = true diff --git a/msg_tool_build/src/kr_arc.rs b/msg_tool_build/src/kr_arc.rs new file mode 100644 index 0000000..f3ac7f9 --- /dev/null +++ b/msg_tool_build/src/kr_arc.rs @@ -0,0 +1,38 @@ +use crate::simple_pack::SimplePack; +use std::path::Path; + +/// Pack all binary files in cx_cb into a single archive. +pub fn gen_cx_cb + ?Sized, D: AsRef + ?Sized>( + json_path: &P, + outdir: &D, + level: i32, +) -> std::io::Result<()> { + let p = json_path.as_ref(); + let pb = p + .parent() + .unwrap_or_else(|| Path::new("")) + .join("crypt") + .join("cx_cb"); + let json_data = std::fs::read_to_string(p)?; + let json = json::parse(&json_data) + .map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e))?; + let mut pack = SimplePack::new(&outdir.as_ref().join("cx_cb.pck"))?; + for (_, obj) in json.entries() { + if let Some(name) = obj["ControlBlockName"].as_str() { + let file_path = pb.join(name); + if !file_path.exists() { + return Err(std::io::Error::new( + std::io::ErrorKind::NotFound, + format!("File not found: {}", file_path.display()), + )); + } + let file = std::fs::File::open(file_path)?; + let file = std::io::BufReader::new(file); + pack.add_file(name, file)?; + } + } + if level >= 0 && level <= 22 { + pack.compress(level)?; + } + Ok(()) +} diff --git a/msg_tool_build/src/lib.rs b/msg_tool_build/src/lib.rs new file mode 100644 index 0000000..e30515c --- /dev/null +++ b/msg_tool_build/src/lib.rs @@ -0,0 +1,4 @@ +#[cfg(feature = "kirikiri-arc")] +pub mod kr_arc; +#[cfg(feature = "simple-pack")] +mod simple_pack; diff --git a/msg_tool_build/src/simple_pack.rs b/msg_tool_build/src/simple_pack.rs new file mode 100644 index 0000000..54988b3 --- /dev/null +++ b/msg_tool_build/src/simple_pack.rs @@ -0,0 +1,66 @@ +//! A simple implementation of a pack file +use std::fs::File; +use std::io::{BufWriter, Read, Result, Seek, Write}; +use std::path::{Path, PathBuf}; + +pub struct SimplePack { + file: File, + path: PathBuf, + tmp_path: PathBuf, +} + +impl SimplePack { + pub fn new + ?Sized>(path: &P) -> Result { + let mut file = File::create(path.as_ref())?; + file.write_all(b"SPCK")?; + file.write_all(&[0])?; // No compression + Ok(Self { + file, + path: path.as_ref().to_path_buf(), + tmp_path: path.as_ref().with_added_extension(".tmp"), + }) + } + pub fn add_file(&mut self, name: &str, mut data: R) -> Result<()> { + let mut writer = BufWriter::new(&mut self.file); + writer.write_all(name.as_bytes())?; + writer.write_all(&[0])?; // Null terminator for the name + let file_size_loc = writer.stream_position()?; + writer.write_all(&0u64.to_le_bytes())?; // Placeholder for file size + let size = std::io::copy(&mut data, &mut writer)?; + let current_pos = writer.stream_position()?; + writer.seek(std::io::SeekFrom::Start(file_size_loc))?; + writer.write_all(&size.to_le_bytes())?; // Write the actual file size + writer.seek(std::io::SeekFrom::Start(current_pos))?; // Move back to the end of the file + writer.flush()?; + Ok(()) + } + pub fn compress(mut self, level: i32) -> Result<()> { + self.file.flush()?; + std::mem::drop(self.file); // Close the file before renaming + // Move the file to a temporary location + std::fs::rename(&self.path, &self.tmp_path)?; + { + let tmp_file = File::open(&self.tmp_path)?; + let mut reader = std::io::BufReader::new(tmp_file); + reader.seek_relative(5)?; // Skip header + let original_size = reader.get_ref().metadata()?.len() - 5; + let outfile = File::create(&self.path)?; + let mut writer = std::io::BufWriter::new(outfile); + writer.write_all(b"SPCK")?; + writer.write_all(&[1])?; // Compression flag + let compress_size_loc = writer.stream_position()?; + writer.write_all(&0u64.to_le_bytes())?; // Placeholder for compressed size + writer.write_all(&original_size.to_le_bytes())?; + let cur_loc = writer.stream_position()?; + let mut encoder = zstd::stream::write::Encoder::new(&mut writer, level)?; + std::io::copy(&mut reader, &mut encoder)?; + encoder.finish()?; + writer.flush()?; + let compressed_size = writer.stream_position()? - cur_loc; + writer.seek(std::io::SeekFrom::Start(compress_size_loc))?; + writer.write_all(&compressed_size.to_le_bytes())?; // Write the actual compressed size + } + std::fs::remove_file(&self.tmp_path)?; // Clean up the temporary file + Ok(()) + } +} diff --git a/src/scripts/kirikiri/archive/xp3/archive.rs b/src/scripts/kirikiri/archive/xp3/archive.rs index 873c998..914aaef 100644 --- a/src/scripts/kirikiri/archive/xp3/archive.rs +++ b/src/scripts/kirikiri/archive/xp3/archive.rs @@ -1,3 +1,4 @@ +use super::consts::*; use super::crypt::Crypt; use crate::scripts::base::ReadSeek; use std::sync::{Arc, Mutex}; @@ -50,6 +51,12 @@ pub struct ExtraProp { pub data: Vec, } +impl ExtraProp { + pub fn is_filename_hash(&self) -> bool { + &self.tag == CHUNK_HNFN + } +} + /// Represents the entire XP3 archive #[derive(Debug)] #[allow(dead_code)] diff --git a/src/scripts/kirikiri/archive/xp3/consts.rs b/src/scripts/kirikiri/archive/xp3/consts.rs index bd8c333..85c9223 100644 --- a/src/scripts/kirikiri/archive/xp3/consts.rs +++ b/src/scripts/kirikiri/archive/xp3/consts.rs @@ -6,6 +6,7 @@ pub const CHUNK_FILE: &[u8; 4] = b"File"; 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"; // Index entry flags pub const TVP_XP3_INDEX_ENCODE_METHOD_MASK: u8 = 0x07; diff --git a/src/scripts/kirikiri/archive/xp3/crypt.json b/src/scripts/kirikiri/archive/xp3/crypt.json index 07a69ae..59412cf 100644 --- a/src/scripts/kirikiri/archive/xp3/crypt.json +++ b/src/scripts/kirikiri/archive/xp3/crypt.json @@ -3,6 +3,25 @@ "$type": "HashCrypt", "Title": "25c -その箱は少女の悲鳴を漏らさない-" }, + "9 -Nine- Kokonotsu Kokonoka Kokonoiro": { + "$type": "CxEncryption", + "Mask": 518, + "Offset": 77, + "PrologOrder": "AgEA", + "OddBranchOrder": "AQIDBAAF", + "EvenBranchOrder": "BQMEBgAHAgE=", + "ControlBlockName": "9nine_ep1.bin", + "Title": "9-nine-ここのつここのかここのいろ | 9-nine-九次九日九重色 | 9-nine-九次九日九色" + }, + "9 -Nine-:Episode 1": { + "$type": "CxEncryption", + "Mask": 299, + "Offset": 1963, + "PrologOrder": "AAIB", + "OddBranchOrder": "BAUAAgED", + "EvenBranchOrder": "BQcBBgMCAAQ=", + "ControlBlockName": "9nine_ep1_sekai.bin" + }, "Aibo Nyuujoku": { "$type": "FlyingShineCrypt", "Title": "愛母乳辱~妄執の巨乳責めザンマイ~" diff --git a/src/scripts/kirikiri/archive/xp3/crypt/cx.rs b/src/scripts/kirikiri/archive/xp3/crypt/cx.rs index bb586a7..359ca4e 100644 --- a/src/scripts/kirikiri/archive/xp3/crypt/cx.rs +++ b/src/scripts/kirikiri/archive/xp3/crypt/cx.rs @@ -29,8 +29,20 @@ impl CxEncryption { } let control_block = if let Some(tpm_path) = &schema.tpm_file_name { Self::read_tpm(tpm_path, filename)? + } else if let Some(control_block_name) = &schema.control_block_name { + CX_CB_TABLE + .get(control_block_name) + .ok_or_else(|| { + anyhow::anyhow!( + "Control block not found in cx_cb.pck: {}", + control_block_name + ) + })? + .clone() } else { - return Err(anyhow::anyhow!("TPM file name is required in schema")); + return Err(anyhow::anyhow!( + "TPM file name or control block is required in schema" + )); }; let control_block = Arc::new(control_block); let programs = Vec::with_capacity(0x80); diff --git a/src/scripts/kirikiri/archive/xp3/crypt/cx_cb/9nine_ep1.bin b/src/scripts/kirikiri/archive/xp3/crypt/cx_cb/9nine_ep1.bin new file mode 100644 index 0000000..673619a --- /dev/null +++ b/src/scripts/kirikiri/archive/xp3/crypt/cx_cb/9nine_ep1.bin @@ -0,0 +1,18 @@ +w \ɨi7EDx ZVwy0Hk#",0Kqe52 + f@l셷GF#liSJC[cnz5eh &3U&+$:56Lxcmrg]y[uè2teIj׍$ :_cF f6i+b:9}*\v< +;N!7Oד*9[",m 6H$z2E7<)8?j.kgTf7VHWCm1b 7\ .hc+3*+,ڇP\lP&I#ps|*wnd|Y ԰G08=mNc!JWdbiTFŪx֜Ҹ%c9W}Q>sD36,q&uj9$ iK]J\j>$K17#yi־Es{J}dPc*2 tǢb7mҤrW'ZYqUIvoVb˕&@Y{N+xA1|(I>VC;w]q]^bI\8)P?8F߽2V#\5q&zSW IC\Ayv2 w{d^?SʓFlvP}owKDڣA,Ji,,0-.H-L@Τ0S) Wk ;]Ny4kT#oKG/ѩa|uUص5\>9 @p+):5 +S-)Wc[( !̰^qƒ"C:UԱ$w +"de(]pٚWZZg7Ky9[ w@R6pj*xNQC OC&6ӋAW+E<hr6YT 趩v˜Z}kd6n%'-}+˒TK;4ݱ)|TzJmGdog2@P\!h %eqO} y m +:M<"KnF!E[rA'KSyY vY"3I{\R🎦0c="e&rᔆݚEHۄQUdm.V5z] ob3F,TW7[#Όb37U^t;]gxb)'>K6ZφK[H ?ńY2{ m3#v&xH,kI*bUrK +.bbUkoid7C= CQ 'fmM7uΩˆ +G:#^}tKǻ5`IWJfxm'7Mg"2% l]!հsiM*[R,2 +f$*ShHc$ +z+-.^6EFukCDE`° yB +#,{d][8ǙF~u;́ٓ6sz\PZ?Ԇ07U4Q|bw,NZ8\=)zzUl}s^p菦x[ltH(~K7z'Dg.u7&ٱ;]=)W7_5\V6&LF'+!#c0S|X`M/Shlu_?XcI<Ԛr'X2Yg|=`l#$g&U2kYG,J]!m<6h4([/HO& JڃV%2WHVm+-1XK>Z:;'oRZvҟDEA$$Qu^>^tQpp};l%O5i48(!fLŁ+MglCBAܽ·Iˆ؟'J[(#AE\w=W1?yp`]4TCIAUڧGIoiQN4eZda5SB!mM W9B!O }|ec'썻gְ%swӋfYZ93c{ +^;bY!(n*׫&.TI1Z+8*E+%m6h:v75כIRm7:@_Cדp/`mf cSW .@Jc \{^ф?m 뒄0sQQkbN2^fqiH@ȡͳ%8\8Eex=Ue#QX`Wɗ;qѶ/{4x4St{;FZYo_1=zuں&`}mWf'p񂿵,p94>x@* ge+0ӆT),DgH_ +ݩ9a;p3r2sLe4ïp]tF4 C=ZD.L/J" ַR591z/lmW6=kcU_D#F}v}swc\ŶL[I  -iԹ{ʔ= xgy .Ki,EEz|F:7H s2|6\u6f.bR`W/]cE6v7=vRRˇ<㯢imp%F-4*W cfYY.G6^~*q7iZA Z}͵MC䜋jm?>pnFL=2z |z)FotYi&¢Da͙>?kwUhJrޙmtYOJ>?`_z ,/Bm= Et"sMWhr2:z1o0 +Wz$EoBzIF Y}b<ХʩB1f5L;k"VcM>ܫl)F[Ӝ Ӵ;8( ;jT|Hrw(-%2ȉIeRPþK8ƄĨ(T g/h /qe}F!ǥ"qTED 'D( F#c;=q;'L醏MD퓯 Q{)_}T:<,2IHr"orvEөI fL 5DJ3m>5".QzlO{$Ny&0;vSf0<A݆$ b{U{M-@H^E\0sƚn2֧ZgvKi EO=I05v;d ^U=пuO[}gSnJbxYcgo; I>&0GbsGE;(Ao6&\moȣMt]7lXImn#2ؤG7^WO6nPfkԹyzr1aLN^NqԔ <E.jfu]VЭpt23J6.$Ɩ1U\>*ւXw b8Xѣn + 0FEbf>,ܵ_HZNmҌHoإ/BH̙1r*v{9ICJ`S#.tEx~ˉ9^3Dy`(CqѴQI3O +aŠ tC+jAЧ &1s83f:  M$%/pJ`%%v߉/;~SoA0_Y[iD@?zK=3xآA.%0TQ6'*r|}0Ivm䥀*<GTcrTc|Uy,pҎGa_ܟ }Emg"=;]n#& +?繻>'a:&Pִ}òG#JĄ +±% O1݉!s0XV%LU7`3n'>UL)Ḅ5y5TrV JM8zГ88LZw-/@ *2 Έqʧ|pv?J+Vc̅gaySe)Ҫ[LGJ\vF@bx R.rqvz;:/~ưu+83J+QeN M$~{Q LG;4m yߦoo,$tÝ(w 0o:hWD 9-Hpd+F&Ͱ.:X'A]E+1ɝf^\$!5=;6.Z\VOA;߆B J7;xTjs-i>OXXh.rPUb = Hzq5Mgk!J] ol~CLIJ͌"Pd՝Qr[4l K +S>5ZN͛ kLwz'OG7R )uTb'@p[*k˹2v#[CR#iXZ#S wi`ObQ'6-x$ȄM$ҙT:*hΒ(mwgF*`4'AhwN ])"c.^pg&Wk(i~Zx~:b/7|SLԀiݻqx,d=onohUŒZ0e뭇7B&"Q-}?>9`XGeQ"mI}=q)\g⧅6|[&ک4B<|-FRPdC<-q~f)yRэ~ ̓PNv'?9wP,BLS@ZhnfV٦%}-~&FF/\?0vp0;,[MH0ylAnCY&j=lִYyPB|x.A%W0]7_O)}|bjtXL~C~Ёo̡ (gTIJfPhb{ʡ%JJG&PcB_]Ho|it<_)Oɇr{BB(ŠRK86JrWYF21cO\|_WaO.Gg6uy Fɖ>y:(7PO>n18D6!&3I:*/`4 +#X0sC#c۫eN)B}QAQoL'bk=E S yyC~̯uz'PQLPOiBKD`PVC)-v3՛^e[QLjVW5с4M6Zuz+e3{V(X"y{NXEiN ›W'h1Gl>~1᮸9QR:  I$SD @SXD۫3eIt~), +xkci k1mj<PE\೵vo3~QOWGcꗾmSPw{,Ɣ) BdԒƋ.gJoե`]?]O/7}Lz$R:o?vW@!+JIe[J92*xs N^ \ No newline at end of file diff --git a/src/scripts/kirikiri/archive/xp3/crypt/mod.rs b/src/scripts/kirikiri/archive/xp3/crypt/mod.rs index ea050dd..4625adf 100644 --- a/src/scripts/kirikiri/archive/xp3/crypt/mod.rs +++ b/src/scripts/kirikiri/archive/xp3/crypt/mod.rs @@ -6,16 +6,39 @@ use crate::scripts::base::*; use crate::types::*; use crate::utils::encoding::*; use crate::utils::serde_base64bytes::*; +use crate::utils::simple_pack::*; use anyhow::Result; use serde::Deserialize; use std::collections::{BTreeMap, HashMap}; use std::io::{Read, Seek, SeekFrom}; use std::sync::Arc; +pub fn default_init_crypt(archive: &mut Xp3Archive) -> Result<()> { + if archive.extras.iter().any(|extra| extra.is_filename_hash()) { + let mut filename_map = HashMap::new(); + for extra in &archive.extras { + if extra.is_filename_hash() { + let mut reader = MemReaderRef::new(&extra.data); + let hash = reader.read_u32()?; + let name_length = reader.read_u16()?; + let name = reader.read_exact_vec(name_length as usize * 2)?; + let name = decode_to_string(Encoding::Utf16LE, &name, true)?; + filename_map.insert(hash, name); + } + } + for entry in &mut archive.entries { + if let Some(name) = filename_map.get(&entry.file_hash) { + entry.name = name.clone(); + } + } + } + Ok(()) +} + pub trait Crypt: std::fmt::Debug { /// Initializes the cryptographic context for the archive. - fn init(&self, _archive: &mut Xp3Archive) -> Result<()> { - Ok(()) + fn init(&self, archive: &mut Xp3Archive) -> Result<()> { + default_init_crypt(archive) } /// Read a entry name from archive index @@ -111,6 +134,7 @@ impl Schema { } include_flate::flate!(static CRYPT_DATA: str from "src/scripts/kirikiri/archive/xp3/crypt.json" with zstd); +const CX_CB_DATA: &[u8] = include_bytes!(concat!(env!("OUT_DIR"), "/cx_cb.pck")); lazy_static::lazy_static! { static ref CRYPT_SCHEMA: BTreeMap = { @@ -134,6 +158,20 @@ lazy_static::lazy_static! { } table }; + static ref CX_CB_TABLE: HashMap> = { + let reader = MemReaderRef::new(CX_CB_DATA); + let mut pack = read_simple_pack(reader).expect("Failed to read cx_cb.pck"); + let mut table = HashMap::new(); + while let Some(mut entry) = pack.next().expect("Failed to read entry in cx_cb.pck") { + let mut list = Vec::with_capacity(0x400); + let errmsg = format!("Failed to read u32 in cx_cb.pck entry {}", entry.name); + for _ in 0..0x400 { + list.push(entry.read_u32().expect(&errmsg)); + } + table.insert(entry.name.clone(), list); + } + table + }; } /// Get the supported game titles for encrypted xp3 archives. @@ -510,3 +548,10 @@ fn test_deserialize_crypt() { println!("Title: {}, Schema: {:?}", key, schema); } } + +#[test] +fn test_cx_cb_table() { + for (key, list) in CX_CB_TABLE.iter() { + println!("Key: {}, List length: {}", key, list.len()); + } +} diff --git a/src/utils/mod.rs b/src/utils/mod.rs index 5a33f95..6d58893 100644 --- a/src/utils/mod.rs +++ b/src/utils/mod.rs @@ -33,6 +33,8 @@ pub mod psd; pub mod rc4; #[cfg(feature = "utils-serde-base64bytes")] pub mod serde_base64bytes; +#[cfg(feature = "utils-simple-pack")] +pub mod simple_pack; #[cfg(feature = "utils-str")] pub mod str; pub mod struct_pack; diff --git a/src/utils/simple_pack.rs b/src/utils/simple_pack.rs new file mode 100644 index 0000000..a6ab2d4 --- /dev/null +++ b/src/utils/simple_pack.rs @@ -0,0 +1,100 @@ +use crate::ext::io::*; +use crate::types::*; +use crate::utils::encoding::*; +use anyhow::Result; +use std::io::{Read, Seek}; + +pub struct SimplePack { + inner: T, + total: u64, + current: u64, +} + +impl SimplePack { + pub fn next<'a>(&'a mut self) -> Result>> { + if self.current >= self.total { + return Ok(None); + } + let name = self.read_cstring()?; + let name = decode_to_string(Encoding::Utf8, name.as_bytes(), true)?; + let entry_size = self.read_u64()?; + Ok(Some(SimplePackEntry { + pack: self, + total: entry_size, + current: 0, + name, + })) + } +} + +impl Read for SimplePack { + fn read(&mut self, buf: &mut [u8]) -> std::io::Result { + let remaining = self.total - self.current; + if remaining == 0 { + return Ok(0); + } + let to_read = std::cmp::min(remaining, buf.len() as u64) as usize; + let bytes_read = self.inner.read(&mut buf[..to_read])?; + self.current += bytes_read as u64; + Ok(bytes_read) + } +} + +pub struct SimplePackEntry<'a, T: Read> { + pub pack: &'a mut SimplePack, + total: u64, + current: u64, + pub name: String, +} + +impl<'a, T: Read> Drop for SimplePackEntry<'a, T> { + fn drop(&mut self) { + let to_skip = self.total - self.current; + if to_skip > 0 { + if let Err(e) = self.pack.skip(to_skip) { + eprintln!("Failed to skip remaining bytes in SimplePackEntry: {}", e); + crate::COUNTER.inc_error(); + } + } + } +} + +impl<'a, T: Read> Read for SimplePackEntry<'a, T> { + fn read(&mut self, buf: &mut [u8]) -> std::io::Result { + let remaining = self.total - self.current; + if remaining == 0 { + return Ok(0); + } + let to_read = std::cmp::min(remaining, buf.len() as u64) as usize; + let bytes_read = self.pack.read(&mut buf[..to_read])?; + self.current += bytes_read as u64; + Ok(bytes_read) + } +} + +pub fn read_simple_pack<'a, T: Read + Seek + 'a>( + mut reader: T, +) -> Result>> { + reader.read_and_equal(b"SPCK")?; + let flags = reader.read_u8()?; + // not compressed + if flags == 0 { + let pos = reader.stream_position()?; + let total = reader.stream_length()? - pos; + Ok(SimplePack { + inner: Box::new(reader), + total, + current: 0, + }) + } else { + let compressed_size = reader.read_u64()?; + let uncompressed_size = reader.read_u64()?; + let compressed = reader.take(compressed_size); + let decompressed = zstd::stream::read::Decoder::new(compressed)?; + Ok(SimplePack { + inner: Box::new(decompressed), + total: uncompressed_size, + current: 0, + }) + } +}