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.
Unified · Split
| @@ -1572,6 +1572,7 @@ dependencies = [ · 1571 unmodified lines | |||
| 1572 | "gix-worktree", | 1572 | "gix-worktree", |
| 1573 | "gix-worktree-state", | 1573 | "gix-worktree-state", |
| 1574 | "gix-worktree-stream", | 1574 | "gix-worktree-stream", |
| 1575 | "gix-zlib", | ||
| 1575 | "insta", | 1576 | "insta", |
| 1576 | "is_ci", | 1577 | "is_ci", |
| 1577 | "nonempty", | 1578 | "nonempty", |
| @@ -2675,6 +2676,7 @@ name = "gix-zlib" · 1097 unmodified lines | |||
| 2675 | version = "0.1.0" | 2676 | version = "0.1.0" |
| 2676 | dependencies = [ | 2677 | dependencies = [ |
| 2677 | "bstr", | 2678 | "bstr", |
| 2679 | "serde", | ||
| 2678 | "thiserror 2.0.18", | 2680 | "thiserror 2.0.18", |
| 2679 | "zlib-rs", | 2681 | "zlib-rs", |
| 2680 | ] | 2682 | ] |
| @@ -15,9 +15,13 @@ include = ["/src/**/*", "/LICENSE-*"] · 14 unmodified lines | |||
| 15 | doctest = false | 15 | doctest = false |
| 16 | test = false | 16 | test = false |
| 17 | 17 | ||
| 18 | [features] | ||
| 19 | serde = ["dep:serde"] | ||
| 20 | |||
| 18 | [dependencies] | 21 | [dependencies] |
| 19 | zlib-rs = "0.6.2" | 22 | zlib-rs = "0.6.2" |
| 20 | thiserror = "2.0.18" | 23 | thiserror = "2.0.18" |
| 24 | serde = { version = "1.0.114", optional = true, default-features = false, features = ["derive"] } | ||
| 21 | 25 | ||
| 22 | [dev-dependencies] | 26 | [dev-dependencies] |
| 23 | bstr = { version = "1.12.0", default-features = false } | 27 | bstr = { version = "1.12.0", default-features = false } |
| @@ -1,6 +1,40 @@ | |||
| 1 | #![deny(missing_docs)] | 1 | #![deny(missing_docs)] |
| 2 | //! Streaming compression and decompression utilities used by gitoxide. | 2 | //! Streaming compression and decompression utilities used by gitoxide. |
| 3 | 3 | ||
| 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 | |||
| 4 | /// A type to hold all state needed for decompressing a ZLIB encoded stream. | 38 | /// A type to hold all state needed for decompressing a ZLIB encoded stream. |
| 5 | pub struct Decompress(zlib_rs::Inflate); | 39 | pub struct Decompress(zlib_rs::Inflate); |
| 6 | 40 | ||
| @@ -1,6 +1,6 @@ | |||
| 1 | //! Compression state and a [`std::io::Write`] adapter for producing zlib streams. | 1 | //! Compression state and a [`std::io::Write`] adapter for producing zlib streams. |
| 2 | 2 | ||
| 3 | use crate::Status; | 3 | use crate::{Compression, Status}; |
| 4 | use zlib_rs::DeflateError; | 4 | use zlib_rs::DeflateError; |
| 5 | 5 | ||
| 6 | const BUF_SIZE: usize = 4096 * 8; | 6 | const BUF_SIZE: usize = 4096 * 8; |
| @@ -10,6 +10,7 @@ const BUF_SIZE: usize = 4096 * 8; · 3 unmodified lines | |||
| 10 | /// Be sure to call `flush()` when done to finalize the deflate stream. | 10 | /// Be sure to call `flush()` when done to finalize the deflate stream. |
| 11 | pub struct Write<W> { | 11 | pub struct Write<W> { |
| 12 | compressor: Compress, | 12 | compressor: Compress, |
| 13 | compression: Compression, | ||
| 13 | inner: W, | 14 | inner: W, |
| 14 | buf: [u8; BUF_SIZE], | 15 | buf: [u8; BUF_SIZE], |
| 15 | } | 16 | } |
| @@ -20,7 +21,8 @@ where · 4 unmodified lines | |||
| 20 | { | 21 | { |
| 21 | fn clone(&self) -> Self { | 22 | fn clone(&self) -> Self { |
| 22 | Write { | 23 | Write { |
| 23 | compressor: impls::new_compress(), | 24 | compressor: impls::new_compress(self.compression), |
| 25 | compression: self.compression, | ||
| 24 | inner: self.inner.clone(), | 26 | inner: self.inner.clone(), |
| 25 | buf: self.buf, | 27 | buf: self.buf, |
| 26 | } | 28 | } |
| @@ -30,12 +32,6 @@ where · 3 unmodified lines | |||
| 30 | /// Hold all state needed for compressing data. | 32 | /// Hold all state needed for compressing data. |
| 31 | pub struct Compress(zlib_rs::Deflate); | 33 | pub struct Compress(zlib_rs::Deflate); |
| 32 | 34 | ||
| 33 | impl Default for Compress { | ||
| 34 | fn default() -> Self { | ||
| 35 | Self::new() | ||
| 36 | } | ||
| 37 | } | ||
| 38 | |||
| 39 | impl Compress { | 35 | impl Compress { |
| 40 | /// The number of bytes that were read from the input. | 36 | /// The number of bytes that were read from the input. |
| 41 | pub fn total_in(&self) -> u64 { | 37 | pub fn total_in(&self) -> u64 { |
| @@ -47,9 +43,9 @@ impl Compress { · 5 unmodified lines | |||
| 47 | self.0.total_out() | 43 | self.0.total_out() |
| 48 | } | 44 | } |
| 49 | 45 | ||
| 50 | /// Create a new instance - this allocates so should be done with care. | 46 | /// Create a new instance compressing with `level` - this allocates so should be done with care. |
| 51 | pub fn new() -> Self { | 47 | pub fn new(level: Compression) -> Self { |
| 52 | let config = zlib_rs::DeflateConfig::best_speed(); | 48 | let config = zlib_rs::DeflateConfig::new(level.level()); |
| 53 | let header = true; | 49 | let header = true; |
| 54 | let inner = zlib_rs::Deflate::new(config.level, header, config.window_bits as u8); | 50 | let inner = zlib_rs::Deflate::new(config.level, header, config.window_bits as u8); |
| 55 | Self(inner) | 51 | Self(inner) |
| @@ -148,21 +144,22 @@ pub enum FlushCompress { · 92 unmodified lines | |||
| 148 | mod impls { | 144 | mod impls { |
| 149 | use std::io; | 145 | use std::io; |
| 150 | 146 | ||
| 151 | use crate::Status; | ||
| 152 | use crate::stream::deflate::{self, Compress, FlushCompress}; | 147 | use crate::stream::deflate::{self, Compress, FlushCompress}; |
| 148 | use crate::{Compression, Status}; | ||
| 153 | 149 | ||
| 154 | pub(crate) fn new_compress() -> Compress { | 150 | pub(crate) fn new_compress(level: Compression) -> Compress { |
| 155 | Compress::new() | 151 | Compress::new(level) |
| 156 | } | 152 | } |
| 157 | 153 | ||
| 158 | impl<W> deflate::Write<W> | 154 | impl<W> deflate::Write<W> |
| 159 | where | 155 | where |
| 160 | W: io::Write, | 156 | W: io::Write, |
| 161 | { | 157 | { |
| 162 | /// Create a new instance writing compressed bytes to `inner`. | 158 | /// Create a new instance writing bytes compressed with `level` to `inner`. |
| 163 | pub fn new(inner: W) -> deflate::Write<W> { | 159 | pub fn new(inner: W, level: Compression) -> deflate::Write<W> { |
| 164 | deflate::Write { | 160 | deflate::Write { |
| 165 | compressor: new_compress(), | 161 | compressor: new_compress(level), |
| 162 | compression: level, | ||
| 166 | inner, | 163 | inner, |
| 167 | buf: [0; deflate::BUF_SIZE], | 164 | buf: [0; deflate::BUF_SIZE], |
| 168 | } | 165 | } |
| @@ -5,11 +5,11 @@ use std::{ · 4 unmodified lines | |||
| 5 | 5 | ||
| 6 | use bstr::ByteSlice; | 6 | use bstr::ByteSlice; |
| 7 | 7 | ||
| 8 | use gix_zlib::Decompress; | ||
| 9 | use gix_zlib::stream::deflate::{self, Compress, FlushCompress}; | 8 | use gix_zlib::stream::deflate::{self, Compress, FlushCompress}; |
| 9 | use gix_zlib::{Compression, Decompress}; | ||
| 10 | 10 | ||
| 11 | pub(crate) fn compressed(data: &[u8]) -> Vec<u8> { | 11 | 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); |
| 13 | writer.write_all(data).expect("in-memory writes never fail"); | 13 | writer.write_all(data).expect("in-memory writes never fail"); |
| 14 | writer.flush().expect("in-memory flushes never fail"); | 14 | writer.flush().expect("in-memory flushes never fail"); |
| 15 | writer.into_inner() | 15 | writer.into_inner() |
| @@ -63,7 +63,7 @@ fn small_file_decompress() -> Result<(), Box<dyn std::error::Error>> { · 47 unmodified lines | |||
| 63 | 63 | ||
| 64 | #[test] | 64 | #[test] |
| 65 | fn all_at_once() -> Result<(), Box<dyn std::error::Error>> { | 65 | 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); |
| 67 | assert_eq!(w.write(b"hello")?, 5); | 67 | assert_eq!(w.write(b"hello")?, 5); |
| 68 | w.flush()?; | 68 | w.flush()?; |
| 69 | 69 | ||
| @@ -80,9 +80,28 @@ fn assert_deflate_buffer(out: Vec<u8>, expected: &[u8]) -> Result<(), Box<dyn st · 10 unmodified lines | |||
| 80 | Ok(()) | 80 | Ok(()) |
| 81 | } | 81 | } |
| 82 | 82 | ||
| 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 | |||
| 83 | #[test] | 102 | #[test] |
| 84 | fn big_file_small_writes() -> Result<(), Box<dyn std::error::Error>> { | 103 | 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); |
| 86 | let bytes = include_bytes!( | 105 | let bytes = include_bytes!( |
| 87 | "../../../../gix-odb/tests/fixtures/objects/pack/pack-11fdfa9e156ab73caae3b6da867192221f2089c2.pack" | 106 | "../../../../gix-odb/tests/fixtures/objects/pack/pack-11fdfa9e156ab73caae3b6da867192221f2089c2.pack" |
| 88 | ); | 107 | ); |
| @@ -96,7 +115,7 @@ fn big_file_small_writes() -> Result<(), Box<dyn std::error::Error>> { · 7 unmodified lines | |||
| 96 | 115 | ||
| 97 | #[test] | 116 | #[test] |
| 98 | fn big_file_a_few_big_writes() -> Result<(), Box<dyn std::error::Error>> { | 117 | 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); |
| 100 | let bytes = include_bytes!( | 119 | let bytes = include_bytes!( |
| 101 | "../../../../gix-odb/tests/fixtures/objects/pack/pack-11fdfa9e156ab73caae3b6da867192221f2089c2.pack" | 120 | "../../../../gix-odb/tests/fixtures/objects/pack/pack-11fdfa9e156ab73caae3b6da867192221f2089c2.pack" |
| 102 | ); | 121 | ); |
| @@ -116,7 +135,7 @@ fn compressor_lifecycle_counters_and_flush_modes() -> Result<(), Box<dyn std::er · 13 unmodified lines | |||
| 116 | FlushCompress::Sync, | 135 | FlushCompress::Sync, |
| 117 | FlushCompress::Full, | 136 | FlushCompress::Full, |
| 118 | ] { | 137 | ] { |
| 119 | let mut compressor = Compress::default(); | 138 | let mut compressor = Compress::new(Compression::BEST_SPEED); |
| 120 | assert_eq!(compressor.total_in(), 0); | 139 | assert_eq!(compressor.total_in(), 0); |
| 121 | assert_eq!(compressor.total_out(), 0); | 140 | assert_eq!(compressor.total_out(), 0); |
| 122 | 141 | ||
| @@ -146,14 +165,14 @@ fn compressor_lifecycle_counters_and_flush_modes() -> Result<(), Box<dyn std::er · 23 unmodified lines | |||
| 146 | 165 | ||
| 147 | #[test] | 166 | #[test] |
| 148 | fn writer_clone_and_reset() -> Result<(), Box<dyn std::error::Error>> { | 167 | 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); |
| 150 | let mut cloned = original.clone(); | 169 | let mut cloned = original.clone(); |
| 151 | cloned.write_all(b"clone owns a fresh compressor")?; | 170 | cloned.write_all(b"clone owns a fresh compressor")?; |
| 152 | cloned.flush()?; | 171 | cloned.flush()?; |
| 153 | assert_deflate_buffer(cloned.into_inner(), b"clone owns a fresh compressor")?; | 172 | assert_deflate_buffer(cloned.into_inner(), b"clone owns a fresh compressor")?; |
| 154 | assert!(original.into_inner().is_empty()); | 173 | assert!(original.into_inner().is_empty()); |
| 155 | 174 | ||
| 156 | let mut writer = deflate::Write::new(Vec::new()); | 175 | let mut writer = deflate::Write::new(Vec::new(), Compression::BEST_SPEED); |
| 157 | writer.write_all(b"first")?; | 176 | writer.write_all(b"first")?; |
| 158 | writer.flush()?; | 177 | writer.flush()?; |
| 159 | writer.reset(); | 178 | writer.reset(); |
| @@ -21,7 +21,7 @@ fn errors_keep_the_underlying_cause() { · 20 unmodified lines | |||
| 21 | } | 21 | } |
| 22 | 22 | ||
| 23 | fn deflated(data: &[u8]) -> Vec<u8> { | 23 | 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); |
| 25 | out.write_all(data).expect("in-memory writes never fail"); | 25 | out.write_all(data).expect("in-memory writes never fail"); |
| 26 | out.flush().expect("in-memory flushes never fail"); | 26 | out.flush().expect("in-memory flushes never fail"); |
| 27 | out.into_inner() | 27 | out.into_inner() |