From 487d403d602c9363b12f2db8afb5ec0b03fbf431 Mon Sep 17 00:00:00 2001 From: lifegpc Date: Sat, 31 Jan 2026 00:05:11 +0800 Subject: [PATCH] Add pack support for Qlie Pack Archive (.pack) v3.1 --- Cargo.toml | 2 +- README.md | 2 +- src/args.rs | 4 + src/ext/io.rs | 83 +++ src/main.rs | 40 +- src/scripts/base.rs | 6 + src/scripts/qlie/archive/pack/encryption.rs | 196 ++++++- src/scripts/qlie/archive/pack/mod.rs | 22 +- src/scripts/qlie/archive/pack/types.rs | 7 +- src/scripts/qlie/archive/pack/v31.rs | 586 ++++++++++++++++++++ src/types.rs | 3 + 11 files changed, 937 insertions(+), 14 deletions(-) create mode 100644 src/scripts/qlie/archive/pack/v31.rs diff --git a/Cargo.toml b/Cargo.toml index a78d8cc..36eb0af 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -94,7 +94,7 @@ kirikiri-img = ["kirikiri", "image", "libtlg-rs"] musica = [] musica-arc = ["musica", "crc32fast", "flate2", "include-flate", "utils-blowfish", "utils-rc4", "utils-serde-base64bytes", "utils-xored-stream"] qlie = [] -qlie-arc = ["qlie", "utils-mmx"] +qlie-arc = ["qlie", "utils-mmx", "rand"] qlie-img = ["qlie", "image"] silky = [] softpal = ["int-enum"] diff --git a/README.md b/README.md index b44dfd3..74d9011 100644 --- a/README.md +++ b/README.md @@ -220,7 +220,7 @@ msg-tool create -t | Archive Type | Feature Name | Name | Unpack | Pack | Remarks | |---|---|---|---|---|---| -| `qlie-pack` | `qlie-arc` | Qlie Pack Archive (.pack) | ✔️ | ❌ | Currently only v3.1 are supported | +| `qlie-pack` | `qlie-arc` | Qlie Pack Archive (.pack) | ✔️ | ✔️ | Currently only v3.1 are supported. `--backslash` are needed to correctly handle file paths when packing. | | Image Type | Feature Name | Name | Export | Import | Export Multiple | Import Multiple | Create | Remarks | |---|---|---|---|---|---|---|---|---| diff --git a/src/args.rs b/src/args.rs index 30994ea..e2cf186 100644 --- a/src/args.rs +++ b/src/args.rs @@ -639,6 +639,10 @@ pub struct Arg { #[arg(long, global = true, action = ArgAction::SetTrue)] /// Disable process ABMP10 images in ABMP10 images. pub qlie_abmp10_no_process_abmp10: bool, + #[cfg(feature = "qlie-arc")] + #[arg(long, global = true)] + /// Path to qlie pack archive key file (pack_keyfile_kfueheish15538fa9or.key) + pub qlie_pack_keyfile: Option, #[command(subcommand)] /// Command pub command: Command, diff --git a/src/ext/io.rs b/src/ext/io.rs index ba0601e..c2b60e7 100644 --- a/src/ext/io.rs +++ b/src/ext/io.rs @@ -2583,3 +2583,86 @@ impl Read for AlignedReader { Ok(total_read) } } + +/// A writer that forwards reads to an inner writer, but ensures that all writes are aligned to a specified alignment boundary. +#[derive(Debug)] +pub struct AlignedWriter { + inner: T, + buffer: [u8; A], + buffer_size: usize, +} + +impl AlignedWriter { + /// Creates a new `AlignedWriter` with the given inner writer. + pub fn new(inner: T) -> Self { + AlignedWriter { + inner, + buffer: [0; A], + buffer_size: 0, + } + } +} + +impl Write for AlignedWriter { + fn write(&mut self, buf: &[u8]) -> Result { + let mut total_writted = 0; + let mut needed = buf.len(); + if self.buffer_size > 0 { + let to_copy = (A - self.buffer_size).min(needed); + self.buffer[self.buffer_size..self.buffer_size + to_copy] + .copy_from_slice(&buf[..to_copy]); + self.buffer_size += to_copy; + total_writted += to_copy; + needed -= to_copy; + if self.buffer_size == A { + let writed = self.inner.write(&self.buffer)?; + if writed % A != 0 { + return Err(std::io::Error::new( + std::io::ErrorKind::WriteZero, + "Failed to write all aligned bytes", + )); + } + self.buffer_size -= writed; + } else { + return Ok(total_writted); + } + } + let mut write_len = needed / A * A; + while write_len > 0 { + let writed = self + .inner + .write(&buf[total_writted..total_writted + write_len])?; + if writed % A != 0 { + return Err(std::io::Error::new( + std::io::ErrorKind::WriteZero, + "Failed to write all aligned bytes", + )); + } + total_writted += writed; + needed -= writed; + write_len = needed / A * A; + } + if needed > 0 { + self.buffer[..needed].copy_from_slice(&buf[total_writted..total_writted + needed]); + self.buffer_size = needed; + total_writted += needed; + } + Ok(total_writted) + } + + fn flush(&mut self) -> Result<()> { + self.inner.flush() + } +} + +impl Drop for AlignedWriter { + fn drop(&mut self) { + if self.buffer_size > 0 { + if let Err(err) = self.inner.write_all(&self.buffer[..self.buffer_size]) { + eprintln!("Failed to flush AlignedWriter buffer: {}", err); + crate::COUNTER.inc_error(); + } + self.buffer_size = 0; + } + } +} diff --git a/src/main.rs b/src/main.rs index 4cb8654..e3c5d23 100644 --- a/src/main.rs +++ b/src/main.rs @@ -2469,7 +2469,7 @@ pub fn pack_archive( return Err(anyhow::anyhow!("No script type specified")); } }; - let (files, isdir) = utils::files::collect_files(input, arg.recursive, true) + let (mut files, isdir) = utils::files::collect_files(input, arg.recursive, true) .map_err(|e| anyhow::anyhow!("Error collecting files: {}", e))?; if !isdir { return Err(anyhow::anyhow!("Input must be a directory for packing")); @@ -2491,7 +2491,7 @@ pub fn pack_archive( }) }) .collect(); - let reff = re_files.iter().map(|s| s.as_str()).collect::>(); + let mut reff = re_files.iter().map(|s| s.as_str()).collect::>(); let builder = scripts::BUILDER .iter() .find(|b| b.script_type() == typ) @@ -2514,6 +2514,22 @@ pub fn pack_archive( get_archived_encoding(arg, builder, get_encoding(arg, builder)), &config, )?; + if let Some(pre) = archive.prelist()? { + let mut index = 0; + for name in pre { + let name = name?; + if let Some(pos) = reff.iter().position(|&n| n == name) { + let re = reff.remove(pos); + let ff = files.remove(pos); + reff.insert(index, re); + files.insert(index, ff); + index += 1; + } else { + eprintln!("Warning: Prelist file {} not found in input files", name); + COUNTER.inc_warning(); + } + } + } for (file, name) in files.iter().zip(reff) { let mut f = match std::fs::File::open(file) { Ok(f) => f, @@ -2615,7 +2631,7 @@ pub fn pack_archive_v2( } } } - let reff = re_files.iter().map(|s| s.as_str()).collect::>(); + let mut reff = re_files.iter().map(|s| s.as_str()).collect::>(); let builder = scripts::BUILDER .iter() .find(|b| b.script_type() == typ) @@ -2638,6 +2654,22 @@ pub fn pack_archive_v2( get_archived_encoding(arg, builder, get_encoding(arg, builder)), &config, )?; + if let Some(pre) = archive.prelist()? { + let mut index = 0; + for name in pre { + let name = name?; + if let Some(pos) = reff.iter().position(|&n| n == name) { + let re = reff.remove(pos); + let ff = files.remove(pos); + reff.insert(index, re); + files.insert(index, ff); + index += 1; + } else { + eprintln!("Warning: Prelist file {} not found in input files", name); + COUNTER.inc_warning(); + } + } + } if let Some(dep_file) = dep_file { let df = std::fs::File::create(dep_file) .map_err(|e| anyhow::anyhow!("Failed to create dep file {}: {}", dep_file, e))?; @@ -3323,6 +3355,8 @@ fn main() { entis_gls_csx_no_part_label: arg.entis_gls_csx_no_part_label, #[cfg(feature = "qlie-img")] qlie_abmp10_process_abmp10: !arg.qlie_abmp10_no_process_abmp10, + #[cfg(feature = "qlie-arc")] + qlie_pack_keyfile: arg.qlie_pack_keyfile.clone(), }); match &arg.command { args::Command::Export { input, output } => { diff --git a/src/scripts/base.rs b/src/scripts/base.rs index 87e6262..2b18b34 100644 --- a/src/scripts/base.rs +++ b/src/scripts/base.rs @@ -589,6 +589,12 @@ pub trait Script: std::fmt::Debug + std::any::Any { /// A trait for creating archives. pub trait Archive { + /// Returns an iterator of a list of filenames must writed before other files. + /// + /// Should return None if no such requirement. + fn prelist<'a>(&'a self) -> Result> + 'a>>> { + Ok(None) + } /// Creates a new file in the archive. /// /// size is optional, if provided, size must be exactly the size of the file to be created. diff --git a/src/scripts/qlie/archive/pack/encryption.rs b/src/scripts/qlie/archive/pack/encryption.rs index 153bf7d..3b52b68 100644 --- a/src/scripts/qlie/archive/pack/encryption.rs +++ b/src/scripts/qlie/archive/pack/encryption.rs @@ -5,7 +5,7 @@ use crate::types::*; use crate::utils::encoding::*; use crate::utils::mmx::*; use anyhow::Result; -use std::io::Read; +use std::io::{Read, Write}; pub trait Hasher { fn update(&mut self, data: &[u8]) -> Result<()>; @@ -67,6 +67,29 @@ pub fn decrypt(data: &mut [u8], key: u32) -> Result<()> { Ok(()) } +pub fn encrypt(data: &mut [u8], key: u32) -> Result<()> { + let length = data.len(); + if length < 8 { + // Nothing to encrypt + return Ok(()); + } + let mut data = MemWriterRef::new(data); + const C1: u64 = 0xA73C5F9D; + const C2: u64 = 0xCE24F523; + const C3: u64 = 0xFEC9753E; + let mut v5 = mmx_punpckldq2(C1); + const V7: u64 = mmx_punpckldq2(C2); + let mut v9 = mmx_punpckldq2(((length as u32).wrapping_add(key) as u64) ^ C3); + for _ in 0..length / 8 { + let mut d = data.peek_u64()?; + v5 = mmx_p_add_d(v5, V7) ^ v9; + v9 = d; + d ^= v5; + data.write_u64(d)?; + } + Ok(()) +} + pub fn get_common_key(data: &[u8]) -> Result> { let mut reader = MemReaderRef::new(data); let mut key = vec![0u8; 0x400]; @@ -112,6 +135,44 @@ impl Encryption31 { } Ok(mem.into_inner()) } + + pub fn compute_name_hash(&self, name: &[u16]) -> Result { + let mut v2 = 0u32; + let mut v3 = name.len() as u32; + let mut v4 = 1u32; + if v3 > 0 { + loop { + let n = (name[(v4 - 1) as usize] as u32) << (v4 & 7); + v2 = v2.wrapping_add(n) & 0x3FFFFFFF; + v4 += 1; + v3 -= 1; + if v3 == 0 { + break; + } + } + } + Ok(v2) + } + + pub fn encrypt_name(&self, name: &mut [u8], hash: i32) -> Result<()> { + if name.len() % 2 != 0 { + return Err(anyhow::anyhow!( + "Invalid name length for Unicode encryption" + )); + } + let char_len = name.len() / 2; + let cl = char_len as i32; + let temp = (cl.wrapping_mul(cl) ^ cl ^ 0x3e13 ^ (hash >> 16) ^ hash) & 0xFFFF; + let mut key = temp; + for i in 0..char_len { + key = temp + .wrapping_add(i as i32) + .wrapping_add(key.wrapping_mul(8)); + name[i * 2] ^= key as u8; + name[i * 2 + 1] ^= (key >> 8) as u8; + } + Ok(()) + } } impl Encryption for Encryption31 { @@ -298,6 +359,67 @@ impl<'a> Read for Encryption31DecryptV1<'a> { } } +#[derive(Debug)] +pub struct Encryption31EncryptV1 { + stream: T, + table: MemReader, + v4: u32, + v6: u64, +} + +impl Encryption31EncryptV1 { + pub fn new(stream: T, size: u32, name: String, key: u32) -> Result> { + let mut v1 = 0x85F532u32; + let mut v2 = 0x33F641u32; + for (i, n) in name.encode_utf16().enumerate() { + v1 = v1.wrapping_add((n as u32) << (i & 7)); + v2 ^= v1; + } + v2 = v2.wrapping_add( + key ^ ((7 * (size & 0xFFFFFF)) + .wrapping_add(size) + .wrapping_add(v1) + .wrapping_add(v1 ^ size ^ 0x8F32DC)), + ); + v2 = 9 * (v2 & 0xFFFFFF); + let table = MemReader::new(Encryption31::create_table(0x40, v2, true)?); + let v4 = 8 * (table.cpeek_u32_at(52)? & 0xF); + let v6 = table.cpeek_u64_at(24)?; + let inner = Self { + stream, + table, + v4, + v6, + }; + Ok(AlignedWriter::new(inner)) + } +} + +impl Write for Encryption31EncryptV1 { + fn write(&mut self, buf: &[u8]) -> std::io::Result { + let round = buf.len() / 8; + let mut reader = MemReaderRef::new(buf); + for _ in 0..round { + let d = reader.read_u64()?; + let temp = self.table.cpeek_u64_at(self.v4 as u64)?; + let v7 = mmx_p_add_d(self.v6 ^ temp, temp); + let v8 = d ^ v7; + self.stream.write_u64(v8)?; + self.v6 = mmx_p_add_w(mmx_p_sll_d(mmx_p_add_b(v7, d) ^ d, 1), d); + self.v4 = (self.v4 + 8) & 0x7F; + } + let remain = buf.len() % 8; + if remain > 0 { + self.stream.write_all(&buf[buf.len() - remain..])?; + } + Ok(buf.len()) + } + + fn flush(&mut self) -> std::io::Result<()> { + self.stream.flush() + } +} + #[derive(Debug)] struct Encryption31DecryptV2<'a> { stream: Box, @@ -363,6 +485,78 @@ impl<'a> Read for Encryption31DecryptV2<'a> { } } +#[derive(Debug)] +pub struct Encryption31EncryptV2 { + stream: T, + table: MemReader, + v4: u32, + v6: u64, + common_key: MemReader, +} + +impl Encryption31EncryptV2 { + pub fn new( + stream: T, + size: u32, + name: String, + key: u32, + common_key: Vec, + ) -> Result> { + let mut v1 = 0x86F7E2u32; + let mut v2 = 0x4437F1u32; + for (i, n) in name.encode_utf16().enumerate() { + v1 = v1.wrapping_add((n as u32) << (i & 7)); + v2 ^= v1; + } + v2 = v2.wrapping_add( + key ^ ((13 * (size & 0xFFFFFF)) + .wrapping_add(size) + .wrapping_add(v1) + .wrapping_add(v1 ^ size ^ 0x56E213)), + ); + v2 = 13 * (v2 & 0xFFFFFF); + let table = MemReader::new(Encryption31::create_table(0x40, v2, false)?); + let v4 = 8 * (table.cpeek_u32_at(32)? & 0xD); + let v6 = table.cpeek_u64_at(24)?; + let inner = Self { + stream, + table, + v4, + v6, + common_key: MemReader::new(common_key), + }; + Ok(AlignedWriter::new(inner)) + } +} + +impl Write for Encryption31EncryptV2 { + fn write(&mut self, buf: &[u8]) -> std::io::Result { + let round = buf.len() / 8; + let mut reader = MemReaderRef::new(buf); + for _ in 0..round { + let d = reader.read_u64()?; + let temp_index1 = ((self.v4 & 0xF) * 8) as u64; + let temp_index2 = ((self.v4 & 0x7F) * 8) as u64; + let temp = self.table.cpeek_u64_at(temp_index1)? + ^ self.common_key.cpeek_u64_at(temp_index2)?; + let v7 = mmx_p_add_d(self.v6 ^ temp, temp); + let v8 = d ^ v7; + self.stream.write_u64(v8)?; + self.v6 = mmx_p_add_w(mmx_p_sll_d(mmx_p_add_b(v7, d) ^ d, 1), d); + self.v4 = (self.v4 + 1) & 0x7F; + } + let remain = buf.len() % 8; + if remain > 0 { + self.stream.write_all(&buf[buf.len() - remain..])?; + } + Ok(buf.len()) + } + + fn flush(&mut self) -> std::io::Result<()> { + self.stream.flush() + } +} + #[derive(Debug)] pub struct Decompressor<'a> { stream: Box, diff --git a/src/scripts/qlie/archive/pack/mod.rs b/src/scripts/qlie/archive/pack/mod.rs index 628947c..1046cc5 100644 --- a/src/scripts/qlie/archive/pack/mod.rs +++ b/src/scripts/qlie/archive/pack/mod.rs @@ -2,6 +2,7 @@ mod encryption; mod twister; mod types; +mod v31; use crate::ext::io::*; use crate::scripts::base::*; @@ -110,6 +111,20 @@ impl ScriptBuilder for QliePackArchiveBuilder { fn is_archive(&self) -> bool { true } + + fn create_archive( + &self, + filename: &str, + files: &[&str], + _encoding: Encoding, + config: &ExtraConfig, + ) -> Result> { + let f = std::fs::File::create(filename)?; + let buf = std::io::BufWriter::new(f); + Ok(Box::new(v31::QliePackArchiveWriterV31::new( + buf, files, config, + )?)) + } } /// Check if the given file is Qlie Pack Archive format @@ -163,7 +178,7 @@ impl QliePackArchive { key = encryption.compute_hash(&qk.key[..0x100])? & 0xFFFFFFF; } encryption::decrypt(&mut qk.signature, key)?; - if &qk.signature != b"8hr48uky,8ugi8ewra4g8d5vbf5hb5s6" { + if &qk.signature != QLIE_KEY_SIGNATURE { eprintln!( "WARNING: Invalid Qlie Pack Archive key signature, decryption key may be incorrect" ); @@ -204,10 +219,7 @@ impl QliePackArchive { } let mut common_key = None; if major >= 3 && minor >= 1 { - if let Some(common_key_entry) = entries - .iter() - .find(|e| e.name == "pack_keyfile_kfueheish15538fa9or.key") - { + if let Some(common_key_entry) = entries.iter().find(|e| e.name == QLIE_KEY_FILE) { reader.seek(SeekFrom::Start(common_key_entry.offset))?; let stream = StreamRegion::with_size(&mut reader, common_key_entry.size as u64)?; let mut decrypted = encryption.decrypt_entry(Box::new(stream), common_key_entry)?; diff --git a/src/scripts/qlie/archive/pack/types.rs b/src/scripts/qlie/archive/pack/types.rs index 8b68a90..0b3abae 100644 --- a/src/scripts/qlie/archive/pack/types.rs +++ b/src/scripts/qlie/archive/pack/types.rs @@ -8,6 +8,8 @@ use std::io::{Read, Seek, Write}; pub const HASH_VER_1_2_SIGNATURE: &[u8; 16] = b"HashVer1.2\x00\x00\x00\x00\x00\x00"; pub const HASH_VER_1_3_SIGNATURE: &[u8; 16] = b"HashVer1.3\x00\x00\x00\x00\x00\x00"; pub const HASH_VER_1_4_SIGNATURE: &[u8; 16] = b"HashVer1.4\x00\x00\x00\x00\x00\x00"; +pub const QLIE_KEY_SIGNATURE: &[u8; 32] = b"8hr48uky,8ugi8ewra4g8d5vbf5hb5s6"; +pub const QLIE_KEY_FILE: &'static str = "pack_keyfile_kfueheish15538fa9or.key"; /// HashVer 1.2 #[derive(StructPack, StructUnpack, Debug, Clone)] @@ -37,8 +39,7 @@ pub struct QlieHash13 { #[derive(StructPack, StructUnpack, Debug, Clone)] pub struct QlieHash14 { pub signature: [u8; 16], - /// Always 0x100 - pub const_: u32, + pub table_size: u32, pub file_count: u32, pub index_size: u32, pub hash_data_size: u32, @@ -81,7 +82,7 @@ pub struct QlieKey { pub key: [u8; 0x400], } -#[derive(Debug, Clone)] +#[derive(Debug, Clone, Default)] pub struct QlieEntry { pub name: String, pub offset: u64, diff --git a/src/scripts/qlie/archive/pack/v31.rs b/src/scripts/qlie/archive/pack/v31.rs new file mode 100644 index 0000000..e68c6b3 --- /dev/null +++ b/src/scripts/qlie/archive/pack/v31.rs @@ -0,0 +1,586 @@ +use super::encryption::*; +use super::types::*; +use crate::ext::io::*; +use crate::scripts::base::*; +use crate::types::*; +use crate::utils::encoding::*; +use anyhow::Result; +use rand::Rng; +use std::io::{Seek, Write}; + +struct MListEntry { + back: *mut MListEntry, + next: *mut MListEntry, + data: T, +} + +struct MList { + head: *mut MListEntry, + depth: usize, +} + +impl MList { + pub fn new() -> Self { + Self { + head: std::ptr::null_mut(), + depth: 0, + } + } + + pub fn push(&mut self, data: T, way: bool) -> usize { + let entry = Box::new(MListEntry { + back: std::ptr::null_mut(), + next: std::ptr::null_mut(), + data, + }); + let entry_ptr = Box::into_raw(entry); + if self.head.is_null() { + self.head = entry_ptr; + unsafe { + (*self.head).back = self.head; + (*self.head).next = self.head; + } + } else { + if way { + unsafe { + (*(*self.head).back).next = entry_ptr; + (*entry_ptr).back = (*self.head).back; + (*entry_ptr).next = self.head; + (*self.head).back = entry_ptr; + } + } else { + unsafe { + (*(*self.head).back).next = entry_ptr; + (*entry_ptr).back = (*self.head).back; + (*entry_ptr).next = self.head; + (*self.head).back = entry_ptr; + self.head = entry_ptr; + } + } + } + self.depth += 1; + self.depth + } + + pub fn pop(&mut self, way: bool) -> Option { + if self.head.is_null() { + return None; + } + if self.depth > 0 { + self.depth -= 1; + let ret; + if way { + unsafe { + ret = (*self.head).back; + (*(*ret).back).next = (*ret).next; + (*self.head).back = (*ret).back; + } + } else { + unsafe { + ret = self.head; + (*(*ret).back).next = (*ret).next; + (*(*ret).next).back = (*ret).back; + self.head = (*ret).next; + } + } + if self.depth == 0 { + self.head = std::ptr::null_mut(); + } + let boxed = unsafe { Box::from_raw(ret) }; + return Some(boxed.data); + } + None + } +} + +impl Drop for MList { + fn drop(&mut self) { + if self.head.is_null() { + return; + } + let mut current = self.head; + loop { + unsafe { + let next = (*current).next; + let _ = Box::from_raw(current); + if next == self.head { + break; + } + current = next; + } + } + } +} + +pub struct QliePackArchiveWriterV31 { + writer: T, + encryption: Encryption31, + qkey: QlieKey, + header: QlieHeader, + hash: QlieHash14, + has_key_file: bool, + entries: Vec, + key: u32, + common_key: Option>, +} + +struct FilenameEntry { + name: Vec, + hash: u32, + index: u32, +} + +fn get_pos(hash: u32, count: u32) -> u32 { + let v = (hash as u16 as u32) + .wrapping_add(hash >> 8) + .wrapping_add(hash >> 16); + v % count +} + +impl QliePackArchiveWriterV31 { + pub fn new(writer: T, files: &[&str], config: &ExtraConfig) -> Result { + let has_key_file = files.iter().any(|f| *f == QLIE_KEY_FILE); + let mut file_count = files.len() as u32; + if !has_key_file { + if config.qlie_pack_keyfile.is_none() { + anyhow::bail!( + "Qlie Pack Archive key file is required but not provided. Put a key file named '{}' in the directory or specify the path using '--qlie-pack-keyfile' option.", + QLIE_KEY_FILE + ); + } + // Add 1 for the key file + file_count += 1; + } + let header = QlieHeader { + signature: *b"FilePackVer3.1\x00\x00", + file_count, + index_offset: 0, + }; + let encryption = Encryption31::new(); + let mut qkey = QlieKey { + signature: *QLIE_KEY_SIGNATURE, + hash_size: 0, + key: [0; 0x400], + }; + rand::rng().fill(&mut qkey.key[..0x100]); + let key = encryption.compute_hash(&qkey.key[..0x100])? & 0xFFFFFFF; + encrypt(&mut qkey.signature, key)?; + let mut entries = Vec::new(); + let mut list = Vec::with_capacity(256); + for _ in 0..256 { + list.push(MList::::new()); + } + let key_entry = QlieEntry { + name: QLIE_KEY_FILE.to_string(), + ..Default::default() + }; + entries.push(key_entry); + let key_filename: Vec<_> = QLIE_KEY_FILE.encode_utf16().collect(); + let key_hash = encryption.compute_name_hash(&key_filename)?; + let key_name_entry = FilenameEntry { + name: key_filename, + hash: key_hash, + index: 0, + }; + let pos = get_pos(key_hash, 256); + list[pos as usize].push(key_name_entry, true); + for name in files { + if *name == QLIE_KEY_FILE { + continue; + } + let filename: Vec<_> = name.encode_utf16().collect(); + let name_hash = encryption.compute_name_hash(&filename)?; + let entry = QlieEntry { + name: name.to_string(), + ..Default::default() + }; + entries.push(entry); + let name_entry = FilenameEntry { + name: filename, + hash: name_hash, + index: (entries.len() - 1) as u32, + }; + let pos = get_pos(name_hash, 256); + list[pos as usize].push(name_entry, true); + } + let mut hash_data = MemWriter::new(); + for mut list in list { + hash_data.write_u32(list.depth as u32)?; + while let Some(entry) = list.pop(false) { + hash_data.write_u16(entry.name.len() as u16)?; + hash_data.write_struct(&entry.name, false, Encoding::Utf16LE, &None)?; + hash_data.write_u64(entry.index as u64 * 4)?; + hash_data.write_u32(entry.hash)?; + } + } + for i in 0..file_count { + hash_data.write_u32(i)?; + } + let mut hash_data = hash_data.into_inner(); + encrypt(&mut hash_data, 0x0428)?; + let hash = QlieHash14 { + signature: *HASH_VER_1_4_SIGNATURE, + table_size: 256, + file_count: header.file_count, + index_size: header.file_count * 4, + hash_data_size: hash_data.len() as u32, + is_compressed: 0, + unk: [0; 32], + hash_data, + }; + qkey.hash_size = hash.hash_data_size + 68; + let mut inner = Self { + writer, + encryption, + qkey, + header, + hash, + has_key_file, + entries, + key, + common_key: None, + }; + if !has_key_file { + let key_path = config.qlie_pack_keyfile.as_ref().unwrap(); + let key_data = std::fs::read(key_path)?; + inner.write_key(key_data)?; + } + Ok(inner) + } + + fn write_key(&mut self, key_data: Vec) -> Result<()> { + let entry = &mut self.entries[0]; + entry.size = key_data.len() as u32; + entry.offset = self.writer.stream_position()?; + entry.unpacked_size = entry.size; + entry.is_packed = 0; + entry.is_encrypted = 1; + self.common_key = Some(get_common_key(&key_data)?); + let hasher = Encryption31Hasher::new(); + let size = entry.size; + let compute = EntryWriter { + entry, + inner: &mut self.writer, + hasher, + }; + let mut encryptor = + Encryption31EncryptV1::new(compute, size, QLIE_KEY_FILE.to_string(), self.key)?; + encryptor.write_all(&key_data)?; + Ok(()) + } +} + +struct Writer<'a> { + inner: Box, + mem: MemWriter, +} + +impl std::fmt::Debug for Writer<'_> { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("Writer").field("mem", &self.mem).finish() + } +} + +impl<'a> Write for Writer<'a> { + fn write(&mut self, buf: &[u8]) -> std::io::Result { + self.mem.write(buf) + } + + fn flush(&mut self) -> std::io::Result<()> { + self.mem.flush() + } +} + +impl<'a> Seek for Writer<'a> { + fn seek(&mut self, pos: std::io::SeekFrom) -> std::io::Result { + self.mem.seek(pos) + } + + fn stream_position(&mut self) -> std::io::Result { + self.mem.stream_position() + } + + fn rewind(&mut self) -> std::io::Result<()> { + self.mem.rewind() + } +} + +impl<'a> Drop for Writer<'a> { + fn drop(&mut self) { + let _ = self.inner.write_all(&self.mem.data); + let _ = self.inner.flush(); + } +} + +struct Writer2<'a, T: Write + Seek> { + inner: &'a mut QliePackArchiveWriterV31, + entry_idx: usize, + mem: MemWriter, + is_v1: bool, +} + +impl<'a, T: Write + Seek> Writer2<'a, T> { + fn close(&mut self) -> Result<()> { + let entry = &mut self.inner.entries[self.entry_idx]; + entry.size = self.mem.data.len() as u32; + entry.offset = self.inner.writer.stream_position()?; + entry.unpacked_size = entry.size; + entry.is_packed = 0; + let hasher = Encryption31Hasher::new(); + let size = entry.size; + let compute = EntryWriter { + entry, + inner: &mut self.inner.writer, + hasher, + }; + if self.is_v1 { + compute.entry.is_encrypted = 1; + let name = compute.entry.name.clone(); + let mut encryptor = Encryption31EncryptV1::new(compute, size, name, self.inner.key)?; + encryptor.write_all(&self.mem.data)?; + self.inner.common_key = Some(get_common_key(&self.mem.data)?); + } else { + compute.entry.is_encrypted = 2; + let name = compute.entry.name.clone(); + let common_key = self + .inner + .common_key + .as_ref() + .ok_or_else(|| anyhow::anyhow!("Common key is not available"))?; + let mut encryptor = Encryption31EncryptV2::new( + compute, + size, + name, + self.inner.key, + common_key.to_vec(), + )?; + encryptor.write_all(&self.mem.data)?; + } + Ok(()) + } +} + +impl std::fmt::Debug for Writer2<'_, T> { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("Writer").field("mem", &self.mem).finish() + } +} + +impl<'a, T: Write + Seek> Write for Writer2<'a, T> { + fn write(&mut self, buf: &[u8]) -> std::io::Result { + self.mem.write(buf) + } + + fn flush(&mut self) -> std::io::Result<()> { + self.mem.flush() + } +} + +impl<'a, T: Write + Seek> Seek for Writer2<'a, T> { + fn seek(&mut self, pos: std::io::SeekFrom) -> std::io::Result { + self.mem.seek(pos) + } + + fn stream_position(&mut self) -> std::io::Result { + self.mem.stream_position() + } + + fn rewind(&mut self) -> std::io::Result<()> { + self.mem.rewind() + } +} + +impl<'a, T: Write + Seek> Drop for Writer2<'a, T> { + fn drop(&mut self) { + let _ = self.close(); + } +} + +struct EntryWriter<'a, T: Write> { + entry: &'a mut QlieEntry, + inner: T, + hasher: Encryption31Hasher, +} + +impl<'a, T: Write> Write for EntryWriter<'a, T> { + fn write(&mut self, buf: &[u8]) -> std::io::Result { + let writed = self.inner.write(buf)?; + self.hasher + .update(&buf[..writed]) + .map_err(|e| std::io::Error::new(std::io::ErrorKind::Other, e))?; + Ok(writed) + } + + fn flush(&mut self) -> std::io::Result<()> { + self.inner.flush() + } +} + +impl<'a, T: Write> Drop for EntryWriter<'a, T> { + fn drop(&mut self) { + if let Ok(hash) = self.hasher.finalize() { + self.entry.hash = hash; + } + } +} + +impl Archive for QliePackArchiveWriterV31 { + fn prelist<'a>(&'a self) -> Result> + 'a>>> { + if !self.has_key_file { + Ok(None) + } else { + let iter = std::iter::once(Ok(QLIE_KEY_FILE.to_string())); + Ok(Some(Box::new(iter))) + } + } + + fn new_file<'a>( + &'a mut self, + name: &str, + size: Option, + ) -> Result> { + let inner = self.new_file_non_seek(name, size)?; + Ok(Box::new(Writer { + inner, + mem: MemWriter::new(), + })) + } + + fn new_file_non_seek<'a>( + &'a mut self, + name: &str, + size: Option, + ) -> Result> { + if self.common_key.is_none() { + if name != QLIE_KEY_FILE { + anyhow::bail!("Common key is not available before writing key file"); + } + let entry_idx = self + .entries + .iter() + .position(|e| e.name == name) + .ok_or_else(|| anyhow::anyhow!("File {} not found in entries", name))?; + return Ok(Box::new(Writer2 { + inner: self, + entry_idx, + mem: MemWriter::new(), + is_v1: true, + })); + } + if size.is_none() { + let entry_idx = self + .entries + .iter() + .position(|e| e.name == name) + .ok_or_else(|| anyhow::anyhow!("File {} not found in entries", name))?; + return Ok(Box::new(Writer2 { + inner: self, + entry_idx, + mem: MemWriter::new(), + is_v1: false, + })); + } + let entry_idx = self + .entries + .iter() + .position(|e| e.name == name) + .ok_or_else(|| anyhow::anyhow!("File {} not found in entries", name))?; + let entry = &mut self.entries[entry_idx]; + entry.size = size.unwrap() as u32; + entry.offset = self.writer.stream_position()?; + entry.unpacked_size = entry.size; + entry.is_packed = 0; + entry.is_encrypted = 2; + let common_key = self + .common_key + .as_ref() + .ok_or_else(|| anyhow::anyhow!("Common key is not available"))?; + let hasher = Encryption31Hasher::new(); + let size = entry.size; + let compute = EntryWriter { + entry, + inner: &mut self.writer, + hasher, + }; + let encryptor = Encryption31EncryptV2::new( + compute, + size, + name.to_string(), + self.key, + common_key.to_vec(), + )?; + Ok(Box::new(encryptor)) + } + + fn write_header(&mut self) -> Result<()> { + self.header.index_offset = self.writer.stream_position()?; + for entry in &self.entries { + let name_length = entry.name.encode_utf16().count() as u16; + self.writer.write_u16(name_length)?; + let mut encoded = encode_string(Encoding::Utf16LE, &entry.name, true)?; + self.encryption + .encrypt_name(&mut encoded, self.key as i32)?; + self.writer.write_all(&encoded)?; + self.writer.write_u64(entry.offset)?; + self.writer.write_u32(entry.size)?; + self.writer.write_u32(entry.unpacked_size)?; + self.writer.write_u32(entry.is_packed)?; + self.writer.write_u32(entry.is_encrypted)?; + self.writer.write_u32(entry.hash)?; + } + self.writer + .write_struct(&self.hash, false, Encoding::Utf8, &None)?; + self.writer + .write_struct(&self.qkey, false, Encoding::Utf8, &None)?; + self.writer + .write_struct(&self.header, false, Encoding::Utf8, &None)?; + Ok(()) + } +} + +#[test] +fn test_drop_mlist() { + use std::sync::Arc; + use std::sync::atomic::{AtomicI32, Ordering}; + let t = Arc::new(AtomicI32::new(0)); + struct Test { + value: i32, + t: Arc, + } + + impl Test { + fn new(value: i32, t: Arc) -> Self { + Self { value, t } + } + } + + impl Drop for Test { + fn drop(&mut self) { + self.t.fetch_add(self.value, Ordering::SeqCst); + } + } + { + let mut list: MList = MList::new(); + list.push(Test::new(1, t.clone()), true); + list.push(Test::new(2, t.clone()), true); + list.push(Test::new(3, t.clone()), true); + } + let v = t.load(Ordering::SeqCst); + assert_eq!(v, 6); +} + +#[test] +fn test_mlist() { + let mut list = MList::new(); + list.push(1, true); + list.push(2, true); + list.push(3, true); + assert_eq!(list.depth, 3); + assert_eq!(list.pop(false), Some(1)); + assert_eq!(list.depth, 2); + assert_eq!(list.pop(false), Some(2)); + assert_eq!(list.depth, 1); + assert_eq!(list.pop(false), Some(3)); + assert_eq!(list.depth, 0); + assert_eq!(list.pop(false), None); +} diff --git a/src/types.rs b/src/types.rs index ed58f8a..9f798cd 100644 --- a/src/types.rs +++ b/src/types.rs @@ -596,6 +596,9 @@ pub struct ExtraConfig { #[default(true)] /// Whether to process ABMP10 images in ABMP10 images. pub qlie_abmp10_process_abmp10: bool, + #[cfg(feature = "qlie-arc")] + /// Path to qlie pack archive key file (pack_keyfile_kfueheish15538fa9or.key) + pub qlie_pack_keyfile: Option, } #[derive(Clone, Copy, Debug, ValueEnum, PartialEq, Eq, PartialOrd, Ord)]