make proper api + example + README

This commit is contained in:
spuds 2023-01-14 18:32:06 +00:00
parent 07f97f6c98
commit 0959394241
15 changed files with 118 additions and 63 deletions

View file

@ -6,7 +6,7 @@ description = "a no-nonsense unreal pak parsing crate"
license = "MIT OR Apache-2.0" license = "MIT OR Apache-2.0"
keywords = ["modding", "parsing", "compression"] keywords = ["modding", "parsing", "compression"]
categories = ["filesystem", "parser-implementations"] categories = ["filesystem", "parser-implementations"]
version = "0.1.0" version = "0.2.0"
edition = "2021" edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

19
README.md Normal file
View file

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

View file

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

View file

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

View file

@ -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::Pak<std::io::BufReader<std::fs::File>>, 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"))
}

View file

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

View file

@ -0,0 +1,4 @@
pub fn version(path: String, key: String) -> Result<(), unpak::Error> {
println!("{}", super::load_pak(path, key)?.version());
Ok(())
}

View file

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

37
examples/unpak.rs Normal file
View file

@ -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 <file> <subcommand> <optional AES key>
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
";

View file

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

View file

@ -1,4 +1,4 @@
use super::{Compression, ReadExt, Version}; use super::{ext::ReadExt, Compression, Version};
use byteorder::{ReadBytesExt, LE}; use byteorder::{ReadBytesExt, LE};
use std::io; use std::io;

View file

@ -27,5 +27,5 @@ pub enum Error {
#[error("pak is encrypted but no key was provided")] #[error("pak is encrypted but no key was provided")]
Encrypted, Encrypted,
#[error("{0}")] #[error("{0}")]
Other(String), Other(&'static str),
} }

View file

@ -1,4 +1,4 @@
use super::{Compression, ReadExt, Version}; use super::{ext::ReadExt, Compression, Version};
use byteorder::{ReadBytesExt, LE}; use byteorder::{ReadBytesExt, LE};
use std::str::FromStr; use std::str::FromStr;

View file

@ -5,7 +5,7 @@ mod ext;
mod footer; mod footer;
mod pak; mod pak;
pub use {entry::*, error::*, ext::*, footer::*, pak::*}; pub use {error::*, pak::*};
pub const MAGIC: u32 = 0x5A6F12E1; pub const MAGIC: u32 = 0x5A6F12E1;

View file

@ -6,7 +6,7 @@ pub struct Pak<R: io::Read + io::Seek> {
version: Version, version: Version,
mount_point: String, mount_point: String,
key: Option<aes::Aes256Dec>, key: Option<aes::Aes256Dec>,
entries: hashbrown::HashMap<String, super::Entry>, entries: hashbrown::HashMap<String, super::entry::Entry>,
reader: R, reader: R,
} }
@ -16,11 +16,11 @@ impl<R: io::Read + io::Seek> Pak<R> {
version: super::Version, version: super::Version,
key_hash: Option<&[u8]>, key_hash: Option<&[u8]>,
) -> Result<Self, super::Error> { ) -> Result<Self, super::Error> {
use super::ReadExt; use super::ext::ReadExt;
use byteorder::{ReadBytesExt, LE}; use byteorder::{ReadBytesExt, LE};
// read footer to get index, encryption & compression info // read footer to get index, encryption & compression info
reader.seek(io::SeekFrom::End(-version.size()))?; 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 // read index to get all the entry info
reader.seek(io::SeekFrom::Start(footer.index_offset))?; reader.seek(io::SeekFrom::Start(footer.index_offset))?;
let mut index = reader.read_len(footer.index_size as usize)?; let mut index = reader.read_len(footer.index_size as usize)?;
@ -46,7 +46,7 @@ impl<R: io::Read + io::Seek> Pak<R> {
for _ in 0..len { for _ in 0..len {
entries.insert( entries.insert(
index.read_string()?, index.read_string()?,
super::Entry::new(&mut index, version)?, super::entry::Entry::new(&mut index, version)?,
); );
} }
Ok(Self { Ok(Self {