use anyhow::anyhow;
use crate::bench::BenchConfig;
use crate::cli::arg_utils::{long_command_w_arg, read_u32_from_str};
use crate::cli::constants::{display_level, set_display_level, AUTHOR, COMPRESSOR_NAME, IO_MT};
use crate::cli::help::{print_long_help, print_usage_advanced};
use crate::cli::init::CliInit;
use crate::cli::op_mode::OpMode;
use crate::displaylevel;
use crate::hc::types::LZ4HC_CLEVEL_MAX;
use crate::io::file_io::{NULL_OUTPUT, NUL_MARK, STDIN_MARK, STDOUT_MARK};
use crate::io::prefs::{BlockMode, Prefs};
const LEGACY_BLOCK_SIZE: usize = 8 * (1 << 20);
#[derive(Debug)]
pub struct ParsedArgs {
pub prefs: Prefs,
pub op_mode: OpMode,
pub c_level: i32,
pub c_level_last: i32,
pub legacy_format: bool,
pub force_stdout: bool,
pub force_overwrite: bool,
pub main_pause: bool,
pub multiple_inputs: bool,
pub nb_workers: usize,
pub input_filename: Option<String>,
pub output_filename: Option<String>,
pub dictionary_filename: Option<String>,
pub in_file_names: Vec<String>,
#[cfg(feature = "recursive")]
pub recursive: bool,
pub block_size: usize,
pub bench_config: BenchConfig,
pub exit_early: bool,
pub exe_name: String,
}
pub fn parse_args(init: CliInit) -> anyhow::Result<ParsedArgs> {
let exe_name = std::env::args().next().unwrap_or_default();
let argv: Vec<String> = std::env::args().skip(1).collect();
parse_args_from(init, &exe_name, &argv)
}
pub fn parse_args_from(
init: CliInit,
exe_name: &str,
argv: &[String],
) -> anyhow::Result<ParsedArgs> {
let CliInit {
mut prefs,
op_mode: init_op_mode,
lz4c_legacy,
multiple_inputs: init_multiple_inputs,
c_level: init_c_level,
nb_workers: init_nb_workers,
force_stdout: init_force_stdout,
output_filename: init_output_filename,
display_level_override: _,
} = init;
let mut op_mode = init_op_mode;
let mut c_level: i32 = init_c_level;
let mut c_level_last: i32 = -10_000; let mut legacy_format = false;
let mut force_stdout = init_force_stdout;
let mut force_overwrite = false;
let mut main_pause = false;
let mut multiple_inputs = init_multiple_inputs;
let mut all_arguments_are_files = false;
let mut nb_workers: usize = init_nb_workers;
let mut input_filename: Option<String> = None;
let mut output_filename: Option<String> = init_output_filename;
let mut dictionary_filename: Option<String> = None;
let mut in_file_names: Vec<String> = Vec::new();
#[cfg(feature = "recursive")]
let mut recursive = false;
let mut block_size: usize = prefs.block_size; let mut bench_config = BenchConfig::default();
let mut exit_early = false;
let exe_name_str = exe_name.to_owned();
let mut arg_idx = 0usize;
while arg_idx < argv.len() {
let argument = &argv[arg_idx];
if argument.is_empty() {
arg_idx += 1;
continue;
}
let bytes = argument.as_bytes();
if all_arguments_are_files || bytes[0] != b'-' {
if multiple_inputs {
in_file_names.push(argument.clone());
} else if input_filename.is_none() {
input_filename = Some(argument.clone());
} else if output_filename.is_none() {
let s = if argument == NULL_OUTPUT {
NUL_MARK.to_owned()
} else {
argument.clone()
};
output_filename = Some(s);
} else {
if force_overwrite {
displaylevel!(
1,
"Warning: {} won't be used ! Do you want multiple input files (-m) ? \n",
argument
);
} else {
return Err(anyhow!(
"Error: {} won't be used ! Do you want multiple input files (-m) ?",
argument
));
}
}
arg_idx += 1;
continue;
}
if bytes.len() == 1 {
if input_filename.is_none() {
input_filename = Some(STDIN_MARK.to_owned());
} else {
output_filename = Some(STDOUT_MARK.to_owned());
}
arg_idx += 1;
continue;
}
if bytes[1] == b'-' {
if argument == "--" {
all_arguments_are_files = true;
arg_idx += 1;
continue;
}
if argument == "--compress" {
op_mode = OpMode::Compress;
} else if argument == "--decompress" || argument == "--uncompress" {
if op_mode != OpMode::Bench {
op_mode = OpMode::Decompress;
}
bench_config.set_decode_only(true);
} else if argument == "--multiple" {
multiple_inputs = true;
} else if argument == "--test" {
op_mode = OpMode::Test;
} else if argument == "--force" {
prefs.set_overwrite(true);
} else if argument == "--no-force" {
prefs.set_overwrite(false);
} else if argument == "--stdout" || argument == "--to-stdout" {
force_stdout = true;
output_filename = Some(STDOUT_MARK.to_owned());
} else if argument == "--frame-crc" {
prefs.set_stream_checksum_mode(true);
bench_config.set_skip_checksums(false);
} else if argument == "--no-frame-crc" {
prefs.set_stream_checksum_mode(false);
bench_config.set_skip_checksums(true);
} else if argument == "--no-crc" {
prefs.set_stream_checksum_mode(false);
prefs.set_block_checksum_mode(false);
bench_config.set_skip_checksums(true);
} else if argument == "--content-size" {
prefs.set_content_size(true);
} else if argument == "--no-content-size" {
prefs.set_content_size(false);
} else if argument == "--list" {
op_mode = OpMode::List;
multiple_inputs = true;
} else if argument == "--sparse" {
prefs.sparse_file_support = 2;
} else if argument == "--no-sparse" {
prefs.sparse_file_support = 0;
} else if argument == "--favor-decSpeed" {
prefs.favor_dec_speed(true);
} else if argument == "--verbose" {
let lvl = display_level().saturating_add(1);
set_display_level(lvl);
} else if argument == "--quiet" {
let lvl = display_level();
if lvl > 0 {
set_display_level(lvl - 1);
}
} else if argument == "--version" {
print_welcome_message(exe_name);
exit_early = true;
break;
} else if argument == "--help" {
print_usage_advanced(exe_name);
exit_early = true;
break;
} else if argument == "--keep" {
prefs.set_remove_src_file(false);
} else if argument == "--rm" {
prefs.set_remove_src_file(true);
} else if let Some(rest) = long_command_w_arg(argument, "--threads") {
let (val, rest_pos) = parse_next_uint32(rest, argv, &mut arg_idx, exe_name)?;
if !rest_pos.is_empty() {
return Err(anyhow!(
"bad usage: --threads: only numeric values are allowed"
));
}
nb_workers = val as usize;
} else if let Some(rest) = long_command_w_arg(argument, "--fast") {
if let Some(value_str) = rest.strip_prefix('=') {
if let Some((fast_level, remainder)) = read_u32_from_str(value_str) {
if !remainder.is_empty() {
return Err(anyhow!("bad usage: --fast: invalid argument"));
}
if fast_level == 0 {
return Err(anyhow!("bad usage: --fast: level must be > 0"));
}
c_level = -(fast_level as i32);
} else {
return Err(anyhow!("bad usage: --fast: expected a numeric level"));
}
} else if rest.is_empty() {
c_level = -1; } else {
return Err(anyhow!(
"bad usage: --fast: unexpected characters after option"
));
}
} else if argument == "--best" {
c_level = LZ4HC_CLEVEL_MAX;
} else {
return Err(anyhow!("bad usage: unknown option: {}", argument));
}
arg_idx += 1;
continue;
}
let mut char_pos: usize = 1; while char_pos < bytes.len() {
if lz4c_legacy {
let rest = &argument[char_pos..];
if rest.starts_with("c0") {
c_level = 0;
char_pos += 2;
continue;
}
if rest.starts_with("c1") {
c_level = 9;
char_pos += 2;
continue;
}
if rest.starts_with("c2") {
c_level = 12;
char_pos += 2;
continue;
}
if rest.starts_with("hc") {
c_level = 12;
char_pos += 2;
continue;
}
if rest.starts_with('y') {
prefs.set_overwrite(true);
char_pos += 1;
continue;
}
}
if bytes[char_pos].is_ascii_digit() {
let (val, remainder) = read_u32_from_str(&argument[char_pos..])
.expect("is_ascii_digit guarantees at least one digit");
c_level = val as i32;
let consumed = argument[char_pos..].len() - remainder.len();
char_pos += consumed; char_pos = char_pos.saturating_sub(1);
char_pos += 1;
continue;
}
match bytes[char_pos] {
b'V' => {
print_welcome_message(exe_name);
exit_early = true;
break; }
b'h' => {
print_usage_advanced(exe_name);
exit_early = true;
break;
}
b'H' => {
print_long_help(exe_name);
exit_early = true;
break;
}
b'e' => {
let next = char_pos + 1;
if next < bytes.len() && bytes[next].is_ascii_digit() {
let (val, remainder) = read_u32_from_str(&argument[next..]).unwrap();
c_level_last = val as i32;
let consumed = argument[next..].len() - remainder.len();
char_pos = next + consumed - 1;
} else {
return Err(anyhow!("bad usage: -e requires a numeric argument"));
}
}
b'z' => {
op_mode = OpMode::Compress;
}
b'T' => {
let next = char_pos + 1;
if next < bytes.len() && bytes[next].is_ascii_digit() {
let (val, remainder) = read_u32_from_str(&argument[next..]).unwrap();
nb_workers = val as usize;
let consumed = argument[next..].len() - remainder.len();
char_pos = next + consumed - 1;
} else if next >= bytes.len() {
arg_idx += 1;
if arg_idx >= argv.len() {
return Err(anyhow!("bad usage: -T requires a numeric argument"));
}
let (val, _rest) = read_u32_from_str(&argv[arg_idx])
.ok_or_else(|| anyhow!("bad usage: -T: expected numeric value"))?;
nb_workers = val as usize;
char_pos = bytes.len() - 1; } else {
return Err(anyhow!("bad usage: -T requires a numeric argument"));
}
}
b'D' => {
let next = char_pos + 1;
if next >= bytes.len() {
arg_idx += 1;
if arg_idx >= argv.len() {
return Err(anyhow!("bad usage: -D requires a path argument"));
}
dictionary_filename = Some(argv[arg_idx].clone());
} else {
dictionary_filename = Some(argument[next..].to_owned());
}
char_pos = bytes.len() - 1;
}
b'l' => {
legacy_format = true;
block_size = LEGACY_BLOCK_SIZE;
prefs.block_size = block_size;
}
b'd' => {
if op_mode != OpMode::Bench {
op_mode = OpMode::Decompress;
}
bench_config.set_decode_only(true);
}
b'c' => {
force_stdout = true;
output_filename = Some(STDOUT_MARK.to_owned());
prefs.set_pass_through(true);
}
b't' => {
op_mode = OpMode::Test;
}
b'f' => {
force_overwrite = true;
prefs.set_overwrite(true);
}
b'v' => {
let lvl = display_level().saturating_add(1);
set_display_level(lvl);
}
b'q' => {
let lvl = display_level();
if lvl > 0 {
set_display_level(lvl - 1);
}
}
b'k' => {
prefs.set_remove_src_file(false);
}
b'B' => {
let mut j = char_pos + 1;
loop {
if j >= bytes.len() {
break;
}
match bytes[j] {
b'D' => {
prefs.set_block_mode(BlockMode::Linked);
j += 1;
}
b'I' => {
prefs.set_block_mode(BlockMode::Independent);
j += 1;
}
b'X' => {
prefs.set_block_checksum_mode(true);
j += 1;
}
c if c.is_ascii_digit() => {
let (b_val, remainder) = read_u32_from_str(&argument[j..]).unwrap();
let consumed = argument[j..].len() - remainder.len();
j += consumed;
if b_val < 4 {
return Err(anyhow!("bad usage: block size ID must be >= 4"));
}
if b_val <= 7 {
block_size = prefs.set_block_size_id(b_val);
bench_config.set_block_size(block_size);
displaylevel!(
2,
"using blocks of size {} KB \n",
block_size >> 10
);
} else {
if b_val < 32 {
return Err(anyhow!(
"bad usage: block size must be >= 32 bytes when > 7"
));
}
block_size = prefs.set_block_size(b_val as usize);
bench_config.set_block_size(block_size);
if block_size >= 1024 {
displaylevel!(
2,
"using blocks of size {} KB \n",
block_size >> 10
);
} else {
displaylevel!(
2,
"using blocks of size {} bytes \n",
block_size
);
}
}
}
_ => break, }
}
char_pos = j.saturating_sub(1);
}
b'b' => {
op_mode = OpMode::Bench;
multiple_inputs = true;
}
b'S' => {
bench_config.set_bench_separately(true);
}
b'r' => {
#[cfg(feature = "recursive")]
{
recursive = true;
}
multiple_inputs = true;
}
b'm' => {
multiple_inputs = true;
}
b'i' => {
let next = char_pos + 1;
if next < bytes.len() && bytes[next].is_ascii_digit() {
let (iters, remainder) = read_u32_from_str(&argument[next..]).unwrap();
let consumed = argument[next..].len() - remainder.len();
bench_config.set_notification_level(display_level());
bench_config.set_nb_seconds(iters);
char_pos = next + consumed - 1;
} else {
return Err(anyhow!("bad usage: -i requires a numeric argument"));
}
}
b'p' => {
main_pause = true;
}
_ => {
return Err(anyhow!(
"bad usage: unrecognised option: -{c}",
c = bytes[char_pos] as char
));
}
}
if exit_early {
break; }
char_pos += 1;
}
if exit_early {
break; }
arg_idx += 1;
}
Ok(ParsedArgs {
prefs,
op_mode,
c_level,
c_level_last,
legacy_format,
force_stdout,
force_overwrite,
main_pause,
multiple_inputs,
nb_workers,
input_filename,
output_filename,
dictionary_filename,
in_file_names,
#[cfg(feature = "recursive")]
recursive,
block_size,
bench_config,
exit_early,
exe_name: exe_name_str,
})
}
fn print_welcome_message(exe_name: &str) {
let bits = (std::mem::size_of::<usize>() * 8) as u32;
println!(
"*** {} v{} {}-bit {}, by {} ***",
COMPRESSOR_NAME,
env!("CARGO_PKG_VERSION"),
bits,
IO_MT,
AUTHOR
);
let _ = exe_name; }
fn parse_next_uint32<'a>(
rest: &'a str,
argv: &[String],
arg_idx: &mut usize,
exe_name: &str,
) -> anyhow::Result<(u32, &'a str)> {
if let Some(value_str) = rest.strip_prefix('=') {
let (val, suffix) = read_u32_from_str(value_str)
.ok_or_else(|| anyhow!("bad usage: {} expected numeric argument", exe_name))?;
Ok((val, suffix))
} else if rest.is_empty() {
*arg_idx += 1;
let next = argv
.get(*arg_idx)
.ok_or_else(|| anyhow!("bad usage: {}: missing command argument", exe_name))?;
if next.starts_with('-') {
return Err(anyhow!(
"bad usage: {}: option argument cannot be another option",
exe_name
));
}
let (val, suffix) = read_u32_from_str(next)
.ok_or_else(|| anyhow!("bad usage: {}: expected numeric argument", exe_name))?;
let _ = suffix;
Ok((val, ""))
} else {
Err(anyhow!(
"bad usage: {}: unexpected text after option",
exe_name
))
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::cli::init::detect_alias;
use crate::cli::op_mode::OpMode;
fn make_args(args: &[&str]) -> Vec<String> {
args.iter().map(|s| s.to_string()).collect()
}
fn parse(args: &[&str]) -> ParsedArgs {
let init = detect_alias("lz4");
parse_args_from(init, "lz4", &make_args(args)).expect("parse failed")
}
fn parse_err(args: &[&str]) -> anyhow::Error {
let init = detect_alias("lz4");
parse_args_from(init, "lz4", &make_args(args)).expect_err("expected error")
}
#[test]
fn short_level_9() {
let p = parse(&["-9"]);
assert_eq!(p.c_level, 9);
}
#[test]
fn short_level_12() {
let p = parse(&["-12"]);
assert_eq!(p.c_level, 12);
}
#[test]
fn best_flag() {
let p = parse(&["--best"]);
assert_eq!(p.c_level, LZ4HC_CLEVEL_MAX);
}
#[test]
fn fast_default() {
let p = parse(&["--fast"]);
assert_eq!(p.c_level, -1);
}
#[test]
fn fast_equals_3() {
let p = parse(&["--fast=3"]);
assert_eq!(p.c_level, -3);
}
#[test]
fn compress_flag() {
let p = parse(&["--compress"]);
assert_eq!(p.op_mode, OpMode::Compress);
}
#[test]
fn short_compress_flag() {
let p = parse(&["-z"]);
assert_eq!(p.op_mode, OpMode::Compress);
}
#[test]
fn decompress_flag() {
let p = parse(&["--decompress"]);
assert_eq!(p.op_mode, OpMode::Decompress);
}
#[test]
fn uncompress_alias() {
let p = parse(&["--uncompress"]);
assert_eq!(p.op_mode, OpMode::Decompress);
}
#[test]
fn short_decompress_flag() {
let p = parse(&["-d"]);
assert_eq!(p.op_mode, OpMode::Decompress);
}
#[test]
fn test_mode() {
let p = parse(&["--test"]);
assert_eq!(p.op_mode, OpMode::Test);
}
#[test]
fn short_test_mode() {
let p = parse(&["-t"]);
assert_eq!(p.op_mode, OpMode::Test);
}
#[test]
fn list_mode() {
let p = parse(&["--list"]);
assert_eq!(p.op_mode, OpMode::List);
assert!(p.multiple_inputs);
}
#[test]
fn bench_mode() {
let p = parse(&["-b"]);
assert_eq!(p.op_mode, OpMode::Bench);
assert!(p.multiple_inputs);
}
#[test]
fn aggregated_9fv() {
let init = detect_alias("lz4");
let lvl_before = display_level();
let p = parse_args_from(init, "lz4", &make_args(&["-9fv"])).unwrap();
assert_eq!(p.c_level, 9);
assert!(p.force_overwrite);
assert!(display_level() > lvl_before);
set_display_level(lvl_before);
}
#[test]
fn block_size_id_4() {
let p = parse(&["-B4"]);
assert_eq!(p.prefs.block_size_id, 4);
}
#[test]
fn block_size_id_7() {
let p = parse(&["-B7"]);
assert_eq!(p.prefs.block_size_id, 7);
}
#[test]
fn block_linked() {
let p = parse(&["-BD"]);
assert!(!p.prefs.block_independence);
}
#[test]
fn block_independent() {
let p = parse(&["-BI"]);
assert!(p.prefs.block_independence);
}
#[test]
fn block_checksum() {
let p = parse(&["-BX"]);
assert!(p.prefs.block_checksum);
}
#[test]
fn threads_long_equals() {
let p = parse(&["--threads=2"]);
assert_eq!(p.nb_workers, 2);
}
#[test]
fn threads_long_space() {
let p = parse(&["--threads", "2"]);
assert_eq!(p.nb_workers, 2);
}
#[test]
fn threads_short_inline() {
let p = parse(&["-T4"]);
assert_eq!(p.nb_workers, 4);
}
#[test]
fn dictionary_inline() {
let p = parse(&["-Ddict.bin"]);
assert_eq!(p.dictionary_filename.as_deref(), Some("dict.bin"));
}
#[test]
fn dictionary_separate() {
let p = parse(&["-D", "dict.bin"]);
assert_eq!(p.dictionary_filename.as_deref(), Some("dict.bin"));
}
#[test]
fn input_file() {
let p = parse(&["input.txt"]);
assert_eq!(p.input_filename.as_deref(), Some("input.txt"));
}
#[test]
fn input_and_output() {
let p = parse(&["input.txt", "output.lz4"]);
assert_eq!(p.input_filename.as_deref(), Some("input.txt"));
assert_eq!(p.output_filename.as_deref(), Some("output.lz4"));
}
#[test]
fn null_output_translated() {
let p = parse(&["input.txt", "null"]);
assert_eq!(p.output_filename.as_deref(), Some(NUL_MARK));
}
#[test]
fn stdin_dash() {
let p = parse(&["-"]);
assert_eq!(p.input_filename.as_deref(), Some(STDIN_MARK));
}
#[test]
fn multiple_inputs_flag() {
let p = parse(&["-m", "a.txt", "b.txt"]);
assert!(p.multiple_inputs);
assert_eq!(p.in_file_names, vec!["a.txt", "b.txt"]);
}
#[test]
fn end_of_options_sentinel() {
let p = parse(&["--", "-not-a-flag"]);
assert_eq!(p.input_filename.as_deref(), Some("-not-a-flag"));
}
#[test]
fn force_flag() {
let p = parse(&["--force"]);
assert!(p.prefs.overwrite);
}
#[test]
fn keep_flag() {
let p = parse(&["--keep"]);
assert!(!p.prefs.remove_src_file);
}
#[test]
fn no_frame_crc() {
let p = parse(&["--no-frame-crc"]);
assert!(!p.prefs.stream_checksum);
assert!(p.bench_config.skip_checksums);
}
#[test]
fn content_size() {
let p = parse(&["--content-size"]);
assert!(p.prefs.content_size_flag);
}
#[test]
fn sparse_flag() {
let p = parse(&["--sparse"]);
assert_eq!(p.prefs.sparse_file_support, 2);
}
#[test]
fn no_sparse_flag() {
let p = parse(&["--no-sparse"]);
assert_eq!(p.prefs.sparse_file_support, 0);
}
#[test]
fn version_flag_exit_early() {
let p = parse(&["--version"]);
assert!(p.exit_early);
}
#[test]
fn short_version_flag_exit_early() {
let p = parse(&["-V"]);
assert!(p.exit_early);
}
#[test]
fn help_flag_exit_early() {
let p = parse(&["--help"]);
assert!(p.exit_early);
}
#[test]
fn legacy_c1_sets_level_9() {
let init = detect_alias("lz4c");
let p = parse_args_from(init, "lz4c", &make_args(&["-c1"])).unwrap();
assert_eq!(p.c_level, 9);
}
#[test]
fn legacy_hc_sets_level_12() {
let init = detect_alias("lz4c");
let p = parse_args_from(init, "lz4c", &make_args(&["-hc"])).unwrap();
assert_eq!(p.c_level, 12);
}
#[test]
fn unknown_long_option() {
let e = parse_err(&["--unknown-option"]);
assert!(e.to_string().contains("bad usage"));
}
#[test]
fn bad_block_size_under_4() {
let e = parse_err(&["-B3"]);
assert!(e.to_string().contains("bad usage"));
}
#[test]
fn fast_zero_level_is_error() {
let e = parse_err(&["--fast=0"]);
assert!(e.to_string().contains("bad usage"));
}
}