use crate::layout::{
size_of_node, BlobGuid, BlobHeader, NodeType, SlotEntry, SlotEntryRaw, DATA_AREA_START,
HEADER_SIZE, MAX_SLOTS, PAGE_SIZE,
};
pub const SPILLOVER_RESERVATION: u32 = 128;
#[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(Debug, Clone, Copy)]
pub struct ExtentAllocOutcome {
pub byte_offset: u32,
}
#[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>() }
}
#[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())
}
#[must_use]
pub fn body_of_slot(&self, slot: u16) -> Option<&'a [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 = size_of_node(ntype) as usize;
if off + size > self.buf.len() {
return None;
}
Some(&self.buf[off..off + size])
}
#[must_use]
pub fn bytes_at(&self, offset: u32, len: u32) -> Option<&'a [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(&self.buf[o..end])
}
}
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);
frame.header_mut().root_slot = out.slot;
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());
}
#[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 = size_of_node(ntype) as usize;
if off + size > self.buf.len() {
return None;
}
Some(&self.buf[off..off + size])
}
pub fn body_of_slot_mut(&mut self, slot: u16) -> Option<&mut [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 = size_of_node(ntype) as usize;
if off + size > self.buf.len() {
return None;
}
Some(&mut self.buf[off..off + size])
}
pub fn alloc_node(&mut self, ntype: NodeType) -> Result<AllocOutcome, AllocError> {
if ntype == NodeType::Invalid {
return Err(AllocError::InvalidRequest);
}
let size = size_of_node(ntype);
let ntype_idx = (ntype.as_u8() - 1) as usize;
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 })
}
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,
})?;
if ntype == NodeType::Invalid {
return Err(FreeError::TypeMismatch {
slot,
tag: e.ntype_or_next_free,
});
}
let ntype_idx = (ntype.as_u8() - 1) as usize;
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_extent(&mut self, size: u32) -> Result<ExtentAllocOutcome, AllocError> {
if size == 0 {
return Err(AllocError::InvalidRequest);
}
let aligned = (size + 7) & !7;
let h = self.header();
let avail = PAGE_SIZE
.saturating_sub(h.space_used)
.saturating_sub(SPILLOVER_RESERVATION);
if avail < aligned {
return Err(AllocError::OutOfSpace {
need: aligned,
avail,
});
}
let off = h.space_used;
self.header_mut().space_used += aligned;
Ok(ExtentAllocOutcome { byte_offset: off })
}
#[must_use]
pub fn bytes_at(&self, offset: u32, len: u32) -> Option<&[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(&self.buf[o..end])
}
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, 1);
assert_eq!(h.space_used, DATA_AREA_START + 8);
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!(body.iter().all(|b| *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 + 24);
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_node(NodeType::Leaf).unwrap();
let b = frame.alloc_node(NodeType::Leaf).unwrap();
let c = frame.alloc_node(NodeType::Leaf).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 alloc_extent_does_not_touch_slot_table() {
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 e = frame.alloc_extent(13).unwrap();
assert_eq!(e.byte_offset, space_before);
assert_eq!(frame.header().space_used, space_before + 16);
assert_eq!(frame.header().num_slots, slots_before);
}
#[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_extent(remaining - 8).unwrap();
frame.alloc_extent(8).unwrap();
assert!(matches!(
frame.alloc_extent(8),
Err(AllocError::OutOfSpace { .. })
));
let _bn = frame.alloc_node(NodeType::Blob).unwrap();
}
}