gix_pack/data/file/decode/header.rs
1use crate::{
2 data,
3 data::{File, delta, file::decode::Error},
4};
5
6/// A return value of a resolve function, which given an [`ObjectId`][gix_hash::ObjectId] determines where an object can be found.
7#[derive(Debug, PartialEq, Eq, Hash, Ord, PartialOrd, Clone)]
8#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
9pub enum ResolvedBase {
10 /// Indicate an object is within this pack, at the given entry, and thus can be looked up locally.
11 InPack(data::Entry),
12 /// Indicates the object of `kind` was found outside of the pack.
13 OutOfPack {
14 /// The kind of object we found when reading the header of the out-of-pack base.
15 kind: gix_object::Kind,
16 /// The amount of deltas encountered if the object was packed as well.
17 num_deltas: Option<u32>,
18 },
19}
20
21/// Additional information and statistics about a successfully decoded object produced by [`File::decode_header()`].
22///
23/// Useful to understand the effectiveness of the pack compression or the cost of decompression.
24#[derive(Debug, PartialEq, Eq, Hash, Ord, PartialOrd, Clone, Copy)]
25#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
26pub struct Outcome {
27 /// The kind of resolved object.
28 pub kind: gix_object::Kind,
29 /// The decompressed size of the object.
30 pub object_size: u64,
31 /// The amount of deltas in the chain of objects that had to be resolved beforehand.
32 pub num_deltas: u32,
33}
34
35/// Obtain object information quickly.
36impl<T> File<T>
37where
38 T: crate::FileData,
39{
40 /// Resolve the object header information starting at `entry`, following the chain of entries as needed.
41 ///
42 /// The `entry` determines which object to decode, and is commonly obtained with the help of a pack index file or through pack iteration.
43 /// `inflate` will be used for (partially) decompressing entries, and will be reset before first use, but not after the last use.
44 ///
45 /// `resolve` is a function to lookup objects with the given [`ObjectId`][gix_hash::ObjectId], in case the full object id
46 /// is used to refer to a base object, instead of an in-pack offset.
47 ///
48 /// For delta entries, this only probes the initial delta header bytes to determine the result
49 /// object size. It can reject streams that end or overflow within that probe, but it does not
50 /// fully validate that the compressed stream produces exactly the decompressed size declared in
51 /// the pack entry header. Use [`File::decode_entry()`][crate::data::File::decode_entry()] when
52 /// callers need that full validation.
53 pub fn decode_header(
54 &self,
55 mut entry: data::Entry,
56 inflate: &mut gix_zlib::Inflate,
57 resolve: &dyn Fn(&gix_hash::oid) -> Option<ResolvedBase>,
58 ) -> Result<Outcome, Error> {
59 use crate::data::entry::Header::*;
60 let mut num_deltas = 0;
61 let mut first_delta_decompressed_size = None::<u64>;
62 loop {
63 match entry.header {
64 Tree | Blob | Commit | Tag => {
65 return Ok(Outcome {
66 kind: entry.header.as_kind().expect("always valid for non-refs"),
67 object_size: first_delta_decompressed_size.unwrap_or(entry.decompressed_size),
68 num_deltas,
69 });
70 }
71 OfsDelta { base_distance } => {
72 num_deltas += 1;
73 if first_delta_decompressed_size.is_none() {
74 first_delta_decompressed_size = Some(self.decode_delta_object_size(inflate, &entry)?);
75 }
76 entry = self.entry(entry.checked_base_pack_offset(base_distance).ok_or(
77 crate::data::entry::decode::Error::Corrupt {
78 message: "an ofs-delta base distance pointing before pack start",
79 },
80 )?)?;
81 }
82 RefDelta { base_id } => {
83 num_deltas += 1;
84 if first_delta_decompressed_size.is_none() {
85 first_delta_decompressed_size = Some(self.decode_delta_object_size(inflate, &entry)?);
86 }
87 match resolve(base_id.as_ref()) {
88 Some(ResolvedBase::InPack(base_entry)) => entry = base_entry,
89 Some(ResolvedBase::OutOfPack {
90 kind,
91 num_deltas: origin_num_deltas,
92 }) => {
93 return Ok(Outcome {
94 kind,
95 object_size: first_delta_decompressed_size.unwrap_or(entry.decompressed_size),
96 num_deltas: origin_num_deltas.unwrap_or_default() + num_deltas,
97 });
98 }
99 None => return Err(Error::DeltaBaseUnresolved(base_id)),
100 }
101 }
102 }
103 }
104 }
105
106 /// Decode the result object size from the initial delta header bytes in `inflate`, using `entry`
107 /// for offsets.
108 ///
109 /// This intentionally mirrors Git's cheap header probe: only the first 20 decompressed bytes
110 /// are inspected, which is enough for the two `u64` varints that make up a valid delta header.
111 /// If the zlib stream ends within that probe, we can reject declared-size mismatches here.
112 /// Otherwise this result only proves that the delta header prefix is parseable; full
113 /// decompression through `decode_entry()` must still validate that the stream length matches
114 /// the pack entry header.
115 #[inline]
116 fn decode_delta_object_size(&self, inflate: &mut gix_zlib::Inflate, entry: &data::Entry) -> Result<u64, Error> {
117 let mut buf = [0_u8; 20];
118 let max_size = entry.decompressed_size.min(buf.len() as u64) as usize;
119 let (status, _consumed_in, consumed_out) =
120 self.decompress_entry_from_data_offset_unchecked(entry.data_offset, inflate, &mut buf[..max_size])?;
121 if status == gix_zlib::Status::StreamEnd {
122 if consumed_out as u64 != entry.decompressed_size {
123 return Err(data::entry::decode::Error::Corrupt {
124 message: "pack entry decompressed to fewer bytes than declared in the entry header",
125 }
126 .into());
127 }
128 } else if entry.decompressed_size == max_size as u64 {
129 return Err(data::entry::decode::Error::Corrupt {
130 message: "pack entry decompressed to more bytes than declared in the entry header",
131 }
132 .into());
133 }
134 let buf = &buf[..consumed_out];
135 let (_base_size, offset) = delta::decode_header_size(buf)?;
136 let (result_size, _offset) = delta::decode_header_size(&buf[offset..])?;
137 Ok(result_size)
138 }
139}
140
141#[cfg(test)]
142mod tests {
143 use super::*;
144
145 #[test]
146 fn size_of_decode_entry_outcome() {
147 assert_eq!(
148 std::mem::size_of::<Outcome>(),
149 16,
150 "this shouldn't change without use noticing as it's returned a lot"
151 );
152 }
153}