Your browser sucks, JavaScript disabled! Gunnar's console needs JavaScript and WebAssembly. Git over SSH on port 2222 does not.

nordisk/edda

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"
        );
    }
}