this repo has no description
0
fork

Configure Feed

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

at eeb60a130d5c62af7e5265c53679a7a52ce4f786 278 lines 8.3 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#[derive(Args, Debug)] 9pub struct CommonArgs { 10 /// Input file/directory 11 #[arg(short, long)] 12 pub input: Option<String>, 13 14 /// Output file/directory 15 #[arg(short, long)] 16 pub output: Option<String>, 17 18 /// Compress the input (default) 19 #[arg(short, long)] 20 pub compress: bool, 21 22 /// Extract the input 23 #[arg(short, long)] 24 pub extract: bool, 25 26 /// Decompress the input. Alias of --extract 27 #[arg(short, long)] 28 pub decompress: bool, 29 30 /// List of I/O. 31 /// This consists of all the inputs followed by the single output, with intelligent fallback to stdin/stdout. 32 #[arg()] 33 pub io_list: Vec<String>, 34 35 /// Ignore pipes when inferring I/O 36 #[arg(long)] 37 pub ignore_pipes: bool, 38 39 /// Ignore stdin when inferring I/O 40 #[arg(long)] 41 pub ignore_stdin: bool, 42 43 /// Ignore stdout when inferring I/O 44 #[arg(long)] 45 pub ignore_stdout: bool, 46} 47 48/// Trait for validating compression levels for different compressors 49#[allow(dead_code)] 50pub trait CompressionLevelValidator { 51 /// Get the minimum valid compression level 52 fn min_level(&self) -> i32; 53 54 /// Get the maximum valid compression level 55 fn max_level(&self) -> i32; 56 57 /// Get the default compression level 58 fn default_level(&self) -> i32; 59 60 /// Map special names to compression levels 61 fn name_to_level(&self, name: &str) -> Option<i32>; 62 63 /// Validate if a compression level is within the valid range 64 fn is_valid_level(&self, level: i32) -> bool { 65 level >= self.min_level() && level <= self.max_level() 66 } 67 68 /// Validate and clamp a compression level to the valid range 69 fn validate_and_clamp_level(&self, level: i32) -> i32 { 70 if level < self.min_level() { 71 self.min_level() 72 } else if level > self.max_level() { 73 self.max_level() 74 } else { 75 level 76 } 77 } 78} 79 80/// Default implementation for most compressors (0-9 range) 81#[derive(Debug, Clone, Copy)] 82pub struct DefaultCompressionValidator; 83 84impl CompressionLevelValidator for DefaultCompressionValidator { 85 fn min_level(&self) -> i32 { 86 0 87 } 88 fn max_level(&self) -> i32 { 89 9 90 } 91 fn default_level(&self) -> i32 { 92 6 93 } 94 95 fn name_to_level(&self, name: &str) -> Option<i32> { 96 match name.to_lowercase().as_str() { 97 "none" => Some(0), 98 "fast" => Some(1), 99 "best" => Some(9), 100 _ => None, 101 } 102 } 103} 104 105#[derive(Debug, Clone, Copy)] 106pub struct CompressionLevel { 107 pub level: i32, 108} 109 110impl Default for CompressionLevel { 111 fn default() -> Self { 112 CompressionLevel { level: 6 } 113 } 114} 115 116impl FromStr for CompressionLevel { 117 type Err = &'static str; 118 119 fn from_str(s: &str) -> Result<Self, Self::Err> { 120 // Check for an int 121 if let Ok(level) = s.parse::<i32>() { 122 return Ok(CompressionLevel { level }); 123 } 124 125 // Try to parse special names 126 let s = s.to_lowercase(); 127 match s.as_str() { 128 "none" | "fast" | "best" => Ok(CompressionLevel { 129 // We'll use the DefaultCompressionValidator values here 130 // The actual compressor will interpret these values according to its own validator 131 level: DefaultCompressionValidator.name_to_level(&s).unwrap(), 132 }), 133 _ => Err("Invalid compression level"), 134 } 135 } 136} 137 138#[derive(Args, Debug, Default, Clone, Copy)] 139pub struct LevelArgs { 140 /// Level of compression. 141 /// `none`, `fast`, and `best` are mapped to appropriate values for each compressor. 142 #[arg(long, default_value = "fast")] 143 pub level: CompressionLevel, 144} 145 146/// Common interface for all compressor implementations 147#[allow(unused_variables)] 148pub trait Compressor { 149 /// Name of this Compressor 150 fn name(&self) -> &str; 151 152 /// Default extension for this Compressor 153 fn extension(&self) -> &str { 154 self.name() 155 } 156 157 /// Detect if the input is an archive of this type 158 /// Just checks the extension by default 159 /// Some compressors may overwrite this to do more advanced detection 160 fn is_archive(&self, in_path: &Path) -> bool { 161 if in_path.extension().is_none() { 162 return false; 163 } 164 in_path.extension().unwrap() == self.extension() 165 } 166 167 /// Generate the default name for the compressed file 168 fn default_compressed_filename(&self, in_path: &Path) -> String { 169 format!( 170 "{}.{}", 171 in_path 172 .file_name() 173 .unwrap_or_else(|| OsStr::new("archive")) 174 .to_str() 175 .unwrap(), 176 self.extension() 177 ) 178 } 179 180 /// Generate the default extracted filename 181 fn default_extracted_filename(&self, in_path: &Path) -> String { 182 // If the file has the extension for this type, return the filename without the extension 183 if in_path.extension().unwrap() == self.extension() { 184 return in_path.file_stem().unwrap().to_str().unwrap().to_string(); 185 } 186 // If the file has no extension, return the current directory 187 if in_path.extension().is_none() { 188 return ".".to_string(); 189 } 190 // Otherwise, return the current directory and hope for the best 191 ".".to_string() 192 } 193 194 fn compress(&self, input: CmprssInput, output: CmprssOutput) -> Result<(), io::Error> { 195 cmprss_error("compress_target unimplemented") 196 } 197 198 fn extract(&self, input: CmprssInput, output: CmprssOutput) -> Result<(), io::Error> { 199 cmprss_error("extract_target unimplemented") 200 } 201} 202 203impl fmt::Debug for dyn Compressor { 204 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 205 write!(f, "Compressor {{ name: {} }}", self.name()) 206 } 207} 208 209pub fn cmprss_error(message: &str) -> Result<(), io::Error> { 210 Err(io::Error::new(io::ErrorKind::Other, message)) 211} 212 213/// Defines the possible inputs of a compressor 214#[derive(Debug)] 215pub enum CmprssInput { 216 /// Path(s) to the input files. 217 Path(Vec<PathBuf>), 218 /// Input pipe 219 Pipe(std::io::Stdin), 220} 221 222/// Defines the possible outputs of a compressor 223#[derive(Debug)] 224pub enum CmprssOutput { 225 Path(PathBuf), 226 Pipe(std::io::Stdout), 227} 228 229#[cfg(test)] 230mod tests { 231 use super::*; 232 233 #[test] 234 fn test_compression_level_parsing() { 235 // Test numeric values 236 assert_eq!(CompressionLevel::from_str("-7").unwrap().level, -7); 237 assert_eq!(CompressionLevel::from_str("0").unwrap().level, 0); 238 assert_eq!(CompressionLevel::from_str("1").unwrap().level, 1); 239 assert_eq!(CompressionLevel::from_str("9").unwrap().level, 9); 240 assert_eq!(CompressionLevel::from_str("22").unwrap().level, 22); 241 242 // Test special names (these use DefaultCompressionValidator values) 243 assert_eq!(CompressionLevel::from_str("none").unwrap().level, 0); 244 assert_eq!(CompressionLevel::from_str("fast").unwrap().level, 1); 245 assert_eq!(CompressionLevel::from_str("best").unwrap().level, 9); 246 247 // Test invalid values 248 assert!(CompressionLevel::from_str("foo").is_err()); 249 } 250 251 #[test] 252 fn test_default_compression_validator() { 253 let validator = DefaultCompressionValidator; 254 255 // Test range 256 assert_eq!(validator.min_level(), 0); 257 assert_eq!(validator.max_level(), 9); 258 assert_eq!(validator.default_level(), 6); 259 260 // Test validation 261 assert!(validator.is_valid_level(0)); 262 assert!(validator.is_valid_level(5)); 263 assert!(validator.is_valid_level(9)); 264 assert!(!validator.is_valid_level(-1)); 265 assert!(!validator.is_valid_level(10)); 266 267 // Test clamping 268 assert_eq!(validator.validate_and_clamp_level(-1), 0); 269 assert_eq!(validator.validate_and_clamp_level(5), 5); 270 assert_eq!(validator.validate_and_clamp_level(10), 9); 271 272 // Test special names 273 assert_eq!(validator.name_to_level("none"), Some(0)); 274 assert_eq!(validator.name_to_level("fast"), Some(1)); 275 assert_eq!(validator.name_to_level("best"), Some(9)); 276 assert_eq!(validator.name_to_level("invalid"), None); 277 } 278}