use crate::error::{Error, Result};
use crate::features::{Feature, FeatureSet};
use crate::filter::FilterPolicy;
use crate::version::{ArchiveFamily, ArchiveVersion};
#[derive(Debug, Clone, Default, PartialEq, Eq)]
#[non_exhaustive]
pub enum MemberCoding {
Stored,
#[default]
Compressed,
Filtered(FilterPolicy),
}
impl MemberCoding {
pub fn compresses(&self) -> bool {
!matches!(self, Self::Stored)
}
pub fn is_filtered(&self) -> bool {
matches!(self, Self::Filtered(_))
}
pub fn shape(&self) -> PlanShape {
PlanShape::new()
.compressed(self.compresses())
.filtered(self.is_filtered())
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
#[non_exhaustive]
pub struct PlanShape {
pub compressed: bool,
pub volumes: bool,
pub filtered: bool,
}
impl PlanShape {
pub const fn new() -> Self {
Self {
compressed: false,
volumes: false,
filtered: false,
}
}
pub const fn compressed(mut self, compressed: bool) -> Self {
self.compressed = compressed;
self
}
pub const fn volumes(mut self, volumes: bool) -> Self {
self.volumes = volumes;
self
}
pub const fn filtered(mut self, filtered: bool) -> Self {
self.filtered = filtered;
self
}
pub fn all() -> Vec<Self> {
let mut shapes = Vec::with_capacity(8);
for compressed in [false, true] {
for volumes in [false, true] {
for filtered in [false, true] {
shapes.push(Self {
compressed,
volumes,
filtered,
});
}
}
}
shapes
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum WriterOption {
Feature(Feature),
CompressionLevel,
CompressionMethod,
DictionarySize,
Filter,
RecoveryRecord,
VolumeSize,
ArchiveComment,
FileComment,
ArchiveMetadata,
Password,
MemoryLimit,
TempDir,
}
impl WriterOption {
pub const ALL: [Self; 15] = [
Self::Feature(Feature::Solid),
Self::Feature(Feature::HeaderEncryption),
Self::Feature(Feature::QuickOpen),
Self::CompressionLevel,
Self::CompressionMethod,
Self::DictionarySize,
Self::Filter,
Self::RecoveryRecord,
Self::VolumeSize,
Self::ArchiveComment,
Self::FileComment,
Self::ArchiveMetadata,
Self::Password,
Self::MemoryLimit,
Self::TempDir,
];
pub const fn name(self) -> &'static str {
match self {
Self::Feature(feature) => feature.name(),
Self::CompressionLevel => "a compression level",
Self::CompressionMethod => "an alternative compression method",
Self::DictionarySize => "a dictionary size",
Self::Filter => "a data filter",
Self::RecoveryRecord => "a recovery record",
Self::VolumeSize => "splitting into volumes",
Self::ArchiveComment => "an archive comment",
Self::FileComment => "a per-file comment",
Self::ArchiveMetadata => "archive metadata",
Self::Password => "encryption",
Self::MemoryLimit => "a memory limit",
Self::TempDir => "a temporary directory",
}
}
}
pub fn supported_features(target: ArchiveVersion, shape: PlanShape) -> FeatureSet {
FeatureSet {
solid: supports(target, WriterOption::Feature(Feature::Solid), shape),
header_encryption: supports(
target,
WriterOption::Feature(Feature::HeaderEncryption),
shape,
),
quick_open: supports(target, WriterOption::Feature(Feature::QuickOpen), shape),
}
}
pub fn supports(target: ArchiveVersion, option: WriterOption, shape: PlanShape) -> bool {
let family = target.family();
match option {
WriterOption::Feature(Feature::Solid) => shape.compressed,
WriterOption::Feature(Feature::HeaderEncryption) => matches!(
target,
ArchiveVersion::Rar30
| ArchiveVersion::Rar40
| ArchiveVersion::Rar50
| ArchiveVersion::Rar70
),
WriterOption::Feature(Feature::QuickOpen) => {
family == ArchiveFamily::Rar50Plus && !shape.volumes
}
WriterOption::CompressionLevel => true,
WriterOption::CompressionMethod => {
matches!(
target,
ArchiveVersion::Rar29 | ArchiveVersion::Rar30 | ArchiveVersion::Rar40
) && !shape.volumes
}
WriterOption::DictionarySize => !family_is(family, ArchiveFamily::Rar13),
WriterOption::Filter => {
!matches!(
target,
ArchiveVersion::Rar13
| ArchiveVersion::Rar14
| ArchiveVersion::Rar15
| ArchiveVersion::Rar20
) && (family == ArchiveFamily::Rar50Plus || !shape.volumes)
}
WriterOption::RecoveryRecord => family == ArchiveFamily::Rar50Plus,
WriterOption::VolumeSize => true,
WriterOption::ArchiveComment => !shape.volumes,
WriterOption::FileComment => {
!matches!(target, ArchiveVersion::Rar30 | ArchiveVersion::Rar40) && !shape.volumes
}
WriterOption::ArchiveMetadata => family == ArchiveFamily::Rar50Plus && !shape.volumes,
WriterOption::Password => true,
WriterOption::MemoryLimit => family == ArchiveFamily::Rar50Plus || !shape.volumes,
WriterOption::TempDir => family == ArchiveFamily::Rar50Plus,
}
}
const fn family_is(family: ArchiveFamily, other: ArchiveFamily) -> bool {
matches!(
(family, other),
(ArchiveFamily::Rar13, ArchiveFamily::Rar13)
| (ArchiveFamily::Rar15To40, ArchiveFamily::Rar15To40)
| (ArchiveFamily::Rar50Plus, ArchiveFamily::Rar50Plus)
)
}
pub fn formats_supporting(option: WriterOption, shape: PlanShape) -> Vec<ArchiveVersion> {
ArchiveVersion::ALL
.into_iter()
.filter(|&target| supports(target, option, shape))
.collect()
}
pub fn validate_features(
target: ArchiveVersion,
asked: FeatureSet,
shape: PlanShape,
) -> Result<()> {
match asked.first_unsupported(supported_features(target, shape)) {
Some(feature) => Err(Error::UnsupportedWriterOption {
target,
option: WriterOption::Feature(feature),
because: None,
}),
None => Ok(()),
}
}
pub fn validate_compression_level(target: ArchiveVersion, level: Option<u8>) -> Result<()> {
match level {
None | Some(0..=5) => Ok(()),
Some(_) => Err(Error::UnsupportedWriterOption {
target,
option: WriterOption::CompressionLevel,
because: Some("levels run from 0 to 5"),
}),
}
}
pub fn validate_option(
target: ArchiveVersion,
option: WriterOption,
shape: PlanShape,
) -> Result<()> {
if supports(target, option, shape) {
Ok(())
} else {
Err(Error::UnsupportedWriterOption {
target,
option,
because: None,
})
}
}
#[derive(Debug, Clone, Copy)]
pub(crate) struct StoreFallback {
pub(crate) min_size: usize,
pub(crate) allow_solid: bool,
pub(crate) filter_requested: bool,
}
impl StoreFallback {
pub(crate) const fn new() -> Self {
Self {
min_size: 0,
allow_solid: false,
filter_requested: false,
}
}
pub(crate) const fn min_size(mut self, size: usize) -> Self {
self.min_size = size;
self
}
pub(crate) const fn allow_solid(mut self, allow: bool) -> Self {
self.allow_solid = allow;
self
}
pub(crate) const fn filter_requested(mut self, requested: bool) -> Self {
self.filter_requested = requested;
self
}
pub(crate) fn applies(&self, solid: bool, unpacked: usize, packed: usize) -> bool {
if packed < unpacked || self.filter_requested {
return false;
}
if solid && !self.allow_solid {
return false;
}
unpacked >= self.min_size
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn asking_and_validating_agree_for_every_combination() {
for target in ArchiveVersion::ALL {
for option in WriterOption::ALL {
for shape in PlanShape::all() {
let supported = supports(target, option, shape);
let validated = validate_option(target, option, shape);
assert_eq!(
supported,
validated.is_ok(),
"{target} {option:?} {shape:?}: supports={supported} validate={validated:?}"
);
if let Err(Error::UnsupportedWriterOption { option: named, .. }) = validated {
assert_eq!(named, option, "the refusal named the wrong option");
}
}
}
}
}
#[test]
fn every_option_is_supported_by_some_format() {
for option in WriterOption::ALL {
assert!(!option.name().is_empty());
assert!(
PlanShape::all()
.into_iter()
.any(|shape| !formats_supporting(option, shape).is_empty()),
"{option:?} is supported by nothing, so a refusal could not point anywhere"
);
}
}
#[test]
fn feature_capability_matches_the_per_option_answer() {
for target in ArchiveVersion::ALL {
for shape in PlanShape::all() {
let features = supported_features(target, shape);
for feature in Feature::ALL {
assert_eq!(
feature_of(features, feature),
supports(target, WriterOption::Feature(feature), shape),
"{target} {feature:?} {shape:?}"
);
}
}
}
}
fn feature_of(set: FeatureSet, feature: Feature) -> bool {
match feature {
Feature::Solid => set.solid,
Feature::HeaderEncryption => set.header_encryption,
Feature::QuickOpen => set.quick_open,
}
}
#[test]
fn a_member_is_only_stored_when_compressing_it_gained_nothing() {
let plain = StoreFallback::new();
assert!(plain.applies(false, 100, 100));
assert!(plain.applies(false, 100, 120));
assert!(!plain.applies(false, 100, 99));
assert!(!plain.filter_requested(true).applies(false, 100, 200));
assert!(!plain.applies(true, 100, 100));
assert!(plain.allow_solid(true).applies(true, 100, 100));
let floored = plain.min_size(1024);
assert!(!floored.applies(false, 1023, 1023));
assert!(floored.applies(false, 1024, 1024));
}
#[test]
fn a_level_above_five_is_refused_by_name() {
let refused = validate_compression_level(ArchiveVersion::Rar50, Some(9));
assert!(matches!(
refused,
Err(Error::UnsupportedWriterOption {
option: WriterOption::CompressionLevel,
..
})
));
assert!(validate_compression_level(ArchiveVersion::Rar50, Some(5)).is_ok());
assert!(validate_compression_level(ArchiveVersion::Rar50, None).is_ok());
}
}