Report errors for non-UTF-8 entries in Zip and 7z
[ouch.git] / src / check.rs
blob9e1bdac37f8117aa76d8411e36bcc506d0679e5b
1 //! Checks for errors.
3 #![warn(missing_docs)]
5 use std::{
6     ffi::OsString,
7     ops::ControlFlow,
8     path::{Path, PathBuf},
9 };
11 use crate::{
12     error::FinalError,
13     extension::{build_archive_file_suggestion, Extension, PRETTY_SUPPORTED_ALIASES, PRETTY_SUPPORTED_EXTENSIONS},
14     info,
15     utils::{pretty_format_list_of_paths, try_infer_extension, user_wants_to_continue, EscapedPathDisplay},
16     warning, QuestionAction, QuestionPolicy, Result,
19 /// Check if the mime type matches the detected extensions.
20 ///
21 /// In case the file doesn't has any extensions, try to infer the format.
22 ///
23 /// TODO: maybe the name of this should be "magic numbers" or "file signature",
24 /// and not MIME.
25 pub fn check_mime_type(
26     path: &Path,
27     formats: &mut Vec<Extension>,
28     question_policy: QuestionPolicy,
29 ) -> Result<ControlFlow<()>> {
30     if formats.is_empty() {
31         // File with no extension
32         // Try to detect it automatically and prompt the user about it
33         if let Some(detected_format) = try_infer_extension(path) {
34             // Inferring the file extension can have unpredicted consequences (e.g. the user just
35             // mistyped, ...) which we should always inform the user about.
36             info!(
37                 accessible,
38                 "Detected file: `{}` extension as `{}`",
39                 path.display(),
40                 detected_format
41             );
42             if user_wants_to_continue(path, question_policy, QuestionAction::Decompression)? {
43                 formats.push(detected_format);
44             } else {
45                 return Ok(ControlFlow::Break(()));
46             }
47         }
48     } else if let Some(detected_format) = try_infer_extension(path) {
49         // File ending with extension
50         // Try to detect the extension and warn the user if it differs from the written one
52         let outer_ext = formats.iter().next_back().unwrap();
53         if !outer_ext
54             .compression_formats
55             .ends_with(detected_format.compression_formats)
56         {
57             warning!(
58                 "The file extension: `{}` differ from the detected extension: `{}`",
59                 outer_ext,
60                 detected_format
61             );
62             if !user_wants_to_continue(path, question_policy, QuestionAction::Decompression)? {
63                 return Ok(ControlFlow::Break(()));
64             }
65         }
66     } else {
67         // NOTE: If this actually produces no false positives, we can upgrade it in the future
68         // to a warning and ask the user if he wants to continue decompressing.
69         info!(
70             accessible,
71             "Failed to confirm the format of `{}` by sniffing the contents, file might be misnamed",
72             path.display()
73         );
74     }
75     Ok(ControlFlow::Continue(()))
78 /// In the context of listing archives, this function checks if `ouch` was told to list
79 /// the contents of a compressed file that is not an archive
80 pub fn check_for_non_archive_formats(files: &[PathBuf], formats: &[Vec<Extension>]) -> Result<()> {
81     let mut not_archives = files
82         .iter()
83         .zip(formats)
84         .filter(|(_, formats)| !formats.first().map(Extension::is_archive).unwrap_or(false))
85         .map(|(path, _)| path)
86         .peekable();
88     if not_archives.peek().is_some() {
89         let not_archives: Vec<_> = not_archives.collect();
90         let error = FinalError::with_title("Cannot list archive contents")
91             .detail("Only archives can have their contents listed")
92             .detail(format!(
93                 "Files are not archives: {}",
94                 pretty_format_list_of_paths(&not_archives)
95             ));
97         return Err(error.into());
98     }
100     Ok(())
103 /// Show error if archive format is not the first format in the chain.
104 pub fn check_archive_formats_position(formats: &[Extension], output_path: &Path) -> Result<()> {
105     if let Some(format) = formats.iter().skip(1).find(|format| format.is_archive()) {
106         let error = FinalError::with_title(format!(
107             "Cannot compress to '{}'.",
108             EscapedPathDisplay::new(output_path)
109         ))
110         .detail(format!("Found the format '{format}' in an incorrect position."))
111         .detail(format!(
112             "'{format}' can only be used at the start of the file extension."
113         ))
114         .hint(format!(
115             "If you wish to compress multiple files, start the extension with '{format}'."
116         ))
117         .hint(format!(
118             "Otherwise, remove the last '{}' from '{}'.",
119             format,
120             EscapedPathDisplay::new(output_path)
121         ));
123         return Err(error.into());
124     }
125     Ok(())
128 /// Check if all provided files have formats to decompress.
129 pub fn check_missing_formats_when_decompressing(files: &[PathBuf], formats: &[Vec<Extension>]) -> Result<()> {
130     let files_with_broken_extension: Vec<&PathBuf> = files
131         .iter()
132         .zip(formats)
133         .filter(|(_, format)| format.is_empty())
134         .map(|(input_path, _)| input_path)
135         .collect();
137     if files_with_broken_extension.is_empty() {
138         return Ok(());
139     }
141     let (files_with_unsupported_extensions, files_missing_extension): (Vec<&PathBuf>, Vec<&PathBuf>) =
142         files_with_broken_extension
143             .iter()
144             .partition(|path| path.extension().is_some());
146     let mut error = FinalError::with_title("Cannot decompress files");
148     if !files_with_unsupported_extensions.is_empty() {
149         error = error.detail(format!(
150             "Files with unsupported extensions: {}",
151             pretty_format_list_of_paths(&files_with_unsupported_extensions)
152         ));
153     }
155     if !files_missing_extension.is_empty() {
156         error = error.detail(format!(
157             "Files with missing extensions: {}",
158             pretty_format_list_of_paths(&files_missing_extension)
159         ));
160     }
162     error = error
163         .detail("Decompression formats are detected automatically from file extension")
164         .hint(format!("Supported extensions are: {}", PRETTY_SUPPORTED_EXTENSIONS))
165         .hint(format!("Supported aliases are: {}", PRETTY_SUPPORTED_ALIASES));
167     // If there's exactly one file, give a suggestion to use `--format`
168     if let &[path] = files_with_broken_extension.as_slice() {
169         error = error
170             .hint("")
171             .hint("Alternatively, you can pass an extension to the '--format' flag:")
172             .hint(format!(
173                 "  ouch decompress {} --format tar.gz",
174                 EscapedPathDisplay::new(path),
175             ));
176     }
178     Err(error.into())
181 /// Check if there is a first format when compressing, and returns it.
182 pub fn check_first_format_when_compressing<'a>(formats: &'a [Extension], output_path: &Path) -> Result<&'a Extension> {
183     formats.first().ok_or_else(|| {
184         let output_path = EscapedPathDisplay::new(output_path);
185         FinalError::with_title(format!("Cannot compress to '{output_path}'."))
186             .detail("You shall supply the compression format")
187             .hint("Try adding supported extensions (see --help):")
188             .hint(format!("  ouch compress <FILES>... {output_path}.tar.gz"))
189             .hint(format!("  ouch compress <FILES>... {output_path}.zip"))
190             .hint("")
191             .hint("Alternatively, you can overwrite this option by using the '--format' flag:")
192             .hint(format!("  ouch compress <FILES>... {output_path} --format tar.gz"))
193             .into()
194     })
197 /// Check if compression is invalid because an archive format is necessary.
199 /// Non-archive formats don't support multiple file compression or folder compression.
200 pub fn check_invalid_compression_with_non_archive_format(
201     formats: &[Extension],
202     output_path: &Path,
203     files: &[PathBuf],
204     formats_from_flag: Option<&OsString>,
205 ) -> Result<()> {
206     let first_format = check_first_format_when_compressing(formats, output_path)?;
208     let is_some_input_a_folder = files.iter().any(|path| path.is_dir());
209     let is_multiple_inputs = files.len() > 1;
211     // If format is archive, nothing to check
212     // If there's no folder or multiple inputs, non-archive formats can handle it
213     if first_format.is_archive() || !is_some_input_a_folder && !is_multiple_inputs {
214         return Ok(());
215     }
217     let first_detail_message = if is_multiple_inputs {
218         "You are trying to compress multiple files."
219     } else {
220         "You are trying to compress a folder."
221     };
223     let (from_hint, to_hint) = if let Some(formats) = formats_from_flag {
224         let formats = formats.to_string_lossy();
225         (
226             format!("From: --format {formats}"),
227             format!("To:   --format tar.{formats}"),
228         )
229     } else {
230         // This piece of code creates a suggestion for compressing multiple files
231         // It says:
232         // Change from file.bz.xz
233         // To          file.tar.bz.xz
234         let suggested_output_path = build_archive_file_suggestion(output_path, ".tar")
235             .expect("output path should contain a compression format");
237         (
238             format!("From: {}", EscapedPathDisplay::new(output_path)),
239             format!("To:   {suggested_output_path}"),
240         )
241     };
242     let output_path = EscapedPathDisplay::new(output_path);
244     let error = FinalError::with_title(format!("Cannot compress to '{output_path}'."))
245         .detail(first_detail_message)
246         .detail(format!(
247             "The compression format '{first_format}' does not accept multiple files.",
248         ))
249         .detail("Formats that bundle files into an archive are tar and zip.")
250         .hint(format!("Try inserting 'tar.' or 'zip.' before '{first_format}'."))
251         .hint(from_hint)
252         .hint(to_hint);
254     Err(error.into())