this repo has no description
0
fork

Configure Feed

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

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