2023-02-01 03:41:36 +00:00
|
|
|
use std::fs::{self, File};
|
|
|
|
use std::io::{self, BufReader, BufWriter};
|
|
|
|
use std::path::{Path, PathBuf};
|
|
|
|
|
2023-02-08 04:21:09 +00:00
|
|
|
use clap::builder::TypedValueParser;
|
2023-02-01 03:41:36 +00:00
|
|
|
use clap::{Parser, Subcommand};
|
|
|
|
use path_clean::PathClean;
|
2023-02-08 02:54:04 +00:00
|
|
|
use rayon::prelude::*;
|
2023-02-08 04:21:09 +00:00
|
|
|
use strum::VariantNames;
|
2023-02-01 03:41:36 +00:00
|
|
|
|
|
|
|
#[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<String>,
|
|
|
|
}
|
|
|
|
|
|
|
|
#[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<String>,
|
|
|
|
}
|
|
|
|
|
|
|
|
#[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<String>,
|
|
|
|
|
|
|
|
/// 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<String>,
|
2023-02-01 04:55:37 +00:00
|
|
|
|
|
|
|
/// Verbose
|
|
|
|
#[arg(short, long, default_value = "false")]
|
|
|
|
verbose: bool,
|
2023-02-16 02:05:40 +00:00
|
|
|
|
|
|
|
/// Files or directories to include. Can be specified multiple times. If not specified, everything is extracted.
|
|
|
|
#[arg(action = clap::ArgAction::Append, short, long)]
|
|
|
|
include: Vec<String>,
|
2023-02-01 03:41:36 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
#[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<String>,
|
|
|
|
|
|
|
|
/// Mount point
|
|
|
|
#[arg(short, long, default_value = "../../../")]
|
|
|
|
mount_point: String,
|
2023-02-01 04:55:37 +00:00
|
|
|
|
2023-02-08 04:21:09 +00:00
|
|
|
/// Version
|
|
|
|
#[arg(
|
|
|
|
long,
|
|
|
|
default_value_t = repak::Version::V8B,
|
|
|
|
value_parser = clap::builder::PossibleValuesParser::new(repak::Version::VARIANTS).map(|s| s.parse::<repak::Version>().unwrap())
|
|
|
|
)]
|
|
|
|
version: repak::Version,
|
|
|
|
|
|
|
|
/// Path hash seed for >= V10
|
|
|
|
#[arg(short, long, default_value = "0")]
|
|
|
|
path_hash_seed: u64,
|
|
|
|
|
2023-02-01 04:55:37 +00:00
|
|
|
/// Verbose
|
|
|
|
#[arg(short, long, default_value = "false")]
|
|
|
|
verbose: bool,
|
2023-02-01 03:41:36 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
#[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,
|
|
|
|
}
|
|
|
|
|
2023-02-01 23:44:57 +00:00
|
|
|
fn main() -> Result<(), repak::Error> {
|
2023-02-01 03:41:36 +00:00
|
|
|
let args = Args::parse();
|
|
|
|
|
2023-02-08 04:21:09 +00:00
|
|
|
//let aasdf = repak::Version::iter().map(|v| format!("{v}"));
|
|
|
|
//clap::builder::PossibleValuesParser::new(aasdf.map(|a| a.as_str()));
|
|
|
|
|
2023-02-01 03:41:36 +00:00
|
|
|
match args.action {
|
|
|
|
Action::Info(args) => info(args),
|
|
|
|
Action::List(args) => list(args),
|
|
|
|
Action::Unpack(args) => unpack(args),
|
|
|
|
Action::Pack(args) => pack(args),
|
2023-01-14 18:32:06 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-02-10 23:10:40 +00:00
|
|
|
fn aes_key(key: &str) -> Result<aes::Aes256, repak::Error> {
|
2023-02-01 03:41:36 +00:00
|
|
|
use aes::cipher::KeyInit;
|
|
|
|
use base64::{engine::general_purpose, Engine as _};
|
|
|
|
general_purpose::STANDARD
|
|
|
|
.decode(key)
|
|
|
|
.as_ref()
|
2023-02-01 23:44:57 +00:00
|
|
|
.map_err(|_| repak::Error::Base64)
|
2023-02-10 23:10:40 +00:00
|
|
|
.and_then(|bytes| aes::Aes256::new_from_slice(bytes).map_err(|_| repak::Error::Aes))
|
2023-02-01 03:41:36 +00:00
|
|
|
}
|
|
|
|
|
2023-02-01 23:44:57 +00:00
|
|
|
fn info(args: ActionInfo) -> Result<(), repak::Error> {
|
|
|
|
let pak = repak::PakReader::new_any(
|
2023-02-01 03:41:36 +00:00
|
|
|
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(())
|
2023-01-14 18:32:06 +00:00
|
|
|
}
|
|
|
|
|
2023-02-01 23:44:57 +00:00
|
|
|
fn list(args: ActionInfo) -> Result<(), repak::Error> {
|
|
|
|
let pak = repak::PakReader::new_any(
|
2023-02-01 03:41:36 +00:00
|
|
|
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(())
|
|
|
|
}
|
2023-01-14 18:32:06 +00:00
|
|
|
|
2023-02-01 23:44:57 +00:00
|
|
|
fn unpack(args: ActionUnpack) -> Result<(), repak::Error> {
|
2023-02-08 02:54:04 +00:00
|
|
|
let pak = repak::PakReader::new_any(
|
2023-02-01 03:41:36 +00:00
|
|
|
BufReader::new(File::open(&args.input)?),
|
|
|
|
args.aes_key.map(|k| aes_key(k.as_str())).transpose()?,
|
|
|
|
)?;
|
2023-02-01 05:13:54 +00:00
|
|
|
let output = args
|
|
|
|
.output
|
|
|
|
.map(PathBuf::from)
|
|
|
|
.unwrap_or_else(|| Path::new(&args.input).with_extension(""));
|
|
|
|
match fs::create_dir(&output) {
|
2023-02-01 03:41:36 +00:00
|
|
|
Ok(_) => Ok(()),
|
|
|
|
Err(ref e) if e.kind() == std::io::ErrorKind::AlreadyExists => Ok(()),
|
|
|
|
Err(e) => Err(e),
|
|
|
|
}?;
|
|
|
|
if output.read_dir()?.next().is_some() {
|
2023-02-01 23:44:57 +00:00
|
|
|
return Err(repak::Error::Other("output directory not empty"));
|
2023-02-01 03:41:36 +00:00
|
|
|
}
|
|
|
|
let mount_point = PathBuf::from(pak.mount_point());
|
|
|
|
let prefix = Path::new(&args.strip_prefix);
|
2023-02-16 02:05:40 +00:00
|
|
|
|
|
|
|
let includes = args
|
|
|
|
.include
|
|
|
|
.iter()
|
|
|
|
.map(|i| prefix.join(Path::new(i)))
|
|
|
|
.collect::<Vec<_>>();
|
|
|
|
|
2023-02-08 02:54:04 +00:00
|
|
|
pak.files().into_par_iter().try_for_each_init(
|
2023-02-16 02:05:40 +00:00
|
|
|
|| (File::open(&args.input), includes.clone()),
|
|
|
|
|(file, includes), path| -> Result<(), repak::Error> {
|
|
|
|
let full_path = mount_point.join(&path);
|
2023-02-16 18:14:57 +00:00
|
|
|
if !includes.is_empty() && !includes.iter().any(|i| full_path.starts_with(i)) {
|
2023-02-16 02:05:40 +00:00
|
|
|
return Ok(());
|
|
|
|
}
|
2023-02-08 02:54:04 +00:00
|
|
|
if args.verbose {
|
|
|
|
println!("extracting {path}");
|
|
|
|
}
|
|
|
|
let file_path = output.join(
|
2023-02-16 02:05:40 +00:00
|
|
|
full_path
|
2023-02-08 02:54:04 +00:00
|
|
|
.strip_prefix(prefix)
|
|
|
|
.map_err(|_| repak::Error::Other("prefix does not match"))?,
|
|
|
|
);
|
|
|
|
if !file_path.clean().starts_with(&output) {
|
|
|
|
return Err(repak::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(
|
|
|
|
&path,
|
|
|
|
&mut BufReader::new(file.as_ref().unwrap()), // TODO: avoid this unwrap
|
|
|
|
&mut fs::File::create(file_path)?,
|
|
|
|
)
|
|
|
|
},
|
|
|
|
)
|
2023-02-01 03:41:36 +00:00
|
|
|
}
|
|
|
|
|
2023-02-01 23:44:57 +00:00
|
|
|
fn pack(args: ActionPack) -> Result<(), repak::Error> {
|
2023-02-01 03:41:36 +00:00
|
|
|
let output = args
|
|
|
|
.output
|
|
|
|
.map(PathBuf::from)
|
|
|
|
.unwrap_or_else(|| Path::new(&args.input).with_extension("pak"));
|
|
|
|
|
|
|
|
fn collect_files(paths: &mut Vec<PathBuf>, dir: &Path) -> io::Result<()> {
|
2023-02-01 05:14:39 +00:00
|
|
|
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());
|
2023-02-01 03:41:36 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
Ok(())
|
|
|
|
}
|
|
|
|
let input_path = Path::new(&args.input);
|
2023-02-01 05:14:39 +00:00
|
|
|
if !input_path.is_dir() {
|
2023-02-01 23:44:57 +00:00
|
|
|
return Err(repak::Error::Other("input is not a directory"));
|
2023-02-01 05:14:39 +00:00
|
|
|
}
|
2023-02-01 03:41:36 +00:00
|
|
|
let mut paths = vec![];
|
|
|
|
collect_files(&mut paths, input_path)?;
|
|
|
|
paths.sort();
|
|
|
|
|
2023-02-01 23:44:57 +00:00
|
|
|
let mut pak = repak::PakWriter::new(
|
2023-02-01 03:41:36 +00:00
|
|
|
BufWriter::new(File::create(output)?),
|
|
|
|
None,
|
2023-02-08 04:21:09 +00:00
|
|
|
args.version,
|
2023-02-01 03:41:36 +00:00
|
|
|
args.mount_point,
|
2023-02-08 04:21:09 +00:00
|
|
|
Some(args.path_hash_seed),
|
2023-02-01 03:41:36 +00:00
|
|
|
);
|
|
|
|
|
|
|
|
for p in paths {
|
2023-02-01 04:55:37 +00:00
|
|
|
let rel = &p
|
|
|
|
.strip_prefix(input_path)
|
|
|
|
.expect("file not in input directory")
|
|
|
|
.to_string_lossy();
|
|
|
|
if args.verbose {
|
|
|
|
println!("packing {}", &rel);
|
|
|
|
}
|
2023-02-01 05:14:52 +00:00
|
|
|
pak.write_file(rel, &mut BufReader::new(File::open(&p)?))?;
|
2023-02-01 03:41:36 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
pak.write_index()?;
|
|
|
|
|
|
|
|
Ok(())
|
|
|
|
}
|