use crate::codec::CodecMethod;
use crate::format::streams::ResourceLimits;
#[cfg(feature = "aes")]
use crate::crypto::{NoncePolicy, Password};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
#[non_exhaustive]
pub enum WriteFilter {
#[default]
None,
BcjX86,
BcjArm,
BcjArm64,
BcjArmThumb,
BcjPpc,
BcjSparc,
BcjIa64,
BcjRiscv,
Bcj2,
Delta {
distance: u8,
},
}
impl WriteFilter {
pub fn delta(distance: u8) -> Self {
assert!(
distance > 0,
"Delta filter distance must be non-zero (1-255)"
);
Self::Delta { distance }
}
}
impl WriteFilter {
pub fn method_id(&self) -> Option<&'static [u8]> {
use crate::codec::method;
match self {
Self::None => None,
Self::BcjX86 => Some(method::BCJ_X86),
Self::BcjArm => Some(method::BCJ_ARM),
Self::BcjArm64 => Some(method::BCJ_ARM64),
Self::BcjArmThumb => Some(method::BCJ_ARM_THUMB),
Self::BcjPpc => Some(method::BCJ_PPC),
Self::BcjSparc => Some(method::BCJ_SPARC),
Self::BcjIa64 => Some(method::BCJ_IA64),
Self::BcjRiscv => Some(method::BCJ_RISCV),
Self::Bcj2 => Some(method::BCJ2),
Self::Delta { .. } => Some(method::DELTA),
}
}
pub fn is_bcj2(&self) -> bool {
matches!(self, Self::Bcj2)
}
pub fn is_active(&self) -> bool {
!matches!(self, Self::None)
}
pub fn properties(&self) -> Option<Vec<u8>> {
match self {
Self::Delta { distance } => {
Some(vec![distance - 1])
}
_ => None,
}
}
}
#[derive(Clone)]
pub struct WriteOptions {
pub method: CodecMethod,
pub level: u32,
pub lzma2_variant: Lzma2Variant,
pub filter: WriteFilter,
pub solid: SolidOptions,
pub limits: ResourceLimits,
pub deterministic: bool,
pub comment: Option<String>,
#[cfg(feature = "aes")]
pub password: Option<Password>,
#[cfg(feature = "aes")]
pub nonce_policy: NoncePolicy,
#[cfg(feature = "aes")]
pub encrypt_header: bool,
#[cfg(feature = "aes")]
pub encrypt_data: bool,
}
impl Default for WriteOptions {
fn default() -> Self {
Self {
method: CodecMethod::Lzma2,
level: 5,
lzma2_variant: Lzma2Variant::Standard,
filter: WriteFilter::None,
solid: SolidOptions::default(),
limits: ResourceLimits::default(),
deterministic: false,
comment: None,
#[cfg(feature = "aes")]
password: None,
#[cfg(feature = "aes")]
nonce_policy: NoncePolicy::default(),
#[cfg(feature = "aes")]
encrypt_header: false,
#[cfg(feature = "aes")]
encrypt_data: false,
}
}
}
impl std::fmt::Debug for WriteOptions {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let mut s = f.debug_struct("WriteOptions");
s.field("method", &self.method)
.field("level", &self.level)
.field("lzma2_variant", &self.lzma2_variant)
.field("filter", &self.filter)
.field("solid", &self.solid)
.field("deterministic", &self.deterministic)
.field("comment", &self.comment);
#[cfg(feature = "aes")]
s.field("has_password", &self.password.is_some());
s.finish()
}
}
impl WriteOptions {
pub fn new() -> Self {
Self::default()
}
pub fn method(mut self, method: CodecMethod) -> Self {
self.method = method;
self
}
pub fn level(mut self, level: u32) -> crate::Result<Self> {
if level > 9 {
return Err(crate::Error::InvalidCompressionLevel { level });
}
self.level = level;
Ok(self)
}
pub fn level_clamped(mut self, level: u32) -> Self {
self.level = level.min(9);
self
}
pub fn lzma2_variant(mut self, variant: Lzma2Variant) -> Self {
self.lzma2_variant = variant;
self
}
pub fn fast_lzma2(self) -> Self {
self.lzma2_variant(Lzma2Variant::Fast)
}
pub fn filter(mut self, filter: WriteFilter) -> Self {
self.filter = filter;
self
}
pub fn bcj_x86(self) -> Self {
self.filter(WriteFilter::BcjX86)
}
pub fn bcj_arm(self) -> Self {
self.filter(WriteFilter::BcjArm)
}
pub fn bcj_arm64(self) -> Self {
self.filter(WriteFilter::BcjArm64)
}
pub fn bcj2(self) -> Self {
self.filter(WriteFilter::Bcj2)
}
pub fn delta(self, distance: u8) -> Self {
self.filter(WriteFilter::delta(distance))
}
pub fn has_filter(&self) -> bool {
self.filter.is_active()
}
pub fn solid(mut self) -> Self {
self.solid = SolidOptions::enabled();
self
}
pub fn solid_options(mut self, options: SolidOptions) -> Self {
self.solid = options;
self
}
pub fn deterministic(mut self, enabled: bool) -> Self {
self.deterministic = enabled;
self
}
pub fn comment(mut self, comment: impl Into<String>) -> Self {
self.comment = Some(comment.into());
self
}
#[cfg(feature = "aes")]
pub fn password(mut self, password: impl Into<Password>) -> Self {
self.password = Some(password.into());
self
}
#[cfg(feature = "aes")]
pub fn nonce_policy(mut self, policy: NoncePolicy) -> Self {
self.nonce_policy = policy;
self
}
#[cfg(feature = "aes")]
pub fn encrypt_header(mut self, encrypt: bool) -> Self {
self.encrypt_header = encrypt;
self
}
#[cfg(feature = "aes")]
pub fn encrypt_data(mut self, encrypt: bool) -> Self {
self.encrypt_data = encrypt;
self
}
#[cfg(feature = "aes")]
pub fn is_encrypted(&self) -> bool {
self.password.is_some()
}
#[cfg(feature = "aes")]
pub fn is_header_encrypted(&self) -> bool {
self.encrypt_header && self.password.is_some()
}
#[cfg(feature = "aes")]
pub fn is_data_encrypted(&self) -> bool {
self.encrypt_data && self.password.is_some()
}
#[cfg(not(feature = "aes"))]
pub fn is_header_encrypted(&self) -> bool {
false
}
#[cfg(not(feature = "aes"))]
pub fn is_data_encrypted(&self) -> bool {
false
}
#[cfg(not(feature = "aes"))]
pub fn is_encrypted(&self) -> bool {
false
}
}
#[derive(Debug, Clone, Default)]
pub struct SolidOptions {
pub enabled: bool,
pub block_size: Option<u64>,
pub files_per_block: Option<usize>,
}
impl SolidOptions {
pub fn disabled() -> Self {
Self {
enabled: false,
block_size: None,
files_per_block: None,
}
}
pub fn enabled() -> Self {
Self {
enabled: true,
block_size: Some(64 * 1024 * 1024), files_per_block: None,
}
}
pub fn block_size(mut self, size: u64) -> Self {
self.block_size = Some(size);
self
}
pub fn files_per_block(mut self, count: usize) -> Self {
self.files_per_block = Some(count);
self
}
pub fn is_solid(&self) -> bool {
self.enabled
}
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub enum Lzma2Variant {
#[default]
Standard,
Fast,
}
impl Lzma2Variant {
pub fn is_fast(&self) -> bool {
matches!(self, Self::Fast)
}
pub fn is_standard(&self) -> bool {
matches!(self, Self::Standard)
}
}
#[derive(Debug, Clone, Default)]
pub struct EntryMeta {
pub is_directory: bool,
pub size: u64,
pub modification_time: Option<u64>,
pub creation_time: Option<u64>,
pub access_time: Option<u64>,
pub attributes: Option<u32>,
pub is_anti: bool,
}
impl EntryMeta {
pub fn file(size: u64) -> Self {
Self {
is_directory: false,
size,
..Default::default()
}
}
pub fn directory() -> Self {
Self {
is_directory: true,
size: 0,
..Default::default()
}
}
pub fn from_path(path: impl AsRef<std::path::Path>) -> crate::Result<Self> {
let metadata = std::fs::metadata(path)?;
Ok(Self::from_metadata(&metadata))
}
pub fn from_metadata(metadata: &std::fs::Metadata) -> Self {
Self {
is_directory: metadata.is_dir(),
size: if metadata.is_dir() { 0 } else { metadata.len() },
modification_time: metadata.modified().ok().map(system_time_to_filetime),
creation_time: metadata.created().ok().map(system_time_to_filetime),
access_time: metadata.accessed().ok().map(system_time_to_filetime),
attributes: None, is_anti: false,
}
}
pub fn modification_time(mut self, time: u64) -> Self {
self.modification_time = Some(time);
self
}
pub fn creation_time(mut self, time: u64) -> Self {
self.creation_time = Some(time);
self
}
pub fn attributes(mut self, attrs: u32) -> Self {
self.attributes = Some(attrs);
self
}
pub fn anti_item() -> Self {
Self {
is_directory: false,
is_anti: true,
size: 0,
..Default::default()
}
}
pub fn anti_directory() -> Self {
Self {
is_directory: true,
is_anti: true,
size: 0,
..Default::default()
}
}
pub fn as_anti(mut self) -> Self {
self.is_anti = true;
self.size = 0; self
}
}
fn system_time_to_filetime(time: std::time::SystemTime) -> u64 {
use std::time::UNIX_EPOCH;
const FILETIME_UNIX_DIFF: u64 = 116444736000000000;
match time.duration_since(UNIX_EPOCH) {
Ok(duration) => {
let hundred_nanos = duration.as_nanos() / 100;
FILETIME_UNIX_DIFF + hundred_nanos as u64
}
Err(e) => {
let diff = e.duration();
let hundred_nanos = diff.as_nanos() / 100;
FILETIME_UNIX_DIFF.saturating_sub(hundred_nanos as u64)
}
}
}
#[must_use = "write results should be checked to ensure archive was created successfully"]
#[derive(Debug, Clone, Default)]
pub struct WriteResult {
pub entries_written: usize,
pub directories_written: usize,
pub total_size: u64,
pub compressed_size: u64,
pub volume_count: u32,
pub volume_sizes: Vec<u64>,
}
impl WriteResult {
pub fn compression_ratio(&self) -> f64 {
if self.total_size == 0 {
1.0
} else {
self.compressed_size as f64 / self.total_size as f64
}
}
pub fn space_savings(&self) -> f64 {
if self.total_size == 0 {
0.0
} else {
1.0 - self.compression_ratio()
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_write_options_default() {
let opts = WriteOptions::default();
assert_eq!(opts.level, 5);
assert!(!opts.solid.is_solid());
assert!(!opts.deterministic);
}
#[test]
fn test_write_options_builder() {
let opts = WriteOptions::new()
.method(CodecMethod::Lzma)
.level(9)
.unwrap()
.solid()
.deterministic(true);
assert_eq!(opts.method, CodecMethod::Lzma);
assert_eq!(opts.level, 9);
assert!(opts.solid.is_solid());
assert!(opts.deterministic);
}
#[test]
fn test_level_valid() {
for level in 0..=9 {
let result = WriteOptions::new().level(level);
assert!(result.is_ok(), "level {} should be valid", level);
assert_eq!(result.unwrap().level, level);
}
}
#[test]
fn test_level_invalid() {
for level in [10, 15, 100, u32::MAX] {
let result = WriteOptions::new().level(level);
assert!(result.is_err(), "level {} should be invalid", level);
assert!(matches!(
result.unwrap_err(),
crate::Error::InvalidCompressionLevel { level: l } if l == level
));
}
}
#[test]
fn test_level_clamped() {
assert_eq!(WriteOptions::new().level_clamped(0).level, 0);
assert_eq!(WriteOptions::new().level_clamped(5).level, 5);
assert_eq!(WriteOptions::new().level_clamped(9).level, 9);
assert_eq!(WriteOptions::new().level_clamped(10).level, 9);
assert_eq!(WriteOptions::new().level_clamped(15).level, 9);
assert_eq!(WriteOptions::new().level_clamped(100).level, 9);
assert_eq!(WriteOptions::new().level_clamped(u32::MAX).level, 9);
}
#[test]
fn test_delta_filter_valid() {
let filter = WriteFilter::delta(1);
assert!(matches!(filter, WriteFilter::Delta { distance: 1 }));
let filter = WriteFilter::delta(2);
assert!(matches!(filter, WriteFilter::Delta { distance: 2 }));
let filter = WriteFilter::delta(255);
assert!(matches!(filter, WriteFilter::Delta { distance: 255 }));
}
#[test]
#[should_panic(expected = "Delta filter distance must be non-zero")]
fn test_delta_filter_zero_panics() {
WriteFilter::delta(0);
}
#[test]
fn test_solid_options() {
let opts = SolidOptions::enabled()
.block_size(1024 * 1024)
.files_per_block(100);
assert!(opts.is_solid());
assert_eq!(opts.block_size, Some(1024 * 1024));
assert_eq!(opts.files_per_block, Some(100));
}
#[test]
fn test_entry_meta_file() {
let meta = EntryMeta::file(1000);
assert!(!meta.is_directory);
assert_eq!(meta.size, 1000);
}
#[test]
fn test_entry_meta_directory() {
let meta = EntryMeta::directory();
assert!(meta.is_directory);
assert_eq!(meta.size, 0);
}
#[test]
fn test_write_result() {
let result = WriteResult {
entries_written: 10,
directories_written: 2,
total_size: 1000,
compressed_size: 500,
volume_count: 1,
volume_sizes: vec![500],
};
assert!((result.compression_ratio() - 0.5).abs() < 0.001);
assert!((result.space_savings() - 0.5).abs() < 0.001);
}
#[test]
fn test_write_result_empty() {
let result = WriteResult::default();
assert!((result.compression_ratio() - 1.0).abs() < 0.001);
assert!((result.space_savings() - 0.0).abs() < 0.001);
}
}