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