use crate::cli::TargetFormat;
use crate::error::CliError;
use rars::{ArchiveVersion, Feature, FilterKind, PlanShape, WriterOption};
pub(crate) fn flag_for(option: WriterOption, asked: &AskedFilters) -> &'static str {
match option {
WriterOption::Feature(Feature::Solid) => "--solid",
WriterOption::Feature(Feature::HeaderEncryption) => "--encrypt-headers",
WriterOption::Feature(Feature::QuickOpen) => "--quick-open",
WriterOption::CompressionLevel => "--level",
WriterOption::CompressionMethod => "--ppmd",
WriterOption::DictionarySize => "--dict-size",
WriterOption::Filter => asked.flag(),
WriterOption::RecoveryRecord => "--recovery-percent",
WriterOption::VolumeSize => "--volume-size",
WriterOption::ArchiveComment => "--comment",
WriterOption::FileComment => "--file-comment",
WriterOption::ArchiveMetadata => "--archive-name",
WriterOption::Password => "--password",
WriterOption::MemoryLimit => "--memory-limit",
WriterOption::TempDir => "--temp-dir",
_ => "that option",
}
}
#[derive(Debug, Clone, Copy, Default)]
pub(crate) struct AskedFilters {
pub(crate) delta: bool,
pub(crate) e8: bool,
pub(crate) e8e9: bool,
pub(crate) itanium: bool,
pub(crate) rgb: bool,
pub(crate) audio: bool,
pub(crate) arm: bool,
}
impl AskedFilters {
fn flag(&self) -> &'static str {
if self.delta {
"--delta-filter"
} else if self.e8e9 {
"--e8e9-filter"
} else if self.e8 {
"--e8-filter"
} else if self.itanium {
"--itanium-filter"
} else if self.rgb {
"--rgb-filter"
} else if self.audio {
"--audio-filter"
} else if self.arm {
"--arm-filter"
} else {
"--auto-filter"
}
}
pub(crate) fn count(&self) -> usize {
usize::from(self.delta)
+ usize::from(self.e8 || self.e8e9)
+ usize::from(self.itanium)
+ usize::from(self.rgb)
+ usize::from(self.audio)
+ usize::from(self.arm)
}
}
pub(crate) fn asked_filter_kind(filters: &AskedFilters) -> Option<FilterKind> {
if filters.itanium {
Some(FilterKind::Itanium)
} else if filters.rgb {
Some(FilterKind::Rgb { width: 3, pos_r: 0 })
} else if filters.audio {
Some(FilterKind::Audio { channels: 1 })
} else if filters.arm {
Some(FilterKind::Arm)
} else if filters.e8 || filters.e8e9 {
Some(FilterKind::E8)
} else if filters.delta {
Some(FilterKind::Delta { channels: 1 })
} else {
None
}
}
pub(crate) fn reject_unsupported_filter(
target: ArchiveVersion,
filters: &AskedFilters,
) -> Result<(), CliError> {
let Some(kind) = asked_filter_kind(filters) else {
return Ok(());
};
if kind.is_supported_by(target) {
return Ok(());
}
let names = format_list(rars::formats_supporting_filter(kind), target);
Err(CliError::usage(format!(
"{} is not supported by --format {target}{names}",
flag_for(WriterOption::Filter, filters),
)))
}
pub(crate) fn reject_multiple_filters(filters: &AskedFilters) -> Result<(), CliError> {
if filters.count() > 1 {
return Err(CliError::usage(
"only one filter can be asked for at a time",
));
}
Ok(())
}
pub(crate) fn reject_coding_without_compression(
filters: &AskedFilters,
auto_filter: bool,
ppmd: bool,
) -> Result<(), CliError> {
let flag = if ppmd {
"--ppmd"
} else if filters.count() > 0 || auto_filter {
flag_for(WriterOption::Filter, filters)
} else {
return Ok(());
};
Err(CliError::usage(format!(
"{flag} needs something to compress; drop --store or --level 0"
)))
}
pub(crate) fn reject_filter_with_solid(
target: ArchiveVersion,
filters: &AskedFilters,
auto_filter: bool,
solid: bool,
) -> Result<(), CliError> {
if !solid || target.family() != rars::ArchiveFamily::Rar50Plus {
return Ok(());
}
if filters.count() == 0 && !auto_filter {
return Ok(());
}
Err(CliError::usage(format!(
"{} cannot be used with --solid; solid members share one dictionary",
flag_for(WriterOption::Filter, filters),
)))
}
fn alternatives(option: WriterOption, shape: PlanShape, exclude: ArchiveVersion) -> String {
format_list(rars::formats_supporting(option, shape), exclude)
}
fn format_list(targets: Vec<ArchiveVersion>, exclude: ArchiveVersion) -> String {
let names: Vec<_> = targets
.into_iter()
.filter(|&target| target != exclude)
.filter(|&target| TargetFormat::from_archive_version(target).is_some())
.map(|target| format!("--format {target}"))
.collect();
match names.len() {
0 => String::new(),
1 => format!("; use {}", names[0]),
_ => format!(
"; use {} or {}",
names[..names.len() - 1].join(", "),
names[names.len() - 1]
),
}
}
pub(crate) fn map_write_error(
error: rars::Error,
shape: PlanShape,
filters: &AskedFilters,
) -> CliError {
match error {
rars::Error::UnsupportedWriterOption {
target,
option,
because,
} => CliError::usage(format!(
"{} is not supported by --format {target}{}{}",
flag_for(option, filters),
because.map(|why| format!(" ({why})")).unwrap_or_default(),
alternatives(option, shape, target),
)),
other => CliError::general(other.to_string()),
}
}
pub(crate) fn reject_unsupported(
target: ArchiveVersion,
shape: PlanShape,
filters: &AskedFilters,
asked: &[(WriterOption, bool)],
) -> Result<(), CliError> {
for &(option, wanted) in asked {
if wanted && !rars::supports(target, option, shape) {
let because = (shape.volumes && rars::supports(target, option, without_volumes(shape)))
.then_some("in a volume set");
return Err(map_write_error(
rars::Error::UnsupportedWriterOption {
target,
option,
because,
},
shape,
filters,
));
}
}
Ok(())
}
fn without_volumes(shape: PlanShape) -> PlanShape {
PlanShape::new()
.compressed(shape.compressed)
.filtered(shape.filtered)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn flags_filters_and_conflicts_have_actionable_messages() {
let cases = [
(
AskedFilters {
delta: true,
..Default::default()
},
"--delta-filter",
FilterKind::Delta { channels: 1 },
),
(
AskedFilters {
e8: true,
..Default::default()
},
"--e8-filter",
FilterKind::E8,
),
(
AskedFilters {
e8e9: true,
..Default::default()
},
"--e8e9-filter",
FilterKind::E8,
),
(
AskedFilters {
itanium: true,
..Default::default()
},
"--itanium-filter",
FilterKind::Itanium,
),
(
AskedFilters {
rgb: true,
..Default::default()
},
"--rgb-filter",
FilterKind::Rgb { width: 3, pos_r: 0 },
),
(
AskedFilters {
audio: true,
..Default::default()
},
"--audio-filter",
FilterKind::Audio { channels: 1 },
),
(
AskedFilters {
arm: true,
..Default::default()
},
"--arm-filter",
FilterKind::Arm,
),
];
for (filters, flag, kind) in cases {
assert_eq!(flag_for(WriterOption::Filter, &filters), flag);
assert_eq!(asked_filter_kind(&filters), Some(kind));
assert_eq!(filters.count(), 1);
assert!(reject_multiple_filters(&filters).is_ok());
assert!(reject_coding_without_compression(&filters, false, false)
.unwrap_err()
.to_string()
.starts_with(flag));
}
let none = AskedFilters::default();
assert_eq!(flag_for(WriterOption::Filter, &none), "--auto-filter");
assert_eq!(asked_filter_kind(&none), None);
assert!(reject_coding_without_compression(&none, false, false).is_ok());
assert!(reject_coding_without_compression(&none, true, false)
.unwrap_err()
.to_string()
.starts_with("--auto-filter"));
assert!(reject_coding_without_compression(&none, true, true)
.unwrap_err()
.to_string()
.starts_with("--ppmd"));
let aliases = AskedFilters {
e8: true,
e8e9: true,
..Default::default()
};
assert_eq!(aliases.count(), 1);
let multiple = AskedFilters {
delta: true,
arm: true,
..Default::default()
};
assert_eq!(
reject_multiple_filters(&multiple).unwrap_err().exit_code(),
2
);
}
#[test]
fn solid_filter_refusals_depend_on_family_and_request() {
let none = AskedFilters::default();
let delta = AskedFilters {
delta: true,
..Default::default()
};
assert!(reject_filter_with_solid(ArchiveVersion::Rar50, &delta, false, false).is_ok());
assert!(reject_filter_with_solid(ArchiveVersion::Rar29, &delta, false, true).is_ok());
assert!(reject_filter_with_solid(ArchiveVersion::Rar50, &none, false, true).is_ok());
assert!(
reject_filter_with_solid(ArchiveVersion::Rar50, &none, true, true)
.unwrap_err()
.to_string()
.starts_with("--auto-filter")
);
assert!(
reject_filter_with_solid(ArchiveVersion::Rar70, &delta, false, true)
.unwrap_err()
.to_string()
.starts_with("--delta-filter")
);
assert!(reject_unsupported_filter(ArchiveVersion::Rar50, &none).is_ok());
assert!(reject_unsupported_filter(ArchiveVersion::Rar50, &delta).is_ok());
}
#[test]
fn refusals_cover_every_current_option_and_preserve_other_errors() {
let flags = [
"--solid",
"--encrypt-headers",
"--quick-open",
"--level",
"--ppmd",
"--dict-size",
"--auto-filter",
"--recovery-percent",
"--volume-size",
"--comment",
"--file-comment",
"--archive-name",
"--password",
"--memory-limit",
"--temp-dir",
];
for (option, flag) in WriterOption::ALL.into_iter().zip(flags) {
assert_eq!(flag_for(option, &AskedFilters::default()), flag);
}
let error = map_write_error(
rars::Error::InvalidHeader("broken"),
PlanShape::new(),
&AskedFilters::default(),
);
assert_eq!(error.exit_code(), 1);
assert_eq!(
error.to_string(),
rars::Error::InvalidHeader("broken").to_string()
);
assert_eq!(format_list(vec![], ArchiveVersion::Rar50), "");
assert_eq!(
format_list(
vec![
ArchiveVersion::Rar13,
ArchiveVersion::Rar50,
ArchiveVersion::Rar29
],
ArchiveVersion::Rar50
),
"; use --format rar29"
);
let shape = PlanShape::new()
.compressed(true)
.filtered(true)
.volumes(true);
let error = reject_unsupported(
ArchiveVersion::Rar50,
shape,
&AskedFilters::default(),
&[
(WriterOption::FileComment, false),
(WriterOption::ArchiveMetadata, true),
],
)
.unwrap_err();
assert_eq!(error.exit_code(), 2);
assert!(error.to_string().contains("--archive-name"));
assert!(error.to_string().contains("in a volume set"), "{error}");
}
#[test]
fn a_refusal_names_the_flag_that_was_typed() {
let filters = AskedFilters {
rgb: true,
..AskedFilters::default()
};
let error = reject_unsupported_filter(ArchiveVersion::Rar50, &filters).unwrap_err();
assert_eq!(
error.to_string(),
"--rgb-filter is not supported by --format rar50; \
use --format rar29, --format rar30 or --format rar40"
);
}
#[test]
fn a_refusal_offers_only_formats_the_argument_accepts() {
let error = reject_unsupported(
ArchiveVersion::Rar15,
PlanShape::new(),
&AskedFilters::default(),
&[(WriterOption::Feature(Feature::HeaderEncryption), true)],
)
.unwrap_err();
assert_eq!(
error.to_string(),
"--encrypt-headers is not supported by --format rar15; \
use --format rar30, --format rar40, --format rar50 or --format rar70"
);
assert!(!error.to_string().contains("rar13"));
}
#[test]
fn what_the_format_can_do_is_not_refused() {
assert!(reject_unsupported(
ArchiveVersion::Rar50,
PlanShape::new().compressed(true),
&AskedFilters::default(),
&[
(WriterOption::Feature(Feature::Solid), true),
(WriterOption::RecoveryRecord, true),
(WriterOption::ArchiveMetadata, true),
],
)
.is_ok());
}
}