History

feat!: make zlib compression levels configurable

Introduce a validated Compression type and require callers to choose a level when creating deflate streams. Optional serde support allows options in dependent crates to retain their serialization APIs.

Amey Pawar committed 4 months ago

Commit 6936e851 parent 9884f486 files · +82 −26Browse files

Unified · Split

6 files
  1. Cargo.lock +2 −0
  2. gix-zlib/Cargo.toml +4 −0
  3. gix-zlib/src/lib.rs +34 −0
  4. gix-zlib/src/stream/deflate.rs +14 −17
  5. gix-zlib/tests/zlib/stream/deflate.rs +27 −8
  6. gix-zlib/tests/zlib/stream/inflate.rs +1 −1
Cargo.lock+2−0
@@ -1572,6 +1572,7 @@ dependencies = [ · 1571 unmodified lines
15721572 "gix-worktree",
15731573 "gix-worktree-state",
15741574 "gix-worktree-stream",
1575+ "gix-zlib",
15751576 "insta",
15761577 "is_ci",
15771578 "nonempty",
@@ -2675,6 +2676,7 @@ name = "gix-zlib" · 1097 unmodified lines
26752676 version = "0.1.0"
26762677 dependencies = [
26772678 "bstr",
2679+ "serde",
26782680 "thiserror 2.0.18",
26792681 "zlib-rs",
26802682 ]
gix-zlib/Cargo.toml+4−0
@@ -15,9 +15,13 @@ include = ["/src/**/*", "/LICENSE-*"] · 14 unmodified lines
1515 doctest = false
1616 test = false
1717
18+[features]
19+serde = ["dep:serde"]
20+
1821 [dependencies]
1922 zlib-rs = "0.6.2"
2023 thiserror = "2.0.18"
24+serde = { version = "1.0.114", optional = true, default-features = false, features = ["derive"] }
2125
2226 [dev-dependencies]
2327 bstr = { version = "1.12.0", default-features = false }
gix-zlib/src/lib.rs+34−0
@@ -1,6 +1,40 @@
11 #![deny(missing_docs)]
22 //! Streaming compression and decompression utilities used by gitoxide.
33
4+/// The compression level to use for zlib-based streams, in the range from 0 (no compression)
5+/// to 9 (best compression, slowest).
6+///
7+/// Note that `git` maps its configured level of `-1` to the zlib default, which is level 6
8+/// and available as [`Compression::DEFAULT`].
9+#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
10+#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
11+pub struct Compression(i32);
12+
13+impl Compression {
14+ /// Do not compress at all, while still producing a valid zlib stream.
15+ pub const NONE: Compression = Compression(0);
16+ /// The fastest compression, with the lowest compression ratio, also known as level 1.
17+ ///
18+ /// This is what `git` uses for loose objects unless configured otherwise with `core.looseCompression`.
19+ pub const BEST_SPEED: Compression = Compression(1);
20+ /// The default compromise between speed and compression ratio, also known as level 6.
21+ ///
22+ /// This is what `git` uses when writing packs unless configured otherwise with `pack.compression`.
23+ pub const DEFAULT: Compression = Compression(6);
24+ /// The best compression ratio at the expense of speed, also known as level 9.
25+ pub const BEST: Compression = Compression(9);
26+
27+ /// Create a new instance from `level` if it is within the valid range from 0 to 9, inclusive.
28+ pub fn new(level: i32) -> Option<Self> {
29+ (0..=9).contains(&level).then_some(Compression(level))
30+ }
31+
32+ /// Return the compression level as integer in the range from 0 to 9, inclusive.
33+ pub fn level(&self) -> i32 {
34+ self.0
35+ }
36+}
37+
438 /// A type to hold all state needed for decompressing a ZLIB encoded stream.
539 pub struct Decompress(zlib_rs::Inflate);
640
gix-zlib/src/stream/deflate.rs+14−17
@@ -1,6 +1,6 @@
11 //! Compression state and a [`std::io::Write`] adapter for producing zlib streams.
22
3−use crate::Status;
3+use crate::{Compression, Status};
44 use zlib_rs::DeflateError;
55
66 const BUF_SIZE: usize = 4096 * 8;
@@ -10,6 +10,7 @@ const BUF_SIZE: usize = 4096 * 8; · 3 unmodified lines
1010 /// Be sure to call `flush()` when done to finalize the deflate stream.
1111 pub struct Write<W> {
1212 compressor: Compress,
13+ compression: Compression,
1314 inner: W,
1415 buf: [u8; BUF_SIZE],
1516 }
@@ -20,7 +21,8 @@ where · 4 unmodified lines
2021 {
2122 fn clone(&self) -> Self {
2223 Write {
23− compressor: impls::new_compress(),
24+ compressor: impls::new_compress(self.compression),
25+ compression: self.compression,
2426 inner: self.inner.clone(),
2527 buf: self.buf,
2628 }
@@ -30,12 +32,6 @@ where · 3 unmodified lines
3032 /// Hold all state needed for compressing data.
3133 pub struct Compress(zlib_rs::Deflate);
3234
33−impl Default for Compress {
34− fn default() -> Self {
35− Self::new()
36− }
37−}
38−
3935 impl Compress {
4036 /// The number of bytes that were read from the input.
4137 pub fn total_in(&self) -> u64 {
@@ -47,9 +43,9 @@ impl Compress { · 5 unmodified lines
4743 self.0.total_out()
4844 }
4945
50− /// Create a new instance - this allocates so should be done with care.
51− pub fn new() -> Self {
52− let config = zlib_rs::DeflateConfig::best_speed();
46+ /// Create a new instance compressing with `level` - this allocates so should be done with care.
47+ pub fn new(level: Compression) -> Self {
48+ let config = zlib_rs::DeflateConfig::new(level.level());
5349 let header = true;
5450 let inner = zlib_rs::Deflate::new(config.level, header, config.window_bits as u8);
5551 Self(inner)
@@ -148,21 +144,22 @@ pub enum FlushCompress { · 92 unmodified lines
148144 mod impls {
149145 use std::io;
150146
151− use crate::Status;
152147 use crate::stream::deflate::{self, Compress, FlushCompress};
148+ use crate::{Compression, Status};
153149
154− pub(crate) fn new_compress() -> Compress {
155− Compress::new()
150+ pub(crate) fn new_compress(level: Compression) -> Compress {
151+ Compress::new(level)
156152 }
157153
158154 impl<W> deflate::Write<W>
159155 where
160156 W: io::Write,
161157 {
162− /// Create a new instance writing compressed bytes to `inner`.
163− pub fn new(inner: W) -> deflate::Write<W> {
158+ /// Create a new instance writing bytes compressed with `level` to `inner`.
159+ pub fn new(inner: W, level: Compression) -> deflate::Write<W> {
164160 deflate::Write {
165− compressor: new_compress(),
161+ compressor: new_compress(level),
162+ compression: level,
166163 inner,
167164 buf: [0; deflate::BUF_SIZE],
168165 }
gix-zlib/tests/zlib/stream/deflate.rs+27−8
@@ -5,11 +5,11 @@ use std::{ · 4 unmodified lines
55
66 use bstr::ByteSlice;
77
8−use gix_zlib::Decompress;
98 use gix_zlib::stream::deflate::{self, Compress, FlushCompress};
9+use gix_zlib::{Compression, Decompress};
1010
1111 pub(crate) fn compressed(data: &[u8]) -> Vec<u8> {
12− let mut writer = deflate::Write::new(Vec::new());
12+ let mut writer = deflate::Write::new(Vec::new(), Compression::BEST_SPEED);
1313 writer.write_all(data).expect("in-memory writes never fail");
1414 writer.flush().expect("in-memory flushes never fail");
1515 writer.into_inner()
@@ -63,7 +63,7 @@ fn small_file_decompress() -> Result<(), Box<dyn std::error::Error>> { · 47 unmodified lines
6363
6464 #[test]
6565 fn all_at_once() -> Result<(), Box<dyn std::error::Error>> {
66− let mut w = deflate::Write::new(Vec::new());
66+ let mut w = deflate::Write::new(Vec::new(), Compression::BEST_SPEED);
6767 assert_eq!(w.write(b"hello")?, 5);
6868 w.flush()?;
6969
@@ -80,9 +80,28 @@ fn assert_deflate_buffer(out: Vec<u8>, expected: &[u8]) -> Result<(), Box<dyn st · 10 unmodified lines
8080 Ok(())
8181 }
8282
83+#[test]
84+fn higher_levels_compress_better() -> Result<(), Box<dyn std::error::Error>> {
85+ let data: Vec<u8> = (0..128 * 1024).map(|i| (i % 100) as u8).collect();
86+ let mut sizes = Vec::new();
87+ for level in [Compression::NONE, Compression::BEST_SPEED, Compression::DEFAULT] {
88+ let mut writer = deflate::Write::new(Vec::new(), level);
89+ writer.write_all(&data)?;
90+ writer.flush()?;
91+ let compressed = writer.into_inner();
92+ assert_deflate_buffer(compressed.clone(), &data)?;
93+ sizes.push(compressed.len());
94+ }
95+ assert!(
96+ sizes[0] > sizes[1] && sizes[1] > sizes[2],
97+ "each level compresses better than the one before it: {sizes:?}"
98+ );
99+ Ok(())
100+}
101+
83102 #[test]
84103 fn big_file_small_writes() -> Result<(), Box<dyn std::error::Error>> {
85− let mut w = deflate::Write::new(Vec::new());
104+ let mut w = deflate::Write::new(Vec::new(), Compression::BEST_SPEED);
86105 let bytes = include_bytes!(
87106 "../../../../gix-odb/tests/fixtures/objects/pack/pack-11fdfa9e156ab73caae3b6da867192221f2089c2.pack"
88107 );
@@ -96,7 +115,7 @@ fn big_file_small_writes() -> Result<(), Box<dyn std::error::Error>> { · 7 unmodified lines
96115
97116 #[test]
98117 fn big_file_a_few_big_writes() -> Result<(), Box<dyn std::error::Error>> {
99− let mut w = deflate::Write::new(Vec::new());
118+ let mut w = deflate::Write::new(Vec::new(), Compression::BEST_SPEED);
100119 let bytes = include_bytes!(
101120 "../../../../gix-odb/tests/fixtures/objects/pack/pack-11fdfa9e156ab73caae3b6da867192221f2089c2.pack"
102121 );
@@ -116,7 +135,7 @@ fn compressor_lifecycle_counters_and_flush_modes() -> Result<(), Box<dyn std::er · 13 unmodified lines
116135 FlushCompress::Sync,
117136 FlushCompress::Full,
118137 ] {
119− let mut compressor = Compress::default();
138+ let mut compressor = Compress::new(Compression::BEST_SPEED);
120139 assert_eq!(compressor.total_in(), 0);
121140 assert_eq!(compressor.total_out(), 0);
122141
@@ -146,14 +165,14 @@ fn compressor_lifecycle_counters_and_flush_modes() -> Result<(), Box<dyn std::er · 23 unmodified lines
146165
147166 #[test]
148167 fn writer_clone_and_reset() -> Result<(), Box<dyn std::error::Error>> {
149− let original = deflate::Write::new(Vec::new());
168+ let original = deflate::Write::new(Vec::new(), Compression::BEST_SPEED);
150169 let mut cloned = original.clone();
151170 cloned.write_all(b"clone owns a fresh compressor")?;
152171 cloned.flush()?;
153172 assert_deflate_buffer(cloned.into_inner(), b"clone owns a fresh compressor")?;
154173 assert!(original.into_inner().is_empty());
155174
156− let mut writer = deflate::Write::new(Vec::new());
175+ let mut writer = deflate::Write::new(Vec::new(), Compression::BEST_SPEED);
157176 writer.write_all(b"first")?;
158177 writer.flush()?;
159178 writer.reset();
gix-zlib/tests/zlib/stream/inflate.rs+1−1
@@ -21,7 +21,7 @@ fn errors_keep_the_underlying_cause() { · 20 unmodified lines
2121 }
2222
2323 fn deflated(data: &[u8]) -> Vec<u8> {
24− let mut out = deflate::Write::new(Vec::new());
24+ let mut out = deflate::Write::new(Vec::new(), gix_zlib::Compression::BEST_SPEED);
2525 out.write_all(data).expect("in-memory writes never fail");
2626 out.flush().expect("in-memory flushes never fail");
2727 out.into_inner()