use anyhow::{anyhow, bail, Context as _, Result};
use flate2::{Decompress, FlushDecompress, Status};
use crate::index_layout::ObjType;
use crate::object::GitHashKind;
const SCRATCH: usize = 64 * 1024;
const HEADER_LEN: usize = 12;
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum DeltaBase {
None,
Offset(u64),
Ref(Vec<u8>),
}
impl DeltaBase {
pub fn as_offset(&self) -> u64 {
match self {
DeltaBase::Offset(o) => *o,
DeltaBase::None | DeltaBase::Ref(_) => 0,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct PackEntry {
pub offset: u64,
pub len: u64,
pub obj_type: ObjType,
pub uncompressed_size: u64,
pub delta_base: DeltaBase,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct PackWalk {
pub version: u32,
pub entries: Vec<PackEntry>,
pub trailer: Vec<u8>,
}
#[derive(Debug, Clone, PartialEq, Eq, Default)]
pub struct Closure {
pub broken_offsets: Vec<u64>,
pub external_refs: Vec<Vec<u8>>,
}
impl Closure {
pub fn is_self_contained(&self) -> bool {
self.broken_offsets.is_empty() && self.external_refs.is_empty()
}
}
impl PackWalk {
pub fn rebased(mut self, archive_offset: u64) -> Self {
for e in &mut self.entries {
e.offset += archive_offset;
if let DeltaBase::Offset(o) = &mut e.delta_base {
*o += archive_offset;
}
}
self
}
pub fn closure(&self) -> Closure {
let boundaries: std::collections::HashSet<u64> =
self.entries.iter().map(|e| e.offset).collect();
let mut c = Closure::default();
for e in &self.entries {
match &e.delta_base {
DeltaBase::None => {}
DeltaBase::Offset(o) => {
if !boundaries.contains(o) {
c.broken_offsets.push(*o);
}
}
DeltaBase::Ref(oid) => c.external_refs.push(oid.clone()),
}
}
c
}
pub fn declared_bytes(&self) -> u64 {
self.entries.iter().map(|e| e.uncompressed_size).sum()
}
}
pub fn walk(pack: &[u8], oid_len: usize) -> Result<PackWalk> {
if oid_len != 20 && oid_len != 32 {
bail!("oid width {oid_len} is neither sha1 (20) nor sha256 (32)");
}
if pack.len() < HEADER_LEN + oid_len {
bail!(
"a pack is at least {} bytes (header + trailer), this one is {}",
HEADER_LEN + oid_len,
pack.len()
);
}
if &pack[0..4] != b"PACK" {
bail!("not a packfile: it does not start with `PACK`");
}
let version = u32::from_be_bytes([pack[4], pack[5], pack[6], pack[7]]);
if version != 2 && version != 3 {
bail!("pack version {version} is not 2 or 3");
}
let count = u32::from_be_bytes([pack[8], pack[9], pack[10], pack[11]]) as usize;
let body_end = pack.len() - oid_len;
let mut pos = HEADER_LEN;
let mut entries = Vec::with_capacity(count);
let mut scratch = vec![0u8; SCRATCH];
for i in 0..count {
if pos >= body_end {
bail!(
"the pack header claims {count} objects but the bytes ran out after {i} — {} of \
{} bytes consumed",
pos,
pack.len()
);
}
let start = pos;
let (obj_type, size, n) = type_and_size(&pack[pos..body_end])
.map_err(|e| anyhow!("entry {i} at offset {start}: {e}"))?;
pos += n;
let delta_base = match obj_type {
ObjType::OfsDelta => {
let (distance, n) = ofs_distance(&pack[pos..body_end])
.map_err(|e| anyhow!("entry {i} at offset {start}: {e}"))?;
pos += n;
let start_u64 = start as u64;
if distance == 0 || distance > start_u64 {
bail!(
"entry {i} at offset {start} is an ofs-delta whose base is {distance} \
bytes back, which is outside the pack"
);
}
DeltaBase::Offset(start_u64 - distance)
}
ObjType::RefDelta => {
if pos + oid_len > body_end {
bail!("entry {i} at offset {start}: a ref-delta base oid runs off the pack");
}
let oid = pack[pos..pos + oid_len].to_vec();
pos += oid_len;
DeltaBase::Ref(oid)
}
_ => DeltaBase::None,
};
let consumed = inflate_and_discard(&pack[pos..body_end], size, &mut scratch)
.map_err(|e| anyhow!("entry {i} at offset {start}: {e}"))?;
pos += consumed;
entries.push(PackEntry {
offset: start as u64,
len: (pos - start) as u64,
obj_type,
uncompressed_size: size,
delta_base,
});
}
if pos != body_end {
bail!(
"the pack's {count} entries end at {pos} but its trailer starts at {body_end} — \
{} bytes are unaccounted for",
body_end - pos.min(body_end)
);
}
Ok(PackWalk {
version,
entries,
trailer: pack[body_end..].to_vec(),
})
}
fn type_and_size(b: &[u8]) -> Result<(ObjType, u64, usize)> {
let first = *b.first().ok_or_else(|| anyhow!("no type/size header"))?;
let code = (first >> 4) & 0b111;
let obj_type = ObjType::from_code(code).ok_or_else(|| {
anyhow!("object type code {code} is not one git writes — refusing to guess it")
})?;
let mut size = u64::from(first & 0x0f);
let mut shift = 4u32;
let mut i = 1usize;
let mut cont = first & 0x80 != 0;
while cont {
let byte = *b
.get(i)
.ok_or_else(|| anyhow!("the type/size varint runs off the pack"))?;
if shift >= 64 {
bail!("the type/size varint is longer than a u64 can hold");
}
size |= u64::from(byte & 0x7f) << shift;
shift += 7;
cont = byte & 0x80 != 0;
i += 1;
}
Ok((obj_type, size, i))
}
fn ofs_distance(b: &[u8]) -> Result<(u64, usize)> {
let mut i = 0usize;
let mut byte = *b
.first()
.ok_or_else(|| anyhow!("no ofs-delta distance varint"))?;
i += 1;
let mut d = u64::from(byte & 0x7f);
while byte & 0x80 != 0 {
byte = *b
.get(i)
.ok_or_else(|| anyhow!("the ofs-delta distance varint runs off the pack"))?;
i += 1;
d = d
.checked_add(1)
.and_then(|d| d.checked_shl(7))
.ok_or_else(|| anyhow!("the ofs-delta distance overflows a u64"))?
| u64::from(byte & 0x7f);
}
Ok((d, i))
}
fn inflate_and_discard(input: &[u8], declared: u64, scratch: &mut [u8]) -> Result<usize> {
let mut d = Decompress::new(true);
loop {
let before_in = d.total_in();
let before_out = d.total_out();
let status = d
.decompress(&input[before_in as usize..], scratch, FlushDecompress::None)
.map_err(|e| anyhow!("zlib: {e}"))?;
match status {
Status::StreamEnd => break,
Status::Ok | Status::BufError => {
if d.total_in() == before_in && d.total_out() == before_out {
bail!("the zlib stream is truncated after {} bytes", d.total_in());
}
}
}
}
if d.total_out() != declared {
bail!(
"the entry header declares {declared} bytes but its stream inflates to {}",
d.total_out()
);
}
Ok(d.total_in() as usize)
}
#[cfg(test)]
mod tests {
use super::*;
use flate2::{write::ZlibEncoder, Compression};
use std::io::Write;
fn deflate(bytes: &[u8]) -> Vec<u8> {
let mut e = ZlibEncoder::new(Vec::new(), Compression::default());
e.write_all(bytes).unwrap();
e.finish().unwrap()
}
fn header(code: u8, mut size: u64) -> Vec<u8> {
let mut out = vec![(code << 4) | (size as u8 & 0x0f)];
size >>= 4;
while size > 0 {
let last = out.len() - 1;
out[last] |= 0x80;
out.push((size & 0x7f) as u8);
size >>= 7;
}
out
}
fn ofs(mut d: u64) -> Vec<u8> {
let mut out = vec![(d & 0x7f) as u8];
d >>= 7;
while d > 0 {
d -= 1;
out.insert(0, 0x80 | (d & 0x7f) as u8);
d >>= 7;
}
out
}
fn three_entry_pack() -> (Vec<u8>, Vec<u64>) {
let a = b"the quick brown fox jumps over the lazy dog".to_vec();
let b = vec![b'x'; 300];
let delta = b"\x2b\x2b\x90\x01\x00".to_vec();
let mut pack = b"PACK".to_vec();
pack.extend_from_slice(&2u32.to_be_bytes());
pack.extend_from_slice(&3u32.to_be_bytes());
let mut offsets = Vec::new();
offsets.push(pack.len() as u64);
pack.extend_from_slice(&header(3, a.len() as u64));
pack.extend_from_slice(&deflate(&a));
offsets.push(pack.len() as u64);
pack.extend_from_slice(&header(3, b.len() as u64));
pack.extend_from_slice(&deflate(&b));
let third = pack.len() as u64;
offsets.push(third);
pack.extend_from_slice(&header(6, delta.len() as u64));
pack.extend_from_slice(&ofs(third - offsets[0]));
pack.extend_from_slice(&deflate(&delta));
pack.extend_from_slice(&[0u8; 20]); (pack, offsets)
}
fn thin_pack(base_oid: &[u8]) -> Vec<u8> {
let delta = b"\x0a\x0a\x91\x00\x0a".to_vec();
let mut pack = b"PACK".to_vec();
pack.extend_from_slice(&2u32.to_be_bytes());
pack.extend_from_slice(&1u32.to_be_bytes());
pack.extend_from_slice(&header(7, delta.len() as u64));
pack.extend_from_slice(base_oid);
pack.extend_from_slice(&deflate(&delta));
pack.extend_from_slice(&[0u8; 20]);
pack
}
#[test]
fn every_entry_boundary_is_where_the_bytes_say_it_is() {
let (pack, expected) = three_entry_pack();
let w = walk(&pack, 20).expect("a well-formed pack walks");
assert_eq!(w.version, 2);
assert_eq!(w.entries.len(), 3);
let got: Vec<u64> = w.entries.iter().map(|e| e.offset).collect();
assert_eq!(got, expected, "entry offsets");
for (i, e) in w.entries.iter().enumerate() {
let next = w
.entries
.get(i + 1)
.map(|n| n.offset)
.unwrap_or((pack.len() - 20) as u64);
assert_eq!(
e.offset + e.len,
next,
"entry {i} claims to end at {} but the next starts at {next}",
e.offset + e.len
);
}
assert_eq!(w.entries[0].obj_type, ObjType::Blob);
assert_eq!(w.entries[0].uncompressed_size, 43);
assert_eq!(w.entries[1].uncompressed_size, 300);
assert_eq!(w.entries[2].obj_type, ObjType::OfsDelta);
assert_eq!(
w.entries[2].delta_base,
DeltaBase::Offset(expected[0]),
"the ofs-delta base must resolve to the first entry's offset"
);
assert_eq!(w.trailer.len(), 20);
}
#[test]
fn the_closure_check_falls_out_of_the_walk_and_consults_nothing() {
let (pack, offsets) = three_entry_pack();
let c = walk(&pack, 20).unwrap().closure();
assert!(
c.is_self_contained(),
"a pack whose only delta is an ofs-delta into itself needs nobody: {c:?}"
);
assert!(c.external_refs.is_empty());
let base = vec![0xab; 20];
let c = walk(&thin_pack(&base), 20).unwrap().closure();
assert!(!c.is_self_contained(), "a thin pack is not self-contained");
assert_eq!(c.external_refs, vec![base], "the one oid to ask about");
assert!(c.broken_offsets.is_empty(), "a thin pack is not corrupt");
let mut w = walk(&pack, 20).unwrap();
w.entries[2].delta_base = DeltaBase::Offset(offsets[0] + 1);
let c = w.closure();
assert_eq!(c.broken_offsets, vec![offsets[0] + 1]);
assert!(c.external_refs.is_empty());
}
#[test]
fn rebasing_moves_an_entry_and_its_base_by_the_same_amount() {
let (pack, offsets) = three_entry_pack();
let base = 1_000_000u64;
let w = walk(&pack, 20).unwrap().rebased(base);
assert_eq!(w.entries[0].offset, offsets[0] + base);
assert_eq!(
w.entries[2].delta_base,
DeltaBase::Offset(offsets[0] + base)
);
assert!(
w.closure().is_self_contained(),
"a rebased pack is still self-contained — that is the point"
);
assert_eq!(DeltaBase::None.as_offset(), 0);
assert_eq!(DeltaBase::Ref(vec![1; 20]).as_offset(), 0);
}
#[test]
fn a_hostile_pack_is_an_error_with_a_reason_never_a_panic() {
let (good, _) = three_entry_pack();
let mut wrong_magic = good.clone();
wrong_magic[0] = b'N';
let mut wrong_version = good.clone();
wrong_version[7] = 9;
let mut too_many = good.clone();
too_many[11] = 99;
let truncated = good[..good.len() / 2].to_vec();
let mut liar = b"PACK".to_vec();
liar.extend_from_slice(&2u32.to_be_bytes());
liar.extend_from_slice(&1u32.to_be_bytes());
liar.extend_from_slice(&header(3, 999));
liar.extend_from_slice(&deflate(b"short"));
liar.extend_from_slice(&[0u8; 20]);
let mut bad_type = b"PACK".to_vec();
bad_type.extend_from_slice(&2u32.to_be_bytes());
bad_type.extend_from_slice(&1u32.to_be_bytes());
bad_type.extend_from_slice(&header(5, 5));
bad_type.extend_from_slice(&deflate(b"hello"));
bad_type.extend_from_slice(&[0u8; 20]);
let mut bad_ofs = b"PACK".to_vec();
bad_ofs.extend_from_slice(&2u32.to_be_bytes());
bad_ofs.extend_from_slice(&1u32.to_be_bytes());
bad_ofs.extend_from_slice(&header(6, 5));
bad_ofs.extend_from_slice(&ofs(1_000_000));
bad_ofs.extend_from_slice(&deflate(b"delta"));
bad_ofs.extend_from_slice(&[0u8; 20]);
for (what, bytes) in [
("wrong magic", wrong_magic),
("wrong version", wrong_version),
("more objects than bytes", too_many),
("truncated", truncated),
("lies about its size", liar),
("unused type code", bad_type),
("base before the pack", bad_ofs),
("empty", Vec::new()),
("header only", b"PACK\0\0\0\x02\0\0\0\x01".to_vec()),
] {
let r = walk(&bytes, 20);
assert!(r.is_err(), "{what} must be refused, got {r:?}");
}
assert!(walk(&good, 21).is_err(), "an oid width of 21 is nonsense");
}
#[test]
fn ours_and_gix_agree_on_every_entry_of_every_pack() {
let mut compared = 0usize;
let mut packs = 0usize;
let (synthetic, _) = three_entry_pack();
for p in [synthetic, thin_pack(&[0x7f; 20])] {
compared += agree(&p);
packs += 1;
}
assert!(
packs == 2 && compared >= 4,
"the synthetic packs must compare"
);
let mut real = 0usize;
for pack in real_packs(8) {
let bytes = std::fs::read(&pack).expect("reading a real pack");
let n = agree(&bytes);
eprintln!("{}: {n} entries agree", pack.display());
compared += n;
real += 1;
}
eprintln!(
"compared {compared} entries over {} packs ({real} real)",
packs + real
);
}
fn agree(pack: &[u8]) -> usize {
let ours = walk(pack, 20).expect("ours walks it");
let theirs = gix_pack::data::input::BytesToEntriesIter::new_from_header(
std::io::BufReader::new(pack),
gix_pack::data::input::Mode::AsIs,
gix_pack::data::input::EntryDataMode::Ignore,
gix_hash::Kind::Sha1,
)
.expect("gix reads the header");
let mut n = 0usize;
for (i, entry) in theirs.enumerate() {
let g = entry.expect("gix walks it");
let o = &ours.entries[i];
assert_eq!(o.offset, g.pack_offset, "entry {i} offset");
assert_eq!(
o.len,
g.bytes_in_pack(),
"entry {i} length: ours {} vs gix {}",
o.len,
g.bytes_in_pack()
);
assert_eq!(
o.uncompressed_size, g.decompressed_size,
"entry {i} decompressed size"
);
let (gt, gbase) = match g.header {
gix_pack::data::entry::Header::Commit => (ObjType::Commit, DeltaBase::None),
gix_pack::data::entry::Header::Tree => (ObjType::Tree, DeltaBase::None),
gix_pack::data::entry::Header::Blob => (ObjType::Blob, DeltaBase::None),
gix_pack::data::entry::Header::Tag => (ObjType::Tag, DeltaBase::None),
gix_pack::data::entry::Header::OfsDelta { base_distance } => (
ObjType::OfsDelta,
DeltaBase::Offset(g.pack_offset - base_distance),
),
gix_pack::data::entry::Header::RefDelta { base_id } => (
ObjType::RefDelta,
DeltaBase::Ref(base_id.as_slice().to_vec()),
),
};
assert_eq!(o.obj_type, gt, "entry {i} type");
assert_eq!(o.delta_base, gbase, "entry {i} delta base");
n += 1;
}
assert_eq!(
n,
ours.entries.len(),
"gix found a different number of entries"
);
n
}
fn real_packs(cap: usize) -> Vec<std::path::PathBuf> {
let mut out = Vec::new();
let root = std::path::Path::new("/home/rickard/git");
let Ok(repos) = std::fs::read_dir(root) else {
return out;
};
for repo in repos.flatten() {
let dir = repo.path().join(".git/objects/pack");
let Ok(files) = std::fs::read_dir(&dir) else {
continue;
};
for f in files.flatten() {
let p = f.path();
let small = f.metadata().map(|m| m.len() < 64 << 20).unwrap_or(false);
if small && p.extension().is_some_and(|e| e == "pack") {
out.push(p);
if out.len() >= cap {
return out;
}
}
}
}
out
}
}
pub fn type_and_size_of(b: &[u8]) -> Result<(ObjType, u64, usize)> {
type_and_size(b)
}
pub fn ofs_distance_of(b: &[u8]) -> Result<(u64, usize)> {
ofs_distance(b)
}
pub fn encode_type_and_size(out: &mut Vec<u8>, obj_type: ObjType, size: u64) {
let mut byte = (obj_type.code() << 4) | ((size & 0x0f) as u8);
let mut rest = size >> 4;
while rest > 0 {
out.push(byte | 0x80);
byte = (rest & 0x7f) as u8;
rest >>= 7;
}
out.push(byte);
}
pub fn encode_ofs_distance(out: &mut Vec<u8>, distance: u64) {
let mut buf = [0u8; 10];
let mut i = buf.len() - 1;
let mut d = distance;
buf[i] = (d & 0x7f) as u8;
while d >= 0x80 {
d >>= 7;
d -= 1;
i -= 1;
buf[i] = 0x80 | (d & 0x7f) as u8;
}
out.extend_from_slice(&buf[i..]);
}
#[cfg(test)]
mod encode_tests {
use super::*;
#[test]
fn the_encoders_are_the_inverses_of_the_decoders() {
let sizes = [
0u64,
1,
15,
16,
17,
2047,
2048,
2049,
262_143,
262_144,
1 << 20,
1 << 31,
(1u64 << 57) - 1,
];
for &size in &sizes {
for t in [
ObjType::Commit,
ObjType::Tree,
ObjType::Blob,
ObjType::Tag,
ObjType::OfsDelta,
ObjType::RefDelta,
] {
let mut buf = Vec::new();
encode_type_and_size(&mut buf, t, size);
let (got_t, got_size, n) =
type_and_size(&buf).expect("what we wrote must parse back");
assert_eq!(got_t, t, "type round trip at size {size}");
assert_eq!(got_size, size, "size round trip for {t:?}");
assert_eq!(
n,
buf.len(),
"the decoder must consume exactly what was written"
);
}
}
let distances = [
0u64,
1,
126,
127,
128,
129,
16_383,
16_511,
16_512,
16_513,
1 << 20,
1 << 40,
u32::MAX as u64,
];
for &d in &distances {
let mut buf = Vec::new();
encode_ofs_distance(&mut buf, d);
let (got, n) = ofs_distance(&buf).expect("what we wrote must parse back");
assert_eq!(got, d, "distance round trip");
assert_eq!(
n,
buf.len(),
"the decoder must consume exactly what was written"
);
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum EntryBytes {
Extent { offset: u64, len: u64 },
Owned(Vec<u8>),
}
impl EntryBytes {
pub fn len(&self) -> u64 {
match self {
EntryBytes::Extent { len, .. } => *len,
EntryBytes::Owned(v) => v.len() as u64,
}
}
pub fn is_empty(&self) -> bool {
self.len() == 0
}
pub fn owned(&self) -> Option<&[u8]> {
match self {
EntryBytes::Owned(v) => Some(v),
EntryBytes::Extent { .. } => None,
}
}
}
pub fn resolve_against<'b>(e: &'b EmitEntry, archive: &'b [u8]) -> Result<&'b [u8]> {
match &e.stored {
EntryBytes::Owned(v) => Ok(v),
EntryBytes::Extent { offset, len } => {
let (a, b) = (*offset as usize, (*offset + *len) as usize);
archive.get(a..b).ok_or_else(|| {
anyhow!(
"the entry at archive offset {offset} spans ({offset}, {len}), which runs off \
the end of a {}-byte buffer",
archive.len()
)
})
}
}
}
#[derive(Debug, Clone)]
pub struct EmitEntry {
pub oid: Vec<u8>,
pub stored: EntryBytes,
pub obj_type: ObjType,
pub uncompressed_size: u64,
pub delta_base: u64,
pub offset: u64,
pub recompressed: bool,
pub deltified: bool,
}
pub fn topological_order(entries: Vec<EmitEntry>) -> (Vec<EmitEntry>, Vec<u64>) {
use std::collections::{HashMap, HashSet};
let present: HashMap<u64, usize> = entries
.iter()
.enumerate()
.map(|(i, e)| (e.offset, i))
.collect();
let mut missing = Vec::new();
let mut done: HashSet<usize> = HashSet::new();
let mut order: Vec<usize> = Vec::with_capacity(entries.len());
for start in 0..entries.len() {
if done.contains(&start) {
continue;
}
let mut stack = vec![start];
let mut on_path: HashSet<usize> = HashSet::new();
while let Some(&i) = stack.last() {
if done.contains(&i) {
stack.pop();
continue;
}
let base = entries[i].delta_base;
let pending = if base == 0 {
None
} else {
match present.get(&base) {
Some(&b) if !done.contains(&b) => {
if on_path.contains(&b) {
None
} else {
Some(b)
}
}
Some(_) => None,
None => {
missing.push(base);
None
}
}
};
match pending {
Some(b) => {
on_path.insert(i);
stack.push(b);
}
None => {
stack.pop();
on_path.remove(&i);
done.insert(i);
order.push(i);
}
}
}
}
missing.sort_unstable();
missing.dedup();
let mut slots: Vec<Option<EmitEntry>> = entries.into_iter().map(Some).collect();
let out: Vec<EmitEntry> = order
.into_iter()
.map(|i| {
slots[i]
.take()
.expect("topological_order emitted an index twice")
})
.collect();
(out, missing)
}
#[cfg(test)]
mod order_tests {
use super::*;
fn e(offset: u64, delta_base: u64) -> EmitEntry {
EmitEntry {
oid: vec![offset as u8],
stored: EntryBytes::Owned(Vec::new()),
obj_type: if delta_base == 0 {
ObjType::Blob
} else {
ObjType::OfsDelta
},
uncompressed_size: 0,
delta_base,
offset,
recompressed: false,
deltified: false,
}
}
#[test]
fn a_base_always_precedes_the_delta_that_names_it() {
let entries = vec![e(400, 200), e(300, 200), e(200, 100), e(100, 0)];
let (ordered, missing) = topological_order(entries);
assert!(missing.is_empty(), "nothing was absent: {missing:?}");
assert_eq!(ordered.len(), 4, "every entry must be emitted exactly once");
let at = |off: u64| ordered.iter().position(|x| x.offset == off).unwrap();
for (delta, base) in [(200u64, 100u64), (300, 200), (400, 200)] {
assert!(
at(base) < at(delta),
"base {base} at {} must precede delta {delta} at {}",
at(base),
at(delta)
);
}
}
#[test]
fn a_base_outside_the_set_is_reported_rather_than_dropped() {
let (ordered, missing) = topological_order(vec![e(300, 999), e(100, 0)]);
assert_eq!(missing, vec![999], "the absent base must be named");
assert_eq!(ordered.len(), 2, "the entry stays; the caller decides");
}
#[test]
fn a_cycle_terminates_instead_of_hanging() {
let (ordered, _) = topological_order(vec![e(100, 200), e(200, 100)]);
assert_eq!(ordered.len(), 2, "both entries must still be emitted");
}
}
#[derive(Debug, Clone, PartialEq, Eq, Default)]
pub struct EmitReport {
pub written: u32,
pub copied: u32,
pub recompressed: u32,
pub deltified: u32,
pub rebased: u32,
pub bytes: u64,
}
pub fn emit_pack<'p>(
entries: &'p [EmitEntry],
hash: GitHashKind,
out: &mut dyn std::io::Write,
stored_of: &dyn Fn(usize) -> Result<&'p [u8]>,
) -> Result<EmitReport> {
use std::collections::HashMap;
let count = u32::try_from(entries.len()).map_err(|_| {
anyhow!(
"a pack holds at most u32::MAX entries, was given {}",
entries.len()
)
})?;
let mut out = Trailing::new(out, hash);
out.put(b"PACK")?;
out.put(&2u32.to_be_bytes())?;
out.put(&count.to_be_bytes())?;
let mut hdr: Vec<u8> = Vec::with_capacity(32);
let mut placed: HashMap<u64, u64> = HashMap::with_capacity(entries.len());
let mut report = EmitReport::default();
for (i, e) in entries.iter().enumerate() {
let here = out.written();
let stored = stored_of(i)?;
let (_, stated_size, varint_len) = type_and_size(stored)?;
match e.obj_type {
ObjType::OfsDelta => {
let base_at = *placed.get(&e.delta_base).ok_or_else(|| {
anyhow!(
"entry at {} deltas against archive offset {}, which is not in this pack \
— the set is not closed and was not ordered by `topological_order`",
e.offset,
e.delta_base
)
})?;
let distance = here - base_at;
hdr.clear();
encode_type_and_size(&mut hdr, ObjType::OfsDelta, stated_size);
encode_ofs_distance(&mut hdr, distance);
out.put(&hdr)?;
report.rebased += 1;
}
ObjType::RefDelta => {
hdr.clear();
encode_type_and_size(&mut hdr, ObjType::RefDelta, stated_size);
let oid_len = hash.oid_len();
let base = stored.get(varint_len..).and_then(|r| r.get(..oid_len));
let base = base.ok_or_else(|| {
anyhow!(
"a ref-delta entry at {} has no base oid after its header",
e.offset
)
})?;
hdr.extend_from_slice(base);
out.put(&hdr)?;
}
t => {
hdr.clear();
encode_type_and_size(&mut hdr, t, stated_size);
out.put(&hdr)?;
}
}
out.put(&stored[header_len(stored, hash)?..])?;
if e.recompressed {
report.recompressed += 1;
if e.deltified {
report.deltified += 1;
}
} else {
report.copied += 1;
debug_assert!(
!e.deltified,
"a computed delta was inflated and re-deflated to produce it; counting it as a \
copy would make the receipt a claim rather than a measurement"
);
}
report.written += 1;
placed.insert(e.offset, here);
}
report.bytes = out.finish()?;
Ok(report)
}
struct Trailing<'a> {
inner: &'a mut dyn std::io::Write,
digest: Digest,
written: u64,
}
enum Digest {
Sha1(sha1::Sha1),
Sha256(sha2::Sha256),
}
impl<'a> Trailing<'a> {
fn new(inner: &'a mut dyn std::io::Write, hash: GitHashKind) -> Self {
use sha1::Digest as _;
Trailing {
inner,
digest: match hash {
GitHashKind::Sha1 => Digest::Sha1(sha1::Sha1::new()),
GitHashKind::Sha256 => Digest::Sha256(sha2::Sha256::new()),
},
written: 0,
}
}
fn written(&self) -> u64 {
self.written
}
fn put(&mut self, bytes: &[u8]) -> Result<()> {
use sha1::Digest as _;
match &mut self.digest {
Digest::Sha1(d) => d.update(bytes),
Digest::Sha256(d) => d.update(bytes),
}
self.inner
.write_all(bytes)
.context("writing an emitted pack")?;
self.written += bytes.len() as u64;
Ok(())
}
fn finish(self) -> Result<u64> {
use sha1::Digest as _;
let digest: Vec<u8> = match self.digest {
Digest::Sha1(d) => d.finalize().to_vec(),
Digest::Sha256(d) => d.finalize().to_vec(),
};
self.inner
.write_all(&digest)
.context("writing the pack trailer")?;
self.inner.flush().context("flushing an emitted pack")?;
Ok(self.written + digest.len() as u64)
}
}
pub fn header_len(stored: &[u8], hash: GitHashKind) -> Result<usize> {
let (t, _, n) = type_and_size(stored)?;
Ok(match t {
ObjType::OfsDelta => {
let (_, d) = ofs_distance(stored.get(n..).unwrap_or(&[]))?;
n + d
}
ObjType::RefDelta => n + hash.oid_len(),
_ => n,
})
}
#[cfg(test)]
mod emit_tests {
use super::*;
#[test]
fn stock_git_accepts_a_pack_we_emitted() {
if std::process::Command::new("git")
.arg("--version")
.output()
.is_err()
{
eprintln!("skipping: no git on PATH");
return;
}
let (pack, _) = crate::store::tests::real_pack();
let walk = walk(&pack, GitHashKind::Sha1.oid_len()).expect("the corpus pack walks");
let entries: Vec<EmitEntry> = walk
.entries
.iter()
.map(|e| EmitEntry {
oid: Vec::new(),
stored: EntryBytes::Extent {
offset: e.offset,
len: e.len,
},
obj_type: e.obj_type,
uncompressed_size: e.uncompressed_size,
delta_base: e.delta_base.as_offset(),
offset: e.offset,
recompressed: false,
deltified: false,
})
.collect();
let n = entries.len();
assert!(
n > 0,
"the corpus pack has no entries; nothing is being tested"
);
let (ordered, missing) = topological_order(entries);
assert!(
missing.is_empty(),
"a whole pack must be closed: {missing:?}"
);
let mut bytes = Vec::new();
let report = emit_pack(&ordered, GitHashKind::Sha1, &mut bytes, &|i| {
resolve_against(&ordered[i], &pack)
})
.expect("emitting");
assert_eq!(
report.bytes as usize,
bytes.len(),
"the report's byte count must be what was actually written"
);
assert_eq!(report.written as usize, n, "every entry must be written");
assert_eq!(
report.copied, report.written,
"every payload must be COPIED"
);
let dir = tempfile::tempdir().expect("tempdir");
crate::git_oracle::assert_git_accepts(
dir.path(),
"emitted.git",
&bytes,
crate::git_oracle::Strictness::Connected,
);
}
}