this repo has no description
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

at 52ec9ed3a28388caee2e1d877b9b3eaf727655b6 417 lines 12 kB view raw
1use clap::Args; 2use std::ffi::OsStr; 3use std::fmt; 4use std::io; 5use std::path::{Path, PathBuf}; 6use std::str::FromStr; 7 8/// Enum to represent whether a compressor extracts to a file or directory by default 9#[derive(Debug, Clone, Copy, PartialEq, Eq)] 10pub enum ExtractedTarget { 11 /// Extract to a single file (e.g., gzip, bzip2, xz) 12 FILE, 13 /// Extract to a directory (e.g., zip, tar) 14 DIRECTORY, 15} 16 17#[derive(Args, Debug)] 18pub struct CommonArgs { 19 /// Input file/directory 20 #[arg(short, long)] 21 pub input: Option<String>, 22 23 /// Output file/directory 24 #[arg(short, long)] 25 pub output: Option<String>, 26 27 /// Compress the input (default) 28 #[arg(short, long)] 29 pub compress: bool, 30 31 /// Extract the input 32 #[arg(short, long)] 33 pub extract: bool, 34 35 /// Decompress the input. Alias of --extract 36 #[arg(short, long)] 37 pub decompress: bool, 38 39 /// List of I/O. 40 /// This consists of all the inputs followed by the single output, with intelligent fallback to stdin/stdout. 41 #[arg()] 42 pub io_list: Vec<String>, 43 44 /// Ignore pipes when inferring I/O 45 #[arg(long)] 46 pub ignore_pipes: bool, 47 48 /// Ignore stdin when inferring I/O 49 #[arg(long)] 50 pub ignore_stdin: bool, 51 52 /// Ignore stdout when inferring I/O 53 #[arg(long)] 54 pub ignore_stdout: bool, 55} 56 57/// Trait for validating compression levels for different compressors 58#[allow(dead_code)] 59pub trait CompressionLevelValidator { 60 /// Get the minimum valid compression level 61 fn min_level(&self) -> i32; 62 63 /// Get the maximum valid compression level 64 fn max_level(&self) -> i32; 65 66 /// Get the default compression level 67 fn default_level(&self) -> i32; 68 69 /// Map special names to compression levels 70 fn name_to_level(&self, name: &str) -> Option<i32>; 71 72 /// Validate if a compression level is within the valid range 73 fn is_valid_level(&self, level: i32) -> bool { 74 level >= self.min_level() && level <= self.max_level() 75 } 76 77 /// Validate and clamp a compression level to the valid range 78 fn validate_and_clamp_level(&self, level: i32) -> i32 { 79 if level < self.min_level() { 80 self.min_level() 81 } else if level > self.max_level() { 82 self.max_level() 83 } else { 84 level 85 } 86 } 87} 88 89/// Default implementation for most compressors (0-9 range) 90#[derive(Debug, Clone, Copy)] 91pub struct DefaultCompressionValidator; 92 93impl CompressionLevelValidator for DefaultCompressionValidator { 94 fn min_level(&self) -> i32 { 95 0 96 } 97 fn max_level(&self) -> i32 { 98 9 99 } 100 fn default_level(&self) -> i32 { 101 6 102 } 103 104 fn name_to_level(&self, name: &str) -> Option<i32> { 105 match name.to_lowercase().as_str() { 106 "none" => Some(0), 107 "fast" => Some(1), 108 "best" => Some(9), 109 _ => None, 110 } 111 } 112} 113 114#[derive(Debug, Clone, Copy)] 115pub struct CompressionLevel { 116 pub level: i32, 117} 118 119impl Default for CompressionLevel { 120 fn default() -> Self { 121 CompressionLevel { level: 6 } 122 } 123} 124 125impl FromStr for CompressionLevel { 126 type Err = &'static str; 127 128 fn from_str(s: &str) -> Result<Self, Self::Err> { 129 // Check for an int 130 if let Ok(level) = s.parse::<i32>() { 131 return Ok(CompressionLevel { level }); 132 } 133 134 // Try to parse special names 135 let s = s.to_lowercase(); 136 match s.as_str() { 137 "none" | "fast" | "best" => Ok(CompressionLevel { 138 // We'll use the DefaultCompressionValidator values here 139 // The actual compressor will interpret these values according to its own validator 140 level: DefaultCompressionValidator.name_to_level(&s).unwrap(), 141 }), 142 _ => Err("Invalid compression level"), 143 } 144 } 145} 146 147#[derive(Args, Debug, Default, Clone, Copy)] 148pub struct LevelArgs { 149 /// Level of compression. 150 /// `none`, `fast`, and `best` are mapped to appropriate values for each compressor. 151 #[arg(long, default_value = "fast")] 152 pub level: CompressionLevel, 153} 154 155/// Common interface for all compressor implementations 156#[allow(unused_variables)] 157pub trait Compressor { 158 /// Name of this Compressor 159 fn name(&self) -> &str; 160 161 /// Default extension for this Compressor 162 fn extension(&self) -> &str { 163 self.name() 164 } 165 166 /// Determine if this compressor extracts to a file or directory by default 167 /// FILE compressors (like gzip, bzip2, xz) extract to a single file 168 /// DIRECTORY compressors (like zip, tar) extract to a directory 169 fn default_extracted_target(&self) -> ExtractedTarget { 170 ExtractedTarget::FILE 171 } 172 173 /// Detect if the input is an archive of this type 174 /// Just checks the extension by default 175 /// Some compressors may overwrite this to do more advanced detection 176 fn is_archive(&self, in_path: &Path) -> bool { 177 if in_path.extension().is_none() { 178 return false; 179 } 180 in_path.extension().unwrap() == self.extension() 181 } 182 183 /// Generate the default name for the compressed file 184 fn default_compressed_filename(&self, in_path: &Path) -> String { 185 format!( 186 "{}.{}", 187 in_path 188 .file_name() 189 .unwrap_or_else(|| OsStr::new("archive")) 190 .to_str() 191 .unwrap(), 192 self.extension() 193 ) 194 } 195 196 /// Generate the default extracted filename 197 fn default_extracted_filename(&self, in_path: &Path) -> String { 198 if self.default_extracted_target() == ExtractedTarget::DIRECTORY { 199 return ".".to_string(); 200 } 201 202 // If the file has no extension, return the current directory 203 if let Some(ext) = in_path.extension() { 204 // If the file has the extension for this type, return the filename without the extension 205 if let Some(ext_str) = ext.to_str() { 206 if ext_str == self.extension() { 207 if let Some(stem) = in_path.file_stem() { 208 if let Some(stem_str) = stem.to_str() { 209 return stem_str.to_string(); 210 } 211 } 212 } 213 } 214 } 215 "archive".to_string() 216 } 217 218 fn compress(&self, input: CmprssInput, output: CmprssOutput) -> Result<(), io::Error>; 219 220 fn extract(&self, input: CmprssInput, output: CmprssOutput) -> Result<(), io::Error>; 221} 222 223impl fmt::Debug for dyn Compressor { 224 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 225 write!(f, "Compressor {{ name: {} }}", self.name()) 226 } 227} 228 229pub fn cmprss_error(message: &str) -> Result<(), io::Error> { 230 Err(io::Error::new(io::ErrorKind::Other, message)) 231} 232 233/// Defines the possible inputs of a compressor 234#[derive(Debug)] 235pub enum CmprssInput { 236 /// Path(s) to the input files. 237 Path(Vec<PathBuf>), 238 /// Input pipe 239 Pipe(std::io::Stdin), 240} 241 242/// Defines the possible outputs of a compressor 243#[derive(Debug)] 244pub enum CmprssOutput { 245 Path(PathBuf), 246 Pipe(std::io::Stdout), 247} 248 249#[cfg(test)] 250mod tests { 251 use super::*; 252 use std::io; 253 use std::path::Path; 254 255 /// A simple implementation of the Compressor trait for testing 256 struct TestCompressor; 257 258 impl Compressor for TestCompressor { 259 fn name(&self) -> &str { 260 "test" 261 } 262 263 // We'll use the default implementation for extension() and other methods 264 265 fn compress(&self, _: CmprssInput, _: CmprssOutput) -> Result<(), io::Error> { 266 // Return success for testing purposes 267 Ok(()) 268 } 269 270 fn extract(&self, _: CmprssInput, _: CmprssOutput) -> Result<(), io::Error> { 271 // Return success for testing purposes 272 Ok(()) 273 } 274 } 275 276 /// A compressor that overrides the default extension 277 struct CustomExtensionCompressor; 278 279 impl Compressor for CustomExtensionCompressor { 280 fn name(&self) -> &str { 281 "custom" 282 } 283 284 fn extension(&self) -> &str { 285 "cst" 286 } 287 288 fn compress(&self, _: CmprssInput, _: CmprssOutput) -> Result<(), io::Error> { 289 Ok(()) 290 } 291 292 fn extract(&self, _: CmprssInput, _: CmprssOutput) -> Result<(), io::Error> { 293 Ok(()) 294 } 295 } 296 297 #[test] 298 fn test_default_name_extension() { 299 let compressor = TestCompressor; 300 assert_eq!(compressor.name(), "test"); 301 assert_eq!(compressor.extension(), "test"); 302 } 303 304 #[test] 305 fn test_custom_extension() { 306 let compressor = CustomExtensionCompressor; 307 assert_eq!(compressor.name(), "custom"); 308 assert_eq!(compressor.extension(), "cst"); 309 } 310 311 #[test] 312 fn test_is_archive_detection() { 313 use tempfile::tempdir; 314 315 let compressor = TestCompressor; 316 let temp_dir = tempdir().expect("Failed to create temp dir"); 317 318 // Test with matching extension 319 let archive_path = temp_dir.path().join("archive.test"); 320 std::fs::File::create(&archive_path).expect("Failed to create test file"); 321 assert!(compressor.is_archive(&archive_path)); 322 323 // Test with non-matching extension 324 let non_archive_path = temp_dir.path().join("archive.txt"); 325 std::fs::File::create(&non_archive_path).expect("Failed to create test file"); 326 assert!(!compressor.is_archive(&non_archive_path)); 327 328 // Test with no extension 329 let no_ext_path = temp_dir.path().join("archive"); 330 std::fs::File::create(&no_ext_path).expect("Failed to create test file"); 331 assert!(!compressor.is_archive(&no_ext_path)); 332 } 333 334 #[test] 335 fn test_default_compressed_filename() { 336 let compressor = TestCompressor; 337 338 // Test with normal filename 339 let path = Path::new("file.txt"); 340 assert_eq!( 341 compressor.default_compressed_filename(path), 342 "file.txt.test" 343 ); 344 345 // Test with no extension 346 let path = Path::new("file"); 347 assert_eq!(compressor.default_compressed_filename(path), "file.test"); 348 } 349 350 #[test] 351 fn test_default_extracted_filename() { 352 let compressor = TestCompressor; 353 354 // Test with matching extension 355 let path = Path::new("archive.test"); 356 assert_eq!(compressor.default_extracted_filename(path), "archive"); 357 358 // Test with non-matching extension 359 let path = Path::new("archive.txt"); 360 assert_eq!(compressor.default_extracted_filename(path), "archive"); 361 362 // Test with no extension 363 let path = Path::new("archive"); 364 assert_eq!(compressor.default_extracted_filename(path), "archive"); 365 } 366 367 #[test] 368 fn test_compression_level_parsing() { 369 // Test numeric levels 370 assert_eq!(CompressionLevel::from_str("1").unwrap().level, 1); 371 assert_eq!(CompressionLevel::from_str("9").unwrap().level, 9); 372 373 // Test named levels 374 let validator = DefaultCompressionValidator; 375 assert_eq!( 376 CompressionLevel::from_str("fast").unwrap().level, 377 validator.name_to_level("fast").unwrap() 378 ); 379 assert_eq!( 380 CompressionLevel::from_str("best").unwrap().level, 381 validator.name_to_level("best").unwrap() 382 ); 383 384 // Test invalid values 385 assert!(CompressionLevel::from_str("invalid").is_err()); 386 } 387 388 #[test] 389 fn test_compression_level_defaults() { 390 let default_level = CompressionLevel::default(); 391 let validator = DefaultCompressionValidator; 392 assert_eq!(default_level.level, validator.default_level()); 393 } 394 395 #[test] 396 fn test_cmprss_error() { 397 let result = cmprss_error("test error"); 398 assert!(result.is_err()); 399 assert_eq!(result.unwrap_err().to_string(), "test error"); 400 } 401 402 #[test] 403 fn test_default_compression_validator() { 404 let validator = DefaultCompressionValidator; 405 406 use crate::test_utils::test_compression_validator_helper; 407 test_compression_validator_helper( 408 &validator, 409 0, // min_level 410 9, // max_level 411 6, // default_level 412 Some(1), // fast_name_level 413 Some(9), // best_name_level 414 Some(0), // none_name_level 415 ); 416 } 417}