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