Merge branch 'master' into fix-zip-warning
[ouch.git] / src / commands.rs
blob1264efd7970b0bc2168470a2a1670da7981a68f3
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 // 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(
47     args: Opts,
48     question_policy: QuestionPolicy,
49     file_visibility_policy: FileVisibilityPolicy,
50 ) -> crate::Result<()> {
51     match args.cmd {
52         Subcommand::Compress { mut files, output: output_path } => {
53             // 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)
54             if output_path.exists() {
55                 clean_input_files_if_needed(&mut files, &fs::canonicalize(&output_path)?);
56             }
57             // After cleaning, if there are no input files left, exit
58             if files.is_empty() {
59                 return Err(FinalError::with_title("No files to compress").into());
60             }
62             // Formats from path extension, like "file.tar.gz.xz" -> vec![Tar, Gzip, Lzma]
63             let mut formats = extension::extensions_from_path(&output_path);
65             if formats.is_empty() {
66                 let error = FinalError::with_title(format!("Cannot compress to '{}'.", to_utf(&output_path)))
67                     .detail("You shall supply the compression format")
68                     .hint("Try adding supported extensions (see --help):")
69                     .hint(format!("  ouch compress <FILES>... {}.tar.gz", to_utf(&output_path)))
70                     .hint(format!("  ouch compress <FILES>... {}.zip", to_utf(&output_path)))
71                     .hint("")
72                     .hint("Alternatively, you can overwrite this option by using the '--format' flag:")
73                     .hint(format!("  ouch compress <FILES>... {} --format tar.gz", to_utf(&output_path)));
75                 return Err(error.into());
76             }
78             if !formats.get(0).map(Extension::is_archive).unwrap_or(false) && represents_several_files(&files) {
79                 // This piece of code creates a suggestion for compressing multiple files
80                 // It says:
81                 // Change from file.bz.xz
82                 // To          file.tar.bz.xz
83                 let extensions_text: String = formats.iter().map(|format| format.to_string()).collect();
85                 let output_path = to_utf(output_path);
87                 // Breaks if Lzma is .lz or .lzma and not .xz
88                 // Or if Bzip is .bz2 and not .bz
89                 let extensions_start_position = output_path.rfind(&extensions_text).unwrap();
90                 let pos = extensions_start_position - 1;
91                 let mut suggested_output_path = output_path.clone();
92                 suggested_output_path.insert_str(pos, ".tar");
94                 let error = FinalError::with_title(format!("Cannot compress to '{}'.", to_utf(&output_path)))
95                     .detail("You are trying to compress multiple files.")
96                     .detail(format!("The compression format '{}' cannot receive multiple files.", &formats[0]))
97                     .detail("The only supported formats that archive files into an archive are .tar and .zip.")
98                     .hint(format!("Try inserting '.tar' or '.zip' before '{}'.", &formats[0]))
99                     .hint(format!("From: {}", output_path))
100                     .hint(format!("To:   {}", suggested_output_path));
102                 return Err(error.into());
103             }
105             if let Some(format) = formats.iter().skip(1).find(|format| format.is_archive()) {
106                 let error = FinalError::with_title(format!("Cannot compress to '{}'.", to_utf(&output_path)))
107                     .detail(format!("Found the format '{}' in an incorrect position.", format))
108                     .detail(format!("'{}' can only be used at the start of the file extension.", format))
109                     .hint(format!("If you wish to compress multiple files, start the extension with '{}'.", format))
110                     .hint(format!("Otherwise, remove the last '{}' from '{}'.", format, to_utf(&output_path)));
112                 return Err(error.into());
113             }
115             if output_path.exists() && !utils::user_wants_to_overwrite(&output_path, question_policy)? {
116                 // User does not want to overwrite this file, skip and return without any errors
117                 return Ok(());
118             }
120             let output_file = fs::File::create(&output_path)?;
122             if !represents_several_files(&files) {
123                 // It's possible the file is already partially compressed so we don't want to compress it again
124                 // `ouch compress file.tar.gz file.tar.gz.xz` should produce `file.tar.gz.xz` and not `file.tar.gz.tar.gz.xz`
125                 let input_extensions = extension::extensions_from_path(&files[0]);
127                 // 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
128                 let mut new_formats = Vec::with_capacity(formats.len());
129                 for (inp_ext, out_ext) in input_extensions.iter().zip(&formats) {
130                     if inp_ext.compression_formats == out_ext.compression_formats {
131                         new_formats.push(out_ext.clone());
132                     } else if inp_ext
133                         .compression_formats
134                         .iter()
135                         .zip(out_ext.compression_formats.iter())
136                         .all(|(inp, out)| inp == out)
137                     {
138                         let new_ext = Extension::new(
139                             &out_ext.compression_formats[..inp_ext.compression_formats.len()],
140                             &out_ext.display_text,
141                         );
142                         new_formats.push(new_ext);
143                         break;
144                     }
145                 }
146                 // If the input is a sublist at the start of `formats` then remove the extensions
147                 // Note: If input_extensions is empty then it will make `formats` empty too, which we don't want
148                 if !input_extensions.is_empty() && new_formats != formats {
149                     // Safety:
150                     //   We checked above that input_extensions isn't empty, so files[0] has an extension.
151                     //
152                     //   Path::extension says: "if there is no file_name, then there is no extension".
153                     //   Contrapositive statement: "if there is extension, then there is file_name".
154                     info!(
155                         accessible, // important information
156                         "Partial compression detected. Compressing {} into {}",
157                         to_utf(files[0].as_path().file_name().unwrap()),
158                         to_utf(&output_path)
159                     );
160                     formats = new_formats;
161                 }
162             }
163             let compress_result =
164                 compress_files(files, formats, output_file, &output_path, question_policy, file_visibility_policy);
166             if let Ok(true) = compress_result {
167                 // this is only printed once, so it doesn't result in much text. On the other hand,
168                 // having a final status message is important especially in an accessibility context
169                 // as screen readers may not read a commands exit code, making it hard to reason
170                 // about whether the command succeeded without such a message
171                 info!(accessible, "Successfully compressed '{}'.", to_utf(output_path));
172             } else {
173                 // If Ok(false) or Err() occurred, delete incomplete file
174                 // Print an extra alert message pointing out that we left a possibly
175                 // CORRUPTED FILE at `output_path`
176                 if let Err(err) = fs::remove_file(&output_path) {
177                     eprintln!("{red}FATAL ERROR:\n", red = *colors::RED);
178                     eprintln!("  Please manually delete '{}'.", to_utf(&output_path));
179                     eprintln!("  Compression failed and we could not delete '{}'.", to_utf(&output_path),);
180                     eprintln!("  Error:{reset} {}{red}.{reset}\n", err, reset = *colors::RESET, red = *colors::RED);
181                 }
182             }
184             compress_result?;
185         }
186         Subcommand::Decompress { files, output_dir } => {
187             let mut output_paths = vec![];
188             let mut formats = vec![];
190             for path in files.iter() {
191                 let (file_output_path, file_formats) = extension::separate_known_extensions_from_name(path);
192                 output_paths.push(file_output_path);
193                 formats.push(file_formats);
194             }
196             if let ControlFlow::Break(_) = check_mime_type(&files, &mut formats, question_policy)? {
197                 return Ok(());
198             }
200             let files_missing_format: Vec<PathBuf> = files
201                 .iter()
202                 .zip(&formats)
203                 .filter(|(_, formats)| formats.is_empty())
204                 .map(|(input_path, _)| PathBuf::from(input_path))
205                 .collect();
207             if !files_missing_format.is_empty() {
208                 let error = FinalError::with_title("Cannot decompress files without extensions")
209                     .detail(format!(
210                         "Files without supported extensions: {}",
211                         concatenate_os_str_list(&files_missing_format)
212                     ))
213                     .detail("Decompression formats are detected automatically by the file extension")
214                     .hint("Provide a file with a supported extension:")
215                     .hint("  ouch decompress example.tar.gz")
216                     .hint("")
217                     .hint("Or overwrite this option with the '--format' flag:")
218                     .hint(format!("  ouch decompress {} --format tar.gz", to_utf(&files_missing_format[0])));
220                 return Err(error.into());
221             }
223             // The directory that will contain the output files
224             // We default to the current directory if the user didn't specify an output directory with --dir
225             let output_dir = if let Some(dir) = output_dir {
226                 if !utils::clear_path(&dir, question_policy)? {
227                     // User doesn't want to overwrite
228                     return Ok(());
229                 }
230                 utils::create_dir_if_non_existent(&dir)?;
231                 dir
232             } else {
233                 PathBuf::from(".")
234             };
236             for ((input_path, formats), file_name) in files.iter().zip(formats).zip(output_paths) {
237                 let output_file_path = output_dir.join(file_name); // Path used by single file format archives
238                 decompress_file(input_path, formats, &output_dir, output_file_path, question_policy)?;
239             }
240         }
241         Subcommand::List { archives: files, tree } => {
242             let mut formats = vec![];
244             for path in files.iter() {
245                 let (_, file_formats) = extension::separate_known_extensions_from_name(path);
246                 formats.push(file_formats);
247             }
249             if let ControlFlow::Break(_) = check_mime_type(&files, &mut formats, question_policy)? {
250                 return Ok(());
251             }
253             let not_archives: Vec<PathBuf> = files
254                 .iter()
255                 .zip(&formats)
256                 .filter(|(_, formats)| !formats.get(0).map(Extension::is_archive).unwrap_or(false))
257                 .map(|(path, _)| path.clone())
258                 .collect();
260             if !not_archives.is_empty() {
261                 let error = FinalError::with_title("Cannot list archive contents")
262                     .detail("Only archives can have their contents listed")
263                     .detail(format!("Files are not archives: {}", concatenate_os_str_list(&not_archives)));
265                 return Err(error.into());
266             }
268             let list_options = ListOptions { tree };
270             for (i, (archive_path, formats)) in files.iter().zip(formats).enumerate() {
271                 if i > 0 {
272                     println!();
273                 }
274                 let formats = formats.iter().flat_map(Extension::iter).map(Clone::clone).collect();
275                 list_archive_contents(archive_path, formats, list_options, question_policy)?;
276             }
277         }
278     }
279     Ok(())
282 // Compress files into an `output_file`
284 // files are the list of paths to be compressed: ["dir/file1.txt", "dir/file2.txt"]
285 // formats contains each format necessary for compression, example: [Tar, Gz] (in compression order)
286 // output_file is the resulting compressed file name, example: "compressed.tar.gz"
288 // Returns Ok(true) if compressed all files successfully, and Ok(false) if user opted to skip files
289 fn compress_files(
290     files: Vec<PathBuf>,
291     formats: Vec<Extension>,
292     output_file: fs::File,
293     output_dir: &Path,
294     question_policy: QuestionPolicy,
295     file_visibility_policy: FileVisibilityPolicy,
296 ) -> crate::Result<bool> {
297     // The next lines are for displaying the progress bar
298     // If the input files contain a directory, then the total size will be underestimated
299     let (total_input_size, precise) = files
300         .iter()
301         .map(|f| (f.metadata().expect("file exists").len(), f.is_file()))
302         .fold((0, true), |(total_size, and_precise), (size, precise)| (total_size + size, and_precise & precise));
303     //NOTE: canonicalize is here to avoid a weird bug:
304     //      > If output_file_path is a nested path and it exists and the user overwrite it
305     //      >> output_file_path.exists() will always return false (somehow)
306     //      - canonicalize seems to fix this
307     let output_file_path = output_file.path().canonicalize()?;
309     let file_writer = BufWriter::with_capacity(BUFFER_CAPACITY, output_file);
311     let mut writer: Box<dyn Write> = Box::new(file_writer);
313     // Grab previous encoder and wrap it inside of a new one
314     let chain_writer_encoder = |format: &CompressionFormat, encoder: Box<dyn Write>| -> crate::Result<Box<dyn Write>> {
315         let encoder: Box<dyn Write> = match format {
316             Gzip => Box::new(flate2::write::GzEncoder::new(encoder, Default::default())),
317             Bzip => Box::new(bzip2::write::BzEncoder::new(encoder, Default::default())),
318             Lz4 => Box::new(lzzzz::lz4f::WriteCompressor::new(encoder, Default::default())?),
319             Lzma => Box::new(xz2::write::XzEncoder::new(encoder, 6)),
320             Snappy => Box::new(snap::write::FrameEncoder::new(encoder)),
321             Zstd => {
322                 let zstd_encoder = zstd::stream::write::Encoder::new(encoder, Default::default());
323                 // Safety:
324                 //     Encoder::new() can only fail if `level` is invalid, but Default::default()
325                 //     is guaranteed to be valid
326                 Box::new(zstd_encoder.unwrap().auto_finish())
327             }
328             Tar | Zip => unreachable!(),
329         };
330         Ok(encoder)
331     };
333     for format in formats.iter().flat_map(Extension::iter).skip(1).collect::<Vec<_>>().iter().rev() {
334         writer = chain_writer_encoder(format, writer)?;
335     }
337     match formats[0].compression_formats[0] {
338         Gzip | Bzip | Lz4 | Lzma | Snappy | Zstd => {
339             let _progress = Progress::new_accessible_aware(
340                 total_input_size,
341                 precise,
342                 Some(Box::new(move || output_file_path.metadata().expect("file exists").len())),
343             );
345             writer = chain_writer_encoder(&formats[0].compression_formats[0], writer)?;
346             let mut reader = fs::File::open(&files[0]).unwrap();
347             io::copy(&mut reader, &mut writer)?;
348         }
349         Tar => {
350             let mut progress = Progress::new_accessible_aware(
351                 total_input_size,
352                 precise,
353                 Some(Box::new(move || output_file_path.metadata().expect("file exists").len())),
354             );
356             archive::tar::build_archive_from_paths(
357                 &files,
358                 &mut writer,
359                 file_visibility_policy,
360                 progress.as_mut().map(Progress::display_handle).unwrap_or(&mut io::stdout()),
361             )?;
362             writer.flush()?;
363         }
364         Zip => {
365             if formats.len() > 1 {
366                 eprintln!("{orange}[WARNING]{reset}", orange = *colors::ORANGE, reset = *colors::RESET);
367                 eprintln!(
368                     "\tThere is a limitation for .zip archives with extra extensions. (e.g. <file>.zip.gz)\
369                     \n\tThe design of .zip makes it impossible to compress via stream, so it must be done entirely in memory.\
370                     \n\tBy compressing .zip with extra compression formats, you can run out of RAM if the file is too large!"
371                 );
373                 // give user the option to continue compressing after warning is shown
374                 if !user_wants_to_continue(output_dir, question_policy, QuestionAction::Compression)? {
375                     return Ok(false);
376                 }
377             }
379             let mut vec_buffer = io::Cursor::new(vec![]);
381             let current_position_fn = {
382                 let vec_buffer_ptr = {
383                     struct FlyPtr(*const io::Cursor<Vec<u8>>);
384                     unsafe impl Send for FlyPtr {}
385                     FlyPtr(&vec_buffer as *const _)
386                 };
387                 Box::new(move || {
388                     let vec_buffer_ptr = &vec_buffer_ptr;
389                     // Safety: ptr is valid and vec_buffer is still alive
390                     unsafe { &*vec_buffer_ptr.0 }.position()
391                 })
392             };
394             let mut progress = Progress::new_accessible_aware(total_input_size, precise, Some(current_position_fn));
396             archive::zip::build_archive_from_paths(
397                 &files,
398                 &mut vec_buffer,
399                 file_visibility_policy,
400                 progress.as_mut().map(Progress::display_handle).unwrap_or(&mut io::stdout()),
401             )?;
402             let vec_buffer = vec_buffer.into_inner();
403             io::copy(&mut vec_buffer.as_slice(), &mut writer)?;
404         }
405     }
407     Ok(true)
410 // Decompress a file
412 // File at input_file_path is opened for reading, example: "archive.tar.gz"
413 // formats contains each format necessary for decompression, example: [Gz, Tar] (in decompression order)
414 // output_dir it's where the file will be decompressed to, this function assumes that the directory exists
415 // output_file_path is only used when extracting single file formats, not archive formats like .tar or .zip
416 fn decompress_file(
417     input_file_path: &Path,
418     formats: Vec<Extension>,
419     output_dir: &Path,
420     output_file_path: PathBuf,
421     question_policy: QuestionPolicy,
422 ) -> crate::Result<()> {
423     assert!(output_dir.exists());
424     let total_input_size = input_file_path.metadata().expect("file exists").len();
425     let reader = fs::File::open(&input_file_path)?;
426     // Zip archives are special, because they require io::Seek, so it requires it's logic separated
427     // from decoder chaining.
428     //
429     // This is the only case where we can read and unpack it directly, without having to do
430     // in-memory decompression/copying first.
431     //
432     // Any other Zip decompression done can take up the whole RAM and freeze ouch.
433     if formats.len() == 1 && *formats[0].compression_formats == [Zip] {
434         let zip_archive = zip::ZipArchive::new(reader)?;
435         let files = if let ControlFlow::Continue(files) = smart_unpack(
436             Box::new(move |output_dir| {
437                 let mut progress = Progress::new_accessible_aware(total_input_size, true, None);
438                 crate::archive::zip::unpack_archive(
439                     zip_archive,
440                     output_dir,
441                     progress.as_mut().map(Progress::display_handle).unwrap_or(&mut io::stdout()),
442                 )
443             }),
444             output_dir,
445             &output_file_path,
446             question_policy,
447         )? {
448             files
449         } else {
450             return Ok(());
451         };
453         // this is only printed once, so it doesn't result in much text. On the other hand,
454         // having a final status message is important especially in an accessibility context
455         // as screen readers may not read a commands exit code, making it hard to reason
456         // about whether the command succeeded without such a message
457         info!(
458             accessible,
459             "Successfully decompressed archive in {} ({} files).",
460             nice_directory_display(output_dir),
461             files.len()
462         );
464         return Ok(());
465     }
467     // Will be used in decoder chaining
468     let reader = BufReader::with_capacity(BUFFER_CAPACITY, reader);
469     let mut reader: Box<dyn Read> = Box::new(reader);
471     // Grab previous decoder and wrap it inside of a new one
472     let chain_reader_decoder = |format: &CompressionFormat, decoder: Box<dyn Read>| -> crate::Result<Box<dyn Read>> {
473         let decoder: Box<dyn Read> = match format {
474             Gzip => Box::new(flate2::read::GzDecoder::new(decoder)),
475             Bzip => Box::new(bzip2::read::BzDecoder::new(decoder)),
476             Lz4 => Box::new(lzzzz::lz4f::ReadDecompressor::new(decoder)?),
477             Lzma => Box::new(xz2::read::XzDecoder::new(decoder)),
478             Snappy => Box::new(snap::read::FrameDecoder::new(decoder)),
479             Zstd => Box::new(zstd::stream::Decoder::new(decoder)?),
480             Tar | Zip => unreachable!(),
481         };
482         Ok(decoder)
483     };
485     for format in formats.iter().flat_map(Extension::iter).skip(1).collect::<Vec<_>>().iter().rev() {
486         reader = chain_reader_decoder(format, reader)?;
487     }
489     let files_unpacked;
490     match formats[0].compression_formats[0] {
491         Gzip | Bzip | Lz4 | Lzma | Snappy | Zstd => {
492             reader = chain_reader_decoder(&formats[0].compression_formats[0], reader)?;
494             let writer = utils::create_or_ask_overwrite(&output_file_path, question_policy)?;
495             if writer.is_none() {
496                 // Means that the user doesn't want to overwrite
497                 return Ok(());
498             }
499             let mut writer = writer.unwrap();
501             let current_position_fn = Box::new({
502                 let output_file_path = output_file_path.clone();
503                 move || output_file_path.clone().metadata().expect("file exists").len()
504             });
505             let _progress = Progress::new_accessible_aware(total_input_size, true, Some(current_position_fn));
507             io::copy(&mut reader, &mut writer)?;
508             files_unpacked = vec![output_file_path];
509         }
510         Tar => {
511             files_unpacked = if let ControlFlow::Continue(files) = smart_unpack(
512                 Box::new(move |output_dir| {
513                     let mut progress = Progress::new_accessible_aware(total_input_size, true, None);
514                     crate::archive::tar::unpack_archive(
515                         reader,
516                         output_dir,
517                         progress.as_mut().map(Progress::display_handle).unwrap_or(&mut io::stdout()),
518                     )
519                 }),
520                 output_dir,
521                 &output_file_path,
522                 question_policy,
523             )? {
524                 files
525             } else {
526                 return Ok(());
527             };
528         }
529         Zip => {
530             if formats.len() > 1 {
531                 eprintln!("{orange}[WARNING]{reset}", orange = *colors::ORANGE, reset = *colors::RESET);
532                 eprintln!(
533                     "\tThere is a limitation for .zip archives with extra extensions. (e.g. <file>.zip.gz)\
534                     \n\tThe design of .zip makes it impossible to compress via stream, so it must be done entirely in memory.\
535                     \n\tBy compressing .zip with extra compression formats, you can run out of RAM if the file is too large!"
536                 );
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!(
632                     "\tThere is a limitation for .zip archives with extra extensions. (e.g. <file>.zip.gz)\
633                     \n\tThe design of .zip makes it impossible to compress via stream, so it must be done entirely in memory.\
634                     \n\tBy compressing .zip with extra compression formats, you can run out of RAM if the file is too large!"
635                 );
637                 // give user the option to continue decompressing after warning is shown
638                 if !user_wants_to_continue(archive_path, question_policy, QuestionAction::Decompression)? {
639                     return Ok(());
640                 }
641             }
643             let mut vec = vec![];
644             io::copy(&mut reader, &mut vec)?;
645             let zip_archive = zip::ZipArchive::new(io::Cursor::new(vec))?;
647             Box::new(crate::archive::zip::list_archive(zip_archive))
648         }
649         Gzip | Bzip | Lz4 | Lzma | Snappy | Zstd => {
650             panic!("Not an archive! This should never happen, if it does, something is wrong with `CompressionFormat::is_archive()`. Please report this error!");
651         }
652     };
653     list::list_files(archive_path, files, list_options)?;
654     Ok(())
657 /// Unpacks an archive with some heuristics
658 /// - If the archive contains only one file, it will be extracted to the `output_dir`
659 /// - 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`)
660 /// Note: This functions assumes that `output_dir` exists
661 fn smart_unpack(
662     unpack_fn: Box<dyn FnOnce(&Path) -> crate::Result<Vec<PathBuf>>>,
663     output_dir: &Path,
664     output_file_path: &Path,
665     question_policy: QuestionPolicy,
666 ) -> crate::Result<ControlFlow<(), Vec<PathBuf>>> {
667     assert!(output_dir.exists());
668     let temp_dir = tempfile::tempdir_in(output_dir)?;
669     let temp_dir_path = temp_dir.path();
670     info!(
671         accessible,
672         "Created temporary directory {} to hold decompressed elements.",
673         nice_directory_display(temp_dir_path)
674     );
676     // unpack the files
677     let files = unpack_fn(temp_dir_path)?;
679     let root_contains_only_one_element = fs::read_dir(&temp_dir_path)?.count() == 1;
680     if root_contains_only_one_element {
681         // Only one file in the root directory, so we can just move it to the output directory
682         let file = fs::read_dir(&temp_dir_path)?.next().expect("item exists")?;
683         let file_path = file.path();
684         let file_name =
685             file_path.file_name().expect("Should be safe because paths in archives should not end with '..'");
686         let correct_path = output_dir.join(file_name);
687         // One case to handle tough is we need to check if a file with the same name already exists
688         if !utils::clear_path(&correct_path, question_policy)? {
689             return Ok(ControlFlow::Break(()));
690         }
691         fs::rename(&file_path, &correct_path)?;
692         info!(
693             accessible,
694             "Successfully moved {} to {}.",
695             nice_directory_display(&file_path),
696             nice_directory_display(&correct_path)
697         );
698     } else {
699         // Multiple files in the root directory, so:
700         // Rename  the temporary directory to the archive name, which is output_file_path
701         // One case to handle tough is we need to check if a file with the same name already exists
702         if !utils::clear_path(output_file_path, question_policy)? {
703             return Ok(ControlFlow::Break(()));
704         }
705         fs::rename(&temp_dir_path, &output_file_path)?;
706         info!(
707             accessible,
708             "Successfully moved {} to {}.",
709             nice_directory_display(&temp_dir_path),
710             nice_directory_display(&output_file_path)
711         );
712     }
713     Ok(ControlFlow::Continue(files))
716 fn check_mime_type(
717     files: &[PathBuf],
718     formats: &mut Vec<Vec<Extension>>,
719     question_policy: QuestionPolicy,
720 ) -> crate::Result<ControlFlow<()>> {
721     for (path, format) in files.iter().zip(formats.iter_mut()) {
722         if format.is_empty() {
723             // File with no extension
724             // Try to detect it automatically and prompt the user about it
725             if let Some(detected_format) = try_infer_extension(path) {
726                 // Infering the file extension can have unpredicted consequences (e.g. the user just
727                 // mistyped, ...) which we should always inform the user about.
728                 info!(accessible, "Detected file: `{}` extension as `{}`", path.display(), detected_format);
729                 if user_wants_to_continue(path, question_policy, QuestionAction::Decompression)? {
730                     format.push(detected_format);
731                 } else {
732                     return Ok(ControlFlow::Break(()));
733                 }
734             }
735         } else if let Some(detected_format) = try_infer_extension(path) {
736             // File ending with extension
737             // Try to detect the extension and warn the user if it differs from the written one
738             let outer_ext = format.iter().next_back().unwrap();
739             if outer_ext != &detected_format {
740                 warning!(
741                     "The file extension: `{}` differ from the detected extension: `{}`",
742                     outer_ext,
743                     detected_format
744                 );
745                 if !user_wants_to_continue(path, question_policy, QuestionAction::Decompression)? {
746                     return Ok(ControlFlow::Break(()));
747                 }
748             }
749         } else {
750             // NOTE: If this actually produces no false positives, we can upgrade it in the future
751             // to a warning and ask the user if he wants to continue decompressing.
752             info!(accessible, "Could not detect the extension of `{}`", path.display());
753         }
754     }
755     Ok(ControlFlow::Continue(()))
758 fn clean_input_files_if_needed(files: &mut Vec<PathBuf>, output_path: &Path) {
759     let mut idx = 0;
760     while idx < files.len() {
761         if files[idx] == output_path {
762             warning!("The output file and the input file are the same: `{}`, skipping...", output_path.display());
763             files.remove(idx);
764         } else {
765             idx += 1;
766         }
767     }