Merge pull request #6 from bananaturtlesandwich/features-redo

Move functionality behind `compression`, `encryption`, and `oodle` features
This commit is contained in:
Truman Kilen 2023-08-28 11:23:30 -05:00 committed by GitHub
commit a749449f8c
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
8 changed files with 239 additions and 54 deletions

View file

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

View file

@ -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,18 +311,24 @@ 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 {
#[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;
@ -331,7 +337,9 @@ impl Entry {
}
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::<Vec<_>>(),
#[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<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};

View file

@ -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<ureq::Error>), // 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),

View file

@ -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<aes::Aes256> for Key {
fn from(value: aes::Aes256) -> Self {
Self::Some(value)
}
}

View file

@ -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<aes::Aes256>,
key: super::Key,
}
#[derive(Debug)]
pub struct PakWriter<W: Write + Seek> {
pak: Pak,
writer: W,
key: Option<aes::Aes256>,
key: super::Key,
}
#[derive(Debug)]
pub struct Pak {
pub(crate) struct Pak {
version: Version,
mount_point: String,
index_offset: Option<u64>,
@ -44,7 +43,7 @@ impl Pak {
}
#[derive(Debug, Default)]
pub struct Index {
pub(crate) struct Index {
path_hash_seed: Option<u64>,
entries: BTreeMap<String, super::entry::Entry>,
}
@ -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<R: Read + Seek>(
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>(
reader: &mut R,
key: super::Key,
) -> Result<Self, super::Error> {
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<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> {
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<R: Read + Seek>(
reader: &mut R,
version: super::Version,
key: super::Key,
) -> Result<Self, super::Error> {
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<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(
writer: W,
key: super::Key,
version: Version,
mount_point: String,
path_hash_seed: Option<u64>,
) -> Self {
PakWriter {
pak: Pak::new(version, mount_point, path_hash_seed),
@ -219,26 +312,30 @@ impl<W: Write + Seek> PakWriter<W> {
}
pub fn write_index(mut self) -> Result<W, super::Error> {
self.pak.write(&mut self.writer, self.key)?;
self.pak.write(&mut self.writer, &self.key)?;
Ok(self.writer)
}
}
impl Pak {
fn read<R: Read + Seek>(
mut reader: R,
reader: &mut R,
version: super::Version,
key: Option<&aes::Aes256>,
#[allow(unused)] key: &super::Key,
) -> Result<Self, super::Error> {
// 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<W: Write + Seek>(
&self,
writer: &mut W,
_key: Option<aes::Aes256>,
_key: &super::Key,
) -> Result<(), super::Error> {
let index_offset = writer.stream_position()?;
@ -594,7 +698,8 @@ fn pad_zeros_to_alignment(v: &mut Vec<u8>, 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))

View file

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

View file

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

View file

@ -175,7 +175,10 @@ fn main() -> Result<(), repak::Error> {
}
fn info(aes_key: Option<aes::Aes256>, 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<aes::Aes256>, action: ActionInfo) -> Result<(), repak::E
}
fn list(aes_key: Option<aes::Aes256>, 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<aes::Aes256>, action: ActionList) -> Result<(), repak::E
}
fn hash_list(aes_key: Option<aes::Aes256>, 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<aes::Aes256>, action: ActionHashList) -> Result<(),
const STYLE: &str = "[{elapsed_precise}] [{wide_bar}] {pos}/{len} ({eta})";
fn unpack(aes_key: Option<aes::Aes256>, 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<aes::Aes256>, 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);