use flate2::{Decompress, FlushDecompress, Status};
use thiserror::Error;
use crate::objects::HashAlgo;
use crate::progress::ReceivedObjects;
#[derive(Debug, Clone, PartialEq, Eq, Error)]
#[non_exhaustive]
pub enum PackStreamScanError {
#[error("stream does not start with a pack signature")]
BadSignature,
#[error("unsupported pack version {0}")]
UnsupportedVersion(u32),
#[error("invalid pack entry type {0}")]
InvalidEntryType(u8),
#[error("pack entry header is too long")]
HeaderTooLong,
#[error("corrupt compressed data in pack entry {0}")]
Inflate(u64),
}
const MAX_VARINT_BYTES: u8 = 10;
const SCRATCH_LEN: usize = 64 * 1024;
const OBJ_OFS_DELTA: u8 = 6;
const OBJ_REF_DELTA: u8 = 7;
#[derive(Debug)]
enum State {
PackHeader { buf: [u8; 12], len: usize },
EntryHeader { read: u8, kind: u8 },
BaseOffset { read: u8 },
BaseId { remaining: usize },
Data,
Trailer,
}
pub struct PackStreamScanner {
hash_len: usize,
state: State,
total: Option<u64>,
received: u64,
deltas: u64,
entry_is_delta: bool,
inflate: Decompress,
scratch: Box<[u8]>,
}
impl std::fmt::Debug for PackStreamScanner {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("PackStreamScanner")
.field("hash_len", &self.hash_len)
.field("state", &self.state)
.field("total", &self.total)
.field("received", &self.received)
.field("deltas", &self.deltas)
.finish_non_exhaustive()
}
}
impl PackStreamScanner {
#[must_use]
pub fn new(hash: HashAlgo) -> Self {
Self {
hash_len: hash.len(),
state: State::PackHeader {
buf: [0; 12],
len: 0,
},
total: None,
received: 0,
deltas: 0,
entry_is_delta: false,
inflate: Decompress::new(true),
scratch: vec![0; SCRATCH_LEN].into_boxed_slice(),
}
}
#[must_use]
pub fn counts(&self) -> Option<ReceivedObjects> {
self.total.map(|total| ReceivedObjects {
received: self.received,
total,
deltas: self.deltas,
})
}
#[must_use]
pub fn is_complete(&self) -> bool {
matches!(self.state, State::Trailer)
}
pub fn feed(&mut self, mut data: &[u8]) -> Result<(), PackStreamScanError> {
while !data.is_empty() {
match &mut self.state {
State::PackHeader { buf, len } => {
let take = (buf.len() - *len).min(data.len());
buf[*len..*len + take].copy_from_slice(&data[..take]);
*len += take;
data = &data[take..];
if *len == buf.len() {
let header = *buf;
self.read_pack_header(&header)?;
}
}
State::EntryHeader { read, kind } => {
let byte = data[0];
data = &data[1..];
if *read == 0 {
*kind = (byte >> 4) & 0x7;
if !matches!(*kind, 1..=4 | OBJ_OFS_DELTA | OBJ_REF_DELTA) {
return Err(PackStreamScanError::InvalidEntryType(*kind));
}
}
*read += 1;
if *read > MAX_VARINT_BYTES {
return Err(PackStreamScanError::HeaderTooLong);
}
if byte & 0x80 == 0 {
let kind = *kind;
self.entry_is_delta = matches!(kind, OBJ_OFS_DELTA | OBJ_REF_DELTA);
self.state = match kind {
OBJ_OFS_DELTA => State::BaseOffset { read: 0 },
OBJ_REF_DELTA => State::BaseId {
remaining: self.hash_len,
},
_ => State::Data,
};
}
}
State::BaseOffset { read } => {
let byte = data[0];
data = &data[1..];
*read += 1;
if *read > MAX_VARINT_BYTES {
return Err(PackStreamScanError::HeaderTooLong);
}
if byte & 0x80 == 0 {
self.state = State::Data;
}
}
State::BaseId { remaining } => {
let take = (*remaining).min(data.len());
*remaining -= take;
data = &data[take..];
if *remaining == 0 {
self.state = State::Data;
}
}
State::Data => {
data = self.inflate_entry(data)?;
}
State::Trailer => return Ok(()),
}
}
Ok(())
}
fn read_pack_header(&mut self, header: &[u8; 12]) -> Result<(), PackStreamScanError> {
if &header[..4] != b"PACK" {
return Err(PackStreamScanError::BadSignature);
}
let version = u32::from_be_bytes([header[4], header[5], header[6], header[7]]);
if !matches!(version, 2 | 3) {
return Err(PackStreamScanError::UnsupportedVersion(version));
}
let total = u64::from(u32::from_be_bytes([
header[8], header[9], header[10], header[11],
]));
self.total = Some(total);
self.state = if total == 0 {
State::Trailer
} else {
State::EntryHeader { read: 0, kind: 0 }
};
Ok(())
}
fn inflate_entry<'d>(&mut self, mut data: &'d [u8]) -> Result<&'d [u8], PackStreamScanError> {
loop {
let in_before = self.inflate.total_in();
let out_before = self.inflate.total_out();
let status = self
.inflate
.decompress(data, &mut self.scratch, FlushDecompress::None)
.map_err(|_| PackStreamScanError::Inflate(self.received))?;
let used = usize::try_from(self.inflate.total_in() - in_before).unwrap_or(data.len());
data = &data[used..];
match status {
Status::StreamEnd => {
self.finish_entry();
return Ok(data);
}
Status::Ok | Status::BufError => {
let produced = self.inflate.total_out() != out_before;
if data.is_empty() || (used == 0 && !produced) {
return Ok(data);
}
}
}
}
}
fn finish_entry(&mut self) {
self.inflate.reset(true);
self.received += 1;
if self.entry_is_delta {
self.deltas += 1;
}
self.state = if Some(self.received) == self.total {
State::Trailer
} else {
State::EntryHeader { read: 0, kind: 0 }
};
}
}
#[cfg(test)]
mod tests {
use super::*;
use grit_test_support::objects::{HashAlgo as FixtureAlgo, ObjectKind as PK, PackBuilder};
fn sample_pack() -> Vec<u8> {
let mut pb = PackBuilder::new(FixtureAlgo::Sha1);
pb.add_full(
PK::Commit,
b"tree 0000000000000000000000000000000000000000\n\nc\n",
);
pb.add_full(PK::Tree, b"");
let base = b"blob base contents\n";
let base_idx = pb.add_full(PK::Blob, base);
let big: Vec<u8> = (0..300_000u32).flat_map(u32::to_le_bytes).collect();
pb.add_full(PK::Blob, &big);
let delta = crate::delta_encode::encode_lcp_delta(base, b"blob base contents, edited\n")
.expect("delta");
pb.add_ofs_delta(base_idx, &delta, delta.len());
pb.add_ref_delta(&[0xab; 20], &delta, delta.len());
pb.add_full(
PK::Tag,
b"object 0000000000000000000000000000000000000000\n",
);
pb.build().bytes
}
fn expected() -> ReceivedObjects {
ReceivedObjects {
received: 7,
total: 7,
deltas: 2,
}
}
#[test]
fn counts_a_whole_pack_fed_at_once() {
let mut scanner = PackStreamScanner::new(HashAlgo::Sha1);
scanner.feed(&sample_pack()).expect("scan");
assert_eq!(scanner.counts(), Some(expected()));
assert!(scanner.is_complete());
}
#[test]
fn counts_the_same_however_the_stream_is_split() {
let pack = sample_pack();
for chunk in [1usize, 7, 4096, 65_519] {
let mut scanner = PackStreamScanner::new(HashAlgo::Sha1);
for piece in pack.chunks(chunk) {
scanner.feed(piece).expect("scan");
}
assert_eq!(scanner.counts(), Some(expected()), "chunk {chunk}");
}
}
#[test]
fn reports_partial_progress_mid_stream() {
let pack = sample_pack();
let mut scanner = PackStreamScanner::new(HashAlgo::Sha1);
assert_eq!(scanner.counts(), None);
scanner.feed(&pack[..12]).expect("header");
let header_only = scanner.counts().expect("total known from header");
assert_eq!((header_only.received, header_only.total), (0, 7));
scanner.feed(&pack[12..pack.len() / 2]).expect("half");
let half = scanner.counts().expect("counts");
assert!(half.received > 0 && half.received < 7, "{half:?}");
assert!(!scanner.is_complete());
}
#[test]
fn rejects_streams_that_are_not_packs() {
let mut scanner = PackStreamScanner::new(HashAlgo::Sha1);
assert_eq!(
scanner.feed(b"NOPE\0\0\0\x02\0\0\0\x01"),
Err(PackStreamScanError::BadSignature)
);
let mut scanner = PackStreamScanner::new(HashAlgo::Sha1);
assert_eq!(
scanner.feed(b"PACK\0\0\0\x09\0\0\0\x01"),
Err(PackStreamScanError::UnsupportedVersion(9))
);
let mut scanner = PackStreamScanner::new(HashAlgo::Sha1);
assert_eq!(
scanner.feed(b"PACK\0\0\0\x02\0\0\0\x01\x50"),
Err(PackStreamScanError::InvalidEntryType(5))
);
let mut scanner = PackStreamScanner::new(HashAlgo::Sha1);
let mut runaway = b"PACK\0\0\0\x02\0\0\0\x01".to_vec();
runaway.extend([0xb0; 12]);
assert_eq!(
scanner.feed(&runaway),
Err(PackStreamScanError::HeaderTooLong)
);
let mut scanner = PackStreamScanner::new(HashAlgo::Sha1);
assert_eq!(
scanner.feed(b"PACK\0\0\0\x02\0\0\0\x01\x30\xff\xff\xff\xff"),
Err(PackStreamScanError::Inflate(0))
);
}
#[test]
fn ignores_the_trailer_after_the_last_object() {
let mut pack = sample_pack();
pack.extend_from_slice(b"anything after the checksum");
let mut scanner = PackStreamScanner::new(HashAlgo::Sha1);
scanner.feed(&pack).expect("scan");
assert_eq!(scanner.counts(), Some(expected()));
}
}