use std::collections::HashSet;
use std::fmt;
use std::path::Path;
use std::sync::Mutex;
use bstack::BStack;
use crc32fast::Hasher as CrcHasher;
use crate::Error;
pub const MAGIC: [u8; 4] = *b"BLLD";
pub const VERSION: u32 = 2;
pub const HEADER_SIZE: u64 = 272;
pub const NUM_BINS: usize = 32;
pub const BLOCK_HEADER_SIZE: usize = 20;
pub const MIN_BIN: usize = 5;
pub const MAX_SPLIT: usize = 3;
const MAX_BLOCK_SIZE: usize = 1 << 31;
#[inline]
fn bin_index(block_size: usize) -> usize {
block_size.trailing_zeros() as usize
}
struct DynHeader {
root: u64,
bin_heads: [u64; NUM_BINS],
}
impl DynHeader {
fn from_bytes(buf: &[u8; 272]) -> Result<Self, Error> {
if &buf[0..4] == b"BLLS" {
return Err(Error::Corruption(
"file was created by FixedBlockList; use FixedBlockList::open".into(),
));
}
if buf[0..4] != MAGIC {
return Err(Error::Corruption(format!(
"invalid magic: expected {:?} (\"BLLD\"), found {:?}",
MAGIC,
&buf[0..4]
)));
}
let version = u32::from_le_bytes(buf[4..8].try_into().unwrap());
if version != VERSION {
return Err(Error::Corruption(format!(
"unsupported dynamic-list version {version}, expected {VERSION}"
)));
}
let root = u64::from_le_bytes(buf[8..16].try_into().unwrap());
let mut bin_heads = [0u64; NUM_BINS];
for (k, bh) in bin_heads.iter_mut().enumerate() {
let s = 16 + k * 8;
*bh = u64::from_le_bytes(buf[s..s + 8].try_into().unwrap());
}
Ok(Self { root, bin_heads })
}
fn to_bytes(&self) -> [u8; 272] {
let mut buf = [0u8; 272];
buf[0..4].copy_from_slice(&MAGIC);
buf[4..8].copy_from_slice(&VERSION.to_le_bytes());
buf[8..16].copy_from_slice(&self.root.to_le_bytes());
for (k, bh) in self.bin_heads.iter().enumerate() {
let s = 16 + k * 8;
buf[s..s + 8].copy_from_slice(&bh.to_le_bytes());
}
buf
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub struct DynBlockRef(pub u64);
impl fmt::Display for DynBlockRef {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "@{}", self.0)
}
}
impl fmt::LowerHex for DynBlockRef {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str("@")?;
fmt::LowerHex::fmt(&self.0, f)
}
}
impl fmt::UpperHex for DynBlockRef {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str("@")?;
fmt::UpperHex::fmt(&self.0, f)
}
}
impl From<u64> for DynBlockRef {
fn from(offset: u64) -> Self {
DynBlockRef(offset)
}
}
impl From<DynBlockRef> for u64 {
fn from(r: DynBlockRef) -> u64 {
r.0
}
}
impl DynBlockRef {
#[inline]
pub fn data_start(self) -> u64 {
self.0 + BLOCK_HEADER_SIZE as u64
}
}
pub struct DynamicBlockList {
stack: BStack,
mu: Mutex<()>,
}
unsafe impl Send for DynamicBlockList {}
unsafe impl Sync for DynamicBlockList {}
impl fmt::Debug for DynamicBlockList {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("DynamicBlockList")
.field("num_bins", &NUM_BINS)
.finish()
}
}
impl fmt::Display for DynamicBlockList {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str("DynamicBlockList")
}
}
impl DynamicBlockList {
pub fn open(path: impl AsRef<Path>) -> Result<Self, Error> {
let stack = BStack::open(path)?;
let total = stack.len()?;
if total == 0 {
let hdr = DynHeader {
root: 0,
bin_heads: [0u64; NUM_BINS],
};
let offset = stack.push(&hdr.to_bytes())?;
debug_assert_eq!(
offset, 0,
"dynamic-list header must land at logical offset 0"
);
return Ok(Self {
stack,
mu: Mutex::new(()),
});
}
if total < HEADER_SIZE {
return Err(Error::Corruption(format!(
"file payload is {total} bytes, too small for the {HEADER_SIZE}-byte \
dynamic-list header"
)));
}
let mut hdr_buf = [0u8; 272];
stack.get_into(0, &mut hdr_buf)?;
let mut header = DynHeader::from_bytes(&hdr_buf)?;
Self::recover_orphans(&stack, &mut header, total)?;
Ok(Self {
stack,
mu: Mutex::new(()),
})
}
pub fn alloc(&self, size: usize) -> Result<DynBlockRef, Error> {
let bs = Self::block_size_for(size);
if bs > MAX_BLOCK_SIZE {
return Err(Error::DataTooLarge {
capacity: MAX_BLOCK_SIZE - BLOCK_HEADER_SIZE,
provided: size,
});
}
let _g = self.mu.lock().unwrap();
let mut header = self.read_header_locked()?;
self.alloc_locked(bs, &mut header)
}
pub fn free(&self, block: DynBlockRef) -> Result<(), Error> {
self.validate_block_offset(block.0)?;
let _g = self.mu.lock().unwrap();
let mut header = self.read_header_locked()?;
self.free_locked(block, &mut header)
}
pub fn write(&self, block: DynBlockRef, data: &[u8]) -> Result<(), Error> {
self.validate_block_offset(block.0)?;
let mut fields = [0u8; 12];
self.stack.get_into(block.0 + 4, &mut fields)?;
let next = u64::from_le_bytes(fields[0..8].try_into().unwrap());
let block_size = u32::from_le_bytes(fields[8..12].try_into().unwrap()) as usize;
let payload_cap = block_size.saturating_sub(BLOCK_HEADER_SIZE);
if data.len() > payload_cap {
return Err(Error::DataTooLarge {
capacity: payload_cap,
provided: data.len(),
});
}
self.write_block_raw(block.0, block_size, next, data.len() as u32, data)
}
pub fn read(&self, block: DynBlockRef) -> Result<Vec<u8>, Error> {
self.validate_block_offset(block.0)?;
let (_, _, data) = Self::read_block_full_static(&self.stack, block.0)?;
Ok(data)
}
pub fn read_into(&self, block: DynBlockRef, buf: &mut [u8]) -> Result<bool, Error> {
self.validate_block_offset(block.0)?;
let mut hdr = [0u8; BLOCK_HEADER_SIZE];
self.stack.get_into(block.0, &mut hdr)?;
let stored_crc = u32::from_le_bytes(hdr[0..4].try_into().unwrap());
let block_size = u32::from_le_bytes(hdr[12..16].try_into().unwrap()) as usize;
let data_len = u32::from_le_bytes(hdr[16..20].try_into().unwrap()) as usize;
let payload_cap = block_size.saturating_sub(BLOCK_HEADER_SIZE);
if data_len > buf.len() {
return Err(Error::ChecksumMismatch { block: block.0 });
}
if data_len > 0 {
self.stack
.get_into(block.0 + BLOCK_HEADER_SIZE as u64, &mut buf[0..data_len])?;
}
let mut hasher = CrcHasher::new();
hasher.update(&hdr[4..]); hasher.update(&buf[0..data_len]);
if payload_cap > data_len {
hasher.update(&vec![0u8; payload_cap - data_len]);
}
if hasher.finalize() != stored_crc {
return Err(Error::ChecksumMismatch { block: block.0 });
}
Ok(data_len > 0)
}
pub fn set_next(&self, block: DynBlockRef, next: Option<DynBlockRef>) -> Result<(), Error> {
self.validate_block_offset(block.0)?;
let next_val = next.map(|r| r.0).unwrap_or(0u64);
let mut bs_buf = [0u8; 4];
self.stack.get_into(block.0 + 12, &mut bs_buf)?;
let block_size = u32::from_le_bytes(bs_buf) as usize;
let mut buf = vec![0u8; block_size];
self.stack.get_into(block.0, &mut buf)?;
buf[4..12].copy_from_slice(&next_val.to_le_bytes());
let crc = crc32fast::hash(&buf[4..]);
buf[0..4].copy_from_slice(&crc.to_le_bytes());
self.stack.set(block.0, &buf)?;
Ok(())
}
pub fn get_next(&self, block: DynBlockRef) -> Result<Option<DynBlockRef>, Error> {
self.validate_block_offset(block.0)?;
let mut buf = [0u8; 8];
self.stack.get_into(block.0 + 4, &mut buf)?;
let next = u64::from_le_bytes(buf);
Ok(if next == 0 {
None
} else {
Some(DynBlockRef(next))
})
}
pub fn root(&self) -> Result<Option<DynBlockRef>, Error> {
let _g = self.mu.lock().unwrap();
let header = self.read_header_locked()?;
Ok(if header.root == 0 {
None
} else {
Some(DynBlockRef(header.root))
})
}
pub fn capacity(&self, block: DynBlockRef) -> Result<usize, Error> {
self.validate_block_offset(block.0)?;
let mut buf = [0u8; 4];
self.stack.get_into(block.0 + 12, &mut buf)?;
let block_size = u32::from_le_bytes(buf) as usize;
Ok(block_size.saturating_sub(BLOCK_HEADER_SIZE))
}
pub fn data_len(&self, block: DynBlockRef) -> Result<usize, Error> {
self.validate_block_offset(block.0)?;
let mut buf = [0u8; 4];
self.stack.get_into(block.0 + 16, &mut buf)?;
Ok(u32::from_le_bytes(buf) as usize)
}
pub fn data_start(&self, block: DynBlockRef) -> Result<u64, Error> {
self.validate_block_offset(block.0)?;
Ok(block.data_start())
}
pub fn data_end(&self, block: DynBlockRef) -> Result<u64, Error> {
self.validate_block_offset(block.0)?;
let mut buf = [0u8; 4];
self.stack.get_into(block.0 + 16, &mut buf)?;
let data_len = u32::from_le_bytes(buf) as u64;
Ok(block.data_start() + data_len)
}
pub fn push_front(&self, data: &[u8]) -> Result<DynBlockRef, Error> {
let bs = Self::block_size_for(data.len());
if bs > MAX_BLOCK_SIZE {
return Err(Error::DataTooLarge {
capacity: MAX_BLOCK_SIZE - BLOCK_HEADER_SIZE,
provided: data.len(),
});
}
let _g = self.mu.lock().unwrap();
let mut header = self.read_header_locked()?;
let old_root = header.root;
let new_block = self.alloc_locked(bs, &mut header)?;
self.write_block_raw(new_block.0, bs, old_root, data.len() as u32, data)?;
header.root = new_block.0;
self.write_header_locked(&header)?;
Ok(new_block)
}
pub fn pop_front(&self) -> Result<Option<Vec<u8>>, Error> {
let _g = self.mu.lock().unwrap();
let mut header = self.read_header_locked()?;
if header.root == 0 {
return Ok(None);
}
let old_root = header.root;
let (next, data) = self.read_block_full(old_root)?;
header.root = next;
self.write_header_locked(&header)?;
self.free_locked(DynBlockRef(old_root), &mut header)?;
Ok(Some(data))
}
pub fn pop_front_into(&self, buf: &mut [u8]) -> Result<bool, Error> {
let _g = self.mu.lock().unwrap();
let mut header = self.read_header_locked()?;
if header.root == 0 {
return Ok(false);
}
let old_root = header.root;
self.read_into(DynBlockRef(old_root), buf)?;
let mut next_buf = [0u8; 8];
self.stack.get_into(old_root + 4, &mut next_buf)?;
let next = u64::from_le_bytes(next_buf);
header.root = next;
self.write_header_locked(&header)?;
self.free_locked(DynBlockRef(old_root), &mut header)?;
Ok(true)
}
pub fn bstack(&self) -> &BStack {
&self.stack
}
pub const fn block_size_for(size: usize) -> usize {
let total = BLOCK_HEADER_SIZE + size;
let min = 1usize << MIN_BIN;
if total <= min {
return min;
}
let mut p = min;
while p < total {
p <<= 1;
}
p
}
fn validate_block_offset(&self, offset: u64) -> Result<(), Error> {
if offset < HEADER_SIZE {
return Err(Error::InvalidBlock);
}
Ok(())
}
fn read_header_locked(&self) -> Result<DynHeader, Error> {
let mut buf = [0u8; 272];
self.stack.get_into(0, &mut buf)?;
DynHeader::from_bytes(&buf)
}
fn write_header_locked(&self, header: &DynHeader) -> Result<(), Error> {
self.stack.set(0, &header.to_bytes())?;
Ok(())
}
fn write_free_block_raw(&self, offset: u64, block_size: usize, next: u64) -> Result<(), Error> {
let mut buf = vec![0u8; block_size];
buf[4..12].copy_from_slice(&next.to_le_bytes());
buf[12..16].copy_from_slice(&(block_size as u32).to_le_bytes());
let crc = crc32fast::hash(&buf[4..]);
buf[0..4].copy_from_slice(&crc.to_le_bytes());
self.stack.set(offset, &buf)?;
Ok(())
}
fn alloc_locked(
&self,
block_size: usize,
header: &mut DynHeader,
) -> Result<DynBlockRef, Error> {
let target_bin = bin_index(block_size);
if header.bin_heads[target_bin] != 0 {
let bh = header.bin_heads[target_bin];
let mut next_buf = [0u8; 8];
self.stack.get_into(bh + 4, &mut next_buf)?;
header.bin_heads[target_bin] = u64::from_le_bytes(next_buf);
self.write_header_locked(header)?;
return Ok(DynBlockRef(bh));
}
let search_limit = (target_bin + MAX_SPLIT).min(NUM_BINS - 1);
for k in (target_bin + 1)..=search_limit {
if header.bin_heads[k] != 0 {
let bh = header.bin_heads[k];
let mut next_buf = [0u8; 8];
self.stack.get_into(bh + 4, &mut next_buf)?;
header.bin_heads[k] = u64::from_le_bytes(next_buf);
return self.split_to_bin(bh, k, target_bin, header);
}
}
let mut buf = vec![0u8; block_size];
buf[12..16].copy_from_slice(&(block_size as u32).to_le_bytes());
let crc = crc32fast::hash(&buf[4..]);
buf[0..4].copy_from_slice(&crc.to_le_bytes());
let offset = self.stack.push(&buf)?;
Ok(DynBlockRef(offset))
}
fn split_to_bin(
&self,
offset: u64,
from_bin: usize,
to_bin: usize,
header: &mut DynHeader,
) -> Result<DynBlockRef, Error> {
let mut cur_size = 1usize << from_bin;
while bin_index(cur_size) > to_bin {
let half = cur_size / 2;
let upper = offset + half as u64;
let upper_bin = bin_index(half);
let old_head = header.bin_heads[upper_bin];
self.write_free_block_raw(offset, half, 0)?;
self.write_free_block_raw(upper, half, old_head)?;
header.bin_heads[upper_bin] = upper;
cur_size = half;
}
self.write_header_locked(header)?;
Ok(DynBlockRef(offset))
}
fn free_locked(&self, block: DynBlockRef, header: &mut DynHeader) -> Result<(), Error> {
let mut bs_buf = [0u8; 4];
self.stack.get_into(block.0 + 12, &mut bs_buf)?;
let block_size = u32::from_le_bytes(bs_buf) as usize;
let bin = bin_index(block_size);
let old_bin_head = header.bin_heads[bin];
self.write_free_block_raw(block.0, block_size, old_bin_head)?;
header.bin_heads[bin] = block.0;
self.write_header_locked(header)?;
Ok(())
}
fn write_block_raw(
&self,
offset: u64,
block_size: usize,
next: u64,
data_len: u32,
data: &[u8],
) -> Result<(), Error> {
let mut buf = vec![0u8; block_size];
buf[4..12].copy_from_slice(&next.to_le_bytes());
buf[12..16].copy_from_slice(&(block_size as u32).to_le_bytes());
buf[16..20].copy_from_slice(&data_len.to_le_bytes());
buf[20..20 + data.len()].copy_from_slice(data);
let crc = crc32fast::hash(&buf[4..]);
buf[0..4].copy_from_slice(&crc.to_le_bytes());
self.stack.set(offset, &buf)?;
Ok(())
}
fn read_block_full_static(stack: &BStack, offset: u64) -> Result<(u64, usize, Vec<u8>), Error> {
let mut hdr = [0u8; BLOCK_HEADER_SIZE];
stack.get_into(offset, &mut hdr)?;
let stored_crc = u32::from_le_bytes(hdr[0..4].try_into().unwrap());
let block_size = u32::from_le_bytes(hdr[12..16].try_into().unwrap()) as usize;
let data_len = u32::from_le_bytes(hdr[16..20].try_into().unwrap()) as usize;
if block_size < (1 << MIN_BIN)
|| !block_size.is_power_of_two()
|| block_size > MAX_BLOCK_SIZE
{
return Err(Error::Corruption(format!(
"block at offset {offset} has invalid block_size {block_size}"
)));
}
let payload_cap = block_size - BLOCK_HEADER_SIZE;
if data_len > payload_cap {
return Err(Error::Corruption(format!(
"block at offset {offset}: data_len {data_len} > payload capacity {payload_cap}"
)));
}
let payload = stack.get(
offset + BLOCK_HEADER_SIZE as u64,
offset + block_size as u64,
)?;
if payload.len() != payload_cap {
return Err(Error::InvalidBlock);
}
let mut hasher = CrcHasher::new();
hasher.update(&hdr[4..]);
hasher.update(&payload);
if hasher.finalize() != stored_crc {
return Err(Error::ChecksumMismatch { block: offset });
}
let next = u64::from_le_bytes(hdr[4..12].try_into().unwrap());
let data = payload[..data_len].to_vec();
Ok((next, block_size, data))
}
fn read_block_full(&self, offset: u64) -> Result<(u64, Vec<u8>), Error> {
Self::read_block_full_static(&self.stack, offset).map(|(next, _, data)| (next, data))
}
fn recover_orphans(stack: &BStack, header: &mut DynHeader, total: u64) -> Result<(), Error> {
if total <= HEADER_SIZE {
return Ok(());
}
let max_steps = ((total - HEADER_SIZE) / BLOCK_HEADER_SIZE as u64 + 1) as usize;
let mut active: HashSet<u64> = HashSet::new();
let mut cur = header.root;
let mut steps = 0usize;
while cur != 0 {
if steps >= max_steps {
return Err(Error::Corruption("cycle detected in active list".into()));
}
let (next, _, _) = Self::read_block_full_static(stack, cur)?;
active.insert(cur);
cur = next;
steps += 1;
}
let mut free_blocks: Vec<(u64, usize)> = Vec::new();
let mut scan = HEADER_SIZE;
while scan < total {
if scan + BLOCK_HEADER_SIZE as u64 > total {
break;
}
let mut bs_buf = [0u8; 4];
stack.get_into(scan + 12, &mut bs_buf)?;
let block_size = u32::from_le_bytes(bs_buf) as usize;
if block_size < (1 << MIN_BIN)
|| !block_size.is_power_of_two()
|| block_size > MAX_BLOCK_SIZE
{
return Err(Error::Corruption(format!(
"block at offset {scan} has invalid block_size {block_size} during scan"
)));
}
let block_end = scan + block_size as u64;
if block_end > total {
break;
}
if !active.contains(&scan) {
free_blocks.push((scan, block_size));
}
scan = block_end;
}
if free_blocks.is_empty() {
return Ok(());
}
let mut merged: Vec<(u64, usize)> = Vec::new();
let mut i = 0;
while i < free_blocks.len() {
let run_start = free_blocks[i].0;
let mut run_total = free_blocks[i].1;
let mut j = i + 1;
while j < free_blocks.len() {
let prev_end = free_blocks[j - 1].0 + free_blocks[j - 1].1 as u64;
if prev_end != free_blocks[j].0 {
break;
}
run_total += free_blocks[j].1;
j += 1;
}
if j > i + 1 && run_total.is_power_of_two() {
merged.push((run_start, run_total));
} else {
for &block in free_blocks.iter().take(j).skip(i) {
merged.push(block);
}
}
i = j;
}
header.bin_heads = [0u64; NUM_BINS];
stack.set(0, &header.to_bytes())?;
for &(off, bs) in &merged {
let bin = bin_index(bs);
let old_head = header.bin_heads[bin];
let mut buf = vec![0u8; bs];
buf[4..12].copy_from_slice(&old_head.to_le_bytes());
buf[12..16].copy_from_slice(&(bs as u32).to_le_bytes());
let crc = crc32fast::hash(&buf[4..]);
buf[0..4].copy_from_slice(&crc.to_le_bytes());
stack.set(off, &buf)?;
header.bin_heads[bin] = off;
}
stack.set(0, &header.to_bytes())?;
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::sync::atomic::{AtomicU64, Ordering};
static COUNTER: AtomicU64 = AtomicU64::new(0);
fn tmp(label: &str) -> std::path::PathBuf {
let n = COUNTER.fetch_add(1, Ordering::Relaxed);
let mut p = std::env::temp_dir();
p.push(format!(
"bllist_dyn_{}_{}_{}.blld",
std::process::id(),
label,
n
));
p
}
#[test]
fn block_size_for_values() {
assert_eq!(DynamicBlockList::block_size_for(0), 32); assert_eq!(DynamicBlockList::block_size_for(1), 32); assert_eq!(DynamicBlockList::block_size_for(12), 32); assert_eq!(DynamicBlockList::block_size_for(13), 64); assert_eq!(DynamicBlockList::block_size_for(44), 64); assert_eq!(DynamicBlockList::block_size_for(45), 128); assert_eq!(DynamicBlockList::block_size_for(108), 128); assert_eq!(DynamicBlockList::block_size_for(109), 256); }
#[test]
fn bin_index_values() {
assert_eq!(bin_index(32), 5);
assert_eq!(bin_index(64), 6);
assert_eq!(bin_index(128), 7);
assert_eq!(bin_index(1024), 10);
}
#[test]
fn fresh_open_empty() {
let path = tmp("fresh");
let list = DynamicBlockList::open(&path).unwrap();
assert_eq!(list.root().unwrap(), None);
let _ = std::fs::remove_file(&path);
}
#[test]
fn open_rejects_fixed_list_file() {
let path = tmp("crosstype");
{
crate::FixedBlockList::<52>::open(&path).unwrap();
}
let err = DynamicBlockList::open(&path).unwrap_err();
assert!(matches!(err, Error::Corruption(_)));
let _ = std::fs::remove_file(&path);
}
#[test]
fn alloc_free_reuse_same_bin() {
let path = tmp("reuse");
let list = DynamicBlockList::open(&path).unwrap();
let b0 = list.alloc(1).unwrap();
let b1 = list.alloc(1).unwrap();
let b2 = list.alloc(1).unwrap();
list.free(b1).unwrap();
let b3 = list.alloc(1).unwrap();
assert_eq!(b3, b1);
let _ = (b0, b2, b3);
let _ = std::fs::remove_file(&path);
}
#[test]
fn alloc_different_sizes_different_bins() {
let path = tmp("bins");
let list = DynamicBlockList::open(&path).unwrap();
let small = list.alloc(12).unwrap();
let large = list.alloc(44).unwrap();
assert_eq!(list.capacity(small).unwrap(), 12);
assert_eq!(list.capacity(large).unwrap(), 44);
let _ = (small, large);
let _ = std::fs::remove_file(&path);
}
#[test]
fn split_one_level() {
let path = tmp("split1");
let list = DynamicBlockList::open(&path).unwrap();
let large = list.alloc(44).unwrap(); let large_off = large.0;
list.free(large).unwrap();
let small = list.alloc(1).unwrap(); assert_eq!(small.0, large_off); assert_eq!(list.capacity(small).unwrap(), 12);
let small2 = list.alloc(1).unwrap();
assert_eq!(small2.0, large_off + 32);
let _ = std::fs::remove_file(&path);
}
#[test]
fn split_two_levels() {
let path = tmp("split2");
let list = DynamicBlockList::open(&path).unwrap();
let big = list.alloc(108).unwrap(); let big_off = big.0;
list.free(big).unwrap();
let s = list.alloc(1).unwrap();
assert_eq!(s.0, big_off);
assert_eq!(list.capacity(s).unwrap(), 12);
let s2 = list.alloc(1).unwrap(); assert_eq!(s2.0, big_off + 32);
let m = list.alloc(44).unwrap(); assert_eq!(m.0, big_off + 64);
let _ = std::fs::remove_file(&path);
}
#[test]
fn no_split_beyond_max_split() {
let path = tmp("nosplit");
let list = DynamicBlockList::open(&path).unwrap();
let big = list.alloc(492).unwrap(); let big_off = big.0;
list.free(big).unwrap();
let s = list.alloc(1).unwrap();
assert_ne!(s.0, big_off);
let _ = std::fs::remove_file(&path);
}
#[test]
fn write_read_roundtrip() {
let path = tmp("rw");
let list = DynamicBlockList::open(&path).unwrap();
let block = list.alloc(14).unwrap();
list.write(block, b"hello dynamic!").unwrap();
let out = list.read(block).unwrap();
assert_eq!(out, b"hello dynamic!");
let _ = std::fs::remove_file(&path);
}
#[test]
fn write_updates_data_len() {
let path = tmp("datalen");
let list = DynamicBlockList::open(&path).unwrap();
let block = list.alloc(12).unwrap();
assert_eq!(list.data_len(block).unwrap(), 0);
list.write(block, b"five!").unwrap();
assert_eq!(list.data_len(block).unwrap(), 5);
assert_eq!(list.capacity(block).unwrap(), 12);
let _ = std::fs::remove_file(&path);
}
#[test]
fn overwrite_shorter_zeroes_tail() {
let path = tmp("overwrite");
let list = DynamicBlockList::open(&path).unwrap();
let block = list.alloc(12).unwrap();
list.write(block, b"longer text!").unwrap(); list.write(block, b"short").unwrap();
let out = list.read(block).unwrap();
assert_eq!(out, b"short");
assert_eq!(list.data_len(block).unwrap(), 5);
let _ = std::fs::remove_file(&path);
}
#[test]
fn read_into_exact_length() {
let path = tmp("read_into");
let list = DynamicBlockList::open(&path).unwrap();
let block = list.alloc(12).unwrap();
list.write(block, b"abcdefgh").unwrap();
let mut buf = vec![0u8; 8];
let had_data = list.read_into(block, &mut buf).unwrap();
assert!(had_data);
assert_eq!(buf, b"abcdefgh");
let _ = std::fs::remove_file(&path);
}
#[test]
fn read_into_oversized_buf_ok() {
let path = tmp("read_into_big");
let list = DynamicBlockList::open(&path).unwrap();
let block = list.alloc(12).unwrap();
list.write(block, b"hi").unwrap();
let mut buf = vec![0xFFu8; 12];
let had_data = list.read_into(block, &mut buf).unwrap();
assert!(had_data);
assert_eq!(&buf[0..2], b"hi");
let _ = std::fs::remove_file(&path);
}
#[test]
fn read_into_short_buf_fails_crc() {
let path = tmp("read_into_crc");
let list = DynamicBlockList::open(&path).unwrap();
let block = list.alloc(12).unwrap();
list.write(block, b"twelve bytes").unwrap();
let mut buf = vec![0u8; 4];
let err = list.read_into(block, &mut buf).unwrap_err();
assert!(matches!(err, Error::ChecksumMismatch { .. }));
let _ = std::fs::remove_file(&path);
}
#[test]
fn read_into_empty_block_returns_false() {
let path = tmp("read_into_empty");
let list = DynamicBlockList::open(&path).unwrap();
let block = list.alloc(1).unwrap();
let mut buf = vec![0u8; 12];
let had_data = list.read_into(block, &mut buf).unwrap();
assert!(!had_data);
let _ = std::fs::remove_file(&path);
}
#[test]
fn checksum_mismatch_on_corrupt_block() {
let path = tmp("crc");
{
let list = DynamicBlockList::open(&path).unwrap();
list.push_front(b"integrity check").unwrap();
}
{
let stack = BStack::open(&path).unwrap();
let block_offset = HEADER_SIZE;
let payload_offset = block_offset + BLOCK_HEADER_SIZE as u64;
let mut byte = [0u8; 1];
stack.get_into(payload_offset, &mut byte).unwrap();
byte[0] ^= 0xFF;
stack.set(payload_offset, &byte).unwrap();
}
let err = DynamicBlockList::open(&path).unwrap_err();
assert!(matches!(err, Error::ChecksumMismatch { .. }));
let _ = std::fs::remove_file(&path);
}
#[test]
fn set_get_next() {
let path = tmp("next");
let list = DynamicBlockList::open(&path).unwrap();
let b0 = list.alloc(1).unwrap();
let b1 = list.alloc(1).unwrap();
assert_eq!(list.get_next(b0).unwrap(), None);
list.set_next(b0, Some(b1)).unwrap();
assert_eq!(list.get_next(b0).unwrap(), Some(b1));
list.set_next(b0, None).unwrap();
assert_eq!(list.get_next(b0).unwrap(), None);
let _ = std::fs::remove_file(&path);
}
#[test]
fn set_next_preserves_payload() {
let path = tmp("next_payload");
let list = DynamicBlockList::open(&path).unwrap();
let b0 = list.alloc(12).unwrap();
let b1 = list.alloc(12).unwrap();
list.write(b0, b"preserved!!!").unwrap();
list.set_next(b0, Some(b1)).unwrap();
let out = list.read(b0).unwrap();
assert_eq!(out, b"preserved!!!");
let _ = std::fs::remove_file(&path);
}
#[test]
fn push_pop_lifo() {
let path = tmp("lifo");
let list = DynamicBlockList::open(&path).unwrap();
list.push_front(b"first").unwrap();
list.push_front(b"second longer").unwrap();
list.push_front(b"third").unwrap();
assert_eq!(list.pop_front().unwrap().unwrap(), b"third");
assert_eq!(list.pop_front().unwrap().unwrap(), b"second longer");
assert_eq!(list.pop_front().unwrap().unwrap(), b"first");
assert_eq!(list.pop_front().unwrap(), None);
let _ = std::fs::remove_file(&path);
}
#[test]
fn push_pop_mixed_sizes() {
let path = tmp("mixed");
let list = DynamicBlockList::open(&path).unwrap();
list.push_front(&[0u8; 1]).unwrap(); list.push_front(&[1u8; 100]).unwrap(); list.push_front(&[2u8; 10]).unwrap();
let d3 = list.pop_front().unwrap().unwrap();
assert_eq!(d3, vec![2u8; 10]);
let d2 = list.pop_front().unwrap().unwrap();
assert_eq!(d2, vec![1u8; 100]);
let d1 = list.pop_front().unwrap().unwrap();
assert_eq!(d1, vec![0u8; 1]);
let _ = std::fs::remove_file(&path);
}
#[test]
fn pop_front_into_basic() {
let path = tmp("pop_into");
let list = DynamicBlockList::open(&path).unwrap();
list.push_front(b"hello").unwrap();
let mut buf = vec![0u8; 5];
assert!(list.pop_front_into(&mut buf).unwrap());
assert_eq!(buf, b"hello");
assert!(!list.pop_front_into(&mut buf).unwrap());
let _ = std::fs::remove_file(&path);
}
#[test]
fn pop_front_empty() {
let path = tmp("pop_empty");
let list = DynamicBlockList::open(&path).unwrap();
assert_eq!(list.pop_front().unwrap(), None);
let _ = std::fs::remove_file(&path);
}
#[test]
fn orphan_recovery() {
let path = tmp("orphan");
let orphan_offset;
{
let stack = BStack::open(&path).unwrap();
let hdr = DynHeader {
root: 0,
bin_heads: [0u64; NUM_BINS],
};
let off = stack.push(&hdr.to_bytes()).unwrap();
assert_eq!(off, 0);
let bs: usize = 32;
let mut block_buf = vec![0u8; bs];
block_buf[12..16].copy_from_slice(&(bs as u32).to_le_bytes());
let crc = crc32fast::hash(&block_buf[4..]);
block_buf[0..4].copy_from_slice(&crc.to_le_bytes());
orphan_offset = stack.push(&block_buf).unwrap();
}
let list = DynamicBlockList::open(&path).unwrap();
assert_eq!(list.root().unwrap(), None);
let b = list.alloc(1).unwrap();
assert_eq!(b.0, orphan_offset);
let _ = std::fs::remove_file(&path);
}
#[test]
fn coalesce_two_adjacent_free_blocks() {
let path = tmp("coalesce2");
{
let list = DynamicBlockList::open(&path).unwrap();
let b0 = list.alloc(1).unwrap();
let b1 = list.alloc(1).unwrap();
assert_eq!(b1.0, b0.0 + 32);
list.free(b0).unwrap();
list.free(b1).unwrap();
}
let list = DynamicBlockList::open(&path).unwrap();
let big = list.alloc(44).unwrap(); assert_eq!(big.0, HEADER_SIZE);
let _ = std::fs::remove_file(&path);
}
#[test]
fn coalesce_three_adjacent_free_blocks() {
let path = tmp("coalesce3");
let b0_off;
{
let list = DynamicBlockList::open(&path).unwrap();
let b0 = list.alloc(236).unwrap(); let b1 = list.alloc(492).unwrap(); let b2 = list.alloc(236).unwrap(); b0_off = b0.0;
assert_eq!(b1.0, b0.0 + 256);
assert_eq!(b2.0, b0.0 + 768);
list.free(b0).unwrap();
list.free(b1).unwrap();
list.free(b2).unwrap();
}
let list = DynamicBlockList::open(&path).unwrap();
let big = list.alloc(1004).unwrap(); assert_eq!(big.0, b0_off);
let _ = std::fs::remove_file(&path);
}
#[test]
fn data_too_large_for_block() {
let path = tmp("toolarge");
let list = DynamicBlockList::open(&path).unwrap();
let block = list.alloc(1).unwrap(); let err = list.write(block, &[0u8; 13]).unwrap_err(); assert!(matches!(err, Error::DataTooLarge { .. }));
let _ = std::fs::remove_file(&path);
}
#[test]
fn invalid_block_offset() {
let path = tmp("invalid");
let list = DynamicBlockList::open(&path).unwrap();
let err = list.read(DynBlockRef(0)).unwrap_err();
assert!(matches!(err, Error::InvalidBlock));
let _ = std::fs::remove_file(&path);
}
#[test]
fn reopen_persists_data() {
let path = tmp("reopen");
{
let list = DynamicBlockList::open(&path).unwrap();
list.push_front(b"persisted across reopen").unwrap();
}
{
let list = DynamicBlockList::open(&path).unwrap();
let data = list.pop_front().unwrap().unwrap();
assert_eq!(data, b"persisted across reopen");
assert_eq!(list.pop_front().unwrap(), None);
}
let _ = std::fs::remove_file(&path);
}
#[test]
fn free_list_persists_across_reopen() {
let path = tmp("freelist_reopen");
let freed_offset;
{
let list = DynamicBlockList::open(&path).unwrap();
let b = list.alloc(12).unwrap();
freed_offset = b.0;
list.free(b).unwrap();
}
{
let list = DynamicBlockList::open(&path).unwrap();
let b = list.alloc(12).unwrap();
assert_eq!(b.0, freed_offset);
}
let _ = std::fs::remove_file(&path);
}
}