From ea3c764360a85e81ac9d45f796460f8d7a2b825a Mon Sep 17 00:00:00 2001 From: lifegpc Date: Sat, 4 Apr 2026 16:35:05 +0800 Subject: [PATCH] Add support to unpack qlie pack v3.0 file --- Cargo.toml | 2 +- src/scripts/qlie/archive/pack/delphi.rs | 153 +++++++++ src/scripts/qlie/archive/pack/encryption.rs | 349 +++++++++++++++++++- src/scripts/qlie/archive/pack/mod.rs | 37 ++- src/scripts/qlie/archive/pack/twister.rs | 6 + src/scripts/qlie/archive/pack/types.rs | 2 + 6 files changed, 539 insertions(+), 10 deletions(-) create mode 100644 src/scripts/qlie/archive/pack/delphi.rs diff --git a/Cargo.toml b/Cargo.toml index 3e53c47..236ef74 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -95,7 +95,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", "rand"] +qlie-arc = ["qlie", "utils-mmx", "pelite", "rand"] qlie-img = ["qlie", "image", "utils-psd"] silky = [] softpal = ["int-enum"] diff --git a/src/scripts/qlie/archive/pack/delphi.rs b/src/scripts/qlie/archive/pack/delphi.rs new file mode 100644 index 0000000..9cd80e6 --- /dev/null +++ b/src/scripts/qlie/archive/pack/delphi.rs @@ -0,0 +1,153 @@ +use crate::ext::io::*; +use crate::types::*; +use crate::utils::encoding::*; +use crate::utils::struct_pack::*; +use anyhow::Result; +use serde::{Deserialize, Serialize}; +use std::collections::HashMap; +use std::io::{Read, Seek}; + +#[derive(Serialize, Deserialize, Debug, Clone)] +#[serde(tag = "type", content = "value")] +pub enum DelphiValue { + Uint8(u8), + Uint16(u16), + LongDouble([u8; 10]), + Unk6, + String(String), + Unk8, + Bool(bool), + ByteString(Vec), + StringArray(Vec), + UnicodeString(String), +} + +impl DelphiValue { + pub fn as_bytes<'a>(&'a self) -> Option<&'a [u8]> { + match self { + DelphiValue::ByteString(b) => Some(b), + _ => None, + } + } +} + +impl StructUnpack for DelphiValue { + fn unpack( + reader: &mut R, + big: bool, + encoding: Encoding, + info: &Option>, + ) -> Result { + let type_id = u8::unpack(reader, big, encoding, info)?; + match type_id { + 2 => Ok(DelphiValue::Uint8(u8::unpack(reader, big, encoding, info)?)), + 3 => Ok(DelphiValue::Uint16(u16::unpack( + reader, big, encoding, info, + )?)), + 5 => Ok(DelphiValue::LongDouble({ + let mut buf = [0u8; 10]; + reader.read_exact(&mut buf)?; + buf + })), + 6 | 7 => Ok(DelphiValue::String({ + let slen = u8::unpack(reader, big, encoding, info)? as usize; + let buf = reader.read_exact_vec(slen)?; + decode_to_string(encoding, &buf, true)? + })), + 8 => Ok(DelphiValue::Bool(false)), + 9 => Ok(DelphiValue::Bool(true)), + 10 => Ok(DelphiValue::ByteString({ + let slen = u32::unpack(reader, big, encoding, info)? as usize; + reader.read_exact_vec(slen)? + })), + 11 => Ok(DelphiValue::StringArray({ + let mut arr = Vec::new(); + let mut len; + while { + len = u8::unpack(reader, big, encoding, info)?; + len > 0 + } { + let buf = reader.read_exact_vec(len as usize)?; + arr.push(decode_to_string(encoding, &buf, true)?); + } + arr + })), + 18 => Ok(DelphiValue::UnicodeString({ + let slen = u32::unpack(reader, big, encoding, info)? as usize; + let buf = reader.read_exact_vec(slen * 2)?; + decode_to_string( + if big { + Encoding::Utf16BE + } else { + Encoding::Utf16LE + }, + &buf, + true, + )? + })), + _ => Err(anyhow::anyhow!("Unknown Delphi value type: {}", type_id)), + } + } +} + +#[derive(Serialize, Deserialize, Debug, Clone)] + +pub struct DelphiObject { + pub type_name: String, + pub name: String, + pub properties: HashMap, + pub contents: Vec, +} + +impl StructUnpack for DelphiObject { + fn unpack( + reader: &mut R, + big: bool, + encoding: Encoding, + info: &Option>, + ) -> Result { + let type_len = u8::unpack(reader, big, encoding, info)? as usize; + let type_name = { + let buf = reader.read_exact_vec(type_len)?; + decode_to_string(encoding, &buf, true)? + }; + let name_len = u8::unpack(reader, big, encoding, info)? as usize; + let name = { + let buf = reader.read_exact_vec(name_len)?; + decode_to_string(encoding, &buf, true)? + }; + let mut properties = HashMap::new(); + let mut keylen; + while { + keylen = u8::unpack(reader, big, encoding, info)?; + keylen > 0 + } { + let key_buf = reader.read_exact_vec(keylen as usize)?; + let key = decode_to_string(encoding, &key_buf, true)?; + let value = DelphiValue::unpack(reader, big, encoding, info)?; + properties.insert(key, value); + } + let mut contents = Vec::new(); + while reader.peek_u8()? != 0 { + contents.push(DelphiObject::unpack(reader, big, encoding, info)?); + } + reader.read_u8()?; // consume the terminating 0 + return Ok(Self { + type_name, + name, + properties, + contents, + }); + } +} + +pub fn deser_delphi(reader: &mut R) -> Result { + let sig = reader.read_u32()?; + if sig != 0x30465054 { + return Err(anyhow::anyhow!( + "Invalid Delphi object signature: {:08X}", + sig + )); + } + Ok(DelphiObject::unpack(reader, false, Encoding::Cp932, &None)?) +} diff --git a/src/scripts/qlie/archive/pack/encryption.rs b/src/scripts/qlie/archive/pack/encryption.rs index 4741bae..1a2f501 100644 --- a/src/scripts/qlie/archive/pack/encryption.rs +++ b/src/scripts/qlie/archive/pack/encryption.rs @@ -1,3 +1,5 @@ +use super::delphi::*; +use super::twister::*; use super::types::*; use crate::ext::io::*; use crate::scripts::base::*; @@ -5,7 +7,10 @@ use crate::types::*; use crate::utils::encoding::*; use crate::utils::mmx::*; use anyhow::Result; +use pelite::FileMap; +use pelite::pe32::{Pe, PeFile}; use std::io::{Read, Seek, SeekFrom, Write}; +use std::path::{Path, PathBuf}; pub trait Hasher { fn update(&mut self, data: &[u8]) -> Result<()>; @@ -30,9 +35,14 @@ pub trait Encryption: std::fmt::Debug { ) -> Result>; } -pub fn create_encryption(major: u8, minor: u8) -> Result> { +pub fn create_encryption( + major: u8, + minor: u8, + game_key: Option>, +) -> Result> { match (major, minor) { (3, 1) => Ok(Box::new(Encryption31::new())), + (3, 0) => Ok(Box::new(Encryption30::new(game_key))), _ => Err(anyhow::anyhow!( "Unsupported encryption version: {}.{}", major, @@ -909,6 +919,343 @@ pub fn compress(data: &[u8]) -> Result> { Ok(cursor.into_inner()) } +const KEY_DIR: [&'static str; 4] = [".", "..", "../DLL", "DLL"]; + +pub fn find_game_key(filename: &str) -> Result>> { + let path = PathBuf::from(filename); + if !path.is_file() { + return Ok(None); + } + if let Some(pdir) = path.parent() { + for dir in KEY_DIR { + let key_path = pdir.join(dir).join("key.fkey"); + if key_path.is_file() { + eprintln!("Found res key file: {}", key_path.display()); + return Ok(Some(std::fs::read(key_path)?)); + } + } + let exe_dir = pdir.join(".."); + if exe_dir.is_dir() { + for entry in std::fs::read_dir(exe_dir)? { + let entry = entry?; + if entry.file_type()?.is_file() { + if let Some(ext) = entry.path().extension() { + if ext.eq_ignore_ascii_case("exe") { + if let Ok(key) = get_game_key_from_exe(&entry.path()) { + eprintln!( + "Found res key in executable: {}", + entry.path().display() + ); + return Ok(Some(key)); + } + } + } + } + } + } + } + Ok(None) +} + +pub fn get_game_key_from_exe + ?Sized>(exe_path: &S) -> Result> { + let path = exe_path.as_ref(); + let file_map = FileMap::open(path)?; + let file = PeFile::from_bytes(&file_map)?; + let resources = file.resources()?; + Ok(resources + .find_resource(&["#10".into(), "RESKEY".into()])? + .to_vec()) +} + +pub fn find_key_data(filename: &str) -> Result>> { + let path = PathBuf::from(filename); + if !path.is_file() { + return Ok(None); + } + if let Some(pdir) = path.parent() { + let exe_dir = pdir.join(".."); + if exe_dir.is_dir() { + for entry in std::fs::read_dir(exe_dir)? { + let entry = entry?; + if entry.file_type()?.is_file() { + if let Some(ext) = entry.path().extension() { + if ext.eq_ignore_ascii_case("exe") { + match get_key_data_from_exe(&entry.path()) { + Ok(key) => { + eprintln!( + "Found key data in executable: {}", + entry.path().display() + ); + return Ok(Some(key)); + } + Err(e) => { + eprintln!( + "Failed to get key data from executable {}: {}\n{}", + entry.path().display(), + e, + e.backtrace(), + ); + } + } + } + } + } + } + } + } + Ok(None) +} + +pub fn get_key_data_from_exe + ?Sized>(exe_path: &S) -> Result> { + let path = exe_path.as_ref(); + let file_map = FileMap::open(path)?; + let file = PeFile::from_bytes(&file_map)?; + let resources = file.resources()?; + let key_data = resources.find_resource(&["#10".into(), "TFORM1".into()])?; + let mut reader = MemReaderRef::new(&key_data); + let form = deser_delphi(&mut reader)?; + let image = form + .contents + .iter() + .find(|s| s.name == "IconKeyImage") + .ok_or(anyhow::anyhow!("IconKeyImage not found in form"))?; + let picture_data = image + .properties + .get("Picture.Data") + .ok_or(anyhow::anyhow!("Picture.Data not found in IconKeyImage"))? + .as_bytes() + .ok_or(anyhow::anyhow!("Picture.Data is not bytes"))?; + let mut reader = MemReaderRef::new(picture_data); + let mut sig = [0u8; 6]; + reader.read_exact(&mut sig)?; + if &sig != b"\x05TIcon" { + return Err(anyhow::anyhow!("Invalid TIcon signature")); + } + Ok(reader.read_exact_vec(0x100)?) +} + +#[derive(Debug)] +pub struct Encryption30 { + key: Option>, +} + +impl Encryption30 { + pub fn new(game_key: Option>) -> Self { + Self { key: game_key } + } +} + +impl Encryption for Encryption30 { + fn decrypt_name(&self, name: &mut [u8], hash: i32, encoding: Encoding) -> Result { + let key = (hash ^ 0x3E) + name.len() as i32; + for i in 1..=name.len() { + name[i - 1] ^= ((key ^ i as i32).wrapping_add(i as i32)) as u8; + } + Ok(decode_to_string(encoding, name, true)?) + } + + fn create_hash(&self) -> Result> { + Ok(Box::new(Encryption30Hasher::new())) + } + + fn compute_hash(&self, data: &[u8]) -> Result { + let mut hasher = Encryption30Hasher::new(); + hasher.update(data)?; + hasher.finalize() + } + + fn decrypt_entry<'a>( + &self, + stream: Box, + entry: &QlieEntry, + ) -> Result> { + if self.key.is_none() || entry.common_key.is_none() { + return Ok(Box::new(Decrypter::new(stream, entry.key, entry.size))); + } + return Ok(Box::new(Encryption30Decrypt::new( + stream, + &entry.raw_name, + entry.common_key.as_ref().unwrap(), + entry.size, + entry.key, + self.key.as_ref().unwrap(), + ))); + } +} + +#[derive(Debug)] +pub struct Encryption30Hasher { + hash: u64, + key: u64, + buffer: [u8; 8], + buffer_len: usize, +} + +impl Encryption30Hasher { + pub fn new() -> Self { + Self { + hash: 0, + key: 0, + buffer: [0; 8], + buffer_len: 0, + } + } + + fn update_internal(&mut self, data: u64) { + const C: u64 = mmx_punpckldq2(0x03070307); + self.hash = mmx_p_add_w(self.hash, C); + self.key = mmx_p_add_w(self.key, self.hash ^ data); + } +} + +impl Hasher for Encryption30Hasher { + fn update(&mut self, data: &[u8]) -> Result<()> { + let mut used = 0; + if self.buffer_len > 0 { + let to_copy = (8 - self.buffer_len).min(data.len()); + self.buffer[self.buffer_len..self.buffer_len + to_copy] + .copy_from_slice(&data[..to_copy]); + self.buffer_len += to_copy; + used += to_copy; + } + if self.buffer_len == 8 { + let v = u64::from_le_bytes(self.buffer); + self.update_internal(v); + self.buffer_len = 0; + } + let round = (data.len() - used) / 8; + let mut reader = MemReaderRef::new(&data[used..]); + for _ in 0..round { + let v = reader.read_u64()?; + self.update_internal(v); + used += 8; + } + let remaining = data.len() - used; + if remaining > 0 { + self.buffer[..remaining].copy_from_slice(&data[used..]); + self.buffer_len = remaining; + } + Ok(()) + } + + fn finalize(&mut self) -> Result { + let key = self.key ^ (self.key >> 32); + Ok(key as u32) + } +} + +#[derive(Debug)] +pub struct Decrypter<'a> { + stream: Box, + v5: u64, + v9: u64, +} + +impl<'a> Decrypter<'a> { + pub fn new(stream: Box, key: u32, length: u32) -> AlignedReader<8, Self> { + const C1: u64 = 0xA73C5F9D; + const C3: u64 = 0xFEC9753E; + const V5_INIT: u64 = mmx_punpckldq2(C1); + let v9 = mmx_punpckldq2((length.wrapping_add(key) as u64) ^ C3); + AlignedReader::new(Self { + stream, + v5: V5_INIT, + v9, + }) + } +} + +impl<'a> Read for Decrypter<'a> { + fn read(&mut self, buf: &mut [u8]) -> std::io::Result { + let readed = self.stream.read_most(buf)?; + let round = readed / 8; + let mut writer = MemWriterRef::new(buf); + const C2: u64 = 0xCE24F523; + const V7: u64 = mmx_punpckldq2(C2); + for _ in 0..round { + let d = writer.peek_u64()?; + self.v5 = mmx_p_add_d(self.v5, V7) ^ self.v9; + self.v9 = d ^ self.v5; + writer.write_u64(self.v9)?; + } + Ok(readed) + } +} + +#[derive(Debug)] +struct Encryption30Decrypt<'a> { + stream: Box, + table: [u64; 0x10], + hash64: u64, + t: usize, +} + +impl<'a> Encryption30Decrypt<'a> { + pub fn new<'b>( + stream: Box, + raw_name: &'b [u8], + common_key: &'b [u8], + size: u32, + key: u32, + game_key: &'b [u8], + ) -> AlignedReader<8, Self> { + let mut hash = 0x85F532u32; + let mut seed = 0x33F641u32; + for (i, n) in raw_name.iter().enumerate() { + hash = hash.wrapping_add(((i & 0xFF) as u32) * (*n as u32)); + seed ^= hash; + } + seed = seed.wrapping_add( + key ^ ((7 * (size & 0xFFFFFF)) + .wrapping_add(size) + .wrapping_add(hash) + .wrapping_add(hash ^ size ^ 0x8F32DC)), + ); + seed = 9 * (seed & 0xFFFFFF); + seed ^= 0x453A; + let mut mt = MersenneTwister::new(seed); + if !common_key.is_empty() { + mt.xor_state(common_key); + } + if !game_key.is_empty() { + mt.xor_state(game_key); + } + let mut table = [0u64; 0x10]; + for i in 0..0x10 { + table[i] = mt.rand64(); + } + for _ in 0..9 { + mt.rand(); + } + let hash64 = mt.rand64(); + let t = mt.rand() as usize & 0xF; + AlignedReader::new(Self { + stream, + table, + hash64, + t, + }) + } +} + +impl<'a> Read for Encryption30Decrypt<'a> { + fn read(&mut self, buf: &mut [u8]) -> std::io::Result { + let readed = self.stream.read_most(buf)?; + let round = readed / 8; + let mut writer = MemWriterRef::new(buf); + for _ in 0..round { + let data64 = writer.peek_u64()?; + self.hash64 = mmx_p_add_d(self.hash64 ^ self.table[self.t], self.table[self.t]); + let d = data64 ^ self.hash64; + writer.write_u64(d)?; + self.hash64 = mmx_p_add_b(self.hash64, d) ^ d; + self.hash64 = mmx_p_add_w(mmx_p_sll_d(self.hash64, 1), d); + self.t = (self.t + 1) & 0xF; + } + Ok(readed) + } +} + #[test] fn test_compress_decompress() -> Result<()> { let data = b"The quick brown fox jumps over the lazy dog.".repeat(100); diff --git a/src/scripts/qlie/archive/pack/mod.rs b/src/scripts/qlie/archive/pack/mod.rs index 1046cc5..a7a6c92 100644 --- a/src/scripts/qlie/archive/pack/mod.rs +++ b/src/scripts/qlie/archive/pack/mod.rs @@ -1,4 +1,5 @@ //! Qlie Pack Archive (.pack) +mod delphi; mod encryption; mod twister; mod types; @@ -35,7 +36,7 @@ impl ScriptBuilder for QliePackArchiveBuilder { fn build_script( &self, data: Vec, - _filename: &str, + filename: &str, _encoding: Encoding, archive_encoding: Encoding, config: &ExtraConfig, @@ -45,6 +46,7 @@ impl ScriptBuilder for QliePackArchiveBuilder { MemReader::new(data), archive_encoding, config, + filename, )?)) } @@ -62,6 +64,7 @@ impl ScriptBuilder for QliePackArchiveBuilder { MemReader::new(data), archive_encoding, config, + filename, )?)) } else { let f = std::fs::File::open(filename)?; @@ -70,6 +73,7 @@ impl ScriptBuilder for QliePackArchiveBuilder { reader, archive_encoding, config, + filename, )?)) } } @@ -77,7 +81,7 @@ impl ScriptBuilder for QliePackArchiveBuilder { fn build_script_from_reader( &self, reader: Box, - _filename: &str, + filename: &str, _encoding: Encoding, archive_encoding: Encoding, config: &ExtraConfig, @@ -87,6 +91,7 @@ impl ScriptBuilder for QliePackArchiveBuilder { reader, archive_encoding, config, + filename, )?)) } @@ -150,7 +155,12 @@ pub struct QliePackArchive { } impl QliePackArchive { - pub fn new(mut reader: T, archive_encoding: Encoding, _config: &ExtraConfig) -> Result { + pub fn new( + mut reader: T, + archive_encoding: Encoding, + _config: &ExtraConfig, + filename: &str, + ) -> Result { reader.seek(SeekFrom::End(-0x1C))?; let header = QlieHeader::unpack(&mut reader, false, archive_encoding, &None)?; if !header.is_valid() { @@ -164,7 +174,11 @@ impl QliePackArchive { } let major = header.major_version(); let minor = header.minor_version(); - let encryption = encryption::create_encryption(major, minor)?; + let mut game_key = None; + if major == 3 && minor == 0 { + game_key = encryption::find_key_data(filename)?; + } + let encryption = encryption::create_encryption(major, minor, game_key)?; // Read key let mut key = 0; let mut qkey = None; @@ -205,6 +219,7 @@ impl QliePackArchive { let is_encrypted = reader.read_u32()?; let hash = reader.read_u32()?; let entry = QlieEntry { + raw_name, name, offset, size, @@ -218,9 +233,11 @@ impl QliePackArchive { entries.push(entry); } let mut common_key = None; - if major >= 3 && minor >= 1 { - if let Some(common_key_entry) = entries.iter().find(|e| e.name == QLIE_KEY_FILE) { + if major >= 3 { + common_key = encryption::find_game_key(filename)?; + if let Some(common_key_entry) = entries.iter_mut().find(|e| e.name == QLIE_KEY_FILE) { reader.seek(SeekFrom::Start(common_key_entry.offset))?; + common_key_entry.common_key = common_key.clone(); 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)?; if common_key_entry.is_packed != 0 { @@ -228,7 +245,11 @@ impl QliePackArchive { } let mut key_data = Vec::new(); decrypted.read_to_end(&mut key_data)?; - common_key = Some(encryption::get_common_key(&key_data)?); + if minor == 1 { + common_key = Some(encryption::get_common_key(&key_data)?); + } else { + common_key = Some(key_data); + } } } Ok(Self { @@ -267,7 +288,7 @@ impl Script for QliePackArchive { .get(index) .ok_or_else(|| anyhow::anyhow!("Invalid file index {} for Qlie Pack Archive", index))? .clone(); - if self.common_key.is_some() { + if self.common_key.is_some() && entry.common_key.is_none() { entry.common_key = self.common_key.clone(); } let stream = StreamRegion::with_size( diff --git a/src/scripts/qlie/archive/pack/twister.rs b/src/scripts/qlie/archive/pack/twister.rs index 217f20b..1878f93 100644 --- a/src/scripts/qlie/archive/pack/twister.rs +++ b/src/scripts/qlie/archive/pack/twister.rs @@ -84,3 +84,9 @@ impl MersenneTwister { (high << 32) | low } } + +impl Default for MersenneTwister { + fn default() -> Self { + Self::new(DEFAULT_SEED) + } +} diff --git a/src/scripts/qlie/archive/pack/types.rs b/src/scripts/qlie/archive/pack/types.rs index 0b3abae..fd662de 100644 --- a/src/scripts/qlie/archive/pack/types.rs +++ b/src/scripts/qlie/archive/pack/types.rs @@ -84,6 +84,8 @@ pub struct QlieKey { #[derive(Debug, Clone, Default)] pub struct QlieEntry { + /// Used in some versions of file decryption. + pub raw_name: Vec, pub name: String, pub offset: u64, pub size: u32,