use clap::Parser;
use clap::Subcommand;
use clap_complete::Shell;
use std::path::PathBuf;
#[derive(Parser)]
#[command(name = "exarch")]
#[command(author, version, about, long_about = None)]
#[command(propagate_version = true)]
pub struct Cli {
#[command(subcommand)]
pub command: Commands,
#[arg(short, long, global = true)]
pub verbose: bool,
#[arg(short, long, global = true, conflicts_with = "verbose")]
pub quiet: bool,
#[arg(short, long, global = true)]
pub json: bool,
}
#[derive(Subcommand)]
pub enum Commands {
Extract(ExtractArgs),
Create(CreateArgs),
List(ListArgs),
Verify(VerifyArgs),
Completion(CompletionArgs),
}
#[derive(clap::Args)]
pub struct CompletionArgs {
#[arg(value_enum)]
pub shell: Shell,
}
#[derive(clap::Args)]
pub struct ExtractArgs {
#[arg(value_name = "ARCHIVE")]
pub archive: PathBuf,
#[arg(value_name = "OUTPUT_DIR")]
pub output_dir: Option<PathBuf>,
#[arg(long, default_value = "10000")]
pub max_files: usize,
#[arg(long, value_parser = parse_byte_size)]
pub max_total_size: Option<u64>,
#[arg(long, value_parser = parse_byte_size)]
pub max_file_size: Option<u64>,
#[arg(long, default_value = "100", value_parser = clap::value_parser!(u32).range(1..))]
pub max_compression_ratio: u32,
#[arg(long)]
pub allow_symlinks: bool,
#[arg(long)]
pub allow_hardlinks: bool,
#[arg(long)]
pub allow_solid_archives: bool,
#[arg(long)]
pub allow_world_writable: bool,
#[arg(long)]
pub preserve_permissions: bool,
#[arg(long)]
pub force: bool,
#[arg(long)]
pub atomic: bool,
}
#[derive(clap::Args)]
pub struct CreateArgs {
#[arg(value_name = "OUTPUT")]
pub output: PathBuf,
#[arg(value_name = "SOURCE", required = true)]
pub sources: Vec<PathBuf>,
#[arg(short = 'l', long, value_parser = clap::value_parser!(u8).range(1..=9))]
pub compression_level: Option<u8>,
#[arg(long)]
pub follow_symlinks: bool,
#[arg(long)]
pub include_hidden: bool,
#[arg(long = "exclude", short = 'x', value_name = "PATTERN")]
pub exclude: Vec<String>,
#[arg(long, value_name = "PREFIX")]
pub strip_prefix: Option<PathBuf>,
#[arg(short = 'f', long)]
pub force: bool,
}
#[derive(clap::Args)]
pub struct ListArgs {
#[arg(value_name = "ARCHIVE")]
pub archive: PathBuf,
#[arg(short, long)]
pub long: bool,
#[arg(short = 'H', long)]
pub human_readable: bool,
#[arg(long, default_value = "10000")]
pub max_files: usize,
#[arg(long, value_parser = parse_byte_size)]
pub max_total_size: Option<u64>,
#[arg(long)]
pub allow_solid_archives: bool,
}
#[derive(clap::Args)]
pub struct VerifyArgs {
#[arg(value_name = "ARCHIVE")]
pub archive: PathBuf,
#[arg(long)]
pub check_integrity: bool,
#[arg(long)]
pub check_security: bool,
#[arg(long, default_value = "10000")]
pub max_files: usize,
#[arg(long, value_parser = parse_byte_size)]
pub max_total_size: Option<u64>,
#[arg(long)]
pub allow_solid_archives: bool,
}
#[allow(clippy::option_if_let_else)]
fn parse_byte_size(s: &str) -> Result<u64, String> {
let s = s.trim();
if s.is_empty() {
return Err("empty byte size".to_string());
}
let (num_str, multiplier) = if let Some(stripped) = s.strip_suffix('T') {
(stripped, 1024_u64.pow(4))
} else if let Some(stripped) = s.strip_suffix('G') {
(stripped, 1024_u64.pow(3))
} else if let Some(stripped) = s.strip_suffix('M') {
(stripped, 1024_u64.pow(2))
} else if let Some(stripped) = s.strip_suffix('K') {
(stripped, 1024)
} else {
(s, 1)
};
num_str
.parse::<u64>()
.map_err(|_| format!("invalid byte size: {s}"))
.and_then(|n| {
n.checked_mul(multiplier)
.ok_or_else(|| format!("byte size overflow: {s}"))
})
}
#[cfg(test)]
#[allow(clippy::unwrap_used)]
mod tests {
use super::*;
#[test]
fn test_parse_byte_size() {
assert_eq!(parse_byte_size("100").unwrap(), 100);
assert_eq!(parse_byte_size("1K").unwrap(), 1024);
assert_eq!(parse_byte_size("2M").unwrap(), 2 * 1024 * 1024);
assert_eq!(parse_byte_size("3G").unwrap(), 3 * 1024 * 1024 * 1024);
assert_eq!(parse_byte_size("1T").unwrap(), 1024_u64.pow(4));
assert!(parse_byte_size("invalid").is_err());
assert!(parse_byte_size("").is_err());
}
#[test]
fn test_allow_solid_archives_flag() {
let cli =
Cli::try_parse_from(["exarch", "extract", "a.7z", "--allow-solid-archives"]).unwrap();
let Commands::Extract(args) = cli.command else {
panic!("expected Extract command");
};
assert!(args.allow_solid_archives);
}
#[test]
fn test_parse_byte_size_overflow() {
assert!(parse_byte_size("18446744073709551615K").is_err()); assert!(parse_byte_size("18014398509481984M").is_err()); assert!(parse_byte_size("17592186044416G").is_err()); }
#[test]
fn test_list_args_default_max_files() {
let cli = Cli::parse_from(["exarch", "list", "archive.zip"]);
let Commands::List(args) = cli.command else {
panic!("expected list command");
};
assert_eq!(args.max_files, 10000);
assert!(args.max_total_size.is_none());
}
#[test]
fn test_list_args_max_files_override() {
let cli = Cli::parse_from(["exarch", "list", "--max-files", "99999", "archive.zip"]);
let Commands::List(args) = cli.command else {
panic!("expected list command");
};
assert_eq!(args.max_files, 99999);
}
#[test]
fn test_list_args_max_total_size_override() {
let cli = Cli::parse_from(["exarch", "list", "--max-total-size", "2G", "archive.zip"]);
let Commands::List(args) = cli.command else {
panic!("expected list command");
};
assert_eq!(args.max_total_size, Some(2 * 1024 * 1024 * 1024));
}
#[test]
fn test_verify_args_default_max_files() {
let cli = Cli::parse_from(["exarch", "verify", "archive.zip"]);
let Commands::Verify(args) = cli.command else {
panic!("expected verify command");
};
assert_eq!(args.max_files, 10000);
assert!(args.max_total_size.is_none());
}
#[test]
fn test_verify_args_max_files_override() {
let cli = Cli::parse_from(["exarch", "verify", "--max-files", "65537", "archive.zip"]);
let Commands::Verify(args) = cli.command else {
panic!("expected verify command");
};
assert_eq!(args.max_files, 65537);
}
#[test]
fn test_verify_args_max_total_size_override() {
let cli = Cli::parse_from([
"exarch",
"verify",
"--max-total-size",
"500M",
"archive.zip",
]);
let Commands::Verify(args) = cli.command else {
panic!("expected verify command");
};
assert_eq!(args.max_total_size, Some(500 * 1024 * 1024));
}
#[test]
fn test_list_args_both_flags() {
let cli = Cli::parse_from([
"exarch",
"list",
"--max-files",
"1000000",
"--max-total-size",
"10G",
"archive.zip",
]);
let Commands::List(args) = cli.command else {
panic!("expected list command");
};
assert_eq!(args.max_files, 1_000_000);
assert_eq!(args.max_total_size, Some(10 * 1024 * 1024 * 1024));
}
}