Skip to main content

grit_lib/
pack.rs

1//! Pack and pack-index helpers for object counting and verification.
2//!
3//! This module implements a focused subset of pack functionality required by
4//! `count-objects` and `verify-pack`.
5
6pub use crate::pack_index::{
7    compute_fanout_from_entries, compute_fanout_from_oid_slices, pack_index_entry_matches_sha1_oid,
8    parse_pack_index_bytes, PackIndex, PackIndexEntry, PackIndexEntryRef,
9};
10
11use crate::error::{Error, Result};
12use crate::git_path::CanonicalizeExt;
13use crate::hash::{hash_object, verify_trailer};
14use crate::objects::{HashAlgo, Object, ObjectId, ObjectInfo, ObjectKind};
15use crate::odb::BoundedRead;
16pub use crate::pack_map::PackData;
17use crate::pack_map::ResidentBudget;
18use crate::unpack_objects::apply_delta_into;
19use crate::zlib_inflate::ZlibInflateScratch;
20use std::cell::RefCell;
21use std::collections::{BTreeMap, HashMap, HashSet};
22use std::fs;
23use std::io;
24use std::path::{Path, PathBuf};
25use std::sync::Arc;
26use std::time::SystemTime;
27
28/// Basic information about local packs.
29#[derive(Debug, Clone, Default)]
30pub struct LocalPackInfo {
31    /// Number of valid local packs.
32    pub pack_count: usize,
33    /// Total objects across all valid local packs.
34    pub object_count: usize,
35    /// Combined on-disk bytes of `.pack` + `.idx`.
36    pub size_bytes: u64,
37    /// Set of all object IDs present in local packs.
38    pub object_ids: HashSet<ObjectId>,
39}
40
41/// Read all valid `.idx` files in `objects/pack`.
42///
43/// # Errors
44///
45/// Returns [`Error::Io`] for directory-level failures. Individual invalid pack
46/// pairs are skipped.
47pub fn read_local_pack_indexes(objects_dir: &Path) -> Result<Vec<PackIndex>> {
48    let pack_dir = objects_dir.join("pack");
49    let rd = match fs::read_dir(&pack_dir) {
50        Ok(rd) => rd,
51        Err(err) if err.kind() == io::ErrorKind::NotFound => return Ok(Vec::new()),
52        Err(err) => return Err(Error::Io(err)),
53    };
54
55    let mut out = Vec::new();
56    for entry in rd {
57        let entry = entry.map_err(Error::Io)?;
58        let path = entry.path();
59        if path.extension().and_then(|s| s.to_str()) != Some("idx") {
60            continue;
61        }
62        if let Ok(idx) = read_pack_index(&path) {
63            // Ignore orphan `.idx` files (no `.pack`). They must not make `fsck` think objects
64            // exist (`t7700-repack`); repack also skips them so a stray index does not block work.
65            if !idx.pack_path.is_file() {
66                continue;
67            }
68            out.push(idx);
69        }
70    }
71    Ok(out)
72}
73
74/// Process-wide cache of parsed pack indexes and pack file bytes.
75///
76/// Hot lookups serve cached directory listings, parsed `.idx` files, and `.pack` bytes
77/// without `stat` per access (Git prepares the pack list once and only re-scans on a
78/// lookup miss when the pack directory's mtime changed). Pack bytes are re-read only on
79/// cache miss or when parsing a pack object fails and the on-disk signature changed.
80///
81/// SHA-1 verification of the index trailer is **not** performed on cached reads: Git only
82/// verifies pack indexes during `fsck`/`verify-pack`, not on every object lookup. Use
83/// [`read_pack_index`] when verification is required.
84pub(crate) mod pack_cache {
85    use super::{read_pack_index_no_verify, Error, ObjectKind, PackIndex, Result};
86    use crate::pack_map::{fingerprint_from_file, PackData, PackFingerprint};
87    use std::collections::{HashMap, HashSet, VecDeque};
88    use std::fs;
89    use std::hash::{Hash, Hasher};
90    use std::io;
91    use std::path::{Path, PathBuf};
92    use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
93    use std::sync::{Arc, Mutex, Weak};
94    use std::time::SystemTime;
95
96    const DELTA_LRU_SHARD_COUNT: usize = 32;
97    // A bounded operation must release descriptors and compressed windows together.
98    // Active readers can keep their own Arc alive across a catalog eviction.
99    const BOUNDED_PACK_READERS: usize = 32;
100
101    type ThreadLocalDirListing = Option<(PathBuf, SystemTime, Vec<Arc<PackIndex>>)>;
102
103    enum CachedPackBytes {
104        Retained(Arc<PackData>),
105        Positioned(Weak<PackData>),
106    }
107
108    impl CachedPackBytes {
109        fn get(&self) -> Option<Arc<PackData>> {
110            match self {
111                Self::Retained(bytes) => Some(Arc::clone(bytes)),
112                Self::Positioned(bytes) => bytes.upgrade(),
113            }
114        }
115    }
116
117    // hygiene: per-thread pack read hot cache (indexes, bytes, dir listing); cleared on invalidate
118    thread_local! {
119        static PACK_DIR_INDEXES: std::cell::RefCell<ThreadLocalDirListing> =
120            const { std::cell::RefCell::new(None) };
121        static PACK_HIT_INDEX: std::cell::RefCell<Option<(PathBuf, Arc<PackIndex>)>> =
122            const { std::cell::RefCell::new(None) };
123        static PACK_BYTES: std::cell::RefCell<HashMap<PathBuf, CachedPackBytes>> =
124            std::cell::RefCell::new(HashMap::new());
125        static PACK_INDEXES: std::cell::RefCell<HashMap<PathBuf, Arc<PackIndex>>> =
126            std::cell::RefCell::new(HashMap::new());
127        /// Looked up for every object read, so keyed by the path's bytes with a fast
128        /// hasher rather than by [`Path`] components; a miss falls back to the catalog.
129        static PACK_IDS: std::cell::RefCell<hashbrown::HashMap<std::ffi::OsString, u32>> =
130            std::cell::RefCell::new(hashbrown::HashMap::new());
131        static PACK_COUNT_VALIDATED: std::cell::RefCell<HashSet<PathBuf>> =
132            std::cell::RefCell::new(HashSet::new());
133        static PACK_DIR_LISTING_TRUSTED: std::cell::Cell<bool> = const { std::cell::Cell::new(false) };
134        /// [`PackStore::cache_epoch`] last seen on this thread (cross-thread invalidation).
135        static THREAD_WARM_EPOCH: std::cell::Cell<u64> = const { std::cell::Cell::new(0) };
136        /// [`PackStore::id`] last warmed on this thread (distinct stores can share an epoch).
137        static THREAD_WARM_STORE: std::cell::Cell<u64> = const { std::cell::Cell::new(0) };
138        static PACK_MONOTONIC_CURSOR: std::cell::RefCell<Option<(PathBuf, usize)>> =
139            const { std::cell::RefCell::new(None) };
140        static SINGLE_READER_MEMO: std::cell::RefCell<Option<SingleReaderMemo>> =
141            const { std::cell::RefCell::new(None) };
142    }
143
144    /// The pack a [`super::SingleLocalPackReader`] last opened on this thread.
145    ///
146    /// Lets repeated opens (one per object read) skip re-deriving the same pack through the
147    /// path-keyed caches and the shared catalog.
148    struct SingleReaderMemo {
149        objects_dir: PathBuf,
150        pack_dir: PathBuf,
151        idx: Arc<PackIndex>,
152        pack_bytes: Arc<PackData>,
153        pack_id: u32,
154    }
155
156    fn forget_single_reader_memo() {
157        SINGLE_READER_MEMO.with(|cell| *cell.borrow_mut() = None);
158    }
159
160    /// Record the single local pack just opened for `objects_dir`.
161    pub(crate) fn remember_single_reader(
162        objects_dir: &Path,
163        idx: &Arc<PackIndex>,
164        pack_bytes: &Arc<PackData>,
165        pack_id: u32,
166    ) {
167        SINGLE_READER_MEMO.with(|cell| {
168            *cell.borrow_mut() = Some(SingleReaderMemo {
169                objects_dir: objects_dir.to_path_buf(),
170                pack_dir: objects_dir.join("pack"),
171                idx: Arc::clone(idx),
172                pack_bytes: Arc::clone(pack_bytes),
173                pack_id,
174            });
175        });
176    }
177
178    /// The single local pack previously opened for `objects_dir`, if this thread's trusted
179    /// pack listing still consists of exactly that pack.
180    ///
181    /// Returns `None` whenever the listing is untrusted (so the caller rechecks the pack
182    /// directory) or has changed since the pack was remembered.
183    pub(crate) fn single_reader_for(
184        objects_dir: &Path,
185    ) -> Option<(Arc<PackIndex>, Arc<PackData>, u32)> {
186        if !PACK_DIR_LISTING_TRUSTED.get() {
187            return None;
188        }
189        SINGLE_READER_MEMO.with(|memo| {
190            let memo = memo.borrow();
191            let memo = memo.as_ref()?;
192            if memo.objects_dir.as_os_str() != objects_dir.as_os_str() {
193                return None;
194            }
195            let listing_unchanged = PACK_DIR_INDEXES.with(|cell| {
196                cell.borrow().as_ref().is_some_and(|(dir, _, indexes)| {
197                    dir.as_os_str() == memo.pack_dir.as_os_str()
198                        && indexes.len() == 1
199                        && Arc::ptr_eq(&indexes[0], &memo.idx)
200                })
201            });
202            if !listing_unchanged {
203                return None;
204            }
205            let hit_current = PACK_HIT_INDEX.with(|cell| {
206                cell.borrow().as_ref().is_some_and(|(dir, idx)| {
207                    dir.as_os_str() == objects_dir.as_os_str() && Arc::ptr_eq(idx, &memo.idx)
208                })
209            });
210            if !hit_current {
211                return None;
212            }
213            Some((
214                Arc::clone(&memo.idx),
215                Arc::clone(&memo.pack_bytes),
216                memo.pack_id,
217            ))
218        })
219    }
220
221    fn store_thread_local_key(store: &crate::pack_store::PackStore) -> u64 {
222        store.id()
223    }
224
225    pub(crate) fn clear_thread_local_dir_indexes() {
226        PACK_DIR_INDEXES.with(|cell| *cell.borrow_mut() = None);
227        PACK_HIT_INDEX.with(|cell| *cell.borrow_mut() = None);
228        PACK_BYTES.with(|cell| cell.borrow_mut().clear());
229        PACK_INDEXES.with(|cell| cell.borrow_mut().clear());
230        PACK_IDS.with(|cell| cell.borrow_mut().clear());
231        PACK_COUNT_VALIDATED.with(|cell| cell.borrow_mut().clear());
232        PACK_DIR_LISTING_TRUSTED.set(false);
233        THREAD_WARM_EPOCH.set(0);
234        THREAD_WARM_STORE.set(0);
235        super::clear_pack_read_info_cache();
236        PACK_MONOTONIC_CURSOR.with(|cell| *cell.borrow_mut() = None);
237        forget_single_reader_memo();
238    }
239
240    pub(crate) fn try_read_object_via_single_pack_reader(
241        objects_dir: &Path,
242        oid: &crate::objects::ObjectId,
243    ) -> Result<Option<crate::objects::Object>> {
244        if !crate::pack_store::PackStore::read_context_matches_objects_dir(objects_dir) {
245            return Ok(None);
246        }
247        let Some(mut reader) = super::SingleLocalPackReader::open_allow_loose(objects_dir)? else {
248            return Ok(None);
249        };
250        match reader.read_object(oid) {
251            Ok(obj) => Ok(Some(obj)),
252            Err(Error::ObjectNotFound(_)) => Ok(None),
253            Err(err) => Err(err),
254        }
255    }
256
257    /// Like [`try_read_object_via_single_pack_reader`], skipping objects the pack stores whole
258    /// with a payload larger than `limit` (see
259    /// [`super::SingleLocalPackReader::read_object_unless_whole_larger`]).
260    pub(crate) fn try_read_object_via_single_pack_reader_unless_whole_larger(
261        objects_dir: &Path,
262        oid: &crate::objects::ObjectId,
263        limit: u64,
264    ) -> Result<Option<super::BoundedRead>> {
265        if !crate::pack_store::PackStore::read_context_matches_objects_dir(objects_dir) {
266            return Ok(None);
267        }
268        let Some(mut reader) = super::SingleLocalPackReader::open_allow_loose(objects_dir)? else {
269            return Ok(None);
270        };
271        match reader.read_object_unless_whole_larger(oid, limit) {
272            Ok(read) => Ok(Some(read)),
273            Err(Error::ObjectNotFound(_)) => Ok(None),
274            Err(err) => Err(err),
275        }
276    }
277
278    pub(crate) fn try_read_object_via_single_pack_reader_into(
279        objects_dir: &Path,
280        oid: &crate::objects::ObjectId,
281        populate_delta_lru: bool,
282        buf: &mut Vec<u8>,
283    ) -> Result<Option<crate::objects::ObjectKind>> {
284        if !crate::pack_store::PackStore::read_context_matches_objects_dir(objects_dir) {
285            return Ok(None);
286        }
287        let Some(mut reader) = super::SingleLocalPackReader::open_allow_loose(objects_dir)? else {
288            return Ok(None);
289        };
290        match reader.read_object_into_with_delta_policy(oid, populate_delta_lru, buf) {
291            Ok(kind) => Ok(Some(kind)),
292            Err(Error::ObjectNotFound(_)) => Ok(None),
293            Err(err) => Err(err),
294        }
295    }
296
297    /// Drop thread-local pack caches after invalidation on this or another thread.
298    pub(crate) fn clear_thread_local_after_invalidation(new_epoch: u64) {
299        clear_thread_local_dir_indexes();
300        THREAD_WARM_EPOCH.set(new_epoch);
301    }
302
303    pub(crate) fn sync_thread_local_with_store(store: &crate::pack_store::PackStore) {
304        if !crate::pack_store::PackStore::has_context() {
305            return;
306        }
307        let epoch = store.cache_epoch();
308        let store_key = store_thread_local_key(store);
309        if THREAD_WARM_EPOCH.get() == epoch && THREAD_WARM_STORE.get() == store_key {
310            return;
311        }
312        clear_thread_local_dir_indexes();
313        THREAD_WARM_EPOCH.set(epoch);
314        THREAD_WARM_STORE.set(store_key);
315    }
316
317    fn drop_thread_local_dir_listing(pack_dir: &Path) {
318        PACK_DIR_INDEXES.with(|cell| {
319            if cell
320                .borrow()
321                .as_ref()
322                .is_some_and(|(dir, _, _)| dir == pack_dir)
323            {
324                *cell.borrow_mut() = None;
325                PACK_DIR_LISTING_TRUSTED.set(false);
326            }
327        });
328    }
329
330    fn drop_thread_local_pack_bytes(pack_path: &Path) {
331        PACK_BYTES.with(|cell| {
332            cell.borrow_mut().remove(pack_path);
333        });
334        forget_single_reader_memo();
335    }
336
337    /// Drop thread-local indexes, hints, and directory listing tied to a replaced pack path.
338    fn invalidate_thread_local_for_pack_path_change(pack_path: &Path) {
339        let idx_path = pack_path.with_extension("idx");
340        drop_thread_local_pack_bytes(pack_path);
341        PACK_INDEXES.with(|cell| {
342            cell.borrow_mut().remove(&idx_path);
343        });
344        PACK_IDS.with(|cell| {
345            cell.borrow_mut().remove(pack_path.as_os_str());
346        });
347        PACK_COUNT_VALIDATED.with(|cell| {
348            cell.borrow_mut().remove(pack_path);
349        });
350        PACK_HIT_INDEX.with(|cell| {
351            let drop_hint = cell
352                .borrow()
353                .as_ref()
354                .is_some_and(|(_, idx)| idx.pack_path == pack_path || idx.idx_path == idx_path);
355            if drop_hint {
356                *cell.borrow_mut() = None;
357            }
358        });
359        if let Some(pack_dir) = pack_path.parent() {
360            drop_thread_local_dir_listing(pack_dir);
361        }
362        super::clear_pack_read_info_cache();
363    }
364
365    /// Seed thread-local pack caches from `store` (one catalog read lock; hot reads avoid the catalog).
366    pub(crate) fn warm_thread_local_from_store(store: &crate::pack_store::PackStore) {
367        if !crate::pack_store::PackStore::has_context() {
368            return;
369        }
370        let epoch = store.cache_epoch();
371        let store_key = store_thread_local_key(store);
372        if THREAD_WARM_EPOCH.get() == epoch
373            && THREAD_WARM_STORE.get() == store_key
374            && PACK_DIR_LISTING_TRUSTED.get()
375        {
376            return;
377        }
378        PACK_BYTES.with(|cell| cell.borrow_mut().clear());
379        PACK_INDEXES.with(|cell| cell.borrow_mut().clear());
380        PACK_IDS.with(|cell| cell.borrow_mut().clear());
381        PACK_COUNT_VALIDATED.with(|cell| cell.borrow_mut().clear());
382        PACK_HIT_INDEX.with(|cell| *cell.borrow_mut() = None);
383        forget_single_reader_memo();
384        super::clear_pack_read_info_cache();
385        let objects_dir = store.objects_dir();
386        let pack_dir = objects_dir.join("pack");
387        store.with_catalog_read(|g| {
388            // Replace the listing even when this store has none yet: one left by another store
389            // on this thread may predate a repack, and the listing is trusted from here on.
390            let listing = g
391                .by_dir
392                .get(&pack_dir)
393                .map(|cached| (pack_dir.clone(), cached.dir_mtime, cached.indexes.clone()));
394            PACK_DIR_INDEXES.with(|cell| *cell.borrow_mut() = listing);
395            for (path, entry) in &g.by_pack {
396                remember_pack_bytes(path, &entry.bytes);
397            }
398            for (path, entry) in &g.by_idx {
399                remember_index(path, &entry.idx);
400            }
401            PACK_IDS.with(|cell| {
402                cell.borrow_mut().extend(
403                    g.pack_ids
404                        .iter()
405                        .map(|(k, v)| (k.as_os_str().to_owned(), *v)),
406                );
407            });
408            PACK_COUNT_VALIDATED.with(|cell| {
409                cell.borrow_mut()
410                    .extend(g.validated_pack_counts.iter().cloned());
411            });
412            PACK_DIR_LISTING_TRUSTED.set(true);
413            THREAD_WARM_EPOCH.set(store.cache_epoch());
414            THREAD_WARM_STORE.set(store_thread_local_key(store));
415        });
416    }
417
418    fn cached_pack_bytes(pack_path: &Path) -> Option<Arc<PackData>> {
419        if !crate::pack_store::PackStore::has_context() {
420            return None;
421        }
422        PACK_BYTES.with(|cell| cell.borrow().get(pack_path).and_then(CachedPackBytes::get))
423    }
424
425    fn remember_pack_bytes(pack_path: &Path, bytes: &Arc<PackData>) {
426        // The operation-local catalog owns positioned readers. A second strong cache
427        // would keep their descriptors alive after eviction from that catalog.
428        if !crate::pack_store::PackStore::has_context() {
429            return;
430        }
431        let cached = if bytes.uses_positioned_reads() {
432            CachedPackBytes::Positioned(Arc::downgrade(bytes))
433        } else {
434            CachedPackBytes::Retained(Arc::clone(bytes))
435        };
436        PACK_BYTES.with(|cell| {
437            cell.borrow_mut().insert(pack_path.to_path_buf(), cached);
438        });
439        forget_single_reader_memo();
440    }
441
442    pub(crate) fn remember_pack_hit(objects_dir: &Path, idx: &Arc<PackIndex>) {
443        if !crate::pack_store::PackStore::read_context_matches_objects_dir(objects_dir) {
444            return;
445        }
446        promote_pack_index(objects_dir, &idx.idx_path);
447        PACK_HIT_INDEX.with(|cell| {
448            *cell.borrow_mut() = Some((objects_dir.to_path_buf(), Arc::clone(idx)));
449        });
450    }
451
452    pub(crate) fn find_offset_in_pack_read_context(
453        objects_dir: &Path,
454        idx: &PackIndex,
455        oid: &crate::objects::ObjectId,
456    ) -> Result<Option<u64>> {
457        if !crate::pack_store::PackStore::read_context_matches_objects_dir(objects_dir) {
458            return idx.find_offset(oid);
459        }
460        PACK_MONOTONIC_CURSOR.with(|cell| {
461            let mut slot = cell.borrow_mut();
462            if slot.as_ref().is_none_or(|(dir, _)| dir != objects_dir) {
463                *slot = Some((objects_dir.to_path_buf(), 0));
464            }
465            let Some((_, cursor)) = slot.as_mut() else {
466                return idx.find_offset(oid);
467            };
468            idx.find_offset_monotonic(oid, cursor)
469        })
470    }
471
472    /// The pack that served the last lookup in `objects_dir` ([`remember_pack_hit`]), when
473    /// the active pack store reads that directory.
474    pub(crate) fn hinted_pack_index(objects_dir: &Path) -> Option<Arc<PackIndex>> {
475        if !crate::pack_store::PackStore::read_context_matches_objects_dir(objects_dir) {
476            return None;
477        }
478        PACK_HIT_INDEX.with(|cell| {
479            cell.borrow()
480                .as_ref()
481                .and_then(|(dir, idx)| (dir == objects_dir).then(|| Arc::clone(idx)))
482        })
483    }
484
485    pub(crate) fn try_read_from_hinted_pack(
486        objects_dir: &Path,
487        oid: &crate::objects::ObjectId,
488    ) -> Result<Option<crate::objects::Object>> {
489        let Some(idx) = hinted_pack_index(objects_dir) else {
490            return Ok(None);
491        };
492        let Some(offset) = find_offset_in_pack_read_context(objects_dir, &idx, oid)? else {
493            return Ok(None);
494        };
495        match super::read_object_from_pack_at_offset(&idx, offset) {
496            Ok(obj) => Ok(Some(obj)),
497            Err(Error::ObjectNotFound(_)) => Ok(None),
498            Err(err) => Err(err),
499        }
500    }
501
502    pub(crate) fn try_read_from_hinted_pack_into(
503        objects_dir: &Path,
504        oid: &crate::objects::ObjectId,
505        populate_delta_lru: bool,
506        buf: &mut Vec<u8>,
507    ) -> Result<Option<crate::objects::ObjectKind>> {
508        let Some(idx) = hinted_pack_index(objects_dir) else {
509            return Ok(None);
510        };
511        let Some(offset) = find_offset_in_pack_read_context(objects_dir, &idx, oid)? else {
512            return Ok(None);
513        };
514        match super::read_object_at_depth_into(&idx, offset, 0, populate_delta_lru, buf) {
515            Ok(kind) => Ok(Some(kind)),
516            Err(Error::ObjectNotFound(_)) => Ok(None),
517            Err(err) => Err(err),
518        }
519    }
520
521    pub(crate) fn try_read_info_from_hinted_pack(
522        objects_dir: &Path,
523        oid: &crate::objects::ObjectId,
524    ) -> Result<Option<crate::objects::ObjectInfo>> {
525        let Some(idx) = hinted_pack_index(objects_dir) else {
526            return Ok(None);
527        };
528        let Some(offset) = idx.find_offset(oid)? else {
529            return Ok(None);
530        };
531        match super::read_object_info_from_pack_at_offset(&idx, offset) {
532            Ok(info) => Ok(Some(info)),
533            Err(Error::ObjectNotFound(_)) => Ok(None),
534            Err(err) => Err(err),
535        }
536    }
537    fn cached_dir_indexes(objects_dir: &Path, pack_dir: &Path) -> Option<Vec<Arc<PackIndex>>> {
538        if !crate::pack_store::PackStore::read_context_matches_objects_dir(objects_dir) {
539            return None;
540        }
541        let trust_listing = PACK_DIR_LISTING_TRUSTED.get();
542        let dir_mt = if trust_listing {
543            None
544        } else {
545            Some(dir_mtime(pack_dir))
546        };
547        PACK_DIR_INDEXES.with(|cell| {
548            cell.borrow()
549                .as_ref()
550                .and_then(|(dir, cached_mt, indexes)| {
551                    if dir != pack_dir {
552                        return None;
553                    }
554                    if trust_listing || dir_mt == Some(*cached_mt) {
555                        Some(indexes.clone())
556                    } else {
557                        None
558                    }
559                })
560        })
561    }
562
563    fn remember_dir_indexes(objects_dir: &Path, pack_dir: &Path, indexes: &[Arc<PackIndex>]) {
564        if !crate::pack_store::PackStore::read_context_matches_objects_dir(objects_dir) {
565            return;
566        }
567        let dir_mt = dir_mtime(pack_dir);
568        PACK_DIR_INDEXES.with(|cell| {
569            *cell.borrow_mut() = Some((pack_dir.to_path_buf(), dir_mt, indexes.to_vec()));
570        });
571    }
572
573    struct CachedDir {
574        dir_mtime: SystemTime,
575        indexes: Vec<Arc<PackIndex>>,
576        midx_mtime: SystemTime,
577        midx_pack_names: HashSet<String>,
578    }
579
580    struct CachedIdx {
581        idx: Arc<PackIndex>,
582    }
583
584    struct CachedPack {
585        mtime: SystemTime,
586        size: u64,
587        bytes: Arc<PackData>,
588    }
589
590    /// Default retained delta-base bytes (`core.deltaBaseCacheLimit`).
591    pub(super) const DELTA_BASE_CACHE_DEFAULT: usize = 96 * 1024 * 1024;
592
593    #[derive(Clone, Copy, PartialEq, Eq, Hash)]
594    pub(crate) struct DeltaCacheKey {
595        pack_id: u32,
596        offset: u64,
597    }
598
599    /// Byte-bounded LRU of resolved **intermediate** delta bases (never the object being returned).
600    struct DeltaBaseLru {
601        byte_limit: usize,
602        bytes_used: usize,
603        entries: HashMap<DeltaCacheKey, (ObjectKind, Arc<[u8]>)>,
604        head: Option<DeltaCacheKey>,
605        tail: Option<DeltaCacheKey>,
606        links: HashMap<DeltaCacheKey, (Option<DeltaCacheKey>, Option<DeltaCacheKey>)>,
607    }
608
609    impl Default for DeltaBaseLru {
610        fn default() -> Self {
611            Self {
612                byte_limit: DELTA_BASE_CACHE_DEFAULT,
613                bytes_used: 0,
614                entries: HashMap::new(),
615                head: None,
616                tail: None,
617                links: HashMap::new(),
618            }
619        }
620    }
621
622    impl DeltaBaseLru {
623        fn set_byte_limit(&mut self, limit: usize) {
624            if limit == 0 {
625                self.clear();
626                self.byte_limit = 0;
627                return;
628            }
629            self.byte_limit = limit;
630            while self.bytes_used > self.byte_limit {
631                let Some(key) = self.head else {
632                    break;
633                };
634                self.evict_key(key);
635            }
636        }
637
638        fn unlink(&mut self, key: DeltaCacheKey) {
639            let Some((prev, next)) = self.links.remove(&key) else {
640                return;
641            };
642            match prev {
643                Some(p) => {
644                    if let Some(link) = self.links.get_mut(&p) {
645                        link.1 = next;
646                    }
647                }
648                None => self.head = next,
649            }
650            match next {
651                Some(n) => {
652                    if let Some(link) = self.links.get_mut(&n) {
653                        link.0 = prev;
654                    }
655                }
656                None => self.tail = prev,
657            }
658        }
659
660        fn append_tail(&mut self, key: DeltaCacheKey) {
661            let prev = self.tail;
662            self.links.insert(key, (prev, None));
663            if let Some(p) = prev {
664                if let Some(link) = self.links.get_mut(&p) {
665                    link.1 = Some(key);
666                }
667            } else {
668                self.head = Some(key);
669            }
670            self.tail = Some(key);
671        }
672
673        fn touch(&mut self, key: DeltaCacheKey) {
674            self.unlink(key);
675            self.append_tail(key);
676        }
677
678        fn evict_key(&mut self, key: DeltaCacheKey) {
679            self.unlink(key);
680            if let Some((_, data)) = self.entries.remove(&key) {
681                self.bytes_used = self.bytes_used.saturating_sub(data.len());
682            }
683        }
684
685        fn get(&mut self, key: DeltaCacheKey) -> Option<(ObjectKind, Arc<[u8]>)> {
686            let (kind, data) = self.entries.get(&key).map(|(k, d)| (*k, Arc::clone(d)))?;
687            self.touch(key);
688            Some((kind, data))
689        }
690
691        fn put(&mut self, key: DeltaCacheKey, kind: ObjectKind, data: Arc<[u8]>) {
692            if self.byte_limit == 0 {
693                return;
694            }
695            let sz = data.len();
696            if sz > self.byte_limit {
697                return;
698            }
699            use std::collections::hash_map::Entry;
700            match self.entries.entry(key) {
701                Entry::Occupied(mut slot) => {
702                    let old_len = slot.get().1.len();
703                    slot.insert((kind, Arc::clone(&data)));
704                    self.bytes_used = self.bytes_used.saturating_sub(old_len).saturating_add(sz);
705                    self.touch(key);
706                }
707                Entry::Vacant(slot) => {
708                    slot.insert((kind, Arc::clone(&data)));
709                    self.bytes_used = self.bytes_used.saturating_add(sz);
710                    self.append_tail(key);
711                }
712            }
713            while self.bytes_used > self.byte_limit {
714                let Some(evict) = self.head else {
715                    break;
716                };
717                self.evict_key(evict);
718            }
719        }
720
721        fn drop_pack(&mut self, pack_id: u32) {
722            let keys: Vec<DeltaCacheKey> = self
723                .entries
724                .keys()
725                .copied()
726                .filter(|k| k.pack_id == pack_id)
727                .collect();
728            for key in keys {
729                self.evict_key(key);
730            }
731        }
732
733        fn clear(&mut self) {
734            self.entries.clear();
735            self.links.clear();
736            self.head = None;
737            self.tail = None;
738            self.bytes_used = 0;
739        }
740    }
741
742    fn shard_index(key: DeltaCacheKey) -> usize {
743        let mut hasher = std::collections::hash_map::DefaultHasher::new();
744        key.hash(&mut hasher);
745        (hasher.finish() as usize) % DELTA_LRU_SHARD_COUNT
746    }
747
748    /// Delta-base LRU sharded by `(pack_id, offset)` so concurrent reads rarely contend.
749    pub(crate) struct ShardedDeltaBaseLru {
750        cache_enabled: AtomicBool,
751        lru_populated: AtomicBool,
752        total_byte_limit: AtomicUsize,
753        shards: Box<[Mutex<DeltaBaseLru>; DELTA_LRU_SHARD_COUNT]>,
754    }
755
756    impl ShardedDeltaBaseLru {
757        pub(crate) fn new() -> Self {
758            let per_shard = DELTA_BASE_CACHE_DEFAULT / DELTA_LRU_SHARD_COUNT;
759            Self {
760                cache_enabled: AtomicBool::new(true),
761                lru_populated: AtomicBool::new(false),
762                total_byte_limit: AtomicUsize::new(DELTA_BASE_CACHE_DEFAULT),
763                shards: Box::new(std::array::from_fn(|_| {
764                    Mutex::new(DeltaBaseLru {
765                        byte_limit: per_shard,
766                        ..DeltaBaseLru::default()
767                    })
768                })),
769            }
770        }
771
772        pub(crate) fn set_byte_limit(&self, limit: usize) {
773            self.total_byte_limit.store(limit, Ordering::Release);
774            if limit == 0 {
775                self.cache_enabled.store(false, Ordering::Release);
776                self.clear();
777                return;
778            }
779            self.cache_enabled.store(true, Ordering::Release);
780            let base = limit / DELTA_LRU_SHARD_COUNT;
781            let rem = limit % DELTA_LRU_SHARD_COUNT;
782            let mut any = false;
783            for (i, shard) in self.shards.iter().enumerate() {
784                let cap = base + usize::from(i < rem);
785                let mut g = shard.lock().unwrap_or_else(|e| e.into_inner());
786                g.set_byte_limit(cap);
787                any |= !g.entries.is_empty();
788            }
789            self.lru_populated.store(any, Ordering::Release);
790        }
791
792        pub(crate) fn get(&self, key: DeltaCacheKey) -> Option<(ObjectKind, Arc<[u8]>)> {
793            if !self.cache_enabled.load(Ordering::Acquire)
794                || !self.lru_populated.load(Ordering::Acquire)
795            {
796                return None;
797            }
798            let shard = &self.shards[shard_index(key)];
799            let mut g = shard.lock().unwrap_or_else(|e| e.into_inner());
800            g.get(key)
801        }
802
803        pub(crate) fn put_many(&self, kind: ObjectKind, items: &[(u32, u64, Arc<[u8]>)]) {
804            if items.is_empty() || !self.cache_enabled.load(Ordering::Acquire) {
805                return;
806            }
807            for &(pack_id, offset, ref data) in items {
808                let key = DeltaCacheKey { pack_id, offset };
809                let shard = &self.shards[shard_index(key)];
810                let mut g = shard.lock().unwrap_or_else(|e| e.into_inner());
811                if g.byte_limit == 0 {
812                    continue;
813                }
814                g.put(key, kind, Arc::clone(data));
815            }
816            self.lru_populated.store(true, Ordering::Release);
817        }
818
819        pub(crate) fn clear(&self) {
820            for shard in self.shards.iter() {
821                shard.lock().unwrap_or_else(|e| e.into_inner()).clear();
822            }
823            self.lru_populated.store(false, Ordering::Release);
824        }
825
826        pub(crate) fn drop_pack(&self, pack_id: u32) {
827            let mut any = false;
828            for shard in self.shards.iter() {
829                let mut g = shard.lock().unwrap_or_else(|e| e.into_inner());
830                g.drop_pack(pack_id);
831                any |= !g.entries.is_empty();
832            }
833            self.lru_populated.store(any, Ordering::Release);
834        }
835
836        pub(crate) fn caching_enabled(&self) -> bool {
837            self.cache_enabled.load(Ordering::Acquire)
838        }
839
840        #[cfg(test)]
841        pub(crate) fn bytes_used(&self) -> usize {
842            self.shards
843                .iter()
844                .map(|s| s.lock().unwrap_or_else(|e| e.into_inner()).bytes_used)
845                .sum()
846        }
847
848        #[cfg(test)]
849        pub(crate) fn contains(&self, pack_id: u32, offset: u64) -> bool {
850            let key = DeltaCacheKey { pack_id, offset };
851            self.shards[shard_index(key)]
852                .lock()
853                .unwrap_or_else(|e| e.into_inner())
854                .entries
855                .contains_key(&key)
856        }
857    }
858
859    #[derive(Default)]
860    pub(crate) struct CatalogState {
861        by_dir: HashMap<PathBuf, CachedDir>,
862        by_idx: HashMap<PathBuf, CachedIdx>,
863        by_pack: HashMap<PathBuf, CachedPack>,
864        bounded_pack_order: VecDeque<PathBuf>,
865        /// Small id assigned when a pack file is first registered in this process.
866        pack_ids: HashMap<PathBuf, u32>,
867        next_pack_id: u32,
868        /// Packs whose header object count was matched to the index (hot read path).
869        validated_pack_counts: HashSet<PathBuf>,
870        /// Per `objects/pack` directory rescan count (parallel-safe; tests only).
871        #[cfg(test)]
872        test_dir_rescan_counts: HashMap<PathBuf, u64>,
873    }
874
875    impl CatalogState {
876        fn retain_bounded_pack(&mut self, pack_path: &Path) {
877            if let Some(pos) = self.bounded_pack_order.iter().position(|p| p == pack_path) {
878                self.bounded_pack_order.remove(pos);
879            }
880            while self.bounded_pack_order.len() >= BOUNDED_PACK_READERS {
881                if let Some(path) = self.bounded_pack_order.pop_front() {
882                    self.by_pack.remove(&path);
883                }
884            }
885            self.bounded_pack_order.push_back(pack_path.to_path_buf());
886        }
887    }
888
889    pub(crate) fn with_current<R>(f: impl FnOnce(&mut CatalogState) -> R) -> R {
890        crate::pack_store::PackStore::with_current_or_implicit(f)
891    }
892
893    pub(crate) fn clear_catalog_on(m: &std::sync::RwLock<CatalogState>) {
894        let mut g = m.write().unwrap_or_else(|e| e.into_inner());
895        clear_inner(&mut g);
896    }
897
898    pub(crate) fn configure_delta_base_on(
899        delta: &ShardedDeltaBaseLru,
900        cfg: Option<&crate::config::ConfigSet>,
901    ) {
902        let bytes = cfg
903            .and_then(|config| config.get("core.deltaBaseCacheLimit"))
904            .as_ref()
905            .and_then(|value| crate::config::parse_i64(value).ok())
906            .and_then(|value| usize::try_from(value).ok())
907            .unwrap_or(DELTA_BASE_CACHE_DEFAULT);
908        delta.set_byte_limit(bytes);
909    }
910
911    #[cfg(test)]
912    thread_local! {
913        static TEST_MARKER_STAT_COUNT: std::cell::Cell<u64> = const { std::cell::Cell::new(0) };
914    }
915
916    fn marker_sidecar_is_file(path: &Path) -> bool {
917        #[cfg(test)]
918        TEST_MARKER_STAT_COUNT.with(|count| count.set(count.get().saturating_add(1)));
919        path.is_file()
920    }
921
922    /// Whether sibling `.promisor` / `.mtimes` markers exist next to `pack_path`.
923    pub fn sidecar_flags(pack_path: &Path) -> (bool, bool) {
924        (
925            marker_sidecar_is_file(&pack_path.with_extension("promisor")),
926            marker_sidecar_is_file(&pack_path.with_extension("mtimes")),
927        )
928    }
929
930    fn refresh_pack_sidecar_flags(idx: Arc<PackIndex>) -> Arc<PackIndex> {
931        let (is_promisor, is_cruft) = sidecar_flags(&idx.pack_path);
932        if idx.is_promisor == is_promisor && idx.is_cruft == is_cruft {
933            return idx;
934        }
935        let updated = Arc::new(idx.with_sidecar_flags(is_promisor, is_cruft));
936        with_current(|g| {
937            g.by_idx.insert(
938                idx.idx_path.clone(),
939                CachedIdx {
940                    idx: Arc::clone(&updated),
941                },
942            );
943        });
944        updated
945    }
946
947    fn dir_mtime(path: &Path) -> SystemTime {
948        fs::metadata(path)
949            .and_then(|m| m.modified())
950            .unwrap_or(SystemTime::UNIX_EPOCH)
951    }
952
953    fn file_signature(path: &Path) -> Option<(SystemTime, u64)> {
954        #[cfg(test)]
955        crate::hot_path_test_metrics::record_pack_signature_stat_for_active_scope();
956        let m = fs::metadata(path).ok()?;
957        let mtime = m.modified().unwrap_or(SystemTime::UNIX_EPOCH);
958        Some((mtime, m.len()))
959    }
960
961    pub fn pack_object_count_validated(pack_path: &Path) -> bool {
962        if crate::pack_store::PackStore::has_context()
963            && PACK_COUNT_VALIDATED.with(|cell| cell.borrow().contains(pack_path))
964        {
965            return true;
966        }
967        crate::pack_store::PackStore::active_for_reads()
968            .with_catalog_read(|g| g.validated_pack_counts.contains(pack_path))
969    }
970
971    pub fn mark_pack_object_count_validated(pack_path: &Path) {
972        if crate::pack_store::PackStore::has_context() {
973            PACK_COUNT_VALIDATED.with(|cell| {
974                cell.borrow_mut().insert(pack_path.to_path_buf());
975            });
976        }
977        let store = crate::pack_store::PackStore::active_for_reads();
978        if store.with_catalog_read(|g| g.validated_pack_counts.contains(pack_path)) {
979            return;
980        }
981        let _ = store.try_with_catalog(|g| {
982            g.validated_pack_counts.insert(pack_path.to_path_buf());
983        });
984    }
985
986    fn cached_index(idx_path: &Path) -> Option<Arc<PackIndex>> {
987        if !crate::pack_store::PackStore::has_context() {
988            return None;
989        }
990        PACK_INDEXES.with(|cell| cell.borrow().get(idx_path).map(Arc::clone))
991    }
992
993    fn remember_index(idx_path: &Path, idx: &Arc<PackIndex>) {
994        if !crate::pack_store::PackStore::has_context() {
995            return;
996        }
997        PACK_INDEXES.with(|cell| {
998            cell.borrow_mut()
999                .insert(idx_path.to_path_buf(), Arc::clone(idx));
1000        });
1001    }
1002
1003    fn index_cache_hit_stale(idx: &PackIndex) -> bool {
1004        !idx.pack_path.is_file()
1005    }
1006
1007    /// Get a parsed pack index from cache, parsing from disk only on cache miss.
1008    pub fn get_index(idx_path: &Path) -> Result<Arc<PackIndex>> {
1009        if let Some(hit) = cached_index(idx_path) {
1010            if index_cache_hit_stale(&hit) {
1011                PACK_INDEXES.with(|cell| {
1012                    cell.borrow_mut().remove(idx_path);
1013                });
1014            } else {
1015                return Ok(hit);
1016            }
1017        }
1018        let store = crate::pack_store::PackStore::active_for_reads();
1019        if let Some(hit) =
1020            store.with_catalog_read(|g| g.by_idx.get(idx_path).map(|c| Arc::clone(&c.idx)))
1021        {
1022            if index_cache_hit_stale(&hit) {
1023                store.with_catalog(|g| {
1024                    g.by_idx.remove(idx_path);
1025                });
1026            } else {
1027                remember_index(idx_path, &hit);
1028                return Ok(hit);
1029            }
1030        }
1031        crate::trace::trace_span!("grit.pack.index.open");
1032        let parsed = Arc::new(read_pack_index_no_verify(idx_path)?);
1033        store.with_catalog(|g| {
1034            g.by_idx.insert(
1035                idx_path.to_path_buf(),
1036                CachedIdx {
1037                    idx: Arc::clone(&parsed),
1038                },
1039            );
1040        });
1041        remember_index(idx_path, &parsed);
1042        Ok(parsed)
1043    }
1044
1045    fn rescan_dir_indexes(objects_dir: &Path) -> Result<Vec<Arc<PackIndex>>> {
1046        let pack_dir = objects_dir.join("pack");
1047        drop_thread_local_dir_listing(&pack_dir);
1048        let dir_mt = dir_mtime(&pack_dir);
1049
1050        let rd = match fs::read_dir(&pack_dir) {
1051            Ok(rd) => rd,
1052            Err(err) if err.kind() == io::ErrorKind::NotFound => {
1053                with_current(|g| {
1054                    g.by_dir.insert(
1055                        pack_dir.clone(),
1056                        CachedDir {
1057                            dir_mtime: dir_mt,
1058                            indexes: Vec::new(),
1059                            midx_mtime: SystemTime::UNIX_EPOCH,
1060                            midx_pack_names: HashSet::new(),
1061                        },
1062                    );
1063                });
1064                return Ok(Vec::new());
1065            }
1066            Err(err) => return Err(Error::Io(err)),
1067        };
1068
1069        let mut out = Vec::new();
1070        for entry in rd {
1071            let entry = entry.map_err(Error::Io)?;
1072            let path = entry.path();
1073            if path.extension().and_then(|s| s.to_str()) != Some("idx") {
1074                continue;
1075            }
1076            let Ok(idx) = get_index(&path) else {
1077                continue;
1078            };
1079            if !idx.pack_path.is_file() {
1080                continue;
1081            }
1082            out.push(refresh_pack_sidecar_flags(idx));
1083        }
1084
1085        let midx_pack_names = load_midx_pack_names(&pack_dir);
1086        with_current(|g| {
1087            #[cfg(test)]
1088            {
1089                *g.test_dir_rescan_counts
1090                    .entry(pack_dir.clone())
1091                    .or_insert(0) += 1;
1092            }
1093            g.by_dir.insert(
1094                pack_dir,
1095                CachedDir {
1096                    dir_mtime: dir_mt,
1097                    indexes: out.clone(),
1098                    midx_mtime: midx_pack_names.0,
1099                    midx_pack_names: midx_pack_names.1,
1100                },
1101            );
1102        });
1103        Ok(out)
1104    }
1105
1106    fn load_midx_pack_names(pack_dir: &Path) -> (SystemTime, HashSet<String>) {
1107        let Some(midx_path) = crate::midx::resolve_tip_midx_path(pack_dir) else {
1108            return (SystemTime::UNIX_EPOCH, HashSet::new());
1109        };
1110        let metadata = fs::metadata(&midx_path).ok();
1111        let mtime = metadata
1112            .as_ref()
1113            .and_then(|m| m.modified().ok())
1114            .unwrap_or(SystemTime::UNIX_EPOCH);
1115        let names = metadata
1116            .as_ref()
1117            .and_then(|metadata| {
1118                crate::midx::read_midx_pack_idx_names_from_path(&midx_path, metadata).ok()
1119            })
1120            .unwrap_or_default()
1121            .into_iter()
1122            .collect();
1123        (mtime, names)
1124    }
1125
1126    fn refresh_midx_pack_names_locked(pack_dir: &Path, cached: &mut CachedDir) {
1127        let (mtime, names) = load_midx_pack_names(pack_dir);
1128        if cached.midx_mtime != mtime {
1129            cached.midx_mtime = mtime;
1130            cached.midx_pack_names = names;
1131        }
1132    }
1133
1134    /// Move `idx_path` to the front of the MRU pack search order for `objects_dir`.
1135    pub fn promote_pack_index(objects_dir: &Path, idx_path: &Path) {
1136        let pack_dir = objects_dir.join("pack");
1137        let store = crate::pack_store::PackStore::active_for_reads();
1138        let _ = store.try_with_catalog(|g| {
1139            let Some(cached) = g.by_dir.get_mut(&pack_dir) else {
1140                return;
1141            };
1142            let Some(pos) = cached
1143                .indexes
1144                .iter()
1145                .position(|idx| idx.idx_path == idx_path)
1146            else {
1147                return;
1148            };
1149            if pos == 0 {
1150                return;
1151            }
1152            let hit = cached.indexes.remove(pos);
1153            cached.indexes.insert(0, hit);
1154        });
1155    }
1156
1157    /// Pack index basenames listed in the active multi-pack-index, if any.
1158    pub fn midx_covered_pack_names(objects_dir: &Path) -> HashSet<String> {
1159        let pack_dir = objects_dir.join("pack");
1160        let cached = with_current(|g| {
1161            let cached = g.by_dir.get_mut(&pack_dir)?;
1162            refresh_midx_pack_names_locked(&pack_dir, cached);
1163            Some(cached.midx_pack_names.clone())
1164        });
1165        if let Some(names) = cached {
1166            return names;
1167        }
1168        load_midx_pack_names(&pack_dir).1
1169    }
1170
1171    /// Get all `.idx` files for `objects_dir`, using the cached directory listing when present.
1172    pub fn get_dir_indexes(objects_dir: &Path) -> Result<Vec<Arc<PackIndex>>> {
1173        let pack_dir = objects_dir.join("pack");
1174        let store = crate::pack_store::PackStore::active_for_reads();
1175        if crate::pack_store::PackStore::has_context() {
1176            sync_thread_local_with_store(&store);
1177        }
1178        if let Some(hit) = cached_dir_indexes(objects_dir, &pack_dir) {
1179            return Ok(hit);
1180        }
1181        let dir_mt = dir_mtime(&pack_dir);
1182        if let Some(hit) = store.with_catalog_read(|g| {
1183            g.by_dir
1184                .get(&pack_dir)
1185                .and_then(|c| (c.dir_mtime == dir_mt).then(|| c.indexes.clone()))
1186        }) {
1187            remember_dir_indexes(objects_dir, &pack_dir, &hit);
1188            return Ok(hit);
1189        }
1190        let fresh = rescan_dir_indexes(objects_dir)?;
1191        remember_dir_indexes(objects_dir, &pack_dir, &fresh);
1192        Ok(fresh)
1193    }
1194
1195    fn idx_paths_on_disk(pack_dir: &Path) -> Result<HashSet<PathBuf>> {
1196        let mut out = HashSet::new();
1197        let rd = match fs::read_dir(pack_dir) {
1198            Ok(rd) => rd,
1199            Err(err) if err.kind() == io::ErrorKind::NotFound => return Ok(out),
1200            Err(err) => return Err(Error::Io(err)),
1201        };
1202        for entry in rd {
1203            let entry = entry.map_err(Error::Io)?;
1204            let path = entry.path();
1205            if path.extension().and_then(|s| s.to_str()) != Some("idx") {
1206                continue;
1207            }
1208            if path.with_extension("pack").is_file() {
1209                out.insert(path);
1210            }
1211        }
1212        Ok(out)
1213    }
1214
1215    fn cached_dir_listing_stale(
1216        pack_dir: &Path,
1217        dir_mt: SystemTime,
1218        cached: &CachedDir,
1219    ) -> Result<bool> {
1220        if cached.dir_mtime != dir_mt {
1221            return Ok(true);
1222        }
1223        // Pack directory mtimes often have one-second resolution; external `git gc` / repack can
1224        // replace `.idx` files without bumping the directory mtime (same wall-clock second).
1225        let on_disk = idx_paths_on_disk(pack_dir)?;
1226        let cached_paths: HashSet<PathBuf> = cached
1227            .indexes
1228            .iter()
1229            .map(|idx| idx.idx_path.clone())
1230            .collect();
1231        Ok(on_disk != cached_paths)
1232    }
1233
1234    /// Re-scan `objects_dir`/`pack/` when its mtime changed since the cached listing (Git
1235    /// `reprepare_packed_git`), or when the set of pack index files no longer matches the
1236    /// cached listing. Returns true if the listing was refreshed.
1237    pub fn reprepare_dir_on_miss(objects_dir: &Path) -> Result<bool> {
1238        let pack_dir = objects_dir.join("pack");
1239        let dir_mt = dir_mtime(&pack_dir);
1240        let needs_rescan =
1241            crate::pack_store::PackStore::active_for_reads().with_catalog_read(|g| {
1242                match g.by_dir.get(&pack_dir) {
1243                    None => true,
1244                    Some(c) => cached_dir_listing_stale(&pack_dir, dir_mt, c).unwrap_or(true),
1245                }
1246            });
1247        if !needs_rescan {
1248            return Ok(false);
1249        }
1250        rescan_dir_indexes(objects_dir)?;
1251        Ok(true)
1252    }
1253
1254    fn load_pack_bytes_from_disk(
1255        pack_path: &Path,
1256        mtime: SystemTime,
1257        size: u64,
1258    ) -> Result<Arc<PackData>> {
1259        invalidate_thread_local_for_pack_path_change(pack_path);
1260        let store = crate::pack_store::PackStore::active_for_reads();
1261        let bytes = if store.bounded_reads() {
1262            PackData::open_random_access(pack_path)?
1263        } else {
1264            PackData::open_for_sparse_reads(pack_path, &store.sparse_read_files)?
1265        };
1266        store.with_catalog(|g| {
1267            drop_delta_entries_locked(g, pack_path, store.delta_bases());
1268            g.by_idx.remove(&pack_path.with_extension("idx"));
1269            g.validated_pack_counts.remove(pack_path);
1270            if store.bounded_reads() {
1271                g.retain_bounded_pack(pack_path);
1272            }
1273            g.by_pack.insert(
1274                pack_path.to_path_buf(),
1275                CachedPack {
1276                    mtime,
1277                    size,
1278                    bytes: Arc::clone(&bytes),
1279                },
1280            );
1281        });
1282        remember_pack_bytes(pack_path, &bytes);
1283        Ok(bytes)
1284    }
1285
1286    /// Get the raw bytes of a pack file from cache, reading from disk only on cache miss.
1287    ///
1288    /// Cache hits skip `stat` for all pack basenames; in-process repack/gc clears the cache
1289    /// when packs change.
1290    pub fn get_pack_bytes(pack_path: &Path) -> Result<Arc<PackData>> {
1291        if let Some(hit) = cached_pack_bytes(pack_path) {
1292            return Ok(hit);
1293        }
1294        let store = crate::pack_store::PackStore::active_for_reads();
1295        let hit = if store.bounded_reads() {
1296            store.with_catalog(|g| {
1297                let bytes = Arc::clone(&g.by_pack.get(pack_path)?.bytes);
1298                g.retain_bounded_pack(pack_path);
1299                Some(bytes)
1300            })
1301        } else {
1302            store.with_catalog_read(|g| g.by_pack.get(pack_path).map(|c| Arc::clone(&c.bytes)))
1303        };
1304        if let Some(hit) = hit {
1305            remember_pack_bytes(pack_path, &hit);
1306            return Ok(hit);
1307        }
1308        let (mtime, size) = file_signature(pack_path).ok_or_else(|| {
1309            Error::Io(io::Error::new(
1310                io::ErrorKind::NotFound,
1311                format!("pack not found: {}", pack_path.display()),
1312            ))
1313        })?;
1314        load_pack_bytes_from_disk(pack_path, mtime, size)
1315    }
1316
1317    fn revalidate_stale_pack_bytes_in_context(pack_path: &Path) -> Result<bool> {
1318        let Some((mtime, size)) = file_signature(pack_path) else {
1319            let store = crate::pack_store::PackStore::active_for_reads();
1320            let removed = store.with_catalog(|g| {
1321                let removed = g.by_pack.remove(pack_path).is_some();
1322                if removed {
1323                    drop_delta_entries_locked(g, pack_path, store.delta_bases());
1324                }
1325                removed
1326            });
1327            invalidate_thread_local_for_pack_path_change(pack_path);
1328            super::drop_pack_read_info_cache_for(pack_path);
1329            return Ok(removed);
1330        };
1331        let store = crate::pack_store::PackStore::active_for_reads();
1332        let stale = store.with_catalog_read(|g| match g.by_pack.get(pack_path) {
1333            Some(c) => c.mtime != mtime || c.size != size,
1334            None => true,
1335        });
1336        if !stale {
1337            return Ok(false);
1338        }
1339        load_pack_bytes_from_disk(pack_path, mtime, size)?;
1340        super::drop_pack_read_info_cache_for(pack_path);
1341        Ok(true)
1342    }
1343
1344    /// Re-read `pack_path` when its on-disk signature differs from the cached entry.
1345    ///
1346    /// Returns true when bytes were reloaded or the cache entry was dropped.
1347    pub fn revalidate_stale_pack_bytes(pack_path: &Path) -> Result<bool> {
1348        if crate::pack_store::PackStore::has_context() {
1349            return revalidate_stale_pack_bytes_in_context(pack_path);
1350        }
1351        let store = crate::pack_store::PackStore::legacy_for_pack_path(pack_path);
1352        crate::pack_store::PackStore::with_context(store, || {
1353            revalidate_stale_pack_bytes_in_context(pack_path)
1354        })
1355    }
1356
1357    /// After a pack parse/decompress failure, reload from disk when cached bytes differ.
1358    ///
1359    /// Signature-only checks miss in-memory stale entries (tests) and same-second replacements
1360    /// with unchanged size; comparing the pack header and trailer fingerprint catches those
1361    /// without reading the whole file.
1362    ///
1363    /// Returns true when the cache was refreshed with disk contents.
1364    pub fn reload_pack_bytes_after_parse_failure(pack_path: &Path) -> Result<bool> {
1365        if revalidate_stale_pack_bytes(pack_path)? {
1366            return Ok(true);
1367        }
1368        let Some((mtime, size)) = file_signature(pack_path) else {
1369            return Ok(false);
1370        };
1371        let hash_bytes = hash_bytes_for_pack_file(pack_path)?;
1372        let disk_fp = fingerprint_from_file(pack_path, hash_bytes)?;
1373        let needs_reload = with_current(|g| {
1374            g.by_pack.get(pack_path).is_some_and(|c| {
1375                pack_fingerprint_mismatch(&c.bytes, hash_bytes, &disk_fp).unwrap_or(true)
1376            })
1377        });
1378        if !needs_reload {
1379            return Ok(false);
1380        }
1381        load_pack_bytes_from_disk(pack_path, mtime, size)?;
1382        super::drop_pack_read_info_cache_for(pack_path);
1383        Ok(true)
1384    }
1385
1386    fn hash_bytes_for_pack_file(pack_path: &Path) -> Result<usize> {
1387        let idx_path = pack_path.with_extension("idx");
1388        if let Some(hash_bytes) =
1389            with_current(|g| g.by_idx.get(&idx_path).map(|c| c.idx.hash_bytes()))
1390        {
1391            return Ok(hash_bytes);
1392        }
1393        Ok(read_pack_index_no_verify(&idx_path)?.hash_bytes())
1394    }
1395
1396    fn pack_fingerprint_mismatch(
1397        cached: &PackData,
1398        hash_bytes: usize,
1399        disk: &PackFingerprint,
1400    ) -> Result<bool> {
1401        Ok(cached.fingerprint(hash_bytes)? != *disk)
1402    }
1403
1404    #[cfg(test)]
1405    pub fn test_reset_dir_rescan_count(pack_dir: &Path) {
1406        let objects_dir = pack_dir.parent().expect("pack dir under objects/");
1407        crate::pack_store::PackStore::legacy_for_objects_dir(objects_dir).with_pack(|g| {
1408            g.by_dir.remove(pack_dir);
1409            g.test_dir_rescan_counts.insert(pack_dir.to_path_buf(), 0);
1410        });
1411    }
1412
1413    #[cfg(test)]
1414    pub fn test_reset_marker_stat_count() {
1415        TEST_MARKER_STAT_COUNT.with(|count| count.set(0));
1416    }
1417
1418    #[cfg(test)]
1419    #[must_use]
1420    pub fn test_marker_stat_count() -> u64 {
1421        TEST_MARKER_STAT_COUNT.with(std::cell::Cell::get)
1422    }
1423
1424    #[cfg(test)]
1425    #[must_use]
1426    pub fn test_dir_rescan_count(pack_dir: &Path) -> u64 {
1427        let objects_dir = pack_dir.parent().expect("pack dir under objects/");
1428        test_dir_rescan_count_on_store(
1429            &crate::pack_store::PackStore::legacy_for_objects_dir(objects_dir),
1430            pack_dir,
1431        )
1432    }
1433
1434    #[cfg(test)]
1435    #[must_use]
1436    pub fn test_dir_rescan_count_on_store(
1437        store: &std::sync::Arc<crate::pack_store::PackStore>,
1438        pack_dir: &Path,
1439    ) -> u64 {
1440        store.with_pack(|g| g.test_dir_rescan_counts.get(pack_dir).copied().unwrap_or(0))
1441    }
1442
1443    #[cfg(test)]
1444    pub fn test_reset_dir_rescan_count_on_store(
1445        store: &std::sync::Arc<crate::pack_store::PackStore>,
1446        pack_dir: &Path,
1447    ) {
1448        store.with_pack(|g| {
1449            g.by_dir.remove(pack_dir);
1450            g.test_dir_rescan_counts.insert(pack_dir.to_path_buf(), 0);
1451        });
1452    }
1453
1454    /// Replace cached pack bytes (tests simulating a stale in-memory mapping).
1455    #[cfg(test)]
1456    pub fn test_inject_stale_pack_bytes(pack_path: &Path, stale: Arc<PackData>) {
1457        crate::pack_store::PackStore::legacy_for_pack_path(pack_path).with_pack(|g| {
1458            let stamp = g
1459                .by_pack
1460                .get(pack_path)
1461                .map(|c| (c.mtime, c.size))
1462                .unwrap_or((SystemTime::UNIX_EPOCH, stale.len() as u64));
1463            g.by_pack.insert(
1464                pack_path.to_path_buf(),
1465                CachedPack {
1466                    mtime: stamp.0,
1467                    size: stamp.1,
1468                    bytes: stale,
1469                },
1470            );
1471        });
1472    }
1473
1474    fn clear_inner(g: &mut CatalogState) {
1475        g.by_dir.clear();
1476        g.by_idx.clear();
1477        g.by_pack.clear();
1478        g.bounded_pack_order.clear();
1479        g.pack_ids.clear();
1480    }
1481
1482    /// Delta-base byte cap for the active read store (default [`DELTA_BASE_CACHE_DEFAULT`]).
1483    pub fn set_delta_base_cache_byte_limit(limit: usize) {
1484        crate::pack_store::PackStore::active_for_reads()
1485            .delta_bases()
1486            .set_byte_limit(limit);
1487    }
1488
1489    #[cfg(test)]
1490    pub fn test_delta_base_cache_bytes_used(pack_path: &Path) -> usize {
1491        crate::pack_store::PackStore::legacy_for_pack_path(pack_path)
1492            .delta_bases()
1493            .bytes_used()
1494    }
1495
1496    #[cfg(test)]
1497    pub fn test_set_delta_base_cache_byte_limit(pack_path: &Path, limit: usize) {
1498        crate::pack_store::PackStore::legacy_for_pack_path(pack_path)
1499            .delta_bases()
1500            .set_byte_limit(limit);
1501    }
1502
1503    #[cfg(test)]
1504    pub fn test_delta_base_cached(pack_path: &Path, pack_id: u32, offset: u64) -> bool {
1505        crate::pack_store::PackStore::legacy_for_pack_path(pack_path)
1506            .delta_bases()
1507            .contains(pack_id, offset)
1508    }
1509
1510    fn pack_id_for_in_context(pack_path: &Path) -> u32 {
1511        if let Some(id) = PACK_IDS.with(|cell| cell.borrow().get(pack_path.as_os_str()).copied()) {
1512            return id;
1513        }
1514        let store = crate::pack_store::PackStore::active_for_reads();
1515        if let Some(id) = store.with_catalog_read(|g| g.pack_ids.get(pack_path).copied()) {
1516            PACK_IDS.with(|cell| {
1517                cell.borrow_mut()
1518                    .insert(pack_path.as_os_str().to_owned(), id);
1519            });
1520            return id;
1521        }
1522        store.with_catalog(|g| {
1523            if let Some(&id) = g.pack_ids.get(pack_path) {
1524                return id;
1525            }
1526            let id = g.next_pack_id;
1527            g.next_pack_id = g.next_pack_id.saturating_add(1);
1528            g.pack_ids.insert(pack_path.to_path_buf(), id);
1529            PACK_IDS.with(|cell| {
1530                cell.borrow_mut()
1531                    .insert(pack_path.as_os_str().to_owned(), id);
1532            });
1533            id
1534        })
1535    }
1536
1537    /// Stable small id for `pack_path`, assigned on first use.
1538    pub fn pack_id_for(pack_path: &Path) -> u32 {
1539        if crate::pack_store::PackStore::has_context() {
1540            return pack_id_for_in_context(pack_path);
1541        }
1542        let store = crate::pack_store::PackStore::legacy_for_pack_path(pack_path);
1543        crate::pack_store::PackStore::with_context(store, || pack_id_for_in_context(pack_path))
1544    }
1545
1546    /// Drop every cached delta base inflated from `pack_path`.
1547    fn drop_delta_entries_locked(
1548        g: &mut CatalogState,
1549        pack_path: &Path,
1550        delta: &ShardedDeltaBaseLru,
1551    ) {
1552        if let Some(&pack_id) = g.pack_ids.get(pack_path) {
1553            delta.drop_pack(pack_id);
1554        }
1555    }
1556
1557    /// Cached resolved object at `(pack_id, offset)`, if still resident.
1558    pub fn get_delta_base(pack_id: u32, offset: u64) -> Option<(ObjectKind, Arc<[u8]>)> {
1559        crate::pack_store::PackStore::active_for_reads()
1560            .delta_bases()
1561            .get(DeltaCacheKey { pack_id, offset })
1562    }
1563
1564    #[must_use]
1565    pub fn delta_base_caching_enabled() -> bool {
1566        crate::pack_store::PackStore::active_for_reads()
1567            .delta_bases()
1568            .caching_enabled()
1569    }
1570
1571    /// Insert many intermediate bases (hot path for delta-chain resolution).
1572    pub fn put_delta_bases(kind: ObjectKind, items: &[(u32, u64, Arc<[u8]>)]) {
1573        if items.is_empty() {
1574            return;
1575        }
1576        crate::pack_store::PackStore::active_for_reads()
1577            .delta_bases()
1578            .put_many(kind, items);
1579    }
1580
1581    /// Drop only cached delta bases (pack indexes and bytes stay cached).
1582    pub fn clear_delta_bases() {
1583        crate::pack_store::PackStore::active_for_reads()
1584            .delta_bases()
1585            .clear();
1586    }
1587
1588    /// Re-stamp the cached signature for `pack_path` after the caller deliberately touched the
1589    /// file's mtime (object freshening). Pack contents are immutable for a given pack name, so
1590    /// a self-inflicted mtime bump must not evict the cached bytes — without this, every
1591    /// `odb.write` of an already-packed object forced a full re-read of the pack on the next
1592    /// lookup. We bump the cached mtime to now without `stat` (the caller just utime'd the file).
1593    /// External modifications still invalidate via [`revalidate_stale_pack_bytes`].
1594    pub fn refresh_pack_signature(pack_path: &Path, touched_at: SystemTime) {
1595        with_current(|g| {
1596            if let Some(c) = g.by_pack.get_mut(pack_path) {
1597                c.mtime = touched_at;
1598            }
1599        });
1600    }
1601
1602    #[cfg(test)]
1603    #[test]
1604    fn bounded_pack_cache_releases_inactive_readers() {
1605        let dir = tempfile::tempdir().expect("dir");
1606        let store = Arc::new(crate::pack_store::PackStore::for_bounded_reads(
1607            dir.path().to_path_buf(),
1608        ));
1609        crate::pack_store::PackStore::with_context(Arc::clone(&store), || {
1610            let mut previous = Vec::new();
1611            let hot_path = dir.path().join("0.pack");
1612            let mut hot = None;
1613            for i in 0..70 {
1614                let path = dir.path().join(format!("{i}.pack"));
1615                let mut bytes = vec![i as u8; 32768];
1616                bytes[..12].copy_from_slice(b"PACK\x00\x00\x00\x02\x00\x00\x00\x00");
1617                fs::write(&path, bytes).expect("write");
1618                let pack = get_pack_bytes(&path).expect("pack");
1619                assert!(pack.uses_positioned_reads());
1620                previous.push(Arc::downgrade(&pack));
1621                hot.get_or_insert_with(|| Arc::clone(&pack));
1622                assert!(Arc::ptr_eq(
1623                    &get_pack_bytes(&hot_path).expect("hot pack"),
1624                    hot.as_ref().expect("hot")
1625                ));
1626            }
1627            warm_thread_local_from_store(&store);
1628            assert!(store.with_catalog_read(|g| g.by_pack.len()) <= 32);
1629            assert!(previous[1..32].iter().all(|pack| pack.upgrade().is_none()));
1630            assert!(previous[69].upgrade().is_some());
1631            let reloaded = get_pack_bytes(&dir.path().join("1.pack")).expect("reopen");
1632            assert_eq!(
1633                reloaded
1634                    .read_range(100, 200, &mut Vec::new())
1635                    .expect("read"),
1636                &[1; 100]
1637            );
1638        });
1639    }
1640}
1641
1642pub use pack_cache::revalidate_stale_pack_bytes;
1643
1644/// Read all pack indexes under `<objects_dir>/pack/` from the process-wide cache.
1645///
1646/// Cached reads skip the `.idx` SHA-1 trailer verification that [`read_pack_index`]
1647/// performs; corruption checks happen during `fsck`/`verify-pack`, not on every object
1648/// lookup (matches Git). The directory listing is prepared once per process and
1649/// re-scanned only on a lookup miss when the pack directory's mtime changed (Git
1650/// `reprepare_packed_git`), or when [`clear_pack_cache`] drops the cache after repack/gc.
1651///
1652/// # Errors
1653///
1654/// Returns [`Error::Io`] when the directory cannot be enumerated.
1655pub fn read_local_pack_indexes_cached(objects_dir: &Path) -> Result<Vec<Arc<PackIndex>>> {
1656    if crate::pack_store::PackStore::has_context() {
1657        return pack_cache::get_dir_indexes(objects_dir);
1658    }
1659    let store = crate::pack_store::PackStore::legacy_for_objects_dir(objects_dir);
1660    crate::pack_store::PackStore::with_context(store, || pack_cache::get_dir_indexes(objects_dir))
1661}
1662
1663/// Re-scan the pack directory when its mtime changed since the cached listing.
1664///
1665/// Called when an object was not found in loose storage or any known pack; returns
1666/// `true` when the listing was refreshed so the caller can retry once.
1667///
1668/// # Errors
1669///
1670/// Returns [`Error::Io`] when the pack directory cannot be read.
1671pub fn reprepare_pack_directory_on_miss(objects_dir: &Path) -> Result<bool> {
1672    if crate::pack_store::PackStore::has_context() {
1673        return reprepare_pack_directory_on_miss_in_context(objects_dir);
1674    }
1675    let store = crate::pack_store::PackStore::legacy_for_objects_dir(objects_dir);
1676    crate::pack_store::PackStore::with_context(store, || {
1677        reprepare_pack_directory_on_miss_in_context(objects_dir)
1678    })
1679}
1680
1681fn reprepare_pack_directory_on_miss_in_context(objects_dir: &Path) -> Result<bool> {
1682    let refreshed = pack_cache::reprepare_dir_on_miss(objects_dir)?;
1683    if refreshed {
1684        crate::midx::evict_midx_read_cache_for_pack_dir(&objects_dir.join("pack"));
1685    }
1686    Ok(refreshed)
1687}
1688
1689/// Read a single pack index from the process-wide cache (parses from disk on miss).
1690/// Skips trailer verification.
1691///
1692/// # Errors
1693///
1694/// Returns [`Error::Io`] when the file is missing or [`Error::CorruptObject`] for
1695/// malformed indexes.
1696pub fn read_pack_index_cached(idx_path: &Path) -> Result<Arc<PackIndex>> {
1697    if crate::pack_store::PackStore::has_context() {
1698        return pack_cache::get_index(idx_path);
1699    }
1700    let pack_path = idx_path.with_extension("pack");
1701    let store = crate::pack_store::PackStore::legacy_for_pack_path(&pack_path);
1702    crate::pack_store::PackStore::with_context(store, || pack_cache::get_index(idx_path))
1703}
1704
1705/// Read pack file bytes from the process-wide cache.
1706///
1707/// # Errors
1708///
1709/// Returns [`Error::Io`] when the pack cannot be read.
1710pub fn read_pack_bytes_cached(pack_path: &Path) -> Result<Arc<PackData>> {
1711    if crate::pack_store::PackStore::has_context() {
1712        return pack_cache::get_pack_bytes(pack_path);
1713    }
1714    let store = crate::pack_store::PackStore::legacy_for_pack_path(pack_path);
1715    crate::pack_store::PackStore::with_context(store, || pack_cache::get_pack_bytes(pack_path))
1716}
1717
1718/// Drop pack/MIDX caches on every registered [`PackStore`](crate::pack_store::PackStore).
1719///
1720/// Prefer [`Odb::invalidate_packs`](crate::odb::Odb::invalidate_packs) when an [`Odb`](crate::odb::Odb)
1721/// handle is available. Tests and benchmarks use this to reset all stores in the process.
1722pub fn clear_pack_cache() {
1723    #[cfg(test)]
1724    let _guard = pack_cache_test_guard();
1725    clear_pack_read_info_cache();
1726    pack_cache::clear_thread_local_dir_indexes();
1727    crate::pack_store::PackStore::invalidate_all_legacy();
1728}
1729
1730/// Test helper: reset pack marker stat counter (integration tests only).
1731#[cfg(test)]
1732pub fn test_reset_pack_marker_stat_count() {
1733    pack_cache::test_reset_marker_stat_count();
1734}
1735
1736/// Test helper: read pack marker stat counter (integration tests only).
1737#[cfg(test)]
1738#[must_use]
1739pub fn test_pack_marker_stat_count() -> u64 {
1740    pack_cache::test_marker_stat_count()
1741}
1742
1743/// Drop only the process-wide delta-base LRU (indexes and pack bytes stay cached).
1744pub fn clear_pack_delta_base_cache() {
1745    pack_cache::clear_delta_bases();
1746}
1747
1748/// Apply `core.deltaBaseCacheLimit` from repository config to the process-wide delta-base LRU.
1749pub fn configure_delta_base_cache_from_config(cfg: Option<&crate::config::ConfigSet>) {
1750    let bytes = cfg
1751        .and_then(|c| c.get("core.deltaBaseCacheLimit"))
1752        .as_ref()
1753        .and_then(|v| crate::config::parse_i64(v).ok())
1754        .and_then(|n| usize::try_from(n).ok())
1755        .unwrap_or(pack_cache::DELTA_BASE_CACHE_DEFAULT);
1756    pack_cache::set_delta_base_cache_byte_limit(bytes);
1757}
1758
1759/// Re-stamp the cached pack-bytes signature after deliberately touching `pack_path`'s mtime
1760/// (object freshening). See the internal `pack_cache::refresh_pack_signature` helper.
1761pub fn refresh_pack_bytes_signature(pack_path: &Path, touched_at: SystemTime) {
1762    pack_cache::refresh_pack_signature(pack_path, touched_at);
1763}
1764
1765/// Collect aggregate local pack metrics.
1766///
1767/// # Errors
1768///
1769/// Returns [`Error::Io`] when reading pack metadata fails.
1770pub fn collect_local_pack_info(objects_dir: &Path) -> Result<LocalPackInfo> {
1771    let indexes = read_local_pack_indexes(objects_dir)?;
1772    let mut info = LocalPackInfo::default();
1773    for idx in indexes {
1774        let pack_meta = fs::metadata(&idx.pack_path).map_err(Error::Io)?;
1775        let idx_meta = fs::metadata(&idx.idx_path).map_err(Error::Io)?;
1776        info.pack_count += 1;
1777        info.object_count += idx.len();
1778        info.size_bytes += pack_meta.len() + idx_meta.len();
1779        let hash_bytes = idx.hash_bytes();
1780        for entry in idx.iter() {
1781            if entry.oid().len() == hash_bytes {
1782                if let Ok(oid) = ObjectId::from_bytes(entry.oid()) {
1783                    info.object_ids.insert(oid);
1784                }
1785            }
1786        }
1787    }
1788    Ok(info)
1789}
1790
1791/// Write a version-2 `.idx` for `entries` describing objects in `pack_path`.
1792///
1793/// `entries` lists `(oid, byte_offset)` pairs; they are sorted by OID before
1794/// writing. The pack file must already exist on disk with a valid trailing
1795/// checksum at the repository hash width `hash_bytes` (`20` or `32`).
1796///
1797/// # Errors
1798///
1799/// Returns [`Error::CorruptObject`] when the pack is too small or an entry
1800/// offset cannot be walked, or [`Error::Io`] on filesystem failure.
1801/// Write a v2 pack index file.
1802///
1803/// `sort_threads` controls OID-table sorting: `None` or `Some(1)` uses a single thread;
1804/// `Some(n)` with `n > 1` may sort chunks on up to `n` workers (for large entry counts).
1805pub fn write_v2_pack_index_with_trailer(
1806    idx_path: &Path,
1807    entries: &[(ObjectId, u64, u32)],
1808    pack_trailer: &[u8],
1809    hash_bytes: usize,
1810    sort_threads: Option<usize>,
1811) -> Result<()> {
1812    write_v2_pack_index_sorting(
1813        idx_path,
1814        &mut entries.to_vec(),
1815        pack_trailer,
1816        hash_bytes,
1817        sort_threads,
1818    )
1819}
1820
1821/// `write_v2_pack_index` function.
1822pub fn write_v2_pack_index(
1823    idx_path: &Path,
1824    pack_path: &Path,
1825    entries: &[(ObjectId, u64, u32)],
1826    hash_bytes: usize,
1827) -> Result<()> {
1828    let pack_bytes = fs::read(pack_path).map_err(Error::Io)?;
1829    if pack_bytes.len() < hash_bytes {
1830        return Err(Error::CorruptObject(
1831            "pack too small for idx trailer".into(),
1832        ));
1833    }
1834    let trailer = &pack_bytes[pack_bytes.len() - hash_bytes..];
1835    write_v2_pack_index_with_trailer(idx_path, entries, trailer, hash_bytes, None)
1836}
1837
1838const PARALLEL_INDEX_SORT_MIN: usize = 8_192;
1839
1840/// Sort `entries` by object id in place and return the v2 fanout table.
1841///
1842/// Entries are first partitioned in place by their leading byte (which also yields the
1843/// fanout), then contiguous runs of buckets are sorted on up to `sort_threads` workers.
1844/// No copy of the entries is made.
1845fn sort_index_entries(
1846    entries: &mut [(ObjectId, u64, u32)],
1847    sort_threads: Option<usize>,
1848) -> [u32; 256] {
1849    let lead = |entry: &(ObjectId, u64, u32)| usize::from(entry.0.as_bytes()[0]);
1850    let mut counts = [0usize; 256];
1851    for entry in entries.iter() {
1852        counts[lead(entry)] += 1;
1853    }
1854    let mut ends = [0usize; 256];
1855    let mut total = 0usize;
1856    for (end, count) in ends.iter_mut().zip(counts) {
1857        total += count;
1858        *end = total;
1859    }
1860    let mut next = [0usize; 256];
1861    next[1..].copy_from_slice(&ends[..255]);
1862    for bucket in 0..256 {
1863        while next[bucket] < ends[bucket] {
1864            let at = next[bucket];
1865            let home = lead(&entries[at]);
1866            if home == bucket {
1867                next[bucket] += 1;
1868            } else {
1869                entries.swap(at, next[home]);
1870                next[home] += 1;
1871            }
1872        }
1873    }
1874
1875    let by_oid =
1876        |a: &(ObjectId, u64, u32), b: &(ObjectId, u64, u32)| a.0.as_bytes().cmp(b.0.as_bytes());
1877    let threads = sort_threads.unwrap_or(1).max(1);
1878    if threads <= 1 || entries.len() < PARALLEL_INDEX_SORT_MIN {
1879        let mut start = 0;
1880        for end in ends {
1881            entries[start..end].sort_unstable_by(by_oid);
1882            start = end;
1883        }
1884    } else {
1885        let share = entries.len().div_ceil(threads);
1886        std::thread::scope(|scope| {
1887            let mut rest = &mut *entries;
1888            let mut taken = 0usize;
1889            for end in ends {
1890                if end - taken >= share || end == total {
1891                    let (run, tail) = std::mem::take(&mut rest).split_at_mut(end - taken);
1892                    rest = tail;
1893                    taken = end;
1894                    if run.len() > 1 {
1895                        scope.spawn(move || run.sort_unstable_by(by_oid));
1896                    }
1897                }
1898            }
1899        });
1900    }
1901    ends.map(|end| u32::try_from(end).unwrap_or(u32::MAX))
1902}
1903
1904/// Sort `entries` in place and write them as a version-2 pack index at `idx_path`.
1905///
1906/// Streams the file through its checksum instead of building it in memory, so the only
1907/// large allocation is the caller's `entries`.
1908///
1909/// # Errors
1910///
1911/// Returns [`Error::CorruptObject`] when an object id or the trailer does not have
1912/// `hash_bytes` bytes, or the large-offset table overflows, and [`Error::Io`] on write
1913/// failure.
1914pub(crate) fn write_v2_pack_index_sorting(
1915    idx_path: &Path,
1916    entries: &mut [(ObjectId, u64, u32)],
1917    pack_trailer: &[u8],
1918    hash_bytes: usize,
1919    sort_threads: Option<usize>,
1920) -> Result<()> {
1921    use std::io::Write;
1922
1923    let Some(algo) = HashAlgo::from_len(hash_bytes) else {
1924        return Err(Error::CorruptObject(format!(
1925            "unsupported index hash width {hash_bytes}"
1926        )));
1927    };
1928    if pack_trailer.len() != hash_bytes {
1929        return Err(Error::CorruptObject(format!(
1930            "pack trailer length {} does not match hash width {hash_bytes}",
1931            pack_trailer.len()
1932        )));
1933    }
1934    if let Some((oid, _, _)) = entries
1935        .iter()
1936        .find(|(oid, _, _)| oid.as_bytes().len() != hash_bytes)
1937    {
1938        return Err(Error::CorruptObject(format!(
1939            "OID width {} does not match index hash width {hash_bytes}",
1940            oid.as_bytes().len()
1941        )));
1942    }
1943    let fanout = sort_index_entries(entries, sort_threads);
1944
1945    let file = fs::File::create(idx_path).map_err(Error::Io)?;
1946    let mut out = std::io::BufWriter::with_capacity(256 * 1024, file);
1947    let mut hasher = algo.checksum_hasher();
1948    let mut put = |bytes: &[u8]| -> Result<()> {
1949        hasher.update(bytes);
1950        out.write_all(bytes).map_err(Error::Io)
1951    };
1952    put(b"\xfftOc")?;
1953    put(&2u32.to_be_bytes())?;
1954    for f in fanout {
1955        put(&f.to_be_bytes())?;
1956    }
1957    for (oid, _, _) in entries.iter() {
1958        put(oid.as_bytes())?;
1959    }
1960    for (_, _, crc) in entries.iter() {
1961        put(&crc.to_be_bytes())?;
1962    }
1963    let mut large = 0u32;
1964    for (_, off, _) in entries.iter() {
1965        match u32::try_from(*off) {
1966            Ok(small) if small < 0x8000_0000 => put(&small.to_be_bytes())?,
1967            _ => {
1968                if large >= 0x8000_0000 {
1969                    return Err(Error::CorruptObject(
1970                        "pack index large-offset table overflow".to_owned(),
1971                    ));
1972                }
1973                put(&(0x8000_0000u32 | large).to_be_bytes())?;
1974                large += 1;
1975            }
1976        }
1977    }
1978    for (_, off, _) in entries.iter() {
1979        if *off >= 0x8000_0000 {
1980            put(&off.to_be_bytes())?;
1981        }
1982    }
1983    put(pack_trailer)?;
1984    let digest = hasher.finalize();
1985    out.write_all(&digest).map_err(Error::Io)?;
1986    out.flush().map_err(Error::Io)?;
1987    Ok(())
1988}
1989
1990#[must_use]
1991/// `oid_bytes_to_hex` function.
1992pub fn oid_bytes_to_hex(oid: &[u8]) -> String {
1993    hex::encode(oid)
1994}
1995
1996/// Hash canonical loose object bytes (`kind SP size NUL data`) with the repo hash width.
1997pub fn hash_object_bytes(kind: ObjectKind, data: &[u8], hash_bytes: usize) -> Result<Vec<u8>> {
1998    let Some(algo) = HashAlgo::from_len(hash_bytes) else {
1999        return Err(Error::CorruptObject(format!(
2000            "unsupported object hash width: {hash_bytes}"
2001        )));
2002    };
2003    Ok(crate::hash::hash_object(algo, kind, data)?
2004        .as_bytes()
2005        .to_vec())
2006}
2007
2008/// Parse a pack index file (version 1 legacy or version 2), verifying the SHA-1
2009/// trailer checksum.
2010///
2011/// Used by `fsck`/`verify-pack` and similar code that wants on-disk validation. Hot
2012/// object-lookup paths should call [`read_pack_index_cached`] (which skips trailer
2013/// verification, matching Git's normal read path).
2014///
2015/// # Errors
2016///
2017/// Returns [`Error::CorruptObject`] when format checks fail and
2018/// [`Error::PackIndexChecksumMismatch`] when the `.idx` trailer is wrong.
2019pub fn read_pack_index(idx_path: &Path) -> Result<PackIndex> {
2020    crate::pack_index::open_pack_index_file(idx_path, true)
2021}
2022
2023/// Parse a pack index file without verifying the SHA-1 trailer checksum.
2024///
2025/// Git reads the `.idx` offset table without re-checking its trailer in the MIDX
2026/// write path (`midx-write.c`/`packfile.c` `open_pack_index`), so a deliberately
2027/// corrupted-but-structurally-valid idx (t5319 64-bit offset tests) still loads.
2028pub fn read_pack_index_no_verify(idx_path: &Path) -> Result<PackIndex> {
2029    crate::pack_index::open_pack_index_file(idx_path, false)
2030}
2031
2032/// A pack object type as encoded in the packed stream header.
2033#[derive(Debug, Clone, Copy, PartialEq, Eq)]
2034pub enum PackedType {
2035    /// Commit object.
2036    Commit,
2037    /// Tree object.
2038    Tree,
2039    /// Blob object.
2040    Blob,
2041    /// Tag object.
2042    Tag,
2043    /// Offset delta.
2044    OfsDelta,
2045    /// Reference delta.
2046    RefDelta,
2047}
2048
2049impl PackedType {
2050    /// Printable name used by `verify-pack -v` output.
2051    #[must_use]
2052    pub fn as_str(self) -> &'static str {
2053        match self {
2054            Self::Commit => "commit",
2055            Self::Tree => "tree",
2056            Self::Blob => "blob",
2057            Self::Tag => "tag",
2058            Self::OfsDelta => "ofs-delta",
2059            Self::RefDelta => "ref-delta",
2060        }
2061    }
2062}
2063
2064/// A decoded object header record used by `verify-pack`.
2065#[derive(Debug, Clone)]
2066pub struct VerifyObjectRecord {
2067    /// Object ID from the index (20 or 32 raw bytes).
2068    pub oid: Vec<u8>,
2069    /// Type from the pack stream header.
2070    pub packed_type: PackedType,
2071    /// Uncompressed object size from the pack header.
2072    pub size: u64,
2073    /// Total bytes in pack occupied by this object slot.
2074    pub size_in_pack: u64,
2075    /// Offset in pack file.
2076    pub offset: u64,
2077    /// Delta chain depth, if deltified.
2078    pub depth: Option<u64>,
2079    /// Base object for ref-delta objects.
2080    pub base_oid: Option<Vec<u8>>,
2081    /// In-pack base offset for ofs-delta objects.
2082    pub base_pack_offset: Option<u64>,
2083}
2084
2085/// How a delta object in a pack references its base, used to compute chain depth order-independently.
2086enum DeltaBaseLink {
2087    /// `REF_DELTA`: base identified by raw object id (20 or 32 bytes).
2088    Oid(Vec<u8>),
2089    /// `OFS_DELTA`: base identified by its absolute offset in the pack.
2090    Offset(u64),
2091}
2092
2093/// Resolve the delta-chain depth of record `i`, memoizing the result into `records[i].depth`.
2094///
2095/// Full (non-delta) objects have depth 0. A delta's depth is one greater than its base's depth.
2096/// Following base links by offset/oid makes this independent of the order objects appear in the
2097/// pack — a ref-delta's base may be stored *after* the delta itself. A base that is not present in
2098/// this pack (thin pack) or a cycle is treated as depth 0 for the missing/looping link.
2099///
2100/// # Errors
2101///
2102/// Returns [`Error::CorruptObject`] when a ref-delta record is missing its base oid.
2103fn resolve_delta_depth(
2104    i: usize,
2105    base_links: &[Option<DeltaBaseLink>],
2106    by_oid: &HashMap<Vec<u8>, usize>,
2107    by_offset_idx: &HashMap<u64, usize>,
2108    records: &mut [VerifyObjectRecord],
2109) -> Result<u64> {
2110    if let Some(d) = records[i].depth {
2111        return Ok(d);
2112    }
2113    let Some(link) = &base_links[i] else {
2114        return Ok(0);
2115    };
2116    let base_idx = match link {
2117        DeltaBaseLink::Oid(oid) => by_oid.get(oid).copied(),
2118        DeltaBaseLink::Offset(off) => by_offset_idx.get(off).copied(),
2119    };
2120    // Mark this record visited before recursing so a malformed cyclic chain cannot recurse forever.
2121    records[i].depth = Some(1);
2122    let depth = match base_idx {
2123        Some(b) if b != i => {
2124            resolve_delta_depth(b, base_links, by_oid, by_offset_idx, records)?.saturating_add(1)
2125        }
2126        // Base absent from this pack (thin) or self-referential: count this delta as depth 1.
2127        _ => 1,
2128    };
2129    records[i].depth = Some(depth);
2130    Ok(depth)
2131}
2132
2133/// Default in-pack delta chain depth when `pack.depth` is unset (matches Git).
2134pub const DEFAULT_PACK_DEPTH: usize = 50;
2135
2136/// Longest delta-chain depth among verify-pack records (number of edges from base to tip).
2137#[must_use]
2138pub fn max_verify_pack_delta_depth(records: &[VerifyObjectRecord]) -> u64 {
2139    records.iter().filter_map(|r| r.depth).max().unwrap_or(0)
2140}
2141
2142/// Targets whose delta edge must be dropped so a target→base graph has no cycle.
2143///
2144/// `base_of` returns the base of each object stored as a delta. Chains are walked from
2145/// `starts` only, so a cycle is found when one of its objects is a start; passing every
2146/// delta target finds them all. A cycle (`a` against `b`, `b` against `a`, or longer)
2147/// can never be resolved by a reader, yet appears when deltas are reused from several
2148/// packs that stored the same objects in opposite directions. For every cycle the
2149/// smallest object id is returned, so the choice does not depend on `starts` order;
2150/// reverting those targets to whole objects leaves every chain ending in a whole object.
2151pub(crate) fn delta_cycle_breaks(
2152    starts: impl IntoIterator<Item = ObjectId>,
2153    base_of: impl Fn(&ObjectId) -> Option<ObjectId>,
2154) -> Vec<ObjectId> {
2155    // `Some(i)`: at position `i` of the walk in progress; `None`: already settled.
2156    let mut seen: crate::objects::OidHashMap<Option<usize>> = crate::objects::oid_hash_map();
2157    let mut path: Vec<ObjectId> = Vec::new();
2158    let mut breaks = Vec::new();
2159    for start in starts {
2160        let mut cur = Some(start);
2161        while let Some(oid) = cur {
2162            match seen.get(&oid) {
2163                Some(None) => break,
2164                Some(Some(pos)) => {
2165                    breaks.extend(path[*pos..].iter().min().copied());
2166                    break;
2167                }
2168                None => {
2169                    seen.insert(oid, Some(path.len()));
2170                    path.push(oid);
2171                    cur = base_of(&oid);
2172                }
2173            }
2174        }
2175        for oid in path.drain(..) {
2176            seen.insert(oid, None);
2177        }
2178    }
2179    breaks
2180}
2181
2182/// Break delta cycles, then delta chains longer than `max_depth` edges, in a
2183/// target→base map.
2184///
2185/// Each entry maps a delta object to its chosen in-pack base. A cycle cannot be
2186/// resolved at any depth, so the smallest object id of each one loses its edge first.
2187/// Chains already within `max_depth` keep their original bases unchanged. Longer
2188/// chains are broken on Git's `(depth + 1)` modulo rule by removing only the delta
2189/// edges at cut points; links inside each resulting segment stay as originally chosen
2190/// (matching `break_delta_chains`).
2191pub fn apply_delta_depth_limit(map: &mut HashMap<ObjectId, ObjectId>, max_depth: usize) {
2192    for target in delta_cycle_breaks(map.keys().copied(), |oid| map.get(oid).copied()) {
2193        map.remove(&target);
2194    }
2195    cap_delta_chain_depth(map, max_depth);
2196}
2197
2198/// The chain-length half of [`apply_delta_depth_limit`], for a map already known to
2199/// be acyclic (a cycle is left as it is).
2200pub(crate) fn cap_delta_chain_depth(map: &mut HashMap<ObjectId, ObjectId>, max_depth: usize) {
2201    let keys: Vec<ObjectId> = map.keys().copied().collect();
2202    let value_set: HashSet<ObjectId> = map.values().copied().collect();
2203    let mut tips: Vec<ObjectId> = keys
2204        .into_iter()
2205        .filter(|k| !value_set.contains(k))
2206        .collect();
2207    tips.sort();
2208
2209    let modulus = max_depth.saturating_add(1);
2210    let mut snip: HashSet<ObjectId> = HashSet::new();
2211
2212    for tip in tips {
2213        let mut chain: Vec<ObjectId> = Vec::new();
2214        let mut cur = tip;
2215        let mut seen = HashSet::new();
2216        while seen.insert(cur) {
2217            chain.push(cur);
2218            let Some(&b) = map.get(&cur) else {
2219                break;
2220            };
2221            cur = b;
2222        }
2223
2224        let n = chain.len();
2225        if n < 2 || n - 1 <= max_depth {
2226            continue;
2227        }
2228
2229        let mut total_depth = (n - 1) as u32;
2230        for &oid in &chain {
2231            let assigned = (total_depth as usize) % modulus;
2232            total_depth = total_depth.saturating_sub(1);
2233            if assigned == 0 {
2234                snip.insert(oid);
2235            }
2236        }
2237    }
2238
2239    for oid in snip {
2240        map.remove(&oid);
2241    }
2242}
2243
2244/// Pack/index pair validated for fsck without inflating every object.
2245///
2246/// Structure, checksums, and per-entry headers are checked; object hashes are
2247/// verified separately when objects are read for semantic fsck.
2248#[derive(Debug)]
2249pub(crate) struct VerifiedPackForFsck {
2250    pub idx: PackIndex,
2251    /// Memory-mapped pack file (owned only for tiny packs or when mapping fails).
2252    pub pack_bytes: Arc<PackData>,
2253    pub records: Vec<VerifyObjectRecord>,
2254}
2255
2256/// Verify pack trailer, index checksum, counts, and object header layout.
2257///
2258/// Does not inflate objects or verify per-object hashes (see [`verify_pack_and_collect`]).
2259///
2260/// # Errors
2261///
2262/// Returns [`Error::CorruptObject`] when the index or pack are malformed.
2263pub(crate) fn verify_pack_for_fsck(idx_path: &Path) -> Result<VerifiedPackForFsck> {
2264    let (idx, pack_bytes, records) = verify_pack_structure_and_records(idx_path, true)?;
2265    Ok(VerifiedPackForFsck {
2266        idx,
2267        pack_bytes,
2268        records,
2269    })
2270}
2271
2272/// Pack-level trailer/index checksum problem detected while preparing object fsck.
2273#[derive(Debug, Clone, Copy, PartialEq, Eq)]
2274pub(crate) enum PackFsckTrailerIssue {
2275    /// Pack file trailing hash does not match payload.
2276    PackChecksum,
2277    /// Embedded pack hash in the `.idx` does not match the pack file.
2278    IdxMismatch,
2279}
2280
2281/// Load pack bytes and header records for per-object fsck even when the pack trailer is wrong.
2282///
2283/// Returns a trailer/index checksum issue separately so callers can still inflate objects.
2284pub(crate) fn prepare_pack_for_object_fsck(
2285    idx_path: &Path,
2286) -> Result<(VerifiedPackForFsck, Option<PackFsckTrailerIssue>)> {
2287    let (idx, pack_bytes, records) = verify_pack_structure_and_records(idx_path, false)?;
2288    let trailer = pack_trailer_fsck_issue(idx_path, &idx, &pack_bytes);
2289    Ok((
2290        VerifiedPackForFsck {
2291            idx,
2292            pack_bytes,
2293            records,
2294        },
2295        trailer,
2296    ))
2297}
2298
2299fn pack_trailer_fsck_issue(
2300    idx_path: &Path,
2301    idx: &PackIndex,
2302    pack_bytes: &[u8],
2303) -> Option<PackFsckTrailerIssue> {
2304    let hb = idx.hash_bytes();
2305    let algo = HashAlgo::from_len(hb)?;
2306    if verify_trailer(algo, pack_bytes).is_err() {
2307        return Some(PackFsckTrailerIssue::PackChecksum);
2308    }
2309    let idx_file_bytes = fs::read(idx_path).ok()?;
2310    if idx_file_bytes.len() >= 2 * hb {
2311        let pack_end = pack_bytes.len().checked_sub(hb)?;
2312        let n = idx_file_bytes.len();
2313        let embedded = &idx_file_bytes[n - 2 * hb..n - hb];
2314        if embedded != &pack_bytes[pack_end..] {
2315            return Some(PackFsckTrailerIssue::IdxMismatch);
2316        }
2317    }
2318    None
2319}
2320
2321/// Verify one pack/index pair and optionally return object records.
2322///
2323/// # Errors
2324///
2325/// Returns [`Error::CorruptObject`] when the index or pack are malformed.
2326pub fn verify_pack_and_collect(idx_path: &Path) -> Result<Vec<VerifyObjectRecord>> {
2327    let VerifiedPackForFsck {
2328        idx,
2329        pack_bytes,
2330        records,
2331    } = verify_pack_for_fsck(idx_path)?;
2332    let hb = idx.hash_bytes();
2333    for entry in idx.iter() {
2334        let obj = read_object_from_pack_bytes(&pack_bytes, &idx, entry.oid())?;
2335        let computed = hash_object_bytes(obj.kind, &obj.data, hb)?;
2336        if computed.as_slice() != entry.oid() {
2337            return Err(Error::CorruptObject(format!(
2338                "pack object hash mismatch at offset {} (index says {})",
2339                entry.offset()?,
2340                oid_bytes_to_hex(entry.oid())
2341            )));
2342        }
2343    }
2344    Ok(records)
2345}
2346
2347fn verify_pack_structure_and_records(
2348    idx_path: &Path,
2349    verify_pack_trailer: bool,
2350) -> Result<(PackIndex, Arc<PackData>, Vec<VerifyObjectRecord>)> {
2351    let idx = read_pack_index(idx_path)?;
2352    let idx_file_bytes = fs::read(idx_path).map_err(Error::Io)?;
2353    let pack_bytes = PackData::open_with_min_len(&idx.pack_path, 0)?;
2354    let hb = idx.hash_bytes();
2355    if pack_bytes.len() < 12 + hb {
2356        return Err(Error::CorruptObject(format!(
2357            "pack file {} is too small",
2358            idx.pack_path.display()
2359        )));
2360    }
2361    let pack_end = pack_bytes.len() - hb;
2362    let Some(algo) = HashAlgo::from_len(hb) else {
2363        return Err(Error::CorruptObject(format!(
2364            "unsupported OID width {hb} for pack {}",
2365            idx.pack_path.display()
2366        )));
2367    };
2368    if verify_pack_trailer {
2369        verify_trailer(algo, &pack_bytes).map_err(|e| {
2370            Error::CorruptObject(format!(
2371                "pack trailing checksum mismatch for {}: {e}",
2372                idx.pack_path.display()
2373            ))
2374        })?;
2375        // The `.idx` ends with the pack checksum followed by its own checksum, both
2376        // at the repository hash width `hb` (20 for SHA-1, 32 for SHA-256).
2377        if idx_file_bytes.len() >= 2 * hb {
2378            let n = idx_file_bytes.len();
2379            let embedded = &idx_file_bytes[n - 2 * hb..n - hb];
2380            if embedded != &pack_bytes[pack_end..] {
2381                return Err(Error::CorruptObject(format!(
2382                    "pack checksum in index does not match {}",
2383                    idx.pack_path.display()
2384                )));
2385            }
2386        }
2387    }
2388    if &pack_bytes[0..4] != b"PACK" {
2389        return Err(Error::CorruptObject(format!(
2390            "pack file {} has invalid signature",
2391            idx.pack_path.display()
2392        )));
2393    }
2394    let version = u32::from_be_bytes(pack_bytes[4..8].try_into().unwrap_or([0, 0, 0, 0]));
2395    if version != 2 && version != 3 {
2396        return Err(Error::CorruptObject(format!(
2397            "unsupported pack version {} in {}",
2398            version,
2399            idx.pack_path.display()
2400        )));
2401    }
2402    let count = u32::from_be_bytes(pack_bytes[8..12].try_into().unwrap_or([0, 0, 0, 0])) as usize;
2403    if count != idx.len() {
2404        return Err(Error::CorruptObject(format!(
2405            "pack/index object count mismatch for {}",
2406            idx.pack_path.display()
2407        )));
2408    }
2409
2410    let mut by_offset: BTreeMap<u64, Vec<u8>> = BTreeMap::new();
2411    for entry in idx.iter() {
2412        by_offset.insert(entry.offset()?, entry.oid().to_vec());
2413    }
2414    let offsets: Vec<u64> = by_offset.keys().copied().collect();
2415    if offsets.is_empty() {
2416        return Ok((idx, pack_bytes, Vec::new()));
2417    }
2418
2419    let mut by_oid: HashMap<Vec<u8>, usize> = HashMap::new();
2420    let mut by_offset_idx: HashMap<u64, usize> = HashMap::new();
2421    let mut records: Vec<VerifyObjectRecord> = Vec::with_capacity(offsets.len());
2422    // Per-record base pointer for delta objects, captured while scanning headers and resolved into
2423    // chain depths afterwards. Delta bases can appear *after* the delta in pack order (ref-deltas in
2424    // particular), so depth must be computed by following these pointers, not in scan order.
2425    let mut base_links: Vec<Option<DeltaBaseLink>> = Vec::with_capacity(offsets.len());
2426    for (i, offset) in offsets.iter().copied().enumerate() {
2427        let oid = by_offset.get(&offset).cloned().ok_or_else(|| {
2428            Error::CorruptObject(format!("missing object id for offset {}", offset))
2429        })?;
2430        let next_off = offsets
2431            .get(i + 1)
2432            .copied()
2433            .unwrap_or((pack_bytes.len() - hb) as u64);
2434        if next_off <= offset || next_off > (pack_bytes.len() - hb) as u64 {
2435            return Err(Error::CorruptObject(format!(
2436                "invalid object boundaries at offset {} in {}",
2437                offset,
2438                idx.pack_path.display()
2439            )));
2440        }
2441        let mut p = offset as usize;
2442        let (packed_type, size) = parse_pack_object_header(&pack_bytes, &mut p)?;
2443        let mut base_oid: Option<Vec<u8>> = None;
2444        let mut base_link: Option<DeltaBaseLink> = None;
2445
2446        match packed_type {
2447            PackedType::RefDelta => {
2448                if p + hb > pack_bytes.len() {
2449                    return Err(Error::CorruptObject(format!(
2450                        "truncated ref-delta base at offset {}",
2451                        offset
2452                    )));
2453                }
2454                let raw = pack_bytes[p..p + hb].to_vec();
2455                base_oid = Some(raw.clone());
2456                base_link = Some(DeltaBaseLink::Oid(raw));
2457            }
2458            PackedType::OfsDelta => {
2459                let base_offset = parse_ofs_delta_base(&pack_bytes, &mut p, offset)?;
2460                base_link = Some(DeltaBaseLink::Offset(base_offset));
2461            }
2462            PackedType::Commit | PackedType::Tree | PackedType::Blob | PackedType::Tag => {}
2463        }
2464
2465        let size_in_pack = next_off - offset;
2466        let base_pack_offset = match &base_link {
2467            Some(DeltaBaseLink::Offset(off)) => Some(*off),
2468            _ => None,
2469        };
2470        records.push(VerifyObjectRecord {
2471            oid: oid.clone(),
2472            packed_type,
2473            size,
2474            size_in_pack,
2475            offset,
2476            depth: None,
2477            base_oid,
2478            base_pack_offset,
2479        });
2480        base_links.push(base_link);
2481        by_oid.insert(oid, i);
2482        by_offset_idx.insert(offset, i);
2483    }
2484
2485    // Resolve delta chain depths by following base links to their record index, regardless of the
2486    // order objects appear in the pack. A delta's depth is one more than its base's depth; full
2487    // objects have depth 0 (represented as `None` in the record). Memoize to keep this O(n).
2488    for i in 0..records.len() {
2489        if base_links[i].is_some() {
2490            let _ = resolve_delta_depth(i, &base_links, &by_oid, &by_offset_idx, &mut records)?;
2491        }
2492    }
2493
2494    Ok((idx, pack_bytes, records))
2495}
2496
2497/// Read alternates recursively, deduplicated in discovery order.
2498///
2499/// # Errors
2500///
2501/// Returns [`Error::Io`] when alternate files cannot be read.
2502pub fn read_alternates_recursive(objects_dir: &Path) -> Result<Vec<PathBuf>> {
2503    let mut visited = HashSet::new();
2504    let mut out = Vec::new();
2505    read_alternates_inner(objects_dir, &mut visited, &mut out, 0)?;
2506    Ok(out)
2507}
2508
2509/// Maximum alternate chain depth (git uses 5).
2510const MAX_ALTERNATE_DEPTH: usize = 5;
2511
2512/// Parse one non-empty line from `objects/info/alternates`.
2513///
2514/// Blank lines and `#` comments are ignored. Paths may be absolute, relative to the
2515/// current `objects/` directory, or C-style quoted (Git `read_info/alternates`).
2516fn parse_alternates_file_line(raw: &str) -> Option<PathBuf> {
2517    let line = raw.trim();
2518    if line.is_empty() || line.starts_with('#') {
2519        return None;
2520    }
2521    if line.starts_with('"') {
2522        let path = unquote_alternates_c_style(line)?;
2523        if path.is_empty() {
2524            return None;
2525        }
2526        return Some(PathBuf::from(path));
2527    }
2528    Some(PathBuf::from(line))
2529}
2530
2531/// Unquote a double-quoted alternates path (Git `unquote_c_style` subset).
2532fn unquote_alternates_c_style(line: &str) -> Option<String> {
2533    let bytes = line.as_bytes();
2534    if bytes.first() != Some(&b'"') {
2535        return None;
2536    }
2537    let mut out = Vec::new();
2538    let mut i = 1usize;
2539    while i < bytes.len() {
2540        if bytes[i] == b'"' {
2541            return String::from_utf8(out).ok();
2542        }
2543        if bytes[i] == b'\\' {
2544            i += 1;
2545            if i >= bytes.len() {
2546                return None;
2547            }
2548            match bytes[i] {
2549                b'\\' => out.push(b'\\'),
2550                b'"' => out.push(b'"'),
2551                b'a' => out.push(7),
2552                b'b' => out.push(8),
2553                b'f' => out.push(12),
2554                b'n' => out.push(b'\n'),
2555                b'r' => out.push(b'\r'),
2556                b't' => out.push(b'\t'),
2557                b'v' => out.push(11),
2558                c if c.is_ascii_digit() => {
2559                    if i + 2 >= bytes.len() {
2560                        return None;
2561                    }
2562                    let oct = std::str::from_utf8(&bytes[i..i + 3]).ok()?;
2563                    out.push(u8::from_str_radix(oct, 8).ok()?);
2564                    i += 2;
2565                }
2566                other => out.push(other),
2567            }
2568            i += 1;
2569            continue;
2570        }
2571        out.push(bytes[i]);
2572        i += 1;
2573    }
2574    None
2575}
2576
2577fn read_alternates_inner(
2578    objects_dir: &Path,
2579    visited: &mut HashSet<PathBuf>,
2580    out: &mut Vec<PathBuf>,
2581    depth: usize,
2582) -> Result<()> {
2583    if depth > MAX_ALTERNATE_DEPTH {
2584        return Ok(());
2585    }
2586    // Read the alternates file before canonicalizing: the canonical form is only needed to
2587    // resolve relative entries and deduplicate, and canonicalize walks every path component
2588    // (a readlink per ancestor) — pure waste in the overwhelmingly common no-alternates case.
2589    let alt_file = objects_dir.join("info").join("alternates");
2590    let text = match fs::read_to_string(&alt_file) {
2591        Ok(text) => text,
2592        Err(err) if err.kind() == io::ErrorKind::NotFound => return Ok(()),
2593        Err(err) => return Err(Error::Io(err)),
2594    };
2595    let canonical = canonical_or_self(objects_dir);
2596
2597    for raw in text.lines() {
2598        let Some(path) = parse_alternates_file_line(raw) else {
2599            continue;
2600        };
2601        let candidate = if path.is_absolute() {
2602            path
2603        } else {
2604            canonical.join(path)
2605        };
2606        let candidate = canonical_or_self(&candidate);
2607        if visited.insert(candidate.clone()) {
2608            out.push(candidate.clone());
2609            read_alternates_inner(&candidate, visited, out, depth + 1)?;
2610        }
2611    }
2612    Ok(())
2613}
2614
2615fn canonical_or_self(path: &Path) -> PathBuf {
2616    path.canonicalize_simplified()
2617        .unwrap_or_else(|_| path.to_path_buf())
2618}
2619
2620/// Convert a [`PackedType`] to an [`ObjectKind`] for non-delta types.
2621fn packed_type_to_kind(pt: PackedType) -> Result<ObjectKind> {
2622    match pt {
2623        PackedType::Commit => Ok(ObjectKind::Commit),
2624        PackedType::Tree => Ok(ObjectKind::Tree),
2625        PackedType::Blob => Ok(ObjectKind::Blob),
2626        PackedType::Tag => Ok(ObjectKind::Tag),
2627        PackedType::OfsDelta | PackedType::RefDelta => Err(Error::CorruptObject(
2628            "cannot convert delta type to object kind directly".to_owned(),
2629        )),
2630    }
2631}
2632
2633// hygiene: per-thread pack zlib scratch and read-info cache (ROADMAP item 7 — pack read hot path)
2634thread_local! {
2635    static DELTA_SIZE_ZLIB: std::cell::RefCell<crate::zlib_inflate::ZlibInflateScratch> =
2636        std::cell::RefCell::new(crate::zlib_inflate::ZlibInflateScratch::default());
2637}
2638// hygiene: scoped batch read_info cache; cleared on pack invalidation and context exit
2639thread_local! {
2640    static PACK_READ_INFO_CACHE: std::cell::RefCell<HashMap<PackReadInfoCacheKey, ObjectInfo>> =
2641        std::cell::RefCell::new(HashMap::new());
2642}
2643
2644const PACK_READ_INFO_CACHE_CAP: usize = 600_000;
2645
2646#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
2647struct PackReadInfoCacheKey {
2648    pack_id: u32,
2649    offset: u64,
2650}
2651
2652fn pack_read_info_cache_key(pack_path: &Path, offset: u64) -> Option<PackReadInfoCacheKey> {
2653    if !crate::pack_store::PackStore::has_context() {
2654        return None;
2655    }
2656    Some(PackReadInfoCacheKey {
2657        pack_id: pack_cache::pack_id_for(pack_path),
2658        offset,
2659    })
2660}
2661
2662/// Drop cached batch `read_info` entries (pack context end, invalidation, repack).
2663pub(crate) fn clear_pack_read_info_cache() {
2664    PACK_READ_INFO_CACHE.with(|c| c.borrow_mut().clear());
2665}
2666
2667fn drop_pack_read_info_cache_for(pack_path: &Path) {
2668    let pack_id = pack_cache::pack_id_for(pack_path);
2669    PACK_READ_INFO_CACHE.with(|c| {
2670        c.borrow_mut().retain(|k, _| k.pack_id != pack_id);
2671    });
2672}
2673
2674fn cached_pack_read_info(pack_path: &Path, offset: u64) -> Option<ObjectInfo> {
2675    let key = pack_read_info_cache_key(pack_path, offset)?;
2676    PACK_READ_INFO_CACHE.with(|c| c.borrow().get(&key).cloned())
2677}
2678
2679fn remember_pack_read_info(pack_path: &Path, offset: u64, info: ObjectInfo) {
2680    let Some(key) = pack_read_info_cache_key(pack_path, offset) else {
2681        return;
2682    };
2683    PACK_READ_INFO_CACHE.with(|c| {
2684        let mut g = c.borrow_mut();
2685        if g.len() >= PACK_READ_INFO_CACHE_CAP {
2686            g.clear();
2687        }
2688        g.insert(key, info);
2689    });
2690}
2691/// Read the delta result-size varint from a pack zlib stream without inflating the full delta.
2692///
2693/// Advances `*pos` past the compressed bytes consumed from `bytes`.
2694pub(crate) fn read_delta_result_size_from_pack_zlib(bytes: &[u8], pos: &mut usize) -> Result<u64> {
2695    DELTA_SIZE_ZLIB.with(|slot| slot.borrow_mut().read_pack_delta_result_size(bytes, pos))
2696}
2697
2698fn decompress_pack_data_into(
2699    bytes: &[u8],
2700    pos: &mut usize,
2701    expected_size: u64,
2702    scratch: &mut ZlibInflateScratch,
2703    out: &mut Vec<u8>,
2704) -> Result<()> {
2705    scratch.decompress_fixed_into(bytes, pos, expected_size, out)
2706}
2707
2708/// Read and fully resolve one object from a pack file given its offset.
2709///
2710/// Handles OFS_DELTA and REF_DELTA by recursively reading the base object.
2711/// The `idx` is used for REF_DELTA resolution (to find a base by OID).
2712/// Deepest delta chain a read will follow before treating the pack as corrupt.
2713///
2714/// Safety valve against cyclic ref-delta chains and unbounded recursion, not a
2715/// format limit — Git reads chains of any depth (`pack.depth` caps only writers).
2716const MAX_DELTA_CHAIN_READ_DEPTH: usize = 4096;
2717
2718/// One hop in an in-flight delta chain (collected tip→base, applied base→tip).
2719#[derive(Clone)]
2720struct PendingDeltaFrame {
2721    pack_id: u32,
2722    offset: u64,
2723    delta: Vec<u8>,
2724}
2725
2726/// Tracks delta-chain depth and the current `(pack_id, offset)` path for cycle detection.
2727///
2728/// Also holds reusable scratch buffers for pack reads on this thread (cleared per resolution).
2729#[derive(Default)]
2730struct DeltaChainState {
2731    depth: usize,
2732    visited: HashSet<(u32, u64)>,
2733    pending: Vec<PendingDeltaFrame>,
2734    inflate: ZlibInflateScratch,
2735    whole_object: Vec<u8>,
2736    delta_decompress: Vec<u8>,
2737    chain_a: Vec<u8>,
2738    chain_b: Vec<u8>,
2739}
2740
2741// hygiene: pack read delta-chain state (ROADMAP item 7 — per-thread pack cache)
2742thread_local! {
2743    static PACK_READ_DELTA_STATE: std::cell::RefCell<DeltaChainState> =
2744        std::cell::RefCell::new(DeltaChainState::default());
2745}
2746
2747fn with_pack_read_delta_state<R>(
2748    start_depth: usize,
2749    f: impl FnOnce(&mut DeltaChainState) -> R,
2750) -> R {
2751    PACK_READ_DELTA_STATE.with(|slot| {
2752        let mut state = slot.borrow_mut();
2753        state.depth = start_depth;
2754        state.visited.clear();
2755        f(&mut state)
2756    })
2757}
2758
2759impl DeltaChainState {
2760    fn visit(&mut self, pack_id: u32, offset: u64) -> Result<()> {
2761        if self.depth > MAX_DELTA_CHAIN_READ_DEPTH {
2762            return Err(Error::DeltaChainTooDeep {
2763                limit: MAX_DELTA_CHAIN_READ_DEPTH,
2764            });
2765        }
2766        if !self.visited.insert((pack_id, offset)) {
2767            return Err(Error::DeltaChainTooDeep {
2768                limit: MAX_DELTA_CHAIN_READ_DEPTH,
2769            });
2770        }
2771        Ok(())
2772    }
2773
2774    fn next_hop(&mut self) -> Result<()> {
2775        self.depth += 1;
2776        if self.depth > MAX_DELTA_CHAIN_READ_DEPTH {
2777            return Err(Error::DeltaChainTooDeep {
2778                limit: MAX_DELTA_CHAIN_READ_DEPTH,
2779            });
2780        }
2781        Ok(())
2782    }
2783}
2784
2785/// Copy `payload` into `out`, reusing `out`'s capacity when sufficient.
2786fn copy_payload_preserve_capacity(out: &mut Vec<u8>, payload: &[u8]) {
2787    out.clear();
2788    out.extend_from_slice(payload);
2789}
2790
2791/// Like [`apply_delta_chain_forward`], writing the final payload into `out` without replacing
2792/// `out`'s allocation when the result fits in the existing capacity.
2793#[allow(clippy::too_many_arguments)]
2794fn apply_delta_chain_forward_into(
2795    out: &mut Vec<u8>,
2796    kind: ObjectKind,
2797    base_data: &[u8],
2798    whole_base: Option<(u32, u64)>,
2799    pending: &[PendingDeltaFrame],
2800    result_pack_id: u32,
2801    result_offset: u64,
2802    populate_delta_lru: bool,
2803    state: &mut DeltaChainState,
2804) -> Result<()> {
2805    if pending.is_empty() {
2806        copy_payload_preserve_capacity(out, base_data);
2807        return Ok(());
2808    }
2809
2810    let caching = populate_delta_lru && pack_cache::delta_base_caching_enabled();
2811    let mut cache_inserts: Vec<(u32, u64, Arc<[u8]>)> =
2812        Vec::with_capacity(if caching { pending.len() + 1 } else { 0 });
2813    if caching {
2814        if let Some((pid, off)) = whole_base {
2815            cache_inserts.push((pid, off, Arc::from(base_data.to_vec())));
2816        }
2817    }
2818
2819    let chain_a = &mut state.chain_a;
2820    let chain_b = &mut state.chain_b;
2821    let mut rev: Vec<&PendingDeltaFrame> = pending.iter().rev().collect();
2822    let last = rev
2823        .pop()
2824        .ok_or_else(|| Error::CorruptObject("empty delta chain".to_owned()))?;
2825    if rev.is_empty() {
2826        apply_delta_into(out, base_data, &last.delta)?;
2827        if caching && (last.pack_id != result_pack_id || last.offset != result_offset) {
2828            cache_inserts.push((last.pack_id, last.offset, Arc::from(out.as_slice())));
2829        }
2830        if caching {
2831            pack_cache::put_delta_bases(kind, &cache_inserts);
2832        }
2833        return Ok(());
2834    }
2835
2836    let Some(first) = rev.first().copied() else {
2837        return Err(Error::CorruptObject(
2838            "delta chain missing base-adjacent frame".to_owned(),
2839        ));
2840    };
2841    apply_delta_into(chain_a, base_data, &first.delta)?;
2842    if caching && (first.pack_id != result_pack_id || first.offset != result_offset) {
2843        cache_inserts.push((first.pack_id, first.offset, Arc::from(chain_a.as_slice())));
2844    }
2845    let mut src_is_a = true;
2846    for frame in rev.iter().skip(1) {
2847        if src_is_a {
2848            apply_delta_into(chain_b, chain_a.as_slice(), &frame.delta)?;
2849            if caching && (frame.pack_id != result_pack_id || frame.offset != result_offset) {
2850                cache_inserts.push((frame.pack_id, frame.offset, Arc::from(chain_b.as_slice())));
2851            }
2852        } else {
2853            apply_delta_into(chain_a, chain_b.as_slice(), &frame.delta)?;
2854            if caching && (frame.pack_id != result_pack_id || frame.offset != result_offset) {
2855                cache_inserts.push((frame.pack_id, frame.offset, Arc::from(chain_a.as_slice())));
2856            }
2857        }
2858        src_is_a = !src_is_a;
2859    }
2860    let penultimate_base = if src_is_a {
2861        chain_a.as_slice()
2862    } else {
2863        chain_b.as_slice()
2864    };
2865    apply_delta_into(out, penultimate_base, &last.delta)?;
2866    if caching && (last.pack_id != result_pack_id || last.offset != result_offset) {
2867        cache_inserts.push((last.pack_id, last.offset, Arc::from(out.as_slice())));
2868    }
2869    if caching {
2870        pack_cache::put_delta_bases(kind, &cache_inserts);
2871    }
2872    Ok(())
2873}
2874
2875#[allow(clippy::too_many_arguments)]
2876fn finish_pack_read_with_pending(
2877    out: &mut Vec<u8>,
2878    kind: ObjectKind,
2879    base: &[u8],
2880    whole_base: Option<(u32, u64)>,
2881    result_pack_id: u32,
2882    result_offset: u64,
2883    populate_delta_lru: bool,
2884    state: &mut DeltaChainState,
2885) -> Result<ObjectKind> {
2886    let pending = std::mem::take(&mut state.pending);
2887    apply_delta_chain_forward_into(
2888        out,
2889        kind,
2890        base,
2891        whole_base,
2892        &pending,
2893        result_pack_id,
2894        result_offset,
2895        populate_delta_lru,
2896        state,
2897    )?;
2898    Ok(kind)
2899}
2900
2901/// Starting index for a pack read: borrow the caller's [`PackIndex`], or hold a cached
2902/// [`Arc`] after hopping to another pack (no deep clone of `entries`).
2903enum PackIndexHandle<'a> {
2904    Borrowed(&'a PackIndex),
2905    Shared(Arc<PackIndex>),
2906}
2907
2908impl<'a> Clone for PackIndexHandle<'a> {
2909    fn clone(&self) -> Self {
2910        match self {
2911            Self::Borrowed(idx) => Self::Borrowed(idx),
2912            Self::Shared(arc) => Self::Shared(Arc::clone(arc)),
2913        }
2914    }
2915}
2916
2917impl PackIndexHandle<'_> {
2918    fn get(&self) -> &PackIndex {
2919        match self {
2920            Self::Borrowed(idx) => idx,
2921            Self::Shared(idx) => idx.as_ref(),
2922        }
2923    }
2924}
2925
2926fn find_other_pack_index(
2927    objects_dir: &Path,
2928    current: &PackIndex,
2929    oid: &ObjectId,
2930) -> Result<Option<Arc<PackIndex>>> {
2931    for idx in read_local_pack_indexes_cached(objects_dir)? {
2932        if idx.idx_path == current.idx_path {
2933            continue;
2934        }
2935        if idx.contains(oid)? {
2936            return Ok(Some(Arc::clone(&idx)));
2937        }
2938    }
2939    Ok(None)
2940}
2941
2942/// When an indexed in-pack base is unreadable, try a verified loose copy or the same OID in
2943/// another pack (cold rescue path preserved from the recursive reader).
2944fn rescue_in_pack_base(
2945    idx: &PackIndex,
2946    base_offset: u64,
2947    objects_dir: Option<&Path>,
2948    state: &mut DeltaChainState,
2949) -> Result<Option<(ObjectKind, Vec<u8>)>> {
2950    let Some(pos) = idx.find_position_by_offset_sorted(base_offset)? else {
2951        return Ok(None);
2952    };
2953    let entry_oid = idx.oid_at(pos);
2954    if entry_oid.len() != idx.hash_bytes() {
2955        return Ok(None);
2956    }
2957    let base_oid = ObjectId::from_bytes(entry_oid)?;
2958    let Some(dir) = objects_dir else {
2959        return Ok(None);
2960    };
2961    let loose = dir
2962        .join(base_oid.loose_prefix())
2963        .join(base_oid.loose_suffix());
2964    if loose.is_file() {
2965        if let Ok(obj) = crate::odb::Odb::read_loose_verify_oid(&loose, &base_oid) {
2966            return Ok(Some((obj.kind, obj.data)));
2967        }
2968    }
2969    if let Some(other_idx) = find_other_pack_index(dir, idx, &base_oid)? {
2970        let Some(off) = other_idx.find_offset(&base_oid)? else {
2971            return Ok(None);
2972        };
2973        // The nested resolution starts its own chain and clears `state.pending`; keep the
2974        // caller's collected deltas aside so they are applied on top of the rescued base.
2975        let caller_pending = std::mem::take(&mut state.pending);
2976        let rescued =
2977            resolve_pack_object_at(PackIndexHandle::Shared(other_idx), off, Some(dir), state);
2978        state.pending = caller_pending;
2979        if let Ok((kind, data)) = rescued {
2980            return Ok(Some((kind, data)));
2981        }
2982    }
2983    Ok(None)
2984}
2985
2986/// True when a pack parse failure might be stale cached bytes rather than on-disk corruption.
2987fn pack_bytes_parse_may_be_stale(err: &Error) -> bool {
2988    matches!(err, Error::CorruptObject(_) | Error::Zlib(_))
2989}
2990
2991/// Resolve one pack object, walking delta chains iteratively across packs with shared
2992/// [`DeltaChainState`] so cycles and depth limits surface as [`Error::DeltaChainTooDeep`].
2993///
2994/// On corruption/zlib errors, revalidates the starting pack's on-disk signature once and
2995/// retries the full resolution before surfacing the error.
2996fn resolve_pack_object_at(
2997    start_idx: PackIndexHandle<'_>,
2998    start_offset: u64,
2999    objects_dir: Option<&Path>,
3000    state: &mut DeltaChainState,
3001) -> Result<(ObjectKind, Vec<u8>)> {
3002    resolve_pack_object_at_with_bytes(start_idx, start_offset, objects_dir, state, None)
3003}
3004
3005fn resolve_pack_object_at_into(
3006    start_idx: PackIndexHandle<'_>,
3007    start_offset: u64,
3008    objects_dir: Option<&Path>,
3009    state: &mut DeltaChainState,
3010    populate_delta_lru: bool,
3011    out: &mut Vec<u8>,
3012) -> Result<ObjectKind> {
3013    resolve_pack_object_at_with_bytes_into(
3014        start_idx,
3015        start_offset,
3016        objects_dir,
3017        state,
3018        None,
3019        populate_delta_lru,
3020        out,
3021    )
3022}
3023
3024fn resolve_pack_object_at_with_bytes(
3025    start_idx: PackIndexHandle<'_>,
3026    start_offset: u64,
3027    objects_dir: Option<&Path>,
3028    state: &mut DeltaChainState,
3029    pack_bytes_override: Option<&[u8]>,
3030) -> Result<(ObjectKind, Vec<u8>)> {
3031    let mut out = Vec::new();
3032    let kind = resolve_pack_object_at_with_bytes_into(
3033        start_idx,
3034        start_offset,
3035        objects_dir,
3036        state,
3037        pack_bytes_override,
3038        true,
3039        &mut out,
3040    )?;
3041    Ok((kind, out))
3042}
3043
3044fn resolve_pack_object_at_with_bytes_into(
3045    start_idx: PackIndexHandle<'_>,
3046    start_offset: u64,
3047    objects_dir: Option<&Path>,
3048    state: &mut DeltaChainState,
3049    pack_bytes_override: Option<&[u8]>,
3050    populate_delta_lru: bool,
3051    out: &mut Vec<u8>,
3052) -> Result<ObjectKind> {
3053    let pack_path = start_idx.get().pack_path.clone();
3054    let start_pack_id = pack_cache::pack_id_for(&pack_path);
3055    for attempt in 0..2 {
3056        match resolve_pack_object_at_body(
3057            start_idx.clone(),
3058            start_offset,
3059            start_pack_id,
3060            objects_dir,
3061            state,
3062            pack_bytes_override,
3063            populate_delta_lru,
3064            out,
3065        ) {
3066            Ok(kind) => return Ok(kind),
3067            Err(e) if attempt == 0 && pack_bytes_parse_may_be_stale(&e) => {
3068                if pack_cache::reload_pack_bytes_after_parse_failure(&pack_path)? {
3069                    state.visited.retain(|(id, _)| *id != start_pack_id);
3070                    continue;
3071                }
3072                return Err(e);
3073            }
3074            Err(e) => return Err(e),
3075        }
3076    }
3077    unreachable!("at most two resolution attempts")
3078}
3079
3080#[allow(clippy::too_many_arguments)]
3081fn resolve_pack_object_at_body(
3082    mut cur_idx: PackIndexHandle<'_>,
3083    start_offset: u64,
3084    result_pack_id: u32,
3085    objects_dir: Option<&Path>,
3086    state: &mut DeltaChainState,
3087    pack_bytes_override: Option<&[u8]>,
3088    populate_delta_lru: bool,
3089    out: &mut Vec<u8>,
3090) -> Result<ObjectKind> {
3091    let result_offset = start_offset;
3092    let mut cur_offset = start_offset;
3093    state.pending.clear();
3094
3095    let mut cur_pack_path = cur_idx.get().pack_path.clone();
3096    let mut cur_pack_id = pack_cache::pack_id_for(&cur_pack_path);
3097    let mut validated_pack_count = false;
3098
3099    enum PackBytesSource<'a> {
3100        Borrowed(&'a [u8]),
3101        Cached(Arc<PackData>),
3102    }
3103
3104    let mut pack_bytes = match pack_bytes_override {
3105        Some(b) => PackBytesSource::Borrowed(b),
3106        None => PackBytesSource::Cached(read_pack_bytes_cached(&cur_pack_path)?),
3107    };
3108
3109    let mut compressed = Vec::new();
3110    loop {
3111        let idx_ref = cur_idx.get();
3112        if idx_ref.pack_path != cur_pack_path {
3113            cur_pack_path = idx_ref.pack_path.clone();
3114            pack_bytes = PackBytesSource::Cached(read_pack_bytes_cached(&cur_pack_path)?);
3115            cur_pack_id = pack_cache::pack_id_for(&cur_pack_path);
3116            validated_pack_count = false;
3117        }
3118        let pack_id = cur_pack_id;
3119        state.visit(pack_id, cur_offset)?;
3120
3121        if !validated_pack_count {
3122            let header: &[u8] = match &pack_bytes {
3123                PackBytesSource::Borrowed(b) => b,
3124                PackBytesSource::Cached(c) => c,
3125            };
3126            validate_pack_index_object_count(header, idx_ref)?;
3127            validated_pack_count = true;
3128        }
3129        let (bytes, mut pos) = match &pack_bytes {
3130            PackBytesSource::Borrowed(b) => (*b, cur_offset as usize),
3131            PackBytesSource::Cached(c) => {
3132                pack_entry_bytes(idx_ref, cur_offset, c, &mut compressed)?
3133            }
3134        };
3135        let object_start = cur_offset;
3136        let (packed_type, size) = match parse_pack_object_header(bytes, &mut pos) {
3137            Ok(v) => v,
3138            Err(err) => {
3139                if let Some((kind, data)) =
3140                    rescue_in_pack_base(idx_ref, cur_offset, objects_dir, state)?
3141                {
3142                    return finish_pack_read_with_pending(
3143                        out,
3144                        kind,
3145                        &data,
3146                        None,
3147                        result_pack_id,
3148                        result_offset,
3149                        populate_delta_lru,
3150                        state,
3151                    );
3152                }
3153                return Err(err);
3154            }
3155        };
3156
3157        match packed_type {
3158            PackedType::Commit | PackedType::Tree | PackedType::Blob | PackedType::Tag => {
3159                let kind = packed_type_to_kind(packed_type)?;
3160                if state.pending.is_empty() {
3161                    if let Err(err) =
3162                        decompress_pack_data_into(bytes, &mut pos, size, &mut state.inflate, out)
3163                    {
3164                        if let Some((rescue_kind, rescued)) =
3165                            rescue_in_pack_base(idx_ref, cur_offset, objects_dir, state)?
3166                        {
3167                            return finish_pack_read_with_pending(
3168                                out,
3169                                rescue_kind,
3170                                &rescued,
3171                                None,
3172                                result_pack_id,
3173                                result_offset,
3174                                populate_delta_lru,
3175                                state,
3176                            );
3177                        }
3178                        return Err(err);
3179                    }
3180                    return Ok(kind);
3181                }
3182                state.whole_object.clear();
3183                if let Err(err) = decompress_pack_data_into(
3184                    bytes,
3185                    &mut pos,
3186                    size,
3187                    &mut state.inflate,
3188                    &mut state.whole_object,
3189                ) {
3190                    if let Some((rescue_kind, rescued)) =
3191                        rescue_in_pack_base(idx_ref, cur_offset, objects_dir, state)?
3192                    {
3193                        return finish_pack_read_with_pending(
3194                            out,
3195                            rescue_kind,
3196                            &rescued,
3197                            None,
3198                            result_pack_id,
3199                            result_offset,
3200                            populate_delta_lru,
3201                            state,
3202                        );
3203                    }
3204                    return Err(err);
3205                }
3206                let whole = std::mem::take(&mut state.whole_object);
3207                return finish_pack_read_with_pending(
3208                    out,
3209                    kind,
3210                    &whole,
3211                    Some((pack_id, cur_offset)),
3212                    result_pack_id,
3213                    result_offset,
3214                    populate_delta_lru,
3215                    state,
3216                );
3217            }
3218            PackedType::OfsDelta => {
3219                state.next_hop()?;
3220                let base_offset = parse_ofs_delta_base(bytes, &mut pos, object_start)?;
3221                state.delta_decompress.clear();
3222                decompress_pack_data_into(
3223                    bytes,
3224                    &mut pos,
3225                    size,
3226                    &mut state.inflate,
3227                    &mut state.delta_decompress,
3228                )?;
3229                state.pending.push(PendingDeltaFrame {
3230                    pack_id,
3231                    offset: object_start,
3232                    delta: std::mem::take(&mut state.delta_decompress),
3233                });
3234                if let Some((base_kind, base_data)) =
3235                    pack_cache::get_delta_base(pack_id, base_offset)
3236                {
3237                    return finish_pack_read_with_pending(
3238                        out,
3239                        base_kind,
3240                        base_data.as_ref(),
3241                        None,
3242                        result_pack_id,
3243                        result_offset,
3244                        populate_delta_lru,
3245                        state,
3246                    );
3247                }
3248                cur_offset = base_offset;
3249            }
3250            PackedType::RefDelta => {
3251                state.next_hop()?;
3252                let hb = idx_ref.hash_bytes();
3253                if pos + hb > bytes.len() {
3254                    return Err(Error::CorruptObject(
3255                        "truncated ref-delta base OID".to_owned(),
3256                    ));
3257                }
3258                let base_raw = bytes[pos..pos + hb].to_vec();
3259                pos += hb;
3260                state.delta_decompress.clear();
3261                decompress_pack_data_into(
3262                    bytes,
3263                    &mut pos,
3264                    size,
3265                    &mut state.inflate,
3266                    &mut state.delta_decompress,
3267                )?;
3268                state.pending.push(PendingDeltaFrame {
3269                    pack_id,
3270                    offset: object_start,
3271                    delta: std::mem::take(&mut state.delta_decompress),
3272                });
3273
3274                if let Ok(base_oid) = ObjectId::from_bytes(base_raw.as_slice()) {
3275                    if let Some(base_offset) = idx_ref.find_offset(&base_oid)? {
3276                        if let Some((base_kind, base_data)) =
3277                            pack_cache::get_delta_base(pack_id, base_offset)
3278                        {
3279                            return finish_pack_read_with_pending(
3280                                out,
3281                                base_kind,
3282                                base_data.as_ref(),
3283                                None,
3284                                result_pack_id,
3285                                result_offset,
3286                                populate_delta_lru,
3287                                state,
3288                            );
3289                        }
3290                        cur_offset = base_offset;
3291                        continue;
3292                    }
3293                }
3294
3295                if hb == 20 {
3296                    if let (Some(dir), Ok(base_oid)) =
3297                        (objects_dir, ObjectId::from_bytes(base_raw.as_slice()))
3298                    {
3299                        let loose = dir
3300                            .join(base_oid.loose_prefix())
3301                            .join(base_oid.loose_suffix());
3302                        if loose.is_file() {
3303                            if let Ok(obj) =
3304                                crate::odb::Odb::read_loose_verify_oid(&loose, &base_oid)
3305                            {
3306                                return finish_pack_read_with_pending(
3307                                    out,
3308                                    obj.kind,
3309                                    &obj.data,
3310                                    None,
3311                                    result_pack_id,
3312                                    result_offset,
3313                                    populate_delta_lru,
3314                                    state,
3315                                );
3316                            }
3317                        }
3318                        if let Some(other_idx) = find_other_pack_index(dir, idx_ref, &base_oid)? {
3319                            let Some(off) = other_idx.find_offset(&base_oid)? else {
3320                                return Err(Error::ObjectNotFound(base_oid.to_hex()));
3321                            };
3322                            cur_idx = PackIndexHandle::Shared(other_idx);
3323                            cur_offset = off;
3324                            continue;
3325                        }
3326                    }
3327                }
3328                return Err(Error::CorruptObject(format!(
3329                    "ref-delta base {} not found in pack",
3330                    oid_bytes_to_hex(&base_raw)
3331                )));
3332            }
3333        }
3334    }
3335}
3336
3337/// Resolve kind and uncompressed size for one pack object without materializing its body.
3338fn resolve_pack_object_info_at(
3339    start_idx: PackIndexHandle<'_>,
3340    start_offset: u64,
3341    objects_dir: Option<&Path>,
3342    state: &mut DeltaChainState,
3343) -> Result<ObjectInfo> {
3344    resolve_pack_object_info_at_with_bytes(start_idx, start_offset, objects_dir, state, None)
3345}
3346
3347fn resolve_pack_object_info_at_body(
3348    cur_idx: PackIndexHandle<'_>,
3349    start_offset: u64,
3350    objects_dir: Option<&Path>,
3351    state: &mut DeltaChainState,
3352    pack_bytes_override: Option<&[u8]>,
3353) -> Result<ObjectInfo> {
3354    let cached_pack;
3355    let cur_bytes: &[u8] = if let Some(b) = pack_bytes_override {
3356        b
3357    } else {
3358        cached_pack = read_pack_bytes_cached(&cur_idx.get().pack_path)?;
3359        cached_pack.as_ref()
3360    };
3361    let mut cur_offset = start_offset;
3362    let mut result_size: Option<u64> = None;
3363
3364    loop {
3365        let idx_ref = cur_idx.get();
3366        let pack_id = pack_cache::pack_id_for(&idx_ref.pack_path);
3367        state.visit(pack_id, cur_offset)?;
3368
3369        if let Some(base) = cached_pack_read_info(&idx_ref.pack_path, cur_offset) {
3370            if let Some(size) = result_size {
3371                return Ok(ObjectInfo {
3372                    kind: base.kind,
3373                    size,
3374                });
3375            }
3376            return Ok(base);
3377        }
3378        let object_start = cur_offset;
3379        let mut pos = cur_offset as usize;
3380        let (packed_type, header_size) = parse_pack_object_header(cur_bytes, &mut pos)?;
3381
3382        match packed_type {
3383            PackedType::Commit | PackedType::Tree | PackedType::Blob | PackedType::Tag => {
3384                let kind = packed_type_to_kind(packed_type)?;
3385                remember_pack_read_info(
3386                    &idx_ref.pack_path,
3387                    cur_offset,
3388                    ObjectInfo {
3389                        kind,
3390                        size: header_size,
3391                    },
3392                );
3393                let size = result_size.unwrap_or(header_size);
3394                return Ok(ObjectInfo { kind, size });
3395            }
3396            PackedType::OfsDelta => {
3397                state.next_hop()?;
3398                let base_offset = parse_ofs_delta_base(cur_bytes, &mut pos, object_start)?;
3399                if result_size.is_none() {
3400                    result_size = Some(read_delta_result_size_from_pack_zlib(cur_bytes, &mut pos)?);
3401                }
3402                cur_offset = base_offset;
3403            }
3404            PackedType::RefDelta => {
3405                state.next_hop()?;
3406                let hb = idx_ref.hash_bytes();
3407                if pos + hb > cur_bytes.len() {
3408                    return Err(Error::CorruptObject(
3409                        "truncated ref-delta base OID".to_owned(),
3410                    ));
3411                }
3412                let base_raw = &cur_bytes[pos..pos + hb];
3413                pos += hb;
3414                if result_size.is_none() {
3415                    result_size = Some(read_delta_result_size_from_pack_zlib(cur_bytes, &mut pos)?);
3416                }
3417
3418                let base_oid = ObjectId::from_bytes(base_raw)?;
3419                if let Some(base_offset) = idx_ref.find_offset(&base_oid)? {
3420                    cur_offset = base_offset;
3421                    continue;
3422                }
3423
3424                if let Some(dir) = objects_dir {
3425                    let loose = dir
3426                        .join(base_oid.loose_prefix())
3427                        .join(base_oid.loose_suffix());
3428                    if loose.is_file() {
3429                        let info = crate::odb::read_loose_object_info(&loose)?;
3430                        let size = result_size.unwrap_or(info.size);
3431                        return Ok(ObjectInfo {
3432                            kind: info.kind,
3433                            size,
3434                        });
3435                    }
3436                    if let Some(other_idx) = find_other_pack_index(dir, idx_ref, &base_oid)? {
3437                        let Some(off) = other_idx.find_offset(&base_oid)? else {
3438                            return Err(Error::ObjectNotFound(base_oid.to_hex()));
3439                        };
3440                        let base_info = resolve_pack_object_info_at(
3441                            PackIndexHandle::Shared(other_idx),
3442                            off,
3443                            objects_dir,
3444                            state,
3445                        )?;
3446                        let size = result_size.unwrap_or(base_info.size);
3447                        return Ok(ObjectInfo {
3448                            kind: base_info.kind,
3449                            size,
3450                        });
3451                    }
3452                }
3453                return Err(Error::CorruptObject(format!(
3454                    "ref-delta base {} not found in pack",
3455                    oid_bytes_to_hex(base_raw)
3456                )));
3457            }
3458        }
3459    }
3460}
3461
3462fn read_pack_object_at(
3463    pack_bytes: &[u8],
3464    start_offset: u64,
3465    idx: &PackIndex,
3466    objects_dir: Option<&Path>,
3467    start_depth: usize,
3468) -> Result<(ObjectKind, Vec<u8>)> {
3469    let mut state = DeltaChainState {
3470        depth: start_depth,
3471        visited: HashSet::new(),
3472        ..Default::default()
3473    };
3474    resolve_pack_object_at_with_bytes(
3475        PackIndexHandle::Borrowed(idx),
3476        start_offset,
3477        objects_dir,
3478        &mut state,
3479        Some(pack_bytes),
3480    )
3481}
3482
3483/// Read an object from a pack file by its OID.
3484///
3485/// Searches the given pack index for the OID, then reads and decompresses
3486/// the object from the corresponding pack file, resolving delta chains.
3487///
3488/// # Errors
3489///
3490/// Returns [`Error::ObjectNotFound`] if the OID is not in this pack.
3491pub fn read_object_from_pack(idx: &PackIndex, oid: &ObjectId) -> Result<Object> {
3492    if crate::pack_store::PackStore::has_context() {
3493        return read_object_from_pack_at_depth(idx, oid, 0);
3494    }
3495    let store = crate::pack_store::PackStore::legacy_for_pack_path(&idx.pack_path);
3496    crate::pack_store::PackStore::with_context(store, || {
3497        read_object_from_pack_at_depth(idx, oid, 0)
3498    })
3499}
3500
3501/// Like [`read_object_from_pack`], inflating into `buf` and reusing its capacity.
3502///
3503/// # Errors
3504///
3505/// Same as [`read_object_from_pack`].
3506pub fn read_object_from_pack_into(
3507    idx: &PackIndex,
3508    oid: &ObjectId,
3509    buf: &mut Vec<u8>,
3510) -> Result<ObjectKind> {
3511    let Some(offset) = idx.find_offset(oid)? else {
3512        return Err(Error::ObjectNotFound(oid.to_hex()));
3513    };
3514    if crate::pack_store::PackStore::has_context() {
3515        return read_object_at_depth_into(idx, offset, 0, false, buf);
3516    }
3517    let store = crate::pack_store::PackStore::legacy_for_pack_path(&idx.pack_path);
3518    crate::pack_store::PackStore::with_context(store, || {
3519        read_object_at_depth_into(idx, offset, 0, false, buf)
3520    })
3521}
3522
3523/// Read [`ObjectInfo`] for `oid` stored in `idx`'s pack without inflating the full object body.
3524///
3525/// # Errors
3526///
3527/// Same as [`read_object_from_pack`], except no hash verification is performed on the payload.
3528pub fn read_object_info_from_pack(idx: &PackIndex, oid: &ObjectId) -> Result<ObjectInfo> {
3529    let Some(offset) = idx.find_offset(oid)? else {
3530        return Err(Error::ObjectNotFound(oid.to_hex()));
3531    };
3532    read_object_info_from_pack_at_offset(idx, offset)
3533}
3534
3535/// Read [`ObjectInfo`] at a known pack offset (avoids a second index lookup in batch workloads).
3536///
3537/// # Errors
3538///
3539/// Same as [`read_object_info_from_pack`].
3540pub fn read_object_info_from_pack_at_offset(idx: &PackIndex, offset: u64) -> Result<ObjectInfo> {
3541    let pack_path = idx.pack_path.clone();
3542    for attempt in 0..2 {
3543        let pack_bytes = read_pack_bytes_cached(&pack_path)?;
3544        if !pack_cache::pack_object_count_validated(&pack_path) {
3545            match validate_pack_index_object_count(&pack_bytes, idx) {
3546                Ok(()) => pack_cache::mark_pack_object_count_validated(&pack_path),
3547                Err(e) if attempt == 0 && pack_bytes_parse_may_be_stale(&e) => {
3548                    if pack_cache::reload_pack_bytes_after_parse_failure(&pack_path)? {
3549                        continue;
3550                    }
3551                    return Err(e);
3552                }
3553                Err(e) => return Err(e),
3554            }
3555        }
3556        // Object-info probes often visit many unrelated historical commits while peeling
3557        // loose tags. A few header bytes must not retain a fault-around window per tip.
3558        let mut header = [0u8; 16];
3559        if let Some(len) = pack_bytes.read_sparse(offset, &mut header) {
3560            if let Some(info) = read_whole_pack_entry_info_fast(idx, 0, &header[..len]) {
3561                if info.kind == ObjectKind::Tag {
3562                    remember_pack_read_info(&pack_path, offset, info);
3563                }
3564                return Ok(info);
3565            }
3566        }
3567        match read_object_info_from_pack_at_offset_with_bytes(idx, offset, pack_bytes.as_ref()) {
3568            Ok(info) => return Ok(info),
3569            Err(e) if attempt == 0 && pack_bytes_parse_may_be_stale(&e) => {
3570                if pack_cache::reload_pack_bytes_after_parse_failure(&pack_path)? {
3571                    continue;
3572                }
3573                return Err(e);
3574            }
3575            Err(e) => return Err(e),
3576        }
3577    }
3578    unreachable!("at most two read attempts")
3579}
3580
3581/// Compressed bytes [`read_packed_commit_header_block_into`] fetches with one positioned read.
3582const COMMIT_HEADER_BLOCK_WINDOW: usize = 4096;
3583
3584/// Most commit bytes [`read_packed_commit_header_block_into`] inflates looking for the blank
3585/// line that ends the headers. Covers ordinary headers; long signatures take the whole read.
3586const COMMIT_HEADER_BLOCK_MAX: usize = 1024;
3587
3588/// Inflate the header block of commit `oid`, stored whole in a local pack, into `out`.
3589///
3590/// On success `out` holds the headers through the blank line that ends them, so it parses as
3591/// the same commit with an empty message. The entry is fetched with one positioned read rather
3592/// than through the pack mapping: walks seeded from every ref read hundreds of scattered tip
3593/// commits, and each mapped read would leave a fault-around window of the pack resident.
3594///
3595/// Returns `false`, leaving `out` unspecified, when no local pack index lists `oid`, the entry
3596/// is not a whole commit, the header block exceeds [`COMMIT_HEADER_BLOCK_MAX`], or the
3597/// positioned read or the inflate fails. Callers then read the whole object, which reports
3598/// any corruption.
3599///
3600/// # Errors
3601///
3602/// Propagates pack directory listing and pack index lookup errors.
3603pub(crate) fn read_packed_commit_header_block_into(
3604    objects_dir: &Path,
3605    oid: &ObjectId,
3606    out: &mut Vec<u8>,
3607) -> Result<bool> {
3608    if crate::pack_store::PackStore::read_context_matches_objects_dir(objects_dir) {
3609        pack_cache::sync_thread_local_with_store(&crate::pack_store::PackStore::active_for_reads());
3610    }
3611    if let Some(idx) = pack_cache::hinted_pack_index(objects_dir) {
3612        if let Some(offset) = idx.find_offset(oid)? {
3613            return Ok(read_commit_header_block_at(&idx, offset, out));
3614        }
3615    }
3616    for idx in pack_indexes_for_lookup(objects_dir, PackLookupOptions::ALL_LOCAL_PACKS)? {
3617        if let Some(offset) = idx.find_offset(oid)? {
3618            pack_cache::remember_pack_hit(objects_dir, &idx);
3619            return Ok(read_commit_header_block_at(&idx, offset, out));
3620        }
3621    }
3622    Ok(false)
3623}
3624
3625fn read_commit_header_block_at(idx: &PackIndex, offset: u64, out: &mut Vec<u8>) -> bool {
3626    let Ok(pack_bytes) = read_pack_bytes_cached(&idx.pack_path) else {
3627        return false;
3628    };
3629    if !pack_cache::pack_object_count_validated(&idx.pack_path) {
3630        if validate_pack_index_object_count(&pack_bytes, idx).is_err() {
3631            return false;
3632        }
3633        pack_cache::mark_pack_object_count_validated(&idx.pack_path);
3634    }
3635    let mut window = [0u8; COMMIT_HEADER_BLOCK_WINDOW];
3636    let Some(len) = pack_bytes.read_sparse(offset, &mut window) else {
3637        return false;
3638    };
3639    let window = &window[..len];
3640    let mut pos = 0usize;
3641    let Ok((PackedType::Commit, size)) = parse_pack_object_header(window, &mut pos) else {
3642        return false;
3643    };
3644    let want = usize::try_from(size).map_or(COMMIT_HEADER_BLOCK_MAX, |size| {
3645        size.min(COMMIT_HEADER_BLOCK_MAX)
3646    });
3647    let inflated = WHOLE_OBJECT_ZLIB.with(|slot| {
3648        slot.borrow_mut()
3649            .decompress_prefix_into(&window[pos..], want, out)
3650    });
3651    if inflated.is_err() || out.len() != want {
3652        return false;
3653    }
3654    let Some(blank) = out.windows(2).position(|pair| pair == b"\n\n") else {
3655        return false;
3656    };
3657    out.truncate(blank + 2);
3658    true
3659}
3660
3661fn read_object_info_from_pack_at_offset_with_bytes(
3662    idx: &PackIndex,
3663    offset: u64,
3664    pack_bytes: &[u8],
3665) -> Result<ObjectInfo> {
3666    if let Some(info) = read_whole_pack_entry_info_fast(idx, offset, pack_bytes) {
3667        return Ok(info);
3668    }
3669    let objects_dir = idx.pack_path.parent().and_then(Path::parent);
3670    with_pack_read_delta_state(0, |state| {
3671        resolve_pack_object_info_at_with_bytes(
3672            PackIndexHandle::Borrowed(idx),
3673            offset,
3674            objects_dir,
3675            state,
3676            Some(pack_bytes),
3677        )
3678    })
3679}
3680
3681fn resolve_pack_object_info_at_with_bytes(
3682    start_idx: PackIndexHandle<'_>,
3683    start_offset: u64,
3684    objects_dir: Option<&Path>,
3685    state: &mut DeltaChainState,
3686    pack_bytes_override: Option<&[u8]>,
3687) -> Result<ObjectInfo> {
3688    let idx_ref = start_idx.get();
3689    let pack_path = idx_ref.pack_path.clone();
3690    let start_pack_id = pack_cache::pack_id_for(&pack_path);
3691    if let Some(info) = cached_pack_read_info(&pack_path, start_offset) {
3692        return Ok(info);
3693    }
3694    for attempt in 0..2 {
3695        match resolve_pack_object_info_at_body(
3696            start_idx.clone(),
3697            start_offset,
3698            objects_dir,
3699            state,
3700            pack_bytes_override,
3701        ) {
3702            Ok(v) => {
3703                remember_pack_read_info(&pack_path, start_offset, v);
3704                return Ok(v);
3705            }
3706            Err(e) if attempt == 0 && pack_bytes_parse_may_be_stale(&e) => {
3707                if pack_cache::reload_pack_bytes_after_parse_failure(&pack_path)? {
3708                    state.visited.retain(|(id, _)| *id != start_pack_id);
3709                    continue;
3710                }
3711                return Err(e);
3712            }
3713            Err(e) => return Err(e),
3714        }
3715    }
3716    unreachable!("at most two resolution attempts")
3717}
3718
3719/// Header-only info for undeltified pack entries (batch-check hot path).
3720#[inline]
3721fn read_whole_pack_entry_info_fast(
3722    _idx: &PackIndex,
3723    offset: u64,
3724    pack_bytes: &[u8],
3725) -> Option<ObjectInfo> {
3726    let mut pos = offset as usize;
3727    if pos >= pack_bytes.len() {
3728        return None;
3729    }
3730    let (packed_type, header_size) = parse_pack_object_header(pack_bytes, &mut pos).ok()?;
3731    match packed_type {
3732        PackedType::Commit | PackedType::Tree | PackedType::Blob | PackedType::Tag => {
3733            let kind = packed_type_to_kind(packed_type).ok()?;
3734            Some(ObjectInfo {
3735                kind,
3736                size: header_size,
3737            })
3738        }
3739        PackedType::OfsDelta | PackedType::RefDelta => None,
3740    }
3741}
3742
3743// hygiene: per-thread zlib scratch for whole pack entry reads (ROADMAP item 7 — pack read hot path)
3744thread_local! {
3745    static WHOLE_OBJECT_ZLIB: RefCell<ZlibInflateScratch> =
3746        RefCell::new(ZlibInflateScratch::default());
3747}
3748
3749/// Max uncompressed bytes to inflate when only commit headers (e.g. author) are needed.
3750pub(crate) const COMMIT_HEADER_READ_PREFIX: usize = 512;
3751
3752/// Select one entry for random reads; ordinary readers keep their zero-copy fast path.
3753fn pack_entry_bytes<'a>(
3754    idx: &PackIndex,
3755    offset: u64,
3756    pack: &'a PackData,
3757    buf: &'a mut Vec<u8>,
3758) -> Result<(&'a [u8], usize)> {
3759    let start = usize::try_from(offset)
3760        .map_err(|_| Error::CorruptObject("pack offset out of range".to_owned()))?;
3761    if !pack.uses_positioned_reads() {
3762        return Ok((pack, start));
3763    }
3764    let pack_end = pack
3765        .len()
3766        .checked_sub(idx.hash_bytes())
3767        .ok_or_else(|| Error::CorruptObject("truncated pack trailer".to_owned()))?;
3768    let end = idx.next_pack_offset_after(offset, pack_end as u64)? as usize;
3769    Ok((pack.read_range(start, end, buf)?, 0))
3770}
3771
3772fn read_whole_pack_entry_object_fast(
3773    idx: &PackIndex,
3774    offset: u64,
3775    pack_bytes: &PackData,
3776) -> Option<Object> {
3777    let mut data = Vec::new();
3778    read_whole_pack_entry_object_into(idx, offset, pack_bytes, &mut data)
3779        .map(|kind| Object::new(kind, data))
3780}
3781
3782fn read_whole_pack_entry_object_into(
3783    idx: &PackIndex,
3784    offset: u64,
3785    pack_bytes: &PackData,
3786    out: &mut Vec<u8>,
3787) -> Option<ObjectKind> {
3788    let pack_id = pack_cache::pack_id_for(&idx.pack_path);
3789    if let Some((kind, data)) = pack_cache::get_delta_base(pack_id, offset) {
3790        out.clear();
3791        out.extend_from_slice(data.as_ref());
3792        return Some(kind);
3793    }
3794    // Single-pack readers and general reads share this path: a preceding tag-info
3795    // probe lets either read a small whole tag without faulting mmap windows.
3796    if cached_pack_read_info(&idx.pack_path, offset)
3797        .is_some_and(|info| info.kind == ObjectKind::Tag)
3798    {
3799        let mut bytes = [0u8; 4096];
3800        if let Some(len) = pack_bytes.read_sparse(offset, &mut bytes) {
3801            if let Some(kind) = inflate_whole_pack_entry_into(&bytes[..len], 0, out) {
3802                return Some(kind);
3803            }
3804        }
3805    }
3806    let mut compressed = Vec::new();
3807    let (pack_bytes, pos) = pack_entry_bytes(idx, offset, pack_bytes, &mut compressed).ok()?;
3808    inflate_whole_pack_entry_into(pack_bytes, pos as u64, out)
3809}
3810
3811fn inflate_whole_pack_entry_into(
3812    pack_bytes: &[u8],
3813    offset: u64,
3814    out: &mut Vec<u8>,
3815) -> Option<ObjectKind> {
3816    let mut pos = offset as usize;
3817    if pos >= pack_bytes.len() {
3818        return None;
3819    }
3820    let (packed_type, size) = parse_pack_object_header(pack_bytes, &mut pos).ok()?;
3821    match packed_type {
3822        PackedType::Commit | PackedType::Tree | PackedType::Blob | PackedType::Tag => {
3823            let kind = packed_type_to_kind(packed_type).ok()?;
3824            WHOLE_OBJECT_ZLIB.with(|slot| {
3825                let mut inflate = slot.borrow_mut();
3826                decompress_pack_data_into(pack_bytes, &mut pos, size, &mut inflate, out)
3827                    .ok()
3828                    .map(|()| kind)
3829            })
3830        }
3831        PackedType::OfsDelta | PackedType::RefDelta => None,
3832    }
3833}
3834
3835fn read_object_from_packs_in_context_into(
3836    objects_dir: &Path,
3837    oid: &ObjectId,
3838    buf: &mut Vec<u8>,
3839) -> Result<ObjectKind> {
3840    match try_read_object_from_packs_with_options_into(
3841        objects_dir,
3842        oid,
3843        PackLookupOptions::ALL_LOCAL_PACKS,
3844        false,
3845        buf,
3846    ) {
3847        Ok(kind) => Ok(kind),
3848        Err(err @ Error::ObjectNotFound(_)) => {
3849            if reprepare_pack_directory_on_miss(objects_dir)? {
3850                try_read_object_from_packs_with_options_into(
3851                    objects_dir,
3852                    oid,
3853                    PackLookupOptions::ALL_LOCAL_PACKS,
3854                    false,
3855                    buf,
3856                )
3857            } else {
3858                Err(err)
3859            }
3860        }
3861        Err(err) => Err(err),
3862    }
3863}
3864
3865fn read_object_from_packs_in_context(objects_dir: &Path, oid: &ObjectId) -> Result<Object> {
3866    match try_read_object_from_packs_with_options(
3867        objects_dir,
3868        oid,
3869        PackLookupOptions::ALL_LOCAL_PACKS,
3870    ) {
3871        Ok(obj) => Ok(obj),
3872        Err(err @ Error::ObjectNotFound(_)) => {
3873            if reprepare_pack_directory_on_miss_in_context(objects_dir)? {
3874                try_read_object_from_packs_with_options(
3875                    objects_dir,
3876                    oid,
3877                    PackLookupOptions::ALL_LOCAL_PACKS,
3878                )
3879            } else {
3880                Err(err)
3881            }
3882        }
3883        Err(err) => Err(err),
3884    }
3885}
3886
3887/// Read and decompress the object stored at `offset` in `idx`'s pack file.
3888///
3889/// The pack index is used for pack path, object count validation, and resolving ref-delta
3890/// bases; the starting location comes from the caller (e.g. a multi-pack-index entry).
3891pub(crate) fn read_object_at(idx: &PackIndex, offset: u64) -> Result<Object> {
3892    read_object_at_depth(idx, offset, 0)
3893}
3894
3895/// Read a packed object when the pack index entry is already known (batch cat-file workloads).
3896///
3897/// Avoids a second index lookup when iterating objects in offset order.
3898pub fn read_object_from_pack_at_offset(idx: &PackIndex, offset: u64) -> Result<Object> {
3899    read_object_at(idx, offset)
3900}
3901
3902/// [`read_object_from_pack`] with an explicit starting delta-chain depth, used when the read
3903/// itself resolves a delta base from another pack (the chain budget must carry across packs).
3904fn read_object_from_pack_at_depth(idx: &PackIndex, oid: &ObjectId, depth: usize) -> Result<Object> {
3905    let Some(offset) = idx.find_offset(oid)? else {
3906        return Err(Error::ObjectNotFound(oid.to_hex()));
3907    };
3908    read_object_at_depth(idx, offset, depth)
3909}
3910
3911fn read_object_at_depth(idx: &PackIndex, offset: u64, depth: usize) -> Result<Object> {
3912    let mut data = Vec::new();
3913    let kind = read_object_at_depth_into(idx, offset, depth, true, &mut data)?;
3914    Ok(Object::new(kind, data))
3915}
3916
3917fn read_object_at_depth_into(
3918    idx: &PackIndex,
3919    offset: u64,
3920    depth: usize,
3921    populate_delta_lru: bool,
3922    out: &mut Vec<u8>,
3923) -> Result<ObjectKind> {
3924    if depth == 0 {
3925        let pack_id = pack_cache::pack_id_for(&idx.pack_path);
3926        if let Some((kind, data)) = pack_cache::get_delta_base(pack_id, offset) {
3927            out.clear();
3928            out.extend_from_slice(data.as_ref());
3929            return Ok(kind);
3930        }
3931    }
3932    let pack_path = &idx.pack_path;
3933    let objects_dir = idx.pack_path.parent().and_then(Path::parent);
3934    for attempt in 0..2 {
3935        let pack_bytes = read_pack_bytes_cached(pack_path)?;
3936        if !pack_cache::pack_object_count_validated(pack_path) {
3937            match validate_pack_index_object_count(&pack_bytes, idx) {
3938                Ok(()) => pack_cache::mark_pack_object_count_validated(pack_path),
3939                Err(e) if attempt == 0 && pack_bytes_parse_may_be_stale(&e) => {
3940                    if pack_cache::reload_pack_bytes_after_parse_failure(pack_path)? {
3941                        continue;
3942                    }
3943                    return Err(e);
3944                }
3945                Err(e) => return Err(e),
3946            }
3947        }
3948        if depth == 0 && !pack_bytes.uses_positioned_reads() {
3949            if let Some(kind) = read_whole_pack_entry_object_into(idx, offset, &pack_bytes, out) {
3950                return Ok(kind);
3951            }
3952        }
3953        let resolved = with_pack_read_delta_state(depth, |state| {
3954            resolve_pack_object_at_into(
3955                PackIndexHandle::Borrowed(idx),
3956                offset,
3957                objects_dir,
3958                state,
3959                populate_delta_lru,
3960                out,
3961            )
3962        });
3963        match resolved {
3964            Ok(kind) => return Ok(kind),
3965            Err(e) if attempt == 0 && pack_bytes_parse_may_be_stale(&e) => {
3966                if pack_cache::reload_pack_bytes_after_parse_failure(pack_path)? {
3967                    continue;
3968                }
3969                return Err(e);
3970            }
3971            Err(e) => return Err(e),
3972        }
3973    }
3974    unreachable!("at most two read attempts")
3975}
3976
3977/// Resolve an object from already-loaded pack bytes (used by `verify-pack`).
3978pub fn read_object_from_pack_bytes(
3979    pack_bytes: &[u8],
3980    idx: &PackIndex,
3981    oid: &[u8],
3982) -> Result<Object> {
3983    validate_pack_index_object_count(pack_bytes, idx)?;
3984    let id = ObjectId::from_bytes(oid).map_err(|_| Error::ObjectNotFound(oid_bytes_to_hex(oid)))?;
3985    let entry_offset = idx
3986        .find_offset(&id)?
3987        .ok_or_else(|| Error::ObjectNotFound(oid_bytes_to_hex(oid)))?;
3988    let store = crate::pack_store::PackStore::legacy_for_pack_path(&idx.pack_path);
3989    crate::pack_store::PackStore::with_context(store, || {
3990        let (kind, data) = read_pack_object_at(pack_bytes, entry_offset, idx, None, 0)?;
3991        verify_packed_object_hash(kind, &data, oid)?;
3992        Ok(Object::new(kind, data))
3993    })
3994}
3995
3996fn validate_pack_index_object_count(pack_bytes: &[u8], idx: &PackIndex) -> Result<()> {
3997    if pack_bytes.len() < 12 || &pack_bytes[0..4] != b"PACK" {
3998        return Err(Error::CorruptObject("bad pack header".to_owned()));
3999    }
4000    let count =
4001        u32::from_be_bytes([pack_bytes[8], pack_bytes[9], pack_bytes[10], pack_bytes[11]]) as usize;
4002    if count != idx.len() {
4003        return Err(Error::CorruptObject(format!(
4004            "pack object count mismatch: pack has {count}, index has {}",
4005            idx.len()
4006        )));
4007    }
4008    Ok(())
4009}
4010
4011fn verify_packed_object_hash(kind: ObjectKind, data: &[u8], expected_oid: &[u8]) -> Result<()> {
4012    let algo = HashAlgo::from_len(expected_oid.len()).ok_or_else(|| {
4013        Error::CorruptObject(format!(
4014            "unsupported packed object id width: {}",
4015            expected_oid.len()
4016        ))
4017    })?;
4018    let actual = hash_object(algo, kind, data)?;
4019    if actual.as_bytes() != expected_oid {
4020        return Err(Error::CorruptObject(format!(
4021            "packed object {} hashes to {}",
4022            oid_bytes_to_hex(expected_oid),
4023            actual.to_hex()
4024        )));
4025    }
4026    Ok(())
4027}
4028
4029/// Options controlling which local packs participate in a lookup pass.
4030#[derive(Clone, Copy, Debug, Default)]
4031pub struct PackLookupOptions {
4032    /// When true, skip pack indexes named in the active multi-pack-index (MIDX lookup runs separately).
4033    pub skip_midx_covered_packs: bool,
4034    /// When true, search only pack indexes named in the active MIDX (rescue pass after a MIDX miss).
4035    pub only_midx_covered_packs: bool,
4036}
4037
4038impl PackLookupOptions {
4039    /// Search every local pack (including MIDX-covered indexes).
4040    pub const ALL_LOCAL_PACKS: Self = Self {
4041        skip_midx_covered_packs: false,
4042        only_midx_covered_packs: false,
4043    };
4044
4045    /// Search only MIDX-listed pack indexes (Git redundant-pack rescue when MIDX lookup missed).
4046    pub const MIDX_COVERED_PACKS: Self = Self {
4047        skip_midx_covered_packs: false,
4048        only_midx_covered_packs: true,
4049    };
4050
4051    /// Whether the single-pack and last-hit-pack shortcuts may run before the full scan.
4052    ///
4053    /// Those shortcuts probe a pack without consulting the MIDX filter, so a filtered pass
4054    /// would re-probe packs the other pass owns (every loose-object read pays both passes).
4055    fn allows_pack_shortcuts(self) -> bool {
4056        !self.skip_midx_covered_packs && !self.only_midx_covered_packs
4057    }
4058}
4059
4060/// Pick one pack index for monotonic commit reads (single pack, or commit pack + blob promisor).
4061fn select_single_local_pack_index(
4062    indexes: &[Arc<PackIndex>],
4063    commit_hint: Option<&crate::objects::ObjectId>,
4064) -> Result<Option<Arc<PackIndex>>> {
4065    if indexes.is_empty() {
4066        return Ok(None);
4067    }
4068    if indexes.len() == 1 {
4069        return Ok(Some(Arc::clone(&indexes[0])));
4070    }
4071    if let Some(oid) = commit_hint {
4072        let mut found: Option<Arc<PackIndex>> = None;
4073        for idx in indexes {
4074            if idx.find_offset(oid)?.is_some() {
4075                if found.is_some() {
4076                    return Ok(None);
4077                }
4078                found = Some(Arc::clone(idx));
4079            }
4080        }
4081        if found.is_some() {
4082            return Ok(found);
4083        }
4084    }
4085    Ok(None)
4086}
4087
4088/// Sorted `cat-file --batch` reader for repos with exactly one local pack (hot-path benchmarks).
4089///
4090/// Keeps a pack index cursor so probes in non-decreasing OID order avoid repeated binary search.
4091pub struct SingleLocalPackReader {
4092    idx: Arc<PackIndex>,
4093    pack_bytes: Arc<PackData>,
4094    pack_id: u32,
4095    cursor: usize,
4096    info_cache: HashMap<u64, ObjectInfo>,
4097    /// Created on first header/size probe; full object reads never touch it, so opening
4098    /// a reader per [`crate::odb::Odb::read`] must not pay for an inflater.
4099    delta_scratch: Option<ZlibInflateScratch>,
4100    commit_header_buf: Vec<u8>,
4101}
4102
4103fn objects_dir_has_loose_objects(objects_dir: &Path) -> bool {
4104    let Ok(entries) = std::fs::read_dir(objects_dir) else {
4105        return false;
4106    };
4107    for entry in entries.flatten() {
4108        let name = entry.file_name();
4109        let name = name.to_string_lossy();
4110        if name.len() != 2 || !name.bytes().all(|b| b.is_ascii_hexdigit()) {
4111            continue;
4112        }
4113        let Ok(hex_entries) = std::fs::read_dir(entry.path()) else {
4114            continue;
4115        };
4116        if hex_entries.flatten().any(|e| e.path().is_file()) {
4117            return true;
4118        }
4119    }
4120    false
4121}
4122
4123impl SingleLocalPackReader {
4124    /// Open when [`crate::pack_store::PackStore`] read context matches and one local pack index exists.
4125    ///
4126    /// # Errors
4127    ///
4128    /// Propagates pack directory read failures from index loading.
4129    pub fn open(objects_dir: &Path) -> Result<Option<Self>> {
4130        Self::open_with_options(objects_dir, false, None)
4131    }
4132
4133    /// Open when one local pack exists, optionally ignoring loose objects (commit parent walks).
4134    pub fn open_allow_loose(objects_dir: &Path) -> Result<Option<Self>> {
4135        Self::open_with_options(objects_dir, true, None)
4136    }
4137
4138    /// Like [`Self::open_allow_loose`], but when several packs exist (e.g. promisor blob
4139    /// packs from partial clone), select the pack that contains `commit_hint`.
4140    pub(crate) fn open_allow_loose_for_commits(
4141        objects_dir: &Path,
4142        commit_hint: &crate::objects::ObjectId,
4143    ) -> Result<Option<Self>> {
4144        Self::open_with_options(objects_dir, true, Some(commit_hint))
4145    }
4146
4147    /// Open a reader bound to a specific on-disk pack index (bitmap / pack-order walks).
4148    pub(crate) fn open_for_pack_index(
4149        objects_dir: &Path,
4150        idx: Arc<PackIndex>,
4151    ) -> Result<Option<Self>> {
4152        if !crate::pack_store::PackStore::read_context_matches_objects_dir(objects_dir) {
4153            return Ok(None);
4154        }
4155        pack_cache::sync_thread_local_with_store(&crate::pack_store::PackStore::active_for_reads());
4156        let pack_bytes = read_pack_bytes_cached(&idx.pack_path)?;
4157        validate_pack_index_object_count(pack_bytes.as_ref(), idx.as_ref())?;
4158        pack_cache::mark_pack_object_count_validated(&idx.pack_path);
4159        pack_cache::remember_pack_hit(objects_dir, &idx);
4160        let pack_id = pack_cache::pack_id_for(&idx.pack_path);
4161        Ok(Some(Self {
4162            idx,
4163            pack_bytes,
4164            pack_id,
4165            cursor: 0,
4166            info_cache: HashMap::new(),
4167            delta_scratch: None,
4168            commit_header_buf: Vec::new(),
4169        }))
4170    }
4171
4172    /// Object kind of every entry starting at `offsets` (ascending), from entry headers alone.
4173    ///
4174    /// A delta takes the kind of its base, so nothing is inflated. The result is `None` for a
4175    /// delta whose base is not an earlier entry of `offsets` (a REF_DELTA base stored later in
4176    /// the pack or outside it), and for deltas on such entries; callers resolve those another way.
4177    ///
4178    /// # Errors
4179    ///
4180    /// [`Error::CorruptObject`] when an offset lies outside the pack or an entry header is
4181    /// malformed.
4182    pub(crate) fn object_kinds_at_sorted_offsets(
4183        &self,
4184        offsets: &[u64],
4185    ) -> Result<Vec<Option<ObjectKind>>> {
4186        let pack_bytes = self.pack_bytes.as_ref();
4187        let hash_bytes = self.idx.hash_bytes();
4188        let mut kinds: Vec<Option<ObjectKind>> = Vec::with_capacity(offsets.len());
4189        for (rank, &offset) in offsets.iter().enumerate() {
4190            let mut pos = usize::try_from(offset)
4191                .map_err(|_| Error::CorruptObject("pack offset out of range".to_owned()))?;
4192            let (packed_type, _) = parse_pack_object_header(pack_bytes, &mut pos)?;
4193            let base_offset = match packed_type {
4194                PackedType::OfsDelta => Some(parse_ofs_delta_base(pack_bytes, &mut pos, offset)?),
4195                PackedType::RefDelta => {
4196                    let raw = pack_bytes.get(pos..pos + hash_bytes).ok_or_else(|| {
4197                        Error::CorruptObject("truncated ref-delta base OID".to_owned())
4198                    })?;
4199                    self.idx.find_offset(&ObjectId::from_bytes(raw)?)?
4200                }
4201                _ => {
4202                    kinds.push(Some(packed_type_to_kind(packed_type)?));
4203                    continue;
4204                }
4205            };
4206            let base_kind = base_offset
4207                .and_then(|base| offsets.binary_search(&base).ok())
4208                .filter(|&base_rank| base_rank < rank)
4209                .and_then(|base_rank| kinds[base_rank]);
4210            kinds.push(base_kind);
4211        }
4212        Ok(kinds)
4213    }
4214
4215    /// Whether `oid` is indexed in this reader's pack.
4216    pub(crate) fn contains_oid(&self, oid: &crate::objects::ObjectId) -> Result<bool> {
4217        Ok(self.idx.find_offset(oid)?.is_some())
4218    }
4219
4220    fn open_with_options(
4221        objects_dir: &Path,
4222        allow_loose: bool,
4223        commit_hint: Option<&crate::objects::ObjectId>,
4224    ) -> Result<Option<Self>> {
4225        if !crate::pack_store::PackStore::read_context_matches_objects_dir(objects_dir) {
4226            return Ok(None);
4227        }
4228        pack_cache::sync_thread_local_with_store(&crate::pack_store::PackStore::active_for_reads());
4229        let memo_eligible = allow_loose && commit_hint.is_none();
4230        if memo_eligible {
4231            if let Some((idx, pack_bytes, pack_id)) = pack_cache::single_reader_for(objects_dir) {
4232                return Ok(Some(Self {
4233                    idx,
4234                    pack_bytes,
4235                    pack_id,
4236                    cursor: 0,
4237                    info_cache: HashMap::new(),
4238                    delta_scratch: None,
4239                    commit_header_buf: Vec::new(),
4240                }));
4241            }
4242        }
4243        let indexes = pack_indexes_for_lookup(objects_dir, PackLookupOptions::ALL_LOCAL_PACKS)?;
4244        let idx = match select_single_local_pack_index(&indexes, commit_hint)? {
4245            Some(idx) => idx,
4246            None => return Ok(None),
4247        };
4248        if !allow_loose && objects_dir_has_loose_objects(objects_dir) {
4249            return Ok(None);
4250        }
4251        let pack_bytes = read_pack_bytes_cached(&idx.pack_path)?;
4252        validate_pack_index_object_count(pack_bytes.as_ref(), idx.as_ref())?;
4253        pack_cache::mark_pack_object_count_validated(&idx.pack_path);
4254        pack_cache::remember_pack_hit(objects_dir, &idx);
4255        let pack_id = pack_cache::pack_id_for(&idx.pack_path);
4256        if memo_eligible && indexes.len() == 1 {
4257            pack_cache::remember_single_reader(objects_dir, &idx, &pack_bytes, pack_id);
4258        }
4259        Ok(Some(Self {
4260            idx,
4261            pack_bytes,
4262            pack_id,
4263            cursor: 0,
4264            info_cache: HashMap::new(),
4265            delta_scratch: None,
4266            commit_header_buf: Vec::new(),
4267        }))
4268    }
4269
4270    /// Inflate the start of commit `oid` into an internal buffer (undeltified entries only).
4271    ///
4272    /// Returns `None` when the object is missing, not a commit, or stored as a delta.
4273    ///
4274    /// # Errors
4275    ///
4276    /// Propagates pack index and zlib errors.
4277    pub fn read_commit_author_prefix(&mut self, oid: &ObjectId) -> Result<Option<&[u8]>> {
4278        self.read_commit_author_prefix_at(oid, true)
4279    }
4280
4281    /// Like [`Self::read_commit_author_prefix`], but pack index probes may be out of OID order.
4282    pub(crate) fn read_commit_author_prefix_unordered(
4283        &mut self,
4284        oid: &ObjectId,
4285    ) -> Result<Option<&[u8]>> {
4286        self.read_commit_author_prefix_at(oid, false)
4287    }
4288
4289    /// Inflate the start of the commit at pack index row `row` (pack-order / bitmap walks).
4290    pub(crate) fn read_commit_author_prefix_at_pack_row(
4291        &mut self,
4292        row: usize,
4293    ) -> Result<Option<&[u8]>> {
4294        self.cursor = row;
4295        let offset = self.idx.offset_at(row)?;
4296        self.read_commit_author_prefix_at_pack_offset(offset)
4297    }
4298
4299    fn read_commit_author_prefix_at(
4300        &mut self,
4301        oid: &ObjectId,
4302        monotonic: bool,
4303    ) -> Result<Option<&[u8]>> {
4304        let offset = if monotonic {
4305            self.idx.find_offset_monotonic(oid, &mut self.cursor)?
4306        } else {
4307            self.idx.find_offset(oid)?
4308        };
4309        let Some(offset) = offset else {
4310            return Ok(None);
4311        };
4312        self.read_commit_author_prefix_at_pack_offset(offset)
4313    }
4314
4315    fn read_commit_author_prefix_at_pack_offset(&mut self, offset: u64) -> Result<Option<&[u8]>> {
4316        let mut compressed = Vec::new();
4317        let (pack_bytes, mut pos) =
4318            pack_entry_bytes(&self.idx, offset, &self.pack_bytes, &mut compressed)?;
4319        if pos >= pack_bytes.len() {
4320            return Ok(None);
4321        }
4322        let (packed_type, _) = match parse_pack_object_header(pack_bytes, &mut pos) {
4323            Ok(v) => v,
4324            Err(_) => return Ok(None),
4325        };
4326        if !matches!(packed_type, PackedType::Commit) {
4327            return Ok(None);
4328        }
4329        self.commit_header_buf.clear();
4330        self.delta_scratch
4331            .get_or_insert_with(ZlibInflateScratch::default)
4332            .decompress_prefix_into(
4333                &pack_bytes[pos..],
4334                COMMIT_HEADER_READ_PREFIX,
4335                &mut self.commit_header_buf,
4336            )?;
4337        if self.commit_header_buf.is_empty() {
4338            return Ok(None);
4339        }
4340        Ok(Some(&self.commit_header_buf))
4341    }
4342
4343    /// Drop any cached commit header prefix (required before delta / ODB fallbacks).
4344    pub(crate) fn clear_commit_header_prefix(&mut self) {
4345        self.commit_header_buf.clear();
4346    }
4347
4348    fn read_info_at_pack_offset(&mut self, start_offset: u64) -> Result<ObjectInfo> {
4349        if let Some(info) = self.info_cache.get(&start_offset) {
4350            return Ok(*info);
4351        }
4352        let pack_path = self.idx.pack_path.clone();
4353        let info = with_pack_read_delta_state(0, |state| {
4354            self.resolve_info_at_pack_offset_body(start_offset, state)
4355        })?;
4356        self.info_cache.insert(start_offset, info);
4357        remember_pack_read_info(&pack_path, start_offset, info);
4358        Ok(info)
4359    }
4360
4361    fn resolve_info_at_pack_offset_body(
4362        &mut self,
4363        start_offset: u64,
4364        state: &mut DeltaChainState,
4365    ) -> Result<ObjectInfo> {
4366        let pack_bytes = self.pack_bytes.as_ref();
4367        let mut cur_offset = start_offset;
4368        let mut result_size: Option<u64> = None;
4369        loop {
4370            state.visit(self.pack_id, cur_offset)?;
4371            if let Some(base) = self.info_cache.get(&cur_offset) {
4372                if let Some(size) = result_size {
4373                    return Ok(ObjectInfo {
4374                        kind: base.kind,
4375                        size,
4376                    });
4377                }
4378                return Ok(*base);
4379            }
4380
4381            let object_start = cur_offset;
4382            let mut pos = cur_offset as usize;
4383            let (packed_type, header_size) = parse_pack_object_header(pack_bytes, &mut pos)?;
4384            match packed_type {
4385                PackedType::Commit | PackedType::Tree | PackedType::Blob | PackedType::Tag => {
4386                    let kind = packed_type_to_kind(packed_type)?;
4387                    let undeltified = ObjectInfo {
4388                        kind,
4389                        size: header_size,
4390                    };
4391                    self.info_cache.insert(cur_offset, undeltified);
4392                    let size = result_size.unwrap_or(header_size);
4393                    return Ok(ObjectInfo { kind, size });
4394                }
4395                PackedType::OfsDelta => {
4396                    state.next_hop()?;
4397                    let base_offset = parse_ofs_delta_base(pack_bytes, &mut pos, object_start)?;
4398                    if result_size.is_none() {
4399                        result_size = Some(
4400                            self.delta_scratch
4401                                .get_or_insert_with(ZlibInflateScratch::default)
4402                                .read_pack_delta_result_size(pack_bytes, &mut pos)?,
4403                        );
4404                    }
4405                    cur_offset = base_offset;
4406                }
4407                PackedType::RefDelta => {
4408                    state.next_hop()?;
4409                    let hb = self.idx.hash_bytes();
4410                    if pos + hb > pack_bytes.len() {
4411                        return Err(Error::CorruptObject(
4412                            "truncated ref-delta base OID".to_owned(),
4413                        ));
4414                    }
4415                    let base_raw = &pack_bytes[pos..pos + hb];
4416                    pos += hb;
4417                    if result_size.is_none() {
4418                        result_size = Some(
4419                            self.delta_scratch
4420                                .get_or_insert_with(ZlibInflateScratch::default)
4421                                .read_pack_delta_result_size(pack_bytes, &mut pos)?,
4422                        );
4423                    }
4424                    let base_oid = ObjectId::from_bytes(base_raw)?;
4425                    let Some(base_offset) = self.idx.find_offset(&base_oid)? else {
4426                        return Err(Error::ObjectNotFound(base_oid.to_hex()));
4427                    };
4428                    cur_offset = base_offset;
4429                }
4430            }
4431        }
4432    }
4433
4434    /// Kind and size for `oid` without loading the object body.
4435    ///
4436    /// # Errors
4437    ///
4438    /// Same as [`read_object_info_from_pack_at_offset`].
4439    pub fn read_info(&mut self, oid: &ObjectId) -> Result<ObjectInfo> {
4440        let Some(offset) = self.idx.find_offset_monotonic(oid, &mut self.cursor)? else {
4441            return Err(Error::ObjectNotFound(oid.to_hex()));
4442        };
4443        if let Some(info) =
4444            read_whole_pack_entry_info_fast(&self.idx, offset, self.pack_bytes.as_ref())
4445        {
4446            self.info_cache.insert(offset, info);
4447            return Ok(info);
4448        }
4449        self.read_info_at_pack_offset(offset)
4450    }
4451
4452    /// Decompress the object for `oid`.
4453    ///
4454    /// # Errors
4455    ///
4456    /// Same as [`read_object_from_pack_at_offset`].
4457    pub fn read_object(&mut self, oid: &ObjectId) -> Result<Object> {
4458        let Some(offset) = self.idx.find_offset_monotonic(oid, &mut self.cursor)? else {
4459            return Err(Error::ObjectNotFound(oid.to_hex()));
4460        };
4461        if !self.pack_bytes.uses_positioned_reads() {
4462            if let Some(obj) =
4463                read_whole_pack_entry_object_fast(&self.idx, offset, &self.pack_bytes)
4464            {
4465                return Ok(obj);
4466            }
4467        }
4468        read_object_from_pack_at_offset(&self.idx, offset)
4469    }
4470
4471    /// Like [`Self::read_object`], but skip an object the pack stores whole with a payload
4472    /// larger than `limit`, returning [`BoundedRead::Larger`] without inflating it.
4473    ///
4474    /// Deltas are always read: reconstructing one needs the whole object in memory anyway.
4475    ///
4476    /// # Errors
4477    ///
4478    /// Same as [`Self::read_object`].
4479    pub(crate) fn read_object_unless_whole_larger(
4480        &mut self,
4481        oid: &ObjectId,
4482        limit: u64,
4483    ) -> Result<BoundedRead> {
4484        let Some(offset) = self.idx.find_offset_monotonic(oid, &mut self.cursor)? else {
4485            return Err(Error::ObjectNotFound(oid.to_hex()));
4486        };
4487        let mut pos = usize::try_from(offset)
4488            .map_err(|_| Error::CorruptObject(format!("pack offset {offset} overflows")))?;
4489        if let Ok((packed_type, size)) = parse_pack_object_header(&self.pack_bytes, &mut pos) {
4490            let whole = !matches!(packed_type, PackedType::OfsDelta | PackedType::RefDelta);
4491            if whole && size > limit {
4492                return Ok(BoundedRead::Larger);
4493            }
4494        }
4495        if let Some(obj) =
4496            read_whole_pack_entry_object_fast(&self.idx, offset, self.pack_bytes.as_ref())
4497        {
4498            return Ok(BoundedRead::Object(obj));
4499        }
4500        read_object_from_pack_at_offset(&self.idx, offset).map(BoundedRead::Object)
4501    }
4502
4503    /// Like [`Self::read_object`], inflating into `out` and preserving its capacity when possible.
4504    ///
4505    /// Delta bases resolved along the way are not added to the delta-base cache.
4506    pub fn read_object_into(&mut self, oid: &ObjectId, out: &mut Vec<u8>) -> Result<ObjectKind> {
4507        self.read_object_into_with_delta_policy(oid, false, out)
4508    }
4509
4510    /// [`Self::read_object_into`], keeping resolved delta bases in the delta-base cache when
4511    /// `populate_delta_lru` is set.
4512    pub(crate) fn read_object_into_with_delta_policy(
4513        &mut self,
4514        oid: &ObjectId,
4515        populate_delta_lru: bool,
4516        out: &mut Vec<u8>,
4517    ) -> Result<ObjectKind> {
4518        let Some(offset) = self.idx.find_offset_monotonic(oid, &mut self.cursor)? else {
4519            return Err(Error::ObjectNotFound(oid.to_hex()));
4520        };
4521        if !self.pack_bytes.uses_positioned_reads() {
4522            if let Some(kind) =
4523                read_whole_pack_entry_object_into(&self.idx, offset, &self.pack_bytes, out)
4524            {
4525                return Ok(kind);
4526            }
4527        }
4528        read_object_at_depth_into(&self.idx, offset, 0, populate_delta_lru, out)
4529    }
4530
4531    /// Pack file offset for `oid` in this reader's index, if present.
4532    ///
4533    /// # Errors
4534    ///
4535    /// Propagates pack index lookup errors.
4536    pub fn pack_offset(&self, oid: &ObjectId) -> Result<Option<u64>> {
4537        self.idx.find_offset(oid)
4538    }
4539}
4540fn pack_indexes_for_lookup(
4541    objects_dir: &Path,
4542    opts: PackLookupOptions,
4543) -> Result<Vec<Arc<PackIndex>>> {
4544    let indexes = read_local_pack_indexes_cached(objects_dir)?;
4545    if !opts.skip_midx_covered_packs && !opts.only_midx_covered_packs {
4546        return Ok(indexes);
4547    }
4548    let covered = pack_cache::midx_covered_pack_names(objects_dir);
4549    if covered.is_empty() {
4550        return if opts.only_midx_covered_packs {
4551            Ok(Vec::new())
4552        } else {
4553            Ok(indexes)
4554        };
4555    }
4556    Ok(indexes
4557        .into_iter()
4558        .filter(|idx| {
4559            idx.idx_path
4560                .file_name()
4561                .and_then(|n| n.to_str())
4562                .map(|name| {
4563                    if opts.only_midx_covered_packs {
4564                        covered.contains(name)
4565                    } else {
4566                        !covered.contains(name)
4567                    }
4568                })
4569                .unwrap_or(!opts.only_midx_covered_packs)
4570        })
4571        .collect())
4572}
4573
4574/// Search all pack indexes in `objects_dir` for the given OID and read it.
4575///
4576/// When more than one pack contains `oid` (a redundant copy), a read failure in
4577/// one pack — e.g. a corrupted delta base or zlib stream — is not fatal: Git
4578/// retries the remaining sources before giving up, so an intact redundant pack
4579/// still satisfies the read (t5303 pack-corruption-resilience). Only when every
4580/// pack that names `oid` fails to produce it do we surface the last error.
4581///
4582/// # Errors
4583///
4584/// Returns [`Error::ObjectNotFound`] if no pack contains the OID.
4585pub fn read_object_from_packs(objects_dir: &Path, oid: &ObjectId) -> Result<Object> {
4586    if crate::pack_store::PackStore::has_context() {
4587        return read_object_from_packs_in_context(objects_dir, oid);
4588    }
4589    let store = crate::pack_store::PackStore::legacy_for_objects_dir(objects_dir);
4590    crate::pack_store::PackStore::with_context(store, || {
4591        read_object_from_packs_in_context(objects_dir, oid)
4592    })
4593}
4594
4595/// Like [`read_object_from_packs`], inflating into `buf` and reusing its capacity.
4596///
4597/// # Errors
4598///
4599/// Same as [`read_object_from_packs`].
4600pub fn read_object_from_packs_into(
4601    objects_dir: &Path,
4602    oid: &ObjectId,
4603    buf: &mut Vec<u8>,
4604) -> Result<ObjectKind> {
4605    if crate::pack_store::PackStore::has_context() {
4606        return read_object_from_packs_in_context_into(objects_dir, oid, buf);
4607    }
4608    let store = crate::pack_store::PackStore::legacy_for_objects_dir(objects_dir);
4609    crate::pack_store::PackStore::with_context(store, || {
4610        read_object_from_packs_in_context_into(objects_dir, oid, buf)
4611    })
4612}
4613
4614/// Search local packs for `oid` and return its kind and uncompressed size.
4615///
4616/// # Errors
4617///
4618/// Returns [`Error::ObjectNotFound`] when no pack lists `oid`.
4619pub fn read_object_info_from_packs(objects_dir: &Path, oid: &ObjectId) -> Result<ObjectInfo> {
4620    match try_read_object_info_from_packs_with_options(
4621        objects_dir,
4622        oid,
4623        PackLookupOptions::ALL_LOCAL_PACKS,
4624    ) {
4625        Ok(info) => Ok(info),
4626        Err(err @ Error::ObjectNotFound(_)) => {
4627            if reprepare_pack_directory_on_miss(objects_dir)? {
4628                try_read_object_info_from_packs_with_options(
4629                    objects_dir,
4630                    oid,
4631                    PackLookupOptions::ALL_LOCAL_PACKS,
4632                )
4633            } else {
4634                Err(err)
4635            }
4636        }
4637        Err(err) => Err(err),
4638    }
4639}
4640
4641/// Search local packs for `oid` without re-preparing the pack directory listing.
4642pub(crate) fn try_read_object_info_from_packs_with_options(
4643    objects_dir: &Path,
4644    oid: &ObjectId,
4645    opts: PackLookupOptions,
4646) -> Result<ObjectInfo> {
4647    if crate::pack_store::PackStore::read_context_matches_objects_dir(objects_dir) {
4648        pack_cache::sync_thread_local_with_store(&crate::pack_store::PackStore::active_for_reads());
4649    }
4650    if opts.allows_pack_shortcuts() {
4651        if let Some(info) = pack_cache::try_read_info_from_hinted_pack(objects_dir, oid)? {
4652            return Ok(info);
4653        }
4654    }
4655    let indexes = pack_indexes_for_lookup(objects_dir, opts)?;
4656    let mut last_err: Option<Error> = None;
4657    for idx in &indexes {
4658        let Some(offset) = idx.find_offset(oid)? else {
4659            continue;
4660        };
4661        match read_object_info_from_pack_at_offset(idx, offset) {
4662            Ok(info) => {
4663                pack_cache::remember_pack_hit(objects_dir, idx);
4664                return Ok(info);
4665            }
4666            Err(Error::ObjectNotFound(_)) => {}
4667            Err(err) => last_err = Some(err),
4668        }
4669    }
4670    Err(last_err.unwrap_or_else(|| Error::ObjectNotFound(oid.to_hex())))
4671}
4672
4673/// Search local packs for `oid` without re-preparing the pack directory listing.
4674pub(crate) fn try_read_object_from_packs_with_options(
4675    objects_dir: &Path,
4676    oid: &ObjectId,
4677    opts: PackLookupOptions,
4678) -> Result<Object> {
4679    if crate::pack_store::PackStore::read_context_matches_objects_dir(objects_dir) {
4680        pack_cache::sync_thread_local_with_store(&crate::pack_store::PackStore::active_for_reads());
4681    }
4682    if opts.allows_pack_shortcuts() {
4683        if let Some(obj) = pack_cache::try_read_object_via_single_pack_reader(objects_dir, oid)? {
4684            return Ok(obj);
4685        }
4686        if let Some(obj) = pack_cache::try_read_from_hinted_pack(objects_dir, oid)? {
4687            return Ok(obj);
4688        }
4689    }
4690    let indexes = pack_indexes_for_lookup(objects_dir, opts)?;
4691    let mut last_err: Option<Error> = None;
4692    for idx in &indexes {
4693        let Some(offset) = pack_cache::find_offset_in_pack_read_context(objects_dir, idx, oid)?
4694        else {
4695            continue;
4696        };
4697        match read_object_from_pack_at_offset(idx, offset) {
4698            Ok(obj) => {
4699                pack_cache::remember_pack_hit(objects_dir, idx);
4700                return Ok(obj);
4701            }
4702            // The object is missing from this particular pack despite the index
4703            // claim — keep looking in the others.
4704            Err(Error::ObjectNotFound(_)) => {}
4705            // The pack copy is unreadable (corrupt delta/zlib/header). A redundant
4706            // pack may still hold an intact copy, so remember the error and retry.
4707            Err(err) => last_err = Some(err),
4708        }
4709    }
4710    Err(last_err.unwrap_or_else(|| Error::ObjectNotFound(oid.to_hex())))
4711}
4712
4713/// Like [`try_read_object_from_packs_with_options`], writing the payload into `buf`.
4714///
4715/// `populate_delta_lru` controls whether delta bases resolved on the way are kept in the
4716/// shared delta-base cache (as [`try_read_object_from_packs_with_options`] always does).
4717pub(crate) fn try_read_object_from_packs_with_options_into(
4718    objects_dir: &Path,
4719    oid: &ObjectId,
4720    opts: PackLookupOptions,
4721    populate_delta_lru: bool,
4722    buf: &mut Vec<u8>,
4723) -> Result<ObjectKind> {
4724    if crate::pack_store::PackStore::read_context_matches_objects_dir(objects_dir) {
4725        pack_cache::sync_thread_local_with_store(&crate::pack_store::PackStore::active_for_reads());
4726    }
4727    if opts.allows_pack_shortcuts() {
4728        if let Some(kind) = pack_cache::try_read_object_via_single_pack_reader_into(
4729            objects_dir,
4730            oid,
4731            populate_delta_lru,
4732            buf,
4733        )? {
4734            return Ok(kind);
4735        }
4736        if let Some(kind) =
4737            pack_cache::try_read_from_hinted_pack_into(objects_dir, oid, populate_delta_lru, buf)?
4738        {
4739            return Ok(kind);
4740        }
4741    }
4742    let indexes = pack_indexes_for_lookup(objects_dir, opts)?;
4743    let mut last_err: Option<Error> = None;
4744    for idx in &indexes {
4745        let Some(offset) = pack_cache::find_offset_in_pack_read_context(objects_dir, idx, oid)?
4746        else {
4747            continue;
4748        };
4749        match read_object_at_depth_into(idx, offset, 0, populate_delta_lru, buf) {
4750            Ok(kind) => {
4751                pack_cache::remember_pack_hit(objects_dir, idx);
4752                return Ok(kind);
4753            }
4754            Err(Error::ObjectNotFound(_)) => {}
4755            Err(err) => last_err = Some(err),
4756        }
4757    }
4758    Err(last_err.unwrap_or_else(|| Error::ObjectNotFound(oid.to_hex())))
4759}
4760
4761/// Whether `idx_path` uses the legacy v1 pack-index format (no `PACK` signature).
4762#[must_use]
4763pub fn pack_index_is_v1(idx_path: &Path) -> bool {
4764    fs::read(idx_path)
4765        .ok()
4766        .is_some_and(|bytes| bytes.len() >= 4 && bytes[0..4] != [0xff, b't', b'O', b'c'])
4767}
4768
4769/// Resolve `oid` from local packs via the process-wide pack cache (cached `.idx`
4770/// parses, cached pack bytes, fanout binary search, delta-base cache).
4771///
4772/// Returns `Ok(None)` when no local pack index names the object. When a pack
4773/// copy fails to decode from a v1 (legacy) index, returns an empty blob placeholder
4774/// (historical pack-objects behavior). Any other decode failure invokes
4775/// `alternate_read` once before trying the next pack.
4776///
4777/// # Errors
4778///
4779/// Returns [`Error::Io`] when pack indexes cannot be enumerated.
4780pub fn try_read_object_from_local_packs_cached(
4781    objects_dir: &Path,
4782    oid: &ObjectId,
4783    mut alternate_read: impl FnMut() -> Result<Object>,
4784) -> Result<Option<Object>> {
4785    let indexes = read_local_pack_indexes_cached(objects_dir)?;
4786    for idx in &indexes {
4787        if idx.find_offset(oid)?.is_none() {
4788            continue;
4789        }
4790        match read_object_from_pack(idx, oid) {
4791            Ok(obj) => return Ok(Some(obj)),
4792            Err(_) if pack_index_is_v1(&idx.idx_path) => {
4793                return Ok(Some(Object::new(ObjectKind::Blob, Vec::new())));
4794            }
4795            Err(_) => {
4796                if let Ok(obj) = alternate_read() {
4797                    return Ok(Some(obj));
4798                }
4799            }
4800        }
4801    }
4802    Ok(None)
4803}
4804
4805/// When `oid` is stored as a delta in a pack, return its delta base object id.
4806/// Returns [`None`] for loose objects and for non-delta packed objects.
4807/// Parse a packed delta entry and inflate its instruction stream when the base OID is in `in_pack`.
4808fn parse_in_pack_delta_reuse_candidate(
4809    idx: &PackIndex,
4810    pack_bytes: &[u8],
4811    entry_offset: u64,
4812    in_pack: &HashSet<ObjectId>,
4813) -> Result<Option<(ObjectId, Vec<u8>)>> {
4814    let hb = idx.hash_bytes();
4815    let mut p = entry_offset as usize;
4816    let (packed_type, size) = parse_pack_object_header(pack_bytes, &mut p)?;
4817    let base = match packed_type {
4818        PackedType::RefDelta => {
4819            if p + hb > pack_bytes.len() {
4820                return Err(Error::CorruptObject(
4821                    "truncated ref-delta base oid while scanning for reuse".to_owned(),
4822                ));
4823            }
4824            let bo = ObjectId::from_bytes(&pack_bytes[p..p + hb])?;
4825            p += hb;
4826            bo
4827        }
4828        PackedType::OfsDelta => {
4829            let base_off = parse_ofs_delta_base(pack_bytes, &mut p, entry_offset)?;
4830            let Some(pos) = idx.find_position_by_offset_sorted(base_off)? else {
4831                return Ok(None);
4832            };
4833            let base_oid_bytes = idx.oid_at(pos);
4834            if base_oid_bytes.len() != hb {
4835                return Ok(None);
4836            }
4837            ObjectId::from_bytes(base_oid_bytes)?
4838        }
4839        _ => return Ok(None),
4840    };
4841    if !in_pack.contains(&base) {
4842        return Ok(None);
4843    }
4844    let zlib_start = p;
4845    let Some(end_pos) = pack_entry_raw_end(idx, pack_bytes, entry_offset)? else {
4846        return Ok(None);
4847    };
4848    if zlib_start >= end_pos {
4849        return Ok(None);
4850    }
4851    let compressed = &pack_bytes[zlib_start..end_pos];
4852    let mut zpos = 0usize;
4853    let mut scratch = ZlibInflateScratch::default();
4854    let delta = match scratch.decompress_fixed(compressed, &mut zpos, size) {
4855        Ok(d) => d,
4856        Err(_) => return Ok(None),
4857    };
4858    Ok(Some((base, delta)))
4859}
4860
4861/// Like [`parse_in_pack_delta_reuse_candidate`], but returns the zlib payload and header size
4862/// without inflating (for verbatim reuse in a new pack).
4863fn parse_in_pack_delta_reuse_zlib(
4864    idx: &PackIndex,
4865    pack_bytes: &[u8],
4866    entry_offset: u64,
4867    reusable_base: &dyn Fn(&ObjectId) -> bool,
4868) -> Result<Option<(ObjectId, u64, usize, usize)>> {
4869    let start = entry_offset as usize;
4870    let hb = idx.hash_bytes();
4871    let mut p = start;
4872    let (packed_type, size) = parse_pack_object_header(pack_bytes, &mut p)?;
4873    let base = match packed_type {
4874        PackedType::RefDelta => {
4875            if p + hb > pack_bytes.len() {
4876                return Err(Error::CorruptObject(
4877                    "truncated ref-delta base oid while scanning for reuse".to_owned(),
4878                ));
4879            }
4880            let bo = ObjectId::from_bytes(&pack_bytes[p..p + hb])?;
4881            p += hb;
4882            bo
4883        }
4884        PackedType::OfsDelta => {
4885            let base_off = parse_ofs_delta_base(pack_bytes, &mut p, entry_offset)?;
4886            let Some(pos) = idx.find_position_by_offset_sorted(base_off)? else {
4887                return Ok(None);
4888            };
4889            let base_oid_bytes = idx.oid_at(pos);
4890            if base_oid_bytes.len() != hb {
4891                return Ok(None);
4892            }
4893            ObjectId::from_bytes(base_oid_bytes)?
4894        }
4895        _ => return Ok(None),
4896    };
4897    if !reusable_base(&base) {
4898        return Ok(None);
4899    }
4900    let zlib_start = p;
4901    let Some(end_pos) = pack_entry_raw_end(idx, pack_bytes, entry_offset)? else {
4902        return Ok(None);
4903    };
4904    if zlib_start >= end_pos {
4905        return Ok(None);
4906    }
4907    let entry_bytes = &pack_bytes[start..end_pos];
4908    if let Some(crc) = idx.crc32_for_pack_offset(entry_offset)? {
4909        if crc32fast::hash(entry_bytes) != crc {
4910            return Ok(None);
4911        }
4912    }
4913    Ok(Some((base, size, zlib_start, end_pos)))
4914}
4915
4916fn reuse_delta_matches_pack_resolution(
4917    idx: &PackIndex,
4918    target: &ObjectId,
4919    base: ObjectId,
4920    delta: &[u8],
4921) -> bool {
4922    let Ok(base_obj) = read_object_from_pack(idx, &base) else {
4923        return false;
4924    };
4925    let Ok(target_obj) = read_object_from_pack(idx, target) else {
4926        return false;
4927    };
4928    crate::unpack_objects::apply_delta(&base_obj.data, delta)
4929        .ok()
4930        .is_some_and(|applied| applied == target_obj.data)
4931}
4932
4933/// The `(id, pack offset)` of every object of `wanted` that `idx` lists.
4934///
4935/// A set much smaller than the index is looked up id by id, so packing a few objects
4936/// from a large pack does not visit every index entry; larger sets scan the index once.
4937fn wanted_index_entries<'a>(
4938    idx: &PackIndex,
4939    wanted: impl ExactSizeIterator<Item = &'a ObjectId>,
4940    contains: impl Fn(&ObjectId) -> bool,
4941) -> Result<Vec<(ObjectId, u64)>> {
4942    // A lookup costs about log2(entries) comparisons; a scan one set probe per entry.
4943    let lookup_cost = wanted
4944        .len()
4945        .saturating_mul(usize::BITS.saturating_sub(idx.len().leading_zeros()) as usize);
4946    if lookup_cost < idx.len() {
4947        let mut out = Vec::new();
4948        for oid in wanted {
4949            if let Some(offset) = idx.find_offset(oid)? {
4950                out.push((*oid, offset));
4951            }
4952        }
4953        return Ok(out);
4954    }
4955    let mut out = Vec::new();
4956    for entry in idx.iter() {
4957        let Ok(oid) = ObjectId::from_bytes(entry.oid()) else {
4958            continue;
4959        };
4960        if contains(&oid) {
4961            out.push((oid, entry.offset()?));
4962        }
4963    }
4964    Ok(out)
4965}
4966
4967/// Where an object to be packed currently lives among the local packs.
4968#[derive(Debug, Clone, Copy, PartialEq, Eq)]
4969pub(crate) struct PackedSlot {
4970    /// Position of the pack in oldest-first order (only compared for equality).
4971    pub pack: u32,
4972    /// The object is stored whole (not as `OFS_DELTA` / `REF_DELTA`) in that pack.
4973    pub whole: bool,
4974}
4975
4976/// Locate each object of `wanted` in the local packs, oldest pack first, as
4977/// [`collect_packed_delta_reuse_for_send`] does when choosing which copy to reuse.
4978///
4979/// Objects that are not packed are absent from the result.
4980///
4981/// # Errors
4982///
4983/// Returns [`Error::CorruptObject`] when a pack entry header is malformed, or
4984/// [`Error::Io`] when a pack cannot be read.
4985pub(crate) fn locate_packed_objects(
4986    objects_dir: &Path,
4987    wanted: &crate::objects::OidHashSet,
4988) -> Result<crate::objects::OidHashMap<PackedSlot>> {
4989    let mut indexes = read_local_pack_indexes_cached(objects_dir)?;
4990    sort_pack_indexes_oldest_first(&mut indexes);
4991    let mut slots = crate::objects::oid_hash_map_with_capacity(wanted.len());
4992    for (pack, idx) in (0u32..).zip(&indexes) {
4993        let bytes = read_pack_bytes_cached(&idx.pack_path)?;
4994        for (oid, offset) in wanted_index_entries(idx, wanted.iter(), |oid| wanted.contains(oid))? {
4995            if slots.contains_key(&oid) {
4996                continue;
4997            }
4998            let mut pos = usize::try_from(offset)
4999                .map_err(|_| Error::CorruptObject("pack offset exceeds memory".to_owned()))?;
5000            let (packed_type, _) = parse_pack_object_header(&bytes, &mut pos)?;
5001            let whole = !matches!(packed_type, PackedType::OfsDelta | PackedType::RefDelta);
5002            slots.insert(oid, PackedSlot { pack, whole });
5003        }
5004    }
5005    Ok(slots)
5006}
5007
5008/// An on-disk delta that can be copied as it is: base, uncompressed delta size, the pack
5009/// it is stored in, and the zlib stream's byte range in that pack.
5010pub(crate) type OnDiskReuseZlibEntry = (ObjectId, u64, Arc<PackData>, usize, usize);
5011
5012/// Scan local packs once and collect on-disk delta reuse candidates for objects in `send`.
5013///
5014/// An object of `send` that some local pack (oldest first) stores as `REF_DELTA` or
5015/// `OFS_DELTA` is kept when `reusable_base` accepts both it and its base; the result maps it
5016/// to its base id, uncompressed delta size, the pack bytes and the zlib stream's start and end
5017/// offsets in them, so the delta can be copied from the source pack verbatim. Every base down
5018/// a stored chain is itself such a target or was accepted when its target was parsed. Entries
5019/// are CRC-checked when the index provides CRC32.
5020///
5021/// The returned edges never form a cycle. Each pack's deltas are acyclic, but two packs
5022/// may store the same objects as deltas in opposite directions (a repository filled by
5023/// separate pushes, whose later thin pack was completed with bases the earlier one
5024/// stores as deltas). Such a cycle loses the edge of its smallest object id, which
5025/// is then sent whole, as [`delta_cycle_breaks`] describes.
5026///
5027/// # Errors
5028///
5029/// Returns [`Error::CorruptObject`] when a pack stream is malformed.
5030pub(crate) fn collect_packed_delta_reuse_for_send(
5031    objects_dir: &Path,
5032    send: &HashSet<ObjectId>,
5033    reusable_base: &dyn Fn(&ObjectId) -> bool,
5034) -> Result<HashMap<ObjectId, OnDiskReuseZlibEntry>> {
5035    let mut indexes = read_local_pack_indexes_cached(objects_dir)?;
5036    sort_pack_indexes_oldest_first(&mut indexes);
5037
5038    let mut found: HashMap<ObjectId, OnDiskReuseZlibEntry> = HashMap::new();
5039    for idx in indexes.iter().filter(|idx| idx.hash_bytes() == 20) {
5040        let pack_bytes = read_pack_bytes_cached(&idx.pack_path)?;
5041        for (oid, offset) in wanted_index_entries(idx, send.iter(), |oid| send.contains(oid))? {
5042            if found.contains_key(&oid) || !reusable_base(&oid) {
5043                continue;
5044            }
5045            let Some((base, size, zlib_start, zlib_end)) =
5046                parse_in_pack_delta_reuse_zlib(idx, &pack_bytes, offset, reusable_base)?
5047            else {
5048                continue;
5049            };
5050            found.insert(
5051                oid,
5052                (base, size, Arc::clone(&pack_bytes), zlib_start, zlib_end),
5053            );
5054        }
5055    }
5056    // Edges whose target and base were both taken from one pack are that pack's own,
5057    // acyclic deltas, so every cycle passes through an edge that crosses packs.
5058    let crosses_packs = |(_, (base, _, pack, ..)): &(&ObjectId, &OnDiskReuseZlibEntry)| {
5059        found
5060            .get(base)
5061            .is_some_and(|(_, _, base_pack, ..)| !Arc::ptr_eq(base_pack, pack))
5062    };
5063    let starts = found.iter().filter(crosses_packs).map(|(t, _)| *t);
5064    for target in delta_cycle_breaks(starts, |oid| found.get(oid).map(|e| e.0)) {
5065        found.remove(&target);
5066    }
5067    Ok(found)
5068}
5069
5070/// If `oid` is stored as `REF_DELTA` or `OFS_DELTA` in a local pack and its base OID is in
5071/// `packed_set`, return the base OID and the **uncompressed** delta payload (Git binary delta).
5072///
5073/// Callers re-zlib when writing a new pack so we do not depend on copying raw deflate streams.
5074///
5075/// # Errors
5076///
5077/// Returns [`Error::CorruptObject`] when the pack stream is malformed.
5078pub fn packed_ref_delta_reuse_slice(
5079    objects_dir: &Path,
5080    oid: &ObjectId,
5081    packed_set: &HashSet<ObjectId>,
5082) -> Result<Option<(ObjectId, Vec<u8>)>> {
5083    for (idx, entry_offset) in packs_holding(objects_dir, oid, PackPreference::Oldest)? {
5084        if idx.hash_bytes() != 20 {
5085            continue;
5086        }
5087        let pack_bytes = read_pack_bytes_cached(&idx.pack_path)?;
5088        let Some((base, delta)) =
5089            parse_in_pack_delta_reuse_candidate(&idx, &pack_bytes, entry_offset, packed_set)?
5090        else {
5091            continue;
5092        };
5093        if reuse_delta_matches_pack_resolution(&idx, oid, base, &delta) {
5094            return Ok(Some((base, delta)));
5095        }
5096    }
5097    Ok(None)
5098}
5099
5100/// Prefer older packs when the same OID exists as a full object in a fresh repack and as a delta
5101/// in an earlier thin pack (t5316).
5102///
5103/// Each pack's mtime is read once per call (not once per comparison).
5104pub(crate) fn sort_pack_indexes_oldest_first(indexes: &mut [Arc<PackIndex>]) {
5105    if indexes.len() < 2 {
5106        return;
5107    }
5108    indexes.sort_by_cached_key(|idx| (pack_mtime(idx), idx.pack_path.clone()));
5109}
5110
5111fn pack_mtime(idx: &PackIndex) -> std::time::SystemTime {
5112    fs::metadata(&idx.pack_path)
5113        .and_then(|m| m.modified())
5114        .unwrap_or(std::time::SystemTime::UNIX_EPOCH)
5115}
5116
5117/// Which pack wins when one object is stored in several.
5118#[derive(Clone, Copy)]
5119enum PackPreference {
5120    /// Oldest pack first, as [`sort_pack_indexes_oldest_first`].
5121    Oldest,
5122    /// Newest pack first (ties: path descending).
5123    Newest,
5124}
5125
5126/// The local packs whose index lists `oid`, with its offset there, preferred pack first.
5127///
5128/// Per-object lookups call this instead of sorting every pack: the order only matters
5129/// among packs that hold the object, which is almost always one, so the common case
5130/// reads no pack mtimes at all.
5131fn packs_holding(
5132    objects_dir: &Path,
5133    oid: &ObjectId,
5134    preference: PackPreference,
5135) -> Result<Vec<(Arc<PackIndex>, u64)>> {
5136    let mut holding = Vec::new();
5137    for idx in read_local_pack_indexes_cached(objects_dir)? {
5138        if let Some(offset) = idx.find_offset(oid)? {
5139            holding.push((idx, offset));
5140        }
5141    }
5142    if holding.len() > 1 {
5143        let key = |idx: &PackIndex| (pack_mtime(idx), idx.pack_path.clone());
5144        match preference {
5145            PackPreference::Oldest => holding.sort_by_cached_key(|(idx, _)| key(idx)),
5146            PackPreference::Newest => {
5147                holding.sort_by_cached_key(|(idx, _)| std::cmp::Reverse(key(idx)));
5148            }
5149        }
5150    }
5151    Ok(holding)
5152}
5153
5154/// `packed_delta_base_oid` function.
5155pub fn packed_delta_base_oid(objects_dir: &Path, oid: &ObjectId) -> Result<Option<ObjectId>> {
5156    for (idx, entry_offset) in packs_holding(objects_dir, oid, PackPreference::Newest)? {
5157        if idx.hash_bytes() != 20 {
5158            continue;
5159        }
5160        let pack_bytes = read_pack_bytes_cached(&idx.pack_path)?;
5161        let mut p = entry_offset as usize;
5162        let (packed_type, _) = parse_pack_object_header(&pack_bytes, &mut p)?;
5163        match packed_type {
5164            PackedType::RefDelta => {
5165                let hb = idx.hash_bytes();
5166                if p + hb > pack_bytes.len() {
5167                    return Err(Error::CorruptObject("truncated ref-delta base".to_owned()));
5168                }
5169                return Ok(Some(ObjectId::from_bytes(&pack_bytes[p..p + hb])?));
5170            }
5171            PackedType::OfsDelta => {
5172                let base_off = parse_ofs_delta_base(&pack_bytes, &mut p, entry_offset)?;
5173                return Ok(idx
5174                    .find_position_by_offset_sorted(base_off)?
5175                    .and_then(|pos| ObjectId::from_bytes(idx.oid_at(pos)).ok()));
5176            }
5177            _ => continue,
5178        }
5179    }
5180    Ok(None)
5181}
5182
5183/// End offset (exclusive) of the raw packed bytes for the object at `entry_offset`.
5184///
5185/// Uses the next larger pack offset from the index, or the pack trailer when this is the last object.
5186pub(crate) fn pack_entry_raw_end(
5187    idx: &PackIndex,
5188    pack_bytes: &[u8],
5189    entry_offset: u64,
5190) -> Result<Option<usize>> {
5191    let hb = idx.hash_bytes();
5192    let Some(pack_end) = pack_bytes.len().checked_sub(hb) else {
5193        return Ok(None);
5194    };
5195    let start = entry_offset as usize;
5196    if start >= pack_end {
5197        return Ok(None);
5198    }
5199    Ok(Some(
5200        idx.next_pack_offset_after(entry_offset, pack_end as u64)? as usize,
5201    ))
5202}
5203
5204/// A non-delta object's zlib stream inside a mapped local pack.
5205pub(crate) struct PackedWholeObject {
5206    /// The pack holding the object.
5207    pub(crate) pack: Arc<PackData>,
5208    /// Start of this object's raw pack entry (type/size header).
5209    pub(crate) entry_offset: u64,
5210    /// End of this object's raw pack entry (exclusive).
5211    pub(crate) entry_end: usize,
5212    /// Offset of the object's zlib stream, just past its entry header.
5213    pub(crate) zlib_offset: usize,
5214    /// End of the pack's object data (start of the trailing checksum).
5215    pub(crate) data_end: usize,
5216    /// Object type from the entry header (never a delta type).
5217    pub(crate) kind: ObjectKind,
5218    /// Uncompressed payload size from the entry header.
5219    pub(crate) size: u64,
5220    /// Pack-index CRC32 for the raw entry when it has not been checked yet.
5221    pub(crate) index_crc: Option<u32>,
5222}
5223
5224/// How many local packs' indexes list `oid`.
5225pub(crate) fn count_local_packs_holding(objects_dir: &Path, oid: &ObjectId) -> Result<usize> {
5226    Ok(packs_holding(objects_dir, oid, PackPreference::Newest)?.len())
5227}
5228
5229/// How the local packs store an object, from [`locate_packed_whole_object`].
5230pub(crate) enum PackedEntry {
5231    /// Stored whole in some pack.
5232    Whole(PackedWholeObject),
5233    /// Present in a pack, but not usable whole: stored as a delta, with an unparsable entry
5234    /// header, or failing the pack index CRC when that is checked.
5235    NotWhole,
5236    /// In no local pack.
5237    Absent,
5238}
5239
5240/// Locate `oid` stored whole (not as a delta) in a local pack, without inflating it.
5241///
5242/// Costs one index lookup and entry-header parse per pack holding the object. Packs that
5243/// hold it only as a delta, or whose entry header does not parse, are skipped so another
5244/// pack can serve it whole. With `verify_crc`, an entry whose bytes do not match the pack
5245/// index CRC (v2 indexes) is skipped too, so a corrupt copy is never streamed and a
5246/// redundant intact copy, or the buffered read path, serves the object instead.
5247///
5248/// # Errors
5249///
5250/// Returns [`Error::Io`] when pack indexes or pack files cannot be read.
5251pub(crate) fn locate_packed_whole_object(
5252    objects_dir: &Path,
5253    oid: &ObjectId,
5254    verify_crc: bool,
5255) -> Result<PackedEntry> {
5256    let indexes = read_local_pack_indexes_cached(objects_dir)?;
5257    let mut found = PackedEntry::Absent;
5258    for idx in &indexes {
5259        let Some(entry_offset) = idx.find_offset(oid)? else {
5260            continue;
5261        };
5262        found = PackedEntry::NotWhole;
5263        let pack = read_pack_bytes_cached(&idx.pack_path)?;
5264        let Some(data_end) = pack.len().checked_sub(idx.hash_bytes()) else {
5265            continue;
5266        };
5267        let Ok(mut pos) = usize::try_from(entry_offset) else {
5268            continue;
5269        };
5270        if pos >= data_end {
5271            continue;
5272        }
5273        let Ok((packed_type, size)) = parse_pack_object_header(&pack[..data_end], &mut pos) else {
5274            continue;
5275        };
5276        let Ok(kind) = packed_type_to_kind(packed_type) else {
5277            continue;
5278        };
5279        if verify_crc && !packed_entry_matches_index_crc(idx, &pack, entry_offset)? {
5280            continue;
5281        }
5282        let entry_end = pack_entry_raw_end(idx, &pack, entry_offset)?.unwrap_or(data_end);
5283        let index_crc = if verify_crc {
5284            None
5285        } else {
5286            idx.crc32_for_pack_offset(entry_offset).ok().flatten()
5287        };
5288        return Ok(PackedEntry::Whole(PackedWholeObject {
5289            pack,
5290            entry_offset,
5291            entry_end,
5292            zlib_offset: pos,
5293            data_end,
5294            kind,
5295            size,
5296            index_crc,
5297        }));
5298    }
5299    Ok(found)
5300}
5301
5302/// Whether the raw pack entry at `entry_offset` matches the CRC32 its pack index records.
5303///
5304/// Version 1 indexes record no CRC; their entries are accepted (the zlib checksum still
5305/// guards the payload).
5306///
5307/// A large entry is hashed with a bounded window of the pack mapping resident (see
5308/// [`VERIFY_RESIDENT_PACK_BYTES`]), so checking a big blob before streaming it does not pull
5309/// its whole compressed size into the resident set.
5310pub(crate) fn packed_entry_matches_index_crc(
5311    idx: &PackIndex,
5312    pack: &PackData,
5313    entry_offset: u64,
5314) -> Result<bool> {
5315    let Some(recorded) = idx.crc32_for_pack_offset(entry_offset)? else {
5316        return Ok(true);
5317    };
5318    let Some(end) = pack_entry_raw_end(idx, pack, entry_offset)? else {
5319        return Ok(false);
5320    };
5321    let start = usize::try_from(entry_offset)
5322        .map_err(|_| Error::CorruptObject(format!("pack offset {entry_offset} overflows")))?;
5323    if start >= end {
5324        return Ok(false);
5325    }
5326    let budget = ResidentBudget::new(pack, VERIFY_RESIDENT_PACK_BYTES);
5327    Ok(budget.crc32(&pack[start..end]) == recorded)
5328}
5329
5330/// Mapped pack bytes [`packed_entry_matches_index_crc`] lets stay resident while it hashes
5331/// one entry before dropping the mapping's pages.
5332const VERIFY_RESIDENT_PACK_BYTES: usize = 4 * 1024 * 1024;
5333
5334/// When `oid` is stored as a full (non-delta) object in a local pack, return its verbatim packed
5335/// bytes: the varint type+size header followed by the zlib stream.
5336///
5337/// The header of a non-delta pack entry is position-independent, so the returned slice can be
5338/// copied into a new pack unchanged — skipping both inflate and deflate. This is Git
5339/// pack-objects' object reuse for objects that stay full in the output pack.
5340///
5341/// # Parameters
5342/// - `objects_dir` — the repository's `objects/` directory.
5343/// - `oid` — the object to look up.
5344///
5345/// Returns `None` when the object is loose only, stored as a delta, or the repository uses a
5346/// hash width other than SHA-1.
5347///
5348/// # Errors
5349///
5350/// Returns [`Error::Io`] when pack files cannot be read; a malformed candidate entry is skipped
5351/// rather than reported so another pack (or the recompression path) can serve the object.
5352pub fn packed_full_object_slice(objects_dir: &Path, oid: &ObjectId) -> Result<Option<Vec<u8>>> {
5353    Ok(packed_full_object_range(objects_dir, oid)?
5354        .map(|(bytes, start, end)| bytes[start..end].to_vec()))
5355}
5356
5357/// [`packed_full_object_slice`] without the copy: the pack's bytes and the entry's range in
5358/// them, so a pack writer can copy straight from the mapping.
5359pub(crate) fn packed_full_object_range(
5360    objects_dir: &Path,
5361    oid: &ObjectId,
5362) -> Result<Option<(Arc<PackData>, usize, usize)>> {
5363    for (idx, entry_offset) in packs_holding(objects_dir, oid, PackPreference::Newest)? {
5364        let pack_bytes = read_pack_bytes_cached(&idx.pack_path)?;
5365        let start = entry_offset as usize;
5366        let mut p = start;
5367        let Ok((packed_type, _size)) = parse_pack_object_header(&pack_bytes, &mut p) else {
5368            continue;
5369        };
5370        if matches!(packed_type, PackedType::OfsDelta | PackedType::RefDelta) {
5371            // The same OID may be a full object in another pack; keep scanning.
5372            continue;
5373        }
5374        let Some(end) = pack_entry_raw_end(&idx, &pack_bytes, entry_offset)? else {
5375            continue;
5376        };
5377        if end <= start {
5378            continue;
5379        }
5380        // Git `check_pack_crc`: verbatim reuse copies bytes unparsed, so guard with the pack
5381        // index's CRC32 when v2 records one. v1 indexes rely on the zlib trailer; inflating
5382        // here only to verify the oid would retain the whole payload in anon RSS (issue #2287).
5383        if !packed_entry_matches_index_crc(&idx, &pack_bytes, entry_offset)? {
5384            continue;
5385        }
5386        return Ok(Some((pack_bytes, start, end)));
5387    }
5388    Ok(None)
5389}
5390
5391pub(crate) fn parse_pack_object_header(bytes: &[u8], pos: &mut usize) -> Result<(PackedType, u64)> {
5392    let first = *bytes.get(*pos).ok_or_else(|| {
5393        Error::CorruptObject("unexpected end of pack header while decoding object".to_owned())
5394    })?;
5395    *pos += 1;
5396
5397    let type_code = (first >> 4) & 0x7;
5398    let mut size = (first & 0x0f) as u64;
5399    let mut shift = 4u32;
5400    let mut c = first;
5401    while (c & 0x80) != 0 {
5402        c = *bytes.get(*pos).ok_or_else(|| {
5403            Error::CorruptObject("unexpected end of variable size header".to_owned())
5404        })?;
5405        *pos += 1;
5406        size |= ((c & 0x7f) as u64) << shift;
5407        shift += 7;
5408    }
5409
5410    let packed_type = match type_code {
5411        1 => PackedType::Commit,
5412        2 => PackedType::Tree,
5413        3 => PackedType::Blob,
5414        4 => PackedType::Tag,
5415        6 => PackedType::OfsDelta,
5416        7 => PackedType::RefDelta,
5417        _ => {
5418            return Err(Error::CorruptObject(format!(
5419                "unsupported packed object type {}",
5420                type_code
5421            )))
5422        }
5423    };
5424    Ok((packed_type, size))
5425}
5426
5427/// Dependency of a packed delta object at `object_offset` within `pack_bytes`.
5428#[derive(Debug, Clone, Copy)]
5429pub enum PackedDeltaDependency {
5430    /// OFS_DELTA: base object offset within the same pack.
5431    OfsBase {
5432        /// Pack offset of the base object.
5433        base_offset: u64,
5434    },
5435    /// REF_DELTA: base object id (may live in another pack).
5436    RefBase {
5437        /// OID of the delta base.
5438        base_oid: ObjectId,
5439    },
5440}
5441
5442/// If the object at `object_offset` is a delta, return how it refers to its base.
5443///
5444/// `hash_bytes` is the ref-delta base OID width in this pack (`20` for SHA-1, `32` for SHA-256).
5445pub fn read_packed_delta_dependency(
5446    pack_bytes: &[u8],
5447    object_offset: u64,
5448    hash_bytes: usize,
5449) -> Result<Option<PackedDeltaDependency>> {
5450    let mut pos = object_offset as usize;
5451    let (ty, _) = parse_pack_object_header(pack_bytes, &mut pos)?;
5452    match ty {
5453        PackedType::OfsDelta => {
5454            let base = parse_ofs_delta_base(pack_bytes, &mut pos, object_offset)?;
5455            Ok(Some(PackedDeltaDependency::OfsBase { base_offset: base }))
5456        }
5457        PackedType::RefDelta => {
5458            if pos + hash_bytes > pack_bytes.len() {
5459                return Err(Error::CorruptObject("truncated ref-delta base oid".into()));
5460            }
5461            let base_oid = ObjectId::from_bytes(&pack_bytes[pos..pos + hash_bytes])?;
5462            Ok(Some(PackedDeltaDependency::RefBase { base_oid }))
5463        }
5464        _ => Ok(None),
5465    }
5466}
5467
5468pub(crate) fn parse_ofs_delta_base(bytes: &[u8], pos: &mut usize, this_offset: u64) -> Result<u64> {
5469    let mut c = *bytes
5470        .get(*pos)
5471        .ok_or_else(|| Error::CorruptObject("truncated ofs-delta header".to_owned()))?;
5472    *pos += 1;
5473    let mut value = (c & 0x7f) as u64;
5474    while (c & 0x80) != 0 {
5475        c = *bytes
5476            .get(*pos)
5477            .ok_or_else(|| Error::CorruptObject("truncated ofs-delta header".to_owned()))?;
5478        *pos += 1;
5479        value = ((value + 1) << 7) | (c & 0x7f) as u64;
5480    }
5481    this_offset
5482        .checked_sub(value)
5483        .ok_or_else(|| Error::CorruptObject("invalid ofs-delta base offset".to_owned()))
5484}
5485
5486/// Advance `pos` past one packed object (including zlib payload).
5487///
5488/// `object_start_offset` is the byte offset of this object within the pack file
5489/// (used for `OFS_DELTA` base resolution).
5490/// Raw bytes of one packed object (header + zlib payload) starting at `object_start_offset`.
5491///
5492/// `hash_bytes` is the ref-delta base OID width in this pack (`20` for SHA-1, `32` for SHA-256).
5493pub fn slice_one_pack_object(
5494    bytes: &[u8],
5495    object_start_offset: u64,
5496    hash_bytes: usize,
5497) -> Result<&[u8]> {
5498    let start = object_start_offset as usize;
5499    let mut pos = start;
5500    skip_one_pack_object(bytes, &mut pos, object_start_offset, hash_bytes)?;
5501    Ok(&bytes[start..pos])
5502}
5503
5504/// `skip_one_pack_object` function.
5505pub fn skip_one_pack_object(
5506    bytes: &[u8],
5507    pos: &mut usize,
5508    object_start_offset: u64,
5509    hash_bytes: usize,
5510) -> Result<()> {
5511    let (packed_type, size) = parse_pack_object_header(bytes, pos)?;
5512    let mut scratch = ZlibInflateScratch::default();
5513    match packed_type {
5514        PackedType::Commit | PackedType::Tree | PackedType::Blob | PackedType::Tag => {
5515            scratch.skip_zlib_stream(bytes, pos, size)?;
5516        }
5517        PackedType::RefDelta => {
5518            if *pos + hash_bytes > bytes.len() {
5519                return Err(Error::CorruptObject("truncated ref-delta base oid".into()));
5520            }
5521            *pos += hash_bytes;
5522            scratch.skip_zlib_stream(bytes, pos, size)?;
5523        }
5524        PackedType::OfsDelta => {
5525            let _base_off = parse_ofs_delta_base(bytes, pos, object_start_offset)?;
5526            scratch.skip_zlib_stream(bytes, pos, size)?;
5527        }
5528    }
5529    Ok(())
5530}
5531
5532/// Read all object IDs from a `.idx` file.
5533pub fn read_idx_object_ids(idx_path: &Path) -> Result<Vec<ObjectId>> {
5534    let index = read_pack_index(idx_path)?;
5535    let mut out = Vec::new();
5536    let hash_bytes = index.hash_bytes();
5537    for e in index.iter() {
5538        if e.oid().len() == hash_bytes {
5539            out.push(ObjectId::from_bytes(e.oid())?);
5540        }
5541    }
5542    Ok(out)
5543}
5544
5545mod pack_cache_test_sync {
5546    use std::cell::Cell;
5547    use std::sync::Mutex;
5548    use std::sync::MutexGuard;
5549
5550    /// Process-global pack cache test lock state (guards real synchronization state).
5551    struct PackCacheTestCoordinator {
5552        top_level_guards: u32,
5553    }
5554
5555    // hygiene: pack cache test synchronization (cfg(test) only)
5556    static COORD: Mutex<PackCacheTestCoordinator> = Mutex::new(PackCacheTestCoordinator {
5557        top_level_guards: 0,
5558    });
5559
5560    // hygiene: pack cache test depth guard (cfg(test) only)
5561    thread_local! {
5562        static DEPTH: Cell<u32> = const { Cell::new(0) };
5563    }
5564
5565    /// Serializes tests that share the process-global pack cache (including `clear_pack_cache`).
5566    pub struct PackCacheTestGuard {
5567        _inner: Option<MutexGuard<'static, PackCacheTestCoordinator>>,
5568    }
5569
5570    pub fn acquire() -> PackCacheTestGuard {
5571        let depth = DEPTH.with(|d| {
5572            let n = d.get();
5573            d.set(n + 1);
5574            n
5575        });
5576        PackCacheTestGuard {
5577            _inner: if depth == 0 {
5578                let mut g = COORD.lock().unwrap_or_else(|e| e.into_inner());
5579                assert_eq!(
5580                    g.top_level_guards, 0,
5581                    "pack cache test coordinator already held"
5582                );
5583                g.top_level_guards = 1;
5584                Some(g)
5585            } else {
5586                None
5587            },
5588        }
5589    }
5590
5591    impl Drop for PackCacheTestGuard {
5592        fn drop(&mut self) {
5593            DEPTH.with(|d| {
5594                let n = d.get();
5595                assert!(n > 0, "pack cache test guard depth underflow");
5596                d.set(n - 1);
5597                if n == 1 {
5598                    if let Some(mut g) = self._inner.take() {
5599                        assert_eq!(g.top_level_guards, 1);
5600                        g.top_level_guards = 0;
5601                    }
5602                }
5603            });
5604        }
5605    }
5606}
5607
5608#[cfg(test)]
5609pub(crate) fn pack_cache_test_guard() -> pack_cache_test_sync::PackCacheTestGuard {
5610    pack_cache_test_sync::acquire()
5611}
5612
5613/// Serializes tests that mutate the process-global pack cache (integration tests).
5614pub fn test_pack_cache_guard() -> pack_cache_test_sync::PackCacheTestGuard {
5615    pack_cache_test_sync::acquire()
5616}
5617
5618#[cfg(test)]
5619mod tests {
5620    use super::*;
5621    use crate::delta_encode::encode_lcp_delta;
5622    use crate::odb::Odb;
5623    use crate::pack_map::PackData;
5624    use crate::transfer::{build_pack, PackBuildOptions};
5625    use flate2::write::ZlibEncoder;
5626    use flate2::Compression;
5627    use std::io::Write;
5628    use std::process::Command;
5629
5630    fn git_try(dir: &std::path::Path, args: &[&str]) -> bool {
5631        Command::new("git") // hygiene: git fixture comparison in cfg(test) module // hygiene: git fixture comparison in cfg(test) module
5632            .current_dir(dir)
5633            .args(args)
5634            .env("GIT_CONFIG_GLOBAL", "/dev/null")
5635            .env("GIT_CONFIG_SYSTEM", "/dev/null")
5636            .output()
5637            .map(|o| o.status.success())
5638            .unwrap_or(false)
5639    }
5640
5641    fn git(dir: &std::path::Path, args: &[&str]) {
5642        let out = Command::new("git") // hygiene: git fixture comparison in cfg(test) module
5643            .current_dir(dir)
5644            .args(args)
5645            .env("GIT_AUTHOR_NAME", "T")
5646            .env("GIT_AUTHOR_EMAIL", "t@example.com")
5647            .env("GIT_COMMITTER_NAME", "T")
5648            .env("GIT_COMMITTER_EMAIL", "t@example.com")
5649            .env("GIT_AUTHOR_DATE", "2005-04-07T22:13:13 +0200")
5650            .env("GIT_COMMITTER_DATE", "2005-04-07T22:13:13 +0200")
5651            .env("GIT_CONFIG_GLOBAL", "/dev/null")
5652            .env("GIT_CONFIG_SYSTEM", "/dev/null")
5653            .output()
5654            .expect("git");
5655        assert!(
5656            out.status.success(),
5657            "git {:?}: {}",
5658            args,
5659            String::from_utf8_lossy(&out.stderr)
5660        );
5661    }
5662
5663    fn append_pack_object_header(buf: &mut Vec<u8>, type_code: u8, payload_len: usize) {
5664        let mut size = payload_len;
5665        let first = ((type_code & 0x7) << 4) | (size & 0x0f) as u8;
5666        size >>= 4;
5667        if size > 0 {
5668            buf.push(first | 0x80);
5669            while size > 0 {
5670                let b = (size & 0x7f) as u8;
5671                size >>= 7;
5672                buf.push(if size > 0 { b | 0x80 } else { b });
5673            }
5674        } else {
5675            buf.push(first);
5676        }
5677    }
5678
5679    fn zlib_pack(payload: &[u8]) -> Vec<u8> {
5680        let mut enc = ZlibEncoder::new(Vec::new(), Compression::default());
5681        enc.write_all(payload).unwrap();
5682        enc.finish().unwrap()
5683    }
5684
5685    fn append_sha1_pack_trailer(buf: &mut Vec<u8>) {
5686        buf.extend_from_slice(HashAlgo::Sha1.checksum_digest(&*buf).as_slice());
5687    }
5688
5689    fn append_ref_delta(buf: &mut Vec<u8>, base_oid: &ObjectId, delta: &[u8]) {
5690        let compressed = zlib_pack(delta);
5691        append_pack_object_header(buf, 7, delta.len());
5692        buf.extend_from_slice(base_oid.as_bytes());
5693        buf.extend_from_slice(&compressed);
5694    }
5695
5696    fn append_ofs_delta(buf: &mut Vec<u8>, base_offset: u64, delta: &[u8]) -> u64 {
5697        let object_start = buf.len() as u64;
5698        let compressed = zlib_pack(delta);
5699        append_pack_object_header(buf, 6, delta.len());
5700        let mut ofs = object_start - base_offset;
5701        while ofs >= 0x80 {
5702            buf.push((ofs as u8 & 0x7f) | 0x80);
5703            ofs >>= 7;
5704        }
5705        buf.push(ofs as u8);
5706        buf.extend_from_slice(&compressed);
5707        object_start
5708    }
5709
5710    fn append_whole_blob(buf: &mut Vec<u8>, data: &[u8]) -> u64 {
5711        let off = buf.len() as u64;
5712        let compressed = zlib_pack(data);
5713        append_pack_object_header(buf, 3, data.len());
5714        buf.extend_from_slice(&compressed);
5715        off
5716    }
5717
5718    fn append_corrupt_whole_blob(buf: &mut Vec<u8>) -> u64 {
5719        let off = buf.len() as u64;
5720        append_pack_object_header(buf, 3, 8);
5721        buf.extend_from_slice(&[0xff, 0xfe, 0xfd]);
5722        off
5723    }
5724
5725    fn write_v2_idx_for_test(
5726        idx_path: &Path,
5727        pack_path: &Path,
5728        entries: &[(ObjectId, u64)],
5729    ) -> Result<()> {
5730        let pack_bytes = std::fs::read(pack_path)?;
5731        let mut with_crc = Vec::with_capacity(entries.len());
5732        for (oid, off) in entries {
5733            let start = *off as usize;
5734            let mut end = start;
5735            skip_one_pack_object(&pack_bytes, &mut end, *off, 20)
5736                .map_err(|e| Error::CorruptObject(format!("idx crc walk: {e}")))?;
5737            with_crc.push((*oid, *off, crc32fast::hash(&pack_bytes[start..end])));
5738        }
5739        write_v2_pack_index(idx_path, pack_path, &with_crc, 20)
5740    }
5741
5742    fn single_blob_pack(data: &[u8]) -> (Vec<u8>, ObjectId, u64) {
5743        let odb = Odb::new(tempfile::tempdir().expect("tempdir").path());
5744        let oid = odb.hash(ObjectKind::Blob, data).expect("hash");
5745        let mut pack = Vec::new();
5746        pack.extend_from_slice(b"PACK");
5747        pack.extend_from_slice(&2u32.to_be_bytes());
5748        pack.extend_from_slice(&1u32.to_be_bytes());
5749        let off = append_whole_blob(&mut pack, data);
5750        append_sha1_pack_trailer(&mut pack);
5751        (pack, oid, off)
5752    }
5753
5754    #[test]
5755    fn single_local_pack_reader_batch_check_caches_deltified_entries() {
5756        let _guard = pack_cache_test_guard();
5757        clear_pack_cache();
5758        let Some((tmp, idx, tip, odb)) = build_ofs_delta_chain_pack(2) else {
5759            return;
5760        };
5761        let objects = tmp.path().join("objects");
5762        let store = crate::pack_store::PackStore::legacy_for_objects_dir(&objects);
5763        crate::pack_store::PackStore::with_context(store, || {
5764            let mut reader = SingleLocalPackReader::open(&objects)
5765                .expect("open")
5766                .expect("single pack");
5767            let info1 = reader.read_info(&tip).expect("first read");
5768            let info2 = reader.read_info(&tip).expect("cached read");
5769            assert_eq!(info1, info2);
5770            let full = read_object_from_pack(&idx, &tip).expect("full read");
5771            assert_eq!(info1.kind, full.kind);
5772            assert_eq!(info1.size, full.data.len() as u64);
5773        });
5774        let _ = odb;
5775    }
5776
5777    #[test]
5778    fn pack_read_info_cache_is_scoped_per_repository_and_pack_bytes() {
5779        let _guard = pack_cache_test_guard();
5780        clear_pack_cache();
5781        const ENTRY_OFFSET: u64 = 200;
5782        let dir_a = tempfile::tempdir().expect("tempdir a");
5783        let dir_b = tempfile::tempdir().expect("tempdir b");
5784        let objects_a = dir_a.path().join("objects");
5785        let objects_b = dir_b.path().join("objects");
5786        fs::create_dir_all(objects_a.join("pack")).expect("pack dir a");
5787        fs::create_dir_all(objects_b.join("pack")).expect("pack dir b");
5788        let pack_a = objects_a.join("pack/p-a.pack");
5789        let pack_b = objects_b.join("pack/p-b.pack");
5790        let (oid_a, _) =
5791            write_sparse_single_blob_pack(dir_a.path(), &pack_a, b"five!", ENTRY_OFFSET)
5792                .expect("pack a");
5793        let (oid_b, _) =
5794            write_sparse_single_blob_pack(dir_b.path(), &pack_b, b"1234567890", ENTRY_OFFSET)
5795                .expect("pack b");
5796        let idx_a = pack_a.with_extension("idx");
5797        let idx_b = pack_b.with_extension("idx");
5798        write_v2_idx_for_test(&idx_a, &pack_a, &[(oid_a, ENTRY_OFFSET)]).expect("idx a");
5799        write_v2_idx_for_test(&idx_b, &pack_b, &[(oid_b, ENTRY_OFFSET)]).expect("idx b");
5800        let idx_a = read_pack_index(&idx_a).expect("parse idx a");
5801        let idx_b = read_pack_index(&idx_b).expect("parse idx b");
5802
5803        let read_size = |objects: &Path, idx: &PackIndex| -> u64 {
5804            let store = crate::pack_store::PackStore::legacy_for_objects_dir(objects);
5805            crate::pack_store::PackStore::with_context(store, || {
5806                super::read_object_info_from_pack_at_offset(idx, ENTRY_OFFSET)
5807                    .expect("read_info")
5808                    .size
5809            })
5810        };
5811
5812        let size_a = read_size(&objects_a, &idx_a);
5813        let size_b = read_size(&objects_b, &idx_b);
5814        assert_eq!(size_a, 5);
5815        assert_eq!(size_b, 10);
5816        // A → B → A again: cache must not return B's size for A's pack entry.
5817        assert_eq!(read_size(&objects_a, &idx_a), 5);
5818        assert_eq!(read_size(&objects_b, &idx_b), 10);
5819        assert_eq!(read_size(&objects_a, &idx_a), 5);
5820    }
5821    /// Write a valid pack with one blob placed at `object_offset` so the on-disk file
5822    /// length exceeds the mmap owned-buffer threshold without storing huge compressed payloads.
5823    fn write_sparse_single_blob_pack(
5824        repo_root: &Path,
5825        pack_path: &Path,
5826        data: &[u8],
5827        object_offset: u64,
5828    ) -> Result<(ObjectId, u64)> {
5829        use std::fs::OpenOptions;
5830        use std::io::{Seek, SeekFrom, Write};
5831
5832        let odb = Odb::new(repo_root);
5833        let oid = odb.hash(ObjectKind::Blob, data).expect("hash");
5834        let mut file = OpenOptions::new()
5835            .create(true)
5836            .write(true)
5837            .read(true)
5838            .truncate(true)
5839            .open(pack_path)
5840            .map_err(Error::Io)?;
5841        file.write_all(b"PACK").map_err(Error::Io)?;
5842        file.write_all(&2u32.to_be_bytes()).map_err(Error::Io)?;
5843        file.write_all(&1u32.to_be_bytes()).map_err(Error::Io)?;
5844        file.seek(SeekFrom::Start(object_offset))
5845            .map_err(Error::Io)?;
5846        let mut tail = Vec::new();
5847        append_whole_blob(&mut tail, data);
5848        file.write_all(&tail).map_err(Error::Io)?;
5849        let trailer = sha1_trailer_for_open_file(&mut file)?;
5850        file.write_all(trailer.as_bytes()).map_err(Error::Io)?;
5851        file.flush().map_err(Error::Io)?;
5852        Ok((oid, object_offset))
5853    }
5854
5855    #[test]
5856    fn skip_one_pack_object_rejects_zero_size_with_zlib_payload() {
5857        let mut bytes = Vec::new();
5858        bytes.push(0x30);
5859        bytes.extend_from_slice(&zlib_pack(b"x"));
5860        let mut pos = 0usize;
5861        let err = skip_one_pack_object(&bytes, &mut pos, 0, 20).unwrap_err();
5862        assert!(matches!(err, Error::CorruptObject(_)), "got {err:?}");
5863    }
5864
5865    fn install_synthetic_pack(
5866        objects_dir: &Path,
5867        stem: &str,
5868        pack: &[u8],
5869        entries: &[(ObjectId, u64)],
5870    ) -> PackIndex {
5871        install_synthetic_pack_inner(objects_dir, stem, pack, entries, true)
5872    }
5873
5874    fn install_synthetic_pack_no_clear(
5875        objects_dir: &Path,
5876        stem: &str,
5877        pack: &[u8],
5878        entries: &[(ObjectId, u64)],
5879    ) -> PackIndex {
5880        install_synthetic_pack_inner(objects_dir, stem, pack, entries, false)
5881    }
5882
5883    fn install_synthetic_pack_inner(
5884        objects_dir: &Path,
5885        stem: &str,
5886        pack: &[u8],
5887        entries: &[(ObjectId, u64)],
5888        clear_cache: bool,
5889    ) -> PackIndex {
5890        let pack_dir = objects_dir.join("pack");
5891        std::fs::create_dir_all(&pack_dir).expect("pack dir");
5892        let pack_path = pack_dir.join(format!("{stem}.pack"));
5893        let idx_path = pack_dir.join(format!("{stem}.idx"));
5894        std::fs::write(&pack_path, pack).expect("write pack");
5895        write_v2_idx_for_test(&idx_path, &pack_path, entries).expect("write idx");
5896        if clear_cache {
5897            clear_pack_cache();
5898        }
5899        read_pack_index(&idx_path).expect("read idx")
5900    }
5901
5902    fn pack_index_for_objects(
5903        pack_path: std::path::PathBuf,
5904        objects: &[(ObjectId, u64)],
5905    ) -> PackIndex {
5906        let idx_path = pack_path.with_extension("idx");
5907        let pack_bytes = fs::read(&pack_path).expect("read pack for idx trailer");
5908        let trailer = &pack_bytes[pack_bytes.len().saturating_sub(20)..];
5909        let rows: Vec<(ObjectId, u64, u32)> = objects
5910            .iter()
5911            .map(|(oid, offset)| (*oid, *offset, 0))
5912            .collect();
5913        write_v2_pack_index_with_trailer(&idx_path, &rows, trailer, 20, None).expect("write idx");
5914        read_pack_index(&idx_path).expect("read idx")
5915    }
5916
5917    fn index_pack_in_scratch(pack: &[u8]) -> (tempfile::TempDir, PackIndex) {
5918        let dir = tempfile::tempdir().expect("tempdir");
5919        let pack_path = dir.path().join("chain.pack");
5920        std::fs::write(&pack_path, pack).expect("write pack");
5921        let out = Command::new("git") // hygiene: git fixture comparison in cfg(test) module
5922            .current_dir(dir.path())
5923            .args(["index-pack", "chain.pack"])
5924            .env("GIT_CONFIG_GLOBAL", "/dev/null")
5925            .env("GIT_CONFIG_SYSTEM", "/dev/null")
5926            .output()
5927            .expect("index-pack");
5928        assert!(
5929            out.status.success(),
5930            "git index-pack failed: {}",
5931            String::from_utf8_lossy(&out.stderr)
5932        );
5933        let idx_path = dir.path().join("chain.idx");
5934        let idx = read_pack_index(&idx_path).expect("read idx");
5935        (dir, idx)
5936    }
5937
5938    /// Repo with many prefix-preserving edits to one blob so delta chains can exceed 50.
5939    fn build_deep_delta_repo(commit_count: usize) -> Option<(tempfile::TempDir, ObjectId, Odb)> {
5940        let tmp = tempfile::tempdir().ok()?;
5941        let dir = tmp.path();
5942        if !git_try(dir, &["init", "-q", "-b", "main", "."]) {
5943            return None;
5944        }
5945        let mut body = String::new();
5946        for i in 0..200 {
5947            body.push_str(&format!("seed line {i:04}\n"));
5948        }
5949        for rev in 0..commit_count {
5950            body.push_str(&format!("edit-{rev}\n"));
5951            std::fs::write(dir.join("blob.txt"), body.as_bytes()).ok()?;
5952            git(dir, &["add", "blob.txt"]);
5953            git(dir, &["commit", "-q", "-m", &format!("c{rev}")]);
5954        }
5955        let tip_hex = Command::new("git") // hygiene: git fixture comparison in cfg(test) module
5956            .current_dir(dir)
5957            .args(["rev-parse", "HEAD"])
5958            .env("GIT_CONFIG_GLOBAL", "/dev/null")
5959            .env("GIT_CONFIG_SYSTEM", "/dev/null")
5960            .output()
5961            .ok()?;
5962        if !tip_hex.status.success() {
5963            return None;
5964        }
5965        let tip = ObjectId::from_hex(std::str::from_utf8(&tip_hex.stdout).ok()?.trim()).ok()?;
5966        let git_dir = dir.join(".git");
5967        let odb = Odb::new(&git_dir.join("objects")).with_config_git_dir(git_dir);
5968        Some((tmp, tip, odb))
5969    }
5970
5971    #[test]
5972    fn verify_packed_object_hash_sha256_detects_corruption() {
5973        use crate::hash::hash_object;
5974        use std::fs;
5975
5976        let data = b"sha256 packed blob for hash verify\n";
5977        let oid = hash_object(HashAlgo::Sha256, ObjectKind::Blob, data).expect("hash");
5978        let wrong_oid =
5979            hash_object(HashAlgo::Sha256, ObjectKind::Blob, b"different payload\n").expect("hash");
5980        let mut pack = Vec::new();
5981        pack.extend_from_slice(b"PACK");
5982        pack.extend_from_slice(&2u32.to_be_bytes());
5983        pack.extend_from_slice(&1u32.to_be_bytes());
5984        let off = append_whole_blob(&mut pack, data);
5985        pack.extend_from_slice(HashAlgo::Sha256.checksum_digest(&pack).as_slice());
5986
5987        let dir = tempfile::tempdir().expect("tempdir");
5988        let pack_path = dir.path().join("sha256-corrupt.pack");
5989        fs::write(&pack_path, &pack).expect("write pack");
5990
5991        let idx_path = pack_path.with_extension("idx");
5992        let trailer = &pack[pack.len() - 32..];
5993        write_v2_pack_index_with_trailer(&idx_path, &[(wrong_oid, off, 0)], trailer, 32, None)
5994            .expect("write idx");
5995        let idx = read_pack_index(&idx_path).expect("read idx");
5996
5997        let err = read_object_from_pack_bytes(&pack, &idx, wrong_oid.as_bytes()).unwrap_err();
5998        match err {
5999            Error::CorruptObject(msg) => {
6000                assert!(
6001                    msg.contains("hashes to"),
6002                    "expected hash mismatch message, got: {msg}"
6003                );
6004                assert!(
6005                    msg.contains(&oid.to_hex()),
6006                    "expected recomputed oid in message, got: {msg}"
6007                );
6008            }
6009            other => panic!("expected CorruptObject, got {other:?}"),
6010        }
6011    }
6012
6013    #[cfg_attr(
6014        not(feature = "full-tests"),
6015        ignore = "starts system git or is heavy; run with --features full-tests"
6016    )]
6017    #[test]
6018    fn reads_delta_chain_deeper_than_50() {
6019        let _guard = pack_cache_test_guard();
6020        let Some((_repo, tip, odb)) = build_deep_delta_repo(120) else {
6021            let _ = "SKIP: git unavailable for deep delta fixture";
6022            return;
6023        };
6024        let pack = build_pack(
6025            &odb,
6026            &[tip],
6027            &[],
6028            &PackBuildOptions {
6029                delta: true,
6030                max_depth: 120,
6031                use_ofs_delta: true,
6032                ..PackBuildOptions::default()
6033            },
6034        )
6035        .expect("build deep delta pack");
6036
6037        let (_scratch, idx) = index_pack_in_scratch(&pack);
6038        assert!(
6039            idx.len() > 1,
6040            "expected a multi-object pack, got {}",
6041            idx.len()
6042        );
6043
6044        for entry in idx.iter() {
6045            let oid = ObjectId::from_bytes(entry.oid()).expect("oid in idx");
6046            let from_pack = read_object_from_pack(&idx, &oid).expect("read packed object");
6047            let expected = odb.read(&oid).expect("loose/alt copy for oid");
6048            assert_eq!(from_pack.kind, expected.kind);
6049            assert_eq!(from_pack.data, expected.data);
6050            assert_eq!(
6051                odb.hash(from_pack.kind, &from_pack.data).expect("hash"),
6052                oid
6053            );
6054        }
6055    }
6056
6057    #[test]
6058    fn cyclic_ref_delta_chain_errors() {
6059        let _guard = pack_cache_test_guard();
6060        let odb = Odb::new(tempfile::tempdir().expect("tempdir").path());
6061        let content_a = b"aaa".as_slice();
6062        let content_b = b"bbb".as_slice();
6063        let oid_a = odb.hash(ObjectKind::Blob, content_a).expect("hash");
6064        let oid_b = odb.hash(ObjectKind::Blob, content_b).expect("hash");
6065        let delta_a = encode_lcp_delta(content_b, content_a).expect("delta a");
6066        let delta_b = encode_lcp_delta(content_a, content_b).expect("delta b");
6067
6068        let mut pack = Vec::new();
6069        pack.extend_from_slice(b"PACK");
6070        pack.extend_from_slice(&2u32.to_be_bytes());
6071        pack.extend_from_slice(&2u32.to_be_bytes());
6072        let off_a = pack.len() as u64;
6073        append_ref_delta(&mut pack, &oid_b, &delta_a);
6074        let off_b = pack.len() as u64;
6075        append_ref_delta(&mut pack, &oid_a, &delta_b);
6076        append_sha1_pack_trailer(&mut pack);
6077
6078        let dir = tempfile::tempdir().expect("tempdir");
6079        let pack_path = dir.path().join("cycle.pack");
6080        std::fs::write(&pack_path, &pack).expect("write cyclic pack");
6081        let idx = pack_index_for_objects(pack_path, &[(oid_a, off_a), (oid_b, off_b)]);
6082        let pack_bytes = std::fs::read(&idx.pack_path).expect("read pack");
6083        for err in [
6084            read_object_from_pack(&idx, &oid_a).expect_err("read_object_from_pack"),
6085            read_object_from_pack_bytes(&pack_bytes, &idx, oid_a.as_bytes())
6086                .expect_err("read_object_from_pack_bytes"),
6087        ] {
6088            assert!(
6089                matches!(err, Error::DeltaChainTooDeep { limit: 4096 }),
6090                "expected typed delta chain limit, got {err:?}"
6091            );
6092        }
6093    }
6094
6095    #[test]
6096    fn resolve_cross_pack_delta_without_index_clone() {
6097        let _guard = pack_cache_test_guard();
6098        clear_pack_cache();
6099        let odb = Odb::new(tempfile::tempdir().expect("tempdir").path());
6100        let base = b"cross-pack-base-payload".as_slice();
6101        let tip = b"cross-pack-base-payload!".as_slice();
6102        let oid_base = odb.hash(ObjectKind::Blob, base).expect("hash");
6103        let oid_tip = odb.hash(ObjectKind::Blob, tip).expect("hash");
6104        let delta = encode_lcp_delta(base, tip).expect("delta");
6105
6106        let mut pack_base = Vec::new();
6107        pack_base.extend_from_slice(b"PACK");
6108        pack_base.extend_from_slice(&2u32.to_be_bytes());
6109        pack_base.extend_from_slice(&1u32.to_be_bytes());
6110        let off_base = append_whole_blob(&mut pack_base, base);
6111        append_sha1_pack_trailer(&mut pack_base);
6112
6113        let mut pack_delta = Vec::new();
6114        pack_delta.extend_from_slice(b"PACK");
6115        pack_delta.extend_from_slice(&2u32.to_be_bytes());
6116        pack_delta.extend_from_slice(&1u32.to_be_bytes());
6117        let off_tip = pack_delta.len() as u64;
6118        append_ref_delta(&mut pack_delta, &oid_base, &delta);
6119        append_sha1_pack_trailer(&mut pack_delta);
6120
6121        let dir = tempfile::tempdir().expect("tempdir");
6122        let objects = dir.path().join("objects");
6123        let _idx_base =
6124            install_synthetic_pack(&objects, "cross-base", &pack_base, &[(oid_base, off_base)]);
6125        let idx_delta =
6126            install_synthetic_pack(&objects, "cross-delta", &pack_delta, &[(oid_tip, off_tip)]);
6127
6128        let from_delta_pack = read_object_from_pack(&idx_delta, &oid_tip).expect("delta pack read");
6129        assert_eq!(from_delta_pack.kind, ObjectKind::Blob);
6130        assert_eq!(from_delta_pack.data, tip);
6131        assert_eq!(
6132            odb.hash(from_delta_pack.kind, &from_delta_pack.data)
6133                .expect("hash"),
6134            oid_tip
6135        );
6136
6137        let from_packs = read_object_from_packs(&objects, &oid_tip).expect("packs dir read");
6138        assert_eq!(from_packs.kind, ObjectKind::Blob);
6139        assert_eq!(from_packs.data, tip);
6140        assert_eq!(
6141            odb.hash(from_packs.kind, &from_packs.data).expect("hash"),
6142            oid_tip
6143        );
6144    }
6145
6146    #[cfg_attr(
6147        not(feature = "full-tests"),
6148        ignore = "starts system git or is heavy; run with --features full-tests"
6149    )]
6150    #[test]
6151    fn read_object_from_pack_bytes_uses_in_memory_index_without_on_disk_idx() {
6152        let odb = Odb::new(tempfile::tempdir().expect("tempdir").path());
6153        let content = b"in-memory-index-blob";
6154        let oid = odb.hash(ObjectKind::Blob, content).expect("hash");
6155
6156        let mut pack = Vec::new();
6157        pack.extend_from_slice(b"PACK");
6158        pack.extend_from_slice(&2u32.to_be_bytes());
6159        pack.extend_from_slice(&1u32.to_be_bytes());
6160        let off = append_whole_blob(&mut pack, content);
6161        append_sha1_pack_trailer(&mut pack);
6162
6163        let dir = tempfile::tempdir().expect("tempdir");
6164        let pack_path = dir.path().join("mem.pack");
6165        std::fs::write(&pack_path, &pack).expect("write pack");
6166        let idx = pack_index_for_objects(pack_path, &[(oid, off)]);
6167        std::fs::remove_file(&idx.idx_path).expect("drop on-disk idx");
6168        assert!(
6169            !idx.idx_path.exists(),
6170            "fixture must not rely on an on-disk .idx"
6171        );
6172
6173        let got = read_object_from_pack_bytes(&pack, &idx, oid.as_bytes()).expect("read bytes");
6174        assert_eq!(got.kind, ObjectKind::Blob);
6175        assert_eq!(got.data, content);
6176    }
6177
6178    fn build_ofs_delta_chain_pack(
6179        depth: usize,
6180    ) -> Option<(tempfile::TempDir, PackIndex, ObjectId, Odb)> {
6181        if depth == 0 {
6182            return None;
6183        }
6184        let tmp = tempfile::tempdir().ok()?;
6185        let odb = Odb::new(&tmp.path().join("objects"));
6186        let mut content = b"ofs-delta-chain-base\n".to_vec();
6187        let mut pack = Vec::new();
6188        pack.extend_from_slice(b"PACK");
6189        pack.extend_from_slice(&2u32.to_be_bytes());
6190        pack.extend_from_slice(&(depth as u32 + 1).to_be_bytes());
6191        let mut base_off = append_whole_blob(&mut pack, &content);
6192        let mut entries = vec![(
6193            odb.hash(ObjectKind::Blob, &content).expect("hash"),
6194            base_off,
6195        )];
6196        for _ in 0..depth {
6197            let mut next = content.clone();
6198            next.push(b'!');
6199            let delta = encode_lcp_delta(&content, &next).ok()?;
6200            let off = append_ofs_delta(&mut pack, base_off, &delta);
6201            let oid = odb.hash(ObjectKind::Blob, &next).expect("hash");
6202            entries.push((oid, off));
6203            content = next;
6204            base_off = off;
6205        }
6206        append_sha1_pack_trailer(&mut pack);
6207        let tip = entries.last()?.0;
6208        let dir = tmp.path();
6209        std::fs::create_dir_all(dir.join("objects/pack")).ok()?;
6210        let pack_path = dir.join("objects/pack/chain.pack");
6211        std::fs::write(&pack_path, &pack).ok()?;
6212        write_v2_idx_for_test(&pack_path.with_extension("idx"), &pack_path, &entries).ok()?;
6213        clear_pack_cache();
6214        let idx = read_pack_index(&pack_path.with_extension("idx")).ok()?;
6215        Some((tmp, idx, tip, odb))
6216    }
6217
6218    #[test]
6219    fn bounded_pack_cache_resolves_deltas_without_changing_shared_cache() {
6220        let _guard = pack_cache_test_guard();
6221        let (_tmp, idx, tip, odb) = build_ofs_delta_chain_pack(800).expect("chain");
6222        let normal = odb.read(&tip).expect("normal");
6223        let shared_bytes = odb.pack_store().expect("store").delta_bases().bytes_used();
6224        let bounded = odb.with_bounded_pack_cache();
6225        assert!(!Arc::ptr_eq(
6226            odb.pack_store().expect("store"),
6227            bounded.pack_store().expect("store")
6228        ));
6229        bounded.with_pack_read_context(|| {
6230            let got = bounded.read(&tip).expect("bounded");
6231            assert_eq!(got.data, normal.data);
6232            let pack = read_pack_bytes_cached(&idx.pack_path).expect("pack");
6233            assert!(pack.uses_positioned_reads());
6234            let cached = bounded
6235                .pack_store()
6236                .expect("store")
6237                .delta_bases()
6238                .bytes_used();
6239            assert!(cached > 0 && cached <= crate::pack_store::BOUNDED_DELTA_BASE_BYTES);
6240        });
6241        assert_eq!(
6242            odb.pack_store().expect("store").delta_bases().bytes_used(),
6243            shared_bytes
6244        );
6245    }
6246
6247    #[test]
6248    fn ofs_delta_chain_depth_4096_resolves() {
6249        let _guard = pack_cache_test_guard();
6250        clear_pack_cache();
6251        let Some((_tmp, idx, tip, odb)) = build_ofs_delta_chain_pack(4096) else {
6252            return;
6253        };
6254        let got = read_object_from_pack(&idx, &tip).expect("4096-deep chain");
6255        let expect = odb.hash(ObjectKind::Blob, &got.data).expect("hash");
6256        assert_eq!(expect, tip);
6257    }
6258
6259    #[test]
6260    fn ofs_delta_chain_depth_4097_errors() {
6261        let _guard = pack_cache_test_guard();
6262        clear_pack_cache();
6263        let Some((_tmp, idx, tip, _odb)) = build_ofs_delta_chain_pack(4097) else {
6264            return;
6265        };
6266        let err = read_object_from_pack(&idx, &tip).expect_err("4097-deep chain");
6267        assert!(
6268            matches!(err, Error::DeltaChainTooDeep { limit: 4096 }),
6269            "got {err:?}"
6270        );
6271    }
6272
6273    #[test]
6274    fn delta_base_cache_limit_zero_disables_caching() {
6275        let _guard = pack_cache_test_guard();
6276        clear_pack_cache();
6277        let Some((_tmp, idx, tip, _odb)) = build_ofs_delta_chain_pack(12) else {
6278            return;
6279        };
6280        pack_cache::test_set_delta_base_cache_byte_limit(&idx.pack_path, 0);
6281        let _ = read_object_from_pack(&idx, &tip).expect("read chain");
6282        assert_eq!(
6283            pack_cache::test_delta_base_cache_bytes_used(&idx.pack_path),
6284            0,
6285            "limit 0 must not retain delta bases"
6286        );
6287        pack_cache::test_set_delta_base_cache_byte_limit(
6288            &idx.pack_path,
6289            pack_cache::DELTA_BASE_CACHE_DEFAULT,
6290        );
6291    }
6292
6293    #[test]
6294    fn delta_base_cache_eviction_respects_byte_limit() {
6295        let _guard = pack_cache_test_guard();
6296        clear_pack_cache();
6297        let Some((_tmp, idx, tip, _odb)) = build_ofs_delta_chain_pack(32) else {
6298            return;
6299        };
6300        let expected = read_object_from_pack(&idx, &tip)
6301            .expect("baseline read")
6302            .data;
6303        pack_cache::test_set_delta_base_cache_byte_limit(&idx.pack_path, 4096);
6304        let _ = read_object_from_pack(&idx, &tip).expect("prime cache");
6305        assert!(
6306            pack_cache::test_delta_base_cache_bytes_used(&idx.pack_path) <= 4096,
6307            "cache bytes {} exceeded cap",
6308            pack_cache::test_delta_base_cache_bytes_used(&idx.pack_path)
6309        );
6310        pack_cache::test_set_delta_base_cache_byte_limit(&idx.pack_path, 512);
6311        for round in 0..8 {
6312            let got = read_object_from_pack(&idx, &tip).expect("read after eviction");
6313            assert_eq!(got.data, expected, "round {round} must stay byte-identical");
6314        }
6315        pack_cache::test_set_delta_base_cache_byte_limit(
6316            &idx.pack_path,
6317            pack_cache::DELTA_BASE_CACHE_DEFAULT,
6318        );
6319    }
6320
6321    #[test]
6322    fn odb_read_into_caching_delta_bases_fills_cache_read_into_does_not() {
6323        let _guard = pack_cache_test_guard();
6324        clear_pack_cache();
6325        let Some((_tmp, idx, tip, odb)) = build_ofs_delta_chain_pack(12) else {
6326            return;
6327        };
6328        let expected = read_object_from_pack(&idx, &tip)
6329            .expect("baseline read")
6330            .data;
6331        clear_pack_cache();
6332        let store = crate::pack_store::PackStore::legacy_for_pack_path(&idx.pack_path);
6333        crate::pack_store::PackStore::with_context(store, || {
6334            let mut buf = Vec::new();
6335            assert_eq!(
6336                odb.read_into(&tip, &mut buf).expect("read_into"),
6337                ObjectKind::Blob
6338            );
6339            assert_eq!(buf, expected);
6340            assert_eq!(
6341                pack_cache::test_delta_base_cache_bytes_used(&idx.pack_path),
6342                0,
6343                "read_into must not retain delta bases"
6344            );
6345
6346            let mut cached = Vec::new();
6347            let kind = odb
6348                .read_into_caching_delta_bases(&tip, &mut cached)
6349                .expect("caching read");
6350            assert_eq!(kind, ObjectKind::Blob);
6351            assert_eq!(cached, expected);
6352            assert!(
6353                pack_cache::test_delta_base_cache_bytes_used(&idx.pack_path) > 0,
6354                "caching read must retain the chain's delta bases"
6355            );
6356
6357            let again = odb
6358                .read_into_caching_delta_bases(&tip, &mut cached)
6359                .expect("second caching read");
6360            assert_eq!(again, ObjectKind::Blob);
6361            assert_eq!(cached, expected);
6362        });
6363    }
6364
6365    #[test]
6366    fn repository_odb_read_into_caching_delta_bases_fills_cache_through_files_primary() {
6367        use crate::repo::{InitOptions, Repository};
6368        let _guard = pack_cache_test_guard();
6369        clear_pack_cache();
6370        let Some((tmp, idx, tip, _odb)) = build_ofs_delta_chain_pack(12) else {
6371            return;
6372        };
6373        let expected = read_object_from_pack(&idx, &tip)
6374            .expect("baseline read")
6375            .data;
6376        Repository::init(
6377            tmp.path(),
6378            &InitOptions {
6379                bare: true,
6380                ..InitOptions::default()
6381            },
6382        )
6383        .expect("init over packed objects");
6384        let repo = Repository::open(tmp.path(), &crate::environment::RepositoryOptions::empty())
6385            .expect("open");
6386        let odb = repo.odb();
6387        let store = Arc::clone(odb.pack_store().expect("pack store"));
6388        store.invalidate_packs();
6389
6390        let mut buf = Vec::new();
6391        assert_eq!(
6392            odb.read_into(&tip, &mut buf).expect("read_into"),
6393            ObjectKind::Blob
6394        );
6395        assert_eq!(buf, expected);
6396        assert_eq!(store.delta_bases().bytes_used(), 0);
6397
6398        let mut cached = Vec::new();
6399        assert_eq!(
6400            odb.read_into_caching_delta_bases(&tip, &mut cached)
6401                .expect("caching read"),
6402            ObjectKind::Blob
6403        );
6404        assert_eq!(cached, expected);
6405        assert!(store.delta_bases().bytes_used() > 0);
6406    }
6407
6408    #[test]
6409    fn odb_read_into_caching_delta_bases_falls_back_outside_packs() {
6410        let tmp = tempfile::tempdir().expect("tempdir");
6411        let odb = Odb::new(&tmp.path().join("objects"));
6412        let oid = odb
6413            .write(ObjectKind::Blob, b"loose only\n")
6414            .expect("write loose");
6415        let mut buf = vec![0u8; 4];
6416        let kind = odb
6417            .read_into_caching_delta_bases(&oid, &mut buf)
6418            .expect("loose read");
6419        assert_eq!(kind, ObjectKind::Blob);
6420        assert_eq!(buf, b"loose only\n");
6421    }
6422
6423    #[test]
6424    fn cached_delta_reads_support_overlay_alternates_and_custom_stores() {
6425        use crate::odb::store::MemoryStore;
6426        use crate::odb::OdbBuilder;
6427
6428        let tmp = tempfile::tempdir().expect("tempdir");
6429        let local_objects = tmp.path().join("local/objects");
6430        let alternate_objects = tmp.path().join("alternate/objects");
6431        let alternate = Odb::new(&alternate_objects);
6432        let alternate_oid = alternate
6433            .write(ObjectKind::Blob, b"alternate\n")
6434            .expect("alternate object");
6435        fs::create_dir_all(local_objects.join("info")).expect("info");
6436        fs::write(
6437            local_objects.join("info/alternates"),
6438            format!("{}\n", alternate_objects.display()),
6439        )
6440        .expect("alternates");
6441
6442        let odb = Odb::new(&local_objects);
6443        let mut buf = Vec::with_capacity(1024);
6444        assert_eq!(
6445            odb.read_into_caching_delta_bases(&alternate_oid, &mut buf)
6446                .expect("alternate read"),
6447            ObjectKind::Blob
6448        );
6449        assert_eq!(buf, b"alternate\n");
6450
6451        odb.enable_mem_overlay();
6452        let overlay_oid = odb
6453            .write(ObjectKind::Blob, b"overlay\n")
6454            .expect("overlay object");
6455        odb.read_into_caching_delta_bases(&overlay_oid, &mut buf)
6456            .expect("overlay read");
6457        assert_eq!(buf, b"overlay\n");
6458
6459        let custom = OdbBuilder::files(tmp.path().join("custom/objects"))
6460            .primary(Arc::new(MemoryStore::new(HashAlgo::Sha1)))
6461            .build();
6462        let custom_oid = custom
6463            .write(ObjectKind::Blob, b"custom\n")
6464            .expect("custom object");
6465        custom
6466            .read_into_caching_delta_bases(&custom_oid, &mut buf)
6467            .expect("custom read");
6468        assert_eq!(buf, b"custom\n");
6469
6470        let missing = HashAlgo::Sha1
6471            .hash_object(ObjectKind::Blob, b"missing")
6472            .expect("missing id");
6473        assert!(matches!(
6474            custom.read_into_caching_delta_bases(&missing, &mut buf),
6475            Err(Error::ObjectNotFound(_))
6476        ));
6477    }
6478
6479    #[test]
6480    fn delta_base_cache_invalidated_when_pack_bytes_reloaded() {
6481        use filetime::{set_file_mtime, FileTime};
6482        let _guard = pack_cache_test_guard();
6483        clear_pack_cache();
6484        let Some((_tmp, idx, tip, _odb)) = build_ofs_delta_chain_pack(8) else {
6485            return;
6486        };
6487        let pack_path = idx.pack_path.clone();
6488        let pack_id = pack_cache::pack_id_for(&pack_path);
6489        let mid_off = idx
6490            .iter()
6491            .nth(4)
6492            .map(|e| e.offset())
6493            .expect("mid hop")
6494            .expect("mid off");
6495        let _ = read_object_from_pack(&idx, &tip).expect("warm cache");
6496        assert!(
6497            pack_cache::test_delta_base_cached(&pack_path, pack_id, mid_off),
6498            "expected intermediate cached at offset {mid_off}"
6499        );
6500        set_file_mtime(&pack_path, FileTime::now()).expect("touch pack mtime");
6501        assert!(
6502            pack_cache::revalidate_stale_pack_bytes(&pack_path).expect("revalidate"),
6503            "expected pack bytes reload"
6504        );
6505        assert!(
6506            !pack_cache::test_delta_base_cached(&pack_path, pack_id, mid_off),
6507            "cache must drop when pack bytes reload"
6508        );
6509    }
6510
6511    #[cfg_attr(
6512        not(feature = "full-tests"),
6513        ignore = "starts system git or is heavy; run with --features full-tests"
6514    )]
6515    #[test]
6516    fn git_repack_depth_250_matches_cat_file_batch() {
6517        let _guard = pack_cache_test_guard();
6518        clear_pack_cache();
6519        let tmp = tempfile::tempdir().expect("tempdir");
6520        let dir = tmp.path();
6521        if Command::new("git") // hygiene: git fixture comparison in cfg(test) module
6522            .current_dir(dir)
6523            .args(["init", "-q"])
6524            .env("GIT_CONFIG_GLOBAL", "/dev/null")
6525            .env("GIT_CONFIG_SYSTEM", "/dev/null")
6526            .status()
6527            .is_err()
6528        {
6529            let _ = "SKIP: git unavailable";
6530            return;
6531        }
6532        for i in 0..64 {
6533            std::fs::write(dir.join(format!("f{i}.txt")), format!("payload {i}\n")).unwrap();
6534            git(dir, &["add", &format!("f{i}.txt")]);
6535            git(dir, &["commit", "-qm", &format!("c{i}")]);
6536        }
6537        git(dir, &["repack", "-adf", "--depth=250", "--window=250"]);
6538        let objects = dir.join(".git/objects");
6539        let indexes = read_local_pack_indexes_cached(&objects).expect("pack indexes");
6540        let idx = indexes
6541            .into_iter()
6542            .max_by_key(|i| i.len())
6543            .expect("repack pack");
6544        for entry in idx.iter() {
6545            let oid = ObjectId::from_bytes(entry.oid()).expect("oid");
6546            let grit_obj = read_object_from_pack(&idx, &oid).expect("grit read");
6547            let git_type = Command::new("git") // hygiene: git fixture comparison in cfg(test) module
6548                .current_dir(dir)
6549                .args(["cat-file", "-t", &oid.to_hex()])
6550                .env("GIT_CONFIG_GLOBAL", "/dev/null")
6551                .env("GIT_CONFIG_SYSTEM", "/dev/null")
6552                .output()
6553                .expect("git cat-file -t");
6554            assert!(git_type.status.success());
6555            let git_kind = ObjectKind::from_bytes(
6556                std::str::from_utf8(git_type.stdout.trim_ascii_end())
6557                    .expect("utf8 type")
6558                    .as_bytes(),
6559            )
6560            .expect("git type");
6561            assert_eq!(git_kind, grit_obj.kind);
6562            let type_arg = match grit_obj.kind {
6563                ObjectKind::Blob => "blob",
6564                ObjectKind::Tree => "tree",
6565                ObjectKind::Commit => "commit",
6566                ObjectKind::Tag => "tag",
6567            };
6568            let git_payload = Command::new("git") // hygiene: git fixture comparison in cfg(test) module
6569                .current_dir(dir)
6570                .args(["cat-file", type_arg, &oid.to_hex()])
6571                .env("GIT_CONFIG_GLOBAL", "/dev/null")
6572                .env("GIT_CONFIG_SYSTEM", "/dev/null")
6573                .output()
6574                .expect("git cat-file raw");
6575            assert!(git_payload.status.success());
6576            assert_eq!(git_payload.stdout, grit_obj.data);
6577        }
6578    }
6579
6580    #[test]
6581    fn cyclic_ref_delta_chain_across_two_packs_errors() {
6582        let _guard = pack_cache_test_guard();
6583        clear_pack_cache();
6584        let odb = Odb::new(tempfile::tempdir().expect("tempdir").path());
6585        let content_a = b"aaa".as_slice();
6586        let content_b = b"bbb".as_slice();
6587        let oid_a = odb.hash(ObjectKind::Blob, content_a).expect("hash");
6588        let oid_b = odb.hash(ObjectKind::Blob, content_b).expect("hash");
6589        let delta_a = encode_lcp_delta(content_b, content_a).expect("delta a");
6590        let delta_b = encode_lcp_delta(content_a, content_b).expect("delta b");
6591
6592        let mut pack_a = Vec::new();
6593        pack_a.extend_from_slice(b"PACK");
6594        pack_a.extend_from_slice(&2u32.to_be_bytes());
6595        pack_a.extend_from_slice(&1u32.to_be_bytes());
6596        let off_a = pack_a.len() as u64;
6597        append_ref_delta(&mut pack_a, &oid_b, &delta_a);
6598        append_sha1_pack_trailer(&mut pack_a);
6599
6600        let mut pack_b = Vec::new();
6601        pack_b.extend_from_slice(b"PACK");
6602        pack_b.extend_from_slice(&2u32.to_be_bytes());
6603        pack_b.extend_from_slice(&1u32.to_be_bytes());
6604        let off_b = pack_b.len() as u64;
6605        append_ref_delta(&mut pack_b, &oid_a, &delta_b);
6606        append_sha1_pack_trailer(&mut pack_b);
6607
6608        let dir = tempfile::tempdir().expect("tempdir");
6609        let objects = dir.path().join("objects");
6610        let idx_b = install_synthetic_pack(&objects, "cycle-b", &pack_b, &[(oid_b, off_b)]);
6611        let idx_a = install_synthetic_pack(&objects, "cycle-a", &pack_a, &[(oid_a, off_a)]);
6612        assert_eq!(idx_a.len(), 1);
6613        assert_eq!(idx_b.len(), 1);
6614        assert!(idx_b.contains(&oid_b).expect("contains"));
6615        let _ = idx_b;
6616
6617        let err = read_object_from_pack(&idx_a, &oid_a).expect_err("cross-pack cycle");
6618        assert!(
6619            matches!(err, Error::DeltaChainTooDeep { limit: 4096 }),
6620            "expected typed delta chain limit, got {err:?}"
6621        );
6622    }
6623
6624    #[test]
6625    fn cross_pack_ref_delta_resolves_through_odb_read() {
6626        let _guard = pack_cache_test_guard();
6627        clear_pack_cache();
6628        let tmp = tempfile::tempdir().expect("tempdir");
6629        let objects = tmp.path().join("objects");
6630        let odb = Odb::new(&objects);
6631        let mut random = 17u64;
6632        let base: Vec<u8> = (0..20000)
6633            .map(|_| {
6634                random ^= random << 13;
6635                random ^= random >> 7;
6636                random ^= random << 17;
6637                random as u8
6638            })
6639            .collect();
6640        let mut target = base.clone();
6641        target.extend_from_slice(b"target suffix");
6642        let oid_base = odb.hash(ObjectKind::Blob, &base).expect("hash");
6643        let delta = encode_lcp_delta(&base, &target).expect("delta");
6644        let oid_target = odb.hash(ObjectKind::Blob, &target).expect("hash");
6645
6646        let mut pack_base = Vec::new();
6647        pack_base.extend_from_slice(b"PACK");
6648        pack_base.extend_from_slice(&2u32.to_be_bytes());
6649        pack_base.extend_from_slice(&1u32.to_be_bytes());
6650        let off_base = append_whole_blob(&mut pack_base, &base);
6651        append_sha1_pack_trailer(&mut pack_base);
6652
6653        let mut pack_tip = Vec::new();
6654        pack_tip.extend_from_slice(b"PACK");
6655        pack_tip.extend_from_slice(&2u32.to_be_bytes());
6656        pack_tip.extend_from_slice(&1u32.to_be_bytes());
6657        let off_tip = pack_tip.len() as u64;
6658        append_ref_delta(&mut pack_tip, &oid_base, &delta);
6659        append_sha1_pack_trailer(&mut pack_tip);
6660
6661        install_synthetic_pack(&objects, "base", &pack_base, &[(oid_base, off_base)]);
6662        install_synthetic_pack(&objects, "tip", &pack_tip, &[(oid_target, off_tip)]);
6663
6664        for reader in [&odb, &odb.with_bounded_pack_cache()] {
6665            let got = reader.read(&oid_target).expect("cross-pack ref-delta");
6666            assert_eq!(got.kind, ObjectKind::Blob);
6667            assert_eq!(got.data, target);
6668            assert_eq!(reader.hash(got.kind, &got.data).expect("hash"), oid_target);
6669        }
6670    }
6671
6672    #[test]
6673    fn corrupt_in_pack_base_rescued_from_loose() {
6674        let _guard = pack_cache_test_guard();
6675        let tmp = tempfile::tempdir().expect("tempdir");
6676        let objects = tmp.path().join("objects");
6677        std::fs::create_dir_all(objects.join("pack")).expect("dirs");
6678        let odb = Odb::new(&objects);
6679        let base = b"good-base-bytes";
6680        let tip = b"good-base-bytes!";
6681        let oid_base = odb.write_local(ObjectKind::Blob, base).expect("loose base");
6682        let delta = encode_lcp_delta(base, tip).expect("delta");
6683        let oid_tip = odb.hash(ObjectKind::Blob, tip).expect("hash");
6684
6685        let mut pack = Vec::new();
6686        pack.extend_from_slice(b"PACK");
6687        pack.extend_from_slice(&2u32.to_be_bytes());
6688        pack.extend_from_slice(&2u32.to_be_bytes());
6689        let off_base = append_corrupt_whole_blob(&mut pack);
6690        let off_tip = pack.len() as u64;
6691        append_ref_delta(&mut pack, &oid_base, &delta);
6692        append_sha1_pack_trailer(&mut pack);
6693
6694        let pack_path = objects.join("pack/rescue.pack");
6695        std::fs::write(&pack_path, &pack).expect("write pack");
6696        let idx = pack_index_for_objects(pack_path, &[(oid_base, off_base), (oid_tip, off_tip)]);
6697
6698        let got = read_object_from_pack(&idx, &oid_tip).expect("rescue read");
6699        assert_eq!(got.kind, ObjectKind::Blob);
6700        assert_eq!(got.data, tip);
6701    }
6702
6703    #[test]
6704    fn pack_added_after_first_lookup_found_on_miss() {
6705        let _guard = pack_cache_test_guard();
6706        clear_pack_cache();
6707        let tmp = tempfile::tempdir().expect("tempdir");
6708        let objects = tmp.path().join("objects");
6709        let pack_dir = objects.join("pack");
6710        let (pack_a, oid_a, off_a) = single_blob_pack(b"pack-a");
6711        install_synthetic_pack(&objects, "first", &pack_a, &[(oid_a, off_a)]);
6712        pack_cache::test_reset_dir_rescan_count(&pack_dir);
6713
6714        read_object_from_packs(&objects, &oid_a).expect("warm cache");
6715        assert_eq!(
6716            pack_cache::test_dir_rescan_count(&pack_dir),
6717            1,
6718            "initial lookup should scan the pack directory once"
6719        );
6720
6721        // Ensure the pack directory mtime advances past the cached stamp (second resolution).
6722        std::thread::sleep(std::time::Duration::from_secs(1));
6723
6724        let (pack_b, oid_b, off_b) = single_blob_pack(b"pack-b");
6725        install_synthetic_pack_no_clear(&objects, "second", &pack_b, &[(oid_b, off_b)]);
6726        assert_eq!(
6727            pack_cache::test_dir_rescan_count(&pack_dir),
6728            1,
6729            "adding a pack must not rescan until a miss"
6730        );
6731
6732        read_object_from_packs(&objects, &oid_b).expect("new pack via miss path");
6733        assert_eq!(
6734            pack_cache::test_dir_rescan_count(&pack_dir),
6735            2,
6736            "miss path should re-scan when pack dir mtime changed"
6737        );
6738    }
6739
6740    #[test]
6741    fn known_object_lookup_does_not_rescan() {
6742        let _guard = pack_cache_test_guard();
6743        clear_pack_cache();
6744        let tmp = tempfile::tempdir().expect("tempdir");
6745        let objects = tmp.path().join("objects");
6746        let pack_dir = objects.join("pack");
6747        pack_cache::test_reset_dir_rescan_count(&pack_dir);
6748        let (pack, oid, off) = single_blob_pack(b"cached-blob");
6749        install_synthetic_pack(&objects, "only", &pack, &[(oid, off)]);
6750
6751        read_object_from_packs(&objects, &oid).expect("prime cache");
6752        let after_first = pack_cache::test_dir_rescan_count(&pack_dir);
6753        for _ in 0..64 {
6754            read_object_from_packs(&objects, &oid).expect("repeat read");
6755        }
6756        assert_eq!(
6757            pack_cache::test_dir_rescan_count(&pack_dir),
6758            after_first,
6759            "repeated hits must not re-scan the pack directory"
6760        );
6761    }
6762
6763    #[test]
6764    fn two_repositories_concurrent_reads() {
6765        use crate::odb::Odb;
6766        use std::sync::Arc;
6767
6768        let _guard = pack_cache_test_guard();
6769        clear_pack_cache();
6770        let tmp_a = tempfile::tempdir().expect("tempdir a");
6771        let tmp_b = tempfile::tempdir().expect("tempdir b");
6772        let objects_a = tmp_a.path().join("objects");
6773        let objects_b = tmp_b.path().join("objects");
6774        let pack_a_dir = objects_a.join("pack");
6775        let pack_b_dir = objects_b.join("pack");
6776        let (pack_a, oid_a, off_a) = single_blob_pack(b"parallel-a");
6777        let (pack_b, oid_b, off_b) = single_blob_pack(b"parallel-b");
6778        install_synthetic_pack(&objects_a, "a", &pack_a, &[(oid_a, off_a)]);
6779        install_synthetic_pack(&objects_b, "b", &pack_b, &[(oid_b, off_b)]);
6780        let odb_a = Odb::new(&objects_a);
6781        let odb_b = Odb::new(&objects_b);
6782        assert!(
6783            !Arc::ptr_eq(odb_a.pack_store().unwrap(), odb_b.pack_store().unwrap()),
6784            "independent Odb opens must not share pack stores"
6785        );
6786        pack_cache::test_reset_dir_rescan_count_on_store(odb_a.pack_store().unwrap(), &pack_a_dir);
6787        pack_cache::test_reset_dir_rescan_count_on_store(odb_b.pack_store().unwrap(), &pack_b_dir);
6788
6789        let barrier = Arc::new(std::sync::Barrier::new(2));
6790        let b0 = Arc::clone(&barrier);
6791        let b1 = Arc::clone(&barrier);
6792        let odb_a_t = odb_a.clone();
6793        let odb_b_t = odb_b.clone();
6794        let id_a = oid_a;
6795        let id_b = oid_b;
6796
6797        let h1 = std::thread::spawn(move || {
6798            b0.wait();
6799            odb_a_t.read(&id_a).expect("read a");
6800        });
6801        let h2 = std::thread::spawn(move || {
6802            b1.wait();
6803            odb_b_t.read(&id_b).expect("read b");
6804        });
6805        h1.join().expect("thread a");
6806        h2.join().expect("thread b");
6807
6808        assert_eq!(
6809            pack_cache::test_dir_rescan_count_on_store(odb_a.pack_store().unwrap(), &pack_a_dir),
6810            1
6811        );
6812        assert_eq!(
6813            pack_cache::test_dir_rescan_count_on_store(odb_b.pack_store().unwrap(), &pack_b_dir),
6814            1
6815        );
6816    }
6817
6818    #[test]
6819    fn stale_cached_pack_bytes_revalidated_before_corruption_error() {
6820        let _guard = pack_cache_test_guard();
6821        clear_pack_cache();
6822        let tmp = tempfile::tempdir().expect("tempdir");
6823        let objects = tmp.path().join("objects");
6824        let (pack, oid, off) = single_blob_pack(b"revalidate-me");
6825        let idx = install_synthetic_pack(&objects, "good", &pack, &[(oid, off)]);
6826        read_object_from_pack(&idx, &oid).expect("prime pack bytes cache");
6827
6828        let good_on_disk = std::fs::read(&idx.pack_path).expect("read pack");
6829        pack_cache::test_inject_stale_pack_bytes(
6830            &idx.pack_path,
6831            PackData::from_owned(b"BAD".to_vec()),
6832        );
6833        std::fs::write(&idx.pack_path, &good_on_disk).expect("refresh on-disk pack");
6834        let got = read_object_from_pack(&idx, &oid).expect("revalidate from disk");
6835        assert_eq!(got.data, b"revalidate-me");
6836    }
6837
6838    #[test]
6839    fn replaced_pack_same_path_reread_after_invalidation() {
6840        let _guard = pack_cache_test_guard();
6841        clear_pack_cache();
6842        let tmp = tempfile::tempdir().expect("tempdir");
6843        let objects = tmp.path().join("objects");
6844        std::fs::create_dir_all(objects.join("pack")).expect("pack dir");
6845        let pack_path = objects.join("pack/repack-me.pack");
6846        let idx_path = objects.join("pack/repack-me.idx");
6847        const OBJECT_OFFSET: u64 = 9000;
6848        let (oid_a, off_a) =
6849            write_sparse_single_blob_pack(tmp.path(), &pack_path, b"before-repack", OBJECT_OFFSET)
6850                .expect("sparse pack a");
6851        assert!(
6852            std::fs::metadata(&pack_path).expect("stat").len() > 8192,
6853            "fixture pack must exceed mmap owned-buffer threshold"
6854        );
6855        write_v2_idx_for_test(&idx_path, &pack_path, &[(oid_a, off_a)]).expect("idx a");
6856        let idx = read_pack_index(&idx_path).expect("parse idx a");
6857        read_object_from_pack(&idx, &oid_a).expect("prime cache");
6858
6859        let (oid_b, off_b) = write_sparse_single_blob_pack(
6860            tmp.path(),
6861            &pack_path,
6862            b"after-repack-content",
6863            OBJECT_OFFSET,
6864        )
6865        .expect("sparse pack b");
6866        write_v2_idx_for_test(&idx_path, &pack_path, &[(oid_b, off_b)]).expect("idx b");
6867
6868        assert!(
6869            pack_cache::revalidate_stale_pack_bytes(&pack_path).expect("revalidate"),
6870            "replacement must invalidate cached bytes"
6871        );
6872        let idx = read_pack_index(&idx_path).expect("reload idx after pack swap");
6873        let got = read_object_from_pack(&idx, &oid_b).expect("read from replaced pack");
6874        assert_eq!(got.data, b"after-repack-content");
6875    }
6876
6877    #[test]
6878    fn read_object_at_large_pack_offset_via_mmap() {
6879        use std::fs::OpenOptions;
6880        use std::io::{Seek, SeekFrom, Write};
6881
6882        let _guard = pack_cache_test_guard();
6883        clear_pack_cache();
6884        let tmp = tempfile::tempdir().expect("tempdir");
6885        let objects = tmp.path().join("objects");
6886        std::fs::create_dir_all(objects.join("pack")).expect("pack dir");
6887
6888        let data = b"large-offset-blob";
6889        let odb = Odb::new(tmp.path());
6890        let oid = odb.hash(ObjectKind::Blob, data).expect("hash");
6891
6892        let pack_path = objects.join("pack/large-off.pack");
6893        let mut file = OpenOptions::new()
6894            .create(true)
6895            .write(true)
6896            .read(true)
6897            .open(&pack_path)
6898            .expect("create sparse pack");
6899        file.write_all(b"PACK").expect("sig");
6900        file.write_all(&2u32.to_be_bytes()).expect("ver");
6901        file.write_all(&1u32.to_be_bytes()).expect("count");
6902        let object_offset: u64 = 1 << 32;
6903        file.seek(SeekFrom::Start(object_offset))
6904            .expect("seek past 4GiB");
6905        let mut tail = Vec::new();
6906        append_whole_blob(&mut tail, data);
6907        file.write_all(&tail).expect("object at huge offset");
6908        let trailer = sha1_trailer_for_open_file(&mut file).expect("trailer hash");
6909        file.write_all(trailer.as_bytes()).expect("append trailer");
6910        file.flush().expect("flush");
6911        let file_len = file.metadata().expect("meta").len();
6912        drop(file);
6913        assert!(file_len > object_offset);
6914
6915        let idx_path = pack_path.with_extension("idx");
6916        write_v2_pack_index_with_trailer(
6917            &idx_path,
6918            &[(oid, object_offset, 0)],
6919            trailer.as_bytes(),
6920            20,
6921            None,
6922        )
6923        .expect("idx");
6924
6925        let idx = read_pack_index(&idx_path).expect("parse idx");
6926        assert_eq!(idx.offset_at(0).expect("off"), object_offset);
6927        let got = read_object_from_pack(&idx, &oid).expect("read at large offset");
6928        assert_eq!(got.data, data);
6929    }
6930
6931    fn synthetic_index_entries(n: u32) -> Vec<(ObjectId, u64, u32)> {
6932        (0..n)
6933            .map(|i| {
6934                let digest = HashAlgo::Sha1.checksum_digest(&i.to_be_bytes());
6935                let oid = ObjectId::from_bytes(&digest).expect("oid");
6936                let offset = if i % 7 == 0 {
6937                    (1u64 << 31) + u64::from(i)
6938                } else {
6939                    12 + u64::from(i) * 40
6940                };
6941                (oid, offset, i.wrapping_mul(2_654_435_761))
6942            })
6943            .collect()
6944    }
6945
6946    #[test]
6947    fn index_entries_sort_in_place_on_any_thread_count() {
6948        for n in [0_u32, 1, 300, 20_000] {
6949            let mut reference = synthetic_index_entries(n);
6950            reference.sort_by(|a, b| a.0.as_bytes().cmp(b.0.as_bytes()));
6951            let mut fanout_ref = [0u32; 256];
6952            for (oid, _, _) in &reference {
6953                for slot in &mut fanout_ref[usize::from(oid.as_bytes()[0])..] {
6954                    *slot += 1;
6955                }
6956            }
6957            for threads in [None, Some(1), Some(2), Some(3), Some(8)] {
6958                let mut entries = synthetic_index_entries(n);
6959                let fanout = sort_index_entries(&mut entries, threads);
6960                assert_eq!(entries, reference, "n={n} threads={threads:?}");
6961                assert_eq!(fanout, fanout_ref, "n={n} threads={threads:?}");
6962            }
6963        }
6964    }
6965
6966    #[test]
6967    fn index_written_in_place_reads_back_every_row_with_large_offsets() {
6968        let tmp = tempfile::tempdir().expect("tempdir");
6969        let idx_path = tmp.path().join("p.idx");
6970        let rows = synthetic_index_entries(20_000);
6971        let trailer = [0x5a_u8; 20];
6972        let mut entries = rows.clone();
6973        write_v2_pack_index_sorting(&idx_path, &mut entries, &trailer, 20, Some(4))
6974            .expect("write idx");
6975        let idx = read_pack_index(&idx_path).expect("read idx");
6976        assert_eq!(idx.len(), rows.len());
6977        let mut want = rows;
6978        want.sort_by(|a, b| a.0.as_bytes().cmp(b.0.as_bytes()));
6979        for (pos, (oid, offset, crc)) in want.iter().enumerate() {
6980            assert_eq!(idx.oid_at(pos), oid.as_bytes(), "row {pos}");
6981            assert_eq!(idx.offset_at(pos).expect("offset"), *offset, "row {pos}");
6982            assert_eq!(idx.crc32_at(pos).expect("crc"), *crc, "row {pos}");
6983        }
6984
6985        let mut bad = vec![(ObjectId::from_bytes(&[1; 32]).expect("oid"), 12, 0)];
6986        assert!(matches!(
6987            write_v2_pack_index_sorting(&idx_path, &mut bad, &trailer, 20, None),
6988            Err(Error::CorruptObject(_))
6989        ));
6990        assert!(matches!(
6991            write_v2_pack_index_sorting(&idx_path, &mut entries, &trailer[..19], 20, None),
6992            Err(Error::CorruptObject(_))
6993        ));
6994    }
6995
6996    /// Hash pack bytes already written to `file` (from offset 0 through current EOF) for a SHA-1 trailer.
6997    fn sha1_trailer_for_open_file(file: &mut std::fs::File) -> Result<ObjectId> {
6998        use std::io::{Read, Seek, SeekFrom};
6999        let len = file.metadata().map_err(Error::Io)?.len();
7000        file.seek(SeekFrom::Start(0)).map_err(Error::Io)?;
7001        let mut hasher = HashAlgo::Sha1.checksum_hasher();
7002        let mut buf = [0u8; 64 * 1024];
7003        let mut left = len;
7004        while left > 0 {
7005            let chunk = left.min(buf.len() as u64) as usize;
7006            let n = file.read(&mut buf[..chunk]).map_err(Error::Io)?;
7007            if n == 0 {
7008                break;
7009            }
7010            hasher.update(&buf[..n]);
7011            left -= n as u64;
7012        }
7013        let digest = hasher.finalize();
7014        ObjectId::from_bytes(&digest).map_err(|e| Error::InvalidObjectId(format!("{e}")))
7015    }
7016
7017    #[test]
7018    fn cleared_cache_sees_removed_packs_disappear() {
7019        let _guard = pack_cache_test_guard();
7020        clear_pack_cache();
7021        let tmp = tempfile::tempdir().expect("tempdir");
7022        let objects = tmp.path().join("objects");
7023        let (pack, oid, off) = single_blob_pack(b"ephemeral");
7024        install_synthetic_pack(&objects, "gone", &pack, &[(oid, off)]);
7025        read_object_from_packs(&objects, &oid).expect("warm cache");
7026
7027        let pack_path = objects.join("pack/gone.pack");
7028        let idx_path = objects.join("pack/gone.idx");
7029        std::fs::remove_file(&pack_path).expect("remove pack");
7030        std::fs::remove_file(&idx_path).expect("remove idx");
7031        clear_pack_cache();
7032
7033        let indexes = read_local_pack_indexes_cached(&objects).expect("rescan");
7034        assert!(
7035            indexes.is_empty(),
7036            "after clear, listing should not retain removed packs"
7037        );
7038    }
7039
7040    #[test]
7041    fn repack_while_sibling_odb_holds_store_reprepares_on_miss() {
7042        use crate::odb::Odb;
7043
7044        let _guard = pack_cache_test_guard();
7045        clear_pack_cache();
7046        let tmp = tempfile::tempdir().expect("tempdir");
7047        let objects = tmp.path().join("objects");
7048        let (pack_old, oid_old, off_old) = single_blob_pack(b"before");
7049        install_synthetic_pack(&objects, "old", &pack_old, &[(oid_old, off_old)]);
7050        let odb_a = Odb::new(&objects);
7051        let odb_b = Odb::new(&objects);
7052        assert!(
7053            !std::sync::Arc::ptr_eq(odb_a.pack_store().unwrap(), odb_b.pack_store().unwrap()),
7054            "sibling Odbs must have distinct pack stores"
7055        );
7056        odb_b.read(&oid_old).expect("warm b store");
7057
7058        std::fs::remove_file(objects.join("pack/old.pack")).expect("rm pack");
7059        std::fs::remove_file(objects.join("pack/old.idx")).expect("rm idx");
7060        let (pack_new, oid_new, off_new) = single_blob_pack(b"after");
7061        install_synthetic_pack(&objects, "new", &pack_new, &[(oid_new, off_new)]);
7062        filetime::set_file_mtime(objects.join("pack"), filetime::FileTime::now())
7063            .expect("bump pack dir mtime for reprepare");
7064        odb_a.invalidate_packs();
7065
7066        // B's store is unchanged by A's invalidation; a miss on the new oid rescans pack/.
7067        let got = odb_b.read(&oid_new).expect("b reprepares after miss");
7068        assert_eq!(got.data.as_slice(), b"after");
7069    }
7070
7071    #[test]
7072    fn fresh_store_does_not_trust_a_listing_left_by_another_store() {
7073        let dir = tempfile::tempdir().expect("tempdir");
7074        let dir = dir.path();
7075        git(dir, &["init", "-q"]);
7076        git(dir, &["commit", "-q", "--allow-empty", "-m", "a"]);
7077        git(dir, &["repack", "-adq"]);
7078        let objects = dir.join(".git/objects");
7079        let pack_names = |indexes: Vec<Arc<PackIndex>>| -> Vec<PathBuf> {
7080            indexes
7081                .into_iter()
7082                .map(|idx| idx.pack_path.clone())
7083                .collect()
7084        };
7085
7086        let first = Arc::new(crate::pack_store::PackStore::new(objects.clone()));
7087        let before = crate::pack_store::PackStore::with_context(first, || {
7088            read_local_pack_indexes_cached(&objects)
7089        })
7090        .expect("first listing");
7091
7092        // Another process replaces the pack.
7093        git(dir, &["commit", "-q", "--allow-empty", "-m", "b"]);
7094        git(dir, &["repack", "-adq"]);
7095
7096        let second = Arc::new(crate::pack_store::PackStore::new(objects.clone()));
7097        let after = crate::pack_store::PackStore::with_context(second, || {
7098            read_local_pack_indexes_cached(&objects)
7099        })
7100        .expect("second listing");
7101        let (before, after) = (pack_names(before), pack_names(after));
7102        assert_ne!(before, after);
7103        assert!(after.iter().all(|p| p.is_file()), "{after:?}");
7104    }
7105}
7106#[cfg(test)]
7107mod cached_lookup_tests {
7108    use super::*;
7109    use std::fs;
7110    use std::process::Command;
7111    use tempfile::TempDir;
7112
7113    fn git(dir: &Path, args: &[&str]) {
7114        let out = Command::new("git") // hygiene: git fixture comparison in cfg(test) module
7115            .current_dir(dir)
7116            .args(args)
7117            .env("GIT_CONFIG_GLOBAL", "/dev/null")
7118            .env("GIT_CONFIG_SYSTEM", "/dev/null")
7119            .env("GIT_AUTHOR_NAME", "t")
7120            .env("GIT_AUTHOR_EMAIL", "t@example.com")
7121            .env("GIT_COMMITTER_NAME", "t")
7122            .env("GIT_COMMITTER_EMAIL", "t@example.com")
7123            .output()
7124            .expect("spawn git");
7125        assert!(
7126            out.status.success(),
7127            "git {:?}: {}",
7128            args,
7129            String::from_utf8_lossy(&out.stderr)
7130        );
7131    }
7132
7133    fn repack_all(dir: &Path) {
7134        git(dir, &["repack", "-a", "-d"]);
7135    }
7136
7137    fn git_stdout(dir: &Path, args: &[&str], stdin: Option<&str>) -> String {
7138        use std::io::Write;
7139        let mut child = Command::new("git") // hygiene: git fixture comparison in cfg(test) module
7140            .current_dir(dir)
7141            .args(args)
7142            .env("GIT_CONFIG_GLOBAL", "/dev/null")
7143            .env("GIT_CONFIG_SYSTEM", "/dev/null")
7144            .stdin(std::process::Stdio::piped())
7145            .stdout(std::process::Stdio::piped())
7146            .spawn()
7147            .expect("spawn git");
7148        if let Some(input) = stdin {
7149            child
7150                .stdin
7151                .as_mut()
7152                .expect("stdin")
7153                .write_all(input.as_bytes())
7154                .expect("write stdin");
7155        }
7156        let out = child.wait_with_output().expect("git output");
7157        assert!(out.status.success(), "git {args:?}");
7158        String::from_utf8(out.stdout)
7159            .expect("utf-8")
7160            .trim()
7161            .to_owned()
7162    }
7163
7164    fn oid_of(dir: &Path, rev: &str) -> ObjectId {
7165        ObjectId::from_hex(&git_stdout(dir, &["rev-parse", rev], None)).expect("oid")
7166    }
7167
7168    #[cfg_attr(
7169        not(feature = "full-tests"),
7170        ignore = "starts system git or is heavy; run with --features full-tests"
7171    )]
7172    #[test]
7173    fn packed_commit_header_block_parses_like_the_whole_commit() {
7174        let _guard = pack_cache_test_guard();
7175        clear_pack_cache();
7176        let dir = TempDir::new().unwrap();
7177        let root = dir.path();
7178        git(root, &["init", "-q"]);
7179        fs::write(root.join("f"), "one\n").unwrap();
7180        git(root, &["add", "f"]);
7181        git(root, &["commit", "-qm", "first"]);
7182        fs::write(root.join("f"), "two\n").unwrap();
7183        let long_message = format!("second\n\n{}", "a long body line\n".repeat(200));
7184        git(root, &["commit", "-qam", &long_message]);
7185        git(root, &["tag", "-a", "-m", "annotated", "v1"]);
7186        let signature: String = (0..60)
7187            .map(|i| format!(" signature line {i:03} with some padding text\n"))
7188            .collect();
7189        let signed_body = format!(
7190            "tree {}\nparent {}\nauthor t <t@example.com> 1 +0000\n\
7191             committer t <t@example.com> 2 +0000\ngpgsig -----BEGIN-----\n{signature} \
7192             -----END-----\n\nsigned\n",
7193            oid_of(root, "HEAD^{tree}"),
7194            oid_of(root, "HEAD"),
7195        );
7196        let signed = git_stdout(
7197            root,
7198            &["hash-object", "-t", "commit", "-w", "--stdin"],
7199            Some(&signed_body),
7200        );
7201        git(root, &["update-ref", "refs/heads/signed", &signed]);
7202        git(root, &["repack", "-a", "-d", "--window=0"]);
7203        fs::write(root.join("f"), "three\n").unwrap();
7204        git(root, &["commit", "-qam", "second pack"]);
7205        git(root, &["repack", "-d", "--window=0"]);
7206        fs::write(root.join("f"), "four\n").unwrap();
7207        git(root, &["commit", "-qam", "loose"]);
7208
7209        let objects = root.join(".git/objects");
7210        let mut buf = Vec::new();
7211        for rev in ["HEAD~3", "HEAD~2", "HEAD~1"] {
7212            let oid = oid_of(root, rev);
7213            assert!(
7214                read_packed_commit_header_block_into(&objects, &oid, &mut buf).unwrap(),
7215                "{rev} is a whole packed commit"
7216            );
7217            assert!(buf.ends_with(b"\n\n"), "{rev} ends at the blank line");
7218            let whole = git_stdout(root, &["cat-file", "commit", rev], None);
7219            let whole = crate::objects::CommitRef::parse(whole.as_bytes()).unwrap();
7220            let header = crate::objects::CommitRef::parse(&buf).unwrap();
7221            assert_eq!(header.tree(), whole.tree(), "{rev}");
7222            assert_eq!(header.parents(), whole.parents(), "{rev}");
7223            assert_eq!(header.committer_raw(), whole.committer_raw(), "{rev}");
7224            assert!(header.message_bytes().is_empty(), "{rev}");
7225        }
7226        let signed = ObjectId::from_hex(&signed).unwrap();
7227        for (oid, why) in [
7228            (signed, "headers longer than the inflate budget"),
7229            (oid_of(root, "v1"), "an annotated tag"),
7230            (oid_of(root, "HEAD~1^{tree}"), "a packed tree"),
7231            (oid_of(root, "HEAD"), "a loose commit"),
7232        ] {
7233            assert!(
7234                !read_packed_commit_header_block_into(&objects, &oid, &mut buf).unwrap(),
7235                "{why}"
7236            );
7237        }
7238    }
7239
7240    fn init_repo_with_pack(dir: &Path) -> ObjectId {
7241        git(dir, &["init", "-q"]);
7242        std::fs::write(dir.join("blob.txt"), b"pack cached lookup fixture\n").unwrap();
7243        git(dir, &["add", "blob.txt"]);
7244        git(dir, &["commit", "-qm", "c"]);
7245        repack_all(dir);
7246        let hex = Command::new("git") // hygiene: git fixture comparison in cfg(test) module
7247            .current_dir(dir)
7248            .args(["rev-parse", "HEAD^{tree}"])
7249            .env("GIT_CONFIG_GLOBAL", "/dev/null")
7250            .env("GIT_CONFIG_SYSTEM", "/dev/null")
7251            .output()
7252            .unwrap();
7253        let tree_hex = String::from_utf8(hex.stdout).unwrap();
7254        ObjectId::from_hex(tree_hex.trim()).unwrap()
7255    }
7256
7257    fn pack_dir(objects: &Path) -> PathBuf {
7258        objects.join("pack")
7259    }
7260
7261    fn v2_idx_path(objects: &Path) -> PathBuf {
7262        pack_dir(objects)
7263            .read_dir()
7264            .unwrap()
7265            .filter_map(|e| e.ok())
7266            .map(|e| e.path())
7267            .find(|p| p.extension().is_some_and(|x| x == "idx"))
7268            .expect("pack idx")
7269    }
7270
7271    /// Replace the repository's v2 `.idx` with an equivalent v1-format index.
7272    fn convert_pack_idx_to_v1(objects: &Path) -> PathBuf {
7273        let idx_path = v2_idx_path(objects);
7274        let parsed = read_pack_index(&idx_path).expect("v2 idx");
7275        let mut body = Vec::new();
7276        for slot in parsed.fanout {
7277            body.extend_from_slice(&slot.to_be_bytes());
7278        }
7279        for entry in parsed.iter() {
7280            body.extend_from_slice(&(entry.offset().expect("off") as u32).to_be_bytes());
7281            body.extend_from_slice(entry.oid());
7282        }
7283        body.extend_from_slice(HashAlgo::Sha1.checksum_digest(&body).as_slice());
7284        rewrite_test_file(&idx_path, &body);
7285        clear_pack_cache();
7286        idx_path
7287    }
7288
7289    fn rewrite_test_file(path: &Path, body: &[u8]) {
7290        let _ = fs::remove_file(path);
7291        fs::write(path, body).unwrap();
7292    }
7293
7294    fn synthetic_fanout_index(entries: &[(Vec<u8>, u64)]) -> PackIndex {
7295        let mut sorted = entries.to_vec();
7296        sorted.sort_by(|a, b| a.0.cmp(&b.0));
7297        let slices: Vec<&[u8]> = sorted.iter().map(|(o, _)| o.as_slice()).collect();
7298        let fanout = compute_fanout_from_oid_slices(&slices);
7299        let mut body = Vec::new();
7300        for f in fanout {
7301            body.extend_from_slice(&f.to_be_bytes());
7302        }
7303        for (oid, off) in sorted {
7304            body.extend_from_slice(&(off as u32).to_be_bytes());
7305            body.extend_from_slice(&oid);
7306        }
7307        body.extend_from_slice(HashAlgo::Sha1.checksum_digest(&body).as_slice());
7308        parse_pack_index_bytes(Path::new("synthetic.idx"), body, false).expect("v1 idx")
7309    }
7310
7311    #[test]
7312    fn find_offset_fanout_bucket_first_last_and_miss() {
7313        let first = vec![0x05u8; 20];
7314        let mut mid = vec![0x05u8; 20];
7315        mid[19] = 1;
7316        let mut last = vec![0x05u8; 20];
7317        last[19] = 2;
7318        let miss = vec![0x06u8; 20];
7319        let idx =
7320            synthetic_fanout_index(&[(first.clone(), 12), (mid.clone(), 24), (last.clone(), 36)]);
7321
7322        let oid_first = ObjectId::from_bytes(&first).unwrap();
7323        let oid_mid = ObjectId::from_bytes(&mid).unwrap();
7324        let oid_last = ObjectId::from_bytes(&last).unwrap();
7325        let oid_miss = ObjectId::from_bytes(&miss).unwrap();
7326
7327        assert_eq!(idx.find_offset(&oid_first).expect("f"), Some(12));
7328        assert_eq!(idx.find_offset(&oid_mid).expect("m"), Some(24));
7329        assert_eq!(idx.find_offset(&oid_last).expect("l"), Some(36));
7330        assert_eq!(idx.find_offset(&oid_miss).expect("miss"), None);
7331    }
7332
7333    #[cfg_attr(
7334        not(feature = "full-tests"),
7335        ignore = "starts system git or is heavy; run with --features full-tests"
7336    )]
7337    #[test]
7338    fn cached_lookup_hit_v2_idx() {
7339        let _guard = pack_cache_test_guard();
7340        clear_pack_cache();
7341        let dir = TempDir::new().unwrap();
7342        let tree = init_repo_with_pack(dir.path());
7343        let objects = dir.path().join(".git").join("objects");
7344        let got = read_object_from_packs(&objects, &tree).expect("read tree from v2 pack");
7345        assert_eq!(got.kind, ObjectKind::Tree);
7346        // Second read must hit the in-memory cache (same result, no reparsing).
7347        let again = read_object_from_packs(&objects, &tree).expect("cached read");
7348        assert_eq!(again.data, got.data);
7349    }
7350
7351    #[cfg_attr(
7352        not(feature = "full-tests"),
7353        ignore = "starts system git or is heavy; run with --features full-tests"
7354    )]
7355    #[test]
7356    fn cached_lookup_hit_v1_idx() {
7357        let _guard = pack_cache_test_guard();
7358        clear_pack_cache();
7359        let dir = TempDir::new().unwrap();
7360        let tree = init_repo_with_pack(dir.path());
7361        let objects = dir.path().join(".git").join("objects");
7362        let v1 = convert_pack_idx_to_v1(&objects);
7363        assert!(pack_index_is_v1(&v1));
7364        let idx = read_pack_index_cached(&v1).expect("parse v1 idx");
7365        let off = idx
7366            .find_offset(&tree)
7367            .expect("fanout lookup in v1 idx")
7368            .expect("off");
7369        assert!(off > 0);
7370        let obj = read_object_from_pack(&idx, &tree).expect("read via v1 index");
7371        assert_eq!(obj.kind, ObjectKind::Tree);
7372    }
7373
7374    #[cfg_attr(
7375        not(feature = "full-tests"),
7376        ignore = "starts system git or is heavy; run with --features full-tests"
7377    )]
7378    #[test]
7379    fn cached_lookup_miss() {
7380        let _guard = pack_cache_test_guard();
7381        clear_pack_cache();
7382        let dir = TempDir::new().unwrap();
7383        init_repo_with_pack(dir.path());
7384        let objects = dir.path().join(".git").join("objects");
7385        let miss = ObjectId::from_hex("deadbeefdeadbeefdeadbeefdeadbeefdeadbeef").unwrap();
7386        assert!(read_object_from_packs(&objects, &miss).is_err());
7387        assert!(
7388            try_read_object_from_local_packs_cached(&objects, &miss, || {
7389                Err(Error::ObjectNotFound(miss.to_hex()))
7390            })
7391            .unwrap()
7392            .is_none()
7393        );
7394    }
7395
7396    #[cfg_attr(
7397        not(feature = "full-tests"),
7398        ignore = "starts system git or is heavy; run with --features full-tests"
7399    )]
7400    #[test]
7401    fn cached_lookup_loose_only() {
7402        let _guard = pack_cache_test_guard();
7403        clear_pack_cache();
7404        let dir = TempDir::new().unwrap();
7405        git(dir.path(), &["init", "-q"]);
7406        let out = Command::new("git") // hygiene: git fixture comparison in cfg(test) module
7407            .current_dir(dir.path())
7408            .args(["hash-object", "-w", "--stdin"])
7409            .env("GIT_CONFIG_GLOBAL", "/dev/null")
7410            .env("GIT_CONFIG_SYSTEM", "/dev/null")
7411            .stdin(std::process::Stdio::piped())
7412            .stdout(std::process::Stdio::piped())
7413            .spawn()
7414            .and_then(|mut c| {
7415                use std::io::Write;
7416                c.stdin
7417                    .as_mut()
7418                    .unwrap()
7419                    .write_all(b"loose-only\n")
7420                    .unwrap();
7421                c.wait_with_output()
7422            })
7423            .unwrap();
7424        assert!(out.status.success());
7425        let oid = ObjectId::from_hex(String::from_utf8(out.stdout).unwrap().trim()).unwrap();
7426        let objects = dir.path().join(".git").join("objects");
7427        assert!(try_read_object_from_local_packs_cached(&objects, &oid, || {
7428            Err(Error::ObjectNotFound(oid.to_hex()))
7429        })
7430        .unwrap()
7431        .is_none());
7432    }
7433
7434    #[cfg_attr(
7435        not(feature = "full-tests"),
7436        ignore = "starts system git or is heavy; run with --features full-tests"
7437    )]
7438    #[test]
7439    fn cached_lookup_multiple_packs() {
7440        let _guard = pack_cache_test_guard();
7441        clear_pack_cache();
7442        let dir = TempDir::new().unwrap();
7443        git(dir.path(), &["init", "-q"]);
7444        std::fs::write(dir.path().join("a.txt"), b"a").unwrap();
7445        git(dir.path(), &["add", "a.txt"]);
7446        git(dir.path(), &["commit", "-qm", "a"]);
7447        repack_all(dir.path());
7448        let oid_a = ObjectId::from_hex(
7449            String::from_utf8(
7450                Command::new("git") // hygiene: git fixture comparison in cfg(test) module // hygiene: git fixture comparison in cfg(test) module
7451                    .current_dir(dir.path())
7452                    .args(["rev-parse", "HEAD:a.txt"])
7453                    .env("GIT_CONFIG_GLOBAL", "/dev/null")
7454                    .env("GIT_CONFIG_SYSTEM", "/dev/null")
7455                    .output()
7456                    .unwrap()
7457                    .stdout,
7458            )
7459            .unwrap()
7460            .trim(),
7461        )
7462        .unwrap();
7463        std::fs::write(dir.path().join("b.txt"), b"b").unwrap();
7464        git(dir.path(), &["add", "b.txt"]);
7465        git(dir.path(), &["commit", "-qm", "b"]);
7466        git(dir.path(), &["repack", "-d"]);
7467        let oid_b = ObjectId::from_hex(
7468            String::from_utf8(
7469                Command::new("git") // hygiene: git fixture comparison in cfg(test) module // hygiene: git fixture comparison in cfg(test) module
7470                    .current_dir(dir.path())
7471                    .args(["rev-parse", "HEAD:b.txt"])
7472                    .env("GIT_CONFIG_GLOBAL", "/dev/null")
7473                    .env("GIT_CONFIG_SYSTEM", "/dev/null")
7474                    .output()
7475                    .unwrap()
7476                    .stdout,
7477            )
7478            .unwrap()
7479            .trim(),
7480        )
7481        .unwrap();
7482        let objects = dir.path().join(".git").join("objects");
7483        let pack_count = pack_dir(&objects)
7484            .read_dir()
7485            .unwrap()
7486            .filter_map(|e| e.ok())
7487            .filter(|e| e.path().extension().is_some_and(|x| x == "pack"))
7488            .count();
7489        assert!(
7490            pack_count >= 2,
7491            "expected multiple pack files, got {pack_count}"
7492        );
7493        let obj_a = read_object_from_packs(&objects, &oid_a).expect("pack a");
7494        let obj_b = read_object_from_packs(&objects, &oid_b).expect("pack b");
7495        assert_eq!(obj_a.kind, ObjectKind::Blob);
7496        assert_eq!(obj_b.kind, ObjectKind::Blob);
7497    }
7498
7499    #[cfg_attr(
7500        not(feature = "full-tests"),
7501        ignore = "starts system git or is heavy; run with --features full-tests"
7502    )]
7503    #[test]
7504    fn packed_delta_base_oid_uses_cached_index() {
7505        let _guard = pack_cache_test_guard();
7506        clear_pack_cache();
7507        let dir = TempDir::new().unwrap();
7508        init_repo_with_pack(dir.path());
7509        let objects = dir.path().join(".git").join("objects");
7510        let miss = ObjectId::from_hex("0101010101010101010101010101010101010101").unwrap();
7511        assert!(packed_delta_base_oid(&objects, &miss).unwrap().is_none());
7512    }
7513
7514    #[test]
7515    fn content_addressed_pack_bytes_skip_stat_revalidation() {
7516        let _guard = pack_cache_test_guard();
7517        clear_pack_cache();
7518        let dir = tempfile::tempdir().expect("tempdir");
7519        let pack_path = dir
7520            .path()
7521            .join("pack-1111111111111111111111111111111111111111.pack");
7522        let v1 = b"PACK\x00\x00\x00\x02\x00\x00\x00\x00";
7523        std::fs::write(&pack_path, v1).expect("write v1");
7524        let cached = read_pack_bytes_cached(&pack_path).expect("prime cache");
7525        assert_eq!(&cached[..], v1);
7526
7527        let v2 = b"PACK-replaced-by-repack";
7528        std::fs::write(&pack_path, v2).expect("overwrite on disk");
7529        let again = read_pack_bytes_cached(&pack_path).expect("cached without stat");
7530        assert_eq!(
7531            &again[..],
7532            v1,
7533            "content-addressed name pins cached bytes in-process"
7534        );
7535    }
7536
7537    #[test]
7538    fn temporary_pack_bytes_served_from_cache_until_cleared() {
7539        let _guard = pack_cache_test_guard();
7540        clear_pack_cache();
7541        let dir = tempfile::tempdir().expect("tempdir");
7542        for pack_path in [
7543            dir.path().join("tmp_pack_abc123.pack"),
7544            dir.path().join("pack-custom.pack"),
7545        ] {
7546            clear_pack_cache();
7547            let v1 = b"PACK-temp-v1";
7548            std::fs::write(&pack_path, v1).expect("write v1");
7549            let _ = read_pack_bytes_cached(&pack_path).expect("prime cache");
7550
7551            let v2 = b"PACK-temp-v2-longer-body";
7552            std::fs::write(&pack_path, v2).expect("overwrite pack");
7553            let stale = read_pack_bytes_cached(&pack_path).expect("cache hit");
7554            assert_eq!(
7555                &stale[..],
7556                v1,
7557                "{} serves cached bytes without stat on hit",
7558                pack_path.display()
7559            );
7560            clear_pack_cache();
7561            let fresh = read_pack_bytes_cached(&pack_path).expect("after clear");
7562            assert_eq!(
7563                &fresh[..],
7564                v2,
7565                "{} reloads from disk after clear",
7566                pack_path.display()
7567            );
7568        }
7569    }
7570    #[cfg_attr(
7571        not(feature = "full-tests"),
7572        ignore = "starts system git or is heavy; run with --features full-tests"
7573    )]
7574    #[test]
7575    fn system_git_repack_requires_clear_pack_cache_for_fresh_listing() {
7576        let _guard = pack_cache_test_guard();
7577        clear_pack_cache();
7578        let dir = TempDir::new().unwrap();
7579        init_repo_with_pack(dir.path());
7580        let objects = dir.path().join(".git").join("objects");
7581        read_local_pack_indexes_cached(&objects).expect("warm listing");
7582        for entry in pack_dir(&objects).read_dir().unwrap().flatten() {
7583            let path = entry.path();
7584            if path.extension().is_some_and(|e| e == "pack" || e == "idx") {
7585                fs::remove_file(path).unwrap();
7586            }
7587        }
7588        clear_pack_cache();
7589        let indexes =
7590            read_local_pack_indexes_cached(&objects).expect("list after repack-like delete");
7591        assert!(
7592            indexes.is_empty(),
7593            "callers must clear_pack_cache after removing pack files"
7594        );
7595    }
7596
7597    fn test_oid(n: u8) -> ObjectId {
7598        ObjectId::from_hex(&format!("{n:040x}")).unwrap()
7599    }
7600
7601    fn max_delta_map_chain_edges(map: &HashMap<ObjectId, ObjectId>) -> usize {
7602        let value_set: HashSet<ObjectId> = map.values().copied().collect();
7603        let tips: Vec<ObjectId> = map
7604            .keys()
7605            .copied()
7606            .filter(|k| !value_set.contains(k))
7607            .collect();
7608        let mut max = 0usize;
7609        for tip in tips {
7610            let mut edges = 0usize;
7611            let mut cur = tip;
7612            let mut seen = HashSet::new();
7613            while seen.insert(cur) {
7614                let Some(&b) = map.get(&cur) else {
7615                    break;
7616                };
7617                edges += 1;
7618                cur = b;
7619            }
7620            max = max.max(edges);
7621        }
7622        max
7623    }
7624
7625    #[test]
7626    fn apply_delta_depth_limit_hits_exact_cap_on_seventy_edge_chain() {
7627        let n = 70usize;
7628        let oids: Vec<ObjectId> = (0..n as u8).map(test_oid).collect();
7629        let mut map: HashMap<ObjectId, ObjectId> = HashMap::new();
7630        for i in 1..n {
7631            map.insert(oids[i], oids[i - 1]);
7632        }
7633        let original = map.clone();
7634        apply_delta_depth_limit(&mut map, 50);
7635        assert_eq!(max_delta_map_chain_edges(&map), 50);
7636        let preserved = map
7637            .iter()
7638            .filter(|(t, b)| original.get(t) == Some(b))
7639            .count();
7640        assert!(
7641            preserved > 40,
7642            "non-cut edges should keep original bases (preserved {preserved})"
7643        );
7644    }
7645
7646    #[test]
7647    fn delta_cycle_breaks_drops_smallest_oid_of_each_cycle_only() {
7648        let o = |n: u8| test_oid(n);
7649        // 1 <-> 2 is a 2-cycle; 5 -> 4 -> 3 -> 5 a 3-cycle with 6 hanging off it;
7650        // 8 -> 7 is a plain chain.
7651        let map = HashMap::from([
7652            (o(1), o(2)),
7653            (o(2), o(1)),
7654            (o(3), o(5)),
7655            (o(4), o(3)),
7656            (o(5), o(4)),
7657            (o(6), o(4)),
7658            (o(8), o(7)),
7659        ]);
7660        let mut keys: Vec<ObjectId> = map.keys().copied().collect();
7661        for order in [false, true] {
7662            keys.sort();
7663            if order {
7664                keys.reverse();
7665            }
7666            let mut breaks = delta_cycle_breaks(keys.iter().copied(), |oid| map.get(oid).copied());
7667            breaks.sort();
7668            assert_eq!(breaks, vec![o(1), o(3)], "independent of walk order");
7669        }
7670    }
7671
7672    #[test]
7673    fn delta_cycle_breaks_handles_self_delta_and_acyclic_maps() {
7674        let a = test_oid(9);
7675        assert_eq!(delta_cycle_breaks([a], |_| Some(a)), vec![a]);
7676        let chain: HashMap<ObjectId, ObjectId> =
7677            (1..60u8).map(|i| (test_oid(i), test_oid(i - 1))).collect();
7678        assert!(delta_cycle_breaks(chain.keys().copied(), |o| chain.get(o).copied()).is_empty());
7679    }
7680
7681    #[test]
7682    fn apply_delta_depth_limit_breaks_cycles_shorter_than_depth() {
7683        let (a, b, c, d) = (test_oid(1), test_oid(2), test_oid(3), test_oid(4));
7684        // a <-> b, plus c -> b and d -> c hanging off the cycle.
7685        let mut map = HashMap::from([(a, b), (b, a), (c, b), (d, c)]);
7686        apply_delta_depth_limit(&mut map, 50);
7687        assert_eq!(map, HashMap::from([(b, a), (c, b), (d, c)]));
7688        assert_eq!(max_delta_map_chain_edges(&map), 3);
7689    }
7690
7691    #[test]
7692    fn apply_delta_depth_limit_leaves_short_chains_untouched() {
7693        let a = test_oid(1);
7694        let b = test_oid(2);
7695        let c = test_oid(3);
7696        let mut map = HashMap::from([(b, a), (c, b)]);
7697        apply_delta_depth_limit(&mut map, 50);
7698        assert_eq!(map.get(&b), Some(&a));
7699        assert_eq!(map.get(&c), Some(&b));
7700        assert_eq!(max_delta_map_chain_edges(&map), 2);
7701    }
7702
7703    #[test]
7704    fn apply_delta_depth_limit_caps_long_chain_and_preserves_blobs() {
7705        let n = 120usize;
7706        let oids: Vec<ObjectId> = (0..n as u8).map(test_oid).collect();
7707        let mut blobs: HashMap<ObjectId, Vec<u8>> = HashMap::new();
7708        blobs.insert(oids[0], b"base".to_vec());
7709        for i in 1..n {
7710            let mut next = blobs[&oids[i - 1]].clone();
7711            next.push(i as u8);
7712            blobs.insert(oids[i], next);
7713        }
7714        let mut map: HashMap<ObjectId, ObjectId> = HashMap::new();
7715        for i in 1..n {
7716            map.insert(oids[i], oids[i - 1]);
7717        }
7718        let original = map.clone();
7719        apply_delta_depth_limit(&mut map, 50);
7720        assert_eq!(
7721            max_delta_map_chain_edges(&map),
7722            50,
7723            "long chain must cap at exactly max_depth edges"
7724        );
7725        let preserved = map
7726            .iter()
7727            .filter(|(target, base)| original.get(target) == Some(base))
7728            .count();
7729        assert!(
7730            preserved > 60,
7731            "segment links should stay on original bases (preserved {preserved} of {})",
7732            map.len()
7733        );
7734
7735        use crate::delta_encode::encode_prefix_extension_delta;
7736        use crate::unpack_objects::apply_delta;
7737
7738        for (&target, &base) in &map {
7739            let delta = encode_prefix_extension_delta(&blobs[&base], &blobs[&target]).unwrap();
7740            let got = apply_delta(&blobs[&base], &delta).unwrap();
7741            assert_eq!(got, blobs[&target]);
7742        }
7743    }
7744
7745    #[cfg_attr(
7746        not(feature = "full-tests"),
7747        ignore = "starts system git or is heavy; run with --features full-tests"
7748    )]
7749    #[test]
7750    fn v2_idx_crc_matches_crc32fast_over_entry_bytes() {
7751        let _guard = pack_cache_test_guard();
7752        clear_pack_cache();
7753        let dir = TempDir::new().unwrap();
7754        let tree = init_repo_with_pack(dir.path());
7755        let objects = dir.path().join(".git").join("objects");
7756        let idx_path = v2_idx_path(&objects);
7757        let idx = read_pack_index(&idx_path).expect("read idx");
7758        let pack_bytes = std::fs::read(&idx.pack_path).expect("read pack");
7759        for entry in idx.iter() {
7760            let crc = entry.crc32().expect("v2 idx entry crc");
7761            let off = entry.offset().expect("entry off");
7762            let start = off as usize;
7763            let end = super::pack_entry_raw_end(&idx, &pack_bytes, off)
7764                .expect("entry end")
7765                .expect("end");
7766            assert_eq!(crc32fast::hash(&pack_bytes[start..end]), crc);
7767        }
7768        let _ = tree;
7769    }
7770
7771    #[cfg_attr(
7772        not(feature = "full-tests"),
7773        ignore = "starts system git or is heavy; run with --features full-tests"
7774    )]
7775    #[test]
7776    fn packed_full_object_slice_returns_on_disk_bytes() {
7777        let _guard = pack_cache_test_guard();
7778        clear_pack_cache();
7779        let dir = TempDir::new().unwrap();
7780        let tree = init_repo_with_pack(dir.path());
7781        let objects = dir.path().join(".git").join("objects");
7782        let idx = read_pack_index(&v2_idx_path(&objects)).expect("idx");
7783        let pack_bytes = std::fs::read(&idx.pack_path).expect("pack");
7784        let off = idx.find_offset(&tree).expect("tree in pack").expect("off");
7785        let start = off as usize;
7786        let end = super::pack_entry_raw_end(&idx, &pack_bytes, off)
7787            .expect("entry end")
7788            .expect("end");
7789        let expected = &pack_bytes[start..end];
7790        let slice = packed_full_object_slice(&objects, &tree)
7791            .expect("lookup")
7792            .expect("full tree slice");
7793        assert_eq!(slice, expected);
7794    }
7795
7796    fn touch_mtime(path: &Path, when: filetime::FileTime) {
7797        filetime::set_file_mtime(path, when).expect("set mtime");
7798    }
7799
7800    #[cfg_attr(
7801        not(feature = "full-tests"),
7802        ignore = "starts system git or is heavy; run with --features full-tests"
7803    )]
7804    #[test]
7805    fn packed_full_object_slice_crc_mismatch_falls_back_to_redundant_pack() {
7806        let _guard = pack_cache_test_guard();
7807        clear_pack_cache();
7808        let dir = TempDir::new().unwrap();
7809        let tree = init_repo_with_pack(dir.path());
7810        let objects = dir.path().join(".git").join("objects");
7811        let pack_dir = pack_dir(&objects);
7812        let primary_idx = read_pack_index(&v2_idx_path(&objects)).expect("idx");
7813        let primary_pack = primary_idx.pack_path.clone();
7814        let primary_idx_path = primary_idx.idx_path.clone();
7815
7816        let redundant_pack = pack_dir.join("pack-redundant.pack");
7817        let redundant_idx = pack_dir.join("pack-redundant.idx");
7818        fs::copy(&primary_pack, &redundant_pack).expect("copy pack");
7819        fs::copy(&primary_idx_path, &redundant_idx).expect("copy idx");
7820
7821        let older = filetime::FileTime::from_unix_time(1_700_000_000, 0);
7822        let newer = filetime::FileTime::from_unix_time(1_700_000_100, 0);
7823        touch_mtime(&primary_pack, older);
7824        touch_mtime(&primary_idx_path, older);
7825        touch_mtime(&redundant_pack, newer);
7826        touch_mtime(&redundant_idx, newer);
7827        clear_pack_cache();
7828
7829        let off = primary_idx
7830            .find_offset(&tree)
7831            .expect("tree offset")
7832            .expect("off");
7833        let start = off as usize;
7834        let end = super::pack_entry_raw_end(&primary_idx, &fs::read(&primary_pack).unwrap(), off)
7835            .expect("entry end")
7836            .expect("end");
7837        let flip = start + (end - start) / 2;
7838        let mut corrupt = fs::read(&redundant_pack).expect("read redundant");
7839        corrupt[flip] ^= 0x01;
7840        rewrite_test_file(&redundant_pack, &corrupt);
7841        clear_pack_cache();
7842
7843        let recorded_crc = read_pack_index(&redundant_idx)
7844            .expect("redundant idx")
7845            .crc32_for_pack_offset(off)
7846            .expect("crc")
7847            .expect("crc value");
7848        let corrupt_slice = &corrupt[start..end];
7849        assert_ne!(
7850            crc32fast::hash(corrupt_slice),
7851            recorded_crc,
7852            "test must flip bytes without updating idx CRC"
7853        );
7854        assert!(
7855            packed_full_object_slice(&objects, &tree)
7856                .expect("lookup")
7857                .is_some(),
7858            "valid redundant pack should serve the slice after CRC rejects corrupt copy"
7859        );
7860
7861        let obj = read_object_from_packs(&objects, &tree).expect("read tree");
7862        assert_eq!(obj.kind, ObjectKind::Tree);
7863        let slice = packed_full_object_slice(&objects, &tree)
7864            .expect("lookup")
7865            .expect("slice");
7866        let good_pack = fs::read(&primary_pack).expect("primary pack");
7867        assert_eq!(&slice, &good_pack[start..end]);
7868    }
7869
7870    #[test]
7871    fn wanted_index_entries_lookups_match_a_full_scan() {
7872        let tmp = TempDir::new().unwrap();
7873        let bare = grit_test_support::git_packed_history(tmp.path(), 30);
7874        let objects = bare.join("objects");
7875        let indexes = read_local_pack_indexes_cached(&objects).unwrap();
7876        assert_eq!(indexes.len(), 1);
7877        let idx = &indexes[0];
7878        let all: Vec<(ObjectId, u64)> = idx
7879            .iter()
7880            .map(|e| (ObjectId::from_bytes(e.oid()).unwrap(), e.offset().unwrap()))
7881            .collect();
7882        assert!(all.len() > 64);
7883        let absent = ObjectId::from_hex("0000000000000000000000000000000000000001").unwrap();
7884        let sorted = |mut v: Vec<(ObjectId, u64)>| {
7885            v.sort();
7886            v
7887        };
7888
7889        let few: HashSet<ObjectId> = [all[3].0, all[all.len() - 1].0, absent].into();
7890        let looked_up = wanted_index_entries(idx, few.iter(), |oid| few.contains(oid)).unwrap();
7891        assert_eq!(sorted(looked_up), sorted(vec![all[3], all[all.len() - 1]]));
7892
7893        let many: HashSet<ObjectId> = all.iter().map(|(oid, _)| *oid).chain([absent]).collect();
7894        let scanned = wanted_index_entries(idx, many.iter(), |oid| many.contains(oid)).unwrap();
7895        assert_eq!(sorted(scanned), sorted(all));
7896
7897        let slots = locate_packed_objects(&objects, &few.iter().copied().collect()).unwrap();
7898        assert_eq!(slots.len(), 2);
7899        assert!(!slots.contains_key(&absent));
7900    }
7901}