Skip to main content

sley_pack/
bounded_read.rs

1//! Bounded, positional decoding of individual pack entries.
2//!
3//! This module deliberately reuses the crate's entry-header, OFS-offset, and
4//! delta-application parsers. It only supplies the random-access I/O and the
5//! iterative chain planner around that authoritative grammar.
6
7use super::*;
8use flate2::{Decompress, FlushDecompress, Status};
9use std::collections::{HashMap, HashSet};
10use std::io;
11
12const ENTRY_PREFIX_BYTES: usize = 64;
13const INFLATE_CHUNK_BYTES: usize = 8 * 1024;
14
15/// A source that can read pack bytes positionally without changing shared
16/// cursor state or requiring the complete pack to be resident in memory.
17pub trait PackReadSource {
18    /// Total source length, including the pack trailer.
19    fn len(&self) -> io::Result<u64>;
20
21    /// Read bytes beginning at `offset`, with the same short-read semantics as
22    /// [`std::io::Read::read`]. Returning `0` means end of source.
23    fn read_at(&self, offset: u64, buf: &mut [u8]) -> io::Result<usize>;
24
25    /// Whether this source contains no bytes.
26    fn is_empty(&self) -> io::Result<bool> {
27        self.len().map(|len| len == 0)
28    }
29}
30
31/// A borrowed in-memory source, useful for parity tests and already-bounded
32/// pack buffers. The decoder borrows the slice for its entire lifetime.
33#[derive(Debug, Clone, Copy)]
34pub struct SlicePackSource<'a> {
35    bytes: &'a [u8],
36}
37
38impl<'a> SlicePackSource<'a> {
39    pub const fn new(bytes: &'a [u8]) -> Self {
40        Self { bytes }
41    }
42}
43
44impl PackReadSource for SlicePackSource<'_> {
45    fn len(&self) -> io::Result<u64> {
46        Ok(self.bytes.len() as u64)
47    }
48
49    fn read_at(&self, offset: u64, buf: &mut [u8]) -> io::Result<usize> {
50        let start = usize::try_from(offset).unwrap_or(usize::MAX);
51        let Some(remaining) = self.bytes.get(start..) else {
52            return Ok(0);
53        };
54        let count = remaining.len().min(buf.len());
55        buf[..count].copy_from_slice(&remaining[..count]);
56        Ok(count)
57    }
58}
59
60impl PackReadSource for Vec<u8> {
61    fn len(&self) -> io::Result<u64> {
62        Ok(self.len() as u64)
63    }
64
65    fn read_at(&self, offset: u64, buf: &mut [u8]) -> io::Result<usize> {
66        let start = usize::try_from(offset).unwrap_or(usize::MAX);
67        let Some(remaining) = self.get(start..) else {
68            return Ok(0);
69        };
70        let count = remaining.len().min(buf.len());
71        buf[..count].copy_from_slice(&remaining[..count]);
72        Ok(count)
73    }
74}
75
76#[cfg(unix)]
77impl PackReadSource for std::fs::File {
78    fn len(&self) -> io::Result<u64> {
79        self.metadata().map(|metadata| metadata.len())
80    }
81
82    fn read_at(&self, offset: u64, buf: &mut [u8]) -> io::Result<usize> {
83        std::os::unix::fs::FileExt::read_at(self, buf, offset)
84    }
85}
86
87#[cfg(windows)]
88impl PackReadSource for std::fs::File {
89    fn len(&self) -> io::Result<u64> {
90        self.metadata().map(|metadata| metadata.len())
91    }
92
93    fn read_at(&self, offset: u64, buf: &mut [u8]) -> io::Result<usize> {
94        std::os::windows::fs::FileExt::seek_read(self, buf, offset)
95    }
96}
97
98/// Hard limits applied while reading packs.
99///
100/// Whole-pack readers use [`Self::max_delta_depth`]. Targeted reads through a
101/// [`BoundedPackDecoder`] additionally enforce the materialization and cache
102/// limits.
103#[derive(Debug, Clone, Copy, PartialEq, Eq)]
104pub struct PackReadLimits {
105    /// Maximum number of delta entries between a target and its base.
106    pub max_delta_depth: usize,
107    /// Maximum logical decoded object and delta bytes owned or actively used by
108    /// the decoder at one time. The bound is checked before allocation. Allocator
109    /// slack/capacity, collection metadata, and fixed-size I/O scratch buffers
110    /// are not included, so this is deliberately not an RSS or heap-usage bound.
111    /// A resolved immutable REF base is counted once while active even when its
112    /// `Arc` is also retained by its outcome or lookup table; the decoder reuses
113    /// that allocation rather than copying it. Returned objects cease to count
114    /// after the call unless cached.
115    pub max_materialized_bytes: usize,
116    /// Maximum logical decoded body bytes retained between calls. The effective
117    /// cache ceiling is also capped by `max_materialized_bytes`.
118    pub max_cached_bytes: usize,
119}
120
121impl Default for PackReadLimits {
122    fn default() -> Self {
123        Self {
124            max_delta_depth: MAX_READ_DELTA_CHAIN_DEPTH,
125            max_materialized_bytes: 64 * 1024 * 1024,
126            max_cached_bytes: 16 * 1024 * 1024,
127        }
128    }
129}
130
131/// Which explicit decoder limit rejected a read.
132#[derive(Debug, Clone, Copy, PartialEq, Eq)]
133pub enum PackLimitKind {
134    DeltaDepth,
135    MaterializedBytes,
136}
137
138/// Deterministic details for a rejected limit check.
139#[derive(Debug, Clone, Copy, PartialEq, Eq)]
140pub struct PackLimitError {
141    pub kind: PackLimitKind,
142    pub limit: usize,
143    pub attempted: usize,
144}
145
146/// A body allocation failed after every configured decoder limit had passed.
147///
148/// This is deliberately distinct from [`PackReadError::Limit`]: allocator
149/// availability is an environmental resource failure, not a deterministic
150/// rejection by [`PackReadLimits`].
151#[derive(Debug, Clone, Copy, PartialEq, Eq)]
152pub struct PackAllocationError {
153    /// Logical body bytes requested from the allocator.
154    pub requested: usize,
155    /// Other active logical body/delta bytes at the allocation point.
156    pub active: usize,
157    /// Logical body bytes retained in the decoder cache at the allocation point.
158    pub cached: usize,
159}
160
161/// Error returned by bounded targeted decoding.
162#[derive(Debug)]
163pub enum PackReadError {
164    Limit(PackLimitError),
165    Allocation(PackAllocationError),
166    Source(io::Error),
167    Pack(GitError),
168}
169
170impl fmt::Display for PackReadError {
171    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
172        match self {
173            Self::Limit(error) => write!(
174                formatter,
175                "pack {:?} limit exceeded: limit {}, attempted {}",
176                error.kind, error.limit, error.attempted
177            ),
178            Self::Allocation(error) => write!(
179                formatter,
180                "pack body allocation failed: requested {}, active {}, cached {}",
181                error.requested, error.active, error.cached
182            ),
183            Self::Source(error) => write!(formatter, "pack source read failed: {error}"),
184            Self::Pack(error) => error.fmt(formatter),
185        }
186    }
187}
188
189impl std::error::Error for PackReadError {
190    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
191        match self {
192            Self::Source(error) => Some(error),
193            Self::Pack(error) => Some(error),
194            Self::Limit(_) | Self::Allocation(_) => None,
195        }
196    }
197}
198
199impl From<io::Error> for PackReadError {
200    fn from(error: io::Error) -> Self {
201        Self::Source(error)
202    }
203}
204
205impl From<GitError> for PackReadError {
206    fn from(error: GitError) -> Self {
207        Self::Pack(error)
208    }
209}
210
211/// Opaque identifier for one source registered with a [`BoundedPackDecoder`].
212#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
213pub struct PackSourceId(usize);
214
215/// A stable entry location within a registered pack source.
216#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
217pub struct PackObjectLocation {
218    source: PackSourceId,
219    offset: u64,
220}
221
222impl PackObjectLocation {
223    pub const fn new(source: PackSourceId, offset: u64) -> Self {
224        Self { source, offset }
225    }
226
227    pub const fn source(self) -> PackSourceId {
228        self.source
229    }
230
231    pub const fn offset(self) -> u64 {
232        self.offset
233    }
234}
235
236/// Immutable, identity-bound materialization accepted as a non-pack REF base.
237///
238/// The object ID and structural depth are compiler-controlled and travel with
239/// the same [`Arc`] as the body. Pack-derived values can only be obtained from
240/// [`PackReadOutcome::resolved_base`]. Loose/non-delta objects can only be
241/// introduced through [`RefDeltaBases::insert_loose`], which computes their ID
242/// and does not expose a reusable depth token.
243///
244/// ```compile_fail
245/// # use sley_core::ObjectId;
246/// # use sley_object::EncodedObject;
247/// # use sley_pack::ResolvedPackObject;
248/// # use std::sync::Arc;
249/// # fn forged(object: Arc<EncodedObject>, oid: ObjectId) {
250/// let _ = ResolvedPackObject { object, oid, depth: 0, origin: None };
251/// # }
252/// ```
253#[derive(Debug, Clone)]
254pub struct ResolvedPackObject {
255    object: Arc<EncodedObject>,
256    oid: ObjectId,
257    depth: usize,
258    origin: Option<PackObjectLocation>,
259}
260
261impl ResolvedPackObject {
262    pub fn object(&self) -> &EncodedObject {
263        &self.object
264    }
265
266    pub const fn object_id(&self) -> ObjectId {
267        self.oid
268    }
269
270    pub const fn delta_depth(&self) -> usize {
271        self.depth
272    }
273}
274
275/// Precomputed REF-base lookup used during one or more targeted reads.
276///
277/// Pack locations are followed by the decoder on its explicit heap work list;
278/// lookup cannot recursively invoke another decoder while an outer decoder is
279/// on the call stack. Immutable loose/materialized bases are reused by `Arc`
280/// and counted in the same per-read materialization budget.
281#[derive(Debug, Clone, Default)]
282pub struct RefDeltaBases {
283    entries: HashMap<ObjectId, RefDeltaBase>,
284}
285
286#[derive(Debug, Clone)]
287enum RefDeltaBase {
288    Location(PackObjectLocation),
289    Resolved(ResolvedPackObject),
290}
291
292impl RefDeltaBases {
293    pub fn new() -> Self {
294        Self::default()
295    }
296
297    pub fn insert_location(&mut self, oid: ObjectId, location: PackObjectLocation) {
298        self.entries.insert(oid, RefDeltaBase::Location(location));
299    }
300
301    /// Register a base supplied by a loose/non-delta object store. Its identity
302    /// is derived from the immutable object; callers cannot provide a separate
303    /// object ID or depth value.
304    pub fn insert_loose(
305        &mut self,
306        object: Arc<EncodedObject>,
307        format: ObjectFormat,
308    ) -> Result<ObjectId> {
309        self.insert_loose_with_cancel(object, format, CancelFlag::never())
310    }
311
312    pub fn insert_loose_with_cancel(
313        &mut self,
314        object: Arc<EncodedObject>,
315        format: ObjectFormat,
316        cancel: CancelFlag<'_>,
317    ) -> Result<ObjectId> {
318        let oid = cancellable_object_id(&object, format, cancel)?;
319        cancel.check()?;
320        let resolved = RefDeltaBase::Resolved(ResolvedPackObject {
321            object,
322            oid,
323            depth: 0,
324            origin: None,
325        });
326        self.replace_transactionally(oid, resolved, || cancel.check())?;
327        Ok(oid)
328    }
329
330    fn replace_transactionally<F>(
331        &mut self,
332        oid: ObjectId,
333        replacement: RefDeltaBase,
334        post_insert: F,
335    ) -> Result<()>
336    where
337        F: FnOnce() -> Result<()>,
338    {
339        let previous = self.entries.insert(oid, replacement);
340        if let Err(error) = post_insert() {
341            match previous {
342                Some(previous) => {
343                    self.entries.insert(oid, previous);
344                }
345                None => {
346                    self.entries.remove(&oid);
347                }
348            }
349            return Err(error);
350        }
351        Ok(())
352    }
353
354    pub fn insert_resolved(&mut self, resolved: ResolvedPackObject) -> ObjectId {
355        let oid = resolved.oid;
356        self.entries.insert(oid, RefDeltaBase::Resolved(resolved));
357        oid
358    }
359
360    fn get(&self, oid: &ObjectId) -> Option<&RefDeltaBase> {
361        self.entries.get(oid)
362    }
363}
364
365/// Usage measured for one targeted read.
366#[derive(Debug, Clone, Copy, PartialEq, Eq)]
367pub struct PackReadStats {
368    /// Total bytes returned by [`PackReadSource`] during this call, including
369    /// entry prefixes and compressed read chunks. Bytes are counted once at the
370    /// decoder's single positional-read chokepoint.
371    source_bytes_read: u64,
372    /// Exact zlib input bytes consumed while decoding entries. Unlike
373    /// `source_bytes_read`, this excludes entry prefixes and input read-ahead
374    /// left after `StreamEnd`.
375    compressed_bytes_read: u64,
376    /// Highest simultaneous logical decoded body/delta byte total during this
377    /// call, including decoder cache contents.
378    peak_materialized_bytes: usize,
379    /// Logical decoded body bytes retained after this call.
380    cached_bytes: usize,
381    cached_objects: usize,
382    cache_evictions: u64,
383    /// Number of deltas resolved for the requested object.
384    delta_depth: usize,
385}
386
387impl PackReadStats {
388    fn start(cached_bytes: usize) -> Self {
389        Self {
390            source_bytes_read: 0,
391            compressed_bytes_read: 0,
392            peak_materialized_bytes: cached_bytes,
393            cached_bytes: 0,
394            cached_objects: 0,
395            cache_evictions: 0,
396            delta_depth: 0,
397        }
398    }
399
400    pub const fn source_bytes_read(&self) -> u64 {
401        self.source_bytes_read
402    }
403
404    pub const fn compressed_bytes_read(&self) -> u64 {
405        self.compressed_bytes_read
406    }
407
408    pub const fn peak_materialized_bytes(&self) -> usize {
409        self.peak_materialized_bytes
410    }
411
412    pub const fn cached_bytes(&self) -> usize {
413        self.cached_bytes
414    }
415
416    pub const fn cached_objects(&self) -> usize {
417        self.cached_objects
418    }
419
420    pub const fn cache_evictions(&self) -> u64 {
421        self.cache_evictions
422    }
423
424    pub const fn delta_depth(&self) -> usize {
425        self.delta_depth
426    }
427}
428
429/// One decoded object and the resources measured while producing it.
430///
431/// Outcome construction and depth statistics are intentionally private. This
432/// prevents safe callers from forging lower structural depth and converting it
433/// into authoritative REF-base evidence.
434///
435/// ```compile_fail
436/// # use sley_pack::{PackReadOutcome, PackReadStats};
437/// # use std::sync::Arc;
438/// # fn forged(object: Arc<sley_object::EncodedObject>) {
439/// let stats = PackReadStats {
440///     source_bytes_read: 0,
441///     compressed_bytes_read: 0,
442///     peak_materialized_bytes: 0,
443///     cached_bytes: 0,
444///     cached_objects: 0,
445///     cache_evictions: 0,
446///     delta_depth: 0,
447/// };
448/// let _ = PackReadOutcome { object, stats };
449/// # }
450/// ```
451#[derive(Debug, Clone)]
452pub struct PackReadOutcome {
453    object: Arc<EncodedObject>,
454    stats: PackReadStats,
455    oid: ObjectId,
456    depth: usize,
457    origin: PackObjectLocation,
458}
459
460impl PackReadOutcome {
461    pub fn object(&self) -> &EncodedObject {
462        &self.object
463    }
464
465    pub const fn stats(&self) -> &PackReadStats {
466        &self.stats
467    }
468
469    pub const fn object_id(&self) -> ObjectId {
470        self.oid
471    }
472
473    pub fn resolved_base(&self) -> ResolvedPackObject {
474        ResolvedPackObject {
475            object: Arc::clone(&self.object),
476            oid: self.oid,
477            depth: self.depth,
478            origin: Some(self.origin),
479        }
480    }
481}
482
483#[derive(Debug)]
484struct CachedObject {
485    object: Arc<EncodedObject>,
486    oid: ObjectId,
487    bytes: usize,
488    depth: usize,
489    less_recent: Option<PackObjectLocation>,
490    more_recent: Option<PackObjectLocation>,
491}
492
493#[derive(Debug)]
494struct ByteCache {
495    budget: usize,
496    used: usize,
497    entries: HashMap<PackObjectLocation, CachedObject>,
498    least_recent: Option<PackObjectLocation>,
499    most_recent: Option<PackObjectLocation>,
500    evictions: u64,
501}
502
503impl ByteCache {
504    fn new(budget: usize) -> Self {
505        Self {
506            budget,
507            used: 0,
508            entries: HashMap::new(),
509            least_recent: None,
510            most_recent: None,
511            evictions: 0,
512        }
513    }
514
515    fn get(
516        &mut self,
517        location: PackObjectLocation,
518        cancel: CancelFlag<'_>,
519    ) -> Result<Option<(Arc<EncodedObject>, ObjectId, usize)>> {
520        cancel.check()?;
521        let Some(entry) = self.entries.get(&location) else {
522            cancel.check()?;
523            return Ok(None);
524        };
525        let found = (Arc::clone(&entry.object), entry.oid, entry.depth);
526        self.touch(location);
527        cancel.check()?;
528        Ok(Some(found))
529    }
530
531    fn peek(&self, location: PackObjectLocation) -> Option<(Arc<EncodedObject>, ObjectId, usize)> {
532        let entry = self.entries.get(&location)?;
533        Some((Arc::clone(&entry.object), entry.oid, entry.depth))
534    }
535
536    fn contains_same(&self, location: PackObjectLocation, object: &Arc<EncodedObject>) -> bool {
537        self.entries
538            .get(&location)
539            .is_some_and(|cached| Arc::ptr_eq(&cached.object, object))
540    }
541
542    fn insert(
543        &mut self,
544        location: PackObjectLocation,
545        object: Arc<EncodedObject>,
546        oid: ObjectId,
547        depth: usize,
548        cancel: CancelFlag<'_>,
549    ) -> Result<()> {
550        cancel.check()?;
551        let bytes = object.body.len();
552        // Zero-length bodies would otherwise permit unbounded cache metadata
553        // under a byte-only budget without retaining any useful body storage.
554        if bytes == 0 || bytes > self.budget || self.budget == 0 {
555            cancel.check()?;
556            return Ok(());
557        }
558        let previous_bytes = self
559            .entries
560            .get(&location)
561            .map_or(0, |previous| previous.bytes);
562        while self
563            .used
564            .checked_sub(previous_bytes)
565            .and_then(|retained| retained.checked_add(bytes))
566            .is_none_or(|projected| projected > self.budget)
567        {
568            cancel.check()?;
569            if !self.evict_one_except(Some(location), cancel)? {
570                return Ok(());
571            }
572        }
573        cancel.check()?;
574        if self.entries.contains_key(&location) {
575            self.remove_entry(location);
576        }
577        self.used += bytes;
578        self.entries.insert(
579            location,
580            CachedObject {
581                object,
582                oid,
583                bytes,
584                depth,
585                less_recent: None,
586                more_recent: None,
587            },
588        );
589        self.link_as_most_recent(location);
590        Ok(())
591    }
592
593    fn evict_one_except(
594        &mut self,
595        pinned: Option<PackObjectLocation>,
596        cancel: CancelFlag<'_>,
597    ) -> Result<bool> {
598        self.evict_one_except_with(pinned, || cancel.check())
599    }
600
601    fn evict_one_except_with<F>(
602        &mut self,
603        pinned: Option<PackObjectLocation>,
604        mut poll: F,
605    ) -> Result<bool>
606    where
607        F: FnMut() -> Result<()>,
608    {
609        poll()?;
610        let location = match self.least_recent {
611            Some(location) if Some(location) != pinned => Some(location),
612            Some(location) => self
613                .entries
614                .get(&location)
615                .and_then(|cached| cached.more_recent),
616            None => None,
617        };
618        let Some(location) = location else {
619            poll()?;
620            return Ok(false);
621        };
622        self.remove_entry(location);
623        self.evictions += 1;
624        poll()?;
625        Ok(true)
626    }
627
628    fn clear(&mut self, cancel: CancelFlag<'_>) -> Result<()> {
629        self.clear_with(|| cancel.check())
630    }
631
632    fn clear_with<F>(&mut self, mut poll: F) -> Result<()>
633    where
634        F: FnMut() -> Result<()>,
635    {
636        poll()?;
637        let mut removed = 0usize;
638        while let Some(location) = self.least_recent {
639            self.remove_entry(location);
640            removed += 1;
641            if removed.is_multiple_of(64) {
642                poll()?;
643            }
644        }
645        poll()?;
646        Ok(())
647    }
648
649    fn touch(&mut self, location: PackObjectLocation) {
650        if self.most_recent == Some(location) {
651            return;
652        }
653        self.unlink(location);
654        self.link_as_most_recent(location);
655    }
656
657    fn remove_entry(&mut self, location: PackObjectLocation) -> Option<CachedObject> {
658        self.unlink(location);
659        let cached = self.entries.remove(&location)?;
660        debug_assert!(self.used >= cached.bytes);
661        self.used -= cached.bytes;
662        Some(cached)
663    }
664
665    fn unlink(&mut self, location: PackObjectLocation) {
666        let Some((less_recent, more_recent)) = self
667            .entries
668            .get(&location)
669            .map(|cached| (cached.less_recent, cached.more_recent))
670        else {
671            return;
672        };
673        if let Some(less_recent) = less_recent {
674            if let Some(cached) = self.entries.get_mut(&less_recent) {
675                cached.more_recent = more_recent;
676            }
677        } else {
678            self.least_recent = more_recent;
679        }
680        if let Some(more_recent) = more_recent {
681            if let Some(cached) = self.entries.get_mut(&more_recent) {
682                cached.less_recent = less_recent;
683            }
684        } else {
685            self.most_recent = less_recent;
686        }
687        if let Some(cached) = self.entries.get_mut(&location) {
688            cached.less_recent = None;
689            cached.more_recent = None;
690        }
691    }
692
693    fn link_as_most_recent(&mut self, location: PackObjectLocation) {
694        let previous = self.most_recent;
695        if let Some(cached) = self.entries.get_mut(&location) {
696            cached.less_recent = previous;
697            cached.more_recent = None;
698        } else {
699            return;
700        }
701        if let Some(previous) = previous {
702            if let Some(cached) = self.entries.get_mut(&previous) {
703                cached.more_recent = Some(location);
704            }
705        } else {
706            self.least_recent = Some(location);
707        }
708        self.most_recent = Some(location);
709    }
710}
711
712#[derive(Debug)]
713struct EntryPlan {
714    location: PackObjectLocation,
715    header: EntryHeader,
716    data_offset: u64,
717    base: Option<DeltaBase>,
718}
719
720struct PackSourceState<S> {
721    source: S,
722    format: ObjectFormat,
723    trailer_offset: u64,
724}
725
726/// A targeted decoder tied to one or more registered pack sources.
727///
728/// Delta chains are planned in a heap vector and resolved from base to target,
729/// so call-stack use is constant with respect to attacker-controlled depth.
730/// The internal cache and materialization budget are shared across every
731/// registered source. Each source's length and contents, including open
732/// [`std::fs::File`] values, must remain stable for the decoder's lifetime.
733/// Cache accounting is by logical body bytes, not entry count, and
734/// [`Self::clear_cache`] releases every decoder-held object. Cross-pack REF
735/// chains must be registered as locations before the read; the decoder follows
736/// them iteratively without a resolver callback or nested decoder call.
737pub struct BoundedPackDecoder<S> {
738    sources: Vec<PackSourceState<S>>,
739    limits: PackReadLimits,
740    cache: ByteCache,
741}
742
743impl<S: PackReadSource> BoundedPackDecoder<S> {
744    pub fn new(
745        source: S,
746        format: ObjectFormat,
747        limits: PackReadLimits,
748    ) -> std::result::Result<Self, PackReadError> {
749        let source = Self::open_source(source, format)?;
750        Ok(Self {
751            sources: vec![source],
752            limits,
753            cache: ByteCache::new(limits.max_cached_bytes.min(limits.max_materialized_bytes)),
754        })
755    }
756
757    fn open_source(
758        source: S,
759        format: ObjectFormat,
760    ) -> std::result::Result<PackSourceState<S>, PackReadError> {
761        let source_len = source.len()?;
762        let trailer_len = format.raw_len() as u64;
763        let trailer_offset = source_len
764            .checked_sub(trailer_len)
765            .ok_or_else(|| GitError::InvalidFormat("pack smaller than its trailer".into()))?;
766        Ok(PackSourceState {
767            source,
768            format,
769            trailer_offset,
770        })
771    }
772
773    pub const fn primary_source(&self) -> PackSourceId {
774        PackSourceId(0)
775    }
776
777    pub fn add_source(
778        &mut self,
779        source: S,
780        format: ObjectFormat,
781    ) -> std::result::Result<PackSourceId, PackReadError> {
782        let source = Self::open_source(source, format)?;
783        let id = PackSourceId(self.sources.len());
784        self.sources.push(source);
785        Ok(id)
786    }
787
788    pub const fn limits(&self) -> PackReadLimits {
789        self.limits
790    }
791
792    pub fn cached_bytes(&self) -> usize {
793        self.cache.used
794    }
795
796    pub fn cached_objects(&self) -> usize {
797        self.cache.entries.len()
798    }
799
800    /// Drop decoded objects retained between targeted reads, polling between
801    /// bounded batches. Cancellation may leave a valid partially-cleared cache;
802    /// its byte accounting and LRU order remain exact for the retained entries.
803    pub fn clear_cache(
804        &mut self,
805        cancel: CancelFlag<'_>,
806    ) -> std::result::Result<(), PackReadError> {
807        self.cache.clear(cancel)?;
808        Ok(())
809    }
810
811    /// Decode an entry in the primary source without loading a complete pack.
812    pub fn read_object_at(
813        &mut self,
814        offset: u64,
815        ref_bases: &RefDeltaBases,
816    ) -> std::result::Result<PackReadOutcome, PackReadError> {
817        self.read_object_at_with_cancel(offset, ref_bases, CancelFlag::never())
818    }
819
820    pub fn read_object_at_with_cancel(
821        &mut self,
822        offset: u64,
823        ref_bases: &RefDeltaBases,
824        cancel: CancelFlag<'_>,
825    ) -> std::result::Result<PackReadOutcome, PackReadError> {
826        self.read_object_at_location_with_cancel(
827            PackObjectLocation::new(self.primary_source(), offset),
828            ref_bases,
829            cancel,
830        )
831    }
832
833    /// Decode an entry in any registered source. REF locations in `ref_bases`
834    /// are followed on the same explicit work list as OFS links, keeping stack
835    /// use constant across cold multi-pack chains.
836    pub fn read_object_at_location(
837        &mut self,
838        location: PackObjectLocation,
839        ref_bases: &RefDeltaBases,
840    ) -> std::result::Result<PackReadOutcome, PackReadError> {
841        self.read_object_at_location_with_cancel(location, ref_bases, CancelFlag::never())
842    }
843
844    /// Cancel-aware form of [`Self::read_object_at_location`]. The flag is
845    /// polled during chain planning, positional reads, inflate, object-ID
846    /// hashing, every delta command, and between cache maintenance operations.
847    pub fn read_object_at_location_with_cancel(
848        &mut self,
849        location: PackObjectLocation,
850        ref_bases: &RefDeltaBases,
851        cancel: CancelFlag<'_>,
852    ) -> std::result::Result<PackReadOutcome, PackReadError> {
853        cancel.check()?;
854        let target_format = self.source_state(location)?.format;
855        let evictions_before = self.cache.evictions;
856        let mut stats = PackReadStats::start(self.cache.used);
857        if let Some((object, oid, depth)) = self.cache.get(location, cancel)? {
858            stats.delta_depth = depth;
859            self.finish_stats(&mut stats, evictions_before);
860            return Ok(PackReadOutcome {
861                object,
862                stats,
863                oid,
864                depth,
865                origin: location,
866            });
867        }
868
869        let mut visited = HashSet::new();
870        let mut deltas = Vec::new();
871        let mut current_location = location;
872        let mut base_object: Option<(
873            Arc<EncodedObject>,
874            ObjectId,
875            Option<PackObjectLocation>,
876            usize,
877        )> = None;
878        let mut base_entry = None;
879
880        loop {
881            cancel.check()?;
882            self.source_state(current_location)?;
883            if !visited.insert(current_location) {
884                return Err(GitError::InvalidFormat("pack delta cycle detected".into()).into());
885            }
886            if current_location != location
887                && let Some((object, oid, cached_depth)) = self.cache.peek(current_location)
888            {
889                let full_depth = deltas.len().saturating_add(cached_depth);
890                self.enforce_depth(full_depth)?;
891                base_object = Some((object, oid, Some(current_location), cached_depth));
892                break;
893            }
894            let entry = self.read_entry_plan(current_location, cancel, &mut stats)?;
895            cancel.check()?;
896            match entry.base.clone() {
897                None => {
898                    base_entry = Some(entry);
899                    break;
900                }
901                Some(base) => {
902                    let depth = deltas.len().saturating_add(1);
903                    self.enforce_depth(depth)?;
904                    deltas.push(entry);
905                    match base {
906                        DeltaBase::Offset(base_offset) => {
907                            current_location =
908                                PackObjectLocation::new(current_location.source, base_offset);
909                        }
910                        DeltaBase::Ref(base_oid) => {
911                            cancel.check()?;
912                            match ref_bases.get(&base_oid) {
913                                Some(RefDeltaBase::Location(base_location)) => {
914                                    current_location = *base_location;
915                                }
916                                Some(RefDeltaBase::Resolved(resolved)) => {
917                                    if resolved.oid != base_oid {
918                                        return Err(GitError::InvalidObject(format!(
919                                            "resolved REF base identity mismatch: expected {base_oid}, got {}",
920                                            resolved.oid
921                                        ))
922                                        .into());
923                                    }
924                                    let full_depth = deltas.len().saturating_add(resolved.depth);
925                                    self.enforce_depth(full_depth)?;
926                                    let pinned = resolved.origin.filter(|origin| {
927                                        self.cache.contains_same(*origin, &resolved.object)
928                                    });
929                                    let active = if pinned.is_some() {
930                                        0
931                                    } else {
932                                        resolved.object.body.len()
933                                    };
934                                    self.ensure_materialized(
935                                        0, active, pinned, cancel, &mut stats,
936                                    )?;
937                                    base_object = Some((
938                                        Arc::clone(&resolved.object),
939                                        resolved.oid,
940                                        pinned,
941                                        resolved.depth,
942                                    ));
943                                    break;
944                                }
945                                None => {
946                                    return Err(GitError::not_found(format!(
947                                        "ref-delta base object {base_oid}"
948                                    ))
949                                    .into());
950                                }
951                            }
952                        }
953                    }
954                }
955            }
956        }
957
958        let cached_base_depth = base_object.as_ref().map_or(0, |(_, _, _, depth)| *depth);
959        stats.delta_depth = deltas.len().saturating_add(cached_base_depth);
960        self.enforce_depth(stats.delta_depth)?;
961        let (mut object, mut object_oid, mut pinned_cache) = match (base_object, base_entry) {
962            (Some((object, oid, pinned, _)), None) => (object, Some(oid), pinned),
963            (None, Some(entry)) => {
964                let object_type = object_type_for_entry(entry.header.kind)?;
965                let body = self.inflate_entry(&entry, 0, None, cancel, &mut stats)?;
966                let object = Arc::new(EncodedObject::new(object_type, body));
967                (object, None, None)
968            }
969            _ => {
970                return Err(
971                    GitError::InvalidFormat("pack delta base planning failed".into()).into(),
972                );
973            }
974        };
975
976        for delta_entry in deltas.iter().rev() {
977            cancel.check()?;
978            if let Some(DeltaBase::Ref(expected_oid)) = delta_entry.base.as_ref() {
979                let actual_oid = match object_oid {
980                    Some(oid) if oid.format() == expected_oid.format() => oid,
981                    _ => cancellable_object_id(&object, expected_oid.format(), cancel)?,
982                };
983                if actual_oid != *expected_oid {
984                    return Err(GitError::InvalidObject(format!(
985                        "resolved REF base identity mismatch: expected {expected_oid}, got {actual_oid}"
986                    ))
987                    .into());
988                }
989            }
990            let base_bytes = if pinned_cache.is_some() {
991                0
992            } else {
993                object.body.len()
994            };
995            let delta =
996                self.inflate_entry(delta_entry, base_bytes, pinned_cache, cancel, &mut stats)?;
997            let plan = plan_pack_delta(&object.body, &delta)?;
998            let result_size_u64 = plan.result_size;
999            let result_size = usize::try_from(result_size_u64).map_err(|_| {
1000                PackReadError::Limit(PackLimitError {
1001                    kind: PackLimitKind::MaterializedBytes,
1002                    limit: self.limits.max_materialized_bytes,
1003                    attempted: usize::MAX,
1004                })
1005            })?;
1006            let active_bytes = self.checked_materialized_add(base_bytes, delta.len())?;
1007            let mut resolved =
1008                self.allocate_body(result_size, active_bytes, pinned_cache, cancel, &mut stats)?;
1009            apply_pack_delta_exact(&object.body, &delta, plan, &mut resolved, cancel)?;
1010            object = Arc::new(EncodedObject::new(object.object_type, resolved));
1011            object_oid = None;
1012            pinned_cache = None;
1013        }
1014
1015        cancel.check()?;
1016        let oid = cancellable_object_id(&object, target_format, cancel)?;
1017        self.cache.insert(
1018            location,
1019            Arc::clone(&object),
1020            oid,
1021            stats.delta_depth,
1022            cancel,
1023        )?;
1024        self.finish_stats(&mut stats, evictions_before);
1025        let depth = stats.delta_depth;
1026        Ok(PackReadOutcome {
1027            object,
1028            stats,
1029            oid,
1030            depth,
1031            origin: location,
1032        })
1033    }
1034
1035    fn finish_stats(&self, stats: &mut PackReadStats, evictions_before: u64) {
1036        stats.cached_bytes = self.cache.used;
1037        stats.cached_objects = self.cache.entries.len();
1038        stats.cache_evictions = self.cache.evictions.saturating_sub(evictions_before);
1039        stats.peak_materialized_bytes = stats.peak_materialized_bytes.max(self.cache.used);
1040    }
1041
1042    fn enforce_depth(&self, depth: usize) -> std::result::Result<(), PackReadError> {
1043        if depth > self.limits.max_delta_depth {
1044            return Err(PackReadError::Limit(PackLimitError {
1045                kind: PackLimitKind::DeltaDepth,
1046                limit: self.limits.max_delta_depth,
1047                attempted: depth,
1048            }));
1049        }
1050        Ok(())
1051    }
1052
1053    fn checked_materialized_add(
1054        &self,
1055        left: usize,
1056        right: usize,
1057    ) -> std::result::Result<usize, PackReadError> {
1058        left.checked_add(right)
1059            .ok_or(PackReadError::Limit(PackLimitError {
1060                kind: PackLimitKind::MaterializedBytes,
1061                limit: self.limits.max_materialized_bytes,
1062                attempted: usize::MAX,
1063            }))
1064    }
1065
1066    fn read_entry_plan(
1067        &self,
1068        location: PackObjectLocation,
1069        cancel: CancelFlag<'_>,
1070        stats: &mut PackReadStats,
1071    ) -> std::result::Result<EntryPlan, PackReadError> {
1072        let source = self.source_state(location)?;
1073        let offset = location.offset;
1074        if offset >= source.trailer_offset {
1075            return Err(GitError::InvalidFormat("pack object offset out of range".into()).into());
1076        }
1077        let available =
1078            usize::try_from((source.trailer_offset - offset).min(ENTRY_PREFIX_BYTES as u64))
1079                .unwrap_or(ENTRY_PREFIX_BYTES);
1080        let mut prefix = [0u8; ENTRY_PREFIX_BYTES];
1081        self.read_exact_at(
1082            location.source,
1083            offset,
1084            &mut prefix[..available],
1085            cancel,
1086            stats,
1087        )?;
1088        let bytes = &prefix[..available];
1089        let mut cursor = 0usize;
1090        let header = parse_entry_header(bytes, &mut cursor)?;
1091        let base = match header.kind {
1092            PackObjectKind::OfsDelta => Some(DeltaBase::Offset(parse_ofs_delta_base_offset(
1093                bytes,
1094                &mut cursor,
1095                offset,
1096            )?)),
1097            PackObjectKind::RefDelta => {
1098                let raw_len = source.format.raw_len();
1099                let end = cursor.checked_add(raw_len).ok_or_else(|| {
1100                    GitError::InvalidFormat("ref-delta base offset overflow".into())
1101                })?;
1102                let raw = bytes.get(cursor..end).ok_or_else(|| {
1103                    GitError::InvalidFormat("truncated ref-delta base object id".into())
1104                })?;
1105                cursor = end;
1106                Some(DeltaBase::Ref(ObjectId::from_raw(source.format, raw)?))
1107            }
1108            _ => None,
1109        };
1110        let data_offset = offset
1111            .checked_add(cursor as u64)
1112            .ok_or_else(|| GitError::InvalidFormat("pack object offset overflow".into()))?;
1113        Ok(EntryPlan {
1114            location,
1115            header,
1116            data_offset,
1117            base,
1118        })
1119    }
1120
1121    fn inflate_entry(
1122        &mut self,
1123        entry: &EntryPlan,
1124        active_bytes: usize,
1125        pinned_cache: Option<PackObjectLocation>,
1126        cancel: CancelFlag<'_>,
1127        stats: &mut PackReadStats,
1128    ) -> std::result::Result<Vec<u8>, PackReadError> {
1129        cancel.check()?;
1130        let expected = usize::try_from(entry.header.size).map_err(|_| {
1131            PackReadError::Limit(PackLimitError {
1132                kind: PackLimitKind::MaterializedBytes,
1133                limit: self.limits.max_materialized_bytes,
1134                attempted: usize::MAX,
1135            })
1136        })?;
1137        let source = self.source_state(entry.location)?;
1138        let source_id = entry.location.source;
1139        let trailer_offset = source.trailer_offset;
1140        let mut body = self.allocate_body(expected, active_bytes, pinned_cache, cancel, stats)?;
1141
1142        let mut decompressor = Decompress::new(true);
1143        let mut input = [0u8; INFLATE_CHUNK_BYTES];
1144        let mut output = [0u8; INFLATE_CHUNK_BYTES];
1145        let mut input_start = 0usize;
1146        let mut input_end = 0usize;
1147        let mut source_offset = entry.data_offset;
1148
1149        loop {
1150            cancel.check()?;
1151            if input_start == input_end {
1152                if source_offset >= trailer_offset {
1153                    return Err(GitError::InvalidObject("truncated zlib stream".into()).into());
1154                }
1155                let wanted = usize::try_from(
1156                    (trailer_offset - source_offset).min(INFLATE_CHUNK_BYTES as u64),
1157                )
1158                .unwrap_or(INFLATE_CHUNK_BYTES);
1159                let read = self.read_source_at(
1160                    source_id,
1161                    source_offset,
1162                    &mut input[..wanted],
1163                    cancel,
1164                    stats,
1165                )?;
1166                if read == 0 {
1167                    return Err(GitError::InvalidObject("truncated zlib stream".into()).into());
1168                }
1169                source_offset = source_offset
1170                    .checked_add(read as u64)
1171                    .ok_or_else(|| GitError::InvalidFormat("pack source offset overflow".into()))?;
1172                input_start = 0;
1173                input_end = read;
1174            }
1175
1176            let before_in = decompressor.total_in();
1177            let before_out = decompressor.total_out();
1178            let status = decompressor
1179                .decompress(
1180                    &input[input_start..input_end],
1181                    &mut output,
1182                    FlushDecompress::None,
1183                )
1184                .map_err(|error| {
1185                    GitError::InvalidObject(format!("zlib inflate failed: {error}"))
1186                })?;
1187            let consumed =
1188                usize::try_from(decompressor.total_in() - before_in).unwrap_or(usize::MAX);
1189            let produced =
1190                usize::try_from(decompressor.total_out() - before_out).unwrap_or(usize::MAX);
1191            input_start = input_start.saturating_add(consumed);
1192            stats.compressed_bytes_read =
1193                stats.compressed_bytes_read.saturating_add(consumed as u64);
1194            let attempted = self.checked_materialized_add(body.len(), produced)?;
1195            if attempted > expected {
1196                return Err(GitError::InvalidObject(format!(
1197                    "pack object declared {} bytes, decoded more than {}",
1198                    entry.header.size, expected
1199                ))
1200                .into());
1201            }
1202            body.extend_from_slice(&output[..produced]);
1203
1204            if status == Status::StreamEnd {
1205                if body.len() != expected {
1206                    return Err(GitError::InvalidObject(format!(
1207                        "pack object declared {} bytes, decoded {}",
1208                        entry.header.size,
1209                        body.len()
1210                    ))
1211                    .into());
1212                }
1213                return Ok(body);
1214            }
1215            if consumed == 0 && produced == 0 && input_start < input_end {
1216                return Err(GitError::InvalidObject("zlib inflate made no progress".into()).into());
1217            }
1218        }
1219    }
1220
1221    fn ensure_materialized(
1222        &mut self,
1223        active_bytes: usize,
1224        additional_bytes: usize,
1225        pinned_cache: Option<PackObjectLocation>,
1226        cancel: CancelFlag<'_>,
1227        stats: &mut PackReadStats,
1228    ) -> std::result::Result<(), PackReadError> {
1229        cancel.check()?;
1230        let working = self.checked_materialized_add(active_bytes, additional_bytes)?;
1231        if working > self.limits.max_materialized_bytes {
1232            return Err(PackReadError::Limit(PackLimitError {
1233                kind: PackLimitKind::MaterializedBytes,
1234                limit: self.limits.max_materialized_bytes,
1235                attempted: working,
1236            }));
1237        }
1238        while self.checked_materialized_add(self.cache.used, working)?
1239            > self.limits.max_materialized_bytes
1240        {
1241            cancel.check()?;
1242            if !self.cache.evict_one_except(pinned_cache, cancel)? {
1243                break;
1244            }
1245        }
1246        let total = self.checked_materialized_add(self.cache.used, working)?;
1247        if total > self.limits.max_materialized_bytes {
1248            return Err(PackReadError::Limit(PackLimitError {
1249                kind: PackLimitKind::MaterializedBytes,
1250                limit: self.limits.max_materialized_bytes,
1251                attempted: total,
1252            }));
1253        }
1254        stats.peak_materialized_bytes = stats.peak_materialized_bytes.max(total);
1255        Ok(())
1256    }
1257
1258    fn allocate_body(
1259        &mut self,
1260        requested: usize,
1261        active_bytes: usize,
1262        pinned_cache: Option<PackObjectLocation>,
1263        cancel: CancelFlag<'_>,
1264        stats: &mut PackReadStats,
1265    ) -> std::result::Result<Vec<u8>, PackReadError> {
1266        cancel.check()?;
1267        self.ensure_materialized(active_bytes, requested, pinned_cache, cancel, stats)?;
1268        cancel.check()?;
1269        let mut body = Vec::new();
1270        let allocation = body.try_reserve_exact(requested);
1271        cancel.check()?;
1272        allocation.map_err(|_| {
1273            PackReadError::Allocation(PackAllocationError {
1274                requested,
1275                active: active_bytes,
1276                cached: self.cache.used,
1277            })
1278        })?;
1279        Ok(body)
1280    }
1281
1282    fn read_source_at(
1283        &self,
1284        source_id: PackSourceId,
1285        offset: u64,
1286        buf: &mut [u8],
1287        cancel: CancelFlag<'_>,
1288        stats: &mut PackReadStats,
1289    ) -> std::result::Result<usize, PackReadError> {
1290        cancel.check()?;
1291        let source = self
1292            .sources
1293            .get(source_id.0)
1294            .ok_or_else(|| GitError::InvalidFormat("unknown pack source id".into()))?;
1295        let read_result = source.source.read_at(offset, buf);
1296        cancel.check()?;
1297        let read = read_result?;
1298        if read > buf.len() {
1299            return Err(GitError::InvalidFormat(
1300                "pack source returned more bytes than requested".into(),
1301            )
1302            .into());
1303        }
1304        stats.source_bytes_read = stats.source_bytes_read.saturating_add(read as u64);
1305        Ok(read)
1306    }
1307
1308    fn read_exact_at(
1309        &self,
1310        source_id: PackSourceId,
1311        mut offset: u64,
1312        mut buf: &mut [u8],
1313        cancel: CancelFlag<'_>,
1314        stats: &mut PackReadStats,
1315    ) -> std::result::Result<(), PackReadError> {
1316        while !buf.is_empty() {
1317            let read = self.read_source_at(source_id, offset, buf, cancel, stats)?;
1318            if read == 0 {
1319                return Err(GitError::InvalidFormat("truncated pack entry header".into()).into());
1320            }
1321            offset = offset
1322                .checked_add(read as u64)
1323                .ok_or_else(|| GitError::InvalidFormat("pack source offset overflow".into()))?;
1324            buf = &mut buf[read..];
1325        }
1326        Ok(())
1327    }
1328
1329    fn source_state(
1330        &self,
1331        location: PackObjectLocation,
1332    ) -> std::result::Result<&PackSourceState<S>, PackReadError> {
1333        self.sources
1334            .get(location.source.0)
1335            .ok_or_else(|| GitError::InvalidFormat("unknown pack source id".into()).into())
1336    }
1337}
1338
1339fn cancellable_object_id(
1340    object: &EncodedObject,
1341    format: ObjectFormat,
1342    cancel: CancelFlag<'_>,
1343) -> Result<ObjectId> {
1344    cancel.check()?;
1345    let mut digest = StreamingDigest::new(format);
1346    digest.update(object.object_type.as_str().as_bytes());
1347    digest.update(b" ");
1348    let body_len = object.body.len().to_string();
1349    digest.update(body_len.as_bytes());
1350    digest.update(b"\0");
1351    for chunk in object.body.chunks(INFLATE_CHUNK_BYTES) {
1352        cancel.check()?;
1353        digest.update(chunk);
1354    }
1355    cancel.check()?;
1356    digest.finalize()
1357}
1358
1359fn object_type_for_entry(kind: PackObjectKind) -> Result<ObjectType> {
1360    match kind {
1361        PackObjectKind::Commit => Ok(ObjectType::Commit),
1362        PackObjectKind::Tree => Ok(ObjectType::Tree),
1363        PackObjectKind::Blob => Ok(ObjectType::Blob),
1364        PackObjectKind::Tag => Ok(ObjectType::Tag),
1365        PackObjectKind::OfsDelta | PackObjectKind::RefDelta => Err(GitError::InvalidFormat(
1366            "delta pack entry decoded without a base".into(),
1367        )),
1368    }
1369}
1370
1371#[cfg(test)]
1372mod tests {
1373    use super::*;
1374
1375    fn test_oid() -> ObjectId {
1376        ObjectId::from_raw(ObjectFormat::Sha1, &[0; 20]).expect("object id")
1377    }
1378
1379    fn location(offset: u64) -> PackObjectLocation {
1380        PackObjectLocation::new(PackSourceId(0), offset)
1381    }
1382
1383    fn insert_cached(cache: &mut ByteCache, offset: u64, bytes: usize) {
1384        cache
1385            .insert(
1386                location(offset),
1387                Arc::new(EncodedObject::new(
1388                    ObjectType::Blob,
1389                    vec![offset as u8; bytes],
1390                )),
1391                test_oid(),
1392                0,
1393                CancelFlag::never(),
1394            )
1395            .expect("cache insert");
1396    }
1397
1398    #[test]
1399    fn cache_eviction_is_constant_work_and_preserves_lru_accounting() {
1400        let mut cache = ByteCache::new(512);
1401        for offset in 0..256 {
1402            insert_cached(&mut cache, offset, 1);
1403        }
1404        cache
1405            .get(location(0), CancelFlag::never())
1406            .expect("cache get")
1407            .expect("cached entry");
1408        assert_eq!(cache.least_recent, Some(location(1)));
1409        assert_eq!(cache.most_recent, Some(location(0)));
1410
1411        let mut polls = 0;
1412        assert!(
1413            cache
1414                .evict_one_except_with(None, || {
1415                    polls += 1;
1416                    Ok(())
1417                })
1418                .expect("eviction")
1419        );
1420        assert_eq!(polls, 2, "eviction work must not grow with cache size");
1421        assert!(!cache.entries.contains_key(&location(1)));
1422        assert_eq!(cache.least_recent, Some(location(2)));
1423        assert_eq!(cache.most_recent, Some(location(0)));
1424        assert_eq!(cache.entries.len(), 255);
1425        assert_eq!(cache.used, 255);
1426        assert_eq!(cache.evictions, 1);
1427    }
1428
1429    #[test]
1430    fn cache_replacement_and_pinned_eviction_keep_exact_order_and_bytes() {
1431        let mut cache = ByteCache::new(8);
1432        insert_cached(&mut cache, 1, 1);
1433        insert_cached(&mut cache, 2, 2);
1434        insert_cached(&mut cache, 3, 3);
1435        assert_eq!(cache.used, 6);
1436
1437        // Replacing the least-recent entry pins it during capacity eviction.
1438        insert_cached(&mut cache, 1, 5);
1439        assert_eq!(cache.used, 8);
1440        assert!(!cache.entries.contains_key(&location(2)));
1441        assert!(cache.entries.contains_key(&location(3)));
1442        assert!(cache.entries.contains_key(&location(1)));
1443        assert_eq!(cache.least_recent, Some(location(3)));
1444        assert_eq!(cache.most_recent, Some(location(1)));
1445        assert_eq!(cache.evictions, 1);
1446    }
1447
1448    #[test]
1449    fn cancelled_clear_leaves_exact_partially_cleared_lru_state() {
1450        let mut cache = ByteCache::new(256);
1451        for offset in 0..256 {
1452            insert_cached(&mut cache, offset, 1);
1453        }
1454        let mut polls = 0;
1455        let error = cache
1456            .clear_with(|| {
1457                polls += 1;
1458                if polls == 3 {
1459                    return Err(GitError::Cancelled);
1460                }
1461                Ok(())
1462            })
1463            .expect_err("clear must observe cancellation between batches");
1464        assert!(matches!(error, GitError::Cancelled));
1465        assert_eq!(cache.entries.len(), 128);
1466        assert_eq!(cache.used, 128);
1467        assert_eq!(cache.least_recent, Some(location(128)));
1468        assert_eq!(cache.most_recent, Some(location(255)));
1469        assert_eq!(
1470            cache.entries[&location(128)].less_recent,
1471            None,
1472            "remaining head must be detached from removed entries"
1473        );
1474
1475        cache.clear_with(|| Ok(())).expect("finish clear");
1476        assert!(cache.entries.is_empty());
1477        assert_eq!(cache.used, 0);
1478        assert_eq!(cache.least_recent, None);
1479        assert_eq!(cache.most_recent, None);
1480    }
1481
1482    #[test]
1483    fn cancelled_loose_replacement_restores_location_and_resolved_bases() {
1484        let oid = test_oid();
1485        let prior_location = location(41);
1486        let replacement = || {
1487            RefDeltaBase::Resolved(ResolvedPackObject {
1488                object: Arc::new(EncodedObject::new(ObjectType::Blob, vec![9])),
1489                oid,
1490                depth: 0,
1491                origin: None,
1492            })
1493        };
1494
1495        let mut bases = RefDeltaBases::new();
1496        bases.insert_location(oid, prior_location);
1497        assert!(matches!(
1498            bases.replace_transactionally(oid, replacement(), || Err(GitError::Cancelled)),
1499            Err(GitError::Cancelled)
1500        ));
1501        assert!(matches!(
1502            bases.get(&oid),
1503            Some(RefDeltaBase::Location(location)) if *location == prior_location
1504        ));
1505
1506        let prior_object = Arc::new(EncodedObject::new(ObjectType::Blob, vec![7]));
1507        bases.entries.insert(
1508            oid,
1509            RefDeltaBase::Resolved(ResolvedPackObject {
1510                object: Arc::clone(&prior_object),
1511                oid,
1512                depth: 7,
1513                origin: Some(prior_location),
1514            }),
1515        );
1516        assert!(matches!(
1517            bases.replace_transactionally(oid, replacement(), || Err(GitError::Cancelled)),
1518            Err(GitError::Cancelled)
1519        ));
1520        let Some(RefDeltaBase::Resolved(restored)) = bases.get(&oid) else {
1521            panic!("resolved base must be restored");
1522        };
1523        assert!(Arc::ptr_eq(&restored.object, &prior_object));
1524        assert_eq!(restored.depth, 7);
1525        assert_eq!(restored.origin, Some(prior_location));
1526    }
1527
1528    #[test]
1529    fn allocator_failure_below_limit_is_not_reported_as_limit_rejection() {
1530        let mut decoder: BoundedPackDecoder<Vec<u8>> = BoundedPackDecoder {
1531            sources: Vec::new(),
1532            limits: PackReadLimits {
1533                max_delta_depth: 0,
1534                max_materialized_bytes: usize::MAX,
1535                max_cached_bytes: 0,
1536            },
1537            cache: ByteCache::new(0),
1538        };
1539        let mut stats = PackReadStats::start(0);
1540        let requested = (isize::MAX as usize) + 1;
1541        let error = decoder
1542            .allocate_body(requested, 0, None, CancelFlag::never(), &mut stats)
1543            .expect_err("Vec capacity overflow must be an allocation error");
1544        assert!(requested < decoder.limits.max_materialized_bytes);
1545        assert!(matches!(
1546            error,
1547            PackReadError::Allocation(PackAllocationError {
1548                requested: actual,
1549                active: 0,
1550                cached: 0,
1551            }) if actual == requested
1552        ));
1553    }
1554
1555    #[test]
1556    fn active_plus_requested_overflow_is_a_limit_without_allocating() {
1557        let mut decoder: BoundedPackDecoder<Vec<u8>> = BoundedPackDecoder {
1558            sources: Vec::new(),
1559            limits: PackReadLimits {
1560                max_delta_depth: 0,
1561                max_materialized_bytes: usize::MAX,
1562                max_cached_bytes: 0,
1563            },
1564            cache: ByteCache::new(0),
1565        };
1566        let mut stats = PackReadStats::start(0);
1567        let error = decoder
1568            .allocate_body(1, usize::MAX, None, CancelFlag::never(), &mut stats)
1569            .expect_err("overflowing active plus requested bytes must be a limit");
1570        assert!(matches!(
1571            error,
1572            PackReadError::Limit(PackLimitError {
1573                kind: PackLimitKind::MaterializedBytes,
1574                limit: usize::MAX,
1575                attempted: usize::MAX,
1576            })
1577        ));
1578    }
1579
1580    #[test]
1581    fn cached_plus_working_overflow_is_a_limit_before_body_allocation() {
1582        let mut decoder: BoundedPackDecoder<Vec<u8>> = BoundedPackDecoder {
1583            sources: Vec::new(),
1584            limits: PackReadLimits {
1585                max_delta_depth: 0,
1586                max_materialized_bytes: usize::MAX,
1587                max_cached_bytes: usize::MAX,
1588            },
1589            cache: ByteCache::new(usize::MAX),
1590        };
1591        insert_cached(&mut decoder.cache, 1, 1);
1592        let mut stats = PackReadStats::start(decoder.cache.used);
1593        let error = decoder
1594            .ensure_materialized(usize::MAX, 0, None, CancelFlag::never(), &mut stats)
1595            .expect_err("overflowing cached plus working bytes must be a limit");
1596        assert!(matches!(
1597            error,
1598            PackReadError::Limit(PackLimitError {
1599                kind: PackLimitKind::MaterializedBytes,
1600                limit: usize::MAX,
1601                attempted: usize::MAX,
1602            })
1603        ));
1604    }
1605}