cmprss 0.4.0

A compression multi-tool for the command line.
use super::stream::{copy_stream, guard_file_output, open_input, prepare_output};
use crate::progress::ProgressArgs;
use crate::utils::{
    CmprssInput, CmprssOutput, CommonArgs, CompressionLevelValidator, Compressor,
    DefaultCompressionValidator, LevelArgs, Result,
};
use clap::Args;
use flate2::write::GzEncoder;
use flate2::{Compression, read::GzDecoder};

#[derive(Args, Debug)]
pub struct GzipArgs {
    #[clap(flatten)]
    pub common_args: CommonArgs,

    #[clap(flatten)]
    pub level_args: LevelArgs,

    #[clap(flatten)]
    pub progress_args: ProgressArgs,
}

#[derive(Clone)]
pub struct Gzip {
    pub compression_level: i32,
    pub progress_args: ProgressArgs,
}

impl Default for Gzip {
    fn default() -> Self {
        let validator = DefaultCompressionValidator;
        Gzip {
            compression_level: validator.default_level(),
            progress_args: ProgressArgs::default(),
        }
    }
}

impl Gzip {
    pub fn new(args: &GzipArgs) -> Gzip {
        Gzip {
            compression_level: args.level_args.resolve(&DefaultCompressionValidator),
            progress_args: args.progress_args,
        }
    }
}

impl Compressor for Gzip {
    /// The standard extension for the gzip format.
    fn extension(&self) -> &str {
        "gz"
    }

    /// Full name for gzip.
    fn name(&self) -> &str {
        "gzip"
    }

    /// Compress an input file or pipe to a gzip archive
    fn compress(&self, input: CmprssInput, output: CmprssOutput) -> Result {
        guard_file_output(&output, "Gzip")?;
        let (input_stream, file_size, pipeline_inner) = open_input(input, "Gzip")?;
        let (writer, target) = prepare_output(output)?;
        let mut encoder = GzEncoder::new(writer, Compression::new(self.compression_level as u32));
        copy_stream(
            input_stream,
            &mut encoder,
            file_size,
            pipeline_inner,
            &self.progress_args,
            target,
        )?;
        encoder.finish()?;
        Ok(())
    }

    /// Extract a gzip archive
    fn extract(&self, input: CmprssInput, output: CmprssOutput) -> Result {
        guard_file_output(&output, "Gzip")?;
        let (input_stream, file_size, pipeline_inner) = open_input(input, "Gzip")?;
        let decoder = GzDecoder::new(input_stream);
        let (writer, target) = prepare_output(output)?;
        copy_stream(
            decoder,
            writer,
            file_size,
            pipeline_inner,
            &self.progress_args,
            target,
        )?;
        Ok(())
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::test_utils::*;
    use std::fs;
    use std::io::{Read, Write};
    use tempfile::tempdir;

    /// Test the basic interface of the Gzip compressor
    #[test]
    fn test_gzip_interface() {
        let compressor = Gzip::default();
        test_compressor_interface(&compressor, "gzip", Some("gz"));
    }

    /// Test the default compression level
    #[test]
    fn test_gzip_default_compression() -> Result {
        let compressor = Gzip::default();
        test_compression(&compressor)
    }

    /// Test fast compression level
    #[test]
    fn test_gzip_fast_compression() -> Result {
        let fast_compressor = Gzip {
            compression_level: 1,
            progress_args: ProgressArgs::default(),
        };
        test_compression(&fast_compressor)
    }

    /// Test best compression level
    #[test]
    fn test_gzip_best_compression() -> Result {
        let best_compressor = Gzip {
            compression_level: 9,
            progress_args: ProgressArgs::default(),
        };
        test_compression(&best_compressor)
    }

    /// Test for gzip-specific behavior: handling of concatenated gzip archives
    #[test]
    fn test_concatenated_gzip() -> Result {
        let compressor = Gzip::default();
        let temp_dir = tempdir().expect("Failed to create temp dir");

        // Create two test files
        let input_path1 = temp_dir.path().join("input1.txt");
        let input_path2 = temp_dir.path().join("input2.txt");
        let test_data1 = "This is the first file";
        let test_data2 = "This is the second file";
        fs::write(&input_path1, test_data1)?;
        fs::write(&input_path2, test_data2)?;

        // Compress each file separately
        let archive_path1 = temp_dir.path().join("archive1.gz");
        let archive_path2 = temp_dir.path().join("archive2.gz");

        compressor.compress(
            CmprssInput::Path(vec![input_path1.clone()]),
            CmprssOutput::Path(archive_path1.clone()),
        )?;

        compressor.compress(
            CmprssInput::Path(vec![input_path2.clone()]),
            CmprssOutput::Path(archive_path2.clone()),
        )?;

        // Create a concatenated archive
        let concat_archive = temp_dir.path().join("concat.gz");
        let mut concat_file = fs::File::create(&concat_archive)?;

        // Concat the two gzip files
        let mut archive1_data = Vec::new();
        let mut archive2_data = Vec::new();
        fs::File::open(&archive_path1)?.read_to_end(&mut archive1_data)?;
        fs::File::open(&archive_path2)?.read_to_end(&mut archive2_data)?;

        concat_file.write_all(&archive1_data)?;
        concat_file.write_all(&archive2_data)?;
        concat_file.flush()?;

        // Extract the concatenated archive - this should yield the first file's contents
        let output_path = temp_dir.path().join("output.txt");

        compressor.extract(
            CmprssInput::Path(vec![concat_archive]),
            CmprssOutput::Path(output_path.clone()),
        )?;

        // Verify the result is the first file's content
        let output_data = fs::read_to_string(output_path)?;
        assert_eq!(output_data, test_data1);

        Ok(())
    }
}