use std::collections::HashMap;
use anyhow::{Result, anyhow, bail};
use crate::exploded::{NoSink, PayloadSink};
use crate::index_layout::{IndexEntry, ObjType};
use crate::object::{GitHashKind, GitObjectKind, canonical};
use crate::pack_walk::{DeltaBase, PackEntry, PackWalk, walk};
pub trait BaseSource {
fn content(&self, oid: &[u8]) -> Option<(GitObjectKind, Vec<u8>)>;
}
pub struct NoBases;
impl BaseSource for NoBases {
fn content(&self, _oid: &[u8]) -> Option<(GitObjectKind, Vec<u8>)> {
None
}
}
#[derive(Debug, Clone)]
pub struct Resolved {
pub oid: Vec<u8>,
pub offset: u64,
pub len: u64,
pub stored_type: ObjType,
pub kind: GitObjectKind,
pub uncompressed_size: u64,
pub delta_base: u64,
}
impl Resolved {
pub fn index_entry(&self) -> IndexEntry {
IndexEntry {
oid: self.oid.clone(),
offset: self.offset,
len: self.len,
obj_type: self.stored_type,
uncompressed_size: self.uncompressed_size,
delta_base: self.delta_base,
}
}
}
#[derive(Debug)]
pub struct ResolveError;
impl ResolveError {
pub fn missing_base(oid: &[u8]) -> anyhow::Error {
anyhow!(
"this pack is thin: it deltas against {} which is not in it, and no base source could \
produce that object's content — §14's exploded table does not hold it and it could \
not be re-derived from the verbatim packs either, so this repository genuinely does \
not have that object. The pack is refused by name rather than half-resolved",
hex::encode(oid)
)
}
}
pub fn resolve(
pack: &[u8],
hash: GitHashKind,
archive_offset: u64,
bases: &dyn BaseSource,
) -> Result<Vec<Resolved>> {
let w = walk(pack, hash.oid_len())?;
resolve_walked(pack, &w, hash, archive_offset, bases, &NoSink)
}
pub fn resolve_walked(
pack: &[u8],
w: &PackWalk,
hash: GitHashKind,
archive_offset: u64,
bases: &dyn BaseSource,
sink: &dyn PayloadSink,
) -> Result<Vec<Resolved>> {
let mut ofs_uses: HashMap<u64, usize> = HashMap::new();
let mut ref_uses: HashMap<&[u8], usize> = HashMap::new();
for e in &w.entries {
match &e.delta_base {
DeltaBase::Offset(o) => *ofs_uses.entry(*o).or_default() += 1,
DeltaBase::Ref(oid) => *ref_uses.entry(oid.as_slice()).or_default() += 1,
DeltaBase::None => {}
}
}
let mut out: Vec<Option<Resolved>> = vec![None; w.entries.len()];
let mut content: HashMap<u64, (GitObjectKind, Vec<u8>, usize)> = HashMap::new();
let mut oid_to_offset: HashMap<Vec<u8>, u64> = HashMap::new();
let mut remaining: Vec<usize> = (0..w.entries.len()).collect();
while !remaining.is_empty() {
let mut progressed = false;
let mut stuck: Vec<usize> = Vec::new();
for &i in &remaining {
let e = &w.entries[i];
let base: Option<(GitObjectKind, Vec<u8>)> = match &e.delta_base {
DeltaBase::None => None,
DeltaBase::Offset(o) => match consume(&mut content, *o) {
Some(c) => Some(c),
None => {
stuck.push(i);
continue;
}
},
DeltaBase::Ref(oid) => match oid_to_offset
.get(oid.as_slice())
.copied()
.and_then(|o| consume(&mut content, o))
{
Some(c) => Some(c),
None => match bases.content(oid) {
Some(c) => Some(c),
None => {
stuck.push(i);
continue;
}
},
},
};
let (kind, payload) = match base {
None => {
let kind = whole_kind(e.obj_type)?;
(kind, inflate(pack, e, hash.oid_len())?)
}
Some((base_kind, base_payload)) => {
let delta = inflate(pack, e, hash.oid_len())?;
(base_kind, apply_delta(&base_payload, &delta)?)
}
};
let oid = hash.oid_of(&canonical(kind, &payload));
sink.explode(&oid, kind, &payload)?;
let size = payload.len() as u64;
let by_ref = ref_uses.get(oid.as_slice()).copied().unwrap_or(0);
let uses = ofs_uses.get(&e.offset).copied().unwrap_or(0) + by_ref;
if uses > 0 {
if by_ref > 0 {
oid_to_offset.insert(oid.clone(), e.offset);
}
content.insert(e.offset, (kind, payload, uses));
}
out[i] = Some(Resolved {
oid,
offset: e.offset + archive_offset,
len: e.len,
stored_type: e.obj_type,
kind,
uncompressed_size: size,
delta_base: match &e.delta_base {
DeltaBase::Offset(o) => o + archive_offset,
_ => 0,
},
});
progressed = true;
}
if !progressed {
let i = stuck[0];
if let DeltaBase::Ref(oid) = &w.entries[i].delta_base {
return Err(ResolveError::missing_base(oid));
}
bail!(
"entry at offset {} deltas against offset {} which no entry starts at — the pack \
is corrupt",
w.entries[i].offset,
w.entries[i].delta_base.as_offset()
);
}
remaining = stuck;
}
Ok(out
.into_iter()
.map(|r| r.expect("the fixpoint only exits when every slot is filled"))
.collect())
}
fn consume(
content: &mut HashMap<u64, (GitObjectKind, Vec<u8>, usize)>,
at: u64,
) -> Option<(GitObjectKind, Vec<u8>)> {
let (kind, payload, left) = content.get_mut(&at)?;
*left -= 1;
if *left > 0 {
return Some((*kind, payload.clone()));
}
let (kind, payload, _) = content.remove(&at).expect("just borrowed it");
Some((kind, payload))
}
fn whole_kind(t: ObjType) -> Result<GitObjectKind> {
Ok(match t {
ObjType::Commit => GitObjectKind::Commit,
ObjType::Tree => GitObjectKind::Tree,
ObjType::Blob => GitObjectKind::Blob,
ObjType::Tag => GitObjectKind::Tag,
ObjType::OfsDelta | ObjType::RefDelta => {
bail!("a delta entry has no type of its own — its base's type is the answer")
}
})
}
fn inflate(pack: &[u8], e: &PackEntry, oid_len: usize) -> Result<Vec<u8>> {
let start = e.offset as usize;
let end = start + e.len as usize;
if end > pack.len() {
bail!("entry at {start} runs past the end of the pack");
}
let header = header_len(&pack[start..end], e, oid_len)?;
let mut out = Vec::with_capacity(e.uncompressed_size as usize);
let mut d = flate2::Decompress::new(true);
d.decompress_vec(
&pack[start + header..end],
&mut out,
flate2::FlushDecompress::Finish,
)
.map_err(|err| anyhow!("inflating the entry at {start}: {err}"))?;
if out.len() as u64 != e.uncompressed_size {
bail!(
"the entry at {start} inflated to {} bytes, the walk measured {}",
out.len(),
e.uncompressed_size
);
}
Ok(out)
}
fn header_len(entry: &[u8], e: &PackEntry, oid_len: usize) -> Result<usize> {
let mut i = 0usize;
let mut cont = true;
while cont {
let b = *entry
.get(i)
.ok_or_else(|| anyhow!("the type/size header runs off the entry"))?;
cont = b & 0x80 != 0;
i += 1;
}
match e.obj_type {
ObjType::OfsDelta => {
let mut cont = true;
while cont {
let b = *entry
.get(i)
.ok_or_else(|| anyhow!("the ofs-delta distance runs off the entry"))?;
cont = b & 0x80 != 0;
i += 1;
}
}
ObjType::RefDelta => i += oid_len,
_ => {}
}
if i >= entry.len() {
bail!("the entry's header consumes all of it, leaving no stream");
}
Ok(i)
}
fn apply_delta(base: &[u8], delta: &[u8]) -> Result<Vec<u8>> {
let mut i = 0usize;
let base_size = delta_varint(delta, &mut i)?;
if base_size != base.len() as u64 {
bail!(
"the delta expects a base of {base_size} bytes, its base is {}",
base.len()
);
}
let target_size = delta_varint(delta, &mut i)?;
let mut out = Vec::with_capacity(target_size as usize);
while i < delta.len() {
let op = delta[i];
i += 1;
if op & 0x80 != 0 {
let mut off = 0u64;
for bit in 0..4 {
if op & (1 << bit) != 0 {
off |= u64::from(*delta.get(i).ok_or_else(|| anyhow!("delta ends mid-copy"))?)
<< (bit * 8);
i += 1;
}
}
let mut size = 0u64;
for bit in 0..3 {
if op & (0x10 << bit) != 0 {
size |= u64::from(*delta.get(i).ok_or_else(|| anyhow!("delta ends mid-copy"))?)
<< (bit * 8);
i += 1;
}
}
if size == 0 {
size = 0x1_0000;
}
let from = off as usize;
let to = from
.checked_add(size as usize)
.ok_or_else(|| anyhow!("a delta copy range overflows"))?;
if to > base.len() {
bail!(
"a delta copies base[{from}..{to}] out of a {}-byte base",
base.len()
);
}
out.extend_from_slice(&base[from..to]);
} else {
let n = op as usize;
if n == 0 {
bail!("a delta carries a zero-length insert instruction");
}
let end = i
.checked_add(n)
.ok_or_else(|| anyhow!("a delta insert overflows"))?;
if end > delta.len() {
bail!("a delta insert of {n} bytes runs off the end");
}
out.extend_from_slice(&delta[i..end]);
i = end;
}
}
if out.len() as u64 != target_size {
bail!(
"the delta declares a {target_size}-byte result and produced {}",
out.len()
);
}
Ok(out)
}
fn delta_varint(b: &[u8], i: &mut usize) -> Result<u64> {
let mut v = 0u64;
let mut shift = 0u32;
loop {
let byte = *b
.get(*i)
.ok_or_else(|| anyhow!("a delta size varint runs off the end"))?;
*i += 1;
if shift >= 64 {
bail!("a delta size varint is longer than a u64");
}
v |= u64::from(byte & 0x7f) << shift;
shift += 7;
if byte & 0x80 == 0 {
return Ok(v);
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::path::{Path, PathBuf};
fn read_idx(bytes: &[u8]) -> Vec<(Vec<u8>, u64)> {
assert_eq!(&bytes[0..4], b"\xfftOc", "not an idx v2");
assert_eq!(u32::from_be_bytes(bytes[4..8].try_into().unwrap()), 2);
let fanout_end = 8 + 256 * 4;
let n = u32::from_be_bytes(bytes[fanout_end - 4..fanout_end].try_into().unwrap()) as usize;
let oids = fanout_end;
let crcs = oids + n * 20;
let offs = crcs + n * 4;
let big = offs + n * 4;
let mut out = Vec::with_capacity(n);
for i in 0..n {
let oid = bytes[oids + i * 20..oids + i * 20 + 20].to_vec();
let raw = u32::from_be_bytes(bytes[offs + i * 4..offs + i * 4 + 4].try_into().unwrap());
let offset = if raw & 0x8000_0000 != 0 {
let j = (raw & 0x7fff_ffff) as usize;
u64::from_be_bytes(bytes[big + j * 8..big + j * 8 + 8].try_into().unwrap())
} else {
u64::from(raw)
};
out.push((oid, offset));
}
out
}
fn real_pairs(cap: usize) -> Vec<(PathBuf, PathBuf)> {
let mut out = Vec::new();
let Ok(repos) = std::fs::read_dir(Path::new("/home/rickard/git")) else {
return out;
};
for repo in repos.flatten() {
let dir = repo.path().join(".git/objects/pack");
let Ok(files) = std::fs::read_dir(&dir) else {
continue;
};
for f in files.flatten() {
let p = f.path();
if p.extension().is_some_and(|e| e == "pack")
&& f.metadata().map(|m| m.len() < 32 << 20).unwrap_or(false)
{
let idx = p.with_extension("idx");
if idx.exists() {
out.push((p, idx));
if out.len() >= cap {
return out;
}
}
}
}
}
out
}
#[test]
fn every_oid_we_compute_is_the_oid_git_wrote_in_its_idx() {
let pairs = real_pairs(6);
assert!(
!pairs.is_empty(),
"no real pack found under /home/rickard/git — this guard has nothing to compare and \
must not pass silently"
);
let mut objects = 0usize;
let mut full = 0usize;
let mut thin = 0usize;
for (pack_path, idx_path) in pairs {
let pack = std::fs::read(&pack_path).unwrap();
let idx = read_idx(&std::fs::read(&idx_path).unwrap());
let ours = match resolve(&pack, GitHashKind::Sha1, 0, &NoBases) {
Ok(r) => r,
Err(e) => {
assert!(
e.to_string().contains("thin"),
"{}: {e}",
pack_path.display()
);
eprintln!("{}: THIN, external base — skipped", pack_path.display());
thin += 1;
continue;
}
};
full += 1;
assert_eq!(
ours.len(),
idx.len(),
"{}: we resolved {} objects, git indexed {}",
pack_path.display(),
ours.len(),
idx.len()
);
let theirs: HashMap<Vec<u8>, u64> = idx.into_iter().collect();
for r in &ours {
match theirs.get(&r.oid) {
Some(&off) => assert_eq!(
off,
r.offset,
"{}: {} is at {} in git's idx and we put it at {}",
pack_path.display(),
hex::encode(&r.oid),
off,
r.offset
),
None => panic!(
"{}: we computed {} at offset {}, which git's idx does not contain — the \
resolution is wrong",
pack_path.display(),
hex::encode(&r.oid),
r.offset
),
}
}
eprintln!("{}: {} objects agree with git", pack_path.display(), ours.len());
objects += ours.len();
}
eprintln!("{objects} objects over {full} packs agree with git's own .idx; {thin} thin");
assert!(full > 0, "every pack on this machine was thin — nothing was compared");
assert!(objects > 1000, "only {objects} objects compared");
}
#[test]
fn a_delta_carries_its_stored_type_and_its_resolved_type_and_size() {
let (pack_path, _) = real_pairs(1).pop().expect("a real pack");
let pack = std::fs::read(&pack_path).unwrap();
let ours = resolve(&pack, GitHashKind::Sha1, 0, &NoBases).unwrap();
let stream_size: HashMap<u64, u64> = walk(&pack, 20)
.unwrap()
.entries
.iter()
.map(|e| (e.offset, e.uncompressed_size))
.collect();
let deltas: Vec<&Resolved> = ours
.iter()
.filter(|r| matches!(r.stored_type, ObjType::OfsDelta | ObjType::RefDelta))
.collect();
assert!(
!deltas.is_empty(),
"{} carries no deltas, so it proves nothing about resolution",
pack_path.display()
);
let by_offset: HashMap<u64, &Resolved> = ours.iter().map(|r| (r.offset, r)).collect();
let mut non_blob = 0usize;
for d in &deltas {
assert!(
matches!(
d.kind,
GitObjectKind::Blob
| GitObjectKind::Tree
| GitObjectKind::Commit
| GitObjectKind::Tag
),
"a resolved type must be a real git type"
);
if d.kind != GitObjectKind::Blob {
non_blob += 1;
}
if d.delta_base != 0 {
let base = by_offset[&d.delta_base];
assert_eq!(
d.kind,
base.kind,
"a delta resolves to its base's type: {} vs base {}",
hex::encode(&d.oid),
hex::encode(&base.oid)
);
assert!(
base.offset < d.offset,
"an ofs-delta base is always earlier in the pack"
);
}
}
assert!(
non_blob > 0,
"{} deltas and not one resolved to a tree/commit/tag — the resolved type is not being \
taken from the base",
deltas.len()
);
let differing = deltas
.iter()
.filter(|d| stream_size[&d.offset] != d.uncompressed_size)
.count();
assert!(
differing > 0,
"not one delta's resolved size differs from its delta-stream size, which cannot be \
true of a real pack — the resolved size is being copied from the header"
);
eprintln!(
"{}: {} deltas, {non_blob} non-blob, {differing} with a resolved size unlike the \
stream size",
pack_path.display(),
deltas.len()
);
}
#[test]
fn a_thin_pack_is_refused_by_name_and_says_what_is_missing() {
let base_oid = vec![0x5a; 20];
let mut pack = b"PACK".to_vec();
pack.extend_from_slice(&2u32.to_be_bytes());
pack.extend_from_slice(&1u32.to_be_bytes());
let delta = vec![0x04, 0x04, 0x90, 0x04];
pack.push(0x70 | (delta.len() as u8 & 0x0f)); pack.extend_from_slice(&base_oid);
pack.extend_from_slice(&{
use std::io::Write;
let mut e = flate2::write::ZlibEncoder::new(Vec::new(), flate2::Compression::default());
e.write_all(&delta).unwrap();
e.finish().unwrap()
});
pack.extend_from_slice(&[0u8; 20]);
let err = resolve(&pack, GitHashKind::Sha1, 0, &NoBases)
.expect_err("a thin pack with no base source cannot resolve");
let msg = err.to_string();
assert!(msg.contains(&hex::encode(&base_oid)), "names the base: {msg}");
assert!(msg.contains("§14"), "says which decision is missing: {msg}");
struct One(Vec<u8>);
impl BaseSource for One {
fn content(&self, oid: &[u8]) -> Option<(GitObjectKind, Vec<u8>)> {
(oid == self.0.as_slice()).then(|| (GitObjectKind::Blob, b"abcd".to_vec()))
}
}
let ok = resolve(&pack, GitHashKind::Sha1, 0, &One(base_oid)).expect("with a base it works");
assert_eq!(ok.len(), 1);
assert_eq!(ok[0].kind, GitObjectKind::Blob);
assert_eq!(ok[0].uncompressed_size, 4);
assert_eq!(
ok[0].oid,
GitHashKind::Sha1.oid_of(&canonical(GitObjectKind::Blob, b"abcd")),
"the resolved object is the base copied whole, so it hashes as `abcd`"
);
}
#[test]
fn the_delta_base_column_survives_all_three_arms_from_a_real_pack() {
use crate::index_layout::{
FourTables, IndexEntry, ObjType, ObjectIndex, OneTableFourColumns, PackedPayload,
};
use crate::read_stack::{ObjectReadStack, RebuildTriggers};
let (pack_path, _) = real_pairs(1).pop().expect("a real pack");
let pack = std::fs::read(&pack_path).unwrap();
const AT: u64 = 4096;
let rows = resolve(&pack, GitHashKind::Sha1, AT, &NoBases).unwrap();
let entries: Vec<IndexEntry> = rows.iter().map(|r| r.index_entry()).collect();
let a = FourTables::build(&entries).unwrap();
let b = OneTableFourColumns::build(&entries).unwrap();
let c = PackedPayload::build(&entries).unwrap();
let stack = ObjectReadStack::<OneTableFourColumns>::in_memory(RebuildTriggers::manual())
.unwrap();
stack.append(&entries).unwrap();
stack.rebuild().unwrap();
let extents: HashMap<u64, u64> = entries
.iter()
.map(|e| {
let r = a.lookup(&e.oid).expect("a stored oid resolves");
(r.offset, r.len)
})
.collect();
assert_eq!(extents.len(), entries.len(), "two objects share an offset");
let named: [(&str, &dyn ObjectIndex); 4] =
[("FourTables", &a), ("OneTableFourColumns", &b), ("PackedPayload", &c), ("ObjectReadStack", &stack)];
let mut ofs_deltas = 0usize;
for (name, idx) in named {
let mut with_base = 0usize;
for e in &entries {
let row = idx.lookup(&e.oid).expect("a stored oid resolves");
assert_eq!(
row,
a.lookup(&e.oid).unwrap(),
"{name} disagrees with FourTables on {}",
hex::encode(&e.oid)
);
if row.obj_type != ObjType::OfsDelta {
continue;
}
with_base += 1;
assert_ne!(
row.delta_base, 0,
"{name}: {} is an ofs-delta with no recorded base",
hex::encode(&e.oid)
);
assert_ne!(
row.delta_base,
row.offset,
"{name}: {} records base {} which is its own offset, not its base's",
hex::encode(&e.oid),
row.delta_base
);
assert!(
row.delta_base >= AT,
"{name}: base {} is below the archive offset {AT} — it was never rebased",
row.delta_base
);
let base_len = extents.get(&row.delta_base).unwrap_or_else(|| {
panic!(
"{name}: {} records base offset {}, which starts no object in this index",
hex::encode(&e.oid),
row.delta_base
)
});
assert!(
row.delta_base + base_len <= row.offset,
"{name}: the base at {} (+{base_len}) overlaps the delta at {}",
row.delta_base,
row.offset
);
}
assert!(
with_base > 0,
"{name}: {} ofs-deltas resolved and 0 of them carry a base offset — the column \
is not being written",
entries
.iter()
.filter(|e| e.obj_type == ObjType::OfsDelta)
.count()
);
ofs_deltas = with_base;
}
assert!(
ofs_deltas > 100,
"{} carries only {ofs_deltas} ofs-deltas — too few to prove anything",
pack_path.display()
);
eprintln!(
"{}: {ofs_deltas} ofs-delta base offsets located their base entry in all four layouts",
pack_path.display()
);
}
#[test]
fn a_base_many_entries_share_outlives_all_of_them() {
let mut checked = 0usize;
let mut widest = 0usize;
for (pack_path, _) in real_pairs(4) {
let pack = std::fs::read(&pack_path).unwrap();
let w = walk(&pack, 20).unwrap();
let mut uses: HashMap<u64, usize> = HashMap::new();
for e in &w.entries {
if let DeltaBase::Offset(o) = e.delta_base {
*uses.entry(o).or_default() += 1;
}
}
let shared = uses.values().filter(|n| **n > 1).count();
let fan_out = uses.values().copied().max().unwrap_or(0);
if shared == 0 {
continue;
}
widest = widest.max(fan_out);
let rows = resolve(&pack, GitHashKind::Sha1, 0, &NoBases)
.unwrap_or_else(|e| panic!("{}: {e}", pack_path.display()));
assert_eq!(
rows.len(),
w.entries.len(),
"{}: {} of {} entries resolved",
pack_path.display(),
rows.len(),
w.entries.len()
);
eprintln!(
"{}: {shared} bases shared by more than one entry, widest fan-out {fan_out}, all \
{} entries resolved",
pack_path.display(),
rows.len()
);
checked += 1;
}
assert!(
checked > 0 && widest > 2,
"no pack in the corpus had a base shared by more than two entries (checked \
{checked}, widest {widest}) — this test cannot see an early free and must not \
report a pass"
);
}
}