use clap::{Args, Parser, Subcommand, ValueEnum};
use rars::ArchiveVersion;
#[derive(Parser)]
#[command(
name = "rars",
version,
about = "Pure-Rust RAR archive toolkit",
long_about = "rars reads, writes, and repairs RAR archives across the RAR 1.3 through RAR 7.x family.",
after_help = "Exit codes:\n \
0 success\n \
1 operation failed\n \
2 invalid command line\n \
3 password required, wrong password, or corrupt encrypted data",
propagate_version = true,
disable_help_subcommand = true
)]
pub(crate) struct Cli {
#[arg(long, value_name = "N", global = true, value_parser = crate::parse_thread_count)]
pub threads: Option<usize>,
#[arg(long, value_enum, default_value_t = ProgressMode::Auto, global = true)]
pub progress: ProgressMode,
#[command(subcommand)]
pub command: Command,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, ValueEnum)]
pub(crate) enum ProgressMode {
Auto,
Always,
Never,
}
#[derive(Subcommand)]
#[allow(clippy::large_enum_variant)]
pub(crate) enum Command {
Info(InfoArgs),
Test(TestArgs),
#[command(visible_alias = "x")]
Extract(ExtractArgs),
Repair(RepairArgs),
#[command(visible_alias = "a")]
Add(AddArgs),
}
#[derive(Args)]
pub(crate) struct PasswordArgs {
#[arg(short = 'p', long, value_name = "PASSWORD")]
pub password: Option<String>,
#[arg(long, value_name = "PATH", conflicts_with = "password")]
pub password_file: Option<std::path::PathBuf>,
}
#[derive(Args, Default)]
pub(crate) struct ReadOptionsArgs {
#[arg(long, value_name = "ENCODING")]
pub legacy_name_encoding: Option<rars::filename::LegacyNameEncoding>,
#[arg(long, value_name = "SIZE", value_parser = crate::parse_size_string)]
pub rar50_buffered_decode_limit: Option<usize>,
#[arg(long, value_name = "SIZE", value_parser = crate::parse_size_string)]
pub rar50_dictionary_size_limit: Option<usize>,
#[arg(long, value_name = "SIZE", value_parser = crate::parse_size_string)]
pub max_member_output_bytes: Option<usize>,
#[arg(long, value_name = "SIZE", value_parser = crate::parse_size_string)]
pub max_total_output_bytes: Option<usize>,
#[arg(long, value_name = "SIZE", value_parser = crate::parse_size_string)]
pub max_reader_workspace_bytes: Option<usize>,
#[arg(long, value_name = "COUNT")]
pub max_header_count: Option<u64>,
#[arg(long, value_name = "SIZE", value_parser = crate::parse_size_string)]
pub max_header_bytes: Option<usize>,
#[arg(long, value_name = "PATH", requires = "rar50_scratch_bytes")]
pub rar50_scratch_dir: Option<std::path::PathBuf>,
#[arg(long, value_name = "SIZE", value_parser = crate::parse_size_string, requires = "rar50_scratch_dir")]
pub rar50_scratch_bytes: Option<usize>,
#[arg(long, value_name = "SIZE", value_parser = crate::parse_size_string, requires = "rar50_scratch_dir")]
pub rar50_filter_memory_limit: Option<usize>,
}
impl ReadOptionsArgs {
pub fn scratch(&self) -> Option<rars::Rar50Scratch> {
self.rar50_scratch_dir
.as_ref()
.zip(self.rar50_scratch_bytes)
.map(|(dir, bytes)| {
let policy = rars::Rar50Scratch::new(dir, bytes as u64);
match self.rar50_filter_memory_limit {
Some(limit) => policy.with_filter_memory_limit(limit as u64),
None => policy,
}
})
}
pub fn options<'a>(
&self,
password: Option<&'a [u8]>,
scratch: Option<&'a rars::Rar50Scratch>,
) -> rars::ArchiveReadOptions<'a> {
let mut options = rars::ArchiveReadOptions::with_optional_password(password);
options.legacy_name_encoding = self.legacy_name_encoding;
options.max_header_count = self.max_header_count;
options.max_header_bytes = self.max_header_bytes.map(|value| value as u64);
options.max_member_output_bytes = self.max_member_output_bytes.map(|value| value as u64);
options.max_total_output_bytes = self.max_total_output_bytes.map(|value| value as u64);
options.max_reader_workspace_bytes =
self.max_reader_workspace_bytes.map(|value| value as u64);
options.rar50_dictionary_size_limit =
self.rar50_dictionary_size_limit.map(|value| value as u64);
options.rar50_buffered_decode_limit =
self.rar50_buffered_decode_limit.map(|value| value as u64);
options.rar50_scratch = scratch;
options
}
}
#[derive(Args)]
pub(crate) struct InfoArgs {
#[command(flatten)]
pub read_options: ReadOptionsArgs,
#[command(flatten)]
pub password: PasswordArgs,
#[arg(short = 'v', long)]
pub verbose: bool,
#[arg(value_name = "ARCHIVE", required = true)]
pub paths: Vec<std::path::PathBuf>,
}
#[derive(Args)]
pub(crate) struct TestArgs {
#[command(flatten)]
pub password: PasswordArgs,
#[command(flatten)]
pub read_options: ReadOptionsArgs,
#[arg(value_name = "ARCHIVE", required = true)]
pub paths: Vec<std::path::PathBuf>,
}
#[derive(Args)]
pub(crate) struct ExtractArgs {
#[command(flatten)]
pub password: PasswordArgs,
#[command(flatten)]
pub read_options: ReadOptionsArgs,
#[arg(long, value_enum, default_value_t = OverwriteCli::Never)]
pub overwrite: OverwriteCli,
#[arg(value_name = "PATH", required = true, num_args = 2..)]
pub paths: Vec<std::path::PathBuf>,
}
#[derive(Args)]
pub(crate) struct RepairArgs {
#[command(flatten)]
pub password: PasswordArgs,
#[arg(value_name = "PATH", required = true, num_args = 2..)]
pub paths: Vec<std::path::PathBuf>,
}
#[derive(Args)]
pub(crate) struct AddArgs {
#[command(flatten)]
pub password: PasswordArgs,
#[arg(long, value_enum, default_value_t = TargetFormat::Rar50)]
pub format: TargetFormat,
#[arg(long)]
pub store: bool,
#[arg(long, value_name = "LEVEL")]
pub level: Option<u8>,
#[arg(long, value_name = "SIZE", value_parser = crate::parse_size_string)]
pub dict_size: Option<usize>,
#[arg(long, value_name = "SIZE", value_parser = crate::parse_size_string)]
pub memory_limit: Option<usize>,
#[arg(long, value_name = "SIZE", value_parser = crate::parse_size_string)]
pub max_memory: Option<usize>,
#[arg(long, value_name = "PATH")]
pub temp_dir: Option<std::path::PathBuf>,
#[arg(long)]
pub solid: bool,
#[arg(long = "encrypt-headers")]
pub encrypt_headers: bool,
#[arg(long = "quick-open")]
pub quick_open: bool,
#[arg(long, value_name = "TEXT")]
pub comment: Option<String>,
#[arg(long = "archive-name", value_name = "NAME")]
pub archive_name: Option<String>,
#[arg(long = "file-comment", value_name = "TEXT")]
pub file_comment: Option<String>,
#[arg(long = "recovery-percent", value_name = "PERCENT")]
pub recovery_percent: Option<u64>,
#[arg(long = "volume-size", value_name = "SIZE", value_parser = crate::parse_size_string)]
pub volume_size: Option<usize>,
#[arg(long = "delta-filter", value_name = "CHANNELS")]
pub delta_filter: Option<usize>,
#[arg(long = "e8-filter", conflicts_with = "e8e9_filter")]
pub e8_filter: bool,
#[arg(long = "e8e9-filter")]
pub e8e9_filter: bool,
#[arg(long = "itanium-filter")]
pub itanium_filter: bool,
#[arg(long = "rgb-filter", value_name = "WIDTH")]
pub rgb_filter: Option<usize>,
#[arg(long = "audio-filter", value_name = "CHANNELS")]
pub audio_filter: Option<usize>,
#[arg(long = "arm-filter")]
pub arm_filter: bool,
#[arg(long = "auto-filter")]
pub auto_filter: bool,
#[arg(long = "no-filter", conflicts_with = "auto_filter")]
pub no_filter: bool,
#[arg(long)]
pub ppmd: bool,
#[arg(value_name = "ARCHIVE")]
pub archive: std::path::PathBuf,
#[arg(value_name = "FILE", required = true)]
pub files: Vec<std::path::PathBuf>,
}
#[derive(Copy, Clone, ValueEnum)]
pub(crate) enum TargetFormat {
Rar14,
Rar15,
Rar20,
Rar29,
Rar30,
Rar40,
Rar50,
Rar70,
}
impl TargetFormat {
pub(crate) fn archive_version(self) -> ArchiveVersion {
match self {
Self::Rar14 => ArchiveVersion::Rar14,
Self::Rar15 => ArchiveVersion::Rar15,
Self::Rar20 => ArchiveVersion::Rar20,
Self::Rar29 => ArchiveVersion::Rar29,
Self::Rar30 => ArchiveVersion::Rar30,
Self::Rar40 => ArchiveVersion::Rar40,
Self::Rar50 => ArchiveVersion::Rar50,
Self::Rar70 => ArchiveVersion::Rar70,
}
}
pub(crate) fn from_archive_version(version: ArchiveVersion) -> Option<Self> {
match version {
ArchiveVersion::Rar14 => Some(Self::Rar14),
ArchiveVersion::Rar15 => Some(Self::Rar15),
ArchiveVersion::Rar20 => Some(Self::Rar20),
ArchiveVersion::Rar29 => Some(Self::Rar29),
ArchiveVersion::Rar30 => Some(Self::Rar30),
ArchiveVersion::Rar40 => Some(Self::Rar40),
ArchiveVersion::Rar50 => Some(Self::Rar50),
ArchiveVersion::Rar70 => Some(Self::Rar70),
_ => None,
}
}
}
#[derive(Copy, Clone, ValueEnum)]
pub(crate) enum OverwriteCli {
Never,
Always,
}
impl From<OverwriteCli> for crate::output::OverwritePolicy {
fn from(value: OverwriteCli) -> Self {
match value {
OverwriteCli::Never => Self::Never,
OverwriteCli::Always => Self::Always,
}
}
}
pub(crate) fn parse() -> Cli {
match Cli::try_parse() {
Ok(cli) => cli,
Err(err) => {
use clap::error::ErrorKind;
let message = err.to_string();
match err.kind() {
ErrorKind::DisplayHelp | ErrorKind::DisplayVersion => {
print!("{message}");
std::process::exit(0);
}
_ => {
eprint!("{message}");
std::process::exit(2);
}
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn accepted_formats_round_trip_through_command_line_spelling() {
for (name, version) in [
("rar14", ArchiveVersion::Rar14),
("rar15", ArchiveVersion::Rar15),
("rar20", ArchiveVersion::Rar20),
("rar29", ArchiveVersion::Rar29),
("rar30", ArchiveVersion::Rar30),
("rar40", ArchiveVersion::Rar40),
("rar50", ArchiveVersion::Rar50),
("rar70", ArchiveVersion::Rar70),
] {
let cli =
Cli::try_parse_from(["rars", "a", "--format", name, "out.rar", "input"]).unwrap();
let Command::Add(args) = cli.command else {
panic!("expected add command")
};
assert_eq!(args.format.archive_version(), version);
assert_eq!(
TargetFormat::from_archive_version(version)
.unwrap()
.archive_version(),
version
);
}
assert!(TargetFormat::from_archive_version(ArchiveVersion::Rar13).is_none());
assert!(
Cli::try_parse_from(["rars", "a", "--format", "rar13", "out.rar", "input"]).is_err()
);
}
#[test]
fn reader_policy_forwards_all_limits_and_optional_password_without_loss() {
let args = ReadOptionsArgs {
rar50_buffered_decode_limit: Some(0),
rar50_dictionary_size_limit: Some(11),
max_member_output_bytes: Some(12),
max_total_output_bytes: Some(13),
max_reader_workspace_bytes: Some(14),
max_header_count: Some(15),
max_header_bytes: Some(16),
rar50_scratch_dir: Some("scratch".into()),
rar50_scratch_bytes: Some(17),
..Default::default()
};
let scratch = args.scratch().unwrap();
let options = args.options(Some(b""), Some(&scratch));
assert_eq!(options.password, Some(b"".as_slice()));
assert_eq!(options.rar50_buffered_decode_limit, Some(0));
assert_eq!(options.rar50_dictionary_size_limit, Some(11));
assert_eq!(options.max_member_output_bytes, Some(12));
assert_eq!(options.max_total_output_bytes, Some(13));
assert_eq!(options.max_reader_workspace_bytes, Some(14));
assert_eq!(options.max_header_count, Some(15));
assert_eq!(options.max_header_bytes, Some(16));
assert!(std::ptr::eq(options.rar50_scratch.unwrap(), &scratch));
let defaults = ReadOptionsArgs::default();
assert!(defaults.scratch().is_none());
assert!(defaults.options(None, None).password.is_none());
assert!(defaults.options(None, None).rar50_scratch.is_none());
let with_filter_limit = ReadOptionsArgs {
rar50_filter_memory_limit: Some(0),
..args
};
assert!(with_filter_limit.scratch().is_some());
}
#[test]
fn parser_enforces_scratch_pairs_password_conflicts_and_path_counts() {
for args in [
vec![
"rars",
"test",
"--rar50-scratch-dir",
"scratch",
"input.rar",
],
vec!["rars", "test", "--rar50-scratch-bytes", "1m", "input.rar"],
vec![
"rars",
"test",
"--password",
"secret",
"--password-file",
"password",
"input.rar",
],
vec!["rars", "x", "input.rar"],
vec!["rars", "repair", "input.rar"],
] {
assert!(Cli::try_parse_from(args).is_err());
}
assert!(matches!(
crate::output::OverwritePolicy::from(OverwriteCli::Never),
crate::output::OverwritePolicy::Never
));
assert!(matches!(
crate::output::OverwritePolicy::from(OverwriteCli::Always),
crate::output::OverwritePolicy::Always
));
}
}