1use flate2::{Decompress, FlushDecompress, Status};
27use thiserror::Error;
28
29use crate::objects::HashAlgo;
30use crate::progress::ReceivedObjects;
31
32#[derive(Debug, Clone, PartialEq, Eq, Error)]
34#[non_exhaustive]
35pub enum PackStreamScanError {
36 #[error("stream does not start with a pack signature")]
38 BadSignature,
39 #[error("unsupported pack version {0}")]
41 UnsupportedVersion(u32),
42 #[error("invalid pack entry type {0}")]
44 InvalidEntryType(u8),
45 #[error("pack entry header is too long")]
47 HeaderTooLong,
48 #[error("corrupt compressed data in pack entry {0}")]
51 Inflate(u64),
52}
53
54const MAX_VARINT_BYTES: u8 = 10;
56const SCRATCH_LEN: usize = 64 * 1024;
58
59const OBJ_OFS_DELTA: u8 = 6;
60const OBJ_REF_DELTA: u8 = 7;
61
62#[derive(Debug)]
63enum State {
64 PackHeader { buf: [u8; 12], len: usize },
66 EntryHeader { read: u8, kind: u8 },
68 BaseOffset { read: u8 },
70 BaseId { remaining: usize },
72 Data,
74 Trailer,
76}
77
78pub struct PackStreamScanner {
84 hash_len: usize,
85 state: State,
86 total: Option<u64>,
87 received: u64,
88 deltas: u64,
89 entry_is_delta: bool,
90 inflate: Decompress,
91 scratch: Box<[u8]>,
92 stream_consumed: u64,
94}
95
96impl std::fmt::Debug for PackStreamScanner {
97 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
98 f.debug_struct("PackStreamScanner")
99 .field("hash_len", &self.hash_len)
100 .field("state", &self.state)
101 .field("total", &self.total)
102 .field("received", &self.received)
103 .field("deltas", &self.deltas)
104 .finish_non_exhaustive()
105 }
106}
107
108impl PackStreamScanner {
109 #[must_use]
116 pub fn new(hash: HashAlgo) -> Self {
117 Self {
118 hash_len: hash.len(),
119 state: State::PackHeader {
120 buf: [0; 12],
121 len: 0,
122 },
123 total: None,
124 received: 0,
125 deltas: 0,
126 entry_is_delta: false,
127 inflate: Decompress::new(true),
128 scratch: vec![0; SCRATCH_LEN].into_boxed_slice(),
129 stream_consumed: 0,
130 }
131 }
132
133 #[must_use]
137 pub(crate) fn pack_body_byte_len(&self) -> u64 {
138 self.stream_consumed
139 }
140
141 #[must_use]
143 pub fn counts(&self) -> Option<ReceivedObjects> {
144 self.total.map(|total| ReceivedObjects {
145 received: self.received,
146 total,
147 deltas: self.deltas,
148 })
149 }
150
151 #[must_use]
153 pub fn is_complete(&self) -> bool {
154 matches!(self.state, State::Trailer)
155 }
156
157 pub fn feed(&mut self, mut data: &[u8]) -> Result<(), PackStreamScanError> {
168 while !data.is_empty() {
169 match &mut self.state {
170 State::PackHeader { buf, len } => {
171 let take = (buf.len() - *len).min(data.len());
172 buf[*len..*len + take].copy_from_slice(&data[..take]);
173 *len += take;
174 self.stream_consumed += take as u64;
175 data = &data[take..];
176 if *len == buf.len() {
177 let header = *buf;
178 self.read_pack_header(&header)?;
179 }
180 }
181 State::EntryHeader { read, kind } => {
182 let byte = data[0];
183 self.stream_consumed += 1;
184 data = &data[1..];
185 if *read == 0 {
186 *kind = (byte >> 4) & 0x7;
187 if !matches!(*kind, 1..=4 | OBJ_OFS_DELTA | OBJ_REF_DELTA) {
188 return Err(PackStreamScanError::InvalidEntryType(*kind));
189 }
190 }
191 *read += 1;
192 if *read > MAX_VARINT_BYTES {
193 return Err(PackStreamScanError::HeaderTooLong);
194 }
195 if byte & 0x80 == 0 {
196 let kind = *kind;
197 self.entry_is_delta = matches!(kind, OBJ_OFS_DELTA | OBJ_REF_DELTA);
198 self.state = match kind {
199 OBJ_OFS_DELTA => State::BaseOffset { read: 0 },
200 OBJ_REF_DELTA => State::BaseId {
201 remaining: self.hash_len,
202 },
203 _ => State::Data,
204 };
205 }
206 }
207 State::BaseOffset { read } => {
208 let byte = data[0];
209 self.stream_consumed += 1;
210 data = &data[1..];
211 *read += 1;
212 if *read > MAX_VARINT_BYTES {
213 return Err(PackStreamScanError::HeaderTooLong);
214 }
215 if byte & 0x80 == 0 {
216 self.state = State::Data;
217 }
218 }
219 State::BaseId { remaining } => {
220 let take = (*remaining).min(data.len());
221 self.stream_consumed += take as u64;
222 *remaining -= take;
223 data = &data[take..];
224 if *remaining == 0 {
225 self.state = State::Data;
226 }
227 }
228 State::Data => {
229 data = self.inflate_entry(data)?;
230 }
231 State::Trailer => return Ok(()),
232 }
233 }
234 Ok(())
235 }
236
237 fn read_pack_header(&mut self, header: &[u8; 12]) -> Result<(), PackStreamScanError> {
238 if &header[..4] != b"PACK" {
239 return Err(PackStreamScanError::BadSignature);
240 }
241 let version = u32::from_be_bytes([header[4], header[5], header[6], header[7]]);
242 if !matches!(version, 2 | 3) {
243 return Err(PackStreamScanError::UnsupportedVersion(version));
244 }
245 let total = u64::from(u32::from_be_bytes([
246 header[8], header[9], header[10], header[11],
247 ]));
248 self.total = Some(total);
249 self.state = if total == 0 {
250 State::Trailer
251 } else {
252 State::EntryHeader { read: 0, kind: 0 }
253 };
254 Ok(())
255 }
256
257 fn inflate_entry<'d>(&mut self, mut data: &'d [u8]) -> Result<&'d [u8], PackStreamScanError> {
260 loop {
261 let in_before = self.inflate.total_in();
262 let out_before = self.inflate.total_out();
263 let status = self
264 .inflate
265 .decompress(data, &mut self.scratch, FlushDecompress::None)
266 .map_err(|_| PackStreamScanError::Inflate(self.received))?;
267 let used = usize::try_from(self.inflate.total_in() - in_before).unwrap_or(data.len());
268 self.stream_consumed += used as u64;
269 data = &data[used..];
270 match status {
271 Status::StreamEnd => {
272 self.finish_entry();
273 return Ok(data);
274 }
275 Status::Ok | Status::BufError => {
276 let produced = self.inflate.total_out() != out_before;
277 if data.is_empty() || (used == 0 && !produced) {
278 return Ok(data);
279 }
280 }
281 }
282 }
283 }
284
285 fn finish_entry(&mut self) {
286 self.inflate.reset(true);
287 self.received += 1;
288 if self.entry_is_delta {
289 self.deltas += 1;
290 }
291 self.state = if Some(self.received) == self.total {
292 State::Trailer
293 } else {
294 State::EntryHeader { read: 0, kind: 0 }
295 };
296 }
297}
298
299#[cfg(test)]
300mod tests {
301 use super::*;
302 use grit_test_support::objects::{HashAlgo as FixtureAlgo, ObjectKind as PK, PackBuilder};
303
304 fn sample_pack() -> Vec<u8> {
307 let mut pb = PackBuilder::new(FixtureAlgo::Sha1);
308 pb.add_full(
309 PK::Commit,
310 b"tree 0000000000000000000000000000000000000000\n\nc\n",
311 );
312 pb.add_full(PK::Tree, b"");
313 let base = b"blob base contents\n";
314 let base_idx = pb.add_full(PK::Blob, base);
315 let big: Vec<u8> = (0..300_000u32).flat_map(u32::to_le_bytes).collect();
316 pb.add_full(PK::Blob, &big);
317 let delta = crate::delta_encode::encode_lcp_delta(base, b"blob base contents, edited\n")
318 .expect("delta");
319 pb.add_ofs_delta(base_idx, &delta, delta.len());
320 pb.add_ref_delta(&[0xab; 20], &delta, delta.len());
321 pb.add_full(
322 PK::Tag,
323 b"object 0000000000000000000000000000000000000000\n",
324 );
325 pb.build().bytes
326 }
327
328 fn expected() -> ReceivedObjects {
329 ReceivedObjects {
330 received: 7,
331 total: 7,
332 deltas: 2,
333 }
334 }
335
336 #[test]
337 fn counts_a_whole_pack_fed_at_once() {
338 let mut scanner = PackStreamScanner::new(HashAlgo::Sha1);
339 scanner.feed(&sample_pack()).expect("scan");
340 assert_eq!(scanner.counts(), Some(expected()));
341 assert!(scanner.is_complete());
342 }
343
344 #[test]
345 fn counts_the_same_however_the_stream_is_split() {
346 let pack = sample_pack();
347 for chunk in [1usize, 7, 4096, 65_519] {
348 let mut scanner = PackStreamScanner::new(HashAlgo::Sha1);
349 for piece in pack.chunks(chunk) {
350 scanner.feed(piece).expect("scan");
351 }
352 assert_eq!(scanner.counts(), Some(expected()), "chunk {chunk}");
353 }
354 }
355
356 #[test]
357 fn reports_partial_progress_mid_stream() {
358 let pack = sample_pack();
359 let mut scanner = PackStreamScanner::new(HashAlgo::Sha1);
360 assert_eq!(scanner.counts(), None);
361 scanner.feed(&pack[..12]).expect("header");
362 let header_only = scanner.counts().expect("total known from header");
363 assert_eq!((header_only.received, header_only.total), (0, 7));
364 scanner.feed(&pack[12..pack.len() / 2]).expect("half");
365 let half = scanner.counts().expect("counts");
366 assert!(half.received > 0 && half.received < 7, "{half:?}");
367 assert!(!scanner.is_complete());
368 }
369
370 #[test]
371 fn rejects_streams_that_are_not_packs() {
372 let mut scanner = PackStreamScanner::new(HashAlgo::Sha1);
373 assert_eq!(
374 scanner.feed(b"NOPE\0\0\0\x02\0\0\0\x01"),
375 Err(PackStreamScanError::BadSignature)
376 );
377 let mut scanner = PackStreamScanner::new(HashAlgo::Sha1);
378 assert_eq!(
379 scanner.feed(b"PACK\0\0\0\x09\0\0\0\x01"),
380 Err(PackStreamScanError::UnsupportedVersion(9))
381 );
382 let mut scanner = PackStreamScanner::new(HashAlgo::Sha1);
383 assert_eq!(
385 scanner.feed(b"PACK\0\0\0\x02\0\0\0\x01\x50"),
386 Err(PackStreamScanError::InvalidEntryType(5))
387 );
388 let mut scanner = PackStreamScanner::new(HashAlgo::Sha1);
389 let mut runaway = b"PACK\0\0\0\x02\0\0\0\x01".to_vec();
390 runaway.extend([0xb0; 12]);
391 assert_eq!(
392 scanner.feed(&runaway),
393 Err(PackStreamScanError::HeaderTooLong)
394 );
395 let mut scanner = PackStreamScanner::new(HashAlgo::Sha1);
396 assert_eq!(
397 scanner.feed(b"PACK\0\0\0\x02\0\0\0\x01\x30\xff\xff\xff\xff"),
398 Err(PackStreamScanError::Inflate(0))
399 );
400 }
401
402 #[test]
403 fn ignores_the_trailer_after_the_last_object() {
404 let mut pack = sample_pack();
405 pack.extend_from_slice(b"anything after the checksum");
406 let mut scanner = PackStreamScanner::new(HashAlgo::Sha1);
407 scanner.feed(&pack).expect("scan");
408 assert_eq!(scanner.counts(), Some(expected()));
409 }
410
411 #[test]
412 fn pack_body_byte_len_matches_pack_without_trailer() {
413 let pack = sample_pack();
414 let hash_len = HashAlgo::Sha1.len();
415 let mut scanner = PackStreamScanner::new(HashAlgo::Sha1);
416 scanner.feed(&pack).expect("scan");
417 assert!(scanner.is_complete());
418 assert_eq!(
419 scanner.pack_body_byte_len() as usize,
420 pack.len() - hash_len,
421 "body length should exclude the pack trailer"
422 );
423 }
424}