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