this repo has no description
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}