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