crates/gunnar-pack/tests/ofs_delta_off.rs
//! **`P-001` for a client that cannot decode `OBJ_OFS_DELTA`.**
//!
//! `write_pack_copy` re-points a copied delta at its base. With the client's
//! `ofs-delta` capability that base is named by *distance backwards*
//! (`OBJ_OFS_DELTA`); without it, by *object id* (`OBJ_REF_DELTA`). The delta
//! bytes are identical either way, so the reuse survives in full — and until
//! 2026-08-03 the serve path did not copy for such a client at all, which
//! MEASURED **18.336× the bytes** on the churned head-to-head fixture.
//!
//! # Why this test exists, and why it uses stock `git`
//!
//! `gix-pack`'s own comment calls an `OBJ_REF_DELTA` whose base lies inside the
//! same pack *"a discontinued practice"*
//! (`gix-pack/src/data/output/entry/mod.rs`). Discontinued is not invalid — `git
//! index-pack` resolves ref-deltas against the pack's own objects before looking
//! outside it, which is precisely how it completes a thin pack — but "gunnar's
//! reader accepts it" would be worth nothing here. A reader written next to the
//! writer can be wrong in exactly the same way and still agree. So the verdict
//! comes from **`git index-pack --strict`** and **`git verify-pack -v`**, and the
//! pack is one gunnar produced.
//!
//! # The assertion that is not the obvious one
//!
//! "`git index-pack` accepted it" passes just as happily on a pack containing no
//! deltas at all — which is exactly the state this change exists to get out of,
//! and exactly what the fixture would produce if `git repack` found nothing to
//! delta. So every case here asserts on `git verify-pack -v`'s own **entry type
//! column**: the `ofs_delta: false` pack must contain `ref delta` entries and no
//! `ofs delta` ones, and the `ofs_delta: true` pack the reverse. That is applied
//! output, read out of stock git, and it is what makes the acceptance mean
//! something.
//!
//! **A missing `git` is a failure, not a skip** (`S-026`).
use std::path::Path;
use gix_hash::{Kind, ObjectId};
use gunnar_pack::{write_pack_copy, WriteOptions};
mod common;
// The git harness and the entry-form reader live in `common` so that this file
// and `tests/deltified.rs` cannot drift apart about what `verify-pack -v`
// prints (LAW 5). They were written here first.
use common::{churned_repo, entry_kinds, git, git_available, packed_objects};
/// Write `ids` with `write_pack_copy`, index it with stock `git index-pack
/// --strict`, and report what stock git says is inside.
fn serve_and_index(
repo: &Path,
out_dir: &Path,
ids: &[ObjectId],
ofs_delta: bool,
) -> (u64, usize, usize, usize) {
let packed = packed_objects(repo);
let options = WriteOptions::new(Kind::Sha1).ofs_delta(ofs_delta);
let mut bytes = Vec::new();
let outcome =
write_pack_copy(ids, &packed, &mut bytes, &options).expect("writing the copied pack");
assert_eq!(
outcome.objects_recompressed, 0,
"the fixture is packed, so every entry should have been copied; {} were recompressed",
outcome.objects_recompressed
);
assert_eq!(
outcome.objects_copied_from_pack,
ids.len(),
"only {} of {} entries were copied",
outcome.objects_copied_from_pack,
ids.len()
);
let pack = out_dir.join(format!("in-{ofs_delta}.pack"));
std::fs::write(&pack, &bytes).expect("saving the pack");
// `--strict` turns "the bytes parsed" into "every object in it is valid and
// every delta resolved". This is the whole safety argument for emitting an
// OBJ_REF_DELTA whose base is inside the same pack.
git(
&["index-pack", "--strict", "-v", &pack.to_string_lossy()],
out_dir,
);
let idx = pack.with_extension("idx");
assert!(
idx.is_file(),
"git index-pack wrote no .idx next to {pack:?}"
);
let (base, ofs, refs) = entry_kinds(&idx, &pack, out_dir);
(bytes.len() as u64, base, ofs, refs)
}
#[test]
fn a_client_without_ofs_delta_still_gets_the_copied_deltas() -> anyhow::Result<()> {
assert!(
git_available(),
"this test requires `git` on PATH; a pack format claim cannot be proven without it"
);
let repo = tempfile::tempdir()?;
let work = tempfile::tempdir()?;
let ids = churned_repo(repo.path());
let (ofs_bytes, ofs_base, ofs_ofs, ofs_ref) =
serve_and_index(repo.path(), work.path(), &ids, true);
let (raw_bytes, raw_base, raw_ofs, raw_ref) =
serve_and_index(repo.path(), work.path(), &ids, false);
// ── the fixture is load-bearing, so it is asserted, not assumed ─────────
assert!(
ofs_ofs > 0,
"the ofs-delta pack holds {ofs_ofs} ofs-deltas over {ofs_base} bases — the fixture \
deltas to nothing, so neither case below proves anything"
);
// ── the applied output: which entry form stock git found ───────────────
assert_eq!(
ofs_ref, 0,
"the ofs-delta pack should name every base by distance, but git found {ofs_ref} \
ref-deltas in it"
);
assert_eq!(
raw_ofs, 0,
"a client without the capability was sent {raw_ofs} OBJ_OFS_DELTA entries it cannot \
decode"
);
assert_eq!(
raw_ref, ofs_ofs,
"the two packs should carry the same deltas under different names: {ofs_ofs} \
ofs-deltas against {raw_ref} ref-deltas"
);
assert_eq!(
raw_base, ofs_base,
"the two packs should hold the same base objects: {ofs_base} against {raw_base}"
);
// ── and the cost of the rename, which is the only thing that may differ ─
//
// One full object id per delta entry in place of a varint distance. At sha1
// that is 20 bytes against 1-3, so the ceiling is derived rather than picked:
// anything materially above it means something other than the header grew.
let ceiling = ofs_bytes + 20 * raw_ref as u64;
assert!(
raw_bytes > ofs_bytes && raw_bytes <= ceiling,
"the ref-delta pack is {raw_bytes} bytes against {ofs_bytes}; one base id per delta \
accounts for at most {ceiling}"
);
Ok(())
}
/// **What this one cannot catch, stated rather than left to be found.** With the
/// flag ignored — the pre-fix behaviour — both packs come out identical and this
/// comparison passes trivially. It went green under exactly that edit while the
/// test above went red. It is not hollow: it exists to catch the two forms
/// *decoding differently*, which is a distinct failure from emitting the wrong
/// form, and the form is what the other test asserts.
#[test]
fn the_two_forms_carry_byte_identical_delta_data() -> anyhow::Result<()> {
assert!(git_available(), "this test requires `git` on PATH");
let repo = tempfile::tempdir()?;
let ids = churned_repo(repo.path());
let packed = packed_objects(repo.path());
// Same fixture guard as the other test: on a corpus that deltas to nothing
// the two forms are trivially identical and this proves nothing.
{
let work = tempfile::tempdir()?;
let (_, _, ofs, _) = serve_and_index(repo.path(), work.path(), &ids, true);
assert!(
ofs > 0,
"the fixture holds no deltas, so this comparison is vacuous"
);
}
// The claim this whole change rests on: only the *header* differs. Read it
// back out of both packs with gix's own decoder and compare the objects —
// not the headers, which are supposed to differ, and not gunnar's own tally,
// which is what produced them.
let mut both = Vec::new();
for ofs_delta in [true, false] {
let options = WriteOptions::new(Kind::Sha1).ofs_delta(ofs_delta);
let mut bytes = Vec::new();
write_pack_copy(&ids, &packed, &mut bytes, &options)?;
let dir = tempfile::tempdir()?;
let pack = dir.path().join("p.pack");
std::fs::write(&pack, &bytes)?;
git(
&["index-pack", "--strict", &pack.to_string_lossy()],
dir.path(),
);
// `cat-file --batch` through stock git, so the comparison is of what a
// real client would end up holding.
let odb = dir.path().join("odb");
std::fs::create_dir_all(&odb)?;
git(&["init", "-q", "--bare", "--object-format=sha1", "."], &odb);
std::fs::copy(&pack, odb.join("objects/pack/p.pack"))?;
std::fs::copy(pack.with_extension("idx"), odb.join("objects/pack/p.idx"))?;
let mut digest: Vec<String> = Vec::with_capacity(ids.len());
for id in &ids {
let hex = id.to_string();
let kind = git(&["cat-file", "-t", &hex], &odb).trim().to_string();
let size = git(&["cat-file", "-s", &hex], &odb).trim().to_string();
digest.push(format!("{hex} {kind} {size}"));
}
both.push(digest);
// `dir` must outlive the reads above.
drop(dir);
}
assert_eq!(
both[0], both[1],
"the two packs decode to different objects, so the delta bytes were not the same"
);
Ok(())
}