1use std::io;
2
3use gix_features::decode::leb64_from_read;
4
5use super::{BLOB, COMMIT, OFS_DELTA, REF_DELTA, TAG, TREE};
6use crate::data;
7
8#[derive(Debug, thiserror::Error)]
10#[expect(missing_docs)]
11pub enum Error {
12 #[error("Object type {type_id} is unsupported")]
13 UnsupportedType { type_id: u8 },
14 #[error("Pack entry is truncated: {message}")]
15 Corrupt { message: &'static str },
16 #[error("Pack entry header value overflowed while decoding")]
17 Overflow,
18}
19
20impl data::Entry {
22 pub fn from_bytes(d: &[u8], pack_offset: data::Offset, object_hash: gix_hash::Kind) -> Result<data::Entry, Error> {
24 let (type_id, size, mut consumed) = parse_header_info(d)?;
25 let hash_len = object_hash.len_in_bytes();
26
27 use crate::data::entry::Header::*;
28 let object = match type_id {
29 OFS_DELTA => {
30 let (distance, leb_bytes) = parse_leb64(&d[consumed..])?;
31 let delta = OfsDelta {
32 base_distance: distance,
33 };
34 consumed += leb_bytes;
35 delta
36 }
37 REF_DELTA => {
38 let hash = d
39 .get(consumed..)
40 .and_then(|d| d.get(..hash_len))
41 .ok_or(Error::Corrupt {
42 message: "ref-delta base object id",
43 })?;
44 let delta = RefDelta {
45 base_id: gix_hash::ObjectId::try_from(hash).map_err(|_| Error::Corrupt {
46 message: "unsupported object hash length",
47 })?,
48 };
49 consumed += hash_len;
50 delta
51 }
52 BLOB => Blob,
53 TREE => Tree,
54 COMMIT => Commit,
55 TAG => Tag,
56 other => return Err(Error::UnsupportedType { type_id: other }),
57 };
58 Ok(data::Entry {
59 header: object,
60 decompressed_size: size,
61 data_offset: pack_offset + consumed as u64,
62 encoded_header_size: encoded_header_size(consumed)?,
63 })
64 }
65
66 pub fn from_read(r: &mut dyn io::Read, pack_offset: data::Offset, hash_len: usize) -> io::Result<data::Entry> {
68 let (type_id, size, mut consumed) = streaming_parse_header_info(r)?;
69
70 use crate::data::entry::Header::*;
71 let object = match type_id {
72 OFS_DELTA => {
73 let (distance, leb_bytes) = leb64_from_read(&mut *r)?;
74 let delta = OfsDelta {
75 base_distance: distance,
76 };
77 consumed += leb_bytes;
78 delta
79 }
80 REF_DELTA => {
81 let mut buf = gix_hash::Kind::buf();
82 let hash = &mut buf[..hash_len];
83 r.read_exact(hash)?;
84 let delta = RefDelta {
85 base_id: gix_hash::ObjectId::from_bytes_or_panic(&hash[..]),
86 };
87 consumed += hash_len;
88 delta
89 }
90 BLOB => Blob,
91 TREE => Tree,
92 COMMIT => Commit,
93 TAG => Tag,
94 other => return Err(io::Error::other(format!("Object type {other} is unsupported"))),
95 };
96 Ok(data::Entry {
97 header: object,
98 decompressed_size: size,
99 data_offset: pack_offset + consumed as u64,
100 encoded_header_size: encoded_header_size(consumed)
101 .map_err(|err| io::Error::new(io::ErrorKind::InvalidData, err))?,
102 })
103 }
104}
105
106fn encoded_header_size(consumed: usize) -> Result<u16, Error> {
107 consumed.try_into().map_err(|_| Error::Corrupt {
108 message: "entry header size does not fit into u16",
109 })
110}
111
112#[inline]
113fn streaming_parse_header_info(read: &mut dyn io::Read) -> Result<(u8, u64, usize), io::Error> {
114 let mut byte = [0u8; 1];
115 read.read_exact(&mut byte)?;
116 let mut c = byte[0];
117 let mut i = 1;
118 let type_id = (c >> 4) & 0b0000_0111;
119 let mut size = u64::from(c) & 0b0000_1111;
120 let mut shift = 4u32;
121 while c & 0b1000_0000 != 0 {
122 read.read_exact(&mut byte)?;
123 c = byte[0];
124 i += 1;
125 let component = u64::from(c & 0b0111_1111)
126 .checked_shl(shift)
127 .ok_or_else(|| io::Error::new(io::ErrorKind::InvalidData, "pack entry header overflowed"))?;
128 size = size
129 .checked_add(component)
130 .ok_or_else(|| io::Error::new(io::ErrorKind::InvalidData, "pack entry header overflowed"))?;
131 shift += 7;
132 }
133 Ok((type_id, size, i))
134}
135
136#[inline]
138fn parse_header_info(data: &[u8]) -> Result<(u8, u64, usize), Error> {
139 let mut c = *data.first().ok_or(Error::Corrupt {
140 message: "need a pack entry header, got empty input",
141 })?;
142 let mut i = 1;
143 let type_id = (c >> 4) & 0b0000_0111;
144 let mut size = u64::from(c) & 0b0000_1111;
145 let mut shift = 4u32;
146 while c & 0b1000_0000 != 0 {
147 c = *data.get(i).ok_or(Error::Corrupt {
148 message: "pack entry header continuation byte",
149 })?;
150 i += 1;
151 let component = u64::from(c & 0b0111_1111).checked_shl(shift).ok_or(Error::Overflow)?;
152 size = size.checked_add(component).ok_or(Error::Overflow)?;
153 shift += 7;
154 }
155 Ok((type_id, size, i))
156}
157
158fn parse_leb64(data: &[u8]) -> Result<(u64, usize), Error> {
159 let mut i = 0;
160 let mut c = *data.first().ok_or(Error::Corrupt {
161 message: "an ofs-delta base distance",
162 })?;
163 i += 1;
164 let mut value = u64::from(c) & 0x7f;
165 while c & 0x80 != 0 {
166 c = *data.get(i).ok_or(Error::Corrupt {
167 message: "an ofs-delta base distance continuation byte",
168 })?;
169 i += 1;
170 value = value
171 .checked_add(1)
172 .and_then(|value| value.checked_shl(7))
173 .and_then(|value| value.checked_add(u64::from(c) & 0x7f))
174 .ok_or(Error::Overflow)?;
175 }
176 Ok((value, i))
177}
178
179#[cfg(test)]
180mod tests {
181 use super::*;
182
183 #[test]
184 fn accepts_non_canonical_pack_entry_header_encoding() {
185 let pack_offset = 42;
186 let entry = data::Entry::from_bytes(&[0xb3, 0x00], pack_offset, gix_hash::Kind::Sha1)
187 .expect("non-canonical size encodings are accepted by git");
188
189 assert_eq!(entry.header, data::entry::Header::Blob);
190 assert_eq!(
191 entry.decompressed_size, 3,
192 "`0xb3` is `1011_0011`: the low nibble `0011` is size 3, and the continued `0x00` adds no size bits"
193 );
194 assert_eq!(
195 entry.encoded_header_size, 2,
196 "the decoded entry records the actual encoded header length, not the canonical length, and has an extra byte 0x00"
197 );
198 assert_eq!(
199 entry.header.size(entry.decompressed_size),
200 1,
201 "canonical re-encoding of a blob with size 3 needs only the single byte `0x33`"
202 );
203 assert_eq!(
204 entry.header_size(),
205 2,
206 "`header_size()` reports the two bytes actually consumed from `b3 00`, unlike `Header::size()` which canonicalizes to one byte"
207 );
208 assert_eq!(entry.pack_offset(), pack_offset);
209 assert_eq!(entry.data_offset, pack_offset + 2);
210 }
211
212 #[test]
213 fn non_canonical_pack_entry_header_keeps_ofs_delta_base_offsets_correct() {
214 let pack_offset = 100;
215 let base_distance = 5;
216 let entry = data::Entry::from_bytes(&[0xe4, 0x00, base_distance], pack_offset, gix_hash::Kind::Sha1)
217 .expect("non-canonical ofs-delta size encodings are accepted by git");
218
219 assert_eq!(
220 entry.header,
221 data::entry::Header::OfsDelta {
222 base_distance: base_distance.into()
223 },
224 "the base_distance is correct, which wouldn't be the case without using the consumed size"
225 );
226 assert_eq!(
227 entry.header_size(),
228 3,
229 "`e4 00` consumes two size-header bytes before the one-byte ofs-delta base distance. entry.size() would be 2"
230 );
231 assert_eq!(
232 entry.pack_offset(),
233 pack_offset,
234 "`pack_offset()` subtracts the actual three-byte header from `data_offset`, preserving the entry start"
235 );
236 assert_eq!(
237 entry.checked_base_pack_offset(base_distance.into()),
238 Some(pack_offset - u64::from(base_distance)),
239 "ofs-delta base distances are relative to the entry start, so preserving `pack_offset()` keeps the base lookup correct"
240 );
241 }
242
243 #[test]
244 fn oversized_encoded_header_size_is_rejected() {
245 assert!(
246 matches!(
247 encoded_header_size(usize::from(u16::MAX) + 1),
248 Err(Error::Corrupt { message }) if message == "entry header size does not fit into u16"
249 ),
250 "entry header lengths that cannot be stored in the Entry metadata must be rejected"
251 );
252 }
253}