crates/gunnar-delta/tests/support/mod.rs
//! Shared test harness: build a **real packfile** from this crate's deltas.
//!
//! # Why a whole packfile, for a unit-level round trip
//!
//! Because that is the only way to reach gix's decoder. `gix_pack::data::delta`
//! is a public module whose two functions — `apply` and `decode_header_size` —
//! are `pub(crate)`. The only reachable path to them from outside gix-pack is
//! `gix_pack::data::File::decode_entry`, which reads a mmapped `.pack`. So the
//! tests write real packs.
//!
//! That is a better bar anyway, and it is the same artefact `git index-pack`
//! and `git fsck` are pointed at, so one writer serves every test in the suite
//! (LAW 5: one writer both paths route through). A second, test-local applier
//! would be a twin of gix's decoder that could drift from it silently, and a
//! delta encoder validated against its author's own decoder is validated
//! against nothing.
//!
//! # The format written here
//!
//! Pack v2. `PACK`, version, object count, then entries, then a SHA-1 trailer
//! over everything preceding. Each entry is a type/size header, then for an
//! `OFS_DELTA` a backwards offset, then the zlib stream.
#![allow(dead_code)]
use std::io::Write;
use gix_hash::ObjectId;
use gix_object::Kind;
/// One thing to put in a pack.
pub enum PackEntry {
/// A whole object.
Base { kind: Kind, data: Vec<u8> },
/// A delta against an earlier entry, by index into the entry list.
OfsDelta {
kind: Kind,
base: usize,
delta: Vec<u8>,
},
}
/// A written pack plus the byte offset of every entry, in order.
pub struct WrittenPack {
pub bytes: Vec<u8>,
pub offsets: Vec<u64>,
}
fn type_code(kind: Kind) -> u8 {
match kind {
Kind::Commit => 1,
Kind::Tree => 2,
Kind::Blob => 3,
Kind::Tag => 4,
}
}
/// git's type/size header: 4 size bits in the first byte alongside the type,
/// then 7 per continuation byte.
fn write_entry_header(out: &mut Vec<u8>, ty: u8, mut size: u64) {
let mut byte = (ty << 4) | (size as u8 & 0x0f);
size >>= 4;
while size > 0 {
out.push(byte | 0x80);
byte = size as u8 & 0x7f;
size >>= 7;
}
out.push(byte);
}
/// git's negative-offset encoding for `OFS_DELTA`, big-endian with an implicit
/// bias per continuation byte.
fn write_ofs(out: &mut Vec<u8>, mut ofs: u64) {
let mut buf = [0u8; 16];
let mut i = buf.len() - 1;
buf[i] = (ofs & 0x7f) as u8;
while {
ofs >>= 7;
ofs > 0
} {
ofs -= 1;
i -= 1;
buf[i] = 0x80 | (ofs & 0x7f) as u8;
}
out.extend_from_slice(&buf[i..]);
}
fn deflate(data: &[u8]) -> Vec<u8> {
let mut e = flate2::write::ZlibEncoder::new(Vec::new(), flate2::Compression::new(6));
e.write_all(data).expect("in-memory write");
e.finish().expect("in-memory finish")
}
/// Write a v2 packfile.
pub fn write_pack(entries: &[PackEntry]) -> WrittenPack {
let mut out = Vec::new();
out.extend_from_slice(b"PACK");
out.extend_from_slice(&2u32.to_be_bytes());
out.extend_from_slice(&(entries.len() as u32).to_be_bytes());
let mut offsets = Vec::with_capacity(entries.len());
for e in entries {
let at = out.len() as u64;
offsets.push(at);
match e {
PackEntry::Base { kind, data } => {
write_entry_header(&mut out, type_code(*kind), data.len() as u64);
out.extend_from_slice(&deflate(data));
}
PackEntry::OfsDelta { base, delta, .. } => {
write_entry_header(&mut out, 6, delta.len() as u64);
write_ofs(&mut out, at - offsets[*base]);
out.extend_from_slice(&deflate(delta));
}
}
}
let mut hasher = <sha1::Sha1 as sha1::Digest>::new();
sha1::Digest::update(&mut hasher, &out);
let digest = sha1::Digest::finalize(hasher);
out.extend_from_slice(&digest);
WrittenPack {
bytes: out,
offsets,
}
}
/// Read the two leading varints of a delta payload: the size the delta claims
/// its base has, the size it claims to produce, and where the instructions
/// start.
///
/// This is `gix_pack::data::delta::decode_header_size` transcribed, because that
/// function is `pub(crate)`. It exists so a test can assert on the header
/// *directly*: gix's `decode_entry` does not compare the stated base size to the
/// base it was handed, so a wrong base-size varint is invisible to the whole
/// gix round trip and only stock `git` rejects it.
pub fn delta_header(delta: &[u8]) -> (u64, u64, usize) {
fn varint(d: &[u8], i: &mut usize) -> u64 {
let (mut v, mut shift) = (0u64, 0u32);
loop {
let b = d[*i];
*i += 1;
v |= u64::from(b & 0x7f) << shift;
shift += 7;
if b & 0x80 == 0 {
return v;
}
}
}
let mut i = 0;
let base = varint(delta, &mut i);
let result = varint(delta, &mut i);
(base, result, i)
}
/// Compute a git object id the way git does: `"<kind> <len>\0" + content`.
pub fn object_id(kind: Kind, data: &[u8]) -> ObjectId {
let mut hasher = <sha1::Sha1 as sha1::Digest>::new();
sha1::Digest::update(&mut hasher, kind.as_bytes());
sha1::Digest::update(&mut hasher, b" ");
sha1::Digest::update(&mut hasher, data.len().to_string().as_bytes());
sha1::Digest::update(&mut hasher, b"\0");
sha1::Digest::update(&mut hasher, data);
let mut id = [0u8; 20];
id.copy_from_slice(&sha1::Digest::finalize(hasher));
ObjectId::from(id)
}
/// Decode every entry of a written pack **with gix's decoder** and return the
/// reconstructed bytes in entry order.
///
/// This is the only reconstruction path the suite trusts. Nothing in
/// `gunnar-delta` participates.
pub fn decode_all_with_gix(pack: &WrittenPack, dir: &std::path::Path) -> Vec<Vec<u8>> {
let path = dir.join("test.pack");
std::fs::write(&path, &pack.bytes).expect("write pack");
let file = gix_pack::data::File::at(&path, gix_hash::Kind::Sha1).expect("gix opens the pack");
let mut inflate = Default::default();
let mut cache = gix_pack::cache::Never;
let resolve = |_: &gix_hash::oid,
_: &mut Vec<u8>|
-> gix_error::Result<Option<gix_pack::data::decode::entry::ResolvedBase>> {
panic!(
"this harness writes no REF_DELTA entries, so nothing may need out-of-pack resolution"
)
};
pack.offsets
.iter()
.map(|&ofs| {
let entry = file.entry(ofs).expect("entry header parses");
let mut out = Vec::new();
file.decode_entry(entry, &mut out, &mut inflate, &resolve, &mut cache)
.expect("gix decodes the entry");
out
})
.collect()
}