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 {
return Ok(());
}
let searches_only = target.family() != rars::ArchiveFamily::Rar50Plus;
let searching = auto_filter && filters.count() == 0;
let refused = if searches_only {
searching
} else {
filters.count() > 0 || auto_filter
};
if !refused {
return Ok(());
}
let because = if searches_only {
"; name a filter instead"
} else {
"; solid members share one dictionary"
};
Err(CliError::usage(format!(
"{} cannot be used with --solid{because}",
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 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());
}
}