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 - 1;
87 let mut suggested_output_path = output_path.clone();
88 suggested_output_path.insert_str(pos, ".tar");
90 let error = FinalError::with_title(format!("Cannot compress to '{}'.", to_utf(&output_path)))
91 .detail("You are trying to compress multiple files.")
92 .detail(format!("The compression format '{}' cannot receive multiple files.", &formats[0]))
93 .detail("The only supported formats that archive files into an archive are .tar and .zip.")
94 .hint(format!("Try inserting '.tar' or '.zip' before '{}'.", &formats[0]))
95 .hint(format!("From: {}", output_path))
96 .hint(format!("To: {}", suggested_output_path));
98 return Err(error.into());
101 if let Some(format) = formats.iter().skip(1).find(|format| format.is_archive()) {
102 let error = FinalError::with_title(format!("Cannot compress to '{}'.", to_utf(&output_path)))
103 .detail(format!("Found the format '{}' in an incorrect position.", format))
104 .detail(format!("'{}' can only be used at the start of the file extension.", format))
105 .hint(format!("If you wish to compress multiple files, start the extension with '{}'.", format))
106 .hint(format!("Otherwise, remove the last '{}' from '{}'.", format, to_utf(&output_path)));
108 return Err(error.into());
111 if output_path.exists() && !utils::user_wants_to_overwrite(&output_path, question_policy)? {
112 // User does not want to overwrite this file, skip and return without any errors
116 let output_file = fs::File::create(&output_path)?;
118 if !represents_several_files(&files) {
119 // It's possible the file is already partially compressed so we don't want to compress it again
120 // `ouch compress file.tar.gz file.tar.gz.xz` should produce `file.tar.gz.xz` and not `file.tar.gz.tar.gz.xz`
121 let input_extensions = extension::extensions_from_path(&files[0]);
123 // We calculate the formats that are left if we filter out a sublist at the start of what we have that's the same as the input formats
124 let mut new_formats = Vec::with_capacity(formats.len());
125 for (inp_ext, out_ext) in input_extensions.iter().zip(&formats) {
126 if inp_ext.compression_formats == out_ext.compression_formats {
127 new_formats.push(out_ext.clone());
131 .zip(out_ext.compression_formats.iter())
132 .all(|(inp, out)| inp == out)
134 let new_ext = Extension::new(
135 &out_ext.compression_formats[..inp_ext.compression_formats.len()],
136 &out_ext.display_text,
138 new_formats.push(new_ext);
142 // If the input is a sublist at the start of `formats` then remove the extensions
143 // Note: If input_extensions is empty then it will make `formats` empty too, which we don't want
144 if !input_extensions.is_empty() && new_formats != formats {
146 // We checked above that input_extensions isn't empty, so files[0] has an extension.
148 // Path::extension says: "if there is no file_name, then there is no extension".
149 // Contrapositive statement: "if there is extension, then there is file_name".
151 accessible, // important information
152 "Partial compression detected. Compressing {} into {}",
153 to_utf(files[0].as_path().file_name().unwrap()),
156 formats = new_formats;
159 let compress_result = compress_files(files, formats, output_file, &output_path, question_policy);
161 // If any error occurred, delete incomplete file
162 if compress_result.is_err() {
163 // Print an extra alert message pointing out that we left a possibly
164 // CORRUPTED FILE at `output_path`
165 if let Err(err) = fs::remove_file(&output_path) {
166 eprintln!("{red}FATAL ERROR:\n", red = *colors::RED);
167 eprintln!(" Please manually delete '{}'.", to_utf(&output_path));
168 eprintln!(" Compression failed and we could not delete '{}'.", to_utf(&output_path),);
169 eprintln!(" Error:{reset} {}{red}.{reset}\n", err, reset = *colors::RESET, red = *colors::RED);
172 // this is only printed once, so it doesn't result in much text. On the other hand,
173 // having a final status message is important especially in an accessibility context
174 // as screen readers may not read a commands exit code, making it hard to reason
175 // about whether the command succeeded without such a message
176 info!(accessible, "Successfully compressed '{}'.", to_utf(output_path));
181 Subcommand::Decompress { files, output_dir } => {
182 let mut output_paths = vec![];
183 let mut formats = vec![];
185 for path in files.iter() {
186 let (file_output_path, file_formats) = extension::separate_known_extensions_from_name(path);
187 output_paths.push(file_output_path);
188 formats.push(file_formats);
191 if let ControlFlow::Break(_) = check_mime_type(&files, &mut formats, question_policy)? {
195 let files_missing_format: Vec<PathBuf> = files
198 .filter(|(_, formats)| formats.is_empty())
199 .map(|(input_path, _)| PathBuf::from(input_path))
202 if !files_missing_format.is_empty() {
203 let error = FinalError::with_title("Cannot decompress files without extensions")
205 "Files without supported extensions: {}",
206 concatenate_os_str_list(&files_missing_format)
208 .detail("Decompression formats are detected automatically by the file extension")
209 .hint("Provide a file with a supported extension:")
210 .hint(" ouch decompress example.tar.gz")
212 .hint("Or overwrite this option with the '--format' flag:")
213 .hint(format!(" ouch decompress {} --format tar.gz", to_utf(&files_missing_format[0])));
215 return Err(error.into());
218 // The directory that will contain the output files
219 // We default to the current directory if the user didn't specify an output directory with --dir
220 let output_dir = if let Some(dir) = output_dir {
221 if !utils::clear_path(&dir, question_policy)? {
222 // User doesn't want to overwrite
225 utils::create_dir_if_non_existent(&dir)?;
231 for ((input_path, formats), file_name) in files.iter().zip(formats).zip(output_paths) {
232 let output_file_path = output_dir.join(file_name); // Path used by single file format archives
233 decompress_file(input_path, formats, &output_dir, output_file_path, question_policy)?;
236 Subcommand::List { archives: files, tree } => {
237 let mut formats = vec![];
239 for path in files.iter() {
240 let (_, file_formats) = extension::separate_known_extensions_from_name(path);
241 formats.push(file_formats);
244 if let ControlFlow::Break(_) = check_mime_type(&files, &mut formats, question_policy)? {
248 let not_archives: Vec<PathBuf> = files
251 .filter(|(_, formats)| !formats.get(0).map(Extension::is_archive).unwrap_or(false))
252 .map(|(path, _)| path.clone())
255 if !not_archives.is_empty() {
256 let error = FinalError::with_title("Cannot list archive contents")
257 .detail("Only archives can have their contents listed")
258 .detail(format!("Files are not archives: {}", concatenate_os_str_list(¬_archives)));
260 return Err(error.into());
263 let list_options = ListOptions { tree };
265 for (i, (archive_path, formats)) in files.iter().zip(formats).enumerate() {
269 let formats = formats.iter().flat_map(Extension::iter).map(Clone::clone).collect();
270 list_archive_contents(archive_path, formats, list_options, question_policy)?;
277 // Compress files into an `output_file`
279 // files are the list of paths to be compressed: ["dir/file1.txt", "dir/file2.txt"]
280 // formats contains each format necessary for compression, example: [Tar, Gz] (in compression order)
281 // output_file is the resulting compressed file name, example: "compressed.tar.gz"
284 formats: Vec<Extension>,
285 output_file: fs::File,
287 question_policy: QuestionPolicy,
288 ) -> crate::Result<()> {
289 // The next lines are for displaying the progress bar
290 // If the input files contain a directory, then the total size will be underestimated
291 let (total_input_size, precise) = files
293 .map(|f| (f.metadata().expect("file exists").len(), f.is_file()))
294 .fold((0, true), |(total_size, and_precise), (size, precise)| (total_size + size, and_precise & precise));
295 //NOTE: canonicalize is here to avoid a weird bug:
296 // > If output_file_path is a nested path and it exists and the user overwrite it
297 // >> output_file_path.exists() will always return false (somehow)
298 // - canonicalize seems to fix this
299 let output_file_path = output_file.path().canonicalize()?;
301 let file_writer = BufWriter::with_capacity(BUFFER_CAPACITY, output_file);
303 let mut writer: Box<dyn Write> = Box::new(file_writer);
305 // Grab previous encoder and wrap it inside of a new one
306 let chain_writer_encoder = |format: &CompressionFormat, encoder: Box<dyn Write>| -> crate::Result<Box<dyn Write>> {
307 let encoder: Box<dyn Write> = match format {
308 Gzip => Box::new(flate2::write::GzEncoder::new(encoder, Default::default())),
309 Bzip => Box::new(bzip2::write::BzEncoder::new(encoder, Default::default())),
310 Lz4 => Box::new(lzzzz::lz4f::WriteCompressor::new(encoder, Default::default())?),
311 Lzma => Box::new(xz2::write::XzEncoder::new(encoder, 6)),
312 Snappy => Box::new(snap::write::FrameEncoder::new(encoder)),
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 | Snappy | 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 Snappy => Box::new(snap::read::FrameDecoder::new(decoder)),
467 Zstd => Box::new(zstd::stream::Decoder::new(decoder)?),
468 Tar | Zip => unreachable!(),
473 for format in formats.iter().flat_map(Extension::iter).skip(1).collect::<Vec<_>>().iter().rev() {
474 reader = chain_reader_decoder(format, reader)?;
478 match formats[0].compression_formats[0] {
479 Gzip | Bzip | Lz4 | Lzma | Snappy | Zstd => {
480 reader = chain_reader_decoder(&formats[0].compression_formats[0], reader)?;
482 let writer = utils::create_or_ask_overwrite(&output_file_path, question_policy)?;
483 if writer.is_none() {
484 // Means that the user doesn't want to overwrite
487 let mut writer = writer.unwrap();
489 let current_position_fn = Box::new({
490 let output_file_path = output_file_path.clone();
491 move || output_file_path.clone().metadata().expect("file exists").len()
493 let _progress = Progress::new_accessible_aware(total_input_size, true, Some(current_position_fn));
495 io::copy(&mut reader, &mut writer)?;
496 files_unpacked = vec![output_file_path];
499 files_unpacked = if let ControlFlow::Continue(files) = smart_unpack(
500 Box::new(move |output_dir| {
501 let mut progress = Progress::new_accessible_aware(total_input_size, true, None);
502 crate::archive::tar::unpack_archive(
505 progress.as_mut().map(Progress::display_handle).unwrap_or(&mut io::stdout()),
518 eprintln!("{orange}[WARNING]{reset}", orange = *colors::ORANGE, reset = *colors::RESET);
520 "\tThere is a limitation for .zip archives with extra extensions. (e.g. <file>.zip.gz)\
521 \n\tThe design of .zip makes it impossible to compress via stream, so it must be done entirely in memory.\
522 \n\tBy compressing .zip with extra compression formats, you can run out of RAM if the file is too large!"
525 // give user the option to continue decompressing after warning is shown
526 if !user_wants_to_continue_decompressing(input_file_path, question_policy)? {
530 let mut vec = vec![];
531 io::copy(&mut reader, &mut vec)?;
532 let zip_archive = zip::ZipArchive::new(io::Cursor::new(vec))?;
534 files_unpacked = if let ControlFlow::Continue(files) = smart_unpack(
535 Box::new(move |output_dir| {
536 let mut progress = Progress::new_accessible_aware(total_input_size, true, None);
537 crate::archive::zip::unpack_archive(
540 progress.as_mut().map(Progress::display_handle).unwrap_or(&mut io::stdout()),
554 // this is only printed once, so it doesn't result in much text. On the other hand,
555 // having a final status message is important especially in an accessibility context
556 // as screen readers may not read a commands exit code, making it hard to reason
557 // about whether the command succeeded without such a message
558 info!(accessible, "Successfully decompressed archive in {}.", nice_directory_display(output_dir));
559 info!(accessible, "Files unpacked: {}", files_unpacked.len());
564 // File at input_file_path is opened for reading, example: "archive.tar.gz"
565 // formats contains each format necessary for decompression, example: [Gz, Tar] (in decompression order)
566 fn list_archive_contents(
568 formats: Vec<CompressionFormat>,
569 list_options: ListOptions,
570 question_policy: QuestionPolicy,
571 ) -> crate::Result<()> {
572 let reader = fs::File::open(&archive_path)?;
574 // Zip archives are special, because they require io::Seek, so it requires it's logic separated
575 // from decoder chaining.
577 // This is the only case where we can read and unpack it directly, without having to do
578 // in-memory decompression/copying first.
580 // Any other Zip decompression done can take up the whole RAM and freeze ouch.
581 if let [Zip] = *formats.as_slice() {
582 let zip_archive = zip::ZipArchive::new(reader)?;
583 let files = crate::archive::zip::list_archive(zip_archive)?;
584 list::list_files(archive_path, files, list_options);
588 // Will be used in decoder chaining
589 let reader = BufReader::with_capacity(BUFFER_CAPACITY, reader);
590 let mut reader: Box<dyn Read> = Box::new(reader);
592 // Grab previous decoder and wrap it inside of a new one
593 let chain_reader_decoder = |format: &CompressionFormat, decoder: Box<dyn Read>| -> crate::Result<Box<dyn Read>> {
594 let decoder: Box<dyn Read> = match format {
595 Gzip => Box::new(flate2::read::GzDecoder::new(decoder)),
596 Bzip => Box::new(bzip2::read::BzDecoder::new(decoder)),
597 Lz4 => Box::new(lzzzz::lz4f::ReadDecompressor::new(decoder)?),
598 Lzma => Box::new(xz2::read::XzDecoder::new(decoder)),
599 Snappy => Box::new(snap::read::FrameDecoder::new(decoder)),
600 Zstd => Box::new(zstd::stream::Decoder::new(decoder)?),
601 Tar | Zip => unreachable!(),
606 for format in formats.iter().skip(1).rev() {
607 reader = chain_reader_decoder(format, reader)?;
610 let files = match formats[0] {
611 Tar => crate::archive::tar::list_archive(reader)?,
613 eprintln!("{orange}[WARNING]{reset}", orange = *colors::ORANGE, reset = *colors::RESET);
615 "\tThere is a limitation for .zip archives with extra extensions. (e.g. <file>.zip.gz)\
616 \n\tThe design of .zip makes it impossible to compress via stream, so it must be done entirely in memory.\
617 \n\tBy compressing .zip with extra compression formats, you can run out of RAM if the file is too large!"
620 // give user the option to continue decompressing after warning is shown
621 if !user_wants_to_continue_decompressing(archive_path, question_policy)? {
625 let mut vec = vec![];
626 io::copy(&mut reader, &mut vec)?;
627 let zip_archive = zip::ZipArchive::new(io::Cursor::new(vec))?;
629 crate::archive::zip::list_archive(zip_archive)?
631 Gzip | Bzip | Lz4 | Lzma | Snappy | Zstd => {
632 panic!("Not an archive! This should never happen, if it does, something is wrong with `CompressionFormat::is_archive()`. Please report this error!");
635 list::list_files(archive_path, files, list_options);
639 /// Unpacks an archive with some heuristics
640 /// - If the archive contains only one file, it will be extracted to the `output_dir`
641 /// - If the archive contains multiple files, it will be extracted to a subdirectory of the output_dir named after the archive (given by `output_file_path`)
642 /// Note: This functions assumes that `output_dir` exists
644 unpack_fn: Box<dyn FnOnce(&Path) -> crate::Result<Vec<PathBuf>>>,
646 output_file_path: &Path,
647 question_policy: QuestionPolicy,
648 ) -> crate::Result<ControlFlow<(), Vec<PathBuf>>> {
649 assert!(output_dir.exists());
650 let temp_dir = tempfile::tempdir_in(output_dir)?;
651 let temp_dir_path = temp_dir.path();
654 "Created temporary directory {} to hold decompressed elements.",
655 nice_directory_display(temp_dir_path)
659 let files = unpack_fn(temp_dir_path)?;
661 let root_contains_only_one_element = fs::read_dir(&temp_dir_path)?.count() == 1;
662 if root_contains_only_one_element {
663 // Only one file in the root directory, so we can just move it to the output directory
664 let file = fs::read_dir(&temp_dir_path)?.next().expect("item exists")?;
665 let file_path = file.path();
667 file_path.file_name().expect("Should be safe because paths in archives should not end with '..'");
668 let correct_path = output_dir.join(file_name);
669 // One case to handle tough is we need to check if a file with the same name already exists
670 if !utils::clear_path(&correct_path, question_policy)? {
671 return Ok(ControlFlow::Break(()));
673 fs::rename(&file_path, &correct_path)?;
676 "Successfully moved {} to {}.",
677 nice_directory_display(&file_path),
678 nice_directory_display(&correct_path)
681 // Multiple files in the root directory, so:
682 // Rename the temporary directory to the archive name, which is output_file_path
683 // One case to handle tough is we need to check if a file with the same name already exists
684 if !utils::clear_path(output_file_path, question_policy)? {
685 return Ok(ControlFlow::Break(()));
687 fs::rename(&temp_dir_path, &output_file_path)?;
690 "Successfully moved {} to {}.",
691 nice_directory_display(&temp_dir_path),
692 nice_directory_display(&output_file_path)
695 Ok(ControlFlow::Continue(files))
700 formats: &mut Vec<Vec<Extension>>,
701 question_policy: QuestionPolicy,
702 ) -> crate::Result<ControlFlow<()>> {
703 for (path, format) in files.iter().zip(formats.iter_mut()) {
704 if format.is_empty() {
705 // File with no extension
706 // Try to detect it automatically and prompt the user about it
707 if let Some(detected_format) = try_infer_extension(path) {
708 // Infering the file extension can have unpredicted consequences (e.g. the user just
709 // mistyped, ...) which we should always inform the user about.
710 info!(accessible, "Detected file: `{}` extension as `{}`", path.display(), detected_format);
711 if user_wants_to_continue_decompressing(path, question_policy)? {
712 format.push(detected_format);
714 return Ok(ControlFlow::Break(()));
717 } else if let Some(detected_format) = try_infer_extension(path) {
718 // File ending with extension
719 // Try to detect the extension and warn the user if it differs from the written one
720 let outer_ext = format.iter().next_back().unwrap();
721 if outer_ext != &detected_format {
723 "The file extension: `{}` differ from the detected extension: `{}`",
727 if !user_wants_to_continue_decompressing(path, question_policy)? {
728 return Ok(ControlFlow::Break(()));
732 // NOTE: If this actually produces no false positives, we can upgrade it in the future
733 // to a warning and ask the user if he wants to continue decompressing.
734 info!(accessible, "Could not detect the extension of `{}`", path.display());
737 Ok(ControlFlow::Continue(()))
740 fn clean_input_files_if_needed(files: &mut Vec<PathBuf>, output_path: &Path) {
742 while idx < files.len() {
743 if files[idx] == output_path {
744 warning!("The output file and the input file are the same: `{}`, skipping...", output_path.display());