Expand description
§bllist
bllist provides durable, crash-safe, checksummed block-based linked-list
allocators built on top of a single bstack file.
Two allocator types are available:
| Type | Block size | File magic | Use when |
|---|---|---|---|
FixedBlockList<PAYLOAD_CAPACITY> | constant | "BLLS" | All records are the same size |
DynamicBlockList | variable (power-of-two bins) | "BLLD" | Records vary in size |
The two types use different file formats and cannot open each other’s files. Both inherit BStack’s exclusive advisory lock, durable fsync writes, and crash-recovery guarantees.
§Quick start — fixed-size blocks
use bllist::FixedBlockList;
// 52 bytes of payload per block (64 bytes total on disk).
let list = FixedBlockList::<52>::open("data.blls")?;
list.push_front(b"hello")?;
list.push_front(b"world")?;
while let Some(data) = list.pop_front()? {
println!("{}", String::from_utf8_lossy(&data));
}
// prints "world", then "hello"§Quick start — variable-size blocks
use bllist::DynamicBlockList;
// The total on-disk block size (header + payload) is a power of two.
// A 5-byte push occupies 32 bytes on disk (5+20=25 → 32, bin 5).
let list = DynamicBlockList::open("data.blld")?;
list.push_front(b"short")?;
list.push_front(b"a somewhat longer record")?;
while let Some(data) = list.pop_front()? {
println!("{}", String::from_utf8_lossy(&data));
}§File layouts
§FixedBlockList ("BLLS")
┌──────────────────────────┬───────────────────────────────────────────┐
│ BStack header (16 B) │ bllist header (24 B at logical offset 0) │
│ "BSTK" magic + clen │ "BLLS" + version + root + free_head │
├──────────────────────────┴───────────────────────────────────────────┤
│ Block 0 (PAYLOAD_CAPACITY+12 bytes, logical offset 24) │
│ checksum(4) │ next(8) │ payload(PAYLOAD_CAPACITY) │
├──────────────────────────────────────────────────────────────────────┤
│ Block 1 … │
└──────────────────────────────────────────────────────────────────────┘§DynamicBlockList ("BLLD")
┌──────────────────────────┬───────────────────────────────────────────────┐
│ BStack header (16 B) │ bllist-dynamic header (272 B, logical off 0) │
│ "BSTK" magic + clen │ "BLLD" + version + root + bin_heads[32] │
├──────────────────────────┴───────────────────────────────────────────────┤
│ Block (total size = 2^k bytes, k ≥ 5) │
│ checksum(4) │ next(8) │ block_size(4) │ data_len(4) │ payload(bs-20 B) │
├──────────────────────────────────────────────────────────────────────────┤
│ Block … │
└──────────────────────────────────────────────────────────────────────────┘Bin k holds free blocks whose total on-disk size equals 2^k bytes. The minimum block is bin 5 (32 bytes total, 12-byte payload). Large free blocks may be split to satisfy smaller requests; adjacent free blocks may be coalesced on open.
The BStack header is managed transparently by the bstack crate;
callers only see logical offsets starting at 0.
§Crash safety
Every mutation flushes durably (via fsync / F_FULLFSYNC) before
returning. If the process is killed mid-operation the worst case is one
orphaned block that is silently reclaimed the next time the file is
opened.
§Streaming reads (DynamicBlockList)
read and read_into
always verify the CRC and copy or allocate on every call. For large
payloads — or when you need to pass a byte range to another layer (e.g.
sendfile, a scatter-gather buffer, or an async runtime) — you can compute
the exact file offsets and issue a single raw read through the underlying
BStack:
use bllist::DynamicBlockList;
let list = DynamicBlockList::open("data.blld")?;
// data_start is pure — no I/O, no Result.
let start: u64 = block.data_start();
// data_end reads the 4-byte data_len field.
let end: u64 = list.data_end(block)?;
// One pread directly into a caller-owned buffer; no CRC, no allocation.
let mut buf = vec![0u8; (end - start) as usize];
list.bstack().get_into(start, &mut buf)?;
// Or stream a sub-range into an existing buffer:
list.bstack().get_into(start, &mut frame[frame_offset..])?;Only read-only BStack operations are safe (get, get_into, peek,
len). Never call push, pop, or set on the handle returned by
bstack() — doing so can silently corrupt the
list structure. Use read or
read_into when CRC verification matters.
§Direct file access — use with extreme caution
Both list types produce valid BStack files, so you can open them with
bstack::BStack::open or inspect raw bytes with any file tool.
Writing to the file outside of bllist is strongly discouraged.
bllist does not re-validate structural invariants on every operation, so
direct writes can silently corrupt the list in ways that are not caught
until much later — or not caught at all.
| Direct BStack operation | Risk |
|---|---|
BStack::push | Appends raw bytes that are not a complete, aligned block; corrupts slot enumeration and orphan recovery |
BStack::pop | May truncate a block mid-stream or destroy the list header |
BStack::set at header offsets | Overwrites root or free-list / bin-head pointers |
BStack::set inside a block | Invalidates the block’s CRC; read returns Error::ChecksumMismatch |
Raw file writes (write(2), etc.) | Bypasses the advisory lock entirely; any of the above, plus torn writes |
The exclusive advisory lock (flock on Unix, LockFileEx on Windows)
held by a live list prevents a second process from opening the same file
through BStack simultaneously. It does not prevent raw file-descriptor
access.
Safe read-only inspection is possible: open the file with
bstack::BStack::open and use only get, peek, and len. These
calls do not write to the file and will not disturb the list state.
Mutating calls (push, pop, set) must not be used.
Re-exports§
pub use dynamic::DynBlockRef;pub use dynamic::DynamicBlockList;pub use error::Error;pub use fixed::BlockRef;pub use fixed::FixedBlockList;