1 //! Core of the crate, where the `compress_files` and `decompress_file` functions are implemented
3 //! Also, where correctly call functions based on the detected `Command`.
6 io::{self, BufReader, BufWriter, Read, Write},
19 CompressionFormat::{self, *},
23 list::{self, ListOptions},
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,
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()
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<()> {
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)?);
53 // After cleaning, if there are no input files left, exit
55 return Err(FinalError::with_title("No files to compress").into());
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)))
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());
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
77 // Change from file.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());
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());
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
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());
132 .zip(out_ext.compression_formats.iter())
133 .all(|(inp, out)| inp == out)
135 let new_ext = Extension::new(
136 &out_ext.compression_formats[..inp_ext.compression_formats.len()],
137 &out_ext.display_text,
139 new_formats.push(new_ext);
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 {
147 // We checked above that input_extensions isn't empty, so files[0] has an extension.
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".
152 accessible, // important information
153 "Partial compression detected. Compressing {} into {}",
154 to_utf(files[0].as_path().file_name().unwrap()),
157 formats = new_formats;
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);
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));
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);
192 if let ControlFlow::Break(_) = check_mime_type(&files, &mut formats, question_policy)? {
196 let files_missing_format: Vec<PathBuf> = files
199 .filter(|(_, formats)| formats.is_empty())
200 .map(|(input_path, _)| PathBuf::from(input_path))
203 if !files_missing_format.is_empty() {
204 let error = FinalError::with_title("Cannot decompress files without extensions")
206 "Files without supported extensions: {}",
207 concatenate_os_str_list(&files_missing_format)
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")
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());
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
226 utils::create_dir_if_non_existent(&dir)?;
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)?;
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);
245 if let ControlFlow::Break(_) = check_mime_type(&files, &mut formats, question_policy)? {
249 let not_archives: Vec<PathBuf> = files
252 .filter(|(_, formats)| !formats.get(0).map(Extension::is_archive).unwrap_or(false))
253 .map(|(path, _)| path.clone())
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(¬_archives)));
261 return Err(error.into());
264 let list_options = ListOptions { tree };
266 for (i, (archive_path, formats)) in files.iter().zip(formats).enumerate() {
270 let formats = formats.iter().flat_map(Extension::iter).map(Clone::clone).collect();
271 list_archive_contents(archive_path, formats, list_options, question_policy)?;
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"
285 formats: Vec<Extension>,
286 output_file: fs::File,
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
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)),
314 let zstd_encoder = zstd::stream::write::Encoder::new(encoder, Default::default());
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())
320 Tar | Zip => unreachable!(),
325 for format in formats.iter().flat_map(Extension::iter).skip(1).collect::<Vec<_>>().iter().rev() {
326 writer = chain_writer_encoder(format, writer)?;
329 match formats[0].compression_formats[0] {
330 Gzip | Bzip | Lz4 | Lzma | Zstd => {
331 let _progress = Progress::new_accessible_aware(
334 Some(Box::new(move || output_file_path.metadata().expect("file exists").len())),
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)?;
342 let mut progress = Progress::new_accessible_aware(
345 Some(Box::new(move || output_file_path.metadata().expect("file exists").len())),
348 archive::tar::build_archive_from_paths(
351 progress.as_mut().map(Progress::display_handle).unwrap_or(&mut io::stdout()),
356 eprintln!("{orange}[WARNING]{reset}", orange = *colors::ORANGE, reset = *colors::RESET);
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!"
363 // give user the option to continue compressing after warning is shown
364 if !user_wants_to_continue_compressing(output_dir, question_policy)? {
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 _)
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()
383 let mut progress = Progress::new_accessible_aware(total_input_size, precise, Some(current_position_fn));
385 archive::zip::build_archive_from_paths(
388 progress.as_mut().map(Progress::display_handle).unwrap_or(&mut io::stdout()),
390 let vec_buffer = vec_buffer.into_inner();
391 io::copy(&mut vec_buffer.as_slice(), &mut writer)?;
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
405 input_file_path: &Path,
406 formats: Vec<Extension>,
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.
417 // This is the only case where we can read and unpack it directly, without having to do
418 // in-memory decompression/copying first.
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(
429 progress.as_mut().map(Progress::display_handle).unwrap_or(&mut io::stdout()),
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
447 "Successfully decompressed archive in {} ({} files).",
448 nice_directory_display(output_dir),
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!(),
472 for format in formats.iter().flat_map(Extension::iter).skip(1).collect::<Vec<_>>().iter().rev() {
473 reader = chain_reader_decoder(format, reader)?;
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
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()
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];
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(
504 progress.as_mut().map(Progress::display_handle).unwrap_or(&mut io::stdout()),
517 eprintln!("{orange}[WARNING]{reset}", orange = *colors::ORANGE, reset = *colors::RESET);
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!"
524 // give user the option to continue decompressing after warning is shown
525 if !user_wants_to_continue_decompressing(input_file_path, question_policy)? {
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(
539 progress.as_mut().map(Progress::display_handle).unwrap_or(&mut io::stdout()),
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());
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(
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.
576 // This is the only case where we can read and unpack it directly, without having to do
577 // in-memory decompression/copying first.
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);
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!(),
604 for format in formats.iter().skip(1).rev() {
605 reader = chain_reader_decoder(format, reader)?;
608 let files = match formats[0] {
609 Tar => crate::archive::tar::list_archive(reader)?,
611 eprintln!("{orange}[WARNING]{reset}", orange = *colors::ORANGE, reset = *colors::RESET);
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!"
618 // give user the option to continue decompressing after warning is shown
619 if !user_wants_to_continue_decompressing(archive_path, question_policy)? {
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)?
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!");
633 list::list_files(archive_path, files, list_options);
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
642 unpack_fn: Box<dyn FnOnce(&Path) -> crate::Result<Vec<PathBuf>>>,
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();
652 "Created temporary directory {} to hold decompressed elements.",
653 nice_directory_display(temp_dir_path)
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();
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(()));
671 fs::rename(&file_path, &correct_path)?;
674 "Successfully moved {} to {}.",
675 nice_directory_display(&file_path),
676 nice_directory_display(&correct_path)
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(()));
685 fs::rename(&temp_dir_path, &output_file_path)?;
688 "Successfully moved {} to {}.",
689 nice_directory_display(&temp_dir_path),
690 nice_directory_display(&output_file_path)
693 Ok(ControlFlow::Continue(files))
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);
712 return Ok(ControlFlow::Break(()));
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 {
721 "The file extension: `{}` differ from the detected extension: `{}`",
725 if !user_wants_to_continue_decompressing(path, question_policy)? {
726 return Ok(ControlFlow::Break(()));
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());
735 Ok(ControlFlow::Continue(()))
738 fn clean_input_files_if_needed(files: &mut Vec<PathBuf>, output_path: &Path) {
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());