Skip to main content

sley_pack/
read.rs

1//! Pack parsing and random-access object/header reads (including delta resolution).
2//!
3//! Split out of `lib.rs` in the W21 mechanical refactor: a pure code move
4//! (no function body changed); all items are re-exported from `lib.rs`.
5use super::*;
6use sley_core::primitives::BiasedVarintError;
7
8impl PackFile {
9    pub fn parse_sha1(bytes: &[u8]) -> Result<Self> {
10        Self::parse_sha1_with_limits(bytes, PackReadLimits::default())
11    }
12
13    pub fn parse_sha1_with_limits(bytes: &[u8], limits: PackReadLimits) -> Result<Self> {
14        Self::parse_with_limits(bytes, ObjectFormat::Sha1, limits)
15    }
16
17    pub fn parse(bytes: &[u8], format: ObjectFormat) -> Result<Self> {
18        Self::parse_with_limits(bytes, format, PackReadLimits::default())
19    }
20
21    /// Parse and resolve a complete pack with explicit read limits.
22    pub fn parse_with_limits(
23        bytes: &[u8],
24        format: ObjectFormat,
25        limits: PackReadLimits,
26    ) -> Result<Self> {
27        Self::parse_with_base_and_limits(bytes, format, |_| Ok(None), limits)
28    }
29
30    pub fn parse_bundle(bundle: &Bundle) -> Result<Self> {
31        Self::parse_bundle_with_limits(bundle, PackReadLimits::default())
32    }
33
34    pub fn parse_bundle_with_limits(bundle: &Bundle, limits: PackReadLimits) -> Result<Self> {
35        Self::parse_with_limits(&bundle.pack, bundle.format, limits)
36    }
37
38    pub fn index_pack(bytes: &[u8], format: ObjectFormat) -> Result<PackWrite> {
39        Self::index_pack_with_limits(bytes, format, PackReadLimits::default())
40    }
41
42    pub fn index_pack_with_limits(
43        bytes: &[u8],
44        format: ObjectFormat,
45        limits: PackReadLimits,
46    ) -> Result<PackWrite> {
47        let PackIndexBuild {
48            index,
49            pack_checksum,
50            entries,
51            objects: _,
52        } = PackIndex::write_v2_for_pack_with_limits(bytes, format, limits)?;
53        Ok(PackWrite {
54            pack: bytes.to_vec(),
55            index,
56            checksum: pack_checksum,
57            entries,
58            delta_count: 0,
59        })
60    }
61
62    pub fn parse_thin<F>(bytes: &[u8], format: ObjectFormat, external_base: F) -> Result<Self>
63    where
64        F: FnMut(&ObjectId) -> Result<Option<EncodedObject>>,
65    {
66        Self::parse_thin_with_limits(bytes, format, external_base, PackReadLimits::default())
67    }
68
69    pub fn parse_thin_with_limits<F>(
70        bytes: &[u8],
71        format: ObjectFormat,
72        external_base: F,
73        limits: PackReadLimits,
74    ) -> Result<Self>
75    where
76        F: FnMut(&ObjectId) -> Result<Option<EncodedObject>>,
77    {
78        Self::parse_with_base_and_limits(bytes, format, external_base, limits)
79    }
80
81    pub(crate) fn parse_with_base_and_limits<F>(
82        bytes: &[u8],
83        format: ObjectFormat,
84        mut external_base: F,
85        limits: PackReadLimits,
86    ) -> Result<Self>
87    where
88        F: FnMut(&ObjectId) -> Result<Option<EncodedObject>>,
89    {
90        parse_parallel_pack(
91            bytes,
92            format,
93            &mut external_base,
94            PackIndexOptions::new(limits),
95            CancelFlag::never(),
96        )
97    }
98
99    /// Walk the pack and produce per-object statistics matching the output of
100    /// `git verify-pack -v` / `git index-pack --verify-stat`.
101    ///
102    /// Objects are returned in pack offset order (the order `git verify-pack -v`
103    /// prints them). Each entry carries the *resolved* object id, type and size,
104    /// the in-pack byte span (`size_in_pack` = the offset delta to the next
105    /// object, or to the trailing checksum for the last object), the in-pack
106    /// offset, the delta chain depth (`0` for undeltified objects), and — for
107    /// deltas — the object id of the *immediate* base (which may itself be a
108    /// delta). This mirrors `builtin/index-pack.c`'s `show_pack_info`.
109    pub fn verify_pack_stats(bytes: &[u8], format: ObjectFormat) -> Result<PackVerifyStats> {
110        Self::verify_pack_stats_with_limits(bytes, format, PackReadLimits::default())
111    }
112
113    pub fn verify_pack_stats_with_limits(
114        bytes: &[u8],
115        format: ObjectFormat,
116        limits: PackReadLimits,
117    ) -> Result<PackVerifyStats> {
118        // Resolve the whole pack first: this validates the trailing checksum,
119        // every object's inflate, and yields the resolved oid/type/size keyed by
120        // offset. `verify-pack` is exactly this validation plus the stat report.
121        let pack = Self::parse_with_limits(bytes, format, limits)?;
122
123        // Independently walk the on-disk entries to recover each object's stored
124        // kind and (for deltas) its base reference — information `PackFile`
125        // discards once deltas are resolved.
126        let trailer_len = format.raw_len();
127        let trailer_offset = bytes.len() - trailer_len;
128        let entry_region = pack_entry_region(bytes, trailer_offset)?;
129        let count = checked_pack_object_count(
130            u32_be(&bytes[8..12]),
131            (trailer_offset.saturating_sub(12)) as u64,
132        )?;
133        let mut offset = 12usize;
134        // Per entry in read (offset) order: (offset, base, on-disk stream size).
135        // The stream size is what git prints in the size column: it is the
136        // resolved object size for an undeltified entry, but the *delta
137        // instruction stream* length for a delta entry (builtin/index-pack.c sets
138        // `obj->size` from the entry header, before any delta is applied).
139        let mut on_disk: Vec<OnDiskEntry> = Vec::with_capacity(pack_entry_prealloc(count));
140        for _ in 0..count {
141            let entry_offset = offset as u64;
142            let header = parse_entry_header(entry_region, &mut offset)?;
143            let stream_size = header.size;
144            let base = match header.kind {
145                PackObjectKind::OfsDelta => Some(DeltaBase::Offset(parse_ofs_delta_base_offset(
146                    entry_region,
147                    &mut offset,
148                    entry_offset,
149                )?)),
150                PackObjectKind::RefDelta => {
151                    let hash_len = format.raw_len();
152                    if offset + hash_len > trailer_offset {
153                        return Err(GitError::InvalidFormat(
154                            "truncated ref-delta base object id".into(),
155                        ));
156                    }
157                    let oid = ObjectId::from_raw(format, &entry_region[offset..offset + hash_len])?;
158                    offset += hash_len;
159                    Some(DeltaBase::Ref(oid))
160                }
161                _ => None,
162            };
163            // Skip the compressed body to reach the next entry header.
164            let mut body = Vec::new();
165            let consumed = inflate_into(
166                &entry_region[offset..],
167                &mut body,
168                header.size.min(usize::MAX as u64) as usize,
169            )?;
170            offset = offset
171                .checked_add(consumed)
172                .ok_or_else(|| GitError::InvalidFormat("pack offset overflow".into()))?;
173            on_disk.push(OnDiskEntry {
174                offset: entry_offset,
175                base,
176                stream_size,
177            });
178        }
179
180        // Map offset -> resolved object so the on-disk walk can join in oid/type.
181        let mut resolved_by_offset: HashMap<u64, &PackObject> =
182            HashMap::with_capacity(pack.entries.len());
183        for object in &pack.entries {
184            resolved_by_offset.insert(object.entry.offset, object);
185        }
186        // Map offset -> resolved oid, for ofs-delta base lookups.
187        let mut oid_by_offset: HashMap<u64, ObjectId> = HashMap::with_capacity(on_disk.len());
188        for entry in &on_disk {
189            if let Some(object) = resolved_by_offset.get(&entry.offset) {
190                oid_by_offset.insert(entry.offset, object.entry.oid);
191            }
192        }
193        // Map base offset -> index in `on_disk`, for delta-depth propagation.
194        let mut index_by_offset: HashMap<u64, usize> = HashMap::with_capacity(on_disk.len());
195        for (idx, entry) in on_disk.iter().enumerate() {
196            index_by_offset.insert(entry.offset, idx);
197        }
198
199        // Sorted offsets give the size-in-pack span (next offset - this offset),
200        // with the trailing checksum offset as the final sentinel.
201        let mut sorted_offsets: Vec<u64> = on_disk.iter().map(|entry| entry.offset).collect();
202        sorted_offsets.sort_unstable();
203        let mut next_offset: HashMap<u64, u64> = HashMap::with_capacity(sorted_offsets.len());
204        for window in sorted_offsets.windows(2) {
205            next_offset.insert(window[0], window[1]);
206        }
207        if let Some(last) = sorted_offsets.last() {
208            next_offset.insert(*last, trailer_offset as u64);
209        }
210
211        // Compute delta depth by following base offsets. Depth of a non-delta is
212        // 0; a delta's depth is its base's depth + 1. `index_by_offset` lets an
213        // ofs-delta find its base's index; a ref-delta resolves its base oid to
214        // an in-pack offset when present (thin-pack external bases are not stored
215        // in this pack, but verify-pack only ever runs on self-contained packs).
216        let mut depth = vec![None; on_disk.len()];
217        fn resolve_depth(
218            idx: usize,
219            on_disk: &[OnDiskEntry],
220            index_by_offset: &HashMap<u64, usize>,
221            offset_of_oid: &HashMap<ObjectId, u64>,
222            depth: &mut [Option<u32>],
223        ) -> u32 {
224            if let Some(d) = depth[idx] {
225                return d;
226            }
227            let computed = match &on_disk[idx].base {
228                None => 0,
229                Some(base) => {
230                    let base_idx = match base {
231                        DeltaBase::Offset(off) => index_by_offset.get(off).copied(),
232                        DeltaBase::Ref(oid) => offset_of_oid
233                            .get(oid)
234                            .and_then(|off| index_by_offset.get(off).copied()),
235                    };
236                    match base_idx {
237                        Some(bi) => {
238                            resolve_depth(bi, on_disk, index_by_offset, offset_of_oid, depth) + 1
239                        }
240                        // Base not in this pack (thin pack); treat as depth 1.
241                        None => 1,
242                    }
243                }
244            };
245            depth[idx] = Some(computed);
246            computed
247        }
248        let mut offset_of_oid: HashMap<ObjectId, u64> = HashMap::with_capacity(oid_by_offset.len());
249        for (off, oid) in &oid_by_offset {
250            offset_of_oid.insert(*oid, *off);
251        }
252        for idx in 0..on_disk.len() {
253            resolve_depth(idx, &on_disk, &index_by_offset, &offset_of_oid, &mut depth);
254        }
255
256        let mut stats = Vec::with_capacity(on_disk.len());
257        for (idx, entry) in on_disk.iter().enumerate() {
258            let off = entry.offset;
259            let object = resolved_by_offset.get(&off).ok_or_else(|| {
260                GitError::InvalidFormat("pack offset missing from resolved set".into())
261            })?;
262            let size_in_pack = next_offset
263                .get(&off)
264                .copied()
265                .unwrap_or(trailer_offset as u64)
266                .saturating_sub(off);
267            let base_oid = match &entry.base {
268                None => None,
269                Some(DeltaBase::Offset(base_off)) => oid_by_offset.get(base_off).copied(),
270                Some(DeltaBase::Ref(oid)) => Some(*oid),
271            };
272            stats.push(PackVerifyStat {
273                oid: object.entry.oid,
274                object_type: object.object.object_type,
275                // git prints the on-disk stream size: object body size for an
276                // undeltified entry, delta-instruction stream size for a delta.
277                size: entry.stream_size,
278                size_in_pack,
279                offset: off,
280                delta_depth: depth[idx].unwrap_or(0),
281                base_oid,
282            });
283        }
284        // Emit in pack offset order, matching git's read order.
285        stats.sort_by_key(|stat| stat.offset);
286
287        Ok(PackVerifyStats {
288            objects: stats,
289            checksum: pack.checksum,
290        })
291    }
292}
293
294/// A cache of objects already decoded from one specific pack, keyed by the
295/// in-pack byte offset at which each object's entry begins.
296///
297/// Delta resolution within a pack walks a chain of base objects by offset; the
298/// same base is the parent of many deltas, so without a cache the entire chain
299/// is re-inflated and re-applied on every read. Implementors let
300/// [`read_object_at_with_cache_arc`] reuse a warm base instead.
301///
302/// Correctness contract: a given `offset` within a given pack's bytes always
303/// decodes to exactly one object, so caching by offset can never serve the wrong
304/// object **provided the same cache is only ever used with one pack's bytes**.
305/// Callers must therefore scope a cache to a single pack (e.g. key it by pack
306/// path). The default [`read_object_at_arc`] uses a no-op cache and is unaffected.
307pub trait PackDeltaCache {
308    /// Return the decoded object whose entry begins at `offset`, if cached.
309    fn get(&self, offset: u64) -> Option<Arc<EncodedObject>>;
310    /// Record that the entry beginning at `offset` decodes to `object`.
311    fn insert(&self, offset: u64, object: Arc<EncodedObject>);
312}
313
314/// A [`PackDeltaCache`] that stores nothing; used by [`read_object_at_arc`] to keep
315/// the original, allocation-free behavior for callers that do not opt in.
316pub(crate) struct NoopDeltaCache;
317
318impl PackDeltaCache for NoopDeltaCache {
319    fn get(&self, _offset: u64) -> Option<Arc<EncodedObject>> {
320        None
321    }
322    fn insert(&self, _offset: u64, _object: Arc<EncodedObject>) {}
323}
324
325// Reused zlib inflate state. Resetting and reusing one `Decompress` avoids
326// allocating a fresh (~10 KiB) `InflateState` for every object and delta decoded —
327// an allocation that dominated bulk reads. Borrowed only for the duration of a
328// single inflate; the recursive pack reader fully inflates each entry's data before
329// recursing to its base, so the borrow never nests.
330thread_local! {
331    static INFLATE: RefCell<flate2::Decompress> = RefCell::new(flate2::Decompress::new(true));
332}
333
334/// The largest ratio by which a single DEFLATE/zlib member can expand its input.
335/// The theoretical worst case for raw DEFLATE is ~1032:1 (a maximally efficient
336/// run of back-references). We pre-reserve no more than this multiple of the
337/// available compressed input, so an attacker who declares a huge `size_hint`
338/// (e.g. `u64::MAX`) cannot make us reserve — and thus commit — gigabytes of
339/// memory before the inflate has produced a single byte. The stream's *actual*
340/// output is still verified against the declared size by the caller; this only
341/// bounds the speculative allocation. git never pre-allocates an attacker's
342/// declared size beyond a streaming buffer either (see index-pack.c's
343/// `unpack_entry_data`).
344///
345/// Inflate the entire zlib stream at the front of `compressed`, appending the
346/// decoded bytes to `out`, reusing the thread-local inflate state. `size_hint`
347/// is the caller's expectation for the decompressed length, but it is treated as
348/// untrusted: the up-front reservation is bounded by [`inflate::bounded_inflate_reserve`]
349/// so a crafted hint can never drive an out-of-memory pre-allocation. Returns the
350/// number of *compressed* bytes consumed (so callers stepping through a pack can
351/// advance to the next entry). Byte-for-byte equivalent to
352/// `ZlibDecoder::read_to_end` + `total_in`.
353pub(crate) fn inflate_into(
354    compressed: &[u8],
355    out: &mut Vec<u8>,
356    size_hint: usize,
357) -> Result<usize> {
358    INFLATE.with(|cell| {
359        let mut decompress = cell.borrow_mut();
360        decompress.reset(true);
361        out.reserve(inflate::bounded_inflate_reserve(
362            size_hint,
363            compressed.len(),
364        ));
365        let mut input = compressed;
366        let mut consumed_total = 0usize;
367        loop {
368            // Always leave output room so a zero-progress result means the input
369            // (not the buffer) is exhausted.
370            if out.len() == out.capacity() {
371                out.reserve(out.len().max(64));
372            }
373            let before_in = decompress.total_in();
374            let before_out = decompress.total_out();
375            let status = decompress
376                .decompress_vec(input, out, flate2::FlushDecompress::None)
377                .map_err(|err| GitError::InvalidObject(format!("zlib inflate failed: {err}")))?;
378            let consumed = (decompress.total_in() - before_in) as usize;
379            let produced = decompress.total_out() - before_out;
380            input = &input[consumed..];
381            consumed_total += consumed;
382            match status {
383                flate2::Status::StreamEnd => return Ok(consumed_total),
384                _ if consumed == 0 && produced == 0 => {
385                    return Err(GitError::InvalidObject("truncated zlib stream".into()));
386                }
387                _ => {}
388            }
389        }
390    })
391}
392
393/// Inflate at most `out.len()` bytes (or until the stream ends) from `compressed`
394/// into `out`, reusing the thread-local state. Used to read a delta's leading
395/// base-size / result-size varints without inflating the whole instruction stream
396/// or allocating a heap prefix buffer (sley#26).
397pub(crate) fn inflate_prefix(compressed: &[u8], out: &mut [u8]) -> Result<usize> {
398    INFLATE.with(|cell| {
399        let mut decompress = cell.borrow_mut();
400        decompress.reset(true);
401        let mut input = compressed;
402        let mut written = 0usize;
403        while written < out.len() {
404            let before_in = decompress.total_in();
405            let before_out = decompress.total_out();
406            let status = decompress
407                .decompress(input, &mut out[written..], flate2::FlushDecompress::None)
408                .map_err(|err| GitError::InvalidObject(format!("zlib inflate failed: {err}")))?;
409            let consumed = (decompress.total_in() - before_in) as usize;
410            let produced = (decompress.total_out() - before_out) as usize;
411            input = &input[consumed..];
412            written = written.saturating_add(produced);
413            if status == flate2::Status::StreamEnd || (consumed == 0 && produced == 0) {
414                break;
415            }
416        }
417        Ok(written)
418    })
419}
420/// Decode the single object stored at byte `offset` within `pack_bytes`, reading
421/// only that object and its delta-base chain instead of parsing the whole pack.
422///
423/// Ofs-delta bases are followed by offset (recursively, within this pack);
424/// ref-delta bases are obtained from `resolve_ref_base`, which the caller backs
425/// with the surrounding object store (so a base in another pack or loose still
426/// resolves). The pack trailer checksum is the final `format.raw_len()` bytes.
427pub fn read_object_at_arc<F>(
428    pack_bytes: &[u8],
429    offset: u64,
430    format: ObjectFormat,
431    resolve_ref_base: F,
432) -> Result<Arc<EncodedObject>>
433where
434    F: FnMut(&ObjectId) -> Result<Option<Arc<EncodedObject>>>,
435{
436    read_object_at_with_cache_arc(
437        pack_bytes,
438        offset,
439        format,
440        resolve_ref_base,
441        &NoopDeltaCache,
442    )
443}
444
445/// Like [`read_object_at_arc`], but reuses already-decoded objects from `cache`
446/// (keyed by in-pack offset) and records every object it decodes.
447///
448/// This turns repeated reads from the same pack — where many deltas share a base
449/// chain — from re-inflating each chain per read into resolving each base once.
450/// `cache` must be scoped to the pack `pack_bytes` belongs to (see
451/// [`PackDeltaCache`]). The decoded object is returned behind an [`Arc`] so
452/// callers can reuse cache handles without cloning full object bodies.
453pub fn read_object_at_with_cache_arc<F, C>(
454    pack_bytes: &[u8],
455    offset: u64,
456    format: ObjectFormat,
457    mut resolve_ref_base: F,
458    cache: &C,
459) -> Result<Arc<EncodedObject>>
460where
461    F: FnMut(&ObjectId) -> Result<Option<Arc<EncodedObject>>>,
462    C: PackDeltaCache + ?Sized,
463{
464    read_object_at_with_cache_and_ofs_base_arc(
465        pack_bytes,
466        offset,
467        format,
468        &mut resolve_ref_base,
469        |_offset| Ok(None),
470        cache,
471    )
472}
473
474/// Like [`read_object_at_with_cache_arc`], but lets an object-database caller
475/// recover an ofs-delta base from another storage copy when the in-pack base
476/// offset cannot be decoded. Direct pack verification should keep using the
477/// strict APIs; this hook mirrors normal object lookup, where a corrupt packed
478/// copy does not hide a good loose or redundant packed copy.
479pub fn read_object_at_with_cache_and_ofs_base_arc<F, G, C>(
480    pack_bytes: &[u8],
481    offset: u64,
482    format: ObjectFormat,
483    mut resolve_ref_base: F,
484    mut resolve_ofs_base: G,
485    cache: &C,
486) -> Result<Arc<EncodedObject>>
487where
488    F: FnMut(&ObjectId) -> Result<Option<Arc<EncodedObject>>>,
489    G: FnMut(u64) -> Result<Option<Arc<EncodedObject>>>,
490    C: PackDeltaCache + ?Sized,
491{
492    read_object_at_inner(
493        pack_bytes,
494        offset,
495        format,
496        &mut resolve_ref_base,
497        &mut resolve_ofs_base,
498        cache,
499    )
500}
501
502/// Like [`read_object_at_with_cache_and_ofs_base_arc`], without an offset-cache.
503pub fn read_object_at_with_ofs_base_arc<F, G>(
504    pack_bytes: &[u8],
505    offset: u64,
506    format: ObjectFormat,
507    resolve_ref_base: F,
508    resolve_ofs_base: G,
509) -> Result<Arc<EncodedObject>>
510where
511    F: FnMut(&ObjectId) -> Result<Option<Arc<EncodedObject>>>,
512    G: FnMut(u64) -> Result<Option<Arc<EncodedObject>>>,
513{
514    read_object_at_with_cache_and_ofs_base_arc(
515        pack_bytes,
516        offset,
517        format,
518        resolve_ref_base,
519        resolve_ofs_base,
520        &NoopDeltaCache,
521    )
522}
523
524pub(crate) fn read_object_at_inner<F, G, C>(
525    pack_bytes: &[u8],
526    offset: u64,
527    format: ObjectFormat,
528    resolve_ref_base: &mut F,
529    resolve_ofs_base: &mut G,
530    cache: &C,
531) -> Result<Arc<EncodedObject>>
532where
533    F: FnMut(&ObjectId) -> Result<Option<Arc<EncodedObject>>>,
534    G: FnMut(u64) -> Result<Option<Arc<EncodedObject>>>,
535    C: PackDeltaCache + ?Sized,
536{
537    // A warm cache entry for this exact offset is already the fully resolved
538    // object, so the whole base chain below can be skipped.
539    if let Some(object) = cache.get(offset) {
540        return Ok(object);
541    }
542    let trailer_offset = pack_bytes
543        .len()
544        .checked_sub(format.raw_len())
545        .ok_or_else(|| GitError::InvalidFormat("pack smaller than its trailer".into()))?;
546    let entry_region = pack_entry_region(pack_bytes, trailer_offset)?;
547    let mut cursor = usize::try_from(offset)
548        .ok()
549        .filter(|&value| value < trailer_offset)
550        .ok_or_else(|| GitError::InvalidFormat("pack object offset out of range".into()))?;
551    let header = parse_entry_header(entry_region, &mut cursor)?;
552    let base = match header.kind {
553        PackObjectKind::OfsDelta => Some(DeltaBase::Offset(parse_ofs_delta_base_offset(
554            entry_region,
555            &mut cursor,
556            offset,
557        )?)),
558        PackObjectKind::RefDelta => {
559            let hash_len = format.raw_len();
560            if cursor + hash_len > trailer_offset {
561                return Err(GitError::InvalidFormat(
562                    "truncated ref-delta base object id".into(),
563                ));
564            }
565            let oid = ObjectId::from_raw(format, &entry_region[cursor..cursor + hash_len])?;
566            cursor += hash_len;
567            Some(DeltaBase::Ref(oid))
568        }
569        _ => None,
570    };
571    let mut body = Vec::new();
572    inflate_into(
573        &entry_region[cursor..],
574        &mut body,
575        header.size.min(usize::MAX as u64) as usize,
576    )?;
577    if body.len() as u64 != header.size {
578        return Err(GitError::InvalidObject(format!(
579            "pack object declared {} bytes, decoded {}",
580            header.size,
581            body.len()
582        )));
583    }
584    let object = match base {
585        None => {
586            let object_type = match header.kind {
587                PackObjectKind::Commit => ObjectType::Commit,
588                PackObjectKind::Tree => ObjectType::Tree,
589                PackObjectKind::Blob => ObjectType::Blob,
590                PackObjectKind::Tag => ObjectType::Tag,
591                PackObjectKind::OfsDelta | PackObjectKind::RefDelta => {
592                    return Err(GitError::InvalidFormat(
593                        "delta pack entry decoded without a base".into(),
594                    ));
595                }
596            };
597            Arc::new(EncodedObject::new(object_type, body))
598        }
599        Some(DeltaBase::Offset(base_offset)) => {
600            let base = match read_object_at_inner(
601                pack_bytes,
602                base_offset,
603                format,
604                resolve_ref_base,
605                resolve_ofs_base,
606                cache,
607            ) {
608                Ok(base) => base,
609                Err(pack_err) => match resolve_ofs_base(base_offset)? {
610                    Some(base) => base,
611                    None => return Err(pack_err),
612                },
613            };
614            let resolved = apply_pack_delta(&base.body, &body)?;
615            Arc::new(EncodedObject::new(base.object_type, resolved))
616        }
617        Some(DeltaBase::Ref(base_oid)) => {
618            let base = resolve_ref_base(&base_oid)?
619                .ok_or_else(|| GitError::not_found(format!("ref-delta base object {base_oid}")))?;
620            let resolved = apply_pack_delta(&base.body, &body)?;
621            Arc::new(EncodedObject::new(base.object_type, resolved))
622        }
623    };
624    // Record the fully resolved object so any later read that walks through this
625    // offset (as a delta base or directly) reuses it. Bases are inserted as the
626    // recursion unwinds, so a chain is decoded at most once across reads.
627    cache.insert(offset, Arc::clone(&object));
628    Ok(object)
629}
630
631/// The object type and final (inflated) size of the entry at `offset`, *without*
632/// materializing the object body — git's `cat-file --batch-check` fast path.
633///
634/// A base object's size is already in its pack entry header, and a delta's result
635/// size is the second varint at the front of its (small) delta stream, so neither
636/// inflates the full content. The reported type is the type at the end of the
637/// delta chain (deltas inherit their base's type). `resolve_ref_base_type` supplies
638/// the type of a ref-delta base that lives outside this pack (resolved through the
639/// wider object store) and receives the cumulative depth after following that
640/// ref-delta. `initial_delta_depth` carries depth already traversed in another
641/// pack so mixed ref/ofs chains share the same finite recursion bound; top-level
642/// callers pass zero. Ofs-delta bases are followed within `pack_bytes` directly.
643pub fn read_object_header_at<F>(
644    pack_bytes: &[u8],
645    offset: u64,
646    format: ObjectFormat,
647    initial_delta_depth: usize,
648    mut resolve_ref_base_type: F,
649) -> Result<PackObjectHeader>
650where
651    F: FnMut(&ObjectId, usize) -> Result<Option<PackObjectHeader>>,
652{
653    read_object_header_at_inner(
654        pack_bytes,
655        offset,
656        format,
657        initial_delta_depth,
658        &mut resolve_ref_base_type,
659        &mut NoopHeaderTypeCache,
660    )
661}
662
663/// A header resolved through its complete delta chain without materializing the
664/// object's body.
665#[derive(Clone, Copy, Debug, PartialEq, Eq)]
666pub struct PackObjectHeader {
667    pub object_type: ObjectType,
668    pub size: u64,
669    /// Delta links below this entry (`0` for an undeltified object).
670    pub delta_depth: usize,
671}
672
673impl PackObjectHeader {
674    pub const fn undeltified(object_type: ObjectType, size: u64) -> Self {
675        Self {
676            object_type,
677            size,
678            delta_depth: 0,
679        }
680    }
681
682    pub const fn type_and_size(self) -> (ObjectType, u64) {
683        (self.object_type, self.size)
684    }
685}
686
687/// Memo of `pack offset -> resolved header` for
688/// the `cat-file --batch-check` header fast path.
689///
690/// Without it, resolving the *type* of an ofs-delta walks the whole delta chain
691/// to its base on every header read, re-inflating each link's leading varints
692/// from scratch — so reading every object in a deeply-deltified pack costs
693/// O(objects x chain-depth) and goes super-linear (sley#26). Two reuses fall out
694/// of memoizing `offset -> (type, size)`:
695///
696/// * a chain's end-of-chain type is resolved at most once, so later objects on
697///   the same chain skip the walk; and
698/// * a repeated lookup of the same object (common in batch input) returns from
699///   the memo without re-inflating its delta header at all.
700///
701/// The size stored is the object's final (inflated) result size — read from its
702/// own pack/delta header, never by materializing the body.
703pub trait HeaderTypeCache {
704    /// The previously resolved header at `pack_offset`, if any.
705    fn get(&self, pack_offset: u64) -> Option<PackObjectHeader>;
706    /// Record the resolved header at `pack_offset` for reuse by later reads.
707    fn put(&mut self, pack_offset: u64, header: PackObjectHeader);
708}
709
710pub(crate) struct NoopHeaderTypeCache;
711
712impl HeaderTypeCache for NoopHeaderTypeCache {
713    fn get(&self, _pack_offset: u64) -> Option<PackObjectHeader> {
714        None
715    }
716    fn put(&mut self, _pack_offset: u64, _header: PackObjectHeader) {}
717}
718
719/// Like [`read_object_header_at`] but threads a caller-owned [`HeaderTypeCache`]
720/// through the read so (a) the ofs-delta chain's end-of-chain type is resolved at
721/// most once per chain and (b) a repeated lookup of the same offset returns from
722/// the memo without re-inflating (sley#26). The cache is keyed by in-pack offset,
723/// so it must be scoped to a single pack's bytes by the caller. Depth semantics
724/// match [`read_object_header_at`].
725pub fn read_object_header_at_with_cache<F, C>(
726    pack_bytes: &[u8],
727    offset: u64,
728    format: ObjectFormat,
729    initial_delta_depth: usize,
730    mut resolve_ref_base_type: F,
731    type_cache: &mut C,
732) -> Result<PackObjectHeader>
733where
734    F: FnMut(&ObjectId, usize) -> Result<Option<PackObjectHeader>>,
735    C: HeaderTypeCache + ?Sized,
736{
737    if let Some(header) = type_cache.get(offset) {
738        checked_cached_header_depth(offset, initial_delta_depth, header.delta_depth)?;
739        return Ok(header);
740    }
741    read_object_header_at_inner(
742        pack_bytes,
743        offset,
744        format,
745        initial_delta_depth,
746        &mut resolve_ref_base_type,
747        type_cache,
748    )
749}
750
751pub(crate) fn read_object_header_at_inner<F, C>(
752    pack_bytes: &[u8],
753    offset: u64,
754    format: ObjectFormat,
755    delta_depth: usize,
756    resolve_ref_base_type: &mut F,
757    type_cache: &mut C,
758) -> Result<PackObjectHeader>
759where
760    F: FnMut(&ObjectId, usize) -> Result<Option<PackObjectHeader>>,
761    C: HeaderTypeCache + ?Sized,
762{
763    let trailer_offset = pack_bytes
764        .len()
765        .checked_sub(format.raw_len())
766        .ok_or_else(|| GitError::InvalidFormat("pack smaller than its trailer".into()))?;
767    let entry_region = pack_entry_region(pack_bytes, trailer_offset)?;
768    let mut cursor = usize::try_from(offset)
769        .ok()
770        .filter(|&value| value < trailer_offset)
771        .ok_or_else(|| GitError::InvalidFormat("pack object offset out of range".into()))?;
772    let header = parse_entry_header(entry_region, &mut cursor)?;
773    let resolved = match header.kind {
774        PackObjectKind::Commit => PackObjectHeader::undeltified(ObjectType::Commit, header.size),
775        PackObjectKind::Tree => PackObjectHeader::undeltified(ObjectType::Tree, header.size),
776        PackObjectKind::Blob => PackObjectHeader::undeltified(ObjectType::Blob, header.size),
777        PackObjectKind::Tag => PackObjectHeader::undeltified(ObjectType::Tag, header.size),
778        PackObjectKind::OfsDelta => {
779            let next_delta_depth = checked_header_delta_depth(offset, delta_depth)?;
780            let base_offset = parse_ofs_delta_base_offset(entry_region, &mut cursor, offset)?;
781            let size = delta_result_size_from_stream(&entry_region[cursor..])?;
782            // The end-of-chain type only depends on the base, so reuse it across
783            // reads instead of re-walking the chain per object (sley#26).
784            let base_header = match type_cache.get(base_offset) {
785                Some(base_header) => {
786                    checked_cached_header_depth(
787                        base_offset,
788                        next_delta_depth,
789                        base_header.delta_depth,
790                    )?;
791                    base_header
792                }
793                None => read_object_header_at_inner(
794                    pack_bytes,
795                    base_offset,
796                    format,
797                    next_delta_depth,
798                    resolve_ref_base_type,
799                    type_cache,
800                )?,
801            };
802            let resolved_delta_depth = checked_header_delta_depth(offset, base_header.delta_depth)?;
803            PackObjectHeader {
804                object_type: base_header.object_type,
805                size,
806                delta_depth: resolved_delta_depth,
807            }
808        }
809        PackObjectKind::RefDelta => {
810            let next_delta_depth = checked_header_delta_depth(offset, delta_depth)?;
811            let hash_len = format.raw_len();
812            if cursor + hash_len > trailer_offset {
813                return Err(GitError::InvalidFormat(
814                    "truncated ref-delta base object id".into(),
815                ));
816            }
817            let oid = ObjectId::from_raw(format, &entry_region[cursor..cursor + hash_len])?;
818            cursor += hash_len;
819            let size = delta_result_size_from_stream(&entry_region[cursor..])?;
820            let base_header = resolve_ref_base_type(&oid, next_delta_depth)?
821                .ok_or_else(|| GitError::not_found(format!("ref-delta base object {oid}")))?;
822            let resolved_delta_depth = checked_header_delta_depth(offset, base_header.delta_depth)?;
823            checked_cached_header_depth(offset, delta_depth, resolved_delta_depth)?;
824            PackObjectHeader {
825                object_type: base_header.object_type,
826                size,
827                delta_depth: resolved_delta_depth,
828            }
829        }
830    };
831    // Memoize the fully resolved header so a repeated lookup of this offset (or a
832    // chain that bases on it) returns without re-inflating (sley#26).
833    type_cache.put(offset, resolved);
834    Ok(resolved)
835}
836
837fn checked_header_delta_depth(offset: u64, delta_depth: usize) -> Result<usize> {
838    let observed_depth = delta_depth.checked_add(1).ok_or_else(|| {
839        GitError::InvalidFormat(format!(
840            "pack delta chain depth overflows at offset {offset}"
841        ))
842    })?;
843    if observed_depth > MAX_READ_DELTA_CHAIN_DEPTH {
844        return Err(GitError::InvalidFormat(format!(
845            "pack delta chain at offset {offset} has observed depth {observed_depth}, which \
846             exceeds maximum depth (configured limit {MAX_READ_DELTA_CHAIN_DEPTH})"
847        )));
848    }
849    Ok(observed_depth)
850}
851
852fn checked_cached_header_depth(
853    offset: u64,
854    initial_delta_depth: usize,
855    cached_delta_depth: usize,
856) -> Result<()> {
857    let observed_depth = initial_delta_depth
858        .checked_add(cached_delta_depth)
859        .ok_or_else(|| {
860            GitError::InvalidFormat(format!(
861                "pack delta chain depth overflows at offset {offset}"
862            ))
863        })?;
864    if observed_depth > MAX_READ_DELTA_CHAIN_DEPTH {
865        return Err(GitError::InvalidFormat(format!(
866            "pack delta chain at offset {offset} has observed depth {observed_depth}, which \
867             exceeds maximum depth (configured limit {MAX_READ_DELTA_CHAIN_DEPTH})"
868        )));
869    }
870    Ok(())
871}
872
873/// Number of inflated delta-stream bytes to read when only the leading base-size
874/// and result-size varints are needed. Each varint is at most 10 bytes, so a short
875/// prefix always covers both without inflating the delta instructions.
876pub(crate) const DELTA_HEADER_PREFIX_LEN: usize = 32;
877
878/// Result size of a delta whose zlib-compressed stream starts at `compressed`,
879/// inflating only the short prefix that holds its two leading varints.
880pub(crate) fn delta_result_size_from_stream(compressed: &[u8]) -> Result<u64> {
881    let mut prefix = [0u8; DELTA_HEADER_PREFIX_LEN];
882    let written = inflate_prefix(compressed, &mut prefix)?;
883    decoded_delta_result_size(&prefix[..written])
884}
885
886/// The pack's entry region: everything between the 12-byte header and the
887/// trailing checksum.
888///
889/// Every varint cursor must walk *this* slice rather than the whole pack.
890/// [`next_byte`] stops at the end of whatever slice it is handed, so passing it
891/// the pack *including* the trailer lets an entry header whose continuation bit
892/// never clears go on consuming checksum bytes, leaving the cursor past
893/// `trailer_offset`. The entry-body slice `[cursor..trailer_offset]` that
894/// follows is then built with `start > end`, which panics — on remote input,
895/// since packs arrive straight off the wire (sley#162).
896///
897/// Bounding the cursor here makes that unrepresentable: a runaway varint simply
898/// runs out of slice and is reported as the truncated header it always was.
899fn pack_entry_region(bytes: &[u8], trailer_offset: usize) -> Result<&[u8]> {
900    bytes
901        .get(..trailer_offset)
902        .ok_or_else(|| GitError::InvalidFormat("pack smaller than its trailer".into()))
903}
904
905pub(crate) fn parse_entry_header(bytes: &[u8], offset: &mut usize) -> Result<EntryHeader> {
906    let first = next_byte(bytes, offset)?;
907    let mut size = u64::from(first & 0x0f);
908    let kind = match (first >> 4) & 0x07 {
909        1 => PackObjectKind::Commit,
910        2 => PackObjectKind::Tree,
911        3 => PackObjectKind::Blob,
912        4 => PackObjectKind::Tag,
913        6 => PackObjectKind::OfsDelta,
914        7 => PackObjectKind::RefDelta,
915        other => {
916            return Err(GitError::InvalidFormat(format!(
917                "invalid pack object type {other}"
918            )));
919        }
920    };
921    let mut shift = 4;
922    let mut byte = first;
923    while byte & 0x80 != 0 {
924        byte = next_byte(bytes, offset)?;
925        let part = u64::from(byte & 0x7f);
926        size = size
927            .checked_add(
928                part.checked_shl(shift)
929                    .ok_or_else(|| GitError::InvalidFormat("pack size overflow".into()))?,
930            )
931            .ok_or_else(|| GitError::InvalidFormat("pack size overflow".into()))?;
932        shift += 7;
933    }
934    Ok(EntryHeader { kind, size })
935}
936
937pub(crate) fn parse_ofs_delta_base_offset(
938    bytes: &[u8],
939    offset: &mut usize,
940    entry_offset: u64,
941) -> Result<u64> {
942    let relative = sley_core::primitives::read_biased_varint(bytes, offset).map_err(|err| {
943        GitError::InvalidFormat(
944            match err {
945                BiasedVarintError::Truncated => "truncated pack entry header",
946                BiasedVarintError::Overflow => "ofs-delta offset overflow",
947            }
948            .into(),
949        )
950    })?;
951    entry_offset
952        .checked_sub(relative)
953        .ok_or_else(|| GitError::InvalidFormat("ofs-delta points before pack start".into()))
954}
955
956pub(crate) fn apply_pack_delta(base: &[u8], delta: &[u8]) -> Result<Vec<u8>> {
957    let plan = plan_pack_delta(base, delta)?;
958    let result_size = plan.result_size;
959    let result_size_hint = usize::try_from(result_size).unwrap_or(usize::MAX);
960    // Preserve the legacy decoder's bounded speculative reservation followed by
961    // geometric Vec growth. Its malformed-input bytes, errors, and complexity
962    // are part of the Git-parity surface.
963    let mut result = Vec::with_capacity(inflate::bounded_inflate_reserve(
964        result_size_hint,
965        delta.len(),
966    ));
967    walk_pack_delta(base, delta, plan, CancelFlag::never(), |slice| {
968        result.extend_from_slice(slice);
969        Ok(())
970    })?;
971    if result.len() as u64 != result_size {
972        return Err(GitError::InvalidObject(format!(
973            "delta result size mismatch: expected {result_size}, got {}",
974            result.len()
975        )));
976    }
977    Ok(result)
978}
979
980#[derive(Debug, Clone, Copy)]
981pub(crate) struct PackDeltaPlan {
982    instructions_offset: usize,
983    pub(crate) result_size: u64,
984}
985
986pub(crate) fn plan_pack_delta(base: &[u8], delta: &[u8]) -> Result<PackDeltaPlan> {
987    let mut instructions_offset = 0usize;
988    let base_size = read_delta_varint(delta, &mut instructions_offset)?;
989    if base_size != base.len() as u64 {
990        return Err(GitError::InvalidObject(format!(
991            "delta base size mismatch: expected {base_size}, got {}",
992            base.len()
993        )));
994    }
995    let result_size = read_delta_varint(delta, &mut instructions_offset)?;
996    Ok(PackDeltaPlan {
997        instructions_offset,
998        result_size,
999    })
1000}
1001
1002pub(crate) fn apply_pack_delta_exact(
1003    base: &[u8],
1004    delta: &[u8],
1005    plan: PackDeltaPlan,
1006    result: &mut Vec<u8>,
1007    cancel: CancelFlag<'_>,
1008) -> Result<()> {
1009    if !result.is_empty() || u64::try_from(result.capacity()).unwrap_or(u64::MAX) < plan.result_size
1010    {
1011        return Err(GitError::InvalidObject(
1012            "delta output buffer is not empty and preallocated to the declared result size".into(),
1013        ));
1014    }
1015    walk_pack_delta(base, delta, plan, cancel, |slice| {
1016        let end = result
1017            .len()
1018            .checked_add(slice.len())
1019            .ok_or_else(|| GitError::InvalidObject("delta output range overflow".into()))?;
1020        if u64::try_from(end).unwrap_or(u64::MAX) > plan.result_size {
1021            return Err(GitError::InvalidObject(
1022                "delta instructions exceed declared result size".into(),
1023            ));
1024        }
1025        result.extend_from_slice(slice);
1026        Ok(())
1027    })?;
1028    cancel.check()?;
1029    if result.len() as u64 != plan.result_size {
1030        return Err(GitError::InvalidObject(format!(
1031            "delta result size mismatch: expected {}, got {}",
1032            plan.result_size,
1033            result.len()
1034        )));
1035    }
1036    Ok(())
1037}
1038
1039fn walk_pack_delta<F>(
1040    base: &[u8],
1041    delta: &[u8],
1042    plan: PackDeltaPlan,
1043    cancel: CancelFlag<'_>,
1044    mut emit: F,
1045) -> Result<()>
1046where
1047    F: FnMut(&[u8]) -> Result<()>,
1048{
1049    let mut cursor = plan.instructions_offset;
1050    while cursor < delta.len() {
1051        cancel.check()?;
1052        let command = delta[cursor];
1053        cursor += 1;
1054        if command & 0x80 != 0 {
1055            let copy_offset =
1056                read_delta_copy_value(delta, &mut cursor, command, &[0x01, 0x02, 0x04, 0x08])?;
1057            let mut copy_size =
1058                read_delta_copy_value(delta, &mut cursor, command, &[0x10, 0x20, 0x40])?;
1059            if copy_size == 0 {
1060                copy_size = 0x10000;
1061            }
1062            let start = usize::try_from(copy_offset)
1063                .map_err(|_| GitError::InvalidObject("delta copy offset overflows usize".into()))?;
1064            let len = usize::try_from(copy_size)
1065                .map_err(|_| GitError::InvalidObject("delta copy size overflows usize".into()))?;
1066            let end = start
1067                .checked_add(len)
1068                .ok_or_else(|| GitError::InvalidObject("delta copy range overflow".into()))?;
1069            let Some(slice) = base.get(start..end) else {
1070                return Err(GitError::InvalidObject(
1071                    "delta copy range exceeds base object".into(),
1072                ));
1073            };
1074            emit(slice)?;
1075        } else if command != 0 {
1076            let len = usize::from(command);
1077            let end = cursor
1078                .checked_add(len)
1079                .ok_or_else(|| GitError::InvalidObject("delta insert range overflow".into()))?;
1080            let Some(slice) = delta.get(cursor..end) else {
1081                return Err(GitError::InvalidObject(
1082                    "delta insert range exceeds delta data".into(),
1083                ));
1084            };
1085            emit(slice)?;
1086            cursor = end;
1087        } else {
1088            return Err(GitError::InvalidObject(
1089                "delta contains reserved zero command".into(),
1090            ));
1091        }
1092    }
1093    cancel.check()?;
1094    Ok(())
1095}
1096
1097pub(crate) fn decoded_delta_result_size(delta: &[u8]) -> Result<u64> {
1098    let mut cursor = 0usize;
1099    let _ = read_delta_varint(delta, &mut cursor)?;
1100    read_delta_varint(delta, &mut cursor)
1101}
1102
1103pub(crate) fn read_delta_varint(delta: &[u8], cursor: &mut usize) -> Result<u64> {
1104    let mut value = 0u64;
1105    let mut shift = 0u32;
1106    loop {
1107        let Some(byte) = delta.get(*cursor).copied() else {
1108            return Err(GitError::InvalidObject("truncated delta size".into()));
1109        };
1110        *cursor += 1;
1111        value = value
1112            .checked_add(
1113                u64::from(byte & 0x7f)
1114                    .checked_shl(shift)
1115                    .ok_or_else(|| GitError::InvalidObject("delta size overflow".into()))?,
1116            )
1117            .ok_or_else(|| GitError::InvalidObject("delta size overflow".into()))?;
1118        if byte & 0x80 == 0 {
1119            return Ok(value);
1120        }
1121        shift = shift
1122            .checked_add(7)
1123            .ok_or_else(|| GitError::InvalidObject("delta size overflow".into()))?;
1124    }
1125}
1126
1127pub(crate) fn read_delta_copy_value(
1128    delta: &[u8],
1129    cursor: &mut usize,
1130    command: u8,
1131    masks: &[u8],
1132) -> Result<u64> {
1133    let mut value = 0u64;
1134    for (shift, mask) in masks.iter().enumerate() {
1135        if command & mask != 0 {
1136            let Some(byte) = delta.get(*cursor).copied() else {
1137                return Err(GitError::InvalidObject(
1138                    "truncated delta copy command".into(),
1139                ));
1140            };
1141            *cursor += 1;
1142            value |= u64::from(byte) << (shift * 8);
1143        }
1144    }
1145    Ok(value)
1146}