crates/storage-git-gix/parked-gix-2/gunnar-store-gix/src/gix_adapter.rs
//! The adapter that makes every gix plumbing algorithm work over any
//! [`ObjectStore`].
//!
//! `gix-object` already defines the traits: [`gix_object::Find`],
//! [`gix_object::Exists`], [`gix_object::FindHeader`], [`gix_object::Write`].
//! Everything generic over them (`gix-pack`'s counting, `gix-traverse`,
//! `gix-revwalk`, `gix-diff`) is storage-agnostic already. What is missing is a
//! bridge from gunnar's trait to gix's.
//!
//! It has to be a wrapper type rather than a blanket
//! `impl<S: ObjectStore> gix_object::Find for S`: `Find` is a foreign trait and
//! `S` a bare type parameter, so the orphan rule rejects that impl outright.
//! [`GixObjects`] is the local type that makes it legal, and it is zero-cost,
//! being a newtype around whatever you already had.
//!
//! ```no_run
//! use gunnar_store::{mem::MemObjectStore, ObjectStore, ObjectStoreExt};
//! use gix_object::FindExt;
//!
//! let store = MemObjectStore::new(gix_hash::Kind::Sha256);
//! let odb = store.as_gix(); // implements Find + Exists + FindHeader + Write
//! let mut buf = Vec::new();
//! # let id = gix_hash::Kind::Sha256.null();
//! let _ = odb.find_commit_iter(&id, &mut buf);
//! ```
use gix_hash::ObjectId;
use gix_object::{Data, Kind};
use crate::object::ObjectStore;
/// A gunnar [`ObjectStore`] wearing gix's object-access traits.
///
/// Wrap anything that implements [`ObjectStore`], including `&S`, `Arc<S>` and
/// `Box<dyn ObjectStore>`, all of which implement it too.
#[derive(Debug, Clone, Copy, Default)]
pub struct GixObjects<S>(pub S);
impl<S> GixObjects<S> {
/// Wrap `store`.
pub fn new(store: S) -> Self {
GixObjects(store)
}
/// Unwrap, returning the store.
pub fn into_inner(self) -> S {
self.0
}
/// Borrow the store.
pub fn inner(&self) -> &S {
&self.0
}
}
/// Errors from an [`ObjectStore`] are boxed into gix's `find::Error`, which is
/// a `Box<dyn Error + Send + Sync>`, so nothing is lost on the way through.
fn to_find_error(err: crate::Error) -> gix_object::find::Error {
Box::new(err)
}
impl<S: ObjectStore> gix_object::Exists for GixObjects<S> {
fn exists(&self, id: &gix_hash::oid) -> bool {
self.0.contains(id)
}
}
impl<S: ObjectStore> gix_object::Find for GixObjects<S> {
fn try_find<'a>(
&self,
id: &gix_hash::oid,
buffer: &'a mut Vec<u8>,
) -> Result<Option<Data<'a>>, gix_object::find::Error> {
// Counted separately from `ObjectStore::read` because this is the seam
// gix calls INWARD through: `Bundle::write_to_directory` asks it for
// every delta base a thin pack left out. The same read, from the other
// side of the boundary, and the census needs the two apart.
gunnar_pack::oidprobe::bump(&gunnar_pack::oidprobe::GIX_OBJECT_FIND_FROM_GIX);
self.0.read(id, buffer).map_err(to_find_error)
}
}
impl<S: ObjectStore> gix_object::FindHeader for GixObjects<S> {
fn try_header(
&self,
id: &gix_hash::oid,
) -> Result<Option<gix_object::Header>, gix_object::find::Error> {
self.0.header(id).map_err(to_find_error)
}
}
impl<S: ObjectStore> gix_object::Write for GixObjects<S> {
fn write_buf(&self, object: Kind, from: &[u8]) -> Result<ObjectId, gix_object::write::Error> {
self.0.write(object, from).map_err(|err| Box::new(err) as _)
}
fn write_buf_with_known_id(
&self,
object: Kind,
from: &[u8],
_id: ObjectId,
) -> Result<ObjectId, gix_object::write::Error> {
// The caller's id is not trusted: an object store that stores bytes
// under an id it never verified is a store that can serve a client an
// object whose hash does not match, which every git client will reject
// at the far end of a clone. Re-hashing is cheap next to that.
self.write_buf(object, from)
}
/// # `size` is a claim, not a measurement (S-027)
///
/// It arrives from whoever is streaming the object, and this used to open
/// with `Vec::with_capacity(size as usize)`, so a declared `u64::MAX`
/// either panicked on capacity overflow or attempted an absurd allocation
/// **before a single byte was read**, and on a 32-bit target `as usize`
/// truncated it silently first. The length check further down catches the
/// mismatch, but only after the allocation it was supposed to prevent.
///
/// The ceiling is [`gunnar_pack::DEFAULT_ALLOC_LIMIT_BYTES`], the number
/// this crate's sibling already applies to exactly this class of
/// caller-declared length while decoding an untrusted pack, rather than a
/// second constant that could drift from it (LAW 5).
///
/// An oversized `size` is **refused, not clamped**. Clamping would read a
/// prefix of the object and store it under the hash of bytes that are not
/// the object, which is a corrupt store rather than a rejected write. The
/// pre-allocation is clamped as well, so the `as usize` truncation cannot
/// turn a rejected size into a small accepted one behind the check.
fn write_stream(
&self,
kind: Kind,
size: u64,
from: &mut dyn std::io::Read,
) -> Result<ObjectId, gix_object::write::Error> {
use std::io::Read;
const CAP: u64 = gunnar_pack::DEFAULT_ALLOC_LIMIT_BYTES as u64;
if size > CAP {
return Err(Box::new(std::io::Error::new(
std::io::ErrorKind::InvalidInput,
format!("declared object size {size} exceeds the {CAP}-byte limit"),
)));
}
let mut buf = Vec::with_capacity(size.min(CAP) as usize);
Read::take(&mut *from, size).read_to_end(&mut buf)?;
if buf.len() as u64 != size {
return Err(Box::new(std::io::Error::new(
std::io::ErrorKind::UnexpectedEof,
format!("stream yielded {} bytes, expected {size}", buf.len()),
)));
}
self.write_buf(kind, &buf)
}
fn write_stream_with_known_id(
&self,
kind: Kind,
size: u64,
from: &mut dyn std::io::Read,
_id: ObjectId,
) -> Result<ObjectId, gix_object::write::Error> {
self.write_stream(kind, size, from)
}
}
/// Conveniences on top of [`ObjectStore`].
pub trait ObjectStoreExt: ObjectStore + Sized {
/// Borrow this store as something gix's plumbing accepts.
fn as_gix(&self) -> GixObjects<&Self> {
GixObjects(self)
}
/// Consume this store, returning something gix's plumbing accepts.
fn into_gix(self) -> GixObjects<Self> {
GixObjects(self)
}
}
impl<S: ObjectStore + Sized> ObjectStoreExt for S {}
#[cfg(test)]
mod tests {
use super::*;
use crate::mem::MemObjectStore;
use gix_object::Write;
/// A caller-declared size above the ceiling is refused before anything is
/// allocated. S-027, **both entry points**.
///
/// `write_stream_with_known_id` delegates here, so it is the same code
/// path; it is asserted anyway, because "delegates today" is a property of
/// the current body and not of the contract, and the second entry point is
/// the one a future edit forgets.
///
/// # The RED, and why the case is `u64::MAX` rather than "something big"
///
/// Before the check, `Vec::with_capacity(u64::MAX as usize)` panicked with
/// `capacity overflow`: a failing test, loudly, which is what makes this
/// assertion able to fail. Run under `ulimit -v` first: a merely *large*
/// declared size such as 2 GiB would have gone to the allocator instead,
/// and an allocation failure aborts the whole test process rather than
/// failing one test. `u64::MAX` exceeds `isize::MAX`, so `Vec` refuses it
/// arithmetically and never reaches `malloc`.
#[test]
fn a_declared_size_above_the_ceiling_is_refused_not_allocated() {
let odb = MemObjectStore::new(gix_hash::Kind::Sha256).into_gix();
let cap = gunnar_pack::DEFAULT_ALLOC_LIMIT_BYTES as u64;
for size in [u64::MAX, cap + 1] {
for (entry, result) in [
(
"write_stream",
odb.write_stream(Kind::Blob, size, &mut &b"hi"[..]),
),
(
"write_stream_with_known_id",
odb.write_stream_with_known_id(
Kind::Blob,
size,
&mut &b"hi"[..],
gix_hash::Kind::Sha256.null(),
),
),
] {
let err = result.expect_err(&format!(
"{entry} accepted a declared size of {size}, which is above \
the {cap}-byte ceiling"
));
let text = err.to_string();
assert!(
text.contains(&size.to_string()) && text.contains("exceeds"),
"{entry} refused a declared size of {size} with an error that \
does not say why: {text}"
);
}
}
}
/// ...and an object at or below the ceiling still writes, unchanged.
///
/// Without this the test above is satisfied by a `write_stream` that
/// refuses everything, which is a worse bug than the one being fixed and
/// would pass every assertion in it.
#[test]
fn a_declared_size_within_the_ceiling_still_writes_the_object() {
let store = MemObjectStore::new(gix_hash::Kind::Sha256);
let odb = (&store).into_gix();
let body = b"the quick brown fox\n";
let streamed = odb
.write_stream(Kind::Blob, body.len() as u64, &mut &body[..])
.expect("a blob of a declared size well under the ceiling");
let buffered = odb.write_buf(Kind::Blob, body).expect("the same blob");
assert_eq!(
streamed, buffered,
"the streamed object landed under a different id than the buffered one"
);
let mut out = Vec::new();
let data = store
.read(&streamed, &mut out)
.expect("read it back")
.expect("it is in the store");
assert_eq!(
(data.kind, data.data),
(Kind::Blob, &body[..]),
"the object that came back is not the one that went in"
);
}
}