diff --git a/Cargo.lock b/Cargo.lock index 03fe297..7ad72c0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1384,8 +1384,6 @@ dependencies = [ [[package]] name = "msg_tool_macro" version = "0.2.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "948d9a6a94cfa8de09f3e4c7030cd28b94ead994784d2b7226c311e3d0a8b0ec" dependencies = [ "quote", "syn 2.0.114", diff --git a/Cargo.toml b/Cargo.toml index c319555..0821ef5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -34,7 +34,7 @@ markup5ever = { version = "0.36", optional = true } markup5ever_rcdom = { version = "0.36", optional = true } memchr = { version = "2.7", optional = true } mozjpeg = { version = "0.10", optional = true } -msg_tool_macro = { version = "0.2.18" } +msg_tool_macro = { path = "./msg_tool_macro" } num_cpus = "1.17" overf = "0.1" parse-size = { version = "1.1", optional = true } @@ -62,7 +62,7 @@ default = ["all-fmt", "image-jpg", "image-jxl", "image-webp", "audio-flac", "jie all-fmt = ["all-script", "all-img", "all-arc", "all-audio"] all-script = ["artemis", "artemis-panmimisoft", "bgi", "cat-system", "circus", "entis-gls", "escude", "ex-hibit", "favorite", "hexen-haus", "kirikiri", "musica", "qlie", "silky", "softpal", "will-plus", "yaneurao", "yaneurao-itufuru"] all-img = ["bgi-img", "cat-system-img", "circus-img", "emote-img", "hexen-haus-img", "kirikiri-img", "softpal-img", "will-plus-img"] -all-arc = ["artemis-arc", "bgi-arc", "cat-system-arc", "circus-arc", "escude-arc", "ex-hibit-arc", "hexen-haus-arc", "kirikiri-arc", "musica-arc", "softpal-arc"] +all-arc = ["artemis-arc", "bgi-arc", "cat-system-arc", "circus-arc", "escude-arc", "ex-hibit-arc", "hexen-haus-arc", "kirikiri-arc", "musica-arc", "qlie-arc", "softpal-arc"] all-audio = ["bgi-audio", "circus-audio"] artemis = ["stylua", "utils-escape"] artemis-panmimisoft = ["artemis", "rust-ini"] @@ -94,6 +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"] silky = [] softpal = ["int-enum"] softpal-arc = ["softpal"] @@ -117,6 +118,7 @@ utils-bit-stream = [] utils-blowfish = ["byteorder"] utils-crc32 = [] utils-escape = ["fancy-regex"] +utils-mmx = [] utils-pcm = [] utils-rc4 = [] utils-serde-base64bytes = ["base64"] diff --git a/msg_tool_macro/src/lib.rs b/msg_tool_macro/src/lib.rs index f3ac872..954fe0a 100644 --- a/msg_tool_macro/src/lib.rs +++ b/msg_tool_macro/src/lib.rs @@ -80,6 +80,7 @@ pub fn struct_unpack_impl_for_num(item: TokenStream) -> TokenStream { /// * `pvec()` attribute can be used to specify a packed vector length for Vec<_> fields, where `` can be `u8`, `u16`, `u32`, or `u64`. /// Length is read as a prefix before the vector data. /// * `skip_pack_if()` attribute can be used to skip packing a field if the expression evaluates to true. The expression must be a valid Rust expression that evaluates to a boolean. +/// * `pack_vec_len()` can be used to specify the length of a Vec field based on an expression. The expression must evaluate to a usize and can reference previously packed fields. #[proc_macro_derive( StructPack, attributes( @@ -90,7 +91,8 @@ pub fn struct_unpack_impl_for_num(item: TokenStream) -> TokenStream { fvec, pstring, pvec, - skip_pack_if + skip_pack_if, + pack_vec_len, ) )] pub fn struct_pack_derive(input: TokenStream) -> TokenStream { @@ -109,6 +111,7 @@ pub fn struct_pack_derive(input: TokenStream) -> TokenStream { let mut cur = None; let mut skip_if = None; let mut is_cstring = false; + let mut vec_len: Option = None; for attr in &field.attrs { let path = attr.path(); if path.is_ident("skip_pack") { @@ -177,6 +180,10 @@ pub fn struct_pack_derive(input: TokenStream) -> TokenStream { if let syn::Meta::List(list) = &attr.meta { skip_if = Some(list.parse_args::().unwrap()); } + } else if path.is_ident("pack_vec_len") { + if let syn::Meta::List(list) = &attr.meta { + vec_len = Some(list.parse_args::().unwrap()); + } } } if skipped { @@ -245,6 +252,16 @@ pub fn struct_pack_derive(input: TokenStream) -> TokenStream { item.pack(writer, big, encoding, __info)?; } }); + } else if let Some(vec_len) = vec_len { + cur = Some(quote::quote! { + let len = (#vec_len) as usize; + if self.#field_name.len() != len { + return Err(anyhow::anyhow!("Vector length was not equal to the specified length for field '{}'", stringify!(#field_name))); + } + for item in &self.#field_name { + item.pack(writer, big, encoding, __info)?; + } + }); } } } @@ -300,6 +317,7 @@ pub fn struct_pack_derive(input: TokenStream) -> TokenStream { let mut cur = None; let mut skip_if = None; let mut is_cstring = false; + let mut vec_len: Option = None; for attr in &field.attrs { let path = attr.path(); if path.is_ident("skip_pack") { @@ -368,6 +386,10 @@ pub fn struct_pack_derive(input: TokenStream) -> TokenStream { if let syn::Meta::List(list) = &attr.meta { skip_if = Some(list.parse_args::().unwrap()); } + } else if path.is_ident("pack_vec_len") { + if let syn::Meta::List(list) = &attr.meta { + vec_len = Some(list.parse_args::().unwrap()); + } } } if skipped { @@ -437,6 +459,16 @@ pub fn struct_pack_derive(input: TokenStream) -> TokenStream { item.pack(writer, big, encoding, __info)?; } }); + } else if let Some(vec_len) = vec_len { + cur = Some(quote::quote! { + let len = (#vec_len) as usize; + if #field_name.len() != len { + return Err(anyhow::anyhow!("Vector length was not equal to the specified length for field '{}'", stringify!(#field_name))); + } + for item in &#field_name { + item.pack(writer, big, encoding, __info)?; + } + }); } } } @@ -502,6 +534,7 @@ pub fn struct_pack_derive(input: TokenStream) -> TokenStream { /// length is read as a prefix before the vector data. /// * `skip_unpack_if()` attribute can be used to skip unpacking a field if the expression evaluates to true. The expression must be a valid Rust expression that evaluates to a boolean. /// * `pvec_max_preallocated_size()` attribute can be used to limit the maximum preallocated size for packed vectors to prevent excessive memory allocation. If the unpacked size exceeds this limit, preallocation will be disabled. Default size is 0x400000 (4 MB). +/// * `unpack_vec_len()` can be used to specify the length of a Vec field based on an expression. The expression must evaluate to a usize and can reference previously unpacked fields. #[proc_macro_derive( StructUnpack, attributes( @@ -514,6 +547,7 @@ pub fn struct_pack_derive(input: TokenStream) -> TokenStream { pvec, skip_unpack_if, pvec_max_preallocated_size, + unpack_vec_len, ) )] pub fn struct_unpack_derive(input: TokenStream) -> TokenStream { @@ -532,6 +566,7 @@ pub fn struct_unpack_derive(input: TokenStream) -> TokenStream { let mut cur = None; let mut skip_if: Option = None; let mut is_cstring = false; + let mut vec_len : Option = None; let mut pvec_max_preallocated_size = syn::Expr::Lit(syn::ExprLit { attrs: Vec::new(), lit: syn::Lit::Int(syn::LitInt::new("4194304", field.span())), // Default 4 MB @@ -602,6 +637,10 @@ pub fn struct_unpack_derive(input: TokenStream) -> TokenStream { if let syn::Meta::List(list) = &attr.meta { pvec_max_preallocated_size = list.parse_args::().unwrap(); } + } else if path.is_ident("unpack_vec_len") { + if let syn::Meta::List(list) = &attr.meta { + vec_len = Some(list.parse_args::().unwrap()); + } } } let field_name = match &field.ident { @@ -653,6 +692,11 @@ pub fn struct_unpack_derive(input: TokenStream) -> TokenStream { let len = <#pvec_type>::unpack(reader, big, encoding, __info)? as usize; let #field_name = reader.read_struct_vec2(len, big, encoding, __info, #pvec_max_preallocated_size)?; }); + } else if let Some(vec_len) = vec_len { + cur = Some(quote::quote! { + let len = (#vec_len) as usize; + let #field_name = reader.read_struct_vec2(len, big, encoding, __info, #pvec_max_preallocated_size)?; + }); } } } diff --git a/src/ext/io.rs b/src/ext/io.rs index c21f7d8..ba0601e 100644 --- a/src/ext/io.rs +++ b/src/ext/io.rs @@ -943,6 +943,10 @@ pub trait ReadExt { /// Reads data and checks if it matches the provided data. fn read_and_equal(&mut self, data: &[u8]) -> Result<()>; + + /// Reads as much data as possible into the provided buffer. + /// Returns the number of bytes read. + fn read_most(&mut self, buf: &mut [u8]) -> Result; } impl ReadExt for T { @@ -1113,6 +1117,18 @@ impl ReadExt for T { } Ok(()) } + + fn read_most(&mut self, buf: &mut [u8]) -> Result { + let mut total_read = 0; + while total_read < buf.len() { + match self.read(&mut buf[total_read..]) { + Ok(0) => break, // EOF reached + Ok(n) => total_read += n, + Err(e) => return Err(e), + } + } + Ok(total_read) + } } /// A trait to help to write data to a writer. @@ -1681,6 +1697,14 @@ impl MemWriter { } } + /// Creates a new `MemWriter` with the given capacity. + pub fn with_capacity(capacity: usize) -> Self { + MemWriter { + data: Vec::with_capacity(capacity), + pos: 0, + } + } + /// Creates a new `MemWriter` with the given data. pub fn from_vec(data: Vec) -> Self { MemWriter { data, pos: 0 } @@ -1791,6 +1815,102 @@ impl CPeek for MemWriter { } } +/// A memory reader that can read data from a slice of bytes. +pub struct MemWriterRef<'a> { + /// The data to read from. + pub data: &'a mut [u8], + /// The current position in the data. + pub pos: usize, +} + +impl<'a> MemWriterRef<'a> { + /// Creates a new `MemWriterRef` with the given data. + pub fn new(data: &'a mut [u8]) -> Self { + MemWriterRef { data, pos: 0 } + } +} + +impl<'a> Read for MemWriterRef<'a> { + fn read(&mut self, buf: &mut [u8]) -> Result { + if self.pos >= self.data.len() { + return Ok(0); + } + let bytes_to_read = buf.len().min(self.data.len() - self.pos); + let mut bu = &self.data[self.pos..self.pos + bytes_to_read]; + bu.read(buf)?; + self.pos += bytes_to_read; + Ok(bytes_to_read) + } +} + +impl<'a> Seek for MemWriterRef<'a> { + fn seek(&mut self, pos: SeekFrom) -> Result { + match pos { + SeekFrom::Start(offset) => { + if offset > self.data.len() as u64 { + return Err(std::io::Error::new( + std::io::ErrorKind::InvalidInput, + "Seek position is beyond the end of the data", + )); + } + self.pos = offset as usize; + } + SeekFrom::End(offset) => { + let end_pos = self.data.len() as i64 + offset; + if end_pos < 0 { + return Err(std::io::Error::new( + std::io::ErrorKind::InvalidInput, + "Seek from end resulted in negative position", + )); + } + if end_pos as usize > self.data.len() { + return Err(std::io::Error::new( + std::io::ErrorKind::InvalidInput, + "Seek position is beyond the end of the data", + )); + } + self.pos = end_pos as usize; + } + SeekFrom::Current(offset) => { + let new_pos = (self.pos as i64 + offset) as usize; + if new_pos > self.data.len() { + return Err(std::io::Error::new( + std::io::ErrorKind::InvalidInput, + "Seek position is beyond the end of the data", + )); + } + self.pos = new_pos; + } + } + Ok(self.pos as u64) + } + + fn stream_position(&mut self) -> Result { + Ok(self.pos as u64) + } + + fn rewind(&mut self) -> Result<()> { + self.pos = 0; + Ok(()) + } +} + +impl Write for MemWriterRef<'_> { + fn write(&mut self, buf: &[u8]) -> Result { + if self.pos >= self.data.len() { + return Ok(0); + } + let bytes_to_write = buf.len().min(self.data.len() - self.pos); + self.data[self.pos..self.pos + bytes_to_write].copy_from_slice(&buf[..bytes_to_write]); + self.pos += bytes_to_write; + Ok(bytes_to_write) + } + + fn flush(&mut self) -> Result<()> { + Ok(()) + } +} + /// A region of a stream that can be read/write and seeked within a specified range. #[derive(Debug)] pub struct StreamRegion { @@ -2404,3 +2524,62 @@ impl<'a, T: Write> Write for TrackStream<'a, T> { self.inner.flush() } } + +/// A reader that forwards reads to an inner reader, but ensures that all reads are aligned to a specified alignment boundary. +#[derive(Debug)] +pub struct AlignedReader { + inner: T, + buffer: [u8; A], + buffer_size: usize, +} + +impl AlignedReader { + /// Creates a new `AlignedReader` with the given inner reader. + pub fn new(inner: T) -> Self { + AlignedReader { + inner, + buffer: [0; A], + buffer_size: 0, + } + } +} + +impl Read for AlignedReader { + fn read(&mut self, buf: &mut [u8]) -> Result { + let mut total_read = 0; + let mut needed = buf.len(); + if needed <= self.buffer_size { + buf[..needed].copy_from_slice(&self.buffer[..needed]); + self.buffer.copy_within(needed..self.buffer_size, 0); + self.buffer_size -= needed; + return Ok(needed); + } + if self.buffer_size > 0 { + buf[..self.buffer_size].copy_from_slice(&self.buffer[..self.buffer_size]); + total_read += self.buffer_size; + needed -= self.buffer_size; + self.buffer_size = 0; + } + let read_len = needed / A * A; + if read_len > 0 { + let readed = self + .inner + .read(&mut buf[total_read..total_read + read_len])?; + total_read += readed; + needed -= readed; + // EOF reached + if readed < read_len { + return Ok(total_read); + } + } + if needed > 0 { + let readed = self.inner.read(&mut self.buffer)?; + let to_copy = needed.min(readed); + buf[total_read..total_read + to_copy].copy_from_slice(&self.buffer[..to_copy]); + total_read += to_copy; + self.buffer_size = readed - to_copy; + self.buffer.copy_within(to_copy..readed, 0); + } + Ok(total_read) + } +} diff --git a/src/scripts/base.rs b/src/scripts/base.rs index 689ae9b..87e6262 100644 --- a/src/scripts/base.rs +++ b/src/scripts/base.rs @@ -14,8 +14,13 @@ pub trait WriteSeek: Write + Seek {} /// A trait for types that can be displayed in debug format and are also support downcasting. pub trait AnyDebug: std::fmt::Debug + std::any::Any {} +/// A trait for reading in a stream with debug format. +pub trait ReadDebug: Read + std::fmt::Debug {} + impl ReadSeek for T {} +impl ReadDebug for T {} + impl WriteSeek for T {} impl AnyDebug for T {} diff --git a/src/scripts/mod.rs b/src/scripts/mod.rs index e7ecca7..2846453 100644 --- a/src/scripts/mod.rs +++ b/src/scripts/mod.rs @@ -168,6 +168,8 @@ lazy_static::lazy_static! { Box::new(entis_gls::csx::CSXScriptBuilder::new()), #[cfg(feature = "qlie")] Box::new(qlie::script::QlieScriptBuilder::new()), + #[cfg(feature = "qlie-arc")] + Box::new(qlie::archive::pack::QliePackArchiveBuilder::new()), ]; /// A list of all script extensions. pub static ref ALL_EXTS: Vec = diff --git a/src/scripts/qlie/archive/mod.rs b/src/scripts/qlie/archive/mod.rs new file mode 100644 index 0000000..3deaf09 --- /dev/null +++ b/src/scripts/qlie/archive/mod.rs @@ -0,0 +1,3 @@ +//! Qlie Engine archive module +#[allow(dead_code)] +pub mod pack; diff --git a/src/scripts/qlie/archive/pack/encryption.rs b/src/scripts/qlie/archive/pack/encryption.rs new file mode 100644 index 0000000..153bf7d --- /dev/null +++ b/src/scripts/qlie/archive/pack/encryption.rs @@ -0,0 +1,475 @@ +use super::types::*; +use crate::ext::io::*; +use crate::scripts::base::*; +use crate::types::*; +use crate::utils::encoding::*; +use crate::utils::mmx::*; +use anyhow::Result; +use std::io::Read; + +pub trait Hasher { + fn update(&mut self, data: &[u8]) -> Result<()>; + fn finalize(&mut self) -> Result; +} + +pub trait Encryption: std::fmt::Debug { + fn is_unicode(&self) -> bool { + false + } + fn compute_hash(&self, _data: &[u8]) -> Result { + Ok(0) + } + fn create_hash(&self) -> Result> { + Err(anyhow::anyhow!("Hasher not implemented")) + } + fn decrypt_name(&self, name: &mut [u8], hash: i32, encoding: Encoding) -> Result; + fn decrypt_entry<'a>( + &self, + stream: Box, + entry: &QlieEntry, + ) -> Result>; +} + +pub fn create_encryption(major: u8, minor: u8) -> Result> { + match (major, minor) { + (3, 1) => Ok(Box::new(Encryption31::new())), + _ => Err(anyhow::anyhow!( + "Unsupported encryption version: {}.{}", + major, + minor + )), + } +} + +pub fn decompress<'a>(data: Box) -> Result> { + Ok(Box::new(Decompressor::new(data)?)) +} + +pub fn decrypt(data: &mut [u8], key: u32) -> Result<()> { + let length = data.len(); + if length < 8 { + // Nothing to decrypt + 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 d = data.peek_u64()?; + v5 = mmx_p_add_d(v5, V7) ^ v9; + v9 = d ^ v5; + data.write_u64(v9)?; + } + Ok(()) +} + +pub fn get_common_key(data: &[u8]) -> Result> { + let mut reader = MemReaderRef::new(data); + let mut key = vec![0u8; 0x400]; + let mut writer = MemWriterRef::new(&mut key); + for i in 0..0x100i32 { + let temp = if (i % 3) != 0 { + (i + 7) * -(i + 3) + } else { + (i + 7) * (i + 3) + }; + writer.write_u32_at(i as u64 * 4, temp as u32)?; + } + let mut v1 = (reader.peek_u8_at(49)? % 0x49) as i32 + 0x80; + let v2 = (reader.peek_u8_at(79)? % 7) as i32 + 7; + let data_len = data.len() as i32; + for i in 0..0x400 { + v1 = (v1.wrapping_add(v2)) % data_len; + key[i] ^= reader.peek_u8_at(v1 as u64)?; + } + // crate::utils::files::write_file("./testscripts/test.bin")?.write_all(&key)?; + Ok(key) +} + +#[derive(Debug)] +pub struct Encryption31 {} + +impl Encryption31 { + pub fn new() -> Self { + Self {} + } + + fn create_table(length: usize, mut value: u32, is_v1: bool) -> Result> { + let mut table = Vec::with_capacity(length); + let key: u32 = if is_v1 { 0x8DF21431 } else { 0x8A77F473 }; + for _ in 0..length { + let t = (key as u64).wrapping_mul((value as u64) ^ (key as u64)); + value = ((t >> 32) + t) as u32; + table.push(value); + } + let mut mem = MemWriter::with_capacity(length * 4); + for i in table { + mem.write_u32(i)?; + } + Ok(mem.into_inner()) + } +} + +impl Encryption for Encryption31 { + fn is_unicode(&self) -> bool { + true + } + fn compute_hash(&self, data: &[u8]) -> Result { + let mut hasher = Encryption31Hasher::new(); + hasher.update(data)?; + Ok(hasher.finalize()?) + } + fn create_hash(&self) -> Result> { + Ok(Box::new(Encryption31Hasher::new())) + } + fn decrypt_name(&self, name: &mut [u8], hash: i32, _encoding: Encoding) -> Result { + if name.len() % 2 != 0 { + return Err(anyhow::anyhow!( + "Invalid name length for Unicode decryption" + )); + } + 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(decode_to_string(Encoding::Utf16LE, &name, true)?) + } + fn decrypt_entry<'a>( + &self, + stream: Box, + entry: &QlieEntry, + ) -> Result> { + match entry.is_encrypted { + // No encryption + 0 => Ok(Box::new(stream)), + 1 => Ok(Box::new(Encryption31DecryptV1::new( + stream, + entry.size, + entry.name.clone(), + entry.key, + )?)), + 2 => Ok(Box::new(Encryption31DecryptV2::new( + stream, + entry.size, + entry.name.clone(), + entry.key, + entry + .common_key + .clone() + .ok_or(anyhow::anyhow!("Missing common key"))?, + )?)), + _ => Err(anyhow::anyhow!( + "Unsupported encryption flag: {}", + entry.is_encrypted + )), + } + } +} + +pub struct Encryption31Hasher { + hash: u64, + key: u64, + buffer: [u8; 8], + buffer_len: usize, +} + +impl Encryption31Hasher { + 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(0xA35793A7); + self.hash = mmx_p_add_w(self.hash, C); + let temp = mmx_p_add_w(self.key, self.hash ^ data); + self.key = mmx_p_sll_d(temp, 3) | mmx_p_srl_d(temp, 0x1d); + } +} + +impl Hasher for Encryption31Hasher { + 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 p1 = ((self.key as i16) as i32).wrapping_mul(((self.key >> 32) as i16) as i32); + let p2 = (((self.key >> 16) as i16) as i32).wrapping_mul(((self.key >> 48) as i16) as i32); + Ok((p1.wrapping_add(p2)) as u32) + } +} + +#[derive(Debug)] +struct Encryption31DecryptV1<'a> { + stream: Box, + table: MemReader, + v4: u32, + v6: u64, +} + +impl<'a> Encryption31DecryptV1<'a> { + pub fn new( + stream: Box, + 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(AlignedReader::new(inner)) + } +} + +impl<'a> Read for Encryption31DecryptV1<'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 d = writer.peek_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; + writer.write_u64(v8)?; + self.v6 = mmx_p_add_w(mmx_p_sll_d(mmx_p_add_b(v7, v8) ^ v8, 1), v8); + self.v4 = (self.v4 + 8) & 0x7F; + } + Ok(readed) + } +} + +#[derive(Debug)] +struct Encryption31DecryptV2<'a> { + stream: Box, + table: MemReader, + v4: u32, + v6: u64, + common_key: MemReader, +} + +impl<'a> Encryption31DecryptV2<'a> { + pub fn new( + stream: Box, + 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(AlignedReader::new(inner)) + } +} + +impl<'a> Read for Encryption31DecryptV2<'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 d = writer.peek_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; + writer.write_u64(v8)?; + self.v6 = mmx_p_add_w(mmx_p_sll_d(mmx_p_add_b(v7, v8) ^ v8, 1), v8); + self.v4 = (self.v4 + 1) & 0x7F; + } + Ok(readed) + } +} + +#[derive(Debug)] +pub struct Decompressor<'a> { + stream: Box, + is_16bit: bool, + temp: Vec, + buf: Vec, + buf_pos: usize, +} + +impl<'a> Decompressor<'a> { + pub fn new(mut stream: Box) -> Result { + let sign = stream.read_u32()?; + if sign != 0xFF435031 { + return Err(anyhow::anyhow!("Invalid compression signature")); + } + let is_16bit = stream.read_u32()? & 1 != 0; + let _unpacked_size = stream.read_u32()?; + let temp = vec![0u8; 0x1000]; + Ok(Self { + stream, + is_16bit, + temp, + buf: Vec::new(), + buf_pos: 0, + }) + } + + fn next_block(&mut self) -> Result<()> { + self.buf.clear(); + self.buf_pos = 0; + let mut buf = [0u8; 1]; + let readed = self.stream.read(&mut buf)?; + if readed == 0 { + return Ok(()); + } + let mut buf_used = false; + let mut table = [[0u8; 2]; 0x100]; + let mut i = 0u32; + while i < 0x100 { + let mut c = if !buf_used { + buf_used = true; + buf[0] as u32 + } else { + self.stream.read_u8()? as u32 + }; + if c > 127 { + c -= 127; + while c > 0 { + table[i as usize][0] = i as u8; + c -= 1; + i += 1; + } + } + c += 1; + while c > 0 && i < 0x100 { + table[i as usize][0] = self.stream.read_u8()?; + if i as u8 != table[i as usize][0] { + table[i as usize][1] = self.stream.read_u8()?; + } + c -= 1; + i += 1; + } + } + let mut block_size = if self.is_16bit { + self.stream.read_u16()? as usize + } else { + self.stream.read_u32()? as usize + }; + let mut temp_length = 0usize; + while block_size > 0 || temp_length > 0 { + let c = if temp_length > 0 { + temp_length -= 1; + self.temp[temp_length] + } else { + block_size -= 1; + self.stream.read_u8()? + }; + if c == table[c as usize][0] { + self.buf.push(c); + } else { + self.temp[temp_length] = table[c as usize][1]; + temp_length += 1; + self.temp[temp_length] = table[c as usize][0]; + temp_length += 1; + } + } + Ok(()) + } +} + +impl<'a> Read for Decompressor<'a> { + fn read(&mut self, buf: &mut [u8]) -> std::io::Result { + let mut used = 0; + while used < buf.len() { + if self.buf_pos >= self.buf.len() { + self.next_block() + .map_err(|e| std::io::Error::new(std::io::ErrorKind::Other, e))?; + if self.buf.is_empty() { + break; + } + } + let to_copy = (self.buf.len() - self.buf_pos).min(buf.len() - used); + buf[used..used + to_copy] + .copy_from_slice(&self.buf[self.buf_pos..self.buf_pos + to_copy]); + self.buf_pos += to_copy; + used += to_copy; + } + Ok(used) + } +} diff --git a/src/scripts/qlie/archive/pack/mod.rs b/src/scripts/qlie/archive/pack/mod.rs new file mode 100644 index 0000000..b964c35 --- /dev/null +++ b/src/scripts/qlie/archive/pack/mod.rs @@ -0,0 +1,295 @@ +//! Qlie Pack Archive (.pack) +mod encryption; +mod twister; +mod types; + +use crate::ext::io::*; +use crate::scripts::base::*; +use crate::types::*; +use crate::utils::struct_pack::*; +use anyhow::Result; +use encryption::Encryption; +use std::io::{Read, Seek, SeekFrom}; +use std::sync::{Arc, Mutex}; +use types::*; + +#[derive(Debug)] +pub struct QliePackArchiveBuilder {} + +impl QliePackArchiveBuilder { + pub fn new() -> Self { + Self {} + } +} + +impl ScriptBuilder for QliePackArchiveBuilder { + fn default_encoding(&self) -> Encoding { + Encoding::Cp932 + } + + fn default_archive_encoding(&self) -> Option { + Some(Encoding::Cp932) + } + + fn build_script( + &self, + data: Vec, + _filename: &str, + _encoding: Encoding, + archive_encoding: Encoding, + config: &ExtraConfig, + _archive: Option<&Box>, + ) -> Result> { + Ok(Box::new(QliePackArchive::new( + MemReader::new(data), + archive_encoding, + config, + )?)) + } + + fn build_script_from_file( + &self, + filename: &str, + _encoding: Encoding, + archive_encoding: Encoding, + config: &ExtraConfig, + _archive: Option<&Box>, + ) -> Result> { + if filename == "-" { + let data = crate::utils::files::read_file(filename)?; + Ok(Box::new(QliePackArchive::new( + MemReader::new(data), + archive_encoding, + config, + )?)) + } else { + let f = std::fs::File::open(filename)?; + let reader = std::io::BufReader::new(f); + Ok(Box::new(QliePackArchive::new( + reader, + archive_encoding, + config, + )?)) + } + } + + fn build_script_from_reader( + &self, + reader: Box, + _filename: &str, + _encoding: Encoding, + archive_encoding: Encoding, + config: &ExtraConfig, + _archive: Option<&Box>, + ) -> Result> { + Ok(Box::new(QliePackArchive::new( + reader, + archive_encoding, + config, + )?)) + } + + fn extensions(&self) -> &'static [&'static str] { + &["pack"] + } + + fn script_type(&self) -> &'static ScriptType { + &ScriptType::QliePack + } + + fn is_this_format(&self, filename: &str, _buf: &[u8], _buf_len: usize) -> Option { + // Workround: Check only if filename exists in filesystem. + // This means that we cannot detect .pack files in another archive. + // Pack file header is at the end of file, so we cannot check signature here with header buffer. + match is_this_format(filename) { + Ok(true) => Some(30), + _ => None, + } + } + + fn is_archive(&self) -> bool { + true + } +} + +/// Check if the given file is Qlie Pack Archive format +pub fn is_this_format + ?Sized>(path: &P) -> Result { + let path = path.as_ref(); + if !path.exists() || !path.is_file() { + return Ok(false); + } + let mut file = std::fs::File::open(path)?; + file.seek(SeekFrom::End(-0x1C))?; + let header = QlieHeader::unpack(&mut file, false, Encoding::Utf8, &None)?; + Ok(header.is_valid()) +} + +#[derive(Debug)] +pub struct QliePackArchive { + header: QlieHeader, + encryption: Box, + reader: Arc>, + qkey: Option, + entries: Vec, + common_key: Option>, +} + +impl QliePackArchive { + pub fn new(mut reader: T, archive_encoding: Encoding, _config: &ExtraConfig) -> Result { + reader.seek(SeekFrom::End(-0x1C))?; + let header = QlieHeader::unpack(&mut reader, false, archive_encoding, &None)?; + if !header.is_valid() { + return Err(anyhow::anyhow!("Invalid Qlie Pack Archive header")); + } + let file_size = reader.stream_position()?; + if header.index_offset > file_size - 0x1C { + return Err(anyhow::anyhow!( + "Invalid index offset in Qlie Pack Archive header" + )); + } + let major = header.major_version(); + let minor = header.minor_version(); + let encryption = encryption::create_encryption(major, minor)?; + // Read key + let mut key = 0; + let mut qkey = None; + if major >= 2 { + reader.seek(SeekFrom::End(-0x440))?; + let mut qk = QlieKey::unpack(&mut reader, false, archive_encoding, &None)?; + if qk.hash_size as u64 > file_size || qk.hash_size < 0x44 { + return Err(anyhow::anyhow!("Invalid Qlie Pack Archive key")); + } + if major >= 3 { + key = encryption.compute_hash(&qk.key[..0x100])? & 0xFFFFFFF; + } + encryption::decrypt(&mut qk.signature, key)?; + if &qk.signature != b"8hr48uky,8ugi8ewra4g8d5vbf5hb5s6" { + eprintln!( + "WARNING: Invalid Qlie Pack Archive key signature, decryption key may be incorrect" + ); + crate::COUNTER.inc_warning(); + } + 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 { + name, + offset, + size, + unpacked_size, + is_packed, + is_encrypted, + hash, + key, + common_key: None, + }; + 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 == "pack_keyfile_kfueheish15538fa9or.key") + { + 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)?; + if common_key_entry.is_packed != 0 { + decrypted = encryption::decompress(decrypted)?; + } + let mut key_data = Vec::new(); + decrypted.read_to_end(&mut key_data)?; + common_key = Some(encryption::get_common_key(&key_data)?); + } + } + Ok(Self { + header, + encryption, + reader: Arc::new(Mutex::new(reader)), + qkey, + entries, + common_key, + }) + } +} + +impl Script for QliePackArchive { + fn default_output_script_type(&self) -> OutputScriptType { + OutputScriptType::Json + } + + fn default_format_type(&self) -> FormatOptions { + FormatOptions::None + } + + fn is_archive(&self) -> bool { + true + } + + fn iter_archive_filename<'a>( + &'a self, + ) -> Result> + 'a>> { + Ok(Box::new(self.entries.iter().map(|e| Ok(e.name.clone())))) + } + + fn open_file<'a>(&'a self, index: usize) -> Result> { + let mut entry = self + .entries + .get(index) + .ok_or_else(|| anyhow::anyhow!("Invalid file index {} for Qlie Pack Archive", index))? + .clone(); + if self.common_key.is_some() { + entry.common_key = self.common_key.clone(); + } + let stream = StreamRegion::with_size( + MutexWrapper::new(self.reader.clone(), entry.offset), + entry.size as u64, + )?; + let mut stream = self.encryption.decrypt_entry(Box::new(stream), &entry)?; + if entry.is_packed != 0 { + stream = encryption::decompress(stream)?; + } + Ok(Box::new(QliePackArchiveContent::new(stream, entry))) + } +} + +#[derive(Debug)] +struct QliePackArchiveContent { + reader: T, + entry: QlieEntry, +} + +impl QliePackArchiveContent { + pub fn new(reader: T, entry: QlieEntry) -> Self { + Self { reader, entry } + } +} + +impl Read for QliePackArchiveContent { + fn read(&mut self, buf: &mut [u8]) -> std::io::Result { + self.reader.read(buf) + } +} + +impl ArchiveContent for QliePackArchiveContent { + fn name(&self) -> &str { + &self.entry.name + } +} diff --git a/src/scripts/qlie/archive/pack/twister.rs b/src/scripts/qlie/archive/pack/twister.rs new file mode 100644 index 0000000..217f20b --- /dev/null +++ b/src/scripts/qlie/archive/pack/twister.rs @@ -0,0 +1,86 @@ +const DEFAULT_SEED: u32 = 5489; +const STATE_LENGTH: usize = 64; +const STATE_M: usize = 39; +const MATRIX_A: u32 = 0x9908B0DF; +const SIGN_MASK: u32 = 0x80000000; +const LOWER_MASK: u32 = 0x7FFFFFFF; +const TEMPERING_MASK_B: u32 = 0x9C4F88E3; +const TEMPERING_MASK_C: u32 = 0xE7F70000; + +pub struct MersenneTwister { + mt: [u32; STATE_LENGTH], + mti: usize, +} + +impl MersenneTwister { + pub fn new(seed: u32) -> Self { + let mut twister = Self { + mt: [0; STATE_LENGTH], + mti: STATE_LENGTH, + }; + twister.s_rand(seed); + twister + } + + pub fn s_rand(&mut self, seed: u32) { + self.mt[0] = seed; + for i in 1..STATE_LENGTH { + self.mt[i] = (0x6611BC19u32.wrapping_mul(self.mt[i - 1] ^ (self.mt[i - 1] >> 30))) + .wrapping_add(i as u32); + } + } + + pub fn xor_state(&mut self, hash: &[u8]) { + let length = (hash.len() / 4).min(STATE_LENGTH); + if length == 0 { + return; + } + for i in 0..length { + let part = u32::from_le_bytes([ + hash[i * 4], + hash[i * 4 + 1], + hash[i * 4 + 2], + hash[i * 4 + 3], + ]); + self.mt[i] ^= part; + } + } + + pub fn rand(&mut self) -> u32 { + const MAG01: [u32; 2] = [0, MATRIX_A]; + + if self.mti >= STATE_LENGTH { + for kk in 0..(STATE_LENGTH - STATE_M) { + let y = (self.mt[kk] & SIGN_MASK) | (self.mt[kk + 1] & LOWER_MASK) >> 1; + self.mt[kk] = self.mt[kk + STATE_M] ^ y ^ MAG01[(self.mt[kk + 1] & 1) as usize]; + } + for kk in (STATE_LENGTH - STATE_M)..(STATE_LENGTH - 1) { + let y = (self.mt[kk] & SIGN_MASK) | (self.mt[kk + 1] & LOWER_MASK) >> 1; + self.mt[kk] = self.mt[kk - (STATE_LENGTH - STATE_M)] + ^ y + ^ MAG01[(self.mt[kk + 1] & 1) as usize]; + } + let y = (self.mt[STATE_LENGTH - 1] & SIGN_MASK) | (self.mt[0] & LOWER_MASK) >> 1; + self.mt[STATE_LENGTH - 1] = + self.mt[STATE_M - 1] ^ y ^ MAG01[(self.mt[STATE_LENGTH - 2] & 1) as usize]; + + self.mti = 0; + } + + let mut y = self.mt[self.mti]; + self.mti += 1; + + y ^= y >> 11; + y ^= (y << 7) & TEMPERING_MASK_B; + y ^= (y << 15) & TEMPERING_MASK_C; + y ^= y >> 18; + + y + } + + pub fn rand64(&mut self) -> u64 { + let low = self.rand() as u64; + let high = self.rand() as u64; + (high << 32) | low + } +} diff --git a/src/scripts/qlie/archive/pack/types.rs b/src/scripts/qlie/archive/pack/types.rs new file mode 100644 index 0000000..8b68a90 --- /dev/null +++ b/src/scripts/qlie/archive/pack/types.rs @@ -0,0 +1,95 @@ +use crate::ext::io::*; +use crate::types::*; +use crate::utils::struct_pack::*; +use anyhow::Result; +use msg_tool_macro::{StructPack, StructUnpack}; +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"; + +/// HashVer 1.2 +#[derive(StructPack, StructUnpack, Debug, Clone)] +pub struct QlieHash12 { + pub signature: [u8; 16], + /// Always 0x200 + pub const_: u32, + pub file_count: u32, + pub index_size: u32, + #[pvec(u32)] + pub hash_data: Vec, +} + +/// HashVer 1.3 +#[derive(StructPack, StructUnpack, Debug, Clone)] +pub struct QlieHash13 { + pub signature: [u8; 16], + /// Always 0x100 + pub const_: u32, + pub file_count: u32, + pub index_size: u32, + #[pvec(u32)] + pub hash_data: Vec, +} + +/// HashVer 1.4 +#[derive(StructPack, StructUnpack, Debug, Clone)] +pub struct QlieHash14 { + pub signature: [u8; 16], + /// Always 0x100 + pub const_: u32, + pub file_count: u32, + pub index_size: u32, + pub hash_data_size: u32, + pub is_compressed: u32, + pub unk: [u8; 32], + #[unpack_vec_len(hash_data_size)] + #[pack_vec_len(self.hash_data_size)] + pub hash_data: Vec, +} + +#[derive(StructPack, StructUnpack, Debug, Clone)] +pub struct QlieHeader { + pub signature: [u8; 16], + pub file_count: u32, + pub index_offset: u64, +} + +impl QlieHeader { + pub fn is_valid(&self) -> bool { + self.signature.starts_with(b"FilePackVer") + && self.signature[12] == b'.' + && &self.signature[14..] == b"\x00\x00" + && self.signature[11].is_ascii_digit() + && self.signature[13].is_ascii_digit() + } + + pub fn major_version(&self) -> u8 { + self.signature[11] - b'0' + } + + pub fn minor_version(&self) -> u8 { + self.signature[13] - b'0' + } +} + +#[derive(StructPack, StructUnpack, Debug, Clone)] +pub struct QlieKey { + pub signature: [u8; 32], + pub hash_size: u32, + pub key: [u8; 0x400], +} + +#[derive(Debug, Clone)] +pub struct QlieEntry { + pub name: String, + pub offset: u64, + pub size: u32, + pub unpacked_size: u32, + pub is_packed: u32, + pub is_encrypted: u32, + pub hash: u32, + pub key: u32, + pub common_key: Option>, +} diff --git a/src/scripts/qlie/mod.rs b/src/scripts/qlie/mod.rs index 8d54297..1c1d586 100644 --- a/src/scripts/qlie/mod.rs +++ b/src/scripts/qlie/mod.rs @@ -1,2 +1,4 @@ //! Qlie Engine script module +#[cfg(feature = "qlie-arc")] +pub mod archive; pub mod script; diff --git a/src/types.rs b/src/types.rs index 3fc6090..28809b4 100644 --- a/src/types.rs +++ b/src/types.rs @@ -778,6 +778,9 @@ pub enum ScriptType { #[cfg(feature = "qlie")] /// Qlie Engine Scenario script (.s) Qlie, + #[cfg(feature = "qlie-arc")] + /// Qlie Pack Archive (.pack) + QliePack, #[cfg(feature = "silky")] /// Silky Engine Mes script Silky, diff --git a/src/utils/mmx.rs b/src/utils/mmx.rs new file mode 100644 index 0000000..110cbd8 --- /dev/null +++ b/src/utils/mmx.rs @@ -0,0 +1,74 @@ +//! MMX Operations in pure Rust + +/// Packed Add of Bytes (8-bit integers) +pub const fn mmx_p_add_b(a: u64, b: u64) -> u64 { + let mut mask = 0xFF; + let mut r = ((a & mask).wrapping_add(b & mask)) & mask; // First byte + mask <<= 8; + r |= ((a & mask).wrapping_add(b & mask)) & mask; // Second byte + mask <<= 8; + r |= ((a & mask).wrapping_add(b & mask)) & mask; // Third byte + mask <<= 8; + r |= ((a & mask).wrapping_add(b & mask)) & mask; // Fourth byte + mask <<= 8; + r |= ((a & mask).wrapping_add(b & mask)) & mask; // Fifth byte + mask <<= 8; + r |= ((a & mask).wrapping_add(b & mask)) & mask; // Sixth byte + mask <<= 8; + r |= ((a & mask).wrapping_add(b & mask)) & mask; // Seventh byte + mask <<= 8; + r |= ((a & mask).wrapping_add(b & mask)) & mask; // Eighth byte + r +} + +/// Packed Add of Doublewords (32-bit integers) +pub const fn mmx_p_add_d(a: u64, b: u64) -> u64 { + let mut mask = 0xffffffff; + let r = ((a & mask).wrapping_add(b & mask)) & mask; + mask <<= 32; + r | ((a & mask).wrapping_add(b & mask)) & mask +} + +/// Packed Add of Words (16-bit integers) +pub const fn mmx_p_add_w(a: u64, b: u64) -> u64 { + let mut mask = 0xFFFF; + let mut r = ((a & mask).wrapping_add(b & mask)) & mask; + mask <<= 16; + r |= ((a & mask).wrapping_add(b & mask)) & mask; + mask <<= 16; + r |= ((a & mask).wrapping_add(b & mask)) & mask; + mask <<= 16; + r |= ((a & mask).wrapping_add(b & mask)) & mask; + r +} + +/// Packed Shift Left Logical of Doublewords (32-bit integers) +pub const fn mmx_p_sll_d(a: u64, mut count: u32) -> u64 { + count &= 0x1F; + let mut mask = (0xFFFFFFFFu32 << count) as u64; + mask |= mask << 32; + (a << count) & mask +} + +/// Packed Shift Right Logical of Doublewords (32-bit integers) +pub const fn mmx_p_srl_d(a: u64, mut count: u32) -> u64 { + count &= 0x1F; + let mut mask = (0xFFFFFFFFu32 >> count) as u64; + mask |= mask << 32; + (a >> count) & mask +} + +/// Packed Unpack Low Doubleword +pub const fn mmx_punpckldq(a: u64, b: u64) -> u64 { + (a & 0xFFFFFFFF) | (b & 0xFFFFFFFF) << 32 +} + +/// Packed Unpack Low Doubleword with itself +pub const fn mmx_punpckldq2(a: u64) -> u64 { + mmx_punpckldq(a, a) +} + +#[test] +fn test_mmx() { + assert_eq!(mmx_punpckldq(0x1, 0x2), 0x200000001); +} diff --git a/src/utils/mod.rs b/src/utils/mod.rs index ce7afd9..3183b27 100644 --- a/src/utils/mod.rs +++ b/src/utils/mod.rs @@ -21,6 +21,8 @@ pub mod jxl; #[cfg(feature = "lossless-audio")] pub mod lossless_audio; mod macros; +#[cfg(feature = "utils-mmx")] +pub mod mmx; pub mod name_replacement; pub mod num_range; #[cfg(feature = "utils-pcm")]