this repo has no description
0
fork

Configure Feed

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

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