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