Skip to main content

gix_pack/data/file/decode/
entry.rs

1use smallvec::SmallVec;
2use std::ops::Range;
3
4use crate::{
5    cache, data,
6    data::{File, delta, file::decode::Error},
7};
8
9/// A return value of a resolve function, which given an [`ObjectId`][gix_hash::ObjectId] determines where an object can be found.
10#[derive(Debug, PartialEq, Eq, Hash, Ord, PartialOrd, Clone)]
11#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
12pub enum ResolvedBase {
13    /// Indicate an object is within this pack, at the given entry, and thus can be looked up locally.
14    InPack(data::Entry),
15    /// Indicates the object of `kind` was found outside of the pack, and its data was written into an output
16    /// vector which now has a length of `end`.
17    #[expect(missing_docs)]
18    OutOfPack { kind: gix_object::Kind, end: usize },
19}
20
21#[derive(Debug)]
22struct Delta {
23    data: Range<usize>,
24    base_size: usize,
25    result_size: usize,
26
27    decompressed_size: usize,
28    data_offset: data::Offset,
29}
30
31/// Additional information and statistics about a successfully decoded object produced by [`File::decode_entry()`].
32///
33/// Useful to understand the effectiveness of the pack compression or the cost of decompression.
34#[derive(Debug, PartialEq, Eq, Hash, Ord, PartialOrd, Clone)]
35#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
36pub struct Outcome {
37    /// The kind of resolved object.
38    pub kind: gix_object::Kind,
39    /// The amount of deltas in the chain of objects that had to be resolved beforehand.
40    ///
41    /// This number is affected by the [`Cache`][cache::DecodeEntry] implementation, with cache hits shortening the
42    /// delta chain accordingly
43    pub num_deltas: u32,
44    /// The total decompressed size of all pack entries in the delta chain
45    pub decompressed_size: u64,
46    /// The total compressed size of all pack entries in the delta chain
47    pub compressed_size: usize,
48    /// The total size of the decoded object.
49    pub object_size: u64,
50}
51
52impl Outcome {
53    pub(crate) fn default_from_kind(kind: gix_object::Kind) -> Self {
54        Self {
55            kind,
56            num_deltas: 0,
57            decompressed_size: 0,
58            compressed_size: 0,
59            object_size: 0,
60        }
61    }
62    fn from_object_entry(kind: gix_object::Kind, entry: &data::Entry, compressed_size: usize) -> Self {
63        Self {
64            kind,
65            num_deltas: 0,
66            decompressed_size: entry.decompressed_size,
67            compressed_size,
68            object_size: entry.decompressed_size,
69        }
70    }
71}
72
73/// Decompression of objects
74impl<T> File<T>
75where
76    T: crate::FileData,
77{
78    fn decoded_object_size(&self, size: u64) -> Result<usize, Error> {
79        decoded_object_size(size, self.alloc_limit_bytes)
80    }
81
82    /// Decompress the given `entry` into `out` and return the amount of bytes read from the pack data.
83    /// Note that `inflate` is not reset after usage, but will be reset before using it.
84    ///
85    /// _Note_ that this method does not resolve deltified objects, but merely decompresses their content
86    /// `out` is expected to be large enough to hold `entry.size` bytes.
87    pub fn decompress_entry(
88        &self,
89        entry: &data::Entry,
90        inflate: &mut gix_zlib::Inflate,
91        out: &mut [u8],
92    ) -> Result<usize, Error> {
93        let size: usize = entry.decompressed_size.try_into().map_err(|_| Error::OutOfMemory)?;
94        if out.len() < size {
95            return Err(Error::OutOfMemory);
96        }
97        self.decompress_entry_from_data_offset(entry.data_offset, inflate, &mut out[..size])
98    }
99
100    /// Obtain the [`Entry`][crate::data::Entry] at the given `offset` into the pack.
101    ///
102    /// The `offset` is typically obtained from the pack index file.
103    pub fn entry(&self, offset: data::Offset) -> Result<data::Entry, data::entry::decode::Error> {
104        let pack_offset: usize = offset.try_into().expect("offset representable by machine");
105        if pack_offset > self.data.len() {
106            return Err(data::entry::decode::Error::Corrupt {
107                message: "an entry offset pointing beyond pack data",
108            });
109        }
110
111        let object_data = &self.data[pack_offset..];
112        data::Entry::from_bytes(object_data, offset, self.object_hash)
113    }
114
115    /// Decompress the object expected at the given data offset, sans pack header. This information is only
116    /// known after the pack header was parsed.
117    /// Note that this method does not resolve deltified objects, but merely decompresses their content
118    /// `out` is expected to be large enough to hold `entry.size` bytes.
119    /// Returns the amount of packed bytes there read from the pack data file.
120    pub(crate) fn decompress_entry_from_data_offset(
121        &self,
122        data_offset: data::Offset,
123        inflate: &mut gix_zlib::Inflate,
124        out: &mut [u8],
125    ) -> Result<usize, Error> {
126        let (consumed_in, _consumed_out) =
127            self.decompress_complete_entry_from_data_offset(data_offset, inflate, out)?;
128        Ok(consumed_in)
129    }
130
131    /// Like `decompress_entry_from_data_offset`, but returns `(consumed-input, consumed-output)`.
132    ///
133    /// The compressed stream must end exactly after producing `out.len()` bytes. Pack entry
134    /// headers are untrusted, so callers must not accept streams that stop early and
135    /// leave zero-filled slack in the destination buffer, nor streams that require more output
136    /// than the header promised. Both cases would make later delta parsing operate on bytes that
137    /// are not the entry payload described by the pack header.
138    pub(crate) fn decompress_complete_entry_from_data_offset(
139        &self,
140        data_offset: data::Offset,
141        inflate: &mut gix_zlib::Inflate,
142        out: &mut [u8],
143    ) -> Result<(usize, usize), Error> {
144        let (status, consumed_in, consumed_out) =
145            self.decompress_entry_from_data_offset_unchecked(data_offset, inflate, out)?;
146        if status != gix_zlib::Status::StreamEnd || consumed_out != out.len() {
147            return Err(data::entry::decode::Error::Corrupt {
148                message: "pack entry decompressed size does not match entry header",
149            }
150            .into());
151        }
152        Ok((consumed_in, consumed_out))
153    }
154
155    /// Like [`Self::decompress_commplete_entry_from_data_offset()`], but allows callers to inspect incomplete streams.
156    ///
157    /// This is only for callers that intentionally decompress a prefix into a smaller buffer, such as
158    /// delta header probing. Full pack entry decoding should use [`Self::decompress_commplete_entry_from_data_offset()`].
159    pub(crate) fn decompress_entry_from_data_offset_unchecked(
160        &self,
161        data_offset: data::Offset,
162        inflate: &mut gix_zlib::Inflate,
163        out: &mut [u8],
164    ) -> Result<(gix_zlib::Status, usize, usize), Error> {
165        let offset: usize = data_offset.try_into().expect("offset representable by machine");
166        if offset >= self.data.len() {
167            return Err(data::entry::decode::Error::Corrupt {
168                message: "an entry data offset pointing beyond pack data",
169            }
170            .into());
171        }
172
173        inflate.reset();
174        inflate.once(&self.data[offset..], out).map_err(Into::into)
175    }
176
177    /// Decode an entry, resolving delta's as needed, while growing the `out` vector if there is not enough
178    /// space to hold the result object.
179    ///
180    /// The `entry` determines which object to decode, and is commonly obtained with the help of a pack index file or through pack iteration.
181    /// `inflate` will be used for decompressing entries, and will not be reset after usage, but before first using it.
182    ///
183    /// `resolve` is a function to lookup objects with the given [`ObjectId`][gix_hash::ObjectId], in case the full object id is used to refer to
184    /// a base object, instead of an in-pack offset.
185    ///
186    /// `delta_cache` is a mechanism to avoid looking up base objects multiple times when decompressing multiple objects in a row.
187    /// Use a [Noop-Cache][cache::Never] to disable caching all together at the cost of repeating work.
188    pub fn decode_entry(
189        &self,
190        entry: data::Entry,
191        out: &mut Vec<u8>,
192        inflate: &mut gix_zlib::Inflate,
193        resolve: &dyn Fn(&gix_hash::oid, &mut Vec<u8>) -> Option<ResolvedBase>,
194        delta_cache: &mut dyn cache::DecodeEntry,
195    ) -> Result<Outcome, Error> {
196        use crate::data::entry::Header::*;
197        match entry.header {
198            Tree | Blob | Commit | Tag => {
199                let size = self.decoded_object_size(entry.decompressed_size)?;
200                if let Some(additional) = size.checked_sub(out.len()) {
201                    out.try_reserve(additional)?;
202                }
203                out.resize(size, 0);
204                self.decompress_entry(&entry, inflate, out.as_mut_slice())
205                    .map(|consumed_input| {
206                        Outcome::from_object_entry(
207                            entry.header.as_kind().expect("a non-delta entry"),
208                            &entry,
209                            consumed_input,
210                        )
211                    })
212            }
213            OfsDelta { .. } | RefDelta { .. } => self.resolve_deltas(entry, resolve, inflate, out, delta_cache),
214        }
215    }
216
217    /// resolve: technically, this shouldn't ever be required as stored local packs don't refer to objects by id
218    /// that are outside of the pack. Unless, of course, the ref refers to an object within this pack, which means
219    /// it's very, very large as 20bytes are smaller than the corresponding MSB encoded number
220    fn resolve_deltas(
221        &self,
222        last: data::Entry,
223        resolve: &dyn Fn(&gix_hash::oid, &mut Vec<u8>) -> Option<ResolvedBase>,
224        inflate: &mut gix_zlib::Inflate,
225        out: &mut Vec<u8>,
226        cache: &mut dyn cache::DecodeEntry,
227    ) -> Result<Outcome, Error> {
228        // all deltas, from the one that produces the desired object (first) to the oldest at the end of the chain
229        let mut chain = SmallVec::<[Delta; 10]>::default();
230        let first_entry = last.clone();
231        let mut cursor = last;
232        let mut base_buffer_size: Option<usize> = None;
233        let mut object_kind: Option<gix_object::Kind> = None;
234        let mut consumed_input: Option<usize> = None;
235
236        // Find the first full base, either an undeltified object in the pack or a reference to another object.
237        let mut total_delta_data_size: u64 = 0;
238        while cursor.header.is_delta() {
239            if let Some((kind, packed_size)) = cache.get(self.id, cursor.data_offset, out) {
240                base_buffer_size = Some(out.len());
241                object_kind = Some(kind);
242                // If the input entry is a cache hit, keep the packed size as it must be returned.
243                // Otherwise, the packed size will be determined later when decompressing the input delta
244                if total_delta_data_size == 0 {
245                    consumed_input = Some(packed_size);
246                }
247                break;
248            }
249            // This is a pessimistic guess, as worst possible compression should not be bigger than the data itself.
250            // TODO: is this assumption actually true?
251            total_delta_data_size = total_delta_data_size
252                .checked_add(cursor.decompressed_size)
253                .ok_or(Error::OutOfMemory)?;
254            if self
255                .alloc_limit_bytes
256                .is_some_and(|limit| total_delta_data_size > limit as u64)
257            {
258                return Err(Error::OutOfMemory);
259            }
260            let decompressed_size = self.decoded_object_size(cursor.decompressed_size)?;
261            chain.push(Delta {
262                data: Range {
263                    start: 0,
264                    end: decompressed_size,
265                },
266                base_size: 0,
267                result_size: 0,
268                decompressed_size,
269                data_offset: cursor.data_offset,
270            });
271            use crate::data::entry::Header;
272            cursor = match cursor.header {
273                Header::OfsDelta { base_distance } => {
274                    self.entry(cursor.checked_base_pack_offset(base_distance).ok_or(
275                        crate::data::entry::decode::Error::Corrupt {
276                            message: "an ofs-delta base distance pointing before pack start",
277                        },
278                    )?)?
279                }
280                Header::RefDelta { base_id } => match resolve(base_id.as_ref(), out) {
281                    Some(ResolvedBase::InPack(entry)) => entry,
282                    Some(ResolvedBase::OutOfPack { end, kind }) => {
283                        base_buffer_size = Some(end);
284                        object_kind = Some(kind);
285                        break;
286                    }
287                    None => return Err(Error::DeltaBaseUnresolved(base_id)),
288                },
289                _ => unreachable!("cursor.is_delta() only allows deltas here"),
290            };
291        }
292
293        // This can happen if the cache held the first entry itself
294        // We will just treat it as an object then, even though it's technically incorrect.
295        if chain.is_empty() {
296            return Ok(Outcome::from_object_entry(
297                object_kind.expect("object kind as set by cache"),
298                &first_entry,
299                consumed_input.expect("consumed bytes as set by cache"),
300            ));
301        }
302
303        // First pass will decompress all delta data and keep it in our output buffer
304        // [<possibly resolved base object>]<delta-1..delta-n>...
305        // so that we can find the biggest result size.
306        let total_delta_data_size: usize = total_delta_data_size.try_into().map_err(|_| Error::OutOfMemory)?;
307
308        let chain_len = chain.len();
309        let actual_base_size = match base_buffer_size {
310            Some(size) => size,
311            None => self.decoded_object_size(cursor.decompressed_size)?,
312        };
313        let (first_buffer_end, second_buffer_end) = {
314            let delta_start = base_buffer_size.unwrap_or(0);
315
316            let delta_range = Range {
317                start: delta_start,
318                end: delta_start
319                    .checked_add(total_delta_data_size)
320                    .ok_or(Error::OutOfMemory)?,
321            };
322            out.try_reserve(delta_range.end.saturating_sub(out.len()))?;
323            out.resize(delta_range.end, 0);
324
325            let mut instructions = &mut out[delta_range.clone()];
326            let mut relative_delta_start = 0;
327            let mut biggest_result_size = 0;
328            let mut expected_base_size = actual_base_size;
329            for (delta_idx, delta) in chain.iter_mut().rev().enumerate() {
330                let (consumed_from_data_offset, consumed_out) = self.decompress_complete_entry_from_data_offset(
331                    delta.data_offset,
332                    inflate,
333                    &mut instructions[..delta.decompressed_size],
334                )?;
335                let is_last_delta_to_be_applied = delta_idx + 1 == chain_len;
336                if is_last_delta_to_be_applied {
337                    consumed_input = Some(consumed_from_data_offset);
338                }
339
340                let current_delta = &instructions[..consumed_out];
341                let (base_size, offset) = delta::decode_header_size(current_delta)?;
342                let mut bytes_consumed_by_header = offset;
343                delta.base_size = self.decoded_object_size(base_size)?;
344                if delta.base_size != expected_base_size {
345                    return Err(delta::apply::Error::Corrupt {
346                        message: "delta base size does not match base object size",
347                    }
348                    .into());
349                }
350                biggest_result_size = biggest_result_size.max(base_size);
351
352                let (result_size, offset) = delta::decode_header_size(&current_delta[offset..])?;
353                bytes_consumed_by_header += offset;
354                biggest_result_size = biggest_result_size.max(result_size);
355                delta.result_size = self.decoded_object_size(result_size)?;
356                expected_base_size = delta.result_size;
357
358                // the absolute location into the instructions buffer, so we keep track of the end point of the last
359                delta.data.start = relative_delta_start + bytes_consumed_by_header;
360                delta.data.end = relative_delta_start + consumed_out;
361                relative_delta_start += delta.decompressed_size;
362
363                instructions = &mut instructions[delta.decompressed_size..];
364            }
365
366            // Now we can produce a buffer like this
367            // [<biggest-result-buffer, possibly filled with resolved base object data>]<biggest-result-buffer><delta-1..delta-n>
368            // from [<possibly resolved base object>]<delta-1..delta-n>...
369            if base_buffer_size.is_none() {
370                biggest_result_size = biggest_result_size.max(cursor.decompressed_size);
371            }
372            let biggest_result_size = self.decoded_object_size(biggest_result_size)?;
373            let first_buffer_size = biggest_result_size;
374            let second_buffer_size = first_buffer_size;
375            let out_size = first_buffer_size
376                .checked_add(second_buffer_size)
377                .and_then(|size| size.checked_add(total_delta_data_size))
378                .ok_or(Error::OutOfMemory)?;
379            out.try_reserve(out_size.saturating_sub(out.len()))?;
380            out.resize(out_size, 0);
381
382            // Now 'rescue' the deltas, because in the next step we possibly overwrite that portion
383            // of memory with the base object (in the majority of cases)
384            let second_buffer_end = {
385                let end = first_buffer_size
386                    .checked_add(second_buffer_size)
387                    .ok_or(Error::OutOfMemory)?;
388                // Move the decompressed delta instructions behind the two work buffers so they remain intact
389                // while we repurpose the front of `out` for base-object materialization and delta application.
390                out.copy_within(delta_range, end);
391                end
392            };
393
394            // If we don't have a out-of-pack object already, fill the base-buffer by decompressing the full object
395            // at which the cursor is left after the iteration
396            if base_buffer_size.is_none() {
397                let base_entry = cursor;
398                debug_assert!(!base_entry.header.is_delta());
399                object_kind = base_entry.header.as_kind();
400                let base_size = self.decoded_object_size(base_entry.decompressed_size)?;
401                let out_base = &mut out[..base_size];
402                self.decompress_entry_from_data_offset(base_entry.data_offset, inflate, out_base)?;
403            }
404
405            (first_buffer_size, second_buffer_end)
406        };
407
408        // From oldest to most recent, apply all deltas, swapping the buffer back and forth
409        // TODO: once we have more tests, we could optimize this memory-intensive work to
410        //       analyse the delta-chains to only copy data once - after all, with 'copy-from-base' deltas,
411        //       all data originates from one base at some point.
412        // `out` is: [source-buffer][target-buffer][max-delta-instructions-buffer]
413        let (buffers, instructions) = out.split_at_mut(second_buffer_end);
414        let (mut source_buf, mut target_buf) = buffers.split_at_mut(first_buffer_end);
415
416        let mut last_result_size = None;
417        for (
418            delta_idx,
419            Delta {
420                data,
421                base_size,
422                result_size,
423                ..
424            },
425        ) in chain.into_iter().rev().enumerate()
426        {
427            let data = &mut instructions[data];
428            if delta_idx + 1 == chain_len {
429                last_result_size = Some(result_size);
430            }
431            delta::apply(&source_buf[..base_size], &mut target_buf[..result_size], data)?;
432            // use the target as source for the next delta
433            std::mem::swap(&mut source_buf, &mut target_buf);
434        }
435
436        let last_result_size = last_result_size.expect("at least one delta chain item");
437        // uneven chains leave the target buffer after the source buffer
438        // FIXME(Performance) If delta-chains are uneven, we know we will have to copy bytes over here
439        //      Instead we could use a different start buffer, to naturally end up with the result in the
440        //      right one.
441        //      However, this is a bit more complicated than just that - you have to deal with the base
442        //      object, which should also be placed in the second buffer right away. You don't have that
443        //      control/knowledge for out-of-pack bases, so this is a special case to deal with, too.
444        //      Maybe these invariants can be represented in the type system though.
445        if chain_len % 2 == 1 {
446            // this seems inverted, but remember: we swapped the buffers on the last iteration
447            target_buf[..last_result_size].copy_from_slice(&source_buf[..last_result_size]);
448        }
449        debug_assert!(out.len() >= last_result_size);
450        out.truncate(last_result_size);
451
452        let object_kind = object_kind.expect("a base object as root of any delta chain that we are here to resolve");
453        let consumed_input = consumed_input.expect("at least one decompressed delta object");
454        cache.put(
455            self.id,
456            first_entry.data_offset,
457            out.as_slice(),
458            object_kind,
459            consumed_input,
460        );
461        Ok(Outcome {
462            kind: object_kind,
463            // technically depending on the cache, the chain size is not correct as it might
464            // have been cut short by a cache hit. The caller must deactivate the cache to get
465            // actual results
466            num_deltas: chain_len as u32,
467            decompressed_size: first_entry.decompressed_size,
468            compressed_size: consumed_input,
469            object_size: last_result_size as u64,
470        })
471    }
472}
473
474/// Convert user-controlled sizes from pack data into allocation sizes while enforcing the configured allocation cap.
475fn decoded_object_size(size: u64, alloc_limit_bytes: Option<usize>) -> Result<usize, Error> {
476    let size: usize = size.try_into().map_err(|_| Error::OutOfMemory)?;
477    if alloc_limit_bytes.is_some_and(|limit| size > limit) {
478        return Err(Error::OutOfMemory);
479    }
480    Ok(size)
481}
482
483#[cfg(test)]
484mod tests {
485    use gix_testtools::size_ok;
486
487    use super::*;
488
489    #[test]
490    fn size_of_decode_entry_outcome() {
491        let actual = std::mem::size_of::<Outcome>();
492        let expected = 32;
493        assert!(
494            size_ok(actual, expected),
495            "this shouldn't change without use noticing as it's returned a lot: {actual} <~ {expected}"
496        );
497    }
498}