From 953be3726c71922c05db393eff197e279252fbb5 Mon Sep 17 00:00:00 2001 From: Truman Kilen Date: Tue, 31 Jan 2023 21:41:36 -0600 Subject: [PATCH] Rewrite unpak command --- Cargo.toml | 2 + examples/subcommands/list.rs | 6 - examples/subcommands/mod.rs | 28 ---- examples/subcommands/unpack.rs | 17 --- examples/subcommands/version.rs | 4 - examples/unpak.rs | 234 ++++++++++++++++++++++++++++---- 6 files changed, 207 insertions(+), 84 deletions(-) delete mode 100644 examples/subcommands/list.rs delete mode 100644 examples/subcommands/mod.rs delete mode 100644 examples/subcommands/unpack.rs delete mode 100644 examples/subcommands/version.rs diff --git a/Cargo.toml b/Cargo.toml index 663108a..cafa9c5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -21,4 +21,6 @@ sha1 = "0.10.5" [dev-dependencies] base64 = "0.21.0" +clap = { version = "4.1.4", features = ["derive"] } paste = "1.0.11" +path-clean = "0.1.0" diff --git a/examples/subcommands/list.rs b/examples/subcommands/list.rs deleted file mode 100644 index 853824e..0000000 --- a/examples/subcommands/list.rs +++ /dev/null @@ -1,6 +0,0 @@ -pub fn list(path: String, key: Option) -> 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 deleted file mode 100644 index 1c134e7..0000000 --- a/examples/subcommands/mod.rs +++ /dev/null @@ -1,28 +0,0 @@ -mod list; -mod unpack; -mod version; -pub use {list::list, unpack::unpack, version::version}; - -fn load_pak( - path: String, - key: Option, -) -> Result>, unpak::Error> { - use aes::cipher::KeyInit; - use base64::{engine::general_purpose, Engine as _}; - let key = key - .map(|k| { - general_purpose::STANDARD - .decode(k) - .as_ref() - .map_err(|_| unpak::Error::Base64) - .and_then(|bytes| { - aes::Aes256Dec::new_from_slice(bytes).map_err(|_| unpak::Error::Aes) - }) - }) - .transpose()?; - - unpak::PakReader::new_any( - std::io::BufReader::new(std::fs::OpenOptions::new().read(true).open(&path)?), - key, - ) -} diff --git a/examples/subcommands/unpack.rs b/examples/subcommands/unpack.rs deleted file mode 100644 index 8bf268e..0000000 --- a/examples/subcommands/unpack.rs +++ /dev/null @@ -1,17 +0,0 @@ -pub fn unpack(path: String, key: Option) -> 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.read_file(&file, &mut std::fs::File::create(folder.join(&file))?) { - Ok(_) => println!("{file}"), - Err(e) => eprintln!("{e}"), - } - } - Ok(()) -} diff --git a/examples/subcommands/version.rs b/examples/subcommands/version.rs deleted file mode 100644 index 9247d8b..0000000 --- a/examples/subcommands/version.rs +++ /dev/null @@ -1,4 +0,0 @@ -pub fn version(path: String, key: Option) -> Result<(), unpak::Error> { - println!("{}", super::load_pak(path, key)?.version()); - Ok(()) -} diff --git a/examples/unpak.rs b/examples/unpak.rs index 6f09d70..f23da5f 100644 --- a/examples/unpak.rs +++ b/examples/unpak.rs @@ -1,36 +1,212 @@ -mod subcommands; +use std::fs::{self, File}; +use std::io::{self, BufReader, BufWriter}; +use std::path::{Path, PathBuf}; -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 - if let Err(e) = match args.next().unwrap_or_default().as_str() { - "version" => subcommands::version(path, args.next()), - "list" => subcommands::list(path, args.next()), - "unpack" | "" => subcommands::unpack(path, args.next()), - "help" | _ => help(), - } { - eprintln!("{e}") +use clap::{Parser, Subcommand}; +use path_clean::PathClean; + +#[derive(Parser, Debug)] +struct ActionInfo { + /// Input .pak path + #[arg(index = 1)] + input: String, + + /// Base64 encoded AES encryption key if the pak is encrypted + #[arg(short, long)] + aes_key: Option, +} + +#[derive(Parser, Debug)] +struct ActionList { + /// Input .pak path + #[arg(index = 1)] + input: String, + + /// Base64 encoded AES encryption key if the pak is encrypted + #[arg(short, long)] + aes_key: Option, +} + +#[derive(Parser, Debug)] +struct ActionUnpack { + /// Input .pak path + #[arg(index = 1)] + input: String, + + /// Output directory. Defaults to next to input pak + #[arg(index = 2)] + output: Option, + + /// Prefix to strip from entry path + #[arg(short, long, default_value = "../../../")] + strip_prefix: String, + + /// Base64 encoded AES encryption key if the pak is encrypted + #[arg(short, long)] + aes_key: Option, +} + +#[derive(Parser, Debug)] +struct ActionPack { + /// Input directory + #[arg(index = 1)] + input: String, + + /// Output directory. Defaults to next to input dir + #[arg(index = 2)] + output: Option, + + /// Mount point + #[arg(short, long, default_value = "../../../")] + mount_point: String, +} + +#[derive(Subcommand, Debug)] +enum Action { + /// Print .pak info + Info(ActionInfo), + /// List .pak files + List(ActionInfo), + /// Unpack .pak file + Unpack(ActionUnpack), + /// Pack directory into .pak file + Pack(ActionPack), +} + +#[derive(Parser, Debug)] +#[command(author, version)] +struct Args { + #[command(subcommand)] + action: Action, +} + +fn main() -> Result<(), unpak::Error> { + let args = Args::parse(); + + match args.action { + Action::Info(args) => info(args), + Action::List(args) => list(args), + Action::Unpack(args) => unpack(args), + Action::Pack(args) => pack(args), } } -fn help() -> ! { - println!("{HELP}"); - std::process::exit(0) +fn aes_key(key: &str) -> Result { + use aes::cipher::KeyInit; + use base64::{engine::general_purpose, Engine as _}; + general_purpose::STANDARD + .decode(key) + .as_ref() + .map_err(|_| unpak::Error::Base64) + .and_then(|bytes| aes::Aes256Dec::new_from_slice(bytes).map_err(|_| unpak::Error::Aes)) } -const HELP: &str = r" -usage: -unpak - OR -drag the file onto the executable +fn info(args: ActionInfo) -> Result<(), unpak::Error> { + let pak = unpak::PakReader::new_any( + BufReader::new(File::open(&args.input)?), + args.aes_key.map(|k| aes_key(k.as_str())).transpose()?, + )?; + println!("mount point: {}", pak.mount_point()); + println!("version: {}", pak.version()); + println!("version major: {}", pak.version().version_major()); + println!("{} file entries", pak.files().len()); + Ok(()) +} -subcommands: -help - show this message -unpack - decompress the pak -list - print the files in the pak -version - print the version of the pak -"; +fn list(args: ActionInfo) -> Result<(), unpak::Error> { + let pak = unpak::PakReader::new_any( + BufReader::new(File::open(&args.input)?), + args.aes_key.map(|k| aes_key(k.as_str())).transpose()?, + )?; + for f in pak.files() { + println!("{f}"); + } + Ok(()) +} + +fn unpack(args: ActionUnpack) -> Result<(), unpak::Error> { + let mut pak = unpak::PakReader::new_any( + BufReader::new(File::open(&args.input)?), + args.aes_key.map(|k| aes_key(k.as_str())).transpose()?, + )?; + let output = args.output.as_ref().map(Path::new).unwrap_or_else(|| { + Path::new( + Path::new(&args.input) + .file_stem() + .and_then(|name| name.to_str()) + .expect("could not get pak file name"), + ) + }); + match fs::create_dir(output) { + Ok(_) => Ok(()), + Err(ref e) if e.kind() == std::io::ErrorKind::AlreadyExists => Ok(()), + Err(e) => Err(e), + }?; + if output.read_dir()?.next().is_some() { + return Err(unpak::Error::Other("output directory not empty")); + } + let mount_point = PathBuf::from(pak.mount_point()); + let prefix = Path::new(&args.strip_prefix); + for file in pak.files() { + let file_path = output.join( + mount_point + .join(&file) + .strip_prefix(prefix) + .map_err(|_| unpak::Error::Other("prefix does not match"))?, + ); + if !file_path.clean().starts_with(output) { + return Err(unpak::Error::Other( + "tried to write file outside of output directory", + )); + } + fs::create_dir_all(file_path.parent().expect("will be a file"))?; + pak.read_file(&file, &mut fs::File::create(file_path)?)?; + } + Ok(()) +} + +fn pack(args: ActionPack) -> Result<(), unpak::Error> { + let output = args + .output + .map(PathBuf::from) + .unwrap_or_else(|| Path::new(&args.input).with_extension("pak")); + + fn collect_files(paths: &mut Vec, dir: &Path) -> io::Result<()> { + if dir.is_dir() { + for entry in fs::read_dir(dir)? { + let entry = entry?; + let path = entry.path(); + if path.is_dir() { + collect_files(paths, &path)?; + } else { + paths.push(entry.path()); + } + } + } + Ok(()) + } + let input_path = Path::new(&args.input); + let mut paths = vec![]; + collect_files(&mut paths, input_path)?; + paths.sort(); + + let mut pak = unpak::PakWriter::new( + BufWriter::new(File::create(output)?), + None, + unpak::Version::V8B, + args.mount_point, + ); + + for p in paths { + pak.write_file( + &p.strip_prefix(input_path) + .expect("file not in input directory") + .to_string_lossy(), + &mut BufReader::new(File::open(&p)?), + )?; + } + + pak.write_index()?; + + Ok(()) +}