diff --git a/Cargo.toml b/Cargo.toml index 63d145d..43472ec 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -12,8 +12,9 @@ edition = "2021" [workspace.dependencies] aes = "0.8.2" base64 = "0.21.0" -hex = "0.4.3" strum = { version = "0.24", features = ["derive"] } +sha1 = "0.10" +hex = "0.4" # generated by 'cargo dist init' [profile.dist] diff --git a/oodle_loader/Cargo.toml b/oodle_loader/Cargo.toml new file mode 100644 index 0000000..188882d --- /dev/null +++ b/oodle_loader/Cargo.toml @@ -0,0 +1,23 @@ +[package] +name = "oodle_loader" +repository.workspace = true +authors.workspace = true +license.workspace = true +version.workspace = true +edition.workspace = true + +[target.'cfg(windows)'.dependencies] +libloading = "0.7" + +[target.'cfg(unix)'.dependencies] +object = { version = "0.32.1", default-features = false, features = ["std", "read"] } +libc = "0.2.148" +seq-macro = "0.3.5" + +[dependencies] +sha1 = { workspace = true } +ureq = "2.6" +hex-literal = "0.4" +hex = { workspace = true } +anyhow = "1.0.75" +lzma-rs = "0.3.0" diff --git a/oodle_loader/section.bin b/oodle_loader/section.bin new file mode 100644 index 0000000..af4e9eb Binary files /dev/null and b/oodle_loader/section.bin differ diff --git a/oodle_loader/src/lib.rs b/oodle_loader/src/lib.rs new file mode 100644 index 0000000..314848e --- /dev/null +++ b/oodle_loader/src/lib.rs @@ -0,0 +1,407 @@ +use anyhow::{anyhow, Context, Result}; + +use std::sync::OnceLock; + +type OodleDecompress = fn(comp_buf: &[u8], raw_buf: &mut [u8]) -> i32; + +#[allow(non_camel_case_types)] +type OodleLZ_Decompress = unsafe extern "win64" fn( + compBuf: *const u8, + compBufSize: usize, + rawBuf: *mut u8, + rawLen: usize, + fuzzSafe: u32, + checkCRC: u32, + verbosity: u32, + decBufBase: u64, + decBufSize: usize, + fpCallback: u64, + callbackUserData: u64, + decoderMemory: *mut u8, + decoderMemorySize: usize, + threadPhase: u32, +) -> i32; + +pub fn decompress() -> Result> { + #[cfg(windows)] + return Ok(windows_oodle::decompress_wrapper_windows); + #[cfg(unix)] + return Ok(linux_oodle::oodle_loader_linux()); +} + +fn call_decompress(comp_buf: &[u8], raw_buf: &mut [u8], decompress: OodleLZ_Decompress) -> i32 { + unsafe { + decompress( + comp_buf.as_ptr(), + comp_buf.len(), + raw_buf.as_mut_ptr(), + raw_buf.len(), + 1, + 1, + 0, + 0, + 0, + 0, + 0, + std::ptr::null_mut(), + 0, + 3, + ) + } +} + +static OODLE_HASH: [u8; 20] = hex_literal::hex!("4bcc73614cb8fd2b0bce8d0f91ee5f3202d9d624"); +static OODLE_DLL_NAME: &str = "oo2core_9_win64.dll"; + +fn fetch_oodle() -> Result { + use sha1::{Digest, Sha1}; + + let oodle_path = std::env::current_exe()?.with_file_name(OODLE_DLL_NAME); + + if !oodle_path.exists() { + let mut compressed = vec![]; + ureq::get("https://origin.warframe.com/origin/50F7040A/index.txt.lzma") + .call()? + .into_reader() + .read_to_end(&mut compressed)?; + + let mut decompressed = vec![]; + lzma_rs::lzma_decompress(&mut std::io::Cursor::new(compressed), &mut decompressed).unwrap(); + let index = String::from_utf8(decompressed)?; + let line = index + .lines() + .find(|l| l.contains(OODLE_DLL_NAME)) + .with_context(|| format!("{OODLE_DLL_NAME} not found in index"))?; + let path = line.split_once(',').context("failed to parse index")?.0; + + let mut compressed = vec![]; + ureq::get(&format!("https://content.warframe.com{path}")) + .call()? + .into_reader() + .read_to_end(&mut compressed)?; + + let mut decompressed = vec![]; + lzma_rs::lzma_decompress(&mut std::io::Cursor::new(compressed), &mut decompressed).unwrap(); + + std::fs::write(&oodle_path, decompressed)?; + } + + let mut hasher = Sha1::new(); + hasher.update(std::fs::read(&oodle_path)?); + let hash = hasher.finalize(); + (hash[..] == OODLE_HASH).then_some(()).ok_or_else(|| { + anyhow!( + "oodle hash mismatch expected: {} got: {} ", + hex::encode(OODLE_HASH), + hex::encode(hash) + ) + })?; + + Ok(oodle_path) +} + +#[cfg(windows)] +mod windows_oodle { + use super::*; + + use anyhow::Context; + + static DECOMPRESS: OnceLock<(OodleLZ_Decompress, libloading::Library)> = OnceLock::new(); + + pub fn decompress_wrapper_windows(comp_buf: &[u8], raw_buf: &mut [u8]) -> i32 { + let decompress = DECOMPRESS.get_or_init(|| { + let path = fetch_oodle().context("failed to fetch oodle").unwrap(); + + let lib = unsafe { libloading::Library::new(path) } + .context("failed to load oodle") + .unwrap(); + + (*unsafe { lib.get(b"OodleLZ_Decompress") }.unwrap(), lib) + }); + call_decompress(comp_buf, raw_buf, decompress.0) + } +} + +#[cfg(unix)] +mod linux_oodle { + use super::*; + + use anyhow::Result; + use object::pe::{ + ImageNtHeaders64, IMAGE_REL_BASED_DIR64, IMAGE_SCN_MEM_EXECUTE, IMAGE_SCN_MEM_READ, + IMAGE_SCN_MEM_WRITE, + }; + use object::read::pe::{ImageOptionalHeader, ImageThunkData, PeFile64}; + + use object::{LittleEndian as LE, Object, ObjectSection}; + use std::collections::HashMap; + use std::ffi::{c_void, CStr}; + + #[repr(C)] + struct ThreadInformationBlock { + exception_list: *const c_void, + stack_base: *const c_void, + stack_limit: *const c_void, + sub_system_tib: *const c_void, + fiber_data: *const c_void, + arbitrary_user_pointer: *const c_void, + teb: *const c_void, + } + + const TIB: ThreadInformationBlock = ThreadInformationBlock { + exception_list: std::ptr::null(), + stack_base: std::ptr::null(), + stack_limit: std::ptr::null(), + sub_system_tib: std::ptr::null(), + fiber_data: std::ptr::null(), + arbitrary_user_pointer: std::ptr::null(), + teb: std::ptr::null(), + }; + + static DECOMPRESS: OnceLock = OnceLock::new(); + + fn decompress_wrapper(comp_buf: &[u8], raw_buf: &mut [u8]) -> i32 { + unsafe { + // Set GS register in calling thread + const ARCH_SET_GS: i32 = 0x1001; + libc::syscall(libc::SYS_arch_prctl, ARCH_SET_GS, &TIB); + + // Call actual decompress function + call_decompress(comp_buf, raw_buf, *DECOMPRESS.get().unwrap()) + } + } + + #[allow(non_snake_case)] + mod imports { + use super::*; + + pub unsafe extern "win64" fn OutputDebugStringA(string: *const std::ffi::c_char) { + print!("[OODLE] {}", CStr::from_ptr(string).to_string_lossy()); + } + pub unsafe extern "win64" fn GetProcessHeap() -> *const c_void { + 0x12345678 as *const c_void + } + pub unsafe extern "win64" fn HeapAlloc( + _heap: *const c_void, + flags: i32, + size: usize, + ) -> *const c_void { + assert_eq!(0, flags); + libc::malloc(size) + } + pub unsafe extern "win64" fn HeapFree( + _heap: *const c_void, + _flags: i32, + ptr: *mut c_void, + ) -> bool { + libc::free(ptr); + true + } + pub unsafe extern "win64" fn memset( + ptr: *mut c_void, + value: i32, + num: usize, + ) -> *const c_void { + libc::memset(ptr, value, num) + } + pub unsafe extern "win64" fn memmove( + destination: *mut c_void, + source: *const c_void, + num: usize, + ) -> *const c_void { + libc::memmove(destination, source, num) + } + pub unsafe extern "win64" fn memcpy( + destination: *mut c_void, + source: *const c_void, + num: usize, + ) -> *const c_void { + libc::memcpy(destination, source, num) + } + } + + // Create some unique function pointers to use for unimplemented imports + const DEBUG_FNS: [*const fn(); 100] = gen_debug_fns(); + static mut DEBUG_NAMES: [&str; 100] = [""; 100]; + const fn gen_debug_fns() -> [*const fn(); 100] { + fn log() { + unimplemented!("import {:?}", unsafe { DEBUG_NAMES[I] }); + } + let mut array = [std::ptr::null(); 100]; + seq_macro::seq!(N in 0..100 { + array[N] = log:: as *const fn(); + }); + array + } + + pub fn oodle_loader_linux() -> OodleDecompress { + DECOMPRESS.get_or_init(|| get_decompress_inner().unwrap()); + decompress_wrapper + } + + fn get_decompress_inner() -> Result { + fetch_oodle()?; + let oodle = std::env::current_exe() + .unwrap() + .with_file_name(OODLE_DLL_NAME); + let dll = std::fs::read(oodle)?; + + let obj_file = PeFile64::parse(&*dll)?; + + let size = obj_file.nt_headers().optional_header.size_of_image() as usize; + let header_size = obj_file.nt_headers().optional_header.size_of_headers() as usize; + + let image_base = obj_file.relative_address_base() as usize; + + // Create map + let mmap = unsafe { + std::slice::from_raw_parts_mut( + libc::mmap( + std::ptr::null_mut(), + size, + libc::PROT_READ | libc::PROT_WRITE, + libc::MAP_PRIVATE | libc::MAP_ANONYMOUS, + -1, + 0, + ) as *mut u8, + size, + ) + }; + + let map_base = mmap.as_ptr(); + + // Copy header to map + mmap[0..header_size].copy_from_slice(&dll[0..header_size]); + unsafe { + assert_eq!( + 0, + libc::mprotect( + mmap.as_mut_ptr() as *mut c_void, + header_size, + libc::PROT_READ + ) + ); + } + + // Copy section data to map + for section in obj_file.sections() { + let address = section.address() as usize; + let data = section.data()?; + mmap[(address - image_base)..(address - image_base + data.len())] + .copy_from_slice(section.data()?); + } + + // Apply relocations + let sections = obj_file.section_table(); + let mut blocks = obj_file + .data_directories() + .relocation_blocks(&*dll, §ions)? + .unwrap(); + + while let Some(block) = blocks.next()? { + let block_address = block.virtual_address(); + let block_data = sections.pe_data_at(&*dll, block_address).map(object::Bytes); + for reloc in block { + let offset = (reloc.virtual_address - block_address) as usize; + match reloc.typ { + IMAGE_REL_BASED_DIR64 => { + let addend = block_data + .and_then(|data| data.read_at::>(offset).ok()) + .map(|addend| addend.get(LE)); + if let Some(addend) = addend { + mmap[reloc.virtual_address as usize + ..reloc.virtual_address as usize + 8] + .copy_from_slice(&u64::to_le_bytes( + addend - image_base as u64 + map_base as u64, + )); + } + } + _ => unimplemented!(), + } + } + } + + // Fix up imports + let import_table = obj_file.import_table()?.unwrap(); + let mut import_descs = import_table.descriptors()?; + + let mut i = 0; + while let Some(import_desc) = import_descs.next()? { + let mut thunks = import_table.thunks(import_desc.original_first_thunk.get(LE))?; + + let mut address = import_desc.first_thunk.get(LE) as usize; + while let Some(thunk) = thunks.next::()? { + let (_hint, name) = import_table.hint_name(thunk.address())?; + let name = String::from_utf8_lossy(name).to_string(); + + use imports::*; + + let fn_addr = match name.as_str() { + "OutputDebugStringA" => OutputDebugStringA as usize, + "GetProcessHeap" => GetProcessHeap as usize, + "HeapAlloc" => HeapAlloc as usize, + "HeapFree" => HeapFree as usize, + "memset" => memset as usize, + "memcpy" => memcpy as usize, + "memmove" => memmove as usize, + _ => { + unsafe { DEBUG_NAMES[i] = name.leak() } + let a = DEBUG_FNS[i] as usize; + i += 1; + a + } + }; + + mmap[address..address + 8].copy_from_slice(&usize::to_le_bytes(fn_addr)); + + address += 8; + } + } + + // Build export table + let mut exports = HashMap::new(); + for export in obj_file.exports()? { + let name = String::from_utf8_lossy(export.name()); + let address = export.address() - image_base as u64 + map_base as u64; + exports.insert(name, address as *const c_void); + } + + // Fix section permissions + for section in obj_file.sections() { + let address = section.address() as usize; + let data = section.data()?; + let size = data.len(); + + let mut permissions = 0; + + let flags = match section.flags() { + object::SectionFlags::Coff { characteristics } => characteristics, + _ => unreachable!(), + }; + + if 0 != flags & IMAGE_SCN_MEM_READ { + permissions |= libc::PROT_READ; + } + if 0 != flags & IMAGE_SCN_MEM_WRITE { + permissions |= libc::PROT_WRITE; + } + if 0 != flags & IMAGE_SCN_MEM_EXECUTE { + permissions |= libc::PROT_EXEC; + } + + unsafe { + assert_eq!( + 0, + libc::mprotect( + mmap.as_mut_ptr().add(address - image_base) as *mut c_void, + size, + permissions + ) + ); + } + } + + // Break things! + Ok(unsafe { std::mem::transmute(exports["OodleLZ_Decompress"]) }) + } +} diff --git a/repak/Cargo.toml b/repak/Cargo.toml index 332e236..38fce10 100644 --- a/repak/Cargo.toml +++ b/repak/Cargo.toml @@ -9,26 +9,20 @@ edition.workspace = true [features] default = ["compression", "encryption"] compression = ["dep:flate2", "dep:zstd"] -oodle = ["dep:libloading", "dep:ureq", "dep:once_cell", "dep:hex-literal", "dep:hex"] +oodle = [] +oodle_loader = ["dep:oodle_loader"] encryption = ["dep:aes"] [dependencies] byteorder = "1.4" -aes = { version = "0.8", optional = true } +aes = { workspace = true, optional = true } flate2 = { version = "1.0", optional = true } zstd = { version = "0.12", optional = true } +oodle_loader = { path = "../oodle_loader", optional = true} thiserror = "1.0" -sha1 = "0.10.5" +sha1 = { workspace = true } strum = { workspace = true } -libloading = { version = "0.7", optional = true } -ureq = { version = "2.6", optional = true } -once_cell = { version = "1.17", optional = true} -hex-literal = { version = "0.4", optional = true } -hex = { workspace = true, optional = true } [dev-dependencies] base64 = { workspace = true } paste = "1.0.11" - -[package.metadata.cargo-all-features] -denylist = ["oodle"] diff --git a/repak/src/entry.rs b/repak/src/entry.rs index 00b3dfb..04e47f4 100644 --- a/repak/src/entry.rs +++ b/repak/src/entry.rs @@ -306,6 +306,7 @@ impl Entry { version: Version, compression: &[Compression], #[allow(unused)] key: &super::Key, + #[allow(unused)] oodle: &super::Oodle, buf: &mut W, ) -> Result<(), super::Error> { reader.seek(io::SeekFrom::Start(self.offset))?; @@ -376,110 +377,39 @@ impl Entry { } #[cfg(feature = "oodle")] Some(Compression::Oodle) => { - #[cfg(not(target_os = "windows"))] - return Err(super::Error::Oodle); + let oodle = match oodle { + crate::Oodle::Some(getter) => getter().map_err(|_| super::Error::OodleFailed), + crate::Oodle::None => Err(super::Error::OodleFailed), + }?; + let mut decompressed = vec![0; self.uncompressed as usize]; - #[cfg(target_os = "windows")] - unsafe { - use std::ops::Deref; - let lib = match OODLE.deref() { - Ok(lib) => Ok(lib), - Err(e) => Err(super::Error::Other(e.to_string())), - }?; - - /* - let set_printf: libloading::Symbol< - unsafe extern "C" fn( - unsafe extern "C" fn( - i32, - *const std::ffi::c_char, - i32, - *const std::ffi::c_char, - ... - ) -> std::ffi::c_int, - ), - > = lib.get(b"OodleCore_Plugins_SetPrintf").unwrap(); - - pub unsafe extern "C" fn printf( - a: i32, - b: *const std::ffi::c_char, - c: i32, - str: *const std::ffi::c_char, - mut args: ... - ) -> std::ffi::c_int { - use printf_compat::{format, output}; - let mut s = String::new(); - let bytes_written = format(str, args.as_va_list(), output::fmt_write(&mut s)); - print!("[OODLE]: {}", s); - bytes_written - } - - set_printf(printf); - */ - - #[allow(non_snake_case)] - #[allow(clippy::type_complexity)] - let OodleLZ_Decompress: libloading::Symbol< - extern "C" fn( - compBuf: *mut u8, - compBufSize: usize, - rawBuf: *mut u8, - rawLen: usize, - fuzzSafe: u32, - checkCRC: u32, - verbosity: u32, - decBufBase: u64, - decBufSize: usize, - fpCallback: u64, - callbackUserData: u64, - decoderMemory: *mut u8, - decoderMemorySize: usize, - threadPhase: u32, - ) -> i32, - > = lib.get(b"OodleLZ_Decompress").unwrap(); - - let mut decompressed = vec![0; self.uncompressed as usize]; - - let mut compress_offset = 0; - let mut decompress_offset = 0; - let block_count = ranges.len(); - for range in ranges { - let decomp = if block_count == 1 { - self.uncompressed as usize - } else { - (self.compression_block_size as usize) - .min(self.uncompressed as usize - compress_offset) - }; - let buffer = &mut data[range]; - let out = OodleLZ_Decompress( - buffer.as_mut_ptr(), - buffer.len(), - decompressed.as_mut_ptr().offset(decompress_offset), - decomp, - 1, - 1, - 0, //verbose 3 - 0, - 0, - 0, - 0, - std::ptr::null_mut(), - 0, - 3, - ); - if out == 0 { - return Err(super::Error::DecompressionFailed(Compression::Oodle)); - } - compress_offset += self.compression_block_size as usize; - decompress_offset += out as isize; - } - - assert_eq!( - decompress_offset, self.uncompressed as isize, - "Oodle decompression length mismatch" + let mut compress_offset = 0; + let mut decompress_offset = 0; + let block_count = ranges.len(); + for range in ranges { + let decomp = if block_count == 1 { + self.uncompressed as usize + } else { + (self.compression_block_size as usize) + .min(self.uncompressed as usize - compress_offset) + }; + let buffer = &mut data[range]; + let out = oodle( + buffer, + &mut decompressed[decompress_offset..decompress_offset + decomp], ); - buf.write_all(&decompressed)?; + if out == 0 { + return Err(super::Error::DecompressionFailed(Compression::Oodle)); + } + compress_offset += self.compression_block_size as usize; + decompress_offset += out as usize; } + + debug_assert_eq!( + decompress_offset, self.uncompressed as usize, + "Oodle decompression length mismatch" + ); + buf.write_all(&decompressed)?; } #[cfg(not(feature = "oodle"))] Some(Compression::Oodle) => return Err(super::Error::Oodle), @@ -491,42 +421,6 @@ impl Entry { } } -#[cfg(feature = "oodle")] -use once_cell::sync::Lazy; -#[cfg(feature = "oodle")] -static OODLE: Lazy> = - Lazy::new(|| get_oodle().map_err(|e| e.to_string())); -#[cfg(feature = "oodle")] -static OODLE_HASH: [u8; 20] = hex_literal::hex!("4bcc73614cb8fd2b0bce8d0f91ee5f3202d9d624"); - -#[cfg(feature = "oodle")] -fn get_oodle() -> Result { - use sha1::{Digest, Sha1}; - - let oodle = std::env::current_exe()?.with_file_name("oo2core_9_win64.dll"); - if !oodle.exists() { - let mut data = vec![]; - ureq::get("https://cdn.discordapp.com/attachments/817251677086285848/992648087371792404/oo2core_9_win64.dll") - .call().map_err(Box::new)? - .into_reader().read_to_end(&mut data)?; - - std::fs::write(&oodle, data)?; - } - - let mut hasher = Sha1::new(); - hasher.update(std::fs::read(&oodle)?); - let hash = hasher.finalize(); - (hash[..] == OODLE_HASH).then_some(()).ok_or_else(|| { - super::Error::Other(format!( - "oodle hash mismatch expected: {} got: {} ", - hex::encode(OODLE_HASH), - hex::encode(hash) - )) - })?; - - unsafe { libloading::Library::new(oodle) }.map_err(|_| super::Error::OodleFailed) -} - mod test { #[test] fn test_entry() { diff --git a/repak/src/error.rs b/repak/src/error.rs index 29a7642..f023917 100644 --- a/repak/src/error.rs +++ b/repak/src/error.rs @@ -16,14 +16,7 @@ pub enum Error { #[error("enable the encryption feature to read encrypted paks")] Encryption, - #[cfg_attr( - windows, - error("enable the oodle feature to read Oodle compressed paks") - )] - #[cfg_attr( - not(windows), - error("Oodle compression only supported on Windows (or WINE)") - )] + #[error("enable the oodle feature to read Oodle compressed paks")] Oodle, // std errors @@ -39,10 +32,6 @@ pub enum Error { #[error("utf16 conversion: {0}")] Utf16(#[from] std::string::FromUtf16Error), - #[cfg(feature = "oodle")] - #[error("ureq error: {0}")] - Ureq(#[from] Box), // boxed because ureq::Error is quite large - #[error("bufwriter dereference: {0}")] IntoInner(#[from] std::io::IntoInnerError>>), @@ -53,7 +42,7 @@ pub enum Error { #[error("found magic of {0:#x} instead of {:#x}", super::MAGIC)] Magic(u32), - #[error("Could not load oo2core_9_win64.dll")] + #[error("pointer to OodleLZ_Decompress was not provided")] OodleFailed, #[error("No entry found at {0}")] diff --git a/repak/src/lib.rs b/repak/src/lib.rs index e6ea5a1..159c06e 100644 --- a/repak/src/lib.rs +++ b/repak/src/lib.rs @@ -7,11 +7,17 @@ mod pak; pub use {error::*, pak::*}; -#[cfg(all(feature = "oodle", not(target_os = "windows")))] -compile_error!("Oodle compression only supported on Windows (or WINE)"); - pub const MAGIC: u32 = 0x5A6F12E1; +#[cfg(feature = "oodle")] +mod oodle { + pub type OodleGetter = fn() -> Result>; + pub type OodleDecompress = fn(comp_buf: &[u8], raw_buf: &mut [u8]) -> i32; +} + +#[cfg(feature = "oodle_loader")] +pub use oodle_loader; + #[derive( Clone, Copy, @@ -124,10 +130,11 @@ pub enum Compression { } #[allow(clippy::large_enum_variant)] -#[derive(Debug)] +#[derive(Debug, Default)] pub(crate) enum Key { #[cfg(feature = "encryption")] Some(aes::Aes256), + #[default] None, } @@ -137,3 +144,11 @@ impl From for Key { Self::Some(value) } } + +#[derive(Debug, Default)] +pub(crate) enum Oodle { + #[cfg(feature = "oodle")] + Some(oodle::OodleGetter), + #[default] + None, +} diff --git a/repak/src/pak.rs b/repak/src/pak.rs index 0452a84..dac7b85 100644 --- a/repak/src/pak.rs +++ b/repak/src/pak.rs @@ -4,10 +4,52 @@ use byteorder::{ReadBytesExt, WriteBytesExt, LE}; use std::collections::BTreeMap; use std::io::{self, Read, Seek, Write}; +#[derive(Debug, Default)] +pub struct PakBuilder { + key: super::Key, + oodle: super::Oodle, +} + +impl PakBuilder { + pub fn new() -> Self { + Self::default() + } + #[cfg(feature = "encryption")] + pub fn key(mut self, key: aes::Aes256) -> Self { + self.key = super::Key::Some(key); + self + } + #[cfg(feature = "oodle")] + pub fn oodle(mut self, oodle_getter: super::oodle::OodleGetter) -> Self { + self.oodle = super::Oodle::Some(oodle_getter); + self + } + pub fn reader(self, reader: &mut R) -> Result { + PakReader::new_any_inner(reader, self.key, self.oodle) + } + pub fn reader_with_version( + self, + reader: &mut R, + version: super::Version, + ) -> Result { + PakReader::new_inner(reader, version, self.key, self.oodle) + } + pub fn writer( + self, + writer: W, + version: super::Version, + mount_point: String, + path_hash_seed: Option, + ) -> PakWriter { + PakWriter::new_inner(writer, self.key, version, mount_point, path_hash_seed) + } +} + #[derive(Debug)] pub struct PakReader { pak: Pak, key: super::Key, + oodle: super::Oodle, } #[derive(Debug)] @@ -83,79 +125,30 @@ fn decrypt(key: &super::Key, bytes: &mut [u8]) -> Result<(), super::Error> { } impl PakReader { - pub fn new_any(reader: &mut R) -> Result { - Self::new_any_inner(reader, super::Key::None) - } - - #[cfg(feature = "encryption")] - pub fn new_any_with_key( - reader: &mut R, - key: aes::Aes256, - ) -> Result { - Self::new_any_inner(reader, key.into()) - } - - #[cfg(feature = "encryption")] - pub fn new_any_with_optional_key( - reader: &mut R, - key: Option, - ) -> Result { - match key { - Some(key) => Self::new_any_with_key(reader, key), - None => Self::new_any(reader), - } - } - fn new_any_inner( reader: &mut R, key: super::Key, + oodle: super::Oodle, ) -> Result { use std::fmt::Write; let mut log = "\n".to_owned(); for ver in Version::iter() { match Pak::read(&mut *reader, ver, &key) { - Ok(pak) => return Ok(Self { pak, key }), + Ok(pak) => return Ok(Self { pak, key, oodle }), Err(err) => writeln!(log, "trying version {} failed: {}", ver, err)?, } } Err(super::Error::UnsupportedOrEncrypted(log)) } - pub fn new( - reader: &mut R, - version: super::Version, - ) -> Result { - Self::new_inner(reader, version, super::Key::None) - } - - #[cfg(feature = "encryption")] - pub fn new_with_key( - reader: &mut R, - version: super::Version, - key: aes::Aes256, - ) -> Result { - Self::new_inner(reader, version, key.into()) - } - - #[cfg(feature = "encryption")] - pub fn new_with_optional_key( - reader: &mut R, - version: super::Version, - key: Option, - ) -> Result { - match key { - Some(key) => Self::new_with_key(reader, version, key), - None => Self::new(reader, version), - } - } - fn new_inner( reader: &mut R, version: super::Version, key: super::Key, + oodle: super::Oodle, ) -> Result { - Pak::read(reader, version, &key).map(|pak| Self { pak, key }) + Pak::read(reader, version, &key).map(|pak| Self { pak, key, oodle }) } pub fn version(&self) -> super::Version { @@ -192,6 +185,7 @@ impl PakReader { self.pak.version, &self.pak.compression, &self.key, + &self.oodle, writer, ), None => Err(super::Error::MissingEntry(path.to_owned())), @@ -216,48 +210,6 @@ impl PakReader { } impl PakWriter { - pub fn new( - writer: W, - version: Version, - mount_point: String, - path_hash_seed: Option, - ) -> Self { - PakWriter { - pak: Pak::new(version, mount_point, path_hash_seed), - writer, - key: super::Key::None, - } - } - - #[cfg(feature = "encryption")] - pub fn new_with_key( - writer: W, - key: aes::Aes256, - version: Version, - mount_point: String, - path_hash_seed: Option, - ) -> Self { - PakWriter { - pak: Pak::new(version, mount_point, path_hash_seed), - writer, - key: key.into(), - } - } - - #[cfg(feature = "encryption")] - pub fn new_with_optional_key( - writer: W, - key: Option, - version: Version, - mount_point: String, - path_hash_seed: Option, - ) -> Self { - match key { - Some(key) => Self::new_with_key(writer, key, version, mount_point, path_hash_seed), - None => Self::new(writer, version, mount_point, path_hash_seed), - } - } - fn new_inner( writer: W, key: super::Key, diff --git a/repak/tests/test.rs b/repak/tests/test.rs index ccbf90c..eaa4b53 100644 --- a/repak/tests/test.rs +++ b/repak/tests/test.rs @@ -97,7 +97,10 @@ fn test_read(version: repak::Version, _file_name: &str, bytes: &[u8]) { let len = inner_reader.seek(SeekFrom::End(0)).unwrap(); let mut reader = ReadCounter::new_size(inner_reader, len as usize); - let pak = repak::PakReader::new_any_with_key(&mut reader, key).unwrap(); + let pak = repak::PakBuilder::new() + .key(key) + .reader(&mut reader) + .unwrap(); assert_eq!(pak.mount_point(), "../mount/point/root/"); assert_eq!(pak.version(), version); @@ -160,10 +163,13 @@ fn test_write(_version: repak::Version, _file_name: &str, bytes: &[u8]) { .unwrap(); let mut reader = std::io::Cursor::new(bytes); - let pak_reader = repak::PakReader::new_any_with_key(&mut reader, key).unwrap(); + let pak_reader = repak::PakBuilder::new() + .key(key) + .reader(&mut reader) + .unwrap(); let writer = Cursor::new(vec![]); - let mut pak_writer = repak::PakWriter::new( + let mut pak_writer = repak::PakBuilder::new().writer( writer, pak_reader.version(), pak_reader.mount_point().to_owned(), @@ -189,7 +195,7 @@ fn test_rewrite_index(_version: repak::Version, _file_name: &str, bytes: &[u8]) .unwrap(); let mut buf = std::io::Cursor::new(bytes.to_vec()); - let pak_reader = repak::PakReader::new_any_with_key(&mut buf, key).unwrap(); + let pak_reader = repak::PakBuilder::new().key(key).reader(&mut buf).unwrap(); let rewrite = pak_reader .into_pakwriter(buf) diff --git a/repak_cli/Cargo.toml b/repak_cli/Cargo.toml index 5ff6f4f..a8171d9 100644 --- a/repak_cli/Cargo.toml +++ b/repak_cli/Cargo.toml @@ -10,12 +10,9 @@ edition.workspace = true name = "repak" path = "src/main.rs" -[target.'cfg(windows)'.dependencies] -repak = { path = "../repak", features = ["oodle"] } -[target.'cfg(not(windows))'.dependencies] -repak = { path = "../repak" } - [dependencies] +oodle_loader = { path = "../oodle_loader" } +repak = { path = "../repak", features = ["oodle"] } aes = { workspace = true } base64 = { workspace = true } clap = { version = "4.1.4", features = ["derive"] } diff --git a/repak_cli/src/main.rs b/repak_cli/src/main.rs index 1e1616b..43ef79e 100644 --- a/repak_cli/src/main.rs +++ b/repak_cli/src/main.rs @@ -175,10 +175,11 @@ fn main() -> Result<(), repak::Error> { } fn info(aes_key: Option, action: ActionInfo) -> Result<(), repak::Error> { - let pak = repak::PakReader::new_any_with_optional_key( - &mut BufReader::new(File::open(action.input)?), - aes_key, - )?; + let mut builder = repak::PakBuilder::new(); + if let Some(aes_key) = aes_key { + builder = builder.key(aes_key); + } + let pak = builder.reader(&mut BufReader::new(File::open(action.input)?))?; println!("mount point: {}", pak.mount_point()); println!("version: {}", pak.version()); println!("version major: {}", pak.version().version_major()); @@ -189,10 +190,11 @@ fn info(aes_key: Option, action: ActionInfo) -> Result<(), repak::E } fn list(aes_key: Option, action: ActionList) -> Result<(), repak::Error> { - let pak = repak::PakReader::new_any_with_optional_key( - &mut BufReader::new(File::open(action.input)?), - aes_key, - )?; + let mut builder = repak::PakBuilder::new(); + if let Some(aes_key) = aes_key { + builder = builder.key(aes_key); + } + let pak = builder.reader(&mut BufReader::new(File::open(action.input)?))?; let mount_point = PathBuf::from(pak.mount_point()); let prefix = Path::new(&action.strip_prefix); @@ -221,10 +223,11 @@ fn list(aes_key: Option, action: ActionList) -> Result<(), repak::E } fn hash_list(aes_key: Option, action: ActionHashList) -> Result<(), repak::Error> { - let pak = repak::PakReader::new_any_with_optional_key( - &mut BufReader::new(File::open(&action.input)?), - aes_key, - )?; + let mut builder = repak::PakBuilder::new(); + if let Some(aes_key) = aes_key { + builder = builder.key(aes_key); + } + let pak = builder.reader(&mut BufReader::new(File::open(&action.input)?))?; let mount_point = PathBuf::from(pak.mount_point()); let prefix = Path::new(&action.strip_prefix); @@ -248,7 +251,6 @@ fn hash_list(aes_key: Option, action: ActionHashList) -> Result<(), let hashes: std::sync::Arc, Vec>>> = Default::default(); - full_paths.par_iter().zip(stripped).try_for_each_init( || (hashes.clone(), File::open(&action.input)), |(hashes, file), ((_full_path, path), stripped)| -> Result<(), repak::Error> { @@ -280,10 +282,11 @@ const STYLE: &str = "[{elapsed_precise}] [{wide_bar}] {pos}/{len} ({eta})"; fn unpack(aes_key: Option, action: ActionUnpack) -> Result<(), repak::Error> { for input in &action.input { - let pak = repak::PakReader::new_any_with_optional_key( - &mut BufReader::new(File::open(input)?), - aes_key.clone(), - )?; + let mut builder = repak::PakBuilder::new().oodle(oodle_loader::decompress); + if let Some(aes_key) = aes_key.clone() { + builder = builder.key(aes_key); + } + let pak = builder.reader(&mut BufReader::new(File::open(input)?))?; let output = action .output .as_ref() @@ -354,6 +357,7 @@ fn unpack(aes_key: Option, action: ActionUnpack) -> Result<(), repa let progress = indicatif::ProgressBar::new(entries.len() as u64) .with_style(indicatif::ProgressStyle::with_template(STYLE).unwrap()); + entries.par_iter().try_for_each_init( || (progress.clone(), File::open(input)), |(progress, file), entry| -> Result<(), repak::Error> { @@ -415,9 +419,8 @@ fn pack(args: ActionPack) -> Result<(), repak::Error> { collect_files(&mut paths, input_path)?; paths.sort(); - let mut pak = repak::PakWriter::new_with_optional_key( + let mut pak = repak::PakBuilder::new().writer( BufWriter::new(File::create(&output)?), - None, args.version, args.mount_point, Some(args.path_hash_seed), @@ -450,7 +453,11 @@ fn pack(args: ActionPack) -> Result<(), repak::Error> { fn get(aes_key: Option, args: ActionGet) -> Result<(), repak::Error> { let mut reader = BufReader::new(File::open(&args.input)?); - let pak = repak::PakReader::new_any_with_optional_key(&mut reader, aes_key)?; + let mut builder = repak::PakBuilder::new().oodle(oodle_loader::decompress); + if let Some(aes_key) = aes_key { + builder = builder.key(aes_key); + } + let pak = builder.reader(&mut reader)?; let mount_point = PathBuf::from(pak.mount_point()); let prefix = Path::new(&args.strip_prefix);