Add support to unpack qlie pack ver 1.0/2.0

This commit is contained in:
2026-04-05 10:14:04 +08:00
parent 0a2918ad56
commit cf8bae238d
2 changed files with 191 additions and 32 deletions

View File

@@ -33,6 +33,9 @@ pub trait Encryption: std::fmt::Debug {
stream: Box<dyn ReadSeek + 'a>,
entry: &QlieEntry,
) -> Result<Box<dyn ReadDebug + 'a>>;
fn index_has_hash(&self) -> bool {
true
}
}
pub fn create_encryption(
@@ -43,6 +46,8 @@ pub fn create_encryption(
match (major, minor) {
(3, 1) => Ok(Box::new(Encryption31::new())),
(3, 0) => Ok(Box::new(Encryption30::new(game_key))),
(2, 0) => Ok(Box::new(Encryption20::new())),
(1, 0) => Ok(Box::new(Encryption10::new())),
_ => Err(anyhow::anyhow!(
"Unsupported encryption version: {}.{}",
major,
@@ -1249,6 +1254,124 @@ impl<'a> Read for Encryption30Decrypt<'a> {
}
}
#[derive(Debug)]
pub struct Encryption10 {}
impl Encryption10 {
pub fn new() -> Self {
Self {}
}
}
impl Encryption for Encryption10 {
fn decrypt_name(&self, name: &mut [u8], _hash: i32, encoding: Encoding) -> Result<String> {
const KEY: u32 = 0xC4 ^ 0x3E;
for (i, b) in name.iter_mut().enumerate() {
let i = (i + 1) as u32;
*b ^= ((KEY ^ i).wrapping_add(i)) as u8;
}
decode_to_string(encoding, name, true)
}
fn decrypt_entry<'a>(
&self,
stream: Box<dyn ReadSeek + 'a>,
entry: &QlieEntry,
) -> Result<Box<dyn ReadDebug + 'a>> {
if entry.is_encrypted == 0 {
return Ok(Box::new(stream));
}
Ok(Box::new(Encryption10Decrypt::new(stream, 0)))
}
fn index_has_hash(&self) -> bool {
false
}
}
#[derive(Debug)]
pub struct Encryption20 {
index_has_hash: bool,
}
impl Encryption20 {
pub fn new() -> Self {
Self {
index_has_hash: true,
}
}
pub fn new_no_hash() -> Self {
Self {
index_has_hash: false,
}
}
}
impl Encryption for Encryption20 {
fn decrypt_name(&self, name: &mut [u8], _hash: i32, encoding: Encoding) -> Result<String> {
let key = (0xC4u32 ^ 0x3E).wrapping_add(name.len() as u32);
for (i, b) in name.iter_mut().enumerate() {
let i = (i + 1) as u32;
*b ^= ((key ^ i).wrapping_add(i)) as u8;
}
decode_to_string(encoding, name, true)
}
fn decrypt_entry<'a>(
&self,
stream: Box<dyn ReadSeek + 'a>,
entry: &QlieEntry,
) -> Result<Box<dyn ReadDebug + 'a>> {
if entry.is_encrypted == 0 {
return Ok(Box::new(stream));
}
Ok(Box::new(Decrypter::new(stream, 0, entry.size)))
}
fn index_has_hash(&self) -> bool {
self.index_has_hash
}
}
#[derive(Debug)]
struct Encryption10Decrypt<'a> {
stream: Box<dyn ReadSeek + 'a>,
v5: u64,
v9: u64,
}
impl<'a> Encryption10Decrypt<'a> {
pub fn new(stream: Box<dyn ReadSeek + 'a>, key: u32) -> AlignedReader<8, Self> {
const C1: u64 = 0xA73C5F9D;
const C3: u64 = 0xFEC9753E;
const V5_INIT: u64 = mmx_punpckldq2(C1);
let v9 = mmx_punpckldq2((key as u64) ^ C3);
AlignedReader::new(Self {
stream,
v5: V5_INIT,
v9,
})
}
}
impl<'a> Read for Encryption10Decrypt<'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)
}
}
#[test]
fn test_compress_decompress() -> Result<()> {
let data = b"The quick brown fox jumps over the lazy dog.".repeat(100);

View File

@@ -178,7 +178,7 @@ impl<T: Read + Seek + std::fmt::Debug> QliePackArchive<T> {
if major == 3 && minor == 0 {
game_key = encryption::find_key_data(filename)?;
}
let encryption = encryption::create_encryption(major, minor, game_key)?;
let mut encryption = encryption::create_encryption(major, minor, game_key)?;
// Read key
let mut key = 0;
let mut qkey = None;
@@ -201,37 +201,31 @@ impl<T: Read + Seek + std::fmt::Debug> QliePackArchive<T> {
qkey = Some(qk);
}
// Read entries
let mut entries = Vec::new();
reader.seek(SeekFrom::Start(header.index_offset))?;
for _ in 0..header.file_count {
let name_length = reader.read_u16()?;
let raw_name_length = if encryption.is_unicode() {
name_length as usize * 2
} else {
name_length as usize
};
let mut raw_name = reader.read_exact_vec(raw_name_length)?;
let name = encryption.decrypt_name(&mut raw_name, key as i32, archive_encoding)?;
let offset = reader.read_u64()?;
let size = reader.read_u32()?;
let unpacked_size = reader.read_u32()?;
let is_packed = reader.read_u32()?;
let is_encrypted = reader.read_u32()?;
let hash = reader.read_u32()?;
let entry = QlieEntry {
raw_name,
name,
offset,
size,
unpacked_size,
is_packed,
is_encrypted,
hash,
key,
common_key: None,
};
entries.push(entry);
}
let mut entries = if major >= 2 {
Self::read_entries(&mut reader, key, &header, archive_encoding, &encryption)?
} else {
let possible_encs: [Box<dyn Encryption>; 3] = [
Box::new(encryption::Encryption10::new()),
Box::new(encryption::Encryption20::new_no_hash()),
Box::new(encryption::Encryption20::new()),
];
let mut t = None;
for enc in possible_encs {
match Self::read_entries(&mut reader, key, &header, archive_encoding, &enc) {
Ok(entries) => {
encryption = enc;
t = Some(entries);
break;
}
Err(_) => continue,
}
}
t.ok_or_else(|| {
anyhow::anyhow!(
"Failed to read Qlie Pack Archive entries with any encryption method"
)
})?
};
let mut common_key = None;
if major >= 3 {
common_key = encryption::find_game_key(filename)?;
@@ -261,6 +255,48 @@ impl<T: Read + Seek + std::fmt::Debug> QliePackArchive<T> {
common_key,
})
}
fn read_entries(
reader: &mut T,
key: u32,
header: &QlieHeader,
archive_encoding: Encoding,
encryption: &Box<dyn Encryption>,
) -> Result<Vec<QlieEntry>> {
let mut entries = Vec::new();
reader.seek(SeekFrom::Start(header.index_offset))?;
let has_hash = encryption.index_has_hash();
for _ in 0..header.file_count {
let name_length = reader.read_u16()?;
let raw_name_length = if encryption.is_unicode() {
name_length as usize * 2
} else {
name_length as usize
};
let mut raw_name = reader.read_exact_vec(raw_name_length)?;
let name = encryption.decrypt_name(&mut raw_name, key as i32, archive_encoding)?;
let offset = reader.read_u64()?;
let size = reader.read_u32()?;
let unpacked_size = reader.read_u32()?;
let is_packed = reader.read_u32()?;
let is_encrypted = reader.read_u32()?;
let hash = if has_hash { reader.read_u32()? } else { 0 };
let entry = QlieEntry {
raw_name,
name,
offset,
size,
unpacked_size,
is_packed,
is_encrypted,
hash,
key,
common_key: None,
};
entries.push(entry);
}
Ok(entries)
}
}
impl<T: Read + Seek + std::fmt::Debug + 'static> Script for QliePackArchive<T> {