diff --git a/repak/Cargo.toml b/repak/Cargo.toml index 532a01e..332e236 100644 --- a/repak/Cargo.toml +++ b/repak/Cargo.toml @@ -6,20 +6,29 @@ license.workspace = true version.workspace = true 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"] +encryption = ["dep:aes"] + [dependencies] byteorder = "1.4" -aes = "0.8" -flate2 = "1.0" +aes = { version = "0.8", optional = true } +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 } paste = "1.0.11" + +[package.metadata.cargo-all-features] +denylist = ["oodle"] diff --git a/repak/src/entry.rs b/repak/src/entry.rs index df1a8fc..bf9e5ba 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,27 +311,35 @@ impl Entry { reader: &mut R, version: Version, compression: &[Compression], - key: Option<&aes::Aes256>, + #[allow(unused)] key: &super::Key, buf: &mut W, ) -> 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() { 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 super::Key::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); } + #[cfg(any(feature = "compression", feature = "oodle"))] let ranges = match &self.blocks { Some(blocks) => blocks .iter() @@ -347,9 +355,11 @@ impl Entry { }, ) .collect::>(), + #[allow(clippy::single_range_in_vec_init)] None => vec![0..data.len()], }; + #[cfg(feature = "compression")] macro_rules! decompress { ($decompressor: ty) => { for range in ranges { @@ -359,14 +369,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); @@ -410,6 +424,7 @@ impl Entry { */ #[allow(non_snake_case)] + #[allow(clippy::type_complexity)] let OodleLZ_Decompress: libloading::Symbol< extern "C" fn( compBuf: *mut u8, @@ -472,18 +487,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..bcb42b3 100644 --- a/repak/src/error.rs +++ b/repak/src/error.rs @@ -9,6 +9,23 @@ 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 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)") + )] + Oodle, + // std errors #[error("io error: {0}")] Io(#[from] std::io::Error), @@ -22,6 +39,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 +53,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 +87,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..e6ea5a1 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( @@ -119,3 +122,18 @@ pub enum Compression { Oodle, Zstd, } + +#[allow(clippy::large_enum_variant)] +#[derive(Debug)] +pub(crate) enum Key { + #[cfg(feature = "encryption")] + Some(aes::Aes256), + 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 7b4a738..cf3f596 100644 --- a/repak/src/pak.rs +++ b/repak/src/pak.rs @@ -1,6 +1,5 @@ use super::ext::{ReadExt, WriteExt}; use super::{Version, VersionMajor}; -use aes::Aes256; use byteorder::{ReadBytesExt, WriteBytesExt, LE}; use std::collections::BTreeMap; use std::io::{self, Read, Seek, Write}; @@ -8,18 +7,18 @@ use std::io::{self, Read, Seek, Write}; #[derive(Debug)] pub struct PakReader { pak: Pak, - key: Option, + key: super::Key, } #[derive(Debug)] pub struct PakWriter { pak: Pak, writer: W, - key: Option, + key: super::Key, } #[derive(Debug)] -pub struct Pak { +pub(crate) struct Pak { version: Version, mount_point: String, index_offset: Option, @@ -44,7 +43,7 @@ impl Pak { } #[derive(Debug, Default)] -pub struct Index { +pub(crate) struct Index { path_hash_seed: Option, entries: BTreeMap, } @@ -70,8 +69,9 @@ impl Index { } } -fn decrypt(key: Option<&aes::Aes256>, bytes: &mut [u8]) -> Result<(), super::Error> { - if let Some(key) = key { +#[cfg(feature = "encryption")] +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)) @@ -83,28 +83,79 @@ fn decrypt(key: Option<&aes::Aes256>, bytes: &mut [u8]) -> Result<(), super::Err } impl PakReader { - pub fn new_any( + 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, ) -> 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()) { + match Pak::read(&mut *reader, ver, &key) { Ok(pak) => return Ok(Self { pak, key }), Err(err) => writeln!(log, "trying version {} failed: {}", ver, err)?, } } - Err(super::Error::UnsuportedOrEncrypted(log)) + 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 { - Pak::read(reader, version, key.as_ref()).map(|pak| Self { pak, key }) + 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, + ) -> Result { + Pak::read(reader, version, &key).map(|pak| Self { pak, key }) } pub fn version(&self) -> super::Version { @@ -140,7 +191,7 @@ impl PakReader { reader, self.pak.version, &self.pak.compression, - self.key.as_ref(), + &self.key, writer, ), None => Err(super::Error::MissingEntry(path.to_owned())), @@ -166,11 +217,53 @@ 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, + version: Version, + mount_point: String, + path_hash_seed: Option, ) -> Self { PakWriter { pak: Pak::new(version, mount_point, path_hash_seed), @@ -219,26 +312,30 @@ impl PakWriter { } pub fn write_index(mut self) -> Result { - self.pak.write(&mut self.writer, self.key)?; + self.pak.write(&mut self.writer, &self.key)?; Ok(self.writer) } } impl Pak { fn read( - mut reader: R, + reader: &mut R, version: super::Version, - key: Option<&aes::Aes256>, + #[allow(unused)] key: &super::Key, ) -> 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))?; + #[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 +357,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 +383,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 +471,7 @@ impl Pak { fn write( &self, writer: &mut W, - _key: Option, + _key: &super::Key, ) -> Result<(), super::Error> { let index_offset = writer.stream_position()?; @@ -594,7 +698,8 @@ fn pad_zeros_to_alignment(v: &mut Vec, alignment: usize) { assert!(v.len() % alignment == 0); } -fn encrypt(key: Aes256, bytes: &mut [u8]) { +#[cfg(feature = "encryption")] +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..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}; @@ -96,7 +97,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, key).unwrap(); assert_eq!(pak.mount_point(), "../mount/point/root/"); assert_eq!(pak.version(), version); @@ -159,12 +160,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(&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( writer, - None, pak_reader.version(), pak_reader.mount_point().to_owned(), Some(0x205C5A7D), @@ -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, key).unwrap(); let rewrite = pak_reader .into_pakwriter(buf) diff --git a/repak_cli/Cargo.toml b/repak_cli/Cargo.toml index 3a1709b..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" } sha2 = "0.10.7" strum = { workspace = true } diff --git a/repak_cli/src/main.rs b/repak_cli/src/main.rs index 7b68d61..b1f33c8 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_optional_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_optional_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_optional_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_optional_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_optional_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_optional_key(&mut reader, aes_key)?; let mount_point = PathBuf::from(pak.mount_point()); let prefix = Path::new(&args.strip_prefix);