refactor: improve code formatting in `mod.rs` and `logger.rs`
[ouch.git] / src / commands / decompress.rs
blob1d0152a614924295f3252e6a0d58368c3b5c9c41
1 use std::{
2     io::{self, BufReader, Read},
3     ops::ControlFlow,
4     path::{Path, PathBuf},
5 };
7 use fs_err as fs;
9 use crate::{
10     commands::{warn_user_about_loading_sevenz_in_memory, warn_user_about_loading_zip_in_memory},
11     extension::{
12         split_first_compression_format,
13         CompressionFormat::{self, *},
14         Extension,
15     },
16     utils::{
17         self,
18         io::lock_and_flush_output_stdio,
19         is_path_stdin,
20         logger::{info, info_accessible},
21         nice_directory_display, user_wants_to_continue,
22     },
23     QuestionAction, QuestionPolicy, BUFFER_CAPACITY,
26 trait ReadSeek: Read + io::Seek {}
27 impl<T: Read + io::Seek> ReadSeek for T {}
29 pub struct DecompressOptions<'a> {
30     pub input_file_path: &'a Path,
31     pub formats: Vec<Extension>,
32     pub output_dir: &'a Path,
33     pub output_file_path: PathBuf,
34     pub question_policy: QuestionPolicy,
35     pub quiet: bool,
36     pub password: Option<&'a [u8]>,
37     pub remove: bool,
40 /// Decompress a file
41 ///
42 /// File at input_file_path is opened for reading, example: "archive.tar.gz"
43 /// formats contains each format necessary for decompression, example: [Gz, Tar] (in decompression order)
44 /// output_dir it's where the file will be decompressed to, this function assumes that the directory exists
45 /// output_file_path is only used when extracting single file formats, not archive formats like .tar or .zip
46 pub fn decompress_file(options: DecompressOptions) -> crate::Result<()> {
47     assert!(options.output_dir.exists());
48     let input_is_stdin = is_path_stdin(options.input_file_path);
50     // Zip archives are special, because they require io::Seek, so it requires it's logic separated
51     // from decoder chaining.
52     //
53     // This is the only case where we can read and unpack it directly, without having to do
54     // in-memory decompression/copying first.
55     //
56     // Any other Zip decompression done can take up the whole RAM and freeze ouch.
57     if let [Extension {
58         compression_formats: [Zip],
59         ..
60     }] = options.formats.as_slice()
61     {
62         let mut vec = vec![];
63         let reader: Box<dyn ReadSeek> = if input_is_stdin {
64             warn_user_about_loading_zip_in_memory();
65             io::copy(&mut io::stdin(), &mut vec)?;
66             Box::new(io::Cursor::new(vec))
67         } else {
68             Box::new(fs::File::open(options.input_file_path)?)
69         };
70         let zip_archive = zip::ZipArchive::new(reader)?;
71         let files_unpacked = if let ControlFlow::Continue(files) = smart_unpack(
72             |output_dir| crate::archive::zip::unpack_archive(zip_archive, output_dir, options.password, options.quiet),
73             options.output_dir,
74             &options.output_file_path,
75             options.question_policy,
76         )? {
77             files
78         } else {
79             return Ok(());
80         };
82         // this is only printed once, so it doesn't result in much text. On the other hand,
83         // having a final status message is important especially in an accessibility context
84         // as screen readers may not read a commands exit code, making it hard to reason
85         // about whether the command succeeded without such a message
86         info_accessible(format!(
87             "Successfully decompressed archive in {} ({} files)",
88             nice_directory_display(options.output_dir),
89             files_unpacked
90         ));
92         if !input_is_stdin && options.remove {
93             fs::remove_file(options.input_file_path)?;
94             info(format!(
95                 "Removed input file {}",
96                 nice_directory_display(options.input_file_path)
97             ));
98         }
100         return Ok(());
101     }
103     // Will be used in decoder chaining
104     let reader: Box<dyn Read> = if input_is_stdin {
105         Box::new(io::stdin())
106     } else {
107         Box::new(fs::File::open(options.input_file_path)?)
108     };
109     let reader = BufReader::with_capacity(BUFFER_CAPACITY, reader);
110     let mut reader: Box<dyn Read> = Box::new(reader);
112     // Grab previous decoder and wrap it inside of a new one
113     let chain_reader_decoder = |format: &CompressionFormat, decoder: Box<dyn Read>| -> crate::Result<Box<dyn Read>> {
114         let decoder: Box<dyn Read> = match format {
115             Gzip => Box::new(flate2::read::GzDecoder::new(decoder)),
116             Bzip => Box::new(bzip2::read::BzDecoder::new(decoder)),
117             Bzip3 => Box::new(bzip3::read::Bz3Decoder::new(decoder)?),
118             Lz4 => Box::new(lz4_flex::frame::FrameDecoder::new(decoder)),
119             Lzma => Box::new(xz2::read::XzDecoder::new(decoder)),
120             Snappy => Box::new(snap::read::FrameDecoder::new(decoder)),
121             Zstd => Box::new(zstd::stream::Decoder::new(decoder)?),
122             Tar | Zip | Rar | SevenZip => unreachable!(),
123         };
124         Ok(decoder)
125     };
127     let (first_extension, extensions) = split_first_compression_format(&options.formats);
129     for format in extensions.iter().rev() {
130         reader = chain_reader_decoder(format, reader)?;
131     }
133     let files_unpacked = match first_extension {
134         Gzip | Bzip | Bzip3 | Lz4 | Lzma | Snappy | Zstd => {
135             reader = chain_reader_decoder(&first_extension, reader)?;
137             let mut writer = match utils::ask_to_create_file(&options.output_file_path, options.question_policy)? {
138                 Some(file) => file,
139                 None => return Ok(()),
140             };
142             io::copy(&mut reader, &mut writer)?;
144             1
145         }
146         Tar => {
147             if let ControlFlow::Continue(files) = smart_unpack(
148                 |output_dir| crate::archive::tar::unpack_archive(reader, output_dir, options.quiet),
149                 options.output_dir,
150                 &options.output_file_path,
151                 options.question_policy,
152             )? {
153                 files
154             } else {
155                 return Ok(());
156             }
157         }
158         Zip => {
159             if options.formats.len() > 1 {
160                 // Locking necessary to guarantee that warning and question
161                 // messages stay adjacent
162                 let _locks = lock_and_flush_output_stdio();
164                 warn_user_about_loading_zip_in_memory();
165                 if !user_wants_to_continue(
166                     options.input_file_path,
167                     options.question_policy,
168                     QuestionAction::Decompression,
169                 )? {
170                     return Ok(());
171                 }
172             }
174             let mut vec = vec![];
175             io::copy(&mut reader, &mut vec)?;
176             let zip_archive = zip::ZipArchive::new(io::Cursor::new(vec))?;
178             if let ControlFlow::Continue(files) = smart_unpack(
179                 |output_dir| {
180                     crate::archive::zip::unpack_archive(zip_archive, output_dir, options.password, options.quiet)
181                 },
182                 options.output_dir,
183                 &options.output_file_path,
184                 options.question_policy,
185             )? {
186                 files
187             } else {
188                 return Ok(());
189             }
190         }
191         #[cfg(feature = "unrar")]
192         Rar => {
193             type UnpackResult = crate::Result<usize>;
194             let unpack_fn: Box<dyn FnOnce(&Path) -> UnpackResult> = if options.formats.len() > 1 || input_is_stdin {
195                 let mut temp_file = tempfile::NamedTempFile::new()?;
196                 io::copy(&mut reader, &mut temp_file)?;
197                 Box::new(move |output_dir| {
198                     crate::archive::rar::unpack_archive(temp_file.path(), output_dir, options.password, options.quiet)
199                 })
200             } else {
201                 Box::new(|output_dir| {
202                     crate::archive::rar::unpack_archive(
203                         options.input_file_path,
204                         output_dir,
205                         options.password,
206                         options.quiet,
207                     )
208                 })
209             };
211             if let ControlFlow::Continue(files) = smart_unpack(
212                 unpack_fn,
213                 options.output_dir,
214                 &options.output_file_path,
215                 options.question_policy,
216             )? {
217                 files
218             } else {
219                 return Ok(());
220             }
221         }
222         #[cfg(not(feature = "unrar"))]
223         Rar => {
224             return Err(crate::archive::rar_stub::no_support());
225         }
226         SevenZip => {
227             if options.formats.len() > 1 {
228                 // Locking necessary to guarantee that warning and question
229                 // messages stay adjacent
230                 let _locks = lock_and_flush_output_stdio();
232                 warn_user_about_loading_sevenz_in_memory();
233                 if !user_wants_to_continue(
234                     options.input_file_path,
235                     options.question_policy,
236                     QuestionAction::Decompression,
237                 )? {
238                     return Ok(());
239                 }
240             }
242             let mut vec = vec![];
243             io::copy(&mut reader, &mut vec)?;
245             if let ControlFlow::Continue(files) = smart_unpack(
246                 |output_dir| {
247                     crate::archive::sevenz::decompress_sevenz(
248                         io::Cursor::new(vec),
249                         output_dir,
250                         options.password,
251                         options.quiet,
252                     )
253                 },
254                 options.output_dir,
255                 &options.output_file_path,
256                 options.question_policy,
257             )? {
258                 files
259             } else {
260                 return Ok(());
261             }
262         }
263     };
265     // this is only printed once, so it doesn't result in much text. On the other hand,
266     // having a final status message is important especially in an accessibility context
267     // as screen readers may not read a commands exit code, making it hard to reason
268     // about whether the command succeeded without such a message
269     info_accessible(format!(
270         "Successfully decompressed archive in {}",
271         nice_directory_display(options.output_dir)
272     ));
273     info_accessible(format!("Files unpacked: {}", files_unpacked));
275     if !input_is_stdin && options.remove {
276         fs::remove_file(options.input_file_path)?;
277         info(format!(
278             "Removed input file {}",
279             nice_directory_display(options.input_file_path)
280         ));
281     }
283     Ok(())
286 /// Unpacks an archive with some heuristics
287 /// - If the archive contains only one file, it will be extracted to the `output_dir`
288 /// - If the archive contains multiple files, it will be extracted to a subdirectory of the
289 ///   output_dir named after the archive (given by `output_file_path`)
291 /// Note: This functions assumes that `output_dir` exists
292 fn smart_unpack(
293     unpack_fn: impl FnOnce(&Path) -> crate::Result<usize>,
294     output_dir: &Path,
295     output_file_path: &Path,
296     question_policy: QuestionPolicy,
297 ) -> crate::Result<ControlFlow<(), usize>> {
298     assert!(output_dir.exists());
299     let temp_dir = tempfile::Builder::new().prefix(".tmp-ouch-").tempdir_in(output_dir)?;
300     let temp_dir_path = temp_dir.path();
302     info_accessible(format!(
303         "Created temporary directory {} to hold decompressed elements",
304         nice_directory_display(temp_dir_path)
305     ));
307     let files = unpack_fn(temp_dir_path)?;
309     let root_contains_only_one_element = fs::read_dir(temp_dir_path)?.count() == 1;
311     let (previous_path, new_path) = if root_contains_only_one_element {
312         // Only one file in the root directory, so we can just move it to the output directory
313         let file = fs::read_dir(temp_dir_path)?.next().expect("item exists")?;
314         let file_path = file.path();
315         let file_name = file_path
316             .file_name()
317             .expect("Should be safe because paths in archives should not end with '..'");
318         let correct_path = output_dir.join(file_name);
320         (file_path, correct_path)
321     } else {
322         (temp_dir_path.to_owned(), output_file_path.to_owned())
323     };
325     // Before moving, need to check if a file with the same name already exists
326     if !utils::clear_path(&new_path, question_policy)? {
327         return Ok(ControlFlow::Break(()));
328     }
330     // Rename the temporary directory to the archive name, which is output_file_path
331     fs::rename(&previous_path, &new_path)?;
332     info_accessible(format!(
333         "Successfully moved \"{}\" to \"{}\"",
334         nice_directory_display(&previous_path),
335         nice_directory_display(&new_path),
336     ));
338     Ok(ControlFlow::Continue(files))