From 4921d3d3d22429ae7ebff7bd7913195c796dc98b Mon Sep 17 00:00:00 2001 From: Spyros Roum Date: Mon, 1 Nov 2021 01:09:37 +0200 Subject: [PATCH] Add `Extension` wrapper that lets us keep tgz instead of forcing tar.gz --- src/commands.rs | 54 ++++++++++++++++++++++++++---------------- src/extension.rs | 61 +++++++++++++++++++++++++++++++++++++----------- 2 files changed, 81 insertions(+), 34 deletions(-) diff --git a/src/commands.rs b/src/commands.rs index ba4e0b8..28872cf 100644 --- a/src/commands.rs +++ b/src/commands.rs @@ -17,6 +17,7 @@ use crate::{ extension::{ self, CompressionFormat::{self, *}, + Extension, }, info, utils::nice_directory_display, @@ -56,9 +57,7 @@ pub fn run(args: Opts, skip_questions_positively: Option) -> crate::Result return Err(Error::with_reason(reason)); } - if !formats.get(0).map(CompressionFormat::is_archive_format).unwrap_or(false) - && represents_several_files(&files) - { + if !formats.get(0).map(Extension::is_archive).unwrap_or(false) && represents_several_files(&files) { // This piece of code creates a suggestion for compressing multiple files // It says: // Change from file.bz.xz @@ -86,7 +85,7 @@ pub fn run(args: Opts, skip_questions_positively: Option) -> crate::Result return Err(Error::with_reason(reason)); } - if let Some(format) = formats.iter().skip(1).find(|format| format.is_archive_format()) { + if let Some(format) = formats.iter().skip(1).find(|format| format.is_archive()) { let reason = FinalError::with_title(format!("Cannot compress to '{}'.", to_utf(&output_path))) .detail(format!("Found the format '{}' in an incorrect position.", format)) .detail(format!("'{}' can only be used at the start of the file extension.", format)) @@ -108,12 +107,28 @@ pub fn run(args: Opts, skip_questions_positively: Option) -> crate::Result // `ouch compress file.tar.gz file.tar.gz.xz` should produce `file.tar.gz.xz` and not `file.tar.gz.tar.gz.xz` let input_extensions = extension::extensions_from_path(&files[0]); + // We calculate the formats that are left if we filter out a sublist at the start of what we have that's the same as the input formats + let mut new_formats = Vec::with_capacity(formats.len()); + for (inp_ext, out_ext) in input_extensions.iter().zip(&formats) { + if inp_ext.compression_formats == out_ext.compression_formats { + new_formats.push(out_ext.clone()); + } else if inp_ext + .compression_formats + .iter() + .zip(&out_ext.compression_formats) + .all(|(inp, out)| inp == out) + { + let new_ext = Extension::new( + &out_ext.compression_formats[..inp_ext.compression_formats.len()], + &out_ext.display_text, + ); + new_formats.push(new_ext); + break; + } + } // If the input is a sublist at the start of `formats` then remove the extensions - // Note: If input_extensions is empty this counts as true - if !input_extensions.is_empty() - && input_extensions.len() < formats.len() - && input_extensions.iter().zip(&formats).all(|(inp, out)| inp == out) - { + // Note: If input_extensions is empty then it will make `formats` empty too, which we don't want + if !input_extensions.is_empty() && new_formats != formats { // Safety: // We checked above that input_extensions isn't empty, so files[0] has a extension. // @@ -124,8 +139,7 @@ pub fn run(args: Opts, skip_questions_positively: Option) -> crate::Result to_utf(files[0].as_path().file_name().unwrap()), to_utf(&output_path) ); - let drain_iter = formats.drain(..input_extensions.len()); - drop(drain_iter); // Remove the extensions from `formats` + formats = new_formats; } } let compress_result = compress_files(files, formats, output_file); @@ -185,7 +199,7 @@ pub fn run(args: Opts, skip_questions_positively: Option) -> crate::Result Ok(()) } -fn compress_files(files: Vec, formats: Vec, output_file: fs::File) -> crate::Result<()> { +fn compress_files(files: Vec, formats: Vec, output_file: fs::File) -> crate::Result<()> { let file_writer = BufWriter::with_capacity(BUFFER_CAPACITY, output_file); let mut writer: Box = Box::new(file_writer); @@ -208,13 +222,13 @@ fn compress_files(files: Vec, formats: Vec, output_f encoder }; - for format in formats.iter().skip(1).rev() { + for format in formats.iter().flat_map(Extension::iter).skip(1).collect::>().iter().rev() { writer = chain_writer_encoder(format, writer); } - match formats[0] { + match formats[0].compression_formats[0] { Gzip | Bzip | Lzma | Zstd => { - writer = chain_writer_encoder(&formats[0], writer); + writer = chain_writer_encoder(&formats[0].compression_formats[0], writer); let mut reader = fs::File::open(&files[0]).unwrap(); io::copy(&mut reader, &mut writer)?; } @@ -248,7 +262,7 @@ fn compress_files(files: Vec, formats: Vec, output_f // file_name is only used when extracting single file formats, no archive formats like .tar or .zip fn decompress_file( input_file_path: &Path, - formats: Vec, + formats: Vec, output_folder: Option<&Path>, file_name: &Path, skip_questions_positively: Option, @@ -270,7 +284,7 @@ fn decompress_file( // in-memory decompression/copying first. // // Any other Zip decompression done can take up the whole RAM and freeze ouch. - if let [Zip] = *formats.as_slice() { + if formats.len() == 1 && *formats[0].compression_formats.as_slice() == [Zip] { utils::create_dir_if_non_existent(output_folder)?; let zip_archive = zip::ZipArchive::new(reader)?; let _files = crate::archive::zip::unpack_archive(zip_archive, output_folder, skip_questions_positively)?; @@ -294,15 +308,15 @@ fn decompress_file( Ok(decoder) }; - for format in formats.iter().skip(1).rev() { + for format in formats.iter().flat_map(Extension::iter).skip(1).collect::>().iter().rev() { reader = chain_reader_decoder(format, reader)?; } utils::create_dir_if_non_existent(output_folder)?; - match formats[0] { + match formats[0].compression_formats[0] { Gzip | Bzip | Lzma | Zstd => { - reader = chain_reader_decoder(&formats[0], reader)?; + reader = chain_reader_decoder(&formats[0].compression_formats[0], reader)?; // TODO: improve error treatment let mut writer = fs::File::create(&output_path)?; diff --git a/src/extension.rs b/src/extension.rs index 54ba75e..0ac1ec1 100644 --- a/src/extension.rs +++ b/src/extension.rs @@ -4,7 +4,40 @@ use std::{ffi::OsStr, fmt, path::Path}; use self::CompressionFormat::*; -#[derive(Clone, PartialEq, Eq, Debug)] +/// A wrapper around `CompressionFormat` that allows combinations like `tgz` +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct Extension { + pub compression_formats: Vec, + pub display_text: String, +} + +impl Extension { + /// # Panics: + /// Will panic if `formats` is empty + pub fn new(formats: impl Into>, text: impl Into) -> Self { + let formats = formats.into(); + assert!(!formats.is_empty()); + Self { compression_formats: formats, display_text: text.into() } + } + + /// Checks if the first format in `compression_formats` is an archive + pub fn is_archive(&self) -> bool { + // Safety: we check that `compression_formats` is not empty in `Self::new` + self.compression_formats[0].is_archive_format() + } + + pub fn iter(&self) -> impl Iterator { + self.compression_formats.iter() + } +} + +impl fmt::Display for Extension { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.write_str(&self.display_text) + } +} + +#[derive(Copy, Clone, PartialEq, Eq, Debug)] /// Accepted extensions for input and output pub enum CompressionFormat { Gzip, // .gz @@ -45,7 +78,7 @@ impl fmt::Display for CompressionFormat { } } -pub fn separate_known_extensions_from_name(mut path: &Path) -> (&Path, Vec) { +pub fn separate_known_extensions_from_name(mut path: &Path) -> (&Path, Vec) { // // TODO: check for file names with the name of an extension // // TODO2: warn the user that currently .tar.gz is a .gz file named .tar // @@ -58,17 +91,17 @@ pub fn separate_known_extensions_from_name(mut path: &Path) -> (&Path, Vec vec![Tar], - "tgz" => vec![Gzip, Tar], - "tbz" | "tbz2" => vec![Bzip, Tar], - "txz" | "tlz" | "tlzma" => vec![Lzma, Tar], - "tzst" => vec![Zstd, Tar], - "zip" => vec![Zip], - "bz" | "bz2" => vec![Bzip], - "gz" => vec![Gzip], - "xz" | "lzma" | "lz" => vec![Lzma], - "zst" => vec![Zstd], + extensions.push(match extension { + "tar" => Extension::new([Tar], extension), + "tgz" => Extension::new([Tar, Gzip], extension), + "tbz" | "tbz2" => Extension::new([Tar, Bzip], extension), + "txz" | "tlz" | "tlzma" => Extension::new([Tar, Lzma], extension), + "tzst" => Extension::new([Tar, Zstd], ".tzst"), + "zip" => Extension::new([Zip], extension), + "bz" | "bz2" => Extension::new([Bzip], extension), + "gz" => Extension::new([Gzip], extension), + "xz" | "lzma" | "lz" => Extension::new([Lzma], extension), + "zst" => Extension::new([Zstd], extension), _ => break, }); @@ -81,7 +114,7 @@ pub fn separate_known_extensions_from_name(mut path: &Path) -> (&Path, Vec Vec { +pub fn extensions_from_path(path: &Path) -> Vec { let (_, extensions) = separate_known_extensions_from_name(path); extensions }