Add support to unpack qlie pack v3.0 file

This commit is contained in:
2026-04-04 16:35:05 +08:00
parent d4101ae493
commit ea3c764360
6 changed files with 539 additions and 10 deletions

View File

@@ -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<u8>),
StringArray(Vec<String>),
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<R: Read + Seek>(
reader: &mut R,
big: bool,
encoding: Encoding,
info: &Option<Box<dyn std::any::Any>>,
) -> Result<Self> {
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<String, DelphiValue>,
pub contents: Vec<DelphiObject>,
}
impl StructUnpack for DelphiObject {
fn unpack<R: Read + Seek>(
reader: &mut R,
big: bool,
encoding: Encoding,
info: &Option<Box<dyn std::any::Any>>,
) -> Result<Self> {
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<R: Read + Seek>(reader: &mut R) -> Result<DelphiObject> {
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)?)
}

View File

@@ -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<Box<dyn ReadDebug + 'a>>;
}
pub fn create_encryption(major: u8, minor: u8) -> Result<Box<dyn Encryption>> {
pub fn create_encryption(
major: u8,
minor: u8,
game_key: Option<Vec<u8>>,
) -> Result<Box<dyn Encryption>> {
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<Vec<u8>> {
Ok(cursor.into_inner())
}
const KEY_DIR: [&'static str; 4] = [".", "..", "../DLL", "DLL"];
pub fn find_game_key(filename: &str) -> Result<Option<Vec<u8>>> {
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<S: AsRef<Path> + ?Sized>(exe_path: &S) -> Result<Vec<u8>> {
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<Option<Vec<u8>>> {
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<S: AsRef<Path> + ?Sized>(exe_path: &S) -> Result<Vec<u8>> {
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<Vec<u8>>,
}
impl Encryption30 {
pub fn new(game_key: Option<Vec<u8>>) -> Self {
Self { key: game_key }
}
}
impl Encryption for Encryption30 {
fn decrypt_name(&self, name: &mut [u8], hash: i32, encoding: Encoding) -> Result<String> {
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<Box<dyn Hasher>> {
Ok(Box::new(Encryption30Hasher::new()))
}
fn compute_hash(&self, data: &[u8]) -> Result<u32> {
let mut hasher = Encryption30Hasher::new();
hasher.update(data)?;
hasher.finalize()
}
fn decrypt_entry<'a>(
&self,
stream: Box<dyn ReadSeek + 'a>,
entry: &QlieEntry,
) -> Result<Box<dyn ReadDebug + 'a>> {
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<u32> {
let key = self.key ^ (self.key >> 32);
Ok(key as u32)
}
}
#[derive(Debug)]
pub struct Decrypter<'a> {
stream: Box<dyn ReadSeek + 'a>,
v5: u64,
v9: u64,
}
impl<'a> Decrypter<'a> {
pub fn new(stream: Box<dyn ReadSeek + 'a>, 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<usize> {
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<dyn ReadSeek + 'a>,
table: [u64; 0x10],
hash64: u64,
t: usize,
}
impl<'a> Encryption30Decrypt<'a> {
pub fn new<'b>(
stream: Box<dyn ReadSeek + 'a>,
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<usize> {
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);

View File

@@ -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<u8>,
_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<dyn ReadSeek>,
_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<T: Read + Seek + std::fmt::Debug> {
}
impl<T: Read + Seek + std::fmt::Debug> QliePackArchive<T> {
pub fn new(mut reader: T, archive_encoding: Encoding, _config: &ExtraConfig) -> Result<Self> {
pub fn new(
mut reader: T,
archive_encoding: Encoding,
_config: &ExtraConfig,
filename: &str,
) -> Result<Self> {
reader.seek(SeekFrom::End(-0x1C))?;
let header = QlieHeader::unpack(&mut reader, false, archive_encoding, &None)?;
if !header.is_valid() {
@@ -164,7 +174,11 @@ impl<T: Read + Seek + std::fmt::Debug> QliePackArchive<T> {
}
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<T: Read + Seek + std::fmt::Debug> QliePackArchive<T> {
let is_encrypted = reader.read_u32()?;
let hash = reader.read_u32()?;
let entry = QlieEntry {
raw_name,
name,
offset,
size,
@@ -218,9 +233,11 @@ impl<T: Read + Seek + std::fmt::Debug> QliePackArchive<T> {
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<T: Read + Seek + std::fmt::Debug> QliePackArchive<T> {
}
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<T: Read + Seek + std::fmt::Debug + 'static> Script for QliePackArchive<T> {
.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(

View File

@@ -84,3 +84,9 @@ impl MersenneTwister {
(high << 32) | low
}
}
impl Default for MersenneTwister {
fn default() -> Self {
Self::new(DEFAULT_SEED)
}
}

View File

@@ -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<u8>,
pub name: String,
pub offset: u64,
pub size: u32,