Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

12 changes: 12 additions & 0 deletions gitoxide-core/src/pack/create.rs
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,17 @@ where
type ObjectIdIter = dyn Iterator<Item = Result<ObjectId, Box<dyn std::error::Error + Send + Sync>>> + Send;

let repo = gix::discover(repository_path)?.into_sync();
let pack_compression = {
use gix::config::tree::{Core, Pack};
let repo = repo.to_thread_local();
let config = repo.config_snapshot();
config
.integer(Pack::COMPRESSION)
.or_else(|| config.integer(Core::COMPRESSION))
Comment on lines +117 to +118

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Preserve errors when reading pack.compression

Here Snapshot::integer() returns None both when the key is absent and when its value cannot be parsed as an integer. In a repo with pack.compression = bad (or a malformed core.compression fallback), gix free pack create silently falls through to the next/default level instead of reporting the invalid configuration, so the new knob can be ignored and packs are produced at the wrong level. Use try_integer() and pass the Result through the key conversion so malformed values are not treated as unset.

Useful? React with 👍 / 👎.

.map(|level| Pack::COMPRESSION.try_into_compression(Ok(level)))
.transpose()?
.unwrap_or(gix::features::zlib::Compression::DEFAULT)
};
progress.init(Some(2), progress::steps());
let tips = tips.into_iter();
let make_cancellation_err = || anyhow!("Cancelled by user");
Expand Down Expand Up @@ -233,6 +244,7 @@ where
allow_thin_pack: thin,
chunk_size,
version: Default::default(),
compression: pack_compression,
},
))
};
Expand Down
17 changes: 14 additions & 3 deletions gitoxide-core/src/pack/explode.rs
Original file line number Diff line number Diff line change
Expand Up @@ -146,8 +146,15 @@ impl gix::objs::Write for OutputWriter {
impl OutputWriter {
fn new(path: Option<impl AsRef<Path>>, compress: bool, object_hash: gix::hash::Kind) -> Self {
match path {
Some(path) => OutputWriter::Loose(loose::Store::at(path.as_ref(), object_hash, None)),
None => OutputWriter::Sink(odb::sink(object_hash).compress(compress)),
Some(path) => OutputWriter::Loose(loose::Store::at(
path.as_ref(),
object_hash,
None,
gix::features::zlib::Compression::BEST_SPEED,
)),
None => OutputWriter::Sink(
odb::sink(object_hash).compress(compress.then_some(gix::features::zlib::Compression::BEST_SPEED)),
),
}
}
}
Expand Down Expand Up @@ -217,7 +224,11 @@ pub fn pack_or_pack_index(
let object_path = object_path.map(|p| p.as_ref().to_owned());
let out = OutputWriter::new(object_path.clone(), sink_compress, object_hash);
let loose_odb = verify
.then(|| object_path.as_ref().map(|path| loose::Store::at(path, object_hash, None)))
.then(|| {
object_path.as_ref().map(|path| {
loose::Store::at(path, object_hash, None, gix::features::zlib::Compression::BEST_SPEED)
})
})
.flatten();
let mut read_buf = Vec::new();
move |object_kind, buf, index_entry, progress| {
Expand Down
1 change: 1 addition & 0 deletions gitoxide-core/src/pack/index.rs
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,7 @@ pub fn from_pack(
index_version: pack::index::Version::default(),
object_hash: ctx.object_hash,
alloc_limit_bytes: None,
compression: gix::features::zlib::Compression::BEST_SPEED,
};
let out = ctx.out;
let format = ctx.format;
Expand Down
1 change: 1 addition & 0 deletions gitoxide-core/src/pack/receive.rs
Original file line number Diff line number Diff line change
Expand Up @@ -290,6 +290,7 @@ fn receive_pack_blocking(
iteration_mode: pack::data::input::Mode::Verify,
object_hash,
alloc_limit_bytes: None,
compression: gix::features::zlib::Compression::BEST_SPEED,
};
let outcome = pack::Bundle::write_to_directory(
&mut input,
Expand Down
5 changes: 5 additions & 0 deletions gix-features/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,9 @@ zlib = ["dep:zlib-rs", "dep:thiserror"]

#! ### Other

## Data structures implement `serde::Serialize` and `serde::Deserialize`.
serde = ["dep:serde"]

## Count cache hits and misses and print that debug information on drop.
## Caches implement this by default, which costs nothing unless this feature is enabled
cache-efficiency-debug = []
Expand Down Expand Up @@ -111,6 +114,8 @@ bytes = { version = "1.11.1", optional = true }
zlib-rs = { version = "0.6.2", optional = true }
thiserror = { version = "2.0.18", optional = true }

serde = { version = "1.0.114", optional = true, default-features = false, features = ["derive"] }

# Note: once_cell is kept for OnceCell type because std::sync::OnceLock::get_or_try_init() is not yet stable.
# Once it's stabilized (tracking issue #109737), we can remove this dependency.
once_cell = { version = "1.21.3", optional = true }
Expand Down
34 changes: 34 additions & 0 deletions gix-features/src/zlib/mod.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,39 @@
use zlib_rs::InflateError;

/// The compression level to use for zlib-based streams, in the range from 0 (no compression)
/// to 9 (best compression, slowest).
///
/// Note that `git` maps its configured level of `-1` to the zlib default, which is level 6
/// and available as [`Compression::DEFAULT`].
#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct Compression(i32);

impl Compression {
/// Do not compress at all, while still producing a valid zlib stream.
pub const NONE: Compression = Compression(0);
/// The fastest compression, with the lowest compression ratio, also known as level 1.
///
/// This is what `git` uses for loose objects unless configured otherwise with `core.looseCompression`.
pub const BEST_SPEED: Compression = Compression(1);
/// The default compromise between speed and compression ratio, also known as level 6.
///
/// This is what `git` uses when writing packs unless configured otherwise with `pack.compression`.
pub const DEFAULT: Compression = Compression(6);
/// The best compression ratio at the expense of speed, also known as level 9.
pub const BEST: Compression = Compression(9);

/// Create a new instance from `level` if it is within the valid range from 0 to 9, inclusive.
pub fn new(level: i32) -> Option<Self> {
(0..=9).contains(&level).then_some(Compression(level))
}

/// Return the compression level as integer in the range from 0 to 9, inclusive.
pub fn level(&self) -> i32 {
self.0
}
}

/// A type to hold all state needed for decompressing a ZLIB encoded stream.
pub struct Decompress(zlib_rs::Inflate);

Expand Down
31 changes: 14 additions & 17 deletions gix-features/src/zlib/stream/deflate/mod.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
use crate::zlib::Status;
use crate::zlib::{Compression, Status};
use zlib_rs::DeflateError;

const BUF_SIZE: usize = 4096 * 8;
Expand All @@ -8,6 +8,7 @@ const BUF_SIZE: usize = 4096 * 8;
/// Be sure to call `flush()` when done to finalize the deflate stream.
pub struct Write<W> {
compressor: Compress,
compression: Compression,
inner: W,
buf: [u8; BUF_SIZE],
}
Expand All @@ -18,7 +19,8 @@ where
{
fn clone(&self) -> Self {
Write {
compressor: impls::new_compress(),
compressor: impls::new_compress(self.compression),
compression: self.compression,
inner: self.inner.clone(),
buf: self.buf,
}
Expand All @@ -28,12 +30,6 @@ where
/// Hold all state needed for compressing data.
pub struct Compress(zlib_rs::Deflate);

impl Default for Compress {
fn default() -> Self {
Self::new()
}
}

impl Compress {
/// The number of bytes that were read from the input.
pub fn total_in(&self) -> u64 {
Expand All @@ -45,9 +41,9 @@ impl Compress {
self.0.total_out()
}

/// Create a new instance - this allocates so should be done with care.
pub fn new() -> Self {
let config = zlib_rs::DeflateConfig::best_speed();
/// Create a new instance compressing with `level` - this allocates so should be done with care.
pub fn new(level: Compression) -> Self {
let config = zlib_rs::DeflateConfig::new(level.level());
let header = true;
let inner = zlib_rs::Deflate::new(config.level, header, config.window_bits as u8);
Self(inner)
Expand Down Expand Up @@ -145,21 +141,22 @@ pub enum FlushCompress {
mod impls {
use std::io;

use crate::zlib::Status;
use crate::zlib::stream::deflate::{self, Compress, FlushCompress};
use crate::zlib::{Compression, Status};

pub(crate) fn new_compress() -> Compress {
Compress::new()
pub(crate) fn new_compress(level: Compression) -> Compress {
Compress::new(level)
}

impl<W> deflate::Write<W>
where
W: io::Write,
{
/// Create a new instance writing compressed bytes to `inner`.
pub fn new(inner: W) -> deflate::Write<W> {
/// Create a new instance writing bytes compressed with `level` to `inner`.
pub fn new(inner: W, level: Compression) -> deflate::Write<W> {
deflate::Write {
compressor: new_compress(),
compressor: new_compress(level),
compression: level,
inner,
buf: [0; deflate::BUF_SIZE],
}
Expand Down
26 changes: 22 additions & 4 deletions gix-features/src/zlib/stream/deflate/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@ mod deflate_stream {

use bstr::ByteSlice;

use crate::zlib::Decompress;
use crate::zlib::stream::deflate;
use crate::zlib::{Compression, Decompress};

/// Provide streaming decompression using the `std::io::Read` trait.
/// If `std::io::BufReader` is used, an allocation for the input buffer will be performed.
Expand Down Expand Up @@ -55,7 +55,7 @@ mod deflate_stream {

#[test]
fn all_at_once() -> Result<(), Box<dyn std::error::Error>> {
let mut w = deflate::Write::new(Vec::new());
let mut w = deflate::Write::new(Vec::new(), Compression::BEST_SPEED);
assert_eq!(w.write(b"hello")?, 5);
w.flush()?;

Expand All @@ -65,6 +65,24 @@ mod deflate_stream {
assert_deflate_buffer(out, b"hello")
}

#[test]
fn higher_levels_compress_better() -> Result<(), Box<dyn std::error::Error>> {
let data: Vec<u8> = (0..128 * 1024).map(|i| (i % 100) as u8).collect();
let mut sizes = Vec::new();
for level in [Compression::NONE, Compression::BEST_SPEED, Compression::DEFAULT] {
let mut w = deflate::Write::new(Vec::new(), level);
w.write_all(&data)?;
w.flush()?;
assert_deflate_buffer(w.inner.clone(), &data)?;
sizes.push(w.inner.len());
}
assert!(
sizes[0] > sizes[1] && sizes[1] > sizes[2],
"each level compresses better than the one before it: {sizes:?}"
);
Ok(())
}

fn assert_deflate_buffer(out: Vec<u8>, expected: &[u8]) -> Result<(), Box<dyn std::error::Error>> {
let mut actual = Vec::new();
InflateReader::from_read(out.as_slice()).read_to_end(&mut actual)?;
Expand All @@ -74,7 +92,7 @@ mod deflate_stream {

#[test]
fn big_file_small_writes() -> Result<(), Box<dyn std::error::Error>> {
let mut w = deflate::Write::new(Vec::new());
let mut w = deflate::Write::new(Vec::new(), Compression::BEST_SPEED);
let bytes = include_bytes!(
"../../../../../gix-odb/tests/fixtures/objects/pack/pack-11fdfa9e156ab73caae3b6da867192221f2089c2.pack"
);
Expand All @@ -88,7 +106,7 @@ mod deflate_stream {

#[test]
fn big_file_a_few_big_writes() -> Result<(), Box<dyn std::error::Error>> {
let mut w = deflate::Write::new(Vec::new());
let mut w = deflate::Write::new(Vec::new(), Compression::BEST_SPEED);
let bytes = include_bytes!(
"../../../../../gix-odb/tests/fixtures/objects/pack/pack-11fdfa9e156ab73caae3b6da867192221f2089c2.pack"
);
Expand Down
2 changes: 2 additions & 0 deletions gix-odb/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -170,6 +170,8 @@ pub struct Store {
object_hash: gix_hash::Kind,
/// The maximum size of a single allocation caused by user-controlled on-disk pack data.
alloc_limit_bytes: Option<usize>,
/// The compression level to use when writing loose objects.
loose_compression: gix_features::zlib::Compression,
}

/// Create a new cached handle to the object store with support for additional options.
Expand Down
10 changes: 3 additions & 7 deletions gix-odb/src/sink.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,9 @@ use gix_features::zlib::stream::deflate;
use crate::Sink;

impl Sink {
/// Enable or disable compression. Compression is disabled by default
pub fn compress(mut self, enable: bool) -> Self {
if enable {
self.compressor = Some(RefCell::new(deflate::Write::new(io::sink())));
} else {
self.compressor = None;
}
/// Compress with the given level, or disable compression with `None`. Compression is disabled by default.
pub fn compress(mut self, compression: Option<gix_features::zlib::Compression>) -> Self {
self.compressor = compression.map(|level| RefCell::new(deflate::Write::new(io::sink(), level)));
self
}
}
Expand Down
1 change: 1 addition & 0 deletions gix-odb/src/store_impls/dynamic/handle.rs
Original file line number Diff line number Diff line change
Expand Up @@ -353,6 +353,7 @@ impl TryFrom<&super::Store> for super::Store {
use_multi_pack_index: false,
alloc_limit_bytes: s.alloc_limit_bytes,
current_dir: s.current_dir.clone().into(),
loose_compression: s.loose_compression,
},
)
}
Expand Down
8 changes: 8 additions & 0 deletions gix-odb/src/store_impls/dynamic/init.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,11 @@ pub struct Options {
/// The current directory of the process at the time of instantiation.
/// If unset, it will be retrieved using `gix_fs::current_dir(false)`.
pub current_dir: Option<std::path::PathBuf>,
/// The compression level to use when writing loose objects.
///
/// Defaults to [`Compression::BEST_SPEED`](gix_features::zlib::Compression::BEST_SPEED), which is
/// also what `git` uses unless configured otherwise with `core.looseCompression` or `core.compression`.
pub loose_compression: gix_features::zlib::Compression,
}

impl Default for Options {
Expand All @@ -33,6 +38,7 @@ impl Default for Options {
use_multi_pack_index: true,
alloc_limit_bytes: None,
current_dir: None,
loose_compression: gix_features::zlib::Compression::BEST_SPEED,
}
}
}
Expand Down Expand Up @@ -83,6 +89,7 @@ impl Store {
use_multi_pack_index,
alloc_limit_bytes,
current_dir,
loose_compression,
}: Options,
) -> std::io::Result<Self> {
let _span = gix_features::trace::detail!("gix_odb::Store::at()");
Expand Down Expand Up @@ -138,6 +145,7 @@ impl Store {
use_multi_pack_index,
object_hash,
alloc_limit_bytes,
loose_compression,
num_handles_stable: Default::default(),
num_handles_unstable: Default::default(),
num_disk_state_consolidation: Default::default(),
Expand Down
4 changes: 3 additions & 1 deletion gix-odb/src/store_impls/dynamic/load_index.rs
Original file line number Diff line number Diff line change
Expand Up @@ -231,7 +231,9 @@ impl super::Store {
Arc::new(
db_paths
.iter()
.map(|path| crate::loose::Store::at(path, self.object_hash, self.alloc_limit_bytes))
.map(|path| {
crate::loose::Store::at(path, self.object_hash, self.alloc_limit_bytes, self.loose_compression)
})
.collect::<Vec<_>>(),
)
} else {
Expand Down
8 changes: 8 additions & 0 deletions gix-odb/src/store_impls/loose/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ pub struct Store {
pub(crate) object_hash: gix_hash::Kind,
/// The maximum size of a single allocation caused by user-controlled loose object data.
pub(crate) alloc_limit_bytes: Option<usize>,
/// The compression level to use when writing loose objects.
pub(crate) compression: gix_features::zlib::Compression,
}

/// Initialization
Expand All @@ -27,15 +29,21 @@ impl Store {
///
/// `alloc_limit_bytes` limits allocations caused by loose object bodies declared on disk, useful for untrusted input.
/// Use `None` to disable the limit.
///
/// `compression` is the level to deflate loose objects with when writing them. `git` uses
/// [`Compression::BEST_SPEED`](gix_features::zlib::Compression::BEST_SPEED) unless configured
/// otherwise with `core.looseCompression` or `core.compression`.
pub fn at(
objects_directory: impl Into<PathBuf>,
object_hash: gix_hash::Kind,
alloc_limit_bytes: Option<usize>,
compression: gix_features::zlib::Compression,
) -> Store {
Store {
path: objects_directory.into(),
object_hash,
alloc_limit_bytes,
compression,
}
}

Expand Down
Loading
Loading