pub mod bitmap;
pub mod delta;
pub mod ewah;
mod writer;
pub use writer::{PackOutput, PackWriter};
use alloc::vec::Vec;
use crate::compress;
use crate::error::{Error, Result};
use crate::object::ObjectType;
use crate::oid::{HashAlgo, ObjectId};
const PACK_SIGNATURE: &[u8; 4] = b"PACK";
const IDX_MAGIC: &[u8; 4] = b"\xfftOc";
const MAX_DELTA_CHAIN_DEPTH: usize = 10_000;
const MIN_PACK_ENTRY_SIZE: usize = 3;
pub type ExternalResolver<'a> = dyn Fn(&ObjectId) -> Result<(ObjectType, Vec<u8>)> + 'a;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum PackObjectType {
Commit,
Tree,
Blob,
Tag,
OfsDelta,
RefDelta,
}
impl PackObjectType {
fn from_tag(tag: u8) -> Result<Self> {
Ok(match tag {
1 => PackObjectType::Commit,
2 => PackObjectType::Tree,
3 => PackObjectType::Blob,
4 => PackObjectType::Tag,
6 => PackObjectType::OfsDelta,
7 => PackObjectType::RefDelta,
other => {
use alloc::format;
return Err(Error::Pack(format!("unknown pack object type {other}")));
}
})
}
fn as_object_type(self) -> Option<ObjectType> {
Some(match self {
PackObjectType::Commit => ObjectType::Commit,
PackObjectType::Tree => ObjectType::Tree,
PackObjectType::Blob => ObjectType::Blob,
PackObjectType::Tag => ObjectType::Tag,
_ => return None,
})
}
}
#[derive(Debug, Clone)]
pub struct Pack {
data: Vec<u8>,
algo: HashAlgo,
object_count: u32,
}
impl Pack {
pub fn new(data: Vec<u8>, algo: HashAlgo) -> Result<Self> {
if data.len() < 12 {
return Err(Error::Pack("pack: too short".into()));
}
if &data[..4] != PACK_SIGNATURE {
return Err(Error::Pack("pack: bad signature".into()));
}
let version = u32::from_be_bytes([data[4], data[5], data[6], data[7]]);
if version != 2 && version != 3 {
use alloc::format;
return Err(Error::Pack(format!("pack: unsupported version {version}")));
}
let object_count = u32::from_be_bytes([data[8], data[9], data[10], data[11]]);
Ok(Pack {
data,
algo,
object_count,
})
}
pub fn object_count(&self) -> u32 {
self.object_count
}
pub fn read_at(
&self,
offset: usize,
resolve_external: &ExternalResolver<'_>,
) -> Result<(ObjectType, Vec<u8>)> {
self.read_at_with(offset, &|_: &ObjectId| None, resolve_external)
}
pub fn read_at_with(
&self,
offset: usize,
locate: &dyn Fn(&ObjectId) -> Option<usize>,
resolve_external: &ExternalResolver<'_>,
) -> Result<(ObjectType, Vec<u8>)> {
let mut deltas: Vec<Vec<u8>> = Vec::new();
let mut visited = alloc::collections::BTreeSet::new();
let mut cur = offset;
let (base_ty, mut data) = loop {
if deltas.len() > MAX_DELTA_CHAIN_DEPTH {
return Err(Error::Pack("pack: delta chain too deep".into()));
}
if !visited.insert(cur) {
return Err(Error::Pack("pack: delta chain is cyclic".into()));
}
let (ptype, size, hdr_len) = self.parse_entry_header(cur)?;
let body_start = cur + hdr_len;
if let Some(ty) = ptype.as_object_type() {
break (
ty,
compress::inflate_capped(&self.data[body_start..], size)?,
);
}
match ptype {
PackObjectType::OfsDelta => {
let (rel, n) = read_offset_varint(&self.data, body_start)?;
if rel == 0 {
return Err(Error::Pack(
"pack: ofs-delta base is self-referential".into(),
));
}
let base_offset = cur.checked_sub(rel).ok_or_else(|| {
Error::Pack("pack: ofs-delta base before pack start".into())
})?;
deltas.push(compress::inflate_capped(
&self.data[body_start + n..],
size,
)?);
cur = base_offset;
}
PackObjectType::RefDelta => {
let id_len = self.algo.raw_len();
if body_start + id_len > self.data.len() {
return Err(Error::Pack("pack: truncated ref-delta base id".into()));
}
let base_id = ObjectId::from_bytes(
self.algo,
&self.data[body_start..body_start + id_len],
)?;
deltas.push(compress::inflate_capped(
&self.data[body_start + id_len..],
size,
)?);
match locate(&base_id) {
Some(base_offset) => cur = base_offset,
None => break resolve_external(&base_id)?,
}
}
_ => unreachable!("non-delta handled above"),
}
};
for delta in deltas.iter().rev() {
data = delta::apply_delta(&data, delta)?;
}
Ok((base_ty, data))
}
fn parse_entry_header(&self, offset: usize) -> Result<(PackObjectType, usize, usize)> {
parse_entry_header_at(&self.data, offset)
}
}
pub type ExplodedObject = (ObjectId, ObjectType, alloc::vec::Vec<u8>);
pub fn explode_pack(
data: &[u8],
algo: HashAlgo,
resolve_external: &ExternalResolver<'_>,
) -> Result<alloc::vec::Vec<ExplodedObject>> {
Ok(explode_pack_prefix(data, algo, resolve_external)?.0)
}
pub fn explode_pack_prefix(
data: &[u8],
algo: HashAlgo,
resolve_external: &ExternalResolver<'_>,
) -> Result<(alloc::vec::Vec<ExplodedObject>, usize)> {
use crate::hash::hash_object;
use alloc::collections::BTreeMap;
if data.len() < 12 || &data[..4] != PACK_SIGNATURE {
return Err(Error::Pack("pack: bad signature".into()));
}
let count = be32(&data[8..12]);
let id_len = algo.raw_len();
let mut by_offset: BTreeMap<usize, (ObjectType, alloc::vec::Vec<u8>)> = BTreeMap::new();
let mut by_id: BTreeMap<ObjectId, (ObjectType, alloc::vec::Vec<u8>)> = BTreeMap::new();
let reserve = (count as usize).min(data.len() / MIN_PACK_ENTRY_SIZE);
let mut results = alloc::vec::Vec::with_capacity(reserve);
let mut offset = 12usize;
for _ in 0..count {
let (ptype, size, hdr_len) = parse_entry_header_at(data, offset)?;
let body = offset + hdr_len;
let (obj_type, content, next) = match ptype.as_object_type() {
Some(ty) => {
let (content, clen) = compress::inflate_exact(&data[body..], size)?;
(ty, content, body + clen)
}
None => match ptype {
PackObjectType::OfsDelta => {
let (rel, n) = read_offset_varint(data, body)?;
let base_offset = offset.checked_sub(rel).ok_or_else(|| {
Error::Pack("pack: ofs-delta base before pack start".into())
})?;
let (delta, clen) = compress::inflate_exact(&data[body + n..], size)?;
let (base_ty, base) = by_offset
.get(&base_offset)
.ok_or_else(|| Error::Pack("pack: ofs-delta base not yet seen".into()))?;
let target = delta::apply_delta(base, &delta)?;
(*base_ty, target, body + n + clen)
}
PackObjectType::RefDelta => {
if body + id_len > data.len() {
return Err(Error::Pack("pack: truncated ref-delta base id".into()));
}
let base_id = ObjectId::from_bytes(algo, &data[body..body + id_len])?;
let (delta, clen) = compress::inflate_exact(&data[body + id_len..], size)?;
let (base_ty, base) = match by_id.get(&base_id) {
Some((t, b)) => (*t, b.clone()),
None => resolve_external(&base_id)?,
};
let target = delta::apply_delta(&base, &delta)?;
(base_ty, target, body + id_len + clen)
}
_ => unreachable!("base types handled above"),
},
};
let id = hash_object(algo, obj_type, &content);
by_offset.insert(offset, (obj_type, content.clone()));
by_id.insert(id, (obj_type, content.clone()));
results.push((id, obj_type, content));
offset = next;
}
let consumed = offset
.checked_add(id_len)
.filter(|&c| c <= data.len())
.ok_or_else(|| Error::Pack("pack: truncated trailing checksum".into()))?;
Ok((results, consumed))
}
fn parse_entry_header_at(data: &[u8], offset: usize) -> Result<(PackObjectType, usize, usize)> {
let mut p = offset;
let first = *data
.get(p)
.ok_or_else(|| Error::Pack("pack: offset past end".into()))?;
p += 1;
let ptype = PackObjectType::from_tag((first >> 4) & 0x7)?;
let mut size = (first & 0x0f) as usize;
let mut shift = 4;
let mut c = first;
while c & 0x80 != 0 {
c = *data
.get(p)
.ok_or_else(|| Error::Pack("pack: truncated entry header".into()))?;
p += 1;
size |= ((c & 0x7f) as usize)
.checked_shl(shift)
.ok_or_else(|| Error::Pack("pack: size varint overflow".into()))?;
shift += 7;
}
Ok((ptype, size, p - offset))
}
fn read_offset_varint(data: &[u8], start: usize) -> Result<(usize, usize)> {
let mut p = start;
let mut c = *data
.get(p)
.ok_or_else(|| Error::Pack("pack: truncated ofs-delta offset".into()))?;
p += 1;
let mut value = (c & 0x7f) as usize;
while c & 0x80 != 0 {
c = *data
.get(p)
.ok_or_else(|| Error::Pack("pack: truncated ofs-delta offset".into()))?;
p += 1;
value = ((value + 1) << 7) | (c & 0x7f) as usize;
}
Ok((value, p - start))
}
pub fn pack_frame_end(data: &[u8], offset: usize, algo: HashAlgo) -> Result<usize> {
let (ptype, size, hdr_len) = parse_entry_header_at(data, offset)?;
let body = offset + hdr_len;
let consumed = match ptype {
PackObjectType::OfsDelta => {
let (_rel, n) = read_offset_varint(data, body)?;
let (_delta, clen) = compress::inflate_exact(&data[body + n..], size)?;
hdr_len + n + clen
}
PackObjectType::RefDelta => {
let id_len = algo.raw_len();
if body + id_len > data.len() {
return Err(Error::Pack("pack: truncated ref-delta base id".into()));
}
let (_delta, clen) = compress::inflate_exact(&data[body + id_len..], size)?;
hdr_len + id_len + clen
}
_ => {
let (_content, clen) = compress::inflate_exact(&data[body..], size)?;
hdr_len + clen
}
};
Ok(offset + consumed)
}
#[derive(Debug, Clone)]
pub struct PackIndex {
algo: HashAlgo,
oids: Vec<u8>,
offsets: Vec<u8>,
large_offsets: Vec<u8>,
count: usize,
}
impl PackIndex {
pub fn parse(algo: HashAlgo, data: &[u8]) -> Result<Self> {
let id_len = algo.raw_len();
if data.len() < 8 + 256 * 4 {
return Err(Error::Pack("idx: too short".into()));
}
if &data[..4] != IDX_MAGIC {
return Err(Error::Pack(
"idx: bad magic (v1 indexes unsupported)".into(),
));
}
let version = be32(&data[4..8]);
if version != 2 {
use alloc::format;
return Err(Error::Pack(format!("idx: unsupported version {version}")));
}
let fanout_start = 8;
let count = be32(&data[fanout_start + 255 * 4..fanout_start + 256 * 4]) as usize;
let overflow = || Error::Pack("idx: truncated".into());
let oids_start = fanout_start + 256 * 4;
let oids_len = count.checked_mul(id_len).ok_or_else(overflow)?;
let crc_start = oids_start.checked_add(oids_len).ok_or_else(overflow)?;
let crc_len = count.checked_mul(4).ok_or_else(overflow)?;
let off_start = crc_start.checked_add(crc_len).ok_or_else(overflow)?;
let off_len = count.checked_mul(4).ok_or_else(overflow)?;
let large_start = off_start.checked_add(off_len).ok_or_else(overflow)?;
if large_start > data.len() {
return Err(Error::Pack("idx: truncated".into()));
}
let large_end = data.len().saturating_sub(2 * id_len);
let large_offsets = data[large_start..large_end.max(large_start)].to_vec();
Ok(PackIndex {
algo,
oids: data[oids_start..oids_start + oids_len].to_vec(),
offsets: data[off_start..off_start + off_len].to_vec(),
large_offsets,
count,
})
}
pub fn len(&self) -> usize {
self.count
}
pub fn algo(&self) -> HashAlgo {
self.algo
}
pub fn is_empty(&self) -> bool {
self.count == 0
}
pub fn offset_of(&self, id: &ObjectId) -> Option<usize> {
let idx = self.position_of(id)?;
let raw = be32(&self.offsets[idx * 4..idx * 4 + 4]);
if raw & 0x8000_0000 == 0 {
return Some(raw as usize);
}
let large_idx = (raw & 0x7fff_ffff) as usize;
let off = large_idx * 8;
if off + 8 > self.large_offsets.len() {
return None;
}
let v = be64(&self.large_offsets[off..off + 8]);
Some(v as usize)
}
pub fn contains(&self, id: &ObjectId) -> bool {
self.position_of(id).is_some()
}
pub fn id_at(&self, row: usize) -> Option<ObjectId> {
let id_len = self.algo.raw_len();
let bytes = self.oids.get(row * id_len..row * id_len + id_len)?;
ObjectId::from_bytes(self.algo, bytes).ok()
}
pub fn offset_at(&self, row: usize) -> Option<u64> {
let raw = be32(self.offsets.get(row * 4..row * 4 + 4)?);
if raw & 0x8000_0000 == 0 {
return Some(raw as u64);
}
let off = (raw & 0x7fff_ffff) as usize * 8;
Some(be64(self.large_offsets.get(off..off + 8)?))
}
fn position_of(&self, id: &ObjectId) -> Option<usize> {
if id.algo() != self.algo {
return None;
}
let id_len = self.algo.raw_len();
let needle = id.as_bytes();
let (mut lo, mut hi) = (0usize, self.count);
while lo < hi {
let mid = (lo + hi) / 2;
let row = &self.oids[mid * id_len..mid * id_len + id_len];
match row.cmp(needle) {
core::cmp::Ordering::Less => lo = mid + 1,
core::cmp::Ordering::Greater => hi = mid,
core::cmp::Ordering::Equal => return Some(mid),
}
}
None
}
}
#[derive(Debug, Clone, Default)]
pub struct PackVerification {
pub objects: usize,
pub problems: alloc::vec::Vec<alloc::string::String>,
}
impl PackVerification {
pub fn is_ok(&self) -> bool {
self.problems.is_empty()
}
}
pub fn verify_pack(
pack: &[u8],
idx: &[u8],
algo: HashAlgo,
resolve_external: &ExternalResolver<'_>,
) -> Result<PackVerification> {
let index = PackIndex::parse(algo, idx)?;
let objects = explode_pack(pack, algo, resolve_external)?;
let mut problems = alloc::vec::Vec::new();
for (id, _, _) in &objects {
if !index.contains(id) {
problems.push(alloc::format!(
"object {id} decoded from the pack is absent from the index (corrupt or wrong idx)"
));
}
}
if objects.len() != index.len() {
problems.push(alloc::format!(
"pack contains {} object(s) but the index lists {}",
objects.len(),
index.len()
));
}
Ok(PackVerification {
objects: objects.len(),
problems,
})
}
fn be32(b: &[u8]) -> u32 {
u32::from_be_bytes([b[0], b[1], b[2], b[3]])
}
fn be64(b: &[u8]) -> u64 {
u64::from_be_bytes([b[0], b[1], b[2], b[3], b[4], b[5], b[6], b[7]])
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn rejects_bad_pack_signature() {
assert!(Pack::new(alloc::vec![0u8; 32], HashAlgo::Sha1).is_err());
}
#[test]
fn verify_pack_accepts_good_and_flags_corrupt() {
use crate::pack::writer::PackWriter;
let mut w = PackWriter::new(HashAlgo::Sha1);
w.add(ObjectType::Blob, b"hello\n");
w.add(ObjectType::Blob, b"another object\n");
let out = w.finish().unwrap();
let no_ext = |id: &ObjectId| {
Err(Error::NotFound(crate::error::ObjectKindHint::Object(
id.to_hex(),
)))
};
let v = verify_pack(&out.pack, &out.idx, HashAlgo::Sha1, &no_ext).unwrap();
assert!(v.is_ok(), "unexpected problems: {:?}", v.problems);
assert_eq!(v.objects, 2);
let mut bad = out.pack.clone();
let i = bad.len() - 25; bad[i] ^= 0xff;
if let Ok(v) = verify_pack(&bad, &out.idx, HashAlgo::Sha1, &no_ext) {
assert!(!v.is_ok(), "corrupt pack unexpectedly verified");
}
}
#[test]
fn parses_pack_header() {
let mut data = Vec::new();
data.extend_from_slice(b"PACK");
data.extend_from_slice(&2u32.to_be_bytes());
data.extend_from_slice(&7u32.to_be_bytes());
data.resize(64, 0);
let pack = Pack::new(data, HashAlgo::Sha1).unwrap();
assert_eq!(pack.object_count(), 7);
}
#[test]
fn offset_varint_roundtrip() {
let encoded = [0x80u8, 0x48]; let (v, n) = read_offset_varint(&encoded, 0).unwrap();
assert_eq!(n, 2);
assert_eq!(v, 200);
}
fn entry_header(tag: u8, mut size: usize) -> Vec<u8> {
let mut out = Vec::new();
let mut first = (tag << 4) | (size & 0x0f) as u8;
size >>= 4;
if size != 0 {
first |= 0x80;
}
out.push(first);
while size != 0 {
let mut b = (size & 0x7f) as u8;
size >>= 7;
if size != 0 {
b |= 0x80;
}
out.push(b);
}
out
}
#[test]
fn read_at_rejects_self_referential_ofs_delta() {
let delta_body = compress::deflate(b"\x00\x00").unwrap(); let mut pack = 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(&entry_header(6, 2));
pack.push(0x00); pack.extend_from_slice(&delta_body);
pack.resize(pack.len() + 20, 0);
let pack = Pack::new(pack, HashAlgo::Sha1).unwrap();
let never =
|_: &ObjectId| -> Result<(ObjectType, Vec<u8>)> { Err(Error::Pack("unused".into())) };
let err = pack.read_at(12, &never).unwrap_err();
assert!(matches!(err, Error::Pack(_)));
}
fn ofs_varint(rel: usize) -> Vec<u8> {
let mut bytes = Vec::new();
let mut v = rel;
bytes.push((v & 0x7f) as u8);
v >>= 7;
while v != 0 {
v -= 1;
bytes.push(0x80 | (v & 0x7f) as u8);
v >>= 7;
}
bytes.reverse();
bytes
}
fn ofs_chain_pack(links: usize) -> (Pack, usize) {
let mut pack = Vec::new();
pack.extend_from_slice(b"PACK");
pack.extend_from_slice(&2u32.to_be_bytes());
pack.extend_from_slice(&((links + 1) as u32).to_be_bytes());
let base_off = pack.len();
pack.extend_from_slice(&entry_header(3, 1)); pack.extend_from_slice(&compress::deflate(b"x").unwrap());
let mut prev = base_off;
for i in 0..links {
let (src, dst) = (i + 1, i + 2);
assert!(dst < 0x80, "sizes fit one varint byte in this helper");
let mut d = alloc::vec![src as u8, dst as u8];
d.extend_from_slice(&[0x80 | 0x10, src as u8]); d.extend_from_slice(&[1, b'y']); let here = pack.len();
pack.extend_from_slice(&entry_header(6, d.len()));
pack.extend_from_slice(&ofs_varint(here - prev));
pack.extend_from_slice(&compress::deflate(&d).unwrap());
prev = here;
}
pack.resize(pack.len() + 20, 0); (Pack::new(pack, HashAlgo::Sha1).unwrap(), prev)
}
#[test]
fn read_at_follows_chains_deeper_than_git_default() {
let (pack, last) = ofs_chain_pack(120);
let never =
|_: &ObjectId| -> Result<(ObjectType, Vec<u8>)> { Err(Error::Pack("unused".into())) };
let (ty, data) = pack.read_at(last, &never).unwrap();
assert_eq!(ty, ObjectType::Blob);
let mut want = b"x".to_vec();
want.resize(121, b'y');
assert_eq!(data, want);
}
#[test]
fn read_at_rejects_overlong_ofs_delta_chain() {
let mut pack = Vec::new();
pack.extend_from_slice(b"PACK");
pack.extend_from_slice(&2u32.to_be_bytes());
pack.extend_from_slice(&1u32.to_be_bytes());
let base_off = pack.len();
pack.extend_from_slice(&entry_header(3, 0)); pack.extend_from_slice(&compress::deflate(b"").unwrap());
let delta_body = compress::deflate(b"\x00\x00").unwrap();
let mut prev = base_off;
for _ in 0..(MAX_DELTA_CHAIN_DEPTH + 5) {
let here = pack.len();
pack.extend_from_slice(&entry_header(6, 2));
pack.extend_from_slice(&ofs_varint(here - prev));
pack.extend_from_slice(&delta_body);
prev = here;
}
pack.resize(pack.len() + 20, 0);
let pack = Pack::new(pack, HashAlgo::Sha1).unwrap();
let never =
|_: &ObjectId| -> Result<(ObjectType, Vec<u8>)> { Err(Error::Pack("unused".into())) };
let err = pack.read_at(prev, &never).unwrap_err();
assert!(matches!(err, Error::Pack(_)));
}
#[test]
fn read_at_with_rejects_ref_delta_cycle() {
let delta_body = compress::deflate(b"\x00\x00").unwrap();
let fake = |b: u8| ObjectId::from_bytes(HashAlgo::Sha1, &[b; 20]).unwrap();
let mut pack = Vec::new();
pack.extend_from_slice(b"PACK");
pack.extend_from_slice(&2u32.to_be_bytes());
pack.extend_from_slice(&2u32.to_be_bytes());
let a = pack.len();
pack.extend_from_slice(&entry_header(7, 2));
pack.extend_from_slice(fake(0xbb).as_bytes());
pack.extend_from_slice(&delta_body);
let b = pack.len();
pack.extend_from_slice(&entry_header(7, 2));
pack.extend_from_slice(fake(0xaa).as_bytes());
pack.extend_from_slice(&delta_body);
pack.resize(pack.len() + 20, 0);
let pack = Pack::new(pack, HashAlgo::Sha1).unwrap();
let locate = |id: &ObjectId| {
if *id == fake(0xaa) {
Some(a)
} else if *id == fake(0xbb) {
Some(b)
} else {
None
}
};
let never =
|_: &ObjectId| -> Result<(ObjectType, Vec<u8>)> { Err(Error::Pack("unused".into())) };
let err = pack.read_at_with(a, &locate, &never).unwrap_err();
assert!(matches!(err, Error::Pack(_)));
}
#[test]
fn explode_pack_does_not_preallocate_untrusted_count() {
let mut data = Vec::new();
data.extend_from_slice(b"PACK");
data.extend_from_slice(&2u32.to_be_bytes());
data.extend_from_slice(&u32::MAX.to_be_bytes());
let never =
|_: &ObjectId| -> Result<(ObjectType, Vec<u8>)> { Err(Error::Pack("unused".into())) };
let err = explode_pack(&data, HashAlgo::Sha1, &never).unwrap_err();
assert!(matches!(err, Error::Pack(_)));
}
#[test]
fn pack_index_parse_rejects_overflowing_count() {
let mut data = Vec::new();
data.extend_from_slice(IDX_MAGIC);
data.extend_from_slice(&2u32.to_be_bytes()); for _ in 0..255 {
data.extend_from_slice(&0u32.to_be_bytes());
}
data.extend_from_slice(&u32::MAX.to_be_bytes());
let err = PackIndex::parse(HashAlgo::Sha1, &data).unwrap_err();
assert!(matches!(err, Error::Pack(_)));
}
#[test]
fn writer_then_explode_roundtrip() {
use crate::pack::PackWriter;
let mut w = PackWriter::new(HashAlgo::Sha1);
let blob = w.add(ObjectType::Blob, b"hello world\n");
let empty_tree = w.add(ObjectType::Tree, b"");
let commitish = w.add(ObjectType::Commit, b"tree x\n\nmsg\n");
let out = w.finish().unwrap();
let never = |_: &ObjectId| -> Result<(ObjectType, Vec<u8>)> {
Err(Error::Pack("no external bases expected".into()))
};
let objs = explode_pack(&out.pack, HashAlgo::Sha1, &never).unwrap();
assert_eq!(objs.len(), 3);
let find = |id: &ObjectId| objs.iter().find(|(oid, _, _)| oid == id).cloned();
assert_eq!(find(&blob).unwrap().2, b"hello world\n");
assert_eq!(find(&empty_tree).unwrap().1, ObjectType::Tree);
assert_eq!(find(&empty_tree).unwrap().2, b"");
assert_eq!(find(&commitish).unwrap().1, ObjectType::Commit);
}
#[test]
fn explode_pack_prefix_decodes_concatenated_packs() {
use crate::pack::PackWriter;
let mut w1 = PackWriter::new(HashAlgo::Sha1);
let a = w1.add(ObjectType::Blob, b"first pack blob\n");
let p1 = w1.finish().unwrap();
let mut w2 = PackWriter::new(HashAlgo::Sha1);
let b = w2.add(ObjectType::Blob, b"second pack blob\n");
let p2 = w2.finish().unwrap();
let mut concat = p1.pack.clone();
concat.extend_from_slice(&p2.pack);
let never = |_: &ObjectId| -> Result<(ObjectType, Vec<u8>)> {
Err(Error::Pack("no external bases".into()))
};
let (objs1, consumed) = explode_pack_prefix(&concat, HashAlgo::Sha1, &never).unwrap();
assert_eq!(consumed, p1.pack.len());
assert_eq!(objs1.len(), 1);
assert_eq!(objs1[0].0, a);
let rest = &concat[consumed..];
assert_eq!(rest, p2.pack.as_slice());
let (objs2, consumed2) = explode_pack_prefix(rest, HashAlgo::Sha1, &never).unwrap();
assert_eq!(consumed2, p2.pack.len());
assert_eq!(objs2[0].0, b);
}
}