From f2c2255d532b5bbe0d5ecb4b411561d7cfce33df Mon Sep 17 00:00:00 2001 From: spuds <71292624+bananaturtlesandwich@users.noreply.github.com> Date: Wed, 16 Aug 2023 22:13:03 +0100 Subject: [PATCH 1/8] add compression and oodle features --- repak/Cargo.toml | 19 ++++++++++++------- repak/src/entry.rs | 16 ++++++++++++++-- repak/src/error.rs | 13 +++++++++---- repak/src/lib.rs | 3 +++ repak/src/pak.rs | 2 +- repak_cli/Cargo.toml | 2 +- 6 files changed, 40 insertions(+), 15 deletions(-) diff --git a/repak/Cargo.toml b/repak/Cargo.toml index 532a01e..5c31a19 100644 --- a/repak/Cargo.toml +++ b/repak/Cargo.toml @@ -6,19 +6,24 @@ license.workspace = true version.workspace = true edition.workspace = true +[features] +default = ["compression"] +compression = ["dep:flate2", "dep:zstd"] +oodle = ["dep:libloading", "dep:ureq", "dep:once_cell", "dep:hex-literal", "dep:hex"] + [dependencies] byteorder = "1.4" aes = "0.8" -flate2 = "1.0" +flate2 = { version = "1.0", optional = true } +zstd = { version = "0.12", optional = true } thiserror = "1.0" sha1 = "0.10.5" strum = { workspace = true } -libloading = "0.7.4" -ureq = "2.6.2" -hex-literal = "0.4.1" -hex = { workspace = true } -once_cell = "1.17.1" -zstd = "0.12.3" +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 } diff --git a/repak/src/entry.rs b/repak/src/entry.rs index df1a8fc..71b5af2 100644 --- a/repak/src/entry.rs +++ b/repak/src/entry.rs @@ -350,6 +350,7 @@ impl Entry { None => vec![0..data.len()], }; + #[cfg(feature = "compression")] macro_rules! decompress { ($decompressor: ty) => { for range in ranges { @@ -359,14 +360,18 @@ impl Entry { } match self.compression.map(|c| compression[c as usize]) { - None => buf.write_all(&data)?, + None | Some(Compression::None) => buf.write_all(&data)?, + #[cfg(feature = "compression")] Some(Compression::Zlib) => decompress!(flate2::read::ZlibDecoder<&[u8]>), + #[cfg(feature = "compression")] Some(Compression::Gzip) => decompress!(flate2::read::GzDecoder<&[u8]>), + #[cfg(feature = "compression")] Some(Compression::Zstd) => { for range in ranges { io::copy(&mut zstd::stream::read::Decoder::new(&data[range])?, buf)?; } } + #[cfg(feature = "oodle")] Some(Compression::Oodle) => { #[cfg(not(target_os = "windows"))] return Err(super::Error::Oodle); @@ -472,18 +477,25 @@ impl Entry { buf.write_all(&decompressed)?; } } - _ => todo!(), + #[cfg(not(feature = "oodle"))] + Some(Compression::Oodle) => return Err(super::Error::Oodle), + #[cfg(not(feature = "compression"))] + _ => return Err(super::Error::Compression), } buf.flush()?; Ok(()) } } +#[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}; diff --git a/repak/src/error.rs b/repak/src/error.rs index affffae..a961fad 100644 --- a/repak/src/error.rs +++ b/repak/src/error.rs @@ -9,6 +9,13 @@ pub enum Error { #[error("expect 256 bit AES key as base64 or hex string")] Aes, + // feature errors + #[error("enable the compression feature to read compressed paks")] + Compression, + + #[error("enable the oodle feature to read oodle paks")] + Oodle, + // std errors #[error("io error: {0}")] Io(#[from] std::io::Error), @@ -22,6 +29,7 @@ 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 @@ -35,9 +43,6 @@ pub enum Error { #[error("found magic of {0:#x} instead of {:#x}", super::MAGIC)] Magic(u32), - #[error("Oodle compression only supported on Windows (or WINE)")] - Oodle, - #[error("Could not load oo2core_9_win64.dll")] OodleFailed, @@ -72,7 +77,7 @@ pub enum Error { OsString(std::ffi::OsString), #[error("{0}version unsupported or is encrypted (possibly missing --aes-key?)")] - UnsuportedOrEncrypted(String), + UnsupportedOrEncrypted(String), #[error("{0}")] Other(String), diff --git a/repak/src/lib.rs b/repak/src/lib.rs index 5b64447..e66c4e0 100644 --- a/repak/src/lib.rs +++ b/repak/src/lib.rs @@ -7,6 +7,9 @@ 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; #[derive( diff --git a/repak/src/pak.rs b/repak/src/pak.rs index 7b4a738..deaaec2 100644 --- a/repak/src/pak.rs +++ b/repak/src/pak.rs @@ -96,7 +96,7 @@ impl PakReader { Err(err) => writeln!(log, "trying version {} failed: {}", ver, err)?, } } - Err(super::Error::UnsuportedOrEncrypted(log)) + Err(super::Error::UnsupportedOrEncrypted(log)) } pub fn new( diff --git a/repak_cli/Cargo.toml b/repak_cli/Cargo.toml index 3a1709b..0c3ef14 100644 --- a/repak_cli/Cargo.toml +++ b/repak_cli/Cargo.toml @@ -19,6 +19,6 @@ indicatif = { version = "0.17.3", features = ["rayon"] } path-clean = "0.1.0" path-slash = "0.2.1" rayon = "1.6.1" -repak = { version = "0.1.7", path = "../repak" } +repak = { version = "0.1.7", path = "../repak", features = ["oodle"] } sha2 = "0.10.7" strum = { workspace = true } From 079693af2293e5c7c4f9f34f79725cecd17cc611 Mon Sep 17 00:00:00 2001 From: spuds <71292624+bananaturtlesandwich@users.noreply.github.com> Date: Sun, 27 Aug 2023 17:16:06 +0100 Subject: [PATCH 2/8] Pak::read should accept reference too whoops --- repak/src/pak.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/repak/src/pak.rs b/repak/src/pak.rs index deaaec2..1785f81 100644 --- a/repak/src/pak.rs +++ b/repak/src/pak.rs @@ -226,13 +226,13 @@ impl PakWriter { impl Pak { fn read( - mut reader: R, + reader: &mut R, version: super::Version, key: Option<&aes::Aes256>, ) -> Result { // read footer to get index, encryption & compression info reader.seek(io::SeekFrom::End(-version.size()))?; - let footer = super::footer::Footer::read(&mut reader, version)?; + let footer = super::footer::Footer::read(reader, version)?; // read index to get all the entry info reader.seek(io::SeekFrom::Start(footer.index_offset))?; let mut index = reader.read_len(footer.index_size as usize)?; From aae65c12cfa672c6e402b70a42d1c3035edad283 Mon Sep 17 00:00:00 2001 From: spuds <71292624+bananaturtlesandwich@users.noreply.github.com> Date: Wed, 16 Aug 2023 22:41:58 +0100 Subject: [PATCH 3/8] add encryption feature --- repak/Cargo.toml | 5 +- repak/src/entry.rs | 22 +++++--- repak/src/error.rs | 3 ++ repak/src/pak.rs | 116 +++++++++++++++++++++++++++++++++++++++--- repak_cli/src/main.rs | 24 ++++++--- 5 files changed, 147 insertions(+), 23 deletions(-) diff --git a/repak/Cargo.toml b/repak/Cargo.toml index 5c31a19..1e35868 100644 --- a/repak/Cargo.toml +++ b/repak/Cargo.toml @@ -7,13 +7,14 @@ version.workspace = true edition.workspace = true [features] -default = ["compression"] +default = ["compression", "encryption"] compression = ["dep:flate2", "dep:zstd"] oodle = ["dep:libloading", "dep:ureq", "dep:once_cell", "dep:hex-literal", "dep:hex"] +encryption = ["dep:aes"] [dependencies] byteorder = "1.4" -aes = "0.8" +aes = { version = "0.8", optional = true } flate2 = { version = "1.0", optional = true } zstd = { version = "0.12", optional = true } thiserror = "1.0" diff --git a/repak/src/entry.rs b/repak/src/entry.rs index 71b5af2..b198c58 100644 --- a/repak/src/entry.rs +++ b/repak/src/entry.rs @@ -311,25 +311,31 @@ impl Entry { reader: &mut R, version: Version, compression: &[Compression], - key: Option<&aes::Aes256>, + #[cfg(feature = "encryption")] key: Option<&aes::Aes256>, buf: &mut W, ) -> Result<(), super::Error> { reader.seek(io::SeekFrom::Start(self.offset))?; Entry::read(reader, version)?; let data_offset = reader.stream_position()?; + #[allow(unused_mut)] let mut data = reader.read_len(match self.is_encrypted() { true => align(self.compressed), false => self.compressed, } as usize)?; if self.is_encrypted() { - let Some(key) = key else { - return Err(super::Error::Encrypted); - }; - use aes::cipher::BlockDecrypt; - for block in data.chunks_mut(16) { - key.decrypt_block(aes::Block::from_mut_slice(block)) + #[cfg(not(feature = "encryption"))] + return Err(super::Error::Encryption); + #[cfg(feature = "encryption")] + { + let Some(key) = key else { + return Err(super::Error::Encrypted); + }; + use aes::cipher::BlockDecrypt; + for block in data.chunks_mut(16) { + key.decrypt_block(aes::Block::from_mut_slice(block)) + } + data.truncate(self.compressed as usize); } - data.truncate(self.compressed as usize); } let ranges = match &self.blocks { diff --git a/repak/src/error.rs b/repak/src/error.rs index a961fad..4bfd7df 100644 --- a/repak/src/error.rs +++ b/repak/src/error.rs @@ -13,6 +13,9 @@ pub enum Error { #[error("enable the compression feature to read compressed paks")] Compression, + #[error("enable the encryption feature to read encrypted paks")] + Encryption, + #[error("enable the oodle feature to read oodle paks")] Oodle, diff --git a/repak/src/pak.rs b/repak/src/pak.rs index 1785f81..8161b42 100644 --- a/repak/src/pak.rs +++ b/repak/src/pak.rs @@ -1,5 +1,6 @@ use super::ext::{ReadExt, WriteExt}; use super::{Version, VersionMajor}; +#[cfg(feature = "encryption")] use aes::Aes256; use byteorder::{ReadBytesExt, WriteBytesExt, LE}; use std::collections::BTreeMap; @@ -8,6 +9,7 @@ use std::io::{self, Read, Seek, Write}; #[derive(Debug)] pub struct PakReader { pak: Pak, + #[cfg(feature = "encryption")] key: Option, } @@ -15,6 +17,7 @@ pub struct PakReader { pub struct PakWriter { pak: Pak, writer: W, + #[cfg(feature = "encryption")] key: Option, } @@ -70,6 +73,7 @@ impl Index { } } +#[cfg(feature = "encryption")] fn decrypt(key: Option<&aes::Aes256>, bytes: &mut [u8]) -> Result<(), super::Error> { if let Some(key) = key { use aes::cipher::BlockDecrypt; @@ -83,28 +87,79 @@ fn decrypt(key: Option<&aes::Aes256>, bytes: &mut [u8]) -> Result<(), super::Err } impl PakReader { - pub fn new_any( + #[cfg(not(feature = "encryption"))] + pub fn new_any(reader: &mut R) -> Result { + Self::new_any_inner(reader) + } + + #[cfg(feature = "encryption")] + pub fn new_any_with_key( reader: &mut R, key: Option, + ) -> Result { + Self::new_any_inner(reader, key) + } + + fn new_any_inner( + reader: &mut R, + #[cfg(feature = "encryption")] key: Option, ) -> Result { use std::fmt::Write; let mut log = "\n".to_owned(); for ver in Version::iter() { - match Pak::read(&mut *reader, ver, key.as_ref()) { - Ok(pak) => return Ok(Self { pak, key }), + match Pak::read( + &mut *reader, + ver, + #[cfg(feature = "encryption")] + key.as_ref(), + ) { + Ok(pak) => { + return Ok(Self { + pak, + #[cfg(feature = "encryption")] + key, + }) + } Err(err) => writeln!(log, "trying version {} failed: {}", ver, err)?, } } Err(super::Error::UnsupportedOrEncrypted(log)) } + #[cfg(not(feature = "encryption"))] pub fn new( reader: &mut R, version: super::Version, + ) -> Result { + Self::new_inner(reader, version) + } + + #[cfg(feature = "encryption")] + pub fn new_with_key( + reader: &mut R, + version: super::Version, key: Option, ) -> Result { - Pak::read(reader, version, key.as_ref()).map(|pak| Self { pak, key }) + Self::new_inner(reader, version, key) + } + + fn new_inner( + reader: &mut R, + version: super::Version, + #[cfg(feature = "encryption")] key: Option, + ) -> Result { + Pak::read( + reader, + version, + #[cfg(feature = "encryption")] + key.as_ref(), + ) + .map(|pak| Self { + pak, + #[cfg(feature = "encryption")] + key, + }) } pub fn version(&self) -> super::Version { @@ -140,6 +195,7 @@ impl PakReader { reader, self.pak.version, &self.pak.compression, + #[cfg(feature = "encryption")] self.key.as_ref(), writer, ), @@ -158,6 +214,7 @@ impl PakReader { writer.seek(io::SeekFrom::Start(self.pak.index_offset.unwrap()))?; Ok(PakWriter { pak: self.pak, + #[cfg(feature = "encryption")] key: self.key, writer, }) @@ -165,7 +222,21 @@ impl PakReader { } impl PakWriter { + #[cfg(not(feature = "encryption"))] 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, + } + } + + #[cfg(feature = "encryption")] + pub fn new_with_key( writer: W, key: Option, version: Version, @@ -179,6 +250,21 @@ impl PakWriter { } } + fn new_inner( + writer: W, + #[cfg(feature = "encryption")] key: Option, + version: Version, + mount_point: String, + path_hash_seed: Option, + ) -> Self { + PakWriter { + pak: Pak::new(version, mount_point, path_hash_seed), + writer, + #[cfg(feature = "encryption")] + key, + } + } + pub fn into_writer(self) -> W { self.writer } @@ -219,7 +305,11 @@ impl PakWriter { } pub fn write_index(mut self) -> Result { - self.pak.write(&mut self.writer, self.key)?; + self.pak.write( + &mut self.writer, + #[cfg(feature = "encryption")] + self.key, + )?; Ok(self.writer) } } @@ -228,17 +318,21 @@ impl Pak { fn read( reader: &mut R, version: super::Version, - key: Option<&aes::Aes256>, + #[cfg(feature = "encryption")] key: Option<&aes::Aes256>, ) -> Result { // read footer to get index, encryption & compression info reader.seek(io::SeekFrom::End(-version.size()))?; let footer = super::footer::Footer::read(reader, version)?; // read index to get all the entry info reader.seek(io::SeekFrom::Start(footer.index_offset))?; + #[allow(unused_mut)] let mut index = reader.read_len(footer.index_size as usize)?; // decrypt index if needed if footer.encrypted { + #[cfg(not(feature = "encryption"))] + return Err(super::Error::Encryption); + #[cfg(feature = "encryption")] decrypt(key, &mut index)?; } @@ -260,6 +354,9 @@ impl Pak { // TODO verify hash if footer.encrypted { + #[cfg(not(feature = "encryption"))] + return Err(super::Error::Encryption); + #[cfg(feature = "encryption")] decrypt(key, &mut path_hash_index_buf)?; } @@ -283,11 +380,15 @@ impl Pak { let _full_directory_index_hash = index.read_len(20)?; reader.seek(io::SeekFrom::Start(full_directory_index_offset))?; + #[allow(unused_mut)] let mut full_directory_index = reader.read_len(full_directory_index_size as usize)?; // TODO verify hash if footer.encrypted { + #[cfg(not(feature = "encryption"))] + return Err(super::Error::Encryption); + #[cfg(feature = "encryption")] decrypt(key, &mut full_directory_index)?; } let mut fdi = io::Cursor::new(full_directory_index); @@ -367,7 +468,7 @@ impl Pak { fn write( &self, writer: &mut W, - _key: Option, + #[cfg(feature = "encryption")] _key: Option, ) -> Result<(), super::Error> { let index_offset = writer.stream_position()?; @@ -594,6 +695,7 @@ fn pad_zeros_to_alignment(v: &mut Vec, alignment: usize) { assert!(v.len() % alignment == 0); } +#[cfg(feature = "encryption")] fn encrypt(key: Aes256, bytes: &mut [u8]) { use aes::cipher::BlockEncrypt; for chunk in bytes.chunks_mut(16) { diff --git a/repak_cli/src/main.rs b/repak_cli/src/main.rs index 7b68d61..78ddbd8 100644 --- a/repak_cli/src/main.rs +++ b/repak_cli/src/main.rs @@ -175,7 +175,10 @@ fn main() -> Result<(), repak::Error> { } fn info(aes_key: Option, action: ActionInfo) -> Result<(), repak::Error> { - let pak = repak::PakReader::new_any(&mut BufReader::new(File::open(action.input)?), aes_key)?; + let pak = repak::PakReader::new_any_with_key( + &mut BufReader::new(File::open(action.input)?), + aes_key, + )?; println!("mount point: {}", pak.mount_point()); println!("version: {}", pak.version()); println!("version major: {}", pak.version().version_major()); @@ -186,7 +189,10 @@ 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(&mut BufReader::new(File::open(action.input)?), aes_key)?; + let pak = repak::PakReader::new_any_with_key( + &mut BufReader::new(File::open(action.input)?), + aes_key, + )?; let mount_point = PathBuf::from(pak.mount_point()); let prefix = Path::new(&action.strip_prefix); @@ -215,7 +221,10 @@ 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(&mut BufReader::new(File::open(&action.input)?), aes_key)?; + let pak = repak::PakReader::new_any_with_key( + &mut BufReader::new(File::open(&action.input)?), + aes_key, + )?; let mount_point = PathBuf::from(pak.mount_point()); let prefix = Path::new(&action.strip_prefix); @@ -270,7 +279,10 @@ fn hash_list(aes_key: Option, action: ActionHashList) -> Result<(), const STYLE: &str = "[{elapsed_precise}] [{wide_bar}] {pos}/{len} ({eta})"; fn unpack(aes_key: Option, action: ActionUnpack) -> Result<(), repak::Error> { - let pak = repak::PakReader::new_any(&mut BufReader::new(File::open(&action.input)?), aes_key)?; + let pak = repak::PakReader::new_any_with_key( + &mut BufReader::new(File::open(&action.input)?), + aes_key, + )?; let output = action .output .map(PathBuf::from) @@ -388,7 +400,7 @@ fn pack(args: ActionPack) -> Result<(), repak::Error> { collect_files(&mut paths, input_path)?; paths.sort(); - let mut pak = repak::PakWriter::new( + let mut pak = repak::PakWriter::new_with_key( BufWriter::new(File::create(&output)?), None, args.version, @@ -423,7 +435,7 @@ 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(&mut reader, aes_key)?; + let pak = repak::PakReader::new_any_with_key(&mut reader, aes_key)?; let mount_point = PathBuf::from(pak.mount_point()); let prefix = Path::new(&args.strip_prefix); From 7004e1c203a644dcf453750de7a933c00abcd917 Mon Sep 17 00:00:00 2001 From: spuds <71292624+bananaturtlesandwich@users.noreply.github.com> Date: Sun, 27 Aug 2023 17:54:02 +0100 Subject: [PATCH 4/8] use clippy suggestions --- repak/src/entry.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/repak/src/entry.rs b/repak/src/entry.rs index b198c58..35d6224 100644 --- a/repak/src/entry.rs +++ b/repak/src/entry.rs @@ -316,6 +316,7 @@ impl Entry { ) -> Result<(), super::Error> { reader.seek(io::SeekFrom::Start(self.offset))?; Entry::read(reader, version)?; + #[cfg(any(feature = "compression", feature = "oodle"))] let data_offset = reader.stream_position()?; #[allow(unused_mut)] let mut data = reader.read_len(match self.is_encrypted() { @@ -338,6 +339,7 @@ impl Entry { } } + #[cfg(any(feature = "compression", feature = "oodle"))] let ranges = match &self.blocks { Some(blocks) => blocks .iter() From 50f54837cabbea5ca89267336da120a0d9d53828 Mon Sep 17 00:00:00 2001 From: spuds <71292624+bananaturtlesandwich@users.noreply.github.com> Date: Sun, 27 Aug 2023 19:46:36 +0100 Subject: [PATCH 5/8] use Key enum to reduce cfg attributes used --- repak/src/entry.rs | 11 ++++--- repak/src/lib.rs | 18 +++++++++++ repak/src/pak.rs | 75 +++++++++++++-------------------------------- repak/tests/test.rs | 8 ++--- 4 files changed, 50 insertions(+), 62 deletions(-) diff --git a/repak/src/entry.rs b/repak/src/entry.rs index 35d6224..a6476b0 100644 --- a/repak/src/entry.rs +++ b/repak/src/entry.rs @@ -3,13 +3,13 @@ use byteorder::{ReadBytesExt, WriteBytesExt, LE}; use std::io; #[derive(Debug, PartialEq, Clone, Copy)] -pub enum EntryLocation { +pub(crate) enum EntryLocation { Data, Index, } #[derive(Debug)] -pub struct Block { +pub(crate) struct Block { pub start: u64, pub end: u64, } @@ -35,7 +35,7 @@ fn align(offset: u64) -> u64 { } #[derive(Debug)] -pub struct Entry { +pub(crate) struct Entry { pub offset: u64, pub compressed: u64, pub uncompressed: u64, @@ -311,7 +311,7 @@ impl Entry { reader: &mut R, version: Version, compression: &[Compression], - #[cfg(feature = "encryption")] key: Option<&aes::Aes256>, + key: &super::Key, buf: &mut W, ) -> Result<(), super::Error> { reader.seek(io::SeekFrom::Start(self.offset))?; @@ -328,7 +328,7 @@ impl Entry { return Err(super::Error::Encryption); #[cfg(feature = "encryption")] { - let Some(key) = key else { + let super::Key::Some(key) = key else { return Err(super::Error::Encrypted); }; use aes::cipher::BlockDecrypt; @@ -423,6 +423,7 @@ impl Entry { */ #[allow(non_snake_case)] + #[allow(clippy::type_complexity)] let OodleLZ_Decompress: libloading::Symbol< extern "C" fn( compBuf: *mut u8, diff --git a/repak/src/lib.rs b/repak/src/lib.rs index e66c4e0..7c1f99b 100644 --- a/repak/src/lib.rs +++ b/repak/src/lib.rs @@ -122,3 +122,21 @@ pub enum Compression { Oodle, Zstd, } + +#[allow(clippy::large_enum_variant)] +#[derive(Debug)] +pub(crate) enum Key { + #[cfg(feature = "encryption")] + Some(aes::Aes256), + None, +} + +impl From> for Key { + fn from(value: Option) -> Self { + match value { + #[cfg(feature = "encryption")] + Some(key) => Self::Some(key), + _ => Self::None, + } + } +} diff --git a/repak/src/pak.rs b/repak/src/pak.rs index 8161b42..c5de066 100644 --- a/repak/src/pak.rs +++ b/repak/src/pak.rs @@ -1,7 +1,5 @@ use super::ext::{ReadExt, WriteExt}; use super::{Version, VersionMajor}; -#[cfg(feature = "encryption")] -use aes::Aes256; use byteorder::{ReadBytesExt, WriteBytesExt, LE}; use std::collections::BTreeMap; use std::io::{self, Read, Seek, Write}; @@ -9,20 +7,18 @@ use std::io::{self, Read, Seek, Write}; #[derive(Debug)] pub struct PakReader { pak: Pak, - #[cfg(feature = "encryption")] - key: Option, + key: super::Key, } #[derive(Debug)] pub struct PakWriter { pak: Pak, writer: W, - #[cfg(feature = "encryption")] - key: Option, + key: super::Key, } #[derive(Debug)] -pub struct Pak { +pub(crate) struct Pak { version: Version, mount_point: String, index_offset: Option, @@ -47,7 +43,7 @@ impl Pak { } #[derive(Debug, Default)] -pub struct Index { +pub(crate) struct Index { path_hash_seed: Option, entries: BTreeMap, } @@ -74,8 +70,8 @@ impl Index { } #[cfg(feature = "encryption")] -fn decrypt(key: Option<&aes::Aes256>, bytes: &mut [u8]) -> Result<(), super::Error> { - if let Some(key) = key { +fn decrypt(key: &super::Key, bytes: &mut [u8]) -> Result<(), super::Error> { + if let super::Key::Some(key) = key { use aes::cipher::BlockDecrypt; for chunk in bytes.chunks_mut(16) { key.decrypt_block(aes::Block::from_mut_slice(chunk)) @@ -89,7 +85,7 @@ fn decrypt(key: Option<&aes::Aes256>, bytes: &mut [u8]) -> Result<(), super::Err impl PakReader { #[cfg(not(feature = "encryption"))] pub fn new_any(reader: &mut R) -> Result { - Self::new_any_inner(reader) + Self::new_any_inner(reader, super::Key::None) } #[cfg(feature = "encryption")] @@ -97,30 +93,19 @@ impl PakReader { reader: &mut R, key: Option, ) -> Result { - Self::new_any_inner(reader, key) + Self::new_any_inner(reader, key.into()) } fn new_any_inner( reader: &mut R, - #[cfg(feature = "encryption")] key: Option, + key: super::Key, ) -> Result { use std::fmt::Write; let mut log = "\n".to_owned(); for ver in Version::iter() { - match Pak::read( - &mut *reader, - ver, - #[cfg(feature = "encryption")] - key.as_ref(), - ) { - Ok(pak) => { - return Ok(Self { - pak, - #[cfg(feature = "encryption")] - key, - }) - } + match Pak::read(&mut *reader, ver, &key) { + Ok(pak) => return Ok(Self { pak, key }), Err(err) => writeln!(log, "trying version {} failed: {}", ver, err)?, } } @@ -141,25 +126,15 @@ impl PakReader { version: super::Version, key: Option, ) -> Result { - Self::new_inner(reader, version, key) + Self::new_inner(reader, version, key.into()) } fn new_inner( reader: &mut R, version: super::Version, - #[cfg(feature = "encryption")] key: Option, + key: super::Key, ) -> Result { - Pak::read( - reader, - version, - #[cfg(feature = "encryption")] - key.as_ref(), - ) - .map(|pak| Self { - pak, - #[cfg(feature = "encryption")] - key, - }) + Pak::read(reader, version, &key).map(|pak| Self { pak, key }) } pub fn version(&self) -> super::Version { @@ -195,8 +170,7 @@ impl PakReader { reader, self.pak.version, &self.pak.compression, - #[cfg(feature = "encryption")] - self.key.as_ref(), + &self.key, writer, ), None => Err(super::Error::MissingEntry(path.to_owned())), @@ -214,7 +188,6 @@ impl PakReader { writer.seek(io::SeekFrom::Start(self.pak.index_offset.unwrap()))?; Ok(PakWriter { pak: self.pak, - #[cfg(feature = "encryption")] key: self.key, writer, }) @@ -232,6 +205,7 @@ impl PakWriter { PakWriter { pak: Pak::new(version, mount_point, path_hash_seed), writer, + key: super::Key::None, } } @@ -246,13 +220,13 @@ impl PakWriter { PakWriter { pak: Pak::new(version, mount_point, path_hash_seed), writer, - key, + key: key.into(), } } fn new_inner( writer: W, - #[cfg(feature = "encryption")] key: Option, + key: super::Key, version: Version, mount_point: String, path_hash_seed: Option, @@ -260,7 +234,6 @@ impl PakWriter { PakWriter { pak: Pak::new(version, mount_point, path_hash_seed), writer, - #[cfg(feature = "encryption")] key, } } @@ -305,11 +278,7 @@ impl PakWriter { } pub fn write_index(mut self) -> Result { - self.pak.write( - &mut self.writer, - #[cfg(feature = "encryption")] - self.key, - )?; + self.pak.write(&mut self.writer, &self.key)?; Ok(self.writer) } } @@ -318,7 +287,7 @@ impl Pak { fn read( reader: &mut R, version: super::Version, - #[cfg(feature = "encryption")] key: Option<&aes::Aes256>, + key: &super::Key, ) -> Result { // read footer to get index, encryption & compression info reader.seek(io::SeekFrom::End(-version.size()))?; @@ -468,7 +437,7 @@ impl Pak { fn write( &self, writer: &mut W, - #[cfg(feature = "encryption")] _key: Option, + _key: &super::Key, ) -> Result<(), super::Error> { let index_offset = writer.stream_position()?; @@ -696,7 +665,7 @@ fn pad_zeros_to_alignment(v: &mut Vec, alignment: usize) { } #[cfg(feature = "encryption")] -fn encrypt(key: Aes256, bytes: &mut [u8]) { +fn encrypt(key: aes::Aes256, bytes: &mut [u8]) { use aes::cipher::BlockEncrypt; for chunk in bytes.chunks_mut(16) { key.encrypt_block(aes::Block::from_mut_slice(chunk)) diff --git a/repak/tests/test.rs b/repak/tests/test.rs index 1a7da1a..1355ac5 100644 --- a/repak/tests/test.rs +++ b/repak/tests/test.rs @@ -96,7 +96,7 @@ 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(&mut reader, Some(key)).unwrap(); + let pak = repak::PakReader::new_any_with_key(&mut reader, Some(key)).unwrap(); assert_eq!(pak.mount_point(), "../mount/point/root/"); assert_eq!(pak.version(), version); @@ -159,10 +159,10 @@ 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(&mut reader, Some(key)).unwrap(); + let pak_reader = repak::PakReader::new_any_with_key(&mut reader, Some(key)).unwrap(); let writer = Cursor::new(vec![]); - let mut pak_writer = repak::PakWriter::new( + let mut pak_writer = repak::PakWriter::new_with_key( writer, None, pak_reader.version(), @@ -191,7 +191,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(&mut buf, Some(key)).unwrap(); + let pak_reader = repak::PakReader::new_any_with_key(&mut buf, Some(key)).unwrap(); let rewrite = pak_reader .into_pakwriter(buf) From f166ee685f5908549e4696d968a590dc62135960 Mon Sep 17 00:00:00 2001 From: spuds <71292624+bananaturtlesandwich@users.noreply.github.com> Date: Sun, 27 Aug 2023 21:51:42 +0100 Subject: [PATCH 6/8] make features more clearly additive --- repak/src/lib.rs | 11 ++++------ repak/src/pak.rs | 48 ++++++++++++++++++++++++++++++++++++------- repak/tests/test.rs | 9 ++++---- repak_cli/src/main.rs | 12 +++++------ 4 files changed, 55 insertions(+), 25 deletions(-) diff --git a/repak/src/lib.rs b/repak/src/lib.rs index 7c1f99b..e6ea5a1 100644 --- a/repak/src/lib.rs +++ b/repak/src/lib.rs @@ -131,12 +131,9 @@ pub(crate) enum Key { None, } -impl From> for Key { - fn from(value: Option) -> Self { - match value { - #[cfg(feature = "encryption")] - Some(key) => Self::Some(key), - _ => Self::None, - } +#[cfg(feature = "encryption")] +impl From for Key { + fn from(value: aes::Aes256) -> Self { + Self::Some(value) } } diff --git a/repak/src/pak.rs b/repak/src/pak.rs index c5de066..730f233 100644 --- a/repak/src/pak.rs +++ b/repak/src/pak.rs @@ -83,7 +83,6 @@ fn decrypt(key: &super::Key, bytes: &mut [u8]) -> Result<(), super::Error> { } impl PakReader { - #[cfg(not(feature = "encryption"))] pub fn new_any(reader: &mut R) -> Result { Self::new_any_inner(reader, super::Key::None) } @@ -91,11 +90,22 @@ impl PakReader { #[cfg(feature = "encryption")] pub fn new_any_with_key( reader: &mut R, - key: Option, + 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, @@ -112,23 +122,34 @@ impl PakReader { Err(super::Error::UnsupportedOrEncrypted(log)) } - #[cfg(not(feature = "encryption"))] pub fn new( reader: &mut R, version: super::Version, ) -> Result { - Self::new_inner(reader, version) + Self::new_inner(reader, version, super::Key::None) } #[cfg(feature = "encryption")] pub fn new_with_key( reader: &mut R, version: super::Version, - key: Option, + 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, @@ -195,7 +216,6 @@ impl PakReader { } impl PakWriter { - #[cfg(not(feature = "encryption"))] pub fn new( writer: W, version: Version, @@ -212,7 +232,7 @@ impl PakWriter { #[cfg(feature = "encryption")] pub fn new_with_key( writer: W, - key: Option, + key: aes::Aes256, version: Version, mount_point: String, path_hash_seed: Option, @@ -224,6 +244,20 @@ impl PakWriter { } } + #[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 1355ac5..2c0c06a 100644 --- a/repak/tests/test.rs +++ b/repak/tests/test.rs @@ -96,7 +96,7 @@ 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, Some(key)).unwrap(); + let pak = repak::PakReader::new_any_with_key(&mut reader, key).unwrap(); assert_eq!(pak.mount_point(), "../mount/point/root/"); assert_eq!(pak.version(), version); @@ -159,12 +159,11 @@ 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, Some(key)).unwrap(); + let pak_reader = repak::PakReader::new_any_with_key(&mut reader, key).unwrap(); let writer = Cursor::new(vec![]); - let mut pak_writer = repak::PakWriter::new_with_key( + let mut pak_writer = repak::PakWriter::new( writer, - None, pak_reader.version(), pak_reader.mount_point().to_owned(), Some(0x205C5A7D), @@ -191,7 +190,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, Some(key)).unwrap(); + let pak_reader = repak::PakReader::new_any_with_key(&mut buf, key).unwrap(); let rewrite = pak_reader .into_pakwriter(buf) diff --git a/repak_cli/src/main.rs b/repak_cli/src/main.rs index 78ddbd8..b1f33c8 100644 --- a/repak_cli/src/main.rs +++ b/repak_cli/src/main.rs @@ -175,7 +175,7 @@ fn main() -> Result<(), repak::Error> { } fn info(aes_key: Option, action: ActionInfo) -> Result<(), repak::Error> { - let pak = repak::PakReader::new_any_with_key( + let pak = repak::PakReader::new_any_with_optional_key( &mut BufReader::new(File::open(action.input)?), aes_key, )?; @@ -189,7 +189,7 @@ 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_key( + let pak = repak::PakReader::new_any_with_optional_key( &mut BufReader::new(File::open(action.input)?), aes_key, )?; @@ -221,7 +221,7 @@ 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_key( + let pak = repak::PakReader::new_any_with_optional_key( &mut BufReader::new(File::open(&action.input)?), aes_key, )?; @@ -279,7 +279,7 @@ fn hash_list(aes_key: Option, action: ActionHashList) -> Result<(), const STYLE: &str = "[{elapsed_precise}] [{wide_bar}] {pos}/{len} ({eta})"; fn unpack(aes_key: Option, action: ActionUnpack) -> Result<(), repak::Error> { - let pak = repak::PakReader::new_any_with_key( + let pak = repak::PakReader::new_any_with_optional_key( &mut BufReader::new(File::open(&action.input)?), aes_key, )?; @@ -400,7 +400,7 @@ fn pack(args: ActionPack) -> Result<(), repak::Error> { collect_files(&mut paths, input_path)?; paths.sort(); - let mut pak = repak::PakWriter::new_with_key( + let mut pak = repak::PakWriter::new_with_optional_key( BufWriter::new(File::create(&output)?), None, args.version, @@ -435,7 +435,7 @@ 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_key(&mut reader, aes_key)?; + let pak = repak::PakReader::new_any_with_optional_key(&mut reader, aes_key)?; let mount_point = PathBuf::from(pak.mount_point()); let prefix = Path::new(&args.strip_prefix); From 87efb35d661a6e81cb285919b3d1ddaa9798c19c Mon Sep 17 00:00:00 2001 From: spuds <71292624+bananaturtlesandwich@users.noreply.github.com> Date: Mon, 28 Aug 2023 06:40:45 +0100 Subject: [PATCH 7/8] only run tests when default features are enabled --- repak/src/entry.rs | 3 ++- repak/src/pak.rs | 2 +- repak/tests/test.rs | 1 + 3 files changed, 4 insertions(+), 2 deletions(-) diff --git a/repak/src/entry.rs b/repak/src/entry.rs index a6476b0..bf9e5ba 100644 --- a/repak/src/entry.rs +++ b/repak/src/entry.rs @@ -311,7 +311,7 @@ impl Entry { reader: &mut R, version: Version, compression: &[Compression], - key: &super::Key, + #[allow(unused)] key: &super::Key, buf: &mut W, ) -> Result<(), super::Error> { reader.seek(io::SeekFrom::Start(self.offset))?; @@ -355,6 +355,7 @@ impl Entry { }, ) .collect::>(), + #[allow(clippy::single_range_in_vec_init)] None => vec![0..data.len()], }; diff --git a/repak/src/pak.rs b/repak/src/pak.rs index 730f233..cf3f596 100644 --- a/repak/src/pak.rs +++ b/repak/src/pak.rs @@ -321,7 +321,7 @@ impl Pak { fn read( reader: &mut R, version: super::Version, - key: &super::Key, + #[allow(unused)] key: &super::Key, ) -> Result { // read footer to get index, encryption & compression info reader.seek(io::SeekFrom::End(-version.size()))?; diff --git a/repak/tests/test.rs b/repak/tests/test.rs index 2c0c06a..9c70099 100644 --- a/repak/tests/test.rs +++ b/repak/tests/test.rs @@ -1,3 +1,4 @@ +#![cfg(feature = "default")] use byteorder::{ReadBytesExt, WriteBytesExt}; use paste::paste; use std::io::{self, Cursor, Read, Seek, SeekFrom}; From aa6d5f49b1fa20af7d1d58b102053627844cc053 Mon Sep 17 00:00:00 2001 From: Truman Kilen Date: Mon, 28 Aug 2023 10:58:22 -0500 Subject: [PATCH 8/8] Fix oodle feature config for non-windows targets --- repak/Cargo.toml | 3 +++ repak/src/error.rs | 9 ++++++++- repak_cli/Cargo.toml | 6 +++++- 3 files changed, 16 insertions(+), 2 deletions(-) diff --git a/repak/Cargo.toml b/repak/Cargo.toml index 1e35868..332e236 100644 --- a/repak/Cargo.toml +++ b/repak/Cargo.toml @@ -29,3 +29,6 @@ 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/error.rs b/repak/src/error.rs index 4bfd7df..bcb42b3 100644 --- a/repak/src/error.rs +++ b/repak/src/error.rs @@ -16,7 +16,14 @@ pub enum Error { #[error("enable the encryption feature to read encrypted paks")] Encryption, - #[error("enable the oodle feature to read oodle paks")] + #[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)") + )] Oodle, // std errors diff --git a/repak_cli/Cargo.toml b/repak_cli/Cargo.toml index 0c3ef14..b31338b 100644 --- a/repak_cli/Cargo.toml +++ b/repak_cli/Cargo.toml @@ -10,6 +10,11 @@ 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] aes = { workspace = true } base64 = { workspace = true } @@ -19,6 +24,5 @@ indicatif = { version = "0.17.3", features = ["rayon"] } path-clean = "0.1.0" path-slash = "0.2.1" rayon = "1.6.1" -repak = { version = "0.1.7", path = "../repak", features = ["oodle"] } sha2 = "0.10.7" strum = { workspace = true }