use std::path::{Path, PathBuf};
use std::process::{Command, Output};
use anyhow::{anyhow, bail, Context, Result};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Strictness {
SelfContained,
Connected,
}
impl Strictness {
fn args(self) -> &'static [&'static str] {
match self {
Strictness::SelfContained => &["index-pack"],
Strictness::Connected => &[
"index-pack",
"--strict",
"--check-self-contained-and-connected",
],
}
}
}
pub fn empty_bare_repo(scratch: &Path, name: &str) -> Result<PathBuf> {
let repo = scratch.join(name);
let out = Command::new("git")
.args(["init", "-q", "--bare"])
.arg(&repo)
.output()
.context("running `git init --bare` for the oracle's repository")?;
verdict(&out, "git init --bare")?;
Ok(repo)
}
pub fn git_accepts(scratch: &Path, name: &str, pack: &[u8], how: Strictness) -> Result<()> {
let repo = empty_bare_repo(scratch, name)?;
let path = repo.join("oracle.pack");
std::fs::write(&path, pack)
.with_context(|| format!("writing the pack under test to {}", path.display()))?;
let out = Command::new("git")
.args(how.args())
.arg(&path)
.current_dir(&repo)
.output()
.context("running `git index-pack` as the oracle")?;
verdict(&out, "git index-pack")
.with_context(|| format!("{how:?}, in the repository at {}", repo.display()))
}
#[track_caller]
pub fn assert_git_accepts(scratch: &Path, name: &str, pack: &[u8], how: Strictness) {
if let Err(e) = git_accepts(scratch, name, pack, how) {
panic!("stock git refused a pack this crate emitted: {e:#}");
}
}
pub fn git_reads_back(scratch: &Path, name: &str, pack: &[u8]) -> Result<Vec<(String, Vec<u8>)>> {
use std::io::Write as _;
let repo = empty_bare_repo(scratch, name)?;
let mut child = Command::new("git")
.args(["index-pack", "--stdin"])
.current_dir(&repo)
.stdin(std::process::Stdio::piped())
.stdout(std::process::Stdio::piped())
.stderr(std::process::Stdio::piped())
.spawn()
.context("spawning `git index-pack --stdin` to adopt the pack")?;
child
.stdin
.take()
.ok_or_else(|| anyhow!("git index-pack --stdin has no stdin"))?
.write_all(pack)
.context("streaming the pack to `git index-pack --stdin`")?;
let out = child
.wait_with_output()
.context("waiting for `git index-pack --stdin`")?;
verdict(&out, "git index-pack --stdin")?;
let listed = Command::new("git")
.args(["cat-file", "--batch-all-objects", "--batch"])
.current_dir(&repo)
.output()
.context("running `git cat-file --batch-all-objects --batch`")?;
verdict(&listed, "git cat-file --batch")?;
let buf = listed.stdout;
let mut at = 0usize;
let mut objects = Vec::new();
while at < buf.len() {
let nl = buf[at..]
.iter()
.position(|b| *b == b'\n')
.ok_or_else(|| anyhow!("a cat-file record has no header terminator"))?
+ at;
let header = std::str::from_utf8(&buf[at..nl]).context("a cat-file header is not utf-8")?;
let mut f = header.split_whitespace();
let oid = f
.next()
.ok_or_else(|| anyhow!("a cat-file header has no oid: {header:?}"))?
.to_owned();
let size: usize = f
.nth(1)
.ok_or_else(|| anyhow!("a cat-file header has no size: {header:?}"))?
.parse()
.with_context(|| format!("a cat-file size is not a number: {header:?}"))?;
let from = nl + 1;
let to = from
.checked_add(size)
.filter(|to| *to <= buf.len())
.ok_or_else(|| anyhow!("a cat-file body of {size} bytes runs off the output"))?;
objects.push((oid, buf[from..to].to_vec()));
at = to + 1;
}
Ok(objects)
}
fn verdict(out: &Output, what: &str) -> Result<()> {
#[cfg(unix)]
{
use std::os::unix::process::ExitStatusExt as _;
if let Some(signal) = out.status.signal() {
bail!(
"`{what}` DIED ON SIGNAL {signal} instead of judging the pack. That is the \
oracle crashing, not the pack failing, and the two must never be confused: \
outside a repository git 2.53.0 segfaults on any pack `--strict` would have \
rejected, so a green here would have meant nothing. Do NOT make this go away \
by shrinking the input.\nstderr: {}",
String::from_utf8_lossy(&out.stderr).trim()
);
}
}
if !out.status.success() {
return Err(anyhow!(
"`{what}` exited {:?}\nstderr: {}\nstdout: {}",
out.status.code(),
String::from_utf8_lossy(&out.stderr).trim(),
String::from_utf8_lossy(&out.stdout).trim()
));
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use crate::object::{canonical, GitHashKind, GitObjectKind};
use crate::pack_walk::{emit_pack, EmitEntry};
use crate::store::tests::tmpdir;
fn whole_pack(bodies: &[(GitObjectKind, Vec<u8>)]) -> Vec<u8> {
use std::io::Write as _;
let hash = GitHashKind::Sha1;
let entries: Vec<EmitEntry> = bodies
.iter()
.enumerate()
.map(|(i, (kind, body))| {
let t = crate::serve::resolved_type(*kind);
let mut stored = Vec::new();
crate::pack_walk::encode_type_and_size(&mut stored, t, body.len() as u64);
let mut z =
flate2::write::ZlibEncoder::new(Vec::new(), flate2::Compression::fast());
z.write_all(body).unwrap();
stored.extend_from_slice(&z.finish().unwrap());
EmitEntry {
oid: hash.oid_of(&canonical(*kind, body)),
stored: crate::pack_walk::EntryBytes::Owned(stored),
obj_type: t,
uncompressed_size: body.len() as u64,
delta_base: 0,
offset: i as u64 + 1,
recompressed: false,
deltified: false,
}
})
.collect();
let mut out = Vec::new();
emit_pack(&entries, hash, &mut out, &|i| {
crate::pack_walk::resolve_against(&entries[i], &[])
})
.expect("emitting the fixture pack");
out
}
#[test]
fn the_oracle_rejects_a_pack_whose_tree_owes_a_child_and_does_not_crash_doing_it() {
let hash = GitHashKind::Sha1;
let orphan = b"a blob that is deliberately left out of the pack\n".to_vec();
let orphan_oid = hash.oid_of(&canonical(GitObjectKind::Blob, &orphan));
let mut tree = b"100644 f.txt\0".to_vec();
tree.extend_from_slice(&orphan_oid);
let scratch = tmpdir("oracle-red");
let pack = whole_pack(&[(GitObjectKind::Tree, tree.clone())]);
let err = git_accepts(&scratch, "red.git", &pack, Strictness::Connected)
.expect_err("a tree whose child is absent must be refused");
let msg = format!("{err:#}");
assert!(
msg.contains(&hex::encode(&orphan_oid)),
"the refusal must name the object the pack owes; got: {msg}"
);
assert!(
!msg.contains("DIED ON SIGNAL"),
"the oracle crashed instead of judging β it is not running inside a repository: \
{msg}"
);
git_accepts(&scratch, "green.git", &pack, Strictness::SelfContained)
.expect("a self-containment check has nothing to complain about here");
let bare_dir = scratch.join("not-a-repo");
std::fs::create_dir_all(&bare_dir).unwrap();
let path = bare_dir.join("oracle.pack");
std::fs::write(&path, &pack).unwrap();
let out = Command::new("git")
.args(Strictness::Connected.args())
.arg(&path)
.current_dir(&bare_dir)
.output()
.expect("running git index-pack outside a repository");
let outside = verdict(&out, "git index-pack")
.expect_err("outside a repository this cannot possibly succeed");
assert!(
format!("{outside:#}").contains("DIED ON SIGNAL"),
"git stopped segfaulting outside a repository β this module's premise has changed \
and its docs must be re-measured, not deleted. It said: {outside:#}"
);
}
}