Merge pull request #14 from bananaturtlesandwich/oodle

Split oodle loading into dedicated crate
This commit is contained in:
Truman Kilen 2023-12-16 18:51:45 -06:00 committed by GitHub
commit 9362db07e2
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
12 changed files with 575 additions and 290 deletions

View file

@ -12,8 +12,9 @@ edition = "2021"
[workspace.dependencies] [workspace.dependencies]
aes = "0.8.2" aes = "0.8.2"
base64 = "0.21.0" base64 = "0.21.0"
hex = "0.4.3"
strum = { version = "0.24", features = ["derive"] } strum = { version = "0.24", features = ["derive"] }
sha1 = "0.10"
hex = "0.4"
# generated by 'cargo dist init' # generated by 'cargo dist init'
[profile.dist] [profile.dist]

23
oodle_loader/Cargo.toml Normal file
View file

@ -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"

BIN
oodle_loader/section.bin Normal file

Binary file not shown.

407
oodle_loader/src/lib.rs Normal file
View file

@ -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<OodleDecompress, Box<dyn std::error::Error>> {
#[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<std::path::PathBuf> {
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<OodleLZ_Decompress> = 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<const I: usize>() {
unimplemented!("import {:?}", unsafe { DEBUG_NAMES[I] });
}
let mut array = [std::ptr::null(); 100];
seq_macro::seq!(N in 0..100 {
array[N] = log::<N> 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<OodleLZ_Decompress> {
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, &sections)?
.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::<object::U64Bytes<LE>>(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::<ImageNtHeaders64>()? {
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"]) })
}
}

View file

@ -9,26 +9,20 @@ edition.workspace = true
[features] [features]
default = ["compression", "encryption"] default = ["compression", "encryption"]
compression = ["dep:flate2", "dep:zstd"] 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"] encryption = ["dep:aes"]
[dependencies] [dependencies]
byteorder = "1.4" byteorder = "1.4"
aes = { version = "0.8", optional = true } aes = { workspace = true, optional = true }
flate2 = { version = "1.0", optional = true } flate2 = { version = "1.0", optional = true }
zstd = { version = "0.12", optional = true } zstd = { version = "0.12", optional = true }
oodle_loader = { path = "../oodle_loader", optional = true}
thiserror = "1.0" thiserror = "1.0"
sha1 = "0.10.5" sha1 = { workspace = true }
strum = { 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] [dev-dependencies]
base64 = { workspace = true } base64 = { workspace = true }
paste = "1.0.11" paste = "1.0.11"
[package.metadata.cargo-all-features]
denylist = ["oodle"]

View file

@ -306,6 +306,7 @@ impl Entry {
version: Version, version: Version,
compression: &[Compression], compression: &[Compression],
#[allow(unused)] key: &super::Key, #[allow(unused)] key: &super::Key,
#[allow(unused)] oodle: &super::Oodle,
buf: &mut W, buf: &mut W,
) -> Result<(), super::Error> { ) -> Result<(), super::Error> {
reader.seek(io::SeekFrom::Start(self.offset))?; reader.seek(io::SeekFrom::Start(self.offset))?;
@ -376,68 +377,10 @@ impl Entry {
} }
#[cfg(feature = "oodle")] #[cfg(feature = "oodle")]
Some(Compression::Oodle) => { Some(Compression::Oodle) => {
#[cfg(not(target_os = "windows"))] let oodle = match oodle {
return Err(super::Error::Oodle); crate::Oodle::Some(getter) => getter().map_err(|_| super::Error::OodleFailed),
crate::Oodle::None => Err(super::Error::OodleFailed),
#[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 decompressed = vec![0; self.uncompressed as usize];
let mut compress_offset = 0; let mut compress_offset = 0;
@ -451,36 +394,23 @@ impl Entry {
.min(self.uncompressed as usize - compress_offset) .min(self.uncompressed as usize - compress_offset)
}; };
let buffer = &mut data[range]; let buffer = &mut data[range];
let out = OodleLZ_Decompress( let out = oodle(
buffer.as_mut_ptr(), buffer,
buffer.len(), &mut decompressed[decompress_offset..decompress_offset + decomp],
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 { if out == 0 {
return Err(super::Error::DecompressionFailed(Compression::Oodle)); return Err(super::Error::DecompressionFailed(Compression::Oodle));
} }
compress_offset += self.compression_block_size as usize; compress_offset += self.compression_block_size as usize;
decompress_offset += out as isize; decompress_offset += out as usize;
} }
assert_eq!( debug_assert_eq!(
decompress_offset, self.uncompressed as isize, decompress_offset, self.uncompressed as usize,
"Oodle decompression length mismatch" "Oodle decompression length mismatch"
); );
buf.write_all(&decompressed)?; buf.write_all(&decompressed)?;
} }
}
#[cfg(not(feature = "oodle"))] #[cfg(not(feature = "oodle"))]
Some(Compression::Oodle) => return Err(super::Error::Oodle), Some(Compression::Oodle) => return Err(super::Error::Oodle),
#[cfg(not(feature = "compression"))] #[cfg(not(feature = "compression"))]
@ -491,42 +421,6 @@ impl Entry {
} }
} }
#[cfg(feature = "oodle")]
use once_cell::sync::Lazy;
#[cfg(feature = "oodle")]
static OODLE: Lazy<Result<libloading::Library, String>> =
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<libloading::Library, super::Error> {
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 { mod test {
#[test] #[test]
fn test_entry() { fn test_entry() {

View file

@ -16,14 +16,7 @@ pub enum Error {
#[error("enable the encryption feature to read encrypted paks")] #[error("enable the encryption feature to read encrypted paks")]
Encryption, Encryption,
#[cfg_attr( #[error("enable the oodle feature to read Oodle compressed paks")]
windows,
error("enable the oodle feature to read Oodle compressed paks")
)]
#[cfg_attr(
not(windows),
error("Oodle compression only supported on Windows (or WINE)")
)]
Oodle, Oodle,
// std errors // std errors
@ -39,10 +32,6 @@ pub enum Error {
#[error("utf16 conversion: {0}")] #[error("utf16 conversion: {0}")]
Utf16(#[from] std::string::FromUtf16Error), Utf16(#[from] std::string::FromUtf16Error),
#[cfg(feature = "oodle")]
#[error("ureq error: {0}")]
Ureq(#[from] Box<ureq::Error>), // boxed because ureq::Error is quite large
#[error("bufwriter dereference: {0}")] #[error("bufwriter dereference: {0}")]
IntoInner(#[from] std::io::IntoInnerError<std::io::BufWriter<Vec<u8>>>), IntoInner(#[from] std::io::IntoInnerError<std::io::BufWriter<Vec<u8>>>),
@ -53,7 +42,7 @@ pub enum Error {
#[error("found magic of {0:#x} instead of {:#x}", super::MAGIC)] #[error("found magic of {0:#x} instead of {:#x}", super::MAGIC)]
Magic(u32), Magic(u32),
#[error("Could not load oo2core_9_win64.dll")] #[error("pointer to OodleLZ_Decompress was not provided")]
OodleFailed, OodleFailed,
#[error("No entry found at {0}")] #[error("No entry found at {0}")]

View file

@ -7,11 +7,17 @@ mod pak;
pub use {error::*, 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; pub const MAGIC: u32 = 0x5A6F12E1;
#[cfg(feature = "oodle")]
mod oodle {
pub type OodleGetter = fn() -> Result<OodleDecompress, Box<dyn std::error::Error>>;
pub type OodleDecompress = fn(comp_buf: &[u8], raw_buf: &mut [u8]) -> i32;
}
#[cfg(feature = "oodle_loader")]
pub use oodle_loader;
#[derive( #[derive(
Clone, Clone,
Copy, Copy,
@ -124,10 +130,11 @@ pub enum Compression {
} }
#[allow(clippy::large_enum_variant)] #[allow(clippy::large_enum_variant)]
#[derive(Debug)] #[derive(Debug, Default)]
pub(crate) enum Key { pub(crate) enum Key {
#[cfg(feature = "encryption")] #[cfg(feature = "encryption")]
Some(aes::Aes256), Some(aes::Aes256),
#[default]
None, None,
} }
@ -137,3 +144,11 @@ impl From<aes::Aes256> for Key {
Self::Some(value) Self::Some(value)
} }
} }
#[derive(Debug, Default)]
pub(crate) enum Oodle {
#[cfg(feature = "oodle")]
Some(oodle::OodleGetter),
#[default]
None,
}

View file

@ -4,10 +4,52 @@ use byteorder::{ReadBytesExt, WriteBytesExt, LE};
use std::collections::BTreeMap; use std::collections::BTreeMap;
use std::io::{self, Read, Seek, Write}; 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<R: Read + Seek>(self, reader: &mut R) -> Result<PakReader, super::Error> {
PakReader::new_any_inner(reader, self.key, self.oodle)
}
pub fn reader_with_version<R: Read + Seek>(
self,
reader: &mut R,
version: super::Version,
) -> Result<PakReader, super::Error> {
PakReader::new_inner(reader, version, self.key, self.oodle)
}
pub fn writer<W: Write + Seek>(
self,
writer: W,
version: super::Version,
mount_point: String,
path_hash_seed: Option<u64>,
) -> PakWriter<W> {
PakWriter::new_inner(writer, self.key, version, mount_point, path_hash_seed)
}
}
#[derive(Debug)] #[derive(Debug)]
pub struct PakReader { pub struct PakReader {
pak: Pak, pak: Pak,
key: super::Key, key: super::Key,
oodle: super::Oodle,
} }
#[derive(Debug)] #[derive(Debug)]
@ -83,79 +125,30 @@ fn decrypt(key: &super::Key, bytes: &mut [u8]) -> Result<(), super::Error> {
} }
impl PakReader { impl PakReader {
pub fn new_any<R: Read + Seek>(reader: &mut R) -> Result<Self, super::Error> {
Self::new_any_inner(reader, super::Key::None)
}
#[cfg(feature = "encryption")]
pub fn new_any_with_key<R: Read + Seek>(
reader: &mut R,
key: aes::Aes256,
) -> Result<Self, super::Error> {
Self::new_any_inner(reader, key.into())
}
#[cfg(feature = "encryption")]
pub fn new_any_with_optional_key<R: Read + Seek>(
reader: &mut R,
key: Option<aes::Aes256>,
) -> Result<Self, super::Error> {
match key {
Some(key) => Self::new_any_with_key(reader, key),
None => Self::new_any(reader),
}
}
fn new_any_inner<R: Read + Seek>( fn new_any_inner<R: Read + Seek>(
reader: &mut R, reader: &mut R,
key: super::Key, key: super::Key,
oodle: super::Oodle,
) -> Result<Self, super::Error> { ) -> Result<Self, super::Error> {
use std::fmt::Write; use std::fmt::Write;
let mut log = "\n".to_owned(); let mut log = "\n".to_owned();
for ver in Version::iter() { for ver in Version::iter() {
match Pak::read(&mut *reader, ver, &key) { 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(err) => writeln!(log, "trying version {} failed: {}", ver, err)?,
} }
} }
Err(super::Error::UnsupportedOrEncrypted(log)) Err(super::Error::UnsupportedOrEncrypted(log))
} }
pub fn new<R: Read + Seek>(
reader: &mut R,
version: super::Version,
) -> Result<Self, super::Error> {
Self::new_inner(reader, version, super::Key::None)
}
#[cfg(feature = "encryption")]
pub fn new_with_key<R: Read + Seek>(
reader: &mut R,
version: super::Version,
key: aes::Aes256,
) -> Result<Self, super::Error> {
Self::new_inner(reader, version, key.into())
}
#[cfg(feature = "encryption")]
pub fn new_with_optional_key<R: Read + Seek>(
reader: &mut R,
version: super::Version,
key: Option<aes::Aes256>,
) -> Result<Self, super::Error> {
match key {
Some(key) => Self::new_with_key(reader, version, key),
None => Self::new(reader, version),
}
}
fn new_inner<R: Read + Seek>( fn new_inner<R: Read + Seek>(
reader: &mut R, reader: &mut R,
version: super::Version, version: super::Version,
key: super::Key, key: super::Key,
oodle: super::Oodle,
) -> Result<Self, super::Error> { ) -> Result<Self, super::Error> {
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 { pub fn version(&self) -> super::Version {
@ -192,6 +185,7 @@ impl PakReader {
self.pak.version, self.pak.version,
&self.pak.compression, &self.pak.compression,
&self.key, &self.key,
&self.oodle,
writer, writer,
), ),
None => Err(super::Error::MissingEntry(path.to_owned())), None => Err(super::Error::MissingEntry(path.to_owned())),
@ -216,48 +210,6 @@ impl PakReader {
} }
impl<W: Write + Seek> PakWriter<W> { impl<W: Write + Seek> PakWriter<W> {
pub fn new(
writer: W,
version: Version,
mount_point: String,
path_hash_seed: Option<u64>,
) -> 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<u64>,
) -> 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<aes::Aes256>,
version: Version,
mount_point: String,
path_hash_seed: Option<u64>,
) -> 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( fn new_inner(
writer: W, writer: W,
key: super::Key, key: super::Key,

View file

@ -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 len = inner_reader.seek(SeekFrom::End(0)).unwrap();
let mut reader = ReadCounter::new_size(inner_reader, len as usize); 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.mount_point(), "../mount/point/root/");
assert_eq!(pak.version(), version); assert_eq!(pak.version(), version);
@ -160,10 +163,13 @@ fn test_write(_version: repak::Version, _file_name: &str, bytes: &[u8]) {
.unwrap(); .unwrap();
let mut reader = std::io::Cursor::new(bytes); 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 writer = Cursor::new(vec![]);
let mut pak_writer = repak::PakWriter::new( let mut pak_writer = repak::PakBuilder::new().writer(
writer, writer,
pak_reader.version(), pak_reader.version(),
pak_reader.mount_point().to_owned(), pak_reader.mount_point().to_owned(),
@ -189,7 +195,7 @@ fn test_rewrite_index(_version: repak::Version, _file_name: &str, bytes: &[u8])
.unwrap(); .unwrap();
let mut buf = std::io::Cursor::new(bytes.to_vec()); 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 let rewrite = pak_reader
.into_pakwriter(buf) .into_pakwriter(buf)

View file

@ -10,12 +10,9 @@ edition.workspace = true
name = "repak" name = "repak"
path = "src/main.rs" path = "src/main.rs"
[target.'cfg(windows)'.dependencies]
repak = { path = "../repak", features = ["oodle"] }
[target.'cfg(not(windows))'.dependencies]
repak = { path = "../repak" }
[dependencies] [dependencies]
oodle_loader = { path = "../oodle_loader" }
repak = { path = "../repak", features = ["oodle"] }
aes = { workspace = true } aes = { workspace = true }
base64 = { workspace = true } base64 = { workspace = true }
clap = { version = "4.1.4", features = ["derive"] } clap = { version = "4.1.4", features = ["derive"] }

View file

@ -175,10 +175,11 @@ fn main() -> Result<(), repak::Error> {
} }
fn info(aes_key: Option<aes::Aes256>, action: ActionInfo) -> Result<(), repak::Error> { fn info(aes_key: Option<aes::Aes256>, action: ActionInfo) -> Result<(), repak::Error> {
let pak = repak::PakReader::new_any_with_optional_key( let mut builder = repak::PakBuilder::new();
&mut BufReader::new(File::open(action.input)?), if let Some(aes_key) = 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!("mount point: {}", pak.mount_point());
println!("version: {}", pak.version()); println!("version: {}", pak.version());
println!("version major: {}", pak.version().version_major()); println!("version major: {}", pak.version().version_major());
@ -189,10 +190,11 @@ fn info(aes_key: Option<aes::Aes256>, action: ActionInfo) -> Result<(), repak::E
} }
fn list(aes_key: Option<aes::Aes256>, action: ActionList) -> Result<(), repak::Error> { fn list(aes_key: Option<aes::Aes256>, action: ActionList) -> Result<(), repak::Error> {
let pak = repak::PakReader::new_any_with_optional_key( let mut builder = repak::PakBuilder::new();
&mut BufReader::new(File::open(action.input)?), if let Some(aes_key) = 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 mount_point = PathBuf::from(pak.mount_point());
let prefix = Path::new(&action.strip_prefix); let prefix = Path::new(&action.strip_prefix);
@ -221,10 +223,11 @@ fn list(aes_key: Option<aes::Aes256>, action: ActionList) -> Result<(), repak::E
} }
fn hash_list(aes_key: Option<aes::Aes256>, action: ActionHashList) -> Result<(), repak::Error> { fn hash_list(aes_key: Option<aes::Aes256>, action: ActionHashList) -> Result<(), repak::Error> {
let pak = repak::PakReader::new_any_with_optional_key( let mut builder = repak::PakBuilder::new();
&mut BufReader::new(File::open(&action.input)?), if let Some(aes_key) = 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 mount_point = PathBuf::from(pak.mount_point());
let prefix = Path::new(&action.strip_prefix); let prefix = Path::new(&action.strip_prefix);
@ -248,7 +251,6 @@ fn hash_list(aes_key: Option<aes::Aes256>, action: ActionHashList) -> Result<(),
let hashes: std::sync::Arc<std::sync::Mutex<BTreeMap<std::borrow::Cow<'_, str>, Vec<u8>>>> = let hashes: std::sync::Arc<std::sync::Mutex<BTreeMap<std::borrow::Cow<'_, str>, Vec<u8>>>> =
Default::default(); Default::default();
full_paths.par_iter().zip(stripped).try_for_each_init( full_paths.par_iter().zip(stripped).try_for_each_init(
|| (hashes.clone(), File::open(&action.input)), || (hashes.clone(), File::open(&action.input)),
|(hashes, file), ((_full_path, path), stripped)| -> Result<(), repak::Error> { |(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<aes::Aes256>, action: ActionUnpack) -> Result<(), repak::Error> { fn unpack(aes_key: Option<aes::Aes256>, action: ActionUnpack) -> Result<(), repak::Error> {
for input in &action.input { for input in &action.input {
let pak = repak::PakReader::new_any_with_optional_key( let mut builder = repak::PakBuilder::new().oodle(oodle_loader::decompress);
&mut BufReader::new(File::open(input)?), if let Some(aes_key) = aes_key.clone() {
aes_key.clone(), builder = builder.key(aes_key);
)?; }
let pak = builder.reader(&mut BufReader::new(File::open(input)?))?;
let output = action let output = action
.output .output
.as_ref() .as_ref()
@ -354,6 +357,7 @@ fn unpack(aes_key: Option<aes::Aes256>, action: ActionUnpack) -> Result<(), repa
let progress = indicatif::ProgressBar::new(entries.len() as u64) let progress = indicatif::ProgressBar::new(entries.len() as u64)
.with_style(indicatif::ProgressStyle::with_template(STYLE).unwrap()); .with_style(indicatif::ProgressStyle::with_template(STYLE).unwrap());
entries.par_iter().try_for_each_init( entries.par_iter().try_for_each_init(
|| (progress.clone(), File::open(input)), || (progress.clone(), File::open(input)),
|(progress, file), entry| -> Result<(), repak::Error> { |(progress, file), entry| -> Result<(), repak::Error> {
@ -415,9 +419,8 @@ fn pack(args: ActionPack) -> Result<(), repak::Error> {
collect_files(&mut paths, input_path)?; collect_files(&mut paths, input_path)?;
paths.sort(); paths.sort();
let mut pak = repak::PakWriter::new_with_optional_key( let mut pak = repak::PakBuilder::new().writer(
BufWriter::new(File::create(&output)?), BufWriter::new(File::create(&output)?),
None,
args.version, args.version,
args.mount_point, args.mount_point,
Some(args.path_hash_seed), Some(args.path_hash_seed),
@ -450,7 +453,11 @@ fn pack(args: ActionPack) -> Result<(), repak::Error> {
fn get(aes_key: Option<aes::Aes256>, args: ActionGet) -> Result<(), repak::Error> { fn get(aes_key: Option<aes::Aes256>, args: ActionGet) -> Result<(), repak::Error> {
let mut reader = BufReader::new(File::open(&args.input)?); 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 mount_point = PathBuf::from(pak.mount_point());
let prefix = Path::new(&args.strip_prefix); let prefix = Path::new(&args.strip_prefix);