use crate::ArchiveError;
use crate::Result;
use crate::config::Unvalidated;
use crate::config::Validated;
use crate::formats::detect::ArchiveType;
use std::marker::PhantomData;
use std::ops::Deref;
use std::ops::DerefMut;
use std::path::PathBuf;
#[derive(Debug, Clone)]
#[non_exhaustive]
pub struct CreationConfigFields {
pub follow_symlinks: bool,
pub include_hidden: bool,
pub max_file_size: Option<u64>,
pub exclude_patterns: Vec<String>,
pub strip_prefix: Option<PathBuf>,
pub compression_level: Option<u8>,
pub preserve_permissions: bool,
pub format: Option<ArchiveType>,
}
impl Default for CreationConfigFields {
fn default() -> Self {
Self {
follow_symlinks: false,
include_hidden: false,
max_file_size: None,
exclude_patterns: vec![
".git".to_string(),
".DS_Store".to_string(),
"*.tmp".to_string(),
],
strip_prefix: None,
compression_level: Some(6),
preserve_permissions: true,
format: None,
}
}
}
#[derive(Debug, Clone)]
pub struct CreationConfig<State = Unvalidated> {
fields: CreationConfigFields,
_marker: PhantomData<State>,
}
impl<State> Deref for CreationConfig<State> {
type Target = CreationConfigFields;
fn deref(&self) -> &CreationConfigFields {
&self.fields
}
}
impl DerefMut for CreationConfig<Unvalidated> {
fn deref_mut(&mut self) -> &mut CreationConfigFields {
&mut self.fields
}
}
impl Default for CreationConfig<Unvalidated> {
fn default() -> Self {
Self {
fields: CreationConfigFields::default(),
_marker: PhantomData,
}
}
}
impl CreationConfig<Unvalidated> {
#[must_use]
pub fn new() -> Self {
Self::default()
}
#[must_use]
pub fn with_follow_symlinks(mut self, follow: bool) -> Self {
self.fields.follow_symlinks = follow;
self
}
#[must_use]
pub fn with_include_hidden(mut self, include: bool) -> Self {
self.fields.include_hidden = include;
self
}
#[must_use]
pub fn with_max_file_size(mut self, max_size: Option<u64>) -> Self {
self.fields.max_file_size = max_size;
self
}
#[must_use]
pub fn with_exclude_patterns(mut self, patterns: Vec<String>) -> Self {
self.fields.exclude_patterns = patterns;
self
}
#[must_use]
pub fn with_strip_prefix(mut self, prefix: Option<PathBuf>) -> Self {
self.fields.strip_prefix = prefix;
self
}
pub fn with_compression_level(mut self, level: u8) -> Result<Self> {
if !(1..=9).contains(&level) {
return Err(ArchiveError::InvalidCompressionLevel { level });
}
self.fields.compression_level = Some(level);
Ok(self)
}
#[must_use]
pub fn with_preserve_permissions(mut self, preserve: bool) -> Self {
self.fields.preserve_permissions = preserve;
self
}
#[must_use]
pub fn with_format(mut self, format: Option<ArchiveType>) -> Self {
self.fields.format = format;
self
}
pub fn validate(self) -> Result<CreationConfig<Validated>> {
if let Some(level) = self.fields.compression_level
&& !(1..=9).contains(&level)
{
return Err(ArchiveError::InvalidCompressionLevel { level });
}
Ok(CreationConfig {
fields: self.fields,
_marker: PhantomData,
})
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::assert_matches;
#[test]
fn test_creation_config_default() {
let config = CreationConfig::default();
assert!(!config.follow_symlinks);
assert!(!config.include_hidden);
assert_eq!(config.max_file_size, None);
assert_eq!(config.exclude_patterns.len(), 3);
assert!(config.exclude_patterns.contains(&".git".to_string()));
assert!(config.exclude_patterns.contains(&".DS_Store".to_string()));
assert!(config.exclude_patterns.contains(&"*.tmp".to_string()));
assert_eq!(config.strip_prefix, None);
assert_eq!(config.compression_level, Some(6));
assert!(config.preserve_permissions);
assert_eq!(config.format, None);
}
#[test]
#[allow(clippy::unwrap_used)]
fn test_creation_config_builder() {
let config = CreationConfig::default()
.with_follow_symlinks(true)
.with_include_hidden(true)
.with_max_file_size(Some(1024 * 1024))
.with_exclude_patterns(vec!["*.log".to_string()])
.with_strip_prefix(Some(PathBuf::from("/base")))
.with_compression_level(9)
.unwrap()
.with_preserve_permissions(false)
.with_format(Some(ArchiveType::TarGz));
assert!(config.follow_symlinks);
assert!(config.include_hidden);
assert_eq!(config.max_file_size, Some(1024 * 1024));
assert_eq!(config.exclude_patterns, vec!["*.log".to_string()]);
assert_eq!(config.strip_prefix, Some(PathBuf::from("/base")));
assert_eq!(config.compression_level, Some(9));
assert!(!config.preserve_permissions);
assert_eq!(config.format, Some(ArchiveType::TarGz));
}
#[test]
#[allow(clippy::unwrap_used, clippy::field_reassign_with_default)]
fn test_creation_config_validate_valid() {
let config = CreationConfig::default();
assert!(config.validate().is_ok());
let config = CreationConfig::default().with_compression_level(1).unwrap();
assert!(config.validate().is_ok());
let config = CreationConfig::default().with_compression_level(9).unwrap();
assert!(config.validate().is_ok());
let mut config = CreationConfig::default();
config.compression_level = None;
assert!(config.validate().is_ok());
}
#[test]
#[allow(clippy::unwrap_used, clippy::field_reassign_with_default)]
fn test_creation_config_validate_invalid() {
let mut config = CreationConfig::default();
config.compression_level = Some(0);
let result = config.validate();
assert!(result.is_err());
assert_matches!(
result.unwrap_err(),
ArchiveError::InvalidCompressionLevel { level: 0 }
);
let mut config = CreationConfig::default();
config.compression_level = Some(10);
let result = config.validate();
assert!(result.is_err());
assert_matches!(
result.unwrap_err(),
ArchiveError::InvalidCompressionLevel { level: 10 }
);
}
#[test]
fn test_creation_config_builder_invalid_compression() {
assert_matches!(
CreationConfig::default().with_compression_level(0),
Err(ArchiveError::InvalidCompressionLevel { level: 0 })
);
assert_matches!(
CreationConfig::default().with_compression_level(10),
Err(ArchiveError::InvalidCompressionLevel { level: 10 })
);
}
#[test]
fn test_creation_config_new() {
let config = CreationConfig::new();
assert_eq!(config.compression_level, Some(6));
assert!(config.preserve_permissions);
}
#[test]
fn test_creation_config_secure_defaults() {
let config = CreationConfig::default();
assert!(
!config.follow_symlinks,
"should not follow symlinks by default (security)"
);
assert!(
!config.include_hidden,
"should not include hidden files by default"
);
assert!(
config.exclude_patterns.contains(&".git".to_string()),
"should exclude .git by default"
);
}
#[test]
#[allow(clippy::field_reassign_with_default)]
fn test_validate_rejects_forged_out_of_range_compression_level() {
let mut config = CreationConfig::default();
config.compression_level = Some(200);
let result = config.validate();
assert_matches!(
result,
Err(ArchiveError::InvalidCompressionLevel { level: 200 })
);
}
}