Merge pull request #239 from Crypto-Spartan/fix-zip-warning
[ouch.git] / src / commands.rs
blob2aafcbe6e104fb5ecd5c42520e256a93feb0b465
1 //! Core of the crate, where the `compress_files` and `decompress_file` functions are implemented
2 //!
3 //! Also, where correctly call functions based on the detected `Command`.
5 use std::{
6     io::{self, BufReader, BufWriter, Read, Write},
7     ops::ControlFlow,
8     path::{Path, PathBuf},
9 };
11 use fs_err as fs;
12 use utils::colors;
14 use crate::{
15     archive,
16     error::FinalError,
17     extension::{
18         self,
19         CompressionFormat::{self, *},
20         Extension,
21     },
22     info,
23     list::{self, FileInArchive, ListOptions},
24     progress::Progress,
25     utils::{
26         self, concatenate_os_str_list, dir_is_empty, nice_directory_display, to_utf, try_infer_extension,
27         user_wants_to_continue, FileVisibilityPolicy,
28     },
29     warning, Opts, QuestionAction, QuestionPolicy, Subcommand,
32 // Message used to advice the user that .zip archives have limitations that require it to load everything into memory at once
33 // and this can lead to out-of-memory scenarios for archives that are big enough.
34 const ZIP_IN_MEMORY_LIMITATION_WARNING: &str =
35     "\tThere is a limitation for .zip archives with extra extensions. (e.g. <file>.zip.gz)
36 \tThe design of .zip makes it impossible to compress via stream, so it must be done entirely in memory.
37 \tBy compressing .zip with extra compression formats, you can run out of RAM if the file is too large!";
39 // Used in BufReader and BufWriter to perform less syscalls
40 const BUFFER_CAPACITY: usize = 1024 * 64;
42 fn represents_several_files(files: &[PathBuf]) -> bool {
43     let is_non_empty_dir = |path: &PathBuf| {
44         let is_non_empty = || !dir_is_empty(path);
46         path.is_dir().then(is_non_empty).unwrap_or_default()
47     };
49     files.iter().any(is_non_empty_dir) || files.len() > 1
52 /// Entrypoint of ouch, receives cli options and matches Subcommand to decide what to do
53 pub fn run(
54     args: Opts,
55     question_policy: QuestionPolicy,
56     file_visibility_policy: FileVisibilityPolicy,
57 ) -> crate::Result<()> {
58     match args.cmd {
59         Subcommand::Compress { mut files, output: output_path } => {
60             // If the output_path file exists and is the same as some of the input files, warn the user and skip those inputs (in order to avoid compression recursion)
61             if output_path.exists() {
62                 clean_input_files_if_needed(&mut files, &fs::canonicalize(&output_path)?);
63             }
64             // After cleaning, if there are no input files left, exit
65             if files.is_empty() {
66                 return Err(FinalError::with_title("No files to compress").into());
67             }
69             // Formats from path extension, like "file.tar.gz.xz" -> vec![Tar, Gzip, Lzma]
70             let mut formats = extension::extensions_from_path(&output_path);
72             if formats.is_empty() {
73                 let error = FinalError::with_title(format!("Cannot compress to '{}'.", to_utf(&output_path)))
74                     .detail("You shall supply the compression format")
75                     .hint("Try adding supported extensions (see --help):")
76                     .hint(format!("  ouch compress <FILES>... {}.tar.gz", to_utf(&output_path)))
77                     .hint(format!("  ouch compress <FILES>... {}.zip", to_utf(&output_path)))
78                     .hint("")
79                     .hint("Alternatively, you can overwrite this option by using the '--format' flag:")
80                     .hint(format!("  ouch compress <FILES>... {} --format tar.gz", to_utf(&output_path)));
82                 return Err(error.into());
83             }
85             if !formats.get(0).map(Extension::is_archive).unwrap_or(false) && represents_several_files(&files) {
86                 // This piece of code creates a suggestion for compressing multiple files
87                 // It says:
88                 // Change from file.bz.xz
89                 // To          file.tar.bz.xz
90                 let extensions_text: String = formats.iter().map(|format| format.to_string()).collect();
92                 let output_path = to_utf(&output_path).to_string();
94                 // Breaks if Lzma is .lz or .lzma and not .xz
95                 // Or if Bzip is .bz2 and not .bz
96                 let extensions_start_position = output_path.rfind(&extensions_text).unwrap();
97                 let pos = extensions_start_position - 1;
98                 let mut suggested_output_path = output_path.to_string();
99                 suggested_output_path.insert_str(pos, ".tar");
101                 let error = FinalError::with_title(format!("Cannot compress to '{}'.", output_path))
102                     .detail("You are trying to compress multiple files.")
103                     .detail(format!("The compression format '{}' cannot receive multiple files.", &formats[0]))
104                     .detail("The only supported formats that archive files into an archive are .tar and .zip.")
105                     .hint(format!("Try inserting '.tar' or '.zip' before '{}'.", &formats[0]))
106                     .hint(format!("From: {}", output_path))
107                     .hint(format!("To:   {}", suggested_output_path));
109                 return Err(error.into());
110             }
112             if let Some(format) = formats.iter().skip(1).find(|format| format.is_archive()) {
113                 let error = FinalError::with_title(format!("Cannot compress to '{}'.", to_utf(&output_path)))
114                     .detail(format!("Found the format '{}' in an incorrect position.", format))
115                     .detail(format!("'{}' can only be used at the start of the file extension.", format))
116                     .hint(format!("If you wish to compress multiple files, start the extension with '{}'.", format))
117                     .hint(format!("Otherwise, remove the last '{}' from '{}'.", format, to_utf(&output_path)));
119                 return Err(error.into());
120             }
122             if output_path.exists() && !utils::user_wants_to_overwrite(&output_path, question_policy)? {
123                 // User does not want to overwrite this file, skip and return without any errors
124                 return Ok(());
125             }
127             let output_file = fs::File::create(&output_path)?;
129             if !represents_several_files(&files) {
130                 // It is possible the file is already partially compressed so we don't want to compress it again
131                 // `ouch compress file.tar.gz file.tar.gz.xz` should produce `file.tar.gz.xz` and not `file.tar.gz.tar.gz.xz`
132                 let input_extensions = extension::extensions_from_path(&files[0]);
134                 // We calculate the formats that are left if we filter out a sublist at the start of what we have that's the same as the input formats
135                 let mut new_formats = Vec::with_capacity(formats.len());
136                 for (inp_ext, out_ext) in input_extensions.iter().zip(&formats) {
137                     if inp_ext.compression_formats == out_ext.compression_formats {
138                         new_formats.push(out_ext.clone());
139                     } else if inp_ext
140                         .compression_formats
141                         .iter()
142                         .zip(out_ext.compression_formats.iter())
143                         .all(|(inp, out)| inp == out)
144                     {
145                         let new_ext = Extension::new(
146                             &out_ext.compression_formats[..inp_ext.compression_formats.len()],
147                             &out_ext.display_text,
148                         );
149                         new_formats.push(new_ext);
150                         break;
151                     }
152                 }
153                 // If the input is a sublist at the start of `formats` then remove the extensions
154                 // Note: If input_extensions is empty then it will make `formats` empty too, which we don't want
155                 if !input_extensions.is_empty() && new_formats != formats {
156                     // Safety:
157                     //   We checked above that input_extensions isn't empty, so files[0] has an extension.
158                     //
159                     //   Path::extension says: "if there is no file_name, then there is no extension".
160                     //   Contrapositive statement: "if there is extension, then there is file_name".
161                     info!(
162                         accessible, // important information
163                         "Partial compression detected. Compressing {} into {}",
164                         to_utf(files[0].as_path().file_name().unwrap().as_ref()),
165                         to_utf(&output_path)
166                     );
167                     formats = new_formats;
168                 }
169             }
170             let compress_result =
171                 compress_files(files, formats, output_file, &output_path, question_policy, file_visibility_policy);
173             if let Ok(true) = compress_result {
174                 // this is only printed once, so it doesn't result in much text. On the other hand,
175                 // having a final status message is important especially in an accessibility context
176                 // as screen readers may not read a commands exit code, making it hard to reason
177                 // about whether the command succeeded without such a message
178                 info!(accessible, "Successfully compressed '{}'.", to_utf(&output_path));
179             } else {
180                 // If Ok(false) or Err() occurred, delete incomplete file
181                 // Print an extra alert message pointing out that we left a possibly
182                 // CORRUPTED FILE at `output_path`
183                 if let Err(err) = fs::remove_file(&output_path) {
184                     eprintln!("{red}FATAL ERROR:\n", red = *colors::RED);
185                     eprintln!("  Please manually delete '{}'.", to_utf(&output_path));
186                     eprintln!("  Compression failed and we could not delete '{}'.", to_utf(&output_path),);
187                     eprintln!("  Error:{reset} {}{red}.{reset}\n", err, reset = *colors::RESET, red = *colors::RED);
188                 }
189             }
191             compress_result?;
192         }
193         Subcommand::Decompress { files, output_dir } => {
194             let mut output_paths = vec![];
195             let mut formats = vec![];
197             for path in files.iter() {
198                 let (file_output_path, file_formats) = extension::separate_known_extensions_from_name(path);
199                 output_paths.push(file_output_path);
200                 formats.push(file_formats);
201             }
203             if let ControlFlow::Break(_) = check_mime_type(&files, &mut formats, question_policy)? {
204                 return Ok(());
205             }
207             let files_missing_format: Vec<PathBuf> = files
208                 .iter()
209                 .zip(&formats)
210                 .filter(|(_, formats)| formats.is_empty())
211                 .map(|(input_path, _)| PathBuf::from(input_path))
212                 .collect();
214             if !files_missing_format.is_empty() {
215                 let error = FinalError::with_title("Cannot decompress files without extensions")
216                     .detail(format!(
217                         "Files without supported extensions: {}",
218                         concatenate_os_str_list(&files_missing_format)
219                     ))
220                     .detail("Decompression formats are detected automatically by the file extension")
221                     .hint("Provide a file with a supported extension:")
222                     .hint("  ouch decompress example.tar.gz")
223                     .hint("")
224                     .hint("Or overwrite this option with the '--format' flag:")
225                     .hint(format!("  ouch decompress {} --format tar.gz", to_utf(&files_missing_format[0])));
227                 return Err(error.into());
228             }
230             // The directory that will contain the output files
231             // We default to the current directory if the user didn't specify an output directory with --dir
232             let output_dir = if let Some(dir) = output_dir {
233                 if !utils::clear_path(&dir, question_policy)? {
234                     // User doesn't want to overwrite
235                     return Ok(());
236                 }
237                 utils::create_dir_if_non_existent(&dir)?;
238                 dir
239             } else {
240                 PathBuf::from(".")
241             };
243             for ((input_path, formats), file_name) in files.iter().zip(formats).zip(output_paths) {
244                 let output_file_path = output_dir.join(file_name); // Path used by single file format archives
245                 decompress_file(input_path, formats, &output_dir, output_file_path, question_policy)?;
246             }
247         }
248         Subcommand::List { archives: files, tree } => {
249             let mut formats = vec![];
251             for path in files.iter() {
252                 let (_, file_formats) = extension::separate_known_extensions_from_name(path);
253                 formats.push(file_formats);
254             }
256             if let ControlFlow::Break(_) = check_mime_type(&files, &mut formats, question_policy)? {
257                 return Ok(());
258             }
260             let not_archives: Vec<PathBuf> = files
261                 .iter()
262                 .zip(&formats)
263                 .filter(|(_, formats)| !formats.get(0).map(Extension::is_archive).unwrap_or(false))
264                 .map(|(path, _)| path.clone())
265                 .collect();
267             if !not_archives.is_empty() {
268                 let error = FinalError::with_title("Cannot list archive contents")
269                     .detail("Only archives can have their contents listed")
270                     .detail(format!("Files are not archives: {}", concatenate_os_str_list(&not_archives)));
272                 return Err(error.into());
273             }
275             let list_options = ListOptions { tree };
277             for (i, (archive_path, formats)) in files.iter().zip(formats).enumerate() {
278                 if i > 0 {
279                     println!();
280                 }
281                 let formats = formats.iter().flat_map(Extension::iter).map(Clone::clone).collect();
282                 list_archive_contents(archive_path, formats, list_options, question_policy)?;
283             }
284         }
285     }
286     Ok(())
289 // Compress files into an `output_file`
291 // files are the list of paths to be compressed: ["dir/file1.txt", "dir/file2.txt"]
292 // formats contains each format necessary for compression, example: [Tar, Gz] (in compression order)
293 // output_file is the resulting compressed file name, example: "compressed.tar.gz"
295 // Returns Ok(true) if compressed all files successfully, and Ok(false) if user opted to skip files
296 fn compress_files(
297     files: Vec<PathBuf>,
298     formats: Vec<Extension>,
299     output_file: fs::File,
300     output_dir: &Path,
301     question_policy: QuestionPolicy,
302     file_visibility_policy: FileVisibilityPolicy,
303 ) -> crate::Result<bool> {
304     // The next lines are for displaying the progress bar
305     // If the input files contain a directory, then the total size will be underestimated
306     let (total_input_size, precise) = files
307         .iter()
308         .map(|f| (f.metadata().expect("file exists").len(), f.is_file()))
309         .fold((0, true), |(total_size, and_precise), (size, precise)| (total_size + size, and_precise & precise));
311     // NOTE: canonicalize is here to avoid a weird bug:
312     //      > If output_file_path is a nested path and it exists and the user overwrite it
313     //      >> output_file_path.exists() will always return false (somehow)
314     //      - canonicalize seems to fix this
315     let output_file_path = output_file.path().canonicalize()?;
317     let file_writer = BufWriter::with_capacity(BUFFER_CAPACITY, output_file);
319     let mut writer: Box<dyn Write> = Box::new(file_writer);
321     // Grab previous encoder and wrap it inside of a new one
322     let chain_writer_encoder = |format: &CompressionFormat, encoder: Box<dyn Write>| -> crate::Result<Box<dyn Write>> {
323         let encoder: Box<dyn Write> = match format {
324             Gzip => Box::new(flate2::write::GzEncoder::new(encoder, Default::default())),
325             Bzip => Box::new(bzip2::write::BzEncoder::new(encoder, Default::default())),
326             Lz4 => Box::new(lzzzz::lz4f::WriteCompressor::new(encoder, Default::default())?),
327             Lzma => Box::new(xz2::write::XzEncoder::new(encoder, 6)),
328             Snappy => Box::new(snap::write::FrameEncoder::new(encoder)),
329             Zstd => {
330                 let zstd_encoder = zstd::stream::write::Encoder::new(encoder, Default::default());
331                 // Safety:
332                 //     Encoder::new() can only fail if `level` is invalid, but Default::default()
333                 //     is guaranteed to be valid
334                 Box::new(zstd_encoder.unwrap().auto_finish())
335             }
336             Tar | Zip => unreachable!(),
337         };
338         Ok(encoder)
339     };
341     for format in formats.iter().flat_map(Extension::iter).skip(1).collect::<Vec<_>>().iter().rev() {
342         writer = chain_writer_encoder(format, writer)?;
343     }
345     match formats[0].compression_formats[0] {
346         Gzip | Bzip | Lz4 | Lzma | Snappy | Zstd => {
347             let _progress = Progress::new_accessible_aware(
348                 total_input_size,
349                 precise,
350                 Some(Box::new(move || output_file_path.metadata().expect("file exists").len())),
351             );
353             writer = chain_writer_encoder(&formats[0].compression_formats[0], writer)?;
354             let mut reader = fs::File::open(&files[0]).unwrap();
355             io::copy(&mut reader, &mut writer)?;
356         }
357         Tar => {
358             let mut progress = Progress::new_accessible_aware(
359                 total_input_size,
360                 precise,
361                 Some(Box::new(move || output_file_path.metadata().expect("file exists").len())),
362             );
364             archive::tar::build_archive_from_paths(
365                 &files,
366                 &mut writer,
367                 file_visibility_policy,
368                 progress.as_mut().map(Progress::display_handle).unwrap_or(&mut io::stdout()),
369             )?;
370             writer.flush()?;
371         }
372         Zip => {
373             if formats.len() > 1 {
374                 eprintln!("{orange}[WARNING]{reset}", orange = *colors::ORANGE, reset = *colors::RESET);
375                 eprintln!("{}", ZIP_IN_MEMORY_LIMITATION_WARNING);
377                 // give user the option to continue compressing after warning is shown
378                 if !user_wants_to_continue(output_dir, question_policy, QuestionAction::Compression)? {
379                     return Ok(false);
380                 }
381             }
383             let mut vec_buffer = io::Cursor::new(vec![]);
385             let current_position_fn = {
386                 let vec_buffer_ptr = {
387                     struct FlyPtr(*const io::Cursor<Vec<u8>>);
388                     unsafe impl Send for FlyPtr {}
389                     FlyPtr(&vec_buffer as *const _)
390                 };
391                 Box::new(move || {
392                     let vec_buffer_ptr = &vec_buffer_ptr;
393                     // Safety: ptr is valid and vec_buffer is still alive
394                     unsafe { &*vec_buffer_ptr.0 }.position()
395                 })
396             };
398             let mut progress = Progress::new_accessible_aware(total_input_size, precise, Some(current_position_fn));
400             archive::zip::build_archive_from_paths(
401                 &files,
402                 &mut vec_buffer,
403                 file_visibility_policy,
404                 progress.as_mut().map(Progress::display_handle).unwrap_or(&mut io::stdout()),
405             )?;
406             let vec_buffer = vec_buffer.into_inner();
407             io::copy(&mut vec_buffer.as_slice(), &mut writer)?;
408         }
409     }
411     Ok(true)
414 // Decompress a file
416 // File at input_file_path is opened for reading, example: "archive.tar.gz"
417 // formats contains each format necessary for decompression, example: [Gz, Tar] (in decompression order)
418 // output_dir it's where the file will be decompressed to, this function assumes that the directory exists
419 // output_file_path is only used when extracting single file formats, not archive formats like .tar or .zip
420 fn decompress_file(
421     input_file_path: &Path,
422     formats: Vec<Extension>,
423     output_dir: &Path,
424     output_file_path: PathBuf,
425     question_policy: QuestionPolicy,
426 ) -> crate::Result<()> {
427     assert!(output_dir.exists());
428     let total_input_size = input_file_path.metadata().expect("file exists").len();
429     let reader = fs::File::open(&input_file_path)?;
430     // Zip archives are special, because they require io::Seek, so it requires it's logic separated
431     // from decoder chaining.
432     //
433     // This is the only case where we can read and unpack it directly, without having to do
434     // in-memory decompression/copying first.
435     //
436     // Any other Zip decompression done can take up the whole RAM and freeze ouch.
437     if formats.len() == 1 && *formats[0].compression_formats == [Zip] {
438         let zip_archive = zip::ZipArchive::new(reader)?;
439         let files = if let ControlFlow::Continue(files) = smart_unpack(
440             Box::new(move |output_dir| {
441                 let mut progress = Progress::new_accessible_aware(total_input_size, true, None);
442                 crate::archive::zip::unpack_archive(
443                     zip_archive,
444                     output_dir,
445                     progress.as_mut().map(Progress::display_handle).unwrap_or(&mut io::stdout()),
446                 )
447             }),
448             output_dir,
449             &output_file_path,
450             question_policy,
451         )? {
452             files
453         } else {
454             return Ok(());
455         };
457         // this is only printed once, so it doesn't result in much text. On the other hand,
458         // having a final status message is important especially in an accessibility context
459         // as screen readers may not read a commands exit code, making it hard to reason
460         // about whether the command succeeded without such a message
461         info!(
462             accessible,
463             "Successfully decompressed archive in {} ({} files).",
464             nice_directory_display(output_dir),
465             files.len()
466         );
468         return Ok(());
469     }
471     // Will be used in decoder chaining
472     let reader = BufReader::with_capacity(BUFFER_CAPACITY, reader);
473     let mut reader: Box<dyn Read> = Box::new(reader);
475     // Grab previous decoder and wrap it inside of a new one
476     let chain_reader_decoder = |format: &CompressionFormat, decoder: Box<dyn Read>| -> crate::Result<Box<dyn Read>> {
477         let decoder: Box<dyn Read> = match format {
478             Gzip => Box::new(flate2::read::GzDecoder::new(decoder)),
479             Bzip => Box::new(bzip2::read::BzDecoder::new(decoder)),
480             Lz4 => Box::new(lzzzz::lz4f::ReadDecompressor::new(decoder)?),
481             Lzma => Box::new(xz2::read::XzDecoder::new(decoder)),
482             Snappy => Box::new(snap::read::FrameDecoder::new(decoder)),
483             Zstd => Box::new(zstd::stream::Decoder::new(decoder)?),
484             Tar | Zip => unreachable!(),
485         };
486         Ok(decoder)
487     };
489     for format in formats.iter().flat_map(Extension::iter).skip(1).collect::<Vec<_>>().iter().rev() {
490         reader = chain_reader_decoder(format, reader)?;
491     }
493     let files_unpacked;
494     match formats[0].compression_formats[0] {
495         Gzip | Bzip | Lz4 | Lzma | Snappy | Zstd => {
496             reader = chain_reader_decoder(&formats[0].compression_formats[0], reader)?;
498             let writer = utils::create_or_ask_overwrite(&output_file_path, question_policy)?;
499             if writer.is_none() {
500                 // Means that the user doesn't want to overwrite
501                 return Ok(());
502             }
503             let mut writer = writer.unwrap();
505             let current_position_fn = Box::new({
506                 let output_file_path = output_file_path.clone();
507                 move || output_file_path.clone().metadata().expect("file exists").len()
508             });
509             let _progress = Progress::new_accessible_aware(total_input_size, true, Some(current_position_fn));
511             io::copy(&mut reader, &mut writer)?;
512             files_unpacked = vec![output_file_path];
513         }
514         Tar => {
515             files_unpacked = if let ControlFlow::Continue(files) = smart_unpack(
516                 Box::new(move |output_dir| {
517                     let mut progress = Progress::new_accessible_aware(total_input_size, true, None);
518                     crate::archive::tar::unpack_archive(
519                         reader,
520                         output_dir,
521                         progress.as_mut().map(Progress::display_handle).unwrap_or(&mut io::stdout()),
522                     )
523                 }),
524                 output_dir,
525                 &output_file_path,
526                 question_policy,
527             )? {
528                 files
529             } else {
530                 return Ok(());
531             };
532         }
533         Zip => {
534             if formats.len() > 1 {
535                 eprintln!("{orange}[WARNING]{reset}", orange = *colors::ORANGE, reset = *colors::RESET);
536                 eprintln!("{}", ZIP_IN_MEMORY_LIMITATION_WARNING);
538                 // give user the option to continue decompressing after warning is shown
539                 if !user_wants_to_continue(input_file_path, question_policy, QuestionAction::Decompression)? {
540                     return Ok(());
541                 }
542             }
544             let mut vec = vec![];
545             io::copy(&mut reader, &mut vec)?;
546             let zip_archive = zip::ZipArchive::new(io::Cursor::new(vec))?;
548             files_unpacked = if let ControlFlow::Continue(files) = smart_unpack(
549                 Box::new(move |output_dir| {
550                     let mut progress = Progress::new_accessible_aware(total_input_size, true, None);
551                     crate::archive::zip::unpack_archive(
552                         zip_archive,
553                         output_dir,
554                         progress.as_mut().map(Progress::display_handle).unwrap_or(&mut io::stdout()),
555                     )
556                 }),
557                 output_dir,
558                 &output_file_path,
559                 question_policy,
560             )? {
561                 files
562             } else {
563                 return Ok(());
564             };
565         }
566     }
568     // this is only printed once, so it doesn't result in much text. On the other hand,
569     // having a final status message is important especially in an accessibility context
570     // as screen readers may not read a commands exit code, making it hard to reason
571     // about whether the command succeeded without such a message
572     info!(accessible, "Successfully decompressed archive in {}.", nice_directory_display(output_dir));
573     info!(accessible, "Files unpacked: {}", files_unpacked.len());
575     Ok(())
578 // File at input_file_path is opened for reading, example: "archive.tar.gz"
579 // formats contains each format necessary for decompression, example: [Gz, Tar] (in decompression order)
580 fn list_archive_contents(
581     archive_path: &Path,
582     formats: Vec<CompressionFormat>,
583     list_options: ListOptions,
584     question_policy: QuestionPolicy,
585 ) -> crate::Result<()> {
586     let reader = fs::File::open(&archive_path)?;
588     // Zip archives are special, because they require io::Seek, so it requires it's logic separated
589     // from decoder chaining.
590     //
591     // This is the only case where we can read and unpack it directly, without having to do
592     // in-memory decompression/copying first.
593     //
594     // Any other Zip decompression done can take up the whole RAM and freeze ouch.
595     if let [Zip] = *formats.as_slice() {
596         let zip_archive = zip::ZipArchive::new(reader)?;
597         let files = crate::archive::zip::list_archive(zip_archive);
598         list::list_files(archive_path, files, list_options)?;
600         return Ok(());
601     }
603     // Will be used in decoder chaining
604     let reader = BufReader::with_capacity(BUFFER_CAPACITY, reader);
605     let mut reader: Box<dyn Read + Send> = Box::new(reader);
607     // Grab previous decoder and wrap it inside of a new one
608     let chain_reader_decoder =
609         |format: &CompressionFormat, decoder: Box<dyn Read + Send>| -> crate::Result<Box<dyn Read + Send>> {
610             let decoder: Box<dyn Read + Send> = match format {
611                 Gzip => Box::new(flate2::read::GzDecoder::new(decoder)),
612                 Bzip => Box::new(bzip2::read::BzDecoder::new(decoder)),
613                 Lz4 => Box::new(lzzzz::lz4f::ReadDecompressor::new(decoder)?),
614                 Lzma => Box::new(xz2::read::XzDecoder::new(decoder)),
615                 Snappy => Box::new(snap::read::FrameDecoder::new(decoder)),
616                 Zstd => Box::new(zstd::stream::Decoder::new(decoder)?),
617                 Tar | Zip => unreachable!(),
618             };
619             Ok(decoder)
620         };
622     for format in formats.iter().skip(1).rev() {
623         reader = chain_reader_decoder(format, reader)?;
624     }
626     let files: Box<dyn Iterator<Item = crate::Result<FileInArchive>>> = match formats[0] {
627         Tar => Box::new(crate::archive::tar::list_archive(tar::Archive::new(reader))),
628         Zip => {
629             if formats.len() > 1 {
630                 eprintln!("{orange}[WARNING]{reset}", orange = *colors::ORANGE, reset = *colors::RESET);
631                 eprintln!("{}", ZIP_IN_MEMORY_LIMITATION_WARNING);
633                 // give user the option to continue decompressing after warning is shown
634                 if !user_wants_to_continue(archive_path, question_policy, QuestionAction::Decompression)? {
635                     return Ok(());
636                 }
637             }
639             let mut vec = vec![];
640             io::copy(&mut reader, &mut vec)?;
641             let zip_archive = zip::ZipArchive::new(io::Cursor::new(vec))?;
643             Box::new(crate::archive::zip::list_archive(zip_archive))
644         }
645         Gzip | Bzip | Lz4 | Lzma | Snappy | Zstd => {
646             panic!("Not an archive! This should never happen, if it does, something is wrong with `CompressionFormat::is_archive()`. Please report this error!");
647         }
648     };
649     list::list_files(archive_path, files, list_options)?;
650     Ok(())
653 /// Unpacks an archive with some heuristics
654 /// - If the archive contains only one file, it will be extracted to the `output_dir`
655 /// - If the archive contains multiple files, it will be extracted to a subdirectory of the output_dir named after the archive (given by `output_file_path`)
656 /// Note: This functions assumes that `output_dir` exists
657 fn smart_unpack(
658     unpack_fn: Box<dyn FnOnce(&Path) -> crate::Result<Vec<PathBuf>>>,
659     output_dir: &Path,
660     output_file_path: &Path,
661     question_policy: QuestionPolicy,
662 ) -> crate::Result<ControlFlow<(), Vec<PathBuf>>> {
663     assert!(output_dir.exists());
664     let temp_dir = tempfile::tempdir_in(output_dir)?;
665     let temp_dir_path = temp_dir.path();
666     info!(
667         accessible,
668         "Created temporary directory {} to hold decompressed elements.",
669         nice_directory_display(temp_dir_path)
670     );
672     // unpack the files
673     let files = unpack_fn(temp_dir_path)?;
675     let root_contains_only_one_element = fs::read_dir(&temp_dir_path)?.count() == 1;
676     if root_contains_only_one_element {
677         // Only one file in the root directory, so we can just move it to the output directory
678         let file = fs::read_dir(&temp_dir_path)?.next().expect("item exists")?;
679         let file_path = file.path();
680         let file_name =
681             file_path.file_name().expect("Should be safe because paths in archives should not end with '..'");
682         let correct_path = output_dir.join(file_name);
683         // One case to handle tough is we need to check if a file with the same name already exists
684         if !utils::clear_path(&correct_path, question_policy)? {
685             return Ok(ControlFlow::Break(()));
686         }
687         fs::rename(&file_path, &correct_path)?;
688         info!(
689             accessible,
690             "Successfully moved {} to {}.",
691             nice_directory_display(&file_path),
692             nice_directory_display(&correct_path)
693         );
694     } else {
695         // Multiple files in the root directory, so:
696         // Rename  the temporary directory to the archive name, which is output_file_path
697         // One case to handle tough is we need to check if a file with the same name already exists
698         if !utils::clear_path(output_file_path, question_policy)? {
699             return Ok(ControlFlow::Break(()));
700         }
701         fs::rename(&temp_dir_path, &output_file_path)?;
702         info!(
703             accessible,
704             "Successfully moved {} to {}.",
705             nice_directory_display(temp_dir_path),
706             nice_directory_display(output_file_path)
707         );
708     }
709     Ok(ControlFlow::Continue(files))
712 fn check_mime_type(
713     files: &[PathBuf],
714     formats: &mut Vec<Vec<Extension>>,
715     question_policy: QuestionPolicy,
716 ) -> crate::Result<ControlFlow<()>> {
717     for (path, format) in files.iter().zip(formats.iter_mut()) {
718         if format.is_empty() {
719             // File with no extension
720             // Try to detect it automatically and prompt the user about it
721             if let Some(detected_format) = try_infer_extension(path) {
722                 // Infering the file extension can have unpredicted consequences (e.g. the user just
723                 // mistyped, ...) which we should always inform the user about.
724                 info!(accessible, "Detected file: `{}` extension as `{}`", path.display(), detected_format);
725                 if user_wants_to_continue(path, question_policy, QuestionAction::Decompression)? {
726                     format.push(detected_format);
727                 } else {
728                     return Ok(ControlFlow::Break(()));
729                 }
730             }
731         } else if let Some(detected_format) = try_infer_extension(path) {
732             // File ending with extension
733             // Try to detect the extension and warn the user if it differs from the written one
734             let outer_ext = format.iter().next_back().unwrap();
735             if outer_ext != &detected_format {
736                 warning!(
737                     "The file extension: `{}` differ from the detected extension: `{}`",
738                     outer_ext,
739                     detected_format
740                 );
741                 if !user_wants_to_continue(path, question_policy, QuestionAction::Decompression)? {
742                     return Ok(ControlFlow::Break(()));
743                 }
744             }
745         } else {
746             // NOTE: If this actually produces no false positives, we can upgrade it in the future
747             // to a warning and ask the user if he wants to continue decompressing.
748             info!(accessible, "Could not detect the extension of `{}`", path.display());
749         }
750     }
751     Ok(ControlFlow::Continue(()))
754 fn clean_input_files_if_needed(files: &mut Vec<PathBuf>, output_path: &Path) {
755     let mut idx = 0;
756     while idx < files.len() {
757         if files[idx] == output_path {
758             warning!("The output file and the input file are the same: `{}`, skipping...", output_path.display());
759             files.remove(idx);
760         } else {
761             idx += 1;
762         }
763     }