Skip to main content

grit_lib/
pack_stream_scan.rs

1//! Count the objects in a pack while its bytes are still arriving.
2//!
3//! A fetched pack is spooled to disk and indexed only once the transfer ends,
4//! so during the transfer a caller knows how many bytes have arrived but not
5//! how far through the pack's objects it is. [`PackStreamScanner`] walks the
6//! entry headers as bytes are fed to it and inflates each entry's zlib stream
7//! just far enough to find where the next entry starts. The object total comes
8//! from the 12-byte pack header, so callers can show `received / total`.
9//!
10//! The scanner keeps no inflated data; it costs one inflate pass over the
11//! stream, which is why fetch only runs it (on a background thread) when the
12//! progress sink asks for it.
13//!
14//! ```rust
15//! use grit_lib::objects::pack_stream_scan::PackStreamScanner;
16//! use grit_lib::objects::HashAlgo;
17//!
18//! let mut scanner = PackStreamScanner::new(HashAlgo::Sha1);
19//! // An empty pack: signature, version 2, zero objects.
20//! scanner.feed(b"PACK\0\0\0\x02\0\0\0\0").unwrap();
21//! let counts = scanner.counts().unwrap();
22//! assert_eq!((counts.received, counts.total), (0, 0));
23//! assert!(scanner.is_complete());
24//! ```
25
26use flate2::{Decompress, FlushDecompress, Status};
27use thiserror::Error;
28
29use crate::objects::HashAlgo;
30use crate::progress::ReceivedObjects;
31
32/// Why a pack stream could not be scanned.
33#[derive(Debug, Clone, PartialEq, Eq, Error)]
34#[non_exhaustive]
35pub enum PackStreamScanError {
36    /// The stream does not begin with the `PACK` signature.
37    #[error("stream does not start with a pack signature")]
38    BadSignature,
39    /// The pack header names a version other than 2 or 3.
40    #[error("unsupported pack version {0}")]
41    UnsupportedVersion(u32),
42    /// An entry header carries a type code that no pack entry uses.
43    #[error("invalid pack entry type {0}")]
44    InvalidEntryType(u8),
45    /// An entry's size or delta offset varint runs past any valid length.
46    #[error("pack entry header is too long")]
47    HeaderTooLong,
48    /// An entry's compressed data is not a valid zlib stream (or the pack
49    /// uses a different hash length than the scanner was created with).
50    #[error("corrupt compressed data in pack entry {0}")]
51    Inflate(u64),
52}
53
54/// Longest valid size or offset varint: 64 bits at 7 bits per byte.
55const MAX_VARINT_BYTES: u8 = 10;
56/// Inflated bytes are discarded; this is only scratch space for zlib.
57const 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    /// Collecting the 12-byte pack header.
65    PackHeader { buf: [u8; 12], len: usize },
66    /// Reading an entry's type-and-size varint; `kind` is its type code.
67    EntryHeader { read: u8, kind: u8 },
68    /// Reading an `OFS_DELTA` entry's base offset varint.
69    BaseOffset { read: u8 },
70    /// Skipping a `REF_DELTA` entry's base object id.
71    BaseId { remaining: usize },
72    /// Inflating an entry's data to find where it ends.
73    Data,
74    /// Every object is accounted for; the rest is the trailing checksum.
75    Trailer,
76}
77
78/// Incremental object counter over a pack byte stream.
79///
80/// Feed it the pack's bytes in order, starting at the `PACK` signature, in
81/// chunks of any size; [`Self::counts`] reports how many entries have been
82/// fully received.
83pub 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    /// Pack bytes consumed through the last object (trailer hash not included).
93    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    /// Create a scanner for a pack whose `REF_DELTA` bases are `hash` ids.
110    ///
111    /// # Parameters
112    ///
113    /// - `hash`: the object format of the pack (decides the length of a
114    ///   `REF_DELTA` entry's base id).
115    #[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    /// Byte length of the pack body (header + objects), excluding the trailing hash.
134    ///
135    /// Meaningful once [`Self::is_complete`] is true.
136    #[must_use]
137    pub(crate) fn pack_body_byte_len(&self) -> u64 {
138        self.stream_consumed
139    }
140
141    /// Objects received so far, or `None` until the pack header has arrived.
142    #[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    /// Whether every object named in the pack header has been received.
152    #[must_use]
153    pub fn is_complete(&self) -> bool {
154        matches!(self.state, State::Trailer)
155    }
156
157    /// Scan the next `data` bytes of the pack.
158    ///
159    /// # Parameters
160    ///
161    /// - `data`: the bytes that follow everything fed so far.
162    ///
163    /// # Errors
164    ///
165    /// Returns a [`PackStreamScanError`] when the bytes are not a well-formed
166    /// pack. The scanner should be discarded after an error.
167    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    /// Inflate as much of the current entry as `data` holds; return the bytes
258    /// left over once the entry ends (empty when it needs more input).
259    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    /// A pack with every entry shape: full objects, a blob larger than the
305    /// scratch buffer, an `OFS_DELTA` and a `REF_DELTA`.
306    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        // Type code 5 is reserved.
384        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}