use anyhow::{bail, Result};
use flate2::{Decompress, FlushDecompress, Status};
const SCRATCH_BYTES: usize = 64 * 1024;
mod kind {
pub(super) const OFS_DELTA: u8 = 6;
pub(super) const REF_DELTA: u8 = 7;
pub(super) const RESERVED: u8 = 5;
}
enum Phase {
Header {
seen: usize,
buf: [u8; 12],
},
EntryHeader {
first: bool,
kind: u8,
},
OfsBase,
RefBase {
left: usize,
},
Data,
Trailer {
left: usize,
},
Done,
}
pub(super) struct PackScanner {
phase: Phase,
entries_left: u32,
hash_len: usize,
scratch: Vec<u8>,
stream: Decompress,
version: u32,
num_objects: u32,
offset: u64,
}
impl PackScanner {
pub(super) fn new(hash_len: usize) -> Self {
PackScanner {
phase: Phase::Header {
seen: 0,
buf: [0u8; 12],
},
entries_left: 0,
hash_len,
scratch: vec![0u8; SCRATCH_BYTES],
stream: Decompress::new(true),
version: 0,
num_objects: 0,
offset: 0,
}
}
pub(super) fn done(&self) -> bool {
matches!(self.phase, Phase::Done)
}
pub(super) fn header(&self) -> (u32, u32) {
(self.version, self.num_objects)
}
pub(super) fn waiting_for(&self) -> &'static str {
match self.phase {
Phase::Header { .. } => "the 12-byte pack header",
Phase::EntryHeader { .. } => "an object entry's type-and-size header",
Phase::OfsBase => "an ofs-delta entry's base offset",
Phase::RefBase { .. } => "a ref-delta entry's base object id",
Phase::Data => "an object entry's compressed data",
Phase::Trailer { .. } => "the pack's trailing checksum",
Phase::Done => "nothing: the pack is complete",
}
}
pub(super) fn feed(&mut self, chunk: &[u8]) -> Result<usize> {
let Self {
phase,
entries_left,
hash_len,
scratch,
stream,
version,
num_objects,
offset,
} = self;
let hash_len = *hash_len;
let mut at = 0usize;
while at < chunk.len() {
match phase {
Phase::Done => break,
Phase::Header { seen, buf } => {
let want = 12 - *seen;
let take = want.min(chunk.len() - at);
buf[*seen..*seen + take].copy_from_slice(&chunk[at..at + take]);
*seen += take;
at += take;
if *seen < 12 {
continue;
}
if &buf[..4] != b"PACK" {
bail!(
"a push must begin with the four bytes `PACK`; this one begins \
{:?}. The command list and the pack are on one stream, so a \
wrong magic here means the pkt-line framing left the stream in \
the wrong place, not that the pack is corrupt",
&buf[..4]
);
}
*version = u32::from_be_bytes([buf[4], buf[5], buf[6], buf[7]]);
*num_objects = u32::from_be_bytes([buf[8], buf[9], buf[10], buf[11]]);
*entries_left = *num_objects;
*phase = if *entries_left == 0 {
Phase::Trailer { left: hash_len }
} else {
Phase::EntryHeader {
first: true,
kind: 0,
}
};
}
Phase::EntryHeader { first, kind } => {
let byte = chunk[at];
at += 1;
if *first {
*kind = (byte >> 4) & 0b111;
if *kind == 0 || *kind == kind::RESERVED {
bail!(
"object entry {} of this push declares type {}, which no pack \
may contain",
*num_objects - *entries_left + 1,
*kind
);
}
*first = false;
}
if byte & 0b1000_0000 == 0 {
*phase = match *kind {
kind::OFS_DELTA => Phase::OfsBase,
kind::REF_DELTA => Phase::RefBase { left: hash_len },
_ => {
stream.reset(true);
Phase::Data
}
};
}
}
Phase::OfsBase => {
let byte = chunk[at];
at += 1;
if byte & 0b1000_0000 == 0 {
stream.reset(true);
*phase = Phase::Data;
}
}
Phase::RefBase { left } => {
let take = (*left).min(chunk.len() - at);
at += take;
*left -= take;
if *left == 0 {
stream.reset(true);
*phase = Phase::Data;
}
}
Phase::Data => {
let before = stream.total_in();
let produced_before = stream.total_out();
let status = stream
.decompress(&chunk[at..], scratch, FlushDecompress::None)
.map_err(|err| {
anyhow::anyhow!(
"object entry {} of this push does not inflate at pack offset \
{}: {err}",
*num_objects - *entries_left + 1,
*offset + at as u64
)
})?;
let used = (stream.total_in() - before) as usize;
at += used;
match status {
Status::StreamEnd => {
*entries_left -= 1;
*phase = if *entries_left == 0 {
Phase::Trailer { left: hash_len }
} else {
Phase::EntryHeader {
first: true,
kind: 0,
}
};
}
Status::Ok | Status::BufError
if used == 0 && stream.total_out() == produced_before =>
{
bail!(
"object entry {} of this push stopped inflating at pack offset \
{} with {} byte(s) still to read, which no well-formed zlib \
stream does",
*num_objects - *entries_left + 1,
*offset + at as u64,
chunk.len() - at
);
}
Status::Ok | Status::BufError => {}
}
}
Phase::Trailer { left } => {
let take = (*left).min(chunk.len() - at);
at += take;
*left -= take;
if *left == 0 {
*phase = Phase::Done;
break;
}
}
}
}
*offset += at as u64;
Ok(at)
}
}
#[cfg(test)]
pub(super) mod fixture {
use std::io::Write as _;
use super::kind;
pub(in crate::storage) fn deflate(content: &[u8]) -> Vec<u8> {
let mut encoder =
flate2::write::ZlibEncoder::new(Vec::new(), flate2::Compression::default());
encoder.write_all(content).expect("a Vec sink cannot fail");
encoder.finish().expect("a Vec sink cannot fail")
}
pub(in crate::storage) fn entry_header(kind: u8, size: usize) -> Vec<u8> {
let mut out = Vec::new();
let mut byte = ((kind & 0b111) << 4) | (size & 0x0f) as u8;
let mut rest = size >> 4;
while rest > 0 {
out.push(byte | 0b1000_0000);
byte = (rest & 0x7f) as u8;
rest >>= 7;
}
out.push(byte);
out
}
pub(in crate::storage) fn ofs_base(mut n: u64) -> Vec<u8> {
let mut out = vec![(n & 0x7f) as u8];
while n > 127 {
n = (n >> 7) - 1;
out.insert(0, 0b1000_0000 | (n & 0x7f) as u8);
}
out
}
pub(in crate::storage) fn fixture_pack() -> Vec<u8> {
let mut seed = 0x2545_F491_4F6C_DD1Du64;
let big: Vec<u8> = (0..5000)
.map(|_| {
seed ^= seed << 13;
seed ^= seed >> 7;
seed ^= seed << 17;
(seed >> 24) as u8
})
.collect();
let mut out = Vec::new();
out.extend_from_slice(b"PACK");
out.extend_from_slice(&2u32.to_be_bytes());
out.extend_from_slice(&4u32.to_be_bytes());
out.extend_from_slice(&entry_header(3, 5));
out.extend_from_slice(&deflate(b"hello"));
out.extend_from_slice(&entry_header(3, big.len()));
out.extend_from_slice(&deflate(&big));
out.extend_from_slice(&entry_header(kind::OFS_DELTA, 9));
out.extend_from_slice(&ofs_base(4321));
out.extend_from_slice(&deflate(b"delta-ofs"));
out.extend_from_slice(&entry_header(kind::REF_DELTA, 9));
out.extend_from_slice(&[0xABu8; 20]);
out.extend_from_slice(&deflate(b"delta-ref"));
out.extend_from_slice(&[0x11u8; 20]);
out
}
}
#[cfg(test)]
mod tests {
use super::fixture::{deflate, entry_header, fixture_pack};
use super::*;
#[test]
fn the_fixture_is_a_pack_with_four_entries_of_four_shapes() {
let pack = fixture_pack();
assert_eq!(&pack[..4], b"PACK");
assert_eq!(
u32::from_be_bytes([pack[8], pack[9], pack[10], pack[11]]),
4
);
assert!(
entry_header(3, 5000).len() > 1,
"the large entry is in the fixture so a multi-byte size varint is walked; that \
size now fits in one byte and the varint loop is untested"
);
assert!(
pack.len() > 4000,
"the large entry is also what makes the input span several reads; the whole \
fixture is only {} bytes, so every chunked path is skipped",
pack.len()
);
}
#[test]
fn the_scanner_stops_on_the_trailer_and_not_after_it() {
let pack = fixture_pack();
let mut stream = pack.clone();
stream.extend_from_slice(b"THIS IS NOT THE PACK");
let mut scanner = PackScanner::new(20);
let used = scanner.feed(&stream).expect("a well-formed pack");
assert_eq!(
used,
pack.len(),
"the scanner claimed {used} of {} pack bytes; anything past {} is the report \
the client is waiting for, and reading it is the deadlock",
stream.len(),
pack.len()
);
assert!(scanner.done());
assert_eq!(scanner.header(), (2, 4));
}
#[test]
fn the_scanner_gives_the_same_answer_byte_by_byte() {
let pack = fixture_pack();
let mut stream = pack.clone();
stream.extend_from_slice(b"TRAILING RUBBISH");
let mut scanner = PackScanner::new(20);
let mut consumed = 0usize;
for i in 0..stream.len() {
if scanner.done() {
break;
}
consumed += scanner.feed(&stream[i..i + 1]).expect("a well-formed pack");
}
assert!(scanner.done(), "the scanner never reached the trailer");
assert_eq!(consumed, pack.len());
}
#[test]
fn an_empty_pack_is_header_plus_trailer() {
let mut pack = Vec::new();
pack.extend_from_slice(b"PACK");
pack.extend_from_slice(&2u32.to_be_bytes());
pack.extend_from_slice(&0u32.to_be_bytes());
pack.extend_from_slice(&[0x22u8; 20]);
let mut stream = pack.clone();
stream.extend_from_slice(b"after");
let mut scanner = PackScanner::new(20);
assert_eq!(scanner.feed(&stream).unwrap(), pack.len());
assert!(scanner.done());
}
#[test]
fn the_hash_width_is_the_one_the_repository_uses() {
let mut pack = Vec::new();
pack.extend_from_slice(b"PACK");
pack.extend_from_slice(&2u32.to_be_bytes());
pack.extend_from_slice(&1u32.to_be_bytes());
pack.extend_from_slice(&entry_header(kind::REF_DELTA, 4));
pack.extend_from_slice(&[0xCDu8; 32]);
pack.extend_from_slice(&deflate(b"base"));
pack.extend_from_slice(&[0x33u8; 32]);
let mut stream = pack.clone();
stream.extend_from_slice(b"and the report");
let mut scanner = PackScanner::new(32);
assert_eq!(scanner.feed(&stream).unwrap(), pack.len());
assert!(scanner.done());
}
#[test]
fn a_truncated_pack_never_completes() {
let pack = fixture_pack();
let mut scanner = PackScanner::new(20);
let short = &pack[..pack.len() - 5];
assert_eq!(scanner.feed(short).unwrap(), short.len());
assert!(!scanner.done());
assert_eq!(scanner.waiting_for(), "the pack's trailing checksum");
}
#[test]
fn something_that_is_not_a_pack_is_refused_by_name() {
let mut scanner = PackScanner::new(20);
let err = scanner
.feed(b"0009done\n0000AAAA")
.expect_err("`0009done` is not a packfile");
assert!(
format!("{err}").contains("`PACK`"),
"the refusal must name the magic it wanted: {err}"
);
}
}