Merge pull request #217 from Crypto-Spartan/zip-mem-warnings
[ouch.git] / src / commands.rs
blob20a4f45d120505be8da2ba956c54fbd79a4264f5
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;
87                 let empty_range = pos..pos;
88                 let mut suggested_output_path = output_path.clone();
89                 suggested_output_path.replace_range(empty_range, ".tar");
91                 let error = FinalError::with_title(format!("Cannot compress to '{}'.", to_utf(&output_path)))
92                     .detail("You are trying to compress multiple files.")
93                     .detail(format!("The compression format '{}' cannot receive multiple files.", &formats[0]))
94                     .detail("The only supported formats that archive files into an archive are .tar and .zip.")
95                     .hint(format!("Try inserting '.tar' or '.zip' before '{}'.", &formats[0]))
96                     .hint(format!("From: {}", output_path))
97                     .hint(format!("To:   {}", suggested_output_path));
99                 return Err(error.into());
100             }
102             if let Some(format) = formats.iter().skip(1).find(|format| format.is_archive()) {
103                 let error = FinalError::with_title(format!("Cannot compress to '{}'.", to_utf(&output_path)))
104                     .detail(format!("Found the format '{}' in an incorrect position.", format))
105                     .detail(format!("'{}' can only be used at the start of the file extension.", format))
106                     .hint(format!("If you wish to compress multiple files, start the extension with '{}'.", format))
107                     .hint(format!("Otherwise, remove the last '{}' from '{}'.", format, to_utf(&output_path)));
109                 return Err(error.into());
110             }
112             if output_path.exists() && !utils::user_wants_to_overwrite(&output_path, question_policy)? {
113                 // User does not want to overwrite this file, skip and return without any errors
114                 return Ok(());
115             }
117             let output_file = fs::File::create(&output_path)?;
119             if !represents_several_files(&files) {
120                 // It's possible the file is already partially compressed so we don't want to compress it again
121                 // `ouch compress file.tar.gz file.tar.gz.xz` should produce `file.tar.gz.xz` and not `file.tar.gz.tar.gz.xz`
122                 let input_extensions = extension::extensions_from_path(&files[0]);
124                 // 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
125                 let mut new_formats = Vec::with_capacity(formats.len());
126                 for (inp_ext, out_ext) in input_extensions.iter().zip(&formats) {
127                     if inp_ext.compression_formats == out_ext.compression_formats {
128                         new_formats.push(out_ext.clone());
129                     } else if inp_ext
130                         .compression_formats
131                         .iter()
132                         .zip(out_ext.compression_formats.iter())
133                         .all(|(inp, out)| inp == out)
134                     {
135                         let new_ext = Extension::new(
136                             &out_ext.compression_formats[..inp_ext.compression_formats.len()],
137                             &out_ext.display_text,
138                         );
139                         new_formats.push(new_ext);
140                         break;
141                     }
142                 }
143                 // If the input is a sublist at the start of `formats` then remove the extensions
144                 // Note: If input_extensions is empty then it will make `formats` empty too, which we don't want
145                 if !input_extensions.is_empty() && new_formats != formats {
146                     // Safety:
147                     //   We checked above that input_extensions isn't empty, so files[0] has an extension.
148                     //
149                     //   Path::extension says: "if there is no file_name, then there is no extension".
150                     //   Contrapositive statement: "if there is extension, then there is file_name".
151                     info!(
152                         accessible, // important information
153                         "Partial compression detected. Compressing {} into {}",
154                         to_utf(files[0].as_path().file_name().unwrap()),
155                         to_utf(&output_path)
156                     );
157                     formats = new_formats;
158                 }
159             }
160             let compress_result = compress_files(files, formats, output_file, &output_path, question_policy);
162             // If any error occurred, delete incomplete file
163             if compress_result.is_err() {
164                 // Print an extra alert message pointing out that we left a possibly
165                 // CORRUPTED FILE at `output_path`
166                 if let Err(err) = fs::remove_file(&output_path) {
167                     eprintln!("{red}FATAL ERROR:\n", red = *colors::RED);
168                     eprintln!("  Please manually delete '{}'.", to_utf(&output_path));
169                     eprintln!("  Compression failed and we could not delete '{}'.", to_utf(&output_path),);
170                     eprintln!("  Error:{reset} {}{red}.{reset}\n", err, reset = *colors::RESET, red = *colors::RED);
171                 }
172             } else {
173                 // this is only printed once, so it doesn't result in much text. On the other hand,
174                 // having a final status message is important especially in an accessibility context
175                 // as screen readers may not read a commands exit code, making it hard to reason
176                 // about whether the command succeeded without such a message
177                 info!(accessible, "Successfully compressed '{}'.", to_utf(output_path));
178             }
180             compress_result?;
181         }
182         Subcommand::Decompress { files, output_dir } => {
183             let mut output_paths = vec![];
184             let mut formats = vec![];
186             for path in files.iter() {
187                 let (file_output_path, file_formats) = extension::separate_known_extensions_from_name(path);
188                 output_paths.push(file_output_path);
189                 formats.push(file_formats);
190             }
192             if let ControlFlow::Break(_) = check_mime_type(&files, &mut formats, question_policy)? {
193                 return Ok(());
194             }
196             let files_missing_format: Vec<PathBuf> = files
197                 .iter()
198                 .zip(&formats)
199                 .filter(|(_, formats)| formats.is_empty())
200                 .map(|(input_path, _)| PathBuf::from(input_path))
201                 .collect();
203             if !files_missing_format.is_empty() {
204                 let error = FinalError::with_title("Cannot decompress files without extensions")
205                     .detail(format!(
206                         "Files without supported extensions: {}",
207                         concatenate_os_str_list(&files_missing_format)
208                     ))
209                     .detail("Decompression formats are detected automatically by the file extension")
210                     .hint("Provide a file with a supported extension:")
211                     .hint("  ouch decompress example.tar.gz")
212                     .hint("")
213                     .hint("Or overwrite this option with the '--format' flag:")
214                     .hint(format!("  ouch decompress {} --format tar.gz", to_utf(&files_missing_format[0])));
216                 return Err(error.into());
217             }
219             // The directory that will contain the output files
220             // We default to the current directory if the user didn't specify an output directory with --dir
221             let output_dir = if let Some(dir) = output_dir {
222                 if !utils::clear_path(&dir, question_policy)? {
223                     // User doesn't want to overwrite
224                     return Ok(());
225                 }
226                 utils::create_dir_if_non_existent(&dir)?;
227                 dir
228             } else {
229                 PathBuf::from(".")
230             };
232             for ((input_path, formats), file_name) in files.iter().zip(formats).zip(output_paths) {
233                 let output_file_path = output_dir.join(file_name); // Path used by single file format archives
234                 decompress_file(input_path, formats, &output_dir, output_file_path, question_policy)?;
235             }
236         }
237         Subcommand::List { archives: files, tree } => {
238             let mut formats = vec![];
240             for path in files.iter() {
241                 let (_, file_formats) = extension::separate_known_extensions_from_name(path);
242                 formats.push(file_formats);
243             }
245             if let ControlFlow::Break(_) = check_mime_type(&files, &mut formats, question_policy)? {
246                 return Ok(());
247             }
249             let not_archives: Vec<PathBuf> = files
250                 .iter()
251                 .zip(&formats)
252                 .filter(|(_, formats)| !formats.get(0).map(Extension::is_archive).unwrap_or(false))
253                 .map(|(path, _)| path.clone())
254                 .collect();
256             if !not_archives.is_empty() {
257                 let error = FinalError::with_title("Cannot list archive contents")
258                     .detail("Only archives can have their contents listed")
259                     .detail(format!("Files are not archives: {}", concatenate_os_str_list(&not_archives)));
261                 return Err(error.into());
262             }
264             let list_options = ListOptions { tree };
266             for (i, (archive_path, formats)) in files.iter().zip(formats).enumerate() {
267                 if i > 0 {
268                     println!();
269                 }
270                 let formats = formats.iter().flat_map(Extension::iter).map(Clone::clone).collect();
271                 list_archive_contents(archive_path, formats, list_options, question_policy)?;
272             }
273         }
274     }
275     Ok(())
278 // Compress files into an `output_file`
280 // files are the list of paths to be compressed: ["dir/file1.txt", "dir/file2.txt"]
281 // formats contains each format necessary for compression, example: [Tar, Gz] (in compression order)
282 // output_file is the resulting compressed file name, example: "compressed.tar.gz"
283 fn compress_files(
284     files: Vec<PathBuf>,
285     formats: Vec<Extension>,
286     output_file: fs::File,
287     output_dir: &Path,
288     question_policy: QuestionPolicy,
289 ) -> crate::Result<()> {
290     // The next lines are for displaying the progress bar
291     // If the input files contain a directory, then the total size will be underestimated
292     let (total_input_size, precise) = files
293         .iter()
294         .map(|f| (f.metadata().expect("file exists").len(), f.is_file()))
295         .fold((0, true), |(total_size, and_precise), (size, precise)| (total_size + size, and_precise & precise));
296     //NOTE: canonicalize is here to avoid a weird bug:
297     //      > If output_file_path is a nested path and it exists and the user overwrite it
298     //      >> output_file_path.exists() will always return false (somehow)
299     //      - canonicalize seems to fix this
300     let output_file_path = output_file.path().canonicalize()?;
302     let file_writer = BufWriter::with_capacity(BUFFER_CAPACITY, output_file);
304     let mut writer: Box<dyn Write> = Box::new(file_writer);
306     // Grab previous encoder and wrap it inside of a new one
307     let chain_writer_encoder = |format: &CompressionFormat, encoder: Box<dyn Write>| -> crate::Result<Box<dyn Write>> {
308         let encoder: Box<dyn Write> = match format {
309             Gzip => Box::new(flate2::write::GzEncoder::new(encoder, Default::default())),
310             Bzip => Box::new(bzip2::write::BzEncoder::new(encoder, Default::default())),
311             Lz4 => Box::new(lzzzz::lz4f::WriteCompressor::new(encoder, Default::default())?),
312             Lzma => Box::new(xz2::write::XzEncoder::new(encoder, 6)),
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 | 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             Zstd => Box::new(zstd::stream::Decoder::new(decoder)?),
467             Tar | Zip => unreachable!(),
468         };
469         Ok(decoder)
470     };
472     for format in formats.iter().flat_map(Extension::iter).skip(1).collect::<Vec<_>>().iter().rev() {
473         reader = chain_reader_decoder(format, reader)?;
474     }
476     let files_unpacked;
477     match formats[0].compression_formats[0] {
478         Gzip | Bzip | Lz4 | Lzma | Zstd => {
479             reader = chain_reader_decoder(&formats[0].compression_formats[0], reader)?;
481             let writer = utils::create_or_ask_overwrite(&output_file_path, question_policy)?;
482             if writer.is_none() {
483                 // Means that the user doesn't want to overwrite
484                 return Ok(());
485             }
486             let mut writer = writer.unwrap();
488             let current_position_fn = Box::new({
489                 let output_file_path = output_file_path.clone();
490                 move || output_file_path.clone().metadata().expect("file exists").len()
491             });
492             let _progress = Progress::new_accessible_aware(total_input_size, true, Some(current_position_fn));
494             io::copy(&mut reader, &mut writer)?;
495             files_unpacked = vec![output_file_path];
496         }
497         Tar => {
498             files_unpacked = if let ControlFlow::Continue(files) = smart_unpack(
499                 Box::new(move |output_dir| {
500                     let mut progress = Progress::new_accessible_aware(total_input_size, true, None);
501                     crate::archive::tar::unpack_archive(
502                         reader,
503                         output_dir,
504                         progress.as_mut().map(Progress::display_handle).unwrap_or(&mut io::stdout()),
505                     )
506                 }),
507                 output_dir,
508                 &output_file_path,
509                 question_policy,
510             )? {
511                 files
512             } else {
513                 return Ok(());
514             };
515         }
516         Zip => {
517             eprintln!("{orange}[WARNING]{reset}", orange = *colors::ORANGE, reset = *colors::RESET);
518             eprintln!(
519                 "\tThere is a limitation for .zip archives with extra extensions. (e.g. <file>.zip.gz)\
520             \n\tThe design of .zip makes it impossible to compress via stream, so it must be done entirely in memory.\
521             \n\tBy compressing .zip with extra compression formats, you can run out of RAM if the file is too large!"
522             );
524             // give user the option to continue decompressing after warning is shown
525             if !user_wants_to_continue_decompressing(input_file_path, question_policy)? {
526                 return Ok(());
527             }
529             let mut vec = vec![];
530             io::copy(&mut reader, &mut vec)?;
531             let zip_archive = zip::ZipArchive::new(io::Cursor::new(vec))?;
533             files_unpacked = if let ControlFlow::Continue(files) = smart_unpack(
534                 Box::new(move |output_dir| {
535                     let mut progress = Progress::new_accessible_aware(total_input_size, true, None);
536                     crate::archive::zip::unpack_archive(
537                         zip_archive,
538                         output_dir,
539                         progress.as_mut().map(Progress::display_handle).unwrap_or(&mut io::stdout()),
540                     )
541                 }),
542                 output_dir,
543                 &output_file_path,
544                 question_policy,
545             )? {
546                 files
547             } else {
548                 return Ok(());
549             };
550         }
551     }
553     // this is only printed once, so it doesn't result in much text. On the other hand,
554     // having a final status message is important especially in an accessibility context
555     // as screen readers may not read a commands exit code, making it hard to reason
556     // about whether the command succeeded without such a message
557     info!(accessible, "Successfully decompressed archive in {}.", nice_directory_display(output_dir));
558     info!(accessible, "Files unpacked: {}", files_unpacked.len());
560     Ok(())
563 // File at input_file_path is opened for reading, example: "archive.tar.gz"
564 // formats contains each format necessary for decompression, example: [Gz, Tar] (in decompression order)
565 fn list_archive_contents(
566     archive_path: &Path,
567     formats: Vec<CompressionFormat>,
568     list_options: ListOptions,
569     question_policy: QuestionPolicy,
570 ) -> crate::Result<()> {
571     let reader = fs::File::open(&archive_path)?;
573     // Zip archives are special, because they require io::Seek, so it requires it's logic separated
574     // from decoder chaining.
575     //
576     // This is the only case where we can read and unpack it directly, without having to do
577     // in-memory decompression/copying first.
578     //
579     // Any other Zip decompression done can take up the whole RAM and freeze ouch.
580     if let [Zip] = *formats.as_slice() {
581         let zip_archive = zip::ZipArchive::new(reader)?;
582         let files = crate::archive::zip::list_archive(zip_archive)?;
583         list::list_files(archive_path, files, list_options);
584         return Ok(());
585     }
587     // Will be used in decoder chaining
588     let reader = BufReader::with_capacity(BUFFER_CAPACITY, reader);
589     let mut reader: Box<dyn Read> = Box::new(reader);
591     // Grab previous decoder and wrap it inside of a new one
592     let chain_reader_decoder = |format: &CompressionFormat, decoder: Box<dyn Read>| -> crate::Result<Box<dyn Read>> {
593         let decoder: Box<dyn Read> = match format {
594             Gzip => Box::new(flate2::read::GzDecoder::new(decoder)),
595             Bzip => Box::new(bzip2::read::BzDecoder::new(decoder)),
596             Lz4 => Box::new(lzzzz::lz4f::ReadDecompressor::new(decoder)?),
597             Lzma => Box::new(xz2::read::XzDecoder::new(decoder)),
598             Zstd => Box::new(zstd::stream::Decoder::new(decoder)?),
599             Tar | Zip => unreachable!(),
600         };
601         Ok(decoder)
602     };
604     for format in formats.iter().skip(1).rev() {
605         reader = chain_reader_decoder(format, reader)?;
606     }
608     let files = match formats[0] {
609         Tar => crate::archive::tar::list_archive(reader)?,
610         Zip => {
611             eprintln!("{orange}[WARNING]{reset}", orange = *colors::ORANGE, reset = *colors::RESET);
612             eprintln!(
613                 "\tThere is a limitation for .zip archives with extra extensions. (e.g. <file>.zip.gz)\
614             \n\tThe design of .zip makes it impossible to compress via stream, so it must be done entirely in memory.\
615             \n\tBy compressing .zip with extra compression formats, you can run out of RAM if the file is too large!"
616             );
618             // give user the option to continue decompressing after warning is shown
619             if !user_wants_to_continue_decompressing(archive_path, question_policy)? {
620                 return Ok(());
621             }
623             let mut vec = vec![];
624             io::copy(&mut reader, &mut vec)?;
625             let zip_archive = zip::ZipArchive::new(io::Cursor::new(vec))?;
627             crate::archive::zip::list_archive(zip_archive)?
628         }
629         Gzip | Bzip | Lz4 | Lzma | Zstd => {
630             panic!("Not an archive! This should never happen, if it does, something is wrong with `CompressionFormat::is_archive()`. Please report this error!");
631         }
632     };
633     list::list_files(archive_path, files, list_options);
634     Ok(())
637 /// Unpacks an archive with some heuristics
638 /// - If the archive contains only one file, it will be extracted to the `output_dir`
639 /// - 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`)
640 /// Note: This functions assumes that `output_dir` exists
641 fn smart_unpack(
642     unpack_fn: Box<dyn FnOnce(&Path) -> crate::Result<Vec<PathBuf>>>,
643     output_dir: &Path,
644     output_file_path: &Path,
645     question_policy: QuestionPolicy,
646 ) -> crate::Result<ControlFlow<(), Vec<PathBuf>>> {
647     assert!(output_dir.exists());
648     let temp_dir = tempfile::tempdir_in(output_dir)?;
649     let temp_dir_path = temp_dir.path();
650     info!(
651         accessible,
652         "Created temporary directory {} to hold decompressed elements.",
653         nice_directory_display(temp_dir_path)
654     );
656     // unpack the files
657     let files = unpack_fn(temp_dir_path)?;
659     let root_contains_only_one_element = fs::read_dir(&temp_dir_path)?.count() == 1;
660     if root_contains_only_one_element {
661         // Only one file in the root directory, so we can just move it to the output directory
662         let file = fs::read_dir(&temp_dir_path)?.next().expect("item exists")?;
663         let file_path = file.path();
664         let file_name =
665             file_path.file_name().expect("Should be safe because paths in archives should not end with '..'");
666         let correct_path = output_dir.join(file_name);
667         // One case to handle tough is we need to check if a file with the same name already exists
668         if !utils::clear_path(&correct_path, question_policy)? {
669             return Ok(ControlFlow::Break(()));
670         }
671         fs::rename(&file_path, &correct_path)?;
672         info!(
673             accessible,
674             "Successfully moved {} to {}.",
675             nice_directory_display(&file_path),
676             nice_directory_display(&correct_path)
677         );
678     } else {
679         // Multiple files in the root directory, so:
680         // Rename  the temporary directory to the archive name, which is output_file_path
681         // One case to handle tough is we need to check if a file with the same name already exists
682         if !utils::clear_path(output_file_path, question_policy)? {
683             return Ok(ControlFlow::Break(()));
684         }
685         fs::rename(&temp_dir_path, &output_file_path)?;
686         info!(
687             accessible,
688             "Successfully moved {} to {}.",
689             nice_directory_display(&temp_dir_path),
690             nice_directory_display(&output_file_path)
691         );
692     }
693     Ok(ControlFlow::Continue(files))
696 fn check_mime_type(
697     files: &[PathBuf],
698     formats: &mut Vec<Vec<Extension>>,
699     question_policy: QuestionPolicy,
700 ) -> crate::Result<ControlFlow<()>> {
701     for (path, format) in files.iter().zip(formats.iter_mut()) {
702         if format.is_empty() {
703             // File with no extension
704             // Try to detect it automatically and prompt the user about it
705             if let Some(detected_format) = try_infer_extension(path) {
706                 // Infering the file extension can have unpredicted consequences (e.g. the user just
707                 // mistyped, ...) which we should always inform the user about.
708                 info!(accessible, "Detected file: `{}` extension as `{}`", path.display(), detected_format);
709                 if user_wants_to_continue_decompressing(path, question_policy)? {
710                     format.push(detected_format);
711                 } else {
712                     return Ok(ControlFlow::Break(()));
713                 }
714             }
715         } else if let Some(detected_format) = try_infer_extension(path) {
716             // File ending with extension
717             // Try to detect the extension and warn the user if it differs from the written one
718             let outer_ext = format.iter().next_back().unwrap();
719             if outer_ext != &detected_format {
720                 warning!(
721                     "The file extension: `{}` differ from the detected extension: `{}`",
722                     outer_ext,
723                     detected_format
724                 );
725                 if !user_wants_to_continue_decompressing(path, question_policy)? {
726                     return Ok(ControlFlow::Break(()));
727                 }
728             }
729         } else {
730             // NOTE: If this actually produces no false positives, we can upgrade it in the future
731             // to a warning and ask the user if he wants to continue decompressing.
732             info!(accessible, "Could not detect the extension of `{}`", path.display());
733         }
734     }
735     Ok(ControlFlow::Continue(()))
738 fn clean_input_files_if_needed(files: &mut Vec<PathBuf>, output_path: &Path) {
739     let mut idx = 0;
740     while idx < files.len() {
741         if files[idx] == output_path {
742             warning!("The output file and the input file are the same: `{}`, skipping...", output_path.display());
743             files.remove(idx);
744         } else {
745             idx += 1;
746         }
747     }