use crate::layout::{
leaf_body_size, size_of_node, BlobGuid, BlobHeader, Leaf, NodeType, SlotEntry, SlotEntryRaw,
DATA_AREA_START, HEADER_SIZE, MAX_SLOTS, PAGE_SIZE,
};
use std::mem::size_of;
pub const SPILLOVER_RESERVATION: u32 = 128;
const DATA_BASE_DIV8: u32 = DATA_AREA_START / 8;
const MAX_CHILD_BIAS: u32 = (PAGE_SIZE - DATA_AREA_START) / 8 + 1;
const _: () = assert!(MAX_CHILD_BIAS < u16::MAX as u32);
#[inline]
#[must_use]
pub fn encode_child_off(byte_offset: u32) -> u16 {
debug_assert_eq!(byte_offset % 8, 0, "child body must be 8-byte aligned");
debug_assert!(
byte_offset >= DATA_AREA_START,
"child body offset below data area: {byte_offset:#x}"
);
debug_assert!(byte_offset < PAGE_SIZE, "child body offset past page");
let biased = byte_offset / 8 - DATA_BASE_DIV8 + 1;
debug_assert!(
biased >= 1,
"encoded child offset must never be the 0 sentinel"
);
debug_assert!(biased <= MAX_CHILD_BIAS);
biased as u16
}
#[inline]
#[must_use]
pub fn decode_child_off(encoded: u16) -> u32 {
debug_assert_ne!(encoded, 0, "decode_child_off on the 0 null sentinel");
(u32::from(encoded) - 1 + DATA_BASE_DIV8) * 8
}
#[inline]
#[must_use]
pub fn ntype_at_offset(buf: &[u8], off: usize) -> Option<NodeType> {
let tag_off = off.checked_add(1)?;
if tag_off >= buf.len() {
return None;
}
NodeType::from_raw(buf[tag_off])
}
const fn free_list_class(ntype: NodeType) -> usize {
ntype.as_u8() as usize - 1
}
fn leaf_body_len_at(buf: &[u8], off: usize) -> Option<usize> {
let hdr_end = off.checked_add(size_of::<Leaf>())?;
if hdr_end > buf.len() {
return None;
}
let value_len = u32::from(u16::from_le_bytes([buf[off + 2], buf[off + 3]]));
let key_len = u32::from(u16::from_le_bytes([buf[off + 4], buf[off + 5]]));
let total = leaf_body_size(key_len, value_len) as usize;
if off.checked_add(total)? > buf.len() {
return None;
}
Some(total)
}
#[derive(Debug, PartialEq, Eq)]
pub enum AllocError {
OutOfSlots,
OutOfSpace {
need: u32,
avail: u32,
},
InvalidRequest,
}
impl std::fmt::Display for AllocError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::OutOfSlots => write!(f, "blob slot table exhausted ({MAX_SLOTS} slots)"),
Self::OutOfSpace { need, avail } => {
write!(
f,
"blob data area exhausted (need {need} bytes, {avail} available)"
)
}
Self::InvalidRequest => write!(f, "invalid allocation request"),
}
}
}
impl std::error::Error for AllocError {}
#[derive(Debug, PartialEq, Eq)]
pub enum FreeError {
InvalidSlot(u16),
TypeMismatch {
slot: u16,
tag: u16,
},
}
impl std::fmt::Display for FreeError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::InvalidSlot(s) => write!(f, "free_node: invalid slot index {s}"),
Self::TypeMismatch { slot, tag } => {
write!(f, "free_node: slot {slot} has invalid type tag {tag}")
}
}
}
}
impl std::error::Error for FreeError {}
#[derive(Debug, Clone, Copy)]
pub struct AllocOutcome {
pub slot: u16,
}
#[derive(Clone, Copy)]
pub struct BlobFrameRef<'a> {
buf: &'a [u8],
}
impl<'a> BlobFrameRef<'a> {
#[must_use]
pub fn wrap(buf: &'a [u8]) -> Self {
assert_eq!(
buf.len(),
PAGE_SIZE as usize,
"BlobFrameRef requires PAGE_SIZE buffer"
);
Self { buf }
}
#[must_use]
pub fn header(&self) -> &'a BlobHeader {
unsafe { &*self.buf.as_ptr().cast::<BlobHeader>() }
}
#[inline]
#[must_use]
pub fn ntype_at(&self, byte_offset: u32) -> Option<NodeType> {
ntype_at_offset(self.buf, byte_offset as usize)
}
#[must_use]
pub fn body_at_offset(&self, byte_offset: u32) -> Option<&'a [u8]> {
let off = byte_offset as usize;
let ntype = ntype_at_offset(self.buf, off)?;
if ntype == NodeType::Invalid {
return None;
}
let size = if ntype == NodeType::Leaf {
leaf_body_len_at(self.buf, off)?
} else {
size_of_node(ntype) as usize
};
let end = off.checked_add(size)?;
if end > self.buf.len() {
return None;
}
Some(&self.buf[off..end])
}
#[inline]
pub fn prefetch_at(&self, byte_offset: u32) {
let off = byte_offset as usize;
if off < self.buf.len() {
crate::engine::prefetch_read_data(unsafe { self.buf.as_ptr().add(off) });
}
}
}
pub struct BlobFrame<'a> {
buf: &'a mut [u8],
}
impl<'a> BlobFrame<'a> {
pub fn wrap(buf: &'a mut [u8]) -> Self {
assert_eq!(
buf.len(),
PAGE_SIZE as usize,
"BlobFrame requires PAGE_SIZE buffer"
);
Self { buf }
}
#[must_use]
pub fn as_ref(&self) -> BlobFrameRef<'_> {
BlobFrameRef { buf: self.buf }
}
pub fn init(buf: &'a mut [u8], guid: BlobGuid) -> Result<Self, AllocError> {
assert_eq!(buf.len(), PAGE_SIZE as usize);
for b in buf.iter_mut() {
*b = 0;
}
let mut frame = Self { buf };
{
let h = frame.header_mut();
h.blob_guid = guid;
h.space_used = DATA_AREA_START;
}
let out = frame.alloc_node(NodeType::EmptyRoot)?;
debug_assert_eq!(out.slot, 1);
let root_off = frame
.offset_of_slot(out.slot)
.expect("freshly allocated sentinel has a slot offset");
if let Some(body) = frame.bytes_at_mut(root_off, 8) {
body[1] = NodeType::EmptyRoot.as_u8();
}
frame.header_mut().root_slot = encode_child_off(root_off);
Ok(frame)
}
#[must_use]
pub fn header(&self) -> &BlobHeader {
unsafe { &*self.buf.as_ptr().cast::<BlobHeader>() }
}
#[must_use]
pub fn header_mut(&mut self) -> &mut BlobHeader {
unsafe { &mut *self.buf.as_mut_ptr().cast::<BlobHeader>() }
}
#[must_use]
pub fn slot_entry(&self, slot: u16) -> Option<SlotEntry> {
let h = self.header();
if slot == 0 || slot > h.num_slots {
return None;
}
let off = HEADER_SIZE as usize + (slot as usize - 1) * 4;
let raw_bytes = &self.buf[off..off + 4];
let raw = u32::from_le_bytes([raw_bytes[0], raw_bytes[1], raw_bytes[2], raw_bytes[3]]);
Some(SlotEntryRaw(raw).decode())
}
fn write_slot_entry(&mut self, slot: u16, entry: SlotEntry) {
debug_assert!(slot >= 1);
debug_assert!(slot <= self.header().num_slots || slot == self.header().num_slots + 1);
let off = HEADER_SIZE as usize + (slot as usize - 1) * 4;
let raw = entry.raw().0;
self.buf[off..off + 4].copy_from_slice(&raw.to_le_bytes());
}
#[cfg_attr(not(test), allow(dead_code))]
#[must_use]
pub fn body_of_slot(&self, slot: u16) -> Option<&[u8]> {
let e = self.slot_entry(slot)?;
let ntype = e.node_type()?;
if ntype == NodeType::Invalid {
return None;
}
let off = e.byte_offset() as usize;
let size = if ntype == NodeType::Leaf {
leaf_body_len_at(self.buf, off)?
} else {
size_of_node(ntype) as usize
};
if off + size > self.buf.len() {
return None;
}
Some(&self.buf[off..off + size])
}
#[inline]
#[must_use]
pub fn ntype_at(&self, byte_offset: u32) -> Option<NodeType> {
ntype_at_offset(self.buf, byte_offset as usize)
}
#[must_use]
pub fn body_at_offset(&self, byte_offset: u32) -> Option<&[u8]> {
let off = byte_offset as usize;
let ntype = ntype_at_offset(self.buf, off)?;
if ntype == NodeType::Invalid {
return None;
}
let size = if ntype == NodeType::Leaf {
leaf_body_len_at(self.buf, off)?
} else {
size_of_node(ntype) as usize
};
let end = off.checked_add(size)?;
if end > self.buf.len() {
return None;
}
Some(&self.buf[off..end])
}
pub fn body_at_offset_mut(&mut self, byte_offset: u32) -> Option<&mut [u8]> {
let off = byte_offset as usize;
let ntype = ntype_at_offset(self.buf, off)?;
if ntype == NodeType::Invalid {
return None;
}
let size = if ntype == NodeType::Leaf {
leaf_body_len_at(self.buf, off)?
} else {
size_of_node(ntype) as usize
};
let end = off.checked_add(size)?;
if end > self.buf.len() {
return None;
}
Some(&mut self.buf[off..end])
}
#[must_use]
pub fn offset_of_slot(&self, slot: u16) -> Option<u32> {
self.slot_entry(slot).map(SlotEntry::byte_offset)
}
pub fn note_abandoned(&mut self, byte_offset: u32) {
let size = {
let off = byte_offset as usize;
match ntype_at_offset(self.buf, off) {
Some(NodeType::Leaf) => leaf_body_len_at(self.buf, off).unwrap_or(0) as u32,
Some(nt) if nt != NodeType::Invalid => size_of_node(nt),
_ => 0,
}
};
let h = self.header_mut();
h.dead_bytes = h.dead_bytes.saturating_add(size);
}
pub fn alloc_node(&mut self, ntype: NodeType) -> Result<AllocOutcome, AllocError> {
let outcome = self.alloc_node_inner(ntype)?;
if ntype == NodeType::Blob {
self.header_mut().num_ext_blobs = self.header().num_ext_blobs.saturating_add(1);
}
Ok(outcome)
}
fn alloc_node_inner(&mut self, ntype: NodeType) -> Result<AllocOutcome, AllocError> {
if ntype == NodeType::Invalid {
return Err(AllocError::InvalidRequest);
}
let size = size_of_node(ntype);
let ntype_idx = free_list_class(ntype);
let free_head = self.header().free_list_head[ntype_idx];
if free_head != 0 {
let e = self
.slot_entry(free_head)
.ok_or(AllocError::InvalidRequest)?;
let next_free = e.next_free();
self.header_mut().free_list_head[ntype_idx] = next_free;
let off = e.byte_offset();
self.write_slot_entry(free_head, SlotEntry::live(ntype, off));
self.header_mut().gap_space = self.header().gap_space.wrapping_add(size);
return Ok(AllocOutcome { slot: free_head });
}
let sibling_idx_opt = match ntype {
NodeType::Blob => Some((NodeType::Prefix.as_u8() - 1) as usize),
NodeType::Prefix => Some((NodeType::Blob.as_u8() - 1) as usize),
_ => None,
};
if let Some(sibling_idx) = sibling_idx_opt {
let sibling_head = self.header().free_list_head[sibling_idx];
if sibling_head != 0 {
let e = self
.slot_entry(sibling_head)
.ok_or(AllocError::InvalidRequest)?;
let next_free = e.next_free();
self.header_mut().free_list_head[sibling_idx] = next_free;
let off = e.byte_offset();
self.write_slot_entry(sibling_head, SlotEntry::live(ntype, off));
self.header_mut().gap_space = self.header().gap_space.wrapping_add(size);
return Ok(AllocOutcome { slot: sibling_head });
}
}
let h = self.header();
if h.num_slots >= MAX_SLOTS as u16 {
return Err(AllocError::OutOfSlots);
}
let raw_avail = PAGE_SIZE.saturating_sub(h.space_used);
let avail = if ntype == NodeType::Blob {
raw_avail
} else {
raw_avail.saturating_sub(SPILLOVER_RESERVATION)
};
if avail < size {
return Err(AllocError::OutOfSpace { need: size, avail });
}
let body_off = h.space_used;
debug_assert!(body_off >= DATA_AREA_START);
debug_assert!(body_off + size <= PAGE_SIZE);
let new_slot = h.num_slots + 1; self.write_slot_entry(new_slot, SlotEntry::live(ntype, body_off));
let h = self.header_mut();
h.num_slots += 1;
h.space_used += size;
h.gap_space = h.gap_space.wrapping_add(size);
Ok(AllocOutcome { slot: new_slot })
}
#[cfg_attr(not(test), allow(dead_code))]
pub fn free_node(&mut self, slot: u16) -> Result<(), FreeError> {
let e = self.slot_entry(slot).ok_or(FreeError::InvalidSlot(slot))?;
let ntype = e.node_type().ok_or(FreeError::TypeMismatch {
slot,
tag: e.ntype_or_next_free,
})?;
self.free_node_inner(slot)?;
if ntype == NodeType::Blob {
self.header_mut().num_ext_blobs = self.header().num_ext_blobs.saturating_sub(1);
}
Ok(())
}
#[cfg_attr(not(test), allow(dead_code))]
fn free_node_inner(&mut self, slot: u16) -> Result<(), FreeError> {
let e = self.slot_entry(slot).ok_or(FreeError::InvalidSlot(slot))?;
let ntype = e.node_type().ok_or(FreeError::TypeMismatch {
slot,
tag: e.ntype_or_next_free,
})?;
if ntype == NodeType::Invalid {
return Err(FreeError::TypeMismatch {
slot,
tag: e.ntype_or_next_free,
});
}
let ntype_idx = free_list_class(ntype);
let old_head = self.header().free_list_head[ntype_idx];
let off = e.byte_offset();
self.write_slot_entry(slot, SlotEntry::freed(old_head, off));
self.header_mut().free_list_head[ntype_idx] = slot;
Ok(())
}
pub fn alloc_leaf(&mut self, total_aligned: u32) -> Result<AllocOutcome, AllocError> {
if total_aligned == 0 {
return Err(AllocError::InvalidRequest);
}
debug_assert_eq!(total_aligned & 7, 0, "alloc_leaf size must be 8-aligned");
let h = self.header();
if h.num_slots >= MAX_SLOTS as u16 {
return Err(AllocError::OutOfSlots);
}
let avail = PAGE_SIZE
.saturating_sub(h.space_used)
.saturating_sub(SPILLOVER_RESERVATION);
if avail < total_aligned {
return Err(AllocError::OutOfSpace {
need: total_aligned,
avail,
});
}
let body_off = h.space_used;
debug_assert!(body_off >= DATA_AREA_START);
debug_assert!(body_off + total_aligned <= PAGE_SIZE);
let new_slot = h.num_slots + 1; self.write_slot_entry(new_slot, SlotEntry::live(NodeType::Leaf, body_off));
let h = self.header_mut();
h.num_slots += 1;
h.space_used += total_aligned;
h.gap_space = h.gap_space.wrapping_add(total_aligned);
Ok(AllocOutcome { slot: new_slot })
}
pub fn bytes_at_mut(&mut self, offset: u32, len: u32) -> Option<&mut [u8]> {
let o = offset as usize;
let l = len as usize;
let end = o.checked_add(l)?;
if end > self.buf.len() {
return None;
}
Some(&mut self.buf[o..end])
}
}
#[cfg(test)]
mod tests {
use super::*;
fn fresh() -> Vec<u8> {
vec![0u8; PAGE_SIZE as usize]
}
#[test]
fn init_seeds_empty_root_sentinel() {
let mut buf = fresh();
let frame = BlobFrame::init(&mut buf, [0xAB; 16]).unwrap();
let h = frame.header();
assert_eq!(h.blob_guid, [0xAB; 16]);
assert_eq!(h.num_slots, 1);
assert_eq!(h.root_slot, encode_child_off(DATA_AREA_START));
assert_eq!(h.root_slot, 1);
assert_eq!(h.space_used, DATA_AREA_START + 8);
let root_off = decode_child_off(h.root_slot);
assert_eq!(frame.ntype_at(root_off), Some(NodeType::EmptyRoot));
let e = frame.slot_entry(1).unwrap();
assert_eq!(e.node_type(), Some(NodeType::EmptyRoot));
let body = frame.body_of_slot(1).unwrap();
assert_eq!(body.len(), 8);
assert_eq!(body[1], NodeType::EmptyRoot.as_u8());
assert!(body.iter().enumerate().all(|(i, b)| i == 1 || *b == 0));
}
#[test]
fn alloc_node_bumps_space_used() {
let mut buf = fresh();
let mut frame = BlobFrame::init(&mut buf, [0; 16]).unwrap();
let space_before = frame.header().space_used;
let out = frame.alloc_node(NodeType::Node4).unwrap();
assert_eq!(out.slot, 2);
assert_eq!(frame.header().space_used, space_before + 16);
assert_eq!(frame.header().num_slots, 2);
let e = frame.slot_entry(2).unwrap();
assert_eq!(e.node_type(), Some(NodeType::Node4));
assert_eq!(e.byte_offset(), space_before);
}
#[test]
fn free_then_realloc_reuses_slot() {
let mut buf = fresh();
let mut frame = BlobFrame::init(&mut buf, [0; 16]).unwrap();
let a = frame.alloc_leaf(24).unwrap();
let b = frame.alloc_leaf(24).unwrap();
let c = frame.alloc_leaf(24).unwrap();
assert_eq!(a.slot, 2);
assert_eq!(b.slot, 3);
assert_eq!(c.slot, 4);
frame.free_node(b.slot).unwrap();
let r = frame.alloc_node(NodeType::Leaf).unwrap();
assert_eq!(r.slot, 3);
}
#[test]
fn num_ext_blobs_tracks_live_blob_nodes() {
let mut buf = fresh();
let mut frame = BlobFrame::init(&mut buf, [0; 16]).unwrap();
assert_eq!(frame.header().num_ext_blobs, 0);
let blob = frame.alloc_node(NodeType::Blob).unwrap();
assert_eq!(frame.header().num_ext_blobs, 1);
frame.free_node(blob.slot).unwrap();
assert_eq!(frame.header().num_ext_blobs, 0);
let prefix = frame.alloc_node(NodeType::Prefix).unwrap();
assert_eq!(frame.header().num_ext_blobs, 0);
frame.free_node(prefix.slot).unwrap();
assert_eq!(frame.header().num_ext_blobs, 0);
let blob_from_prefix_slot = frame.alloc_node(NodeType::Blob).unwrap();
assert_eq!(frame.header().num_ext_blobs, 1);
frame.free_node(blob_from_prefix_slot.slot).unwrap();
assert_eq!(frame.header().num_ext_blobs, 0);
}
#[test]
fn alloc_leaf_registers_slot_and_is_self_describing() {
use crate::layout::{leaf_body_size, Leaf};
let mut buf = fresh();
let mut frame = BlobFrame::init(&mut buf, [0; 16]).unwrap();
let space_before = frame.header().space_used;
let slots_before = frame.header().num_slots;
let total = leaf_body_size(5, 7);
assert_eq!(total, 32);
let out = frame.alloc_leaf(total).unwrap();
assert_eq!(out.slot, slots_before + 1);
assert_eq!(frame.header().num_slots, slots_before + 1);
assert_eq!(frame.header().space_used, space_before + total);
let e = frame.slot_entry(out.slot).unwrap();
assert_eq!(e.node_type(), Some(NodeType::Leaf));
assert_eq!(e.byte_offset(), space_before);
let body_off = e.byte_offset();
assert_eq!(frame.body_of_slot(out.slot).unwrap().len(), 16);
let leaf = Leaf::live(5, 7, 99, 0xAB);
{
let body = frame.bytes_at_mut(body_off, total).unwrap();
assert_eq!(body.len(), total as usize);
let bytes = unsafe {
std::slice::from_raw_parts(
std::ptr::from_ref::<Leaf>(&leaf).cast::<u8>(),
std::mem::size_of::<Leaf>(),
)
};
body[..16].copy_from_slice(bytes);
}
assert_eq!(frame.body_of_slot(out.slot).unwrap().len(), total as usize);
}
#[test]
fn out_of_space_at_data_area_boundary() {
let mut buf = fresh();
let mut frame = BlobFrame::init(&mut buf, [0; 16]).unwrap();
let remaining = PAGE_SIZE - frame.header().space_used - SPILLOVER_RESERVATION;
frame.alloc_leaf(remaining - 8).unwrap();
frame.alloc_leaf(8).unwrap();
assert!(matches!(
frame.alloc_leaf(8),
Err(AllocError::OutOfSpace { .. })
));
let _bn = frame.alloc_node(NodeType::Blob).unwrap();
}
}