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