2 io::{self, BufReader, Read},
10 commands::{warn_user_about_loading_sevenz_in_memory, warn_user_about_loading_zip_in_memory},
12 split_first_compression_format,
13 CompressionFormat::{self, *},
17 self, io::lock_and_flush_output_stdio, is_path_stdin, logger::info_accessible, nice_directory_display,
18 user_wants_to_continue,
20 QuestionAction, QuestionPolicy, BUFFER_CAPACITY,
23 trait ReadSeek: Read + io::Seek {}
24 impl<T: Read + io::Seek> ReadSeek for T {}
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>,
36 output_file_path: PathBuf,
37 question_policy: QuestionPolicy,
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.
47 // This is the only case where we can read and unpack it directly, without having to do
48 // in-memory decompression/copying first.
50 // Any other Zip decompression done can take up the whole RAM and freeze ouch.
52 compression_formats: [Zip],
54 }] = formats.as_slice()
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))
62 Box::new(fs::File::open(input_file_path)?)
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),
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),
89 // Will be used in decoder chaining
90 let reader: Box<dyn Read> = if input_is_stdin {
93 Box::new(fs::File::open(input_file_path)?)
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!(),
113 let (first_extension, extensions) = split_first_compression_format(&formats);
115 for format in extensions.iter().rev() {
116 reader = chain_reader_decoder(format, reader)?;
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)? {
125 None => return Ok(()),
128 io::copy(&mut reader, &mut writer)?;
133 if let ControlFlow::Continue(files) = smart_unpack(
134 |output_dir| crate::archive::tar::unpack_archive(reader, output_dir, quiet),
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)? {
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),
171 #[cfg(feature = "unrar")]
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)
181 Box::new(|output_dir| crate::archive::rar::unpack_archive(input_file_path, output_dir, password, quiet))
184 if let ControlFlow::Continue(files) =
185 smart_unpack(unpack_fn, output_dir, &output_file_path, question_policy)?
192 #[cfg(not(feature = "unrar"))]
194 return Err(crate::archive::rar_stub::no_support());
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)? {
208 let mut vec = vec![];
209 io::copy(&mut reader, &mut vec)?;
211 if let ControlFlow::Continue(files) = smart_unpack(
213 crate::archive::sevenz::decompress_sevenz(io::Cursor::new(vec), output_dir, password, quiet)
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)
234 info_accessible(format!("Files unpacked: {}", files_unpacked));
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
246 unpack_fn: impl FnOnce(&Path) -> crate::Result<usize>,
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)
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
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(()));
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)
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(()));
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)
297 Ok(ControlFlow::Continue(files))