this repo has no description
1pub mod backends;
2pub mod progress;
3pub mod test_utils;
4pub mod utils;
5
6use backends::*;
7use clap::{Parser, Subcommand};
8use is_terminal::IsTerminal;
9use std::path::{Path, PathBuf};
10use std::{io, vec};
11use utils::*;
12
13/// A compression multi-tool
14#[derive(Parser, Debug)]
15#[command(author, version, about, long_about = None)]
16struct CmprssArgs {
17 /// Format
18 #[command(subcommand)]
19 format: Option<Format>,
20
21 // Base arguments for the non-subcommand behavior
22 #[clap(flatten)]
23 pub base_args: CommonArgs,
24}
25#[derive(Subcommand, Debug)]
26enum Format {
27 /// tar archive format
28 Tar(TarArgs),
29
30 /// gzip compression
31 #[clap(visible_alias = "gz")]
32 Gzip(GzipArgs),
33
34 /// xz compression
35 Xz(XzArgs),
36
37 /// bzip2 compression
38 #[clap(visible_alias = "bz2")]
39 Bzip2(Bzip2Args),
40
41 /// zip archive format
42 Zip(ZipArgs),
43
44 /// zstd compression
45 #[clap(visible_alias = "zst")]
46 Zstd(ZstdArgs),
47
48 /// lz4 compression
49 Lz4(Lz4Args),
50}
51
52/// Get the input filename or return a default file
53/// This file will be used to generate the output filename
54fn get_input_filename(input: &CmprssInput) -> Result<&Path, io::Error> {
55 match input {
56 CmprssInput::Path(paths) => {
57 if paths.is_empty() {
58 return Err(io::Error::new(
59 io::ErrorKind::Other,
60 "error: no input specified",
61 ));
62 }
63 Ok(paths.first().unwrap())
64 }
65 CmprssInput::Pipe(_) => Ok(Path::new("archive")),
66 }
67}
68
69#[derive(Debug, PartialEq, Clone, Copy)]
70enum Action {
71 Compress,
72 Extract,
73 Unknown,
74}
75
76/// Defines a single compress/extract action to take.
77#[derive(Debug)]
78struct Job {
79 compressor: Box<dyn Compressor>,
80 input: CmprssInput,
81 output: CmprssOutput,
82 action: Action,
83}
84
85/// Get a compressor from a filename
86fn get_compressor_from_filename(filename: &Path) -> Option<Box<dyn Compressor>> {
87 // TODO: Support multi-level files, like tar.gz
88 let compressors: Vec<Box<dyn Compressor>> = vec![
89 Box::<Tar>::default(),
90 Box::<Gzip>::default(),
91 Box::<Xz>::default(),
92 Box::<Bzip2>::default(),
93 Box::<Zip>::default(),
94 Box::<Zstd>::default(),
95 Box::<Lz4>::default(),
96 ];
97 compressors.into_iter().find(|c| c.is_archive(filename))
98}
99
100/// Convert an input path into a Path
101fn get_path(input: &str) -> Option<PathBuf> {
102 let path = PathBuf::from(input);
103 if !path.try_exists().unwrap_or(false) {
104 return None;
105 }
106 Some(path)
107}
108
109/// Guess compressor/action from the two filenames
110/// The compressor may already be given
111fn guess_from_filenames(
112 input: &[PathBuf],
113 output: &Path,
114 compressor: Option<Box<dyn Compressor>>,
115) -> (Option<Box<dyn Compressor>>, Action) {
116 if input.len() != 1 {
117 if let Some(guessed_compressor) = get_compressor_from_filename(output) {
118 return (Some(guessed_compressor), Action::Compress);
119 }
120 // In theory we could be extracting multiple files to a directory
121 // We'll fail somewhere else if that's not the case
122 return (compressor, Action::Extract);
123 }
124 let input = input.first().unwrap();
125
126 let guessed_compressor = get_compressor_from_filename(output);
127 let guessed_extractor = get_compressor_from_filename(input);
128 let guessed_compressor_name = if let Some(c) = &guessed_compressor {
129 c.name()
130 } else {
131 ""
132 };
133 let guessed_extractor_name = if let Some(e) = &guessed_extractor {
134 e.name()
135 } else {
136 ""
137 };
138
139 if let Some(c) = &compressor {
140 if guessed_compressor_name == c.name() {
141 return (compressor, Action::Compress);
142 } else if guessed_extractor_name == c.name() {
143 return (compressor, Action::Extract);
144 } else {
145 // Default to compressing
146 return (compressor, Action::Compress);
147 }
148 }
149
150 match (guessed_compressor, guessed_extractor) {
151 (None, None) => (None, Action::Unknown),
152 (Some(c), None) => (Some(c), Action::Compress),
153 (None, Some(e)) => (Some(e), Action::Extract),
154 (Some(c), Some(e)) => {
155 if c.name() == e.name() {
156 return (Some(c), Action::Unknown);
157 }
158 // Compare the input and output extensions to see if one has an extra extension
159 let input_file = input.file_name().unwrap().to_str().unwrap();
160 let input_ext = input.extension().unwrap_or_default();
161 let output_file = output.file_name().unwrap().to_str().unwrap();
162 let output_ext = output.extension().unwrap_or_default();
163 let guessed_output = input_file.to_string() + "." + output_ext.to_str().unwrap();
164 let guessed_input = output_file.to_string() + "." + input_ext.to_str().unwrap();
165 if guessed_output == output_file {
166 (Some(c), Action::Compress)
167 } else if guessed_input == input_file {
168 (Some(e), Action::Extract)
169 } else {
170 (None, Action::Unknown)
171 }
172 }
173 }
174}
175
176/// Parse the common args and determine the details of the job requested
177fn get_job(
178 compressor: Option<Box<dyn Compressor>>,
179 common_args: &CommonArgs,
180) -> Result<Job, io::Error> {
181 let mut compressor = compressor;
182 let mut action = {
183 if common_args.compress {
184 Action::Compress
185 } else if common_args.extract || common_args.decompress {
186 Action::Extract
187 } else {
188 Action::Unknown
189 }
190 };
191
192 let mut inputs = Vec::new();
193 if let Some(in_file) = &common_args.input {
194 match get_path(in_file) {
195 Some(path) => inputs.push(path),
196 None => {
197 return Err(io::Error::new(
198 io::ErrorKind::Other,
199 "Specified input path does not exist",
200 ));
201 }
202 }
203 }
204
205 let mut output = match &common_args.output {
206 Some(output) => {
207 let path = Path::new(output);
208 if path.try_exists()? && !path.is_dir() {
209 // Output path exists, bail out
210 return Err(io::Error::new(
211 io::ErrorKind::Other,
212 "Specified output path already exists",
213 ));
214 }
215 Some(path)
216 }
217 None => None,
218 };
219
220 // Process the io_list, check if there is an output first
221 let mut io_list = common_args.io_list.clone();
222 if output.is_none() {
223 if let Some(possible_output) = common_args.io_list.last() {
224 let path = Path::new(possible_output);
225 if !path.try_exists()? {
226 // Use the given path if it doesn't exist
227 output = Some(path);
228 io_list.pop();
229 } else if path.is_dir() {
230 match action {
231 Action::Compress => {
232 // A directory can potentially be a target output location or
233 // an input, for now assume it is an input.
234 }
235 Action::Extract => {
236 // Can extract to a directory, and it wouldn't make any sense as an input
237 output = Some(path);
238 io_list.pop();
239 }
240 _ => {
241 // TODO: don't know if this is an input or output, assume we're compressing this directory
242 // This does cause problems for inferencing "cat archive.tar | cmprss tar ."
243 // Probably need to add some special casing
244 }
245 };
246 } else {
247 // TODO: check for scenarios where we want to append to an existing archive
248 }
249 }
250 }
251
252 // Validate the specified inputs
253 // Everything in the io_list should be an input
254 for input in &io_list {
255 if let Some(path) = get_path(input) {
256 inputs.push(path);
257 } else {
258 return Err(io::Error::new(
259 io::ErrorKind::Other,
260 "Specified input path does not exist",
261 ));
262 }
263 }
264
265 // Fallback to stdin/stdout if we're missing files
266 let cmprss_input = match inputs.is_empty() {
267 true => {
268 if !std::io::stdin().is_terminal()
269 && !&common_args.ignore_pipes
270 && !&common_args.ignore_stdin
271 {
272 CmprssInput::Pipe(std::io::stdin())
273 } else {
274 return Err(io::Error::new(io::ErrorKind::Other, "No specified input"));
275 }
276 }
277 false => CmprssInput::Path(inputs),
278 };
279
280 let cmprss_output = match output {
281 Some(path) => CmprssOutput::Path(path.to_path_buf()),
282 None => {
283 if !std::io::stdout().is_terminal()
284 && !&common_args.ignore_pipes
285 && !&common_args.ignore_stdout
286 {
287 CmprssOutput::Pipe(std::io::stdout())
288 } else {
289 match action {
290 Action::Compress => {
291 if compressor.is_none() {
292 return Err(io::Error::new(
293 io::ErrorKind::Other,
294 "Must specify a compressor",
295 ));
296 }
297 CmprssOutput::Path(PathBuf::from(
298 compressor
299 .as_ref()
300 .unwrap()
301 .default_compressed_filename(get_input_filename(&cmprss_input)?),
302 ))
303 }
304 Action::Extract => {
305 if compressor.is_none() {
306 compressor =
307 get_compressor_from_filename(get_input_filename(&cmprss_input)?);
308 if compressor.is_none() {
309 return Err(io::Error::new(
310 io::ErrorKind::Other,
311 "Must specify a compressor",
312 ));
313 }
314 }
315 CmprssOutput::Path(PathBuf::from(
316 compressor
317 .as_ref()
318 .unwrap()
319 .default_extracted_filename(get_input_filename(&cmprss_input)?),
320 ))
321 }
322 Action::Unknown => {
323 if compressor.is_none() {
324 // Can still work if the input is an archive
325 compressor =
326 get_compressor_from_filename(get_input_filename(&cmprss_input)?);
327 if compressor.is_none() {
328 return Err(io::Error::new(
329 io::ErrorKind::Other,
330 "Must specify a compressor",
331 ));
332 }
333 action = Action::Extract;
334 CmprssOutput::Path(PathBuf::from(
335 compressor
336 .as_ref()
337 .unwrap()
338 .default_extracted_filename(get_input_filename(&cmprss_input)?),
339 ))
340 } else {
341 // We know the compressor, does the input have the same extension?
342 if let Some(compressor_from_input) =
343 get_compressor_from_filename(get_input_filename(&cmprss_input)?)
344 {
345 if compressor.as_ref().unwrap().name()
346 == compressor_from_input.name()
347 {
348 action = Action::Extract;
349 CmprssOutput::Path(PathBuf::from(
350 compressor.as_ref().unwrap().default_extracted_filename(
351 get_input_filename(&cmprss_input)?,
352 ),
353 ))
354 } else {
355 action = Action::Compress;
356 CmprssOutput::Path(PathBuf::from(
357 compressor.as_ref().unwrap().default_compressed_filename(
358 get_input_filename(&cmprss_input)?,
359 ),
360 ))
361 }
362 } else {
363 action = Action::Compress;
364 CmprssOutput::Path(PathBuf::from(
365 compressor.as_ref().unwrap().default_compressed_filename(
366 get_input_filename(&cmprss_input)?,
367 ),
368 ))
369 }
370 }
371 }
372 }
373 }
374 }
375 };
376
377 // If we don't have the compressor/action, we can attempt to infer
378 if compressor.is_none() || action == Action::Unknown {
379 match action {
380 Action::Compress => {
381 // Look at the output name
382 // TODO: tar.gz ??
383 if let CmprssOutput::Path(path) = &cmprss_output {
384 compressor = get_compressor_from_filename(path);
385 }
386 }
387 Action::Extract => {
388 // Look at the input name
389 if let CmprssInput::Path(paths) = &cmprss_input {
390 if paths.len() != 1 {
391 // Can't guess if there are multiple inputs
392 return Err(io::Error::new(
393 io::ErrorKind::Other,
394 "Can't guess compressor with multiple inputs",
395 ));
396 }
397 compressor = get_compressor_from_filename(paths.first().unwrap());
398 }
399 }
400 Action::Unknown => match (&cmprss_input, &cmprss_output) {
401 (CmprssInput::Pipe(_), CmprssOutput::Path(path)) => {
402 if compressor.is_none() {
403 compressor = get_compressor_from_filename(path);
404 if compressor.is_some() {
405 action = Action::Compress;
406 } else {
407 return Err(io::Error::new(
408 io::ErrorKind::Other,
409 "Can't guess compressor to use",
410 ));
411 }
412 } else if compressor.as_ref().unwrap().name()
413 == get_compressor_from_filename(path).unwrap().name()
414 {
415 action = Action::Compress;
416 } else {
417 action = Action::Extract;
418 }
419 }
420 (CmprssInput::Path(paths), CmprssOutput::Pipe(_)) => {
421 if compressor.is_none() {
422 if paths.len() != 1 {
423 return Err(io::Error::new(
424 io::ErrorKind::Other,
425 "Can't guess compressor with multiple inputs",
426 ));
427 }
428 compressor = get_compressor_from_filename(paths.first().unwrap());
429 if compressor.is_some() {
430 action = Action::Extract;
431 } else {
432 return Err(io::Error::new(
433 io::ErrorKind::Other,
434 "Can't guess compressor to use",
435 ));
436 }
437 } else if let Some(c) = get_compressor_from_filename(paths.first().unwrap()) {
438 if compressor.as_ref().unwrap().name() == c.name() {
439 action = Action::Extract;
440 } else {
441 action = Action::Compress;
442 }
443 } else {
444 action = Action::Compress;
445 }
446 }
447 (CmprssInput::Pipe(_), CmprssOutput::Pipe(_)) => {
448 action = Action::Compress;
449 }
450 (CmprssInput::Path(paths), CmprssOutput::Path(path)) => {
451 let (guessed_compressor, guessed_action) =
452 guess_from_filenames(paths, path, compressor);
453 compressor = guessed_compressor;
454 action = guessed_action;
455 }
456 },
457 }
458 }
459
460 if compressor.is_none() {
461 return Err(io::Error::new(
462 io::ErrorKind::Other,
463 "Could not determine compressor to use",
464 ));
465 }
466 if action == Action::Unknown {
467 return Err(io::Error::new(
468 io::ErrorKind::Other,
469 "Could not determine action to take",
470 ));
471 }
472
473 Ok(Job {
474 compressor: compressor.unwrap(),
475 input: cmprss_input,
476 output: cmprss_output,
477 action,
478 })
479}
480
481fn command(compressor: Option<Box<dyn Compressor>>, args: &CommonArgs) -> Result<(), io::Error> {
482 let job = get_job(compressor, args)?;
483
484 match job.action {
485 Action::Compress => job.compressor.compress(job.input, job.output)?,
486 Action::Extract => job.compressor.extract(job.input, job.output)?,
487 _ => {
488 return Err(io::Error::new(
489 io::ErrorKind::Other,
490 "Unknown action requested",
491 ));
492 }
493 };
494
495 Ok(())
496}
497
498fn main() {
499 let args = CmprssArgs::parse();
500 match args.format {
501 Some(Format::Tar(a)) => command(Some(Box::new(Tar::new(&a))), &a.common_args),
502 Some(Format::Gzip(a)) => command(Some(Box::new(Gzip::new(&a))), &a.common_args),
503 Some(Format::Xz(a)) => command(Some(Box::new(Xz::new(&a))), &a.common_args),
504 Some(Format::Bzip2(a)) => command(Some(Box::new(Bzip2::new(&a))), &a.common_args),
505 Some(Format::Zip(a)) => command(Some(Box::new(Zip::new(&a))), &a.common_args),
506 Some(Format::Zstd(a)) => command(Some(Box::new(Zstd::new(&a))), &a.common_args),
507 Some(Format::Lz4(a)) => command(Some(Box::new(Lz4::new(&a))), &a.common_args),
508 _ => command(None, &args.base_args),
509 }
510 .unwrap_or_else(|e| {
511 eprintln!("ERROR(cmprss): {}", e);
512 std::process::exit(1);
513 });
514}