Your browser sucks, JavaScript disabled! Gunnar's console needs JavaScript and WebAssembly. Git over SSH on port 2222 does not.

nordisk/edda

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()
}