use alloc::vec::Vec;
use purecrypto::hash::{Digest, Sha1, Sha256};
use crate::compress;
use crate::error::Result;
use crate::hash::hash_object;
use crate::object::ObjectType;
use crate::oid::{HashAlgo, ObjectId};
pub struct PackWriter {
algo: HashAlgo,
entries: Vec<Entry>,
external_bases: Vec<Entry>,
}
struct Entry {
id: ObjectId,
ty: ObjectType,
payload: Vec<u8>,
}
const OFS_DELTA_TAG: u8 = 6;
const REF_DELTA_TAG: u8 = 7;
const EXTERNAL_BASE_WINDOW: usize = 16;
const MAX_DELTA_SIZE: usize = 1 << 20;
const DELTA_WINDOW: usize = 10;
struct BaseCand {
ty: ObjectType,
offset: u64,
idx: usize,
}
fn encode_ofs(mut ofs: u64) -> Vec<u8> {
let mut buf = alloc::vec![(ofs & 0x7f) as u8];
ofs >>= 7;
while ofs != 0 {
ofs -= 1;
buf.push(0x80 | (ofs & 0x7f) as u8);
ofs >>= 7;
}
buf.reverse();
buf
}
impl PackWriter {
pub fn new(algo: HashAlgo) -> Self {
PackWriter {
algo,
entries: Vec::new(),
external_bases: Vec::new(),
}
}
pub fn add_external_base(&mut self, ty: ObjectType, payload: &[u8]) -> ObjectId {
let id = hash_object(self.algo, ty, payload);
if !self.external_bases.iter().any(|e| e.id == id) {
self.external_bases.push(Entry {
id,
ty,
payload: payload.to_vec(),
});
}
id
}
pub fn add(&mut self, ty: ObjectType, payload: &[u8]) -> ObjectId {
let id = hash_object(self.algo, ty, payload);
if !self.entries.iter().any(|e| e.id == id) {
self.entries.push(Entry {
id,
ty,
payload: payload.to_vec(),
});
}
id
}
pub fn len(&self) -> usize {
self.entries.len()
}
pub fn is_empty(&self) -> bool {
self.entries.is_empty()
}
pub fn finish(&self) -> Result<PackOutput> {
let mut pack = Vec::new();
pack.extend_from_slice(b"PACK");
pack.extend_from_slice(&2u32.to_be_bytes());
pack.extend_from_slice(&(self.entries.len() as u32).to_be_bytes());
let id_len = self.algo.raw_len();
let mut meta: Vec<(ObjectId, u64, u32)> = Vec::with_capacity(self.entries.len());
let mut bases: Vec<BaseCand> = Vec::new();
for (k, e) in self.entries.iter().enumerate() {
let offset = pack.len() as u64;
let entry_start = pack.len();
let raw = compress::deflate(&e.payload)?;
let mut best: Option<(Vec<u8>, Vec<u8>, usize, u8)> = None;
if e.payload.len() <= MAX_DELTA_SIZE {
let better = |z: &[u8], pfx: &[u8], cur: &Option<(Vec<u8>, Vec<u8>, usize, u8)>| {
z.len() + pfx.len() < raw.len()
&& cur
.as_ref()
.is_none_or(|b| z.len() + pfx.len() < b.0.len() + b.1.len())
};
for cand in bases
.iter()
.rev()
.filter(|c| c.ty == e.ty)
.take(DELTA_WINDOW)
{
let base = &self.entries[cand.idx].payload;
if base.len() > MAX_DELTA_SIZE {
continue;
}
let delta = super::delta::create_delta(base, &e.payload);
let zdelta = compress::deflate(&delta)?;
let ofs = encode_ofs(offset - cand.offset);
if better(&zdelta, &ofs, &best) {
best = Some((zdelta, ofs, delta.len(), OFS_DELTA_TAG));
}
}
for base in self
.external_bases
.iter()
.filter(|b| b.ty == e.ty && b.payload.len() <= MAX_DELTA_SIZE)
.take(EXTERNAL_BASE_WINDOW)
{
let delta = super::delta::create_delta(&base.payload, &e.payload);
let zdelta = compress::deflate(&delta)?;
let id_bytes = base.id.as_bytes()[..id_len].to_vec();
if better(&zdelta, &id_bytes, &best) {
best = Some((zdelta, id_bytes, delta.len(), REF_DELTA_TAG));
}
}
}
match best {
Some((zdelta, prefix, delta_len, tag)) => {
write_entry_header(&mut pack, tag, delta_len);
pack.extend_from_slice(&prefix);
pack.extend_from_slice(&zdelta);
}
None => {
write_entry_header(&mut pack, pack_tag(e.ty), e.payload.len());
pack.extend_from_slice(&raw);
bases.push(BaseCand {
ty: e.ty,
offset,
idx: k,
});
}
}
let crc = crc32(&pack[entry_start..]);
meta.push((e.id, offset, crc));
}
let pack_hash = hash_bytes(self.algo, &pack);
pack.extend_from_slice(&pack_hash);
let idx = build_index(self.algo, &mut meta, &pack_hash);
Ok(PackOutput {
pack,
idx,
hash: ObjectId::from_bytes(self.algo, &pack_hash).expect("hash length matches algo"),
})
}
}
pub struct PackOutput {
pub pack: Vec<u8>,
pub idx: Vec<u8>,
pub hash: ObjectId,
}
fn build_index(algo: HashAlgo, meta: &mut [(ObjectId, u64, u32)], pack_hash: &[u8]) -> Vec<u8> {
let id_len = algo.raw_len();
meta.sort_by(|a, b| a.0.as_bytes().cmp(b.0.as_bytes()));
let mut idx = Vec::new();
idx.extend_from_slice(b"\xfftOc");
idx.extend_from_slice(&2u32.to_be_bytes());
let mut fanout = [0u32; 256];
for (id, _, _) in meta.iter() {
let first = id.as_bytes()[0] as usize;
fanout[first] += 1;
}
let mut acc = 0u32;
for slot in fanout.iter_mut() {
acc += *slot;
*slot = acc;
}
for count in fanout {
idx.extend_from_slice(&count.to_be_bytes());
}
for (id, _, _) in meta.iter() {
idx.extend_from_slice(&id.as_bytes()[..id_len]);
}
for (_, _, crc) in meta.iter() {
idx.extend_from_slice(&crc.to_be_bytes());
}
let mut large: Vec<u64> = Vec::new();
for (_, offset, _) in meta.iter() {
if *offset < 0x8000_0000 {
idx.extend_from_slice(&(*offset as u32).to_be_bytes());
} else {
let large_idx = large.len() as u32;
idx.extend_from_slice(&(0x8000_0000 | large_idx).to_be_bytes());
large.push(*offset);
}
}
for off in large {
idx.extend_from_slice(&off.to_be_bytes());
}
idx.extend_from_slice(pack_hash);
let idx_hash = hash_bytes(algo, &idx);
idx.extend_from_slice(&idx_hash);
idx
}
fn write_entry_header(out: &mut Vec<u8>, tag: u8, size: usize) {
let mut byte = (tag << 4) | (size & 0x0f) as u8;
let mut rest = size >> 4;
if rest != 0 {
byte |= 0x80;
}
out.push(byte);
while rest != 0 {
let mut b = (rest & 0x7f) as u8;
rest >>= 7;
if rest != 0 {
b |= 0x80;
}
out.push(b);
}
}
fn pack_tag(ty: ObjectType) -> u8 {
match ty {
ObjectType::Commit => 1,
ObjectType::Tree => 2,
ObjectType::Blob => 3,
ObjectType::Tag => 4,
}
}
fn hash_bytes(algo: HashAlgo, data: &[u8]) -> Vec<u8> {
match algo {
HashAlgo::Sha1 => Sha1::digest(data).as_ref().to_vec(),
HashAlgo::Sha256 => Sha256::digest(data).as_ref().to_vec(),
}
}
fn crc32(data: &[u8]) -> u32 {
let mut crc = 0xffff_ffffu32;
for &b in data {
crc ^= b as u32;
for _ in 0..8 {
let mask = (crc & 1).wrapping_neg();
crc = (crc >> 1) ^ (0xedb8_8320 & mask);
}
}
!crc
}
#[cfg(test)]
mod tests {
use super::*;
use crate::pack::{Pack, PackIndex};
#[test]
fn write_then_read_back() {
let mut w = PackWriter::new(HashAlgo::Sha1);
let blob = w.add(ObjectType::Blob, b"hello\n");
let tree = w.add(ObjectType::Tree, b""); let out = w.finish().unwrap();
let pack = Pack::new(out.pack.clone(), HashAlgo::Sha1).unwrap();
assert_eq!(pack.object_count(), 2);
let index = PackIndex::parse(HashAlgo::Sha1, &out.idx).unwrap();
assert_eq!(index.len(), 2);
assert!(index.contains(&blob));
assert!(index.contains(&tree));
let never = |_: &ObjectId| -> Result<(ObjectType, Vec<u8>)> {
panic!("no external bases in this pack")
};
let off = index.offset_of(&blob).unwrap();
let (ty, payload) = pack.read_at(off, &never).unwrap();
assert_eq!(ty, ObjectType::Blob);
assert_eq!(payload, b"hello\n");
}
#[test]
fn deltifies_similar_objects_and_reads_back() {
use crate::pack::explode_pack;
let base: Vec<u8> = (0..8000u32)
.map(|i| i.wrapping_mul(2654435761).rotate_left(7) as u8)
.collect();
let mut variant = base.clone();
for b in variant.iter_mut().skip(4000).take(200) {
*b = b.wrapping_add(1);
}
let mut w = PackWriter::new(HashAlgo::Sha1);
let id_base = w.add(ObjectType::Blob, &base);
let id_var = w.add(ObjectType::Blob, &variant);
let out = w.finish().unwrap();
assert!(
out.pack.len() < base.len() + variant.len(),
"expected delta compression to shrink the pack"
);
let index = PackIndex::parse(HashAlgo::Sha1, &out.idx).unwrap();
let pack = Pack::new(out.pack.clone(), HashAlgo::Sha1).unwrap();
let never = |_: &ObjectId| -> Result<(ObjectType, Vec<u8>)> { panic!("no external bases") };
let (ty, payload) = pack
.read_at(index.offset_of(&id_var).unwrap(), &never)
.unwrap();
assert_eq!(ty, ObjectType::Blob);
assert_eq!(payload, variant);
let objs = explode_pack(&out.pack, HashAlgo::Sha1, &never).unwrap();
assert_eq!(objs.len(), 2);
assert!(objs.iter().any(|(id, ty, p)| *id == id_base
&& *ty == ObjectType::Blob
&& p.as_slice() == base.as_slice()));
assert!(objs.iter().any(|(id, ty, p)| *id == id_var
&& *ty == ObjectType::Blob
&& p.as_slice() == variant.as_slice()));
}
#[test]
fn thin_pack_ref_deltas_against_external_base() {
use crate::pack::explode_pack;
let base: Vec<u8> = (0..6000u32)
.map(|i| i.wrapping_mul(2246822519).rotate_left(3) as u8)
.collect();
let mut target = base.clone();
for b in target.iter_mut().skip(3000).take(150) {
*b = b.wrapping_add(7);
}
let mut w = PackWriter::new(HashAlgo::Sha1);
let base_id = w.add_external_base(ObjectType::Blob, &base);
let target_id = w.add(ObjectType::Blob, &target);
let out = w.finish().unwrap();
let pack = Pack::new(out.pack.clone(), HashAlgo::Sha1).unwrap();
assert_eq!(pack.object_count(), 1);
assert!(out.pack.len() < target.len());
let index = PackIndex::parse(HashAlgo::Sha1, &out.idx).unwrap();
let off = index.offset_of(&target_id).unwrap();
let missing = |_: &ObjectId| -> Result<(ObjectType, Vec<u8>)> {
Err(crate::error::Error::NotFound(
crate::error::ObjectKindHint::Object("x".into()),
))
};
assert!(pack.read_at(off, &missing).is_err());
let base_clone = base.clone();
let resolver = move |id: &ObjectId| -> Result<(ObjectType, Vec<u8>)> {
if *id == base_id {
Ok((ObjectType::Blob, base_clone.clone()))
} else {
Err(crate::error::Error::NotFound(
crate::error::ObjectKindHint::Object(id.to_hex()),
))
}
};
let (ty, payload) = pack.read_at(off, &resolver).unwrap();
assert_eq!(ty, ObjectType::Blob);
assert_eq!(payload, target);
let objs = explode_pack(&out.pack, HashAlgo::Sha1, &resolver).unwrap();
assert_eq!(objs.len(), 1);
assert_eq!(objs[0].0, target_id);
assert_eq!(objs[0].2, target);
}
#[test]
fn crc32_known_vector() {
assert_eq!(crc32(b"123456789"), 0xCBF4_3926);
}
}