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
6#![allow(dead_code)]
7
8pub(crate) use crate::pack_index::parse_pack_index_bytes;
9pub use crate::pack_index::{PackIndex, PackIndexEntryRef};
10
11#[cfg(test)]
12use crate::pack_index::compute_fanout_from_oid_slices;
13
14use crate::error::{Error, Result};
15use crate::git_path::CanonicalizeExt;
16use crate::hash::{hash_object, verify_trailer};
17use crate::objects::{HashAlgo, Object, ObjectId, ObjectInfo, ObjectKind};
18use crate::odb::BoundedRead;
19pub use crate::pack_map::PackData;
20use crate::pack_map::ResidentBudget;
21use crate::unpack_objects::{apply_delta_into, apply_delta_prefix_into};
22use crate::zlib_inflate::ZlibInflateScratch;
23use std::cell::RefCell;
24use std::collections::{BTreeMap, HashMap, HashSet};
25use std::fs;
26use std::io;
27use std::path::{Path, PathBuf};
28use std::sync::Arc;
29use std::time::SystemTime;
30
31/// Basic information about local packs.
32#[derive(Debug, Clone, Default)]
33pub struct LocalPackInfo {
34    /// Number of valid local packs.
35    pub pack_count: usize,
36    /// Total objects across all valid local packs.
37    pub object_count: usize,
38    /// Combined on-disk bytes of `.pack` + `.idx`.
39    pub size_bytes: u64,
40    /// Set of all object IDs present in local packs.
41    pub object_ids: HashSet<ObjectId>,
42}
43
44/// Read all valid `.idx` files in `objects/pack`.
45///
46/// # Errors
47///
48/// Returns [`Error::Io`] for directory-level failures. Individual invalid pack
49/// pairs are skipped.
50pub fn read_local_pack_indexes(objects_dir: &Path) -> Result<Vec<PackIndex>> {
51    let pack_dir = objects_dir.join("pack");
52    let rd = match fs::read_dir(&pack_dir) {
53        Ok(rd) => rd,
54        Err(err) if err.kind() == io::ErrorKind::NotFound => return Ok(Vec::new()),
55        Err(err) => return Err(Error::Io(err)),
56    };
57
58    let mut out = Vec::new();
59    for entry in rd {
60        let entry = entry.map_err(Error::Io)?;
61        let path = entry.path();
62        if path.extension().and_then(|s| s.to_str()) != Some("idx") {
63            continue;
64        }
65        if let Ok(idx) = read_pack_index(&path) {
66            // Ignore orphan `.idx` files (no `.pack`). They must not make `fsck` think objects
67            // exist (`t7700-repack`); repack also skips them so a stray index does not block work.
68            if !idx.pack_path.is_file() {
69                continue;
70            }
71            out.push(idx);
72        }
73    }
74    Ok(out)
75}
76
77/// Process-wide cache of parsed pack indexes and pack file bytes.
78///
79/// Hot lookups serve cached directory listings, parsed `.idx` files, and `.pack` bytes
80/// without `stat` per access (Git prepares the pack list once and only re-scans on a
81/// lookup miss when the pack directory's mtime changed). Pack bytes are re-read only on
82/// cache miss or when parsing a pack object fails and the on-disk signature changed.
83///
84/// SHA-1 verification of the index trailer is **not** performed on cached reads: Git only
85/// verifies pack indexes during `fsck`/`verify-pack`, not on every object lookup. Use
86/// [`read_pack_index`] when verification is required.
87struct PackReadDamage {
88    pack: PathBuf,
89    offset: u64,
90    err: Error,
91}
92
93fn finish_pack_object_lookup(oid: &ObjectId, last: Option<PackReadDamage>) -> Error {
94    match last {
95        Some(d) => Error::wrap_packed_read_failure(*oid, d.pack, d.offset, d.err),
96        None => Error::ObjectNotFound(oid.to_hex()),
97    }
98}
99
100fn wrap_pack_cache_read_err(
101    objects_dir: &Path,
102    idx: &PackIndex,
103    oid: &ObjectId,
104    err: Error,
105) -> Error {
106    let offset = pack_cache::find_offset_in_pack_read_context(objects_dir, idx, oid)
107        .ok()
108        .flatten()
109        .unwrap_or(0);
110    Error::wrap_packed_read_failure(*oid, idx.pack_path.clone(), offset, err)
111}
112
113pub(crate) mod pack_cache {
114    use super::{read_pack_index_no_verify, Error, ObjectKind, PackIndex, Result};
115    use crate::pack_map::{fingerprint_from_file, PackData, PackFingerprint};
116    use std::collections::{HashMap, HashSet, VecDeque};
117    use std::fs;
118    use std::hash::{Hash, Hasher};
119    use std::io;
120    use std::path::{Path, PathBuf};
121    use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
122    use std::sync::{Arc, Mutex, Weak};
123    use std::time::SystemTime;
124
125    const DELTA_LRU_SHARD_COUNT: usize = 32;
126    // A bounded operation must release descriptors and compressed windows together.
127    // Active readers can keep their own Arc alive across a catalog eviction.
128    const BOUNDED_PACK_READERS: usize = 32;
129
130    type ThreadLocalDirListing = Option<(PathBuf, SystemTime, Vec<Arc<PackIndex>>)>;
131
132    #[derive(Clone)]
133    enum CachedPackBytes {
134        Retained(Arc<PackData>),
135        Positioned(Weak<PackData>),
136    }
137
138    impl CachedPackBytes {
139        fn get(&self) -> Option<Arc<PackData>> {
140            match self {
141                Self::Retained(bytes) => Some(Arc::clone(bytes)),
142                Self::Positioned(bytes) => bytes.upgrade(),
143            }
144        }
145    }
146
147    // hygiene: per-thread pack read hot cache (indexes, bytes, dir listing); cleared on invalidate
148    thread_local! {
149        static PACK_DIR_INDEXES: std::cell::RefCell<ThreadLocalDirListing> =
150            const { std::cell::RefCell::new(None) };
151        static PACK_HIT_INDEX: std::cell::RefCell<Option<(PathBuf, Arc<PackIndex>)>> =
152            const { std::cell::RefCell::new(None) };
153        static PACK_BYTES: std::cell::RefCell<HashMap<PathBuf, CachedPackBytes>> =
154            std::cell::RefCell::new(HashMap::new());
155        static PACK_INDEXES: std::cell::RefCell<HashMap<PathBuf, Arc<PackIndex>>> =
156            std::cell::RefCell::new(HashMap::new());
157        /// Looked up for every object read, so keyed by the path's bytes with a fast
158        /// hasher rather than by [`Path`] components; a miss falls back to the catalog.
159        static PACK_IDS: std::cell::RefCell<hashbrown::HashMap<std::ffi::OsString, u32>> =
160            std::cell::RefCell::new(hashbrown::HashMap::new());
161        static PACK_COUNT_VALIDATED: std::cell::RefCell<HashSet<PathBuf>> =
162            std::cell::RefCell::new(HashSet::new());
163        static PACK_DIR_LISTING_TRUSTED: std::cell::Cell<bool> = const { std::cell::Cell::new(false) };
164        /// [`PackStore::cache_epoch`] last seen on this thread (cross-thread invalidation).
165        static THREAD_WARM_EPOCH: std::cell::Cell<u64> = const { std::cell::Cell::new(0) };
166        /// [`PackStore::id`] last warmed on this thread (distinct stores can share an epoch).
167        static THREAD_WARM_STORE: std::cell::Cell<u64> = const { std::cell::Cell::new(0) };
168        static PACK_MONOTONIC_CURSOR: std::cell::RefCell<Option<(PathBuf, usize)>> =
169            const { std::cell::RefCell::new(None) };
170        static SINGLE_READER_MEMO: std::cell::RefCell<Option<SingleReaderMemo>> =
171            const { std::cell::RefCell::new(None) };
172        static STORE_TLS_SNAPSHOTS: std::cell::RefCell<HashMap<u64, PackThreadLocalSnapshot>> =
173            std::cell::RefCell::new(HashMap::new());
174        static ACTIVE_TLS_STORE_ID: std::cell::Cell<u64> = const { std::cell::Cell::new(0) };
175        static ACTIVE_STORE_LIFETIME: std::cell::RefCell<Option<Weak<()>>> =
176            const { std::cell::RefCell::new(None) };
177        /// Suppress [`release_store_thread_local`] while pack-read TLS slots are being torn down.
178        static SKIP_STORE_TLS_RELEASE: std::cell::Cell<bool> =
179            const { std::cell::Cell::new(false) };
180        /// When set, catalog and pack-byte reads target this store without swapping the
181        /// active pack-read context (shared-clone alternates during a primary walk).
182        static ALTERNATE_PACK_READ_STORE: std::cell::RefCell<
183            Option<std::sync::Arc<crate::pack_store::PackStore>>,
184        > = const { std::cell::RefCell::new(None) };
185        /// Cached per-thread: primary `objects/` has a trusted verified-empty pack listing.
186        static PRIMARY_EMPTY: std::cell::Cell<Option<bool>> =
187            const { std::cell::Cell::new(None) };
188    }
189
190    struct AlternatePackReadStoreGuard {
191        previous: Option<std::sync::Arc<crate::pack_store::PackStore>>,
192    }
193
194    impl AlternatePackReadStoreGuard {
195        fn install(store: std::sync::Arc<crate::pack_store::PackStore>) -> Option<Self> {
196            ALTERNATE_PACK_READ_STORE.with(|cell| {
197                if cell
198                    .borrow()
199                    .as_ref()
200                    .is_some_and(|active| std::sync::Arc::ptr_eq(active, &store))
201                {
202                    return None;
203                }
204                let previous = cell.borrow_mut().replace(store);
205                Some(Self { previous })
206            })
207        }
208    }
209
210    impl Drop for AlternatePackReadStoreGuard {
211        fn drop(&mut self) {
212            ALTERNATE_PACK_READ_STORE.with(|cell| {
213                *cell.borrow_mut() = self.previous.take();
214            });
215        }
216    }
217
218    pub(crate) fn clear_alternate_pack_read_store() {
219        ALTERNATE_PACK_READ_STORE.with(|cell| *cell.borrow_mut() = None);
220    }
221
222    pub(crate) fn with_alternate_pack_store<R>(
223        store: std::sync::Arc<crate::pack_store::PackStore>,
224        f: impl FnOnce() -> R,
225    ) -> R {
226        if crate::pack_store::PackStore::has_context()
227            && !crate::pack_store::PackStore::read_context_matches_objects_dir(store.objects_dir())
228        {
229            ALTERNATE_PACK_READ_STORE.with(|cell| {
230                let mut slot = cell.borrow_mut();
231                if slot
232                    .as_ref()
233                    .is_none_or(|active| !std::sync::Arc::ptr_eq(active, &store))
234                {
235                    *slot = Some(store);
236                }
237            });
238            return f();
239        }
240        let _guard = AlternatePackReadStoreGuard::install(store);
241        f()
242    }
243
244    pub(crate) fn alternate_read_store() -> Option<std::sync::Arc<crate::pack_store::PackStore>> {
245        ALTERNATE_PACK_READ_STORE.with(|cell| cell.borrow().clone())
246    }
247
248    pub(crate) fn catalog_store() -> std::sync::Arc<crate::pack_store::PackStore> {
249        alternate_read_store().unwrap_or_else(crate::pack_store::PackStore::active_for_reads)
250    }
251
252    pub(crate) fn sync_thread_local_for_objects_dir(objects_dir: &Path) {
253        if pack_thread_locals_apply_to(objects_dir) {
254            sync_thread_local_with_store(&catalog_store());
255        }
256    }
257
258    /// Thread-local pack hints and monotonic index cursors apply to the primary read context
259    /// and to alternate object directories served through [`with_alternate_pack_store`].
260    pub(crate) fn pack_thread_locals_apply_to(objects_dir: &Path) -> bool {
261        if crate::pack_store::PackStore::read_context_matches_objects_dir(objects_dir) {
262            return true;
263        }
264        alternate_read_store().is_some_and(|store| store.objects_dir() == objects_dir)
265    }
266
267    /// Cached per-thread: primary `objects/` has a trusted verified-empty pack listing.
268    pub(crate) fn primary_has_verified_empty_listing(objects_dir: &Path) -> bool {
269        if !crate::pack_store::PackStore::read_context_matches_objects_dir(objects_dir) {
270            return false;
271        }
272        PRIMARY_EMPTY.with(|cell| {
273            if let Some(hit) = cell.get() {
274                return hit;
275            }
276            let hit = trusted_verified_empty_pack_listing(objects_dir);
277            cell.set(Some(hit));
278            hit
279        })
280    }
281
282    pub(crate) fn set_skip_store_tls_release(skip: bool) {
283        SKIP_STORE_TLS_RELEASE.set(skip);
284    }
285
286    #[derive(Clone)]
287    struct PackThreadLocalSnapshot {
288        dir_indexes: ThreadLocalDirListing,
289        hit_index: Option<(PathBuf, Arc<PackIndex>)>,
290        pack_bytes: HashMap<PathBuf, CachedPackBytes>,
291        pack_indexes: HashMap<PathBuf, Arc<PackIndex>>,
292        pack_ids: hashbrown::HashMap<std::ffi::OsString, u32>,
293        count_validated: HashSet<PathBuf>,
294        listing_trusted: bool,
295        warm_epoch: u64,
296        warm_store: u64,
297        monotonic_cursor: Option<(PathBuf, usize)>,
298        single_reader_memo: Option<SingleReaderMemo>,
299        store_lifetime: Weak<()>,
300    }
301
302    fn capture_tls_snapshot(store_lifetime: Weak<()>) -> PackThreadLocalSnapshot {
303        PackThreadLocalSnapshot {
304            dir_indexes: PACK_DIR_INDEXES.with(|cell| cell.borrow().clone()),
305            hit_index: PACK_HIT_INDEX.with(|cell| cell.borrow().clone()),
306            pack_bytes: PACK_BYTES.with(|cell| cell.borrow().clone()),
307            pack_indexes: PACK_INDEXES.with(|cell| cell.borrow().clone()),
308            pack_ids: PACK_IDS.with(|cell| cell.borrow().clone()),
309            count_validated: PACK_COUNT_VALIDATED.with(|cell| cell.borrow().clone()),
310            listing_trusted: PACK_DIR_LISTING_TRUSTED.get(),
311            warm_epoch: THREAD_WARM_EPOCH.get(),
312            warm_store: THREAD_WARM_STORE.get(),
313            monotonic_cursor: PACK_MONOTONIC_CURSOR.with(|cell| cell.borrow().clone()),
314            single_reader_memo: SINGLE_READER_MEMO.with(|cell| cell.borrow().clone()),
315            store_lifetime,
316        }
317    }
318
319    fn apply_tls_snapshot(snap: PackThreadLocalSnapshot) {
320        PACK_DIR_INDEXES.with(|cell| *cell.borrow_mut() = snap.dir_indexes);
321        PACK_HIT_INDEX.with(|cell| *cell.borrow_mut() = snap.hit_index);
322        PACK_BYTES.with(|cell| *cell.borrow_mut() = snap.pack_bytes);
323        PACK_INDEXES.with(|cell| *cell.borrow_mut() = snap.pack_indexes);
324        PACK_IDS.with(|cell| *cell.borrow_mut() = snap.pack_ids);
325        PACK_COUNT_VALIDATED.with(|cell| *cell.borrow_mut() = snap.count_validated);
326        PACK_DIR_LISTING_TRUSTED.set(snap.listing_trusted);
327        THREAD_WARM_EPOCH.set(snap.warm_epoch);
328        THREAD_WARM_STORE.set(snap.warm_store);
329        PACK_MONOTONIC_CURSOR.with(|cell| *cell.borrow_mut() = snap.monotonic_cursor);
330        SINGLE_READER_MEMO.with(|cell| *cell.borrow_mut() = snap.single_reader_memo);
331        ACTIVE_STORE_LIFETIME.with(|cell| *cell.borrow_mut() = Some(snap.store_lifetime));
332    }
333
334    fn clear_live_tls_pack_mappings() {
335        PACK_DIR_INDEXES.with(|cell| *cell.borrow_mut() = None);
336        PACK_HIT_INDEX.with(|cell| *cell.borrow_mut() = None);
337        PACK_BYTES.with(|cell| cell.borrow_mut().clear());
338        PACK_INDEXES.with(|cell| cell.borrow_mut().clear());
339        PACK_IDS.with(|cell| cell.borrow_mut().clear());
340        PACK_COUNT_VALIDATED.with(|cell| cell.borrow_mut().clear());
341        PACK_DIR_LISTING_TRUSTED.set(false);
342        PACK_MONOTONIC_CURSOR.with(|cell| *cell.borrow_mut() = None);
343        forget_single_reader_memo();
344        super::clear_pack_read_info_cache();
345    }
346
347    fn seed_tls_from_store_catalog(store: &crate::pack_store::PackStore) {
348        PACK_BYTES.with(|cell| cell.borrow_mut().clear());
349        PACK_INDEXES.with(|cell| cell.borrow_mut().clear());
350        PACK_IDS.with(|cell| cell.borrow_mut().clear());
351        PACK_COUNT_VALIDATED.with(|cell| cell.borrow_mut().clear());
352        PACK_HIT_INDEX.with(|cell| *cell.borrow_mut() = None);
353        forget_single_reader_memo();
354        super::clear_pack_read_info_cache();
355        let objects_dir = store.objects_dir();
356        let pack_dir = objects_dir.join("pack");
357        store.with_catalog_read(|g| {
358            let listing = g
359                .by_dir
360                .get(&pack_dir)
361                .map(|cached| (pack_dir.clone(), cached.dir_mtime, cached.indexes.clone()));
362            PACK_DIR_INDEXES.with(|cell| *cell.borrow_mut() = listing);
363            for (path, entry) in &g.by_pack {
364                remember_pack_bytes(path, &entry.bytes);
365            }
366            for (path, entry) in &g.by_idx {
367                remember_index(path, &entry.idx);
368            }
369            PACK_IDS.with(|cell| {
370                cell.borrow_mut().extend(
371                    g.pack_ids
372                        .iter()
373                        .map(|(k, v)| (k.as_os_str().to_owned(), *v)),
374                );
375            });
376            PACK_COUNT_VALIDATED.with(|cell| {
377                cell.borrow_mut()
378                    .extend(g.validated_pack_counts.iter().cloned());
379            });
380            PACK_DIR_LISTING_TRUSTED.set(true);
381            THREAD_WARM_EPOCH.set(store.cache_epoch());
382            THREAD_WARM_STORE.set(store_thread_local_key(store));
383            ACTIVE_STORE_LIFETIME.with(|cell| *cell.borrow_mut() = Some(store.lifetime_weak()));
384        });
385    }
386
387    pub(crate) fn drop_store_tls_snapshot(store_id: u64) {
388        STORE_TLS_SNAPSHOTS.with(|map| map.borrow_mut().remove(&store_id));
389    }
390
391    fn release_store_thread_local_inner(store_id: u64) {
392        if SKIP_STORE_TLS_RELEASE.get() {
393            return;
394        }
395        drop_store_tls_snapshot(store_id);
396        if ACTIVE_TLS_STORE_ID.get() != store_id && THREAD_WARM_STORE.get() != store_id {
397            return;
398        }
399        clear_live_tls_pack_mappings();
400        if ACTIVE_TLS_STORE_ID.get() == store_id {
401            ACTIVE_TLS_STORE_ID.set(0);
402        }
403        if THREAD_WARM_STORE.get() == store_id {
404            THREAD_WARM_STORE.set(0);
405            THREAD_WARM_EPOCH.set(0);
406        }
407        ACTIVE_STORE_LIFETIME.with(|cell| *cell.borrow_mut() = None);
408    }
409
410    /// Drop this store's thread-local pack/idx mappings when its [`PackStore`] is destroyed.
411    pub(crate) fn release_store_thread_local(store_id: u64) {
412        let _ = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
413            release_store_thread_local_inner(store_id);
414        }));
415    }
416
417    pub(crate) fn switch_tls_for_store(store: &crate::pack_store::PackStore) {
418        if !crate::pack_store::PackStore::has_context() {
419            return;
420        }
421        let new_id = store.id();
422        let epoch = store.cache_epoch();
423        // Match legacy `sync_thread_local_with_store`: epoch + store identity only.
424        // Pack-path invalidation clears listing trust (and may reseed hints) without
425        // bumping the store epoch, so requiring `PACK_DIR_LISTING_TRUSTED` here would
426        // re-seed from the catalog and drop a warm pack hint on every directory sync.
427        if ACTIVE_TLS_STORE_ID.get() == new_id
428            && THREAD_WARM_EPOCH.get() == epoch
429            && THREAD_WARM_STORE.get() == new_id
430        {
431            return;
432        }
433        let old_id = ACTIVE_TLS_STORE_ID.get();
434        if old_id != 0 && old_id != new_id {
435            let outgoing_alive = ACTIVE_STORE_LIFETIME.with(|cell| {
436                cell.borrow()
437                    .as_ref()
438                    .and_then(|weak| weak.upgrade())
439                    .is_some()
440            });
441            if outgoing_alive {
442                if let Some(lifetime) = ACTIVE_STORE_LIFETIME.with(|cell| cell.borrow().clone()) {
443                    let snap = capture_tls_snapshot(lifetime);
444                    STORE_TLS_SNAPSHOTS.with(|map| map.borrow_mut().insert(old_id, snap));
445                } else {
446                    clear_live_tls_pack_mappings();
447                }
448            } else {
449                clear_live_tls_pack_mappings();
450            }
451        }
452        ACTIVE_TLS_STORE_ID.set(new_id);
453        ACTIVE_STORE_LIFETIME.with(|cell| *cell.borrow_mut() = Some(store.lifetime_weak()));
454        if let Some(snap) = STORE_TLS_SNAPSHOTS.with(|map| map.borrow_mut().remove(&new_id)) {
455            if snap.warm_epoch == epoch && snap.store_lifetime.upgrade().is_some() {
456                apply_tls_snapshot(snap);
457                return;
458            }
459        }
460        seed_tls_from_store_catalog(store);
461        STORE_TLS_SNAPSHOTS.with(|map| {
462            map.borrow_mut()
463                .insert(new_id, capture_tls_snapshot(store.lifetime_weak()));
464        });
465    }
466
467    /// The pack a [`super::SingleLocalPackReader`] last opened on this thread.
468    ///
469    /// Lets repeated opens (one per object read) skip re-deriving the same pack through the
470    /// path-keyed caches and the shared catalog.
471    #[derive(Clone)]
472    struct SingleReaderMemo {
473        objects_dir: PathBuf,
474        pack_dir: PathBuf,
475        idx: Arc<PackIndex>,
476        pack_bytes: Arc<PackData>,
477        pack_id: u32,
478    }
479
480    fn forget_single_reader_memo() {
481        SINGLE_READER_MEMO.with(|cell| *cell.borrow_mut() = None);
482    }
483
484    /// Record the single local pack just opened for `objects_dir`.
485    pub(crate) fn remember_single_reader(
486        objects_dir: &Path,
487        idx: &Arc<PackIndex>,
488        pack_bytes: &Arc<PackData>,
489        pack_id: u32,
490    ) {
491        SINGLE_READER_MEMO.with(|cell| {
492            *cell.borrow_mut() = Some(SingleReaderMemo {
493                objects_dir: objects_dir.to_path_buf(),
494                pack_dir: objects_dir.join("pack"),
495                idx: Arc::clone(idx),
496                pack_bytes: Arc::clone(pack_bytes),
497                pack_id,
498            });
499        });
500    }
501
502    /// The single local pack previously opened for `objects_dir`, if this thread's trusted
503    /// pack listing still consists of exactly that pack.
504    ///
505    /// Returns `None` whenever the listing is untrusted (so the caller rechecks the pack
506    /// directory) or has changed since the pack was remembered.
507    pub(crate) fn single_reader_for(
508        objects_dir: &Path,
509    ) -> Option<(Arc<PackIndex>, Arc<PackData>, u32)> {
510        if !PACK_DIR_LISTING_TRUSTED.get() {
511            return None;
512        }
513        SINGLE_READER_MEMO.with(|memo| {
514            let memo = memo.borrow();
515            let memo = memo.as_ref()?;
516            if memo.objects_dir.as_os_str() != objects_dir.as_os_str() {
517                return None;
518            }
519            let listing_unchanged = PACK_DIR_INDEXES.with(|cell| {
520                cell.borrow().as_ref().is_some_and(|(dir, _, indexes)| {
521                    dir.as_os_str() == memo.pack_dir.as_os_str()
522                        && indexes.len() == 1
523                        && Arc::ptr_eq(&indexes[0], &memo.idx)
524                })
525            });
526            if !listing_unchanged {
527                return None;
528            }
529            let hit_current = PACK_HIT_INDEX.with(|cell| {
530                cell.borrow().as_ref().is_some_and(|(dir, idx)| {
531                    dir.as_os_str() == objects_dir.as_os_str() && Arc::ptr_eq(idx, &memo.idx)
532                })
533            });
534            if !hit_current {
535                return None;
536            }
537            Some((
538                Arc::clone(&memo.idx),
539                Arc::clone(&memo.pack_bytes),
540                memo.pack_id,
541            ))
542        })
543    }
544
545    fn store_thread_local_key(store: &crate::pack_store::PackStore) -> u64 {
546        store.id()
547    }
548
549    pub(crate) fn clear_thread_local_dir_indexes() {
550        PACK_DIR_INDEXES.with(|cell| *cell.borrow_mut() = None);
551        PACK_HIT_INDEX.with(|cell| *cell.borrow_mut() = None);
552        PACK_BYTES.with(|cell| cell.borrow_mut().clear());
553        PACK_INDEXES.with(|cell| cell.borrow_mut().clear());
554        PACK_IDS.with(|cell| cell.borrow_mut().clear());
555        PACK_COUNT_VALIDATED.with(|cell| cell.borrow_mut().clear());
556        PACK_DIR_LISTING_TRUSTED.set(false);
557        THREAD_WARM_EPOCH.set(0);
558        THREAD_WARM_STORE.set(0);
559        STORE_TLS_SNAPSHOTS.with(|map| map.borrow_mut().clear());
560        ACTIVE_TLS_STORE_ID.set(0);
561        ACTIVE_STORE_LIFETIME.with(|cell| *cell.borrow_mut() = None);
562        super::clear_pack_read_info_cache();
563        PACK_MONOTONIC_CURSOR.with(|cell| *cell.borrow_mut() = None);
564        forget_single_reader_memo();
565    }
566
567    pub(crate) fn try_read_object_via_single_pack_reader(
568        objects_dir: &Path,
569        oid: &crate::objects::ObjectId,
570    ) -> Result<Option<crate::objects::Object>> {
571        if !pack_thread_locals_apply_to(objects_dir) {
572            return Ok(None);
573        }
574        sync_thread_local_for_objects_dir(objects_dir);
575        let Some(mut reader) = super::SingleLocalPackReader::open_allow_loose(objects_dir)? else {
576            return Ok(None);
577        };
578        match reader.read_object(oid) {
579            Ok(obj) => Ok(Some(obj)),
580            Err(Error::ObjectNotFound(_)) => Ok(None),
581            Err(err) => Err(super::wrap_pack_cache_read_err(
582                objects_dir,
583                reader.idx.as_ref(),
584                oid,
585                err,
586            )),
587        }
588    }
589
590    /// Like [`try_read_object_via_single_pack_reader`], skipping objects the pack stores whole
591    /// with a payload larger than `limit` (see
592    /// [`super::SingleLocalPackReader::read_object_unless_whole_larger`]).
593    pub(crate) fn try_read_object_via_single_pack_reader_unless_whole_larger(
594        objects_dir: &Path,
595        oid: &crate::objects::ObjectId,
596        limit: u64,
597    ) -> Result<Option<super::BoundedRead>> {
598        if !pack_thread_locals_apply_to(objects_dir) {
599            return Ok(None);
600        }
601        sync_thread_local_for_objects_dir(objects_dir);
602        let Some(mut reader) = super::SingleLocalPackReader::open_allow_loose(objects_dir)? else {
603            return Ok(None);
604        };
605        match reader.read_object_unless_whole_larger(oid, limit) {
606            Ok(read) => Ok(Some(read)),
607            Err(Error::ObjectNotFound(_)) => Ok(None),
608            Err(err) => Err(super::wrap_pack_cache_read_err(
609                objects_dir,
610                reader.idx.as_ref(),
611                oid,
612                err,
613            )),
614        }
615    }
616
617    /// Commit `oid` through its subject line from the single local pack (see
618    /// [`super::SingleLocalPackReader::read_commit_through_subject`]); `None` when that reader
619    /// is not in use or cannot serve `oid` that way.
620    pub(crate) fn try_read_commit_through_subject_via_single_pack_reader(
621        objects_dir: &Path,
622        oid: &crate::objects::ObjectId,
623    ) -> Result<Option<Vec<u8>>> {
624        if !pack_thread_locals_apply_to(objects_dir) {
625            return Ok(None);
626        }
627        sync_thread_local_for_objects_dir(objects_dir);
628        let Some(mut reader) = super::SingleLocalPackReader::open_allow_loose(objects_dir)? else {
629            return Ok(None);
630        };
631        reader.read_commit_through_subject(oid)
632    }
633
634    pub(crate) fn try_read_object_via_single_pack_reader_into(
635        objects_dir: &Path,
636        oid: &crate::objects::ObjectId,
637        populate_delta_lru: bool,
638        buf: &mut Vec<u8>,
639    ) -> Result<Option<crate::objects::ObjectKind>> {
640        if !pack_thread_locals_apply_to(objects_dir) {
641            return Ok(None);
642        }
643        sync_thread_local_for_objects_dir(objects_dir);
644        let Some(mut reader) = super::SingleLocalPackReader::open_allow_loose(objects_dir)? else {
645            return Ok(None);
646        };
647        match reader.read_object_into_with_delta_policy(oid, populate_delta_lru, buf) {
648            Ok(kind) => Ok(Some(kind)),
649            Err(Error::ObjectNotFound(_)) => Ok(None),
650            Err(err) => Err(super::wrap_pack_cache_read_err(
651                objects_dir,
652                reader.idx.as_ref(),
653                oid,
654                err,
655            )),
656        }
657    }
658
659    /// Drop thread-local pack caches after invalidation on this or another thread.
660    pub(crate) fn clear_thread_local_after_invalidation(new_epoch: u64) {
661        clear_thread_local_dir_indexes();
662        THREAD_WARM_EPOCH.set(new_epoch);
663    }
664
665    pub(crate) fn sync_thread_local_with_store(store: &crate::pack_store::PackStore) {
666        switch_tls_for_store(store);
667    }
668
669    fn drop_thread_local_dir_listing(pack_dir: &Path) {
670        PACK_DIR_INDEXES.with(|cell| {
671            if cell
672                .borrow()
673                .as_ref()
674                .is_some_and(|(dir, _, _)| dir == pack_dir)
675            {
676                *cell.borrow_mut() = None;
677                PACK_DIR_LISTING_TRUSTED.set(false);
678            }
679        });
680    }
681
682    fn drop_thread_local_pack_bytes(pack_path: &Path) {
683        PACK_BYTES.with(|cell| {
684            cell.borrow_mut().remove(pack_path);
685        });
686        forget_single_reader_memo();
687    }
688
689    /// Drop thread-local indexes, hints, and directory listing tied to a replaced pack path.
690    fn invalidate_thread_local_for_pack_path_change(pack_path: &Path) {
691        let idx_path = pack_path.with_extension("idx");
692        drop_thread_local_pack_bytes(pack_path);
693        PACK_INDEXES.with(|cell| {
694            cell.borrow_mut().remove(&idx_path);
695        });
696        PACK_IDS.with(|cell| {
697            cell.borrow_mut().remove(pack_path.as_os_str());
698        });
699        PACK_COUNT_VALIDATED.with(|cell| {
700            cell.borrow_mut().remove(pack_path);
701        });
702        PACK_HIT_INDEX.with(|cell| {
703            let drop_hint = cell
704                .borrow()
705                .as_ref()
706                .is_some_and(|(_, idx)| idx.pack_path == pack_path || idx.idx_path == idx_path);
707            if drop_hint {
708                *cell.borrow_mut() = None;
709            }
710        });
711        if let Some(pack_dir) = pack_path.parent() {
712            drop_thread_local_dir_listing(pack_dir);
713        }
714        super::clear_pack_read_info_cache();
715    }
716
717    /// Seed thread-local pack caches from `store` (one catalog read lock; hot reads avoid the catalog).
718    ///
719    /// When this thread already holds `store`'s state but a rescan or pack load dropped
720    /// listing trust, re-seed from the catalog (which the rescan refreshed); otherwise every
721    /// later pack lookup in the context re-stats the pack directory.
722    pub(crate) fn warm_thread_local_from_store(store: &crate::pack_store::PackStore) {
723        if crate::pack_store::PackStore::has_context()
724            && !PACK_DIR_LISTING_TRUSTED.get()
725            && ACTIVE_TLS_STORE_ID.get() == store.id()
726            && THREAD_WARM_STORE.get() == store.id()
727            && THREAD_WARM_EPOCH.get() == store.cache_epoch()
728        {
729            seed_tls_from_store_catalog(store);
730            return;
731        }
732        switch_tls_for_store(store);
733    }
734
735    /// True when this thread trusts an verified-empty pack listing for `objects_dir`.
736    pub(crate) fn trusted_verified_empty_pack_listing(objects_dir: &Path) -> bool {
737        if !crate::pack_store::PackStore::read_context_matches_objects_dir(objects_dir) {
738            return false;
739        }
740        if !PACK_DIR_LISTING_TRUSTED.get() {
741            return false;
742        }
743        let pack_dir = objects_dir.join("pack");
744        crate::pack_store::PackStore::active_for_reads().with_catalog_read(|g| {
745            g.by_dir
746                .get(&pack_dir)
747                .is_some_and(|c| c.idx_listing_verified && c.indexes.is_empty())
748        })
749    }
750
751    fn cached_pack_bytes(pack_path: &Path) -> Option<Arc<PackData>> {
752        if !crate::pack_store::PackStore::has_context() {
753            return None;
754        }
755        PACK_BYTES.with(|cell| cell.borrow().get(pack_path).and_then(CachedPackBytes::get))
756    }
757
758    fn remember_pack_bytes(pack_path: &Path, bytes: &Arc<PackData>) {
759        // The operation-local catalog owns positioned readers. A second strong cache
760        // would keep their descriptors alive after eviction from that catalog.
761        if !crate::pack_store::PackStore::has_context() {
762            return;
763        }
764        let cached = if bytes.uses_positioned_reads() {
765            CachedPackBytes::Positioned(Arc::downgrade(bytes))
766        } else {
767            CachedPackBytes::Retained(Arc::clone(bytes))
768        };
769        PACK_BYTES.with(|cell| {
770            cell.borrow_mut().insert(pack_path.to_path_buf(), cached);
771        });
772        forget_single_reader_memo();
773    }
774
775    pub(crate) fn remember_pack_hit(objects_dir: &Path, idx: &Arc<PackIndex>) {
776        if !pack_thread_locals_apply_to(objects_dir) {
777            return;
778        }
779        promote_pack_index(objects_dir, &idx.idx_path);
780        PACK_HIT_INDEX.with(|cell| {
781            *cell.borrow_mut() = Some((objects_dir.to_path_buf(), Arc::clone(idx)));
782        });
783    }
784
785    pub(crate) fn find_offset_in_pack_read_context(
786        objects_dir: &Path,
787        idx: &PackIndex,
788        oid: &crate::objects::ObjectId,
789    ) -> Result<Option<u64>> {
790        if !pack_thread_locals_apply_to(objects_dir) {
791            return idx.find_offset(oid);
792        }
793        PACK_MONOTONIC_CURSOR.with(|cell| {
794            let mut slot = cell.borrow_mut();
795            if slot.as_ref().is_none_or(|(dir, _)| dir != objects_dir) {
796                *slot = Some((objects_dir.to_path_buf(), 0));
797            }
798            let Some((_, cursor)) = slot.as_mut() else {
799                return idx.find_offset(oid);
800            };
801            idx.find_offset_monotonic(oid, cursor)
802        })
803    }
804
805    /// The pack that served the last lookup in `objects_dir` ([`remember_pack_hit`]), when
806    /// the active pack store reads that directory.
807    pub(crate) fn hinted_pack_index(objects_dir: &Path) -> Option<Arc<PackIndex>> {
808        if !pack_thread_locals_apply_to(objects_dir) {
809            return None;
810        }
811        PACK_HIT_INDEX.with(|cell| {
812            cell.borrow()
813                .as_ref()
814                .and_then(|(dir, idx)| (dir == objects_dir).then(|| Arc::clone(idx)))
815        })
816    }
817
818    pub(crate) fn try_read_from_hinted_pack(
819        objects_dir: &Path,
820        oid: &crate::objects::ObjectId,
821    ) -> Result<Option<crate::objects::Object>> {
822        let Some(idx) = hinted_pack_index(objects_dir) else {
823            return Ok(None);
824        };
825        let Some(offset) = find_offset_in_pack_read_context(objects_dir, &idx, oid)? else {
826            return Ok(None);
827        };
828        match super::read_object_from_pack_at_offset(&idx, offset) {
829            Ok(obj) => Ok(Some(obj)),
830            Err(Error::ObjectNotFound(_)) => Ok(None),
831            Err(err) if err.is_object_read_wrap_source() => Ok(None),
832            Err(err) => Err(err),
833        }
834    }
835
836    pub(crate) fn try_read_from_hinted_pack_into(
837        objects_dir: &Path,
838        oid: &crate::objects::ObjectId,
839        populate_delta_lru: bool,
840        buf: &mut Vec<u8>,
841    ) -> Result<Option<crate::objects::ObjectKind>> {
842        let Some(idx) = hinted_pack_index(objects_dir) else {
843            return Ok(None);
844        };
845        let Some(offset) = find_offset_in_pack_read_context(objects_dir, &idx, oid)? else {
846            return Ok(None);
847        };
848        match super::read_object_at_depth_into(&idx, offset, 0, populate_delta_lru, buf) {
849            Ok(kind) => Ok(Some(kind)),
850            Err(Error::ObjectNotFound(_)) => Ok(None),
851            Err(err) if err.is_object_read_wrap_source() => Ok(None),
852            Err(err) => Err(err),
853        }
854    }
855
856    pub(crate) fn try_read_info_from_hinted_pack(
857        objects_dir: &Path,
858        oid: &crate::objects::ObjectId,
859    ) -> Result<Option<crate::objects::ObjectInfo>> {
860        let Some(idx) = hinted_pack_index(objects_dir) else {
861            return Ok(None);
862        };
863        let Some(offset) = idx.find_offset(oid)? else {
864            return Ok(None);
865        };
866        match super::read_object_info_from_pack_at_offset(&idx, offset) {
867            Ok(info) => Ok(Some(info)),
868            Err(Error::ObjectNotFound(_)) => Ok(None),
869            Err(err) => Err(err),
870        }
871    }
872    fn cached_dir_indexes(objects_dir: &Path, pack_dir: &Path) -> Option<Vec<Arc<PackIndex>>> {
873        if !crate::pack_store::PackStore::read_context_matches_objects_dir(objects_dir) {
874            return None;
875        }
876        let trust_listing = PACK_DIR_LISTING_TRUSTED.get();
877        let dir_mt = if trust_listing {
878            None
879        } else {
880            Some(dir_mtime(pack_dir))
881        };
882        PACK_DIR_INDEXES.with(|cell| {
883            cell.borrow()
884                .as_ref()
885                .and_then(|(dir, cached_mt, indexes)| {
886                    if dir != pack_dir {
887                        return None;
888                    }
889                    if trust_listing || dir_mt == Some(*cached_mt) {
890                        Some(indexes.clone())
891                    } else {
892                        None
893                    }
894                })
895        })
896    }
897
898    fn remember_dir_indexes(objects_dir: &Path, pack_dir: &Path, indexes: &[Arc<PackIndex>]) {
899        if !crate::pack_store::PackStore::read_context_matches_objects_dir(objects_dir) {
900            return;
901        }
902        let dir_mt = dir_mtime(pack_dir);
903        PACK_DIR_INDEXES.with(|cell| {
904            *cell.borrow_mut() = Some((pack_dir.to_path_buf(), dir_mt, indexes.to_vec()));
905        });
906    }
907
908    struct CachedDir {
909        dir_mtime: SystemTime,
910        indexes: Vec<Arc<PackIndex>>,
911        midx_mtime: SystemTime,
912        midx_pack_names: HashSet<String>,
913        /// True after [`idx_paths_on_disk`] matched the cached index set at [`Self::dir_mtime`].
914        idx_listing_verified: bool,
915    }
916
917    struct CachedIdx {
918        idx: Arc<PackIndex>,
919    }
920
921    struct CachedPack {
922        mtime: SystemTime,
923        size: u64,
924        bytes: Arc<PackData>,
925    }
926
927    /// Default retained delta-base bytes (`core.deltaBaseCacheLimit`).
928    pub(super) const DELTA_BASE_CACHE_DEFAULT: usize = 96 * 1024 * 1024;
929
930    #[derive(Clone, Copy, PartialEq, Eq, Hash)]
931    pub(crate) struct DeltaCacheKey {
932        pack_id: u32,
933        offset: u64,
934    }
935
936    /// Byte-bounded LRU of resolved **intermediate** delta bases (never the object being returned).
937    struct DeltaBaseLru {
938        byte_limit: usize,
939        bytes_used: usize,
940        entries: HashMap<DeltaCacheKey, (ObjectKind, Arc<[u8]>)>,
941        head: Option<DeltaCacheKey>,
942        tail: Option<DeltaCacheKey>,
943        links: HashMap<DeltaCacheKey, (Option<DeltaCacheKey>, Option<DeltaCacheKey>)>,
944    }
945
946    impl Default for DeltaBaseLru {
947        fn default() -> Self {
948            Self {
949                byte_limit: DELTA_BASE_CACHE_DEFAULT,
950                bytes_used: 0,
951                entries: HashMap::new(),
952                head: None,
953                tail: None,
954                links: HashMap::new(),
955            }
956        }
957    }
958
959    impl DeltaBaseLru {
960        fn set_byte_limit(&mut self, limit: usize) {
961            if limit == 0 {
962                self.clear();
963                self.byte_limit = 0;
964                return;
965            }
966            self.byte_limit = limit;
967            while self.bytes_used > self.byte_limit {
968                let Some(key) = self.head else {
969                    break;
970                };
971                self.evict_key(key);
972            }
973        }
974
975        fn unlink(&mut self, key: DeltaCacheKey) {
976            let Some((prev, next)) = self.links.remove(&key) else {
977                return;
978            };
979            match prev {
980                Some(p) => {
981                    if let Some(link) = self.links.get_mut(&p) {
982                        link.1 = next;
983                    }
984                }
985                None => self.head = next,
986            }
987            match next {
988                Some(n) => {
989                    if let Some(link) = self.links.get_mut(&n) {
990                        link.0 = prev;
991                    }
992                }
993                None => self.tail = prev,
994            }
995        }
996
997        fn append_tail(&mut self, key: DeltaCacheKey) {
998            let prev = self.tail;
999            self.links.insert(key, (prev, None));
1000            if let Some(p) = prev {
1001                if let Some(link) = self.links.get_mut(&p) {
1002                    link.1 = Some(key);
1003                }
1004            } else {
1005                self.head = Some(key);
1006            }
1007            self.tail = Some(key);
1008        }
1009
1010        fn touch(&mut self, key: DeltaCacheKey) {
1011            self.unlink(key);
1012            self.append_tail(key);
1013        }
1014
1015        fn evict_key(&mut self, key: DeltaCacheKey) {
1016            self.unlink(key);
1017            if let Some((_, data)) = self.entries.remove(&key) {
1018                self.bytes_used = self.bytes_used.saturating_sub(data.len());
1019            }
1020        }
1021
1022        fn get(&mut self, key: DeltaCacheKey) -> Option<(ObjectKind, Arc<[u8]>)> {
1023            let (kind, data) = self.entries.get(&key).map(|(k, d)| (*k, Arc::clone(d)))?;
1024            self.touch(key);
1025            Some((kind, data))
1026        }
1027
1028        fn put(&mut self, key: DeltaCacheKey, kind: ObjectKind, data: Arc<[u8]>) {
1029            if self.byte_limit == 0 {
1030                return;
1031            }
1032            let sz = data.len();
1033            if sz > self.byte_limit {
1034                return;
1035            }
1036            use std::collections::hash_map::Entry;
1037            match self.entries.entry(key) {
1038                Entry::Occupied(mut slot) => {
1039                    let old_len = slot.get().1.len();
1040                    slot.insert((kind, Arc::clone(&data)));
1041                    self.bytes_used = self.bytes_used.saturating_sub(old_len).saturating_add(sz);
1042                    self.touch(key);
1043                }
1044                Entry::Vacant(slot) => {
1045                    slot.insert((kind, Arc::clone(&data)));
1046                    self.bytes_used = self.bytes_used.saturating_add(sz);
1047                    self.append_tail(key);
1048                }
1049            }
1050            while self.bytes_used > self.byte_limit {
1051                let Some(evict) = self.head else {
1052                    break;
1053                };
1054                self.evict_key(evict);
1055            }
1056        }
1057
1058        fn drop_pack(&mut self, pack_id: u32) {
1059            let keys: Vec<DeltaCacheKey> = self
1060                .entries
1061                .keys()
1062                .copied()
1063                .filter(|k| k.pack_id == pack_id)
1064                .collect();
1065            for key in keys {
1066                self.evict_key(key);
1067            }
1068        }
1069
1070        fn clear(&mut self) {
1071            self.entries.clear();
1072            self.links.clear();
1073            self.head = None;
1074            self.tail = None;
1075            self.bytes_used = 0;
1076        }
1077    }
1078
1079    fn shard_index(key: DeltaCacheKey) -> usize {
1080        let mut hasher = std::collections::hash_map::DefaultHasher::new();
1081        key.hash(&mut hasher);
1082        (hasher.finish() as usize) % DELTA_LRU_SHARD_COUNT
1083    }
1084
1085    /// Delta-base LRU sharded by `(pack_id, offset)` so concurrent reads rarely contend.
1086    pub(crate) struct ShardedDeltaBaseLru {
1087        cache_enabled: AtomicBool,
1088        lru_populated: AtomicBool,
1089        total_byte_limit: AtomicUsize,
1090        shards: Box<[Mutex<DeltaBaseLru>; DELTA_LRU_SHARD_COUNT]>,
1091    }
1092
1093    impl ShardedDeltaBaseLru {
1094        pub fn new() -> Self {
1095            let per_shard = DELTA_BASE_CACHE_DEFAULT / DELTA_LRU_SHARD_COUNT;
1096            Self {
1097                cache_enabled: AtomicBool::new(true),
1098                lru_populated: AtomicBool::new(false),
1099                total_byte_limit: AtomicUsize::new(DELTA_BASE_CACHE_DEFAULT),
1100                shards: Box::new(std::array::from_fn(|_| {
1101                    Mutex::new(DeltaBaseLru {
1102                        byte_limit: per_shard,
1103                        ..DeltaBaseLru::default()
1104                    })
1105                })),
1106            }
1107        }
1108
1109        pub(crate) fn set_byte_limit(&self, limit: usize) {
1110            self.total_byte_limit.store(limit, Ordering::Release);
1111            if limit == 0 {
1112                self.cache_enabled.store(false, Ordering::Release);
1113                self.clear();
1114                return;
1115            }
1116            self.cache_enabled.store(true, Ordering::Release);
1117            let base = limit / DELTA_LRU_SHARD_COUNT;
1118            let rem = limit % DELTA_LRU_SHARD_COUNT;
1119            let mut any = false;
1120            for (i, shard) in self.shards.iter().enumerate() {
1121                let cap = base + usize::from(i < rem);
1122                let mut g = shard.lock().unwrap_or_else(|e| e.into_inner());
1123                g.set_byte_limit(cap);
1124                any |= !g.entries.is_empty();
1125            }
1126            self.lru_populated.store(any, Ordering::Release);
1127        }
1128
1129        pub(crate) fn get(&self, key: DeltaCacheKey) -> Option<(ObjectKind, Arc<[u8]>)> {
1130            if !self.cache_enabled.load(Ordering::Acquire)
1131                || !self.lru_populated.load(Ordering::Acquire)
1132            {
1133                return None;
1134            }
1135            let shard = &self.shards[shard_index(key)];
1136            let mut g = shard.lock().unwrap_or_else(|e| e.into_inner());
1137            g.get(key)
1138        }
1139
1140        pub(crate) fn put_many(&self, kind: ObjectKind, items: &[(u32, u64, Arc<[u8]>)]) {
1141            if items.is_empty() || !self.cache_enabled.load(Ordering::Acquire) {
1142                return;
1143            }
1144            for &(pack_id, offset, ref data) in items {
1145                let key = DeltaCacheKey { pack_id, offset };
1146                let shard = &self.shards[shard_index(key)];
1147                let mut g = shard.lock().unwrap_or_else(|e| e.into_inner());
1148                if g.byte_limit == 0 {
1149                    continue;
1150                }
1151                g.put(key, kind, Arc::clone(data));
1152            }
1153            self.lru_populated.store(true, Ordering::Release);
1154        }
1155
1156        pub(crate) fn clear(&self) {
1157            for shard in self.shards.iter() {
1158                shard.lock().unwrap_or_else(|e| e.into_inner()).clear();
1159            }
1160            self.lru_populated.store(false, Ordering::Release);
1161        }
1162
1163        pub(crate) fn drop_pack(&self, pack_id: u32) {
1164            let mut any = false;
1165            for shard in self.shards.iter() {
1166                let mut g = shard.lock().unwrap_or_else(|e| e.into_inner());
1167                g.drop_pack(pack_id);
1168                any |= !g.entries.is_empty();
1169            }
1170            self.lru_populated.store(any, Ordering::Release);
1171        }
1172
1173        pub(crate) fn caching_enabled(&self) -> bool {
1174            self.cache_enabled.load(Ordering::Acquire)
1175        }
1176
1177        #[cfg(test)]
1178        pub(crate) fn bytes_used(&self) -> usize {
1179            self.shards
1180                .iter()
1181                .map(|s| s.lock().unwrap_or_else(|e| e.into_inner()).bytes_used)
1182                .sum()
1183        }
1184
1185        #[cfg(test)]
1186        pub(crate) fn contains(&self, pack_id: u32, offset: u64) -> bool {
1187            let key = DeltaCacheKey { pack_id, offset };
1188            self.shards[shard_index(key)]
1189                .lock()
1190                .unwrap_or_else(|e| e.into_inner())
1191                .entries
1192                .contains_key(&key)
1193        }
1194    }
1195
1196    #[derive(Default)]
1197    pub(crate) struct CatalogState {
1198        by_dir: HashMap<PathBuf, CachedDir>,
1199        by_idx: HashMap<PathBuf, CachedIdx>,
1200        by_pack: HashMap<PathBuf, CachedPack>,
1201        bounded_pack_order: VecDeque<PathBuf>,
1202        /// Small id assigned when a pack file is first registered in this process.
1203        pack_ids: HashMap<PathBuf, u32>,
1204        next_pack_id: u32,
1205        /// Packs whose header object count was matched to the index (hot read path).
1206        validated_pack_counts: HashSet<PathBuf>,
1207        /// Per `objects/pack` directory rescan count (parallel-safe; tests only).
1208        #[cfg(any(test, feature = "test-support"))]
1209        test_dir_rescan_counts: HashMap<PathBuf, u64>,
1210    }
1211
1212    impl CatalogState {
1213        fn retain_bounded_pack(&mut self, pack_path: &Path) {
1214            if let Some(pos) = self.bounded_pack_order.iter().position(|p| p == pack_path) {
1215                self.bounded_pack_order.remove(pos);
1216            }
1217            while self.bounded_pack_order.len() >= BOUNDED_PACK_READERS {
1218                if let Some(path) = self.bounded_pack_order.pop_front() {
1219                    self.by_pack.remove(&path);
1220                }
1221            }
1222            self.bounded_pack_order.push_back(pack_path.to_path_buf());
1223        }
1224    }
1225
1226    pub(crate) fn with_current<R>(f: impl FnOnce(&mut CatalogState) -> R) -> R {
1227        crate::pack_store::PackStore::with_current_or_implicit(f)
1228    }
1229
1230    pub(crate) fn clear_catalog_on(m: &std::sync::RwLock<CatalogState>) {
1231        let mut g = m.write().unwrap_or_else(|e| e.into_inner());
1232        clear_inner(&mut g);
1233    }
1234
1235    pub(crate) fn configure_delta_base_on(
1236        delta: &ShardedDeltaBaseLru,
1237        cfg: Option<&crate::config::ConfigSet>,
1238    ) {
1239        let bytes = cfg
1240            .and_then(|config| config.get("core.deltaBaseCacheLimit"))
1241            .as_ref()
1242            .and_then(|value| crate::config::parse_i64(value).ok())
1243            .and_then(|value| usize::try_from(value).ok())
1244            .unwrap_or(DELTA_BASE_CACHE_DEFAULT);
1245        delta.set_byte_limit(bytes);
1246    }
1247
1248    #[cfg(any(test, feature = "test-support"))]
1249    thread_local! {
1250        static TEST_MARKER_STAT_COUNT: std::cell::Cell<u64> = const { std::cell::Cell::new(0) };
1251    }
1252
1253    fn marker_sidecar_is_file(path: &Path) -> bool {
1254        #[cfg(any(test, feature = "test-support"))]
1255        TEST_MARKER_STAT_COUNT.with(|count| count.set(count.get().saturating_add(1)));
1256        path.is_file()
1257    }
1258
1259    /// Whether sibling `.promisor` / `.mtimes` markers exist next to `pack_path`.
1260    pub fn sidecar_flags(pack_path: &Path) -> (bool, bool) {
1261        (
1262            marker_sidecar_is_file(&pack_path.with_extension("promisor")),
1263            marker_sidecar_is_file(&pack_path.with_extension("mtimes")),
1264        )
1265    }
1266
1267    fn refresh_pack_sidecar_flags(idx: Arc<PackIndex>) -> Arc<PackIndex> {
1268        let (is_promisor, is_cruft) = sidecar_flags(&idx.pack_path);
1269        if idx.is_promisor == is_promisor && idx.is_cruft == is_cruft {
1270            return idx;
1271        }
1272        let updated = Arc::new(idx.with_sidecar_flags(is_promisor, is_cruft));
1273        with_current(|g| {
1274            g.by_idx.insert(
1275                idx.idx_path.clone(),
1276                CachedIdx {
1277                    idx: Arc::clone(&updated),
1278                },
1279            );
1280        });
1281        updated
1282    }
1283
1284    fn dir_mtime(path: &Path) -> SystemTime {
1285        #[cfg(test)]
1286        crate::hot_path_test_metrics::record_pack_dir_stat_for_active_scope();
1287        #[cfg(test)]
1288        {
1289            if let Some(store) = alternate_read_store() {
1290                if path.starts_with(store.objects_dir()) {
1291                    crate::hot_path_test_metrics::record_pack_dir_metadata_for_active_scope();
1292                }
1293            }
1294            crate::hot_path_test_metrics::record_pack_dir_stat_for_active_scope();
1295        }
1296        fs::metadata(path)
1297            .and_then(|m| m.modified())
1298            .unwrap_or(SystemTime::UNIX_EPOCH)
1299    }
1300
1301    fn file_signature(path: &Path) -> Option<(SystemTime, u64)> {
1302        #[cfg(test)]
1303        crate::hot_path_test_metrics::record_pack_signature_stat_for_active_scope();
1304        let m = fs::metadata(path).ok()?;
1305        let mtime = m.modified().unwrap_or(SystemTime::UNIX_EPOCH);
1306        Some((mtime, m.len()))
1307    }
1308
1309    pub fn pack_object_count_validated(pack_path: &Path) -> bool {
1310        if crate::pack_store::PackStore::has_context()
1311            && PACK_COUNT_VALIDATED.with(|cell| cell.borrow().contains(pack_path))
1312        {
1313            return true;
1314        }
1315        crate::pack_store::PackStore::active_for_reads()
1316            .with_catalog_read(|g| g.validated_pack_counts.contains(pack_path))
1317    }
1318
1319    pub fn mark_pack_object_count_validated(pack_path: &Path) {
1320        if crate::pack_store::PackStore::has_context() {
1321            PACK_COUNT_VALIDATED.with(|cell| {
1322                cell.borrow_mut().insert(pack_path.to_path_buf());
1323            });
1324        }
1325        let store = crate::pack_store::PackStore::active_for_reads();
1326        if store.with_catalog_read(|g| g.validated_pack_counts.contains(pack_path)) {
1327            return;
1328        }
1329        let _ = store.try_with_catalog(|g| {
1330            g.validated_pack_counts.insert(pack_path.to_path_buf());
1331        });
1332    }
1333
1334    fn cached_index(idx_path: &Path) -> Option<Arc<PackIndex>> {
1335        if !crate::pack_store::PackStore::has_context() {
1336            return None;
1337        }
1338        PACK_INDEXES.with(|cell| cell.borrow().get(idx_path).map(Arc::clone))
1339    }
1340
1341    fn remember_index(idx_path: &Path, idx: &Arc<PackIndex>) {
1342        if !crate::pack_store::PackStore::has_context() {
1343            return;
1344        }
1345        PACK_INDEXES.with(|cell| {
1346            cell.borrow_mut()
1347                .insert(idx_path.to_path_buf(), Arc::clone(idx));
1348        });
1349    }
1350
1351    fn index_cache_hit_stale(idx: &PackIndex) -> bool {
1352        !idx.pack_path.is_file()
1353    }
1354
1355    /// Get a parsed pack index from cache, parsing from disk only on cache miss.
1356    pub fn get_index(idx_path: &Path) -> Result<Arc<PackIndex>> {
1357        if let Some(hit) = cached_index(idx_path) {
1358            if index_cache_hit_stale(&hit) {
1359                PACK_INDEXES.with(|cell| {
1360                    cell.borrow_mut().remove(idx_path);
1361                });
1362            } else {
1363                return Ok(hit);
1364            }
1365        }
1366        let store = catalog_store();
1367        if let Some(hit) =
1368            store.with_catalog_read(|g| g.by_idx.get(idx_path).map(|c| Arc::clone(&c.idx)))
1369        {
1370            if index_cache_hit_stale(&hit) {
1371                store.with_catalog(|g| {
1372                    g.by_idx.remove(idx_path);
1373                });
1374            } else {
1375                remember_index(idx_path, &hit);
1376                return Ok(hit);
1377            }
1378        }
1379        crate::trace::trace_span!("grit.pack.index.open");
1380        let parsed = Arc::new(read_pack_index_no_verify(idx_path)?);
1381        store.with_catalog(|g| {
1382            g.by_idx.insert(
1383                idx_path.to_path_buf(),
1384                CachedIdx {
1385                    idx: Arc::clone(&parsed),
1386                },
1387            );
1388        });
1389        remember_index(idx_path, &parsed);
1390        Ok(parsed)
1391    }
1392
1393    fn rescan_dir_indexes(objects_dir: &Path) -> Result<Vec<Arc<PackIndex>>> {
1394        let pack_dir = objects_dir.join("pack");
1395        drop_thread_local_dir_listing(&pack_dir);
1396        let dir_mt = dir_mtime(&pack_dir);
1397
1398        let rd = match fs::read_dir(&pack_dir) {
1399            Ok(rd) => rd,
1400            Err(err) if err.kind() == io::ErrorKind::NotFound => {
1401                with_current(|g| {
1402                    g.by_dir.insert(
1403                        pack_dir.clone(),
1404                        CachedDir {
1405                            dir_mtime: dir_mt,
1406                            indexes: Vec::new(),
1407                            midx_mtime: SystemTime::UNIX_EPOCH,
1408                            midx_pack_names: HashSet::new(),
1409                            idx_listing_verified: true,
1410                        },
1411                    );
1412                });
1413                return Ok(Vec::new());
1414            }
1415            Err(err) => return Err(Error::Io(err)),
1416        };
1417
1418        let mut out = Vec::new();
1419        for entry in rd {
1420            let entry = entry.map_err(Error::Io)?;
1421            let path = entry.path();
1422            if path.extension().and_then(|s| s.to_str()) != Some("idx") {
1423                continue;
1424            }
1425            let Ok(idx) = get_index(&path) else {
1426                continue;
1427            };
1428            if !idx.pack_path.is_file() {
1429                continue;
1430            }
1431            out.push(refresh_pack_sidecar_flags(idx));
1432        }
1433
1434        let midx_pack_names = load_midx_pack_names(&pack_dir);
1435        with_current(|g| {
1436            #[cfg(any(test, feature = "test-support"))]
1437            {
1438                *g.test_dir_rescan_counts
1439                    .entry(pack_dir.clone())
1440                    .or_insert(0) += 1;
1441            }
1442            g.by_dir.insert(
1443                pack_dir,
1444                CachedDir {
1445                    dir_mtime: dir_mt,
1446                    indexes: out.clone(),
1447                    midx_mtime: midx_pack_names.0,
1448                    midx_pack_names: midx_pack_names.1,
1449                    idx_listing_verified: false,
1450                },
1451            );
1452        });
1453        STORE_TLS_SNAPSHOTS.with(|map| {
1454            map.borrow_mut()
1455                .remove(&crate::pack_store::PackStore::active_for_reads().id());
1456        });
1457        Ok(out)
1458    }
1459
1460    fn load_midx_pack_names(pack_dir: &Path) -> (SystemTime, HashSet<String>) {
1461        let Some(midx_path) = crate::midx::resolve_tip_midx_path(pack_dir) else {
1462            return (SystemTime::UNIX_EPOCH, HashSet::new());
1463        };
1464        let metadata = fs::metadata(&midx_path).ok();
1465        let mtime = metadata
1466            .as_ref()
1467            .and_then(|m| m.modified().ok())
1468            .unwrap_or(SystemTime::UNIX_EPOCH);
1469        let names = metadata
1470            .as_ref()
1471            .and_then(|metadata| {
1472                crate::midx::read_midx_pack_idx_names_from_path(&midx_path, metadata).ok()
1473            })
1474            .unwrap_or_default()
1475            .into_iter()
1476            .collect();
1477        (mtime, names)
1478    }
1479
1480    fn refresh_midx_pack_names_locked(pack_dir: &Path, cached: &mut CachedDir) {
1481        let (mtime, names) = load_midx_pack_names(pack_dir);
1482        if cached.midx_mtime != mtime {
1483            cached.midx_mtime = mtime;
1484            cached.midx_pack_names = names;
1485        }
1486    }
1487
1488    /// Move `idx_path` to the front of the MRU pack search order for `objects_dir`.
1489    pub fn promote_pack_index(objects_dir: &Path, idx_path: &Path) {
1490        let pack_dir = objects_dir.join("pack");
1491        let store = crate::pack_store::PackStore::active_for_reads();
1492        let _ = store.try_with_catalog(|g| {
1493            let Some(cached) = g.by_dir.get_mut(&pack_dir) else {
1494                return;
1495            };
1496            let Some(pos) = cached
1497                .indexes
1498                .iter()
1499                .position(|idx| idx.idx_path == idx_path)
1500            else {
1501                return;
1502            };
1503            if pos == 0 {
1504                return;
1505            }
1506            let hit = cached.indexes.remove(pos);
1507            cached.indexes.insert(0, hit);
1508        });
1509    }
1510
1511    /// Pack index basenames listed in the active multi-pack-index, if any.
1512    pub fn midx_covered_pack_names(objects_dir: &Path) -> HashSet<String> {
1513        let pack_dir = objects_dir.join("pack");
1514        let cached = with_current(|g| {
1515            let cached = g.by_dir.get_mut(&pack_dir)?;
1516            refresh_midx_pack_names_locked(&pack_dir, cached);
1517            Some(cached.midx_pack_names.clone())
1518        });
1519        if let Some(names) = cached {
1520            return names;
1521        }
1522        load_midx_pack_names(&pack_dir).1
1523    }
1524
1525    /// Get all `.idx` files for `objects_dir`, using the cached directory listing when present.
1526    pub fn get_dir_indexes(objects_dir: &Path) -> Result<Vec<Arc<PackIndex>>> {
1527        if let Some(store) = alternate_read_store() {
1528            if store.objects_dir() == objects_dir
1529                && !crate::pack_store::PackStore::read_context_matches_objects_dir(objects_dir)
1530            {
1531                return get_dir_indexes_with_store(&store, objects_dir);
1532            }
1533        }
1534        let pack_dir = objects_dir.join("pack");
1535        let store = crate::pack_store::PackStore::active_for_reads();
1536        if crate::pack_store::PackStore::has_context() {
1537            sync_thread_local_with_store(&store);
1538        }
1539        if let Some(hit) = cached_dir_indexes(objects_dir, &pack_dir) {
1540            return Ok(hit);
1541        }
1542        let dir_mt = dir_mtime(&pack_dir);
1543        if let Some(hit) = store.with_catalog_read(|g| {
1544            g.by_dir
1545                .get(&pack_dir)
1546                .and_then(|c| (c.dir_mtime == dir_mt).then(|| c.indexes.clone()))
1547        }) {
1548            remember_dir_indexes(objects_dir, &pack_dir, &hit);
1549            return Ok(hit);
1550        }
1551        let fresh = rescan_dir_indexes(objects_dir)?;
1552        remember_dir_indexes(objects_dir, &pack_dir, &fresh);
1553        Ok(fresh)
1554    }
1555
1556    /// Pack indexes for `objects_dir` using `store`'s catalog without swapping the active
1557    /// pack-read context (shared-clone alternates during a primary object walk).
1558    pub(crate) fn get_dir_indexes_with_store(
1559        store: &crate::pack_store::PackStore,
1560        objects_dir: &Path,
1561    ) -> Result<Vec<Arc<PackIndex>>> {
1562        debug_assert_eq!(store.objects_dir(), objects_dir);
1563        let pack_dir = objects_dir.join("pack");
1564        if let Some(hit) = store.with_catalog_read(|g| {
1565            g.by_dir.get(&pack_dir).and_then(|c| {
1566                (c.idx_listing_verified || !c.indexes.is_empty()).then(|| c.indexes.clone())
1567            })
1568        }) {
1569            return Ok(hit);
1570        }
1571        rescan_dir_indexes_for_store(store, objects_dir)
1572    }
1573
1574    fn get_index_for_store(
1575        store: &crate::pack_store::PackStore,
1576        idx_path: &Path,
1577    ) -> Result<Arc<PackIndex>> {
1578        if let Some(hit) =
1579            store.with_catalog_read(|g| g.by_idx.get(idx_path).map(|c| Arc::clone(&c.idx)))
1580        {
1581            if !index_cache_hit_stale(&hit) {
1582                return Ok(hit);
1583            }
1584            store.with_catalog(|g| {
1585                g.by_idx.remove(idx_path);
1586            });
1587        }
1588        crate::trace::trace_span!("grit.pack.index.open");
1589        let parsed = Arc::new(read_pack_index_no_verify(idx_path)?);
1590        store.with_catalog(|g| {
1591            g.by_idx.insert(
1592                idx_path.to_path_buf(),
1593                CachedIdx {
1594                    idx: Arc::clone(&parsed),
1595                },
1596            );
1597        });
1598        Ok(parsed)
1599    }
1600
1601    fn rescan_dir_indexes_for_store(
1602        store: &crate::pack_store::PackStore,
1603        objects_dir: &Path,
1604    ) -> Result<Vec<Arc<PackIndex>>> {
1605        let pack_dir = objects_dir.join("pack");
1606        let dir_mt = dir_mtime(&pack_dir);
1607        let rd = match fs::read_dir(&pack_dir) {
1608            Ok(rd) => rd,
1609            Err(err) if err.kind() == io::ErrorKind::NotFound => {
1610                store.with_catalog(|g| {
1611                    g.by_dir.insert(
1612                        pack_dir.clone(),
1613                        CachedDir {
1614                            dir_mtime: dir_mt,
1615                            indexes: Vec::new(),
1616                            midx_mtime: SystemTime::UNIX_EPOCH,
1617                            midx_pack_names: HashSet::new(),
1618                            idx_listing_verified: true,
1619                        },
1620                    );
1621                });
1622                return Ok(Vec::new());
1623            }
1624            Err(err) => return Err(Error::Io(err)),
1625        };
1626        let mut out = Vec::new();
1627        for entry in rd {
1628            let entry = entry.map_err(Error::Io)?;
1629            let path = entry.path();
1630            if path.extension().and_then(|s| s.to_str()) != Some("idx") {
1631                continue;
1632            }
1633            let Ok(idx) = get_index_for_store(store, &path) else {
1634                continue;
1635            };
1636            if !idx.pack_path.is_file() {
1637                continue;
1638            }
1639            out.push(refresh_pack_sidecar_flags(idx));
1640        }
1641        let midx_pack_names = load_midx_pack_names(&pack_dir);
1642        store.with_catalog(|g| {
1643            #[cfg(any(test, feature = "test-support"))]
1644            {
1645                *g.test_dir_rescan_counts
1646                    .entry(pack_dir.clone())
1647                    .or_insert(0) += 1;
1648            }
1649            g.by_dir.insert(
1650                pack_dir,
1651                CachedDir {
1652                    dir_mtime: dir_mt,
1653                    indexes: out.clone(),
1654                    midx_mtime: midx_pack_names.0,
1655                    midx_pack_names: midx_pack_names.1,
1656                    idx_listing_verified: false,
1657                },
1658            );
1659        });
1660        Ok(out)
1661    }
1662
1663    pub(crate) fn get_pack_bytes_for_store(
1664        store: &crate::pack_store::PackStore,
1665        pack_path: &Path,
1666    ) -> Result<Arc<PackData>> {
1667        if let Some(hit) =
1668            store.with_catalog_read(|g| g.by_pack.get(pack_path).map(|c| Arc::clone(&c.bytes)))
1669        {
1670            return Ok(hit);
1671        }
1672        let (mtime, size) = file_signature(pack_path).ok_or_else(|| {
1673            Error::Io(io::Error::new(
1674                io::ErrorKind::NotFound,
1675                format!("pack not found: {}", pack_path.display()),
1676            ))
1677        })?;
1678        load_pack_bytes_from_disk_for_store(store, pack_path, mtime, size)
1679    }
1680
1681    fn load_pack_bytes_from_disk_for_store(
1682        store: &crate::pack_store::PackStore,
1683        pack_path: &Path,
1684        mtime: SystemTime,
1685        size: u64,
1686    ) -> Result<Arc<PackData>> {
1687        let bytes = if store.bounded_reads() {
1688            PackData::open_random_access(pack_path)?
1689        } else {
1690            PackData::open_for_sparse_reads(pack_path, &store.sparse_read_files)?
1691        };
1692        store.with_catalog(|g| {
1693            drop_delta_entries_locked(g, pack_path, store.delta_bases());
1694            g.by_idx.remove(&pack_path.with_extension("idx"));
1695            g.validated_pack_counts.remove(pack_path);
1696            if store.bounded_reads() {
1697                g.retain_bounded_pack(pack_path);
1698            }
1699            g.by_pack.insert(
1700                pack_path.to_path_buf(),
1701                CachedPack {
1702                    mtime,
1703                    size,
1704                    bytes: Arc::clone(&bytes),
1705                },
1706            );
1707        });
1708        Ok(bytes)
1709    }
1710
1711    pub(crate) fn reprepare_dir_on_miss_for_store(
1712        store: &crate::pack_store::PackStore,
1713        objects_dir: &Path,
1714    ) -> Result<bool> {
1715        let pack_dir = objects_dir.join("pack");
1716        let dir_mt = dir_mtime(&pack_dir);
1717        if store.with_catalog_read(|g| {
1718            g.by_dir.get(&pack_dir).is_some_and(|c| {
1719                c.idx_listing_verified && c.indexes.is_empty() && c.dir_mtime == dir_mt
1720            })
1721        }) {
1722            return Ok(false);
1723        }
1724        enum ReprepareDecision {
1725            Fresh,
1726            Rescan,
1727            VerifyIdxSet,
1728        }
1729        let decision = store.with_catalog_read(|g| match g.by_dir.get(&pack_dir) {
1730            None => ReprepareDecision::Rescan,
1731            Some(c) if c.dir_mtime != dir_mt => ReprepareDecision::Rescan,
1732            Some(c) if c.idx_listing_verified && !c.indexes.is_empty() => ReprepareDecision::Fresh,
1733            Some(_) => ReprepareDecision::VerifyIdxSet,
1734        });
1735        match decision {
1736            ReprepareDecision::Fresh => return Ok(false),
1737            ReprepareDecision::Rescan => {
1738                rescan_dir_indexes_for_store(store, objects_dir)?;
1739                return Ok(true);
1740            }
1741            ReprepareDecision::VerifyIdxSet => {}
1742        }
1743        let cached_paths = store.with_catalog_read(|g| {
1744            g.by_dir.get(&pack_dir).map(|c| {
1745                c.indexes
1746                    .iter()
1747                    .map(|idx| idx.idx_path.clone())
1748                    .collect::<HashSet<_>>()
1749            })
1750        });
1751        let Some(cached_paths) = cached_paths else {
1752            rescan_dir_indexes_for_store(store, objects_dir)?;
1753            return Ok(true);
1754        };
1755        let on_disk = idx_paths_on_disk(&pack_dir)?;
1756        if on_disk == cached_paths {
1757            store.with_catalog(|g| {
1758                if let Some(c) = g.by_dir.get_mut(&pack_dir) {
1759                    c.idx_listing_verified = true;
1760                }
1761            });
1762            return Ok(false);
1763        }
1764        rescan_dir_indexes_for_store(store, objects_dir)?;
1765        Ok(true)
1766    }
1767
1768    fn idx_paths_on_disk(pack_dir: &Path) -> Result<HashSet<PathBuf>> {
1769        let mut out = HashSet::new();
1770        let rd = match fs::read_dir(pack_dir) {
1771            Ok(rd) => rd,
1772            Err(err) if err.kind() == io::ErrorKind::NotFound => return Ok(out),
1773            Err(err) => return Err(Error::Io(err)),
1774        };
1775        for entry in rd {
1776            let entry = entry.map_err(Error::Io)?;
1777            let path = entry.path();
1778            if path.extension().and_then(|s| s.to_str()) != Some("idx") {
1779                continue;
1780            }
1781            if path.with_extension("pack").is_file() {
1782                out.insert(path);
1783            }
1784        }
1785        Ok(out)
1786    }
1787
1788    /// Re-scan `objects_dir`/`pack/` when its mtime changed since the cached listing (Git
1789    /// `reprepare_packed_git`), or when the set of pack index files no longer matches the
1790    /// cached listing. Returns true if the listing was refreshed.
1791    pub fn reprepare_dir_on_miss(objects_dir: &Path) -> Result<bool> {
1792        if let Some(store) = alternate_read_store() {
1793            if store.objects_dir() == objects_dir
1794                && !crate::pack_store::PackStore::read_context_matches_objects_dir(objects_dir)
1795            {
1796                return reprepare_dir_on_miss_for_store(&store, objects_dir);
1797            }
1798        }
1799        let pack_dir = objects_dir.join("pack");
1800        let dir_mt = dir_mtime(&pack_dir);
1801        let store = crate::pack_store::PackStore::active_for_reads();
1802        if store.with_catalog_read(|g| {
1803            g.by_dir.get(&pack_dir).is_some_and(|c| {
1804                c.idx_listing_verified && c.indexes.is_empty() && c.dir_mtime == dir_mt
1805            })
1806        }) && crate::pack_store::PackStore::read_context_matches_objects_dir(objects_dir)
1807            && PACK_DIR_LISTING_TRUSTED.get()
1808        {
1809            return Ok(false);
1810        }
1811        enum ReprepareDecision {
1812            Fresh,
1813            Rescan,
1814            VerifyIdxSet,
1815        }
1816        let decision = store.with_catalog_read(|g| match g.by_dir.get(&pack_dir) {
1817            None => ReprepareDecision::Rescan,
1818            Some(c) if c.dir_mtime != dir_mt => ReprepareDecision::Rescan,
1819            Some(c) if c.idx_listing_verified && !c.indexes.is_empty() => ReprepareDecision::Fresh,
1820            Some(_) => ReprepareDecision::VerifyIdxSet,
1821        });
1822        match decision {
1823            ReprepareDecision::Fresh => return Ok(false),
1824            ReprepareDecision::Rescan => {
1825                rescan_dir_indexes(objects_dir)?;
1826                return Ok(true);
1827            }
1828            ReprepareDecision::VerifyIdxSet => {}
1829        }
1830        let cached_paths = store.with_catalog_read(|g| {
1831            g.by_dir.get(&pack_dir).map(|c| {
1832                c.indexes
1833                    .iter()
1834                    .map(|idx| idx.idx_path.clone())
1835                    .collect::<HashSet<_>>()
1836            })
1837        });
1838        let Some(cached_paths) = cached_paths else {
1839            rescan_dir_indexes(objects_dir)?;
1840            return Ok(true);
1841        };
1842        let on_disk = idx_paths_on_disk(&pack_dir)?;
1843        if on_disk == cached_paths {
1844            store.with_catalog(|g| {
1845                if let Some(c) = g.by_dir.get_mut(&pack_dir) {
1846                    c.idx_listing_verified = true;
1847                }
1848            });
1849            return Ok(false);
1850        }
1851        rescan_dir_indexes(objects_dir)?;
1852        Ok(true)
1853    }
1854
1855    fn load_pack_bytes_from_disk(
1856        pack_path: &Path,
1857        mtime: SystemTime,
1858        size: u64,
1859    ) -> Result<Arc<PackData>> {
1860        let store = crate::pack_store::PackStore::active_for_reads();
1861        // A first load replaces nothing, so the thread's trusted pack listing and the
1862        // parsed index stay valid; dropping them would make every later object read
1863        // re-stat the pack directory.
1864        let replaces_cached = store.with_catalog_read(|g| g.by_pack.contains_key(pack_path))
1865            || PACK_BYTES.with(|cell| cell.borrow().contains_key(pack_path));
1866        if replaces_cached {
1867            invalidate_thread_local_for_pack_path_change(pack_path);
1868        }
1869        let bytes = if store.bounded_reads() {
1870            PackData::open_random_access(pack_path)?
1871        } else {
1872            PackData::open_for_sparse_reads(pack_path, &store.sparse_read_files)?
1873        };
1874        store.with_catalog(|g| {
1875            drop_delta_entries_locked(g, pack_path, store.delta_bases());
1876            if replaces_cached {
1877                g.by_idx.remove(&pack_path.with_extension("idx"));
1878                g.validated_pack_counts.remove(pack_path);
1879            }
1880            if store.bounded_reads() {
1881                g.retain_bounded_pack(pack_path);
1882            }
1883            g.by_pack.insert(
1884                pack_path.to_path_buf(),
1885                CachedPack {
1886                    mtime,
1887                    size,
1888                    bytes: Arc::clone(&bytes),
1889                },
1890            );
1891        });
1892        remember_pack_bytes(pack_path, &bytes);
1893        Ok(bytes)
1894    }
1895
1896    /// Get the raw bytes of a pack file from cache, reading from disk only on cache miss.
1897    ///
1898    /// Cache hits skip `stat` for all pack basenames; in-process repack/gc clears the cache
1899    /// when packs change.
1900    pub fn get_pack_bytes(pack_path: &Path) -> Result<Arc<PackData>> {
1901        if let Some(store) = alternate_read_store() {
1902            if pack_path.starts_with(store.objects_dir()) {
1903                return get_pack_bytes_for_store(&store, pack_path);
1904            }
1905        }
1906        if let Some(hit) = cached_pack_bytes(pack_path) {
1907            return Ok(hit);
1908        }
1909        let store = crate::pack_store::PackStore::active_for_reads();
1910        let hit = if store.bounded_reads() {
1911            store.with_catalog(|g| {
1912                let bytes = Arc::clone(&g.by_pack.get(pack_path)?.bytes);
1913                g.retain_bounded_pack(pack_path);
1914                Some(bytes)
1915            })
1916        } else {
1917            store.with_catalog_read(|g| g.by_pack.get(pack_path).map(|c| Arc::clone(&c.bytes)))
1918        };
1919        if let Some(hit) = hit {
1920            remember_pack_bytes(pack_path, &hit);
1921            return Ok(hit);
1922        }
1923        let (mtime, size) = file_signature(pack_path).ok_or_else(|| {
1924            Error::Io(io::Error::new(
1925                io::ErrorKind::NotFound,
1926                format!("pack not found: {}", pack_path.display()),
1927            ))
1928        })?;
1929        load_pack_bytes_from_disk(pack_path, mtime, size)
1930    }
1931
1932    fn revalidate_stale_pack_bytes_in_context(pack_path: &Path) -> Result<bool> {
1933        let Some((mtime, size)) = file_signature(pack_path) else {
1934            let store = crate::pack_store::PackStore::active_for_reads();
1935            let removed = store.with_catalog(|g| {
1936                let removed = g.by_pack.remove(pack_path).is_some();
1937                if removed {
1938                    drop_delta_entries_locked(g, pack_path, store.delta_bases());
1939                }
1940                removed
1941            });
1942            invalidate_thread_local_for_pack_path_change(pack_path);
1943            super::drop_pack_read_info_cache_for(pack_path);
1944            return Ok(removed);
1945        };
1946        let store = crate::pack_store::PackStore::active_for_reads();
1947        let stale = store.with_catalog_read(|g| match g.by_pack.get(pack_path) {
1948            Some(c) => c.mtime != mtime || c.size != size,
1949            None => true,
1950        });
1951        if !stale {
1952            return Ok(false);
1953        }
1954        load_pack_bytes_from_disk(pack_path, mtime, size)?;
1955        super::drop_pack_read_info_cache_for(pack_path);
1956        Ok(true)
1957    }
1958
1959    /// Re-read `pack_path` when its on-disk signature differs from the cached entry.
1960    ///
1961    /// Returns true when bytes were reloaded or the cache entry was dropped.
1962    pub fn revalidate_stale_pack_bytes(pack_path: &Path) -> Result<bool> {
1963        if crate::pack_store::PackStore::has_context() {
1964            return revalidate_stale_pack_bytes_in_context(pack_path);
1965        }
1966        let store = crate::pack_store::PackStore::legacy_for_pack_path(pack_path);
1967        crate::pack_store::PackStore::with_context(store, || {
1968            revalidate_stale_pack_bytes_in_context(pack_path)
1969        })
1970    }
1971
1972    /// After a pack parse/decompress failure, reload from disk when cached bytes differ.
1973    ///
1974    /// Signature-only checks miss in-memory stale entries (tests) and same-second replacements
1975    /// with unchanged size; comparing the pack header and trailer fingerprint catches those
1976    /// without reading the whole file.
1977    ///
1978    /// Returns true when the cache was refreshed with disk contents.
1979    pub fn reload_pack_bytes_after_parse_failure(pack_path: &Path) -> Result<bool> {
1980        if revalidate_stale_pack_bytes(pack_path)? {
1981            return Ok(true);
1982        }
1983        let Some((mtime, size)) = file_signature(pack_path) else {
1984            return Ok(false);
1985        };
1986        let hash_bytes = hash_bytes_for_pack_file(pack_path)?;
1987        let disk_fp = fingerprint_from_file(pack_path, hash_bytes)?;
1988        let needs_reload = with_current(|g| {
1989            g.by_pack.get(pack_path).is_some_and(|c| {
1990                pack_fingerprint_mismatch(&c.bytes, hash_bytes, &disk_fp).unwrap_or(true)
1991            })
1992        });
1993        if !needs_reload {
1994            return Ok(false);
1995        }
1996        load_pack_bytes_from_disk(pack_path, mtime, size)?;
1997        super::drop_pack_read_info_cache_for(pack_path);
1998        Ok(true)
1999    }
2000
2001    fn hash_bytes_for_pack_file(pack_path: &Path) -> Result<usize> {
2002        let idx_path = pack_path.with_extension("idx");
2003        if let Some(hash_bytes) =
2004            with_current(|g| g.by_idx.get(&idx_path).map(|c| c.idx.hash_bytes()))
2005        {
2006            return Ok(hash_bytes);
2007        }
2008        Ok(read_pack_index_no_verify(&idx_path)?.hash_bytes())
2009    }
2010
2011    fn pack_fingerprint_mismatch(
2012        cached: &PackData,
2013        hash_bytes: usize,
2014        disk: &PackFingerprint,
2015    ) -> Result<bool> {
2016        Ok(cached.fingerprint(hash_bytes)? != *disk)
2017    }
2018
2019    #[cfg(test)]
2020    pub fn test_reset_dir_rescan_count(pack_dir: &Path) {
2021        let objects_dir = pack_dir.parent().expect("pack dir under objects/");
2022        crate::pack_store::PackStore::legacy_for_objects_dir(objects_dir).with_pack(|g| {
2023            g.by_dir.remove(pack_dir);
2024            g.test_dir_rescan_counts.insert(pack_dir.to_path_buf(), 0);
2025        });
2026    }
2027
2028    #[cfg(test)]
2029    pub fn test_reset_marker_stat_count() {
2030        TEST_MARKER_STAT_COUNT.with(|count| count.set(0));
2031    }
2032
2033    #[cfg(test)]
2034    #[must_use]
2035    pub fn test_marker_stat_count() -> u64 {
2036        TEST_MARKER_STAT_COUNT.with(std::cell::Cell::get)
2037    }
2038
2039    #[cfg(test)]
2040    #[must_use]
2041    pub fn test_dir_rescan_count(pack_dir: &Path) -> u64 {
2042        let objects_dir = pack_dir.parent().expect("pack dir under objects/");
2043        test_dir_rescan_count_on_store(
2044            &crate::pack_store::PackStore::legacy_for_objects_dir(objects_dir),
2045            pack_dir,
2046        )
2047    }
2048
2049    #[cfg(test)]
2050    #[must_use]
2051    pub fn test_dir_rescan_count_on_store(
2052        store: &std::sync::Arc<crate::pack_store::PackStore>,
2053        pack_dir: &Path,
2054    ) -> u64 {
2055        store.with_pack(|g| g.test_dir_rescan_counts.get(pack_dir).copied().unwrap_or(0))
2056    }
2057
2058    #[cfg(test)]
2059    pub fn test_reset_dir_rescan_count_on_store(
2060        store: &std::sync::Arc<crate::pack_store::PackStore>,
2061        pack_dir: &Path,
2062    ) {
2063        store.with_pack(|g| {
2064            g.by_dir.remove(pack_dir);
2065            g.test_dir_rescan_counts.insert(pack_dir.to_path_buf(), 0);
2066        });
2067    }
2068
2069    /// Replace cached pack bytes (tests simulating a stale in-memory mapping).
2070    #[cfg(test)]
2071    pub fn test_inject_stale_pack_bytes(pack_path: &Path, stale: Arc<PackData>) {
2072        crate::pack_store::PackStore::legacy_for_pack_path(pack_path).with_pack(|g| {
2073            let stamp = g
2074                .by_pack
2075                .get(pack_path)
2076                .map(|c| (c.mtime, c.size))
2077                .unwrap_or((SystemTime::UNIX_EPOCH, stale.len() as u64));
2078            g.by_pack.insert(
2079                pack_path.to_path_buf(),
2080                CachedPack {
2081                    mtime: stamp.0,
2082                    size: stamp.1,
2083                    bytes: stale,
2084                },
2085            );
2086        });
2087    }
2088
2089    fn clear_inner(g: &mut CatalogState) {
2090        g.by_dir.clear();
2091        g.by_idx.clear();
2092        g.by_pack.clear();
2093        g.bounded_pack_order.clear();
2094        g.pack_ids.clear();
2095    }
2096
2097    /// Delta-base byte cap for the active read store (default [`DELTA_BASE_CACHE_DEFAULT`]).
2098    pub fn set_delta_base_cache_byte_limit(limit: usize) {
2099        crate::pack_store::PackStore::active_for_reads()
2100            .delta_bases()
2101            .set_byte_limit(limit);
2102    }
2103
2104    #[cfg(test)]
2105    pub fn test_delta_base_cache_bytes_used(pack_path: &Path) -> usize {
2106        crate::pack_store::PackStore::legacy_for_pack_path(pack_path)
2107            .delta_bases()
2108            .bytes_used()
2109    }
2110
2111    #[cfg(test)]
2112    pub fn test_set_delta_base_cache_byte_limit(pack_path: &Path, limit: usize) {
2113        crate::pack_store::PackStore::legacy_for_pack_path(pack_path)
2114            .delta_bases()
2115            .set_byte_limit(limit);
2116    }
2117
2118    #[cfg(test)]
2119    pub fn test_delta_base_cached(pack_path: &Path, pack_id: u32, offset: u64) -> bool {
2120        crate::pack_store::PackStore::legacy_for_pack_path(pack_path)
2121            .delta_bases()
2122            .contains(pack_id, offset)
2123    }
2124
2125    fn pack_id_for_in_context(pack_path: &Path) -> u32 {
2126        if let Some(id) = PACK_IDS.with(|cell| cell.borrow().get(pack_path.as_os_str()).copied()) {
2127            return id;
2128        }
2129        let store = crate::pack_store::PackStore::active_for_reads();
2130        if let Some(id) = store.with_catalog_read(|g| g.pack_ids.get(pack_path).copied()) {
2131            PACK_IDS.with(|cell| {
2132                cell.borrow_mut()
2133                    .insert(pack_path.as_os_str().to_owned(), id);
2134            });
2135            return id;
2136        }
2137        store.with_catalog(|g| {
2138            if let Some(&id) = g.pack_ids.get(pack_path) {
2139                return id;
2140            }
2141            let id = g.next_pack_id;
2142            g.next_pack_id = g.next_pack_id.saturating_add(1);
2143            g.pack_ids.insert(pack_path.to_path_buf(), id);
2144            PACK_IDS.with(|cell| {
2145                cell.borrow_mut()
2146                    .insert(pack_path.as_os_str().to_owned(), id);
2147            });
2148            id
2149        })
2150    }
2151
2152    /// Stable small id for `pack_path`, assigned on first use.
2153    pub fn pack_id_for(pack_path: &Path) -> u32 {
2154        if crate::pack_store::PackStore::has_context() {
2155            return pack_id_for_in_context(pack_path);
2156        }
2157        let store = crate::pack_store::PackStore::legacy_for_pack_path(pack_path);
2158        crate::pack_store::PackStore::with_context(store, || pack_id_for_in_context(pack_path))
2159    }
2160
2161    /// Drop every cached delta base inflated from `pack_path`.
2162    fn drop_delta_entries_locked(
2163        g: &mut CatalogState,
2164        pack_path: &Path,
2165        delta: &ShardedDeltaBaseLru,
2166    ) {
2167        if let Some(&pack_id) = g.pack_ids.get(pack_path) {
2168            delta.drop_pack(pack_id);
2169        }
2170    }
2171
2172    /// Cached resolved object at `(pack_id, offset)`, if still resident.
2173    pub fn get_delta_base(pack_id: u32, offset: u64) -> Option<(ObjectKind, Arc<[u8]>)> {
2174        crate::pack_store::PackStore::active_for_reads()
2175            .delta_bases()
2176            .get(DeltaCacheKey { pack_id, offset })
2177    }
2178
2179    #[must_use]
2180    pub fn delta_base_caching_enabled() -> bool {
2181        crate::pack_store::PackStore::active_for_reads()
2182            .delta_bases()
2183            .caching_enabled()
2184    }
2185
2186    /// Insert many intermediate bases (hot path for delta-chain resolution).
2187    pub fn put_delta_bases(kind: ObjectKind, items: &[(u32, u64, Arc<[u8]>)]) {
2188        if items.is_empty() {
2189            return;
2190        }
2191        crate::pack_store::PackStore::active_for_reads()
2192            .delta_bases()
2193            .put_many(kind, items);
2194    }
2195
2196    /// Drop only cached delta bases (pack indexes and bytes stay cached).
2197    pub fn clear_delta_bases() {
2198        crate::pack_store::PackStore::active_for_reads()
2199            .delta_bases()
2200            .clear();
2201    }
2202
2203    /// Re-stamp the cached signature for `pack_path` after the caller deliberately touched the
2204    /// file's mtime (object freshening). Pack contents are immutable for a given pack name, so
2205    /// a self-inflicted mtime bump must not evict the cached bytes — without this, every
2206    /// `odb.write` of an already-packed object forced a full re-read of the pack on the next
2207    /// lookup. We bump the cached mtime to now without `stat` (the caller just utime'd the file).
2208    /// External modifications still invalidate via [`revalidate_stale_pack_bytes`].
2209    pub fn refresh_pack_signature(pack_path: &Path, touched_at: SystemTime) {
2210        with_current(|g| {
2211            if let Some(c) = g.by_pack.get_mut(pack_path) {
2212                c.mtime = touched_at;
2213            }
2214        });
2215    }
2216
2217    #[cfg(test)]
2218    #[test]
2219    fn bounded_pack_cache_releases_inactive_readers() {
2220        let dir = tempfile::tempdir().expect("dir");
2221        let store = Arc::new(crate::pack_store::PackStore::for_bounded_reads(
2222            dir.path().to_path_buf(),
2223        ));
2224        crate::pack_store::PackStore::with_context(Arc::clone(&store), || {
2225            let mut previous = Vec::new();
2226            let hot_path = dir.path().join("0.pack");
2227            let mut hot = None;
2228            for i in 0..70 {
2229                let path = dir.path().join(format!("{i}.pack"));
2230                let mut bytes = vec![i as u8; 32768];
2231                bytes[..12].copy_from_slice(b"PACK\x00\x00\x00\x02\x00\x00\x00\x00");
2232                fs::write(&path, bytes).expect("write");
2233                let pack = get_pack_bytes(&path).expect("pack");
2234                assert!(pack.uses_positioned_reads());
2235                previous.push(Arc::downgrade(&pack));
2236                hot.get_or_insert_with(|| Arc::clone(&pack));
2237                assert!(Arc::ptr_eq(
2238                    &get_pack_bytes(&hot_path).expect("hot pack"),
2239                    hot.as_ref().expect("hot")
2240                ));
2241            }
2242            warm_thread_local_from_store(&store);
2243            assert!(store.with_catalog_read(|g| g.by_pack.len()) <= 32);
2244            assert!(previous[1..32].iter().all(|pack| pack.upgrade().is_none()));
2245            assert!(previous[69].upgrade().is_some());
2246            let reloaded = get_pack_bytes(&dir.path().join("1.pack")).expect("reopen");
2247            assert_eq!(
2248                reloaded
2249                    .read_range(100, 200, &mut Vec::new())
2250                    .expect("read"),
2251                &[1; 100]
2252            );
2253        });
2254    }
2255}
2256
2257/// Route pack catalog reads to `store` without changing the active pack-read context.
2258pub(crate) fn with_alternate_pack_store<R>(
2259    store: std::sync::Arc<crate::pack_store::PackStore>,
2260    f: impl FnOnce() -> R,
2261) -> R {
2262    pack_cache::with_alternate_pack_store(store, f)
2263}
2264
2265/// Read all pack indexes under `<objects_dir>/pack/` from the process-wide cache.
2266///
2267/// Cached reads skip the `.idx` SHA-1 trailer verification that [`read_pack_index`]
2268/// performs; corruption checks happen during `fsck`/`verify-pack`, not on every object
2269/// lookup (matches Git). The directory listing is prepared once per process and
2270/// re-scanned only on a lookup miss when the pack directory's mtime changed (Git
2271/// `reprepare_packed_git`), or when [`clear_pack_cache`] drops the cache after repack/gc.
2272///
2273/// # Errors
2274///
2275/// Returns [`Error::Io`] when the directory cannot be enumerated.
2276pub fn read_local_pack_indexes_cached(objects_dir: &Path) -> Result<Vec<Arc<PackIndex>>> {
2277    if crate::pack_store::PackStore::has_context() {
2278        return pack_cache::get_dir_indexes(objects_dir);
2279    }
2280    let store = crate::pack_store::PackStore::legacy_for_objects_dir(objects_dir);
2281    crate::pack_store::PackStore::with_context(store, || pack_cache::get_dir_indexes(objects_dir))
2282}
2283
2284/// Re-scan the pack directory when its mtime changed since the cached listing.
2285///
2286/// Called when an object was not found in loose storage or any known pack; returns
2287/// `true` when the listing was refreshed so the caller can retry once.
2288///
2289/// # Errors
2290///
2291/// Returns [`Error::Io`] when the pack directory cannot be read.
2292pub fn reprepare_pack_directory_on_miss(objects_dir: &Path) -> Result<bool> {
2293    if crate::pack_store::PackStore::has_context() {
2294        return reprepare_pack_directory_on_miss_in_context(objects_dir);
2295    }
2296    let store = crate::pack_store::PackStore::legacy_for_objects_dir(objects_dir);
2297    crate::pack_store::PackStore::with_context(store, || {
2298        reprepare_pack_directory_on_miss_in_context(objects_dir)
2299    })
2300}
2301
2302fn reprepare_pack_directory_on_miss_in_context(objects_dir: &Path) -> Result<bool> {
2303    let refreshed = pack_cache::reprepare_dir_on_miss(objects_dir)?;
2304    if refreshed {
2305        crate::midx::evict_midx_read_cache_for_pack_dir(&objects_dir.join("pack"));
2306    }
2307    Ok(refreshed)
2308}
2309
2310/// Read a single pack index from the process-wide cache (parses from disk on miss).
2311/// Skips trailer verification.
2312///
2313/// # Errors
2314///
2315/// Returns [`Error::Io`] when the file is missing or [`Error::CorruptObject`] for
2316/// malformed indexes.
2317pub fn read_pack_index_cached(idx_path: &Path) -> Result<Arc<PackIndex>> {
2318    if crate::pack_store::PackStore::has_context() {
2319        return pack_cache::get_index(idx_path);
2320    }
2321    let pack_path = idx_path.with_extension("pack");
2322    let store = crate::pack_store::PackStore::legacy_for_pack_path(&pack_path);
2323    crate::pack_store::PackStore::with_context(store, || pack_cache::get_index(idx_path))
2324}
2325
2326/// Read pack file bytes from the process-wide cache.
2327///
2328/// # Errors
2329///
2330/// Returns [`Error::Io`] when the pack cannot be read.
2331pub fn read_pack_bytes_cached(pack_path: &Path) -> Result<Arc<PackData>> {
2332    if crate::pack_store::PackStore::has_context() {
2333        return pack_cache::get_pack_bytes(pack_path);
2334    }
2335    let store = crate::pack_store::PackStore::legacy_for_pack_path(pack_path);
2336    crate::pack_store::PackStore::with_context(store, || pack_cache::get_pack_bytes(pack_path))
2337}
2338
2339/// Drop pack/MIDX caches on every registered [`PackStore`](crate::pack_store::PackStore).
2340///
2341/// Prefer [`Odb::invalidate_packs`](crate::odb::Odb::invalidate_packs) when an [`Odb`](crate::odb::Odb)
2342/// handle is available. Tests and benchmarks use this to reset all stores in the process.
2343pub fn clear_pack_cache() {
2344    #[cfg(any(test, feature = "test-support"))]
2345    let _guard = pack_cache_test_guard();
2346    clear_pack_read_info_cache();
2347    pack_cache::clear_thread_local_dir_indexes();
2348    crate::pack_store::PackStore::invalidate_all_legacy();
2349}
2350
2351/// Test helper: reset pack marker stat counter (integration tests only).
2352#[cfg(test)]
2353pub fn test_reset_pack_marker_stat_count() {
2354    pack_cache::test_reset_marker_stat_count();
2355}
2356
2357/// Test helper: read pack marker stat counter (integration tests only).
2358#[cfg(test)]
2359#[must_use]
2360pub fn test_pack_marker_stat_count() -> u64 {
2361    pack_cache::test_marker_stat_count()
2362}
2363
2364/// Drop only the process-wide delta-base LRU (indexes and pack bytes stay cached).
2365pub fn clear_pack_delta_base_cache() {
2366    pack_cache::clear_delta_bases();
2367}
2368
2369/// Apply `core.deltaBaseCacheLimit` from repository config to the process-wide delta-base LRU.
2370pub fn configure_delta_base_cache_from_config(cfg: Option<&crate::config::ConfigSet>) {
2371    let bytes = cfg
2372        .and_then(|c| c.get("core.deltaBaseCacheLimit"))
2373        .as_ref()
2374        .and_then(|v| crate::config::parse_i64(v).ok())
2375        .and_then(|n| usize::try_from(n).ok())
2376        .unwrap_or(pack_cache::DELTA_BASE_CACHE_DEFAULT);
2377    pack_cache::set_delta_base_cache_byte_limit(bytes);
2378}
2379
2380/// Re-stamp the cached pack-bytes signature after deliberately touching `pack_path`'s mtime
2381/// (object freshening). See the internal `pack_cache::refresh_pack_signature` helper.
2382pub fn refresh_pack_bytes_signature(pack_path: &Path, touched_at: SystemTime) {
2383    pack_cache::refresh_pack_signature(pack_path, touched_at);
2384}
2385
2386/// Collect aggregate local pack metrics.
2387///
2388/// # Errors
2389///
2390/// Returns [`Error::Io`] when reading pack metadata fails.
2391pub fn collect_local_pack_info(objects_dir: &Path) -> Result<LocalPackInfo> {
2392    let indexes = read_local_pack_indexes(objects_dir)?;
2393    let mut info = LocalPackInfo::default();
2394    for idx in indexes {
2395        let pack_meta = fs::metadata(&idx.pack_path).map_err(Error::Io)?;
2396        let idx_meta = fs::metadata(&idx.idx_path).map_err(Error::Io)?;
2397        info.pack_count += 1;
2398        info.object_count += idx.len();
2399        info.size_bytes += pack_meta.len() + idx_meta.len();
2400        let hash_bytes = idx.hash_bytes();
2401        for entry in idx.iter() {
2402            if entry.oid().len() == hash_bytes {
2403                if let Ok(oid) = ObjectId::from_bytes(entry.oid()) {
2404                    info.object_ids.insert(oid);
2405                }
2406            }
2407        }
2408    }
2409    Ok(info)
2410}
2411
2412/// Write a version-2 `.idx` for `entries` describing objects in `pack_path`.
2413///
2414/// `entries` lists `(oid, byte_offset)` pairs; they are sorted by OID before
2415/// writing. The pack file must already exist on disk with a valid trailing
2416/// checksum at the repository hash width `hash_bytes` (`20` or `32`).
2417///
2418/// # Errors
2419///
2420/// Returns [`Error::CorruptObject`] when the pack is too small or an entry
2421/// offset cannot be walked, or [`Error::Io`] on filesystem failure.
2422/// Write a v2 pack index file.
2423///
2424/// `sort_threads` controls OID-table sorting: `None` or `Some(1)` uses a single thread;
2425/// `Some(n)` with `n > 1` may sort chunks on up to `n` workers (for large entry counts).
2426pub fn write_v2_pack_index_with_trailer(
2427    idx_path: &Path,
2428    entries: &[(ObjectId, u64, u32)],
2429    pack_trailer: &[u8],
2430    hash_bytes: usize,
2431    sort_threads: Option<usize>,
2432) -> Result<()> {
2433    write_v2_pack_index_sorting(
2434        idx_path,
2435        &mut entries.to_vec(),
2436        pack_trailer,
2437        hash_bytes,
2438        sort_threads,
2439    )
2440}
2441
2442/// `write_v2_pack_index` function.
2443pub fn write_v2_pack_index(
2444    idx_path: &Path,
2445    pack_path: &Path,
2446    entries: &[(ObjectId, u64, u32)],
2447    hash_bytes: usize,
2448) -> Result<()> {
2449    let pack_bytes = fs::read(pack_path).map_err(Error::Io)?;
2450    if pack_bytes.len() < hash_bytes {
2451        return Err(Error::CorruptObject(
2452            "pack too small for idx trailer".into(),
2453        ));
2454    }
2455    let trailer = &pack_bytes[pack_bytes.len() - hash_bytes..];
2456    write_v2_pack_index_with_trailer(idx_path, entries, trailer, hash_bytes, None)
2457}
2458
2459const PARALLEL_INDEX_SORT_MIN: usize = 8_192;
2460
2461/// Sort `entries` by object id in place and return the v2 fanout table.
2462///
2463/// Entries are first partitioned in place by their leading byte (which also yields the
2464/// fanout), then contiguous runs of buckets are sorted on up to `sort_threads` workers.
2465/// No copy of the entries is made.
2466fn sort_index_entries(
2467    entries: &mut [(ObjectId, u64, u32)],
2468    sort_threads: Option<usize>,
2469) -> [u32; 256] {
2470    let lead = |entry: &(ObjectId, u64, u32)| usize::from(entry.0.as_bytes()[0]);
2471    let mut counts = [0usize; 256];
2472    for entry in entries.iter() {
2473        counts[lead(entry)] += 1;
2474    }
2475    let mut ends = [0usize; 256];
2476    let mut total = 0usize;
2477    for (end, count) in ends.iter_mut().zip(counts) {
2478        total += count;
2479        *end = total;
2480    }
2481    let mut next = [0usize; 256];
2482    next[1..].copy_from_slice(&ends[..255]);
2483    for bucket in 0..256 {
2484        while next[bucket] < ends[bucket] {
2485            let at = next[bucket];
2486            let home = lead(&entries[at]);
2487            if home == bucket {
2488                next[bucket] += 1;
2489            } else {
2490                entries.swap(at, next[home]);
2491                next[home] += 1;
2492            }
2493        }
2494    }
2495
2496    let by_oid =
2497        |a: &(ObjectId, u64, u32), b: &(ObjectId, u64, u32)| a.0.as_bytes().cmp(b.0.as_bytes());
2498    let threads = sort_threads.unwrap_or(1).max(1);
2499    if threads <= 1 || entries.len() < PARALLEL_INDEX_SORT_MIN {
2500        let mut start = 0;
2501        for end in ends {
2502            entries[start..end].sort_unstable_by(by_oid);
2503            start = end;
2504        }
2505    } else {
2506        let share = entries.len().div_ceil(threads);
2507        std::thread::scope(|scope| {
2508            let mut rest = &mut *entries;
2509            let mut taken = 0usize;
2510            for end in ends {
2511                if end - taken >= share || end == total {
2512                    let (run, tail) = std::mem::take(&mut rest).split_at_mut(end - taken);
2513                    rest = tail;
2514                    taken = end;
2515                    if run.len() > 1 {
2516                        scope.spawn(move || run.sort_unstable_by(by_oid));
2517                    }
2518                }
2519            }
2520        });
2521    }
2522    ends.map(|end| u32::try_from(end).unwrap_or(u32::MAX))
2523}
2524
2525/// Sort `entries` in place and write them as a version-2 pack index at `idx_path`.
2526///
2527/// Streams the file through its checksum instead of building it in memory, so the only
2528/// large allocation is the caller's `entries`.
2529///
2530/// # Errors
2531///
2532/// Returns [`Error::CorruptObject`] when an object id or the trailer does not have
2533/// `hash_bytes` bytes, or the large-offset table overflows, and [`Error::Io`] on write
2534/// failure.
2535pub(crate) fn write_v2_pack_index_sorting(
2536    idx_path: &Path,
2537    entries: &mut [(ObjectId, u64, u32)],
2538    pack_trailer: &[u8],
2539    hash_bytes: usize,
2540    sort_threads: Option<usize>,
2541) -> Result<()> {
2542    use std::io::Write;
2543
2544    let Some(algo) = HashAlgo::from_len(hash_bytes) else {
2545        return Err(Error::CorruptObject(format!(
2546            "unsupported index hash width {hash_bytes}"
2547        )));
2548    };
2549    if pack_trailer.len() != hash_bytes {
2550        return Err(Error::CorruptObject(format!(
2551            "pack trailer length {} does not match hash width {hash_bytes}",
2552            pack_trailer.len()
2553        )));
2554    }
2555    if let Some((oid, _, _)) = entries
2556        .iter()
2557        .find(|(oid, _, _)| oid.as_bytes().len() != hash_bytes)
2558    {
2559        return Err(Error::CorruptObject(format!(
2560            "OID width {} does not match index hash width {hash_bytes}",
2561            oid.as_bytes().len()
2562        )));
2563    }
2564    let fanout = sort_index_entries(entries, sort_threads);
2565
2566    let file = fs::File::create(idx_path).map_err(Error::Io)?;
2567    let mut out = std::io::BufWriter::with_capacity(256 * 1024, file);
2568    let mut hasher = algo.checksum_hasher();
2569    let mut put = |bytes: &[u8]| -> Result<()> {
2570        hasher.update(bytes);
2571        out.write_all(bytes).map_err(Error::Io)
2572    };
2573    put(b"\xfftOc")?;
2574    put(&2u32.to_be_bytes())?;
2575    for f in fanout {
2576        put(&f.to_be_bytes())?;
2577    }
2578    for (oid, _, _) in entries.iter() {
2579        put(oid.as_bytes())?;
2580    }
2581    for (_, _, crc) in entries.iter() {
2582        put(&crc.to_be_bytes())?;
2583    }
2584    let mut large = 0u32;
2585    for (_, off, _) in entries.iter() {
2586        match u32::try_from(*off) {
2587            Ok(small) if small < 0x8000_0000 => put(&small.to_be_bytes())?,
2588            _ => {
2589                if large >= 0x8000_0000 {
2590                    return Err(Error::CorruptObject(
2591                        "pack index large-offset table overflow".to_owned(),
2592                    ));
2593                }
2594                put(&(0x8000_0000u32 | large).to_be_bytes())?;
2595                large += 1;
2596            }
2597        }
2598    }
2599    for (_, off, _) in entries.iter() {
2600        if *off >= 0x8000_0000 {
2601            put(&off.to_be_bytes())?;
2602        }
2603    }
2604    put(pack_trailer)?;
2605    let digest = hasher.finalize();
2606    out.write_all(&digest).map_err(Error::Io)?;
2607    out.flush().map_err(Error::Io)?;
2608    Ok(())
2609}
2610
2611#[must_use]
2612/// `oid_bytes_to_hex` function.
2613pub fn oid_bytes_to_hex(oid: &[u8]) -> String {
2614    hex::encode(oid)
2615}
2616
2617/// Hash canonical loose object bytes (`kind SP size NUL data`) with the repo hash width.
2618pub fn hash_object_bytes(kind: ObjectKind, data: &[u8], hash_bytes: usize) -> Result<Vec<u8>> {
2619    let Some(algo) = HashAlgo::from_len(hash_bytes) else {
2620        return Err(Error::CorruptObject(format!(
2621            "unsupported object hash width: {hash_bytes}"
2622        )));
2623    };
2624    Ok(crate::hash::hash_object(algo, kind, data)?
2625        .as_bytes()
2626        .to_vec())
2627}
2628
2629/// Parse a pack index file (version 1 legacy or version 2), verifying the SHA-1
2630/// trailer checksum.
2631///
2632/// Used by `fsck`/`verify-pack` and similar code that wants on-disk validation. Hot
2633/// object-lookup paths should call [`read_pack_index_cached`] (which skips trailer
2634/// verification, matching Git's normal read path).
2635///
2636/// # Errors
2637///
2638/// Returns [`Error::CorruptObject`] when format checks fail and
2639/// [`Error::PackIndexChecksumMismatch`] when the `.idx` trailer is wrong.
2640pub fn read_pack_index(idx_path: &Path) -> Result<PackIndex> {
2641    crate::pack_index::open_pack_index_file(idx_path, true)
2642}
2643
2644/// Parse a pack index file without verifying the SHA-1 trailer checksum.
2645///
2646/// Git reads the `.idx` offset table without re-checking its trailer in the MIDX
2647/// write path (`midx-write.c`/`packfile.c` `open_pack_index`), so a deliberately
2648/// corrupted-but-structurally-valid idx (t5319 64-bit offset tests) still loads.
2649pub fn read_pack_index_no_verify(idx_path: &Path) -> Result<PackIndex> {
2650    crate::pack_index::open_pack_index_file(idx_path, false)
2651}
2652
2653/// A pack object type as encoded in the packed stream header.
2654#[derive(Debug, Clone, Copy, PartialEq, Eq)]
2655pub enum PackedType {
2656    /// Commit object.
2657    Commit,
2658    /// Tree object.
2659    Tree,
2660    /// Blob object.
2661    Blob,
2662    /// Tag object.
2663    Tag,
2664    /// Offset delta.
2665    OfsDelta,
2666    /// Reference delta.
2667    RefDelta,
2668}
2669
2670impl PackedType {
2671    /// Printable name used by `verify-pack -v` output.
2672    #[must_use]
2673    pub fn as_str(self) -> &'static str {
2674        match self {
2675            Self::Commit => "commit",
2676            Self::Tree => "tree",
2677            Self::Blob => "blob",
2678            Self::Tag => "tag",
2679            Self::OfsDelta => "ofs-delta",
2680            Self::RefDelta => "ref-delta",
2681        }
2682    }
2683}
2684
2685/// A decoded object header record used by `verify-pack`.
2686#[derive(Debug, Clone)]
2687pub struct VerifyObjectRecord {
2688    /// Object ID from the index (20 or 32 raw bytes).
2689    pub oid: Vec<u8>,
2690    /// Type from the pack stream header.
2691    pub packed_type: PackedType,
2692    /// Uncompressed object size from the pack header.
2693    pub size: u64,
2694    /// Total bytes in pack occupied by this object slot.
2695    pub size_in_pack: u64,
2696    /// Offset in pack file.
2697    pub offset: u64,
2698    /// Delta chain depth, if deltified.
2699    pub depth: Option<u64>,
2700    /// Base object for ref-delta objects.
2701    pub base_oid: Option<Vec<u8>>,
2702    /// In-pack base offset for ofs-delta objects.
2703    pub base_pack_offset: Option<u64>,
2704}
2705
2706/// How a delta object in a pack references its base, used to compute chain depth order-independently.
2707enum DeltaBaseLink {
2708    /// `REF_DELTA`: base identified by raw object id (20 or 32 bytes).
2709    Oid(Vec<u8>),
2710    /// `OFS_DELTA`: base identified by its absolute offset in the pack.
2711    Offset(u64),
2712}
2713
2714/// Resolve the delta-chain depth of record `i`, memoizing the result into `records[i].depth`.
2715///
2716/// Full (non-delta) objects have depth 0. A delta's depth is one greater than its base's depth.
2717/// Following base links by offset/oid makes this independent of the order objects appear in the
2718/// pack — a ref-delta's base may be stored *after* the delta itself. A base that is not present in
2719/// this pack (thin pack) or a cycle is treated as depth 0 for the missing/looping link.
2720///
2721/// # Errors
2722///
2723/// Returns [`Error::CorruptObject`] when a ref-delta record is missing its base oid.
2724fn resolve_delta_depth(
2725    i: usize,
2726    base_links: &[Option<DeltaBaseLink>],
2727    by_oid: &HashMap<Vec<u8>, usize>,
2728    by_offset_idx: &HashMap<u64, usize>,
2729    records: &mut [VerifyObjectRecord],
2730) -> Result<u64> {
2731    if let Some(d) = records[i].depth {
2732        return Ok(d);
2733    }
2734    let Some(link) = &base_links[i] else {
2735        return Ok(0);
2736    };
2737    let base_idx = match link {
2738        DeltaBaseLink::Oid(oid) => by_oid.get(oid).copied(),
2739        DeltaBaseLink::Offset(off) => by_offset_idx.get(off).copied(),
2740    };
2741    // Mark this record visited before recursing so a malformed cyclic chain cannot recurse forever.
2742    records[i].depth = Some(1);
2743    let depth = match base_idx {
2744        Some(b) if b != i => {
2745            resolve_delta_depth(b, base_links, by_oid, by_offset_idx, records)?.saturating_add(1)
2746        }
2747        // Base absent from this pack (thin) or self-referential: count this delta as depth 1.
2748        _ => 1,
2749    };
2750    records[i].depth = Some(depth);
2751    Ok(depth)
2752}
2753
2754/// Default in-pack delta chain depth when `pack.depth` is unset (matches Git).
2755pub const DEFAULT_PACK_DEPTH: usize = 50;
2756
2757/// Longest delta-chain depth among verify-pack records (number of edges from base to tip).
2758#[must_use]
2759pub fn max_verify_pack_delta_depth(records: &[VerifyObjectRecord]) -> u64 {
2760    records.iter().filter_map(|r| r.depth).max().unwrap_or(0)
2761}
2762
2763/// Targets whose delta edge must be dropped so a target→base graph has no cycle.
2764///
2765/// `base_of` returns the base of each object stored as a delta. Chains are walked from
2766/// `starts` only, so a cycle is found when one of its objects is a start; passing every
2767/// delta target finds them all. A cycle (`a` against `b`, `b` against `a`, or longer)
2768/// can never be resolved by a reader, yet appears when deltas are reused from several
2769/// packs that stored the same objects in opposite directions. For every cycle the
2770/// smallest object id is returned, so the choice does not depend on `starts` order;
2771/// reverting those targets to whole objects leaves every chain ending in a whole object.
2772pub(crate) fn delta_cycle_breaks(
2773    starts: impl IntoIterator<Item = ObjectId>,
2774    base_of: impl Fn(&ObjectId) -> Option<ObjectId>,
2775) -> Vec<ObjectId> {
2776    // `Some(i)`: at position `i` of the walk in progress; `None`: already settled.
2777    let mut seen: crate::objects::OidHashMap<Option<usize>> = crate::objects::oid_hash_map();
2778    let mut path: Vec<ObjectId> = Vec::new();
2779    let mut breaks = Vec::new();
2780    for start in starts {
2781        let mut cur = Some(start);
2782        while let Some(oid) = cur {
2783            match seen.get(&oid) {
2784                Some(None) => break,
2785                Some(Some(pos)) => {
2786                    breaks.extend(path[*pos..].iter().min().copied());
2787                    break;
2788                }
2789                None => {
2790                    seen.insert(oid, Some(path.len()));
2791                    path.push(oid);
2792                    cur = base_of(&oid);
2793                }
2794            }
2795        }
2796        for oid in path.drain(..) {
2797            seen.insert(oid, None);
2798        }
2799    }
2800    breaks
2801}
2802
2803/// Break delta cycles, then delta chains longer than `max_depth` edges, in a
2804/// target→base map.
2805///
2806/// Each entry maps a delta object to its chosen in-pack base. A cycle cannot be
2807/// resolved at any depth, so the smallest object id of each one loses its edge first.
2808/// Chains already within `max_depth` keep their original bases unchanged. Longer
2809/// chains are broken on Git's `(depth + 1)` modulo rule by removing only the delta
2810/// edges at cut points; links inside each resulting segment stay as originally chosen
2811/// (matching `break_delta_chains`).
2812pub fn apply_delta_depth_limit(map: &mut HashMap<ObjectId, ObjectId>, max_depth: usize) {
2813    for target in delta_cycle_breaks(map.keys().copied(), |oid| map.get(oid).copied()) {
2814        map.remove(&target);
2815    }
2816    cap_delta_chain_depth(map, max_depth);
2817}
2818
2819/// The chain-length half of [`apply_delta_depth_limit`], for a map already known to
2820/// be acyclic (a cycle is left as it is).
2821pub(crate) fn cap_delta_chain_depth(map: &mut HashMap<ObjectId, ObjectId>, max_depth: usize) {
2822    match delta_chain_cuts(
2823        map.keys().copied(),
2824        map.len(),
2825        |oid| map.get(oid).copied(),
2826        max_depth,
2827    ) {
2828        Some(cuts) => {
2829            for target in cuts {
2830                map.remove(&target);
2831            }
2832        }
2833        None => cap_delta_chain_depth_per_tip(map, max_depth),
2834    }
2835}
2836
2837/// The targets whose delta edge [`cap_delta_chain_depth`] removes from an acyclic
2838/// target→base map with `len` targets; `None` when the map has a cycle.
2839///
2840/// Every target lies on the chain of some tip at its distance from the chain's end,
2841/// whichever tip it is reached from, so the cut points are the targets at a positive
2842/// multiple of `max_depth + 1` edges from the end (such a chain is always longer than
2843/// `max_depth`).
2844pub(crate) fn delta_chain_cuts(
2845    targets: impl IntoIterator<Item = ObjectId>,
2846    len: usize,
2847    base_of: impl Fn(&ObjectId) -> Option<ObjectId>,
2848    max_depth: usize,
2849) -> Option<Vec<ObjectId>> {
2850    let modulus = max_depth.saturating_add(1);
2851    let ends = delta_chain_ends(targets, len, base_of)?;
2852    Some(
2853        ends.by_target
2854            .into_iter()
2855            .filter(|(_, (_, depth))| (*depth as usize).is_multiple_of(modulus))
2856            .map(|(target, _)| target)
2857            .collect(),
2858    )
2859}
2860
2861/// Where each target of a target→base map sits on its delta chain, from
2862/// [`delta_chain_ends`].
2863pub(crate) struct DeltaChainEnds {
2864    /// Per target: index into [`Self::ends`] and the number of delta edges to that end.
2865    by_target: crate::objects::OidHashMap<(u32, u32)>,
2866    /// Chain ends: objects that are bases but not themselves targets.
2867    ends: Vec<ObjectId>,
2868}
2869
2870impl DeltaChainEnds {
2871    /// The end of `target`'s chain and the number of delta edges to it.
2872    pub(crate) fn get(&self, target: &ObjectId) -> Option<(ObjectId, u32)> {
2873        let &(end, depth) = self.by_target.get(target)?;
2874        Some((self.ends[end as usize], depth))
2875    }
2876}
2877
2878/// For every target of a target→base map with `len` targets, the end of its chain (the
2879/// first object that is not itself a target) and the number of delta edges to it.
2880///
2881/// Each object is visited once, however many chains share it. `None` when the map has
2882/// a cycle (or more than `u32::MAX` chains).
2883pub(crate) fn delta_chain_ends(
2884    targets: impl IntoIterator<Item = ObjectId>,
2885    len: usize,
2886    base_of: impl Fn(&ObjectId) -> Option<ObjectId>,
2887) -> Option<DeltaChainEnds> {
2888    let mut by_target: crate::objects::OidHashMap<(u32, u32)> =
2889        crate::objects::oid_hash_map_with_capacity(len);
2890    let mut ends: Vec<ObjectId> = Vec::new();
2891    let mut path: Vec<ObjectId> = Vec::new();
2892    for start in targets {
2893        if by_target.contains_key(&start) {
2894            continue;
2895        }
2896        path.clear();
2897        let mut cur = start;
2898        let (end, mut depth) = loop {
2899            if let Some(&known) = by_target.get(&cur) {
2900                break known;
2901            }
2902            let Some(base) = base_of(&cur) else {
2903                let end = u32::try_from(ends.len()).ok()?;
2904                ends.push(cur);
2905                break (end, 0);
2906            };
2907            // An acyclic chain holds each target at most once.
2908            if path.len() >= len {
2909                return None;
2910            }
2911            path.push(cur);
2912            cur = base;
2913        };
2914        for &target in path.iter().rev() {
2915            depth += 1;
2916            by_target.insert(target, (end, depth));
2917        }
2918    }
2919    Some(DeltaChainEnds { by_target, ends })
2920}
2921
2922/// [`cap_delta_chain_depth`] by walking each tip's chain, which also defines what
2923/// happens on a chain that runs into a cycle.
2924fn cap_delta_chain_depth_per_tip(map: &mut HashMap<ObjectId, ObjectId>, max_depth: usize) {
2925    let keys: Vec<ObjectId> = map.keys().copied().collect();
2926    let value_set: HashSet<ObjectId> = map.values().copied().collect();
2927    let mut tips: Vec<ObjectId> = keys
2928        .into_iter()
2929        .filter(|k| !value_set.contains(k))
2930        .collect();
2931    tips.sort();
2932
2933    let modulus = max_depth.saturating_add(1);
2934    let mut snip: HashSet<ObjectId> = HashSet::new();
2935
2936    for tip in tips {
2937        let mut chain: Vec<ObjectId> = Vec::new();
2938        let mut cur = tip;
2939        let mut seen = HashSet::new();
2940        while seen.insert(cur) {
2941            chain.push(cur);
2942            let Some(&b) = map.get(&cur) else {
2943                break;
2944            };
2945            cur = b;
2946        }
2947
2948        let n = chain.len();
2949        if n < 2 || n - 1 <= max_depth {
2950            continue;
2951        }
2952
2953        let mut total_depth = (n - 1) as u32;
2954        for &oid in &chain {
2955            let assigned = (total_depth as usize) % modulus;
2956            total_depth = total_depth.saturating_sub(1);
2957            if assigned == 0 {
2958                snip.insert(oid);
2959            }
2960        }
2961    }
2962
2963    for oid in snip {
2964        map.remove(&oid);
2965    }
2966}
2967
2968/// Pack/index pair validated for fsck without inflating every object.
2969///
2970/// Structure, checksums, and per-entry headers are checked; object hashes are
2971/// verified separately when objects are read for semantic fsck.
2972#[derive(Debug)]
2973pub(crate) struct VerifiedPackForFsck {
2974    pub idx: PackIndex,
2975    /// Memory-mapped pack file (owned only for tiny packs or when mapping fails).
2976    pub pack_bytes: Arc<PackData>,
2977    pub records: Vec<VerifyObjectRecord>,
2978}
2979
2980/// Verify pack trailer, index checksum, counts, and object header layout.
2981///
2982/// Does not inflate objects or verify per-object hashes (see [`verify_pack_and_collect`]).
2983///
2984/// # Errors
2985///
2986/// Returns [`Error::CorruptObject`] when the index or pack are malformed.
2987pub(crate) fn verify_pack_for_fsck(idx_path: &Path) -> Result<VerifiedPackForFsck> {
2988    let (idx, pack_bytes, records) = verify_pack_structure_and_records(idx_path, true)?;
2989    Ok(VerifiedPackForFsck {
2990        idx,
2991        pack_bytes,
2992        records,
2993    })
2994}
2995
2996/// Pack-level trailer/index checksum problem detected while preparing object fsck.
2997#[derive(Debug, Clone, Copy, PartialEq, Eq)]
2998pub(crate) enum PackFsckTrailerIssue {
2999    /// Pack file trailing hash does not match payload.
3000    PackChecksum,
3001    /// Embedded pack hash in the `.idx` does not match the pack file.
3002    IdxMismatch,
3003}
3004
3005/// Load pack bytes and header records for per-object fsck even when the pack trailer is wrong.
3006///
3007/// Returns a trailer/index checksum issue separately so callers can still inflate objects.
3008pub(crate) fn prepare_pack_for_object_fsck(
3009    idx_path: &Path,
3010) -> Result<(VerifiedPackForFsck, Option<PackFsckTrailerIssue>)> {
3011    let (idx, pack_bytes, records) = verify_pack_structure_and_records(idx_path, false)?;
3012    let trailer = pack_trailer_fsck_issue(idx_path, &idx, &pack_bytes);
3013    Ok((
3014        VerifiedPackForFsck {
3015            idx,
3016            pack_bytes,
3017            records,
3018        },
3019        trailer,
3020    ))
3021}
3022
3023fn pack_trailer_fsck_issue(
3024    idx_path: &Path,
3025    idx: &PackIndex,
3026    pack_bytes: &[u8],
3027) -> Option<PackFsckTrailerIssue> {
3028    let hb = idx.hash_bytes();
3029    let algo = HashAlgo::from_len(hb)?;
3030    if verify_trailer(algo, pack_bytes).is_err() {
3031        return Some(PackFsckTrailerIssue::PackChecksum);
3032    }
3033    let idx_file_bytes = fs::read(idx_path).ok()?;
3034    if idx_file_bytes.len() >= 2 * hb {
3035        let pack_end = pack_bytes.len().checked_sub(hb)?;
3036        let n = idx_file_bytes.len();
3037        let embedded = &idx_file_bytes[n - 2 * hb..n - hb];
3038        if embedded != &pack_bytes[pack_end..] {
3039            return Some(PackFsckTrailerIssue::IdxMismatch);
3040        }
3041    }
3042    None
3043}
3044
3045/// Verify one pack/index pair and optionally return object records.
3046///
3047/// # Errors
3048///
3049/// Returns [`Error::CorruptObject`] when the index or pack are malformed.
3050pub fn verify_pack_and_collect(idx_path: &Path) -> Result<Vec<VerifyObjectRecord>> {
3051    let VerifiedPackForFsck {
3052        idx,
3053        pack_bytes,
3054        records,
3055    } = verify_pack_for_fsck(idx_path)?;
3056    let hb = idx.hash_bytes();
3057    for entry in idx.iter() {
3058        let obj = read_object_from_pack_bytes(&pack_bytes, &idx, entry.oid())?;
3059        let computed = hash_object_bytes(obj.kind, &obj.data, hb)?;
3060        if computed.as_slice() != entry.oid() {
3061            return Err(Error::CorruptObject(format!(
3062                "pack object hash mismatch at offset {} (index says {})",
3063                entry.offset()?,
3064                oid_bytes_to_hex(entry.oid())
3065            )));
3066        }
3067    }
3068    Ok(records)
3069}
3070
3071fn verify_pack_structure_and_records(
3072    idx_path: &Path,
3073    verify_pack_trailer: bool,
3074) -> Result<(PackIndex, Arc<PackData>, Vec<VerifyObjectRecord>)> {
3075    let idx = read_pack_index(idx_path)?;
3076    let idx_file_bytes = fs::read(idx_path).map_err(Error::Io)?;
3077    let pack_bytes = PackData::open_with_min_len(&idx.pack_path, 0)?;
3078    let hb = idx.hash_bytes();
3079    if pack_bytes.len() < 12 + hb {
3080        return Err(Error::CorruptObject(format!(
3081            "pack file {} is too small",
3082            idx.pack_path.display()
3083        )));
3084    }
3085    let pack_end = pack_bytes.len() - hb;
3086    let Some(algo) = HashAlgo::from_len(hb) else {
3087        return Err(Error::CorruptObject(format!(
3088            "unsupported OID width {hb} for pack {}",
3089            idx.pack_path.display()
3090        )));
3091    };
3092    if verify_pack_trailer {
3093        verify_trailer(algo, &pack_bytes).map_err(|e| {
3094            Error::CorruptObject(format!(
3095                "pack trailing checksum mismatch for {}: {e}",
3096                idx.pack_path.display()
3097            ))
3098        })?;
3099        // The `.idx` ends with the pack checksum followed by its own checksum, both
3100        // at the repository hash width `hb` (20 for SHA-1, 32 for SHA-256).
3101        if idx_file_bytes.len() >= 2 * hb {
3102            let n = idx_file_bytes.len();
3103            let embedded = &idx_file_bytes[n - 2 * hb..n - hb];
3104            if embedded != &pack_bytes[pack_end..] {
3105                return Err(Error::CorruptObject(format!(
3106                    "pack checksum in index does not match {}",
3107                    idx.pack_path.display()
3108                )));
3109            }
3110        }
3111    }
3112    if &pack_bytes[0..4] != b"PACK" {
3113        return Err(Error::CorruptObject(format!(
3114            "pack file {} has invalid signature",
3115            idx.pack_path.display()
3116        )));
3117    }
3118    let version = u32::from_be_bytes(pack_bytes[4..8].try_into().unwrap_or([0, 0, 0, 0]));
3119    if version != 2 && version != 3 {
3120        return Err(Error::CorruptObject(format!(
3121            "unsupported pack version {} in {}",
3122            version,
3123            idx.pack_path.display()
3124        )));
3125    }
3126    let count = u32::from_be_bytes(pack_bytes[8..12].try_into().unwrap_or([0, 0, 0, 0])) as usize;
3127    if count != idx.len() {
3128        return Err(Error::CorruptObject(format!(
3129            "pack/index object count mismatch for {}",
3130            idx.pack_path.display()
3131        )));
3132    }
3133
3134    let mut by_offset: BTreeMap<u64, Vec<u8>> = BTreeMap::new();
3135    for entry in idx.iter() {
3136        by_offset.insert(entry.offset()?, entry.oid().to_vec());
3137    }
3138    let offsets: Vec<u64> = by_offset.keys().copied().collect();
3139    if offsets.is_empty() {
3140        return Ok((idx, pack_bytes, Vec::new()));
3141    }
3142
3143    let mut by_oid: HashMap<Vec<u8>, usize> = HashMap::new();
3144    let mut by_offset_idx: HashMap<u64, usize> = HashMap::new();
3145    let mut records: Vec<VerifyObjectRecord> = Vec::with_capacity(offsets.len());
3146    // Per-record base pointer for delta objects, captured while scanning headers and resolved into
3147    // chain depths afterwards. Delta bases can appear *after* the delta in pack order (ref-deltas in
3148    // particular), so depth must be computed by following these pointers, not in scan order.
3149    let mut base_links: Vec<Option<DeltaBaseLink>> = Vec::with_capacity(offsets.len());
3150    for (i, offset) in offsets.iter().copied().enumerate() {
3151        let oid = by_offset.get(&offset).cloned().ok_or_else(|| {
3152            Error::CorruptObject(format!("missing object id for offset {}", offset))
3153        })?;
3154        let next_off = offsets
3155            .get(i + 1)
3156            .copied()
3157            .unwrap_or((pack_bytes.len() - hb) as u64);
3158        if next_off <= offset || next_off > (pack_bytes.len() - hb) as u64 {
3159            return Err(Error::CorruptObject(format!(
3160                "invalid object boundaries at offset {} in {}",
3161                offset,
3162                idx.pack_path.display()
3163            )));
3164        }
3165        let mut p = offset as usize;
3166        let (packed_type, size) = parse_pack_object_header(&pack_bytes, &mut p)?;
3167        let mut base_oid: Option<Vec<u8>> = None;
3168        let mut base_link: Option<DeltaBaseLink> = None;
3169
3170        match packed_type {
3171            PackedType::RefDelta => {
3172                if p + hb > pack_bytes.len() {
3173                    return Err(Error::CorruptObject(format!(
3174                        "truncated ref-delta base at offset {}",
3175                        offset
3176                    )));
3177                }
3178                let raw = pack_bytes[p..p + hb].to_vec();
3179                base_oid = Some(raw.clone());
3180                base_link = Some(DeltaBaseLink::Oid(raw));
3181            }
3182            PackedType::OfsDelta => {
3183                let base_offset = parse_ofs_delta_base(&pack_bytes, &mut p, offset)?;
3184                base_link = Some(DeltaBaseLink::Offset(base_offset));
3185            }
3186            PackedType::Commit | PackedType::Tree | PackedType::Blob | PackedType::Tag => {}
3187        }
3188
3189        let size_in_pack = next_off - offset;
3190        let base_pack_offset = match &base_link {
3191            Some(DeltaBaseLink::Offset(off)) => Some(*off),
3192            _ => None,
3193        };
3194        records.push(VerifyObjectRecord {
3195            oid: oid.clone(),
3196            packed_type,
3197            size,
3198            size_in_pack,
3199            offset,
3200            depth: None,
3201            base_oid,
3202            base_pack_offset,
3203        });
3204        base_links.push(base_link);
3205        by_oid.insert(oid, i);
3206        by_offset_idx.insert(offset, i);
3207    }
3208
3209    // Resolve delta chain depths by following base links to their record index, regardless of the
3210    // order objects appear in the pack. A delta's depth is one more than its base's depth; full
3211    // objects have depth 0 (represented as `None` in the record). Memoize to keep this O(n).
3212    for i in 0..records.len() {
3213        if base_links[i].is_some() {
3214            let _ = resolve_delta_depth(i, &base_links, &by_oid, &by_offset_idx, &mut records)?;
3215        }
3216    }
3217
3218    Ok((idx, pack_bytes, records))
3219}
3220
3221/// Read alternates recursively, deduplicated in discovery order.
3222///
3223/// # Errors
3224///
3225/// Returns [`Error::Io`] when alternate files cannot be read.
3226pub fn read_alternates_recursive(objects_dir: &Path) -> Result<Vec<PathBuf>> {
3227    let mut visited = HashSet::new();
3228    let mut out = Vec::new();
3229    read_alternates_inner(objects_dir, &mut visited, &mut out, 0)?;
3230    Ok(out)
3231}
3232
3233/// Maximum alternate chain depth (git uses 5).
3234const MAX_ALTERNATE_DEPTH: usize = 5;
3235
3236/// Parse one non-empty line from `objects/info/alternates`.
3237///
3238/// Blank lines and `#` comments are ignored. Paths may be absolute, relative to the
3239/// current `objects/` directory, or C-style quoted (Git `read_info/alternates`).
3240fn parse_alternates_file_line(raw: &str) -> Option<PathBuf> {
3241    let line = raw.trim();
3242    if line.is_empty() || line.starts_with('#') {
3243        return None;
3244    }
3245    if line.starts_with('"') {
3246        let path = unquote_alternates_c_style(line)?;
3247        if path.is_empty() {
3248            return None;
3249        }
3250        return Some(PathBuf::from(path));
3251    }
3252    Some(PathBuf::from(line))
3253}
3254
3255/// Unquote a double-quoted alternates path (Git `unquote_c_style` subset).
3256fn unquote_alternates_c_style(line: &str) -> Option<String> {
3257    let bytes = line.as_bytes();
3258    if bytes.first() != Some(&b'"') {
3259        return None;
3260    }
3261    let mut out = Vec::new();
3262    let mut i = 1usize;
3263    while i < bytes.len() {
3264        if bytes[i] == b'"' {
3265            return String::from_utf8(out).ok();
3266        }
3267        if bytes[i] == b'\\' {
3268            i += 1;
3269            if i >= bytes.len() {
3270                return None;
3271            }
3272            match bytes[i] {
3273                b'\\' => out.push(b'\\'),
3274                b'"' => out.push(b'"'),
3275                b'a' => out.push(7),
3276                b'b' => out.push(8),
3277                b'f' => out.push(12),
3278                b'n' => out.push(b'\n'),
3279                b'r' => out.push(b'\r'),
3280                b't' => out.push(b'\t'),
3281                b'v' => out.push(11),
3282                c if c.is_ascii_digit() => {
3283                    if i + 2 >= bytes.len() {
3284                        return None;
3285                    }
3286                    let oct = std::str::from_utf8(&bytes[i..i + 3]).ok()?;
3287                    out.push(u8::from_str_radix(oct, 8).ok()?);
3288                    i += 2;
3289                }
3290                other => out.push(other),
3291            }
3292            i += 1;
3293            continue;
3294        }
3295        out.push(bytes[i]);
3296        i += 1;
3297    }
3298    None
3299}
3300
3301fn read_alternates_inner(
3302    objects_dir: &Path,
3303    visited: &mut HashSet<PathBuf>,
3304    out: &mut Vec<PathBuf>,
3305    depth: usize,
3306) -> Result<()> {
3307    if depth > MAX_ALTERNATE_DEPTH {
3308        return Ok(());
3309    }
3310    // Read the alternates file before canonicalizing: the canonical form is only needed to
3311    // resolve relative entries and deduplicate, and canonicalize walks every path component
3312    // (a readlink per ancestor) — pure waste in the overwhelmingly common no-alternates case.
3313    let alt_file = objects_dir.join("info").join("alternates");
3314    let text = match fs::read_to_string(&alt_file) {
3315        Ok(text) => text,
3316        Err(err) if err.kind() == io::ErrorKind::NotFound => return Ok(()),
3317        Err(err) => return Err(Error::Io(err)),
3318    };
3319    let canonical = canonical_or_self(objects_dir);
3320
3321    for raw in text.lines() {
3322        let Some(path) = parse_alternates_file_line(raw) else {
3323            continue;
3324        };
3325        let candidate = if path.is_absolute() {
3326            path
3327        } else {
3328            canonical.join(path)
3329        };
3330        let candidate = canonical_or_self(&candidate);
3331        if visited.insert(candidate.clone()) {
3332            out.push(candidate.clone());
3333            read_alternates_inner(&candidate, visited, out, depth + 1)?;
3334        }
3335    }
3336    Ok(())
3337}
3338
3339fn canonical_or_self(path: &Path) -> PathBuf {
3340    path.canonicalize_simplified()
3341        .unwrap_or_else(|_| path.to_path_buf())
3342}
3343
3344/// Convert a [`PackedType`] to an [`ObjectKind`] for non-delta types.
3345fn packed_type_to_kind(pt: PackedType) -> Result<ObjectKind> {
3346    match pt {
3347        PackedType::Commit => Ok(ObjectKind::Commit),
3348        PackedType::Tree => Ok(ObjectKind::Tree),
3349        PackedType::Blob => Ok(ObjectKind::Blob),
3350        PackedType::Tag => Ok(ObjectKind::Tag),
3351        PackedType::OfsDelta | PackedType::RefDelta => Err(Error::CorruptObject(
3352            "cannot convert delta type to object kind directly".to_owned(),
3353        )),
3354    }
3355}
3356
3357// hygiene: per-thread pack zlib scratch and read-info cache (ROADMAP item 7 — pack read hot path)
3358thread_local! {
3359    static DELTA_SIZE_ZLIB: std::cell::RefCell<crate::zlib_inflate::ZlibInflateScratch> =
3360        std::cell::RefCell::new(crate::zlib_inflate::ZlibInflateScratch::default());
3361}
3362// hygiene: scoped batch read_info cache; cleared on pack invalidation and context exit
3363thread_local! {
3364    static PACK_READ_INFO_CACHE: std::cell::RefCell<HashMap<PackReadInfoCacheKey, ObjectInfo>> =
3365        std::cell::RefCell::new(HashMap::new());
3366}
3367
3368const PACK_READ_INFO_CACHE_CAP: usize = 600_000;
3369
3370#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
3371struct PackReadInfoCacheKey {
3372    pack_id: u32,
3373    offset: u64,
3374}
3375
3376fn pack_read_info_cache_key(pack_path: &Path, offset: u64) -> Option<PackReadInfoCacheKey> {
3377    if !crate::pack_store::PackStore::has_context() {
3378        return None;
3379    }
3380    Some(PackReadInfoCacheKey {
3381        pack_id: pack_cache::pack_id_for(pack_path),
3382        offset,
3383    })
3384}
3385
3386/// Drop cached batch `read_info` entries (pack context end, invalidation, repack).
3387pub(crate) fn clear_pack_read_info_cache() {
3388    PACK_READ_INFO_CACHE.with(|c| c.borrow_mut().clear());
3389}
3390
3391fn drop_pack_read_info_cache_for(pack_path: &Path) {
3392    let pack_id = pack_cache::pack_id_for(pack_path);
3393    PACK_READ_INFO_CACHE.with(|c| {
3394        c.borrow_mut().retain(|k, _| k.pack_id != pack_id);
3395    });
3396}
3397
3398fn cached_pack_read_info(pack_path: &Path, offset: u64) -> Option<ObjectInfo> {
3399    let key = pack_read_info_cache_key(pack_path, offset)?;
3400    PACK_READ_INFO_CACHE.with(|c| c.borrow().get(&key).cloned())
3401}
3402
3403fn remember_pack_read_info(pack_path: &Path, offset: u64, info: ObjectInfo) {
3404    let Some(key) = pack_read_info_cache_key(pack_path, offset) else {
3405        return;
3406    };
3407    PACK_READ_INFO_CACHE.with(|c| {
3408        let mut g = c.borrow_mut();
3409        if g.len() >= PACK_READ_INFO_CACHE_CAP {
3410            g.clear();
3411        }
3412        g.insert(key, info);
3413    });
3414}
3415/// Read the delta result-size varint from a pack zlib stream without inflating the full delta.
3416///
3417/// Advances `*pos` past the compressed bytes consumed from `bytes`.
3418pub(crate) fn read_delta_result_size_from_pack_zlib(bytes: &[u8], pos: &mut usize) -> Result<u64> {
3419    DELTA_SIZE_ZLIB.with(|slot| slot.borrow_mut().read_pack_delta_result_size(bytes, pos))
3420}
3421
3422fn decompress_pack_data_into(
3423    bytes: &[u8],
3424    pos: &mut usize,
3425    expected_size: u64,
3426    scratch: &mut ZlibInflateScratch,
3427    out: &mut Vec<u8>,
3428) -> Result<()> {
3429    scratch.decompress_fixed_into(bytes, pos, expected_size, out)
3430}
3431
3432/// Read and fully resolve one object from a pack file given its offset.
3433///
3434/// Handles OFS_DELTA and REF_DELTA by recursively reading the base object.
3435/// The `idx` is used for REF_DELTA resolution (to find a base by OID).
3436/// Deepest delta chain a read will follow before treating the pack as corrupt.
3437///
3438/// Safety valve against cyclic ref-delta chains and unbounded recursion, not a
3439/// format limit — Git reads chains of any depth (`pack.depth` caps only writers).
3440const MAX_DELTA_CHAIN_READ_DEPTH: usize = 4096;
3441
3442/// One hop in an in-flight delta chain (collected tip→base, applied base→tip).
3443#[derive(Clone)]
3444struct PendingDeltaFrame {
3445    pack_id: u32,
3446    offset: u64,
3447    delta: Vec<u8>,
3448}
3449
3450/// Tracks delta-chain depth and the current `(pack_id, offset)` path for cycle detection.
3451///
3452/// Also holds reusable scratch buffers for pack reads on this thread (cleared per resolution).
3453#[derive(Default)]
3454struct DeltaChainState {
3455    depth: usize,
3456    visited: HashSet<(u32, u64)>,
3457    pending: Vec<PendingDeltaFrame>,
3458    inflate: ZlibInflateScratch,
3459    whole_object: Vec<u8>,
3460    delta_decompress: Vec<u8>,
3461    chain_a: Vec<u8>,
3462    chain_b: Vec<u8>,
3463}
3464
3465// hygiene: pack read delta-chain state (ROADMAP item 7 — per-thread pack cache)
3466thread_local! {
3467    static PACK_READ_DELTA_STATE: std::cell::RefCell<DeltaChainState> =
3468        std::cell::RefCell::new(DeltaChainState::default());
3469}
3470
3471fn with_pack_read_delta_state<R>(
3472    start_depth: usize,
3473    f: impl FnOnce(&mut DeltaChainState) -> R,
3474) -> R {
3475    PACK_READ_DELTA_STATE.with(|slot| {
3476        let mut state = slot.borrow_mut();
3477        state.depth = start_depth;
3478        state.visited.clear();
3479        f(&mut state)
3480    })
3481}
3482
3483impl DeltaChainState {
3484    fn visit(&mut self, pack_id: u32, offset: u64) -> Result<()> {
3485        if self.depth > MAX_DELTA_CHAIN_READ_DEPTH {
3486            return Err(Error::DeltaChainTooDeep {
3487                limit: MAX_DELTA_CHAIN_READ_DEPTH,
3488            });
3489        }
3490        if !self.visited.insert((pack_id, offset)) {
3491            return Err(Error::DeltaChainTooDeep {
3492                limit: MAX_DELTA_CHAIN_READ_DEPTH,
3493            });
3494        }
3495        Ok(())
3496    }
3497
3498    fn next_hop(&mut self) -> Result<()> {
3499        self.depth += 1;
3500        if self.depth > MAX_DELTA_CHAIN_READ_DEPTH {
3501            return Err(Error::DeltaChainTooDeep {
3502                limit: MAX_DELTA_CHAIN_READ_DEPTH,
3503            });
3504        }
3505        Ok(())
3506    }
3507}
3508
3509/// Copy `payload` into `out`, reusing `out`'s capacity when sufficient.
3510fn copy_payload_preserve_capacity(out: &mut Vec<u8>, payload: &[u8]) {
3511    out.clear();
3512    out.extend_from_slice(payload);
3513}
3514
3515/// Like [`apply_delta_chain_forward`], writing the final payload into `out` without replacing
3516/// `out`'s allocation when the result fits in the existing capacity.
3517#[allow(clippy::too_many_arguments)]
3518fn apply_delta_chain_forward_into(
3519    out: &mut Vec<u8>,
3520    kind: ObjectKind,
3521    base_data: &[u8],
3522    whole_base: Option<(u32, u64)>,
3523    pending: &[PendingDeltaFrame],
3524    result_pack_id: u32,
3525    result_offset: u64,
3526    populate_delta_lru: bool,
3527    state: &mut DeltaChainState,
3528) -> Result<()> {
3529    if pending.is_empty() {
3530        copy_payload_preserve_capacity(out, base_data);
3531        return Ok(());
3532    }
3533
3534    let caching = populate_delta_lru && pack_cache::delta_base_caching_enabled();
3535    let mut cache_inserts: Vec<(u32, u64, Arc<[u8]>)> =
3536        Vec::with_capacity(if caching { pending.len() + 1 } else { 0 });
3537    if caching {
3538        if let Some((pid, off)) = whole_base {
3539            cache_inserts.push((pid, off, Arc::from(base_data.to_vec())));
3540        }
3541    }
3542
3543    let chain_a = &mut state.chain_a;
3544    let chain_b = &mut state.chain_b;
3545    let mut rev: Vec<&PendingDeltaFrame> = pending.iter().rev().collect();
3546    let last = rev
3547        .pop()
3548        .ok_or_else(|| Error::CorruptObject("empty delta chain".to_owned()))?;
3549    if rev.is_empty() {
3550        apply_delta_into(out, base_data, &last.delta)?;
3551        if caching && (last.pack_id != result_pack_id || last.offset != result_offset) {
3552            cache_inserts.push((last.pack_id, last.offset, Arc::from(out.as_slice())));
3553        }
3554        if caching {
3555            pack_cache::put_delta_bases(kind, &cache_inserts);
3556        }
3557        return Ok(());
3558    }
3559
3560    let Some(first) = rev.first().copied() else {
3561        return Err(Error::CorruptObject(
3562            "delta chain missing base-adjacent frame".to_owned(),
3563        ));
3564    };
3565    apply_delta_into(chain_a, base_data, &first.delta)?;
3566    if caching && (first.pack_id != result_pack_id || first.offset != result_offset) {
3567        cache_inserts.push((first.pack_id, first.offset, Arc::from(chain_a.as_slice())));
3568    }
3569    let mut src_is_a = true;
3570    for frame in rev.iter().skip(1) {
3571        if src_is_a {
3572            apply_delta_into(chain_b, chain_a.as_slice(), &frame.delta)?;
3573            if caching && (frame.pack_id != result_pack_id || frame.offset != result_offset) {
3574                cache_inserts.push((frame.pack_id, frame.offset, Arc::from(chain_b.as_slice())));
3575            }
3576        } else {
3577            apply_delta_into(chain_a, chain_b.as_slice(), &frame.delta)?;
3578            if caching && (frame.pack_id != result_pack_id || frame.offset != result_offset) {
3579                cache_inserts.push((frame.pack_id, frame.offset, Arc::from(chain_a.as_slice())));
3580            }
3581        }
3582        src_is_a = !src_is_a;
3583    }
3584    let penultimate_base = if src_is_a {
3585        chain_a.as_slice()
3586    } else {
3587        chain_b.as_slice()
3588    };
3589    apply_delta_into(out, penultimate_base, &last.delta)?;
3590    if caching && (last.pack_id != result_pack_id || last.offset != result_offset) {
3591        cache_inserts.push((last.pack_id, last.offset, Arc::from(out.as_slice())));
3592    }
3593    if caching {
3594        pack_cache::put_delta_bases(kind, &cache_inserts);
3595    }
3596    Ok(())
3597}
3598
3599fn commit_prefix_has_author(data: &[u8]) -> bool {
3600    let mut pos = 0usize;
3601    while pos < data.len() {
3602        let line_start = pos;
3603        let mut line_end = pos;
3604        while line_end < data.len() && data[line_end] != b'\n' {
3605            line_end += 1;
3606        }
3607        let line = &data[line_start..line_end];
3608        let after_nl = line_end.saturating_add(1);
3609        if line.is_empty() {
3610            return false;
3611        }
3612        if line.first() == Some(&b' ') {
3613            return false;
3614        }
3615        let Some(key_end) = line.iter().position(|&b| b == b' ') else {
3616            return false;
3617        };
3618        if &line[..key_end] == b"author" {
3619            return true;
3620        }
3621        pos = after_nl;
3622    }
3623    false
3624}
3625
3626#[allow(clippy::too_many_arguments)]
3627fn apply_delta_chain_forward_prefix_into(
3628    out: &mut Vec<u8>,
3629    kind: ObjectKind,
3630    base_data: &[u8],
3631    whole_base: Option<(u32, u64)>,
3632    pending: &[PendingDeltaFrame],
3633    result_pack_id: u32,
3634    result_offset: u64,
3635    populate_delta_lru: bool,
3636    state: &mut DeltaChainState,
3637    max_out: usize,
3638) -> Result<()> {
3639    if pending.is_empty() {
3640        out.clear();
3641        let take = base_data.len().min(max_out);
3642        out.extend_from_slice(&base_data[..take]);
3643        return Ok(());
3644    }
3645
3646    let caching = populate_delta_lru && pack_cache::delta_base_caching_enabled();
3647    let mut cache_inserts: Vec<(u32, u64, Arc<[u8]>)> =
3648        Vec::with_capacity(if caching { pending.len() + 1 } else { 0 });
3649    if caching {
3650        if let Some((pid, off)) = whole_base {
3651            cache_inserts.push((pid, off, Arc::from(base_data.to_vec())));
3652        }
3653    }
3654
3655    let chain_a = &mut state.chain_a;
3656    let chain_b = &mut state.chain_b;
3657    let mut rev: Vec<&PendingDeltaFrame> = pending.iter().rev().collect();
3658    let last = rev
3659        .pop()
3660        .ok_or_else(|| Error::CorruptObject("empty delta chain".to_owned()))?;
3661    let prefix_ok = |buf: &[u8]| commit_prefix_has_author(buf);
3662    if rev.is_empty() {
3663        apply_delta_prefix_into(out, base_data, &last.delta, max_out, prefix_ok)?;
3664        if caching && (last.pack_id != result_pack_id || last.offset != result_offset) {
3665            cache_inserts.push((last.pack_id, last.offset, Arc::from(out.as_slice())));
3666        }
3667        if caching {
3668            pack_cache::put_delta_bases(kind, &cache_inserts);
3669        }
3670        return Ok(());
3671    }
3672
3673    let Some(first) = rev.first().copied() else {
3674        return Err(Error::CorruptObject(
3675            "delta chain missing base-adjacent frame".to_owned(),
3676        ));
3677    };
3678    apply_delta_into(chain_a, base_data, &first.delta)?;
3679    if caching && (first.pack_id != result_pack_id || first.offset != result_offset) {
3680        cache_inserts.push((first.pack_id, first.offset, Arc::from(chain_a.as_slice())));
3681    }
3682    let mut src_is_a = true;
3683    for frame in rev.iter().skip(1) {
3684        if src_is_a {
3685            apply_delta_into(chain_b, chain_a.as_slice(), &frame.delta)?;
3686            if caching && (frame.pack_id != result_pack_id || frame.offset != result_offset) {
3687                cache_inserts.push((frame.pack_id, frame.offset, Arc::from(chain_b.as_slice())));
3688            }
3689        } else {
3690            apply_delta_into(chain_a, chain_b.as_slice(), &frame.delta)?;
3691            if caching && (frame.pack_id != result_pack_id || frame.offset != result_offset) {
3692                cache_inserts.push((frame.pack_id, frame.offset, Arc::from(chain_a.as_slice())));
3693            }
3694        }
3695        src_is_a = !src_is_a;
3696    }
3697    let penultimate_base = if src_is_a {
3698        chain_a.as_slice()
3699    } else {
3700        chain_b.as_slice()
3701    };
3702    apply_delta_prefix_into(out, penultimate_base, &last.delta, max_out, prefix_ok)?;
3703    if caching && (last.pack_id != result_pack_id || last.offset != result_offset) {
3704        cache_inserts.push((last.pack_id, last.offset, Arc::from(out.as_slice())));
3705    }
3706    if caching {
3707        pack_cache::put_delta_bases(kind, &cache_inserts);
3708    }
3709    Ok(())
3710}
3711
3712#[allow(clippy::too_many_arguments)]
3713fn finish_pack_read_prefix_with_pending(
3714    out: &mut Vec<u8>,
3715    kind: ObjectKind,
3716    base: &[u8],
3717    whole_base: Option<(u32, u64)>,
3718    result_pack_id: u32,
3719    result_offset: u64,
3720    populate_delta_lru: bool,
3721    state: &mut DeltaChainState,
3722) -> Result<ObjectKind> {
3723    let pending = std::mem::take(&mut state.pending);
3724    apply_delta_chain_forward_prefix_into(
3725        out,
3726        kind,
3727        base,
3728        whole_base,
3729        &pending,
3730        result_pack_id,
3731        result_offset,
3732        populate_delta_lru,
3733        state,
3734        COMMIT_HEADER_READ_PREFIX,
3735    )?;
3736    Ok(kind)
3737}
3738
3739#[allow(clippy::too_many_arguments)]
3740fn finish_pack_read_with_pending(
3741    out: &mut Vec<u8>,
3742    kind: ObjectKind,
3743    base: &[u8],
3744    whole_base: Option<(u32, u64)>,
3745    result_pack_id: u32,
3746    result_offset: u64,
3747    populate_delta_lru: bool,
3748    state: &mut DeltaChainState,
3749) -> Result<ObjectKind> {
3750    let pending = std::mem::take(&mut state.pending);
3751    apply_delta_chain_forward_into(
3752        out,
3753        kind,
3754        base,
3755        whole_base,
3756        &pending,
3757        result_pack_id,
3758        result_offset,
3759        populate_delta_lru,
3760        state,
3761    )?;
3762    Ok(kind)
3763}
3764
3765/// Starting index for a pack read: borrow the caller's [`PackIndex`], or hold a cached
3766/// [`Arc`] after hopping to another pack (no deep clone of `entries`).
3767enum PackIndexHandle<'a> {
3768    Borrowed(&'a PackIndex),
3769    Shared(Arc<PackIndex>),
3770}
3771
3772impl<'a> Clone for PackIndexHandle<'a> {
3773    fn clone(&self) -> Self {
3774        match self {
3775            Self::Borrowed(idx) => Self::Borrowed(idx),
3776            Self::Shared(arc) => Self::Shared(Arc::clone(arc)),
3777        }
3778    }
3779}
3780
3781impl PackIndexHandle<'_> {
3782    fn get(&self) -> &PackIndex {
3783        match self {
3784            Self::Borrowed(idx) => idx,
3785            Self::Shared(idx) => idx.as_ref(),
3786        }
3787    }
3788}
3789
3790fn find_other_pack_index(
3791    objects_dir: &Path,
3792    current: &PackIndex,
3793    oid: &ObjectId,
3794) -> Result<Option<Arc<PackIndex>>> {
3795    for idx in read_local_pack_indexes_cached(objects_dir)? {
3796        if idx.idx_path == current.idx_path {
3797            continue;
3798        }
3799        if idx.contains(oid)? {
3800            return Ok(Some(Arc::clone(&idx)));
3801        }
3802    }
3803    Ok(None)
3804}
3805
3806/// When an indexed in-pack base is unreadable, try a verified loose copy or the same OID in
3807/// another pack (cold rescue path preserved from the recursive reader).
3808fn rescue_in_pack_base(
3809    idx: &PackIndex,
3810    base_offset: u64,
3811    objects_dir: Option<&Path>,
3812    state: &mut DeltaChainState,
3813) -> Result<Option<(ObjectKind, Vec<u8>)>> {
3814    let Some(pos) = idx.find_position_by_offset_sorted(base_offset)? else {
3815        return Ok(None);
3816    };
3817    let entry_oid = idx.oid_at(pos);
3818    if entry_oid.len() != idx.hash_bytes() {
3819        return Ok(None);
3820    }
3821    let base_oid = ObjectId::from_bytes(entry_oid)?;
3822    let Some(dir) = objects_dir else {
3823        return Ok(None);
3824    };
3825    let loose = dir
3826        .join(base_oid.loose_prefix())
3827        .join(base_oid.loose_suffix());
3828    if loose.is_file() {
3829        if let Ok(obj) = crate::odb::Odb::read_loose_verify_oid(&loose, &base_oid) {
3830            return Ok(Some((obj.kind, obj.data)));
3831        }
3832    }
3833    if let Some(other_idx) = find_other_pack_index(dir, idx, &base_oid)? {
3834        let Some(off) = other_idx.find_offset(&base_oid)? else {
3835            return Ok(None);
3836        };
3837        // The nested resolution starts its own chain and clears `state.pending`; keep the
3838        // caller's collected deltas aside so they are applied on top of the rescued base.
3839        let caller_pending = std::mem::take(&mut state.pending);
3840        let rescued =
3841            resolve_pack_object_at(PackIndexHandle::Shared(other_idx), off, Some(dir), state);
3842        state.pending = caller_pending;
3843        if let Ok((kind, data)) = rescued {
3844            return Ok(Some((kind, data)));
3845        }
3846    }
3847    Ok(None)
3848}
3849
3850/// True when a pack parse failure might be stale cached bytes rather than on-disk corruption.
3851fn pack_bytes_parse_may_be_stale(err: &Error) -> bool {
3852    matches!(err, Error::CorruptObject(_) | Error::Zlib(_))
3853}
3854
3855/// Resolve one pack object, walking delta chains iteratively across packs with shared
3856/// [`DeltaChainState`] so cycles and depth limits surface as [`Error::DeltaChainTooDeep`].
3857///
3858/// On corruption/zlib errors, revalidates the starting pack's on-disk signature once and
3859/// retries the full resolution before surfacing the error.
3860fn resolve_pack_object_at(
3861    start_idx: PackIndexHandle<'_>,
3862    start_offset: u64,
3863    objects_dir: Option<&Path>,
3864    state: &mut DeltaChainState,
3865) -> Result<(ObjectKind, Vec<u8>)> {
3866    resolve_pack_object_at_with_bytes(start_idx, start_offset, objects_dir, state, None)
3867}
3868
3869fn resolve_pack_object_at_into(
3870    start_idx: PackIndexHandle<'_>,
3871    start_offset: u64,
3872    objects_dir: Option<&Path>,
3873    state: &mut DeltaChainState,
3874    populate_delta_lru: bool,
3875    out: &mut Vec<u8>,
3876) -> Result<ObjectKind> {
3877    resolve_pack_object_at_with_bytes_into(
3878        start_idx,
3879        start_offset,
3880        objects_dir,
3881        state,
3882        None,
3883        populate_delta_lru,
3884        out,
3885    )
3886}
3887
3888fn resolve_pack_object_at_with_bytes(
3889    start_idx: PackIndexHandle<'_>,
3890    start_offset: u64,
3891    objects_dir: Option<&Path>,
3892    state: &mut DeltaChainState,
3893    pack_bytes_override: Option<&[u8]>,
3894) -> Result<(ObjectKind, Vec<u8>)> {
3895    let mut out = Vec::new();
3896    let kind = resolve_pack_object_at_with_bytes_into(
3897        start_idx,
3898        start_offset,
3899        objects_dir,
3900        state,
3901        pack_bytes_override,
3902        true,
3903        &mut out,
3904    )?;
3905    Ok((kind, out))
3906}
3907
3908fn resolve_pack_object_at_with_bytes_into(
3909    start_idx: PackIndexHandle<'_>,
3910    start_offset: u64,
3911    objects_dir: Option<&Path>,
3912    state: &mut DeltaChainState,
3913    pack_bytes_override: Option<&[u8]>,
3914    populate_delta_lru: bool,
3915    out: &mut Vec<u8>,
3916) -> Result<ObjectKind> {
3917    let pack_path = start_idx.get().pack_path.clone();
3918    let start_pack_id = pack_cache::pack_id_for(&pack_path);
3919    for attempt in 0..2 {
3920        match resolve_pack_object_at_body(
3921            start_idx.clone(),
3922            start_offset,
3923            start_pack_id,
3924            objects_dir,
3925            state,
3926            pack_bytes_override,
3927            populate_delta_lru,
3928            out,
3929        ) {
3930            Ok(kind) => return Ok(kind),
3931            Err(e) if attempt == 0 && pack_bytes_parse_may_be_stale(&e) => {
3932                if pack_cache::reload_pack_bytes_after_parse_failure(&pack_path)? {
3933                    state.visited.retain(|(id, _)| *id != start_pack_id);
3934                    continue;
3935                }
3936                return Err(e);
3937            }
3938            Err(e) => return Err(e),
3939        }
3940    }
3941    unreachable!("at most two resolution attempts")
3942}
3943
3944#[allow(clippy::too_many_arguments)]
3945fn resolve_pack_object_at_body(
3946    mut cur_idx: PackIndexHandle<'_>,
3947    start_offset: u64,
3948    result_pack_id: u32,
3949    objects_dir: Option<&Path>,
3950    state: &mut DeltaChainState,
3951    pack_bytes_override: Option<&[u8]>,
3952    populate_delta_lru: bool,
3953    out: &mut Vec<u8>,
3954) -> Result<ObjectKind> {
3955    let result_offset = start_offset;
3956    let mut cur_offset = start_offset;
3957    state.pending.clear();
3958
3959    let mut cur_pack_path = cur_idx.get().pack_path.clone();
3960    let mut cur_pack_id = pack_cache::pack_id_for(&cur_pack_path);
3961    let mut validated_pack_count = false;
3962
3963    enum PackBytesSource<'a> {
3964        Borrowed(&'a [u8]),
3965        Cached(Arc<PackData>),
3966    }
3967
3968    let mut pack_bytes = match pack_bytes_override {
3969        Some(b) => PackBytesSource::Borrowed(b),
3970        None => PackBytesSource::Cached(read_pack_bytes_cached(&cur_pack_path)?),
3971    };
3972
3973    let mut compressed = Vec::new();
3974    loop {
3975        let idx_ref = cur_idx.get();
3976        if idx_ref.pack_path != cur_pack_path {
3977            cur_pack_path = idx_ref.pack_path.clone();
3978            pack_bytes = PackBytesSource::Cached(read_pack_bytes_cached(&cur_pack_path)?);
3979            cur_pack_id = pack_cache::pack_id_for(&cur_pack_path);
3980            validated_pack_count = false;
3981        }
3982        let pack_id = cur_pack_id;
3983        state.visit(pack_id, cur_offset)?;
3984
3985        if !validated_pack_count {
3986            let header: &[u8] = match &pack_bytes {
3987                PackBytesSource::Borrowed(b) => b,
3988                PackBytesSource::Cached(c) => c,
3989            };
3990            validate_pack_index_object_count(header, idx_ref)?;
3991            validated_pack_count = true;
3992        }
3993        let (bytes, mut pos) = match &pack_bytes {
3994            PackBytesSource::Borrowed(b) => (*b, cur_offset as usize),
3995            PackBytesSource::Cached(c) => {
3996                pack_entry_bytes(idx_ref, cur_offset, c, &mut compressed)?
3997            }
3998        };
3999        let object_start = cur_offset;
4000        let (packed_type, size) = match parse_pack_object_header(bytes, &mut pos) {
4001            Ok(v) => v,
4002            Err(err) => {
4003                if let Some((kind, data)) =
4004                    rescue_in_pack_base(idx_ref, cur_offset, objects_dir, state)?
4005                {
4006                    return finish_pack_read_with_pending(
4007                        out,
4008                        kind,
4009                        &data,
4010                        None,
4011                        result_pack_id,
4012                        result_offset,
4013                        populate_delta_lru,
4014                        state,
4015                    );
4016                }
4017                return Err(err);
4018            }
4019        };
4020
4021        match packed_type {
4022            PackedType::Commit | PackedType::Tree | PackedType::Blob | PackedType::Tag => {
4023                let kind = packed_type_to_kind(packed_type)?;
4024                if state.pending.is_empty() {
4025                    if let Err(err) =
4026                        decompress_pack_data_into(bytes, &mut pos, size, &mut state.inflate, out)
4027                    {
4028                        if let Some((rescue_kind, rescued)) =
4029                            rescue_in_pack_base(idx_ref, cur_offset, objects_dir, state)?
4030                        {
4031                            return finish_pack_read_with_pending(
4032                                out,
4033                                rescue_kind,
4034                                &rescued,
4035                                None,
4036                                result_pack_id,
4037                                result_offset,
4038                                populate_delta_lru,
4039                                state,
4040                            );
4041                        }
4042                        return Err(err);
4043                    }
4044                    return Ok(kind);
4045                }
4046                state.whole_object.clear();
4047                if let Err(err) = decompress_pack_data_into(
4048                    bytes,
4049                    &mut pos,
4050                    size,
4051                    &mut state.inflate,
4052                    &mut state.whole_object,
4053                ) {
4054                    if let Some((rescue_kind, rescued)) =
4055                        rescue_in_pack_base(idx_ref, cur_offset, objects_dir, state)?
4056                    {
4057                        return finish_pack_read_with_pending(
4058                            out,
4059                            rescue_kind,
4060                            &rescued,
4061                            None,
4062                            result_pack_id,
4063                            result_offset,
4064                            populate_delta_lru,
4065                            state,
4066                        );
4067                    }
4068                    return Err(err);
4069                }
4070                let whole = std::mem::take(&mut state.whole_object);
4071                return finish_pack_read_with_pending(
4072                    out,
4073                    kind,
4074                    &whole,
4075                    Some((pack_id, cur_offset)),
4076                    result_pack_id,
4077                    result_offset,
4078                    populate_delta_lru,
4079                    state,
4080                );
4081            }
4082            PackedType::OfsDelta => {
4083                state.next_hop()?;
4084                let base_offset = parse_ofs_delta_base(bytes, &mut pos, object_start)?;
4085                state.delta_decompress.clear();
4086                decompress_pack_data_into(
4087                    bytes,
4088                    &mut pos,
4089                    size,
4090                    &mut state.inflate,
4091                    &mut state.delta_decompress,
4092                )?;
4093                state.pending.push(PendingDeltaFrame {
4094                    pack_id,
4095                    offset: object_start,
4096                    delta: std::mem::take(&mut state.delta_decompress),
4097                });
4098                if let Some((base_kind, base_data)) =
4099                    pack_cache::get_delta_base(pack_id, base_offset)
4100                {
4101                    return finish_pack_read_with_pending(
4102                        out,
4103                        base_kind,
4104                        base_data.as_ref(),
4105                        None,
4106                        result_pack_id,
4107                        result_offset,
4108                        populate_delta_lru,
4109                        state,
4110                    );
4111                }
4112                cur_offset = base_offset;
4113            }
4114            PackedType::RefDelta => {
4115                state.next_hop()?;
4116                let hb = idx_ref.hash_bytes();
4117                if pos + hb > bytes.len() {
4118                    return Err(Error::CorruptObject(
4119                        "truncated ref-delta base OID".to_owned(),
4120                    ));
4121                }
4122                let base_raw = bytes[pos..pos + hb].to_vec();
4123                pos += hb;
4124                state.delta_decompress.clear();
4125                decompress_pack_data_into(
4126                    bytes,
4127                    &mut pos,
4128                    size,
4129                    &mut state.inflate,
4130                    &mut state.delta_decompress,
4131                )?;
4132                state.pending.push(PendingDeltaFrame {
4133                    pack_id,
4134                    offset: object_start,
4135                    delta: std::mem::take(&mut state.delta_decompress),
4136                });
4137
4138                if let Ok(base_oid) = ObjectId::from_bytes(base_raw.as_slice()) {
4139                    if let Some(base_offset) = idx_ref.find_offset(&base_oid)? {
4140                        if let Some((base_kind, base_data)) =
4141                            pack_cache::get_delta_base(pack_id, base_offset)
4142                        {
4143                            return finish_pack_read_with_pending(
4144                                out,
4145                                base_kind,
4146                                base_data.as_ref(),
4147                                None,
4148                                result_pack_id,
4149                                result_offset,
4150                                populate_delta_lru,
4151                                state,
4152                            );
4153                        }
4154                        cur_offset = base_offset;
4155                        continue;
4156                    }
4157                }
4158
4159                if hb == 20 {
4160                    if let (Some(dir), Ok(base_oid)) =
4161                        (objects_dir, ObjectId::from_bytes(base_raw.as_slice()))
4162                    {
4163                        let loose = dir
4164                            .join(base_oid.loose_prefix())
4165                            .join(base_oid.loose_suffix());
4166                        if loose.is_file() {
4167                            if let Ok(obj) =
4168                                crate::odb::Odb::read_loose_verify_oid(&loose, &base_oid)
4169                            {
4170                                return finish_pack_read_with_pending(
4171                                    out,
4172                                    obj.kind,
4173                                    &obj.data,
4174                                    None,
4175                                    result_pack_id,
4176                                    result_offset,
4177                                    populate_delta_lru,
4178                                    state,
4179                                );
4180                            }
4181                        }
4182                        if let Some(other_idx) = find_other_pack_index(dir, idx_ref, &base_oid)? {
4183                            let Some(off) = other_idx.find_offset(&base_oid)? else {
4184                                return Err(Error::ObjectNotFound(base_oid.to_hex()));
4185                            };
4186                            cur_idx = PackIndexHandle::Shared(other_idx);
4187                            cur_offset = off;
4188                            continue;
4189                        }
4190                    }
4191                }
4192                return Err(Error::CorruptObject(format!(
4193                    "ref-delta base {} not found in pack",
4194                    oid_bytes_to_hex(&base_raw)
4195                )));
4196            }
4197        }
4198    }
4199}
4200
4201/// Resolve kind and uncompressed size for one pack object without materializing its body.
4202fn resolve_pack_object_info_at(
4203    start_idx: PackIndexHandle<'_>,
4204    start_offset: u64,
4205    objects_dir: Option<&Path>,
4206    state: &mut DeltaChainState,
4207) -> Result<ObjectInfo> {
4208    resolve_pack_object_info_at_with_bytes(start_idx, start_offset, objects_dir, state, None)
4209}
4210
4211fn resolve_pack_object_info_at_body(
4212    cur_idx: PackIndexHandle<'_>,
4213    start_offset: u64,
4214    objects_dir: Option<&Path>,
4215    state: &mut DeltaChainState,
4216    pack_bytes_override: Option<&[u8]>,
4217) -> Result<ObjectInfo> {
4218    let cached_pack;
4219    let cur_bytes: &[u8] = if let Some(b) = pack_bytes_override {
4220        b
4221    } else {
4222        cached_pack = read_pack_bytes_cached(&cur_idx.get().pack_path)?;
4223        cached_pack.as_ref()
4224    };
4225    let mut cur_offset = start_offset;
4226    let mut result_size: Option<u64> = None;
4227
4228    loop {
4229        let idx_ref = cur_idx.get();
4230        let pack_id = pack_cache::pack_id_for(&idx_ref.pack_path);
4231        state.visit(pack_id, cur_offset)?;
4232
4233        if let Some(base) = cached_pack_read_info(&idx_ref.pack_path, cur_offset) {
4234            if let Some(size) = result_size {
4235                return Ok(ObjectInfo {
4236                    kind: base.kind,
4237                    size,
4238                });
4239            }
4240            return Ok(base);
4241        }
4242        let object_start = cur_offset;
4243        let mut pos = cur_offset as usize;
4244        let (packed_type, header_size) = parse_pack_object_header(cur_bytes, &mut pos)?;
4245
4246        match packed_type {
4247            PackedType::Commit | PackedType::Tree | PackedType::Blob | PackedType::Tag => {
4248                let kind = packed_type_to_kind(packed_type)?;
4249                remember_pack_read_info(
4250                    &idx_ref.pack_path,
4251                    cur_offset,
4252                    ObjectInfo {
4253                        kind,
4254                        size: header_size,
4255                    },
4256                );
4257                let size = result_size.unwrap_or(header_size);
4258                return Ok(ObjectInfo { kind, size });
4259            }
4260            PackedType::OfsDelta => {
4261                state.next_hop()?;
4262                let base_offset = parse_ofs_delta_base(cur_bytes, &mut pos, object_start)?;
4263                if result_size.is_none() {
4264                    result_size = Some(read_delta_result_size_from_pack_zlib(cur_bytes, &mut pos)?);
4265                }
4266                cur_offset = base_offset;
4267            }
4268            PackedType::RefDelta => {
4269                state.next_hop()?;
4270                let hb = idx_ref.hash_bytes();
4271                if pos + hb > cur_bytes.len() {
4272                    return Err(Error::CorruptObject(
4273                        "truncated ref-delta base OID".to_owned(),
4274                    ));
4275                }
4276                let base_raw = &cur_bytes[pos..pos + hb];
4277                pos += hb;
4278                if result_size.is_none() {
4279                    result_size = Some(read_delta_result_size_from_pack_zlib(cur_bytes, &mut pos)?);
4280                }
4281
4282                let base_oid = ObjectId::from_bytes(base_raw)?;
4283                if let Some(base_offset) = idx_ref.find_offset(&base_oid)? {
4284                    cur_offset = base_offset;
4285                    continue;
4286                }
4287
4288                if let Some(dir) = objects_dir {
4289                    let loose = dir
4290                        .join(base_oid.loose_prefix())
4291                        .join(base_oid.loose_suffix());
4292                    if loose.is_file() {
4293                        let info = crate::odb::read_loose_object_info(&loose)?;
4294                        let size = result_size.unwrap_or(info.size);
4295                        return Ok(ObjectInfo {
4296                            kind: info.kind,
4297                            size,
4298                        });
4299                    }
4300                    if let Some(other_idx) = find_other_pack_index(dir, idx_ref, &base_oid)? {
4301                        let Some(off) = other_idx.find_offset(&base_oid)? else {
4302                            return Err(Error::ObjectNotFound(base_oid.to_hex()));
4303                        };
4304                        let base_info = resolve_pack_object_info_at(
4305                            PackIndexHandle::Shared(other_idx),
4306                            off,
4307                            objects_dir,
4308                            state,
4309                        )?;
4310                        let size = result_size.unwrap_or(base_info.size);
4311                        return Ok(ObjectInfo {
4312                            kind: base_info.kind,
4313                            size,
4314                        });
4315                    }
4316                }
4317                return Err(Error::CorruptObject(format!(
4318                    "ref-delta base {} not found in pack",
4319                    oid_bytes_to_hex(base_raw)
4320                )));
4321            }
4322        }
4323    }
4324}
4325
4326fn read_pack_object_at(
4327    pack_bytes: &[u8],
4328    start_offset: u64,
4329    idx: &PackIndex,
4330    objects_dir: Option<&Path>,
4331    start_depth: usize,
4332) -> Result<(ObjectKind, Vec<u8>)> {
4333    let mut state = DeltaChainState {
4334        depth: start_depth,
4335        visited: HashSet::new(),
4336        ..Default::default()
4337    };
4338    resolve_pack_object_at_with_bytes(
4339        PackIndexHandle::Borrowed(idx),
4340        start_offset,
4341        objects_dir,
4342        &mut state,
4343        Some(pack_bytes),
4344    )
4345}
4346
4347/// Read an object from a pack file by its OID.
4348///
4349/// Searches the given pack index for the OID, then reads and decompresses
4350/// the object from the corresponding pack file, resolving delta chains.
4351///
4352/// # Errors
4353///
4354/// Returns [`Error::ObjectNotFound`] if the OID is not in this pack.
4355pub fn read_object_from_pack(idx: &PackIndex, oid: &ObjectId) -> Result<Object> {
4356    if crate::pack_store::PackStore::has_context() {
4357        return read_object_from_pack_at_depth(idx, oid, 0);
4358    }
4359    let store = crate::pack_store::PackStore::legacy_for_pack_path(&idx.pack_path);
4360    crate::pack_store::PackStore::with_context(store, || {
4361        read_object_from_pack_at_depth(idx, oid, 0)
4362    })
4363}
4364
4365/// Like [`read_object_from_pack`], inflating into `buf` and reusing its capacity.
4366///
4367/// # Errors
4368///
4369/// Same as [`read_object_from_pack`].
4370pub fn read_object_from_pack_into(
4371    idx: &PackIndex,
4372    oid: &ObjectId,
4373    buf: &mut Vec<u8>,
4374) -> Result<ObjectKind> {
4375    let Some(offset) = idx.find_offset(oid)? else {
4376        return Err(Error::ObjectNotFound(oid.to_hex()));
4377    };
4378    if crate::pack_store::PackStore::has_context() {
4379        return read_object_at_depth_into(idx, offset, 0, false, buf);
4380    }
4381    let store = crate::pack_store::PackStore::legacy_for_pack_path(&idx.pack_path);
4382    crate::pack_store::PackStore::with_context(store, || {
4383        read_object_at_depth_into(idx, offset, 0, false, buf)
4384    })
4385}
4386
4387/// Read [`ObjectInfo`] for `oid` stored in `idx`'s pack without inflating the full object body.
4388///
4389/// # Errors
4390///
4391/// Same as [`read_object_from_pack`], except no hash verification is performed on the payload.
4392pub fn read_object_info_from_pack(idx: &PackIndex, oid: &ObjectId) -> Result<ObjectInfo> {
4393    let Some(offset) = idx.find_offset(oid)? else {
4394        return Err(Error::ObjectNotFound(oid.to_hex()));
4395    };
4396    read_object_info_from_pack_at_offset(idx, offset)
4397}
4398
4399/// Read [`ObjectInfo`] at a known pack offset (avoids a second index lookup in batch workloads).
4400///
4401/// # Errors
4402///
4403/// Same as [`read_object_info_from_pack`].
4404pub fn read_object_info_from_pack_at_offset(idx: &PackIndex, offset: u64) -> Result<ObjectInfo> {
4405    let pack_path = idx.pack_path.clone();
4406    for attempt in 0..2 {
4407        let pack_bytes = read_pack_bytes_cached(&pack_path)?;
4408        if !pack_cache::pack_object_count_validated(&pack_path) {
4409            match validate_pack_index_object_count(&pack_bytes, idx) {
4410                Ok(()) => pack_cache::mark_pack_object_count_validated(&pack_path),
4411                Err(e) if attempt == 0 && pack_bytes_parse_may_be_stale(&e) => {
4412                    if pack_cache::reload_pack_bytes_after_parse_failure(&pack_path)? {
4413                        continue;
4414                    }
4415                    return Err(e);
4416                }
4417                Err(e) => return Err(e),
4418            }
4419        }
4420        // Object-info probes often visit many unrelated historical commits while peeling
4421        // loose tags. A few header bytes must not retain a fault-around window per tip.
4422        let mut header = [0u8; 16];
4423        if let Some(len) = pack_bytes.read_sparse(offset, &mut header) {
4424            if let Some(info) = read_whole_pack_entry_info_fast(idx, 0, &header[..len]) {
4425                if info.kind == ObjectKind::Tag {
4426                    remember_pack_read_info(&pack_path, offset, info);
4427                }
4428                return Ok(info);
4429            }
4430        }
4431        match read_object_info_from_pack_at_offset_with_bytes(idx, offset, pack_bytes.as_ref()) {
4432            Ok(info) => return Ok(info),
4433            Err(e) if attempt == 0 && pack_bytes_parse_may_be_stale(&e) => {
4434                if pack_cache::reload_pack_bytes_after_parse_failure(&pack_path)? {
4435                    continue;
4436                }
4437                return Err(e);
4438            }
4439            Err(e) => return Err(e),
4440        }
4441    }
4442    unreachable!("at most two read attempts")
4443}
4444
4445/// Compressed bytes [`read_packed_commit_header_block_into`] fetches with one positioned read.
4446const COMMIT_HEADER_BLOCK_WINDOW: usize = 4096;
4447
4448/// Commit bytes [`read_packed_commit_header_block_into`] must find the blank line that ends the
4449/// headers within. Covers ordinary headers; long signatures take the whole read.
4450const COMMIT_HEADER_BLOCK_MAX: usize = 1024;
4451
4452/// Output space past the bytes a header read needs. zlib decodes with its fast loop only while
4453/// at least this much output room remains, so a buffer sized exactly to the object would decode
4454/// most of a short commit one symbol at a time.
4455const INFLATE_FAST_OUTPUT_SLACK: usize = 258;
4456
4457/// Bytes the first read of [`read_packed_commit_header_block_into`] fetches. Compressed commit
4458/// headers rarely reach this, so most reads copy a quarter of the full window.
4459const COMMIT_HEADER_BLOCK_FIRST_READ: usize = 1024;
4460
4461/// Compressed bytes [`inflate_commit_header_block`] feeds the inflater before it looks for the
4462/// end of the headers again. Ordinary headers compress to a single step.
4463const COMMIT_HEADER_BLOCK_INPUT_STEP: usize = 256;
4464
4465/// Inflate the header block of commit `oid`, stored whole in a local pack, into `out`.
4466///
4467/// On success `out` holds the headers through the blank line that ends them, so it parses as
4468/// the same commit with an empty message. The entry is fetched with one positioned read rather
4469/// than through the pack mapping: walks seeded from every ref read hundreds of scattered tip
4470/// commits, and each mapped read would leave a fault-around window of the pack resident.
4471///
4472/// Returns `false`, leaving `out` unspecified, when no local pack index lists `oid`, the entry
4473/// is not a whole commit, the header block exceeds [`COMMIT_HEADER_BLOCK_MAX`], or the
4474/// positioned read or the inflate fails. Callers then read the whole object, which reports
4475/// any corruption.
4476///
4477/// # Errors
4478///
4479/// Propagates pack directory listing and pack index lookup errors.
4480pub(crate) fn read_packed_commit_header_block_into(
4481    objects_dir: &Path,
4482    oid: &ObjectId,
4483    out: &mut Vec<u8>,
4484) -> Result<bool> {
4485    pack_cache::sync_thread_local_for_objects_dir(objects_dir);
4486    if let Some(idx) = pack_cache::hinted_pack_index(objects_dir) {
4487        if let Some(offset) = pack_cache::find_offset_in_pack_read_context(objects_dir, &idx, oid)?
4488        {
4489            return Ok(read_commit_header_block_at(&idx, offset, out));
4490        }
4491    }
4492    for idx in pack_indexes_for_lookup(objects_dir, PackLookupOptions::ALL_LOCAL_PACKS)? {
4493        if let Some(offset) = pack_cache::find_offset_in_pack_read_context(objects_dir, &idx, oid)?
4494        {
4495            pack_cache::remember_pack_hit(objects_dir, &idx);
4496            return Ok(read_commit_header_block_at(&idx, offset, out));
4497        }
4498    }
4499    Ok(false)
4500}
4501
4502/// [`read_packed_commit_header_block_into`] for many commits at once.
4503///
4504/// Calls `found(i, header)` for every `oids[i]` the shortcut applies to, with the bytes
4505/// [`read_packed_commit_header_block_into`] would leave in its buffer; commits it does not
4506/// apply to get no call, and callers read those whole. The pack lookup, the pack's cached
4507/// bytes and the read buffers are set up once for the batch rather than once per commit,
4508/// which is most of the cost of a header read when a walk seeds from hundreds of tags.
4509///
4510/// # Errors
4511///
4512/// Propagates pack directory listing and pack index lookup errors.
4513pub(crate) fn read_packed_commit_header_blocks(
4514    objects_dir: &Path,
4515    oids: &[ObjectId],
4516    mut found: impl FnMut(usize, &[u8]),
4517) -> Result<()> {
4518    if oids.is_empty() {
4519        return Ok(());
4520    }
4521    pack_cache::sync_thread_local_for_objects_dir(objects_dir);
4522    let indexes = pack_indexes_for_lookup(objects_dir, PackLookupOptions::ALL_LOCAL_PACKS)?;
4523    let mut packs: Vec<Option<Option<Arc<PackData>>>> = vec![None; indexes.len()];
4524    let mut readers: Vec<Option<Option<crate::pack_index::PackIndexRowReader<'_>>>> =
4525        std::iter::repeat_with(|| None)
4526            .take(indexes.len())
4527            .collect();
4528    let mut window = vec![0u8; COMMIT_HEADER_BLOCK_WINDOW];
4529    let mut out = Vec::with_capacity(COMMIT_HEADER_BLOCK_MAX + INFLATE_FAST_OUTPUT_SLACK);
4530    let mut last_hit = 0usize;
4531    WHOLE_OBJECT_ZLIB.with(|slot| {
4532        let mut zlib = slot.borrow_mut();
4533        for (i, oid) in oids.iter().enumerate() {
4534            let order =
4535                std::iter::once(last_hit).chain((0..indexes.len()).filter(|&p| p != last_hit));
4536            for p in order {
4537                let (Some(idx), Some(reader)) = (indexes.get(p), readers.get_mut(p)) else {
4538                    continue;
4539                };
4540                let offset = match reader.get_or_insert_with(|| idx.row_reader()) {
4541                    Some(reader) => reader.find_offset(oid)?,
4542                    None => idx.find_offset(oid)?,
4543                };
4544                let Some(offset) = offset else {
4545                    continue;
4546                };
4547                last_hit = p;
4548                let pack = packs[p].get_or_insert_with(|| validated_pack_bytes(idx));
4549                if let Some(pack) = pack {
4550                    if read_commit_header_block_from(pack, offset, &mut window, &mut zlib, &mut out)
4551                    {
4552                        found(i, &out);
4553                    }
4554                }
4555                break;
4556            }
4557        }
4558        Ok(())
4559    })
4560}
4561
4562/// The bytes of `idx`'s pack, once their object count is known to match the index.
4563fn validated_pack_bytes(idx: &PackIndex) -> Option<Arc<PackData>> {
4564    let pack_bytes = read_pack_bytes_cached(&idx.pack_path).ok()?;
4565    if !pack_cache::pack_object_count_validated(&idx.pack_path) {
4566        validate_pack_index_object_count(&pack_bytes, idx).ok()?;
4567        pack_cache::mark_pack_object_count_validated(&idx.pack_path);
4568    }
4569    Some(pack_bytes)
4570}
4571
4572fn read_commit_header_block_at(idx: &PackIndex, offset: u64, out: &mut Vec<u8>) -> bool {
4573    let Some(pack_bytes) = validated_pack_bytes(idx) else {
4574        return false;
4575    };
4576    let mut window = [0u8; COMMIT_HEADER_BLOCK_WINDOW];
4577    WHOLE_OBJECT_ZLIB.with(|slot| {
4578        read_commit_header_block_from(
4579            &pack_bytes,
4580            offset,
4581            &mut window,
4582            &mut slot.borrow_mut(),
4583            out,
4584        )
4585    })
4586}
4587
4588/// Read the commit entry at `offset` with a positioned read into `window` and inflate its
4589/// header block into `out` (see [`read_packed_commit_header_block_into`]).
4590///
4591/// The first read fetches [`COMMIT_HEADER_BLOCK_FIRST_READ`] bytes, which hold the compressed
4592/// header block of nearly every commit; only an entry that does not inflate from those is
4593/// read again with the whole window.
4594fn read_commit_header_block_from(
4595    pack_bytes: &PackData,
4596    offset: u64,
4597    window: &mut [u8],
4598    zlib: &mut ZlibInflateScratch,
4599    out: &mut Vec<u8>,
4600) -> bool {
4601    let first_read = COMMIT_HEADER_BLOCK_FIRST_READ.min(window.len());
4602    let Some(len) = pack_bytes.read_sparse(offset, &mut window[..first_read]) else {
4603        return false;
4604    };
4605    let mut pos = 0usize;
4606    let Ok((PackedType::Commit, size)) = parse_pack_object_header(&window[..len], &mut pos) else {
4607        return false;
4608    };
4609    let want = usize::try_from(size).map_or(COMMIT_HEADER_BLOCK_MAX, |size| {
4610        size.min(COMMIT_HEADER_BLOCK_MAX)
4611    });
4612    if inflate_commit_header_block(&window[pos..len], size, want, zlib, out) {
4613        return true;
4614    }
4615    if len < first_read || first_read == window.len() {
4616        return false;
4617    }
4618    let Some(len) = pack_bytes.read_sparse(offset, window) else {
4619        return false;
4620    };
4621    inflate_commit_header_block(&window[pos..len], size, want, zlib, out)
4622}
4623
4624/// Inflate the zlib stream of a `size`-byte commit from `compressed` into `out` and keep its
4625/// headers through the blank line that ends them, which must lie within its first `want` bytes.
4626///
4627/// Inflation stops at that blank line, so the message is never decoded: walks that read
4628/// every commit of a history (a push whose new commits are dated before the peer's tips,
4629/// say) spend most of their time inflating.
4630fn inflate_commit_header_block(
4631    compressed: &[u8],
4632    size: u64,
4633    want: usize,
4634    zlib: &mut ZlibInflateScratch,
4635    out: &mut Vec<u8>,
4636) -> bool {
4637    let inflated = zlib.decompress_prefix_until(
4638        compressed,
4639        size,
4640        COMMIT_HEADER_BLOCK_INPUT_STEP,
4641        out,
4642        |so_far| memchr::memmem::find(so_far, b"\n\n").map(|blank| blank + 2),
4643    );
4644    if inflated.is_err() {
4645        return false;
4646    }
4647    match memchr::memmem::find(out, b"\n\n") {
4648        Some(blank) if blank + 2 <= want => {
4649            out.truncate(blank + 2);
4650            true
4651        }
4652        _ => false,
4653    }
4654}
4655
4656fn read_object_info_from_pack_at_offset_with_bytes(
4657    idx: &PackIndex,
4658    offset: u64,
4659    pack_bytes: &[u8],
4660) -> Result<ObjectInfo> {
4661    if let Some(info) = read_whole_pack_entry_info_fast(idx, offset, pack_bytes) {
4662        return Ok(info);
4663    }
4664    let objects_dir = idx.pack_path.parent().and_then(Path::parent);
4665    with_pack_read_delta_state(0, |state| {
4666        resolve_pack_object_info_at_with_bytes(
4667            PackIndexHandle::Borrowed(idx),
4668            offset,
4669            objects_dir,
4670            state,
4671            Some(pack_bytes),
4672        )
4673    })
4674}
4675
4676fn resolve_pack_object_info_at_with_bytes(
4677    start_idx: PackIndexHandle<'_>,
4678    start_offset: u64,
4679    objects_dir: Option<&Path>,
4680    state: &mut DeltaChainState,
4681    pack_bytes_override: Option<&[u8]>,
4682) -> Result<ObjectInfo> {
4683    let idx_ref = start_idx.get();
4684    let pack_path = idx_ref.pack_path.clone();
4685    let start_pack_id = pack_cache::pack_id_for(&pack_path);
4686    if let Some(info) = cached_pack_read_info(&pack_path, start_offset) {
4687        return Ok(info);
4688    }
4689    for attempt in 0..2 {
4690        match resolve_pack_object_info_at_body(
4691            start_idx.clone(),
4692            start_offset,
4693            objects_dir,
4694            state,
4695            pack_bytes_override,
4696        ) {
4697            Ok(v) => {
4698                remember_pack_read_info(&pack_path, start_offset, v);
4699                return Ok(v);
4700            }
4701            Err(e) if attempt == 0 && pack_bytes_parse_may_be_stale(&e) => {
4702                if pack_cache::reload_pack_bytes_after_parse_failure(&pack_path)? {
4703                    state.visited.retain(|(id, _)| *id != start_pack_id);
4704                    continue;
4705                }
4706                return Err(e);
4707            }
4708            Err(e) => return Err(e),
4709        }
4710    }
4711    unreachable!("at most two resolution attempts")
4712}
4713
4714/// Header-only info for undeltified pack entries (batch-check hot path).
4715#[inline]
4716fn read_whole_pack_entry_info_fast(
4717    _idx: &PackIndex,
4718    offset: u64,
4719    pack_bytes: &[u8],
4720) -> Option<ObjectInfo> {
4721    let mut pos = offset as usize;
4722    if pos >= pack_bytes.len() {
4723        return None;
4724    }
4725    let (packed_type, header_size) = parse_pack_object_header(pack_bytes, &mut pos).ok()?;
4726    match packed_type {
4727        PackedType::Commit | PackedType::Tree | PackedType::Blob | PackedType::Tag => {
4728            let kind = packed_type_to_kind(packed_type).ok()?;
4729            Some(ObjectInfo {
4730                kind,
4731                size: header_size,
4732            })
4733        }
4734        PackedType::OfsDelta | PackedType::RefDelta => None,
4735    }
4736}
4737
4738// hygiene: per-thread zlib scratch for whole pack entry reads (ROADMAP item 7 — pack read hot path)
4739thread_local! {
4740    static WHOLE_OBJECT_ZLIB: RefCell<ZlibInflateScratch> =
4741        RefCell::new(ZlibInflateScratch::default());
4742}
4743
4744/// Max uncompressed bytes to inflate when only commit headers (e.g. author) are needed.
4745pub(crate) const COMMIT_HEADER_READ_PREFIX: usize = 512;
4746
4747/// Largest declared commit size [`SingleLocalPackReader::read_commit_parents`] inflates into
4748/// its reused buffer; bigger (or corrupt, huge) sizes take the ordinary object read.
4749const COMMIT_PARENTS_WHOLE_READ_MAX: u64 = 1 << 20;
4750
4751/// Compressed bytes per inflate step when reading a commit only through its subject line.
4752///
4753/// Commit text inflates to roughly three times this, so the step is small enough that most of
4754/// a long message body is never inflated.
4755const COMMIT_SUBJECT_READ_STEP: usize = 128;
4756
4757/// Select one entry for random reads; ordinary readers keep their zero-copy fast path.
4758fn pack_entry_bytes<'a>(
4759    idx: &PackIndex,
4760    offset: u64,
4761    pack: &'a PackData,
4762    buf: &'a mut Vec<u8>,
4763) -> Result<(&'a [u8], usize)> {
4764    let start = usize::try_from(offset)
4765        .map_err(|_| Error::CorruptObject("pack offset out of range".to_owned()))?;
4766    if !pack.uses_positioned_reads() {
4767        return Ok((pack, start));
4768    }
4769    let pack_end = pack
4770        .len()
4771        .checked_sub(idx.hash_bytes())
4772        .ok_or_else(|| Error::CorruptObject("truncated pack trailer".to_owned()))?;
4773    let end = idx.next_pack_offset_after(offset, pack_end as u64)? as usize;
4774    Ok((pack.read_range(start, end, buf)?, 0))
4775}
4776
4777fn read_whole_pack_entry_object_fast(
4778    idx: &PackIndex,
4779    offset: u64,
4780    pack_bytes: &PackData,
4781) -> Option<Object> {
4782    let mut data = Vec::new();
4783    read_whole_pack_entry_object_into(idx, offset, pack_bytes, &mut data)
4784        .map(|kind| Object::new(kind, data))
4785}
4786
4787fn read_whole_pack_entry_object_into(
4788    idx: &PackIndex,
4789    offset: u64,
4790    pack_bytes: &PackData,
4791    out: &mut Vec<u8>,
4792) -> Option<ObjectKind> {
4793    let pack_id = pack_cache::pack_id_for(&idx.pack_path);
4794    if let Some((kind, data)) = pack_cache::get_delta_base(pack_id, offset) {
4795        out.clear();
4796        out.extend_from_slice(data.as_ref());
4797        return Some(kind);
4798    }
4799    // Single-pack readers and general reads share this path: a preceding tag-info
4800    // probe lets either read a small whole tag without faulting mmap windows.
4801    if cached_pack_read_info(&idx.pack_path, offset)
4802        .is_some_and(|info| info.kind == ObjectKind::Tag)
4803    {
4804        let mut bytes = [0u8; 4096];
4805        if let Some(len) = pack_bytes.read_sparse(offset, &mut bytes) {
4806            if let Some(kind) = inflate_whole_pack_entry_into(&bytes[..len], 0, out) {
4807                return Some(kind);
4808            }
4809        }
4810    }
4811    let mut compressed = Vec::new();
4812    let (pack_bytes, pos) = pack_entry_bytes(idx, offset, pack_bytes, &mut compressed).ok()?;
4813    inflate_whole_pack_entry_into(pack_bytes, pos as u64, out)
4814}
4815
4816fn inflate_whole_pack_entry_into(
4817    pack_bytes: &[u8],
4818    offset: u64,
4819    out: &mut Vec<u8>,
4820) -> Option<ObjectKind> {
4821    let mut pos = offset as usize;
4822    if pos >= pack_bytes.len() {
4823        return None;
4824    }
4825    let (packed_type, size) = parse_pack_object_header(pack_bytes, &mut pos).ok()?;
4826    match packed_type {
4827        PackedType::Commit | PackedType::Tree | PackedType::Blob | PackedType::Tag => {
4828            let kind = packed_type_to_kind(packed_type).ok()?;
4829            WHOLE_OBJECT_ZLIB.with(|slot| {
4830                let mut inflate = slot.borrow_mut();
4831                decompress_pack_data_into(pack_bytes, &mut pos, size, &mut inflate, out)
4832                    .ok()
4833                    .map(|()| kind)
4834            })
4835        }
4836        PackedType::OfsDelta | PackedType::RefDelta => None,
4837    }
4838}
4839
4840fn read_object_from_packs_in_context_into(
4841    objects_dir: &Path,
4842    oid: &ObjectId,
4843    buf: &mut Vec<u8>,
4844) -> Result<ObjectKind> {
4845    match try_read_object_from_packs_with_options_into(
4846        objects_dir,
4847        oid,
4848        PackLookupOptions::ALL_LOCAL_PACKS,
4849        false,
4850        buf,
4851    ) {
4852        Ok(kind) => Ok(kind),
4853        Err(err @ Error::ObjectNotFound(_)) => {
4854            if reprepare_pack_directory_on_miss(objects_dir)? {
4855                try_read_object_from_packs_with_options_into(
4856                    objects_dir,
4857                    oid,
4858                    PackLookupOptions::ALL_LOCAL_PACKS,
4859                    false,
4860                    buf,
4861                )
4862            } else {
4863                Err(err)
4864            }
4865        }
4866        Err(err) => Err(err),
4867    }
4868}
4869
4870fn read_object_from_packs_in_context(objects_dir: &Path, oid: &ObjectId) -> Result<Object> {
4871    match try_read_object_from_packs_with_options(
4872        objects_dir,
4873        oid,
4874        PackLookupOptions::ALL_LOCAL_PACKS,
4875    ) {
4876        Ok(obj) => Ok(obj),
4877        Err(err @ Error::ObjectNotFound(_)) => {
4878            if reprepare_pack_directory_on_miss_in_context(objects_dir)? {
4879                try_read_object_from_packs_with_options(
4880                    objects_dir,
4881                    oid,
4882                    PackLookupOptions::ALL_LOCAL_PACKS,
4883                )
4884            } else {
4885                Err(err)
4886            }
4887        }
4888        Err(err) => Err(err),
4889    }
4890}
4891
4892/// Read and decompress the object stored at `offset` in `idx`'s pack file.
4893///
4894/// The pack index is used for pack path, object count validation, and resolving ref-delta
4895/// bases; the starting location comes from the caller (e.g. a multi-pack-index entry).
4896pub(crate) fn read_object_at(idx: &PackIndex, offset: u64) -> Result<Object> {
4897    read_object_at_depth(idx, offset, 0)
4898}
4899
4900/// Read a packed object when the pack index entry is already known (batch cat-file workloads).
4901///
4902/// Avoids a second index lookup when iterating objects in offset order.
4903pub fn read_object_from_pack_at_offset(idx: &PackIndex, offset: u64) -> Result<Object> {
4904    read_object_at(idx, offset)
4905}
4906
4907/// [`read_object_from_pack`] with an explicit starting delta-chain depth, used when the read
4908/// itself resolves a delta base from another pack (the chain budget must carry across packs).
4909fn read_object_from_pack_at_depth(idx: &PackIndex, oid: &ObjectId, depth: usize) -> Result<Object> {
4910    let Some(offset) = idx.find_offset(oid)? else {
4911        return Err(Error::ObjectNotFound(oid.to_hex()));
4912    };
4913    read_object_at_depth(idx, offset, depth)
4914}
4915
4916fn read_object_at_depth(idx: &PackIndex, offset: u64, depth: usize) -> Result<Object> {
4917    let mut data = Vec::new();
4918    let kind = read_object_at_depth_into(idx, offset, depth, true, &mut data)?;
4919    Ok(Object::new(kind, data))
4920}
4921
4922fn read_object_at_depth_into(
4923    idx: &PackIndex,
4924    offset: u64,
4925    depth: usize,
4926    populate_delta_lru: bool,
4927    out: &mut Vec<u8>,
4928) -> Result<ObjectKind> {
4929    if depth == 0 {
4930        let pack_id = pack_cache::pack_id_for(&idx.pack_path);
4931        if let Some((kind, data)) = pack_cache::get_delta_base(pack_id, offset) {
4932            out.clear();
4933            out.extend_from_slice(data.as_ref());
4934            return Ok(kind);
4935        }
4936    }
4937    let pack_path = &idx.pack_path;
4938    let objects_dir = idx.pack_path.parent().and_then(Path::parent);
4939    for attempt in 0..2 {
4940        let pack_bytes = read_pack_bytes_cached(pack_path)?;
4941        if !pack_cache::pack_object_count_validated(pack_path) {
4942            match validate_pack_index_object_count(&pack_bytes, idx) {
4943                Ok(()) => pack_cache::mark_pack_object_count_validated(pack_path),
4944                Err(e) if attempt == 0 && pack_bytes_parse_may_be_stale(&e) => {
4945                    if pack_cache::reload_pack_bytes_after_parse_failure(pack_path)? {
4946                        continue;
4947                    }
4948                    return Err(e);
4949                }
4950                Err(e) => return Err(e),
4951            }
4952        }
4953        if depth == 0 && !pack_bytes.uses_positioned_reads() {
4954            if let Some(kind) = read_whole_pack_entry_object_into(idx, offset, &pack_bytes, out) {
4955                return Ok(kind);
4956            }
4957        }
4958        let resolved = with_pack_read_delta_state(depth, |state| {
4959            resolve_pack_object_at_into(
4960                PackIndexHandle::Borrowed(idx),
4961                offset,
4962                objects_dir,
4963                state,
4964                populate_delta_lru,
4965                out,
4966            )
4967        });
4968        match resolved {
4969            Ok(kind) => return Ok(kind),
4970            Err(e) if attempt == 0 && pack_bytes_parse_may_be_stale(&e) => {
4971                if pack_cache::reload_pack_bytes_after_parse_failure(pack_path)? {
4972                    continue;
4973                }
4974                return Err(e);
4975            }
4976            Err(e) => return Err(e),
4977        }
4978    }
4979    unreachable!("at most two read attempts")
4980}
4981
4982/// Resolve an object from already-loaded pack bytes (used by `verify-pack`).
4983pub fn read_object_from_pack_bytes(
4984    pack_bytes: &[u8],
4985    idx: &PackIndex,
4986    oid: &[u8],
4987) -> Result<Object> {
4988    validate_pack_index_object_count(pack_bytes, idx)?;
4989    let id = ObjectId::from_bytes(oid).map_err(|_| Error::ObjectNotFound(oid_bytes_to_hex(oid)))?;
4990    let entry_offset = idx
4991        .find_offset(&id)?
4992        .ok_or_else(|| Error::ObjectNotFound(oid_bytes_to_hex(oid)))?;
4993    let store = crate::pack_store::PackStore::legacy_for_pack_path(&idx.pack_path);
4994    crate::pack_store::PackStore::with_context(store, || {
4995        let (kind, data) = read_pack_object_at(pack_bytes, entry_offset, idx, None, 0)?;
4996        verify_packed_object_hash(kind, &data, oid)?;
4997        Ok(Object::new(kind, data))
4998    })
4999}
5000
5001fn validate_pack_index_object_count(pack_bytes: &[u8], idx: &PackIndex) -> Result<()> {
5002    if pack_bytes.len() < 12 || &pack_bytes[0..4] != b"PACK" {
5003        return Err(Error::CorruptObject("bad pack header".to_owned()));
5004    }
5005    let count =
5006        u32::from_be_bytes([pack_bytes[8], pack_bytes[9], pack_bytes[10], pack_bytes[11]]) as usize;
5007    if count != idx.len() {
5008        return Err(Error::CorruptObject(format!(
5009            "pack object count mismatch: pack has {count}, index has {}",
5010            idx.len()
5011        )));
5012    }
5013    Ok(())
5014}
5015
5016fn verify_packed_object_hash(kind: ObjectKind, data: &[u8], expected_oid: &[u8]) -> Result<()> {
5017    let algo = HashAlgo::from_len(expected_oid.len()).ok_or_else(|| {
5018        Error::CorruptObject(format!(
5019            "unsupported packed object id width: {}",
5020            expected_oid.len()
5021        ))
5022    })?;
5023    let actual = hash_object(algo, kind, data)?;
5024    if actual.as_bytes() != expected_oid {
5025        return Err(Error::CorruptObject(format!(
5026            "packed object {} hashes to {}",
5027            oid_bytes_to_hex(expected_oid),
5028            actual.to_hex()
5029        )));
5030    }
5031    Ok(())
5032}
5033
5034/// Options controlling which local packs participate in a lookup pass.
5035#[derive(Clone, Copy, Debug, Default)]
5036pub struct PackLookupOptions {
5037    /// When true, skip pack indexes named in the active multi-pack-index (MIDX lookup runs separately).
5038    pub skip_midx_covered_packs: bool,
5039    /// When true, search only pack indexes named in the active MIDX (rescue pass after a MIDX miss).
5040    pub only_midx_covered_packs: bool,
5041}
5042
5043impl PackLookupOptions {
5044    /// Search every local pack (including MIDX-covered indexes).
5045    pub const ALL_LOCAL_PACKS: Self = Self {
5046        skip_midx_covered_packs: false,
5047        only_midx_covered_packs: false,
5048    };
5049
5050    /// Search only MIDX-listed pack indexes (Git redundant-pack rescue when MIDX lookup missed).
5051    pub const MIDX_COVERED_PACKS: Self = Self {
5052        skip_midx_covered_packs: false,
5053        only_midx_covered_packs: true,
5054    };
5055
5056    /// Whether the single-pack and last-hit-pack shortcuts may run before the full scan.
5057    ///
5058    /// Those shortcuts probe a pack without consulting the MIDX filter, so a filtered pass
5059    /// would re-probe packs the other pass owns (every loose-object read pays both passes).
5060    fn allows_pack_shortcuts(self) -> bool {
5061        !self.skip_midx_covered_packs && !self.only_midx_covered_packs
5062    }
5063}
5064
5065/// Pick one pack index for monotonic commit reads (single pack, or commit pack + blob promisor).
5066fn select_single_local_pack_index(
5067    indexes: &[Arc<PackIndex>],
5068    commit_hint: Option<&crate::objects::ObjectId>,
5069) -> Result<Option<Arc<PackIndex>>> {
5070    if indexes.is_empty() {
5071        return Ok(None);
5072    }
5073    if indexes.len() == 1 {
5074        return Ok(Some(Arc::clone(&indexes[0])));
5075    }
5076    if let Some(oid) = commit_hint {
5077        let mut found: Option<Arc<PackIndex>> = None;
5078        for idx in indexes {
5079            if idx.find_offset(oid)?.is_some() {
5080                if found.is_some() {
5081                    return Ok(None);
5082                }
5083                found = Some(Arc::clone(idx));
5084            }
5085        }
5086        if found.is_some() {
5087            return Ok(found);
5088        }
5089    }
5090    Ok(None)
5091}
5092
5093/// Sorted `cat-file --batch` reader for repos with exactly one local pack (hot-path benchmarks).
5094///
5095/// Keeps a pack index cursor so probes in non-decreasing OID order avoid repeated binary search.
5096pub struct SingleLocalPackReader {
5097    idx: Arc<PackIndex>,
5098    pack_bytes: Arc<PackData>,
5099    pack_id: u32,
5100    cursor: usize,
5101    info_cache: HashMap<u64, ObjectInfo>,
5102    /// Created on first header/size probe; full object reads never touch it, so opening
5103    /// a reader per [`crate::odb::Odb::read`] must not pay for an inflater.
5104    delta_scratch: Option<ZlibInflateScratch>,
5105    commit_header_buf: Vec<u8>,
5106}
5107
5108pub(crate) fn objects_dir_has_loose_objects(objects_dir: &Path) -> bool {
5109    let Ok(entries) = std::fs::read_dir(objects_dir) else {
5110        return false;
5111    };
5112    for entry in entries.flatten() {
5113        let name = entry.file_name();
5114        let name = name.to_string_lossy();
5115        if name.len() != 2 || !name.bytes().all(|b| b.is_ascii_hexdigit()) {
5116            continue;
5117        }
5118        let Ok(hex_entries) = std::fs::read_dir(entry.path()) else {
5119            continue;
5120        };
5121        if hex_entries.flatten().any(|e| e.path().is_file()) {
5122            return true;
5123        }
5124    }
5125    false
5126}
5127
5128impl SingleLocalPackReader {
5129    /// Open when [`crate::pack_store::PackStore`] read context matches and one local pack index exists.
5130    ///
5131    /// # Errors
5132    ///
5133    /// Propagates pack directory read failures from index loading.
5134    pub fn open(objects_dir: &Path) -> Result<Option<Self>> {
5135        Self::open_with_options(objects_dir, false, None)
5136    }
5137
5138    /// Open when one local pack exists, optionally ignoring loose objects (commit parent walks).
5139    pub fn open_allow_loose(objects_dir: &Path) -> Result<Option<Self>> {
5140        Self::open_with_options(objects_dir, true, None)
5141    }
5142
5143    /// Like [`Self::open_allow_loose`], but when several packs exist (e.g. promisor blob
5144    /// packs from partial clone), select the pack that contains `commit_hint`.
5145    pub(crate) fn open_allow_loose_for_commits(
5146        objects_dir: &Path,
5147        commit_hint: &crate::objects::ObjectId,
5148    ) -> Result<Option<Self>> {
5149        Self::open_with_options(objects_dir, true, Some(commit_hint))
5150    }
5151
5152    /// Open a reader bound to a specific on-disk pack index (bitmap / pack-order walks).
5153    pub(crate) fn open_for_pack_index(
5154        objects_dir: &Path,
5155        idx: Arc<PackIndex>,
5156    ) -> Result<Option<Self>> {
5157        if !crate::pack_store::PackStore::read_context_matches_objects_dir(objects_dir) {
5158            return Ok(None);
5159        }
5160        pack_cache::sync_thread_local_with_store(&crate::pack_store::PackStore::active_for_reads());
5161        let pack_bytes = read_pack_bytes_cached(&idx.pack_path)?;
5162        validate_pack_index_object_count(pack_bytes.as_ref(), idx.as_ref())?;
5163        pack_cache::mark_pack_object_count_validated(&idx.pack_path);
5164        pack_cache::remember_pack_hit(objects_dir, &idx);
5165        let pack_id = pack_cache::pack_id_for(&idx.pack_path);
5166        Ok(Some(Self {
5167            idx,
5168            pack_bytes,
5169            pack_id,
5170            cursor: 0,
5171            info_cache: HashMap::new(),
5172            delta_scratch: None,
5173            commit_header_buf: Vec::new(),
5174        }))
5175    }
5176
5177    /// Object kind of every entry starting at `offsets` (ascending), from entry headers alone.
5178    ///
5179    /// A delta takes the kind of its base, so nothing is inflated. The result is `None` for a
5180    /// delta whose base is not an earlier entry of `offsets` (a REF_DELTA base stored later in
5181    /// the pack or outside it), and for deltas on such entries; callers resolve those another way.
5182    ///
5183    /// # Errors
5184    ///
5185    /// [`Error::CorruptObject`] when an offset lies outside the pack or an entry header is
5186    /// malformed.
5187    pub(crate) fn object_kinds_at_sorted_offsets(
5188        &self,
5189        offsets: &[u64],
5190    ) -> Result<Vec<Option<ObjectKind>>> {
5191        let pack_bytes = self.pack_bytes.as_ref();
5192        let hash_bytes = self.idx.hash_bytes();
5193        let mut kinds: Vec<Option<ObjectKind>> = Vec::with_capacity(offsets.len());
5194        for (rank, &offset) in offsets.iter().enumerate() {
5195            let mut pos = usize::try_from(offset)
5196                .map_err(|_| Error::CorruptObject("pack offset out of range".to_owned()))?;
5197            let (packed_type, _) = parse_pack_object_header(pack_bytes, &mut pos)?;
5198            let base_offset = match packed_type {
5199                PackedType::OfsDelta => Some(parse_ofs_delta_base(pack_bytes, &mut pos, offset)?),
5200                PackedType::RefDelta => {
5201                    let raw = pack_bytes.get(pos..pos + hash_bytes).ok_or_else(|| {
5202                        Error::CorruptObject("truncated ref-delta base OID".to_owned())
5203                    })?;
5204                    self.idx.find_offset(&ObjectId::from_bytes(raw)?)?
5205                }
5206                _ => {
5207                    kinds.push(Some(packed_type_to_kind(packed_type)?));
5208                    continue;
5209                }
5210            };
5211            let base_kind = base_offset
5212                .and_then(|base| offsets.binary_search(&base).ok())
5213                .filter(|&base_rank| base_rank < rank)
5214                .and_then(|base_rank| kinds[base_rank]);
5215            kinds.push(base_kind);
5216        }
5217        Ok(kinds)
5218    }
5219
5220    /// Whether `oid` is indexed in this reader's pack.
5221    pub(crate) fn contains_oid(&self, oid: &crate::objects::ObjectId) -> Result<bool> {
5222        Ok(self.idx.find_offset(oid)?.is_some())
5223    }
5224
5225    fn open_with_options(
5226        objects_dir: &Path,
5227        allow_loose: bool,
5228        commit_hint: Option<&crate::objects::ObjectId>,
5229    ) -> Result<Option<Self>> {
5230        if !pack_cache::pack_thread_locals_apply_to(objects_dir) {
5231            return Ok(None);
5232        }
5233        pack_cache::sync_thread_local_for_objects_dir(objects_dir);
5234        let memo_eligible = allow_loose && commit_hint.is_none();
5235        if memo_eligible {
5236            if let Some((idx, pack_bytes, pack_id)) = pack_cache::single_reader_for(objects_dir) {
5237                return Ok(Some(Self {
5238                    idx,
5239                    pack_bytes,
5240                    pack_id,
5241                    cursor: 0,
5242                    info_cache: HashMap::new(),
5243                    delta_scratch: None,
5244                    commit_header_buf: Vec::new(),
5245                }));
5246            }
5247        }
5248        let indexes = pack_indexes_for_lookup(objects_dir, PackLookupOptions::ALL_LOCAL_PACKS)?;
5249        let idx = match select_single_local_pack_index(&indexes, commit_hint)? {
5250            Some(idx) => idx,
5251            None => return Ok(None),
5252        };
5253        if !allow_loose && objects_dir_has_loose_objects(objects_dir) {
5254            return Ok(None);
5255        }
5256        let pack_bytes = read_pack_bytes_cached(&idx.pack_path)?;
5257        validate_pack_index_object_count(pack_bytes.as_ref(), idx.as_ref())?;
5258        pack_cache::mark_pack_object_count_validated(&idx.pack_path);
5259        pack_cache::remember_pack_hit(objects_dir, &idx);
5260        let pack_id = pack_cache::pack_id_for(&idx.pack_path);
5261        if memo_eligible && indexes.len() == 1 {
5262            pack_cache::remember_single_reader(objects_dir, &idx, &pack_bytes, pack_id);
5263        }
5264        Ok(Some(Self {
5265            idx,
5266            pack_bytes,
5267            pack_id,
5268            cursor: 0,
5269            info_cache: HashMap::new(),
5270            delta_scratch: None,
5271            commit_header_buf: Vec::new(),
5272        }))
5273    }
5274
5275    /// Inflate the start of commit `oid` into an internal buffer.
5276    ///
5277    /// Resolves deltified pack entries via the delta-base cache and prefix delta apply.
5278    /// Returns `None` when the object is missing, not a commit, or cannot be resolved.
5279    ///
5280    /// # Errors
5281    ///
5282    /// Propagates pack index and zlib errors.
5283    pub fn read_commit_author_prefix(&mut self, oid: &ObjectId) -> Result<Option<&[u8]>> {
5284        self.read_commit_author_prefix_at(oid, true)
5285    }
5286
5287    /// Like [`Self::read_commit_author_prefix`], but pack index probes may be out of OID order.
5288    pub(crate) fn read_commit_author_prefix_unordered(
5289        &mut self,
5290        oid: &ObjectId,
5291    ) -> Result<Option<&[u8]>> {
5292        self.read_commit_author_prefix_at(oid, false)
5293    }
5294
5295    /// Inflate the start of the commit at pack index row `row` (pack-order / bitmap walks).
5296    pub(crate) fn read_commit_author_prefix_at_pack_row(
5297        &mut self,
5298        row: usize,
5299    ) -> Result<Option<&[u8]>> {
5300        self.cursor = row;
5301        let offset = self.idx.offset_at(row)?;
5302        self.read_commit_author_prefix_at_pack_offset(offset)
5303    }
5304
5305    fn read_commit_author_prefix_at(
5306        &mut self,
5307        oid: &ObjectId,
5308        monotonic: bool,
5309    ) -> Result<Option<&[u8]>> {
5310        let offset = if monotonic {
5311            self.idx.find_offset_monotonic(oid, &mut self.cursor)?
5312        } else {
5313            self.idx.find_offset(oid)?
5314        };
5315        let Some(offset) = offset else {
5316            return Ok(None);
5317        };
5318        self.read_commit_author_prefix_at_pack_offset(offset)
5319    }
5320
5321    fn read_commit_author_prefix_at_pack_offset(&mut self, offset: u64) -> Result<Option<&[u8]>> {
5322        self.commit_header_buf.clear();
5323        let filled = with_pack_read_delta_state(0, |state| {
5324            self.resolve_commit_author_prefix_at_pack_offset(offset, state)
5325        })?;
5326        if !filled || self.commit_header_buf.is_empty() {
5327            return Ok(None);
5328        }
5329        Ok(Some(&self.commit_header_buf))
5330    }
5331
5332    fn resolve_commit_author_prefix_at_pack_offset(
5333        &mut self,
5334        start_offset: u64,
5335        state: &mut DeltaChainState,
5336    ) -> Result<bool> {
5337        let pack_bytes = self.pack_bytes.as_ref();
5338        let mut cur_offset = start_offset;
5339        let result_offset = start_offset;
5340        let result_pack_id = self.pack_id;
5341        state.pending.clear();
5342
5343        let mut compressed = Vec::new();
5344        loop {
5345            state.visit(self.pack_id, cur_offset)?;
5346            let (bytes, mut pos) =
5347                pack_entry_bytes(&self.idx, cur_offset, pack_bytes, &mut compressed)?;
5348            let object_start = cur_offset;
5349            let (packed_type, size) = match parse_pack_object_header(bytes, &mut pos) {
5350                Ok(v) => v,
5351                Err(_) => return Ok(false),
5352            };
5353            match packed_type {
5354                PackedType::Commit => {
5355                    let kind = ObjectKind::Commit;
5356                    if state.pending.is_empty() {
5357                        self.delta_scratch
5358                            .get_or_insert_with(ZlibInflateScratch::default)
5359                            .decompress_prefix_into(
5360                                &bytes[pos..],
5361                                COMMIT_HEADER_READ_PREFIX,
5362                                &mut self.commit_header_buf,
5363                            )?;
5364                        return Ok(!self.commit_header_buf.is_empty());
5365                    }
5366                    if let Some((base_kind, base_data)) =
5367                        pack_cache::get_delta_base(self.pack_id, cur_offset)
5368                    {
5369                        if base_kind != ObjectKind::Commit {
5370                            return Ok(false);
5371                        }
5372                        finish_pack_read_prefix_with_pending(
5373                            &mut self.commit_header_buf,
5374                            kind,
5375                            base_data.as_ref(),
5376                            None,
5377                            result_pack_id,
5378                            result_offset,
5379                            true,
5380                            state,
5381                        )?;
5382                        return Ok(commit_prefix_has_author(&self.commit_header_buf));
5383                    }
5384                    state.whole_object.clear();
5385                    decompress_pack_data_into(
5386                        bytes,
5387                        &mut pos,
5388                        size,
5389                        &mut state.inflate,
5390                        &mut state.whole_object,
5391                    )?;
5392                    let whole = std::mem::take(&mut state.whole_object);
5393                    finish_pack_read_prefix_with_pending(
5394                        &mut self.commit_header_buf,
5395                        kind,
5396                        &whole,
5397                        Some((self.pack_id, cur_offset)),
5398                        result_pack_id,
5399                        result_offset,
5400                        true,
5401                        state,
5402                    )?;
5403                    return Ok(commit_prefix_has_author(&self.commit_header_buf));
5404                }
5405                PackedType::Tree | PackedType::Blob | PackedType::Tag => return Ok(false),
5406                PackedType::OfsDelta => {
5407                    state.next_hop()?;
5408                    let base_offset = parse_ofs_delta_base(bytes, &mut pos, object_start)?;
5409                    state.delta_decompress.clear();
5410                    decompress_pack_data_into(
5411                        bytes,
5412                        &mut pos,
5413                        size,
5414                        &mut state.inflate,
5415                        &mut state.delta_decompress,
5416                    )?;
5417                    state.pending.push(PendingDeltaFrame {
5418                        pack_id: self.pack_id,
5419                        offset: object_start,
5420                        delta: std::mem::take(&mut state.delta_decompress),
5421                    });
5422                    if let Some((base_kind, base_data)) =
5423                        pack_cache::get_delta_base(self.pack_id, base_offset)
5424                    {
5425                        if base_kind != ObjectKind::Commit {
5426                            return Ok(false);
5427                        }
5428                        finish_pack_read_prefix_with_pending(
5429                            &mut self.commit_header_buf,
5430                            base_kind,
5431                            base_data.as_ref(),
5432                            None,
5433                            result_pack_id,
5434                            result_offset,
5435                            true,
5436                            state,
5437                        )?;
5438                        return Ok(commit_prefix_has_author(&self.commit_header_buf));
5439                    }
5440                    cur_offset = base_offset;
5441                }
5442                PackedType::RefDelta => {
5443                    state.next_hop()?;
5444                    let hb = self.idx.hash_bytes();
5445                    if pos + hb > bytes.len() {
5446                        return Ok(false);
5447                    }
5448                    let base_raw = &bytes[pos..pos + hb];
5449                    pos += hb;
5450                    state.delta_decompress.clear();
5451                    decompress_pack_data_into(
5452                        bytes,
5453                        &mut pos,
5454                        size,
5455                        &mut state.inflate,
5456                        &mut state.delta_decompress,
5457                    )?;
5458                    state.pending.push(PendingDeltaFrame {
5459                        pack_id: self.pack_id,
5460                        offset: object_start,
5461                        delta: std::mem::take(&mut state.delta_decompress),
5462                    });
5463                    let base_oid = ObjectId::from_bytes(base_raw)?;
5464                    let Some(base_offset) = self.idx.find_offset(&base_oid)? else {
5465                        return Ok(false);
5466                    };
5467                    if let Some((base_kind, base_data)) =
5468                        pack_cache::get_delta_base(self.pack_id, base_offset)
5469                    {
5470                        if base_kind != ObjectKind::Commit {
5471                            return Ok(false);
5472                        }
5473                        finish_pack_read_prefix_with_pending(
5474                            &mut self.commit_header_buf,
5475                            base_kind,
5476                            base_data.as_ref(),
5477                            None,
5478                            result_pack_id,
5479                            result_offset,
5480                            true,
5481                            state,
5482                        )?;
5483                        return Ok(commit_prefix_has_author(&self.commit_header_buf));
5484                    }
5485                    cur_offset = base_offset;
5486                }
5487            }
5488        }
5489    }
5490
5491    /// Drop any cached commit header prefix (required before delta / ODB fallbacks).
5492    pub(crate) fn clear_commit_header_prefix(&mut self) {
5493        self.commit_header_buf.clear();
5494    }
5495
5496    /// Prefix from the last successful [`Self::read_commit_author_prefix`] call.
5497    pub(crate) fn commit_header_prefix(&self) -> &[u8] {
5498        &self.commit_header_buf
5499    }
5500
5501    fn read_info_at_pack_offset(&mut self, start_offset: u64) -> Result<ObjectInfo> {
5502        if let Some(info) = self.info_cache.get(&start_offset) {
5503            return Ok(*info);
5504        }
5505        let pack_path = self.idx.pack_path.clone();
5506        let info = with_pack_read_delta_state(0, |state| {
5507            self.resolve_info_at_pack_offset_body(start_offset, state)
5508        })?;
5509        self.info_cache.insert(start_offset, info);
5510        remember_pack_read_info(&pack_path, start_offset, info);
5511        Ok(info)
5512    }
5513
5514    fn resolve_info_at_pack_offset_body(
5515        &mut self,
5516        start_offset: u64,
5517        state: &mut DeltaChainState,
5518    ) -> Result<ObjectInfo> {
5519        let pack_bytes = self.pack_bytes.as_ref();
5520        let mut cur_offset = start_offset;
5521        let mut result_size: Option<u64> = None;
5522        loop {
5523            state.visit(self.pack_id, cur_offset)?;
5524            if let Some(base) = self.info_cache.get(&cur_offset) {
5525                if let Some(size) = result_size {
5526                    return Ok(ObjectInfo {
5527                        kind: base.kind,
5528                        size,
5529                    });
5530                }
5531                return Ok(*base);
5532            }
5533
5534            let object_start = cur_offset;
5535            let mut pos = cur_offset as usize;
5536            let (packed_type, header_size) = parse_pack_object_header(pack_bytes, &mut pos)?;
5537            match packed_type {
5538                PackedType::Commit | PackedType::Tree | PackedType::Blob | PackedType::Tag => {
5539                    let kind = packed_type_to_kind(packed_type)?;
5540                    let undeltified = ObjectInfo {
5541                        kind,
5542                        size: header_size,
5543                    };
5544                    self.info_cache.insert(cur_offset, undeltified);
5545                    let size = result_size.unwrap_or(header_size);
5546                    return Ok(ObjectInfo { kind, size });
5547                }
5548                PackedType::OfsDelta => {
5549                    state.next_hop()?;
5550                    let base_offset = parse_ofs_delta_base(pack_bytes, &mut pos, object_start)?;
5551                    if result_size.is_none() {
5552                        result_size = Some(
5553                            self.delta_scratch
5554                                .get_or_insert_with(ZlibInflateScratch::default)
5555                                .read_pack_delta_result_size(pack_bytes, &mut pos)?,
5556                        );
5557                    }
5558                    cur_offset = base_offset;
5559                }
5560                PackedType::RefDelta => {
5561                    state.next_hop()?;
5562                    let hb = self.idx.hash_bytes();
5563                    if pos + hb > pack_bytes.len() {
5564                        return Err(Error::CorruptObject(
5565                            "truncated ref-delta base OID".to_owned(),
5566                        ));
5567                    }
5568                    let base_raw = &pack_bytes[pos..pos + hb];
5569                    pos += hb;
5570                    if result_size.is_none() {
5571                        result_size = Some(
5572                            self.delta_scratch
5573                                .get_or_insert_with(ZlibInflateScratch::default)
5574                                .read_pack_delta_result_size(pack_bytes, &mut pos)?,
5575                        );
5576                    }
5577                    let base_oid = ObjectId::from_bytes(base_raw)?;
5578                    let Some(base_offset) = self.idx.find_offset(&base_oid)? else {
5579                        return Err(Error::ObjectNotFound(base_oid.to_hex()));
5580                    };
5581                    cur_offset = base_offset;
5582                }
5583            }
5584        }
5585    }
5586
5587    /// Kind and size for `oid` without loading the object body.
5588    ///
5589    /// # Errors
5590    ///
5591    /// Same as [`read_object_info_from_pack_at_offset`].
5592    pub fn read_info(&mut self, oid: &ObjectId) -> Result<ObjectInfo> {
5593        let Some(offset) = self.idx.find_offset_monotonic(oid, &mut self.cursor)? else {
5594            return Err(Error::ObjectNotFound(oid.to_hex()));
5595        };
5596        if let Some(info) =
5597            read_whole_pack_entry_info_fast(&self.idx, offset, self.pack_bytes.as_ref())
5598        {
5599            self.info_cache.insert(offset, info);
5600            return Ok(info);
5601        }
5602        self.read_info_at_pack_offset(offset)
5603    }
5604
5605    /// Decompress the object for `oid`.
5606    ///
5607    /// # Errors
5608    ///
5609    /// Same as [`read_object_from_pack_at_offset`].
5610    pub fn read_object(&mut self, oid: &ObjectId) -> Result<Object> {
5611        let Some(offset) = self.idx.find_offset_monotonic(oid, &mut self.cursor)? else {
5612            return Err(Error::ObjectNotFound(oid.to_hex()));
5613        };
5614        if !self.pack_bytes.uses_positioned_reads() {
5615            if let Some(obj) =
5616                read_whole_pack_entry_object_fast(&self.idx, offset, &self.pack_bytes)
5617            {
5618                return Ok(obj);
5619            }
5620        }
5621        read_object_from_pack_at_offset(&self.idx, offset)
5622    }
5623
5624    /// Commit `oid`'s headers and message through its first non-blank line, inflating no
5625    /// further than that, when this pack stores the commit whole.
5626    ///
5627    /// The bytes are a prefix of the commit payload as described by
5628    /// [`crate::objects::commit_through_subject_len`], or the whole payload when it has no such
5629    /// prefix.
5630    ///
5631    /// # Returns
5632    ///
5633    /// `None` when the pack does not hold `oid` as a whole commit entry (absent, a delta, or
5634    /// another kind) or the entry does not inflate cleanly; read it the ordinary way then,
5635    /// which also reports any corruption.
5636    ///
5637    /// # Errors
5638    ///
5639    /// Propagates pack index lookup errors.
5640    pub(crate) fn read_commit_through_subject(
5641        &mut self,
5642        oid: &ObjectId,
5643    ) -> Result<Option<Vec<u8>>> {
5644        let Some(offset) = self.idx.find_offset_monotonic(oid, &mut self.cursor)? else {
5645            return Ok(None);
5646        };
5647        let mut compressed = Vec::new();
5648        let Ok((pack_bytes, mut pos)) =
5649            pack_entry_bytes(&self.idx, offset, &self.pack_bytes, &mut compressed)
5650        else {
5651            return Ok(None);
5652        };
5653        let Ok((PackedType::Commit, size)) = parse_pack_object_header(pack_bytes, &mut pos) else {
5654            return Ok(None);
5655        };
5656        let Some(input) = pack_bytes.get(pos..) else {
5657            return Ok(None);
5658        };
5659        let mut out = Vec::new();
5660        let inflated = WHOLE_OBJECT_ZLIB.with(|slot| {
5661            slot.borrow_mut().decompress_prefix_until(
5662                input,
5663                size,
5664                COMMIT_SUBJECT_READ_STEP,
5665                &mut out,
5666                crate::objects::commit_through_subject_len,
5667            )
5668        });
5669        Ok(inflated.is_ok().then_some(out))
5670    }
5671
5672    /// Parents of commit `oid` when this pack stores the commit whole.
5673    ///
5674    /// The entry is inflated in full into a buffer the reader reuses, so its declared size and
5675    /// zlib checksum are verified exactly as for an ordinary read, and only the `tree` and
5676    /// `parent` headers are parsed (see
5677    /// [`crate::objects::parse_commit_tree_and_parents_lenient`]). Nothing is allocated per
5678    /// commit beyond the returned parents.
5679    ///
5680    /// # Parameters
5681    ///
5682    /// - `oid` - the commit; lookups may come in any object id order.
5683    /// - `hex_len` - hex length of object ids in this repository's hash.
5684    ///
5685    /// # Returns
5686    ///
5687    /// The parents in header order, or `None` when the pack does not hold `oid` as a whole
5688    /// commit entry (absent, a delta, or another kind), declares more than
5689    /// [`COMMIT_PARENTS_WHOLE_READ_MAX`] bytes, does not inflate cleanly, or its headers do
5690    /// not parse; read the whole object then, which reports any corruption.
5691    ///
5692    /// # Errors
5693    ///
5694    /// Propagates pack index lookup errors.
5695    pub(crate) fn read_commit_parents(
5696        &mut self,
5697        oid: &ObjectId,
5698        hex_len: usize,
5699    ) -> Result<Option<Vec<ObjectId>>> {
5700        let Some(offset) = self.idx.find_offset(oid)? else {
5701            return Ok(None);
5702        };
5703        let mut compressed = Vec::new();
5704        let Ok((pack_bytes, mut pos)) =
5705            pack_entry_bytes(&self.idx, offset, &self.pack_bytes, &mut compressed)
5706        else {
5707            return Ok(None);
5708        };
5709        let Ok((PackedType::Commit, size)) = parse_pack_object_header(pack_bytes, &mut pos) else {
5710            return Ok(None);
5711        };
5712        if size > COMMIT_PARENTS_WHOLE_READ_MAX {
5713            return Ok(None);
5714        }
5715        let scratch = self
5716            .delta_scratch
5717            .get_or_insert_with(ZlibInflateScratch::default);
5718        if decompress_pack_data_into(
5719            pack_bytes,
5720            &mut pos,
5721            size,
5722            scratch,
5723            &mut self.commit_header_buf,
5724        )
5725        .is_err()
5726        {
5727            return Ok(None);
5728        }
5729        Ok(
5730            crate::objects::parse_commit_tree_and_parents_lenient(&self.commit_header_buf, hex_len)
5731                .map(|(_, parents)| parents),
5732        )
5733    }
5734
5735    /// Like [`Self::read_object`], but skip an object the pack stores whole with a payload
5736    /// larger than `limit`, returning [`BoundedRead::Larger`] without inflating it.
5737    ///
5738    /// Deltas are always read: reconstructing one needs the whole object in memory anyway.
5739    ///
5740    /// # Errors
5741    ///
5742    /// Same as [`Self::read_object`].
5743    pub(crate) fn read_object_unless_whole_larger(
5744        &mut self,
5745        oid: &ObjectId,
5746        limit: u64,
5747    ) -> Result<BoundedRead> {
5748        let Some(offset) = self.idx.find_offset_monotonic(oid, &mut self.cursor)? else {
5749            return Err(Error::ObjectNotFound(oid.to_hex()));
5750        };
5751        let mut pos = usize::try_from(offset)
5752            .map_err(|_| Error::CorruptObject(format!("pack offset {offset} overflows")))?;
5753        if let Ok((packed_type, size)) = parse_pack_object_header(&self.pack_bytes, &mut pos) {
5754            let whole = !matches!(packed_type, PackedType::OfsDelta | PackedType::RefDelta);
5755            if whole && size > limit {
5756                return Ok(BoundedRead::Larger);
5757            }
5758        }
5759        if let Some(obj) =
5760            read_whole_pack_entry_object_fast(&self.idx, offset, self.pack_bytes.as_ref())
5761        {
5762            return Ok(BoundedRead::Object(obj));
5763        }
5764        read_object_from_pack_at_offset(&self.idx, offset).map(BoundedRead::Object)
5765    }
5766
5767    /// Like [`Self::read_object`], inflating into `out` and preserving its capacity when possible.
5768    ///
5769    /// Delta bases resolved along the way are not added to the delta-base cache.
5770    pub fn read_object_into(&mut self, oid: &ObjectId, out: &mut Vec<u8>) -> Result<ObjectKind> {
5771        self.read_object_into_with_delta_policy(oid, false, out)
5772    }
5773
5774    /// [`Self::read_object_into`], keeping resolved delta bases in the delta-base cache when
5775    /// `populate_delta_lru` is set.
5776    pub(crate) fn read_object_into_with_delta_policy(
5777        &mut self,
5778        oid: &ObjectId,
5779        populate_delta_lru: bool,
5780        out: &mut Vec<u8>,
5781    ) -> Result<ObjectKind> {
5782        let Some(offset) = self.idx.find_offset_monotonic(oid, &mut self.cursor)? else {
5783            return Err(Error::ObjectNotFound(oid.to_hex()));
5784        };
5785        if !self.pack_bytes.uses_positioned_reads() {
5786            if let Some(kind) =
5787                read_whole_pack_entry_object_into(&self.idx, offset, &self.pack_bytes, out)
5788            {
5789                return Ok(kind);
5790            }
5791        }
5792        read_object_at_depth_into(&self.idx, offset, 0, populate_delta_lru, out)
5793    }
5794
5795    /// Pack file offset for `oid` in this reader's index, if present.
5796    ///
5797    /// # Errors
5798    ///
5799    /// Propagates pack index lookup errors.
5800    pub fn pack_offset(&self, oid: &ObjectId) -> Result<Option<u64>> {
5801        self.idx.find_offset(oid)
5802    }
5803}
5804fn pack_indexes_for_lookup(
5805    objects_dir: &Path,
5806    opts: PackLookupOptions,
5807) -> Result<Vec<Arc<PackIndex>>> {
5808    let indexes = read_local_pack_indexes_cached(objects_dir)?;
5809    if !opts.skip_midx_covered_packs && !opts.only_midx_covered_packs {
5810        return Ok(indexes);
5811    }
5812    let covered = pack_cache::midx_covered_pack_names(objects_dir);
5813    if covered.is_empty() {
5814        return if opts.only_midx_covered_packs {
5815            Ok(Vec::new())
5816        } else {
5817            Ok(indexes)
5818        };
5819    }
5820    Ok(indexes
5821        .into_iter()
5822        .filter(|idx| {
5823            idx.idx_path
5824                .file_name()
5825                .and_then(|n| n.to_str())
5826                .map(|name| {
5827                    if opts.only_midx_covered_packs {
5828                        covered.contains(name)
5829                    } else {
5830                        !covered.contains(name)
5831                    }
5832                })
5833                .unwrap_or(!opts.only_midx_covered_packs)
5834        })
5835        .collect())
5836}
5837
5838/// Search all pack indexes in `objects_dir` for the given OID and read it.
5839///
5840/// When more than one pack contains `oid` (a redundant copy), a read failure in
5841/// one pack — e.g. a corrupted delta base or zlib stream — is not fatal: Git
5842/// retries the remaining sources before giving up, so an intact redundant pack
5843/// still satisfies the read (t5303 pack-corruption-resilience). Only when every
5844/// pack that names `oid` fails to produce it do we surface the last error.
5845///
5846/// # Errors
5847///
5848/// Returns [`Error::ObjectNotFound`] if no pack contains the OID.
5849pub fn read_object_from_packs(objects_dir: &Path, oid: &ObjectId) -> Result<Object> {
5850    if crate::pack_store::PackStore::has_context() {
5851        return read_object_from_packs_in_context(objects_dir, oid);
5852    }
5853    let store = crate::pack_store::PackStore::legacy_for_objects_dir(objects_dir);
5854    crate::pack_store::PackStore::with_context(store, || {
5855        read_object_from_packs_in_context(objects_dir, oid)
5856    })
5857}
5858
5859/// Like [`read_object_from_packs`], inflating into `buf` and reusing its capacity.
5860///
5861/// # Errors
5862///
5863/// Same as [`read_object_from_packs`].
5864pub fn read_object_from_packs_into(
5865    objects_dir: &Path,
5866    oid: &ObjectId,
5867    buf: &mut Vec<u8>,
5868) -> Result<ObjectKind> {
5869    if crate::pack_store::PackStore::has_context() {
5870        return read_object_from_packs_in_context_into(objects_dir, oid, buf);
5871    }
5872    let store = crate::pack_store::PackStore::legacy_for_objects_dir(objects_dir);
5873    crate::pack_store::PackStore::with_context(store, || {
5874        read_object_from_packs_in_context_into(objects_dir, oid, buf)
5875    })
5876}
5877
5878/// Search local packs for `oid` and return its kind and uncompressed size.
5879///
5880/// # Errors
5881///
5882/// Returns [`Error::ObjectNotFound`] when no pack lists `oid`.
5883pub fn read_object_info_from_packs(objects_dir: &Path, oid: &ObjectId) -> Result<ObjectInfo> {
5884    match try_read_object_info_from_packs_with_options(
5885        objects_dir,
5886        oid,
5887        PackLookupOptions::ALL_LOCAL_PACKS,
5888    ) {
5889        Ok(info) => Ok(info),
5890        Err(err @ Error::ObjectNotFound(_)) => {
5891            if reprepare_pack_directory_on_miss(objects_dir)? {
5892                try_read_object_info_from_packs_with_options(
5893                    objects_dir,
5894                    oid,
5895                    PackLookupOptions::ALL_LOCAL_PACKS,
5896                )
5897            } else {
5898                Err(err)
5899            }
5900        }
5901        Err(err) => Err(err),
5902    }
5903}
5904
5905/// Search local packs for `oid` without re-preparing the pack directory listing.
5906pub(crate) fn try_read_object_info_from_packs_with_options(
5907    objects_dir: &Path,
5908    oid: &ObjectId,
5909    opts: PackLookupOptions,
5910) -> Result<ObjectInfo> {
5911    pack_cache::sync_thread_local_for_objects_dir(objects_dir);
5912    if opts.allows_pack_shortcuts() {
5913        if let Some(info) = pack_cache::try_read_info_from_hinted_pack(objects_dir, oid)? {
5914            return Ok(info);
5915        }
5916    }
5917    let indexes = pack_indexes_for_lookup(objects_dir, opts)?;
5918    let mut last_err: Option<Error> = None;
5919    for idx in &indexes {
5920        let Some(offset) = idx.find_offset(oid)? else {
5921            continue;
5922        };
5923        match read_object_info_from_pack_at_offset(idx, offset) {
5924            Ok(info) => {
5925                pack_cache::remember_pack_hit(objects_dir, idx);
5926                return Ok(info);
5927            }
5928            Err(Error::ObjectNotFound(_)) => {}
5929            Err(err) => last_err = Some(err),
5930        }
5931    }
5932    Err(last_err.unwrap_or_else(|| Error::ObjectNotFound(oid.to_hex())))
5933}
5934
5935/// Search local packs for `oid` without re-preparing the pack directory listing.
5936pub(crate) fn try_read_object_from_packs_with_options(
5937    objects_dir: &Path,
5938    oid: &ObjectId,
5939    opts: PackLookupOptions,
5940) -> Result<Object> {
5941    pack_cache::sync_thread_local_for_objects_dir(objects_dir);
5942    if opts.allows_pack_shortcuts() {
5943        if let Some(obj) = pack_cache::try_read_object_via_single_pack_reader(objects_dir, oid)? {
5944            return Ok(obj);
5945        }
5946        if let Some(obj) = pack_cache::try_read_from_hinted_pack(objects_dir, oid)? {
5947            return Ok(obj);
5948        }
5949    }
5950    let indexes = pack_indexes_for_lookup(objects_dir, opts)?;
5951    let mut last_damage: Option<PackReadDamage> = None;
5952    let mut last_err: Option<Error> = None;
5953    for idx in &indexes {
5954        let Some(offset) = pack_cache::find_offset_in_pack_read_context(objects_dir, idx, oid)?
5955        else {
5956            continue;
5957        };
5958        match read_object_from_pack_at_offset(idx, offset) {
5959            Ok(obj) => {
5960                pack_cache::remember_pack_hit(objects_dir, idx);
5961                return Ok(obj);
5962            }
5963            // The object is missing from this particular pack despite the index
5964            // claim — keep looking in the others.
5965            Err(Error::ObjectNotFound(_)) => {}
5966            // The pack copy is unreadable (corrupt delta/zlib/header). A redundant
5967            // pack may still hold an intact copy, so remember the error and retry.
5968            Err(err) if err.is_object_read_wrap_source() => {
5969                last_damage = Some(PackReadDamage {
5970                    pack: idx.pack_path.clone(),
5971                    offset,
5972                    err,
5973                });
5974            }
5975            Err(err) => last_err = Some(err),
5976        }
5977    }
5978    if let Some(err) = last_err {
5979        return Err(err);
5980    }
5981    Err(finish_pack_object_lookup(oid, last_damage))
5982}
5983
5984/// Like [`try_read_object_from_packs_with_options`], writing the payload into `buf`.
5985///
5986/// `populate_delta_lru` controls whether delta bases resolved on the way are kept in the
5987/// shared delta-base cache (as [`try_read_object_from_packs_with_options`] always does).
5988pub(crate) fn try_read_object_from_packs_with_options_into(
5989    objects_dir: &Path,
5990    oid: &ObjectId,
5991    opts: PackLookupOptions,
5992    populate_delta_lru: bool,
5993    buf: &mut Vec<u8>,
5994) -> Result<ObjectKind> {
5995    pack_cache::sync_thread_local_for_objects_dir(objects_dir);
5996    if opts.allows_pack_shortcuts() {
5997        if let Some(kind) = pack_cache::try_read_object_via_single_pack_reader_into(
5998            objects_dir,
5999            oid,
6000            populate_delta_lru,
6001            buf,
6002        )? {
6003            return Ok(kind);
6004        }
6005        if let Some(kind) =
6006            pack_cache::try_read_from_hinted_pack_into(objects_dir, oid, populate_delta_lru, buf)?
6007        {
6008            return Ok(kind);
6009        }
6010    }
6011    let indexes = pack_indexes_for_lookup(objects_dir, opts)?;
6012    let mut last_damage: Option<PackReadDamage> = None;
6013    let mut last_err: Option<Error> = None;
6014    for idx in &indexes {
6015        let Some(offset) = pack_cache::find_offset_in_pack_read_context(objects_dir, idx, oid)?
6016        else {
6017            continue;
6018        };
6019        match read_object_at_depth_into(idx, offset, 0, populate_delta_lru, buf) {
6020            Ok(kind) => {
6021                pack_cache::remember_pack_hit(objects_dir, idx);
6022                return Ok(kind);
6023            }
6024            Err(Error::ObjectNotFound(_)) => {}
6025            Err(err) if err.is_object_read_wrap_source() => {
6026                last_damage = Some(PackReadDamage {
6027                    pack: idx.pack_path.clone(),
6028                    offset,
6029                    err,
6030                });
6031            }
6032            Err(err) => last_err = Some(err),
6033        }
6034    }
6035    if let Some(err) = last_err {
6036        return Err(err);
6037    }
6038    Err(finish_pack_object_lookup(oid, last_damage))
6039}
6040
6041/// Whether `idx_path` uses the legacy v1 pack-index format (no `PACK` signature).
6042#[must_use]
6043pub fn pack_index_is_v1(idx_path: &Path) -> bool {
6044    fs::read(idx_path)
6045        .ok()
6046        .is_some_and(|bytes| bytes.len() >= 4 && bytes[0..4] != [0xff, b't', b'O', b'c'])
6047}
6048
6049/// Resolve `oid` from local packs via the process-wide pack cache (cached `.idx`
6050/// parses, cached pack bytes, fanout binary search, delta-base cache).
6051///
6052/// Returns `Ok(None)` when no local pack index names the object. When a pack
6053/// copy fails to decode from a v1 (legacy) index, returns an empty blob placeholder
6054/// (historical pack-objects behavior). Any other decode failure invokes
6055/// `alternate_read` once before trying the next pack.
6056///
6057/// # Errors
6058///
6059/// Returns [`Error::Io`] when pack indexes cannot be enumerated.
6060pub fn try_read_object_from_local_packs_cached(
6061    objects_dir: &Path,
6062    oid: &ObjectId,
6063    mut alternate_read: impl FnMut() -> Result<Object>,
6064) -> Result<Option<Object>> {
6065    let indexes = read_local_pack_indexes_cached(objects_dir)?;
6066    for idx in &indexes {
6067        if idx.find_offset(oid)?.is_none() {
6068            continue;
6069        }
6070        match read_object_from_pack(idx, oid) {
6071            Ok(obj) => return Ok(Some(obj)),
6072            Err(_) if pack_index_is_v1(&idx.idx_path) => {
6073                return Ok(Some(Object::new(ObjectKind::Blob, Vec::new())));
6074            }
6075            Err(_) => {
6076                if let Ok(obj) = alternate_read() {
6077                    return Ok(Some(obj));
6078                }
6079            }
6080        }
6081    }
6082    Ok(None)
6083}
6084
6085/// When `oid` is stored as a delta in a pack, return its delta base object id.
6086/// Returns [`None`] for loose objects and for non-delta packed objects.
6087/// Parse a packed delta entry and inflate its instruction stream when the base OID is in `in_pack`.
6088fn parse_in_pack_delta_reuse_candidate(
6089    idx: &PackIndex,
6090    pack_bytes: &[u8],
6091    entry_offset: u64,
6092    in_pack: &HashSet<ObjectId>,
6093) -> Result<Option<(ObjectId, Vec<u8>)>> {
6094    let hb = idx.hash_bytes();
6095    let mut p = entry_offset as usize;
6096    let (packed_type, size) = parse_pack_object_header(pack_bytes, &mut p)?;
6097    let base = match packed_type {
6098        PackedType::RefDelta => {
6099            if p + hb > pack_bytes.len() {
6100                return Err(Error::CorruptObject(
6101                    "truncated ref-delta base oid while scanning for reuse".to_owned(),
6102                ));
6103            }
6104            let bo = ObjectId::from_bytes(&pack_bytes[p..p + hb])?;
6105            p += hb;
6106            bo
6107        }
6108        PackedType::OfsDelta => {
6109            let base_off = parse_ofs_delta_base(pack_bytes, &mut p, entry_offset)?;
6110            let Some(pos) = idx.find_position_by_offset_sorted(base_off)? else {
6111                return Ok(None);
6112            };
6113            let base_oid_bytes = idx.oid_at(pos);
6114            if base_oid_bytes.len() != hb {
6115                return Ok(None);
6116            }
6117            ObjectId::from_bytes(base_oid_bytes)?
6118        }
6119        _ => return Ok(None),
6120    };
6121    if !in_pack.contains(&base) {
6122        return Ok(None);
6123    }
6124    let zlib_start = p;
6125    let Some(end_pos) = pack_entry_raw_end(idx, pack_bytes, entry_offset)? else {
6126        return Ok(None);
6127    };
6128    if zlib_start >= end_pos {
6129        return Ok(None);
6130    }
6131    let compressed = &pack_bytes[zlib_start..end_pos];
6132    let mut zpos = 0usize;
6133    let mut scratch = ZlibInflateScratch::default();
6134    let delta = match scratch.decompress_fixed(compressed, &mut zpos, size) {
6135        Ok(d) => d,
6136        Err(_) => return Ok(None),
6137    };
6138    Ok(Some((base, delta)))
6139}
6140
6141/// Like [`parse_in_pack_delta_reuse_candidate`], but returns the zlib payload and header size
6142/// without inflating (for verbatim reuse in a new pack).
6143fn parse_in_pack_delta_reuse_zlib(
6144    idx: &PackIndex,
6145    pack_bytes: &[u8],
6146    entry_offset: u64,
6147    reusable_base: &dyn Fn(&ObjectId) -> bool,
6148) -> Result<Option<(ObjectId, u64, usize, usize)>> {
6149    parse_delta_reuse_zlib_at(
6150        idx,
6151        pack_bytes,
6152        entry_offset,
6153        reusable_base,
6154        DeltaReuseLookups {
6155            row_at_offset: &|offset| idx.find_position_by_offset_sorted(offset),
6156            entry_end: &|| pack_entry_raw_end(idx, pack_bytes, entry_offset),
6157            entry_crc: &|| idx.crc32_for_pack_offset(entry_offset),
6158        },
6159    )
6160}
6161
6162/// How [`parse_delta_reuse_zlib_at`] finds what the pack index records about an entry:
6163/// the index row of the object at a pack offset (an `OFS_DELTA` base), and the end and
6164/// CRC of the entry itself.
6165struct DeltaReuseLookups<'a> {
6166    row_at_offset: &'a dyn Fn(u64) -> Result<Option<usize>>,
6167    entry_end: &'a dyn Fn() -> Result<Option<usize>>,
6168    entry_crc: &'a dyn Fn() -> Result<Option<u32>>,
6169}
6170
6171/// [`parse_in_pack_delta_reuse_zlib`] with the index lookups supplied by the caller.
6172fn parse_delta_reuse_zlib_at(
6173    idx: &PackIndex,
6174    pack_bytes: &[u8],
6175    entry_offset: u64,
6176    reusable_base: &dyn Fn(&ObjectId) -> bool,
6177    lookups: DeltaReuseLookups<'_>,
6178) -> Result<Option<(ObjectId, u64, usize, usize)>> {
6179    let start = entry_offset as usize;
6180    let hb = idx.hash_bytes();
6181    let mut p = start;
6182    let (packed_type, size) = parse_pack_object_header(pack_bytes, &mut p)?;
6183    let base = match packed_type {
6184        PackedType::RefDelta => {
6185            if p + hb > pack_bytes.len() {
6186                return Err(Error::CorruptObject(
6187                    "truncated ref-delta base oid while scanning for reuse".to_owned(),
6188                ));
6189            }
6190            let bo = ObjectId::from_bytes(&pack_bytes[p..p + hb])?;
6191            p += hb;
6192            bo
6193        }
6194        PackedType::OfsDelta => {
6195            let base_off = parse_ofs_delta_base(pack_bytes, &mut p, entry_offset)?;
6196            let Some(pos) = (lookups.row_at_offset)(base_off)? else {
6197                return Ok(None);
6198            };
6199            let base_oid_bytes = idx.oid_at(pos);
6200            if base_oid_bytes.len() != hb {
6201                return Ok(None);
6202            }
6203            ObjectId::from_bytes(base_oid_bytes)?
6204        }
6205        _ => return Ok(None),
6206    };
6207    if !reusable_base(&base) {
6208        return Ok(None);
6209    }
6210    let zlib_start = p;
6211    let Some(end_pos) = (lookups.entry_end)()? else {
6212        return Ok(None);
6213    };
6214    if zlib_start >= end_pos {
6215        return Ok(None);
6216    }
6217    let entry_bytes = &pack_bytes[start..end_pos];
6218    if let Some(crc) = (lookups.entry_crc)()? {
6219        if crc32fast::hash(entry_bytes) != crc {
6220            return Ok(None);
6221        }
6222    }
6223    Ok(Some((base, size, zlib_start, end_pos)))
6224}
6225
6226fn reuse_delta_matches_pack_resolution(
6227    idx: &PackIndex,
6228    target: &ObjectId,
6229    base: ObjectId,
6230    delta: &[u8],
6231) -> bool {
6232    let Ok(base_obj) = read_object_from_pack(idx, &base) else {
6233        return false;
6234    };
6235    let Ok(target_obj) = read_object_from_pack(idx, target) else {
6236        return false;
6237    };
6238    crate::unpack_objects::apply_delta(&base_obj.data, delta)
6239        .ok()
6240        .is_some_and(|applied| applied == target_obj.data)
6241}
6242
6243/// The `(id, pack offset)` of every object of `wanted` that `idx` lists.
6244///
6245/// A set much smaller than the index is looked up id by id, so packing a few objects
6246/// from a large pack does not visit every index entry; larger sets scan the index once.
6247/// Whether visiting every entry of `idx` costs less than looking up `wanted` objects:
6248/// a lookup costs about log2(entries) comparisons, a scan one set probe per entry.
6249fn index_scan_is_cheaper(idx: &PackIndex, wanted: usize) -> bool {
6250    let lookup_cost =
6251        wanted.saturating_mul(usize::BITS.saturating_sub(idx.len().leading_zeros()) as usize);
6252    lookup_cost >= idx.len()
6253}
6254
6255fn wanted_index_entries<'a>(
6256    idx: &PackIndex,
6257    wanted: impl ExactSizeIterator<Item = &'a ObjectId>,
6258    contains: impl Fn(&ObjectId) -> bool,
6259) -> Result<Vec<(ObjectId, u64)>> {
6260    if !index_scan_is_cheaper(idx, wanted.len()) {
6261        let mut out = Vec::new();
6262        for oid in wanted {
6263            if let Some(offset) = idx.find_offset(oid)? {
6264                out.push((*oid, offset));
6265            }
6266        }
6267        return Ok(out);
6268    }
6269    let mut out = Vec::new();
6270    for entry in idx.iter() {
6271        let Ok(oid) = ObjectId::from_bytes(entry.oid()) else {
6272            continue;
6273        };
6274        if contains(&oid) {
6275            out.push((oid, entry.offset()?));
6276        }
6277    }
6278    Ok(out)
6279}
6280
6281/// Where an object to be packed currently lives among the local packs.
6282#[derive(Debug, Clone, Copy, PartialEq, Eq)]
6283pub(crate) struct PackedSlot {
6284    /// Position of the pack in oldest-first order (only compared for equality).
6285    pub pack: u32,
6286    /// The object is stored whole (not as `OFS_DELTA` / `REF_DELTA`) in that pack.
6287    pub whole: bool,
6288}
6289
6290/// Locate each object of `wanted` in the local packs, oldest pack first, as
6291/// [`collect_packed_delta_reuse_for_send`] does when choosing which copy to reuse.
6292///
6293/// Objects that are not packed are absent from the result.
6294///
6295/// # Errors
6296///
6297/// Returns [`Error::CorruptObject`] when a pack entry header is malformed, or
6298/// [`Error::Io`] when a pack cannot be read.
6299pub(crate) fn locate_packed_objects(
6300    objects_dir: &Path,
6301    wanted: &crate::objects::OidHashSet,
6302) -> Result<crate::objects::OidHashMap<PackedSlot>> {
6303    let mut indexes = read_local_pack_indexes_cached(objects_dir)?;
6304    sort_pack_indexes_oldest_first(&mut indexes);
6305    let mut slots = crate::objects::oid_hash_map_with_capacity(wanted.len());
6306    for (pack, idx) in (0u32..).zip(&indexes) {
6307        let bytes = read_pack_bytes_cached(&idx.pack_path)?;
6308        for (oid, offset) in wanted_index_entries(idx, wanted.iter(), |oid| wanted.contains(oid))? {
6309            if slots.contains_key(&oid) {
6310                continue;
6311            }
6312            let mut pos = usize::try_from(offset)
6313                .map_err(|_| Error::CorruptObject("pack offset exceeds memory".to_owned()))?;
6314            let (packed_type, _) = parse_pack_object_header(&bytes, &mut pos)?;
6315            let whole = !matches!(packed_type, PackedType::OfsDelta | PackedType::RefDelta);
6316            slots.insert(oid, PackedSlot { pack, whole });
6317        }
6318    }
6319    Ok(slots)
6320}
6321
6322/// An on-disk delta that can be copied as it is: base, uncompressed delta size, the pack
6323/// it is stored in, and the zlib stream's byte range in that pack.
6324pub(crate) type OnDiskReuseZlibEntry = (ObjectId, u64, Arc<PackData>, usize, usize);
6325
6326/// Scan local packs once and collect on-disk delta reuse candidates for objects in `send`.
6327///
6328/// An object of `send` that some local pack (oldest first) stores as `REF_DELTA` or
6329/// `OFS_DELTA` is kept when `reusable_base` accepts both it and its base; the result maps it
6330/// to its base id, uncompressed delta size, the pack bytes and the zlib stream's start and end
6331/// offsets in them, so the delta can be copied from the source pack verbatim. Every base down
6332/// a stored chain is itself such a target or was accepted when its target was parsed. Entries
6333/// are CRC-checked when the index provides CRC32.
6334///
6335/// The returned edges never form a cycle. Each pack's deltas are acyclic, but two packs
6336/// may store the same objects as deltas in opposite directions (a repository filled by
6337/// separate pushes, whose later thin pack was completed with bases the earlier one
6338/// stores as deltas). Such a cycle loses the edge of its smallest object id, which
6339/// is then sent whole, as [`delta_cycle_breaks`] describes.
6340///
6341/// # Errors
6342///
6343/// Returns [`Error::CorruptObject`] when a pack stream is malformed.
6344pub(crate) fn collect_packed_delta_reuse_for_send(
6345    objects_dir: &Path,
6346    send: &HashSet<ObjectId>,
6347    reusable_base: &dyn Fn(&ObjectId) -> bool,
6348) -> Result<HashMap<ObjectId, OnDiskReuseZlibEntry>> {
6349    let mut indexes = read_local_pack_indexes_cached(objects_dir)?;
6350    sort_pack_indexes_oldest_first(&mut indexes);
6351
6352    let mut found: HashMap<ObjectId, OnDiskReuseZlibEntry> = HashMap::new();
6353    for idx in indexes.iter().filter(|idx| idx.hash_bytes() == 20) {
6354        let pack_bytes = read_pack_bytes_cached(&idx.pack_path)?;
6355        if index_scan_is_cheaper(idx, send.len()) {
6356            scan_packed_delta_reuse(idx, &pack_bytes, send, reusable_base, &mut found)?;
6357            continue;
6358        }
6359        for (oid, offset) in wanted_index_entries(idx, send.iter(), |oid| send.contains(oid))? {
6360            if found.contains_key(&oid) || !reusable_base(&oid) {
6361                continue;
6362            }
6363            let Some((base, size, zlib_start, zlib_end)) =
6364                parse_in_pack_delta_reuse_zlib(idx, &pack_bytes, offset, reusable_base)?
6365            else {
6366                continue;
6367            };
6368            found.insert(
6369                oid,
6370                (base, size, Arc::clone(&pack_bytes), zlib_start, zlib_end),
6371            );
6372        }
6373    }
6374    // Edges whose target and base were both taken from one pack are that pack's own,
6375    // acyclic deltas, so every cycle passes through an edge that crosses packs.
6376    let crosses_packs = |(_, (base, _, pack, ..)): &(&ObjectId, &OnDiskReuseZlibEntry)| {
6377        found
6378            .get(base)
6379            .is_some_and(|(_, _, base_pack, ..)| !Arc::ptr_eq(base_pack, pack))
6380    };
6381    let starts = found.iter().filter(crosses_packs).map(|(t, _)| *t);
6382    for target in delta_cycle_breaks(starts, |oid| found.get(oid).map(|e| e.0)) {
6383        found.remove(&target);
6384    }
6385    Ok(found)
6386}
6387
6388/// The part of [`collect_packed_delta_reuse_for_send`] for one pack holding much of
6389/// `send`: visit the pack's entries in storage order, where each entry's end and CRC
6390/// come from its neighbour in the offset table instead of a search per entry, and the
6391/// pack is read front to back.
6392fn scan_packed_delta_reuse(
6393    idx: &PackIndex,
6394    pack_bytes: &Arc<PackData>,
6395    send: &HashSet<ObjectId>,
6396    reusable_base: &dyn Fn(&ObjectId) -> bool,
6397    found: &mut HashMap<ObjectId, OnDiskReuseZlibEntry>,
6398) -> Result<()> {
6399    let pack_end = pack_bytes.len().checked_sub(idx.hash_bytes());
6400    idx.with_offset_order(|pairs| -> Result<()> {
6401        let row_at_offset = |offset: u64| {
6402            Ok(pairs
6403                .binary_search_by_key(&offset, |p| p.0)
6404                .ok()
6405                .map(|i| pairs[i].1 as usize))
6406        };
6407        for (i, &(offset, row)) in pairs.iter().enumerate() {
6408            let Ok(oid) = ObjectId::from_bytes(idx.oid_at(row as usize)) else {
6409                continue;
6410            };
6411            if !send.contains(&oid) || found.contains_key(&oid) || !reusable_base(&oid) {
6412                continue;
6413            }
6414            let entry_end = pack_end.filter(|&end| (offset as usize) < end).map(|end| {
6415                pairs[i + 1..]
6416                    .iter()
6417                    .find(|p| p.0 > offset)
6418                    .map_or(end, |p| (p.0 as usize).min(end))
6419            });
6420            let Some((base, size, zlib_start, zlib_end)) = parse_delta_reuse_zlib_at(
6421                idx,
6422                pack_bytes,
6423                offset,
6424                reusable_base,
6425                DeltaReuseLookups {
6426                    row_at_offset: &row_at_offset,
6427                    entry_end: &|| Ok(entry_end),
6428                    entry_crc: &|| Ok(idx.crc32_at(row as usize)),
6429                },
6430            )?
6431            else {
6432                continue;
6433            };
6434            found.insert(
6435                oid,
6436                (base, size, Arc::clone(pack_bytes), zlib_start, zlib_end),
6437            );
6438        }
6439        Ok(())
6440    })?
6441}
6442
6443/// If `oid` is stored as `REF_DELTA` or `OFS_DELTA` in a local pack and its base OID is in
6444/// `packed_set`, return the base OID and the **uncompressed** delta payload (Git binary delta).
6445///
6446/// Callers re-zlib when writing a new pack so we do not depend on copying raw deflate streams.
6447///
6448/// # Errors
6449///
6450/// Returns [`Error::CorruptObject`] when the pack stream is malformed.
6451pub fn packed_ref_delta_reuse_slice(
6452    objects_dir: &Path,
6453    oid: &ObjectId,
6454    packed_set: &HashSet<ObjectId>,
6455) -> Result<Option<(ObjectId, Vec<u8>)>> {
6456    for (idx, entry_offset) in packs_holding(objects_dir, oid, PackPreference::Oldest)? {
6457        if idx.hash_bytes() != 20 {
6458            continue;
6459        }
6460        let pack_bytes = read_pack_bytes_cached(&idx.pack_path)?;
6461        let Some((base, delta)) =
6462            parse_in_pack_delta_reuse_candidate(&idx, &pack_bytes, entry_offset, packed_set)?
6463        else {
6464            continue;
6465        };
6466        if reuse_delta_matches_pack_resolution(&idx, oid, base, &delta) {
6467            return Ok(Some((base, delta)));
6468        }
6469    }
6470    Ok(None)
6471}
6472
6473/// Prefer older packs when the same OID exists as a full object in a fresh repack and as a delta
6474/// in an earlier thin pack (t5316).
6475///
6476/// Each pack's mtime is read once per call (not once per comparison).
6477pub(crate) fn sort_pack_indexes_oldest_first(indexes: &mut [Arc<PackIndex>]) {
6478    if indexes.len() < 2 {
6479        return;
6480    }
6481    indexes.sort_by_cached_key(|idx| (pack_mtime(idx), idx.pack_path.clone()));
6482}
6483
6484fn pack_mtime(idx: &PackIndex) -> std::time::SystemTime {
6485    fs::metadata(&idx.pack_path)
6486        .and_then(|m| m.modified())
6487        .unwrap_or(std::time::SystemTime::UNIX_EPOCH)
6488}
6489
6490/// Which pack wins when one object is stored in several.
6491#[derive(Clone, Copy)]
6492enum PackPreference {
6493    /// Oldest pack first, as [`sort_pack_indexes_oldest_first`].
6494    Oldest,
6495    /// Newest pack first (ties: path descending).
6496    Newest,
6497}
6498
6499/// The local packs whose index lists `oid`, with its offset there, preferred pack first.
6500///
6501/// Per-object lookups call this instead of sorting every pack: the order only matters
6502/// among packs that hold the object, which is almost always one, so the common case
6503/// reads no pack mtimes at all.
6504fn packs_holding(
6505    objects_dir: &Path,
6506    oid: &ObjectId,
6507    preference: PackPreference,
6508) -> Result<Vec<(Arc<PackIndex>, u64)>> {
6509    let mut holding = Vec::new();
6510    for idx in read_local_pack_indexes_cached(objects_dir)? {
6511        if let Some(offset) = idx.find_offset(oid)? {
6512            holding.push((idx, offset));
6513        }
6514    }
6515    if holding.len() > 1 {
6516        let key = |idx: &PackIndex| (pack_mtime(idx), idx.pack_path.clone());
6517        match preference {
6518            PackPreference::Oldest => holding.sort_by_cached_key(|(idx, _)| key(idx)),
6519            PackPreference::Newest => {
6520                holding.sort_by_cached_key(|(idx, _)| std::cmp::Reverse(key(idx)));
6521            }
6522        }
6523    }
6524    Ok(holding)
6525}
6526
6527/// `packed_delta_base_oid` function.
6528pub fn packed_delta_base_oid(objects_dir: &Path, oid: &ObjectId) -> Result<Option<ObjectId>> {
6529    for (idx, entry_offset) in packs_holding(objects_dir, oid, PackPreference::Newest)? {
6530        if idx.hash_bytes() != 20 {
6531            continue;
6532        }
6533        let pack_bytes = read_pack_bytes_cached(&idx.pack_path)?;
6534        let mut p = entry_offset as usize;
6535        let (packed_type, _) = parse_pack_object_header(&pack_bytes, &mut p)?;
6536        match packed_type {
6537            PackedType::RefDelta => {
6538                let hb = idx.hash_bytes();
6539                if p + hb > pack_bytes.len() {
6540                    return Err(Error::CorruptObject("truncated ref-delta base".to_owned()));
6541                }
6542                return Ok(Some(ObjectId::from_bytes(&pack_bytes[p..p + hb])?));
6543            }
6544            PackedType::OfsDelta => {
6545                let base_off = parse_ofs_delta_base(&pack_bytes, &mut p, entry_offset)?;
6546                return Ok(idx
6547                    .find_position_by_offset_sorted(base_off)?
6548                    .and_then(|pos| ObjectId::from_bytes(idx.oid_at(pos)).ok()));
6549            }
6550            _ => continue,
6551        }
6552    }
6553    Ok(None)
6554}
6555
6556/// End offset (exclusive) of the raw packed bytes for the object at `entry_offset`.
6557///
6558/// Uses the next larger pack offset from the index, or the pack trailer when this is the last object.
6559pub(crate) fn pack_entry_raw_end(
6560    idx: &PackIndex,
6561    pack_bytes: &[u8],
6562    entry_offset: u64,
6563) -> Result<Option<usize>> {
6564    let hb = idx.hash_bytes();
6565    let Some(pack_end) = pack_bytes.len().checked_sub(hb) else {
6566        return Ok(None);
6567    };
6568    let start = entry_offset as usize;
6569    if start >= pack_end {
6570        return Ok(None);
6571    }
6572    Ok(Some(
6573        idx.next_pack_offset_after(entry_offset, pack_end as u64)? as usize,
6574    ))
6575}
6576
6577/// A non-delta object's zlib stream inside a mapped local pack.
6578pub(crate) struct PackedWholeObject {
6579    /// The pack holding the object.
6580    pub(crate) pack: Arc<PackData>,
6581    /// Start of this object's raw pack entry (type/size header).
6582    pub(crate) entry_offset: u64,
6583    /// End of this object's raw pack entry (exclusive).
6584    pub(crate) entry_end: usize,
6585    /// Offset of the object's zlib stream, just past its entry header.
6586    pub(crate) zlib_offset: usize,
6587    /// End of the pack's object data (start of the trailing checksum).
6588    pub(crate) data_end: usize,
6589    /// Object type from the entry header (never a delta type).
6590    pub(crate) kind: ObjectKind,
6591    /// Uncompressed payload size from the entry header.
6592    pub(crate) size: u64,
6593    /// Pack-index CRC32 for the raw entry when it has not been checked yet.
6594    pub(crate) index_crc: Option<u32>,
6595}
6596
6597/// How many local packs' indexes list `oid`.
6598pub(crate) fn count_local_packs_holding(objects_dir: &Path, oid: &ObjectId) -> Result<usize> {
6599    Ok(packs_holding(objects_dir, oid, PackPreference::Newest)?.len())
6600}
6601
6602/// How the local packs store an object, from [`locate_packed_whole_object`].
6603pub(crate) enum PackedEntry {
6604    /// Stored whole in some pack.
6605    Whole(PackedWholeObject),
6606    /// Present in a pack, but not usable whole: stored as a delta, with an unparsable entry
6607    /// header, or failing the pack index CRC when that is checked.
6608    NotWhole,
6609    /// In no local pack.
6610    Absent,
6611}
6612
6613/// Locate `oid` stored whole (not as a delta) in a local pack, without inflating it.
6614///
6615/// Costs one index lookup and entry-header parse per pack holding the object. Packs that
6616/// hold it only as a delta, or whose entry header does not parse, are skipped so another
6617/// pack can serve it whole. With `verify_crc`, an entry whose bytes do not match the pack
6618/// index CRC (v2 indexes) is skipped too, so a corrupt copy is never streamed and a
6619/// redundant intact copy, or the buffered read path, serves the object instead.
6620///
6621/// # Errors
6622///
6623/// Returns [`Error::Io`] when pack indexes or pack files cannot be read.
6624pub(crate) fn locate_packed_whole_object(
6625    objects_dir: &Path,
6626    oid: &ObjectId,
6627    verify_crc: bool,
6628) -> Result<PackedEntry> {
6629    let indexes = read_local_pack_indexes_cached(objects_dir)?;
6630    let mut found = PackedEntry::Absent;
6631    for idx in &indexes {
6632        let Some(entry_offset) = idx.find_offset(oid)? else {
6633            continue;
6634        };
6635        found = PackedEntry::NotWhole;
6636        let pack = read_pack_bytes_cached(&idx.pack_path)?;
6637        let Some(data_end) = pack.len().checked_sub(idx.hash_bytes()) else {
6638            continue;
6639        };
6640        let Ok(mut pos) = usize::try_from(entry_offset) else {
6641            continue;
6642        };
6643        if pos >= data_end {
6644            continue;
6645        }
6646        let Ok((packed_type, size)) = parse_pack_object_header(&pack[..data_end], &mut pos) else {
6647            continue;
6648        };
6649        let Ok(kind) = packed_type_to_kind(packed_type) else {
6650            continue;
6651        };
6652        if verify_crc && !packed_entry_matches_index_crc(idx, &pack, entry_offset)? {
6653            continue;
6654        }
6655        let entry_end = pack_entry_raw_end(idx, &pack, entry_offset)?.unwrap_or(data_end);
6656        let index_crc = if verify_crc {
6657            None
6658        } else {
6659            idx.crc32_for_pack_offset(entry_offset).ok().flatten()
6660        };
6661        return Ok(PackedEntry::Whole(PackedWholeObject {
6662            pack,
6663            entry_offset,
6664            entry_end,
6665            zlib_offset: pos,
6666            data_end,
6667            kind,
6668            size,
6669            index_crc,
6670        }));
6671    }
6672    Ok(found)
6673}
6674
6675/// Whether the raw pack entry at `entry_offset` matches the CRC32 its pack index records.
6676///
6677/// Version 1 indexes record no CRC; their entries are accepted (the zlib checksum still
6678/// guards the payload).
6679///
6680/// A large entry is hashed with a bounded window of the pack mapping resident (see
6681/// [`VERIFY_RESIDENT_PACK_BYTES`]), so checking a big blob before streaming it does not pull
6682/// its whole compressed size into the resident set.
6683pub(crate) fn packed_entry_matches_index_crc(
6684    idx: &PackIndex,
6685    pack: &PackData,
6686    entry_offset: u64,
6687) -> Result<bool> {
6688    let Some(recorded) = idx.crc32_for_pack_offset(entry_offset)? else {
6689        return Ok(true);
6690    };
6691    let Some(end) = pack_entry_raw_end(idx, pack, entry_offset)? else {
6692        return Ok(false);
6693    };
6694    let start = usize::try_from(entry_offset)
6695        .map_err(|_| Error::CorruptObject(format!("pack offset {entry_offset} overflows")))?;
6696    if start >= end {
6697        return Ok(false);
6698    }
6699    let budget = ResidentBudget::new(pack, VERIFY_RESIDENT_PACK_BYTES);
6700    Ok(budget.crc32(&pack[start..end]) == recorded)
6701}
6702
6703/// Mapped pack bytes [`packed_entry_matches_index_crc`] lets stay resident while it hashes
6704/// one entry before dropping the mapping's pages.
6705const VERIFY_RESIDENT_PACK_BYTES: usize = 4 * 1024 * 1024;
6706
6707/// When `oid` is stored as a full (non-delta) object in a local pack, return its verbatim packed
6708/// bytes: the varint type+size header followed by the zlib stream.
6709///
6710/// The header of a non-delta pack entry is position-independent, so the returned slice can be
6711/// copied into a new pack unchanged — skipping both inflate and deflate. This is Git
6712/// pack-objects' object reuse for objects that stay full in the output pack.
6713///
6714/// # Parameters
6715/// - `objects_dir` — the repository's `objects/` directory.
6716/// - `oid` — the object to look up.
6717///
6718/// Returns `None` when the object is loose only, stored as a delta, or the repository uses a
6719/// hash width other than SHA-1.
6720///
6721/// # Errors
6722///
6723/// Returns [`Error::Io`] when pack files cannot be read; a malformed candidate entry is skipped
6724/// rather than reported so another pack (or the recompression path) can serve the object.
6725pub fn packed_full_object_slice(objects_dir: &Path, oid: &ObjectId) -> Result<Option<Vec<u8>>> {
6726    Ok(packed_full_object_range(objects_dir, oid)?
6727        .map(|(bytes, start, end)| bytes[start..end].to_vec()))
6728}
6729
6730/// [`packed_full_object_slice`] without the copy: the pack's bytes and the entry's range in
6731/// them, so a pack writer can copy straight from the mapping.
6732pub(crate) fn packed_full_object_range(
6733    objects_dir: &Path,
6734    oid: &ObjectId,
6735) -> Result<Option<(Arc<PackData>, usize, usize)>> {
6736    for (idx, entry_offset) in packs_holding(objects_dir, oid, PackPreference::Newest)? {
6737        let pack_bytes = read_pack_bytes_cached(&idx.pack_path)?;
6738        let start = entry_offset as usize;
6739        let mut p = start;
6740        let Ok((packed_type, _size)) = parse_pack_object_header(&pack_bytes, &mut p) else {
6741            continue;
6742        };
6743        if matches!(packed_type, PackedType::OfsDelta | PackedType::RefDelta) {
6744            // The same OID may be a full object in another pack; keep scanning.
6745            continue;
6746        }
6747        let Some(end) = pack_entry_raw_end(&idx, &pack_bytes, entry_offset)? else {
6748            continue;
6749        };
6750        if end <= start {
6751            continue;
6752        }
6753        // Git `check_pack_crc`: verbatim reuse copies bytes unparsed, so guard with the pack
6754        // index's CRC32 when v2 records one. v1 indexes rely on the zlib trailer; inflating
6755        // here only to verify the oid would retain the whole payload in anon RSS (issue #2287).
6756        if !packed_entry_matches_index_crc(&idx, &pack_bytes, entry_offset)? {
6757            continue;
6758        }
6759        return Ok(Some((pack_bytes, start, end)));
6760    }
6761    Ok(None)
6762}
6763
6764/// Decode the variable-length size field starting at the first byte of a pack object header.
6765pub(crate) fn decode_pack_object_size_varint_from_first(
6766    first: u8,
6767    mut next: impl FnMut() -> Result<u8>,
6768) -> Result<u64> {
6769    let mut size = (first & 0x0f) as u64;
6770    let mut shift = 4u32;
6771    let mut c = first;
6772    while (c & 0x80) != 0 {
6773        if shift >= 64 {
6774            return Err(Error::CorruptObject(
6775                "pack object size varint overflow".to_owned(),
6776            ));
6777        }
6778        c = next()?;
6779        let digit = (c & 0x7f) as u64;
6780        size = size
6781            .checked_add(
6782                digit
6783                    .checked_shl(shift)
6784                    .ok_or_else(|| Error::CorruptObject("pack object size overflow".to_owned()))?,
6785            )
6786            .ok_or_else(|| Error::CorruptObject("pack object size overflow".to_owned()))?;
6787        shift += 7;
6788    }
6789    Ok(size)
6790}
6791
6792/// Decode the variable-length size field that follows the first byte of a pack object header.
6793pub(crate) fn decode_pack_object_size_varint(
6794    first: u8,
6795    bytes: &[u8],
6796    pos: &mut usize,
6797) -> Result<u64> {
6798    decode_pack_object_size_varint_from_first(first, || {
6799        let b = *bytes.get(*pos).ok_or_else(|| {
6800            Error::CorruptObject("unexpected end of variable size header".to_owned())
6801        })?;
6802        *pos += 1;
6803        Ok(b)
6804    })
6805}
6806
6807pub(crate) fn parse_pack_object_header(bytes: &[u8], pos: &mut usize) -> Result<(PackedType, u64)> {
6808    let first = *bytes.get(*pos).ok_or_else(|| {
6809        Error::CorruptObject("unexpected end of pack header while decoding object".to_owned())
6810    })?;
6811    *pos += 1;
6812
6813    let type_code = (first >> 4) & 0x7;
6814    let size = decode_pack_object_size_varint(first, bytes, pos)?;
6815
6816    let packed_type = match type_code {
6817        1 => PackedType::Commit,
6818        2 => PackedType::Tree,
6819        3 => PackedType::Blob,
6820        4 => PackedType::Tag,
6821        6 => PackedType::OfsDelta,
6822        7 => PackedType::RefDelta,
6823        _ => {
6824            return Err(Error::CorruptObject(format!(
6825                "unsupported packed object type {}",
6826                type_code
6827            )))
6828        }
6829    };
6830    Ok((packed_type, size))
6831}
6832
6833/// Dependency of a packed delta object at `object_offset` within `pack_bytes`.
6834#[derive(Debug, Clone, Copy)]
6835pub enum PackedDeltaDependency {
6836    /// OFS_DELTA: base object offset within the same pack.
6837    OfsBase {
6838        /// Pack offset of the base object.
6839        base_offset: u64,
6840    },
6841    /// REF_DELTA: base object id (may live in another pack).
6842    RefBase {
6843        /// OID of the delta base.
6844        base_oid: ObjectId,
6845    },
6846}
6847
6848/// If the object at `object_offset` is a delta, return how it refers to its base.
6849///
6850/// `hash_bytes` is the ref-delta base OID width in this pack (`20` for SHA-1, `32` for SHA-256).
6851pub fn read_packed_delta_dependency(
6852    pack_bytes: &[u8],
6853    object_offset: u64,
6854    hash_bytes: usize,
6855) -> Result<Option<PackedDeltaDependency>> {
6856    let mut pos = object_offset as usize;
6857    let (ty, _) = parse_pack_object_header(pack_bytes, &mut pos)?;
6858    match ty {
6859        PackedType::OfsDelta => {
6860            let base = parse_ofs_delta_base(pack_bytes, &mut pos, object_offset)?;
6861            Ok(Some(PackedDeltaDependency::OfsBase { base_offset: base }))
6862        }
6863        PackedType::RefDelta => {
6864            if pos + hash_bytes > pack_bytes.len() {
6865                return Err(Error::CorruptObject("truncated ref-delta base oid".into()));
6866            }
6867            let base_oid = ObjectId::from_bytes(&pack_bytes[pos..pos + hash_bytes])?;
6868            Ok(Some(PackedDeltaDependency::RefBase { base_oid }))
6869        }
6870        _ => Ok(None),
6871    }
6872}
6873
6874pub(crate) fn parse_ofs_delta_base(bytes: &[u8], pos: &mut usize, this_offset: u64) -> Result<u64> {
6875    let mut c = *bytes
6876        .get(*pos)
6877        .ok_or_else(|| Error::CorruptObject("truncated ofs-delta header".to_owned()))?;
6878    *pos += 1;
6879    let mut value = (c & 0x7f) as u64;
6880    while (c & 0x80) != 0 {
6881        c = *bytes
6882            .get(*pos)
6883            .ok_or_else(|| Error::CorruptObject("truncated ofs-delta header".to_owned()))?;
6884        *pos += 1;
6885        value = ((value + 1) << 7) | (c & 0x7f) as u64;
6886    }
6887    this_offset
6888        .checked_sub(value)
6889        .ok_or_else(|| Error::CorruptObject("invalid ofs-delta base offset".to_owned()))
6890}
6891
6892/// Advance `pos` past one packed object (including zlib payload).
6893///
6894/// `object_start_offset` is the byte offset of this object within the pack file
6895/// (used for `OFS_DELTA` base resolution).
6896/// Raw bytes of one packed object (header + zlib payload) starting at `object_start_offset`.
6897///
6898/// `hash_bytes` is the ref-delta base OID width in this pack (`20` for SHA-1, `32` for SHA-256).
6899pub fn slice_one_pack_object(
6900    bytes: &[u8],
6901    object_start_offset: u64,
6902    hash_bytes: usize,
6903) -> Result<&[u8]> {
6904    let start = object_start_offset as usize;
6905    let mut pos = start;
6906    skip_one_pack_object(bytes, &mut pos, object_start_offset, hash_bytes)?;
6907    Ok(&bytes[start..pos])
6908}
6909
6910/// `skip_one_pack_object` function.
6911pub fn skip_one_pack_object(
6912    bytes: &[u8],
6913    pos: &mut usize,
6914    object_start_offset: u64,
6915    hash_bytes: usize,
6916) -> Result<()> {
6917    let (packed_type, size) = parse_pack_object_header(bytes, pos)?;
6918    let mut scratch = ZlibInflateScratch::default();
6919    match packed_type {
6920        PackedType::Commit | PackedType::Tree | PackedType::Blob | PackedType::Tag => {
6921            scratch.skip_zlib_stream(bytes, pos, size)?;
6922        }
6923        PackedType::RefDelta => {
6924            if *pos + hash_bytes > bytes.len() {
6925                return Err(Error::CorruptObject("truncated ref-delta base oid".into()));
6926            }
6927            *pos += hash_bytes;
6928            scratch.skip_zlib_stream(bytes, pos, size)?;
6929        }
6930        PackedType::OfsDelta => {
6931            let _base_off = parse_ofs_delta_base(bytes, pos, object_start_offset)?;
6932            scratch.skip_zlib_stream(bytes, pos, size)?;
6933        }
6934    }
6935    Ok(())
6936}
6937
6938/// Read all object IDs from a `.idx` file.
6939pub fn read_idx_object_ids(idx_path: &Path) -> Result<Vec<ObjectId>> {
6940    let index = read_pack_index(idx_path)?;
6941    let mut out = Vec::new();
6942    let hash_bytes = index.hash_bytes();
6943    for e in index.iter() {
6944        if e.oid().len() == hash_bytes {
6945            out.push(ObjectId::from_bytes(e.oid())?);
6946        }
6947    }
6948    Ok(out)
6949}
6950
6951#[cfg(any(test, feature = "test-support"))]
6952pub(crate) mod pack_cache_test_sync {
6953    use std::cell::Cell;
6954    use std::sync::Mutex;
6955    use std::sync::MutexGuard;
6956
6957    /// Process-global pack cache test lock state (guards real synchronization state).
6958    struct PackCacheTestCoordinator {
6959        top_level_guards: u32,
6960    }
6961
6962    // hygiene: pack cache test synchronization (cfg(test) only)
6963    static COORD: Mutex<PackCacheTestCoordinator> = Mutex::new(PackCacheTestCoordinator {
6964        top_level_guards: 0,
6965    });
6966
6967    // hygiene: pack cache test depth guard (cfg(test) only)
6968    thread_local! {
6969        static DEPTH: Cell<u32> = const { Cell::new(0) };
6970    }
6971
6972    /// Serializes tests that share the process-global pack cache (including `clear_pack_cache`).
6973    pub struct PackCacheTestGuard {
6974        _inner: Option<MutexGuard<'static, PackCacheTestCoordinator>>,
6975    }
6976
6977    pub fn acquire() -> PackCacheTestGuard {
6978        let depth = DEPTH.with(|d| {
6979            let n = d.get();
6980            d.set(n + 1);
6981            n
6982        });
6983        PackCacheTestGuard {
6984            _inner: if depth == 0 {
6985                let mut g = COORD.lock().unwrap_or_else(|e| e.into_inner());
6986                assert_eq!(
6987                    g.top_level_guards, 0,
6988                    "pack cache test coordinator already held"
6989                );
6990                g.top_level_guards = 1;
6991                Some(g)
6992            } else {
6993                None
6994            },
6995        }
6996    }
6997
6998    impl Drop for PackCacheTestGuard {
6999        fn drop(&mut self) {
7000            DEPTH.with(|d| {
7001                let n = d.get();
7002                assert!(n > 0, "pack cache test guard depth underflow");
7003                d.set(n - 1);
7004                if n == 1 {
7005                    if let Some(mut g) = self._inner.take() {
7006                        assert_eq!(g.top_level_guards, 1);
7007                        g.top_level_guards = 0;
7008                    }
7009                }
7010            });
7011        }
7012    }
7013}
7014
7015#[cfg(any(test, feature = "test-support"))]
7016pub(crate) fn pack_cache_test_guard() -> pack_cache_test_sync::PackCacheTestGuard {
7017    pack_cache_test_sync::acquire()
7018}
7019
7020#[cfg(test)]
7021mod tests {
7022    use super::*;
7023    use crate::delta_encode::encode_lcp_delta;
7024    use crate::odb::Odb;
7025    use crate::pack_map::PackData;
7026    use crate::transfer::{build_pack, PackBuildOptions};
7027    use flate2::write::ZlibEncoder;
7028    use flate2::Compression;
7029    use std::io::Write;
7030    use std::process::Command;
7031
7032    fn git_try(dir: &std::path::Path, args: &[&str]) -> bool {
7033        Command::new("git") // hygiene: git fixture comparison in cfg(test) module // hygiene: git fixture comparison in cfg(test) module
7034            .current_dir(dir)
7035            .args(args)
7036            .env("GIT_CONFIG_GLOBAL", "/dev/null")
7037            .env("GIT_CONFIG_SYSTEM", "/dev/null")
7038            .output()
7039            .map(|o| o.status.success())
7040            .unwrap_or(false)
7041    }
7042
7043    fn git(dir: &std::path::Path, args: &[&str]) {
7044        let out = Command::new("git") // hygiene: git fixture comparison in cfg(test) module
7045            .current_dir(dir)
7046            .args(args)
7047            .env("GIT_AUTHOR_NAME", "T")
7048            .env("GIT_AUTHOR_EMAIL", "t@example.com")
7049            .env("GIT_COMMITTER_NAME", "T")
7050            .env("GIT_COMMITTER_EMAIL", "t@example.com")
7051            .env("GIT_AUTHOR_DATE", "2005-04-07T22:13:13 +0200")
7052            .env("GIT_COMMITTER_DATE", "2005-04-07T22:13:13 +0200")
7053            .env("GIT_CONFIG_GLOBAL", "/dev/null")
7054            .env("GIT_CONFIG_SYSTEM", "/dev/null")
7055            .output()
7056            .expect("git");
7057        assert!(
7058            out.status.success(),
7059            "git {:?}: {}",
7060            args,
7061            String::from_utf8_lossy(&out.stderr)
7062        );
7063    }
7064
7065    fn append_pack_object_header(buf: &mut Vec<u8>, type_code: u8, payload_len: usize) {
7066        let mut size = payload_len;
7067        let first = ((type_code & 0x7) << 4) | (size & 0x0f) as u8;
7068        size >>= 4;
7069        if size > 0 {
7070            buf.push(first | 0x80);
7071            while size > 0 {
7072                let b = (size & 0x7f) as u8;
7073                size >>= 7;
7074                buf.push(if size > 0 { b | 0x80 } else { b });
7075            }
7076        } else {
7077            buf.push(first);
7078        }
7079    }
7080
7081    fn zlib_pack(payload: &[u8]) -> Vec<u8> {
7082        let mut enc = ZlibEncoder::new(Vec::new(), Compression::default());
7083        enc.write_all(payload).unwrap();
7084        enc.finish().unwrap()
7085    }
7086
7087    fn append_sha1_pack_trailer(buf: &mut Vec<u8>) {
7088        buf.extend_from_slice(HashAlgo::Sha1.checksum_digest(&*buf).as_slice());
7089    }
7090
7091    fn append_ref_delta(buf: &mut Vec<u8>, base_oid: &ObjectId, delta: &[u8]) {
7092        let compressed = zlib_pack(delta);
7093        append_pack_object_header(buf, 7, delta.len());
7094        buf.extend_from_slice(base_oid.as_bytes());
7095        buf.extend_from_slice(&compressed);
7096    }
7097
7098    fn append_ofs_delta(buf: &mut Vec<u8>, base_offset: u64, delta: &[u8]) -> u64 {
7099        let object_start = buf.len() as u64;
7100        let compressed = zlib_pack(delta);
7101        append_pack_object_header(buf, 6, delta.len());
7102        let mut ofs = object_start - base_offset;
7103        while ofs >= 0x80 {
7104            buf.push((ofs as u8 & 0x7f) | 0x80);
7105            ofs >>= 7;
7106        }
7107        buf.push(ofs as u8);
7108        buf.extend_from_slice(&compressed);
7109        object_start
7110    }
7111
7112    #[test]
7113    fn commit_header_block_inflate_stops_at_the_headers() {
7114        // Hex ids that do not compress, so the headers span several input steps.
7115        let mut state = 0x9e37_79b9_7f4a_7c15_u64;
7116        let mut next = || {
7117            state = state
7118                .wrapping_mul(6_364_136_223_846_793_005)
7119                .wrapping_add(1_442_695_040_888_963_407);
7120            state
7121        };
7122        let parents: String = (0..12)
7123            .map(|_| {
7124                format!(
7125                    "parent {:016x}{:016x}{:08x}\n",
7126                    next(),
7127                    next(),
7128                    next() as u32
7129                )
7130            })
7131            .collect();
7132        let headers = format!(
7133            "tree {:040x}\n{parents}author A <a@example.com> 1 +0000\n\
7134             committer C <c@example.com> 2 +0000\n\n",
7135            7
7136        );
7137        let mut zlib = ZlibInflateScratch::default();
7138        let mut out = Vec::new();
7139        for body in [String::new(), "subject\n".to_owned(), "x\n".repeat(5000)] {
7140            let commit = format!("{headers}{body}");
7141            let compressed = zlib_pack(commit.as_bytes());
7142            assert!(
7143                compressed.len() > COMMIT_HEADER_BLOCK_INPUT_STEP,
7144                "the headers alone should take more than one input step"
7145            );
7146            assert!(inflate_commit_header_block(
7147                &compressed,
7148                commit.len() as u64,
7149                COMMIT_HEADER_BLOCK_MAX,
7150                &mut zlib,
7151                &mut out,
7152            ));
7153            assert_eq!(out, headers.as_bytes(), "body of {} bytes", body.len());
7154        }
7155
7156        let commit = format!("{headers}subject\n");
7157        let compressed = zlib_pack(commit.as_bytes());
7158        let size = commit.len() as u64;
7159        assert!(
7160            !inflate_commit_header_block(&compressed, size, headers.len() - 3, &mut zlib, &mut out),
7161            "headers longer than the budget"
7162        );
7163        assert!(
7164            !inflate_commit_header_block(&compressed[..40], size, 1024, &mut zlib, &mut out),
7165            "a stream cut off before the end of the headers"
7166        );
7167        let no_blank = b"tree 0000000000000000000000000000000000000007\nauthor A";
7168        assert!(
7169            !inflate_commit_header_block(
7170                &zlib_pack(no_blank),
7171                no_blank.len() as u64,
7172                1024,
7173                &mut zlib,
7174                &mut out,
7175            ),
7176            "a payload with no blank line"
7177        );
7178        assert!(
7179            !inflate_commit_header_block(
7180                &compressed,
7181                headers.len() as u64 - 3,
7182                1024,
7183                &mut zlib,
7184                &mut out,
7185            ),
7186            "a stream that inflates past its declared size"
7187        );
7188    }
7189
7190    fn append_whole_blob(buf: &mut Vec<u8>, data: &[u8]) -> u64 {
7191        let off = buf.len() as u64;
7192        let compressed = zlib_pack(data);
7193        append_pack_object_header(buf, 3, data.len());
7194        buf.extend_from_slice(&compressed);
7195        off
7196    }
7197
7198    fn append_corrupt_whole_blob(buf: &mut Vec<u8>) -> u64 {
7199        let off = buf.len() as u64;
7200        append_pack_object_header(buf, 3, 8);
7201        buf.extend_from_slice(&[0xff, 0xfe, 0xfd]);
7202        off
7203    }
7204
7205    fn write_v2_idx_for_test(
7206        idx_path: &Path,
7207        pack_path: &Path,
7208        entries: &[(ObjectId, u64)],
7209    ) -> Result<()> {
7210        let pack_bytes = std::fs::read(pack_path)?;
7211        let mut with_crc = Vec::with_capacity(entries.len());
7212        for (oid, off) in entries {
7213            let start = *off as usize;
7214            let mut end = start;
7215            skip_one_pack_object(&pack_bytes, &mut end, *off, 20)
7216                .map_err(|e| Error::CorruptObject(format!("idx crc walk: {e}")))?;
7217            with_crc.push((*oid, *off, crc32fast::hash(&pack_bytes[start..end])));
7218        }
7219        write_v2_pack_index(idx_path, pack_path, &with_crc, 20)
7220    }
7221
7222    fn single_blob_pack(data: &[u8]) -> (Vec<u8>, ObjectId, u64) {
7223        let odb = Odb::new(tempfile::tempdir().expect("tempdir").path());
7224        let oid = odb.hash(ObjectKind::Blob, data).expect("hash");
7225        let mut pack = Vec::new();
7226        pack.extend_from_slice(b"PACK");
7227        pack.extend_from_slice(&2u32.to_be_bytes());
7228        pack.extend_from_slice(&1u32.to_be_bytes());
7229        let off = append_whole_blob(&mut pack, data);
7230        append_sha1_pack_trailer(&mut pack);
7231        (pack, oid, off)
7232    }
7233
7234    #[cfg_attr(
7235        not(feature = "full-tests"),
7236        ignore = "starts system git or is heavy; run with --features full-tests"
7237    )]
7238    #[test]
7239    fn read_commit_parents_reads_whole_commit_entries() {
7240        let _guard = pack_cache_test_guard();
7241        clear_pack_cache();
7242        let tmp = tempfile::tempdir().expect("tempdir");
7243        let wt = tmp.path();
7244        git(wt, &["init", "-q", "-b", "main"]);
7245        let commit = |file: &str| {
7246            fs::write(wt.join(file), file).expect("write");
7247            git(wt, &["add", file]);
7248            git(wt, &["commit", "-qm", file]);
7249        };
7250        commit("root");
7251        for leg in ["a", "b", "c"] {
7252            git(wt, &["switch", "-qc", leg, "main"]);
7253            commit(leg);
7254        }
7255        git(wt, &["switch", "-q", "main"]);
7256        commit("main");
7257        git(wt, &["merge", "-q", "--no-edit", "a"]);
7258        git(wt, &["merge", "-q", "--no-edit", "b", "c"]);
7259        git(wt, &["repack", "-q", "-a", "-d"]);
7260        let rev_parse = |spec: &str| {
7261            let out = Command::new("git") // hygiene: git fixture comparison in cfg(test) module
7262                .current_dir(wt)
7263                .args(["rev-parse", spec])
7264                .output()
7265                .expect("git rev-parse");
7266            ObjectId::from_hex(String::from_utf8_lossy(&out.stdout).trim()).expect("oid")
7267        };
7268        let octopus = rev_parse("HEAD");
7269        let merge = rev_parse("HEAD^1");
7270        let main = rev_parse("HEAD^1^1");
7271        let root = rev_parse("HEAD^1^1^1");
7272        let objects = wt.join(".git/objects");
7273        let store = crate::pack_store::PackStore::legacy_for_objects_dir(&objects);
7274        crate::pack_store::PackStore::with_context(store, || {
7275            let mut reader = SingleLocalPackReader::open_allow_loose(&objects)
7276                .expect("open")
7277                .expect("single pack");
7278            assert_eq!(
7279                reader.read_commit_parents(&root, 40).expect("root"),
7280                Some(vec![])
7281            );
7282            assert_eq!(
7283                reader.read_commit_parents(&main, 40).expect("main"),
7284                Some(vec![root])
7285            );
7286            assert_eq!(
7287                reader.read_commit_parents(&merge, 40).expect("merge"),
7288                Some(vec![main, rev_parse("a")])
7289            );
7290            assert_eq!(
7291                reader.read_commit_parents(&octopus, 40).expect("octopus"),
7292                Some(vec![merge, rev_parse("b"), rev_parse("c")])
7293            );
7294            let tree = rev_parse("HEAD^{tree}");
7295            assert_eq!(reader.read_commit_parents(&tree, 40).expect("tree"), None);
7296            let absent =
7297                ObjectId::from_hex("1234567890123456789012345678901234567890").expect("absent oid");
7298            assert_eq!(
7299                reader.read_commit_parents(&absent, 40).expect("absent"),
7300                None
7301            );
7302        });
7303    }
7304
7305    #[cfg_attr(
7306        not(feature = "full-tests"),
7307        ignore = "starts system git or is heavy; run with --features full-tests"
7308    )]
7309    #[test]
7310    fn read_commit_parents_rejects_a_bad_zlib_checksum_after_the_headers() {
7311        let _guard = pack_cache_test_guard();
7312        clear_pack_cache();
7313        let tmp = tempfile::tempdir().expect("tempdir");
7314        let wt = tmp.path();
7315        git(wt, &["init", "-q", "-b", "main"]);
7316        fs::write(wt.join("f"), "f").expect("write");
7317        git(wt, &["add", "f"]);
7318        git(wt, &["commit", "-qm", "only commit"]);
7319        git(wt, &["repack", "-q", "-a", "-d", "-f"]);
7320        let git_out = |args: &[&str]| {
7321            let out = Command::new("git") // hygiene: git fixture comparison in cfg(test) module
7322                .current_dir(wt)
7323                .args(args)
7324                .output()
7325                .expect("git");
7326            String::from_utf8_lossy(&out.stdout).into_owned()
7327        };
7328        let head = ObjectId::from_hex(git_out(&["rev-parse", "HEAD"]).trim()).expect("oid");
7329        let pack_dir = wt.join(".git/objects/pack");
7330        let idx_path = fs::read_dir(&pack_dir)
7331            .expect("pack dir")
7332            .filter_map(|e| e.ok().map(|e| e.path()))
7333            .find(|p| p.extension().is_some_and(|x| x == "idx"))
7334            .expect("pack index");
7335        let listing = git_out(&["verify-pack", "-v", idx_path.to_str().expect("utf-8")]);
7336        let (offset, in_pack) = listing
7337            .lines()
7338            .find_map(|line| {
7339                let f: Vec<&str> = line.split_whitespace().collect();
7340                (f.first() == Some(&head.to_hex().as_str())).then(|| {
7341                    (
7342                        f[4].parse::<usize>().expect("offset"),
7343                        f[3].parse::<usize>().expect("size"),
7344                    )
7345                })
7346            })
7347            .expect("commit entry");
7348        // The last byte of the entry belongs to the zlib stream's adler32 checksum, past every
7349        // header a parents-only reader needs.
7350        let pack_path = idx_path.with_extension("pack");
7351        let mut bytes = fs::read(&pack_path).expect("read pack");
7352        bytes[offset + in_pack - 1] ^= 0x01;
7353        let mut perms = fs::metadata(&pack_path).expect("stat pack").permissions();
7354        #[allow(clippy::permissions_set_readonly_false)]
7355        perms.set_readonly(false);
7356        fs::set_permissions(&pack_path, perms).expect("make pack writable");
7357        fs::write(&pack_path, &bytes).expect("write pack");
7358
7359        let objects = wt.join(".git/objects");
7360        let store = crate::pack_store::PackStore::legacy_for_objects_dir(&objects);
7361        crate::pack_store::PackStore::with_context(store, || {
7362            let mut reader = SingleLocalPackReader::open_allow_loose(&objects)
7363                .expect("open")
7364                .expect("single pack");
7365            assert_eq!(
7366                reader.read_commit_parents(&head, 40).expect("lookup"),
7367                None,
7368                "a corrupt stream must not yield parents"
7369            );
7370        });
7371    }
7372
7373    #[test]
7374    fn single_local_pack_reader_batch_check_caches_deltified_entries() {
7375        let _guard = pack_cache_test_guard();
7376        clear_pack_cache();
7377        let Some((tmp, idx, tip, odb)) = build_ofs_delta_chain_pack(2) else {
7378            return;
7379        };
7380        let objects = tmp.path().join("objects");
7381        let store = crate::pack_store::PackStore::legacy_for_objects_dir(&objects);
7382        crate::pack_store::PackStore::with_context(store, || {
7383            let mut reader = SingleLocalPackReader::open(&objects)
7384                .expect("open")
7385                .expect("single pack");
7386            let info1 = reader.read_info(&tip).expect("first read");
7387            let info2 = reader.read_info(&tip).expect("cached read");
7388            assert_eq!(info1, info2);
7389            let full = read_object_from_pack(&idx, &tip).expect("full read");
7390            assert_eq!(info1.kind, full.kind);
7391            assert_eq!(info1.size, full.data.len() as u64);
7392        });
7393        let _ = odb;
7394    }
7395
7396    #[test]
7397    fn pack_read_info_cache_is_scoped_per_repository_and_pack_bytes() {
7398        let _guard = pack_cache_test_guard();
7399        clear_pack_cache();
7400        const ENTRY_OFFSET: u64 = 200;
7401        let dir_a = tempfile::tempdir().expect("tempdir a");
7402        let dir_b = tempfile::tempdir().expect("tempdir b");
7403        let objects_a = dir_a.path().join("objects");
7404        let objects_b = dir_b.path().join("objects");
7405        fs::create_dir_all(objects_a.join("pack")).expect("pack dir a");
7406        fs::create_dir_all(objects_b.join("pack")).expect("pack dir b");
7407        let pack_a = objects_a.join("pack/p-a.pack");
7408        let pack_b = objects_b.join("pack/p-b.pack");
7409        let (oid_a, _) =
7410            write_sparse_single_blob_pack(dir_a.path(), &pack_a, b"five!", ENTRY_OFFSET)
7411                .expect("pack a");
7412        let (oid_b, _) =
7413            write_sparse_single_blob_pack(dir_b.path(), &pack_b, b"1234567890", ENTRY_OFFSET)
7414                .expect("pack b");
7415        let idx_a = pack_a.with_extension("idx");
7416        let idx_b = pack_b.with_extension("idx");
7417        write_v2_idx_for_test(&idx_a, &pack_a, &[(oid_a, ENTRY_OFFSET)]).expect("idx a");
7418        write_v2_idx_for_test(&idx_b, &pack_b, &[(oid_b, ENTRY_OFFSET)]).expect("idx b");
7419        let idx_a = read_pack_index(&idx_a).expect("parse idx a");
7420        let idx_b = read_pack_index(&idx_b).expect("parse idx b");
7421
7422        let read_size = |objects: &Path, idx: &PackIndex| -> u64 {
7423            let store = crate::pack_store::PackStore::legacy_for_objects_dir(objects);
7424            crate::pack_store::PackStore::with_context(store, || {
7425                super::read_object_info_from_pack_at_offset(idx, ENTRY_OFFSET)
7426                    .expect("read_info")
7427                    .size
7428            })
7429        };
7430
7431        let size_a = read_size(&objects_a, &idx_a);
7432        let size_b = read_size(&objects_b, &idx_b);
7433        assert_eq!(size_a, 5);
7434        assert_eq!(size_b, 10);
7435        // A → B → A again: cache must not return B's size for A's pack entry.
7436        assert_eq!(read_size(&objects_a, &idx_a), 5);
7437        assert_eq!(read_size(&objects_b, &idx_b), 10);
7438        assert_eq!(read_size(&objects_a, &idx_a), 5);
7439    }
7440    /// Write a valid pack with one blob placed at `object_offset` so the on-disk file
7441    /// length exceeds the mmap owned-buffer threshold without storing huge compressed payloads.
7442    fn write_sparse_single_blob_pack(
7443        repo_root: &Path,
7444        pack_path: &Path,
7445        data: &[u8],
7446        object_offset: u64,
7447    ) -> Result<(ObjectId, u64)> {
7448        use std::fs::OpenOptions;
7449        use std::io::{Seek, SeekFrom, Write};
7450
7451        let odb = Odb::new(repo_root);
7452        let oid = odb.hash(ObjectKind::Blob, data).expect("hash");
7453        let mut file = OpenOptions::new()
7454            .create(true)
7455            .write(true)
7456            .read(true)
7457            .truncate(true)
7458            .open(pack_path)
7459            .map_err(Error::Io)?;
7460        file.write_all(b"PACK").map_err(Error::Io)?;
7461        file.write_all(&2u32.to_be_bytes()).map_err(Error::Io)?;
7462        file.write_all(&1u32.to_be_bytes()).map_err(Error::Io)?;
7463        file.seek(SeekFrom::Start(object_offset))
7464            .map_err(Error::Io)?;
7465        let mut tail = Vec::new();
7466        append_whole_blob(&mut tail, data);
7467        file.write_all(&tail).map_err(Error::Io)?;
7468        let trailer = sha1_trailer_for_open_file(&mut file)?;
7469        file.write_all(trailer.as_bytes()).map_err(Error::Io)?;
7470        file.flush().map_err(Error::Io)?;
7471        Ok((oid, object_offset))
7472    }
7473
7474    #[test]
7475    fn skip_one_pack_object_rejects_zero_size_with_zlib_payload() {
7476        let mut bytes = Vec::new();
7477        bytes.push(0x30);
7478        bytes.extend_from_slice(&zlib_pack(b"x"));
7479        let mut pos = 0usize;
7480        let err = skip_one_pack_object(&bytes, &mut pos, 0, 20).unwrap_err();
7481        assert!(matches!(err, Error::CorruptObject(_)), "got {err:?}");
7482    }
7483
7484    #[test]
7485    fn parse_pack_object_header_rejects_all_ff_size_continuation() {
7486        let header = [
7487            0x80 | (3 << 4) | 0x0f,
7488            0xff,
7489            0xff,
7490            0xff,
7491            0xff,
7492            0xff,
7493            0xff,
7494            0xff,
7495            0xff,
7496            0xff,
7497        ];
7498        let mut pos = 0usize;
7499        let err = parse_pack_object_header(&header, &mut pos).unwrap_err();
7500        assert!(matches!(err, Error::CorruptObject(_)), "{err:?}");
7501    }
7502
7503    #[test]
7504    fn lying_huge_pack_blob_read_returns_corrupt_packed_object() {
7505        let tmp = tempfile::tempdir().expect("tempdir");
7506        let dir = tmp.path();
7507        if !git_try(dir, &["init", "-q"]) {
7508            return;
7509        }
7510        std::fs::write(dir.join("f"), b"hello\n").expect("write");
7511        git(dir, &["add", "f"]);
7512        git(
7513            dir,
7514            &[
7515                "-c",
7516                "user.name=t",
7517                "-c",
7518                "user.email=t@t",
7519                "commit",
7520                "-qm",
7521                "one",
7522            ],
7523        );
7524        let oid_hex = Command::new("git")
7525            .current_dir(dir)
7526            .args(["rev-parse", "HEAD:f"])
7527            .env("GIT_CONFIG_GLOBAL", "/dev/null")
7528            .env("GIT_CONFIG_SYSTEM", "/dev/null")
7529            .output()
7530            .expect("rev-parse")
7531            .stdout;
7532        let oid =
7533            ObjectId::from_hex(std::str::from_utf8(&oid_hex).unwrap().trim()).expect("parse oid");
7534        let objects = dir.join(".git/objects");
7535        let pack_dir = objects.join("pack");
7536        std::fs::create_dir_all(&pack_dir).expect("pack dir");
7537        let pack_stem = pack_dir.join("pack");
7538        let mut child = Command::new("git")
7539            .current_dir(dir)
7540            .args(["pack-objects", "-q"])
7541            .arg(&pack_stem)
7542            .stdin(std::process::Stdio::piped())
7543            .env("GIT_CONFIG_GLOBAL", "/dev/null")
7544            .env("GIT_CONFIG_SYSTEM", "/dev/null")
7545            .spawn()
7546            .expect("pack-objects spawn");
7547        use std::io::Write;
7548        child
7549            .stdin
7550            .take()
7551            .expect("stdin")
7552            .write_all(format!("{oid}\n").as_bytes())
7553            .expect("write oid");
7554        assert!(
7555            child.wait().expect("pack-objects wait").success(),
7556            "pack-objects failed"
7557        );
7558        let loose = objects.join(format!("{}/{}", &oid.to_hex()[..2], &oid.to_hex()[2..]));
7559        let _ = std::fs::remove_file(loose);
7560        let pack_path = std::fs::read_dir(&pack_dir)
7561            .expect("pack dir")
7562            .filter_map(|e| e.ok())
7563            .map(|e| e.path())
7564            .find(|p| p.extension().is_some_and(|x| x == "pack"))
7565            .expect("pack file");
7566        let idx_path = pack_path.with_extension("idx");
7567        let mut pack = std::fs::read(&pack_path).expect("read pack");
7568        assert_eq!(pack[12] & 0x80, 0, "expected single-byte size header");
7569        let tail = pack[13..].to_vec();
7570        pack.truncate(12);
7571        pack.extend_from_slice(&[0x80 | (3 << 4) | 0x0f, 0xff, 0xff, 0xff, 0xff, 0x7f]);
7572        pack.extend_from_slice(&tail);
7573        let pack_body_end = pack.len() - 20;
7574        let pack_hash = HashAlgo::Sha1.checksum_digest(&pack[..pack_body_end]);
7575        let pack_trailer = pack_hash.as_slice().to_vec();
7576        pack[pack_body_end..].copy_from_slice(&pack_trailer);
7577        let mut idx = std::fs::read(&idx_path).expect("read idx");
7578        let idx_len = idx.len();
7579        let crc_off = 8 + 1024 + 20;
7580        let crc = crc32fast::hash(&pack[12..pack_body_end]);
7581        idx[crc_off..crc_off + 4].copy_from_slice(&crc.to_be_bytes());
7582        idx[idx_len - 40..idx_len - 20].copy_from_slice(&pack_trailer);
7583        let idx_hash = HashAlgo::Sha1.checksum_digest(&idx[..idx_len - 20]);
7584        let idx_trailer = idx_hash.as_slice().to_vec();
7585        idx[idx_len - 20..].copy_from_slice(&idx_trailer);
7586        #[cfg(unix)]
7587        {
7588            use std::os::unix::fs::PermissionsExt;
7589            for path in [&pack_path, &idx_path] {
7590                std::fs::set_permissions(path, std::fs::Permissions::from_mode(0o644))
7591                    .expect("chmod pack/index");
7592            }
7593        }
7594        std::fs::write(&pack_path, &pack).expect("write pack");
7595        std::fs::write(&idx_path, &idx).expect("write idx");
7596        clear_pack_cache();
7597        let odb = Odb::new(&objects);
7598        let err = odb.read(&oid).expect_err("lying header must not abort");
7599        assert!(matches!(err, Error::CorruptPackedObject { .. }), "{err:?}");
7600    }
7601
7602    fn install_synthetic_pack(
7603        objects_dir: &Path,
7604        stem: &str,
7605        pack: &[u8],
7606        entries: &[(ObjectId, u64)],
7607    ) -> PackIndex {
7608        install_synthetic_pack_inner(objects_dir, stem, pack, entries, true)
7609    }
7610
7611    fn install_synthetic_pack_no_clear(
7612        objects_dir: &Path,
7613        stem: &str,
7614        pack: &[u8],
7615        entries: &[(ObjectId, u64)],
7616    ) -> PackIndex {
7617        install_synthetic_pack_inner(objects_dir, stem, pack, entries, false)
7618    }
7619
7620    fn install_synthetic_pack_inner(
7621        objects_dir: &Path,
7622        stem: &str,
7623        pack: &[u8],
7624        entries: &[(ObjectId, u64)],
7625        clear_cache: bool,
7626    ) -> PackIndex {
7627        let pack_dir = objects_dir.join("pack");
7628        std::fs::create_dir_all(&pack_dir).expect("pack dir");
7629        let pack_path = pack_dir.join(format!("{stem}.pack"));
7630        let idx_path = pack_dir.join(format!("{stem}.idx"));
7631        std::fs::write(&pack_path, pack).expect("write pack");
7632        write_v2_idx_for_test(&idx_path, &pack_path, entries).expect("write idx");
7633        if clear_cache {
7634            clear_pack_cache();
7635        }
7636        read_pack_index(&idx_path).expect("read idx")
7637    }
7638
7639    fn pack_index_for_objects(
7640        pack_path: std::path::PathBuf,
7641        objects: &[(ObjectId, u64)],
7642    ) -> PackIndex {
7643        let idx_path = pack_path.with_extension("idx");
7644        let pack_bytes = fs::read(&pack_path).expect("read pack for idx trailer");
7645        let trailer = &pack_bytes[pack_bytes.len().saturating_sub(20)..];
7646        let rows: Vec<(ObjectId, u64, u32)> = objects
7647            .iter()
7648            .map(|(oid, offset)| (*oid, *offset, 0))
7649            .collect();
7650        write_v2_pack_index_with_trailer(&idx_path, &rows, trailer, 20, None).expect("write idx");
7651        read_pack_index(&idx_path).expect("read idx")
7652    }
7653
7654    fn index_pack_in_scratch(pack: &[u8]) -> (tempfile::TempDir, PackIndex) {
7655        let dir = tempfile::tempdir().expect("tempdir");
7656        let pack_path = dir.path().join("chain.pack");
7657        std::fs::write(&pack_path, pack).expect("write pack");
7658        let out = Command::new("git") // hygiene: git fixture comparison in cfg(test) module
7659            .current_dir(dir.path())
7660            .args(["index-pack", "chain.pack"])
7661            .env("GIT_CONFIG_GLOBAL", "/dev/null")
7662            .env("GIT_CONFIG_SYSTEM", "/dev/null")
7663            .output()
7664            .expect("index-pack");
7665        assert!(
7666            out.status.success(),
7667            "git index-pack failed: {}",
7668            String::from_utf8_lossy(&out.stderr)
7669        );
7670        let idx_path = dir.path().join("chain.idx");
7671        let idx = read_pack_index(&idx_path).expect("read idx");
7672        (dir, idx)
7673    }
7674
7675    /// Repo with many prefix-preserving edits to one blob so delta chains can exceed 50.
7676    fn build_deep_delta_repo(commit_count: usize) -> Option<(tempfile::TempDir, ObjectId, Odb)> {
7677        let tmp = tempfile::tempdir().ok()?;
7678        let dir = tmp.path();
7679        if !git_try(dir, &["init", "-q", "-b", "main", "."]) {
7680            return None;
7681        }
7682        let mut body = String::new();
7683        for i in 0..200 {
7684            body.push_str(&format!("seed line {i:04}\n"));
7685        }
7686        for rev in 0..commit_count {
7687            body.push_str(&format!("edit-{rev}\n"));
7688            std::fs::write(dir.join("blob.txt"), body.as_bytes()).ok()?;
7689            git(dir, &["add", "blob.txt"]);
7690            git(dir, &["commit", "-q", "-m", &format!("c{rev}")]);
7691        }
7692        let tip_hex = Command::new("git") // hygiene: git fixture comparison in cfg(test) module
7693            .current_dir(dir)
7694            .args(["rev-parse", "HEAD"])
7695            .env("GIT_CONFIG_GLOBAL", "/dev/null")
7696            .env("GIT_CONFIG_SYSTEM", "/dev/null")
7697            .output()
7698            .ok()?;
7699        if !tip_hex.status.success() {
7700            return None;
7701        }
7702        let tip = ObjectId::from_hex(std::str::from_utf8(&tip_hex.stdout).ok()?.trim()).ok()?;
7703        let git_dir = dir.join(".git");
7704        let odb = Odb::new(&git_dir.join("objects")).with_config_git_dir(git_dir);
7705        Some((tmp, tip, odb))
7706    }
7707
7708    #[test]
7709    fn verify_packed_object_hash_sha256_detects_corruption() {
7710        use crate::hash::hash_object;
7711        use std::fs;
7712
7713        let data = b"sha256 packed blob for hash verify\n";
7714        let oid = hash_object(HashAlgo::Sha256, ObjectKind::Blob, data).expect("hash");
7715        let wrong_oid =
7716            hash_object(HashAlgo::Sha256, ObjectKind::Blob, b"different payload\n").expect("hash");
7717        let mut pack = Vec::new();
7718        pack.extend_from_slice(b"PACK");
7719        pack.extend_from_slice(&2u32.to_be_bytes());
7720        pack.extend_from_slice(&1u32.to_be_bytes());
7721        let off = append_whole_blob(&mut pack, data);
7722        pack.extend_from_slice(HashAlgo::Sha256.checksum_digest(&pack).as_slice());
7723
7724        let dir = tempfile::tempdir().expect("tempdir");
7725        let pack_path = dir.path().join("sha256-corrupt.pack");
7726        fs::write(&pack_path, &pack).expect("write pack");
7727
7728        let idx_path = pack_path.with_extension("idx");
7729        let trailer = &pack[pack.len() - 32..];
7730        write_v2_pack_index_with_trailer(&idx_path, &[(wrong_oid, off, 0)], trailer, 32, None)
7731            .expect("write idx");
7732        let idx = read_pack_index(&idx_path).expect("read idx");
7733
7734        let err = read_object_from_pack_bytes(&pack, &idx, wrong_oid.as_bytes()).unwrap_err();
7735        match err {
7736            Error::CorruptObject(msg) => {
7737                assert!(
7738                    msg.contains("hashes to"),
7739                    "expected hash mismatch message, got: {msg}"
7740                );
7741                assert!(
7742                    msg.contains(&oid.to_hex()),
7743                    "expected recomputed oid in message, got: {msg}"
7744                );
7745            }
7746            other => panic!("expected CorruptObject, got {other:?}"),
7747        }
7748    }
7749
7750    #[cfg_attr(
7751        not(feature = "full-tests"),
7752        ignore = "starts system git or is heavy; run with --features full-tests"
7753    )]
7754    #[test]
7755    fn reads_delta_chain_deeper_than_50() {
7756        let _guard = pack_cache_test_guard();
7757        let Some((_repo, tip, odb)) = build_deep_delta_repo(120) else {
7758            let _ = "SKIP: git unavailable for deep delta fixture";
7759            return;
7760        };
7761        let pack = build_pack(
7762            &odb,
7763            &[tip],
7764            &[],
7765            &PackBuildOptions {
7766                delta: true,
7767                max_depth: 120,
7768                use_ofs_delta: true,
7769                ..PackBuildOptions::default()
7770            },
7771        )
7772        .expect("build deep delta pack");
7773
7774        let (_scratch, idx) = index_pack_in_scratch(&pack);
7775        assert!(
7776            idx.len() > 1,
7777            "expected a multi-object pack, got {}",
7778            idx.len()
7779        );
7780
7781        for entry in idx.iter() {
7782            let oid = ObjectId::from_bytes(entry.oid()).expect("oid in idx");
7783            let from_pack = read_object_from_pack(&idx, &oid).expect("read packed object");
7784            let expected = odb.read(&oid).expect("loose/alt copy for oid");
7785            assert_eq!(from_pack.kind, expected.kind);
7786            assert_eq!(from_pack.data, expected.data);
7787            assert_eq!(
7788                odb.hash(from_pack.kind, &from_pack.data).expect("hash"),
7789                oid
7790            );
7791        }
7792    }
7793
7794    #[test]
7795    fn cyclic_ref_delta_chain_errors() {
7796        let _guard = pack_cache_test_guard();
7797        let odb = Odb::new(tempfile::tempdir().expect("tempdir").path());
7798        let content_a = b"aaa".as_slice();
7799        let content_b = b"bbb".as_slice();
7800        let oid_a = odb.hash(ObjectKind::Blob, content_a).expect("hash");
7801        let oid_b = odb.hash(ObjectKind::Blob, content_b).expect("hash");
7802        let delta_a = encode_lcp_delta(content_b, content_a).expect("delta a");
7803        let delta_b = encode_lcp_delta(content_a, content_b).expect("delta b");
7804
7805        let mut pack = Vec::new();
7806        pack.extend_from_slice(b"PACK");
7807        pack.extend_from_slice(&2u32.to_be_bytes());
7808        pack.extend_from_slice(&2u32.to_be_bytes());
7809        let off_a = pack.len() as u64;
7810        append_ref_delta(&mut pack, &oid_b, &delta_a);
7811        let off_b = pack.len() as u64;
7812        append_ref_delta(&mut pack, &oid_a, &delta_b);
7813        append_sha1_pack_trailer(&mut pack);
7814
7815        let dir = tempfile::tempdir().expect("tempdir");
7816        let pack_path = dir.path().join("cycle.pack");
7817        std::fs::write(&pack_path, &pack).expect("write cyclic pack");
7818        let idx = pack_index_for_objects(pack_path, &[(oid_a, off_a), (oid_b, off_b)]);
7819        let pack_bytes = std::fs::read(&idx.pack_path).expect("read pack");
7820        for err in [
7821            read_object_from_pack(&idx, &oid_a).expect_err("read_object_from_pack"),
7822            read_object_from_pack_bytes(&pack_bytes, &idx, oid_a.as_bytes())
7823                .expect_err("read_object_from_pack_bytes"),
7824        ] {
7825            assert!(
7826                matches!(err, Error::DeltaChainTooDeep { limit: 4096 }),
7827                "expected typed delta chain limit, got {err:?}"
7828            );
7829        }
7830    }
7831
7832    #[test]
7833    fn resolve_cross_pack_delta_without_index_clone() {
7834        let _guard = pack_cache_test_guard();
7835        clear_pack_cache();
7836        let odb = Odb::new(tempfile::tempdir().expect("tempdir").path());
7837        let base = b"cross-pack-base-payload".as_slice();
7838        let tip = b"cross-pack-base-payload!".as_slice();
7839        let oid_base = odb.hash(ObjectKind::Blob, base).expect("hash");
7840        let oid_tip = odb.hash(ObjectKind::Blob, tip).expect("hash");
7841        let delta = encode_lcp_delta(base, tip).expect("delta");
7842
7843        let mut pack_base = Vec::new();
7844        pack_base.extend_from_slice(b"PACK");
7845        pack_base.extend_from_slice(&2u32.to_be_bytes());
7846        pack_base.extend_from_slice(&1u32.to_be_bytes());
7847        let off_base = append_whole_blob(&mut pack_base, base);
7848        append_sha1_pack_trailer(&mut pack_base);
7849
7850        let mut pack_delta = Vec::new();
7851        pack_delta.extend_from_slice(b"PACK");
7852        pack_delta.extend_from_slice(&2u32.to_be_bytes());
7853        pack_delta.extend_from_slice(&1u32.to_be_bytes());
7854        let off_tip = pack_delta.len() as u64;
7855        append_ref_delta(&mut pack_delta, &oid_base, &delta);
7856        append_sha1_pack_trailer(&mut pack_delta);
7857
7858        let dir = tempfile::tempdir().expect("tempdir");
7859        let objects = dir.path().join("objects");
7860        let _idx_base =
7861            install_synthetic_pack(&objects, "cross-base", &pack_base, &[(oid_base, off_base)]);
7862        let idx_delta =
7863            install_synthetic_pack(&objects, "cross-delta", &pack_delta, &[(oid_tip, off_tip)]);
7864
7865        let from_delta_pack = read_object_from_pack(&idx_delta, &oid_tip).expect("delta pack read");
7866        assert_eq!(from_delta_pack.kind, ObjectKind::Blob);
7867        assert_eq!(from_delta_pack.data, tip);
7868        assert_eq!(
7869            odb.hash(from_delta_pack.kind, &from_delta_pack.data)
7870                .expect("hash"),
7871            oid_tip
7872        );
7873
7874        let from_packs = read_object_from_packs(&objects, &oid_tip).expect("packs dir read");
7875        assert_eq!(from_packs.kind, ObjectKind::Blob);
7876        assert_eq!(from_packs.data, tip);
7877        assert_eq!(
7878            odb.hash(from_packs.kind, &from_packs.data).expect("hash"),
7879            oid_tip
7880        );
7881    }
7882
7883    #[cfg_attr(
7884        not(feature = "full-tests"),
7885        ignore = "starts system git or is heavy; run with --features full-tests"
7886    )]
7887    #[test]
7888    fn read_object_from_pack_bytes_uses_in_memory_index_without_on_disk_idx() {
7889        let odb = Odb::new(tempfile::tempdir().expect("tempdir").path());
7890        let content = b"in-memory-index-blob";
7891        let oid = odb.hash(ObjectKind::Blob, content).expect("hash");
7892
7893        let mut pack = Vec::new();
7894        pack.extend_from_slice(b"PACK");
7895        pack.extend_from_slice(&2u32.to_be_bytes());
7896        pack.extend_from_slice(&1u32.to_be_bytes());
7897        let off = append_whole_blob(&mut pack, content);
7898        append_sha1_pack_trailer(&mut pack);
7899
7900        let dir = tempfile::tempdir().expect("tempdir");
7901        let pack_path = dir.path().join("mem.pack");
7902        std::fs::write(&pack_path, &pack).expect("write pack");
7903        let idx = pack_index_for_objects(pack_path, &[(oid, off)]);
7904        std::fs::remove_file(&idx.idx_path).expect("drop on-disk idx");
7905        assert!(
7906            !idx.idx_path.exists(),
7907            "fixture must not rely on an on-disk .idx"
7908        );
7909
7910        let got = read_object_from_pack_bytes(&pack, &idx, oid.as_bytes()).expect("read bytes");
7911        assert_eq!(got.kind, ObjectKind::Blob);
7912        assert_eq!(got.data, content);
7913    }
7914
7915    fn build_ofs_delta_chain_pack(
7916        depth: usize,
7917    ) -> Option<(tempfile::TempDir, PackIndex, ObjectId, Odb)> {
7918        if depth == 0 {
7919            return None;
7920        }
7921        let tmp = tempfile::tempdir().ok()?;
7922        let odb = Odb::new(&tmp.path().join("objects"));
7923        let mut content = b"ofs-delta-chain-base\n".to_vec();
7924        let mut pack = Vec::new();
7925        pack.extend_from_slice(b"PACK");
7926        pack.extend_from_slice(&2u32.to_be_bytes());
7927        pack.extend_from_slice(&(depth as u32 + 1).to_be_bytes());
7928        let mut base_off = append_whole_blob(&mut pack, &content);
7929        let mut entries = vec![(
7930            odb.hash(ObjectKind::Blob, &content).expect("hash"),
7931            base_off,
7932        )];
7933        for _ in 0..depth {
7934            let mut next = content.clone();
7935            next.push(b'!');
7936            let delta = encode_lcp_delta(&content, &next).ok()?;
7937            let off = append_ofs_delta(&mut pack, base_off, &delta);
7938            let oid = odb.hash(ObjectKind::Blob, &next).expect("hash");
7939            entries.push((oid, off));
7940            content = next;
7941            base_off = off;
7942        }
7943        append_sha1_pack_trailer(&mut pack);
7944        let tip = entries.last()?.0;
7945        let dir = tmp.path();
7946        std::fs::create_dir_all(dir.join("objects/pack")).ok()?;
7947        let pack_path = dir.join("objects/pack/chain.pack");
7948        std::fs::write(&pack_path, &pack).ok()?;
7949        write_v2_idx_for_test(&pack_path.with_extension("idx"), &pack_path, &entries).ok()?;
7950        clear_pack_cache();
7951        let idx = read_pack_index(&pack_path.with_extension("idx")).ok()?;
7952        Some((tmp, idx, tip, odb))
7953    }
7954
7955    #[test]
7956    fn warm_hinted_pack_damage_returns_corrupt_packed_object_via_full_lookup() {
7957        let _guard = pack_cache_test_guard();
7958        clear_pack_cache();
7959        let tmp = tempfile::tempdir().expect("tempdir");
7960        let dir = tmp.path();
7961        if !git_try(dir, &["init", "-q"]) {
7962            return;
7963        }
7964        std::fs::write(dir.join("big.txt"), vec![0u8; 20_000]).expect("big");
7965        git(dir, &["add", "big.txt"]);
7966        git(dir, &["commit", "-qm", "one"]);
7967        git(dir, &["repack", "-adq"]);
7968        std::fs::write(dir.join("more.txt"), b"second pack\n").expect("more");
7969        git(dir, &["add", "more.txt"]);
7970        git(dir, &["commit", "-qm", "two"]);
7971        git(dir, &["repack"]);
7972        let objects = dir.join(".git/objects");
7973        let pack_dir = objects.join("pack");
7974        let pack_count = std::fs::read_dir(&pack_dir)
7975            .expect("pack dir")
7976            .filter_map(|e| e.ok())
7977            .filter(|e| e.path().extension().is_some_and(|x| x == "pack"))
7978            .count();
7979        assert!(
7980            pack_count >= 2,
7981            "expected multiple packs for warm-hint regression, got {pack_count}"
7982        );
7983        let odb = Odb::new(&objects);
7984        let oid_hex = Command::new("git")
7985            .current_dir(dir)
7986            .args(["rev-parse", "HEAD~1:big.txt"])
7987            .env("GIT_CONFIG_GLOBAL", "/dev/null")
7988            .env("GIT_CONFIG_SYSTEM", "/dev/null")
7989            .output()
7990            .expect("rev-parse")
7991            .stdout;
7992        let oid =
7993            ObjectId::from_hex(std::str::from_utf8(&oid_hex).unwrap().trim()).expect("parse oid");
7994        let indexes = read_local_pack_indexes(&objects).expect("indexes");
7995        let (idx_arc, offset) = indexes
7996            .iter()
7997            .find_map(|idx| {
7998                idx.find_offset(&oid)
7999                    .ok()
8000                    .flatten()
8001                    .map(|offset| (Arc::new(idx.clone()), offset))
8002            })
8003            .expect("oid in a pack");
8004        let corrupt_pack = |pack_path: &Path| {
8005            let mut bytes = std::fs::read(pack_path).expect("read pack");
8006            let flip_at = (offset as usize + 8).min(bytes.len().saturating_sub(1));
8007            bytes[flip_at] ^= 0x55;
8008            #[cfg(unix)]
8009            {
8010                use std::os::unix::fs::PermissionsExt;
8011                std::fs::set_permissions(pack_path, std::fs::Permissions::from_mode(0o644))
8012                    .expect("chmod pack");
8013            }
8014            std::fs::write(pack_path, &bytes).expect("corrupt pack");
8015        };
8016        let assert_warm_hint = |label: &str| {
8017            let hint = pack_cache::hinted_pack_index(&objects)
8018                .unwrap_or_else(|| panic!("{label}: pack hint must be warm"));
8019            assert_eq!(
8020                hint.pack_path, idx_arc.pack_path,
8021                "{label}: hint must target the corrupted pack"
8022            );
8023        };
8024        let reseed_hint = || pack_cache::remember_pack_hit(&objects, &idx_arc);
8025
8026        odb.with_pack_read_context(|| {
8027            odb.read(&oid).expect("warm pack hint");
8028            assert_warm_hint("after warm read");
8029
8030            corrupt_pack(&idx_arc.pack_path);
8031            assert!(
8032                pack_cache::revalidate_stale_pack_bytes(&idx_arc.pack_path).expect("revalidate"),
8033                "corrupt pack bytes must be observable in cache"
8034            );
8035            reseed_hint();
8036            assert_warm_hint("before try_read_from_hinted_pack");
8037
8038            let hinted = pack_cache::try_read_from_hinted_pack(&objects, &oid)
8039                .expect("hinted pack read must not propagate raw zlib");
8040            assert!(
8041                hinted.is_none(),
8042                "damaged hinted read must fall through to full lookup"
8043            );
8044            assert_warm_hint("before odb.read after hinted shortcut");
8045
8046            let err = odb.read(&oid).expect_err("read after corrupt hinted miss");
8047            assert!(
8048                matches!(err, Error::CorruptPackedObject { .. }),
8049                "read: {err:?}"
8050            );
8051
8052            reseed_hint();
8053            assert_warm_hint("before try_read_from_hinted_pack_into");
8054
8055            let mut buf = Vec::new();
8056            let hinted_into =
8057                pack_cache::try_read_from_hinted_pack_into(&objects, &oid, true, &mut buf)
8058                    .expect("hinted pack read_into must not propagate raw zlib");
8059            assert!(
8060                hinted_into.is_none(),
8061                "damaged hinted read_into must fall through to full lookup"
8062            );
8063            assert_warm_hint("before odb.read_into after hinted shortcut");
8064
8065            let err = odb
8066                .read_into(&oid, &mut Vec::new())
8067                .expect_err("read_into after corrupt hinted miss");
8068            assert!(
8069                matches!(err, Error::CorruptPackedObject { .. }),
8070                "read_into: {err:?}"
8071            );
8072        });
8073    }
8074
8075    #[test]
8076    fn bounded_pack_cache_resolves_deltas_without_changing_shared_cache() {
8077        let _guard = pack_cache_test_guard();
8078        let (_tmp, idx, tip, odb) = build_ofs_delta_chain_pack(800).expect("chain");
8079        let normal = odb.read(&tip).expect("normal");
8080        let shared_bytes = odb.pack_store().expect("store").delta_bases().bytes_used();
8081        let bounded = odb.with_bounded_pack_cache();
8082        assert!(!Arc::ptr_eq(
8083            odb.pack_store().expect("store"),
8084            bounded.pack_store().expect("store")
8085        ));
8086        bounded.with_pack_read_context(|| {
8087            let got = bounded.read(&tip).expect("bounded");
8088            assert_eq!(got.data, normal.data);
8089            let pack = read_pack_bytes_cached(&idx.pack_path).expect("pack");
8090            assert!(pack.uses_positioned_reads());
8091            let cached = bounded
8092                .pack_store()
8093                .expect("store")
8094                .delta_bases()
8095                .bytes_used();
8096            assert!(cached > 0 && cached <= crate::pack_store::BOUNDED_DELTA_BASE_BYTES);
8097        });
8098        assert_eq!(
8099            odb.pack_store().expect("store").delta_bases().bytes_used(),
8100            shared_bytes
8101        );
8102    }
8103
8104    #[test]
8105    fn ofs_delta_chain_depth_4096_resolves() {
8106        let _guard = pack_cache_test_guard();
8107        clear_pack_cache();
8108        let Some((_tmp, idx, tip, odb)) = build_ofs_delta_chain_pack(4096) else {
8109            return;
8110        };
8111        let got = read_object_from_pack(&idx, &tip).expect("4096-deep chain");
8112        let expect = odb.hash(ObjectKind::Blob, &got.data).expect("hash");
8113        assert_eq!(expect, tip);
8114    }
8115
8116    #[test]
8117    fn ofs_delta_chain_depth_4097_errors() {
8118        let _guard = pack_cache_test_guard();
8119        clear_pack_cache();
8120        let Some((_tmp, idx, tip, _odb)) = build_ofs_delta_chain_pack(4097) else {
8121            return;
8122        };
8123        let err = read_object_from_pack(&idx, &tip).expect_err("4097-deep chain");
8124        assert!(
8125            matches!(err, Error::DeltaChainTooDeep { limit: 4096 }),
8126            "got {err:?}"
8127        );
8128    }
8129
8130    #[test]
8131    fn delta_base_cache_limit_zero_disables_caching() {
8132        let _guard = pack_cache_test_guard();
8133        clear_pack_cache();
8134        let Some((_tmp, idx, tip, _odb)) = build_ofs_delta_chain_pack(12) else {
8135            return;
8136        };
8137        pack_cache::test_set_delta_base_cache_byte_limit(&idx.pack_path, 0);
8138        let _ = read_object_from_pack(&idx, &tip).expect("read chain");
8139        assert_eq!(
8140            pack_cache::test_delta_base_cache_bytes_used(&idx.pack_path),
8141            0,
8142            "limit 0 must not retain delta bases"
8143        );
8144        pack_cache::test_set_delta_base_cache_byte_limit(
8145            &idx.pack_path,
8146            pack_cache::DELTA_BASE_CACHE_DEFAULT,
8147        );
8148    }
8149
8150    #[test]
8151    fn delta_base_cache_eviction_respects_byte_limit() {
8152        let _guard = pack_cache_test_guard();
8153        clear_pack_cache();
8154        let Some((_tmp, idx, tip, _odb)) = build_ofs_delta_chain_pack(32) else {
8155            return;
8156        };
8157        let expected = read_object_from_pack(&idx, &tip)
8158            .expect("baseline read")
8159            .data;
8160        pack_cache::test_set_delta_base_cache_byte_limit(&idx.pack_path, 4096);
8161        let _ = read_object_from_pack(&idx, &tip).expect("prime cache");
8162        assert!(
8163            pack_cache::test_delta_base_cache_bytes_used(&idx.pack_path) <= 4096,
8164            "cache bytes {} exceeded cap",
8165            pack_cache::test_delta_base_cache_bytes_used(&idx.pack_path)
8166        );
8167        pack_cache::test_set_delta_base_cache_byte_limit(&idx.pack_path, 512);
8168        for round in 0..8 {
8169            let got = read_object_from_pack(&idx, &tip).expect("read after eviction");
8170            assert_eq!(got.data, expected, "round {round} must stay byte-identical");
8171        }
8172        pack_cache::test_set_delta_base_cache_byte_limit(
8173            &idx.pack_path,
8174            pack_cache::DELTA_BASE_CACHE_DEFAULT,
8175        );
8176    }
8177
8178    #[test]
8179    fn odb_read_into_caching_delta_bases_fills_cache_read_into_does_not() {
8180        let _guard = pack_cache_test_guard();
8181        clear_pack_cache();
8182        let Some((_tmp, idx, tip, odb)) = build_ofs_delta_chain_pack(12) else {
8183            return;
8184        };
8185        let expected = read_object_from_pack(&idx, &tip)
8186            .expect("baseline read")
8187            .data;
8188        clear_pack_cache();
8189        let store = crate::pack_store::PackStore::legacy_for_pack_path(&idx.pack_path);
8190        crate::pack_store::PackStore::with_context(store, || {
8191            let mut buf = Vec::new();
8192            assert_eq!(
8193                odb.read_into(&tip, &mut buf).expect("read_into"),
8194                ObjectKind::Blob
8195            );
8196            assert_eq!(buf, expected);
8197            assert_eq!(
8198                pack_cache::test_delta_base_cache_bytes_used(&idx.pack_path),
8199                0,
8200                "read_into must not retain delta bases"
8201            );
8202
8203            let mut cached = Vec::new();
8204            let kind = odb
8205                .read_into_caching_delta_bases(&tip, &mut cached)
8206                .expect("caching read");
8207            assert_eq!(kind, ObjectKind::Blob);
8208            assert_eq!(cached, expected);
8209            assert!(
8210                pack_cache::test_delta_base_cache_bytes_used(&idx.pack_path) > 0,
8211                "caching read must retain the chain's delta bases"
8212            );
8213
8214            let again = odb
8215                .read_into_caching_delta_bases(&tip, &mut cached)
8216                .expect("second caching read");
8217            assert_eq!(again, ObjectKind::Blob);
8218            assert_eq!(cached, expected);
8219        });
8220    }
8221
8222    #[test]
8223    fn repository_odb_read_into_caching_delta_bases_fills_cache_through_files_primary() {
8224        use crate::repo::{InitOptions, Repository};
8225        let _guard = pack_cache_test_guard();
8226        clear_pack_cache();
8227        let Some((tmp, idx, tip, _odb)) = build_ofs_delta_chain_pack(12) else {
8228            return;
8229        };
8230        let expected = read_object_from_pack(&idx, &tip)
8231            .expect("baseline read")
8232            .data;
8233        Repository::init(
8234            tmp.path(),
8235            &InitOptions {
8236                bare: true,
8237                ..InitOptions::default()
8238            },
8239        )
8240        .expect("init over packed objects");
8241        let repo = Repository::open(tmp.path(), &crate::environment::RepositoryOptions::empty())
8242            .expect("open");
8243        let odb = repo.odb();
8244        let store = Arc::clone(odb.pack_store().expect("pack store"));
8245        store.invalidate_packs();
8246
8247        let mut buf = Vec::new();
8248        assert_eq!(
8249            odb.read_into(&tip, &mut buf).expect("read_into"),
8250            ObjectKind::Blob
8251        );
8252        assert_eq!(buf, expected);
8253        assert_eq!(store.delta_bases().bytes_used(), 0);
8254
8255        let mut cached = Vec::new();
8256        assert_eq!(
8257            odb.read_into_caching_delta_bases(&tip, &mut cached)
8258                .expect("caching read"),
8259            ObjectKind::Blob
8260        );
8261        assert_eq!(cached, expected);
8262        assert!(store.delta_bases().bytes_used() > 0);
8263    }
8264
8265    #[test]
8266    fn odb_read_into_caching_delta_bases_falls_back_outside_packs() {
8267        let tmp = tempfile::tempdir().expect("tempdir");
8268        let odb = Odb::new(&tmp.path().join("objects"));
8269        let oid = odb
8270            .write(ObjectKind::Blob, b"loose only\n")
8271            .expect("write loose");
8272        let mut buf = vec![0u8; 4];
8273        let kind = odb
8274            .read_into_caching_delta_bases(&oid, &mut buf)
8275            .expect("loose read");
8276        assert_eq!(kind, ObjectKind::Blob);
8277        assert_eq!(buf, b"loose only\n");
8278    }
8279
8280    #[test]
8281    fn cached_delta_reads_support_overlay_alternates_and_custom_stores() {
8282        use crate::odb::store::MemoryStore;
8283        use crate::odb::OdbBuilder;
8284
8285        let tmp = tempfile::tempdir().expect("tempdir");
8286        let local_objects = tmp.path().join("local/objects");
8287        let alternate_objects = tmp.path().join("alternate/objects");
8288        let alternate = Odb::new(&alternate_objects);
8289        let alternate_oid = alternate
8290            .write(ObjectKind::Blob, b"alternate\n")
8291            .expect("alternate object");
8292        fs::create_dir_all(local_objects.join("info")).expect("info");
8293        fs::write(
8294            local_objects.join("info/alternates"),
8295            format!("{}\n", alternate_objects.display()),
8296        )
8297        .expect("alternates");
8298
8299        let odb = Odb::new(&local_objects);
8300        let mut buf = Vec::with_capacity(1024);
8301        assert_eq!(
8302            odb.read_into_caching_delta_bases(&alternate_oid, &mut buf)
8303                .expect("alternate read"),
8304            ObjectKind::Blob
8305        );
8306        assert_eq!(buf, b"alternate\n");
8307
8308        odb.enable_mem_overlay();
8309        let overlay_oid = odb
8310            .write(ObjectKind::Blob, b"overlay\n")
8311            .expect("overlay object");
8312        odb.read_into_caching_delta_bases(&overlay_oid, &mut buf)
8313            .expect("overlay read");
8314        assert_eq!(buf, b"overlay\n");
8315
8316        let custom = OdbBuilder::files(tmp.path().join("custom/objects"))
8317            .primary(Arc::new(MemoryStore::new(HashAlgo::Sha1)))
8318            .build();
8319        let custom_oid = custom
8320            .write(ObjectKind::Blob, b"custom\n")
8321            .expect("custom object");
8322        custom
8323            .read_into_caching_delta_bases(&custom_oid, &mut buf)
8324            .expect("custom read");
8325        assert_eq!(buf, b"custom\n");
8326
8327        let missing = HashAlgo::Sha1
8328            .hash_object(ObjectKind::Blob, b"missing")
8329            .expect("missing id");
8330        assert!(matches!(
8331            custom.read_into_caching_delta_bases(&missing, &mut buf),
8332            Err(Error::ObjectNotFound(_))
8333        ));
8334    }
8335
8336    #[test]
8337    fn delta_base_cache_invalidated_when_pack_bytes_reloaded() {
8338        use filetime::{set_file_mtime, FileTime};
8339        let _guard = pack_cache_test_guard();
8340        clear_pack_cache();
8341        let Some((_tmp, idx, tip, _odb)) = build_ofs_delta_chain_pack(8) else {
8342            return;
8343        };
8344        let pack_path = idx.pack_path.clone();
8345        let pack_id = pack_cache::pack_id_for(&pack_path);
8346        let mid_off = idx
8347            .iter()
8348            .nth(4)
8349            .map(|e| e.offset())
8350            .expect("mid hop")
8351            .expect("mid off");
8352        let _ = read_object_from_pack(&idx, &tip).expect("warm cache");
8353        assert!(
8354            pack_cache::test_delta_base_cached(&pack_path, pack_id, mid_off),
8355            "expected intermediate cached at offset {mid_off}"
8356        );
8357        set_file_mtime(&pack_path, FileTime::now()).expect("touch pack mtime");
8358        assert!(
8359            pack_cache::revalidate_stale_pack_bytes(&pack_path).expect("revalidate"),
8360            "expected pack bytes reload"
8361        );
8362        assert!(
8363            !pack_cache::test_delta_base_cached(&pack_path, pack_id, mid_off),
8364            "cache must drop when pack bytes reload"
8365        );
8366    }
8367
8368    #[cfg_attr(
8369        not(feature = "full-tests"),
8370        ignore = "starts system git or is heavy; run with --features full-tests"
8371    )]
8372    #[test]
8373    fn git_repack_depth_250_matches_cat_file_batch() {
8374        let _guard = pack_cache_test_guard();
8375        clear_pack_cache();
8376        let tmp = tempfile::tempdir().expect("tempdir");
8377        let dir = tmp.path();
8378        if Command::new("git") // hygiene: git fixture comparison in cfg(test) module
8379            .current_dir(dir)
8380            .args(["init", "-q"])
8381            .env("GIT_CONFIG_GLOBAL", "/dev/null")
8382            .env("GIT_CONFIG_SYSTEM", "/dev/null")
8383            .status()
8384            .is_err()
8385        {
8386            let _ = "SKIP: git unavailable";
8387            return;
8388        }
8389        for i in 0..64 {
8390            std::fs::write(dir.join(format!("f{i}.txt")), format!("payload {i}\n")).unwrap();
8391            git(dir, &["add", &format!("f{i}.txt")]);
8392            git(dir, &["commit", "-qm", &format!("c{i}")]);
8393        }
8394        git(dir, &["repack", "-adf", "--depth=250", "--window=250"]);
8395        let objects = dir.join(".git/objects");
8396        let indexes = read_local_pack_indexes_cached(&objects).expect("pack indexes");
8397        let idx = indexes
8398            .into_iter()
8399            .max_by_key(|i| i.len())
8400            .expect("repack pack");
8401        for entry in idx.iter() {
8402            let oid = ObjectId::from_bytes(entry.oid()).expect("oid");
8403            let grit_obj = read_object_from_pack(&idx, &oid).expect("grit read");
8404            let git_type = Command::new("git") // hygiene: git fixture comparison in cfg(test) module
8405                .current_dir(dir)
8406                .args(["cat-file", "-t", &oid.to_hex()])
8407                .env("GIT_CONFIG_GLOBAL", "/dev/null")
8408                .env("GIT_CONFIG_SYSTEM", "/dev/null")
8409                .output()
8410                .expect("git cat-file -t");
8411            assert!(git_type.status.success());
8412            let git_kind = ObjectKind::from_bytes(
8413                std::str::from_utf8(git_type.stdout.trim_ascii_end())
8414                    .expect("utf8 type")
8415                    .as_bytes(),
8416            )
8417            .expect("git type");
8418            assert_eq!(git_kind, grit_obj.kind);
8419            let type_arg = match grit_obj.kind {
8420                ObjectKind::Blob => "blob",
8421                ObjectKind::Tree => "tree",
8422                ObjectKind::Commit => "commit",
8423                ObjectKind::Tag => "tag",
8424            };
8425            let git_payload = Command::new("git") // hygiene: git fixture comparison in cfg(test) module
8426                .current_dir(dir)
8427                .args(["cat-file", type_arg, &oid.to_hex()])
8428                .env("GIT_CONFIG_GLOBAL", "/dev/null")
8429                .env("GIT_CONFIG_SYSTEM", "/dev/null")
8430                .output()
8431                .expect("git cat-file raw");
8432            assert!(git_payload.status.success());
8433            assert_eq!(git_payload.stdout, grit_obj.data);
8434        }
8435    }
8436
8437    #[test]
8438    fn cyclic_ref_delta_chain_across_two_packs_errors() {
8439        let _guard = pack_cache_test_guard();
8440        clear_pack_cache();
8441        let odb = Odb::new(tempfile::tempdir().expect("tempdir").path());
8442        let content_a = b"aaa".as_slice();
8443        let content_b = b"bbb".as_slice();
8444        let oid_a = odb.hash(ObjectKind::Blob, content_a).expect("hash");
8445        let oid_b = odb.hash(ObjectKind::Blob, content_b).expect("hash");
8446        let delta_a = encode_lcp_delta(content_b, content_a).expect("delta a");
8447        let delta_b = encode_lcp_delta(content_a, content_b).expect("delta b");
8448
8449        let mut pack_a = Vec::new();
8450        pack_a.extend_from_slice(b"PACK");
8451        pack_a.extend_from_slice(&2u32.to_be_bytes());
8452        pack_a.extend_from_slice(&1u32.to_be_bytes());
8453        let off_a = pack_a.len() as u64;
8454        append_ref_delta(&mut pack_a, &oid_b, &delta_a);
8455        append_sha1_pack_trailer(&mut pack_a);
8456
8457        let mut pack_b = Vec::new();
8458        pack_b.extend_from_slice(b"PACK");
8459        pack_b.extend_from_slice(&2u32.to_be_bytes());
8460        pack_b.extend_from_slice(&1u32.to_be_bytes());
8461        let off_b = pack_b.len() as u64;
8462        append_ref_delta(&mut pack_b, &oid_a, &delta_b);
8463        append_sha1_pack_trailer(&mut pack_b);
8464
8465        let dir = tempfile::tempdir().expect("tempdir");
8466        let objects = dir.path().join("objects");
8467        let idx_b = install_synthetic_pack(&objects, "cycle-b", &pack_b, &[(oid_b, off_b)]);
8468        let idx_a = install_synthetic_pack(&objects, "cycle-a", &pack_a, &[(oid_a, off_a)]);
8469        assert_eq!(idx_a.len(), 1);
8470        assert_eq!(idx_b.len(), 1);
8471        assert!(idx_b.contains(&oid_b).expect("contains"));
8472        let _ = idx_b;
8473
8474        let err = read_object_from_pack(&idx_a, &oid_a).expect_err("cross-pack cycle");
8475        assert!(
8476            matches!(err, Error::DeltaChainTooDeep { limit: 4096 }),
8477            "expected typed delta chain limit, got {err:?}"
8478        );
8479    }
8480
8481    #[test]
8482    fn cross_pack_ref_delta_resolves_through_odb_read() {
8483        let _guard = pack_cache_test_guard();
8484        clear_pack_cache();
8485        let tmp = tempfile::tempdir().expect("tempdir");
8486        let objects = tmp.path().join("objects");
8487        let odb = Odb::new(&objects);
8488        let mut random = 17u64;
8489        let base: Vec<u8> = (0..20000)
8490            .map(|_| {
8491                random ^= random << 13;
8492                random ^= random >> 7;
8493                random ^= random << 17;
8494                random as u8
8495            })
8496            .collect();
8497        let mut target = base.clone();
8498        target.extend_from_slice(b"target suffix");
8499        let oid_base = odb.hash(ObjectKind::Blob, &base).expect("hash");
8500        let delta = encode_lcp_delta(&base, &target).expect("delta");
8501        let oid_target = odb.hash(ObjectKind::Blob, &target).expect("hash");
8502
8503        let mut pack_base = Vec::new();
8504        pack_base.extend_from_slice(b"PACK");
8505        pack_base.extend_from_slice(&2u32.to_be_bytes());
8506        pack_base.extend_from_slice(&1u32.to_be_bytes());
8507        let off_base = append_whole_blob(&mut pack_base, &base);
8508        append_sha1_pack_trailer(&mut pack_base);
8509
8510        let mut pack_tip = Vec::new();
8511        pack_tip.extend_from_slice(b"PACK");
8512        pack_tip.extend_from_slice(&2u32.to_be_bytes());
8513        pack_tip.extend_from_slice(&1u32.to_be_bytes());
8514        let off_tip = pack_tip.len() as u64;
8515        append_ref_delta(&mut pack_tip, &oid_base, &delta);
8516        append_sha1_pack_trailer(&mut pack_tip);
8517
8518        install_synthetic_pack(&objects, "base", &pack_base, &[(oid_base, off_base)]);
8519        install_synthetic_pack(&objects, "tip", &pack_tip, &[(oid_target, off_tip)]);
8520
8521        for reader in [&odb, &odb.with_bounded_pack_cache()] {
8522            let got = reader.read(&oid_target).expect("cross-pack ref-delta");
8523            assert_eq!(got.kind, ObjectKind::Blob);
8524            assert_eq!(got.data, target);
8525            assert_eq!(reader.hash(got.kind, &got.data).expect("hash"), oid_target);
8526        }
8527    }
8528
8529    #[test]
8530    fn corrupt_in_pack_base_rescued_from_loose() {
8531        let _guard = pack_cache_test_guard();
8532        let tmp = tempfile::tempdir().expect("tempdir");
8533        let objects = tmp.path().join("objects");
8534        std::fs::create_dir_all(objects.join("pack")).expect("dirs");
8535        let odb = Odb::new(&objects);
8536        let base = b"good-base-bytes";
8537        let tip = b"good-base-bytes!";
8538        let oid_base = odb.write_local(ObjectKind::Blob, base).expect("loose base");
8539        let delta = encode_lcp_delta(base, tip).expect("delta");
8540        let oid_tip = odb.hash(ObjectKind::Blob, tip).expect("hash");
8541
8542        let mut pack = Vec::new();
8543        pack.extend_from_slice(b"PACK");
8544        pack.extend_from_slice(&2u32.to_be_bytes());
8545        pack.extend_from_slice(&2u32.to_be_bytes());
8546        let off_base = append_corrupt_whole_blob(&mut pack);
8547        let off_tip = pack.len() as u64;
8548        append_ref_delta(&mut pack, &oid_base, &delta);
8549        append_sha1_pack_trailer(&mut pack);
8550
8551        let pack_path = objects.join("pack/rescue.pack");
8552        std::fs::write(&pack_path, &pack).expect("write pack");
8553        let idx = pack_index_for_objects(pack_path, &[(oid_base, off_base), (oid_tip, off_tip)]);
8554
8555        let got = read_object_from_pack(&idx, &oid_tip).expect("rescue read");
8556        assert_eq!(got.kind, ObjectKind::Blob);
8557        assert_eq!(got.data, tip);
8558    }
8559
8560    #[test]
8561    fn pack_added_after_first_lookup_found_on_miss() {
8562        let _guard = pack_cache_test_guard();
8563        clear_pack_cache();
8564        let tmp = tempfile::tempdir().expect("tempdir");
8565        let objects = tmp.path().join("objects");
8566        let pack_dir = objects.join("pack");
8567        let (pack_a, oid_a, off_a) = single_blob_pack(b"pack-a");
8568        install_synthetic_pack(&objects, "first", &pack_a, &[(oid_a, off_a)]);
8569        pack_cache::test_reset_dir_rescan_count(&pack_dir);
8570
8571        read_object_from_packs(&objects, &oid_a).expect("warm cache");
8572        assert_eq!(
8573            pack_cache::test_dir_rescan_count(&pack_dir),
8574            1,
8575            "initial lookup should scan the pack directory once"
8576        );
8577
8578        // Ensure the pack directory mtime advances past the cached stamp (second resolution).
8579        std::thread::sleep(std::time::Duration::from_secs(1));
8580
8581        let (pack_b, oid_b, off_b) = single_blob_pack(b"pack-b");
8582        install_synthetic_pack_no_clear(&objects, "second", &pack_b, &[(oid_b, off_b)]);
8583        assert_eq!(
8584            pack_cache::test_dir_rescan_count(&pack_dir),
8585            1,
8586            "adding a pack must not rescan until a miss"
8587        );
8588
8589        read_object_from_packs(&objects, &oid_b).expect("new pack via miss path");
8590        assert_eq!(
8591            pack_cache::test_dir_rescan_count(&pack_dir),
8592            2,
8593            "miss path should re-scan when pack dir mtime changed"
8594        );
8595    }
8596
8597    #[test]
8598    fn pack_added_same_pack_dir_mtime_found_on_miss() {
8599        use crate::odb::Odb;
8600
8601        let _guard = pack_cache_test_guard();
8602        clear_pack_cache();
8603        let tmp = tempfile::tempdir().expect("tempdir");
8604        let objects = tmp.path().join("objects");
8605        let pack_dir = objects.join("pack");
8606        let (pack_a, oid_a, off_a) = single_blob_pack(b"pack-a");
8607        install_synthetic_pack(&objects, "first", &pack_a, &[(oid_a, off_a)]);
8608
8609        let odb = Odb::new(&objects);
8610        odb.read(&oid_a).expect("warm odb cache");
8611
8612        let saved_mt = filetime::FileTime::from_last_modification_time(
8613            &std::fs::metadata(&pack_dir).expect("pack dir meta"),
8614        );
8615        let (pack_b, oid_b, off_b) = single_blob_pack(b"pack-b-same-second");
8616        install_synthetic_pack_no_clear(&objects, "second", &pack_b, &[(oid_b, off_b)]);
8617        filetime::set_file_mtime(&pack_dir, saved_mt).expect("restore pack dir mtime");
8618
8619        odb.read(&oid_b)
8620            .expect("new pack visible after idx-set compare on miss");
8621    }
8622
8623    #[test]
8624    fn absent_object_misses_do_not_rescan_pack_dir() {
8625        let _guard = pack_cache_test_guard();
8626        clear_pack_cache();
8627        let tmp = tempfile::tempdir().expect("tempdir");
8628        let objects = tmp.path().join("objects");
8629        let pack_dir = objects.join("pack");
8630        let (pack, oid, off) = single_blob_pack(b"cached-blob");
8631        install_synthetic_pack(&objects, "only", &pack, &[(oid, off)]);
8632        pack_cache::test_reset_dir_rescan_count(&pack_dir);
8633
8634        read_object_from_packs(&objects, &oid).expect("warm cache");
8635        let after_warm = pack_cache::test_dir_rescan_count(&pack_dir);
8636        let missing = ObjectId::from_hex("0000000000000000000000000000000000000001").expect("oid");
8637        for _ in 0..128 {
8638            assert!(matches!(
8639                read_object_from_packs(&objects, &missing),
8640                Err(Error::ObjectNotFound(_))
8641            ));
8642        }
8643        assert_eq!(
8644            pack_cache::test_dir_rescan_count(&pack_dir),
8645            after_warm,
8646            "absent oid misses must not re-scan the pack directory"
8647        );
8648    }
8649
8650    #[test]
8651    fn known_object_lookup_does_not_rescan() {
8652        let _guard = pack_cache_test_guard();
8653        clear_pack_cache();
8654        let tmp = tempfile::tempdir().expect("tempdir");
8655        let objects = tmp.path().join("objects");
8656        let pack_dir = objects.join("pack");
8657        pack_cache::test_reset_dir_rescan_count(&pack_dir);
8658        let (pack, oid, off) = single_blob_pack(b"cached-blob");
8659        install_synthetic_pack(&objects, "only", &pack, &[(oid, off)]);
8660
8661        read_object_from_packs(&objects, &oid).expect("prime cache");
8662        let after_first = pack_cache::test_dir_rescan_count(&pack_dir);
8663        for _ in 0..64 {
8664            read_object_from_packs(&objects, &oid).expect("repeat read");
8665        }
8666        assert_eq!(
8667            pack_cache::test_dir_rescan_count(&pack_dir),
8668            after_first,
8669            "repeated hits must not re-scan the pack directory"
8670        );
8671    }
8672
8673    #[test]
8674    fn two_repositories_concurrent_reads() {
8675        use crate::odb::Odb;
8676        use std::sync::Arc;
8677
8678        let _guard = pack_cache_test_guard();
8679        clear_pack_cache();
8680        let tmp_a = tempfile::tempdir().expect("tempdir a");
8681        let tmp_b = tempfile::tempdir().expect("tempdir b");
8682        let objects_a = tmp_a.path().join("objects");
8683        let objects_b = tmp_b.path().join("objects");
8684        let pack_a_dir = objects_a.join("pack");
8685        let pack_b_dir = objects_b.join("pack");
8686        let (pack_a, oid_a, off_a) = single_blob_pack(b"parallel-a");
8687        let (pack_b, oid_b, off_b) = single_blob_pack(b"parallel-b");
8688        install_synthetic_pack(&objects_a, "a", &pack_a, &[(oid_a, off_a)]);
8689        install_synthetic_pack(&objects_b, "b", &pack_b, &[(oid_b, off_b)]);
8690        let odb_a = Odb::new(&objects_a);
8691        let odb_b = Odb::new(&objects_b);
8692        assert!(
8693            !Arc::ptr_eq(odb_a.pack_store().unwrap(), odb_b.pack_store().unwrap()),
8694            "independent Odb opens must not share pack stores"
8695        );
8696        pack_cache::test_reset_dir_rescan_count_on_store(odb_a.pack_store().unwrap(), &pack_a_dir);
8697        pack_cache::test_reset_dir_rescan_count_on_store(odb_b.pack_store().unwrap(), &pack_b_dir);
8698
8699        let barrier = Arc::new(std::sync::Barrier::new(2));
8700        let b0 = Arc::clone(&barrier);
8701        let b1 = Arc::clone(&barrier);
8702        let odb_a_t = odb_a.clone();
8703        let odb_b_t = odb_b.clone();
8704        let id_a = oid_a;
8705        let id_b = oid_b;
8706
8707        let h1 = std::thread::spawn(move || {
8708            b0.wait();
8709            odb_a_t.read(&id_a).expect("read a");
8710        });
8711        let h2 = std::thread::spawn(move || {
8712            b1.wait();
8713            odb_b_t.read(&id_b).expect("read b");
8714        });
8715        h1.join().expect("thread a");
8716        h2.join().expect("thread b");
8717
8718        assert_eq!(
8719            pack_cache::test_dir_rescan_count_on_store(odb_a.pack_store().unwrap(), &pack_a_dir),
8720            1
8721        );
8722        assert_eq!(
8723            pack_cache::test_dir_rescan_count_on_store(odb_b.pack_store().unwrap(), &pack_b_dir),
8724            1
8725        );
8726    }
8727
8728    #[test]
8729    fn stale_cached_pack_bytes_revalidated_before_corruption_error() {
8730        let _guard = pack_cache_test_guard();
8731        clear_pack_cache();
8732        let tmp = tempfile::tempdir().expect("tempdir");
8733        let objects = tmp.path().join("objects");
8734        let (pack, oid, off) = single_blob_pack(b"revalidate-me");
8735        let idx = install_synthetic_pack(&objects, "good", &pack, &[(oid, off)]);
8736        read_object_from_pack(&idx, &oid).expect("prime pack bytes cache");
8737
8738        let good_on_disk = std::fs::read(&idx.pack_path).expect("read pack");
8739        pack_cache::test_inject_stale_pack_bytes(
8740            &idx.pack_path,
8741            PackData::from_owned(b"BAD".to_vec()),
8742        );
8743        std::fs::write(&idx.pack_path, &good_on_disk).expect("refresh on-disk pack");
8744        let got = read_object_from_pack(&idx, &oid).expect("revalidate from disk");
8745        assert_eq!(got.data, b"revalidate-me");
8746    }
8747
8748    #[test]
8749    fn replaced_pack_same_path_reread_after_invalidation() {
8750        let _guard = pack_cache_test_guard();
8751        clear_pack_cache();
8752        let tmp = tempfile::tempdir().expect("tempdir");
8753        let objects = tmp.path().join("objects");
8754        std::fs::create_dir_all(objects.join("pack")).expect("pack dir");
8755        let pack_path = objects.join("pack/repack-me.pack");
8756        let idx_path = objects.join("pack/repack-me.idx");
8757        const OBJECT_OFFSET: u64 = 9000;
8758        let (oid_a, off_a) =
8759            write_sparse_single_blob_pack(tmp.path(), &pack_path, b"before-repack", OBJECT_OFFSET)
8760                .expect("sparse pack a");
8761        assert!(
8762            std::fs::metadata(&pack_path).expect("stat").len() > 8192,
8763            "fixture pack must exceed mmap owned-buffer threshold"
8764        );
8765        write_v2_idx_for_test(&idx_path, &pack_path, &[(oid_a, off_a)]).expect("idx a");
8766        let idx = read_pack_index(&idx_path).expect("parse idx a");
8767        read_object_from_pack(&idx, &oid_a).expect("prime cache");
8768
8769        let (oid_b, off_b) = write_sparse_single_blob_pack(
8770            tmp.path(),
8771            &pack_path,
8772            b"after-repack-content",
8773            OBJECT_OFFSET,
8774        )
8775        .expect("sparse pack b");
8776        write_v2_idx_for_test(&idx_path, &pack_path, &[(oid_b, off_b)]).expect("idx b");
8777
8778        assert!(
8779            pack_cache::revalidate_stale_pack_bytes(&pack_path).expect("revalidate"),
8780            "replacement must invalidate cached bytes"
8781        );
8782        let idx = read_pack_index(&idx_path).expect("reload idx after pack swap");
8783        let got = read_object_from_pack(&idx, &oid_b).expect("read from replaced pack");
8784        assert_eq!(got.data, b"after-repack-content");
8785    }
8786
8787    #[test]
8788    fn read_object_at_large_pack_offset_via_mmap() {
8789        use std::fs::OpenOptions;
8790        use std::io::{Seek, SeekFrom, Write};
8791
8792        let _guard = pack_cache_test_guard();
8793        clear_pack_cache();
8794        let tmp = tempfile::tempdir().expect("tempdir");
8795        let objects = tmp.path().join("objects");
8796        std::fs::create_dir_all(objects.join("pack")).expect("pack dir");
8797
8798        let data = b"large-offset-blob";
8799        let odb = Odb::new(tmp.path());
8800        let oid = odb.hash(ObjectKind::Blob, data).expect("hash");
8801
8802        let pack_path = objects.join("pack/large-off.pack");
8803        let mut file = OpenOptions::new()
8804            .create(true)
8805            .write(true)
8806            .read(true)
8807            .open(&pack_path)
8808            .expect("create sparse pack");
8809        file.write_all(b"PACK").expect("sig");
8810        file.write_all(&2u32.to_be_bytes()).expect("ver");
8811        file.write_all(&1u32.to_be_bytes()).expect("count");
8812        let object_offset: u64 = 1 << 32;
8813        file.seek(SeekFrom::Start(object_offset))
8814            .expect("seek past 4GiB");
8815        let mut tail = Vec::new();
8816        append_whole_blob(&mut tail, data);
8817        file.write_all(&tail).expect("object at huge offset");
8818        let trailer = sha1_trailer_for_open_file(&mut file).expect("trailer hash");
8819        file.write_all(trailer.as_bytes()).expect("append trailer");
8820        file.flush().expect("flush");
8821        let file_len = file.metadata().expect("meta").len();
8822        drop(file);
8823        assert!(file_len > object_offset);
8824
8825        let idx_path = pack_path.with_extension("idx");
8826        write_v2_pack_index_with_trailer(
8827            &idx_path,
8828            &[(oid, object_offset, 0)],
8829            trailer.as_bytes(),
8830            20,
8831            None,
8832        )
8833        .expect("idx");
8834
8835        let idx = read_pack_index(&idx_path).expect("parse idx");
8836        assert_eq!(idx.offset_at(0).expect("off"), object_offset);
8837        let got = read_object_from_pack(&idx, &oid).expect("read at large offset");
8838        assert_eq!(got.data, data);
8839    }
8840
8841    fn synthetic_index_entries(n: u32) -> Vec<(ObjectId, u64, u32)> {
8842        (0..n)
8843            .map(|i| {
8844                let digest = HashAlgo::Sha1.checksum_digest(&i.to_be_bytes());
8845                let oid = ObjectId::from_bytes(&digest).expect("oid");
8846                let offset = if i % 7 == 0 {
8847                    (1u64 << 31) + u64::from(i)
8848                } else {
8849                    12 + u64::from(i) * 40
8850                };
8851                (oid, offset, i.wrapping_mul(2_654_435_761))
8852            })
8853            .collect()
8854    }
8855
8856    #[test]
8857    fn index_entries_sort_in_place_on_any_thread_count() {
8858        for n in [0_u32, 1, 300, 20_000] {
8859            let mut reference = synthetic_index_entries(n);
8860            reference.sort_by(|a, b| a.0.as_bytes().cmp(b.0.as_bytes()));
8861            let mut fanout_ref = [0u32; 256];
8862            for (oid, _, _) in &reference {
8863                for slot in &mut fanout_ref[usize::from(oid.as_bytes()[0])..] {
8864                    *slot += 1;
8865                }
8866            }
8867            for threads in [None, Some(1), Some(2), Some(3), Some(8)] {
8868                let mut entries = synthetic_index_entries(n);
8869                let fanout = sort_index_entries(&mut entries, threads);
8870                assert_eq!(entries, reference, "n={n} threads={threads:?}");
8871                assert_eq!(fanout, fanout_ref, "n={n} threads={threads:?}");
8872            }
8873        }
8874    }
8875
8876    #[test]
8877    fn index_written_in_place_reads_back_every_row_with_large_offsets() {
8878        let tmp = tempfile::tempdir().expect("tempdir");
8879        let idx_path = tmp.path().join("p.idx");
8880        let rows = synthetic_index_entries(20_000);
8881        let trailer = [0x5a_u8; 20];
8882        let mut entries = rows.clone();
8883        write_v2_pack_index_sorting(&idx_path, &mut entries, &trailer, 20, Some(4))
8884            .expect("write idx");
8885        let idx = read_pack_index(&idx_path).expect("read idx");
8886        assert_eq!(idx.len(), rows.len());
8887        let mut want = rows;
8888        want.sort_by(|a, b| a.0.as_bytes().cmp(b.0.as_bytes()));
8889        for (pos, (oid, offset, crc)) in want.iter().enumerate() {
8890            assert_eq!(idx.oid_at(pos), oid.as_bytes(), "row {pos}");
8891            assert_eq!(idx.offset_at(pos).expect("offset"), *offset, "row {pos}");
8892            assert_eq!(idx.crc32_at(pos).expect("crc"), *crc, "row {pos}");
8893        }
8894
8895        let mut bad = vec![(ObjectId::from_bytes(&[1; 32]).expect("oid"), 12, 0)];
8896        assert!(matches!(
8897            write_v2_pack_index_sorting(&idx_path, &mut bad, &trailer, 20, None),
8898            Err(Error::CorruptObject(_))
8899        ));
8900        assert!(matches!(
8901            write_v2_pack_index_sorting(&idx_path, &mut entries, &trailer[..19], 20, None),
8902            Err(Error::CorruptObject(_))
8903        ));
8904    }
8905
8906    /// Hash pack bytes already written to `file` (from offset 0 through current EOF) for a SHA-1 trailer.
8907    fn sha1_trailer_for_open_file(file: &mut std::fs::File) -> Result<ObjectId> {
8908        use std::io::{Read, Seek, SeekFrom};
8909        let len = file.metadata().map_err(Error::Io)?.len();
8910        file.seek(SeekFrom::Start(0)).map_err(Error::Io)?;
8911        let mut hasher = HashAlgo::Sha1.checksum_hasher();
8912        let mut buf = [0u8; 64 * 1024];
8913        let mut left = len;
8914        while left > 0 {
8915            let chunk = left.min(buf.len() as u64) as usize;
8916            let n = file.read(&mut buf[..chunk]).map_err(Error::Io)?;
8917            if n == 0 {
8918                break;
8919            }
8920            hasher.update(&buf[..n]);
8921            left -= n as u64;
8922        }
8923        let digest = hasher.finalize();
8924        ObjectId::from_bytes(&digest).map_err(|e| Error::InvalidObjectId(format!("{e}")))
8925    }
8926
8927    #[test]
8928    fn cleared_cache_sees_removed_packs_disappear() {
8929        let _guard = pack_cache_test_guard();
8930        clear_pack_cache();
8931        let tmp = tempfile::tempdir().expect("tempdir");
8932        let objects = tmp.path().join("objects");
8933        let (pack, oid, off) = single_blob_pack(b"ephemeral");
8934        install_synthetic_pack(&objects, "gone", &pack, &[(oid, off)]);
8935        read_object_from_packs(&objects, &oid).expect("warm cache");
8936
8937        let pack_path = objects.join("pack/gone.pack");
8938        let idx_path = objects.join("pack/gone.idx");
8939        std::fs::remove_file(&pack_path).expect("remove pack");
8940        std::fs::remove_file(&idx_path).expect("remove idx");
8941        clear_pack_cache();
8942
8943        let indexes = read_local_pack_indexes_cached(&objects).expect("rescan");
8944        assert!(
8945            indexes.is_empty(),
8946            "after clear, listing should not retain removed packs"
8947        );
8948    }
8949
8950    #[test]
8951    fn repack_while_sibling_odb_holds_store_reprepares_on_miss() {
8952        use crate::odb::Odb;
8953
8954        let _guard = pack_cache_test_guard();
8955        clear_pack_cache();
8956        let tmp = tempfile::tempdir().expect("tempdir");
8957        let objects = tmp.path().join("objects");
8958        let (pack_old, oid_old, off_old) = single_blob_pack(b"before");
8959        install_synthetic_pack(&objects, "old", &pack_old, &[(oid_old, off_old)]);
8960        let odb_a = Odb::new(&objects);
8961        let odb_b = Odb::new(&objects);
8962        assert!(
8963            !std::sync::Arc::ptr_eq(odb_a.pack_store().unwrap(), odb_b.pack_store().unwrap()),
8964            "sibling Odbs must have distinct pack stores"
8965        );
8966        odb_b.read(&oid_old).expect("warm b store");
8967
8968        std::fs::remove_file(objects.join("pack/old.pack")).expect("rm pack");
8969        std::fs::remove_file(objects.join("pack/old.idx")).expect("rm idx");
8970        let (pack_new, oid_new, off_new) = single_blob_pack(b"after");
8971        install_synthetic_pack(&objects, "new", &pack_new, &[(oid_new, off_new)]);
8972        filetime::set_file_mtime(objects.join("pack"), filetime::FileTime::now())
8973            .expect("bump pack dir mtime for reprepare");
8974        odb_a.invalidate_packs();
8975
8976        // B's store is unchanged by A's invalidation; a miss on the new oid rescans pack/.
8977        let got = odb_b.read(&oid_new).expect("b reprepares after miss");
8978        assert_eq!(got.data.as_slice(), b"after");
8979    }
8980
8981    #[test]
8982    fn fresh_store_does_not_trust_a_listing_left_by_another_store() {
8983        let dir = tempfile::tempdir().expect("tempdir");
8984        let dir = dir.path();
8985        git(dir, &["init", "-q"]);
8986        git(dir, &["commit", "-q", "--allow-empty", "-m", "a"]);
8987        git(dir, &["repack", "-adq"]);
8988        let objects = dir.join(".git/objects");
8989        let pack_names = |indexes: Vec<Arc<PackIndex>>| -> Vec<PathBuf> {
8990            indexes
8991                .into_iter()
8992                .map(|idx| idx.pack_path.clone())
8993                .collect()
8994        };
8995
8996        let first = Arc::new(crate::pack_store::PackStore::new(objects.clone()));
8997        let before = crate::pack_store::PackStore::with_context(first, || {
8998            read_local_pack_indexes_cached(&objects)
8999        })
9000        .expect("first listing");
9001
9002        // Another process replaces the pack.
9003        git(dir, &["commit", "-q", "--allow-empty", "-m", "b"]);
9004        git(dir, &["repack", "-adq"]);
9005
9006        let second = Arc::new(crate::pack_store::PackStore::new(objects.clone()));
9007        let after = crate::pack_store::PackStore::with_context(second, || {
9008            read_local_pack_indexes_cached(&objects)
9009        })
9010        .expect("second listing");
9011        let (before, after) = (pack_names(before), pack_names(after));
9012        assert_ne!(before, after);
9013        assert!(after.iter().all(|p| p.is_file()), "{after:?}");
9014    }
9015
9016    /// A pack holding one commit entry whose zlib stream uses stored blocks, so it takes more
9017    /// compressed bytes than the commit has.
9018    fn stored_commit_entry(body: &[u8]) -> Arc<PackData> {
9019        let mut entry = Vec::new();
9020        crate::pack_objects::encode_pack_object_header(&mut entry, 1, body.len());
9021        let mut zlib = ZlibEncoder::new(Vec::new(), Compression::none());
9022        zlib.write_all(body).unwrap();
9023        entry.extend(zlib.finish().unwrap());
9024        entry.extend_from_slice(&[0u8; 64]);
9025        PackData::from_owned(entry)
9026    }
9027
9028    fn commit_with_long_headers(header_padding: usize) -> (Vec<u8>, usize) {
9029        let mut body = format!(
9030            "tree {}\nauthor a <a@example.com> 1 +0000\ncommitter c <c@example.com> 2 +0000\n",
9031            "4b825dc642cb6eb9a060e54bf8d69288e0ee4904"
9032        )
9033        .into_bytes();
9034        body.extend_from_slice(b"x-pad ");
9035        body.extend(std::iter::repeat_n(b'p', header_padding));
9036        body.extend_from_slice(b"\n\n");
9037        let header_len = body.len();
9038        body.extend(std::iter::repeat_n(b'm', 2000));
9039        (body, header_len)
9040    }
9041
9042    #[test]
9043    fn commit_header_block_read_takes_a_second_read_when_the_first_is_short() {
9044        // Headers just inside the budget, which stored blocks push past the first read.
9045        let (_, unpadded) = commit_with_long_headers(0);
9046        let (body, header_len) = commit_with_long_headers(COMMIT_HEADER_BLOCK_MAX - 4 - unpadded);
9047        assert!(header_len < COMMIT_HEADER_BLOCK_MAX);
9048        let pack = stored_commit_entry(&body);
9049        let mut zlib = ZlibInflateScratch::default();
9050        let mut out = Vec::new();
9051
9052        let mut window = vec![0u8; COMMIT_HEADER_BLOCK_WINDOW];
9053        assert!(read_commit_header_block_from(
9054            &pack,
9055            0,
9056            &mut window,
9057            &mut zlib,
9058            &mut out
9059        ));
9060        assert_eq!(out, &body[..header_len]);
9061
9062        let mut first_read_only = vec![0u8; COMMIT_HEADER_BLOCK_FIRST_READ];
9063        assert!(
9064            !read_commit_header_block_from(&pack, 0, &mut first_read_only, &mut zlib, &mut out),
9065            "the stored stream does not fit the first read alone"
9066        );
9067    }
9068
9069    #[test]
9070    fn commit_header_block_read_rejects_headers_past_the_inflate_budget() {
9071        let (body, header_len) = commit_with_long_headers(COMMIT_HEADER_BLOCK_MAX);
9072        assert!(header_len > COMMIT_HEADER_BLOCK_MAX);
9073        let pack = stored_commit_entry(&body);
9074        let mut window = vec![0u8; COMMIT_HEADER_BLOCK_WINDOW];
9075        let mut out = Vec::new();
9076        assert!(!read_commit_header_block_from(
9077            &pack,
9078            0,
9079            &mut window,
9080            &mut ZlibInflateScratch::default(),
9081            &mut out
9082        ));
9083    }
9084
9085    #[test]
9086    fn commit_header_block_read_rejects_a_stream_longer_than_the_declared_size() {
9087        let (body, header_len) = commit_with_long_headers(10);
9088        let mut entry = Vec::new();
9089        crate::pack_objects::encode_pack_object_header(&mut entry, 1, header_len + 5);
9090        let mut zlib = ZlibEncoder::new(Vec::new(), Compression::default());
9091        zlib.write_all(&body).unwrap();
9092        entry.extend(zlib.finish().unwrap());
9093        let pack = PackData::from_owned(entry);
9094        let mut window = vec![0u8; COMMIT_HEADER_BLOCK_WINDOW];
9095        let mut out = Vec::new();
9096        assert!(!read_commit_header_block_from(
9097            &pack,
9098            0,
9099            &mut window,
9100            &mut ZlibInflateScratch::default(),
9101            &mut out
9102        ));
9103    }
9104}
9105#[cfg(test)]
9106mod cached_lookup_tests {
9107    use super::*;
9108    use std::fs;
9109    use std::process::Command;
9110    use tempfile::TempDir;
9111
9112    fn git(dir: &Path, args: &[&str]) {
9113        let out = Command::new("git") // hygiene: git fixture comparison in cfg(test) module
9114            .current_dir(dir)
9115            .args(args)
9116            .env("GIT_CONFIG_GLOBAL", "/dev/null")
9117            .env("GIT_CONFIG_SYSTEM", "/dev/null")
9118            .env("GIT_AUTHOR_NAME", "t")
9119            .env("GIT_AUTHOR_EMAIL", "t@example.com")
9120            .env("GIT_COMMITTER_NAME", "t")
9121            .env("GIT_COMMITTER_EMAIL", "t@example.com")
9122            .output()
9123            .expect("spawn git");
9124        assert!(
9125            out.status.success(),
9126            "git {:?}: {}",
9127            args,
9128            String::from_utf8_lossy(&out.stderr)
9129        );
9130    }
9131
9132    fn repack_all(dir: &Path) {
9133        git(dir, &["repack", "-a", "-d"]);
9134    }
9135
9136    fn git_stdout(dir: &Path, args: &[&str], stdin: Option<&str>) -> String {
9137        use std::io::Write;
9138        let mut child = Command::new("git") // hygiene: git fixture comparison in cfg(test) module
9139            .current_dir(dir)
9140            .args(args)
9141            .env("GIT_CONFIG_GLOBAL", "/dev/null")
9142            .env("GIT_CONFIG_SYSTEM", "/dev/null")
9143            .stdin(std::process::Stdio::piped())
9144            .stdout(std::process::Stdio::piped())
9145            .spawn()
9146            .expect("spawn git");
9147        if let Some(input) = stdin {
9148            child
9149                .stdin
9150                .as_mut()
9151                .expect("stdin")
9152                .write_all(input.as_bytes())
9153                .expect("write stdin");
9154        }
9155        let out = child.wait_with_output().expect("git output");
9156        assert!(out.status.success(), "git {args:?}");
9157        String::from_utf8(out.stdout)
9158            .expect("utf-8")
9159            .trim()
9160            .to_owned()
9161    }
9162
9163    fn oid_of(dir: &Path, rev: &str) -> ObjectId {
9164        ObjectId::from_hex(&git_stdout(dir, &["rev-parse", rev], None)).expect("oid")
9165    }
9166
9167    #[cfg_attr(
9168        not(feature = "full-tests"),
9169        ignore = "starts system git or is heavy; run with --features full-tests"
9170    )]
9171    #[test]
9172    fn cold_reads_trust_the_pack_listing_after_the_first_scan() {
9173        use crate::hot_path_test_metrics::HotPathMetricsScope;
9174
9175        let _guard = pack_cache_test_guard();
9176        clear_pack_cache();
9177        let dir = TempDir::new().unwrap();
9178        let root = dir.path();
9179        git(root, &["init", "-q"]);
9180        for n in 0..40 {
9181            fs::write(root.join("f"), format!("{n}\n")).unwrap();
9182            git(root, &["add", "f"]);
9183            git(root, &["commit", "-qm", &format!("c{n}")]);
9184        }
9185        git(root, &["gc", "-q"]);
9186        let oids: Vec<ObjectId> = git_stdout(root, &["rev-list", "--objects", "--all"], None)
9187            .lines()
9188            .map(|line| ObjectId::from_hex(&line[..40]).expect("oid"))
9189            .collect();
9190        assert!(oids.len() >= 120);
9191
9192        let repo = crate::repo::Repository::open(
9193            &root.join(".git"),
9194            &crate::environment::RepositoryOptions::empty(),
9195        )
9196        .expect("open");
9197        let metrics = repo.odb().hot_path_metrics_arc();
9198        let _scope = HotPathMetricsScope::install(Arc::clone(&metrics));
9199        metrics.reset_pack_dir_stats();
9200        metrics.set_stamp_counting(true);
9201        for oid in &oids {
9202            repo.odb().read(oid).expect("read");
9203        }
9204        metrics.set_stamp_counting(false);
9205        // The first read scans `objects/pack`; later reads must not re-check its mtime.
9206        assert!(
9207            metrics.pack_dir_stat_calls() <= 8,
9208            "{} pack directory checks for {} reads",
9209            metrics.pack_dir_stat_calls(),
9210            oids.len()
9211        );
9212    }
9213
9214    #[cfg_attr(
9215        not(feature = "full-tests"),
9216        ignore = "starts system git or is heavy; run with --features full-tests"
9217    )]
9218    #[test]
9219    fn packed_commit_header_block_parses_like_the_whole_commit() {
9220        let _guard = pack_cache_test_guard();
9221        clear_pack_cache();
9222        let dir = TempDir::new().unwrap();
9223        let root = dir.path();
9224        git(root, &["init", "-q"]);
9225        fs::write(root.join("f"), "one\n").unwrap();
9226        git(root, &["add", "f"]);
9227        git(root, &["commit", "-qm", "first"]);
9228        fs::write(root.join("f"), "two\n").unwrap();
9229        let long_message = format!("second\n\n{}", "a long body line\n".repeat(200));
9230        git(root, &["commit", "-qam", &long_message]);
9231        git(root, &["tag", "-a", "-m", "annotated", "v1"]);
9232        let signature: String = (0..60)
9233            .map(|i| format!(" signature line {i:03} with some padding text\n"))
9234            .collect();
9235        let signed_body = format!(
9236            "tree {}\nparent {}\nauthor t <t@example.com> 1 +0000\n\
9237             committer t <t@example.com> 2 +0000\ngpgsig -----BEGIN-----\n{signature} \
9238             -----END-----\n\nsigned\n",
9239            oid_of(root, "HEAD^{tree}"),
9240            oid_of(root, "HEAD"),
9241        );
9242        let signed = git_stdout(
9243            root,
9244            &["hash-object", "-t", "commit", "-w", "--stdin"],
9245            Some(&signed_body),
9246        );
9247        git(root, &["update-ref", "refs/heads/signed", &signed]);
9248        git(root, &["repack", "-a", "-d", "--window=0"]);
9249        fs::write(root.join("f"), "three\n").unwrap();
9250        git(root, &["commit", "-qam", "second pack"]);
9251        git(root, &["repack", "-d", "--window=0"]);
9252        fs::write(root.join("f"), "four\n").unwrap();
9253        git(root, &["commit", "-qam", "loose"]);
9254
9255        let objects = root.join(".git/objects");
9256        let mut buf = Vec::new();
9257        for rev in ["HEAD~3", "HEAD~2", "HEAD~1"] {
9258            let oid = oid_of(root, rev);
9259            assert!(
9260                read_packed_commit_header_block_into(&objects, &oid, &mut buf).unwrap(),
9261                "{rev} is a whole packed commit"
9262            );
9263            assert!(buf.ends_with(b"\n\n"), "{rev} ends at the blank line");
9264            let whole = git_stdout(root, &["cat-file", "commit", rev], None);
9265            let whole = crate::objects::CommitRef::parse(whole.as_bytes()).unwrap();
9266            let header = crate::objects::CommitRef::parse(&buf).unwrap();
9267            assert_eq!(header.tree(), whole.tree(), "{rev}");
9268            assert_eq!(header.parents(), whole.parents(), "{rev}");
9269            assert_eq!(header.committer_raw(), whole.committer_raw(), "{rev}");
9270            assert!(header.message_bytes().is_empty(), "{rev}");
9271        }
9272        let signed = ObjectId::from_hex(&signed).unwrap();
9273        for (oid, why) in [
9274            (signed, "headers longer than the inflate budget"),
9275            (oid_of(root, "v1"), "an annotated tag"),
9276            (oid_of(root, "HEAD~1^{tree}"), "a packed tree"),
9277            (oid_of(root, "HEAD"), "a loose commit"),
9278        ] {
9279            assert!(
9280                !read_packed_commit_header_block_into(&objects, &oid, &mut buf).unwrap(),
9281                "{why}"
9282            );
9283        }
9284    }
9285
9286    #[cfg_attr(
9287        not(feature = "full-tests"),
9288        ignore = "starts system git or is heavy; run with --features full-tests"
9289    )]
9290    #[test]
9291    fn packed_commit_header_blocks_match_single_reads() {
9292        let _guard = pack_cache_test_guard();
9293        clear_pack_cache();
9294        let dir = TempDir::new().unwrap();
9295        let root = dir.path();
9296        git(root, &["init", "-q"]);
9297        // Enough commits that the pack index is read with positioned reads.
9298        let commits = 1500;
9299        let mut stream = String::new();
9300        for i in 1..=commits {
9301            let content = format!("revision {i}\n");
9302            let message = format!("commit {i}\n");
9303            stream.push_str(&format!(
9304                "commit refs/heads/main\nmark :{i}\n\
9305                 committer T <t@example.com> {} +0000\ndata {}\n{message}\
9306                 M 100644 inline f\ndata {}\n{content}\n",
9307                1_000_000 + i,
9308                message.len(),
9309                content.len()
9310            ));
9311        }
9312        git_stdout(root, &["fast-import", "--quiet"], Some(&stream));
9313        git(root, &["tag", "-a", "-m", "annotated", "v1", "main~10"]);
9314        git(root, &["repack", "-a", "-d", "--window=0"]);
9315        git(root, &["checkout", "-q", "main"]);
9316        fs::write(root.join("f"), "loose\n").unwrap();
9317        git(root, &["commit", "-qam", "loose"]);
9318
9319        let mut oids: Vec<ObjectId> = (1..commits)
9320            .step_by(7)
9321            .map(|n| oid_of(root, &format!("HEAD~{n}")))
9322            .collect();
9323        oids.extend([
9324            oid_of(root, "v1"),
9325            oid_of(root, "HEAD~1^{tree}"),
9326            oid_of(root, "HEAD"),
9327            ObjectId::from_hex("0123456789abcdef0123456789abcdef01234567").unwrap(),
9328        ]);
9329        let objects = root.join(".git/objects");
9330        let mut batch: Vec<Option<Vec<u8>>> = vec![None; oids.len()];
9331        read_packed_commit_header_blocks(&objects, &oids, |i, header| {
9332            assert!(
9333                batch[i].replace(header.to_vec()).is_none(),
9334                "one call per commit"
9335            );
9336        })
9337        .unwrap();
9338        let mut single = Vec::new();
9339        for (oid, from_batch) in oids.iter().zip(&batch) {
9340            let read = read_packed_commit_header_block_into(&objects, oid, &mut single).unwrap();
9341            assert_eq!(
9342                from_batch.as_deref(),
9343                read.then_some(single.as_slice()),
9344                "{oid}"
9345            );
9346        }
9347        assert_eq!(
9348            batch.iter().filter(|header| header.is_some()).count(),
9349            oids.len() - 4,
9350            "every packed commit, and nothing else, is read"
9351        );
9352    }
9353
9354    fn init_repo_with_pack(dir: &Path) -> ObjectId {
9355        git(dir, &["init", "-q"]);
9356        std::fs::write(dir.join("blob.txt"), b"pack cached lookup fixture\n").unwrap();
9357        git(dir, &["add", "blob.txt"]);
9358        git(dir, &["commit", "-qm", "c"]);
9359        repack_all(dir);
9360        let hex = Command::new("git") // hygiene: git fixture comparison in cfg(test) module
9361            .current_dir(dir)
9362            .args(["rev-parse", "HEAD^{tree}"])
9363            .env("GIT_CONFIG_GLOBAL", "/dev/null")
9364            .env("GIT_CONFIG_SYSTEM", "/dev/null")
9365            .output()
9366            .unwrap();
9367        let tree_hex = String::from_utf8(hex.stdout).unwrap();
9368        ObjectId::from_hex(tree_hex.trim()).unwrap()
9369    }
9370
9371    fn pack_dir(objects: &Path) -> PathBuf {
9372        objects.join("pack")
9373    }
9374
9375    fn v2_idx_path(objects: &Path) -> PathBuf {
9376        pack_dir(objects)
9377            .read_dir()
9378            .unwrap()
9379            .filter_map(|e| e.ok())
9380            .map(|e| e.path())
9381            .find(|p| p.extension().is_some_and(|x| x == "idx"))
9382            .expect("pack idx")
9383    }
9384
9385    /// Replace the repository's v2 `.idx` with an equivalent v1-format index.
9386    fn convert_pack_idx_to_v1(objects: &Path) -> PathBuf {
9387        let idx_path = v2_idx_path(objects);
9388        let parsed = read_pack_index(&idx_path).expect("v2 idx");
9389        let mut body = Vec::new();
9390        for slot in parsed.fanout {
9391            body.extend_from_slice(&slot.to_be_bytes());
9392        }
9393        for entry in parsed.iter() {
9394            body.extend_from_slice(&(entry.offset().expect("off") as u32).to_be_bytes());
9395            body.extend_from_slice(entry.oid());
9396        }
9397        body.extend_from_slice(HashAlgo::Sha1.checksum_digest(&body).as_slice());
9398        rewrite_test_file(&idx_path, &body);
9399        clear_pack_cache();
9400        idx_path
9401    }
9402
9403    fn rewrite_test_file(path: &Path, body: &[u8]) {
9404        let _ = fs::remove_file(path);
9405        fs::write(path, body).unwrap();
9406    }
9407
9408    fn synthetic_fanout_index(entries: &[(Vec<u8>, u64)]) -> PackIndex {
9409        let mut sorted = entries.to_vec();
9410        sorted.sort_by(|a, b| a.0.cmp(&b.0));
9411        let slices: Vec<&[u8]> = sorted.iter().map(|(o, _)| o.as_slice()).collect();
9412        let fanout = compute_fanout_from_oid_slices(&slices);
9413        let mut body = Vec::new();
9414        for f in fanout {
9415            body.extend_from_slice(&f.to_be_bytes());
9416        }
9417        for (oid, off) in sorted {
9418            body.extend_from_slice(&(off as u32).to_be_bytes());
9419            body.extend_from_slice(&oid);
9420        }
9421        body.extend_from_slice(HashAlgo::Sha1.checksum_digest(&body).as_slice());
9422        parse_pack_index_bytes(Path::new("synthetic.idx"), body, false).expect("v1 idx")
9423    }
9424
9425    #[test]
9426    fn find_offset_fanout_bucket_first_last_and_miss() {
9427        let first = vec![0x05u8; 20];
9428        let mut mid = vec![0x05u8; 20];
9429        mid[19] = 1;
9430        let mut last = vec![0x05u8; 20];
9431        last[19] = 2;
9432        let miss = vec![0x06u8; 20];
9433        let idx =
9434            synthetic_fanout_index(&[(first.clone(), 12), (mid.clone(), 24), (last.clone(), 36)]);
9435
9436        let oid_first = ObjectId::from_bytes(&first).unwrap();
9437        let oid_mid = ObjectId::from_bytes(&mid).unwrap();
9438        let oid_last = ObjectId::from_bytes(&last).unwrap();
9439        let oid_miss = ObjectId::from_bytes(&miss).unwrap();
9440
9441        assert_eq!(idx.find_offset(&oid_first).expect("f"), Some(12));
9442        assert_eq!(idx.find_offset(&oid_mid).expect("m"), Some(24));
9443        assert_eq!(idx.find_offset(&oid_last).expect("l"), Some(36));
9444        assert_eq!(idx.find_offset(&oid_miss).expect("miss"), None);
9445    }
9446
9447    #[cfg_attr(
9448        not(feature = "full-tests"),
9449        ignore = "starts system git or is heavy; run with --features full-tests"
9450    )]
9451    #[test]
9452    fn cached_lookup_hit_v2_idx() {
9453        let _guard = pack_cache_test_guard();
9454        clear_pack_cache();
9455        let dir = TempDir::new().unwrap();
9456        let tree = init_repo_with_pack(dir.path());
9457        let objects = dir.path().join(".git").join("objects");
9458        let got = read_object_from_packs(&objects, &tree).expect("read tree from v2 pack");
9459        assert_eq!(got.kind, ObjectKind::Tree);
9460        // Second read must hit the in-memory cache (same result, no reparsing).
9461        let again = read_object_from_packs(&objects, &tree).expect("cached read");
9462        assert_eq!(again.data, got.data);
9463    }
9464
9465    #[cfg_attr(
9466        not(feature = "full-tests"),
9467        ignore = "starts system git or is heavy; run with --features full-tests"
9468    )]
9469    #[test]
9470    fn cached_lookup_hit_v1_idx() {
9471        let _guard = pack_cache_test_guard();
9472        clear_pack_cache();
9473        let dir = TempDir::new().unwrap();
9474        let tree = init_repo_with_pack(dir.path());
9475        let objects = dir.path().join(".git").join("objects");
9476        let v1 = convert_pack_idx_to_v1(&objects);
9477        assert!(pack_index_is_v1(&v1));
9478        let idx = read_pack_index_cached(&v1).expect("parse v1 idx");
9479        let off = idx
9480            .find_offset(&tree)
9481            .expect("fanout lookup in v1 idx")
9482            .expect("off");
9483        assert!(off > 0);
9484        let obj = read_object_from_pack(&idx, &tree).expect("read via v1 index");
9485        assert_eq!(obj.kind, ObjectKind::Tree);
9486    }
9487
9488    #[cfg_attr(
9489        not(feature = "full-tests"),
9490        ignore = "starts system git or is heavy; run with --features full-tests"
9491    )]
9492    #[test]
9493    fn cached_lookup_miss() {
9494        let _guard = pack_cache_test_guard();
9495        clear_pack_cache();
9496        let dir = TempDir::new().unwrap();
9497        init_repo_with_pack(dir.path());
9498        let objects = dir.path().join(".git").join("objects");
9499        let miss = ObjectId::from_hex("deadbeefdeadbeefdeadbeefdeadbeefdeadbeef").unwrap();
9500        assert!(read_object_from_packs(&objects, &miss).is_err());
9501        assert!(
9502            try_read_object_from_local_packs_cached(&objects, &miss, || {
9503                Err(Error::ObjectNotFound(miss.to_hex()))
9504            })
9505            .unwrap()
9506            .is_none()
9507        );
9508    }
9509
9510    #[cfg_attr(
9511        not(feature = "full-tests"),
9512        ignore = "starts system git or is heavy; run with --features full-tests"
9513    )]
9514    #[test]
9515    fn cached_lookup_loose_only() {
9516        let _guard = pack_cache_test_guard();
9517        clear_pack_cache();
9518        let dir = TempDir::new().unwrap();
9519        git(dir.path(), &["init", "-q"]);
9520        let out = Command::new("git") // hygiene: git fixture comparison in cfg(test) module
9521            .current_dir(dir.path())
9522            .args(["hash-object", "-w", "--stdin"])
9523            .env("GIT_CONFIG_GLOBAL", "/dev/null")
9524            .env("GIT_CONFIG_SYSTEM", "/dev/null")
9525            .stdin(std::process::Stdio::piped())
9526            .stdout(std::process::Stdio::piped())
9527            .spawn()
9528            .and_then(|mut c| {
9529                use std::io::Write;
9530                c.stdin
9531                    .as_mut()
9532                    .unwrap()
9533                    .write_all(b"loose-only\n")
9534                    .unwrap();
9535                c.wait_with_output()
9536            })
9537            .unwrap();
9538        assert!(out.status.success());
9539        let oid = ObjectId::from_hex(String::from_utf8(out.stdout).unwrap().trim()).unwrap();
9540        let objects = dir.path().join(".git").join("objects");
9541        assert!(try_read_object_from_local_packs_cached(&objects, &oid, || {
9542            Err(Error::ObjectNotFound(oid.to_hex()))
9543        })
9544        .unwrap()
9545        .is_none());
9546    }
9547
9548    #[cfg_attr(
9549        not(feature = "full-tests"),
9550        ignore = "starts system git or is heavy; run with --features full-tests"
9551    )]
9552    #[test]
9553    fn cached_lookup_multiple_packs() {
9554        let _guard = pack_cache_test_guard();
9555        clear_pack_cache();
9556        let dir = TempDir::new().unwrap();
9557        git(dir.path(), &["init", "-q"]);
9558        std::fs::write(dir.path().join("a.txt"), b"a").unwrap();
9559        git(dir.path(), &["add", "a.txt"]);
9560        git(dir.path(), &["commit", "-qm", "a"]);
9561        repack_all(dir.path());
9562        let oid_a = ObjectId::from_hex(
9563            String::from_utf8(
9564                Command::new("git") // hygiene: git fixture comparison in cfg(test) module // hygiene: git fixture comparison in cfg(test) module
9565                    .current_dir(dir.path())
9566                    .args(["rev-parse", "HEAD:a.txt"])
9567                    .env("GIT_CONFIG_GLOBAL", "/dev/null")
9568                    .env("GIT_CONFIG_SYSTEM", "/dev/null")
9569                    .output()
9570                    .unwrap()
9571                    .stdout,
9572            )
9573            .unwrap()
9574            .trim(),
9575        )
9576        .unwrap();
9577        std::fs::write(dir.path().join("b.txt"), b"b").unwrap();
9578        git(dir.path(), &["add", "b.txt"]);
9579        git(dir.path(), &["commit", "-qm", "b"]);
9580        git(dir.path(), &["repack", "-d"]);
9581        let oid_b = ObjectId::from_hex(
9582            String::from_utf8(
9583                Command::new("git") // hygiene: git fixture comparison in cfg(test) module // hygiene: git fixture comparison in cfg(test) module
9584                    .current_dir(dir.path())
9585                    .args(["rev-parse", "HEAD:b.txt"])
9586                    .env("GIT_CONFIG_GLOBAL", "/dev/null")
9587                    .env("GIT_CONFIG_SYSTEM", "/dev/null")
9588                    .output()
9589                    .unwrap()
9590                    .stdout,
9591            )
9592            .unwrap()
9593            .trim(),
9594        )
9595        .unwrap();
9596        let objects = dir.path().join(".git").join("objects");
9597        let pack_count = pack_dir(&objects)
9598            .read_dir()
9599            .unwrap()
9600            .filter_map(|e| e.ok())
9601            .filter(|e| e.path().extension().is_some_and(|x| x == "pack"))
9602            .count();
9603        assert!(
9604            pack_count >= 2,
9605            "expected multiple pack files, got {pack_count}"
9606        );
9607        let obj_a = read_object_from_packs(&objects, &oid_a).expect("pack a");
9608        let obj_b = read_object_from_packs(&objects, &oid_b).expect("pack b");
9609        assert_eq!(obj_a.kind, ObjectKind::Blob);
9610        assert_eq!(obj_b.kind, ObjectKind::Blob);
9611    }
9612
9613    #[cfg_attr(
9614        not(feature = "full-tests"),
9615        ignore = "starts system git or is heavy; run with --features full-tests"
9616    )]
9617    #[test]
9618    fn packed_delta_base_oid_uses_cached_index() {
9619        let _guard = pack_cache_test_guard();
9620        clear_pack_cache();
9621        let dir = TempDir::new().unwrap();
9622        init_repo_with_pack(dir.path());
9623        let objects = dir.path().join(".git").join("objects");
9624        let miss = ObjectId::from_hex("0101010101010101010101010101010101010101").unwrap();
9625        assert!(packed_delta_base_oid(&objects, &miss).unwrap().is_none());
9626    }
9627
9628    #[test]
9629    fn content_addressed_pack_bytes_skip_stat_revalidation() {
9630        let _guard = pack_cache_test_guard();
9631        clear_pack_cache();
9632        let dir = tempfile::tempdir().expect("tempdir");
9633        let pack_path = dir
9634            .path()
9635            .join("pack-1111111111111111111111111111111111111111.pack");
9636        let v1 = b"PACK\x00\x00\x00\x02\x00\x00\x00\x00";
9637        std::fs::write(&pack_path, v1).expect("write v1");
9638        let cached = read_pack_bytes_cached(&pack_path).expect("prime cache");
9639        assert_eq!(&cached[..], v1);
9640
9641        let v2 = b"PACK-replaced-by-repack";
9642        std::fs::write(&pack_path, v2).expect("overwrite on disk");
9643        let again = read_pack_bytes_cached(&pack_path).expect("cached without stat");
9644        assert_eq!(
9645            &again[..],
9646            v1,
9647            "content-addressed name pins cached bytes in-process"
9648        );
9649    }
9650
9651    #[test]
9652    fn temporary_pack_bytes_served_from_cache_until_cleared() {
9653        let _guard = pack_cache_test_guard();
9654        clear_pack_cache();
9655        let dir = tempfile::tempdir().expect("tempdir");
9656        for pack_path in [
9657            dir.path().join("tmp_pack_abc123.pack"),
9658            dir.path().join("pack-custom.pack"),
9659        ] {
9660            clear_pack_cache();
9661            let v1 = b"PACK-temp-v1";
9662            std::fs::write(&pack_path, v1).expect("write v1");
9663            let _ = read_pack_bytes_cached(&pack_path).expect("prime cache");
9664
9665            let v2 = b"PACK-temp-v2-longer-body";
9666            std::fs::write(&pack_path, v2).expect("overwrite pack");
9667            let stale = read_pack_bytes_cached(&pack_path).expect("cache hit");
9668            assert_eq!(
9669                &stale[..],
9670                v1,
9671                "{} serves cached bytes without stat on hit",
9672                pack_path.display()
9673            );
9674            clear_pack_cache();
9675            let fresh = read_pack_bytes_cached(&pack_path).expect("after clear");
9676            assert_eq!(
9677                &fresh[..],
9678                v2,
9679                "{} reloads from disk after clear",
9680                pack_path.display()
9681            );
9682        }
9683    }
9684    #[cfg_attr(
9685        not(feature = "full-tests"),
9686        ignore = "starts system git or is heavy; run with --features full-tests"
9687    )]
9688    #[test]
9689    fn system_git_repack_requires_clear_pack_cache_for_fresh_listing() {
9690        let _guard = pack_cache_test_guard();
9691        clear_pack_cache();
9692        let dir = TempDir::new().unwrap();
9693        init_repo_with_pack(dir.path());
9694        let objects = dir.path().join(".git").join("objects");
9695        read_local_pack_indexes_cached(&objects).expect("warm listing");
9696        for entry in pack_dir(&objects).read_dir().unwrap().flatten() {
9697            let path = entry.path();
9698            if path.extension().is_some_and(|e| e == "pack" || e == "idx") {
9699                fs::remove_file(path).unwrap();
9700            }
9701        }
9702        clear_pack_cache();
9703        let indexes =
9704            read_local_pack_indexes_cached(&objects).expect("list after repack-like delete");
9705        assert!(
9706            indexes.is_empty(),
9707            "callers must clear_pack_cache after removing pack files"
9708        );
9709    }
9710
9711    fn test_oid(n: u8) -> ObjectId {
9712        ObjectId::from_hex(&format!("{n:040x}")).unwrap()
9713    }
9714
9715    fn max_delta_map_chain_edges(map: &HashMap<ObjectId, ObjectId>) -> usize {
9716        let value_set: HashSet<ObjectId> = map.values().copied().collect();
9717        let tips: Vec<ObjectId> = map
9718            .keys()
9719            .copied()
9720            .filter(|k| !value_set.contains(k))
9721            .collect();
9722        let mut max = 0usize;
9723        for tip in tips {
9724            let mut edges = 0usize;
9725            let mut cur = tip;
9726            let mut seen = HashSet::new();
9727            while seen.insert(cur) {
9728                let Some(&b) = map.get(&cur) else {
9729                    break;
9730                };
9731                edges += 1;
9732                cur = b;
9733            }
9734            max = max.max(edges);
9735        }
9736        max
9737    }
9738
9739    #[test]
9740    fn apply_delta_depth_limit_hits_exact_cap_on_seventy_edge_chain() {
9741        let n = 70usize;
9742        let oids: Vec<ObjectId> = (0..n as u8).map(test_oid).collect();
9743        let mut map: HashMap<ObjectId, ObjectId> = HashMap::new();
9744        for i in 1..n {
9745            map.insert(oids[i], oids[i - 1]);
9746        }
9747        let original = map.clone();
9748        apply_delta_depth_limit(&mut map, 50);
9749        assert_eq!(max_delta_map_chain_edges(&map), 50);
9750        let preserved = map
9751            .iter()
9752            .filter(|(t, b)| original.get(t) == Some(b))
9753            .count();
9754        assert!(
9755            preserved > 40,
9756            "non-cut edges should keep original bases (preserved {preserved})"
9757        );
9758    }
9759
9760    #[test]
9761    fn delta_cycle_breaks_drops_smallest_oid_of_each_cycle_only() {
9762        let o = |n: u8| test_oid(n);
9763        // 1 <-> 2 is a 2-cycle; 5 -> 4 -> 3 -> 5 a 3-cycle with 6 hanging off it;
9764        // 8 -> 7 is a plain chain.
9765        let map = HashMap::from([
9766            (o(1), o(2)),
9767            (o(2), o(1)),
9768            (o(3), o(5)),
9769            (o(4), o(3)),
9770            (o(5), o(4)),
9771            (o(6), o(4)),
9772            (o(8), o(7)),
9773        ]);
9774        let mut keys: Vec<ObjectId> = map.keys().copied().collect();
9775        for order in [false, true] {
9776            keys.sort();
9777            if order {
9778                keys.reverse();
9779            }
9780            let mut breaks = delta_cycle_breaks(keys.iter().copied(), |oid| map.get(oid).copied());
9781            breaks.sort();
9782            assert_eq!(breaks, vec![o(1), o(3)], "independent of walk order");
9783        }
9784    }
9785
9786    #[test]
9787    fn delta_cycle_breaks_handles_self_delta_and_acyclic_maps() {
9788        let a = test_oid(9);
9789        assert_eq!(delta_cycle_breaks([a], |_| Some(a)), vec![a]);
9790        let chain: HashMap<ObjectId, ObjectId> =
9791            (1..60u8).map(|i| (test_oid(i), test_oid(i - 1))).collect();
9792        assert!(delta_cycle_breaks(chain.keys().copied(), |o| chain.get(o).copied()).is_empty());
9793    }
9794
9795    #[test]
9796    fn apply_delta_depth_limit_breaks_cycles_shorter_than_depth() {
9797        let (a, b, c, d) = (test_oid(1), test_oid(2), test_oid(3), test_oid(4));
9798        // a <-> b, plus c -> b and d -> c hanging off the cycle.
9799        let mut map = HashMap::from([(a, b), (b, a), (c, b), (d, c)]);
9800        apply_delta_depth_limit(&mut map, 50);
9801        assert_eq!(map, HashMap::from([(b, a), (c, b), (d, c)]));
9802        assert_eq!(max_delta_map_chain_edges(&map), 3);
9803    }
9804
9805    #[test]
9806    fn cap_delta_chain_depth_matches_per_tip_walk_on_random_forests() {
9807        let oid = |i: u32| {
9808            let mut bytes = [0u8; 20];
9809            bytes[..4].copy_from_slice(&i.to_be_bytes());
9810            ObjectId::from_bytes(&bytes).unwrap()
9811        };
9812        let mut state = 0x2545_f491_4f6c_dd1du64;
9813        let mut next = |bound: u32| {
9814            state ^= state << 13;
9815            state ^= state >> 7;
9816            state ^= state << 17;
9817            (state % u64::from(bound)) as u32
9818        };
9819        for round in 0..200u32 {
9820            let n = 2 + next(400);
9821            let mut map: HashMap<ObjectId, ObjectId> = HashMap::new();
9822            // Object `i` deltas against an earlier object, so the map is a forest; long
9823            // runs of `i - 1` bases build chains well past the depth limit.
9824            for i in 1..n {
9825                if next(5) == 0 {
9826                    continue;
9827                }
9828                let base = if next(3) == 0 { next(i) } else { i - 1 };
9829                map.insert(oid(i), oid(base));
9830            }
9831            for max_depth in [0, 1, 3, 10, 50] {
9832                let mut fast = map.clone();
9833                let mut slow = map.clone();
9834                cap_delta_chain_depth(&mut fast, max_depth);
9835                cap_delta_chain_depth_per_tip(&mut slow, max_depth);
9836                assert_eq!(fast, slow, "round {round}, max_depth {max_depth}");
9837                assert!(max_delta_map_chain_edges(&fast) <= max_depth);
9838            }
9839        }
9840    }
9841
9842    #[test]
9843    fn delta_chain_ends_reports_cycles() {
9844        let (a, b, c) = (test_oid(1), test_oid(2), test_oid(3));
9845        let cyclic = HashMap::from([(a, b), (b, a), (c, a)]);
9846        assert!(
9847            delta_chain_ends(cyclic.keys().copied(), cyclic.len(), |o| cyclic
9848                .get(o)
9849                .copied())
9850            .is_none()
9851        );
9852        let chain = HashMap::from([(c, b), (b, a)]);
9853        let ends = delta_chain_ends(chain.keys().copied(), chain.len(), |o| {
9854            chain.get(o).copied()
9855        })
9856        .unwrap();
9857        assert_eq!(ends.get(&c), Some((a, 2)));
9858        assert_eq!(ends.get(&b), Some((a, 1)));
9859        assert_eq!(ends.get(&a), None);
9860    }
9861
9862    #[test]
9863    fn apply_delta_depth_limit_leaves_short_chains_untouched() {
9864        let a = test_oid(1);
9865        let b = test_oid(2);
9866        let c = test_oid(3);
9867        let mut map = HashMap::from([(b, a), (c, b)]);
9868        apply_delta_depth_limit(&mut map, 50);
9869        assert_eq!(map.get(&b), Some(&a));
9870        assert_eq!(map.get(&c), Some(&b));
9871        assert_eq!(max_delta_map_chain_edges(&map), 2);
9872    }
9873
9874    #[test]
9875    fn apply_delta_depth_limit_caps_long_chain_and_preserves_blobs() {
9876        let n = 120usize;
9877        let oids: Vec<ObjectId> = (0..n as u8).map(test_oid).collect();
9878        let mut blobs: HashMap<ObjectId, Vec<u8>> = HashMap::new();
9879        blobs.insert(oids[0], b"base".to_vec());
9880        for i in 1..n {
9881            let mut next = blobs[&oids[i - 1]].clone();
9882            next.push(i as u8);
9883            blobs.insert(oids[i], next);
9884        }
9885        let mut map: HashMap<ObjectId, ObjectId> = HashMap::new();
9886        for i in 1..n {
9887            map.insert(oids[i], oids[i - 1]);
9888        }
9889        let original = map.clone();
9890        apply_delta_depth_limit(&mut map, 50);
9891        assert_eq!(
9892            max_delta_map_chain_edges(&map),
9893            50,
9894            "long chain must cap at exactly max_depth edges"
9895        );
9896        let preserved = map
9897            .iter()
9898            .filter(|(target, base)| original.get(target) == Some(base))
9899            .count();
9900        assert!(
9901            preserved > 60,
9902            "segment links should stay on original bases (preserved {preserved} of {})",
9903            map.len()
9904        );
9905
9906        use crate::delta_encode::encode_prefix_extension_delta;
9907        use crate::unpack_objects::apply_delta;
9908
9909        for (&target, &base) in &map {
9910            let delta = encode_prefix_extension_delta(&blobs[&base], &blobs[&target]).unwrap();
9911            let got = apply_delta(&blobs[&base], &delta).unwrap();
9912            assert_eq!(got, blobs[&target]);
9913        }
9914    }
9915
9916    #[cfg_attr(
9917        not(feature = "full-tests"),
9918        ignore = "starts system git or is heavy; run with --features full-tests"
9919    )]
9920    #[test]
9921    fn v2_idx_crc_matches_crc32fast_over_entry_bytes() {
9922        let _guard = pack_cache_test_guard();
9923        clear_pack_cache();
9924        let dir = TempDir::new().unwrap();
9925        let tree = init_repo_with_pack(dir.path());
9926        let objects = dir.path().join(".git").join("objects");
9927        let idx_path = v2_idx_path(&objects);
9928        let idx = read_pack_index(&idx_path).expect("read idx");
9929        let pack_bytes = std::fs::read(&idx.pack_path).expect("read pack");
9930        for entry in idx.iter() {
9931            let crc = entry.crc32().expect("v2 idx entry crc");
9932            let off = entry.offset().expect("entry off");
9933            let start = off as usize;
9934            let end = super::pack_entry_raw_end(&idx, &pack_bytes, off)
9935                .expect("entry end")
9936                .expect("end");
9937            assert_eq!(crc32fast::hash(&pack_bytes[start..end]), crc);
9938        }
9939        let _ = tree;
9940    }
9941
9942    #[cfg_attr(
9943        not(feature = "full-tests"),
9944        ignore = "starts system git or is heavy; run with --features full-tests"
9945    )]
9946    #[test]
9947    fn packed_full_object_slice_returns_on_disk_bytes() {
9948        let _guard = pack_cache_test_guard();
9949        clear_pack_cache();
9950        let dir = TempDir::new().unwrap();
9951        let tree = init_repo_with_pack(dir.path());
9952        let objects = dir.path().join(".git").join("objects");
9953        let idx = read_pack_index(&v2_idx_path(&objects)).expect("idx");
9954        let pack_bytes = std::fs::read(&idx.pack_path).expect("pack");
9955        let off = idx.find_offset(&tree).expect("tree in pack").expect("off");
9956        let start = off as usize;
9957        let end = super::pack_entry_raw_end(&idx, &pack_bytes, off)
9958            .expect("entry end")
9959            .expect("end");
9960        let expected = &pack_bytes[start..end];
9961        let slice = packed_full_object_slice(&objects, &tree)
9962            .expect("lookup")
9963            .expect("full tree slice");
9964        assert_eq!(slice, expected);
9965    }
9966
9967    fn touch_mtime(path: &Path, when: filetime::FileTime) {
9968        filetime::set_file_mtime(path, when).expect("set mtime");
9969    }
9970
9971    #[cfg_attr(
9972        not(feature = "full-tests"),
9973        ignore = "starts system git or is heavy; run with --features full-tests"
9974    )]
9975    #[test]
9976    fn packed_full_object_slice_crc_mismatch_falls_back_to_redundant_pack() {
9977        let _guard = pack_cache_test_guard();
9978        clear_pack_cache();
9979        let dir = TempDir::new().unwrap();
9980        let tree = init_repo_with_pack(dir.path());
9981        let objects = dir.path().join(".git").join("objects");
9982        let pack_dir = pack_dir(&objects);
9983        let primary_idx = read_pack_index(&v2_idx_path(&objects)).expect("idx");
9984        let primary_pack = primary_idx.pack_path.clone();
9985        let primary_idx_path = primary_idx.idx_path.clone();
9986
9987        let redundant_pack = pack_dir.join("pack-redundant.pack");
9988        let redundant_idx = pack_dir.join("pack-redundant.idx");
9989        fs::copy(&primary_pack, &redundant_pack).expect("copy pack");
9990        fs::copy(&primary_idx_path, &redundant_idx).expect("copy idx");
9991
9992        let older = filetime::FileTime::from_unix_time(1_700_000_000, 0);
9993        let newer = filetime::FileTime::from_unix_time(1_700_000_100, 0);
9994        touch_mtime(&primary_pack, older);
9995        touch_mtime(&primary_idx_path, older);
9996        touch_mtime(&redundant_pack, newer);
9997        touch_mtime(&redundant_idx, newer);
9998        clear_pack_cache();
9999
10000        let off = primary_idx
10001            .find_offset(&tree)
10002            .expect("tree offset")
10003            .expect("off");
10004        let start = off as usize;
10005        let end = super::pack_entry_raw_end(&primary_idx, &fs::read(&primary_pack).unwrap(), off)
10006            .expect("entry end")
10007            .expect("end");
10008        let flip = start + (end - start) / 2;
10009        let mut corrupt = fs::read(&redundant_pack).expect("read redundant");
10010        corrupt[flip] ^= 0x01;
10011        rewrite_test_file(&redundant_pack, &corrupt);
10012        clear_pack_cache();
10013
10014        let recorded_crc = read_pack_index(&redundant_idx)
10015            .expect("redundant idx")
10016            .crc32_for_pack_offset(off)
10017            .expect("crc")
10018            .expect("crc value");
10019        let corrupt_slice = &corrupt[start..end];
10020        assert_ne!(
10021            crc32fast::hash(corrupt_slice),
10022            recorded_crc,
10023            "test must flip bytes without updating idx CRC"
10024        );
10025        assert!(
10026            packed_full_object_slice(&objects, &tree)
10027                .expect("lookup")
10028                .is_some(),
10029            "valid redundant pack should serve the slice after CRC rejects corrupt copy"
10030        );
10031
10032        let obj = read_object_from_packs(&objects, &tree).expect("read tree");
10033        assert_eq!(obj.kind, ObjectKind::Tree);
10034        let slice = packed_full_object_slice(&objects, &tree)
10035            .expect("lookup")
10036            .expect("slice");
10037        let good_pack = fs::read(&primary_pack).expect("primary pack");
10038        assert_eq!(&slice, &good_pack[start..end]);
10039    }
10040
10041    #[test]
10042    fn delta_reuse_scan_in_storage_order_matches_per_object_lookups() {
10043        let tmp = TempDir::new().unwrap();
10044        let bare = grit_test_support::git_packed_history(tmp.path(), 40);
10045        let objects = bare.join("objects");
10046        let indexes = read_local_pack_indexes_cached(&objects).unwrap();
10047        let idx = &indexes[0];
10048        let pack_bytes = read_pack_bytes_cached(&idx.pack_path).unwrap();
10049        let all: Vec<(ObjectId, u64)> = idx
10050            .iter()
10051            .map(|e| (ObjectId::from_bytes(e.oid()).unwrap(), e.offset().unwrap()))
10052            .collect();
10053        let send: HashSet<ObjectId> = all.iter().map(|(oid, _)| *oid).collect();
10054        let reusable = |oid: &ObjectId| send.contains(oid);
10055
10056        let mut looked_up = HashMap::new();
10057        for &(oid, offset) in &all {
10058            if let Some((base, size, start, end)) =
10059                parse_in_pack_delta_reuse_zlib(idx, &pack_bytes, offset, &reusable).unwrap()
10060            {
10061                looked_up.insert(oid, (base, size, start, end));
10062            }
10063        }
10064        assert!(looked_up.len() > 20, "the fixture pack stores deltas");
10065
10066        let mut scanned = HashMap::new();
10067        scan_packed_delta_reuse(idx, &pack_bytes, &send, &reusable, &mut scanned).unwrap();
10068        let scanned: HashMap<ObjectId, (ObjectId, u64, usize, usize)> = scanned
10069            .into_iter()
10070            .map(|(oid, (base, size, _, start, end))| (oid, (base, size, start, end)))
10071            .collect();
10072        assert_eq!(scanned, looked_up);
10073        assert!(index_scan_is_cheaper(idx, send.len()));
10074        let found = collect_packed_delta_reuse_for_send(&objects, &send, &reusable).unwrap();
10075        assert_eq!(found.len(), looked_up.len());
10076    }
10077
10078    #[test]
10079    fn wanted_index_entries_lookups_match_a_full_scan() {
10080        let tmp = TempDir::new().unwrap();
10081        let bare = grit_test_support::git_packed_history(tmp.path(), 30);
10082        let objects = bare.join("objects");
10083        let indexes = read_local_pack_indexes_cached(&objects).unwrap();
10084        assert_eq!(indexes.len(), 1);
10085        let idx = &indexes[0];
10086        let all: Vec<(ObjectId, u64)> = idx
10087            .iter()
10088            .map(|e| (ObjectId::from_bytes(e.oid()).unwrap(), e.offset().unwrap()))
10089            .collect();
10090        assert!(all.len() > 64);
10091        let absent = ObjectId::from_hex("0000000000000000000000000000000000000001").unwrap();
10092        let sorted = |mut v: Vec<(ObjectId, u64)>| {
10093            v.sort();
10094            v
10095        };
10096
10097        let few: HashSet<ObjectId> = [all[3].0, all[all.len() - 1].0, absent].into();
10098        let looked_up = wanted_index_entries(idx, few.iter(), |oid| few.contains(oid)).unwrap();
10099        assert_eq!(sorted(looked_up), sorted(vec![all[3], all[all.len() - 1]]));
10100
10101        let many: HashSet<ObjectId> = all.iter().map(|(oid, _)| *oid).chain([absent]).collect();
10102        let scanned = wanted_index_entries(idx, many.iter(), |oid| many.contains(oid)).unwrap();
10103        assert_eq!(sorted(scanned), sorted(all));
10104
10105        let slots = locate_packed_objects(&objects, &few.iter().copied().collect()).unwrap();
10106        assert_eq!(slots.len(), 2);
10107        assert!(!slots.contains_key(&absent));
10108    }
10109}