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    /// Offset of the object's zlib stream, just past its entry header.
5209    pub(crate) zlib_offset: usize,
5210    /// End of the pack's object data (start of the trailing checksum).
5211    pub(crate) data_end: usize,
5212    /// Object type from the entry header (never a delta type).
5213    pub(crate) kind: ObjectKind,
5214    /// Uncompressed payload size from the entry header.
5215    pub(crate) size: u64,
5216}
5217
5218/// How the local packs store an object, from [`locate_packed_whole_object`].
5219pub(crate) enum PackedEntry {
5220    /// Stored whole in some pack.
5221    Whole(PackedWholeObject),
5222    /// Present in a pack, but not usable whole: stored as a delta, with an unparsable entry
5223    /// header, or failing the pack index CRC when that is checked.
5224    NotWhole,
5225    /// In no local pack.
5226    Absent,
5227}
5228
5229/// Locate `oid` stored whole (not as a delta) in a local pack, without inflating it.
5230///
5231/// Costs one index lookup and entry-header parse per pack holding the object. Packs that
5232/// hold it only as a delta, or whose entry header does not parse, are skipped so another
5233/// pack can serve it whole. With `verify_crc`, an entry whose bytes do not match the pack
5234/// index CRC (v2 indexes) is skipped too, so a corrupt copy is never streamed and a
5235/// redundant intact copy, or the buffered read path, serves the object instead.
5236///
5237/// # Errors
5238///
5239/// Returns [`Error::Io`] when pack indexes or pack files cannot be read.
5240pub(crate) fn locate_packed_whole_object(
5241    objects_dir: &Path,
5242    oid: &ObjectId,
5243    verify_crc: bool,
5244) -> Result<PackedEntry> {
5245    let indexes = read_local_pack_indexes_cached(objects_dir)?;
5246    let mut found = PackedEntry::Absent;
5247    for idx in &indexes {
5248        let Some(entry_offset) = idx.find_offset(oid)? else {
5249            continue;
5250        };
5251        found = PackedEntry::NotWhole;
5252        let pack = read_pack_bytes_cached(&idx.pack_path)?;
5253        let Some(data_end) = pack.len().checked_sub(idx.hash_bytes()) else {
5254            continue;
5255        };
5256        let Ok(mut pos) = usize::try_from(entry_offset) else {
5257            continue;
5258        };
5259        if pos >= data_end {
5260            continue;
5261        }
5262        let Ok((packed_type, size)) = parse_pack_object_header(&pack[..data_end], &mut pos) else {
5263            continue;
5264        };
5265        let Ok(kind) = packed_type_to_kind(packed_type) else {
5266            continue;
5267        };
5268        if verify_crc {
5269            if !packed_entry_matches_index_crc(idx, &pack, entry_offset)? {
5270                continue;
5271            }
5272            // The CRC pass read the whole compressed entry; leave none of it resident so a
5273            // caller streaming the object starts from a clean resident set.
5274            pack.release_resident();
5275        }
5276        return Ok(PackedEntry::Whole(PackedWholeObject {
5277            pack,
5278            zlib_offset: pos,
5279            data_end,
5280            kind,
5281            size,
5282        }));
5283    }
5284    Ok(found)
5285}
5286
5287/// Whether the raw pack entry at `entry_offset` matches the CRC32 its pack index records.
5288///
5289/// Version 1 indexes record no CRC; their entries are accepted (the zlib checksum still
5290/// guards the payload).
5291///
5292/// A large entry is hashed with a bounded window of the pack mapping resident (see
5293/// [`VERIFY_RESIDENT_PACK_BYTES`]), so checking a big blob before streaming it does not pull
5294/// its whole compressed size into the resident set.
5295pub(crate) fn packed_entry_matches_index_crc(
5296    idx: &PackIndex,
5297    pack: &PackData,
5298    entry_offset: u64,
5299) -> Result<bool> {
5300    let Some(recorded) = idx.crc32_for_pack_offset(entry_offset)? else {
5301        return Ok(true);
5302    };
5303    let Some(end) = pack_entry_raw_end(idx, pack, entry_offset)? else {
5304        return Ok(false);
5305    };
5306    let start = usize::try_from(entry_offset)
5307        .map_err(|_| Error::CorruptObject(format!("pack offset {entry_offset} overflows")))?;
5308    if start >= end {
5309        return Ok(false);
5310    }
5311    let budget = ResidentBudget::new(pack, VERIFY_RESIDENT_PACK_BYTES);
5312    Ok(budget.crc32(&pack[start..end]) == recorded)
5313}
5314
5315/// Mapped pack bytes [`packed_entry_matches_index_crc`] lets stay resident while it hashes
5316/// one entry before dropping the mapping's pages.
5317const VERIFY_RESIDENT_PACK_BYTES: usize = 4 * 1024 * 1024;
5318
5319/// When `oid` is stored as a full (non-delta) object in a local pack, return its verbatim packed
5320/// bytes: the varint type+size header followed by the zlib stream.
5321///
5322/// The header of a non-delta pack entry is position-independent, so the returned slice can be
5323/// copied into a new pack unchanged — skipping both inflate and deflate. This is Git
5324/// pack-objects' object reuse for objects that stay full in the output pack.
5325///
5326/// # Parameters
5327/// - `objects_dir` — the repository's `objects/` directory.
5328/// - `oid` — the object to look up.
5329///
5330/// Returns `None` when the object is loose only, stored as a delta, or the repository uses a
5331/// hash width other than SHA-1.
5332///
5333/// # Errors
5334///
5335/// Returns [`Error::Io`] when pack files cannot be read; a malformed candidate entry is skipped
5336/// rather than reported so another pack (or the recompression path) can serve the object.
5337pub fn packed_full_object_slice(objects_dir: &Path, oid: &ObjectId) -> Result<Option<Vec<u8>>> {
5338    Ok(packed_full_object_range(objects_dir, oid)?
5339        .map(|(bytes, start, end)| bytes[start..end].to_vec()))
5340}
5341
5342/// [`packed_full_object_slice`] without the copy: the pack's bytes and the entry's range in
5343/// them, so a pack writer can copy straight from the mapping.
5344pub(crate) fn packed_full_object_range(
5345    objects_dir: &Path,
5346    oid: &ObjectId,
5347) -> Result<Option<(Arc<PackData>, usize, usize)>> {
5348    for (idx, entry_offset) in packs_holding(objects_dir, oid, PackPreference::Newest)? {
5349        let pack_bytes = read_pack_bytes_cached(&idx.pack_path)?;
5350        let start = entry_offset as usize;
5351        let mut p = start;
5352        let Ok((packed_type, _size)) = parse_pack_object_header(&pack_bytes, &mut p) else {
5353            continue;
5354        };
5355        if matches!(packed_type, PackedType::OfsDelta | PackedType::RefDelta) {
5356            // The same OID may be a full object in another pack; keep scanning.
5357            continue;
5358        }
5359        let Some(end) = pack_entry_raw_end(&idx, &pack_bytes, entry_offset)? else {
5360            continue;
5361        };
5362        if end <= start {
5363            continue;
5364        }
5365        // Git `check_pack_crc`: verbatim reuse copies bytes unparsed, so guard with the pack
5366        // index's CRC32 when v2 records one. v1 indexes rely on the zlib trailer; inflating
5367        // here only to verify the oid would retain the whole payload in anon RSS (issue #2287).
5368        if !packed_entry_matches_index_crc(&idx, &pack_bytes, entry_offset)? {
5369            continue;
5370        }
5371        return Ok(Some((pack_bytes, start, end)));
5372    }
5373    Ok(None)
5374}
5375
5376pub(crate) fn parse_pack_object_header(bytes: &[u8], pos: &mut usize) -> Result<(PackedType, u64)> {
5377    let first = *bytes.get(*pos).ok_or_else(|| {
5378        Error::CorruptObject("unexpected end of pack header while decoding object".to_owned())
5379    })?;
5380    *pos += 1;
5381
5382    let type_code = (first >> 4) & 0x7;
5383    let mut size = (first & 0x0f) as u64;
5384    let mut shift = 4u32;
5385    let mut c = first;
5386    while (c & 0x80) != 0 {
5387        c = *bytes.get(*pos).ok_or_else(|| {
5388            Error::CorruptObject("unexpected end of variable size header".to_owned())
5389        })?;
5390        *pos += 1;
5391        size |= ((c & 0x7f) as u64) << shift;
5392        shift += 7;
5393    }
5394
5395    let packed_type = match type_code {
5396        1 => PackedType::Commit,
5397        2 => PackedType::Tree,
5398        3 => PackedType::Blob,
5399        4 => PackedType::Tag,
5400        6 => PackedType::OfsDelta,
5401        7 => PackedType::RefDelta,
5402        _ => {
5403            return Err(Error::CorruptObject(format!(
5404                "unsupported packed object type {}",
5405                type_code
5406            )))
5407        }
5408    };
5409    Ok((packed_type, size))
5410}
5411
5412/// Dependency of a packed delta object at `object_offset` within `pack_bytes`.
5413#[derive(Debug, Clone, Copy)]
5414pub enum PackedDeltaDependency {
5415    /// OFS_DELTA: base object offset within the same pack.
5416    OfsBase {
5417        /// Pack offset of the base object.
5418        base_offset: u64,
5419    },
5420    /// REF_DELTA: base object id (may live in another pack).
5421    RefBase {
5422        /// OID of the delta base.
5423        base_oid: ObjectId,
5424    },
5425}
5426
5427/// If the object at `object_offset` is a delta, return how it refers to its base.
5428///
5429/// `hash_bytes` is the ref-delta base OID width in this pack (`20` for SHA-1, `32` for SHA-256).
5430pub fn read_packed_delta_dependency(
5431    pack_bytes: &[u8],
5432    object_offset: u64,
5433    hash_bytes: usize,
5434) -> Result<Option<PackedDeltaDependency>> {
5435    let mut pos = object_offset as usize;
5436    let (ty, _) = parse_pack_object_header(pack_bytes, &mut pos)?;
5437    match ty {
5438        PackedType::OfsDelta => {
5439            let base = parse_ofs_delta_base(pack_bytes, &mut pos, object_offset)?;
5440            Ok(Some(PackedDeltaDependency::OfsBase { base_offset: base }))
5441        }
5442        PackedType::RefDelta => {
5443            if pos + hash_bytes > pack_bytes.len() {
5444                return Err(Error::CorruptObject("truncated ref-delta base oid".into()));
5445            }
5446            let base_oid = ObjectId::from_bytes(&pack_bytes[pos..pos + hash_bytes])?;
5447            Ok(Some(PackedDeltaDependency::RefBase { base_oid }))
5448        }
5449        _ => Ok(None),
5450    }
5451}
5452
5453pub(crate) fn parse_ofs_delta_base(bytes: &[u8], pos: &mut usize, this_offset: u64) -> Result<u64> {
5454    let mut c = *bytes
5455        .get(*pos)
5456        .ok_or_else(|| Error::CorruptObject("truncated ofs-delta header".to_owned()))?;
5457    *pos += 1;
5458    let mut value = (c & 0x7f) as u64;
5459    while (c & 0x80) != 0 {
5460        c = *bytes
5461            .get(*pos)
5462            .ok_or_else(|| Error::CorruptObject("truncated ofs-delta header".to_owned()))?;
5463        *pos += 1;
5464        value = ((value + 1) << 7) | (c & 0x7f) as u64;
5465    }
5466    this_offset
5467        .checked_sub(value)
5468        .ok_or_else(|| Error::CorruptObject("invalid ofs-delta base offset".to_owned()))
5469}
5470
5471/// Advance `pos` past one packed object (including zlib payload).
5472///
5473/// `object_start_offset` is the byte offset of this object within the pack file
5474/// (used for `OFS_DELTA` base resolution).
5475/// Raw bytes of one packed object (header + zlib payload) starting at `object_start_offset`.
5476///
5477/// `hash_bytes` is the ref-delta base OID width in this pack (`20` for SHA-1, `32` for SHA-256).
5478pub fn slice_one_pack_object(
5479    bytes: &[u8],
5480    object_start_offset: u64,
5481    hash_bytes: usize,
5482) -> Result<&[u8]> {
5483    let start = object_start_offset as usize;
5484    let mut pos = start;
5485    skip_one_pack_object(bytes, &mut pos, object_start_offset, hash_bytes)?;
5486    Ok(&bytes[start..pos])
5487}
5488
5489/// `skip_one_pack_object` function.
5490pub fn skip_one_pack_object(
5491    bytes: &[u8],
5492    pos: &mut usize,
5493    object_start_offset: u64,
5494    hash_bytes: usize,
5495) -> Result<()> {
5496    let (packed_type, size) = parse_pack_object_header(bytes, pos)?;
5497    let mut scratch = ZlibInflateScratch::default();
5498    match packed_type {
5499        PackedType::Commit | PackedType::Tree | PackedType::Blob | PackedType::Tag => {
5500            scratch.skip_zlib_stream(bytes, pos, size)?;
5501        }
5502        PackedType::RefDelta => {
5503            if *pos + hash_bytes > bytes.len() {
5504                return Err(Error::CorruptObject("truncated ref-delta base oid".into()));
5505            }
5506            *pos += hash_bytes;
5507            scratch.skip_zlib_stream(bytes, pos, size)?;
5508        }
5509        PackedType::OfsDelta => {
5510            let _base_off = parse_ofs_delta_base(bytes, pos, object_start_offset)?;
5511            scratch.skip_zlib_stream(bytes, pos, size)?;
5512        }
5513    }
5514    Ok(())
5515}
5516
5517/// Read all object IDs from a `.idx` file.
5518pub fn read_idx_object_ids(idx_path: &Path) -> Result<Vec<ObjectId>> {
5519    let index = read_pack_index(idx_path)?;
5520    let mut out = Vec::new();
5521    let hash_bytes = index.hash_bytes();
5522    for e in index.iter() {
5523        if e.oid().len() == hash_bytes {
5524            out.push(ObjectId::from_bytes(e.oid())?);
5525        }
5526    }
5527    Ok(out)
5528}
5529
5530mod pack_cache_test_sync {
5531    use std::cell::Cell;
5532    use std::sync::Mutex;
5533    use std::sync::MutexGuard;
5534
5535    /// Process-global pack cache test lock state (guards real synchronization state).
5536    struct PackCacheTestCoordinator {
5537        top_level_guards: u32,
5538    }
5539
5540    // hygiene: pack cache test synchronization (cfg(test) only)
5541    static COORD: Mutex<PackCacheTestCoordinator> = Mutex::new(PackCacheTestCoordinator {
5542        top_level_guards: 0,
5543    });
5544
5545    // hygiene: pack cache test depth guard (cfg(test) only)
5546    thread_local! {
5547        static DEPTH: Cell<u32> = const { Cell::new(0) };
5548    }
5549
5550    /// Serializes tests that share the process-global pack cache (including `clear_pack_cache`).
5551    pub struct PackCacheTestGuard {
5552        _inner: Option<MutexGuard<'static, PackCacheTestCoordinator>>,
5553    }
5554
5555    pub fn acquire() -> PackCacheTestGuard {
5556        let depth = DEPTH.with(|d| {
5557            let n = d.get();
5558            d.set(n + 1);
5559            n
5560        });
5561        PackCacheTestGuard {
5562            _inner: if depth == 0 {
5563                let mut g = COORD.lock().unwrap_or_else(|e| e.into_inner());
5564                assert_eq!(
5565                    g.top_level_guards, 0,
5566                    "pack cache test coordinator already held"
5567                );
5568                g.top_level_guards = 1;
5569                Some(g)
5570            } else {
5571                None
5572            },
5573        }
5574    }
5575
5576    impl Drop for PackCacheTestGuard {
5577        fn drop(&mut self) {
5578            DEPTH.with(|d| {
5579                let n = d.get();
5580                assert!(n > 0, "pack cache test guard depth underflow");
5581                d.set(n - 1);
5582                if n == 1 {
5583                    if let Some(mut g) = self._inner.take() {
5584                        assert_eq!(g.top_level_guards, 1);
5585                        g.top_level_guards = 0;
5586                    }
5587                }
5588            });
5589        }
5590    }
5591}
5592
5593#[cfg(test)]
5594pub(crate) fn pack_cache_test_guard() -> pack_cache_test_sync::PackCacheTestGuard {
5595    pack_cache_test_sync::acquire()
5596}
5597
5598/// Serializes tests that mutate the process-global pack cache (integration tests).
5599pub fn test_pack_cache_guard() -> pack_cache_test_sync::PackCacheTestGuard {
5600    pack_cache_test_sync::acquire()
5601}
5602
5603#[cfg(test)]
5604mod tests {
5605    use super::*;
5606    use crate::delta_encode::encode_lcp_delta;
5607    use crate::odb::Odb;
5608    use crate::pack_map::PackData;
5609    use crate::transfer::{build_pack, PackBuildOptions};
5610    use flate2::write::ZlibEncoder;
5611    use flate2::Compression;
5612    use std::io::Write;
5613    use std::process::Command;
5614
5615    fn git_try(dir: &std::path::Path, args: &[&str]) -> bool {
5616        Command::new("git") // hygiene: git fixture comparison in cfg(test) module // hygiene: git fixture comparison in cfg(test) module
5617            .current_dir(dir)
5618            .args(args)
5619            .env("GIT_CONFIG_GLOBAL", "/dev/null")
5620            .env("GIT_CONFIG_SYSTEM", "/dev/null")
5621            .output()
5622            .map(|o| o.status.success())
5623            .unwrap_or(false)
5624    }
5625
5626    fn git(dir: &std::path::Path, args: &[&str]) {
5627        let out = Command::new("git") // hygiene: git fixture comparison in cfg(test) module
5628            .current_dir(dir)
5629            .args(args)
5630            .env("GIT_AUTHOR_NAME", "T")
5631            .env("GIT_AUTHOR_EMAIL", "t@example.com")
5632            .env("GIT_COMMITTER_NAME", "T")
5633            .env("GIT_COMMITTER_EMAIL", "t@example.com")
5634            .env("GIT_AUTHOR_DATE", "2005-04-07T22:13:13 +0200")
5635            .env("GIT_COMMITTER_DATE", "2005-04-07T22:13:13 +0200")
5636            .env("GIT_CONFIG_GLOBAL", "/dev/null")
5637            .env("GIT_CONFIG_SYSTEM", "/dev/null")
5638            .output()
5639            .expect("git");
5640        assert!(
5641            out.status.success(),
5642            "git {:?}: {}",
5643            args,
5644            String::from_utf8_lossy(&out.stderr)
5645        );
5646    }
5647
5648    fn append_pack_object_header(buf: &mut Vec<u8>, type_code: u8, payload_len: usize) {
5649        let mut size = payload_len;
5650        let first = ((type_code & 0x7) << 4) | (size & 0x0f) as u8;
5651        size >>= 4;
5652        if size > 0 {
5653            buf.push(first | 0x80);
5654            while size > 0 {
5655                let b = (size & 0x7f) as u8;
5656                size >>= 7;
5657                buf.push(if size > 0 { b | 0x80 } else { b });
5658            }
5659        } else {
5660            buf.push(first);
5661        }
5662    }
5663
5664    fn zlib_pack(payload: &[u8]) -> Vec<u8> {
5665        let mut enc = ZlibEncoder::new(Vec::new(), Compression::default());
5666        enc.write_all(payload).unwrap();
5667        enc.finish().unwrap()
5668    }
5669
5670    fn append_sha1_pack_trailer(buf: &mut Vec<u8>) {
5671        buf.extend_from_slice(HashAlgo::Sha1.checksum_digest(&*buf).as_slice());
5672    }
5673
5674    fn append_ref_delta(buf: &mut Vec<u8>, base_oid: &ObjectId, delta: &[u8]) {
5675        let compressed = zlib_pack(delta);
5676        append_pack_object_header(buf, 7, delta.len());
5677        buf.extend_from_slice(base_oid.as_bytes());
5678        buf.extend_from_slice(&compressed);
5679    }
5680
5681    fn append_ofs_delta(buf: &mut Vec<u8>, base_offset: u64, delta: &[u8]) -> u64 {
5682        let object_start = buf.len() as u64;
5683        let compressed = zlib_pack(delta);
5684        append_pack_object_header(buf, 6, delta.len());
5685        let mut ofs = object_start - base_offset;
5686        while ofs >= 0x80 {
5687            buf.push((ofs as u8 & 0x7f) | 0x80);
5688            ofs >>= 7;
5689        }
5690        buf.push(ofs as u8);
5691        buf.extend_from_slice(&compressed);
5692        object_start
5693    }
5694
5695    fn append_whole_blob(buf: &mut Vec<u8>, data: &[u8]) -> u64 {
5696        let off = buf.len() as u64;
5697        let compressed = zlib_pack(data);
5698        append_pack_object_header(buf, 3, data.len());
5699        buf.extend_from_slice(&compressed);
5700        off
5701    }
5702
5703    fn append_corrupt_whole_blob(buf: &mut Vec<u8>) -> u64 {
5704        let off = buf.len() as u64;
5705        append_pack_object_header(buf, 3, 8);
5706        buf.extend_from_slice(&[0xff, 0xfe, 0xfd]);
5707        off
5708    }
5709
5710    fn write_v2_idx_for_test(
5711        idx_path: &Path,
5712        pack_path: &Path,
5713        entries: &[(ObjectId, u64)],
5714    ) -> Result<()> {
5715        let pack_bytes = std::fs::read(pack_path)?;
5716        let mut with_crc = Vec::with_capacity(entries.len());
5717        for (oid, off) in entries {
5718            let start = *off as usize;
5719            let mut end = start;
5720            skip_one_pack_object(&pack_bytes, &mut end, *off, 20)
5721                .map_err(|e| Error::CorruptObject(format!("idx crc walk: {e}")))?;
5722            with_crc.push((*oid, *off, crc32fast::hash(&pack_bytes[start..end])));
5723        }
5724        write_v2_pack_index(idx_path, pack_path, &with_crc, 20)
5725    }
5726
5727    fn single_blob_pack(data: &[u8]) -> (Vec<u8>, ObjectId, u64) {
5728        let odb = Odb::new(tempfile::tempdir().expect("tempdir").path());
5729        let oid = odb.hash(ObjectKind::Blob, data).expect("hash");
5730        let mut pack = Vec::new();
5731        pack.extend_from_slice(b"PACK");
5732        pack.extend_from_slice(&2u32.to_be_bytes());
5733        pack.extend_from_slice(&1u32.to_be_bytes());
5734        let off = append_whole_blob(&mut pack, data);
5735        append_sha1_pack_trailer(&mut pack);
5736        (pack, oid, off)
5737    }
5738
5739    #[test]
5740    fn single_local_pack_reader_batch_check_caches_deltified_entries() {
5741        let _guard = pack_cache_test_guard();
5742        clear_pack_cache();
5743        let Some((tmp, idx, tip, odb)) = build_ofs_delta_chain_pack(2) else {
5744            return;
5745        };
5746        let objects = tmp.path().join("objects");
5747        let store = crate::pack_store::PackStore::legacy_for_objects_dir(&objects);
5748        crate::pack_store::PackStore::with_context(store, || {
5749            let mut reader = SingleLocalPackReader::open(&objects)
5750                .expect("open")
5751                .expect("single pack");
5752            let info1 = reader.read_info(&tip).expect("first read");
5753            let info2 = reader.read_info(&tip).expect("cached read");
5754            assert_eq!(info1, info2);
5755            let full = read_object_from_pack(&idx, &tip).expect("full read");
5756            assert_eq!(info1.kind, full.kind);
5757            assert_eq!(info1.size, full.data.len() as u64);
5758        });
5759        let _ = odb;
5760    }
5761
5762    #[test]
5763    fn pack_read_info_cache_is_scoped_per_repository_and_pack_bytes() {
5764        let _guard = pack_cache_test_guard();
5765        clear_pack_cache();
5766        const ENTRY_OFFSET: u64 = 200;
5767        let dir_a = tempfile::tempdir().expect("tempdir a");
5768        let dir_b = tempfile::tempdir().expect("tempdir b");
5769        let objects_a = dir_a.path().join("objects");
5770        let objects_b = dir_b.path().join("objects");
5771        fs::create_dir_all(objects_a.join("pack")).expect("pack dir a");
5772        fs::create_dir_all(objects_b.join("pack")).expect("pack dir b");
5773        let pack_a = objects_a.join("pack/p-a.pack");
5774        let pack_b = objects_b.join("pack/p-b.pack");
5775        let (oid_a, _) =
5776            write_sparse_single_blob_pack(dir_a.path(), &pack_a, b"five!", ENTRY_OFFSET)
5777                .expect("pack a");
5778        let (oid_b, _) =
5779            write_sparse_single_blob_pack(dir_b.path(), &pack_b, b"1234567890", ENTRY_OFFSET)
5780                .expect("pack b");
5781        let idx_a = pack_a.with_extension("idx");
5782        let idx_b = pack_b.with_extension("idx");
5783        write_v2_idx_for_test(&idx_a, &pack_a, &[(oid_a, ENTRY_OFFSET)]).expect("idx a");
5784        write_v2_idx_for_test(&idx_b, &pack_b, &[(oid_b, ENTRY_OFFSET)]).expect("idx b");
5785        let idx_a = read_pack_index(&idx_a).expect("parse idx a");
5786        let idx_b = read_pack_index(&idx_b).expect("parse idx b");
5787
5788        let read_size = |objects: &Path, idx: &PackIndex| -> u64 {
5789            let store = crate::pack_store::PackStore::legacy_for_objects_dir(objects);
5790            crate::pack_store::PackStore::with_context(store, || {
5791                super::read_object_info_from_pack_at_offset(idx, ENTRY_OFFSET)
5792                    .expect("read_info")
5793                    .size
5794            })
5795        };
5796
5797        let size_a = read_size(&objects_a, &idx_a);
5798        let size_b = read_size(&objects_b, &idx_b);
5799        assert_eq!(size_a, 5);
5800        assert_eq!(size_b, 10);
5801        // A → B → A again: cache must not return B's size for A's pack entry.
5802        assert_eq!(read_size(&objects_a, &idx_a), 5);
5803        assert_eq!(read_size(&objects_b, &idx_b), 10);
5804        assert_eq!(read_size(&objects_a, &idx_a), 5);
5805    }
5806    /// Write a valid pack with one blob placed at `object_offset` so the on-disk file
5807    /// length exceeds the mmap owned-buffer threshold without storing huge compressed payloads.
5808    fn write_sparse_single_blob_pack(
5809        repo_root: &Path,
5810        pack_path: &Path,
5811        data: &[u8],
5812        object_offset: u64,
5813    ) -> Result<(ObjectId, u64)> {
5814        use std::fs::OpenOptions;
5815        use std::io::{Seek, SeekFrom, Write};
5816
5817        let odb = Odb::new(repo_root);
5818        let oid = odb.hash(ObjectKind::Blob, data).expect("hash");
5819        let mut file = OpenOptions::new()
5820            .create(true)
5821            .write(true)
5822            .read(true)
5823            .truncate(true)
5824            .open(pack_path)
5825            .map_err(Error::Io)?;
5826        file.write_all(b"PACK").map_err(Error::Io)?;
5827        file.write_all(&2u32.to_be_bytes()).map_err(Error::Io)?;
5828        file.write_all(&1u32.to_be_bytes()).map_err(Error::Io)?;
5829        file.seek(SeekFrom::Start(object_offset))
5830            .map_err(Error::Io)?;
5831        let mut tail = Vec::new();
5832        append_whole_blob(&mut tail, data);
5833        file.write_all(&tail).map_err(Error::Io)?;
5834        let trailer = sha1_trailer_for_open_file(&mut file)?;
5835        file.write_all(trailer.as_bytes()).map_err(Error::Io)?;
5836        file.flush().map_err(Error::Io)?;
5837        Ok((oid, object_offset))
5838    }
5839
5840    #[test]
5841    fn skip_one_pack_object_rejects_zero_size_with_zlib_payload() {
5842        let mut bytes = Vec::new();
5843        bytes.push(0x30);
5844        bytes.extend_from_slice(&zlib_pack(b"x"));
5845        let mut pos = 0usize;
5846        let err = skip_one_pack_object(&bytes, &mut pos, 0, 20).unwrap_err();
5847        assert!(matches!(err, Error::CorruptObject(_)), "got {err:?}");
5848    }
5849
5850    fn install_synthetic_pack(
5851        objects_dir: &Path,
5852        stem: &str,
5853        pack: &[u8],
5854        entries: &[(ObjectId, u64)],
5855    ) -> PackIndex {
5856        install_synthetic_pack_inner(objects_dir, stem, pack, entries, true)
5857    }
5858
5859    fn install_synthetic_pack_no_clear(
5860        objects_dir: &Path,
5861        stem: &str,
5862        pack: &[u8],
5863        entries: &[(ObjectId, u64)],
5864    ) -> PackIndex {
5865        install_synthetic_pack_inner(objects_dir, stem, pack, entries, false)
5866    }
5867
5868    fn install_synthetic_pack_inner(
5869        objects_dir: &Path,
5870        stem: &str,
5871        pack: &[u8],
5872        entries: &[(ObjectId, u64)],
5873        clear_cache: bool,
5874    ) -> PackIndex {
5875        let pack_dir = objects_dir.join("pack");
5876        std::fs::create_dir_all(&pack_dir).expect("pack dir");
5877        let pack_path = pack_dir.join(format!("{stem}.pack"));
5878        let idx_path = pack_dir.join(format!("{stem}.idx"));
5879        std::fs::write(&pack_path, pack).expect("write pack");
5880        write_v2_idx_for_test(&idx_path, &pack_path, entries).expect("write idx");
5881        if clear_cache {
5882            clear_pack_cache();
5883        }
5884        read_pack_index(&idx_path).expect("read idx")
5885    }
5886
5887    fn pack_index_for_objects(
5888        pack_path: std::path::PathBuf,
5889        objects: &[(ObjectId, u64)],
5890    ) -> PackIndex {
5891        let idx_path = pack_path.with_extension("idx");
5892        let pack_bytes = fs::read(&pack_path).expect("read pack for idx trailer");
5893        let trailer = &pack_bytes[pack_bytes.len().saturating_sub(20)..];
5894        let rows: Vec<(ObjectId, u64, u32)> = objects
5895            .iter()
5896            .map(|(oid, offset)| (*oid, *offset, 0))
5897            .collect();
5898        write_v2_pack_index_with_trailer(&idx_path, &rows, trailer, 20, None).expect("write idx");
5899        read_pack_index(&idx_path).expect("read idx")
5900    }
5901
5902    fn index_pack_in_scratch(pack: &[u8]) -> (tempfile::TempDir, PackIndex) {
5903        let dir = tempfile::tempdir().expect("tempdir");
5904        let pack_path = dir.path().join("chain.pack");
5905        std::fs::write(&pack_path, pack).expect("write pack");
5906        let out = Command::new("git") // hygiene: git fixture comparison in cfg(test) module
5907            .current_dir(dir.path())
5908            .args(["index-pack", "chain.pack"])
5909            .env("GIT_CONFIG_GLOBAL", "/dev/null")
5910            .env("GIT_CONFIG_SYSTEM", "/dev/null")
5911            .output()
5912            .expect("index-pack");
5913        assert!(
5914            out.status.success(),
5915            "git index-pack failed: {}",
5916            String::from_utf8_lossy(&out.stderr)
5917        );
5918        let idx_path = dir.path().join("chain.idx");
5919        let idx = read_pack_index(&idx_path).expect("read idx");
5920        (dir, idx)
5921    }
5922
5923    /// Repo with many prefix-preserving edits to one blob so delta chains can exceed 50.
5924    fn build_deep_delta_repo(commit_count: usize) -> Option<(tempfile::TempDir, ObjectId, Odb)> {
5925        let tmp = tempfile::tempdir().ok()?;
5926        let dir = tmp.path();
5927        if !git_try(dir, &["init", "-q", "-b", "main", "."]) {
5928            return None;
5929        }
5930        let mut body = String::new();
5931        for i in 0..200 {
5932            body.push_str(&format!("seed line {i:04}\n"));
5933        }
5934        for rev in 0..commit_count {
5935            body.push_str(&format!("edit-{rev}\n"));
5936            std::fs::write(dir.join("blob.txt"), body.as_bytes()).ok()?;
5937            git(dir, &["add", "blob.txt"]);
5938            git(dir, &["commit", "-q", "-m", &format!("c{rev}")]);
5939        }
5940        let tip_hex = Command::new("git") // hygiene: git fixture comparison in cfg(test) module
5941            .current_dir(dir)
5942            .args(["rev-parse", "HEAD"])
5943            .env("GIT_CONFIG_GLOBAL", "/dev/null")
5944            .env("GIT_CONFIG_SYSTEM", "/dev/null")
5945            .output()
5946            .ok()?;
5947        if !tip_hex.status.success() {
5948            return None;
5949        }
5950        let tip = ObjectId::from_hex(std::str::from_utf8(&tip_hex.stdout).ok()?.trim()).ok()?;
5951        let git_dir = dir.join(".git");
5952        let odb = Odb::new(&git_dir.join("objects")).with_config_git_dir(git_dir);
5953        Some((tmp, tip, odb))
5954    }
5955
5956    #[test]
5957    fn verify_packed_object_hash_sha256_detects_corruption() {
5958        use crate::hash::hash_object;
5959        use std::fs;
5960
5961        let data = b"sha256 packed blob for hash verify\n";
5962        let oid = hash_object(HashAlgo::Sha256, ObjectKind::Blob, data).expect("hash");
5963        let wrong_oid =
5964            hash_object(HashAlgo::Sha256, ObjectKind::Blob, b"different payload\n").expect("hash");
5965        let mut pack = Vec::new();
5966        pack.extend_from_slice(b"PACK");
5967        pack.extend_from_slice(&2u32.to_be_bytes());
5968        pack.extend_from_slice(&1u32.to_be_bytes());
5969        let off = append_whole_blob(&mut pack, data);
5970        pack.extend_from_slice(HashAlgo::Sha256.checksum_digest(&pack).as_slice());
5971
5972        let dir = tempfile::tempdir().expect("tempdir");
5973        let pack_path = dir.path().join("sha256-corrupt.pack");
5974        fs::write(&pack_path, &pack).expect("write pack");
5975
5976        let idx_path = pack_path.with_extension("idx");
5977        let trailer = &pack[pack.len() - 32..];
5978        write_v2_pack_index_with_trailer(&idx_path, &[(wrong_oid, off, 0)], trailer, 32, None)
5979            .expect("write idx");
5980        let idx = read_pack_index(&idx_path).expect("read idx");
5981
5982        let err = read_object_from_pack_bytes(&pack, &idx, wrong_oid.as_bytes()).unwrap_err();
5983        match err {
5984            Error::CorruptObject(msg) => {
5985                assert!(
5986                    msg.contains("hashes to"),
5987                    "expected hash mismatch message, got: {msg}"
5988                );
5989                assert!(
5990                    msg.contains(&oid.to_hex()),
5991                    "expected recomputed oid in message, got: {msg}"
5992                );
5993            }
5994            other => panic!("expected CorruptObject, got {other:?}"),
5995        }
5996    }
5997
5998    #[cfg_attr(
5999        not(feature = "full-tests"),
6000        ignore = "starts system git or is heavy; run with --features full-tests"
6001    )]
6002    #[test]
6003    fn reads_delta_chain_deeper_than_50() {
6004        let _guard = pack_cache_test_guard();
6005        let Some((_repo, tip, odb)) = build_deep_delta_repo(120) else {
6006            let _ = "SKIP: git unavailable for deep delta fixture";
6007            return;
6008        };
6009        let pack = build_pack(
6010            &odb,
6011            &[tip],
6012            &[],
6013            &PackBuildOptions {
6014                delta: true,
6015                max_depth: 120,
6016                use_ofs_delta: true,
6017                ..PackBuildOptions::default()
6018            },
6019        )
6020        .expect("build deep delta pack");
6021
6022        let (_scratch, idx) = index_pack_in_scratch(&pack);
6023        assert!(
6024            idx.len() > 1,
6025            "expected a multi-object pack, got {}",
6026            idx.len()
6027        );
6028
6029        for entry in idx.iter() {
6030            let oid = ObjectId::from_bytes(entry.oid()).expect("oid in idx");
6031            let from_pack = read_object_from_pack(&idx, &oid).expect("read packed object");
6032            let expected = odb.read(&oid).expect("loose/alt copy for oid");
6033            assert_eq!(from_pack.kind, expected.kind);
6034            assert_eq!(from_pack.data, expected.data);
6035            assert_eq!(
6036                odb.hash(from_pack.kind, &from_pack.data).expect("hash"),
6037                oid
6038            );
6039        }
6040    }
6041
6042    #[test]
6043    fn cyclic_ref_delta_chain_errors() {
6044        let _guard = pack_cache_test_guard();
6045        let odb = Odb::new(tempfile::tempdir().expect("tempdir").path());
6046        let content_a = b"aaa".as_slice();
6047        let content_b = b"bbb".as_slice();
6048        let oid_a = odb.hash(ObjectKind::Blob, content_a).expect("hash");
6049        let oid_b = odb.hash(ObjectKind::Blob, content_b).expect("hash");
6050        let delta_a = encode_lcp_delta(content_b, content_a).expect("delta a");
6051        let delta_b = encode_lcp_delta(content_a, content_b).expect("delta b");
6052
6053        let mut pack = Vec::new();
6054        pack.extend_from_slice(b"PACK");
6055        pack.extend_from_slice(&2u32.to_be_bytes());
6056        pack.extend_from_slice(&2u32.to_be_bytes());
6057        let off_a = pack.len() as u64;
6058        append_ref_delta(&mut pack, &oid_b, &delta_a);
6059        let off_b = pack.len() as u64;
6060        append_ref_delta(&mut pack, &oid_a, &delta_b);
6061        append_sha1_pack_trailer(&mut pack);
6062
6063        let dir = tempfile::tempdir().expect("tempdir");
6064        let pack_path = dir.path().join("cycle.pack");
6065        std::fs::write(&pack_path, &pack).expect("write cyclic pack");
6066        let idx = pack_index_for_objects(pack_path, &[(oid_a, off_a), (oid_b, off_b)]);
6067        let pack_bytes = std::fs::read(&idx.pack_path).expect("read pack");
6068        for err in [
6069            read_object_from_pack(&idx, &oid_a).expect_err("read_object_from_pack"),
6070            read_object_from_pack_bytes(&pack_bytes, &idx, oid_a.as_bytes())
6071                .expect_err("read_object_from_pack_bytes"),
6072        ] {
6073            assert!(
6074                matches!(err, Error::DeltaChainTooDeep { limit: 4096 }),
6075                "expected typed delta chain limit, got {err:?}"
6076            );
6077        }
6078    }
6079
6080    #[test]
6081    fn resolve_cross_pack_delta_without_index_clone() {
6082        let _guard = pack_cache_test_guard();
6083        clear_pack_cache();
6084        let odb = Odb::new(tempfile::tempdir().expect("tempdir").path());
6085        let base = b"cross-pack-base-payload".as_slice();
6086        let tip = b"cross-pack-base-payload!".as_slice();
6087        let oid_base = odb.hash(ObjectKind::Blob, base).expect("hash");
6088        let oid_tip = odb.hash(ObjectKind::Blob, tip).expect("hash");
6089        let delta = encode_lcp_delta(base, tip).expect("delta");
6090
6091        let mut pack_base = Vec::new();
6092        pack_base.extend_from_slice(b"PACK");
6093        pack_base.extend_from_slice(&2u32.to_be_bytes());
6094        pack_base.extend_from_slice(&1u32.to_be_bytes());
6095        let off_base = append_whole_blob(&mut pack_base, base);
6096        append_sha1_pack_trailer(&mut pack_base);
6097
6098        let mut pack_delta = Vec::new();
6099        pack_delta.extend_from_slice(b"PACK");
6100        pack_delta.extend_from_slice(&2u32.to_be_bytes());
6101        pack_delta.extend_from_slice(&1u32.to_be_bytes());
6102        let off_tip = pack_delta.len() as u64;
6103        append_ref_delta(&mut pack_delta, &oid_base, &delta);
6104        append_sha1_pack_trailer(&mut pack_delta);
6105
6106        let dir = tempfile::tempdir().expect("tempdir");
6107        let objects = dir.path().join("objects");
6108        let _idx_base =
6109            install_synthetic_pack(&objects, "cross-base", &pack_base, &[(oid_base, off_base)]);
6110        let idx_delta =
6111            install_synthetic_pack(&objects, "cross-delta", &pack_delta, &[(oid_tip, off_tip)]);
6112
6113        let from_delta_pack = read_object_from_pack(&idx_delta, &oid_tip).expect("delta pack read");
6114        assert_eq!(from_delta_pack.kind, ObjectKind::Blob);
6115        assert_eq!(from_delta_pack.data, tip);
6116        assert_eq!(
6117            odb.hash(from_delta_pack.kind, &from_delta_pack.data)
6118                .expect("hash"),
6119            oid_tip
6120        );
6121
6122        let from_packs = read_object_from_packs(&objects, &oid_tip).expect("packs dir read");
6123        assert_eq!(from_packs.kind, ObjectKind::Blob);
6124        assert_eq!(from_packs.data, tip);
6125        assert_eq!(
6126            odb.hash(from_packs.kind, &from_packs.data).expect("hash"),
6127            oid_tip
6128        );
6129    }
6130
6131    #[cfg_attr(
6132        not(feature = "full-tests"),
6133        ignore = "starts system git or is heavy; run with --features full-tests"
6134    )]
6135    #[test]
6136    fn read_object_from_pack_bytes_uses_in_memory_index_without_on_disk_idx() {
6137        let odb = Odb::new(tempfile::tempdir().expect("tempdir").path());
6138        let content = b"in-memory-index-blob";
6139        let oid = odb.hash(ObjectKind::Blob, content).expect("hash");
6140
6141        let mut pack = Vec::new();
6142        pack.extend_from_slice(b"PACK");
6143        pack.extend_from_slice(&2u32.to_be_bytes());
6144        pack.extend_from_slice(&1u32.to_be_bytes());
6145        let off = append_whole_blob(&mut pack, content);
6146        append_sha1_pack_trailer(&mut pack);
6147
6148        let dir = tempfile::tempdir().expect("tempdir");
6149        let pack_path = dir.path().join("mem.pack");
6150        std::fs::write(&pack_path, &pack).expect("write pack");
6151        let idx = pack_index_for_objects(pack_path, &[(oid, off)]);
6152        std::fs::remove_file(&idx.idx_path).expect("drop on-disk idx");
6153        assert!(
6154            !idx.idx_path.exists(),
6155            "fixture must not rely on an on-disk .idx"
6156        );
6157
6158        let got = read_object_from_pack_bytes(&pack, &idx, oid.as_bytes()).expect("read bytes");
6159        assert_eq!(got.kind, ObjectKind::Blob);
6160        assert_eq!(got.data, content);
6161    }
6162
6163    fn build_ofs_delta_chain_pack(
6164        depth: usize,
6165    ) -> Option<(tempfile::TempDir, PackIndex, ObjectId, Odb)> {
6166        if depth == 0 {
6167            return None;
6168        }
6169        let tmp = tempfile::tempdir().ok()?;
6170        let odb = Odb::new(&tmp.path().join("objects"));
6171        let mut content = b"ofs-delta-chain-base\n".to_vec();
6172        let mut pack = Vec::new();
6173        pack.extend_from_slice(b"PACK");
6174        pack.extend_from_slice(&2u32.to_be_bytes());
6175        pack.extend_from_slice(&(depth as u32 + 1).to_be_bytes());
6176        let mut base_off = append_whole_blob(&mut pack, &content);
6177        let mut entries = vec![(
6178            odb.hash(ObjectKind::Blob, &content).expect("hash"),
6179            base_off,
6180        )];
6181        for _ in 0..depth {
6182            let mut next = content.clone();
6183            next.push(b'!');
6184            let delta = encode_lcp_delta(&content, &next).ok()?;
6185            let off = append_ofs_delta(&mut pack, base_off, &delta);
6186            let oid = odb.hash(ObjectKind::Blob, &next).expect("hash");
6187            entries.push((oid, off));
6188            content = next;
6189            base_off = off;
6190        }
6191        append_sha1_pack_trailer(&mut pack);
6192        let tip = entries.last()?.0;
6193        let dir = tmp.path();
6194        std::fs::create_dir_all(dir.join("objects/pack")).ok()?;
6195        let pack_path = dir.join("objects/pack/chain.pack");
6196        std::fs::write(&pack_path, &pack).ok()?;
6197        write_v2_idx_for_test(&pack_path.with_extension("idx"), &pack_path, &entries).ok()?;
6198        clear_pack_cache();
6199        let idx = read_pack_index(&pack_path.with_extension("idx")).ok()?;
6200        Some((tmp, idx, tip, odb))
6201    }
6202
6203    #[test]
6204    fn bounded_pack_cache_resolves_deltas_without_changing_shared_cache() {
6205        let _guard = pack_cache_test_guard();
6206        let (_tmp, idx, tip, odb) = build_ofs_delta_chain_pack(800).expect("chain");
6207        let normal = odb.read(&tip).expect("normal");
6208        let shared_bytes = odb.pack_store().expect("store").delta_bases().bytes_used();
6209        let bounded = odb.with_bounded_pack_cache();
6210        assert!(!Arc::ptr_eq(
6211            odb.pack_store().expect("store"),
6212            bounded.pack_store().expect("store")
6213        ));
6214        bounded.with_pack_read_context(|| {
6215            let got = bounded.read(&tip).expect("bounded");
6216            assert_eq!(got.data, normal.data);
6217            let pack = read_pack_bytes_cached(&idx.pack_path).expect("pack");
6218            assert!(pack.uses_positioned_reads());
6219            let cached = bounded
6220                .pack_store()
6221                .expect("store")
6222                .delta_bases()
6223                .bytes_used();
6224            assert!(cached > 0 && cached <= crate::pack_store::BOUNDED_DELTA_BASE_BYTES);
6225        });
6226        assert_eq!(
6227            odb.pack_store().expect("store").delta_bases().bytes_used(),
6228            shared_bytes
6229        );
6230    }
6231
6232    #[test]
6233    fn ofs_delta_chain_depth_4096_resolves() {
6234        let _guard = pack_cache_test_guard();
6235        clear_pack_cache();
6236        let Some((_tmp, idx, tip, odb)) = build_ofs_delta_chain_pack(4096) else {
6237            return;
6238        };
6239        let got = read_object_from_pack(&idx, &tip).expect("4096-deep chain");
6240        let expect = odb.hash(ObjectKind::Blob, &got.data).expect("hash");
6241        assert_eq!(expect, tip);
6242    }
6243
6244    #[test]
6245    fn ofs_delta_chain_depth_4097_errors() {
6246        let _guard = pack_cache_test_guard();
6247        clear_pack_cache();
6248        let Some((_tmp, idx, tip, _odb)) = build_ofs_delta_chain_pack(4097) else {
6249            return;
6250        };
6251        let err = read_object_from_pack(&idx, &tip).expect_err("4097-deep chain");
6252        assert!(
6253            matches!(err, Error::DeltaChainTooDeep { limit: 4096 }),
6254            "got {err:?}"
6255        );
6256    }
6257
6258    #[test]
6259    fn delta_base_cache_limit_zero_disables_caching() {
6260        let _guard = pack_cache_test_guard();
6261        clear_pack_cache();
6262        let Some((_tmp, idx, tip, _odb)) = build_ofs_delta_chain_pack(12) else {
6263            return;
6264        };
6265        pack_cache::test_set_delta_base_cache_byte_limit(&idx.pack_path, 0);
6266        let _ = read_object_from_pack(&idx, &tip).expect("read chain");
6267        assert_eq!(
6268            pack_cache::test_delta_base_cache_bytes_used(&idx.pack_path),
6269            0,
6270            "limit 0 must not retain delta bases"
6271        );
6272        pack_cache::test_set_delta_base_cache_byte_limit(
6273            &idx.pack_path,
6274            pack_cache::DELTA_BASE_CACHE_DEFAULT,
6275        );
6276    }
6277
6278    #[test]
6279    fn delta_base_cache_eviction_respects_byte_limit() {
6280        let _guard = pack_cache_test_guard();
6281        clear_pack_cache();
6282        let Some((_tmp, idx, tip, _odb)) = build_ofs_delta_chain_pack(32) else {
6283            return;
6284        };
6285        let expected = read_object_from_pack(&idx, &tip)
6286            .expect("baseline read")
6287            .data;
6288        pack_cache::test_set_delta_base_cache_byte_limit(&idx.pack_path, 4096);
6289        let _ = read_object_from_pack(&idx, &tip).expect("prime cache");
6290        assert!(
6291            pack_cache::test_delta_base_cache_bytes_used(&idx.pack_path) <= 4096,
6292            "cache bytes {} exceeded cap",
6293            pack_cache::test_delta_base_cache_bytes_used(&idx.pack_path)
6294        );
6295        pack_cache::test_set_delta_base_cache_byte_limit(&idx.pack_path, 512);
6296        for round in 0..8 {
6297            let got = read_object_from_pack(&idx, &tip).expect("read after eviction");
6298            assert_eq!(got.data, expected, "round {round} must stay byte-identical");
6299        }
6300        pack_cache::test_set_delta_base_cache_byte_limit(
6301            &idx.pack_path,
6302            pack_cache::DELTA_BASE_CACHE_DEFAULT,
6303        );
6304    }
6305
6306    #[test]
6307    fn odb_read_into_caching_delta_bases_fills_cache_read_into_does_not() {
6308        let _guard = pack_cache_test_guard();
6309        clear_pack_cache();
6310        let Some((_tmp, idx, tip, odb)) = build_ofs_delta_chain_pack(12) else {
6311            return;
6312        };
6313        let expected = read_object_from_pack(&idx, &tip)
6314            .expect("baseline read")
6315            .data;
6316        clear_pack_cache();
6317        let store = crate::pack_store::PackStore::legacy_for_pack_path(&idx.pack_path);
6318        crate::pack_store::PackStore::with_context(store, || {
6319            let mut buf = Vec::new();
6320            assert_eq!(
6321                odb.read_into(&tip, &mut buf).expect("read_into"),
6322                ObjectKind::Blob
6323            );
6324            assert_eq!(buf, expected);
6325            assert_eq!(
6326                pack_cache::test_delta_base_cache_bytes_used(&idx.pack_path),
6327                0,
6328                "read_into must not retain delta bases"
6329            );
6330
6331            let mut cached = Vec::new();
6332            let kind = odb
6333                .read_into_caching_delta_bases(&tip, &mut cached)
6334                .expect("caching read");
6335            assert_eq!(kind, ObjectKind::Blob);
6336            assert_eq!(cached, expected);
6337            assert!(
6338                pack_cache::test_delta_base_cache_bytes_used(&idx.pack_path) > 0,
6339                "caching read must retain the chain's delta bases"
6340            );
6341
6342            let again = odb
6343                .read_into_caching_delta_bases(&tip, &mut cached)
6344                .expect("second caching read");
6345            assert_eq!(again, ObjectKind::Blob);
6346            assert_eq!(cached, expected);
6347        });
6348    }
6349
6350    #[test]
6351    fn repository_odb_read_into_caching_delta_bases_fills_cache_through_files_primary() {
6352        use crate::repo::{InitOptions, Repository};
6353        let _guard = pack_cache_test_guard();
6354        clear_pack_cache();
6355        let Some((tmp, idx, tip, _odb)) = build_ofs_delta_chain_pack(12) else {
6356            return;
6357        };
6358        let expected = read_object_from_pack(&idx, &tip)
6359            .expect("baseline read")
6360            .data;
6361        Repository::init(
6362            tmp.path(),
6363            &InitOptions {
6364                bare: true,
6365                ..InitOptions::default()
6366            },
6367        )
6368        .expect("init over packed objects");
6369        let repo = Repository::open(tmp.path(), &crate::environment::RepositoryOptions::empty())
6370            .expect("open");
6371        let odb = repo.odb();
6372        let store = Arc::clone(odb.pack_store().expect("pack store"));
6373        store.invalidate_packs();
6374
6375        let mut buf = Vec::new();
6376        assert_eq!(
6377            odb.read_into(&tip, &mut buf).expect("read_into"),
6378            ObjectKind::Blob
6379        );
6380        assert_eq!(buf, expected);
6381        assert_eq!(store.delta_bases().bytes_used(), 0);
6382
6383        let mut cached = Vec::new();
6384        assert_eq!(
6385            odb.read_into_caching_delta_bases(&tip, &mut cached)
6386                .expect("caching read"),
6387            ObjectKind::Blob
6388        );
6389        assert_eq!(cached, expected);
6390        assert!(store.delta_bases().bytes_used() > 0);
6391    }
6392
6393    #[test]
6394    fn odb_read_into_caching_delta_bases_falls_back_outside_packs() {
6395        let tmp = tempfile::tempdir().expect("tempdir");
6396        let odb = Odb::new(&tmp.path().join("objects"));
6397        let oid = odb
6398            .write(ObjectKind::Blob, b"loose only\n")
6399            .expect("write loose");
6400        let mut buf = vec![0u8; 4];
6401        let kind = odb
6402            .read_into_caching_delta_bases(&oid, &mut buf)
6403            .expect("loose read");
6404        assert_eq!(kind, ObjectKind::Blob);
6405        assert_eq!(buf, b"loose only\n");
6406    }
6407
6408    #[test]
6409    fn cached_delta_reads_support_overlay_alternates_and_custom_stores() {
6410        use crate::odb::store::MemoryStore;
6411        use crate::odb::OdbBuilder;
6412
6413        let tmp = tempfile::tempdir().expect("tempdir");
6414        let local_objects = tmp.path().join("local/objects");
6415        let alternate_objects = tmp.path().join("alternate/objects");
6416        let alternate = Odb::new(&alternate_objects);
6417        let alternate_oid = alternate
6418            .write(ObjectKind::Blob, b"alternate\n")
6419            .expect("alternate object");
6420        fs::create_dir_all(local_objects.join("info")).expect("info");
6421        fs::write(
6422            local_objects.join("info/alternates"),
6423            format!("{}\n", alternate_objects.display()),
6424        )
6425        .expect("alternates");
6426
6427        let odb = Odb::new(&local_objects);
6428        let mut buf = Vec::with_capacity(1024);
6429        assert_eq!(
6430            odb.read_into_caching_delta_bases(&alternate_oid, &mut buf)
6431                .expect("alternate read"),
6432            ObjectKind::Blob
6433        );
6434        assert_eq!(buf, b"alternate\n");
6435
6436        odb.enable_mem_overlay();
6437        let overlay_oid = odb
6438            .write(ObjectKind::Blob, b"overlay\n")
6439            .expect("overlay object");
6440        odb.read_into_caching_delta_bases(&overlay_oid, &mut buf)
6441            .expect("overlay read");
6442        assert_eq!(buf, b"overlay\n");
6443
6444        let custom = OdbBuilder::files(tmp.path().join("custom/objects"))
6445            .primary(Arc::new(MemoryStore::new(HashAlgo::Sha1)))
6446            .build();
6447        let custom_oid = custom
6448            .write(ObjectKind::Blob, b"custom\n")
6449            .expect("custom object");
6450        custom
6451            .read_into_caching_delta_bases(&custom_oid, &mut buf)
6452            .expect("custom read");
6453        assert_eq!(buf, b"custom\n");
6454
6455        let missing = HashAlgo::Sha1
6456            .hash_object(ObjectKind::Blob, b"missing")
6457            .expect("missing id");
6458        assert!(matches!(
6459            custom.read_into_caching_delta_bases(&missing, &mut buf),
6460            Err(Error::ObjectNotFound(_))
6461        ));
6462    }
6463
6464    #[test]
6465    fn delta_base_cache_invalidated_when_pack_bytes_reloaded() {
6466        use filetime::{set_file_mtime, FileTime};
6467        let _guard = pack_cache_test_guard();
6468        clear_pack_cache();
6469        let Some((_tmp, idx, tip, _odb)) = build_ofs_delta_chain_pack(8) else {
6470            return;
6471        };
6472        let pack_path = idx.pack_path.clone();
6473        let pack_id = pack_cache::pack_id_for(&pack_path);
6474        let mid_off = idx
6475            .iter()
6476            .nth(4)
6477            .map(|e| e.offset())
6478            .expect("mid hop")
6479            .expect("mid off");
6480        let _ = read_object_from_pack(&idx, &tip).expect("warm cache");
6481        assert!(
6482            pack_cache::test_delta_base_cached(&pack_path, pack_id, mid_off),
6483            "expected intermediate cached at offset {mid_off}"
6484        );
6485        set_file_mtime(&pack_path, FileTime::now()).expect("touch pack mtime");
6486        assert!(
6487            pack_cache::revalidate_stale_pack_bytes(&pack_path).expect("revalidate"),
6488            "expected pack bytes reload"
6489        );
6490        assert!(
6491            !pack_cache::test_delta_base_cached(&pack_path, pack_id, mid_off),
6492            "cache must drop when pack bytes reload"
6493        );
6494    }
6495
6496    #[cfg_attr(
6497        not(feature = "full-tests"),
6498        ignore = "starts system git or is heavy; run with --features full-tests"
6499    )]
6500    #[test]
6501    fn git_repack_depth_250_matches_cat_file_batch() {
6502        let _guard = pack_cache_test_guard();
6503        clear_pack_cache();
6504        let tmp = tempfile::tempdir().expect("tempdir");
6505        let dir = tmp.path();
6506        if Command::new("git") // hygiene: git fixture comparison in cfg(test) module
6507            .current_dir(dir)
6508            .args(["init", "-q"])
6509            .env("GIT_CONFIG_GLOBAL", "/dev/null")
6510            .env("GIT_CONFIG_SYSTEM", "/dev/null")
6511            .status()
6512            .is_err()
6513        {
6514            let _ = "SKIP: git unavailable";
6515            return;
6516        }
6517        for i in 0..64 {
6518            std::fs::write(dir.join(format!("f{i}.txt")), format!("payload {i}\n")).unwrap();
6519            git(dir, &["add", &format!("f{i}.txt")]);
6520            git(dir, &["commit", "-qm", &format!("c{i}")]);
6521        }
6522        git(dir, &["repack", "-adf", "--depth=250", "--window=250"]);
6523        let objects = dir.join(".git/objects");
6524        let indexes = read_local_pack_indexes_cached(&objects).expect("pack indexes");
6525        let idx = indexes
6526            .into_iter()
6527            .max_by_key(|i| i.len())
6528            .expect("repack pack");
6529        for entry in idx.iter() {
6530            let oid = ObjectId::from_bytes(entry.oid()).expect("oid");
6531            let grit_obj = read_object_from_pack(&idx, &oid).expect("grit read");
6532            let git_type = Command::new("git") // hygiene: git fixture comparison in cfg(test) module
6533                .current_dir(dir)
6534                .args(["cat-file", "-t", &oid.to_hex()])
6535                .env("GIT_CONFIG_GLOBAL", "/dev/null")
6536                .env("GIT_CONFIG_SYSTEM", "/dev/null")
6537                .output()
6538                .expect("git cat-file -t");
6539            assert!(git_type.status.success());
6540            let git_kind = ObjectKind::from_bytes(
6541                std::str::from_utf8(git_type.stdout.trim_ascii_end())
6542                    .expect("utf8 type")
6543                    .as_bytes(),
6544            )
6545            .expect("git type");
6546            assert_eq!(git_kind, grit_obj.kind);
6547            let type_arg = match grit_obj.kind {
6548                ObjectKind::Blob => "blob",
6549                ObjectKind::Tree => "tree",
6550                ObjectKind::Commit => "commit",
6551                ObjectKind::Tag => "tag",
6552            };
6553            let git_payload = Command::new("git") // hygiene: git fixture comparison in cfg(test) module
6554                .current_dir(dir)
6555                .args(["cat-file", type_arg, &oid.to_hex()])
6556                .env("GIT_CONFIG_GLOBAL", "/dev/null")
6557                .env("GIT_CONFIG_SYSTEM", "/dev/null")
6558                .output()
6559                .expect("git cat-file raw");
6560            assert!(git_payload.status.success());
6561            assert_eq!(git_payload.stdout, grit_obj.data);
6562        }
6563    }
6564
6565    #[test]
6566    fn cyclic_ref_delta_chain_across_two_packs_errors() {
6567        let _guard = pack_cache_test_guard();
6568        clear_pack_cache();
6569        let odb = Odb::new(tempfile::tempdir().expect("tempdir").path());
6570        let content_a = b"aaa".as_slice();
6571        let content_b = b"bbb".as_slice();
6572        let oid_a = odb.hash(ObjectKind::Blob, content_a).expect("hash");
6573        let oid_b = odb.hash(ObjectKind::Blob, content_b).expect("hash");
6574        let delta_a = encode_lcp_delta(content_b, content_a).expect("delta a");
6575        let delta_b = encode_lcp_delta(content_a, content_b).expect("delta b");
6576
6577        let mut pack_a = Vec::new();
6578        pack_a.extend_from_slice(b"PACK");
6579        pack_a.extend_from_slice(&2u32.to_be_bytes());
6580        pack_a.extend_from_slice(&1u32.to_be_bytes());
6581        let off_a = pack_a.len() as u64;
6582        append_ref_delta(&mut pack_a, &oid_b, &delta_a);
6583        append_sha1_pack_trailer(&mut pack_a);
6584
6585        let mut pack_b = Vec::new();
6586        pack_b.extend_from_slice(b"PACK");
6587        pack_b.extend_from_slice(&2u32.to_be_bytes());
6588        pack_b.extend_from_slice(&1u32.to_be_bytes());
6589        let off_b = pack_b.len() as u64;
6590        append_ref_delta(&mut pack_b, &oid_a, &delta_b);
6591        append_sha1_pack_trailer(&mut pack_b);
6592
6593        let dir = tempfile::tempdir().expect("tempdir");
6594        let objects = dir.path().join("objects");
6595        let idx_b = install_synthetic_pack(&objects, "cycle-b", &pack_b, &[(oid_b, off_b)]);
6596        let idx_a = install_synthetic_pack(&objects, "cycle-a", &pack_a, &[(oid_a, off_a)]);
6597        assert_eq!(idx_a.len(), 1);
6598        assert_eq!(idx_b.len(), 1);
6599        assert!(idx_b.contains(&oid_b).expect("contains"));
6600        let _ = idx_b;
6601
6602        let err = read_object_from_pack(&idx_a, &oid_a).expect_err("cross-pack cycle");
6603        assert!(
6604            matches!(err, Error::DeltaChainTooDeep { limit: 4096 }),
6605            "expected typed delta chain limit, got {err:?}"
6606        );
6607    }
6608
6609    #[test]
6610    fn cross_pack_ref_delta_resolves_through_odb_read() {
6611        let _guard = pack_cache_test_guard();
6612        clear_pack_cache();
6613        let tmp = tempfile::tempdir().expect("tempdir");
6614        let objects = tmp.path().join("objects");
6615        let odb = Odb::new(&objects);
6616        let mut random = 17u64;
6617        let base: Vec<u8> = (0..20000)
6618            .map(|_| {
6619                random ^= random << 13;
6620                random ^= random >> 7;
6621                random ^= random << 17;
6622                random as u8
6623            })
6624            .collect();
6625        let mut target = base.clone();
6626        target.extend_from_slice(b"target suffix");
6627        let oid_base = odb.hash(ObjectKind::Blob, &base).expect("hash");
6628        let delta = encode_lcp_delta(&base, &target).expect("delta");
6629        let oid_target = odb.hash(ObjectKind::Blob, &target).expect("hash");
6630
6631        let mut pack_base = Vec::new();
6632        pack_base.extend_from_slice(b"PACK");
6633        pack_base.extend_from_slice(&2u32.to_be_bytes());
6634        pack_base.extend_from_slice(&1u32.to_be_bytes());
6635        let off_base = append_whole_blob(&mut pack_base, &base);
6636        append_sha1_pack_trailer(&mut pack_base);
6637
6638        let mut pack_tip = Vec::new();
6639        pack_tip.extend_from_slice(b"PACK");
6640        pack_tip.extend_from_slice(&2u32.to_be_bytes());
6641        pack_tip.extend_from_slice(&1u32.to_be_bytes());
6642        let off_tip = pack_tip.len() as u64;
6643        append_ref_delta(&mut pack_tip, &oid_base, &delta);
6644        append_sha1_pack_trailer(&mut pack_tip);
6645
6646        install_synthetic_pack(&objects, "base", &pack_base, &[(oid_base, off_base)]);
6647        install_synthetic_pack(&objects, "tip", &pack_tip, &[(oid_target, off_tip)]);
6648
6649        for reader in [&odb, &odb.with_bounded_pack_cache()] {
6650            let got = reader.read(&oid_target).expect("cross-pack ref-delta");
6651            assert_eq!(got.kind, ObjectKind::Blob);
6652            assert_eq!(got.data, target);
6653            assert_eq!(reader.hash(got.kind, &got.data).expect("hash"), oid_target);
6654        }
6655    }
6656
6657    #[test]
6658    fn corrupt_in_pack_base_rescued_from_loose() {
6659        let _guard = pack_cache_test_guard();
6660        let tmp = tempfile::tempdir().expect("tempdir");
6661        let objects = tmp.path().join("objects");
6662        std::fs::create_dir_all(objects.join("pack")).expect("dirs");
6663        let odb = Odb::new(&objects);
6664        let base = b"good-base-bytes";
6665        let tip = b"good-base-bytes!";
6666        let oid_base = odb.write_local(ObjectKind::Blob, base).expect("loose base");
6667        let delta = encode_lcp_delta(base, tip).expect("delta");
6668        let oid_tip = odb.hash(ObjectKind::Blob, tip).expect("hash");
6669
6670        let mut pack = Vec::new();
6671        pack.extend_from_slice(b"PACK");
6672        pack.extend_from_slice(&2u32.to_be_bytes());
6673        pack.extend_from_slice(&2u32.to_be_bytes());
6674        let off_base = append_corrupt_whole_blob(&mut pack);
6675        let off_tip = pack.len() as u64;
6676        append_ref_delta(&mut pack, &oid_base, &delta);
6677        append_sha1_pack_trailer(&mut pack);
6678
6679        let pack_path = objects.join("pack/rescue.pack");
6680        std::fs::write(&pack_path, &pack).expect("write pack");
6681        let idx = pack_index_for_objects(pack_path, &[(oid_base, off_base), (oid_tip, off_tip)]);
6682
6683        let got = read_object_from_pack(&idx, &oid_tip).expect("rescue read");
6684        assert_eq!(got.kind, ObjectKind::Blob);
6685        assert_eq!(got.data, tip);
6686    }
6687
6688    #[test]
6689    fn pack_added_after_first_lookup_found_on_miss() {
6690        let _guard = pack_cache_test_guard();
6691        clear_pack_cache();
6692        let tmp = tempfile::tempdir().expect("tempdir");
6693        let objects = tmp.path().join("objects");
6694        let pack_dir = objects.join("pack");
6695        let (pack_a, oid_a, off_a) = single_blob_pack(b"pack-a");
6696        install_synthetic_pack(&objects, "first", &pack_a, &[(oid_a, off_a)]);
6697        pack_cache::test_reset_dir_rescan_count(&pack_dir);
6698
6699        read_object_from_packs(&objects, &oid_a).expect("warm cache");
6700        assert_eq!(
6701            pack_cache::test_dir_rescan_count(&pack_dir),
6702            1,
6703            "initial lookup should scan the pack directory once"
6704        );
6705
6706        // Ensure the pack directory mtime advances past the cached stamp (second resolution).
6707        std::thread::sleep(std::time::Duration::from_secs(1));
6708
6709        let (pack_b, oid_b, off_b) = single_blob_pack(b"pack-b");
6710        install_synthetic_pack_no_clear(&objects, "second", &pack_b, &[(oid_b, off_b)]);
6711        assert_eq!(
6712            pack_cache::test_dir_rescan_count(&pack_dir),
6713            1,
6714            "adding a pack must not rescan until a miss"
6715        );
6716
6717        read_object_from_packs(&objects, &oid_b).expect("new pack via miss path");
6718        assert_eq!(
6719            pack_cache::test_dir_rescan_count(&pack_dir),
6720            2,
6721            "miss path should re-scan when pack dir mtime changed"
6722        );
6723    }
6724
6725    #[test]
6726    fn known_object_lookup_does_not_rescan() {
6727        let _guard = pack_cache_test_guard();
6728        clear_pack_cache();
6729        let tmp = tempfile::tempdir().expect("tempdir");
6730        let objects = tmp.path().join("objects");
6731        let pack_dir = objects.join("pack");
6732        pack_cache::test_reset_dir_rescan_count(&pack_dir);
6733        let (pack, oid, off) = single_blob_pack(b"cached-blob");
6734        install_synthetic_pack(&objects, "only", &pack, &[(oid, off)]);
6735
6736        read_object_from_packs(&objects, &oid).expect("prime cache");
6737        let after_first = pack_cache::test_dir_rescan_count(&pack_dir);
6738        for _ in 0..64 {
6739            read_object_from_packs(&objects, &oid).expect("repeat read");
6740        }
6741        assert_eq!(
6742            pack_cache::test_dir_rescan_count(&pack_dir),
6743            after_first,
6744            "repeated hits must not re-scan the pack directory"
6745        );
6746    }
6747
6748    #[test]
6749    fn two_repositories_concurrent_reads() {
6750        use crate::odb::Odb;
6751        use std::sync::Arc;
6752
6753        let _guard = pack_cache_test_guard();
6754        clear_pack_cache();
6755        let tmp_a = tempfile::tempdir().expect("tempdir a");
6756        let tmp_b = tempfile::tempdir().expect("tempdir b");
6757        let objects_a = tmp_a.path().join("objects");
6758        let objects_b = tmp_b.path().join("objects");
6759        let pack_a_dir = objects_a.join("pack");
6760        let pack_b_dir = objects_b.join("pack");
6761        let (pack_a, oid_a, off_a) = single_blob_pack(b"parallel-a");
6762        let (pack_b, oid_b, off_b) = single_blob_pack(b"parallel-b");
6763        install_synthetic_pack(&objects_a, "a", &pack_a, &[(oid_a, off_a)]);
6764        install_synthetic_pack(&objects_b, "b", &pack_b, &[(oid_b, off_b)]);
6765        let odb_a = Odb::new(&objects_a);
6766        let odb_b = Odb::new(&objects_b);
6767        assert!(
6768            !Arc::ptr_eq(odb_a.pack_store().unwrap(), odb_b.pack_store().unwrap()),
6769            "independent Odb opens must not share pack stores"
6770        );
6771        pack_cache::test_reset_dir_rescan_count_on_store(odb_a.pack_store().unwrap(), &pack_a_dir);
6772        pack_cache::test_reset_dir_rescan_count_on_store(odb_b.pack_store().unwrap(), &pack_b_dir);
6773
6774        let barrier = Arc::new(std::sync::Barrier::new(2));
6775        let b0 = Arc::clone(&barrier);
6776        let b1 = Arc::clone(&barrier);
6777        let odb_a_t = odb_a.clone();
6778        let odb_b_t = odb_b.clone();
6779        let id_a = oid_a;
6780        let id_b = oid_b;
6781
6782        let h1 = std::thread::spawn(move || {
6783            b0.wait();
6784            odb_a_t.read(&id_a).expect("read a");
6785        });
6786        let h2 = std::thread::spawn(move || {
6787            b1.wait();
6788            odb_b_t.read(&id_b).expect("read b");
6789        });
6790        h1.join().expect("thread a");
6791        h2.join().expect("thread b");
6792
6793        assert_eq!(
6794            pack_cache::test_dir_rescan_count_on_store(odb_a.pack_store().unwrap(), &pack_a_dir),
6795            1
6796        );
6797        assert_eq!(
6798            pack_cache::test_dir_rescan_count_on_store(odb_b.pack_store().unwrap(), &pack_b_dir),
6799            1
6800        );
6801    }
6802
6803    #[test]
6804    fn stale_cached_pack_bytes_revalidated_before_corruption_error() {
6805        let _guard = pack_cache_test_guard();
6806        clear_pack_cache();
6807        let tmp = tempfile::tempdir().expect("tempdir");
6808        let objects = tmp.path().join("objects");
6809        let (pack, oid, off) = single_blob_pack(b"revalidate-me");
6810        let idx = install_synthetic_pack(&objects, "good", &pack, &[(oid, off)]);
6811        read_object_from_pack(&idx, &oid).expect("prime pack bytes cache");
6812
6813        let good_on_disk = std::fs::read(&idx.pack_path).expect("read pack");
6814        pack_cache::test_inject_stale_pack_bytes(
6815            &idx.pack_path,
6816            PackData::from_owned(b"BAD".to_vec()),
6817        );
6818        std::fs::write(&idx.pack_path, &good_on_disk).expect("refresh on-disk pack");
6819        let got = read_object_from_pack(&idx, &oid).expect("revalidate from disk");
6820        assert_eq!(got.data, b"revalidate-me");
6821    }
6822
6823    #[test]
6824    fn replaced_pack_same_path_reread_after_invalidation() {
6825        let _guard = pack_cache_test_guard();
6826        clear_pack_cache();
6827        let tmp = tempfile::tempdir().expect("tempdir");
6828        let objects = tmp.path().join("objects");
6829        std::fs::create_dir_all(objects.join("pack")).expect("pack dir");
6830        let pack_path = objects.join("pack/repack-me.pack");
6831        let idx_path = objects.join("pack/repack-me.idx");
6832        const OBJECT_OFFSET: u64 = 9000;
6833        let (oid_a, off_a) =
6834            write_sparse_single_blob_pack(tmp.path(), &pack_path, b"before-repack", OBJECT_OFFSET)
6835                .expect("sparse pack a");
6836        assert!(
6837            std::fs::metadata(&pack_path).expect("stat").len() > 8192,
6838            "fixture pack must exceed mmap owned-buffer threshold"
6839        );
6840        write_v2_idx_for_test(&idx_path, &pack_path, &[(oid_a, off_a)]).expect("idx a");
6841        let idx = read_pack_index(&idx_path).expect("parse idx a");
6842        read_object_from_pack(&idx, &oid_a).expect("prime cache");
6843
6844        let (oid_b, off_b) = write_sparse_single_blob_pack(
6845            tmp.path(),
6846            &pack_path,
6847            b"after-repack-content",
6848            OBJECT_OFFSET,
6849        )
6850        .expect("sparse pack b");
6851        write_v2_idx_for_test(&idx_path, &pack_path, &[(oid_b, off_b)]).expect("idx b");
6852
6853        assert!(
6854            pack_cache::revalidate_stale_pack_bytes(&pack_path).expect("revalidate"),
6855            "replacement must invalidate cached bytes"
6856        );
6857        let idx = read_pack_index(&idx_path).expect("reload idx after pack swap");
6858        let got = read_object_from_pack(&idx, &oid_b).expect("read from replaced pack");
6859        assert_eq!(got.data, b"after-repack-content");
6860    }
6861
6862    #[test]
6863    fn read_object_at_large_pack_offset_via_mmap() {
6864        use std::fs::OpenOptions;
6865        use std::io::{Seek, SeekFrom, Write};
6866
6867        let _guard = pack_cache_test_guard();
6868        clear_pack_cache();
6869        let tmp = tempfile::tempdir().expect("tempdir");
6870        let objects = tmp.path().join("objects");
6871        std::fs::create_dir_all(objects.join("pack")).expect("pack dir");
6872
6873        let data = b"large-offset-blob";
6874        let odb = Odb::new(tmp.path());
6875        let oid = odb.hash(ObjectKind::Blob, data).expect("hash");
6876
6877        let pack_path = objects.join("pack/large-off.pack");
6878        let mut file = OpenOptions::new()
6879            .create(true)
6880            .write(true)
6881            .read(true)
6882            .open(&pack_path)
6883            .expect("create sparse pack");
6884        file.write_all(b"PACK").expect("sig");
6885        file.write_all(&2u32.to_be_bytes()).expect("ver");
6886        file.write_all(&1u32.to_be_bytes()).expect("count");
6887        let object_offset: u64 = 1 << 32;
6888        file.seek(SeekFrom::Start(object_offset))
6889            .expect("seek past 4GiB");
6890        let mut tail = Vec::new();
6891        append_whole_blob(&mut tail, data);
6892        file.write_all(&tail).expect("object at huge offset");
6893        let trailer = sha1_trailer_for_open_file(&mut file).expect("trailer hash");
6894        file.write_all(trailer.as_bytes()).expect("append trailer");
6895        file.flush().expect("flush");
6896        let file_len = file.metadata().expect("meta").len();
6897        drop(file);
6898        assert!(file_len > object_offset);
6899
6900        let idx_path = pack_path.with_extension("idx");
6901        write_v2_pack_index_with_trailer(
6902            &idx_path,
6903            &[(oid, object_offset, 0)],
6904            trailer.as_bytes(),
6905            20,
6906            None,
6907        )
6908        .expect("idx");
6909
6910        let idx = read_pack_index(&idx_path).expect("parse idx");
6911        assert_eq!(idx.offset_at(0).expect("off"), object_offset);
6912        let got = read_object_from_pack(&idx, &oid).expect("read at large offset");
6913        assert_eq!(got.data, data);
6914    }
6915
6916    fn synthetic_index_entries(n: u32) -> Vec<(ObjectId, u64, u32)> {
6917        (0..n)
6918            .map(|i| {
6919                let digest = HashAlgo::Sha1.checksum_digest(&i.to_be_bytes());
6920                let oid = ObjectId::from_bytes(&digest).expect("oid");
6921                let offset = if i % 7 == 0 {
6922                    (1u64 << 31) + u64::from(i)
6923                } else {
6924                    12 + u64::from(i) * 40
6925                };
6926                (oid, offset, i.wrapping_mul(2_654_435_761))
6927            })
6928            .collect()
6929    }
6930
6931    #[test]
6932    fn index_entries_sort_in_place_on_any_thread_count() {
6933        for n in [0_u32, 1, 300, 20_000] {
6934            let mut reference = synthetic_index_entries(n);
6935            reference.sort_by(|a, b| a.0.as_bytes().cmp(b.0.as_bytes()));
6936            let mut fanout_ref = [0u32; 256];
6937            for (oid, _, _) in &reference {
6938                for slot in &mut fanout_ref[usize::from(oid.as_bytes()[0])..] {
6939                    *slot += 1;
6940                }
6941            }
6942            for threads in [None, Some(1), Some(2), Some(3), Some(8)] {
6943                let mut entries = synthetic_index_entries(n);
6944                let fanout = sort_index_entries(&mut entries, threads);
6945                assert_eq!(entries, reference, "n={n} threads={threads:?}");
6946                assert_eq!(fanout, fanout_ref, "n={n} threads={threads:?}");
6947            }
6948        }
6949    }
6950
6951    #[test]
6952    fn index_written_in_place_reads_back_every_row_with_large_offsets() {
6953        let tmp = tempfile::tempdir().expect("tempdir");
6954        let idx_path = tmp.path().join("p.idx");
6955        let rows = synthetic_index_entries(20_000);
6956        let trailer = [0x5a_u8; 20];
6957        let mut entries = rows.clone();
6958        write_v2_pack_index_sorting(&idx_path, &mut entries, &trailer, 20, Some(4))
6959            .expect("write idx");
6960        let idx = read_pack_index(&idx_path).expect("read idx");
6961        assert_eq!(idx.len(), rows.len());
6962        let mut want = rows;
6963        want.sort_by(|a, b| a.0.as_bytes().cmp(b.0.as_bytes()));
6964        for (pos, (oid, offset, crc)) in want.iter().enumerate() {
6965            assert_eq!(idx.oid_at(pos), oid.as_bytes(), "row {pos}");
6966            assert_eq!(idx.offset_at(pos).expect("offset"), *offset, "row {pos}");
6967            assert_eq!(idx.crc32_at(pos).expect("crc"), *crc, "row {pos}");
6968        }
6969
6970        let mut bad = vec![(ObjectId::from_bytes(&[1; 32]).expect("oid"), 12, 0)];
6971        assert!(matches!(
6972            write_v2_pack_index_sorting(&idx_path, &mut bad, &trailer, 20, None),
6973            Err(Error::CorruptObject(_))
6974        ));
6975        assert!(matches!(
6976            write_v2_pack_index_sorting(&idx_path, &mut entries, &trailer[..19], 20, None),
6977            Err(Error::CorruptObject(_))
6978        ));
6979    }
6980
6981    /// Hash pack bytes already written to `file` (from offset 0 through current EOF) for a SHA-1 trailer.
6982    fn sha1_trailer_for_open_file(file: &mut std::fs::File) -> Result<ObjectId> {
6983        use std::io::{Read, Seek, SeekFrom};
6984        let len = file.metadata().map_err(Error::Io)?.len();
6985        file.seek(SeekFrom::Start(0)).map_err(Error::Io)?;
6986        let mut hasher = HashAlgo::Sha1.checksum_hasher();
6987        let mut buf = [0u8; 64 * 1024];
6988        let mut left = len;
6989        while left > 0 {
6990            let chunk = left.min(buf.len() as u64) as usize;
6991            let n = file.read(&mut buf[..chunk]).map_err(Error::Io)?;
6992            if n == 0 {
6993                break;
6994            }
6995            hasher.update(&buf[..n]);
6996            left -= n as u64;
6997        }
6998        let digest = hasher.finalize();
6999        ObjectId::from_bytes(&digest).map_err(|e| Error::InvalidObjectId(format!("{e}")))
7000    }
7001
7002    #[test]
7003    fn cleared_cache_sees_removed_packs_disappear() {
7004        let _guard = pack_cache_test_guard();
7005        clear_pack_cache();
7006        let tmp = tempfile::tempdir().expect("tempdir");
7007        let objects = tmp.path().join("objects");
7008        let (pack, oid, off) = single_blob_pack(b"ephemeral");
7009        install_synthetic_pack(&objects, "gone", &pack, &[(oid, off)]);
7010        read_object_from_packs(&objects, &oid).expect("warm cache");
7011
7012        let pack_path = objects.join("pack/gone.pack");
7013        let idx_path = objects.join("pack/gone.idx");
7014        std::fs::remove_file(&pack_path).expect("remove pack");
7015        std::fs::remove_file(&idx_path).expect("remove idx");
7016        clear_pack_cache();
7017
7018        let indexes = read_local_pack_indexes_cached(&objects).expect("rescan");
7019        assert!(
7020            indexes.is_empty(),
7021            "after clear, listing should not retain removed packs"
7022        );
7023    }
7024
7025    #[test]
7026    fn repack_while_sibling_odb_holds_store_reprepares_on_miss() {
7027        use crate::odb::Odb;
7028
7029        let _guard = pack_cache_test_guard();
7030        clear_pack_cache();
7031        let tmp = tempfile::tempdir().expect("tempdir");
7032        let objects = tmp.path().join("objects");
7033        let (pack_old, oid_old, off_old) = single_blob_pack(b"before");
7034        install_synthetic_pack(&objects, "old", &pack_old, &[(oid_old, off_old)]);
7035        let odb_a = Odb::new(&objects);
7036        let odb_b = Odb::new(&objects);
7037        assert!(
7038            !std::sync::Arc::ptr_eq(odb_a.pack_store().unwrap(), odb_b.pack_store().unwrap()),
7039            "sibling Odbs must have distinct pack stores"
7040        );
7041        odb_b.read(&oid_old).expect("warm b store");
7042
7043        std::fs::remove_file(objects.join("pack/old.pack")).expect("rm pack");
7044        std::fs::remove_file(objects.join("pack/old.idx")).expect("rm idx");
7045        let (pack_new, oid_new, off_new) = single_blob_pack(b"after");
7046        install_synthetic_pack(&objects, "new", &pack_new, &[(oid_new, off_new)]);
7047        filetime::set_file_mtime(objects.join("pack"), filetime::FileTime::now())
7048            .expect("bump pack dir mtime for reprepare");
7049        odb_a.invalidate_packs();
7050
7051        // B's store is unchanged by A's invalidation; a miss on the new oid rescans pack/.
7052        let got = odb_b.read(&oid_new).expect("b reprepares after miss");
7053        assert_eq!(got.data.as_slice(), b"after");
7054    }
7055
7056    #[test]
7057    fn fresh_store_does_not_trust_a_listing_left_by_another_store() {
7058        let dir = tempfile::tempdir().expect("tempdir");
7059        let dir = dir.path();
7060        git(dir, &["init", "-q"]);
7061        git(dir, &["commit", "-q", "--allow-empty", "-m", "a"]);
7062        git(dir, &["repack", "-adq"]);
7063        let objects = dir.join(".git/objects");
7064        let pack_names = |indexes: Vec<Arc<PackIndex>>| -> Vec<PathBuf> {
7065            indexes
7066                .into_iter()
7067                .map(|idx| idx.pack_path.clone())
7068                .collect()
7069        };
7070
7071        let first = Arc::new(crate::pack_store::PackStore::new(objects.clone()));
7072        let before = crate::pack_store::PackStore::with_context(first, || {
7073            read_local_pack_indexes_cached(&objects)
7074        })
7075        .expect("first listing");
7076
7077        // Another process replaces the pack.
7078        git(dir, &["commit", "-q", "--allow-empty", "-m", "b"]);
7079        git(dir, &["repack", "-adq"]);
7080
7081        let second = Arc::new(crate::pack_store::PackStore::new(objects.clone()));
7082        let after = crate::pack_store::PackStore::with_context(second, || {
7083            read_local_pack_indexes_cached(&objects)
7084        })
7085        .expect("second listing");
7086        let (before, after) = (pack_names(before), pack_names(after));
7087        assert_ne!(before, after);
7088        assert!(after.iter().all(|p| p.is_file()), "{after:?}");
7089    }
7090}
7091#[cfg(test)]
7092mod cached_lookup_tests {
7093    use super::*;
7094    use std::fs;
7095    use std::process::Command;
7096    use tempfile::TempDir;
7097
7098    fn git(dir: &Path, args: &[&str]) {
7099        let out = Command::new("git") // hygiene: git fixture comparison in cfg(test) module
7100            .current_dir(dir)
7101            .args(args)
7102            .env("GIT_CONFIG_GLOBAL", "/dev/null")
7103            .env("GIT_CONFIG_SYSTEM", "/dev/null")
7104            .env("GIT_AUTHOR_NAME", "t")
7105            .env("GIT_AUTHOR_EMAIL", "t@example.com")
7106            .env("GIT_COMMITTER_NAME", "t")
7107            .env("GIT_COMMITTER_EMAIL", "t@example.com")
7108            .output()
7109            .expect("spawn git");
7110        assert!(
7111            out.status.success(),
7112            "git {:?}: {}",
7113            args,
7114            String::from_utf8_lossy(&out.stderr)
7115        );
7116    }
7117
7118    fn repack_all(dir: &Path) {
7119        git(dir, &["repack", "-a", "-d"]);
7120    }
7121
7122    fn git_stdout(dir: &Path, args: &[&str], stdin: Option<&str>) -> String {
7123        use std::io::Write;
7124        let mut child = Command::new("git") // hygiene: git fixture comparison in cfg(test) module
7125            .current_dir(dir)
7126            .args(args)
7127            .env("GIT_CONFIG_GLOBAL", "/dev/null")
7128            .env("GIT_CONFIG_SYSTEM", "/dev/null")
7129            .stdin(std::process::Stdio::piped())
7130            .stdout(std::process::Stdio::piped())
7131            .spawn()
7132            .expect("spawn git");
7133        if let Some(input) = stdin {
7134            child
7135                .stdin
7136                .as_mut()
7137                .expect("stdin")
7138                .write_all(input.as_bytes())
7139                .expect("write stdin");
7140        }
7141        let out = child.wait_with_output().expect("git output");
7142        assert!(out.status.success(), "git {args:?}");
7143        String::from_utf8(out.stdout)
7144            .expect("utf-8")
7145            .trim()
7146            .to_owned()
7147    }
7148
7149    fn oid_of(dir: &Path, rev: &str) -> ObjectId {
7150        ObjectId::from_hex(&git_stdout(dir, &["rev-parse", rev], None)).expect("oid")
7151    }
7152
7153    #[cfg_attr(
7154        not(feature = "full-tests"),
7155        ignore = "starts system git or is heavy; run with --features full-tests"
7156    )]
7157    #[test]
7158    fn packed_commit_header_block_parses_like_the_whole_commit() {
7159        let _guard = pack_cache_test_guard();
7160        clear_pack_cache();
7161        let dir = TempDir::new().unwrap();
7162        let root = dir.path();
7163        git(root, &["init", "-q"]);
7164        fs::write(root.join("f"), "one\n").unwrap();
7165        git(root, &["add", "f"]);
7166        git(root, &["commit", "-qm", "first"]);
7167        fs::write(root.join("f"), "two\n").unwrap();
7168        let long_message = format!("second\n\n{}", "a long body line\n".repeat(200));
7169        git(root, &["commit", "-qam", &long_message]);
7170        git(root, &["tag", "-a", "-m", "annotated", "v1"]);
7171        let signature: String = (0..60)
7172            .map(|i| format!(" signature line {i:03} with some padding text\n"))
7173            .collect();
7174        let signed_body = format!(
7175            "tree {}\nparent {}\nauthor t <t@example.com> 1 +0000\n\
7176             committer t <t@example.com> 2 +0000\ngpgsig -----BEGIN-----\n{signature} \
7177             -----END-----\n\nsigned\n",
7178            oid_of(root, "HEAD^{tree}"),
7179            oid_of(root, "HEAD"),
7180        );
7181        let signed = git_stdout(
7182            root,
7183            &["hash-object", "-t", "commit", "-w", "--stdin"],
7184            Some(&signed_body),
7185        );
7186        git(root, &["update-ref", "refs/heads/signed", &signed]);
7187        git(root, &["repack", "-a", "-d", "--window=0"]);
7188        fs::write(root.join("f"), "three\n").unwrap();
7189        git(root, &["commit", "-qam", "second pack"]);
7190        git(root, &["repack", "-d", "--window=0"]);
7191        fs::write(root.join("f"), "four\n").unwrap();
7192        git(root, &["commit", "-qam", "loose"]);
7193
7194        let objects = root.join(".git/objects");
7195        let mut buf = Vec::new();
7196        for rev in ["HEAD~3", "HEAD~2", "HEAD~1"] {
7197            let oid = oid_of(root, rev);
7198            assert!(
7199                read_packed_commit_header_block_into(&objects, &oid, &mut buf).unwrap(),
7200                "{rev} is a whole packed commit"
7201            );
7202            assert!(buf.ends_with(b"\n\n"), "{rev} ends at the blank line");
7203            let whole = git_stdout(root, &["cat-file", "commit", rev], None);
7204            let whole = crate::objects::CommitRef::parse(whole.as_bytes()).unwrap();
7205            let header = crate::objects::CommitRef::parse(&buf).unwrap();
7206            assert_eq!(header.tree(), whole.tree(), "{rev}");
7207            assert_eq!(header.parents(), whole.parents(), "{rev}");
7208            assert_eq!(header.committer_raw(), whole.committer_raw(), "{rev}");
7209            assert!(header.message_bytes().is_empty(), "{rev}");
7210        }
7211        let signed = ObjectId::from_hex(&signed).unwrap();
7212        for (oid, why) in [
7213            (signed, "headers longer than the inflate budget"),
7214            (oid_of(root, "v1"), "an annotated tag"),
7215            (oid_of(root, "HEAD~1^{tree}"), "a packed tree"),
7216            (oid_of(root, "HEAD"), "a loose commit"),
7217        ] {
7218            assert!(
7219                !read_packed_commit_header_block_into(&objects, &oid, &mut buf).unwrap(),
7220                "{why}"
7221            );
7222        }
7223    }
7224
7225    fn init_repo_with_pack(dir: &Path) -> ObjectId {
7226        git(dir, &["init", "-q"]);
7227        std::fs::write(dir.join("blob.txt"), b"pack cached lookup fixture\n").unwrap();
7228        git(dir, &["add", "blob.txt"]);
7229        git(dir, &["commit", "-qm", "c"]);
7230        repack_all(dir);
7231        let hex = Command::new("git") // hygiene: git fixture comparison in cfg(test) module
7232            .current_dir(dir)
7233            .args(["rev-parse", "HEAD^{tree}"])
7234            .env("GIT_CONFIG_GLOBAL", "/dev/null")
7235            .env("GIT_CONFIG_SYSTEM", "/dev/null")
7236            .output()
7237            .unwrap();
7238        let tree_hex = String::from_utf8(hex.stdout).unwrap();
7239        ObjectId::from_hex(tree_hex.trim()).unwrap()
7240    }
7241
7242    fn pack_dir(objects: &Path) -> PathBuf {
7243        objects.join("pack")
7244    }
7245
7246    fn v2_idx_path(objects: &Path) -> PathBuf {
7247        pack_dir(objects)
7248            .read_dir()
7249            .unwrap()
7250            .filter_map(|e| e.ok())
7251            .map(|e| e.path())
7252            .find(|p| p.extension().is_some_and(|x| x == "idx"))
7253            .expect("pack idx")
7254    }
7255
7256    /// Replace the repository's v2 `.idx` with an equivalent v1-format index.
7257    fn convert_pack_idx_to_v1(objects: &Path) -> PathBuf {
7258        let idx_path = v2_idx_path(objects);
7259        let parsed = read_pack_index(&idx_path).expect("v2 idx");
7260        let mut body = Vec::new();
7261        for slot in parsed.fanout {
7262            body.extend_from_slice(&slot.to_be_bytes());
7263        }
7264        for entry in parsed.iter() {
7265            body.extend_from_slice(&(entry.offset().expect("off") as u32).to_be_bytes());
7266            body.extend_from_slice(entry.oid());
7267        }
7268        body.extend_from_slice(HashAlgo::Sha1.checksum_digest(&body).as_slice());
7269        rewrite_test_file(&idx_path, &body);
7270        clear_pack_cache();
7271        idx_path
7272    }
7273
7274    fn rewrite_test_file(path: &Path, body: &[u8]) {
7275        let _ = fs::remove_file(path);
7276        fs::write(path, body).unwrap();
7277    }
7278
7279    fn synthetic_fanout_index(entries: &[(Vec<u8>, u64)]) -> PackIndex {
7280        let mut sorted = entries.to_vec();
7281        sorted.sort_by(|a, b| a.0.cmp(&b.0));
7282        let slices: Vec<&[u8]> = sorted.iter().map(|(o, _)| o.as_slice()).collect();
7283        let fanout = compute_fanout_from_oid_slices(&slices);
7284        let mut body = Vec::new();
7285        for f in fanout {
7286            body.extend_from_slice(&f.to_be_bytes());
7287        }
7288        for (oid, off) in sorted {
7289            body.extend_from_slice(&(off as u32).to_be_bytes());
7290            body.extend_from_slice(&oid);
7291        }
7292        body.extend_from_slice(HashAlgo::Sha1.checksum_digest(&body).as_slice());
7293        parse_pack_index_bytes(Path::new("synthetic.idx"), body, false).expect("v1 idx")
7294    }
7295
7296    #[test]
7297    fn find_offset_fanout_bucket_first_last_and_miss() {
7298        let first = vec![0x05u8; 20];
7299        let mut mid = vec![0x05u8; 20];
7300        mid[19] = 1;
7301        let mut last = vec![0x05u8; 20];
7302        last[19] = 2;
7303        let miss = vec![0x06u8; 20];
7304        let idx =
7305            synthetic_fanout_index(&[(first.clone(), 12), (mid.clone(), 24), (last.clone(), 36)]);
7306
7307        let oid_first = ObjectId::from_bytes(&first).unwrap();
7308        let oid_mid = ObjectId::from_bytes(&mid).unwrap();
7309        let oid_last = ObjectId::from_bytes(&last).unwrap();
7310        let oid_miss = ObjectId::from_bytes(&miss).unwrap();
7311
7312        assert_eq!(idx.find_offset(&oid_first).expect("f"), Some(12));
7313        assert_eq!(idx.find_offset(&oid_mid).expect("m"), Some(24));
7314        assert_eq!(idx.find_offset(&oid_last).expect("l"), Some(36));
7315        assert_eq!(idx.find_offset(&oid_miss).expect("miss"), None);
7316    }
7317
7318    #[cfg_attr(
7319        not(feature = "full-tests"),
7320        ignore = "starts system git or is heavy; run with --features full-tests"
7321    )]
7322    #[test]
7323    fn cached_lookup_hit_v2_idx() {
7324        let _guard = pack_cache_test_guard();
7325        clear_pack_cache();
7326        let dir = TempDir::new().unwrap();
7327        let tree = init_repo_with_pack(dir.path());
7328        let objects = dir.path().join(".git").join("objects");
7329        let got = read_object_from_packs(&objects, &tree).expect("read tree from v2 pack");
7330        assert_eq!(got.kind, ObjectKind::Tree);
7331        // Second read must hit the in-memory cache (same result, no reparsing).
7332        let again = read_object_from_packs(&objects, &tree).expect("cached read");
7333        assert_eq!(again.data, got.data);
7334    }
7335
7336    #[cfg_attr(
7337        not(feature = "full-tests"),
7338        ignore = "starts system git or is heavy; run with --features full-tests"
7339    )]
7340    #[test]
7341    fn cached_lookup_hit_v1_idx() {
7342        let _guard = pack_cache_test_guard();
7343        clear_pack_cache();
7344        let dir = TempDir::new().unwrap();
7345        let tree = init_repo_with_pack(dir.path());
7346        let objects = dir.path().join(".git").join("objects");
7347        let v1 = convert_pack_idx_to_v1(&objects);
7348        assert!(pack_index_is_v1(&v1));
7349        let idx = read_pack_index_cached(&v1).expect("parse v1 idx");
7350        let off = idx
7351            .find_offset(&tree)
7352            .expect("fanout lookup in v1 idx")
7353            .expect("off");
7354        assert!(off > 0);
7355        let obj = read_object_from_pack(&idx, &tree).expect("read via v1 index");
7356        assert_eq!(obj.kind, ObjectKind::Tree);
7357    }
7358
7359    #[cfg_attr(
7360        not(feature = "full-tests"),
7361        ignore = "starts system git or is heavy; run with --features full-tests"
7362    )]
7363    #[test]
7364    fn cached_lookup_miss() {
7365        let _guard = pack_cache_test_guard();
7366        clear_pack_cache();
7367        let dir = TempDir::new().unwrap();
7368        init_repo_with_pack(dir.path());
7369        let objects = dir.path().join(".git").join("objects");
7370        let miss = ObjectId::from_hex("deadbeefdeadbeefdeadbeefdeadbeefdeadbeef").unwrap();
7371        assert!(read_object_from_packs(&objects, &miss).is_err());
7372        assert!(
7373            try_read_object_from_local_packs_cached(&objects, &miss, || {
7374                Err(Error::ObjectNotFound(miss.to_hex()))
7375            })
7376            .unwrap()
7377            .is_none()
7378        );
7379    }
7380
7381    #[cfg_attr(
7382        not(feature = "full-tests"),
7383        ignore = "starts system git or is heavy; run with --features full-tests"
7384    )]
7385    #[test]
7386    fn cached_lookup_loose_only() {
7387        let _guard = pack_cache_test_guard();
7388        clear_pack_cache();
7389        let dir = TempDir::new().unwrap();
7390        git(dir.path(), &["init", "-q"]);
7391        let out = Command::new("git") // hygiene: git fixture comparison in cfg(test) module
7392            .current_dir(dir.path())
7393            .args(["hash-object", "-w", "--stdin"])
7394            .env("GIT_CONFIG_GLOBAL", "/dev/null")
7395            .env("GIT_CONFIG_SYSTEM", "/dev/null")
7396            .stdin(std::process::Stdio::piped())
7397            .stdout(std::process::Stdio::piped())
7398            .spawn()
7399            .and_then(|mut c| {
7400                use std::io::Write;
7401                c.stdin
7402                    .as_mut()
7403                    .unwrap()
7404                    .write_all(b"loose-only\n")
7405                    .unwrap();
7406                c.wait_with_output()
7407            })
7408            .unwrap();
7409        assert!(out.status.success());
7410        let oid = ObjectId::from_hex(String::from_utf8(out.stdout).unwrap().trim()).unwrap();
7411        let objects = dir.path().join(".git").join("objects");
7412        assert!(try_read_object_from_local_packs_cached(&objects, &oid, || {
7413            Err(Error::ObjectNotFound(oid.to_hex()))
7414        })
7415        .unwrap()
7416        .is_none());
7417    }
7418
7419    #[cfg_attr(
7420        not(feature = "full-tests"),
7421        ignore = "starts system git or is heavy; run with --features full-tests"
7422    )]
7423    #[test]
7424    fn cached_lookup_multiple_packs() {
7425        let _guard = pack_cache_test_guard();
7426        clear_pack_cache();
7427        let dir = TempDir::new().unwrap();
7428        git(dir.path(), &["init", "-q"]);
7429        std::fs::write(dir.path().join("a.txt"), b"a").unwrap();
7430        git(dir.path(), &["add", "a.txt"]);
7431        git(dir.path(), &["commit", "-qm", "a"]);
7432        repack_all(dir.path());
7433        let oid_a = ObjectId::from_hex(
7434            String::from_utf8(
7435                Command::new("git") // hygiene: git fixture comparison in cfg(test) module // hygiene: git fixture comparison in cfg(test) module
7436                    .current_dir(dir.path())
7437                    .args(["rev-parse", "HEAD:a.txt"])
7438                    .env("GIT_CONFIG_GLOBAL", "/dev/null")
7439                    .env("GIT_CONFIG_SYSTEM", "/dev/null")
7440                    .output()
7441                    .unwrap()
7442                    .stdout,
7443            )
7444            .unwrap()
7445            .trim(),
7446        )
7447        .unwrap();
7448        std::fs::write(dir.path().join("b.txt"), b"b").unwrap();
7449        git(dir.path(), &["add", "b.txt"]);
7450        git(dir.path(), &["commit", "-qm", "b"]);
7451        git(dir.path(), &["repack", "-d"]);
7452        let oid_b = ObjectId::from_hex(
7453            String::from_utf8(
7454                Command::new("git") // hygiene: git fixture comparison in cfg(test) module // hygiene: git fixture comparison in cfg(test) module
7455                    .current_dir(dir.path())
7456                    .args(["rev-parse", "HEAD:b.txt"])
7457                    .env("GIT_CONFIG_GLOBAL", "/dev/null")
7458                    .env("GIT_CONFIG_SYSTEM", "/dev/null")
7459                    .output()
7460                    .unwrap()
7461                    .stdout,
7462            )
7463            .unwrap()
7464            .trim(),
7465        )
7466        .unwrap();
7467        let objects = dir.path().join(".git").join("objects");
7468        let pack_count = pack_dir(&objects)
7469            .read_dir()
7470            .unwrap()
7471            .filter_map(|e| e.ok())
7472            .filter(|e| e.path().extension().is_some_and(|x| x == "pack"))
7473            .count();
7474        assert!(
7475            pack_count >= 2,
7476            "expected multiple pack files, got {pack_count}"
7477        );
7478        let obj_a = read_object_from_packs(&objects, &oid_a).expect("pack a");
7479        let obj_b = read_object_from_packs(&objects, &oid_b).expect("pack b");
7480        assert_eq!(obj_a.kind, ObjectKind::Blob);
7481        assert_eq!(obj_b.kind, ObjectKind::Blob);
7482    }
7483
7484    #[cfg_attr(
7485        not(feature = "full-tests"),
7486        ignore = "starts system git or is heavy; run with --features full-tests"
7487    )]
7488    #[test]
7489    fn packed_delta_base_oid_uses_cached_index() {
7490        let _guard = pack_cache_test_guard();
7491        clear_pack_cache();
7492        let dir = TempDir::new().unwrap();
7493        init_repo_with_pack(dir.path());
7494        let objects = dir.path().join(".git").join("objects");
7495        let miss = ObjectId::from_hex("0101010101010101010101010101010101010101").unwrap();
7496        assert!(packed_delta_base_oid(&objects, &miss).unwrap().is_none());
7497    }
7498
7499    #[test]
7500    fn content_addressed_pack_bytes_skip_stat_revalidation() {
7501        let _guard = pack_cache_test_guard();
7502        clear_pack_cache();
7503        let dir = tempfile::tempdir().expect("tempdir");
7504        let pack_path = dir
7505            .path()
7506            .join("pack-1111111111111111111111111111111111111111.pack");
7507        let v1 = b"PACK\x00\x00\x00\x02\x00\x00\x00\x00";
7508        std::fs::write(&pack_path, v1).expect("write v1");
7509        let cached = read_pack_bytes_cached(&pack_path).expect("prime cache");
7510        assert_eq!(&cached[..], v1);
7511
7512        let v2 = b"PACK-replaced-by-repack";
7513        std::fs::write(&pack_path, v2).expect("overwrite on disk");
7514        let again = read_pack_bytes_cached(&pack_path).expect("cached without stat");
7515        assert_eq!(
7516            &again[..],
7517            v1,
7518            "content-addressed name pins cached bytes in-process"
7519        );
7520    }
7521
7522    #[test]
7523    fn temporary_pack_bytes_served_from_cache_until_cleared() {
7524        let _guard = pack_cache_test_guard();
7525        clear_pack_cache();
7526        let dir = tempfile::tempdir().expect("tempdir");
7527        for pack_path in [
7528            dir.path().join("tmp_pack_abc123.pack"),
7529            dir.path().join("pack-custom.pack"),
7530        ] {
7531            clear_pack_cache();
7532            let v1 = b"PACK-temp-v1";
7533            std::fs::write(&pack_path, v1).expect("write v1");
7534            let _ = read_pack_bytes_cached(&pack_path).expect("prime cache");
7535
7536            let v2 = b"PACK-temp-v2-longer-body";
7537            std::fs::write(&pack_path, v2).expect("overwrite pack");
7538            let stale = read_pack_bytes_cached(&pack_path).expect("cache hit");
7539            assert_eq!(
7540                &stale[..],
7541                v1,
7542                "{} serves cached bytes without stat on hit",
7543                pack_path.display()
7544            );
7545            clear_pack_cache();
7546            let fresh = read_pack_bytes_cached(&pack_path).expect("after clear");
7547            assert_eq!(
7548                &fresh[..],
7549                v2,
7550                "{} reloads from disk after clear",
7551                pack_path.display()
7552            );
7553        }
7554    }
7555    #[cfg_attr(
7556        not(feature = "full-tests"),
7557        ignore = "starts system git or is heavy; run with --features full-tests"
7558    )]
7559    #[test]
7560    fn system_git_repack_requires_clear_pack_cache_for_fresh_listing() {
7561        let _guard = pack_cache_test_guard();
7562        clear_pack_cache();
7563        let dir = TempDir::new().unwrap();
7564        init_repo_with_pack(dir.path());
7565        let objects = dir.path().join(".git").join("objects");
7566        read_local_pack_indexes_cached(&objects).expect("warm listing");
7567        for entry in pack_dir(&objects).read_dir().unwrap().flatten() {
7568            let path = entry.path();
7569            if path.extension().is_some_and(|e| e == "pack" || e == "idx") {
7570                fs::remove_file(path).unwrap();
7571            }
7572        }
7573        clear_pack_cache();
7574        let indexes =
7575            read_local_pack_indexes_cached(&objects).expect("list after repack-like delete");
7576        assert!(
7577            indexes.is_empty(),
7578            "callers must clear_pack_cache after removing pack files"
7579        );
7580    }
7581
7582    fn test_oid(n: u8) -> ObjectId {
7583        ObjectId::from_hex(&format!("{n:040x}")).unwrap()
7584    }
7585
7586    fn max_delta_map_chain_edges(map: &HashMap<ObjectId, ObjectId>) -> usize {
7587        let value_set: HashSet<ObjectId> = map.values().copied().collect();
7588        let tips: Vec<ObjectId> = map
7589            .keys()
7590            .copied()
7591            .filter(|k| !value_set.contains(k))
7592            .collect();
7593        let mut max = 0usize;
7594        for tip in tips {
7595            let mut edges = 0usize;
7596            let mut cur = tip;
7597            let mut seen = HashSet::new();
7598            while seen.insert(cur) {
7599                let Some(&b) = map.get(&cur) else {
7600                    break;
7601                };
7602                edges += 1;
7603                cur = b;
7604            }
7605            max = max.max(edges);
7606        }
7607        max
7608    }
7609
7610    #[test]
7611    fn apply_delta_depth_limit_hits_exact_cap_on_seventy_edge_chain() {
7612        let n = 70usize;
7613        let oids: Vec<ObjectId> = (0..n as u8).map(test_oid).collect();
7614        let mut map: HashMap<ObjectId, ObjectId> = HashMap::new();
7615        for i in 1..n {
7616            map.insert(oids[i], oids[i - 1]);
7617        }
7618        let original = map.clone();
7619        apply_delta_depth_limit(&mut map, 50);
7620        assert_eq!(max_delta_map_chain_edges(&map), 50);
7621        let preserved = map
7622            .iter()
7623            .filter(|(t, b)| original.get(t) == Some(b))
7624            .count();
7625        assert!(
7626            preserved > 40,
7627            "non-cut edges should keep original bases (preserved {preserved})"
7628        );
7629    }
7630
7631    #[test]
7632    fn delta_cycle_breaks_drops_smallest_oid_of_each_cycle_only() {
7633        let o = |n: u8| test_oid(n);
7634        // 1 <-> 2 is a 2-cycle; 5 -> 4 -> 3 -> 5 a 3-cycle with 6 hanging off it;
7635        // 8 -> 7 is a plain chain.
7636        let map = HashMap::from([
7637            (o(1), o(2)),
7638            (o(2), o(1)),
7639            (o(3), o(5)),
7640            (o(4), o(3)),
7641            (o(5), o(4)),
7642            (o(6), o(4)),
7643            (o(8), o(7)),
7644        ]);
7645        let mut keys: Vec<ObjectId> = map.keys().copied().collect();
7646        for order in [false, true] {
7647            keys.sort();
7648            if order {
7649                keys.reverse();
7650            }
7651            let mut breaks = delta_cycle_breaks(keys.iter().copied(), |oid| map.get(oid).copied());
7652            breaks.sort();
7653            assert_eq!(breaks, vec![o(1), o(3)], "independent of walk order");
7654        }
7655    }
7656
7657    #[test]
7658    fn delta_cycle_breaks_handles_self_delta_and_acyclic_maps() {
7659        let a = test_oid(9);
7660        assert_eq!(delta_cycle_breaks([a], |_| Some(a)), vec![a]);
7661        let chain: HashMap<ObjectId, ObjectId> =
7662            (1..60u8).map(|i| (test_oid(i), test_oid(i - 1))).collect();
7663        assert!(delta_cycle_breaks(chain.keys().copied(), |o| chain.get(o).copied()).is_empty());
7664    }
7665
7666    #[test]
7667    fn apply_delta_depth_limit_breaks_cycles_shorter_than_depth() {
7668        let (a, b, c, d) = (test_oid(1), test_oid(2), test_oid(3), test_oid(4));
7669        // a <-> b, plus c -> b and d -> c hanging off the cycle.
7670        let mut map = HashMap::from([(a, b), (b, a), (c, b), (d, c)]);
7671        apply_delta_depth_limit(&mut map, 50);
7672        assert_eq!(map, HashMap::from([(b, a), (c, b), (d, c)]));
7673        assert_eq!(max_delta_map_chain_edges(&map), 3);
7674    }
7675
7676    #[test]
7677    fn apply_delta_depth_limit_leaves_short_chains_untouched() {
7678        let a = test_oid(1);
7679        let b = test_oid(2);
7680        let c = test_oid(3);
7681        let mut map = HashMap::from([(b, a), (c, b)]);
7682        apply_delta_depth_limit(&mut map, 50);
7683        assert_eq!(map.get(&b), Some(&a));
7684        assert_eq!(map.get(&c), Some(&b));
7685        assert_eq!(max_delta_map_chain_edges(&map), 2);
7686    }
7687
7688    #[test]
7689    fn apply_delta_depth_limit_caps_long_chain_and_preserves_blobs() {
7690        let n = 120usize;
7691        let oids: Vec<ObjectId> = (0..n as u8).map(test_oid).collect();
7692        let mut blobs: HashMap<ObjectId, Vec<u8>> = HashMap::new();
7693        blobs.insert(oids[0], b"base".to_vec());
7694        for i in 1..n {
7695            let mut next = blobs[&oids[i - 1]].clone();
7696            next.push(i as u8);
7697            blobs.insert(oids[i], next);
7698        }
7699        let mut map: HashMap<ObjectId, ObjectId> = HashMap::new();
7700        for i in 1..n {
7701            map.insert(oids[i], oids[i - 1]);
7702        }
7703        let original = map.clone();
7704        apply_delta_depth_limit(&mut map, 50);
7705        assert_eq!(
7706            max_delta_map_chain_edges(&map),
7707            50,
7708            "long chain must cap at exactly max_depth edges"
7709        );
7710        let preserved = map
7711            .iter()
7712            .filter(|(target, base)| original.get(target) == Some(base))
7713            .count();
7714        assert!(
7715            preserved > 60,
7716            "segment links should stay on original bases (preserved {preserved} of {})",
7717            map.len()
7718        );
7719
7720        use crate::delta_encode::encode_prefix_extension_delta;
7721        use crate::unpack_objects::apply_delta;
7722
7723        for (&target, &base) in &map {
7724            let delta = encode_prefix_extension_delta(&blobs[&base], &blobs[&target]).unwrap();
7725            let got = apply_delta(&blobs[&base], &delta).unwrap();
7726            assert_eq!(got, blobs[&target]);
7727        }
7728    }
7729
7730    #[cfg_attr(
7731        not(feature = "full-tests"),
7732        ignore = "starts system git or is heavy; run with --features full-tests"
7733    )]
7734    #[test]
7735    fn v2_idx_crc_matches_crc32fast_over_entry_bytes() {
7736        let _guard = pack_cache_test_guard();
7737        clear_pack_cache();
7738        let dir = TempDir::new().unwrap();
7739        let tree = init_repo_with_pack(dir.path());
7740        let objects = dir.path().join(".git").join("objects");
7741        let idx_path = v2_idx_path(&objects);
7742        let idx = read_pack_index(&idx_path).expect("read idx");
7743        let pack_bytes = std::fs::read(&idx.pack_path).expect("read pack");
7744        for entry in idx.iter() {
7745            let crc = entry.crc32().expect("v2 idx entry crc");
7746            let off = entry.offset().expect("entry off");
7747            let start = off as usize;
7748            let end = super::pack_entry_raw_end(&idx, &pack_bytes, off)
7749                .expect("entry end")
7750                .expect("end");
7751            assert_eq!(crc32fast::hash(&pack_bytes[start..end]), crc);
7752        }
7753        let _ = tree;
7754    }
7755
7756    #[cfg_attr(
7757        not(feature = "full-tests"),
7758        ignore = "starts system git or is heavy; run with --features full-tests"
7759    )]
7760    #[test]
7761    fn packed_full_object_slice_returns_on_disk_bytes() {
7762        let _guard = pack_cache_test_guard();
7763        clear_pack_cache();
7764        let dir = TempDir::new().unwrap();
7765        let tree = init_repo_with_pack(dir.path());
7766        let objects = dir.path().join(".git").join("objects");
7767        let idx = read_pack_index(&v2_idx_path(&objects)).expect("idx");
7768        let pack_bytes = std::fs::read(&idx.pack_path).expect("pack");
7769        let off = idx.find_offset(&tree).expect("tree in pack").expect("off");
7770        let start = off as usize;
7771        let end = super::pack_entry_raw_end(&idx, &pack_bytes, off)
7772            .expect("entry end")
7773            .expect("end");
7774        let expected = &pack_bytes[start..end];
7775        let slice = packed_full_object_slice(&objects, &tree)
7776            .expect("lookup")
7777            .expect("full tree slice");
7778        assert_eq!(slice, expected);
7779    }
7780
7781    fn touch_mtime(path: &Path, when: filetime::FileTime) {
7782        filetime::set_file_mtime(path, when).expect("set mtime");
7783    }
7784
7785    #[cfg_attr(
7786        not(feature = "full-tests"),
7787        ignore = "starts system git or is heavy; run with --features full-tests"
7788    )]
7789    #[test]
7790    fn packed_full_object_slice_crc_mismatch_falls_back_to_redundant_pack() {
7791        let _guard = pack_cache_test_guard();
7792        clear_pack_cache();
7793        let dir = TempDir::new().unwrap();
7794        let tree = init_repo_with_pack(dir.path());
7795        let objects = dir.path().join(".git").join("objects");
7796        let pack_dir = pack_dir(&objects);
7797        let primary_idx = read_pack_index(&v2_idx_path(&objects)).expect("idx");
7798        let primary_pack = primary_idx.pack_path.clone();
7799        let primary_idx_path = primary_idx.idx_path.clone();
7800
7801        let redundant_pack = pack_dir.join("pack-redundant.pack");
7802        let redundant_idx = pack_dir.join("pack-redundant.idx");
7803        fs::copy(&primary_pack, &redundant_pack).expect("copy pack");
7804        fs::copy(&primary_idx_path, &redundant_idx).expect("copy idx");
7805
7806        let older = filetime::FileTime::from_unix_time(1_700_000_000, 0);
7807        let newer = filetime::FileTime::from_unix_time(1_700_000_100, 0);
7808        touch_mtime(&primary_pack, older);
7809        touch_mtime(&primary_idx_path, older);
7810        touch_mtime(&redundant_pack, newer);
7811        touch_mtime(&redundant_idx, newer);
7812        clear_pack_cache();
7813
7814        let off = primary_idx
7815            .find_offset(&tree)
7816            .expect("tree offset")
7817            .expect("off");
7818        let start = off as usize;
7819        let end = super::pack_entry_raw_end(&primary_idx, &fs::read(&primary_pack).unwrap(), off)
7820            .expect("entry end")
7821            .expect("end");
7822        let flip = start + (end - start) / 2;
7823        let mut corrupt = fs::read(&redundant_pack).expect("read redundant");
7824        corrupt[flip] ^= 0x01;
7825        rewrite_test_file(&redundant_pack, &corrupt);
7826        clear_pack_cache();
7827
7828        let recorded_crc = read_pack_index(&redundant_idx)
7829            .expect("redundant idx")
7830            .crc32_for_pack_offset(off)
7831            .expect("crc")
7832            .expect("crc value");
7833        let corrupt_slice = &corrupt[start..end];
7834        assert_ne!(
7835            crc32fast::hash(corrupt_slice),
7836            recorded_crc,
7837            "test must flip bytes without updating idx CRC"
7838        );
7839        assert!(
7840            packed_full_object_slice(&objects, &tree)
7841                .expect("lookup")
7842                .is_some(),
7843            "valid redundant pack should serve the slice after CRC rejects corrupt copy"
7844        );
7845
7846        let obj = read_object_from_packs(&objects, &tree).expect("read tree");
7847        assert_eq!(obj.kind, ObjectKind::Tree);
7848        let slice = packed_full_object_slice(&objects, &tree)
7849            .expect("lookup")
7850            .expect("slice");
7851        let good_pack = fs::read(&primary_pack).expect("primary pack");
7852        assert_eq!(&slice, &good_pack[start..end]);
7853    }
7854
7855    #[test]
7856    fn wanted_index_entries_lookups_match_a_full_scan() {
7857        let tmp = TempDir::new().unwrap();
7858        let bare = grit_test_support::git_packed_history(tmp.path(), 30);
7859        let objects = bare.join("objects");
7860        let indexes = read_local_pack_indexes_cached(&objects).unwrap();
7861        assert_eq!(indexes.len(), 1);
7862        let idx = &indexes[0];
7863        let all: Vec<(ObjectId, u64)> = idx
7864            .iter()
7865            .map(|e| (ObjectId::from_bytes(e.oid()).unwrap(), e.offset().unwrap()))
7866            .collect();
7867        assert!(all.len() > 64);
7868        let absent = ObjectId::from_hex("0000000000000000000000000000000000000001").unwrap();
7869        let sorted = |mut v: Vec<(ObjectId, u64)>| {
7870            v.sort();
7871            v
7872        };
7873
7874        let few: HashSet<ObjectId> = [all[3].0, all[all.len() - 1].0, absent].into();
7875        let looked_up = wanted_index_entries(idx, few.iter(), |oid| few.contains(oid)).unwrap();
7876        assert_eq!(sorted(looked_up), sorted(vec![all[3], all[all.len() - 1]]));
7877
7878        let many: HashSet<ObjectId> = all.iter().map(|(oid, _)| *oid).chain([absent]).collect();
7879        let scanned = wanted_index_entries(idx, many.iter(), |oid| many.contains(oid)).unwrap();
7880        assert_eq!(sorted(scanned), sorted(all));
7881
7882        let slots = locate_packed_objects(&objects, &few.iter().copied().collect()).unwrap();
7883        assert_eq!(slots.len(), 2);
7884        assert!(!slots.contains_key(&absent));
7885    }
7886}