#![allow(non_snake_case)]
use std::{env, fs, io, io::prelude::*, path, str};
fn main() {
let args: Vec<_> = env::args().collect();
let args: Vec<_> = args.iter().map(|s| &**s).collect();
match &args[1..] {
&[] => print!("{}", HELP_GENERAL),
&["help"] => print!("{}", HELP_GENERAL),
&[_] => eprintln!("Error invalid syntax, see `pakscmd help`."),
&["help", cmd] => help(&[cmd]),
&[_, _] => eprintln!("Error invalid syntax, see `pakscmd help`."),
&[_pak, _key, "help", ref args @ ..] => help(args),
&[paks, key, "new", ref args @ ..] => new(paks, key, args),
&[paks, key, "tree", ref args @ ..] => tree(paks, key, args),
&[paks, key, "add", ref args @ ..] => add(paks, key, args),
&[paks, key, "copy", ref args @ ..] => copy(paks, key, args),
&[paks, key, "link", ref args @ ..] => link(paks, key, args),
&[paks, key, "cat", ref args @ ..] => cat(paks, key, args),
&[paks, key, "rm", ref args @ ..] => rm(paks, key, args),
&[paks, key, "mv", ref args @ ..] => mv(paks, key, args),
&[paks, key, "fsck", ref args @ ..] => fsck(paks, key, args),
&[paks, key, "gc", ref args @ ..] => gc(paks, key, args),
&[paks, key, "dbg", ref args @ ..] => dbg(paks, key, args),
&[_pak, _key, cmd, ..] => eprintln!("Error unknown subcommand: {}", cmd),
}
}
fn parse_key(s: &str) -> Option<paks::Key> {
match paks::parse_key(s) {
Ok(key) => {
Some(key)
},
Err(err) => {
eprintln!("Error parsing key argument: {}", err);
None
},
}
}
const HELP_GENERAL: &str = "\
pakscmd - Copyright (c) 2020-2025 Casper <CasualX@users.noreply.github.com>
USAGE
pakscmd help <COMMAND>
pakscmd <PAKFILE> <KEY> <COMMAND> [..]
ARGUMENTS
PAKFILE Path to a PAKS archive to create or edit.
KEY The 128-bit encryption key encoded in hex.
COMMAND The subcommand to invoke.
Commands are:
new Creates a new empty PAKS archive.
tree Displays the directory of the PAKS archive.
add Adds a file to the PAKS archive.
copy Copies files to the PAKS archive.
link Links the file from alternative paths.
cat Reads files from the PAKS archive and writes to stdout.
rm Removes paths from the PAKS archive.
mv Moves files in the PAKS archive.
fsck File system consistency check.
gc Collects garbage left behind by removed files.
See `pakscmd help <COMMAND>` for more information on a specific command.
EXAMPLES
pakscmd example.paks 0 new
pakscmd example.paks 0 add a/b/example < tests/data/example.txt
pakscmd example.paks 0 link a/b/example aa/bb/example
pakscmd example.paks 0 tree -u
pakscmd example.paks 0 rm a/b/example
pakscmd example.paks 0 cat aa/bb/example
";
fn help(args: &[&str]) {
let text = match args.first().cloned() {
None => HELP_GENERAL,
Some("new") => HELP_NEW,
Some("tree") => HELP_TREE,
Some("add") => HELP_ADD,
Some("copy") => HELP_COPY,
Some("link") => HELP_LINK,
Some("cat") => HELP_CAT,
Some("rm") => HELP_RM,
Some("mv") => HELP_MV,
Some("fsck") => HELP_FSCK,
Some("gc") => HELP_GC,
Some(cmd) => return eprintln!("Error unknown subcommand: {}", cmd),
};
print!("{}", text);
}
const HELP_NEW: &str = "\
NAME
pakscmd-new - Creates a new empty PAKS archive.
DESCRIPTION
Creates a new empty PAKS archive with the given file name and encryption key.
If a file with this name already exists it will be overwritten.
";
fn new(file: &str, key: &str, _args: &[&str]) {
let ref key = match parse_key(key) {
Some(key) => key,
None => return,
};
if let Err(err) = paks::FileEditor::create_empty(file, key) {
eprintln!("Error writing {}: {}", file, err);
}
}
const HELP_TREE: &str = "\
NAME
pakscmd-tree - Displays the directory of the PAKS archive.
SYNOPSIS
pakscmd [..] tree [-au] [PATH]
DESCRIPTION
Displays the directory of the PAKS archive.
ARGUMENTS
-a Display using ASCII art.
-u Display using UNICODE art.
PATH Optional subdirectory to start at.
";
fn tree(file: &str, key: &str, mut args: &[&str]) {
let ref key = match parse_key(key) {
Some(key) => key,
None => return,
};
let mut art = &paks::TreeArt::UNICODE;
while let Some(head) = args.first().cloned() {
if head.starts_with("-") {
args = &args[1..];
match head {
"-a" => art = &paks::TreeArt::ASCII,
"-u" => art = &paks::TreeArt::UNICODE,
_ => eprintln!("Unknown argument: {}", head),
}
}
else {
break;
}
}
let path = match args {
&[path] => Some(path),
[..] => None,
};
let reader = match paks::FileReader::open(file, key) {
Ok(reader) => reader,
Err(err) => return eprintln!("Error opening {}: {}", file, err),
};
let display = match reader.display_children(path, art) {
Some(display) => display,
None => return eprintln!("Error directory not found or is a file: {}", path.unwrap_or("")),
};
println!("{}", display);
}
const HELP_ADD: &str = "\
NAME
pakscmd-add - Adds a file to the PAKS archive.
SYNOPSIS
pakscmd [..] add <PATH> < <CONTENT>
DESCRIPTION
Adds a file to the PAKS archive.
ARGUMENTS
PATH The destination path in the PAKS archive to put the file.
CONTENT The file data to write in the PAKS archive passed via stdin.
";
fn add(file: &str, key: &str, args: &[&str]) {
let ref key = match parse_key(key) {
Some(key) => key,
None => return,
};
let path = match args {
[path] => path,
_ => return eprintln!("Error invalid path: expected exactly 1 argument."),
};
let mut data = Vec::new();
match io::stdin().read_to_end(&mut data) {
Ok(_) => (),
Err(err) => return eprintln!("Error reading stdin: {}", err),
};
let mut edit = match paks::FileEditor::open(file, key) {
Ok(edit) => edit,
Err(err) => return eprintln!("Error opening {}: {}", file, err),
};
if let Err(err) = edit.create_file(path.as_bytes(), &data, key) {
eprintln!("Error creating {}: {}", path, err);
}
if let Err(err) = edit.finish(key) {
eprintln!("Error writing {}: {}", file, err);
}
}
const HELP_COPY: &str = "\
NAME
pakscmd-copy - Copies files to the PAKS archive.
SYNOPSIS
pakscmd [..] copy <PATH> [FILE]..
DESCRIPTION
Copies files to the PAKS archive.
";
fn copy(file: &str, key: &str, args: &[&str]) {
let ref key = match parse_key(key) {
Some(key) => key,
None => return,
};
if args.len() < 1 {
return eprintln!("Error invalid syntax: expecting one path followed by many filenames.");
}
else if args.len() == 1 {
return;
}
let base_path = args[0];
let mut edit = match paks::FileEditor::open(file, key) {
Ok(edit) => edit,
Err(err) => return eprintln!("Error opening {}: {}", file, err),
};
let mut dest_path = String::from(base_path);
for src_path in &args[1..] {
let src_path = path::Path::new(src_path);
let dest_len = dest_path.len();
copy_rec(&mut edit, src_path, &mut dest_path, true, key);
dest_path.truncate(dest_len);
}
if let Err(err) = edit.finish(key) {
eprintln!("Error writing {}: {}", file, err);
}
}
fn copy_rec(edit: &mut paks::FileEditor, src_path: &path::Path, dest_path: &mut String, root: bool, key: &paks::Key) {
if dest_path.len() > 0 && !dest_path.ends_with("/") {
dest_path.push_str("/");
}
if src_path.is_file() {
let data = match fs::read(src_path) {
Ok(data) => data,
Err(err) => {
eprintln!("Error reading {}: {}", src_path.display(), err);
return;
},
};
let file_name = match src_path.file_name().and_then(|s| s.to_str()) {
Some(file_name) => file_name,
None => {
eprintln!("Error invalid file name: {}", src_path.display());
return;
},
};
dest_path.push_str(file_name);
if let Err(err) = edit.create_file(dest_path.as_bytes(), &data, key) {
eprintln!("Error creating {}: {}", dest_path, err);
}
}
else if src_path.is_dir() {
if !root {
let dir_name = match src_path.file_name().and_then(|s| s.to_str()) {
Some(dir_name) => dir_name,
None => {
eprintln!("Error invalid directory name: {}", src_path.display());
return;
},
};
dest_path.push_str(dir_name);
edit.create_dir(dest_path.as_bytes());
}
let read_dir = match fs::read_dir(src_path) {
Ok(read_dir) => read_dir,
Err(err) => {
eprintln!("Error reading {}: {}", src_path.display(), err);
return;
},
};
for entry in read_dir {
let entry = match entry {
Ok(entry) => entry,
Err(err) => {
eprintln!("Error reading {}: {}", src_path.display(), err);
continue;
},
};
let dest_len = dest_path.len();
copy_rec(edit, &entry.path(), dest_path, false, key);
dest_path.truncate(dest_len);
}
}
else {
eprintln!("Warning skipping {}: not a file or directory", src_path.display());
}
}
const HELP_LINK: &str = "\
NAME
pakscmd-link - Links the file from alternative paths.
SYNOPSIS
pakscmd [..] link <SRC> [DEST]..
DESCRIPTION
Links the source file to alternative destination paths.
Returns file not found error if the SRC path does not exist.
ARGUMENTS
SRC Path to the source file to link.
DEST One or more destination paths where to link the SRC.
";
fn link(file: &str, key: &str, args: &[&str]) {
let ref key = match parse_key(key) {
Some(key) => key,
None => return,
};
let (src_path, dest_paths) = match args {
&[src, ref dest @ ..] => (src, dest),
_ => return eprintln!("Error invalid syntax: expecting a source file"),
};
let mut edit = match paks::FileEditor::open(file, key) {
Ok(edit) => edit,
Err(err) => return eprintln!("Error opening {}: {}", file, err),
};
let src_desc = match edit.find_desc(src_path.as_bytes()) {
Some(desc) if desc.is_dir() => return eprintln!("Error file not found: {}", src_path),
Some(desc) => *desc,
None => return eprintln!("Error file not found: {}", src_path),
};
for &dest_path in dest_paths {
edit.create_link(dest_path.as_bytes(), &src_desc);
}
if let Err(err) = edit.finish(key) {
eprintln!("Error writing {}: {}", file, err);
}
}
const HELP_CAT: &str = "\
NAME
pakscmd-cat - Reads files from the PAKS archive and writes to stdout.
SYNOPSIS
pakscmd [..] cat [PATH]..
DESCRIPTION
Reads files from the PAKS archive and writes to stdout.
Each file is read in the order specified and written to stdout one after another.
If an error happens it is printed and continues to write the rest of the files.
ARGUMENTS
PATH Path to the file in the PAKS archive to output.
";
fn cat(file: &str, key: &str, args: &[&str]) {
let ref key = match parse_key(key) {
Some(key) => key,
None => return,
};
let reader = match paks::FileReader::open(file, key) {
Ok(reader) => reader,
Err(err) => return eprintln!("Error opening {}: {}", file, err),
};
for &path in args {
match reader.find_file(path.as_bytes()) {
Some(file_desc) => {
match reader.read_data(&file_desc, key) {
Ok(data) => {
if let Err(err) = io::stdout().write_all(&data) {
eprintln!("Error writing {} to stdout: {}", path, err);
}
},
Err(err) => eprintln!("Error reading {}: {}", path, err),
}
},
None => eprintln!("Error file not found: {}", path),
}
}
}
const HELP_RM: &str = "\
NAME
pakscmd-rm - Removes files from the PAKS archive.
SYNOPSIS
pakscmd [..] rm [PATH]..
DESCRIPTION
Removes files from the PAKS archive.
ARGUMENTS
PATH Path to the file in the PAKS archive to remove.
";
fn rm(file: &str, key: &str, args: &[&str]) {
let ref key = match parse_key(key) {
Some(key) => key,
None => return,
};
let mut edit = match paks::FileEditor::open(file, key) {
Ok(edit) => edit,
Err(err) => return eprintln!("Error opening {}: {}", file, err),
};
for &path in args {
if edit.remove(path.as_bytes()).is_none() {
eprintln!("Unable to remove {}: file not found?", path);
}
}
if let Err(err) = edit.finish(key) {
eprintln!("Error writing {}: {}", file, err);
}
}
const HELP_MV: &str = "\
NAME
pakscmd-mv - Moves files in the PAKS archive.
SYNOPSIS
pakscmd [..] mv <SRC> <DEST>
DESCRIPTION
Moves files in the PAKS archive.
ARGUMENTS
SRC Path to the source file.
DEST Path to the destination file.
";
fn mv(file: &str, key: &str, args: &[&str]) {
let ref key = match parse_key(key) {
Some(key) => key,
None => return,
};
let (src_path, dest_path) = match args {
&[src_path, dest_path] => (src_path, dest_path),
[..] => return eprintln!("Error invalid syntax: expecting exactly two path arguments."),
};
let mut edit = match paks::FileEditor::open(file, key) {
Ok(edit) => edit,
Err(err) => return eprintln!("Error opening {}: {}", file, err),
};
edit.move_file(src_path.as_bytes(), dest_path.as_bytes());
if let Err(err) = edit.finish(key) {
eprintln!("Error writing {}: {}", file, err);
}
}
const HELP_FSCK: &str = "\
NAME
pakscmd-fsck - File system consistency check.
SYNOPSIS
pakscmd [..] fsck
DESCRIPTION
Checks the PAKS file's directory for errors.
";
fn fsck(file: &str, key: &str, _args: &[&str]) {
let ref key = match parse_key(key) {
Some(key) => key,
None => return,
};
let reader = match paks::FileReader::open(file, key) {
Ok(reader) => reader,
Err(err) => return eprintln!("Error opening {}: {}", file, err),
};
let mut log = String::new();
let msg = if !reader.fsck(reader.high_mark(), &mut log) {
"PAKS file contains errors:\n"
}
else {
"No errors found!\n"
};
print!("{}{}", msg, log);
}
const HELP_GC: &str = "\
NAME
pakscmd-gc - Collects garbage left behind by removed files.
SYNOPSIS
pakscmd [..] gc
DESCRIPTION
Collects garbage left behind by removed files.
When files are removed their data is left behind.
These files are unreadable because their cryptographic nonce is forgotten.
";
fn gc(file: &str, key: &str, _args: &[&str]) {
let ref key = match parse_key(key) {
Some(key) => key,
None => return,
};
let f = match fs::File::open(file) {
Ok(f) => f,
Err(err) => return eprintln!("Error opening {}: {}", file, err),
};
let blocks = match paks::read(f, key) {
Ok(blocks) => blocks,
Err(err) => return eprintln!("Error reading {}: {}", file, err),
};
let mut edit = match paks::MemoryEditor::from_blocks(blocks, key) {
Ok(edit) => edit,
Err(_) => return eprintln!("Error invalid {}: not a PAKS file", file),
};
edit.gc();
let (data, _) = edit.finish(key);
if let Err(err) = fs::write(file, dataview::bytes(data.as_slice())) {
eprintln!("Error writing {}: {}", file, err);
}
}
fn dbg(file: &str, key: &str, _args: &[&str]) {
let ref key = match parse_key(key) {
Some(key) => key,
None => return,
};
let reader = match paks::FileReader::open(file, key) {
Ok(reader) => reader,
Err(err) => return eprintln!("Error opening {}: {}", file, err),
};
print!("{:#?}", reader.as_ref());
}