use alloc::vec;
use alloc::vec::Vec;
use core::alloc::Layout;
use core::marker::PhantomData;
use core::ptr::NonNull;
use super::node::Node;
use super::node::internal::Internal;
use super::node::leaf::Leaf;
use super::node::node_plan::NodePlan;
use super::node::page::{NodeHeader, NodePtr, PageType};
use super::node::root_page::RootPage;
use super::node_stack::MAX_DEPTH;
use super::root::Tree;
use super::shared_prefix::SharedPrefix;
use crate::file::storage::Storage;
use crate::file::{FileError, Storable, VarBytes};
use crate::inline_key::InlineKeyStrategy;
use crate::inline_key::sealed::{InlineKeySource, Sealed};
pub(crate) struct InFile<S>(PhantomData<fn() -> S>);
impl<K, S: InlineKeyStrategy<K>> Sealed<K> for InFile<S> {
const SOURCE: InlineKeySource<K> = S::SOURCE;
const IN_FILE: bool = true;
}
impl<K, S: InlineKeyStrategy<K>> InlineKeyStrategy<K> for InFile<S> {}
pub(crate) struct FileFormat<K, V, S> {
_types: PhantomData<fn() -> (K, V)>,
_strategy: PhantomData<fn() -> S>,
}
impl<K: Storable, V: Storable, S: InlineKeyStrategy<K>> FileFormat<K, V, S> {
fn root_page() -> Layout {
RootPage::<K, V, S>::page_layout()
}
fn internal_page() -> Layout {
Internal::<K, V, S>::page_layout()
}
fn leaf_page() -> Layout {
Leaf::<K, V, S>::page_layout()
}
pub(crate) fn alignment() -> usize {
Self::root_page()
.align()
.max(Self::internal_page().align())
.max(Self::leaf_page().align())
}
fn fingerprint() -> u64 {
let (leaf, internal) = (
NodePtr::<K, V, S>::leaf_layout(),
NodePtr::<K, V, S>::internal_layout(),
);
let (leaf_plan, internal_plan) = (
NodePlan::<K>::of::<S, Leaf<K, V, S>>(),
NodePlan::<K>::of::<S, Internal<K, V, S>>(),
);
let width = |plan: NodePlan<K>| plan.inline_width().map_or(0, |width| width.byte_len());
let offset = |offset: Option<usize>| offset.unwrap_or(usize::MAX);
let source = match S::SOURCE {
InlineKeySource::RecognizedStdTypes => 0,
InlineKeySource::InlineKeyTrait { .. } => 1,
};
let root = Self::root_page();
let fields = [
usize::BITS as usize,
usize::from(cfg!(target_endian = "little")),
size_of::<K>(),
align_of::<K>(),
size_of::<V>(),
align_of::<V>(),
usize::from(K::VAR_LEN),
usize::from(V::VAR_LEN),
source,
width(leaf_plan),
usize::from(leaf_plan.has_hash_tags()),
width(internal_plan),
leaf.page.size(),
leaf.page.align(),
leaf.keys.capacity,
offset(leaf.keys.inline_keys_offset),
leaf.keys.keys_offset,
leaf.keys.key_stride,
leaf.head_offset,
leaf.value_stride,
offset(leaf.hash_tags_offset),
offset(leaf.head_key_offset),
internal.page.size(),
internal.page.align(),
internal.keys.capacity,
offset(internal.keys.inline_keys_offset),
internal.keys.keys_offset,
internal.keys.key_stride,
internal.edges_offset,
internal.edge_stride,
root.size(),
root.align(),
RootPage::<K, V, S>::area_offset(),
];
let mut hash: u64 = 0xcbf2_9ce4_8422_2325;
for byte in fields
.iter()
.flat_map(|&field| (field as u64).to_le_bytes())
{
hash = (hash ^ u64::from(byte)).wrapping_mul(0x0000_0100_0000_01b3);
}
hash
}
pub(crate) fn check(file_bytes: &[u8], header: FileHeader) -> Result<(), FileError> {
const LEAF_TAG: usize = 1;
debug_assert_eq!(NodePtr::<K, V, S>::LEAF_TAG, LEAF_TAG);
let Some(root) = header.root() else {
return if header.length == 0 {
Ok(())
} else {
Err(FileError::Corrupt("entries without a tree"))
};
};
let shared_len = file_bytes
[(root & !LEAF_TAG) + RootPage::<K, V, S>::area_offset() + SharedPrefix::LEN_OFFSET];
if usize::from(shared_len) > SharedPrefix::MAX_LEN {
return Err(FileError::Corrupt("a shared prefix longer than its area"));
}
let edges = NodePtr::<K, V, S>::internal_layout();
let mut level = vec![root];
let mut entries = 0;
let mut last_reached = root & !LEAF_TAG;
for depth in 0..=MAX_DEPTH {
let leaves = level[0] & LEAF_TAG != 0;
let page = match (depth, leaves) {
(0, _) => Self::root_page(),
(_, true) => Self::leaf_page(),
(_, false) => Self::internal_page(),
};
let capacity = if leaves {
Leaf::<K, V, S>::capacity()
} else {
Internal::<K, V, S>::capacity()
};
let mut children: Vec<usize> = Vec::new();
for &tagged in &level {
let offset = tagged & !LEAF_TAG;
let bytes = file_bytes
.get(offset..offset.saturating_add(page.size()))
.filter(|_| offset.is_multiple_of(page.align()))
.ok_or(FileError::Corrupt(
"a page outside the file map, or misaligned",
))?;
let len = usize::from(u16::from_ne_bytes([
bytes[NodeHeader::LEN_OFFSET],
bytes[NodeHeader::LEN_OFFSET + 1],
]));
let has_head = bytes[NodeHeader::HAS_HEAD_OFFSET];
if len > capacity || has_head > 1 {
return Err(FileError::Corrupt("a node header out of range"));
}
if leaves {
entries += len + usize::from(has_head);
Self::check_tail(file_bytes, offset, len, has_head == 1)?;
continue;
}
for edge in 0..=len {
let slot = edges.edges_offset + edge * edges.edge_stride;
let mut word = [0; size_of::<usize>()];
word.copy_from_slice(&bytes[slot..slot + size_of::<usize>()]);
let child = offset
.checked_add(usize::from_ne_bytes(word))
.filter(|&child| child & !LEAF_TAG > last_reached)
.ok_or(FileError::Corrupt("an edge that does not lead forward"))?;
if children
.first()
.is_some_and(|&first| (first ^ child) & LEAF_TAG != 0)
{
return Err(FileError::Corrupt("children of both kinds at one depth"));
}
last_reached = child & !LEAF_TAG;
children.push(child);
}
}
if leaves {
return if entries == header.length {
Ok(())
} else {
Err(FileError::Corrupt(
"leaves holding another number of entries",
))
};
}
level = children;
}
Err(FileError::Corrupt("a tree deeper than a node stack"))
}
fn check_tail(
file_bytes: &[u8],
offset: usize,
len: usize,
has_head: bool,
) -> Result<(), FileError> {
let layout = NodePtr::<K, V, S>::leaf_layout();
let mut slots: Vec<usize> = Vec::new();
if K::VAR_LEN {
let keys =
(0..len).map(|index| layout.keys.keys_offset + index * layout.keys.key_stride);
slots.extend(keys);
if has_head {
slots.extend(layout.head_key_offset);
}
}
if V::VAR_LEN {
let first = if has_head { 0 } else { 1 };
let values =
(first..=len).map(|index| layout.head_offset + index * layout.value_stride);
slots.extend(values);
}
for slot in slots {
let at = offset + slot;
let span = file_bytes[at..at + size_of::<VarBytes>()]
.try_into()
.expect("a span's bytes");
let (bytes_len, bytes_offset) = VarBytes::decode(span);
let end = (at as u64) + u64::from(bytes_offset) + u64::from(bytes_len);
if end > file_bytes.len() as u64 {
return Err(FileError::Corrupt(
"a key or value's bytes outside the file map",
));
}
}
Ok(())
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub(crate) struct FileHeader {
fingerprint: u64,
length: usize,
root: usize,
byte_len: usize,
}
impl FileHeader {
const MAGIC: [u8; 8] = *b"bbtreeIM";
const FORMAT_VERSION: u32 = 1;
pub(crate) const LEN: usize = 64;
pub(crate) fn length(self) -> usize {
self.length
}
fn root(self) -> Option<usize> {
(self.root != 0).then_some(self.root)
}
fn to_bytes(self) -> [u8; Self::LEN] {
let mut bytes = [0; Self::LEN];
bytes[..8].copy_from_slice(&Self::MAGIC);
bytes[8..12].copy_from_slice(&Self::FORMAT_VERSION.to_le_bytes());
let words = [
self.fingerprint,
self.length as u64,
self.root as u64,
self.byte_len as u64,
];
for (index, word) in words.iter().enumerate() {
bytes[16 + 8 * index..24 + 8 * index].copy_from_slice(&word.to_le_bytes());
}
bytes
}
pub(crate) fn read<K: Storable, V: Storable, S: InlineKeyStrategy<K>>(
file_bytes: &[u8],
) -> Result<Self, FileError> {
if K::VAR_LEN && !NodePtr::<K, V, S>::has_inline_keys() {
return Err(FileError::LayoutMismatch);
}
let bytes = file_bytes.get(..Self::LEN).ok_or(FileError::NotAFileMap)?;
if bytes[..8] != Self::MAGIC {
return Err(FileError::NotAFileMap);
}
let version = u32::from_le_bytes(bytes[8..12].try_into().expect("four bytes"));
if version != Self::FORMAT_VERSION {
return Err(FileError::UnsupportedVersion(version));
}
let word = |index: usize| {
u64::from_le_bytes(
bytes[16 + 8 * index..24 + 8 * index]
.try_into()
.expect("eight bytes"),
)
};
if word(0) != FileFormat::<K, V, S>::fingerprint() {
return Err(FileError::LayoutMismatch);
}
let too_large = FileError::Corrupt("a size past the address space");
let header = Self {
fingerprint: word(0),
length: usize::try_from(word(1)).map_err(|_| too_large)?,
root: usize::try_from(word(2)).map_err(|_| too_large)?,
byte_len: usize::try_from(word(3)).map_err(|_| too_large)?,
};
if file_bytes.len() < header.byte_len {
return Err(FileError::Truncated {
byte_len: header.byte_len,
available: file_bytes.len(),
});
}
let alignment = FileFormat::<K, V, S>::alignment();
if !file_bytes.as_ptr().addr().is_multiple_of(alignment) {
return Err(FileError::Misaligned { alignment });
}
if let Some(root) = header.root() {
let offset = root & !NodePtr::<K, V, S>::LEAF_TAG;
let page = FileFormat::<K, V, S>::root_page();
if offset < Self::LEN
|| !offset.is_multiple_of(page.align())
|| offset.saturating_add(page.size()) > header.byte_len
{
return Err(FileError::Corrupt("a root page outside the file map"));
}
}
Ok(header)
}
}
type WriterNode<K, V, S> = Node<<K as Storage>::InWriter, <V as Storage>::InWriter, S>;
pub(crate) struct FilePages<'t, K: Storable, V: Storable, S> {
levels: Vec<Vec<WriterNode<K, V, S>>>,
offsets: Vec<Vec<usize>>,
length: usize,
byte_len: usize,
_tree: PhantomData<&'t Tree<K::InWriter, V::InWriter, S>>,
}
impl<'t, K, V, S> FilePages<'t, K, V, S>
where
K: Storable,
V: Storable,
S: InlineKeyStrategy<K> + InlineKeyStrategy<K::InWriter>,
{
pub(crate) fn of(tree: &'t Tree<K::InWriter, V::InWriter, S>, length: usize) -> Self {
assert!(
NodePtr::<K::InWriter, V::InWriter, S>::leaf_layout()
== NodePtr::<K, V, S>::leaf_layout()
&& NodePtr::<K::InWriter, V::InWriter, S>::internal_layout()
== NodePtr::<K, V, S>::internal_layout(),
"a writer's tree has its file map's layout"
);
assert!(
!K::VAR_LEN || NodePtr::<K, V, S>::has_inline_keys(),
"var-length keys need inline keys: the WithInlineKey strategy"
);
let mut levels = Vec::new();
if let Some(root_page) = tree.root_page() {
let mut level = vec![root_page.node()];
while !level.is_empty() {
let children = (level.iter())
.filter_map(|node| match *node {
Node::Internal(internal) => Some(internal),
Node::Leaf(_) => None,
})
.flat_map(|internal| (0..=internal.len()).map(move |edge| internal.child(edge)))
.collect();
levels.push(level);
level = children;
}
}
let mut offsets = Vec::with_capacity(levels.len());
let mut position = FileHeader::LEN;
for (depth, nodes) in levels.iter().enumerate() {
let page = Self::page_at(depth, levels.len());
debug_assert!(depth == 0 || page.size().is_multiple_of(page.align()));
let level_offsets = (nodes.iter())
.map(|&node| {
let offset = position.next_multiple_of(page.align());
position = offset + page.size() + Self::tail_len(node);
offset
})
.collect();
offsets.push(level_offsets);
}
Self {
levels,
offsets,
length,
byte_len: position,
_tree: PhantomData,
}
}
pub(crate) fn byte_len(&self) -> usize {
self.byte_len
}
fn page_at(depth: usize, depths: usize) -> Layout {
if depth == 0 {
FileFormat::<K, V, S>::root_page()
} else if depth + 1 == depths {
FileFormat::<K, V, S>::leaf_page()
} else {
FileFormat::<K, V, S>::internal_page()
}
}
fn offset_of(&self, depth: usize, index: usize) -> usize {
self.offsets[depth][index]
}
fn visit_tail(leaf: Leaf<K::InWriter, V::InWriter, S>, mut visit: impl FnMut(usize, &[u8])) {
if !K::VAR_LEN && !V::VAR_LEN {
return;
}
let layout = NodePtr::<K::InWriter, V::InWriter, S>::leaf_layout();
if leaf.has_head() {
if K::VAR_LEN {
let slot = layout
.head_key_offset
.expect("var-length keys have inline keys");
visit(slot, K::tail_bytes(leaf.head_key()));
}
if V::VAR_LEN {
visit(layout.head_offset, V::tail_bytes(leaf.head_value()));
}
}
for index in 0..leaf.len() {
let (key, value) = leaf.key_value(index);
if K::VAR_LEN {
let slot = layout.keys.keys_offset + index * layout.keys.key_stride;
visit(slot, K::tail_bytes(key));
}
if V::VAR_LEN {
visit(
layout.values_offset() + index * layout.value_stride,
V::tail_bytes(value),
);
}
}
}
fn tail_len(node: Node<K::InWriter, V::InWriter, S>) -> usize {
let Node::Leaf(leaf) = node else {
return 0;
};
let mut len = 0;
Self::visit_tail(leaf, |_, bytes| len += bytes.len());
len
}
fn header(&self) -> FileHeader {
let root = match self.levels.first().map(|level| level[0]) {
None => 0,
Some(Node::Leaf(_)) => self.offset_of(0, 0) | NodePtr::<K, V, S>::LEAF_TAG,
Some(Node::Internal(_)) => self.offset_of(0, 0),
};
FileHeader {
fingerprint: FileFormat::<K, V, S>::fingerprint(),
length: self.length,
root,
byte_len: self.byte_len,
}
}
pub(crate) fn write<E>(&self, write: &mut impl FnMut(&[u8]) -> Result<(), E>) -> Result<(), E> {
write(&self.header().to_bytes())?;
let mut position = FileHeader::LEN;
let largest_page = FileFormat::<K, V, S>::root_page()
.size()
.max(FileFormat::<K, V, S>::internal_page().size())
.max(FileFormat::<K, V, S>::leaf_page().size());
let mut page = vec![0; largest_page];
let mut tail = Vec::new();
for (depth, nodes) in self.levels.iter().enumerate() {
let size = Self::page_at(depth, self.levels.len()).size();
let mut first_child = 0;
for (index, &node) in nodes.iter().enumerate() {
let offset = self.offset_of(depth, index);
page[..offset - position].fill(0);
write(&page[..offset - position])?;
unsafe { node.node_ptr().copy_page_to(&mut page, size) };
tail.clear();
match node {
Node::Internal(internal) => {
self.offset_edges(&mut page, internal, depth, index, first_child);
first_child += internal.len() + 1;
}
Node::Leaf(leaf) => Self::write_tail(&mut page, &mut tail, leaf, size),
}
write(&page[..size])?;
write(&tail)?;
position = offset + size + tail.len();
}
}
debug_assert_eq!(position, self.byte_len);
Ok(())
}
fn write_tail(
page: &mut [u8],
tail: &mut Vec<u8>,
leaf: Leaf<K::InWriter, V::InWriter, S>,
tail_start: usize,
) {
if !K::VAR_LEN && !V::VAR_LEN {
return;
}
let layout = NodePtr::<K::InWriter, V::InWriter, S>::leaf_layout();
let capacity = Leaf::<K::InWriter, V::InWriter, S>::capacity();
let mut zero = |slot: usize| page[slot..slot + size_of::<VarBytes>()].fill(0);
for index in 0..=capacity {
if K::VAR_LEN && index < capacity {
zero(layout.keys.keys_offset + index * layout.keys.key_stride);
}
if V::VAR_LEN {
zero(layout.head_offset + index * layout.value_stride);
}
}
if K::VAR_LEN
&& let Some(slot) = layout.head_key_offset
{
zero(slot);
}
Self::visit_tail(leaf, |slot, bytes| {
let offset = u32::try_from(tail_start + tail.len() - slot)
.expect("a leaf's tail within 4 GiB of its slots");
let len = u32::try_from(bytes.len()).expect("a key or value of at most 4 GiB");
page[slot..slot + size_of::<VarBytes>()]
.copy_from_slice(&VarBytes::encode(len, offset));
tail.extend_from_slice(bytes);
});
}
fn offset_edges(
&self,
page: &mut [u8],
internal: Internal<K::InWriter, V::InWriter, S>,
depth: usize,
index: usize,
first_child: usize,
) {
let layout = NodePtr::<K::InWriter, V::InWriter, S>::internal_layout();
let offset = self.offset_of(depth, index);
let leaf_children = depth + 2 == self.levels.len();
let tag = usize::from(leaf_children) * NodePtr::<K, V, S>::LEAF_TAG;
for edge in 0..=Internal::<K::InWriter, V::InWriter, S>::capacity() {
let slot = layout.edges_offset + edge * layout.edge_stride;
let tagged_offset = if edge <= internal.len() {
(self.offset_of(depth + 1, first_child + edge) - offset) | tag
} else {
0
};
page[slot..slot + size_of::<usize>()].copy_from_slice(&tagged_offset.to_ne_bytes());
}
}
}
impl<K, V, S: InlineKeyStrategy<K>> Tree<K, V, InFile<S>> {
pub(crate) unsafe fn in_file(file_bytes: &[u8], header: FileHeader) -> Self {
let Some(root) = header.root() else {
return Self::EMPTY;
};
let start = NonNull::from(file_bytes).cast::<u8>();
let root = unsafe { NodePtr::at_offset(start, root) };
Self::of_root_page(RootPage::of_node(Node::at(root)))
}
}
#[cfg(test)]
mod tests {
use core::panic::AssertUnwindSafe;
use std::panic::catch_unwind;
use std::vec::Vec;
use super::{FileFormat, InFile};
use crate::file::{FileBuf, FileError, FileMap, FileWriter, Storable, VarBytes, VarBytesBuf};
use crate::inline_key::{Auto, InlineKeyStrategy, WithInlineKey};
use crate::tree::node::internal::Internal;
use crate::tree::node::leaf::Leaf;
use crate::tree::node::page::{NodeHeader, NodePtr};
use crate::tree::test_support::TestRng;
fn file_map_of(count: u64) -> FileBuf {
FileWriter::<u64, u64>::from_ascending((0..count).map(|key| (key, !key)))
.unwrap()
.to_buf()
}
fn root_of(file_bytes: &[u8]) -> (usize, bool) {
let root = u64::from_le_bytes(file_bytes[32..40].try_into().unwrap()) as usize;
(root & !1, root & 1 != 0)
}
fn edge_slot(offset: usize, edge: usize) -> usize {
let layout = NodePtr::<u64, u64, Auto>::internal_layout();
offset + layout.edges_offset + edge * layout.edge_stride
}
fn copy_of(file_bytes: &[u8]) -> FileBuf {
let mut copy = FileBuf::zeroed(file_bytes.len(), 4096);
copy.copy_from_slice(file_bytes);
copy
}
fn corruption(file_bytes: &[u8]) -> Option<&'static str> {
match FileMap::<u64, u64>::open(file_bytes) {
Err(FileError::Corrupt(what)) => Some(what),
Ok(_) => None,
Err(other) => panic!("a file map of the right layout refused as {other:?}"),
}
}
#[test]
fn trees_in_file_maps_have_the_unsync_layout() {
fn same_layout<K: Storable, V: Storable, S: InlineKeyStrategy<K>>() {
assert_eq!(
FileFormat::<K, V, S>::fingerprint(),
FileFormat::<K, V, InFile<S>>::fingerprint()
);
assert_eq!(
FileFormat::<K, V, S>::alignment(),
FileFormat::<K, V, InFile<S>>::alignment()
);
}
same_layout::<u64, u64, Auto>();
same_layout::<u128, [u8; 3], Auto>();
same_layout::<[u8; 32], u64, Auto>();
same_layout::<u32, u32, Auto>();
same_layout::<u64, u64, WithInlineKey>();
assert_ne!(
FileFormat::<u64, u64, Auto>::fingerprint(),
FileFormat::<i64, u64, Auto>::fingerprint() ^ 1,
);
assert_ne!(
FileFormat::<[u8; 32], u64, Auto>::fingerprint(),
FileFormat::<[u8; 32], u64, WithInlineKey>::fingerprint()
);
}
#[test]
fn pages_lie_in_level_order() {
let file_bytes = file_map_of(5000);
let (root, is_leaf) = root_of(&file_bytes);
assert!(!is_leaf);
assert!(root.is_multiple_of(FileMap::<u64, u64>::alignment()));
let leaf_size = FileFormat::<u64, u64, Auto>::leaf_page().size();
let mut level = Vec::from([root]);
let mut depth = 0;
loop {
let mut children = Vec::new();
for &offset in &level {
let len = u16::from_ne_bytes([file_bytes[offset], file_bytes[offset + 1]]) as usize;
for edge in 0..=len {
let slot = edge_slot(offset, edge);
let tagged = usize::from_ne_bytes(
file_bytes[slot..slot + size_of::<usize>()]
.try_into()
.unwrap(),
);
children.push(offset + tagged);
}
}
assert!(children.windows(2).all(|pair| pair[0] < pair[1]));
depth += 1;
if children[0] & 1 != 0 {
let leaves: Vec<usize> = children.iter().map(|child| child & !1).collect();
assert_eq!(*leaves.last().unwrap() + leaf_size, file_bytes.len());
assert!(leaves.windows(2).all(|pair| pair[1] - pair[0] == leaf_size));
break;
}
level = children;
}
assert_eq!(depth, 2, "5000 entries take a root and two levels below");
}
#[test]
fn a_node_holding_more_than_it_fits_is_refused() {
let file_bytes = file_map_of(5000);
let mut damaged = copy_of(&file_bytes);
let last_leaf = file_bytes.len() - FileFormat::<u64, u64, Auto>::leaf_page().size();
let too_many = Leaf::<u64, u64, Auto>::capacity() as u16 + 1;
damaged[last_leaf + NodeHeader::LEN_OFFSET..][..2].copy_from_slice(&too_many.to_ne_bytes());
assert_eq!(corruption(&damaged), Some("a node header out of range"));
let mut damaged = copy_of(&file_bytes);
let (root, _) = root_of(&file_bytes);
let too_many = Internal::<u64, u64, Auto>::capacity() as u16 + 1;
damaged[root..][..2].copy_from_slice(&too_many.to_ne_bytes());
assert_eq!(corruption(&damaged), Some("a node header out of range"));
}
#[test]
fn a_head_flag_that_is_no_bool_is_refused() {
let file_bytes = file_map_of(5000);
let mut damaged = copy_of(&file_bytes);
let last_leaf = file_bytes.len() - FileFormat::<u64, u64, Auto>::leaf_page().size();
damaged[last_leaf + NodeHeader::HAS_HEAD_OFFSET] = 2;
assert_eq!(corruption(&damaged), Some("a node header out of range"));
}
#[test]
fn edges_that_lead_back_outside_or_twice_are_refused() {
let file_bytes = file_map_of(5000);
let read_edge = |node: usize, edge: usize| {
let slot = edge_slot(node, edge);
usize::from_ne_bytes(
file_bytes[slot..slot + size_of::<usize>()]
.try_into()
.unwrap(),
)
};
let last_edge =
|node: usize| u16::from_ne_bytes([file_bytes[node], file_bytes[node + 1]]) as usize;
let damage = |node: usize, edge: usize, tagged: usize| {
let mut damaged = copy_of(&file_bytes);
let slot = edge_slot(node, edge);
damaged[slot..slot + size_of::<usize>()].copy_from_slice(&tagged.to_ne_bytes());
corruption(&damaged)
};
let (root, _) = root_of(&file_bytes);
assert_eq!(
damage(root, 0, 0),
Some("an edge that does not lead forward")
);
assert_eq!(
damage(root, 1, read_edge(root, 0)),
Some("an edge that does not lead forward")
);
assert_eq!(
damage(root, 0, usize::MAX - 1),
Some("an edge that does not lead forward")
);
assert_eq!(
damage(root, 0, read_edge(root, 0) | 1),
Some("children of both kinds at one depth")
);
let last_parent = root + read_edge(root, last_edge(root));
let to_last_leaf = last_edge(last_parent);
let past_the_end = file_bytes.len() - last_parent;
assert_eq!(
damage(last_parent, to_last_leaf, past_the_end | 1),
Some("a page outside the file map, or misaligned")
);
assert_eq!(
damage(
last_parent,
to_last_leaf,
read_edge(last_parent, to_last_leaf) + 8
),
Some("a page outside the file map, or misaligned")
);
}
#[test]
fn a_wrong_number_of_entries_is_refused() {
let file_bytes = file_map_of(5000);
let mut damaged = copy_of(&file_bytes);
damaged[24..32].copy_from_slice(&4999_u64.to_le_bytes());
assert_eq!(
corruption(&damaged),
Some("leaves holding another number of entries")
);
let empty = file_map_of(0);
let mut damaged = copy_of(&empty);
damaged[24..32].copy_from_slice(&1_u64.to_le_bytes());
assert_eq!(corruption(&damaged), Some("entries without a tree"));
}
#[test]
#[cfg_attr(panic = "abort", ignore = "needs unwinding")]
fn damaged_file_maps_are_refused_or_read_in_bounds() {
let rng = TestRng::new(23);
let count = if cfg!(miri) { 300 } else { 3000 };
let file_bytes = file_map_of(count);
let mut damaged = copy_of(&file_bytes);
let (mut refused, mut read) = (0, 0);
for _ in 0..if cfg!(miri) { 10 } else { 400 } {
damaged.copy_from_slice(&file_bytes);
for _ in 0..1 + rng.below(3) {
let at = 64 + rng.below(file_bytes.len() - 64);
damaged[at] = rng.below(256) as u8;
}
let Ok(map) = FileMap::<u64, u64>::open(&damaged) else {
refused += 1;
continue;
};
read += 1;
let _ = catch_unwind(AssertUnwindSafe(|| {
let found = (0..count + 100)
.step_by(7)
.filter(|key| map.get(key).is_some())
.count();
let forward = map.iter().fold(0, |entries, _| entries + 1);
let backward = map.iter().rev().fold(0, |entries, _| entries + 1);
let ranged = map.range(10..count - 10).fold(0, |entries, _| entries + 1);
(found, forward, backward, ranged)
}));
}
if cfg!(miri) {
assert!(read > 0, "{refused} refused, {read} read");
} else {
assert!(refused > 0 && read > 0, "{refused} refused, {read} read");
}
}
type VarBytesMap<'i> = FileMap<'i, VarBytes, VarBytes, WithInlineKey>;
fn var_bytes_file_map_of(count: usize) -> FileBuf {
let key = |number: usize| std::format!("{number}").into_bytes();
let mut keys: Vec<Vec<u8>> = (0..count).map(key).collect();
keys.sort();
let entries = keys.iter().map(|key| {
let value = key.repeat(key.len() % 5);
(key.as_slice().into(), value.into())
});
FileWriter::<VarBytes, VarBytes, WithInlineKey>::from_ascending(entries)
.unwrap()
.to_buf()
}
fn first_leaf(file_bytes: &[u8]) -> usize {
let (mut node, mut is_leaf) = root_of(file_bytes);
let layout = NodePtr::<VarBytes, VarBytes, WithInlineKey>::internal_layout();
while !is_leaf {
let slot = node + layout.edges_offset;
let tagged = usize::from_ne_bytes(
file_bytes[slot..slot + size_of::<usize>()]
.try_into()
.unwrap(),
);
node += tagged & !1;
is_leaf = tagged & 1 != 0;
}
node
}
#[test]
fn spans_whose_bytes_lie_outside_are_refused() {
let file_bytes = var_bytes_file_map_of(3000);
let leaf = first_leaf(&file_bytes);
let layout = NodePtr::<VarBytes, VarBytes, WithInlineKey>::leaf_layout();
for slot in [layout.keys.keys_offset, layout.values_offset()] {
for (len, offset) in [
(1, u32::MAX),
(u32::MAX, 8),
(8, (file_bytes.len() - leaf - slot) as u32),
] {
let mut damaged = copy_of(&file_bytes);
let at = leaf + slot;
damaged[at..at + 8].copy_from_slice(&VarBytes::encode(len, offset));
assert!(matches!(
VarBytesMap::open(&damaged).err(),
Some(FileError::Corrupt(
"a key or value's bytes outside the file map"
))
));
}
}
assert!(VarBytesMap::open(&file_bytes).is_ok());
}
#[test]
#[cfg_attr(panic = "abort", ignore = "needs unwinding")]
fn damaged_var_bytes_file_maps_are_refused_or_read_in_bounds() {
let rng = TestRng::new(29);
let count = if cfg!(miri) { 200 } else { 3000 };
let file_bytes = var_bytes_file_map_of(count);
let mut damaged = copy_of(&file_bytes);
let (mut refused, mut read) = (0, 0);
for _ in 0..if cfg!(miri) { 10 } else { 400 } {
damaged.copy_from_slice(&file_bytes);
for _ in 0..1 + rng.below(3) {
let at = 64 + rng.below(file_bytes.len() - 64);
damaged[at] = rng.below(256) as u8;
}
let Ok(map) = VarBytesMap::open(&damaged) else {
refused += 1;
continue;
};
read += 1;
let _ = catch_unwind(AssertUnwindSafe(|| {
let mut query = VarBytesBuf::new(b"");
let mut found = 0;
for number in (0..count + 100).step_by(7) {
query.set(std::format!("{number}").as_bytes());
found += map.get(&*query).map_or(0, VarBytes::len);
}
let bytes = |sum: usize, (key, value): (&VarBytes, &VarBytes)| {
sum + key.bytes().len() + value.bytes().len()
};
(
found,
map.iter().fold(0, bytes),
map.iter().rev().fold(0, bytes),
)
}));
}
if cfg!(miri) {
assert!(read > 0, "{refused} refused, {read} read");
} else {
assert!(refused > 0 && read > 0, "{refused} refused, {read} read");
}
}
}