use std::path::{Path, PathBuf};
use indicatif::{ProgressBar, ProgressStyle};
use nsz_rs::keys::Keys;
use nsz_rs::pipeline::{compress_nsp, decompress_nsz, Compression};
#[derive(Clone, Copy, PartialEq, Eq)]
enum Mode {
Compress,
Decompress,
}
#[derive(Clone, Copy, PartialEq, Eq)]
enum Stream {
Solid,
Block,
}
struct Args {
mode: Mode,
stream: Stream,
fix_padding: bool,
ldm: bool,
level: i32,
block_exp: i8,
output_dir: Option<PathBuf>,
keys_path: Option<PathBuf>,
skip_key_check: bool,
inputs: Vec<PathBuf>,
}
fn default_keys_path() -> PathBuf {
let home = std::env::var("HOME").unwrap_or_default();
PathBuf::from(home).join(".switch").join("prod.keys")
}
fn parse_args(argv: &[String]) -> Result<Args, String> {
let mut mode: Option<Mode> = None;
let mut stream = Stream::Solid;
let mut fix_padding = false;
let mut ldm = false;
let mut level: i32 = 18;
let mut block_exp: i8 = 20;
let mut output_dir: Option<PathBuf> = None;
let mut keys_path: Option<PathBuf> = None;
let mut skip_key_check = false;
let mut inputs: Vec<PathBuf> = Vec::new();
let mut i = 0;
while i < argv.len() {
let a = argv[i].as_str();
let (flag, inline) = match a.split_once('=') {
Some((f, v)) => (f, Some(v.to_string())),
None => (a, None),
};
let mut take_value = |name: &str| -> Result<String, String> {
if let Some(v) = inline.clone() {
return Ok(v);
}
i += 1;
argv.get(i)
.cloned()
.ok_or_else(|| format!("{name} requires a value"))
};
match flag {
"-C" | "--compress" => mode = Some(Mode::Compress),
"-D" | "--decompress" => mode = Some(Mode::Decompress),
"-F" | "--fix-padding" => fix_padding = true,
"-K" | "--keep" => {} "-L" | "--long-distance" => ldm = true,
"-P" | "--always-parse-cnmt" => {} "-S" | "--solid" => stream = Stream::Solid,
"-B" | "--block" => stream = Stream::Block,
"-l" | "--level" => {
level = take_value("-l")?
.parse()
.map_err(|_| "invalid level".to_string())?
}
"-s" | "--bs-exp" | "--block-size-exp" => {
block_exp = take_value("-s")?
.parse()
.map_err(|_| "invalid block size exponent".to_string())?
}
"-o" | "--output" => output_dir = Some(PathBuf::from(take_value("-o")?)),
"-k" | "--keys" => keys_path = Some(PathBuf::from(take_value("-k")?)),
"-x" | "--skip-key-check" => skip_key_check = true,
"-h" | "--help" => {
print_usage();
std::process::exit(0);
}
other => {
if other.starts_with('-') && other.len() > 1 {
return Err(format!("unknown flag: {other}"));
}
inputs.push(PathBuf::from(other));
}
}
i += 1;
}
let mode = mode.ok_or("specify -C (compress) or -D (decompress)")?;
if inputs.is_empty() {
return Err("no input files given".into());
}
Ok(Args {
mode,
stream,
fix_padding,
ldm,
level,
block_exp,
output_dir,
keys_path,
skip_key_check,
inputs,
})
}
fn print_usage() {
eprintln!(
"usage: nszrs (-C|-D) [-F] [-L] [-S|-B] [-l LEVEL] [-s EXP] [-o DIR] [-k KEYS] FILE...\n\
\n\
\x20 -C compress NSP->NSZ -D decompress NSZ->NSP\n\
\x20 -F fix padding -L long-distance matching\n\
\x20 -S solid (default) -B block (parallel)\n\
\x20 -l level (18) -s block-size exponent (20)\n\
\x20 -o output dir -k prod.keys path (~/.switch/prod.keys)"
);
}
fn out_path(input: &Path, dir: Option<&PathBuf>, new_ext: &str) -> PathBuf {
let stem = input
.file_stem()
.map(|s| s.to_string_lossy().to_string())
.unwrap_or_else(|| "out".into());
let base = match dir {
Some(d) => d.clone(),
None => input
.parent()
.map(|p| p.to_path_buf())
.unwrap_or_else(|| PathBuf::from(".")),
};
base.join(format!("{stem}{new_ext}"))
}
fn main() {
let argv: Vec<String> = std::env::args().skip(1).collect();
let args = match parse_args(&argv) {
Ok(a) => a,
Err(e) => {
eprintln!("nszrs: {e}");
print_usage();
std::process::exit(2);
}
};
let keys_path = args.keys_path.clone().unwrap_or_else(default_keys_path);
let keys = || Keys::load(&keys_path, !args.skip_key_check);
let style = ProgressStyle::with_template(
"{prefix:>13.bold} [{bar:30}] {percent:>3}% {binary_bytes_per_sec:>12} ETA {eta:>3} {wide_msg}",
)
.unwrap()
.progress_chars("=> ");
let mut failures = 0usize;
for input in &args.inputs {
let size = match std::fs::metadata(input) {
Ok(m) => m.len(),
Err(e) => {
eprintln!("nszrs: {}: {e}", input.display());
failures += 1;
continue;
}
};
let bar = ProgressBar::new(size).with_style(style.clone());
bar.set_prefix(match args.mode {
Mode::Compress => "Compressing",
Mode::Decompress => "Decompressing",
});
bar.set_message(
input
.file_name()
.unwrap_or_default()
.to_string_lossy()
.into_owned(),
);
let mut progress = |n| bar.inc(n);
let result = match args.mode {
Mode::Decompress => {
let out = out_path(input, args.output_dir.as_ref(), ".nsp");
decompress_nsz(
input,
&out,
keys,
args.fix_padding,
true,
true,
&mut progress,
)
.map(|_| out)
}
Mode::Compress => {
let out = out_path(input, args.output_dir.as_ref(), ".nsz");
let compression = Compression {
level: args.level,
ldm: args.ldm,
block_size_exponent: match args.stream {
Stream::Solid => None,
Stream::Block => Some(args.block_exp),
},
};
compress_nsp(
input,
&out,
keys,
&compression,
args.fix_padding,
&mut progress,
)
.map(|()| out)
}
};
bar.finish_and_clear();
match result {
Ok(out) => println!("nszrs: wrote {}", out.display()),
Err(e) => {
eprintln!("nszrs: {}: {e}", input.display());
failures += 1;
}
}
}
if failures > 0 {
std::process::exit(1);
}
}