use std::io::Write;
use flate2::write::ZlibEncoder;
use flate2::Compression;
use git_storage_trait::{Extent, ObjType};
use gix_hash::ObjectId;
use gix_object::Kind;
use crate::error::{Error, Result};
#[derive(Debug, Clone)]
pub struct Entry {
pub id: ObjectId,
pub kind: Kind,
pub obj_type: ObjType,
pub content: Vec<u8>,
pub extent: Extent,
}
impl Entry {
pub fn bytes<'a>(&self, pack: &'a [u8]) -> &'a [u8] {
let (offset, len) = self.extent;
&pack[offset as usize..(offset + len) as usize]
}
}
#[derive(Debug, Clone)]
pub struct Fixture {
pub bytes: Vec<u8>,
pub entries: Vec<Entry>,
}
impl Fixture {
pub fn entry(&self, id: &gix_hash::oid) -> Option<&Entry> {
self.entries.iter().find(|e| e.id == id)
}
pub fn ids(&self) -> Vec<ObjectId> {
self.entries.iter().map(|e| e.id).collect()
}
}
enum Item {
Base {
kind: Kind,
content: Vec<u8>,
},
Delta {
base: usize,
content: Vec<u8>,
},
}
pub struct PackBuilder {
hash_kind: gix_hash::Kind,
items: Vec<Item>,
level: Compression,
}
impl PackBuilder {
pub fn new(hash_kind: gix_hash::Kind) -> Self {
PackBuilder {
hash_kind,
items: Vec::new(),
level: Compression::default(),
}
}
#[must_use]
pub fn compression(mut self, level: Compression) -> Self {
self.level = level;
self
}
pub fn object(&mut self, kind: Kind, content: impl Into<Vec<u8>>) -> usize {
self.items.push(Item::Base {
kind,
content: content.into(),
});
self.items.len() - 1
}
pub fn delta(&mut self, base: usize, content: impl Into<Vec<u8>>) -> usize {
assert!(base < self.items.len(), "a delta's base must precede it");
self.items.push(Item::Delta {
base,
content: content.into(),
});
self.items.len() - 1
}
pub fn build(self) -> Result<Fixture> {
let mut bytes = Vec::new();
bytes.extend_from_slice(b"PACK");
bytes.extend_from_slice(&2u32.to_be_bytes());
bytes.extend_from_slice(&(self.items.len() as u32).to_be_bytes());
let mut entries: Vec<Entry> = Vec::with_capacity(self.items.len());
for item in &self.items {
let start = bytes.len() as u64;
let (kind, content, obj_type, payload) = match item {
Item::Base { kind, content } => {
(*kind, content.clone(), obj_type_of(*kind), content.clone())
}
Item::Delta { base, content } => {
let base_entry = &entries[*base];
let payload = encode_delta(&base_entry.content, content);
(base_entry.kind, content.clone(), ObjType::OfsDelta, payload)
}
};
write_entry_header(&mut bytes, obj_type, payload.len() as u64);
if let Item::Delta { base, .. } = item {
let distance = start - entries[*base].extent.0;
write_offset(&mut bytes, distance);
}
deflate_at(&mut bytes, &payload, self.level)?;
let id = gix_object::compute_hash(self.hash_kind, kind, &content)?;
entries.push(Entry {
id,
kind,
obj_type,
content,
extent: (start, bytes.len() as u64 - start),
});
}
let mut hasher = gix_hash::hasher(self.hash_kind);
hasher.update(&bytes);
let trailer = hasher.try_finalize()?;
bytes.extend_from_slice(trailer.as_slice());
Ok(Fixture { bytes, entries })
}
}
pub fn obj_type_of(kind: Kind) -> ObjType {
match kind {
Kind::Commit => ObjType::Commit,
Kind::Tree => ObjType::Tree,
Kind::Blob => ObjType::Blob,
Kind::Tag => ObjType::Tag,
}
}
pub fn kind_of(obj_type: ObjType) -> Option<Kind> {
Some(match obj_type {
ObjType::Commit => Kind::Commit,
ObjType::Tree => Kind::Tree,
ObjType::Blob => Kind::Blob,
ObjType::Tag => Kind::Tag,
ObjType::OfsDelta | ObjType::RefDelta => return None,
})
}
pub(crate) fn write_entry_header(out: &mut Vec<u8>, obj_type: ObjType, mut size: u64) {
let mut byte = (obj_type.code() << 4) | (size as u8 & 0x0f);
size >>= 4;
while size > 0 {
out.push(byte | 0x80);
byte = size as u8 & 0x7f;
size >>= 7;
}
out.push(byte);
}
pub(crate) fn write_offset(out: &mut Vec<u8>, mut value: u64) {
let mut buf = vec![(value & 0x7f) as u8];
value >>= 7;
while value > 0 {
value -= 1;
buf.push(0x80 | (value & 0x7f) as u8);
value >>= 7;
}
buf.reverse();
out.extend_from_slice(&buf);
}
fn write_varint(out: &mut Vec<u8>, mut value: u64) {
loop {
let byte = (value & 0x7f) as u8;
value >>= 7;
if value == 0 {
out.push(byte);
return;
}
out.push(byte | 0x80);
}
}
fn encode_delta(base: &[u8], result: &[u8]) -> Vec<u8> {
let mut out = Vec::new();
write_varint(&mut out, base.len() as u64);
write_varint(&mut out, result.len() as u64);
for chunk in result.chunks(0x7f) {
out.push(chunk.len() as u8);
out.extend_from_slice(chunk);
}
out
}
pub(crate) fn deflate_into(out: &mut Vec<u8>, payload: &[u8]) -> Result<()> {
deflate_at(out, payload, Compression::default())
}
fn deflate_at(out: &mut Vec<u8>, payload: &[u8], level: Compression) -> Result<()> {
let mut encoder = ZlibEncoder::new(Vec::new(), level);
encoder.write_all(payload)?;
out.extend_from_slice(&encoder.finish()?);
Ok(())
}
#[derive(Debug, Clone)]
pub struct RawEntry {
pub obj_type: ObjType,
pub base_offset: Option<u64>,
pub payload: Vec<u8>,
pub extent: Extent,
pub header_len: u64,
}
pub fn parse(pack: &[u8], hash_kind: gix_hash::Kind) -> Result<Vec<RawEntry>> {
let trailer_len = hash_kind.len_in_bytes();
if pack.len() < 12 + trailer_len || &pack[..4] != b"PACK" {
return Err(Error::backend("not a packfile"));
}
let version = u32::from_be_bytes(pack[4..8].try_into().expect("four bytes"));
if version != 2 {
return Err(Error::backend(format!("pack version {version}, want 2")));
}
let count = u32::from_be_bytes(pack[8..12].try_into().expect("four bytes"));
let body_end = pack.len() - trailer_len;
let mut hasher = gix_hash::hasher(hash_kind);
hasher.update(&pack[..body_end]);
if hasher.try_finalize()?.as_slice() != &pack[body_end..] {
return Err(Error::backend("the pack trailer does not match its bytes"));
}
let mut out = Vec::with_capacity(count as usize);
let mut pos = 12usize;
for _ in 0..count {
let start = pos;
let (obj_type, size) = read_entry_header(pack, &mut pos)?;
let base_offset = if obj_type == ObjType::OfsDelta {
let distance = read_offset(pack, &mut pos)?;
Some(
(start as u64)
.checked_sub(distance)
.ok_or_else(|| Error::backend("an OFS_DELTA points before the pack"))?,
)
} else if obj_type == ObjType::RefDelta {
return Err(Error::backend(
"REF_DELTA is outside this fixture reader's subset",
));
} else {
None
};
let header_len = (pos - start) as u64;
let payload = inflate(&pack[pos..body_end], size as usize, &mut pos)?;
out.push(RawEntry {
obj_type,
base_offset,
payload,
extent: (start as u64, (pos - start) as u64),
header_len,
});
}
Ok(out)
}
fn read_entry_header(pack: &[u8], pos: &mut usize) -> Result<(ObjType, u64)> {
let mut byte = *pack
.get(*pos)
.ok_or_else(|| Error::backend("truncated entry header"))?;
*pos += 1;
let code = (byte >> 4) & 0x07;
let obj_type =
ObjType::from_code(code).ok_or_else(|| Error::backend(format!("object type {code}")))?;
let mut size = u64::from(byte & 0x0f);
let mut shift = 4;
while byte & 0x80 != 0 {
byte = *pack
.get(*pos)
.ok_or_else(|| Error::backend("truncated entry header"))?;
*pos += 1;
size |= u64::from(byte & 0x7f) << shift;
shift += 7;
}
Ok((obj_type, size))
}
fn read_offset(pack: &[u8], pos: &mut usize) -> Result<u64> {
let mut byte = *pack
.get(*pos)
.ok_or_else(|| Error::backend("truncated delta offset"))?;
*pos += 1;
let mut value = u64::from(byte & 0x7f);
while byte & 0x80 != 0 {
byte = *pack
.get(*pos)
.ok_or_else(|| Error::backend("truncated delta offset"))?;
*pos += 1;
value = ((value + 1) << 7) | u64::from(byte & 0x7f);
}
Ok(value)
}
fn inflate(rest: &[u8], expected: usize, pos: &mut usize) -> Result<Vec<u8>> {
use std::io::Read;
let mut decoder = flate2::read::ZlibDecoder::new(rest);
let mut payload = Vec::with_capacity(expected);
decoder.read_to_end(&mut payload)?;
if payload.len() != expected {
return Err(Error::backend(format!(
"entry header claims {expected} bytes and {} inflated",
payload.len()
)));
}
*pos += decoder.total_in() as usize;
Ok(payload)
}
pub fn apply_delta(base: &[u8], delta: &[u8]) -> Result<Vec<u8>> {
let mut pos = 0usize;
let base_size = read_varint(delta, &mut pos)?;
if base_size as usize != base.len() {
return Err(Error::backend(format!(
"the delta expects a {base_size}-byte base and the base is {}",
base.len()
)));
}
let result_size = read_varint(delta, &mut pos)?;
let mut out = Vec::with_capacity(result_size as usize);
while pos < delta.len() {
let op = delta[pos];
pos += 1;
if op & 0x80 != 0 {
let mut offset = 0u64;
let mut len = 0u64;
for i in 0..4 {
if op & (1 << i) != 0 {
offset |= u64::from(delta[pos]) << (8 * i);
pos += 1;
}
}
for i in 0..3 {
if op & (1 << (4 + i)) != 0 {
len |= u64::from(delta[pos]) << (8 * i);
pos += 1;
}
}
if len == 0 {
len = 0x10000;
}
let (from, to) = (offset as usize, (offset + len) as usize);
out.extend_from_slice(
base.get(from..to)
.ok_or_else(|| Error::backend("a delta copies past the end of its base"))?,
);
} else if op == 0 {
return Err(Error::backend("delta opcode 0 is reserved"));
} else {
let (from, to) = (pos, pos + op as usize);
out.extend_from_slice(
delta
.get(from..to)
.ok_or_else(|| Error::backend("a delta insert runs past the stream"))?,
);
pos = to;
}
}
if out.len() as u64 != result_size {
return Err(Error::backend(format!(
"the delta claims a {result_size}-byte result and produced {}",
out.len()
)));
}
Ok(out)
}
fn read_varint(bytes: &[u8], pos: &mut usize) -> Result<u64> {
let mut value = 0u64;
let mut shift = 0;
loop {
let byte = *bytes
.get(*pos)
.ok_or_else(|| Error::backend("truncated delta varint"))?;
*pos += 1;
value |= u64::from(byte & 0x7f) << shift;
if byte & 0x80 == 0 {
return Ok(value);
}
shift += 7;
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn a_delta_entry_stores_something_other_than_what_it_resolves_to() {
let mut builder = PackBuilder::new(gix_hash::Kind::Sha256);
let base = builder.object(Kind::Blob, b"the base blob\n".to_vec());
builder.delta(
base,
b"a longer resolved result, not the delta stream\n".to_vec(),
);
let fixture = builder.build().expect("build");
let raw = parse(&fixture.bytes, gix_hash::Kind::Sha256).expect("parse");
assert_eq!(raw.len(), 2);
assert_eq!(raw[1].obj_type, ObjType::OfsDelta);
let resolved = apply_delta(&raw[0].payload, &raw[1].payload).expect("apply");
assert_eq!(resolved, fixture.entries[1].content);
assert_ne!(
raw[1].payload.len(),
resolved.len(),
"the fixture delta must store a different number of bytes than it resolves to, \
or the post-resolution-size check below is measuring nothing"
);
}
#[test]
fn every_entry_reads_back_at_the_extent_the_builder_recorded() {
let mut builder = PackBuilder::new(gix_hash::Kind::Sha1);
builder.object(Kind::Blob, b"one".to_vec());
builder.object(Kind::Blob, vec![7u8; 100_000]);
builder.object(Kind::Tree, Vec::new());
let fixture = builder.build().expect("build");
let raw = parse(&fixture.bytes, gix_hash::Kind::Sha1).expect("parse");
for (entry, raw) in fixture.entries.iter().zip(&raw) {
assert_eq!(entry.extent, raw.extent);
assert_eq!(entry.content, raw.payload);
}
}
#[test]
fn a_corrupted_pack_is_refused() {
let mut builder = PackBuilder::new(gix_hash::Kind::Sha1);
builder.object(Kind::Blob, b"intact".to_vec());
let mut fixture = builder.build().expect("build");
let last = fixture.bytes.len() - 1;
fixture.bytes[last] ^= 0xff;
assert!(parse(&fixture.bytes, gix_hash::Kind::Sha1).is_err());
}
}