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, *},
18 io::lock_and_flush_output_stdio,
20 logger::{info, info_accessible},
21 nice_directory_display, user_wants_to_continue,
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,
36 pub password: Option<&'a [u8]>,
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.
53 // This is the only case where we can read and unpack it directly, without having to do
54 // in-memory decompression/copying first.
56 // Any other Zip decompression done can take up the whole RAM and freeze ouch.
58 compression_formats: [Zip],
60 }] = options.formats.as_slice()
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))
68 Box::new(fs::File::open(options.input_file_path)?)
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),
74 &options.output_file_path,
75 options.question_policy,
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),
92 if !input_is_stdin && options.remove {
93 fs::remove_file(options.input_file_path)?;
95 "Removed input file {}",
96 nice_directory_display(options.input_file_path)
103 // Will be used in decoder chaining
104 let reader: Box<dyn Read> = if input_is_stdin {
105 Box::new(io::stdin())
107 Box::new(fs::File::open(options.input_file_path)?)
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!(),
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)?;
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)? {
139 None => return Ok(()),
142 io::copy(&mut reader, &mut writer)?;
147 if let ControlFlow::Continue(files) = smart_unpack(
148 |output_dir| crate::archive::tar::unpack_archive(reader, output_dir, options.quiet),
150 &options.output_file_path,
151 options.question_policy,
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,
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(
180 crate::archive::zip::unpack_archive(zip_archive, output_dir, options.password, options.quiet)
183 &options.output_file_path,
184 options.question_policy,
191 #[cfg(feature = "unrar")]
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)
201 Box::new(|output_dir| {
202 crate::archive::rar::unpack_archive(
203 options.input_file_path,
211 if let ControlFlow::Continue(files) = smart_unpack(
214 &options.output_file_path,
215 options.question_policy,
222 #[cfg(not(feature = "unrar"))]
224 return Err(crate::archive::rar_stub::no_support());
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,
242 let mut vec = vec![];
243 io::copy(&mut reader, &mut vec)?;
245 if let ControlFlow::Continue(files) = smart_unpack(
247 crate::archive::sevenz::decompress_sevenz(
248 io::Cursor::new(vec),
255 &options.output_file_path,
256 options.question_policy,
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)
273 info_accessible(format!("Files unpacked: {}", files_unpacked));
275 if !input_is_stdin && options.remove {
276 fs::remove_file(options.input_file_path)?;
278 "Removed input file {}",
279 nice_directory_display(options.input_file_path)
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
293 unpack_fn: impl FnOnce(&Path) -> crate::Result<usize>,
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)
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
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)
322 (temp_dir_path.to_owned(), output_file_path.to_owned())
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(()));
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),
338 Ok(ControlFlow::Continue(files))