fix(password): update password handling for archives
[ouch.git] / src / commands / mod.rs
blobbce640f19e7c5766c90d91b6baad2b93bc86ebfa
1 //! Receive command from the cli and call the respective function for that command.
3 mod compress;
4 mod decompress;
5 mod list;
7 use std::{ops::ControlFlow, path::PathBuf};
8 use std::os::unix::prelude::OsStrExt;
9 use rayon::prelude::{IndexedParallelIterator, IntoParallelRefIterator, ParallelIterator};
10 use utils::colors;
12 use crate::{
13     check,
14     cli::Subcommand,
15     commands::{compress::compress_files, decompress::decompress_file, list::list_archive_contents},
16     error::{Error, FinalError},
17     extension::{self, parse_format},
18     list::ListOptions,
19     utils::{
20         self, colors::*, is_path_stdin, logger::info_accessible, to_utf, EscapedPathDisplay, FileVisibilityPolicy,
21     },
22     CliArgs, QuestionPolicy,
25 /// Warn the user that (de)compressing this .zip archive might freeze their system.
26 fn warn_user_about_loading_zip_in_memory() {
27     const ZIP_IN_MEMORY_LIMITATION_WARNING: &str = "\n  \
28         The format '.zip' is limited by design and cannot be (de)compressed with encoding streams.\n  \
29         When chaining '.zip' with other formats, all (de)compression needs to be done in-memory\n  \
30         Careful, you might run out of RAM if the archive is too large!";
32     eprintln!("{}[WARNING]{}: {ZIP_IN_MEMORY_LIMITATION_WARNING}", *ORANGE, *RESET);
35 /// Warn the user that (de)compressing this .7z archive might freeze their system.
36 fn warn_user_about_loading_sevenz_in_memory() {
37     const SEVENZ_IN_MEMORY_LIMITATION_WARNING: &str = "\n  \
38         The format '.7z' is limited by design and cannot be (de)compressed with encoding streams.\n  \
39         When chaining '.7z' with other formats, all (de)compression needs to be done in-memory\n  \
40         Careful, you might run out of RAM if the archive is too large!";
42     eprintln!("{}[WARNING]{}: {SEVENZ_IN_MEMORY_LIMITATION_WARNING}", *ORANGE, *RESET);
45 /// This function checks what command needs to be run and performs A LOT of ahead-of-time checks
46 /// to assume everything is OK.
47 ///
48 /// There are a lot of custom errors to give enough error description and explanation.
49 pub fn run(
50     args: CliArgs,
51     question_policy: QuestionPolicy,
52     file_visibility_policy: FileVisibilityPolicy,
53 ) -> crate::Result<()> {
54     match args.cmd {
55         Subcommand::Compress {
56             files,
57             output: output_path,
58             level,
59             fast,
60             slow,
61         } => {
62             // After cleaning, if there are no input files left, exit
63             if files.is_empty() {
64                 return Err(FinalError::with_title("No files to compress").into());
65             }
67             // Formats from path extension, like "file.tar.gz.xz" -> vec![Tar, Gzip, Lzma]
68             let (formats_from_flag, formats) = match args.format {
69                 Some(formats) => {
70                     let parsed_formats = parse_format(&formats)?;
71                     (Some(formats), parsed_formats)
72                 }
73                 None => (None, extension::extensions_from_path(&output_path)),
74             };
76             check::check_invalid_compression_with_non_archive_format(
77                 &formats,
78                 &output_path,
79                 &files,
80                 formats_from_flag.as_ref(),
81             )?;
82             check::check_archive_formats_position(&formats, &output_path)?;
84             let output_file = match utils::ask_to_create_file(&output_path, question_policy)? {
85                 Some(writer) => writer,
86                 None => return Ok(()),
87             };
89             let level = if fast {
90                 Some(1) // Lowest level of compression
91             } else if slow {
92                 Some(i16::MAX) // Highest level of compression
93             } else {
94                 level
95             };
97             let compress_result = compress_files(
98                 files,
99                 formats,
100                 output_file,
101                 &output_path,
102                 args.quiet,
103                 question_policy,
104                 file_visibility_policy,
105                 level,
106             );
108             if let Ok(true) = compress_result {
109                 // this is only printed once, so it doesn't result in much text. On the other hand,
110                 // having a final status message is important especially in an accessibility context
111                 // as screen readers may not read a commands exit code, making it hard to reason
112                 // about whether the command succeeded without such a message
113                 info_accessible(format!("Successfully compressed '{}'.", to_utf(&output_path)));
114             } else {
115                 // If Ok(false) or Err() occurred, delete incomplete file at `output_path`
116                 //
117                 // if deleting fails, print an extra alert message pointing
118                 // out that we left a possibly CORRUPTED file at `output_path`
119                 if utils::remove_file_or_dir(&output_path).is_err() {
120                     eprintln!("{red}FATAL ERROR:\n", red = *colors::RED);
121                     eprintln!(
122                         "  Ouch failed to delete the file '{}'.",
123                         EscapedPathDisplay::new(&output_path)
124                     );
125                     eprintln!("  Please delete it manually.");
126                     eprintln!("  This file is corrupted if compression didn't finished.");
128                     if compress_result.is_err() {
129                         eprintln!("  Compression failed for reasons below.");
130                     }
131                 }
132             }
134             compress_result.map(|_| ())
135         }
136         Subcommand::Decompress { files, output_dir } => {
137             let mut output_paths = vec![];
138             let mut formats = vec![];
140             if let Some(format) = args.format {
141                 let format = parse_format(&format)?;
142                 for path in files.iter() {
143                     let file_name = path.file_name().ok_or_else(|| Error::NotFound {
144                         error_title: format!("{} does not have a file name", EscapedPathDisplay::new(path)),
145                     })?;
146                     output_paths.push(file_name.as_ref());
147                     formats.push(format.clone());
148                 }
149             } else {
150                 for path in files.iter() {
151                     let (pathbase, mut file_formats) = extension::separate_known_extensions_from_name(path);
153                     if let ControlFlow::Break(_) = check::check_mime_type(path, &mut file_formats, question_policy)? {
154                         return Ok(());
155                     }
157                     output_paths.push(pathbase);
158                     formats.push(file_formats);
159                 }
160             }
162             check::check_missing_formats_when_decompressing(&files, &formats)?;
164             // The directory that will contain the output files
165             // We default to the current directory if the user didn't specify an output directory with --dir
166             let output_dir = if let Some(dir) = output_dir {
167                 utils::create_dir_if_non_existent(&dir)?;
168                 dir
169             } else {
170                 PathBuf::from(".")
171             };
173             files
174                 .par_iter()
175                 .zip(formats)
176                 .zip(output_paths)
177                 .try_for_each(|((input_path, formats), file_name)| {
178                     // Path used by single file format archives
179                     let output_file_path = if is_path_stdin(file_name) {
180                         output_dir.join("stdin-output")
181                     } else {
182                         output_dir.join(file_name)
183                     };
184                     decompress_file(
185                         input_path,
186                         formats,
187                         &output_dir,
188                         output_file_path,
189                         question_policy,
190                         args.quiet,
191                         args.password.as_deref().map(|str|str.as_bytes()),
192                     )
193                 })
194         }
195         Subcommand::List { archives: files, tree } => {
196             let mut formats = vec![];
198             if let Some(format) = args.format {
199                 let format = parse_format(&format)?;
200                 for _ in 0..files.len() {
201                     formats.push(format.clone());
202                 }
203             } else {
204                 for path in files.iter() {
205                     let mut file_formats = extension::extensions_from_path(path);
207                     if let ControlFlow::Break(_) = check::check_mime_type(path, &mut file_formats, question_policy)? {
208                         return Ok(());
209                     }
211                     formats.push(file_formats);
212                 }
213             }
215             // Ensure we were not told to list the content of a non-archive compressed file
216             check::check_for_non_archive_formats(&files, &formats)?;
218             let list_options = ListOptions { tree };
220             for (i, (archive_path, formats)) in files.iter().zip(formats).enumerate() {
221                 if i > 0 {
222                     println!();
223                 }
224                 let formats = extension::flatten_compression_formats(&formats);
225                 list_archive_contents(
226                     archive_path,
227                     formats,
228                     list_options,
229                     question_policy,
230                     args.password.as_deref().map(|str|str.as_bytes()),
231                 )?;
232             }
234             Ok(())
235         }
236     }