diff --git a/Cargo.toml b/Cargo.toml index b341f7b..203fc94 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -6,7 +6,7 @@ description = "a no-nonsense unreal pak parsing crate" license = "MIT OR Apache-2.0" keywords = ["modding", "parsing", "compression"] categories = ["filesystem", "parser-implementations"] -version = "0.1.0" +version = "0.2.0" edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html diff --git a/README.md b/README.md new file mode 100644 index 0000000..d5a8e3e --- /dev/null +++ b/README.md @@ -0,0 +1,19 @@ +# unpak +## a no-nonsense unreal pak parser +- doesn't force files to be extracted +- only converts entries to bytes when requested +- supports up to frozen index (4.25) paks (planned support for higher) +- supports compressed and encrypted paks +- supports iteration over entries +## [click for example code](https://github.com/bananaturtlesandwich/unpak/blob/master/examples/unpak.rs) +## the problem +looking at the libraries for pak reading, they were never not quite right for what i wanted to do: +- [rust-u4pak](https://github.com/panzi/rust-u4pak) - excellent support but very limited api +- [ue4pak](https://github.com/Speedy37/ue4pak-rs) - excellent api but no support for extraction +- [unrealpak](https://github.com/AstroTechies/unrealmodding/tree/main/unreal_pak) - excellent api but only supports version 8 +- [rust-unreal-unpak](https://crates.io/crates/rust-unreal-unpak) - is async only supports version 10 + +so i just though *fuck it i'll do it myself* and did it myself + +## references +although the api of [rust-u4pak](https://github.com/panzi/rust-u4pak) wasn't very friendly, the [`README`](https://github.com/panzi/rust-u4pak#readme) went into beautiful detail into the intricacies of the file format and when the readme had incorrect info *cough cough* `encryption uuid` *cough cough* the source code also had the answers as long as you looked hard enough \ No newline at end of file diff --git a/examples/list.rs b/examples/list.rs deleted file mode 100644 index e5f0423..0000000 --- a/examples/list.rs +++ /dev/null @@ -1,11 +0,0 @@ -fn main() -> Result<(), unpak::Error> { - let pak = unpak::Pak::new( - std::io::BufReader::new(std::io::Cursor::new(include_bytes!("rando_p.pak"))), - unpak::Version::CompressionEncryption, - None, - )?; - for file in pak.files() { - println!("{file}"); - } - Ok(()) -} diff --git a/examples/subcommands/list.rs b/examples/subcommands/list.rs new file mode 100644 index 0000000..fac3d43 --- /dev/null +++ b/examples/subcommands/list.rs @@ -0,0 +1,6 @@ +pub fn list(path: String, key: String) -> Result<(), unpak::Error> { + for file in super::load_pak(path, key)?.files() { + println!("{file}"); + } + Ok(()) +} diff --git a/examples/subcommands/mod.rs b/examples/subcommands/mod.rs new file mode 100644 index 0000000..92ed864 --- /dev/null +++ b/examples/subcommands/mod.rs @@ -0,0 +1,26 @@ +mod list; +mod unpack; +mod version; +pub use {list::list, unpack::unpack, version::version}; + +fn load_pak( + path: String, + key: String, +) -> Result>, unpak::Error> { + for ver in unpak::Version::iter() { + match unpak::Pak::new( + std::io::BufReader::new(std::fs::OpenOptions::new().read(true).open(&path)?), + ver, + match key.as_bytes() { + &[] => None, + key => Some(key), + }, + ) { + Ok(pak) => { + return Ok(pak); + } + _ => continue, + } + } + Err(unpak::Error::Other("version unsupported")) +} diff --git a/examples/subcommands/unpack.rs b/examples/subcommands/unpack.rs new file mode 100644 index 0000000..61baf55 --- /dev/null +++ b/examples/subcommands/unpack.rs @@ -0,0 +1,17 @@ +pub fn unpack(path: String, key: String) -> Result<(), unpak::Error> { + let folder = std::path::Path::new( + std::path::Path::new(&path) + .file_stem() + .and_then(|name| name.to_str()) + .unwrap_or_default(), + ); + let mut pak = super::load_pak(path.clone(), key)?; + for file in pak.files() { + std::fs::create_dir_all(folder.join(&file).parent().expect("will be a file"))?; + match pak.get(&file).expect("file should be in pak") { + Ok(data) => std::fs::write(folder.join(&file), data)?, + Err(e) => eprintln!("{e}"), + } + } + Ok(()) +} diff --git a/examples/subcommands/version.rs b/examples/subcommands/version.rs new file mode 100644 index 0000000..8cf1eac --- /dev/null +++ b/examples/subcommands/version.rs @@ -0,0 +1,4 @@ +pub fn version(path: String, key: String) -> Result<(), unpak::Error> { + println!("{}", super::load_pak(path, key)?.version()); + Ok(()) +} diff --git a/examples/unpack.rs b/examples/unpack.rs deleted file mode 100644 index 8c5a74a..0000000 --- a/examples/unpack.rs +++ /dev/null @@ -1,19 +0,0 @@ -fn main() -> Result<(), unpak::Error> { - let mut pak = unpak::Pak::new( - std::io::BufReader::new(std::io::Cursor::new(include_bytes!("rando_p.pak"))), - unpak::Version::CompressionEncryption, - None, - )?; - for file in pak.files() { - std::fs::create_dir_all( - std::path::Path::new(&file) - .parent() - .expect("will be a file"), - )?; - match pak.get(&file).expect("file should be in pak") { - Ok(data) => std::fs::write(&file, data)?, - Err(e) => eprintln!("{e}"), - } - } - Ok(()) -} diff --git a/examples/unpak.rs b/examples/unpak.rs new file mode 100644 index 0000000..484d0bc --- /dev/null +++ b/examples/unpak.rs @@ -0,0 +1,37 @@ +mod subcommands; + +fn main() { + let mut args = std::env::args(); + args.next(); + let Some(path) = args.next() else { + help() + }; + // can't map key to &[u8] because refers to owned data + match match args.next().unwrap_or_default().as_str() { + "version" => subcommands::version(path, args.next().unwrap_or_default()), + "list" => subcommands::list(path, args.next().unwrap_or_default()), + "unpack" | "" => subcommands::unpack(path, args.next().unwrap_or_default()), + "help" | _ => help(), + } { + Ok(_) => println!("success!"), + Err(e) => eprintln!("{e}"), + } +} + +fn help() -> ! { + println!("{HELP}"); + std::process::exit(0) +} + +const HELP: &str = r" +usage: +unpak + OR +drag the file onto the executable + +subcommands: +help - show this message +unpack - decompress the pak +list - print the files in the pak +version - print the version of the pak +"; diff --git a/examples/version.rs b/examples/version.rs deleted file mode 100644 index b1ba3db..0000000 --- a/examples/version.rs +++ /dev/null @@ -1,24 +0,0 @@ -fn main() -> Result<(), unpak::Error> { - // drag onto or open any pak with the example - let path = std::env::args().nth(1).unwrap_or_default(); - for ver in unpak::Version::iter() { - match unpak::Pak::new( - std::io::BufReader::new(std::fs::OpenOptions::new().read(true).open(&path)?), - ver, - None, - ) { - Ok(pak) => { - println!("{}", pak.version()); - break; - } - Err(unpak::Error::Version { version, .. }) => { - println!("{version}"); - break; - } - _ => continue, - } - } - // so you can read the results - std::thread::sleep(std::time::Duration::from_secs(10)); - Ok(()) -} diff --git a/src/entry.rs b/src/entry.rs index 74baa1d..9f34865 100644 --- a/src/entry.rs +++ b/src/entry.rs @@ -1,4 +1,4 @@ -use super::{Compression, ReadExt, Version}; +use super::{ext::ReadExt, Compression, Version}; use byteorder::{ReadBytesExt, LE}; use std::io; diff --git a/src/error.rs b/src/error.rs index 3821300..1ae8164 100644 --- a/src/error.rs +++ b/src/error.rs @@ -27,5 +27,5 @@ pub enum Error { #[error("pak is encrypted but no key was provided")] Encrypted, #[error("{0}")] - Other(String), + Other(&'static str), } diff --git a/src/footer.rs b/src/footer.rs index 7f6c54c..1e9e114 100644 --- a/src/footer.rs +++ b/src/footer.rs @@ -1,4 +1,4 @@ -use super::{Compression, ReadExt, Version}; +use super::{ext::ReadExt, Compression, Version}; use byteorder::{ReadBytesExt, LE}; use std::str::FromStr; diff --git a/src/lib.rs b/src/lib.rs index 11af6c7..50679e6 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -5,7 +5,7 @@ mod ext; mod footer; mod pak; -pub use {entry::*, error::*, ext::*, footer::*, pak::*}; +pub use {error::*, pak::*}; pub const MAGIC: u32 = 0x5A6F12E1; diff --git a/src/pak.rs b/src/pak.rs index 699f278..dce36db 100644 --- a/src/pak.rs +++ b/src/pak.rs @@ -6,7 +6,7 @@ pub struct Pak { version: Version, mount_point: String, key: Option, - entries: hashbrown::HashMap, + entries: hashbrown::HashMap, reader: R, } @@ -16,11 +16,11 @@ impl Pak { version: super::Version, key_hash: Option<&[u8]>, ) -> Result { - use super::ReadExt; + use super::ext::ReadExt; use byteorder::{ReadBytesExt, LE}; // read footer to get index, encryption & compression info reader.seek(io::SeekFrom::End(-version.size()))?; - let footer = super::Footer::new(&mut reader, version)?; + let footer = super::footer::Footer::new(&mut 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)?; @@ -46,7 +46,7 @@ impl Pak { for _ in 0..len { entries.insert( index.read_string()?, - super::Entry::new(&mut index, version)?, + super::entry::Entry::new(&mut index, version)?, ); } Ok(Self {