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