use std::collections::BTreeMap;
use gix_error::{ErrorExt, ExnResult, Message, ResourceExhaustionKind, ResultExt};
use super::Tree;
pub(super) type RefDeltaChildren = BTreeMap<gix_hash::ObjectId, Vec<u32>>;
pub struct Item<T> {
pub offset: crate::data::Offset,
pub next_offset: crate::data::Offset,
pub data: T,
children: Vec<u32>,
}
impl<T> Item<T> {
pub(super) fn children(&self) -> &[u32] {
&self.children
}
pub(super) fn extend_children(&mut self, children: impl IntoIterator<Item = u32>) {
self.children.extend(children);
}
}
pub(super) enum NodeKind {
Root,
Child,
}
fn allocation_error(kind: ResourceExhaustionKind) -> Message {
gix_error::resource_exhaustion(kind, "The pack delta tree is too large to fit in memory")
}
impl<T> Tree<T> {
pub(crate) fn with_capacity(num_objects: usize, alloc_limit_bytes: Option<usize>) -> ExnResult<Self> {
let capacity = num_objects / 2;
let allocation_bytes = capacity
.checked_mul(std::mem::size_of::<Item<T>>())
.ok_or_else(|| allocation_error(ResourceExhaustionKind::AllocationFailure).raise_erased())?;
if alloc_limit_bytes.is_some_and(|limit| allocation_bytes > limit) {
return Err(allocation_error(ResourceExhaustionKind::AllocationLimit).raise_erased());
}
let mut root_items = Vec::new();
root_items
.try_reserve_exact(capacity)
.or_raise_erased(|| allocation_error(ResourceExhaustionKind::AllocationFailure))?;
let mut child_items = Vec::new();
child_items
.try_reserve_exact(capacity)
.or_raise_erased(|| allocation_error(ResourceExhaustionKind::AllocationFailure))?;
Ok(Tree {
root_items,
child_items,
last_seen: None,
future_child_offsets: Vec::new(),
ref_child_indices: BTreeMap::new(),
})
}
pub(super) fn num_items(&self) -> usize {
self.root_items.len() + self.child_items.len()
}
pub(super) fn take_root_child_and_refs(self) -> (Vec<Item<T>>, Vec<Item<T>>, RefDeltaChildren) {
(self.root_items, self.child_items, self.ref_child_indices)
}
pub(super) fn assert_is_incrementing_and_update_next_offset(&mut self, offset: crate::data::Offset) -> ExnResult {
let items = match &self.last_seen {
Some(NodeKind::Root) => &mut self.root_items,
Some(NodeKind::Child) => &mut self.child_items,
None => return Ok(()),
};
let item = &mut items.last_mut().expect("last seen won't lie");
if offset <= item.offset {
return Err(gix_error::corruption(format!(
"Pack offsets must only increment. The previous pack offset was {}, the current one is {offset}",
item.offset
))
.raise_erased());
}
item.next_offset = offset;
Ok(())
}
pub(super) fn set_pack_entries_end_and_resolve_ref_offsets(
&mut self,
pack_entries_end: crate::data::Offset,
) -> ExnResult {
if !self.future_child_offsets.is_empty() {
for (parent_offset, child_index) in self.future_child_offsets.drain(..) {
if let Ok(i) = self.child_items.binary_search_by_key(&parent_offset, |i| i.offset) {
self.child_items[i].children.push(child_index as u32);
} else if let Ok(i) = self.root_items.binary_search_by_key(&parent_offset, |i| i.offset) {
self.root_items[i].children.push(child_index as u32);
} else {
return Err(gix_error::message!(
"The base at {parent_offset} was referred to by a ref-delta, but it was never added to the tree as if the pack was still thin."
)
.raise_erased());
}
}
}
self.assert_is_incrementing_and_update_next_offset(pack_entries_end)
.expect("BUG: pack now is smaller than all previously seen entries");
Ok(())
}
pub(crate) fn add_root(&mut self, offset: crate::data::Offset, data: T) -> ExnResult {
self.assert_is_incrementing_and_update_next_offset(offset)?;
self.last_seen = NodeKind::Root.into();
self.root_items.push(Item {
offset,
next_offset: 0,
data,
children: Default::default(),
});
Ok(())
}
pub(crate) fn add_child(
&mut self,
base_offset: crate::data::Offset,
offset: crate::data::Offset,
data: T,
) -> ExnResult {
self.assert_is_incrementing_and_update_next_offset(offset)?;
let next_child_index = self.child_items.len();
if let Ok(i) = self.child_items.binary_search_by_key(&base_offset, |i| i.offset) {
self.child_items[i].children.push(next_child_index as u32);
} else if let Ok(i) = self.root_items.binary_search_by_key(&base_offset, |i| i.offset) {
self.root_items[i].children.push(next_child_index as u32);
} else {
self.future_child_offsets.push((base_offset, next_child_index));
}
self.last_seen = NodeKind::Child.into();
self.child_items.push(Item {
offset,
next_offset: 0,
data,
children: Default::default(),
});
Ok(())
}
#[cfg(feature = "streaming-input")]
pub(crate) fn add_child_by_id(
&mut self,
base_id: gix_hash::ObjectId,
offset: crate::data::Offset,
data: T,
) -> ExnResult {
self.assert_is_incrementing_and_update_next_offset(offset)?;
let child_index = self.child_items.len() as u32;
self.ref_child_indices.entry(base_id).or_default().push(child_index);
self.last_seen = NodeKind::Child.into();
self.child_items.push(Item {
offset,
next_offset: 0,
data,
children: Default::default(),
});
Ok(())
}
}
#[cfg(test)]
mod tests {
#[test]
fn allocation_failure_is_reported() {
let err = super::Tree::<()>::with_capacity(usize::MAX, None)
.err()
.expect("an impossible attacker-controlled capacity must return an allocation error");
insta::assert_debug_snapshot!(err, "allocation failure is reported", @"The pack delta tree is too large to fit in memory");
assert_eq!(
err.classify().find_map(|classification| match classification.class() {
gix_error::Class::ResourceExhaustion(kind) => Some(kind),
_ => None,
}),
Some(gix_error::ResourceExhaustionKind::AllocationFailure)
);
let err = super::Tree::<()>::with_capacity(2, Some(0))
.err()
.expect("the configured allocation limit must apply to delta-tree storage");
insta::assert_debug_snapshot!(err, "allocation failure is reported", @"The pack delta tree is too large to fit in memory");
assert_eq!(
err.classify().find_map(|classification| match classification.class() {
gix_error::Class::ResourceExhaustion(kind) => Some(kind),
_ => None,
}),
Some(gix_error::ResourceExhaustionKind::AllocationLimit)
);
}
mod from_offsets_in_pack {
use std::sync::atomic::AtomicBool;
use crate as pack;
const SMALL_PACK_INDEX: &str = "objects/pack/pack-a2bf8e71d8c18879e499335762dd95119d93d9f1.idx";
const SMALL_PACK: &str = "objects/pack/pack-a2bf8e71d8c18879e499335762dd95119d93d9f1.pack";
const INDEX_V1: &str = "objects/pack/pack-c0438c19fb16422b6bbcce24387b3264416d485b.idx";
const PACK_FOR_INDEX_V1: &str = "objects/pack/pack-c0438c19fb16422b6bbcce24387b3264416d485b.pack";
use gix_testtools::fixture_path;
#[test]
fn v1() -> gix_testtools::Result {
tree(INDEX_V1, PACK_FOR_INDEX_V1)
}
#[test]
fn v2() -> gix_testtools::Result {
tree(SMALL_PACK_INDEX, SMALL_PACK)
}
#[test]
fn invalid_ofs_delta_base_distance_is_reported() -> gix_testtools::Result {
let first_entry_offset = pack::data::header::SIZE as pack::data::Offset;
let pack_file = gix_testtools::tempfile::NamedTempFile::new()?;
let mut pack_data = pack::data::header::encode(pack::data::Version::V2, 1).to_vec();
pack::data::entry::Header::OfsDelta {
base_distance: first_entry_offset + 1,
}
.write_to(0, &mut pack_data)?;
std::fs::write(pack_file.path(), pack_data)?;
let result = crate::cache::delta::Tree::from_offsets_in_pack(
pack_file.path(),
std::iter::once(()),
&|_| first_entry_offset,
&|_| None,
&mut gix_features::progress::Discard,
&AtomicBool::new(false),
gix_hash::Kind::Sha1,
);
assert!(result.is_err(), "an out-of-bounds delta base is corrupt pack data");
Ok(())
}
fn tree(index_path: &str, pack_path: &str) -> gix_testtools::Result {
let idx = pack::index::File::at(fixture_path(index_path), gix_hash::Kind::Sha1)?;
crate::cache::delta::Tree::from_offsets_in_pack(
&fixture_path(pack_path),
idx.sorted_offsets().into_iter(),
&|ofs| *ofs,
&|id| idx.lookup(id).map(|index| idx.pack_offset_at_index(index)),
&mut gix_features::progress::Discard,
&AtomicBool::new(false),
gix_hash::Kind::Sha1,
)?;
Ok(())
}
}
mod size {
use gix_testtools::size_ok;
use super::super::Item;
#[test]
fn size_of_pack_tree_item() {
let actual = std::mem::size_of::<[Item<()>; 7_500_000]>();
let expected = 300_000_000;
assert!(
size_ok(actual, expected),
"we don't want these to grow unnoticed: {actual} <~ {expected}"
);
}
#[test]
fn size_of_pack_verify_data_structure() {
pub struct EntryWithDefault {
_index_entry: crate::index::Entry,
_kind: gix_object::Kind,
_object_size: u64,
_decompressed_size: u64,
_compressed_size: u64,
_header_size: u16,
_level: u16,
}
let actual = std::mem::size_of::<[Item<EntryWithDefault>; 7_500_000]>();
let sha1 = 840_000_000;
let sha256_extra = 120_000_000;
let expected = sha1 + sha256_extra;
assert!(
size_ok(actual, expected),
"we don't want these to grow unnoticed: {actual} <~ {expected}"
);
}
}
}