diff --git a/src/cli/args.rs b/src/cli/args.rs index a200dc6..1de90be 100644 --- a/src/cli/args.rs +++ b/src/cli/args.rs @@ -59,6 +59,10 @@ pub enum Subcommand { /// The resulting file. Its extensions can be used to specify the compression formats #[arg(required = true, value_hint = ValueHint::FilePath)] output: PathBuf, + + /// Compression level, applied to all formats + #[arg(short, long)] + level: Option, }, /// Decompresses one or more files, optionally into another folder #[command(visible_alias = "d")] diff --git a/src/commands/compress.rs b/src/commands/compress.rs index fb69a75..a2a5018 100644 --- a/src/commands/compress.rs +++ b/src/commands/compress.rs @@ -23,6 +23,7 @@ use crate::{ /// # Return value /// - Returns `Ok(true)` if compressed all files normally. /// - Returns `Ok(false)` if user opted to abort compression mid-way. +#[allow(clippy::too_many_arguments)] pub fn compress_files( files: Vec, extensions: Vec, @@ -31,6 +32,7 @@ pub fn compress_files( quiet: bool, question_policy: QuestionPolicy, file_visibility_policy: FileVisibilityPolicy, + level: Option, ) -> crate::Result { // If the input files contain a directory, then the total size will be underestimated let file_writer = BufWriter::with_capacity(BUFFER_CAPACITY, output_file); @@ -44,18 +46,42 @@ pub fn compress_files( // by default, ParCompress uses a default compression level of 3 // instead of the regular default that flate2 uses gzp::par::compress::ParCompress::::builder() - .compression_level(Default::default()) + .compression_level( + level.map_or_else(Default::default, |l| gzp::Compression::new((l as u32).clamp(0, 9))), + ) + .from_writer(encoder), + ), + Bzip => Box::new(bzip2::write::BzEncoder::new( + encoder, + level.map_or_else(Default::default, |l| bzip2::Compression::new((l as u32).clamp(1, 9))), + )), + Lz4 => Box::new(lzzzz::lz4f::WriteCompressor::new( + encoder, + lzzzz::lz4f::PreferencesBuilder::new() + .compression_level(level.map_or(1, |l| (l as i32).clamp(1, lzzzz::lz4f::CLEVEL_MAX))) + .build(), + )?), + Lzma => Box::new(xz2::write::XzEncoder::new( + encoder, + level.map_or(6, |l| (l as u32).clamp(0, 9)), + )), + Snappy => Box::new( + gzp::par::compress::ParCompress::::builder() + .compression_level(gzp::par::compress::Compression::new( + level.map_or_else(Default::default, |l| (l as u32).clamp(0, 9)), + )) .from_writer(encoder), ), - Bzip => Box::new(bzip2::write::BzEncoder::new(encoder, Default::default())), - Lz4 => Box::new(lzzzz::lz4f::WriteCompressor::new(encoder, Default::default())?), - Lzma => Box::new(xz2::write::XzEncoder::new(encoder, 6)), - Snappy => Box::new(gzp::par::compress::ParCompress::::builder().from_writer(encoder)), Zstd => { - let zstd_encoder = zstd::stream::write::Encoder::new(encoder, Default::default()); + let zstd_encoder = zstd::stream::write::Encoder::new( + encoder, + level.map_or(zstd::DEFAULT_COMPRESSION_LEVEL, |l| { + (l as i32).clamp(zstd::zstd_safe::min_c_level(), zstd::zstd_safe::max_c_level()) + }), + ); // Safety: - // Encoder::new() can only fail if `level` is invalid, but Default::default() - // is guaranteed to be valid + // Encoder::new() can only fail if `level` is invalid, but the level + // is `clamp`ed and therefore guaranteed to be valid Box::new(zstd_encoder.unwrap().auto_finish()) } Tar | Zip => unreachable!(), diff --git a/src/commands/mod.rs b/src/commands/mod.rs index a96983d..a4a3c82 100644 --- a/src/commands/mod.rs +++ b/src/commands/mod.rs @@ -44,6 +44,7 @@ pub fn run( Subcommand::Compress { files, output: output_path, + level, } => { // After cleaning, if there are no input files left, exit if files.is_empty() { @@ -80,6 +81,7 @@ pub fn run( args.quiet, question_policy, file_visibility_policy, + level, ); if let Ok(true) = compress_result { diff --git a/tests/integration.rs b/tests/integration.rs index eec7a29..5242cff 100644 --- a/tests/integration.rs +++ b/tests/integration.rs @@ -100,7 +100,11 @@ fn single_empty_file(ext: Extension, #[any(size_range(0..8).lift())] exts: Vec) { +fn single_file( + ext: Extension, + #[any(size_range(0..8).lift())] exts: Vec, + #[strategy(proptest::option::of(0i16..30))] level: Option, +) { let dir = tempdir().unwrap(); let dir = dir.path(); let before = &dir.join("before"); @@ -109,7 +113,11 @@ fn single_file(ext: Extension, #[any(size_range(0..8).lift())] exts: Vec