use std::path::{Path, PathBuf};
use std::process::Command;
use ldeflate::{Config, compress_objects};
use sha1::{Digest, Sha1};
fn have_git() -> bool {
Command::new("git")
.arg("--version")
.output()
.map(|o| o.status.success())
.unwrap_or(false)
}
fn scratch(name: &str) -> PathBuf {
let base = std::env::var("LDEFLATE_TEST_SCRATCH")
.unwrap_or_else(|_| "/home/rickard/scratch/cargo/zoomies-deflate/gittest".to_string());
let dir = PathBuf::from(base).join(format!("{name}-{}", std::process::id()));
let _ = std::fs::remove_dir_all(&dir);
std::fs::create_dir_all(&dir).expect("create scratch dir");
dir
}
fn git(dir: &Path, args: &[&str]) -> (bool, String, String) {
let o = Command::new("git")
.current_dir(dir)
.args(args)
.output()
.unwrap_or_else(|e| panic!("git {args:?}: {e}"));
(
o.status.success(),
String::from_utf8_lossy(&o.stdout).into_owned(),
String::from_utf8_lossy(&o.stderr).into_owned(),
)
}
fn corpus() -> Vec<Vec<u8>> {
let mut out = Vec::new();
for i in 0..400usize {
let mut v = Vec::new();
let tag = format!("blob-{i} ");
while v.len() < 40 + (i * 13) % 900 {
v.extend_from_slice(tag.as_bytes());
v.extend_from_slice(b"fn main() { let x = 1; }\n");
}
out.push(v);
}
for i in 0..8usize {
let mut v = Vec::new();
let tag = format!("mid-{i} ");
while v.len() < 300_000 {
v.extend_from_slice(tag.as_bytes());
v.extend_from_slice(b"use std::collections::HashMap; // padding padding\n");
}
out.push(v);
}
for i in 0..2usize {
let mut v = Vec::new();
let tag = format!("big-{i} ");
while v.len() < 12 << 20 {
v.extend_from_slice(tag.as_bytes());
v.extend_from_slice(b"the quick brown fox jumps over the lazy dog 0123456789\n");
}
out.push(v);
}
let mut noise = Vec::with_capacity(4 << 20);
let mut s: u64 = 0x243F6A8885A308D3;
while noise.len() < 4 << 20 {
s ^= s << 13;
s ^= s >> 7;
s ^= s << 17;
noise.extend_from_slice(&s.to_le_bytes());
}
out.push(noise);
out.push(Vec::new());
out
}
fn blob_oid(content: &[u8]) -> [u8; 20] {
let mut h = Sha1::new();
h.update(format!("blob {}\0", content.len()).as_bytes());
h.update(content);
h.finalize().into()
}
fn hex(b: &[u8]) -> String {
b.iter().map(|x| format!("{x:02x}")).collect()
}
#[test]
fn stock_git_reads_a_loose_object_store_we_wrote() {
if !have_git() {
eprintln!("SKIP stock_git loose-object arm: `git` not on PATH");
return;
}
let dir = scratch("loose");
let (ok, _, err) = git(&dir, &["init", "--quiet", "."]);
assert!(ok, "git init failed: {err}");
let data = corpus();
let bodies: Vec<Vec<u8>> = data
.iter()
.map(|c| {
let mut b = format!("blob {}\0", c.len()).into_bytes();
b.extend_from_slice(c);
b
})
.collect();
let refs: Vec<&[u8]> = bodies.iter().map(|b| b.as_slice()).collect();
let streams = compress_objects(&refs, &Config::default()).expect("compress");
assert_eq!(streams.len(), data.len());
let mut oids = Vec::new();
for (content, z) in data.iter().zip(&streams) {
let oid = hex(&blob_oid(content));
let sub = dir.join(".git/objects").join(&oid[..2]);
std::fs::create_dir_all(&sub).unwrap();
std::fs::write(sub.join(&oid[2..]), z).unwrap();
oids.push(oid);
}
let (ok, out, err) = git(&dir, &["fsck", "--full", "--strict", "--no-progress"]);
assert!(
ok,
"git fsck REJECTED an object store written by ldeflate.\nstdout:\n{out}\nstderr:\n{err}"
);
let mut dangling = 0usize;
for line in out.lines().chain(err.lines()) {
let l = line.trim();
if l.is_empty() || l.starts_with("Checking") || l.starts_with("notice:") {
continue;
}
if l.starts_with("dangling ") {
dangling += 1;
continue;
}
panic!("git fsck complained: {l}");
}
assert_eq!(
dangling,
data.len(),
"git fsck reported {dangling} dangling objects but we wrote {} — fsck did \
not actually visit the store, so its clean verdict means nothing",
data.len()
);
for (i, (oid, content)) in oids.iter().zip(&data).enumerate() {
let o = Command::new("git")
.current_dir(&dir)
.args(["cat-file", "blob", oid])
.output()
.unwrap();
assert!(
o.status.success(),
"git cat-file failed for object {i} ({oid})"
);
assert_eq!(
o.stdout.len(),
content.len(),
"object {i} ({oid}, {} B): git read back a different length",
content.len()
);
assert_eq!(
&o.stdout, content,
"object {i} ({oid}): git read back different bytes"
);
}
let _ = std::fs::remove_dir_all(&dir);
}
#[test]
fn stock_git_index_pack_accepts_a_pack_we_wrote() {
if !have_git() {
eprintln!("SKIP stock_git pack arm: `git` not on PATH");
return;
}
let dir = scratch("pack");
let (ok, _, err) = git(&dir, &["init", "--quiet", "."]);
assert!(ok, "git init failed: {err}");
let data = corpus();
let refs: Vec<&[u8]> = data.iter().map(|b| b.as_slice()).collect();
let streams = compress_objects(&refs, &Config::default()).expect("compress");
let mut pack: Vec<u8> = Vec::new();
pack.extend_from_slice(b"PACK");
pack.extend_from_slice(&2u32.to_be_bytes());
pack.extend_from_slice(&(data.len() as u32).to_be_bytes());
for (content, z) in data.iter().zip(&streams) {
pack.extend_from_slice(&pack_entry_header(3 , content.len()));
pack.extend_from_slice(z);
}
let mut h = Sha1::new();
h.update(&pack);
let trailer: [u8; 20] = h.finalize().into();
pack.extend_from_slice(&trailer);
let pack_path = dir.join("ours.pack");
std::fs::write(&pack_path, &pack).unwrap();
let o = Command::new("git")
.current_dir(&dir)
.args([
"index-pack",
"--strict",
"-o",
"ours.idx",
pack_path.to_str().unwrap(),
])
.output()
.unwrap();
assert!(
o.status.success(),
"git index-pack --strict REJECTED a pack written by ldeflate.\nstdout: {}\nstderr: {}",
String::from_utf8_lossy(&o.stdout),
String::from_utf8_lossy(&o.stderr)
);
let (ok, _, err) = git(&dir, &["verify-pack", "-v", "ours.idx"]);
assert!(ok, "git verify-pack failed on our pack: {err}");
let _ = std::fs::remove_dir_all(&dir);
}
#[test]
fn index_pack_actually_rejects_a_corrupted_stream() {
if !have_git() {
eprintln!("SKIP index-pack negative control: `git` not on PATH");
return;
}
let dir = scratch("packneg");
let (ok, _, err) = git(&dir, &["init", "--quiet", "."]);
assert!(ok, "git init failed: {err}");
let content = b"the quick brown fox jumps over the lazy dog".repeat(500);
let refs: Vec<&[u8]> = vec![&content];
let mut streams = compress_objects(&refs, &Config::default()).unwrap();
let mid = streams[0].len() / 2;
streams[0][mid] ^= 0x5a;
let mut pack: Vec<u8> = 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(&pack_entry_header(3, content.len()));
pack.extend_from_slice(&streams[0]);
let mut h = Sha1::new();
h.update(&pack);
let trailer: [u8; 20] = h.finalize().into();
pack.extend_from_slice(&trailer);
let pack_path = dir.join("bad.pack");
std::fs::write(&pack_path, &pack).unwrap();
let o = Command::new("git")
.current_dir(&dir)
.args([
"index-pack",
"--strict",
"-o",
"bad.idx",
pack_path.to_str().unwrap(),
])
.output()
.unwrap();
assert!(
!o.status.success(),
"git index-pack ACCEPTED a pack with a corrupted zlib payload — the \
acceptance test above is not actually validating anything"
);
let _ = std::fs::remove_dir_all(&dir);
}
fn pack_entry_header(obj_type: u8, mut size: usize) -> Vec<u8> {
let mut out = Vec::with_capacity(8);
let mut byte = ((obj_type & 0x7) << 4) | (size as u8 & 0x0f);
size >>= 4;
while size > 0 {
out.push(byte | 0x80);
byte = (size as u8) & 0x7f;
size >>= 7;
}
out.push(byte);
out
}
#[test]
fn pack_entry_header_known_values() {
assert_eq!(pack_entry_header(3, 0), vec![0x30]);
assert_eq!(pack_entry_header(3, 15), vec![0x3f]);
assert_eq!(pack_entry_header(3, 16), vec![0xb0, 0x01]);
assert_eq!(pack_entry_header(3, 2048), vec![0xb0, 0x80, 0x01]);
assert_eq!(pack_entry_header(1, 1), vec![0x11]);
}
#[test]
fn fsck_actually_rejects_a_corrupted_loose_object() {
if !have_git() {
eprintln!("SKIP fsck negative control: `git` not on PATH");
return;
}
let dir = scratch("looseneg");
let (ok, _, err) = git(&dir, &["init", "--quiet", "."]);
assert!(ok, "git init failed: {err}");
let content = b"the quick brown fox jumps over the lazy dog\n".repeat(400);
let mut body = format!("blob {}\0", content.len()).into_bytes();
body.extend_from_slice(&content);
let refs: Vec<&[u8]> = vec![&body];
let mut z = compress_objects(&refs, &Config::default())
.unwrap()
.pop()
.unwrap();
let mid = z.len() / 2;
z[mid] ^= 0x5a;
let oid = hex(&blob_oid(&content));
let sub = dir.join(".git/objects").join(&oid[..2]);
std::fs::create_dir_all(&sub).unwrap();
std::fs::write(sub.join(&oid[2..]), &z).unwrap();
let (ok, out, err) = git(&dir, &["fsck", "--full", "--strict", "--no-progress"]);
assert!(
!ok || out.contains("error") || err.contains("error") || err.contains("corrupt"),
"git fsck ACCEPTED a loose object with a corrupted zlib payload — the \
acceptance test above proves nothing.\nstdout:{out}\nstderr:{err}"
);
let _ = std::fs::remove_dir_all(&dir);
}