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