use super::{DeltaLimits, Entry, EntryKind, Reader, apply_delta};
use crate::{
Error, Object, ObjectId, ObjectKind, Result,
object::Tree,
zlib::{InflateLimits, WINDOW_SIZE},
};
#[repr(C)]
#[derive(Clone, Copy, Debug, Default)]
pub struct MaterializedEntry {
id: [u8; 20],
kind: u32,
pack_offset: u64,
offset: usize,
length: usize,
depth: u32,
}
impl MaterializedEntry {
pub fn id(&self) -> ObjectId {
ObjectId::Sha1(self.id)
}
pub fn kind(&self) -> Result<ObjectKind> {
match self.kind {
1 => Ok(ObjectKind::Commit),
2 => Ok(ObjectKind::Tree),
3 => Ok(ObjectKind::Blob),
4 => Ok(ObjectKind::Tag),
_ => Err(Error::InvalidObject),
}
}
pub fn offset(&self) -> usize {
self.offset
}
pub fn length(&self) -> usize {
self.length
}
pub fn delta_depth(&self) -> u32 {
self.depth
}
pub fn pack_offset(&self) -> u64 {
self.pack_offset
}
}
#[derive(Clone, Copy, Debug)]
pub struct MaterializeLimits {
pub inflate: InflateLimits,
pub delta: DeltaLimits,
pub max_passes: u32,
pub max_decoded_bytes_per_pass: u64,
pub max_base_probes: u64,
pub max_tree_validation_bytes: u64,
}
pub struct MaterializedPack<'a> {
index: &'a [MaterializedEntry],
arena: &'a [u8],
}
impl<'a> MaterializedPack<'a> {
pub fn entries(&self) -> &'a [MaterializedEntry] {
self.index
}
pub fn bytes_used(&self) -> usize {
self.arena.len()
}
pub fn object(&self, position: usize) -> Option<Object<'a>> {
let entry = self.index.get(position)?;
Some(Object {
kind: entry.kind().ok()?,
data: self
.arena
.get(entry.offset..entry.offset.checked_add(entry.length)?)?,
})
}
pub fn find(&self, id: ObjectId) -> Option<Object<'a>> {
let entry = find_materialized(self.index, id)?;
Some(Object {
kind: entry.kind().ok()?,
data: self
.arena
.get(entry.offset..entry.offset.checked_add(entry.length)?)?,
})
}
}
pub fn find_materialized(index: &[MaterializedEntry], id: ObjectId) -> Option<&MaterializedEntry> {
let ObjectId::Sha1(id) = id else {
return None;
};
let position = index.binary_search_by_key(&id, |entry| entry.id).ok()?;
let entry = &index[position];
(entry.kind != 0).then_some(entry)
}
pub fn materialize<'a>(
pack: &[u8],
index: &'a mut [MaterializedEntry],
arena: &'a mut [u8],
scratch: &mut [u8],
window: &mut [u8; WINDOW_SIZE],
limits: MaterializeLimits,
) -> Result<MaterializedPack<'a>> {
let count = Reader::new(pack)?.object_count() as usize;
let index = index.get_mut(..count).ok_or(Error::BufferTooSmall)?;
index.fill(MaterializedEntry::default());
if count == 0 {
return Ok(MaterializedPack {
index,
arena: &arena[..0],
});
}
let mut used = 0;
let mut remaining = count;
let mut base_probes = limits.max_base_probes;
let mut tree_bytes = limits.max_tree_validation_bytes;
for _ in 0..limits.max_passes {
let mut reader = Reader::new(pack)?;
let mut progress = false;
let mut decoded_bytes = 0_u64;
for i in 0..count {
decoded_bytes =
decoded_bytes.saturating_add(reader.next_entry_size()?.ok_or(Error::CorruptPack)?);
if decoded_bytes > limits.max_decoded_bytes_per_pass {
return Err(Error::LimitExceeded);
}
let entry = reader
.read_entry(scratch, window, limits.inflate)?
.ok_or(Error::CorruptPack)?;
if index[i].kind != 0 {
continue;
}
let base = if remaining == count {
None
} else {
find_base(entry.kind(), index, &mut base_probes)?
};
if !matches!(entry.kind(), EntryKind::Object(_)) && base.is_none() {
continue;
}
let (previous, output) = arena.split_at_mut(used);
let (object, depth) = materialize_entry(&entry, base, previous, output, limits.delta)?;
if object.kind == ObjectKind::Tree {
Tree::parse_with_budget(object.data, &mut tree_bytes)?;
}
let ObjectId::Sha1(id) = object.id() else {
return Err(Error::Unsupported);
};
index[i] = MaterializedEntry {
id,
kind: object.kind as u32,
pack_offset: entry.offset(),
offset: used,
length: object.data.len(),
depth,
};
used += object.data.len();
remaining -= 1;
progress = true;
}
reader.finish()?;
if remaining == 0 {
sort_index(index);
return Ok(MaterializedPack {
index,
arena: &arena[..used],
});
}
if !progress {
return Err(Error::CorruptPack);
}
}
Err(Error::LimitExceeded)
}
fn find_base(
kind: EntryKind,
index: &[MaterializedEntry],
probes: &mut u64,
) -> Result<Option<MaterializedEntry>> {
if matches!(kind, EntryKind::Object(_)) {
return Ok(None);
}
for candidate in index {
*probes = probes.checked_sub(1).ok_or(Error::LimitExceeded)?;
let matches = match kind {
EntryKind::OffsetDelta { base_offset } => candidate.pack_offset == base_offset,
EntryKind::ReferenceDelta { base_id } => candidate.id.as_slice() == base_id.as_bytes(),
EntryKind::Object(_) => false,
};
if candidate.kind != 0 && matches {
return Ok(Some(*candidate));
}
}
Ok(None)
}
fn materialize_entry<'out>(
entry: &Entry<'_>,
base: Option<MaterializedEntry>,
previous: &[u8],
output: &'out mut [u8],
limits: DeltaLimits,
) -> Result<(Object<'out>, u32)> {
if let Some(base) = base {
if base.depth >= limits.max_depth {
return Err(Error::LimitExceeded);
}
let end = base
.offset
.checked_add(base.length)
.ok_or(Error::CorruptPack)?;
let body = previous.get(base.offset..end).ok_or(Error::CorruptPack)?;
let object = apply_delta(
Object {
kind: base.kind()?,
data: body,
},
entry.data(),
output,
limits,
)?;
Ok((object, base.depth + 1))
} else {
let EntryKind::Object(kind) = entry.kind() else {
return Err(Error::CorruptPack);
};
let output = output
.get_mut(..entry.data().len())
.ok_or(Error::BufferTooSmall)?;
output.copy_from_slice(entry.data());
Ok((Object { kind, data: output }, 0))
}
}
fn sort_index(index: &mut [MaterializedEntry]) {
fn sift(index: &mut [MaterializedEntry], mut root: usize, length: usize) {
loop {
let left = root * 2 + 1;
if left >= length {
return;
}
let larger = if left + 1 < length && index[left].id < index[left + 1].id {
left + 1
} else {
left
};
if index[root].id >= index[larger].id {
return;
}
index.swap(root, larger);
root = larger;
}
}
for root in (0..index.len() / 2).rev() {
sift(index, root, index.len());
}
for end in (1..index.len()).rev() {
index.swap(0, end);
sift(index, 0, end);
}
}