Skip to main content

sley_odb/
pack.rs

1//! Pack-backed object storage: [`FileObjectDatabase`], in-memory [`ObjectDatabase`],
2//! and the shared decode/index caches that back packed reads.
3//!
4//! ## Cache invariants (thread safety)
5//!
6//! A [`FileObjectDatabase`] is [`Clone`] via `Arc` on every cache map. Cloned handles
7//! share the same caches and may be used from multiple threads concurrently.
8//!
9//! * **Read-mostly maps** (`pack_indexes`, `pack_bytes`, `pack_reverse_indexes`,
10//!   `multi_pack_indexes`, `multi_pack_oid_lookups`) use [`RwLock`]: concurrent readers
11//!   take shared locks; writers take exclusive locks only on insert/clear. Lookups never
12//!   hold a lock across decode or I/O.
13//! * **Mutation-heavy maps** (`decoded`, `pack_deltas`, `pack_header_types`, `pack_registry`)
14//!   stay behind [`Mutex`] because inserts and LRU eviction interleave reads and writes on
15//!   the same critical section.
16//! * **Per-pack state** on [`RegisteredPack`](crate::registry::RegisteredPack) mirrors the
17//!   split: parsed indexes are [`RwLock`]-cached; delta-base LRU caches stay [`Mutex`]-backed.
18//! * **`refresh_read_cache`** clears every shared map so the next read sees packs installed
19//!   out-of-band; callers must not hold a cache guard across it.
20
21use flate2::Compression;
22use flate2::read::ZlibDecoder;
23use flate2::write::ZlibEncoder;
24use flate2::{Decompress, FlushDecompress};
25use parking_lot::RwLock;
26use std::sync::Mutex;
27use sley_core::{GitError, MissingObjectContext, ObjectFormat, ObjectId, Result};
28use sley_formats::{Bundle, BundleReference};
29use sley_object::{
30    Commit, EncodedObject, ObjectType, Tag, TreeEntries, parse_framed_object,
31    tree_entry_object_type,
32};
33use sley_pack::{
34    MultiPackIndex, MultiPackIndexOidLookup, PackBitmapIndex, PackBitmapWriter, PackFile,
35    PackIndex, PackIndexByteSource, PackIndexEntry, PackIndexViewData, PackInput,
36    PackReverseIndex, PackStreamIndexBuild, PackWrite, PackWriteOptions, PackWriteSummary,
37};
38use std::collections::{HashMap, HashSet};
39use std::io::{Read, Write};
40use std::path::{Path, PathBuf};
41use std::sync::atomic::{AtomicU64, Ordering};
42use std::sync::{Arc, OnceLock};
43use std::{env, fs};
44
45use crate::{
46    grafted_parents, implied_empty_tree_object, unique_temp_path, with_missing_object_context,
47    ObjectReader, ObjectWriter,
48};
49
50use crate::install::{ObjectPrefixResolution, ObjectStorageInfo};
51use crate::reachability::{loose_object_ids, loose_object_id_set, zero_oid, pack_entry_delta_base, scan_pack_index_offsets, scan_pack_offsets_without_index, remove_file_if_exists, PackDeltaBase, PackIndexOffsetInfo};
52use crate::loose::{LooseObjectStore, collect_loose_fanout_object_ids, collect_loose_object_ids, present_loose_fanouts};
53use crate::registry::{
54    PackRegistryCache, PackDirFingerprint, PackLookup, PackRegistrySnapshot, RegisteredPack, alternate_object_dirs,
55    collect_incremental_midx_object_ids, collect_incremental_midx_prefix_matches,
56    collect_loose_object_ids_with_prefix, collect_multi_pack_index_prefix_matches,
57    collect_pack_index_prefix_matches, collect_packed_object_ids, collect_packed_object_ids_with_prefix,
58    lower_bound_pack_index_entries, object_id_floor_for_hex_prefix, pack_index_fanout_range,
59    object_ids_in_objects_dir, object_ids_with_prefix_in_objects_dir,
60    read_incremental_midx_chain, repository_common_dir, repository_objects_dir,
61    same_registered_pack_set, scan_pack_registry, validate_object_id_prefix, ObjectPresenceChecker,
62};
63
64pub struct ObjectDatabase {
65    pub(crate) format: ObjectFormat,
66    // Behind a `Mutex` so `write_object` can take `&self` (matching the
67    // `ObjectWriter` trait) and a single handle can interleave reads and writes
68    // without a `&mut` borrow — the same shared-by-`&` shape the file-backed
69    // database uses for its caches. Removes the need for callers to wrap this in
70    // a `RefCell`/`&mut` just to write (see sley-fetch's former `RefCell` dance).
71    pub(crate) objects: Mutex<HashMap<ObjectId, Arc<EncodedObject>>>,
72    pub(crate) promisor: bool,
73}
74
75impl ObjectDatabase {
76    pub fn new(format: ObjectFormat) -> Self {
77        Self {
78            format,
79            objects: Mutex::new(HashMap::new()),
80            promisor: false,
81        }
82    }
83
84    pub fn with_promisor(mut self, promisor: bool) -> Self {
85        self.promisor = promisor;
86        self
87    }
88
89    pub fn contains(&self, oid: &ObjectId) -> bool {
90        self.objects
91            .lock()
92            .map(|objects| objects.contains_key(oid))
93            .unwrap_or(false)
94    }
95
96    pub fn validate(&self, oid: &ObjectId) -> Result<()> {
97        let object = self.read_object(oid)?;
98        let actual = object.object_id(self.format)?;
99        if &actual == oid {
100            Ok(())
101        } else {
102            Err(GitError::InvalidObject(format!(
103                "object id mismatch: expected {oid}, got {actual}"
104            )))
105        }
106    }
107}
108
109impl ObjectReader for ObjectDatabase {
110    fn read_object(&self, oid: &ObjectId) -> Result<Arc<EncodedObject>> {
111        self.objects
112            .lock()
113            .map_err(|_| GitError::object_not_found_in(*oid, MissingObjectContext::Read))?
114            .get(oid)
115            .map(Arc::clone)
116            .or_else(|| implied_empty_tree_object(self.format, oid))
117            .ok_or_else(|| GitError::object_not_found_in(*oid, MissingObjectContext::Read))
118    }
119}
120
121impl ObjectWriter for ObjectDatabase {
122    fn write_object(&self, object: EncodedObject) -> Result<ObjectId> {
123        let oid = object.object_id(self.format)?;
124        self.objects
125            .lock()
126            .map_err(|_| GitError::Io("object cache lock poisoned".into()))?
127            .entry(oid)
128            .or_insert_with(|| Arc::new(object));
129        Ok(oid)
130    }
131}
132
133#[derive(Debug, Clone, PartialEq, Eq)]
134pub struct Alternate {
135    pub path: std::path::PathBuf,
136}
137
138#[derive(Debug, Clone, PartialEq, Eq)]
139pub struct PartialClonePolicy {
140    pub promisor_remote: Option<String>,
141    pub allow_missing_promised_objects: bool,
142}
143
144/// Raw pack-file bytes keyed by pack path, shared across cloned handles. Loaded
145/// once so individual objects can be decoded at their offsets (see
146/// [`sley_pack::read_object_at`]) without re-reading the whole file per read.
147pub(crate) type PackBytesCache = Arc<RwLock<HashMap<PathBuf, Arc<PackData>>>>;
148
149/// Backing bytes of a pack file: either memory-mapped (under the `mmap` feature)
150/// or read into the heap. Both deref to `&[u8]`, so the decode path is identical.
151#[derive(Debug)]
152pub(crate) enum PackData {
153    #[cfg(feature = "mmap")]
154    Mapped(sley_mmap::MappedFile),
155    Heap(Vec<u8>),
156}
157
158impl std::ops::Deref for PackData {
159    type Target = [u8];
160
161    fn deref(&self) -> &[u8] {
162        match self {
163            #[cfg(feature = "mmap")]
164            Self::Mapped(mapped) => mapped,
165            Self::Heap(bytes) => bytes,
166        }
167    }
168}
169
170/// Load a pack file's bytes: memory-mapped when the `mmap` feature is on (falling
171/// back to a heap read if the map fails), otherwise read into the heap.
172#[cfg(feature = "mmap")]
173pub(crate) fn load_pack_data(pack_path: &Path) -> Result<PackData> {
174    match sley_mmap::MappedFile::open_pack(pack_path) {
175        Ok(mapped) => Ok(PackData::Mapped(mapped)),
176        Err(_) => Ok(PackData::Heap(fs::read(pack_path)?)),
177    }
178}
179
180#[cfg(not(feature = "mmap"))]
181pub(crate) fn load_pack_data(pack_path: &Path) -> Result<PackData> {
182    Ok(PackData::Heap(fs::read(pack_path)?))
183}
184
185#[cfg(feature = "mmap")]
186pub(crate) fn load_pack_index_data(index_path: &Path) -> Result<Arc<dyn PackIndexByteSource>> {
187    match sley_mmap::MappedFile::open_pack(index_path) {
188        Ok(mapped) => Ok(Arc::new(mapped)),
189        Err(_) => Ok(Arc::new(fs::read(index_path)?)),
190    }
191}
192
193#[cfg(not(feature = "mmap"))]
194pub(crate) fn load_pack_index_data(index_path: &Path) -> Result<Arc<dyn PackIndexByteSource>> {
195    Ok(Arc::new(fs::read(index_path)?))
196}
197
198#[cfg(feature = "mmap")]
199fn load_multi_pack_index_lookup_data(midx_path: &Path) -> Result<Arc<dyn PackIndexByteSource>> {
200    match sley_mmap::MappedFile::open_multi_pack_index(midx_path) {
201        Ok(mapped) => Ok(Arc::new(mapped)),
202        Err(_) => Ok(Arc::new(fs::read(midx_path)?)),
203    }
204}
205
206#[cfg(not(feature = "mmap"))]
207fn load_multi_pack_index_lookup_data(midx_path: &Path) -> Result<Arc<dyn PackIndexByteSource>> {
208    Ok(Arc::new(fs::read(midx_path)?))
209}
210
211/// Memory-capped LRU of recently decoded objects, shared across cloned handles,
212/// so hot delta bases and repeated reads during a walk aren't re-decoded. The
213/// cache is bounded by an approximate byte budget (not a fixed object count) so
214/// it neither thrashes on bulk reads of small objects nor blows up on a few
215/// large ones.
216pub(crate) type DecodedObjectCache = Arc<Mutex<LruObjectCache>>;
217
218/// Per-pack caches of objects decoded from a pack, keyed by pack path and then by
219/// the in-pack byte offset of each object's entry. Shared across cloned handles.
220/// This is the delta-base cache: resolving a delta chain by offset reuses already
221/// decoded bases instead of re-inflating the whole chain on every read.
222pub(crate) type PackDeltaCaches = Arc<Mutex<HashMap<PathBuf, Arc<Mutex<LruOffsetCache>>>>>;
223
224/// Per-pack memo of `in-pack offset -> end-of-chain object type` for the
225/// `cat-file --batch-check` header fast path. Resolving a packed delta's *type*
226/// walks the delta chain to its base; without this memo every header read
227/// re-walks (and re-inflates) the whole chain, so reading every object in a
228/// deeply-deltified pack is super-linear (sley#26). The type only depends on the
229/// chain base, so memoizing `offset -> type` lets each chain be walked at most
230/// once across a batch. Keyed by pack path so an offset key is never applied to
231/// the wrong pack's bytes; shared across cloned handles.
232/// One pack's offset-keyed header memo (see [`PackHeaderTypeCaches`]).
233pub(crate) type PackHeaderTypeCache = Arc<Mutex<HashMap<u64, (ObjectType, u64)>>>;
234
235pub(crate) type PackHeaderTypeCaches = Arc<Mutex<HashMap<PathBuf, PackHeaderTypeCache>>>;
236
237/// Default approximate byte budget for the decoded-object LRU. Sized to comfortably
238/// hold the working set of a history walk (commits/trees/blobs and their delta
239/// bases) without growing without bound on large repositories. Overridable via the
240/// `SLEY_OBJECT_CACHE_BYTES` environment variable; there is currently no git-config
241/// hook threaded into the object database, so this constant is the default.
242const DEFAULT_OBJECT_CACHE_BYTES: usize = 96 * 1024 * 1024;
243
244/// Default approximate byte budget for each per-pack delta-base cache. Holds the
245/// decoded bases of the delta chains being walked so neighboring reads stay warm.
246/// Overridable via `SLEY_DELTA_BASE_CACHE_BYTES`.
247const DEFAULT_DELTA_BASE_CACHE_BYTES: usize = 96 * 1024 * 1024;
248
249/// Approximate heap cost of caching one [`EncodedObject`]: its body plus a fixed
250/// allowance for the key, enum/`Vec` headers, and per-entry map overhead. Used
251/// only to drive eviction, so an estimate is fine.
252pub(crate) fn cached_object_cost(object: &EncodedObject) -> usize {
253    object.body.len().saturating_add(64)
254}
255
256/// Read an approximate byte budget from `var`, falling back to `default` when the
257/// variable is unset or unparseable. A value of `0` disables the cache.
258fn cache_budget_from_env(var: &str, default: usize) -> usize {
259    match env::var(var) {
260        Ok(value) => value.trim().parse::<usize>().unwrap_or(default),
261        Err(_) => default,
262    }
263}
264
265/// Approximate byte budget for the decoded-object LRU (see
266/// [`DEFAULT_OBJECT_CACHE_BYTES`], `SLEY_OBJECT_CACHE_BYTES`).
267///
268/// Resolved once per process: the environment does not change under us, and a new
269/// `FileObjectDatabase` is built often enough (e.g. once per revision resolved)
270/// that re-reading the variable each time showed up as per-object overhead.
271pub(crate) fn object_cache_budget() -> usize {
272    static BUDGET: OnceLock<usize> = OnceLock::new();
273    *BUDGET.get_or_init(|| {
274        cache_budget_from_env("SLEY_OBJECT_CACHE_BYTES", DEFAULT_OBJECT_CACHE_BYTES)
275    })
276}
277
278/// Approximate byte budget for each per-pack delta-base cache (see
279/// [`DEFAULT_DELTA_BASE_CACHE_BYTES`], `SLEY_DELTA_BASE_CACHE_BYTES`). Resolved
280/// once per process for the same reason as [`object_cache_budget`].
281pub(crate) fn delta_base_cache_budget() -> usize {
282    static BUDGET: OnceLock<usize> = OnceLock::new();
283    *BUDGET.get_or_init(|| {
284        cache_budget_from_env(
285            "SLEY_DELTA_BASE_CACHE_BYTES",
286            DEFAULT_DELTA_BASE_CACHE_BYTES,
287        )
288    })
289}
290
291/// Whether to re-hash every object on read and compare it to the requested id.
292///
293/// Off by default, matching git: reads trust the pack index → offset mapping and
294/// the loose object's on-disk name, and object ids are verified where git verifies
295/// them — when a pack is received (the index build re-hashes every object) and on
296/// demand via [`FileObjectDatabase`]'s `validate`/fsck. Re-hashing on *every* read
297/// dominated bulk-read cost (a scalar pure-Rust SHA-1 over each object's full
298/// body), so it is opt-in via `SLEY_VERIFY_READS` (any value other than unset, ``,
299/// or `0`) for callers that want the paranoid check back. Read once and cached, so
300/// the default path pays only a single relaxed atomic load per read.
301pub(crate) fn verify_reads_enabled() -> bool {
302    static VERIFY: OnceLock<bool> = OnceLock::new();
303    *VERIFY.get_or_init(|| match env::var("SLEY_VERIFY_READS") {
304        Ok(value) => !matches!(value.trim(), "" | "0"),
305        Err(_) => false,
306    })
307}
308
309/// A memory-capped LRU map from a key `K` to a decoded [`EncodedObject`].
310///
311/// Eviction is by approximate byte budget (gix-style), not object count, so the
312/// cache adapts to object size. On access an entry is moved to most-recently-used;
313/// on insert, least-recently-used entries are dropped until the budget holds. A
314/// budget of `0` makes the cache inert. Generic over the key so it backs both the
315/// oid-keyed decoded-object cache and the offset-keyed delta-base cache.
316#[derive(Debug)]
317pub(crate) struct LruCache<K: std::hash::Hash + Eq + Clone> {
318    budget: usize,
319    used: usize,
320    map: HashMap<K, LruEntry<K>>,
321    head: Option<K>,
322    tail: Option<K>,
323}
324
325#[derive(Debug)]
326struct LruEntry<K> {
327    object: Arc<EncodedObject>,
328    prev: Option<K>,
329    next: Option<K>,
330}
331
332impl<K: std::hash::Hash + Eq + Clone> LruCache<K> {
333    pub(crate) fn new(budget: usize) -> Self {
334        Self {
335            budget,
336            used: 0,
337            map: HashMap::new(),
338            head: None,
339            tail: None,
340        }
341    }
342
343    pub(crate) fn get(&mut self, key: &K) -> Option<Arc<EncodedObject>> {
344        let object = Arc::clone(&self.map.get(key)?.object);
345        self.touch(key);
346        Some(object)
347    }
348
349    /// Move `key` to the most-recently-used end in O(1).
350    pub(crate) fn touch(&mut self, key: &K) {
351        if self.tail.as_ref() == Some(key) {
352            return;
353        }
354        if self.map.contains_key(key) {
355            self.detach(key);
356            self.attach_back(key.clone());
357        }
358    }
359
360    /// Drop `key` from both the map and the recency queue, releasing its budget.
361    pub(crate) fn remove(&mut self, key: &K) {
362        if let Some(entry) = self.map.get(key) {
363            self.used = self.used.saturating_sub(cached_object_cost(&entry.object));
364        }
365        self.detach(key);
366        self.map.remove(key);
367    }
368
369    pub(crate) fn detach(&mut self, key: &K) {
370        let Some((prev, next)) = self.map.get_mut(key).map(|entry| {
371            let prev = entry.prev.take();
372            let next = entry.next.take();
373            (prev, next)
374        }) else {
375            return;
376        };
377
378        match &prev {
379            Some(prev_key) => {
380                if let Some(prev_entry) = self.map.get_mut(prev_key) {
381                    prev_entry.next = next.clone();
382                }
383            }
384            None => self.head = next.clone(),
385        }
386        match &next {
387            Some(next_key) => {
388                if let Some(next_entry) = self.map.get_mut(next_key) {
389                    next_entry.prev = prev.clone();
390                }
391            }
392            None => self.tail = prev.clone(),
393        }
394    }
395
396    pub(crate) fn attach_back(&mut self, key: K) {
397        let previous_tail = self.tail.replace(key.clone());
398        match previous_tail {
399            Some(tail_key) => {
400                if let Some(tail_entry) = self.map.get_mut(&tail_key) {
401                    tail_entry.next = Some(key.clone());
402                }
403                if let Some(entry) = self.map.get_mut(&key) {
404                    entry.prev = Some(tail_key);
405                    entry.next = None;
406                }
407            }
408            None => {
409                self.head = Some(key.clone());
410                if let Some(entry) = self.map.get_mut(&key) {
411                    entry.prev = None;
412                    entry.next = None;
413                }
414            }
415        }
416    }
417
418    pub(crate) fn clear(&mut self) {
419        self.map.clear();
420        self.head = None;
421        self.tail = None;
422        self.used = 0;
423    }
424
425    pub(crate) fn put(&mut self, key: K, object: Arc<EncodedObject>) {
426        if self.budget == 0 {
427            return;
428        }
429        let cost = cached_object_cost(&object);
430        // A single object larger than the whole budget is not worth caching; it
431        // would immediately evict everything including itself. Drop any stale
432        // smaller entry stored under the same key so accounting stays exact.
433        if cost > self.budget {
434            self.remove(&key);
435            return;
436        }
437        if let Some(entry) = self.map.get_mut(&key) {
438            let previous = std::mem::replace(&mut entry.object, object);
439            // Replacing an existing entry: adjust accounting and refresh recency.
440            self.used = self
441                .used
442                .saturating_sub(cached_object_cost(&previous))
443                .saturating_add(cost);
444            self.touch(&key);
445        } else {
446            self.used = self.used.saturating_add(cost);
447            self.map.insert(
448                key.clone(),
449                LruEntry {
450                    object,
451                    prev: None,
452                    next: None,
453                },
454            );
455            self.attach_back(key);
456        }
457        while self.used > self.budget {
458            let Some(evicted) = self.head.clone() else {
459                break;
460            };
461            self.remove(&evicted);
462        }
463    }
464}
465
466/// Decoded-object cache keyed by object id (loose + packed reads share it).
467type LruObjectCache = LruCache<ObjectId>;
468/// Delta-base cache keyed by in-pack byte offset, scoped to one pack.
469pub(crate) type LruOffsetCache = LruCache<u64>;
470
471/// Bridges the offset-keyed [`LruOffsetCache`] to [`sley_pack::PackDeltaCache`]
472/// so the pack decoder can reuse decoded delta bases. Holds the shared cache
473/// behind its mutex; a poisoned lock simply behaves as a cache miss/no-op, so a
474/// decode still completes correctly (just without reuse).
475struct PackDeltaCacheAdapter<'a>(&'a Arc<Mutex<LruOffsetCache>>);
476
477impl sley_pack::PackDeltaCache for PackDeltaCacheAdapter<'_> {
478    fn get(&self, offset: u64) -> Option<Arc<EncodedObject>> {
479        self.0.lock().ok()?.get(&offset)
480    }
481
482    fn insert(&self, offset: u64, object: Arc<EncodedObject>) {
483        if let Ok(mut cache) = self.0.lock() {
484            cache.put(offset, object);
485        }
486    }
487}
488
489/// Bridges a per-pack `offset -> ObjectType` memo into the header fast path so
490/// the ofs-delta chain walk is performed at most once per chain across a batch
491/// of `read_object_header` calls (sley#26).
492struct PackHeaderTypeCacheAdapter<'a>(&'a PackHeaderTypeCache);
493
494impl sley_pack::HeaderTypeCache for PackHeaderTypeCacheAdapter<'_> {
495    fn get(&self, pack_offset: u64) -> Option<(ObjectType, u64)> {
496        self.0.lock().ok()?.get(&pack_offset).copied()
497    }
498
499    fn put(&mut self, pack_offset: u64, header: (ObjectType, u64)) {
500        if let Ok(mut cache) = self.0.lock() {
501            cache.insert(pack_offset, header);
502        }
503    }
504}
505
506/// Parsed pack indexes keyed by `.idx` path, shared across cloned handles. This
507/// remains for MIDX and path-only fallback lookups; normal pack-directory scans
508/// use [`PackRegistrySnapshot`] so the lookup hot path can walk already-parsed
509/// pack records directly.
510pub(crate) type PackIndexCache = Arc<RwLock<HashMap<PathBuf, Arc<PackIndexViewData>>>>;
511
512/// Optional `.rev` reverse indexes keyed by `.idx` path, shared across cloned
513/// handles. A cached `None` means no usable reverse index was found.
514pub(crate) type PackReverseIndexCache = Arc<RwLock<HashMap<PathBuf, Option<Arc<PackReverseIndex>>>>>;
515
516/// Parsed multi-pack-index files keyed by path, shared across cloned handles.
517/// Caches the MIDX parse so object lookups in repositories with a MIDX avoid
518/// reparsing the same fanout/object tables for every read.
519pub(crate) type MultiPackIndexCache = Arc<RwLock<HashMap<PathBuf, Arc<MultiPackIndex>>>>;
520
521/// Raw multi-pack-index OID lookup tables keyed by path, shared across cloned
522/// handles. These avoid hashing and materializing every MIDX object when a
523/// command only needs point lookups.
524type MultiPackIndexOidLookupCache = Arc<RwLock<HashMap<PathBuf, Arc<MultiPackIndexOidLookup>>>>;
525
526/// One registered `.idx`/`.pack` pair from a pack directory. The index is parsed
527/// when the registry snapshot is built; pack bytes and per-pack decode/header
528/// caches hang directly off this record so repeated object lookups do not bounce
529/// through path-keyed maps.
530#[derive(Debug, Clone)]
531pub struct FileObjectDatabase {
532    pub(crate) loose: LooseObjectStore,
533    pub(crate) objects_dir: PathBuf,
534    pub(crate) alternates: Vec<PathBuf>,
535    pub(crate) format: ObjectFormat,
536    pub(crate) pack_bytes: PackBytesCache,
537    pub(crate) pack_indexes: PackIndexCache,
538    pub(crate) pack_reverse_indexes: PackReverseIndexCache,
539    pub(crate) multi_pack_indexes: MultiPackIndexCache,
540    pub(crate) multi_pack_oid_lookups: MultiPackIndexOidLookupCache,
541    pub(crate) pack_registry: PackRegistryCache,
542    pub(crate) decoded: DecodedObjectCache,
543    pub(crate) pack_deltas: PackDeltaCaches,
544    pub(crate) pack_header_types: PackHeaderTypeCaches,
545    pub(crate) promisor_objects: Arc<OnceLock<HashSet<ObjectId>>>,
546    /// Whether the owning repository actually has a promisor remote configured
547    /// (`extensions.partialclone` is set, or some `remote.<name>.promisor` is
548    /// true). Mirrors git's `is_promisor_object`, which only treats objects in
549    /// `.promisor` packs as "promised" when `repo_has_promisor_remote()` holds:
550    /// a stray `.promisor` sidecar in a non-partial repo must NOT excuse missing
551    /// objects from fsck. Defaults to `false`; the fsck driver opts in after
552    /// reading the repo config.
553    pub(crate) promisor_remote_present: bool,
554    /// Graft points (`$GIT_DIR/shallow`), loaded lazily on the first
555    /// [`ObjectReader::is_shallow_graft`] query. `$GIT_DIR` is taken to be
556    /// the parent of `objects_dir`, matching the standard layout.
557    pub(crate) shallow_grafts: Arc<std::sync::OnceLock<HashSet<ObjectId>>>,
558}
559
560fn read_shallow_grafts(shallow_file: &Path, format: ObjectFormat) -> HashSet<ObjectId> {
561    let Ok(contents) = std::fs::read_to_string(shallow_file) else {
562        return HashSet::new();
563    };
564    contents
565        .lines()
566        .filter_map(|line| ObjectId::from_hex(format, line.trim()).ok())
567        .collect()
568}
569
570impl FileObjectDatabase {
571    /// The object-id format (hash algorithm) this database was opened with.
572    pub fn object_format(&self) -> ObjectFormat {
573        self.format
574    }
575
576    /// The repository object directory this database reads from.
577    pub fn objects_dir(&self) -> &Path {
578        &self.objects_dir
579    }
580
581    pub fn new(objects_dir: impl Into<PathBuf>, format: ObjectFormat) -> Self {
582        let objects_dir = objects_dir.into();
583        Self {
584            loose: LooseObjectStore::new(objects_dir.clone(), format),
585            alternates: alternate_object_dirs(&objects_dir),
586            objects_dir,
587            format,
588            pack_bytes: Arc::new(RwLock::new(HashMap::new())),
589            pack_indexes: Arc::new(RwLock::new(HashMap::new())),
590            pack_reverse_indexes: Arc::new(RwLock::new(HashMap::new())),
591            multi_pack_indexes: Arc::new(RwLock::new(HashMap::new())),
592            multi_pack_oid_lookups: Arc::new(RwLock::new(HashMap::new())),
593            pack_registry: Arc::new(Mutex::new(None)),
594            decoded: Arc::new(Mutex::new(LruObjectCache::new(object_cache_budget()))),
595            pack_deltas: Arc::new(Mutex::new(HashMap::new())),
596            pack_header_types: Arc::new(Mutex::new(HashMap::new())),
597            promisor_objects: Arc::new(OnceLock::new()),
598            promisor_remote_present: false,
599            shallow_grafts: Arc::new(std::sync::OnceLock::new()),
600        }
601    }
602
603    pub(crate) fn without_alternates(objects_dir: impl Into<PathBuf>, format: ObjectFormat) -> Self {
604        let objects_dir = objects_dir.into();
605        Self {
606            loose: LooseObjectStore::new(objects_dir.clone(), format),
607            alternates: Vec::new(),
608            objects_dir,
609            format,
610            pack_bytes: Arc::new(RwLock::new(HashMap::new())),
611            pack_indexes: Arc::new(RwLock::new(HashMap::new())),
612            pack_reverse_indexes: Arc::new(RwLock::new(HashMap::new())),
613            multi_pack_indexes: Arc::new(RwLock::new(HashMap::new())),
614            multi_pack_oid_lookups: Arc::new(RwLock::new(HashMap::new())),
615            pack_registry: Arc::new(Mutex::new(None)),
616            decoded: Arc::new(Mutex::new(LruObjectCache::new(object_cache_budget()))),
617            pack_deltas: Arc::new(Mutex::new(HashMap::new())),
618            pack_header_types: Arc::new(Mutex::new(HashMap::new())),
619            promisor_objects: Arc::new(OnceLock::new()),
620            promisor_remote_present: false,
621            shallow_grafts: Arc::new(std::sync::OnceLock::new()),
622        }
623    }
624
625    pub fn from_git_dir(git_dir: impl AsRef<Path>, format: ObjectFormat) -> Self {
626        Self::new(repository_objects_dir(git_dir), format)
627    }
628
629    /// Declare whether the owning repository has a promisor remote configured.
630    /// Only when this holds does [`ObjectReader::is_promised_object`] treat
631    /// objects in `.promisor` packs (and their transitive references) as
632    /// promised — matching git's `is_promisor_object`, which is gated on
633    /// `repo_has_promisor_remote()`. Callers that know the repo config (e.g. the
634    /// fsck driver) opt in; readers built without config keep the safe default
635    /// of `false`, so a stray `.promisor` sidecar never silently excuses a
636    /// genuinely missing object.
637    pub fn with_promisor_remote_present(mut self, present: bool) -> Self {
638        self.promisor_remote_present = present;
639        self
640    }
641
642    /// Drop cached pack registries, indexes, and decoded objects so the next read
643    /// sees packs/objects installed after this handle was created (e.g. after
644    /// `fetch` or `install_pack`). Long-lived [`Repository`] sessions call this
645    /// via the owning repository's `refresh_objects` hook.
646    pub fn refresh_read_cache(&self) {
647        if let Ok(mut cache) = self.pack_registry.lock() {
648            *cache = None;
649        }
650        self.pack_indexes.write().clear();
651        self.multi_pack_indexes.write().clear();
652        self.multi_pack_oid_lookups.write().clear();
653        self.pack_bytes.write().clear();
654        if let Ok(mut cache) = self.pack_deltas.lock() {
655            cache.clear();
656        }
657        if let Ok(mut cache) = self.pack_header_types.lock() {
658            cache.clear();
659        }
660        if let Ok(mut cache) = self.decoded.lock() {
661            cache.clear();
662        }
663        self.loose.invalidate_cache();
664    }
665
666    pub fn loose(&self) -> &LooseObjectStore {
667        &self.loose
668    }
669
670    pub fn presence_checker(&self) -> ObjectPresenceChecker {
671        ObjectPresenceChecker::new(self.clone())
672    }
673
674    pub fn contains(&self, oid: &ObjectId) -> Result<bool> {
675        if self.loose.exists(oid)? {
676            return Ok(true);
677        }
678        if self.find_pack_containing(oid)?.is_some() {
679            return Ok(true);
680        }
681        for alternate in &self.alternates {
682            if Self::without_alternates(alternate, self.format).contains(oid)? {
683                return Ok(true);
684            }
685        }
686        // Reprepare-on-miss: a cached negative loose verdict may predate a
687        // sibling write. Drop it and exact-probe once before reporting absence.
688        self.loose.invalidate_cache();
689        self.loose.exists(oid)
690    }
691
692    pub fn object_ids(&self) -> Result<Vec<ObjectId>> {
693        let mut oids = object_ids_in_objects_dir(&self.objects_dir, self.format)?
694            .into_iter()
695            .collect::<HashSet<_>>();
696        for alternate in &self.alternates {
697            oids.extend(Self::without_alternates(alternate, self.format).object_ids()?);
698        }
699        let mut oids = oids.into_iter().collect::<Vec<_>>();
700        oids.sort_by_key(ObjectId::to_hex);
701        Ok(oids)
702    }
703
704    pub fn object_storage_info(&self, oid: &ObjectId) -> Result<Option<ObjectStorageInfo>> {
705        if let Some(disk_size) = self.loose.disk_size(oid)? {
706            return Ok(Some(ObjectStorageInfo {
707                disk_size,
708                deltabase: zero_oid(self.format)?,
709            }));
710        }
711        if let Some(info) = self.packed_object_storage_info(oid)? {
712            return Ok(Some(info));
713        }
714        for alternate in &self.alternates {
715            if let Some(info) =
716                Self::without_alternates(alternate, self.format).object_storage_info(oid)?
717            {
718                return Ok(Some(info));
719            }
720        }
721        // Reprepare-on-miss: drop any stale negative loose cache and exact-probe
722        // once before reporting absence (see `read_object`).
723        self.loose.invalidate_cache();
724        if let Some(disk_size) = self.loose.disk_size(oid)? {
725            return Ok(Some(ObjectStorageInfo {
726                disk_size,
727                deltabase: zero_oid(self.format)?,
728            }));
729        }
730        Ok(None)
731    }
732
733    pub fn resolve_prefix(&self, prefix: &str) -> Result<ObjectPrefixResolution> {
734        let mut matches = self.object_ids_with_prefix(prefix)?;
735        Ok(match matches.len() {
736            0 => ObjectPrefixResolution::Missing,
737            1 => ObjectPrefixResolution::Unique(matches.remove(0)),
738            _ => ObjectPrefixResolution::Ambiguous(matches),
739        })
740    }
741
742    pub fn object_ids_with_prefix(&self, prefix: &str) -> Result<Vec<ObjectId>> {
743        validate_object_id_prefix(self.format, prefix)?;
744        let prefix_bytes = prefix.as_bytes();
745        let mut matches = object_ids_with_prefix_in_objects_dir(
746            &self.objects_dir,
747            self.format,
748            prefix_bytes,
749        )?
750        .into_iter()
751        .collect::<HashSet<_>>();
752        for alternate in &self.alternates {
753            for oid in Self::without_alternates(alternate, self.format)
754                .object_ids_with_prefix(prefix)?
755            {
756                matches.insert(oid);
757            }
758        }
759        let mut matches = matches.into_iter().collect::<Vec<_>>();
760        matches.sort_by_key(ObjectId::to_hex);
761        Ok(matches)
762    }
763
764    /// The object type and content size of `oid` without decoding its full body —
765    /// git's `cat-file --batch-check` fast path. Tries the decoded-object cache,
766    /// then loose storage (inflating only the framing header), then packs (reading
767    /// the entry header and, for deltas, only the delta's leading varints), then
768    /// alternates. Returns `Ok(None)` if the object is not present.
769    ///
770    /// Unlike [`ObjectReader::read_object`], this never materializes the body, so it
771    /// stays cheap on huge blobs and deep delta chains. It does not populate the
772    /// decoded-object cache (nothing is decoded).
773    pub fn read_object_header(&self, oid: &ObjectId) -> Result<Option<(ObjectType, u64)>> {
774        if implied_empty_tree_object(self.format, oid).is_some() {
775            return Ok(Some((ObjectType::Tree, 0)));
776        }
777        if let Ok(mut cache) = self.decoded.lock()
778            && let Some(object) = cache.get(oid)
779        {
780            return Ok(Some((object.object_type, object.body.len() as u64)));
781        }
782        if let Some(header) = self.loose.read_header(oid)? {
783            return Ok(Some(header));
784        }
785        if let Some(pack_lookup) = self.find_pack_containing(oid)? {
786            let bytes = pack_lookup.pack_bytes(self)?;
787            // Per-pack offset->type memo so the ofs-delta chain walk that resolves
788            // a packed object's type runs at most once per chain across the batch,
789            // instead of re-walking (and re-inflating each link's leading varints)
790            // on every header read — the sley#26 super-linear cat-file --batch-check.
791            let type_cache = pack_lookup.header_type_cache(self);
792            let resolve_ref_base = |base: &ObjectId| {
793                self.read_object_header(base)
794                    .map(|header| header.map(|(t, _)| t))
795            };
796            let header = match &type_cache {
797                Some(cache) => {
798                    let mut adapter = PackHeaderTypeCacheAdapter(cache);
799                    sley_pack::read_object_header_at_with_cache(
800                        &bytes,
801                        pack_lookup.offset,
802                        self.format,
803                        resolve_ref_base,
804                        &mut adapter,
805                    )?
806                }
807                None => sley_pack::read_object_header_at(
808                    &bytes,
809                    pack_lookup.offset,
810                    self.format,
811                    resolve_ref_base,
812                )?,
813            };
814            return Ok(Some(header));
815        }
816        for alternate in &self.alternates {
817            if let Some(header) =
818                Self::without_alternates(alternate, self.format).read_object_header(oid)?
819            {
820                return Ok(Some(header));
821            }
822        }
823        // Reprepare-on-miss: discard any stale negative loose cache and retry an
824        // exact path probe once before reporting absence (see `read_object`).
825        self.loose.invalidate_cache();
826        if let Some(header) = self.loose.read_header(oid)? {
827            return Ok(Some(header));
828        }
829        Ok(None)
830    }
831
832    pub(crate) fn read_packed_object(&self, oid: &ObjectId) -> Result<Option<Arc<EncodedObject>>> {
833        // Memory-capped decoded-object cache first (delta-base reuse for ref-delta
834        // bases that resolve back through the store + repeated whole-object reads).
835        if let Ok(mut cache) = self.decoded.lock()
836            && let Some(object) = cache.get(oid)
837        {
838            return Ok(Some(object));
839        }
840        let Some(pack_lookup) = self.find_pack_containing(oid)? else {
841            return Ok(None);
842        };
843        self.read_packed_object_at_lookup(oid, &pack_lookup)
844            .map(Some)
845    }
846
847    pub(crate) fn read_packed_object_at_lookup(
848        &self,
849        oid: &ObjectId,
850        pack_lookup: &PackLookup,
851    ) -> Result<Arc<EncodedObject>> {
852        if let Ok(mut cache) = self.decoded.lock()
853            && let Some(object) = cache.get(oid)
854        {
855            return Ok(object);
856        }
857        let bytes = pack_lookup.pack_bytes(self)?;
858        // Per-pack delta-base cache (keyed by in-pack offset). Resolving an
859        // ofs-delta chain reuses already-decoded bases instead of re-inflating the
860        // whole chain on every read. Scoped to this pack's path so an offset key is
861        // never applied to the wrong pack's bytes.
862        let delta_cache = pack_lookup.delta_cache(self);
863        let delta_adapter = delta_cache.as_ref().map(PackDeltaCacheAdapter);
864        // Decode only this object at its offset (plus its delta-base chain). A
865        // ref-delta base resolves through the full store (loose / other packs) and
866        // reuses the decoded-object cache. No cache lock is held across the decode,
867        // so the recursive resolver re-entry (which may re-enter read_object) is
868        // safe.
869        let resolve_ref_base = |base: &ObjectId| self.read_object(base).map(Some);
870        let resolve_ofs_base =
871            |base_offset| self.read_ofs_delta_base_from_other_sources(pack_lookup, base_offset);
872        let object = match &delta_adapter {
873            Some(adapter) => sley_pack::read_object_at_with_cache_and_ofs_base_arc(
874                &bytes,
875                pack_lookup.offset,
876                self.format,
877                resolve_ref_base,
878                resolve_ofs_base,
879                adapter,
880            )?,
881            None => sley_pack::read_object_at_with_ofs_base_arc(
882                &bytes,
883                pack_lookup.offset,
884                self.format,
885                resolve_ref_base,
886                resolve_ofs_base,
887            )?,
888        };
889        // Trust the index → offset mapping rather than re-hashing every decoded
890        // object on read (see `verify_reads_enabled`); this re-hash dominated
891        // bulk-read cost. Opt back in with `SLEY_VERIFY_READS` for a paranoid check.
892        if verify_reads_enabled() {
893            let actual = object.object_id(self.format)?;
894            if actual != *oid {
895                return Err(GitError::InvalidObject(format!(
896                    "pack object id mismatch: index says {oid}, decoded {actual}"
897                )));
898            }
899        }
900        if let Ok(mut cache) = self.decoded.lock() {
901            cache.put(*oid, Arc::clone(&object));
902        }
903        Ok(object)
904    }
905
906    /// The per-pack delta-base cache for `pack_path`, creating it on first use.
907    /// Returns `None` only if the shared map's lock is poisoned, in which case the
908    /// caller falls back to an uncached decode (correctness preserved).
909    pub(crate) fn pack_delta_cache(&self, pack_path: &Path) -> Option<Arc<Mutex<LruOffsetCache>>> {
910        let mut caches = self.pack_deltas.lock().ok()?;
911        let cache = caches.entry(pack_path.to_path_buf()).or_insert_with(|| {
912            Arc::new(Mutex::new(LruOffsetCache::new(delta_base_cache_budget())))
913        });
914        Some(Arc::clone(cache))
915    }
916
917    /// The per-pack header-type memo for `pack_path`, creating it on first use.
918    /// Returns `None` only if the shared map's lock is poisoned, in which case the
919    /// caller falls back to an unmemoized header walk (correctness preserved).
920    pub(crate) fn pack_header_type_cache(&self, pack_path: &Path) -> Option<PackHeaderTypeCache> {
921        let mut caches = self.pack_header_types.lock().ok()?;
922        let cache = caches
923            .entry(pack_path.to_path_buf())
924            .or_insert_with(|| Arc::new(Mutex::new(HashMap::new())));
925        Some(Arc::clone(cache))
926    }
927
928    /// Backing bytes of the pack at `pack_path`, loaded at most once per database
929    /// handle (cached, shared across clones). Memory-mapped under the `mmap` feature,
930    /// otherwise read into the heap. On a poisoned lock it falls back to loading
931    /// without caching, preserving correctness.
932    pub(crate) fn cached_pack_bytes(&self, pack_path: &Path) -> Result<Arc<PackData>> {
933        if let Some(bytes) = self.pack_bytes.read().get(pack_path)
934        {
935            return Ok(Arc::clone(bytes));
936        }
937        let bytes = Arc::new(load_pack_data(pack_path)?);
938        self.pack_bytes.write().insert(pack_path.to_path_buf(), Arc::clone(&bytes));
939        Ok(bytes)
940    }
941
942    /// Parsed index for the `.idx` at `index_path`, parsed at most once per
943    /// database handle. On a poisoned lock it falls back to parsing without
944    /// caching, preserving correctness.
945    pub(crate) fn cached_pack_index(&self, index_path: &Path) -> Result<Arc<PackIndexViewData>> {
946        if let Some(index) = self.pack_indexes.read().get(index_path)
947        {
948            return Ok(Arc::clone(index));
949        }
950        let index_bytes = load_pack_index_data(index_path)?;
951        let index = Arc::new(PackIndexViewData::parse_trusted_source_without_checksum(
952            index_bytes,
953            self.format,
954        )?);
955        self.pack_indexes.write().insert(index_path.to_path_buf(), Arc::clone(&index));
956        Ok(index)
957    }
958
959    /// Optional reverse index for the `.idx` at `index_path`, loaded at most once
960    /// per database handle when a matching `.rev` sidecar is present.
961    pub(crate) fn cached_pack_reverse_index(
962        &self,
963        index_path: &Path,
964        index: &PackIndexViewData,
965    ) -> Result<Option<Arc<PackReverseIndex>>> {
966        if let Some(cached) = self.pack_reverse_indexes.read().get(index_path)
967        {
968            return Ok(cached.as_ref().map(Arc::clone));
969        }
970        let rev_path = index_path.with_extension("rev");
971        let reverse = if rev_path.exists() {
972            let bytes = fs::read(&rev_path)?;
973            match PackReverseIndex::parse(&bytes, self.format, index.count) {
974                Ok(parsed) if parsed.pack_checksum == index.pack_checksum => {
975                    Some(Arc::new(parsed))
976                }
977                _ => None,
978            }
979        } else {
980            None
981        };
982        self.pack_reverse_indexes.write().insert(index_path.to_path_buf(), reverse.as_ref().map(Arc::clone));
983        Ok(reverse)
984    }
985
986    pub(crate) fn cached_multi_pack_index_oid_lookup(
987        &self,
988        midx_path: &Path,
989    ) -> Result<Option<Arc<MultiPackIndexOidLookup>>> {
990        if !midx_path.exists() {
991            return Ok(None);
992        }
993        if let Some(midx) = self.multi_pack_oid_lookups.read().get(midx_path) {
994            return Ok(Some(Arc::clone(midx)));
995        }
996        let bytes = load_multi_pack_index_lookup_data(midx_path)?;
997        let midx = match MultiPackIndexOidLookup::parse(bytes, self.format) {
998            Ok(midx) => Arc::new(midx),
999            Err(GitError::InvalidFormat(message))
1000                if message.starts_with("multi-pack-index hash id ") =>
1001            {
1002                let actual = message
1003                    .strip_prefix("multi-pack-index hash id ")
1004                    .and_then(|rest| rest.split_whitespace().next())
1005                    .unwrap_or("0");
1006                let expected = match self.format {
1007                    ObjectFormat::Sha1 => 1,
1008                    ObjectFormat::Sha256 => 2,
1009                };
1010                eprintln!(
1011                    "error: multi-pack-index hash version {actual} does not match version {expected}"
1012                );
1013                return Ok(None);
1014            }
1015            Err(err) => return Err(err),
1016        };
1017        self.multi_pack_oid_lookups.write().insert(midx_path.to_path_buf(), Arc::clone(&midx));
1018        Ok(Some(midx))
1019    }
1020
1021    pub(crate) fn cached_multi_pack_index(&self, midx_path: &Path) -> Result<Option<Arc<MultiPackIndex>>> {
1022        if !midx_path.exists() {
1023            return Ok(None);
1024        }
1025        if let Some(midx) = self.multi_pack_indexes.read().get(midx_path)
1026        {
1027            return Ok(Some(Arc::clone(midx)));
1028        }
1029        let bytes = load_multi_pack_index_lookup_data(midx_path)?;
1030        let midx = match MultiPackIndex::parse(bytes.as_bytes(), self.format) {
1031            Ok(midx) => Arc::new(midx),
1032            Err(GitError::InvalidFormat(message))
1033                if message.starts_with("multi-pack-index hash id ") =>
1034            {
1035                let actual = message
1036                    .strip_prefix("multi-pack-index hash id ")
1037                    .and_then(|rest| rest.split_whitespace().next())
1038                    .unwrap_or("0");
1039                let expected = match self.format {
1040                    ObjectFormat::Sha1 => 1,
1041                    ObjectFormat::Sha256 => 2,
1042                };
1043                eprintln!(
1044                    "error: multi-pack-index hash version {actual} does not match version {expected}"
1045                );
1046                return Ok(None);
1047            }
1048            Err(err) => return Err(err),
1049        };
1050        self.multi_pack_indexes.write().insert(midx_path.to_path_buf(), Arc::clone(&midx));
1051        Ok(Some(midx))
1052    }
1053
1054    /// Registry snapshot for this database's pack directory. With `force_rescan`,
1055    /// the directory is re-read; when the fingerprint and pack set match the
1056    /// cached snapshot, the same `Arc` is returned so miss handling can tell that
1057    /// no new packs appeared.
1058    pub(crate) fn cached_pack_registry(
1059        &self,
1060        pack_dir: &Path,
1061        force_rescan: bool,
1062    ) -> Result<Arc<PackRegistrySnapshot>> {
1063        if !force_rescan && let Some(registry) = self.cached_loaded_pack_registry(pack_dir)? {
1064            return Ok(registry);
1065        }
1066        let scanned = Arc::new(scan_pack_registry(pack_dir, self.format)?);
1067        if let Ok(mut cache) = self.pack_registry.lock() {
1068            match cache.as_ref() {
1069                Some(existing)
1070                    if existing.fingerprint == scanned.fingerprint
1071                        && same_registered_pack_set(&existing.packs, &scanned.packs) =>
1072                {
1073                    return Ok(Arc::clone(existing));
1074                }
1075                _ => {
1076                    *cache = Some(Arc::clone(&scanned));
1077                }
1078            }
1079        }
1080        Ok(scanned)
1081    }
1082
1083    pub(crate) fn find_in_pack_registry(
1084        &self,
1085        registry: Arc<PackRegistrySnapshot>,
1086        oid: &ObjectId,
1087    ) -> Result<Option<PackLookup>> {
1088        let hinted_pack_index = registry.cached_hint();
1089        if let Some(pack_index) = hinted_pack_index {
1090            let pack = &registry.packs[pack_index];
1091            match pack.index(self.format) {
1092                Ok(index) => {
1093                    if let Some(entry) = index.find(oid) {
1094                        return Ok(Some(PackLookup::from_registered(
1095                            Arc::clone(pack),
1096                            entry.offset,
1097                        )));
1098                    }
1099                }
1100                Err(_) => {
1101                    eprintln!("error: packfile {} index unavailable", pack.pack.display());
1102                }
1103            }
1104        }
1105        for (pack_index, pack) in registry.packs.iter().enumerate() {
1106            if Some(pack_index) == hinted_pack_index {
1107                continue;
1108            }
1109            let index = match pack.index(self.format) {
1110                Ok(index) => index,
1111                Err(_) => {
1112                    eprintln!("error: packfile {} index unavailable", pack.pack.display());
1113                    continue;
1114                }
1115            };
1116            if let Some(entry) = index.find(oid) {
1117                registry.remember_hint(pack_index);
1118                return Ok(Some(PackLookup::from_registered(
1119                    Arc::clone(pack),
1120                    entry.offset,
1121                )));
1122            }
1123        }
1124        Ok(None)
1125    }
1126
1127    /// Read `oid` from any pack *other than* the one named by `exclude`, used as
1128    /// a corruption fallback: a redundant packed copy survives one pack's
1129    /// damage. Scans the on-disk `.idx` files directly (bypassing the registry
1130    /// cache, whose first hit is the excluded pack) and decodes from the first
1131    /// other pack that both indexes the object and parses cleanly.
1132    pub(crate) fn read_packed_object_from_other_packs(
1133        &self,
1134        oid: &ObjectId,
1135        exclude: &PackLookup,
1136    ) -> Result<Option<Arc<EncodedObject>>> {
1137        let pack_dir = self.objects_dir.join("pack");
1138        let Ok(entries) = fs::read_dir(&pack_dir) else {
1139            return Ok(None);
1140        };
1141        let excluded_pack = exclude.pack_path().to_path_buf();
1142        for entry in entries {
1143            let idx_path = entry?.path();
1144            if idx_path.extension().and_then(|ext| ext.to_str()) != Some("idx") {
1145                continue;
1146            }
1147            let pack_path = idx_path.with_extension("pack");
1148            if pack_path == excluded_pack {
1149                continue;
1150            }
1151            let Ok(idx_bytes) = fs::read(&idx_path) else {
1152                continue;
1153            };
1154            let Ok(index) = PackIndex::parse(&idx_bytes, self.format) else {
1155                continue;
1156            };
1157            let Some(entry) = index.find(oid) else {
1158                continue;
1159            };
1160            let candidate = PackLookup::from_path(pack_path, entry.offset);
1161            if let Ok(object) = self.read_packed_object_at_lookup(oid, &candidate) {
1162                return Ok(Some(object));
1163            }
1164        }
1165        Ok(None)
1166    }
1167
1168    pub(crate) fn pack_oid_at_offset(
1169        &self,
1170        pack_lookup: &PackLookup,
1171        offset: u64,
1172    ) -> Result<Option<ObjectId>> {
1173        let index_path = match &pack_lookup.registered {
1174            Some(pack) => pack.idx.clone(),
1175            None => pack_lookup.pack.with_extension("idx"),
1176        };
1177        match pack_lookup.pack_index(self) {
1178            Ok(index) => {
1179                if let Some(reverse) = self.cached_pack_reverse_index(&index_path, &index)? {
1180                    Ok(reverse.oid_at_offset(&index, offset))
1181                } else {
1182                    Ok(index.oid_at_offset_linear(offset))
1183                }
1184            }
1185            Err(_) => self.midx_oid_for_pack_offset(pack_lookup, offset),
1186        }
1187    }
1188
1189    pub(crate) fn read_ofs_delta_base_from_other_sources(
1190        &self,
1191        pack_lookup: &PackLookup,
1192        base_offset: u64,
1193    ) -> Result<Option<Arc<EncodedObject>>> {
1194        let Some(base_oid) = self.pack_oid_at_offset(pack_lookup, base_offset)? else {
1195            return Ok(None);
1196        };
1197        if let Ok(mut cache) = self.decoded.lock()
1198            && let Some(object) = cache.get(&base_oid)
1199        {
1200            return Ok(Some(object));
1201        }
1202        if let Ok(object) = self.loose.read_object(&base_oid) {
1203            return Ok(Some(object));
1204        }
1205        if let Some(object) = self.read_packed_object_from_other_packs(&base_oid, pack_lookup)? {
1206            return Ok(Some(object));
1207        }
1208        for alternate in &self.alternates {
1209            if let Ok(object) =
1210                Self::without_alternates(alternate, self.format).read_object(&base_oid)
1211            {
1212                return Ok(Some(object));
1213            }
1214        }
1215        Ok(None)
1216    }
1217
1218    pub(crate) fn find_pack_containing(&self, oid: &ObjectId) -> Result<Option<PackLookup>> {
1219        if oid.format() != self.format {
1220            return Err(GitError::InvalidObjectId(format!(
1221                "object {oid} uses {}, store uses {}",
1222                oid.format().name(),
1223                self.format.name()
1224            )));
1225        }
1226        let pack_dir = self.objects_dir.join("pack");
1227        // Hot path: a previously cached pack registry or multi-pack-index already
1228        // names every pack, and locating `oid` in them is pure in-memory index
1229        // work. Try that first so a warm handle does not parse indexes or hash
1230        // pack paths on every lookup.
1231        if let Some(midx) = self.cached_loaded_multi_pack_index_oid_lookup()
1232            && let Some(pack_paths) = self.midx_oid_lookup_pack_paths(&pack_dir, &midx, oid)?
1233        {
1234            return Ok(Some(pack_paths));
1235        }
1236        if let Some(registry) = self.cached_loaded_pack_registry(&pack_dir)?
1237            && let Some(pack_paths) = self.find_in_pack_registry(registry, oid)?
1238        {
1239            return Ok(Some(pack_paths));
1240        }
1241
1242        if !pack_dir.exists() {
1243            return Ok(None);
1244        }
1245        if let Some(pack_paths) = self.find_midx_pack_containing(&pack_dir, oid)? {
1246            return Ok(Some(pack_paths));
1247        }
1248        // Search the cached registry first. On a complete miss, re-scan the
1249        // directory once (picking up any pack added since the registry was
1250        // cached) and search again, so newly written packs are still found.
1251        let registry = self.cached_pack_registry(&pack_dir, false)?;
1252        if let Some(pack_paths) = self.find_in_pack_registry(Arc::clone(&registry), oid)? {
1253            return Ok(Some(pack_paths));
1254        }
1255        let refreshed = self.cached_pack_registry(&pack_dir, true)?;
1256        if Arc::ptr_eq(&registry, &refreshed) {
1257            // The re-scan produced the same registry, so nothing new appeared.
1258            return Ok(None);
1259        }
1260        self.find_in_pack_registry(refreshed, oid)
1261    }
1262
1263    pub(crate) fn packed_object_storage_info(&self, oid: &ObjectId) -> Result<Option<ObjectStorageInfo>> {
1264        let Some(pack_lookup) = self.find_pack_containing(oid)? else {
1265            return Ok(None);
1266        };
1267        let index = pack_lookup.pack_index(self).ok();
1268        let pack = match pack_lookup.pack_bytes(self) {
1269            Ok(pack) => Some(pack),
1270            Err(_err) if index.is_some() => None,
1271            Err(err) => return Err(err),
1272        };
1273        let trailer_offset = pack
1274            .as_ref()
1275            .map(|pack| {
1276                (pack.len() as u64)
1277                    .checked_sub(self.format.raw_len() as u64)
1278                    .ok_or_else(|| {
1279                        GitError::InvalidFormat("pack file shorter than checksum".into())
1280                    })
1281            })
1282            .transpose()?;
1283        let delta_base = match &pack {
1284            Some(pack) => pack_entry_delta_base(self.format, pack, pack_lookup.offset)?,
1285            None => None,
1286        };
1287        let delta_base_offset = match &delta_base {
1288            Some(PackDeltaBase::Offset(offset)) => Some(*offset),
1289            Some(PackDeltaBase::Ref(_)) | None => None,
1290        };
1291        let offset_info = if let Some(index) = &index {
1292            scan_pack_index_offsets(index, pack_lookup.offset, trailer_offset, delta_base_offset)?
1293        } else if let Some(pack) = &pack {
1294            let end_offset =
1295                scan_pack_offsets_without_index(self.format, pack, pack_lookup.offset)?
1296                    .ok_or_else(|| {
1297                        GitError::InvalidFormat(format!(
1298                            "pack offset {} not found",
1299                            pack_lookup.offset
1300                        ))
1301                    })?;
1302            let delta_base_oid = match delta_base_offset {
1303                Some(offset) => self
1304                    .midx_oid_for_pack_offset(&pack_lookup, offset)?
1305                    .ok_or_else(|| {
1306                        GitError::InvalidFormat(format!("ofs-delta base offset {offset} not found"))
1307                    })?,
1308                None => zero_oid(self.format)?,
1309            };
1310            PackIndexOffsetInfo {
1311                end_offset,
1312                delta_base_oid: delta_base_offset.map(|_| delta_base_oid),
1313            }
1314        } else {
1315            return Err(GitError::InvalidFormat(
1316                "packed object metadata source unavailable".into(),
1317            ));
1318        };
1319        let disk_size = offset_info
1320            .end_offset
1321            .checked_sub(pack_lookup.offset)
1322            .ok_or_else(|| GitError::InvalidFormat("pack index offsets are not sorted".into()))?;
1323        let deltabase = match delta_base {
1324            Some(PackDeltaBase::Offset(_)) => offset_info.delta_base_oid.ok_or_else(|| {
1325                // scan_pack_index_offsets returns Err when delta_base_offset is
1326                // Some but no matching entry is found, so this is unreachable for
1327                // valid packs; propagate as an error rather than panic to keep a
1328                // malformed pack from taking down the process if that invariant
1329                // ever drifts.
1330                GitError::InvalidFormat("ofs-delta base oid missing from pack index".into())
1331            })?,
1332            Some(PackDeltaBase::Ref(oid)) => oid,
1333            None => zero_oid(self.format)?,
1334        };
1335        Ok(Some(ObjectStorageInfo {
1336            disk_size,
1337            deltabase,
1338        }))
1339    }
1340
1341    pub(crate) fn midx_oid_for_pack_offset(
1342        &self,
1343        pack_lookup: &PackLookup,
1344        offset: u64,
1345    ) -> Result<Option<ObjectId>> {
1346        let pack_dir = self.objects_dir.join("pack");
1347        let midx_path = pack_dir.join("multi-pack-index");
1348        let Some(midx) = self.cached_multi_pack_index(&midx_path)? else {
1349            return Ok(None);
1350        };
1351        let Some(pack_name) = pack_lookup
1352            .pack_path()
1353            .file_name()
1354            .and_then(|name| name.to_str())
1355        else {
1356            return Ok(None);
1357        };
1358        let idx_name = pack_name
1359            .strip_suffix(".pack")
1360            .map(|stem| format!("{stem}.idx"))
1361            .unwrap_or_else(|| pack_name.to_string());
1362        let Some(pack_int_id) = midx
1363            .pack_names
1364            .iter()
1365            .position(|candidate| candidate == &idx_name)
1366        else {
1367            return Ok(None);
1368        };
1369        Ok(midx
1370            .objects
1371            .iter()
1372            .find(|entry| entry.pack_int_id == pack_int_id as u32 && entry.offset == offset)
1373            .map(|entry| entry.oid))
1374    }
1375
1376    pub(crate) fn find_midx_pack_containing(
1377        &self,
1378        pack_dir: &Path,
1379        oid: &ObjectId,
1380    ) -> Result<Option<PackLookup>> {
1381        let midx_path = pack_dir.join("multi-pack-index");
1382        if let Some(midx) = self.cached_multi_pack_index_oid_lookup(&midx_path)?
1383            && let Some(pack_lookup) = self.midx_oid_lookup_pack_paths(pack_dir, &midx, oid)?
1384        {
1385            return Ok(Some(pack_lookup));
1386        }
1387        self.find_incremental_midx_pack_containing(pack_dir, oid)
1388    }
1389
1390    pub(crate) fn midx_oid_lookup_pack_paths(
1391        &self,
1392        pack_dir: &Path,
1393        midx: &MultiPackIndexOidLookup,
1394        oid: &ObjectId,
1395    ) -> Result<Option<PackLookup>> {
1396        let Some(entry) = midx.find(oid)? else {
1397            return Ok(None);
1398        };
1399        let Some(pack_name) = midx.pack_name(entry.pack_int_id) else {
1400            return Err(GitError::InvalidFormat(
1401                "multi-pack-index object points past pack table".into(),
1402            ));
1403        };
1404        let pack_file_name = pack_name
1405            .strip_suffix(".idx")
1406            .map(|stem| format!("{stem}.pack"))
1407            .unwrap_or_else(|| pack_name.to_string());
1408        let pack = pack_dir.join(pack_file_name);
1409        Ok(Some(PackLookup::from_path(pack, entry.offset)))
1410    }
1411
1412    pub(crate) fn find_incremental_midx_pack_containing(
1413        &self,
1414        pack_dir: &Path,
1415        oid: &ObjectId,
1416    ) -> Result<Option<PackLookup>> {
1417        let chain = read_incremental_midx_chain(pack_dir)?;
1418        if chain.is_empty() {
1419            return Ok(None);
1420        }
1421        let midx_dir = pack_dir.join("multi-pack-index.d");
1422        for checksum in chain.iter().rev() {
1423            let path = midx_dir.join(format!("multi-pack-index-{checksum}.midx"));
1424            if !path.exists() {
1425                continue;
1426            }
1427            let bytes = load_multi_pack_index_lookup_data(&path)?;
1428            let midx = match MultiPackIndexOidLookup::parse(bytes, self.format) {
1429                Ok(midx) => midx,
1430                Err(_) => continue,
1431            };
1432            if let Some(pack_lookup) = self.midx_oid_lookup_pack_paths(pack_dir, &midx, oid)? {
1433                return Ok(Some(pack_lookup));
1434            }
1435        }
1436        Ok(None)
1437    }
1438
1439    pub(crate) fn cached_loaded_multi_pack_index_oid_lookup(&self) -> Option<Arc<MultiPackIndexOidLookup>> {
1440        let midx_path = self.objects_dir.join("pack").join("multi-pack-index");
1441        self.multi_pack_oid_lookups.read().get(&midx_path).map(Arc::clone)
1442    }
1443
1444    /// The pack registry for `pack_dir` *only if already scanned and cached* —
1445    /// never touches the filesystem. Used by the lookup hot path to skip
1446    /// per-object pack-dir metadata checks once a handle is warm. A cold cache
1447    /// returns `None`, so the caller falls back to the scanning path. A complete
1448    /// miss still forces one rescan, preserving the new-pack discovery semantics.
1449    pub(crate) fn cached_loaded_pack_registry(
1450        &self,
1451        _pack_dir: &Path,
1452    ) -> Result<Option<Arc<PackRegistrySnapshot>>> {
1453        let cache = match self.pack_registry.lock() {
1454            Ok(cache) => cache,
1455            Err(_) => return Ok(None),
1456        };
1457        Ok(cache.as_ref().map(Arc::clone))
1458    }
1459}
1460impl ObjectReader for FileObjectDatabase {
1461    fn is_promised_object(&self, oid: &ObjectId) -> bool {
1462        // Gate on a configured promisor remote, exactly like git's
1463        // `is_promisor_object` (which short-circuits when
1464        // `repo_has_promisor_remote()` is false). Without this, a `.promisor`
1465        // sidecar left in an ordinary repository would wrongly excuse missing
1466        // objects from fsck connectivity checks.
1467        self.promisor_remote_present && self.promisor_objects().contains(oid)
1468    }
1469
1470    fn has_shallow_grafts(&self) -> bool {
1471        !self
1472            .shallow_grafts
1473            .get_or_init(|| {
1474                let shallow_file = self
1475                    .objects_dir
1476                    .parent()
1477                    .map(|git_dir| git_dir.join("shallow"));
1478                match shallow_file {
1479                    Some(path) => read_shallow_grafts(&path, self.format),
1480                    None => HashSet::new(),
1481                }
1482            })
1483            .is_empty()
1484    }
1485
1486    fn is_shallow_graft(&self, oid: &ObjectId) -> bool {
1487        self.shallow_grafts
1488            .get_or_init(|| {
1489                let shallow_file = self
1490                    .objects_dir
1491                    .parent()
1492                    .map(|git_dir| git_dir.join("shallow"));
1493                match shallow_file {
1494                    Some(path) => read_shallow_grafts(&path, self.format),
1495                    None => HashSet::new(),
1496                }
1497            })
1498            .contains(oid)
1499    }
1500
1501    fn read_object(&self, oid: &ObjectId) -> Result<Arc<EncodedObject>> {
1502        if let Some(object) = implied_empty_tree_object(self.format, oid) {
1503            return Ok(object);
1504        }
1505        // A corrupt loose copy must not shadow a good packed copy: git's
1506        // `oid_object_info_extended` consults every source, so a repacked object
1507        // whose loose file was later corrupted still reads fine from the pack. If
1508        // a packed copy exists, prefer it WITHOUT touching the corrupt loose file
1509        // (which would otherwise emit a spurious `inflate:` diagnostic on each
1510        // probe). Only when no pack copy exists do we read (and, if corrupt,
1511        // surface the error from) the loose file.
1512        if let Some(pack_lookup) = self.find_pack_containing(oid)? {
1513            match self.read_packed_object_at_lookup(oid, &pack_lookup) {
1514                Ok(object) => return Ok(object),
1515                Err(GitError::NotFound(_)) => {}
1516                // A corrupt packed copy must not be fatal when another good copy
1517                // exists: git's `oid_object_info_extended` keeps consulting the
1518                // remaining sources (loose, other packs, alternates) when a pack
1519                // read fails. Fall through to the loose/other-pack probes and
1520                // only surface the packed error if every source comes up empty.
1521                Err(packed_err) => {
1522                    if let Ok(object) = self.loose.read_object(oid) {
1523                        return Ok(object);
1524                    }
1525                    // Try any *other* pack that also holds the object (a
1526                    // redundant copy survives one pack's corruption).
1527                    if let Some(object) =
1528                        self.read_packed_object_from_other_packs(oid, &pack_lookup)?
1529                    {
1530                        return Ok(object);
1531                    }
1532                    for alternate in &self.alternates {
1533                        if let Ok(object) =
1534                            Self::without_alternates(alternate, self.format).read_object(oid)
1535                        {
1536                            return Ok(object);
1537                        }
1538                    }
1539                    return Err(packed_err);
1540                }
1541            }
1542        }
1543        let loose_err = match self.loose.read_object(oid) {
1544            Ok(object) => return Ok(object),
1545            Err(GitError::NotFound(_)) => None,
1546            Err(err) => Some(err),
1547        };
1548        if let Some(object) = self.read_packed_object(oid)? {
1549            return Ok(object);
1550        }
1551        for alternate in &self.alternates {
1552            match Self::without_alternates(alternate, self.format).read_object(oid) {
1553                Ok(object) => return Ok(object),
1554                Err(GitError::NotFound(_)) => {}
1555                Err(err) => return Err(err),
1556            }
1557        }
1558        // Hard miss against every store. If an earlier enumeration built a loose
1559        // cache, an object written loose afterward by a sibling handle could have
1560        // been skipped above. Mirror git's `oid_object_info_extended`
1561        // reprepare-on-miss: drop stale cache state and retry an exact loose path
1562        // probe once before declaring the object missing.
1563        self.loose.invalidate_cache();
1564        match self.loose.read_object(oid) {
1565            Ok(object) => return Ok(object),
1566            Err(GitError::NotFound(_)) => {}
1567            Err(err) => return Err(err),
1568        }
1569        // No good copy in any store. If the local loose copy was corrupt (not
1570        // merely absent), surface that error — it is more specific than a plain
1571        // "not found".
1572        if let Some(err) = loose_err {
1573            return Err(err);
1574        }
1575        Err(GitError::object_not_found_in(
1576            *oid,
1577            MissingObjectContext::Read,
1578        ))
1579    }
1580}
1581impl FileObjectDatabase {
1582    fn promisor_objects(&self) -> &HashSet<ObjectId> {
1583        self.promisor_objects.get_or_init(|| {
1584            let mut promised =
1585                promisor_pack_object_ids(&self.objects_dir, self.format).unwrap_or_default();
1586            let mut pending = promised.iter().copied().collect::<Vec<_>>();
1587            while let Some(oid) = pending.pop() {
1588                let Ok(object) = self.read_object(&oid) else {
1589                    continue;
1590                };
1591                for link in promisor_object_links(self.format, &object) {
1592                    if promised.insert(link) {
1593                        pending.push(link);
1594                    }
1595                }
1596            }
1597            promised
1598        })
1599    }
1600
1601    fn freshen_existing_object(&self, oid: &ObjectId) -> Result<bool> {
1602        if self.freshen_loose_object(oid)? {
1603            return Ok(true);
1604        }
1605        if self.freshen_packed_object(oid)? {
1606            return Ok(true);
1607        }
1608        for alternate in &self.alternates {
1609            if Self::without_alternates(alternate, self.format).freshen_existing_object(oid)? {
1610                return Ok(true);
1611            }
1612        }
1613        // A previous negative loose-cache probe may predate a sibling write.
1614        self.loose.invalidate_cache();
1615        self.freshen_loose_object(oid)
1616    }
1617
1618    fn freshen_loose_object(&self, oid: &ObjectId) -> Result<bool> {
1619        let path = self.loose.object_path(oid)?;
1620        freshen_file_mtime(&path)
1621    }
1622
1623    fn freshen_packed_object(&self, oid: &ObjectId) -> Result<bool> {
1624        let Some(pack_lookup) = self.find_pack_containing(oid)? else {
1625            return Ok(false);
1626        };
1627        freshen_file_mtime(pack_lookup.pack_path())
1628    }
1629}
1630pub(crate) fn freshen_file_mtime(path: &Path) -> Result<bool> {
1631    let file = match fs::OpenOptions::new().read(true).open(path) {
1632        Ok(file) => file,
1633        Err(err) if err.kind() == std::io::ErrorKind::NotFound => return Ok(false),
1634        Err(err) => return Err(GitError::Io(err.to_string())),
1635    };
1636    file.set_modified(std::time::SystemTime::now())
1637        .map_err(|err| GitError::Io(err.to_string()))?;
1638    Ok(true)
1639}
1640
1641pub(crate) fn promisor_pack_object_ids(objects_dir: &Path, format: ObjectFormat) -> Result<HashSet<ObjectId>> {
1642    let pack_dir = objects_dir.join("pack");
1643    let mut oids = HashSet::new();
1644    if !pack_dir.exists() {
1645        return Ok(oids);
1646    }
1647    for entry in fs::read_dir(pack_dir)? {
1648        let path = entry?.path();
1649        if path.extension().and_then(|ext| ext.to_str()) != Some("idx") {
1650            continue;
1651        }
1652        if !path.with_extension("pack").exists() || !path.with_extension("promisor").exists() {
1653            continue;
1654        }
1655        let index = PackIndex::parse(&fs::read(path)?, format)?;
1656        oids.extend(index.entries.into_iter().map(|entry| entry.oid));
1657    }
1658    Ok(oids)
1659}
1660
1661pub(crate) fn promisor_object_links(format: ObjectFormat, object: &EncodedObject) -> Vec<ObjectId> {
1662    match object.object_type {
1663        ObjectType::Commit => Commit::parse_ref(format, &object.body)
1664            .map(|commit| {
1665                let mut links = Vec::with_capacity(commit.parents.len() + 1);
1666                links.push(commit.tree);
1667                links.extend(commit.parents);
1668                links
1669            })
1670            .unwrap_or_default(),
1671        ObjectType::Tree => TreeEntries::new(format, &object.body)
1672            .filter_map(|entry| entry.ok().map(|entry| entry.oid))
1673            .collect(),
1674        ObjectType::Tag => Tag::parse_ref(format, &object.body)
1675            .map(|tag| vec![tag.object])
1676            .unwrap_or_default(),
1677        ObjectType::Blob => Vec::new(),
1678    }
1679}
1680impl ObjectWriter for FileObjectDatabase {
1681    fn write_object(&self, object: EncodedObject) -> Result<ObjectId> {
1682        // Mirror git's freshen semantics (`write_object_file`:
1683        // `freshen_packed_object || freshen_loose_object`): an object already
1684        // present anywhere in the database is not written again, but its backing
1685        // loose object or pack is touched so concurrent GC treats it as recent.
1686        let oid = object.object_id(self.format)?;
1687        if self.freshen_existing_object(&oid)? {
1688            return Ok(oid);
1689        }
1690        self.loose.write_object(object)
1691    }
1692}