Skip to main content

grit_lib/odb/store/
mod.rs

1//! Pluggable object storage backends and the [`ObjectStore`] trait surface.
2//!
3//! Backends implement read, metadata, streaming, iteration, and (optionally) write
4//! operations without allocating error strings on cache misses: a missing object
5//! is `Ok(None)`, not [`Error::ObjectNotFound`].
6//!
7//! Thread safety: [`ObjectStore`] requires `Send + Sync` so repositories can share
8//! a store across threads; individual methods may take internal locks.
9
10mod composite;
11mod files;
12#[cfg(test)]
13pub(crate) mod flaky;
14pub mod loose;
15mod midx;
16mod packs;
17
18pub use composite::CompositeStore;
19pub use files::FilesSource;
20pub use loose::LooseStore;
21pub(crate) use loose::{LooseNegativeCache, SharedLooseNegativeCache};
22pub use midx::{MidxObjects, MidxObjectsStatus};
23pub(crate) use packs::touch_pack_mtime;
24pub use packs::{PackFilter, PackedObjects};
25
26use std::io::{self, Cursor, Read};
27use std::ops::ControlFlow;
28use std::sync::{Arc, RwLock};
29
30use std::path::Path;
31
32use crate::error::{Error, Result};
33use crate::hash;
34use crate::objects::{
35    oid_hash_map, HashAlgo, Object, ObjectId, ObjectIdPrefix, ObjectInfo, ObjectKind, OidHashMap,
36};
37use crate::odb::WriteOptions;
38use crate::pack;
39
40type MemoryObjectEntry = (ObjectKind, Arc<[u8]>);
41
42/// An object read only when its payload is no larger than a caller's limit.
43#[derive(Debug)]
44pub(crate) enum BoundedRead {
45    /// The whole object; its payload is within the limit.
46    Object(Object),
47    /// The payload exceeds the limit and was not read.
48    Larger,
49}
50
51/// Where [`FilesSource::read_unless_larger`] found an object.
52#[derive(Debug)]
53pub(crate) enum LocalSizedRead {
54    /// In a local pack but not read: the payload size when the pack stores it whole, `None`
55    /// when it is stored as a delta (sizing it would mean inflating the delta).
56    Packed(Option<u64>),
57    /// A loose object, read or sized against the limit.
58    Loose(BoundedRead),
59    /// In neither a local pack nor the loose store.
60    Missing,
61}
62
63/// Streaming handle for a single object's uncompressed payload.
64///
65/// The reader yields bytes after the Git object header (`"<kind> <size>\\0"`).
66pub struct ObjectStream<'a> {
67    /// Resolved object type.
68    pub kind: ObjectKind,
69    /// Uncompressed payload length in bytes.
70    pub size: u64,
71    /// Payload bytes; must not include the object header.
72    pub reader: Box<dyn Read + Send + 'a>,
73}
74
75/// Read-only object storage: lookup, metadata, streaming, and enumeration.
76///
77/// Implementations must be safe to share across threads (`Send + Sync`). Missing
78/// objects are reported as `Ok(None)`, not as errors.
79///
80/// A store used as a repository's primary can give history walks a commit-graph so they
81/// skip commit reads altogether: build one with [`crate::maintenance::build_commit_graph`],
82/// persist its bytes, and attach it with
83/// [`OdbBuilder::commit_graph`](crate::odb::OdbBuilder::commit_graph). Graph files under
84/// `objects/info/` are never read for a custom primary.
85pub trait ObjectStore: Send + Sync + std::fmt::Debug {
86    /// Hash algorithm for objects written through this store.
87    fn hash_algo(&self) -> HashAlgo;
88
89    /// Load the full object, if present.
90    ///
91    /// # Errors
92    ///
93    /// Returns I/O or corruption errors from the backend; never [`Error::ObjectNotFound`].
94    fn read(&self, oid: &ObjectId) -> Result<Option<Object>>;
95
96    /// Load object payload into `buf`, reusing its capacity when possible.
97    ///
98    /// History walks (rev-list, merge-base, path-limited log) read commits through this
99    /// method into one reused buffer, so it is the hot path for a custom backend. The
100    /// default implementation delegates to [`Self::read`], which allocates an owned
101    /// [`Object`] per call, and copies into `buf`. Backends should override it to decode
102    /// or copy their stored bytes straight into `buf`.
103    ///
104    /// # Errors
105    ///
106    /// Same as [`Self::read`].
107    fn read_into(&self, oid: &ObjectId, buf: &mut Vec<u8>) -> Result<Option<ObjectKind>> {
108        let Some(obj) = self.read(oid)? else {
109            return Ok(None);
110        };
111        buf.clear();
112        buf.extend_from_slice(&obj.data);
113        Ok(Some(obj.kind))
114    }
115
116    /// Object kind and size without loading the full payload.
117    ///
118    /// # Errors
119    ///
120    /// Same as [`Self::read`].
121    fn read_info(&self, oid: &ObjectId) -> Result<Option<ObjectInfo>>;
122
123    /// Whether `oid` is stored in this backend (without reading the payload).
124    ///
125    /// # Errors
126    ///
127    /// Propagates backend failures other than "not found".
128    fn contains(&self, oid: &ObjectId) -> Result<bool> {
129        Ok(self.read_info(oid)?.is_some())
130    }
131
132    /// For each id in `oids`, whether it is stored in this backend; the result has
133    /// one entry per input id, in input order.
134    ///
135    /// Callers that need to check many objects at once (for example write-tree
136    /// confirming that every reusable cached subtree still exists) use this instead
137    /// of one [`Self::contains`] call per id. The default implementation calls
138    /// [`Self::contains`] for each id. A database- or network-backed store should
139    /// override it to answer the whole batch in a single round trip.
140    ///
141    /// # Errors
142    ///
143    /// Propagates the first backend failure, like [`Self::contains`].
144    fn contains_many(&self, oids: &[ObjectId]) -> Result<Vec<bool>> {
145        oids.iter().map(|oid| self.contains(oid)).collect()
146    }
147
148    /// Open a stream over the object's uncompressed payload.
149    ///
150    /// Default implementation buffers via [`Self::read`].
151    ///
152    /// # Errors
153    ///
154    /// Same as [`Self::read`].
155    fn open_stream(&self, oid: &ObjectId) -> Result<Option<ObjectStream<'_>>> {
156        let Some(obj) = self.read(oid)? else {
157            return Ok(None);
158        };
159        let size = u64::try_from(obj.data.len()).map_err(|_| {
160            Error::CorruptObject(format!("object size overflow for {}", oid.to_hex()))
161        })?;
162        Ok(Some(ObjectStream {
163            kind: obj.kind,
164            size,
165            reader: Box::new(Cursor::new(obj.data)),
166        }))
167    }
168
169    /// Invoke `f` for every object id in this store until `f` returns [`ControlFlow::Break`].
170    ///
171    /// # Errors
172    ///
173    /// Propagates backend enumeration failures.
174    fn for_each_object(&self, f: &mut dyn FnMut(&ObjectId) -> ControlFlow<()>) -> Result<()>;
175
176    /// Fill `out` with object ids whose hex form starts with `prefix` (case-insensitive).
177    ///
178    /// At most `limit` ids are appended (0 means no limit). When more than one object
179    /// matches and `limit` is 1, callers treat the result as ambiguous if `out` would
180    /// have held multiple matches.
181    ///
182    /// The default implementation parses `prefix` once into an [`ObjectIdPrefix`] and
183    /// compares it against the raw bytes of every id from [`Self::for_each_object`]: one
184    /// allocation-free pass over the whole store per call. Abbreviating ids and resolving
185    /// short ids call this once per id, so a store with many objects and a cheaper way to
186    /// find ids by leading bytes (a sorted index, an ordered key-value range from
187    /// [`ObjectIdPrefix::lowest`] to [`ObjectIdPrefix::highest`]) should override it.
188    ///
189    /// # Errors
190    ///
191    /// Returns [`Error::InvalidObjectId`] when `prefix` is not valid hex or is longer
192    /// than a full object id for this store's hash algorithm.
193    fn lookup_prefix(&self, prefix: &str, limit: usize, out: &mut Vec<ObjectId>) -> Result<()> {
194        let prefix = ObjectIdPrefix::from_hex(prefix, self.hash_algo())?;
195        let mut count = 0usize;
196        self.for_each_object(&mut |oid| {
197            if prefix.matches(oid) {
198                out.push(*oid);
199                count += 1;
200                if limit != 0 && count >= limit {
201                    return ControlFlow::Break(());
202                }
203            }
204            ControlFlow::Continue(())
205        })
206    }
207
208    /// Reload indexes or caches after on-disk changes (packs, alternates, MIDX).
209    ///
210    /// Returns `true` when the store's view of objects changed. Default is no-op.
211    ///
212    /// # Errors
213    ///
214    /// Propagates backend refresh failures.
215    fn refresh(&self) -> Result<bool> {
216        Ok(false)
217    }
218}
219
220/// Object storage that supports inserting new objects.
221pub trait WritableObjectStore: ObjectStore {
222    /// Store canonical object bytes and return the object id.
223    ///
224    /// If the object already exists, returns the existing id without replacing
225    /// stored bytes (Git loose-store idempotence).
226    ///
227    /// # Errors
228    ///
229    /// Returns I/O, hashing, or validation errors from the backend.
230    fn write(&self, kind: ObjectKind, data: &[u8], options: WriteOptions) -> Result<ObjectId>;
231
232    /// Store an object from a byte stream of known uncompressed size.
233    ///
234    /// Default implementation reads the stream into memory and calls [`Self::write`].
235    ///
236    /// # Errors
237    ///
238    /// Same as [`Self::write`], plus stream read failures.
239    fn write_stream(
240        &self,
241        kind: ObjectKind,
242        size: u64,
243        reader: &mut dyn Read,
244        options: WriteOptions,
245    ) -> Result<ObjectId> {
246        let size_usize = usize::try_from(size).map_err(|_| {
247            Error::CorruptObject(format!("object size {size} exceeds address space"))
248        })?;
249        let mut buf = vec![0u8; size_usize];
250        reader.read_exact(&mut buf).map_err(Error::Io)?;
251        self.write(kind, &buf, options)
252    }
253
254    /// Mark `oid` as recently used so retention policies keep it (Git `freshen`).
255    ///
256    /// Returns `true` when the object was present and the backend recorded the touch.
257    ///
258    /// # Errors
259    ///
260    /// Propagates backend failures.
261    fn freshen(&self, oid: &ObjectId) -> Result<bool>;
262
263    /// Make prior successful [`Self::write`] calls durable (commit a transaction, fsync, …).
264    ///
265    /// Grit calls this after bulk ingest and before ref updates that publish new commits.
266    /// Reads on this store must observe every write that returned `Ok` even before
267    /// [`Self::flush`]; flush is the durability barrier for crash recovery.
268    ///
269    /// Default is a no-op. Do not flush automatically from [`Self::write`]; backends batch
270    /// many writes between flush calls.
271    ///
272    /// # Errors
273    ///
274    /// Propagates backend persistence failures.
275    fn flush(&self) -> Result<()> {
276        Ok(())
277    }
278
279    /// Like [`Self::flush`], but rolls back the current batch when flush fails.
280    ///
281    /// # Errors
282    ///
283    /// Propagates flush failures after attempting [`Self::rollback`].
284    fn flush_for_publish(&self) -> Result<()> {
285        if let Err(err) = self.flush() {
286            let _ = self.rollback();
287            return Err(err);
288        }
289        Ok(())
290    }
291
292    /// Discard unflushed writes from the current batch after a failed ingest or similar.
293    ///
294    /// Default is a no-op. After a successful [`Self::flush`], rollback is a no-op.
295    ///
296    /// # Errors
297    ///
298    /// Propagates backend failures while aborting the batch.
299    fn rollback(&self) -> Result<()> {
300        Ok(())
301    }
302}
303
304/// Forward both traits through a pointer type so a store behind `Arc`, `Box` or `&` is
305/// itself a store: a decorator generic over `S: ObjectStore` can wrap a shared
306/// `Arc<dyn ObjectStore>` and still be passed on as one. Every method forwards, so the
307/// pointee's overrides (batched `contains_many`, native `read_into`, ...) are kept.
308macro_rules! forward_object_store {
309    ($pointer:ty) => {
310        impl<T: ObjectStore + ?Sized> ObjectStore for $pointer {
311            fn hash_algo(&self) -> HashAlgo {
312                (**self).hash_algo()
313            }
314
315            fn read(&self, oid: &ObjectId) -> Result<Option<Object>> {
316                (**self).read(oid)
317            }
318
319            fn read_into(&self, oid: &ObjectId, buf: &mut Vec<u8>) -> Result<Option<ObjectKind>> {
320                (**self).read_into(oid, buf)
321            }
322
323            fn read_info(&self, oid: &ObjectId) -> Result<Option<ObjectInfo>> {
324                (**self).read_info(oid)
325            }
326
327            fn contains(&self, oid: &ObjectId) -> Result<bool> {
328                (**self).contains(oid)
329            }
330
331            fn contains_many(&self, oids: &[ObjectId]) -> Result<Vec<bool>> {
332                (**self).contains_many(oids)
333            }
334
335            fn open_stream(&self, oid: &ObjectId) -> Result<Option<ObjectStream<'_>>> {
336                (**self).open_stream(oid)
337            }
338
339            fn for_each_object(
340                &self,
341                f: &mut dyn FnMut(&ObjectId) -> ControlFlow<()>,
342            ) -> Result<()> {
343                (**self).for_each_object(f)
344            }
345
346            fn lookup_prefix(
347                &self,
348                prefix: &str,
349                limit: usize,
350                out: &mut Vec<ObjectId>,
351            ) -> Result<()> {
352                (**self).lookup_prefix(prefix, limit, out)
353            }
354
355            fn refresh(&self) -> Result<bool> {
356                (**self).refresh()
357            }
358        }
359
360        impl<T: WritableObjectStore + ?Sized> WritableObjectStore for $pointer {
361            fn write(
362                &self,
363                kind: ObjectKind,
364                data: &[u8],
365                options: WriteOptions,
366            ) -> Result<ObjectId> {
367                (**self).write(kind, data, options)
368            }
369
370            fn write_stream(
371                &self,
372                kind: ObjectKind,
373                size: u64,
374                reader: &mut dyn Read,
375                options: WriteOptions,
376            ) -> Result<ObjectId> {
377                (**self).write_stream(kind, size, reader, options)
378            }
379
380            fn freshen(&self, oid: &ObjectId) -> Result<bool> {
381                (**self).freshen(oid)
382            }
383
384            fn flush(&self) -> Result<()> {
385                (**self).flush()
386            }
387
388            fn flush_for_publish(&self) -> Result<()> {
389                (**self).flush_for_publish()
390            }
391
392            fn rollback(&self) -> Result<()> {
393                (**self).rollback()
394            }
395        }
396    };
397}
398
399forward_object_store!(Arc<T>);
400forward_object_store!(Box<T>);
401forward_object_store!(&T);
402
403/// True when `oid` is stored as loose or in a non-promisor pack under `objects_dir`.
404pub(crate) fn local_object_materialized(objects_dir: &Path, oid: &ObjectId) -> bool {
405    if local_object_materialized_without_rescan(objects_dir, oid) {
406        return true;
407    }
408    if pack::reprepare_pack_directory_on_miss(objects_dir).ok() == Some(true) {
409        return object_in_local_packs(objects_dir, oid);
410    }
411    false
412}
413
414/// Like [`local_object_materialized`], but trusts the cached pack listing instead of
415/// rescanning `pack/` when the object is not found.
416pub(crate) fn local_object_materialized_without_rescan(objects_dir: &Path, oid: &ObjectId) -> bool {
417    object_in_local_packs(objects_dir, oid) || oid.loose_path_in(objects_dir).is_file()
418}
419
420fn object_in_local_packs(objects_dir: &Path, oid: &ObjectId) -> bool {
421    object_in_non_promisor_pack(objects_dir, oid)
422}
423
424/// Whether `oid` is listed in a non-promisor pack index under `objects_dir` (not loose).
425pub(crate) fn object_in_non_promisor_pack(objects_dir: &Path, oid: &ObjectId) -> bool {
426    let Ok(indexes) = pack::read_local_pack_indexes_cached(objects_dir) else {
427        return false;
428    };
429    for idx in &indexes {
430        if idx.is_promisor {
431            continue;
432        }
433        if idx.contains(oid).unwrap_or(false) {
434            return true;
435        }
436    }
437    false
438}
439
440/// True when `oid` is present as loose or in any local pack index (including promisor).
441///
442/// Does not consult alternates, trigger lazy fetch, or rescan `pack/` on miss beyond the
443/// cached index listing (same as [`local_object_materialized_without_rescan`]).
444pub(crate) fn local_object_stored_without_rescan(objects_dir: &Path, oid: &ObjectId) -> bool {
445    let Ok(indexes) = pack::read_local_pack_indexes_cached(objects_dir) else {
446        return oid.loose_path_in(objects_dir).is_file();
447    };
448    for idx in &indexes {
449        if idx.contains(oid).unwrap_or(false) {
450            return true;
451        }
452    }
453    oid.loose_path_in(objects_dir).is_file()
454}
455
456/// Like [`local_object_stored_without_rescan`], but rescan `pack/` once on miss.
457pub(crate) fn local_object_stored(objects_dir: &Path, oid: &ObjectId) -> bool {
458    if local_object_stored_without_rescan(objects_dir, oid) {
459        return true;
460    }
461    if pack::reprepare_pack_directory_on_miss(objects_dir).ok() == Some(true) {
462        return local_object_stored_without_rescan(objects_dir, oid);
463    }
464    false
465}
466
467/// In-memory object map keyed by [`ObjectId`].
468///
469/// Intended to replace the legacy [`Odb`](super::Odb) overlay hash map in a later step.
470#[derive(Debug)]
471pub struct MemoryStore {
472    algo: HashAlgo,
473    objects: RwLock<OidHashMap<MemoryObjectEntry>>,
474}
475
476impl MemoryStore {
477    /// Create an empty store using `algo` for [`WritableObjectStore::write`].
478    #[must_use]
479    pub fn new(algo: HashAlgo) -> Self {
480        Self {
481            algo,
482            objects: RwLock::new(oid_hash_map()),
483        }
484    }
485
486    /// Insert `oid` without re-hashing (overlay and tests).
487    pub(crate) fn insert_object(&self, oid: ObjectId, kind: ObjectKind, data: Arc<[u8]>) {
488        if let Ok(mut guard) = self.objects.write() {
489            guard.entry(oid).or_insert((kind, data));
490        }
491    }
492}
493
494impl ObjectStore for MemoryStore {
495    fn hash_algo(&self) -> HashAlgo {
496        self.algo
497    }
498
499    fn read(&self, oid: &ObjectId) -> Result<Option<Object>> {
500        let guard = self
501            .objects
502            .read()
503            .map_err(|e| Error::Io(io::Error::other(e.to_string())))?;
504        Ok(guard.get(oid).map(|(kind, data)| Object {
505            kind: *kind,
506            data: data.to_vec(),
507        }))
508    }
509
510    fn read_into(&self, oid: &ObjectId, buf: &mut Vec<u8>) -> Result<Option<ObjectKind>> {
511        let guard = self
512            .objects
513            .read()
514            .map_err(|e| Error::Io(io::Error::other(e.to_string())))?;
515        Ok(guard.get(oid).map(|(kind, data)| {
516            buf.clear();
517            buf.extend_from_slice(data);
518            *kind
519        }))
520    }
521
522    fn read_info(&self, oid: &ObjectId) -> Result<Option<ObjectInfo>> {
523        let guard = self
524            .objects
525            .read()
526            .map_err(|e| Error::Io(io::Error::other(e.to_string())))?;
527        Ok(guard.get(oid).map(|(kind, data)| ObjectInfo {
528            kind: *kind,
529            size: u64::try_from(data.len()).unwrap_or(u64::MAX),
530        }))
531    }
532
533    fn contains(&self, oid: &ObjectId) -> Result<bool> {
534        let guard = self
535            .objects
536            .read()
537            .map_err(|e| Error::Io(io::Error::other(e.to_string())))?;
538        Ok(guard.contains_key(oid))
539    }
540
541    fn contains_many(&self, oids: &[ObjectId]) -> Result<Vec<bool>> {
542        let guard = self
543            .objects
544            .read()
545            .map_err(|e| Error::Io(io::Error::other(e.to_string())))?;
546        Ok(oids.iter().map(|oid| guard.contains_key(oid)).collect())
547    }
548
549    fn open_stream(&self, oid: &ObjectId) -> Result<Option<ObjectStream<'_>>> {
550        let guard = self
551            .objects
552            .read()
553            .map_err(|e| Error::Io(io::Error::other(e.to_string())))?;
554        let Some((kind, data)) = guard.get(oid) else {
555            return Ok(None);
556        };
557        let size = u64::try_from(data.len()).map_err(|_| {
558            Error::CorruptObject(format!("object size overflow for {}", oid.to_hex()))
559        })?;
560        Ok(Some(ObjectStream {
561            kind: *kind,
562            size,
563            reader: Box::new(Cursor::new(Arc::clone(data))),
564        }))
565    }
566
567    fn for_each_object(&self, f: &mut dyn FnMut(&ObjectId) -> ControlFlow<()>) -> Result<()> {
568        let guard = self
569            .objects
570            .read()
571            .map_err(|e| Error::Io(io::Error::other(e.to_string())))?;
572        for oid in guard.keys() {
573            if f(oid).is_break() {
574                break;
575            }
576        }
577        Ok(())
578    }
579
580    fn lookup_prefix(&self, prefix: &str, limit: usize, out: &mut Vec<ObjectId>) -> Result<()> {
581        let prefix = ObjectIdPrefix::from_hex(prefix, self.hash_algo())?;
582        let guard = self
583            .objects
584            .read()
585            .map_err(|e| Error::Io(io::Error::other(e.to_string())))?;
586        let matches = guard.keys().filter(|oid| prefix.matches(oid)).copied();
587        if limit == 0 {
588            out.extend(matches);
589        } else {
590            out.extend(matches.take(limit));
591        }
592        Ok(())
593    }
594}
595
596impl WritableObjectStore for MemoryStore {
597    fn write(&self, kind: ObjectKind, data: &[u8], _options: WriteOptions) -> Result<ObjectId> {
598        let oid = hash::hash_object(self.algo, kind, data)?;
599        let mut guard = self
600            .objects
601            .write()
602            .map_err(|e| Error::Io(io::Error::other(e.to_string())))?;
603        guard
604            .entry(oid)
605            .or_insert_with(|| (kind, Arc::from(data.to_owned())));
606        Ok(oid)
607    }
608
609    fn freshen(&self, oid: &ObjectId) -> Result<bool> {
610        let guard = self
611            .objects
612            .read()
613            .map_err(|e| Error::Io(io::Error::other(e.to_string())))?;
614        Ok(guard.contains_key(oid))
615    }
616}
617
618/// Ask `probe` about the ids of `oids` indexed by `pending`; mark each hit in `present` and
619/// drop it from `pending`, so the next source is only asked about what is still missing.
620///
621/// Does nothing when `pending` is empty. A short answer from `probe` counts as "absent".
622pub(crate) fn resolve_pending(
623    oids: &[ObjectId],
624    present: &mut [bool],
625    pending: &mut Vec<usize>,
626    probe: impl FnOnce(&[ObjectId]) -> Result<Vec<bool>>,
627) -> Result<()> {
628    if pending.is_empty() {
629        return Ok(());
630    }
631    let batch: Vec<ObjectId> = pending.iter().map(|&i| oids[i]).collect();
632    let mut found = probe(&batch)?.into_iter();
633    pending.retain(|&i| {
634        let hit = found.next().unwrap_or(false);
635        present[i] |= hit;
636        !hit
637    });
638    Ok(())
639}
640
641/// Run `store.for_each_object` and stop visiting further stores when `f` returns [`ControlFlow::Break`].
642pub(crate) fn for_each_propagate_break(
643    store: &dyn ObjectStore,
644    f: &mut dyn FnMut(&ObjectId) -> ControlFlow<()>,
645) -> Result<bool> {
646    let mut stop = false;
647    store.for_each_object(&mut |oid| {
648        if stop {
649            return ControlFlow::Break(());
650        }
651        match f(oid) {
652            ControlFlow::Break(()) => {
653                stop = true;
654                ControlFlow::Break(())
655            }
656            ControlFlow::Continue(()) => ControlFlow::Continue(()),
657        }
658    })?;
659    Ok(stop)
660}
661
662#[cfg(test)]
663mod tests {
664    use super::*;
665    use std::io::Read;
666
667    #[test]
668    fn read_hit_and_miss() {
669        let store = MemoryStore::new(HashAlgo::Sha1);
670        let data = b"hello";
671        let oid = store
672            .write(ObjectKind::Blob, data, WriteOptions::default())
673            .unwrap();
674        let obj = store.read(&oid).unwrap().expect("hit");
675        assert_eq!(obj.kind, ObjectKind::Blob);
676        assert_eq!(obj.data, data);
677        assert!(store.read(&ObjectId::zero()).unwrap().is_none());
678        let mut buf = b"stale bytes".to_vec();
679        assert_eq!(
680            store.read_into(&oid, &mut buf).unwrap(),
681            Some(ObjectKind::Blob)
682        );
683        assert_eq!(buf, data);
684        assert_eq!(store.read_into(&ObjectId::zero(), &mut buf).unwrap(), None);
685        assert_eq!(buf, data, "a miss leaves the buffer alone");
686        assert!(store.read_info(&ObjectId::zero()).unwrap().is_none());
687        assert!(!store.contains(&ObjectId::zero()).unwrap());
688        assert!(store.contains(&oid).unwrap());
689        assert_eq!(
690            store.contains_many(&[oid, ObjectId::zero(), oid]).unwrap(),
691            vec![true, false, true]
692        );
693        assert!(store.contains_many(&[]).unwrap().is_empty());
694    }
695
696    #[test]
697    fn resolve_pending_marks_hits_and_keeps_misses() {
698        let ids: Vec<ObjectId> = (1u8..=3)
699            .map(|b| ObjectId::from_bytes(&[b; 20]).unwrap())
700            .collect();
701        let mut present = vec![false; 3];
702        let mut pending = vec![0, 2];
703        resolve_pending(&ids, &mut present, &mut pending, |batch| {
704            assert_eq!(batch, [ids[0], ids[2]]);
705            Ok(vec![false, true])
706        })
707        .unwrap();
708        assert_eq!(present, [false, false, true]);
709        assert_eq!(pending, [0]);
710
711        resolve_pending(&ids, &mut present, &mut pending, |_| Ok(Vec::new())).unwrap();
712        assert_eq!(pending, [0], "a short answer counts as absent");
713
714        pending.clear();
715        resolve_pending(&ids, &mut present, &mut pending, |_| {
716            panic!("no probe when nothing is pending")
717        })
718        .unwrap();
719    }
720
721    #[test]
722    fn stream_four_mib_blob_in_sixty_four_kib_chunks() {
723        let store = MemoryStore::new(HashAlgo::Sha1);
724        let payload = vec![0xABu8; 4 * 1024 * 1024];
725        let oid = store
726            .write(ObjectKind::Blob, &payload, WriteOptions::default())
727            .unwrap();
728        let mut stream = store.open_stream(&oid).unwrap().expect("stream");
729        assert_eq!(stream.kind, ObjectKind::Blob);
730        assert_eq!(stream.size, payload.len() as u64);
731        let mut out = Vec::with_capacity(payload.len());
732        let mut chunk = [0u8; 64 * 1024];
733        loop {
734            let n = stream.reader.read(&mut chunk).unwrap();
735            if n == 0 {
736                break;
737            }
738            out.extend_from_slice(&chunk[..n]);
739        }
740        assert_eq!(out, payload);
741    }
742
743    #[test]
744    fn for_each_object_stops_on_break() {
745        let store = MemoryStore::new(HashAlgo::Sha1);
746        for i in 0u8..5 {
747            store
748                .write(ObjectKind::Blob, &[i], WriteOptions::default())
749                .unwrap();
750        }
751        let mut seen = 0usize;
752        store
753            .for_each_object(&mut |_| {
754                seen += 1;
755                ControlFlow::Break(())
756            })
757            .unwrap();
758        assert_eq!(seen, 1);
759    }
760
761    #[test]
762    fn lookup_prefix_unique_ambiguous_and_none() {
763        let store = MemoryStore::new(HashAlgo::Sha1);
764        let a = store
765            .write(ObjectKind::Blob, b"a", WriteOptions::default())
766            .unwrap();
767        let b = store
768            .write(ObjectKind::Blob, b"b", WriteOptions::default())
769            .unwrap();
770        let hex_a = a.to_hex();
771        let unique_prefix = &hex_a[..12];
772        let mut out = Vec::new();
773        store.lookup_prefix(unique_prefix, 0, &mut out).unwrap();
774        assert_eq!(out, vec![a]);
775
776        let mut ambiguous = Vec::new();
777        store.lookup_prefix("", 2, &mut ambiguous).unwrap();
778        assert_eq!(ambiguous.len(), 2);
779        assert!(ambiguous.contains(&a));
780        assert!(ambiguous.contains(&b));
781
782        let mut none = Vec::new();
783        store
784            .lookup_prefix("ffffffffffffffffffffffffffffffffffffffff", 0, &mut none)
785            .unwrap();
786        assert!(none.is_empty());
787    }
788
789    #[test]
790    fn lookup_prefix_odd_even_empty_and_full_length() {
791        let store = MemoryStore::new(HashAlgo::Sha1);
792        let ids: Vec<ObjectId> = [
793            "abcdef0123000000000000000000000000000000",
794            "abcdef0124000000000000000000000000000000",
795            "abd0000000000000000000000000000000000000",
796        ]
797        .iter()
798        .map(|hex| ObjectId::from_hex(hex).unwrap())
799        .collect();
800        for id in &ids {
801            store.insert_object(*id, ObjectKind::Blob, Arc::from(Vec::new()));
802        }
803        let lookup = |prefix: &str, limit: usize| {
804            let mut out = Vec::new();
805            store.lookup_prefix(prefix, limit, &mut out).unwrap();
806            out.sort();
807            out
808        };
809        assert_eq!(lookup("", 0), ids);
810        assert_eq!(lookup("ab", 1).len(), 1);
811        assert_eq!(lookup("abc", 0), ids[..2]);
812        assert_eq!(lookup("abcdef012", 0), ids[..2]);
813        assert_eq!(lookup("ABCDEF0124", 0), ids[1..2]);
814        assert_eq!(lookup(&ids[2].to_hex(), 0), ids[2..]);
815        assert!(lookup("abe", 0).is_empty());
816        let mut out = Vec::new();
817        assert!(matches!(
818            store.lookup_prefix("xyz", 0, &mut out),
819            Err(Error::InvalidObjectId(_))
820        ));
821    }
822
823    #[test]
824    fn write_idempotent() {
825        let store = MemoryStore::new(HashAlgo::Sha1);
826        let data = b"same";
827        let oid1 = store
828            .write(ObjectKind::Blob, data, WriteOptions::default())
829            .unwrap();
830        let oid2 = store
831            .write(ObjectKind::Blob, data, WriteOptions::default())
832            .unwrap();
833        assert_eq!(oid1, oid2);
834        let guard = store.objects.read().unwrap();
835        assert_eq!(guard.len(), 1);
836    }
837
838    #[test]
839    fn write_stream_default_and_freshen() {
840        let store = MemoryStore::new(HashAlgo::Sha1);
841        let payload = b"streamed";
842        let mut cursor = Cursor::new(*payload);
843        let oid = store
844            .write_stream(
845                ObjectKind::Blob,
846                payload.len() as u64,
847                &mut cursor,
848                WriteOptions::default(),
849            )
850            .unwrap();
851        assert_eq!(
852            store.read(&oid).unwrap().expect("written").data,
853            payload.as_slice()
854        );
855        assert!(store.freshen(&oid).unwrap());
856        assert!(!store.freshen(&ObjectId::zero()).unwrap());
857    }
858
859    #[test]
860    fn refresh_default_false() {
861        let store = MemoryStore::new(HashAlgo::Sha1);
862        assert!(!store.refresh().unwrap());
863    }
864
865    #[test]
866    fn trait_defaults_via_blanket_test_store() {
867        struct DefaultsOnly {
868            inner: MemoryStore,
869        }
870
871        impl std::fmt::Debug for DefaultsOnly {
872            fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
873                self.inner.fmt(f)
874            }
875        }
876
877        impl ObjectStore for DefaultsOnly {
878            fn hash_algo(&self) -> HashAlgo {
879                self.inner.hash_algo()
880            }
881
882            fn read(&self, oid: &ObjectId) -> Result<Option<Object>> {
883                self.inner.read(oid)
884            }
885
886            fn read_info(&self, oid: &ObjectId) -> Result<Option<ObjectInfo>> {
887                self.inner.read_info(oid)
888            }
889
890            fn for_each_object(
891                &self,
892                f: &mut dyn FnMut(&ObjectId) -> ControlFlow<()>,
893            ) -> Result<()> {
894                self.inner.for_each_object(f)
895            }
896        }
897
898        let store = DefaultsOnly {
899            inner: MemoryStore::new(HashAlgo::Sha1),
900        };
901        let oid = WritableObjectStore::write(
902            &store.inner,
903            ObjectKind::Blob,
904            b"x",
905            WriteOptions::default(),
906        )
907        .unwrap();
908        assert!(ObjectStore::contains(&store, &oid).unwrap());
909        assert_eq!(
910            ObjectStore::contains_many(&store, &[ObjectId::zero(), oid]).unwrap(),
911            vec![false, true]
912        );
913        assert!(ObjectStore::open_stream(&store, &oid).unwrap().is_some());
914        let mut prefixed = Vec::new();
915        ObjectStore::lookup_prefix(&store, &oid.to_hex()[..6], 0, &mut prefixed).unwrap();
916        assert_eq!(prefixed, vec![oid]);
917    }
918}