use super::{BStackAllocator, BStackSlice};
use crate::BStack;
#[cfg(not(feature = "atomic"))]
use core::cell::Cell;
#[cfg(not(feature = "atomic"))]
use core::marker::PhantomData;
#[cfg(feature = "atomic")]
use std::sync::Mutex;
use std::{collections::HashSet, fmt, io};
#[cfg(feature = "set")]
const ALCK_MAGIC: [u8; 8] = *b"ALCK\x00\x01\x01\x00";
#[cfg(feature = "set")]
const ALCK_MAGIC_PREFIX: [u8; 6] = *b"ALCK\x00\x01";
#[cfg_attr(not(feature = "atomic"), doc = "```compile_fail")]
#[cfg_attr(feature = "atomic", doc = "```")]
#[cfg(feature = "set")]
pub struct CheckedSlabBStackAllocator {
stack: BStack,
block_size: u64,
#[cfg(feature = "atomic")]
lock: Mutex<()>,
#[cfg(not(feature = "atomic"))]
_not_sync: PhantomData<Cell<()>>,
}
#[cfg(feature = "set")]
enum BlockClass {
Free,
Leaked,
InUse(u64),
Suspicious,
}
#[cfg(feature = "set")]
enum ResyncOutcome {
Resync(u64),
DiscardTail,
LeaveLeaked,
}
#[cfg(feature = "set")]
impl CheckedSlabBStackAllocator {
const OFFSET_SIZE: u64 = 24;
const HEADER_SIZE: u64 = 24;
const ARENA_START: u64 = Self::OFFSET_SIZE + Self::HEADER_SIZE;
const FREE_HEAD_OFFSET: u64 = Self::OFFSET_SIZE + 16;
const OVERHEAD: u64 = 8;
const MIN_BLOCK_SIZE: u64 = 16;
const MIN_DATA_SIZE: u64 = Self::MIN_BLOCK_SIZE - Self::OVERHEAD;
const SENTINEL: u64 = 0;
const IN_USE_BIT: u64 = 0x8000_0000_0000_0000;
const BLOCKS_MASK: u64 = !Self::IN_USE_BIT;
pub fn new(stack: BStack, data_size: u64) -> io::Result<Self> {
if !stack.is_empty()? {
return Err(io::Error::new(
io::ErrorKind::InvalidInput,
"stack is not empty; use CheckedSlabBStackAllocator::open to reopen an existing allocator",
));
}
if data_size < Self::MIN_DATA_SIZE {
return Err(io::Error::new(
io::ErrorKind::InvalidInput,
format!("data_size ({data_size}) must be >= {}", Self::MIN_DATA_SIZE),
));
}
let block_size = data_size.checked_add(Self::OVERHEAD).ok_or_else(|| {
io::Error::new(
io::ErrorKind::InvalidInput,
"data_size is too large (overflows u64)",
)
})?;
if usize::try_from(block_size).is_err() {
return Err(io::Error::new(
io::ErrorKind::InvalidInput,
"data_size is too large for this platform",
));
}
let mut hdr = [0u8; Self::ARENA_START as usize];
let off = Self::OFFSET_SIZE as usize;
hdr[off..off + 8].copy_from_slice(&ALCK_MAGIC);
hdr[off + 8..off + 16].copy_from_slice(&block_size.to_le_bytes());
stack.push(hdr)?;
Ok(Self {
stack,
block_size,
#[cfg(feature = "atomic")]
lock: Mutex::new(()),
#[cfg(not(feature = "atomic"))]
_not_sync: PhantomData,
})
}
pub fn open(stack: BStack) -> io::Result<Self> {
if stack.is_empty()? {
return Err(io::Error::new(
io::ErrorKind::InvalidInput,
"stack is empty; use CheckedSlabBStackAllocator::new to create a new allocator",
));
}
let stack_len = stack.len()?;
if stack_len < Self::ARENA_START {
return Err(io::Error::new(
io::ErrorKind::InvalidData,
"stack too short to contain allocator header",
));
}
let mut header = [0u8; Self::HEADER_SIZE as usize];
stack.get_into(Self::OFFSET_SIZE, &mut header)?;
if header[..ALCK_MAGIC_PREFIX.len()] != ALCK_MAGIC_PREFIX {
return Err(io::Error::new(
io::ErrorKind::InvalidData,
"invalid magic: not a CheckedSlabBStackAllocator file",
));
}
let stored_block_size = u64::from_le_bytes(header[8..16].try_into().unwrap());
if stored_block_size < Self::MIN_BLOCK_SIZE {
return Err(io::Error::new(
io::ErrorKind::InvalidData,
format!("stored block_size ({stored_block_size}) is invalid"),
));
}
if usize::try_from(stored_block_size).is_err() {
return Err(io::Error::new(
io::ErrorKind::InvalidData,
"stored block_size is too large for this platform",
));
}
let stored_free_head = u64::from_le_bytes(header[16..24].try_into().unwrap());
if stored_free_head != Self::SENTINEL
&& (stored_free_head < Self::ARENA_START
|| (stored_free_head - Self::ARENA_START) % stored_block_size != 0
|| stored_free_head >= stack_len)
{
return Err(io::Error::new(
io::ErrorKind::InvalidData,
format!("stored free_head ({stored_free_head}) is not a valid block offset"),
));
}
let arena_bytes = stack_len - Self::ARENA_START; if arena_bytes % stored_block_size != 0 {
return Err(io::Error::new(
io::ErrorKind::InvalidData,
"stack tail is not aligned to block_size",
));
}
if stored_free_head != Self::SENTINEL {
let mut prefix = [0u8; 8];
stack.get_into(stored_free_head, &mut prefix)?;
if u64::from_le_bytes(prefix) != 0 {
return Err(io::Error::new(
io::ErrorKind::InvalidData,
"free-list head block is marked in use; free list corrupt",
));
}
}
let allocator = Self {
stack,
block_size: stored_block_size,
#[cfg(feature = "atomic")]
lock: Mutex::new(()),
#[cfg(not(feature = "atomic"))]
_not_sync: PhantomData,
};
allocator.recover()?;
Ok(allocator)
}
pub fn data_size(&self) -> u64 {
self.block_size - Self::OVERHEAD
}
const MAX_RECOVER_REGION: usize = 1 << 26;
pub fn recover(&self) -> io::Result<u64> {
#[cfg(feature = "atomic")]
let _guard = self.lock.lock().unwrap();
let stack_len = self.stack.len()?;
if stack_len <= Self::ARENA_START {
return Ok(0);
}
let bs = self.block_size;
let (free, free_corrupt) = self.scan_free_list(stack_len)?;
let mut reclaim: Vec<u64> = Vec::new();
let mut unsure: u64 = 0;
let mut tailcut: Option<u64> = None;
let mut p = Self::ARENA_START;
'scan: while p < stack_len {
match self.classify(p, stack_len, &free)? {
BlockClass::Free => p += bs,
BlockClass::Leaked => {
if free_corrupt {
unsure += 1;
} else {
reclaim.push(p);
}
p += bs;
}
BlockClass::InUse(n) => p += n * bs,
BlockClass::Suspicious => {
let idx = free.partition_point(|&x| x <= p);
if let Some(&f) = free.get(idx) {
unsure += (f - p) / bs;
p = f;
} else {
match self.resync_tail(p, stack_len, &free)? {
ResyncOutcome::Resync(q) => {
unsure += (q - p) / bs;
p = q;
}
ResyncOutcome::DiscardTail => {
if free_corrupt {
unsure += (stack_len - p) / bs;
} else {
tailcut = Some(p);
}
break 'scan;
}
ResyncOutcome::LeaveLeaked => {
unsure += (stack_len - p) / bs;
break 'scan;
}
}
}
}
}
}
if let Some(t) = tailcut {
self.stack.discard(stack_len - t)?;
}
for b in reclaim {
self.push_free_blocks(b, 1)?;
}
Ok(unsure)
}
fn scan_free_list(&self, stack_len: u64) -> io::Result<(Vec<u64>, bool)> {
let mut free = Vec::new();
let mut seen: HashSet<u64> = HashSet::new();
let mut head = u64::from_le_bytes(read_bstack!(self.stack, Self::FREE_HEAD_OFFSET => u64));
let mut corrupt = false;
while head != Self::SENTINEL {
if head < Self::ARENA_START
|| (head - Self::ARENA_START) % self.block_size != 0
|| head >= stack_len
|| !seen.insert(head)
{
corrupt = true;
break;
}
let mut prefix = [0u8; 16];
self.stack.get_into(head, &mut prefix)?;
if u64::from_le_bytes(prefix[0..8].try_into().unwrap()) != 0 {
corrupt = true;
break;
}
free.push(head);
head = u64::from_le_bytes(prefix[8..16].try_into().unwrap());
}
free.sort_unstable();
Ok((free, corrupt))
}
fn classify(&self, p: u64, stack_len: u64, free: &[u64]) -> io::Result<BlockClass> {
let overhead = self.read_overhead(p)?;
if overhead == 0 {
return Ok(if free.binary_search(&p).is_ok() {
BlockClass::Free
} else {
BlockClass::Leaked
});
}
Ok(match self.valid_in_use(overhead, p, stack_len, free) {
Some(n) => BlockClass::InUse(n),
None => BlockClass::Suspicious,
})
}
fn valid_in_use(&self, overhead: u64, p: u64, stack_len: u64, free: &[u64]) -> Option<u64> {
if overhead & Self::IN_USE_BIT == 0 {
return None;
}
let n = overhead & Self::BLOCKS_MASK;
if n == 0 {
return None;
}
let span = n.checked_mul(self.block_size)?;
let end = p.checked_add(span)?;
if end > stack_len {
return None;
}
if let Some(&f) = free.get(free.partition_point(|&x| x <= p))
&& f < end
{
return None;
}
Some(n)
}
fn resync_tail(&self, p: u64, stack_len: u64, free: &[u64]) -> io::Result<ResyncOutcome> {
let bs = self.block_size;
let m = match usize::try_from((stack_len - p) / bs) {
Ok(v) if v <= Self::MAX_RECOVER_REGION => v,
_ => return Ok(ResyncOutcome::LeaveLeaked),
};
let mut reach = vec![false; m + 1];
reach[m] = true;
for j in (0..m).rev() {
let off = p + (j as u64) * bs;
let overhead = self.read_overhead(off)?;
reach[j] = if overhead == 0 {
reach[j + 1]
} else if let Some(n) = self.valid_in_use(overhead, off, stack_len, free) {
reach[j + n as usize]
} else {
false
};
}
if let Some(j) = (1..m).find(|&j| reach[j]) {
return Ok(ResyncOutcome::Resync(p + (j as u64) * bs));
}
Ok(ResyncOutcome::DiscardTail)
}
fn read_overhead(&self, block_start: u64) -> io::Result<u64> {
Ok(u64::from_le_bytes(
read_bstack!(self.stack, block_start => u64),
))
}
fn write_overhead(&self, block_start: u64, value: u64) -> io::Result<()> {
self.stack.set(block_start, value.to_le_bytes())
}
fn blocks_needed(&self, len: u64) -> io::Result<u64> {
if len == 0 {
return Ok(0);
}
let total = len.checked_add(Self::OVERHEAD).ok_or_else(|| {
io::Error::new(
io::ErrorKind::InvalidInput,
"allocation length overflows u64",
)
})?;
Ok(total.div_ceil(self.block_size))
}
fn pop_and_claim_block(&self, num_blocks: u64) -> io::Result<Option<u64>> {
let head = u64::from_le_bytes(read_bstack!(self.stack, Self::FREE_HEAD_OFFSET => u64));
if head == Self::SENTINEL {
return Ok(None);
}
let mut prefix = [0u8; 16];
self.stack.get_into(head, &mut prefix)?;
let overhead = u64::from_le_bytes(prefix[0..8].try_into().unwrap());
if overhead != 0 {
return Err(io::Error::new(
io::ErrorKind::InvalidData,
format!(
"free-list block at {head} has non-zero overhead {overhead:#018x}; free list corrupt"
),
));
}
self.stack.set(Self::FREE_HEAD_OFFSET, &prefix[8..16])?;
let mut block_buf = vec![0u8; self.block_size as usize]; block_buf[..8].copy_from_slice(&(Self::IN_USE_BIT | num_blocks).to_le_bytes());
self.stack.set(head, block_buf)?;
Ok(Some(head))
}
fn write_free_run(&self, first_block: u64, count: u64) -> io::Result<()> {
debug_assert!(count > 0);
let old_head = read_bstack!(self.stack, Self::FREE_HEAD_OFFSET => u64);
let total = count.checked_mul(self.block_size).ok_or_else(|| {
io::Error::new(
io::ErrorKind::InvalidInput,
"freed region size overflows u64",
)
})?;
let buf_size = usize::try_from(total).map_err(|_| {
io::Error::new(
io::ErrorKind::InvalidInput,
"freed region exceeds platform pointer size",
)
})?;
let mut buf = vec![0u8; buf_size];
for i in 0..count {
let base = usize::try_from(i.checked_mul(self.block_size).ok_or_else(|| {
io::Error::new(
io::ErrorKind::InvalidInput,
"free-list offset overflows u64",
)
})?)
.map_err(|_| {
io::Error::new(
io::ErrorKind::InvalidInput,
"free-list offset overflows usize",
)
})?;
let next_bytes: [u8; 8] = if i + 1 < count {
let next = first_block
.checked_add((i + 1).checked_mul(self.block_size).ok_or_else(|| {
io::Error::new(
io::ErrorKind::InvalidInput,
"next block index multiplication overflows u64",
)
})?)
.ok_or_else(|| {
io::Error::new(
io::ErrorKind::InvalidInput,
"next block offset overflows u64",
)
})?;
next.to_le_bytes()
} else {
old_head
};
buf[base + 8..base + 16].copy_from_slice(&next_bytes);
}
self.stack.set(first_block, buf)
}
fn push_free_blocks(&self, first_block: u64, count: u64) -> io::Result<()> {
if count == 0 {
return Ok(());
}
self.write_free_run(first_block, count)?;
self.stack
.set(Self::FREE_HEAD_OFFSET, first_block.to_le_bytes())
}
}
#[cfg(feature = "set")]
impl fmt::Debug for CheckedSlabBStackAllocator {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("CheckedSlabBStackAllocator")
.field("data_size", &self.data_size())
.finish_non_exhaustive()
}
}
#[cfg(feature = "set")]
impl BStackAllocator for CheckedSlabBStackAllocator {
type Error = io::Error;
type Allocated<'a> = BStackSlice<'a, Self>;
fn stack(&self) -> &BStack {
&self.stack
}
fn into_stack(self) -> BStack {
self.stack
}
fn alloc(&self, len: u64) -> io::Result<BStackSlice<'_, Self>> {
if len == 0 {
return Ok(BStackSlice::empty(self));
}
let num_blocks = self.blocks_needed(len)?;
if num_blocks == 1 {
let free_block = {
#[cfg(feature = "atomic")]
let _guard = self.lock.lock().unwrap();
self.pop_and_claim_block(1)?
};
if let Some(block_start) = free_block {
return Ok(unsafe {
BStackSlice::from_raw_parts(self, block_start + Self::OVERHEAD, len)
});
}
}
let total = num_blocks.checked_mul(self.block_size).ok_or_else(|| {
io::Error::new(io::ErrorKind::InvalidInput, "allocation size overflows u64")
})?;
let block_start = self.stack.extend(total)?;
self.write_overhead(block_start, Self::IN_USE_BIT | num_blocks)?;
Ok(unsafe { BStackSlice::from_raw_parts(self, block_start + Self::OVERHEAD, len) })
}
fn dealloc(&self, slice: BStackSlice<'_, Self>) -> io::Result<()> {
if slice.is_empty() && slice.start() == 0 {
return Ok(());
}
let block_start = slice.start().checked_sub(Self::OVERHEAD).ok_or_else(|| {
io::Error::new(
io::ErrorKind::InvalidInput,
"slice start is below the overhead prefix; not a valid allocation",
)
})?;
let overhead = self.read_overhead(block_start)?;
if overhead & Self::IN_USE_BIT == 0 {
return Err(io::Error::new(
io::ErrorKind::InvalidInput,
format!("double free detected: block at {block_start} is not marked in use"),
));
}
let num_blocks = overhead & Self::BLOCKS_MASK;
if num_blocks == 0 {
return Err(io::Error::new(
io::ErrorKind::InvalidData,
"in-use block records a zero block count; metadata corrupt",
));
}
let backing = num_blocks.checked_mul(self.block_size).ok_or_else(|| {
io::Error::new(
io::ErrorKind::InvalidInput,
"deallocation size overflows u64",
)
})?;
let slice_end = block_start.checked_add(backing).ok_or_else(|| {
io::Error::new(
io::ErrorKind::InvalidInput,
"deallocation end offset overflows u64",
)
})?;
#[cfg(feature = "atomic")]
if self.stack.try_discard(slice_end, backing)? {
return Ok(());
}
#[cfg(not(feature = "atomic"))]
if slice_end == self.stack.len()? {
return self.stack.discard(backing);
}
#[cfg(feature = "atomic")]
let _guard = self.lock.lock().unwrap();
self.push_free_blocks(block_start, num_blocks)
}
fn realloc<'a>(
&'a self,
slice: BStackSlice<'a, Self>,
new_len: u64,
) -> io::Result<BStackSlice<'a, Self>> {
if slice.is_empty() && slice.start() == 0 {
return self.alloc(new_len);
}
if new_len == 0 {
self.dealloc(slice)?;
return Ok(BStackSlice::empty(self));
}
if new_len == slice.len() {
return Ok(slice);
}
let block_start = slice.start().checked_sub(Self::OVERHEAD).ok_or_else(|| {
io::Error::new(
io::ErrorKind::InvalidInput,
"slice start is below the overhead prefix; not a valid allocation",
)
})?;
let overhead = self.read_overhead(block_start)?;
if overhead & Self::IN_USE_BIT == 0 {
return Err(io::Error::new(
io::ErrorKind::InvalidInput,
format!("realloc of a freed or invalid block at {block_start}"),
));
}
let old_n = overhead & Self::BLOCKS_MASK;
if old_n == 0 {
return Err(io::Error::new(
io::ErrorKind::InvalidData,
"in-use block records a zero block count; metadata corrupt",
));
}
let new_n = self.blocks_needed(new_len)?;
if old_n == new_n {
if new_len > slice.len() {
self.stack.zero(slice.end(), new_len - slice.len())?;
}
return Ok(unsafe { BStackSlice::from_raw_parts(self, slice.start(), new_len) });
}
let old_backing = old_n.checked_mul(self.block_size).ok_or_else(|| {
io::Error::new(
io::ErrorKind::InvalidInput,
"old allocation size overflows u64",
)
})?;
let new_backing = new_n.checked_mul(self.block_size).ok_or_else(|| {
io::Error::new(
io::ErrorKind::InvalidInput,
"new allocation size overflows u64",
)
})?;
let sentinel = block_start.checked_add(old_backing).ok_or_else(|| {
io::Error::new(io::ErrorKind::InvalidInput, "tail check overflows u64")
})?;
if new_n > old_n {
#[cfg(feature = "atomic")]
if self
.stack
.try_extend_zeros(sentinel, new_backing - old_backing)?
{
if new_len > slice.len() {
self.stack.zero(slice.end(), new_len - slice.len())?;
}
self.write_overhead(block_start, Self::IN_USE_BIT | new_n)?;
return Ok(unsafe { BStackSlice::from_raw_parts(self, slice.start(), new_len) });
}
#[cfg(not(feature = "atomic"))]
if sentinel == self.stack.len()? {
self.stack.extend(new_backing - old_backing)?;
if new_len > slice.len() {
self.stack.zero(slice.end(), new_len - slice.len())?;
}
self.write_overhead(block_start, Self::IN_USE_BIT | new_n)?;
return Ok(unsafe { BStackSlice::from_raw_parts(self, slice.start(), new_len) });
}
let new_slice = self.alloc(new_len)?;
let data = slice.read()?;
new_slice.write(&data)?;
self.dealloc(slice)?;
return Ok(new_slice);
}
#[cfg(feature = "atomic")]
let _guard = self.lock.lock().unwrap();
self.write_overhead(block_start, Self::IN_USE_BIT | new_n)?;
#[cfg(feature = "atomic")]
if self
.stack
.try_discard(sentinel, old_backing - new_backing)?
{
return Ok(unsafe { BStackSlice::from_raw_parts(self, slice.start(), new_len) });
}
#[cfg(not(feature = "atomic"))]
if sentinel == self.stack.len()? {
self.stack.discard(old_backing - new_backing)?;
return Ok(unsafe { BStackSlice::from_raw_parts(self, slice.start(), new_len) });
}
let excess_start = block_start
.checked_add(new_n.checked_mul(self.block_size).ok_or_else(|| {
io::Error::new(
io::ErrorKind::InvalidInput,
"free start multiplication overflows u64",
)
})?)
.ok_or_else(|| {
io::Error::new(io::ErrorKind::InvalidInput, "free start overflows u64")
})?;
self.write_free_run(excess_start, old_n - new_n)?;
self.stack
.set(Self::FREE_HEAD_OFFSET, excess_start.to_le_bytes())?;
Ok(unsafe { BStackSlice::from_raw_parts(self, slice.start(), new_len) })
}
}
#[cfg(all(test, feature = "set"))]
mod _assertions {
use super::CheckedSlabBStackAllocator;
fn _send()
where
CheckedSlabBStackAllocator: Send,
{
}
#[cfg(feature = "atomic")]
fn _sync()
where
CheckedSlabBStackAllocator: Sync,
{
}
}
#[cfg(all(test, feature = "set"))]
mod tests {
use super::CheckedSlabBStackAllocator;
use crate::BStack;
use crate::alloc::BStackAllocator;
use std::io::ErrorKind;
use std::sync::atomic::{AtomicU64, Ordering};
struct Guard(std::path::PathBuf);
impl Drop for Guard {
fn drop(&mut self) {
let _ = std::fs::remove_file(&self.0);
}
}
fn temp_path() -> std::path::PathBuf {
static COUNTER: AtomicU64 = AtomicU64::new(0);
let id = COUNTER.fetch_add(1, Ordering::Relaxed);
let pid = std::process::id();
std::env::temp_dir().join(format!("bstack_checked_slab_{pid}_{id}.bin"))
}
fn empty_stack() -> (BStack, std::path::PathBuf) {
let path = temp_path();
(BStack::open(&path).unwrap(), path)
}
#[test]
fn new_initialises_header_and_reports_data_size() {
let (stack, path) = empty_stack();
let _g = Guard(path);
let alloc = CheckedSlabBStackAllocator::new(stack, 8).unwrap();
assert_eq!(alloc.data_size(), 8);
assert_eq!(alloc.stack().len().unwrap(), 48);
}
#[test]
fn new_rejects_data_size_below_minimum() {
let (stack, path) = empty_stack();
let _g = Guard(path);
let err = CheckedSlabBStackAllocator::new(stack, 7).unwrap_err();
assert_eq!(err.kind(), ErrorKind::InvalidInput);
}
#[test]
fn new_rejects_nonempty_stack() {
let (stack, path) = empty_stack();
let _g = Guard(path);
stack.push(b"data").unwrap();
let err = CheckedSlabBStackAllocator::new(stack, 8).unwrap_err();
assert_eq!(err.kind(), ErrorKind::InvalidInput);
}
#[test]
fn open_rejects_empty_stack() {
let (stack, path) = empty_stack();
let _g = Guard(path);
let err = CheckedSlabBStackAllocator::open(stack).unwrap_err();
assert_eq!(err.kind(), ErrorKind::InvalidInput);
}
#[test]
fn open_rejects_stack_too_short() {
let (stack, path) = empty_stack();
let _g = Guard(path.clone());
stack.push([0u8; 24]).unwrap(); drop(stack);
let err = CheckedSlabBStackAllocator::open(BStack::open(&path).unwrap()).unwrap_err();
assert_eq!(err.kind(), ErrorKind::InvalidData);
}
#[test]
fn open_rejects_bad_magic() {
let (stack, path) = empty_stack();
let _g = Guard(path.clone());
stack.push([0u8; 48]).unwrap(); drop(stack);
let err = CheckedSlabBStackAllocator::open(BStack::open(&path).unwrap()).unwrap_err();
assert_eq!(err.kind(), ErrorKind::InvalidData);
}
#[test]
fn open_rejects_invalid_stored_block_size() {
let (stack, path) = empty_stack();
let _g = Guard(path.clone());
let mut hdr = [0u8; 48];
hdr[24..32].copy_from_slice(b"ALCK\x00\x01\x00\x00");
hdr[32..40].copy_from_slice(&8u64.to_le_bytes());
stack.push(hdr).unwrap();
drop(stack);
let err = CheckedSlabBStackAllocator::open(BStack::open(&path).unwrap()).unwrap_err();
assert_eq!(err.kind(), ErrorKind::InvalidData);
}
#[test]
fn open_rejects_misaligned_tail() {
let (stack, path) = empty_stack();
let _g = Guard(path.clone());
CheckedSlabBStackAllocator::new(stack, 8).unwrap();
let reopen = BStack::open(&path).unwrap();
reopen.extend(1).unwrap();
drop(reopen);
let err = CheckedSlabBStackAllocator::open(BStack::open(&path).unwrap()).unwrap_err();
assert_eq!(err.kind(), ErrorKind::InvalidData);
}
#[test]
fn open_succeeds_and_restores_data_size() {
let (stack, path) = empty_stack();
let _g = Guard(path.clone());
CheckedSlabBStackAllocator::new(stack, 24).unwrap();
let alloc = CheckedSlabBStackAllocator::open(BStack::open(&path).unwrap()).unwrap();
assert_eq!(alloc.data_size(), 24);
}
#[test]
fn zero_alloc_returns_empty_slice() {
let (stack, path) = empty_stack();
let _g = Guard(path);
let alloc = CheckedSlabBStackAllocator::new(stack, 8).unwrap();
let s = alloc.alloc(0).unwrap();
assert!(s.is_empty());
}
#[test]
fn dealloc_pushes_to_free_list_and_next_alloc_reuses_block() {
let (stack, path) = empty_stack();
let _g = Guard(path);
let alloc = CheckedSlabBStackAllocator::new(stack, 8).unwrap();
let s1 = alloc.alloc(8).unwrap();
let offset1 = s1.start();
alloc.dealloc(s1).unwrap();
let s2 = alloc.alloc(8).unwrap();
assert_eq!(s2.start(), offset1);
}
#[test]
fn free_list_recycles_all_dealloc_d_blocks() {
let (stack, path) = empty_stack();
let _g = Guard(path);
let alloc = CheckedSlabBStackAllocator::new(stack, 8).unwrap();
let a = alloc.alloc(8).unwrap();
let b = alloc.alloc(8).unwrap();
let c = alloc.alloc(8).unwrap();
let mut original = [a.start(), b.start(), c.start()];
alloc.dealloc(a).unwrap();
alloc.dealloc(b).unwrap();
alloc.dealloc(c).unwrap();
let r1 = alloc.alloc(8).unwrap();
let r2 = alloc.alloc(8).unwrap();
let r3 = alloc.alloc(8).unwrap();
let mut reused = [r1.start(), r2.start(), r3.start()];
original.sort();
reused.sort();
assert_eq!(reused, original);
}
#[test]
fn oversized_tail_dealloc_shrinks_stack() {
let (stack, path) = empty_stack();
let _g = Guard(path);
let alloc = CheckedSlabBStackAllocator::new(stack, 8).unwrap();
let s = alloc.alloc(17).unwrap();
let tail_before = alloc.stack().len().unwrap();
assert_eq!(tail_before, 48 + 32);
alloc.dealloc(s).unwrap();
assert_eq!(alloc.stack().len().unwrap(), 48);
}
#[test]
fn double_free_returns_error() {
let (stack, path) = empty_stack();
let _g = Guard(path);
let alloc = CheckedSlabBStackAllocator::new(stack, 8).unwrap();
let s = alloc.alloc(8).unwrap();
let s_copy = s; alloc.dealloc(s).unwrap();
let err = alloc.dealloc(s_copy).unwrap_err();
assert_eq!(err.kind(), ErrorKind::InvalidInput);
}
#[test]
fn write_and_read_round_trip() {
let (stack, path) = empty_stack();
let _g = Guard(path);
let alloc = CheckedSlabBStackAllocator::new(stack, 16).unwrap();
let s = alloc.alloc(12).unwrap();
s.write(b"hello world!").unwrap();
assert_eq!(s.read().unwrap(), b"hello world!");
}
#[test]
fn data_survives_reopen() {
let (stack, path) = empty_stack();
let _g = Guard(path.clone());
let alloc = CheckedSlabBStackAllocator::new(stack, 8).unwrap();
let s = alloc.alloc(5).unwrap();
let offset = s.start();
s.write(b"hello").unwrap();
drop(alloc);
let alloc2 = CheckedSlabBStackAllocator::open(BStack::open(&path).unwrap()).unwrap();
let s2 = unsafe { crate::alloc::BStackSlice::from_raw_parts(&alloc2, offset, 5) };
assert_eq!(s2.read().unwrap(), b"hello");
}
#[test]
fn multiblock_alloc_round_trip_and_free_reuse() {
let (stack, path) = empty_stack();
let _g = Guard(path);
let alloc = CheckedSlabBStackAllocator::new(stack, 8).unwrap();
let s = alloc.alloc(40).unwrap();
let payload: Vec<u8> = (0..40u8).collect();
s.write(&payload).unwrap();
assert_eq!(s.read().unwrap(), payload);
let start = s.start();
alloc.dealloc(s).unwrap();
let reused = alloc.alloc(8).unwrap();
assert_eq!(reused.start(), start);
}
#[test]
fn realloc_same_block_count_grows_in_place_and_zeroes() {
let (stack, path) = empty_stack();
let _g = Guard(path);
let alloc = CheckedSlabBStackAllocator::new(stack, 24).unwrap();
let s = alloc.alloc(8).unwrap();
s.write(b"abcdefgh").unwrap();
let s2 = alloc.realloc(s, 16).unwrap();
assert_eq!(s2.start(), s.start());
let data = s2.read().unwrap();
assert_eq!(&data[..8], b"abcdefgh");
assert_eq!(&data[8..], &[0u8; 8]); }
#[test]
fn realloc_grow_preserves_data_across_blocks() {
let (stack, path) = empty_stack();
let _g = Guard(path);
let alloc = CheckedSlabBStackAllocator::new(stack, 8).unwrap();
let s = alloc.alloc(8).unwrap();
s.write(b"abcdefgh").unwrap();
let s2 = alloc.realloc(s, 40).unwrap();
assert_eq!(s2.len(), 40);
let data = s2.read().unwrap();
assert_eq!(&data[..8], b"abcdefgh");
assert_eq!(&data[8..], &[0u8; 32]);
}
#[test]
fn realloc_shrink_non_tail_recycles_excess() {
let (stack, path) = empty_stack();
let _g = Guard(path);
let alloc = CheckedSlabBStackAllocator::new(stack, 8).unwrap();
let s = alloc.alloc(40).unwrap(); let payload: Vec<u8> = (0..40u8).collect();
s.write(&payload).unwrap();
let _guard_block = alloc.alloc(8).unwrap();
let s2 = alloc.realloc(s, 8).unwrap();
assert_eq!(s2.start(), s.start());
assert_eq!(s2.read().unwrap(), &payload[..8]);
let r = alloc.alloc(8).unwrap();
assert_eq!(r.start(), s.start() + 16);
}
#[test]
fn realloc_to_zero_deallocs() {
let (stack, path) = empty_stack();
let _g = Guard(path);
let alloc = CheckedSlabBStackAllocator::new(stack, 8).unwrap();
let s = alloc.alloc(8).unwrap();
let start = s.start();
let s2 = alloc.realloc(s, 0).unwrap();
assert!(s2.is_empty());
let r = alloc.alloc(8).unwrap();
assert_eq!(r.start(), start);
}
#[test]
fn realloc_from_empty_allocates() {
let (stack, path) = empty_stack();
let _g = Guard(path);
let alloc = CheckedSlabBStackAllocator::new(stack, 8).unwrap();
let empty = alloc.alloc(0).unwrap();
let s = alloc.realloc(empty, 8).unwrap();
assert_eq!(s.len(), 8);
assert!(!s.is_empty());
}
const fn in_use(n: u64) -> u64 {
0x8000_0000_0000_0000u64 | n
}
#[test]
fn recover_clean_allocator_returns_zero() {
let (stack, path) = empty_stack();
let _g = Guard(path);
let alloc = CheckedSlabBStackAllocator::new(stack, 8).unwrap(); let _a = alloc.alloc(8).unwrap(); let b = alloc.alloc(8).unwrap(); let _c = alloc.alloc(16).unwrap(); alloc.dealloc(b).unwrap(); assert_eq!(alloc.recover().unwrap(), 0);
}
#[test]
fn recover_reclaims_leaked_free_block() {
let (stack, path) = empty_stack();
let _g = Guard(path);
let alloc = CheckedSlabBStackAllocator::new(stack, 8).unwrap(); let _a = alloc.alloc(8).unwrap(); let b = alloc.alloc(8).unwrap(); let _c = alloc.alloc(8).unwrap(); alloc.dealloc(b).unwrap(); alloc.stack().set(40, 0u64.to_le_bytes()).unwrap();
assert_eq!(alloc.recover().unwrap(), 0);
let reused = alloc.alloc(8).unwrap();
assert_eq!(reused.start(), 72);
}
#[test]
fn recover_discards_orphan_tail() {
let (stack, path) = empty_stack();
let _g = Guard(path);
let alloc = CheckedSlabBStackAllocator::new(stack, 8).unwrap(); let s = alloc.alloc(40).unwrap(); s.write(&[0xFFu8; 40]).unwrap(); assert_eq!(alloc.stack().len().unwrap(), 96);
alloc.stack().set(48, in_use(1).to_le_bytes()).unwrap();
assert_eq!(alloc.recover().unwrap(), 0);
assert_eq!(alloc.stack().len().unwrap(), 64); }
#[test]
fn recover_leaves_mid_arena_garbage_leaked() {
let (stack, path) = empty_stack();
let _g = Guard(path);
let alloc = CheckedSlabBStackAllocator::new(stack, 8).unwrap(); let _a = alloc.alloc(8).unwrap(); let _b = alloc.alloc(8).unwrap(); let c = alloc.alloc(8).unwrap(); c.write(b"keepkeep").unwrap();
alloc.stack().set(64, u64::MAX.to_le_bytes()).unwrap();
assert_eq!(alloc.recover().unwrap(), 1); assert_eq!(alloc.stack().len().unwrap(), 96); assert_eq!(c.read().unwrap(), b"keepkeep"); }
#[test]
fn open_auto_recovers_orphan_tail() {
let (stack, path) = empty_stack();
let _g = Guard(path.clone());
{
let alloc = CheckedSlabBStackAllocator::new(stack, 8).unwrap();
let s = alloc.alloc(40).unwrap(); s.write(&[0xFFu8; 40]).unwrap();
alloc.stack().set(48, in_use(1).to_le_bytes()).unwrap(); }
let reopened = CheckedSlabBStackAllocator::open(BStack::open(&path).unwrap()).unwrap();
assert_eq!(reopened.stack().len().unwrap(), 64); }
#[test]
fn recover_is_idempotent() {
let (stack, path) = empty_stack();
let _g = Guard(path);
let alloc = CheckedSlabBStackAllocator::new(stack, 8).unwrap();
let _a = alloc.alloc(8).unwrap();
let b = alloc.alloc(8).unwrap();
let _c = alloc.alloc(8).unwrap();
alloc.dealloc(b).unwrap();
alloc.stack().set(40, 0u64.to_le_bytes()).unwrap(); assert_eq!(alloc.recover().unwrap(), 0);
let len_after = alloc.stack().len().unwrap();
assert_eq!(alloc.recover().unwrap(), 0);
assert_eq!(alloc.stack().len().unwrap(), len_after);
}
#[cfg(feature = "atomic")]
#[test]
fn concurrent_alloc_dealloc_no_live_duplicates() {
use std::collections::HashSet;
use std::sync::{Arc, Mutex};
use std::thread;
const THREADS: usize = 8;
const ROUNDS: usize = 200;
let (stack, path) = empty_stack();
let _g = Guard(path);
let alloc = Arc::new(CheckedSlabBStackAllocator::new(stack, 8).unwrap());
let live: Arc<Mutex<HashSet<u64>>> = Arc::new(Mutex::new(HashSet::new()));
let handles: Vec<_> = (0..THREADS)
.map(|tid| {
let alloc = Arc::clone(&alloc);
let live = Arc::clone(&live);
thread::spawn(move || {
let a: &CheckedSlabBStackAllocator = &alloc;
for _ in 0..ROUNDS {
let slice = a.alloc(8).unwrap();
let off = slice.start();
{
let mut set = live.lock().unwrap();
assert!(set.insert(off), "duplicate live offset {off}");
}
slice.write(&[tid as u8; 8]).unwrap();
let data = slice.read().unwrap();
assert_eq!(data, vec![tid as u8; 8]);
{
let mut set = live.lock().unwrap();
set.remove(&off);
}
a.dealloc(slice).unwrap();
}
})
})
.collect();
for h in handles {
h.join().unwrap();
}
assert_eq!(alloc.recover().unwrap(), 0);
}
#[cfg(feature = "atomic")]
#[test]
fn concurrent_realloc_hammers_tail_paths() {
use std::sync::Arc;
use std::thread;
const THREADS: usize = 6;
const ROUNDS: usize = 150;
const SMALL: u64 = 8;
const LARGE: u64 = 32;
let (stack, path) = empty_stack();
let _g = Guard(path);
let alloc = Arc::new(CheckedSlabBStackAllocator::new(stack, 8).unwrap());
let handles: Vec<_> = (0..THREADS)
.map(|tid| {
let alloc = Arc::clone(&alloc);
thread::spawn(move || {
let a: &CheckedSlabBStackAllocator = &alloc;
let mut slice = a.alloc(SMALL).unwrap();
slice.write(&[tid as u8; SMALL as usize]).unwrap();
for _ in 0..ROUNDS {
slice = a.realloc(slice, LARGE).unwrap();
let data = slice.read().unwrap();
assert_eq!(
&data[..SMALL as usize],
&[tid as u8; SMALL as usize],
"data corrupted after grow (tid {tid})",
);
slice = a.realloc(slice, SMALL).unwrap();
let data = slice.read().unwrap();
assert_eq!(
data,
vec![tid as u8; SMALL as usize],
"data corrupted after shrink (tid {tid})",
);
}
a.dealloc(slice).unwrap();
})
})
.collect();
for h in handles {
h.join().unwrap();
}
assert_eq!(alloc.recover().unwrap(), 0);
}
}