bblock 0.1.0

Persistent checksummed blocks built on top of bstack
Documentation
  • Coverage
  • 100%
    44 out of 44 items documented1 out of 43 items with examples
  • Size
  • Source code size: 67.42 kB This is the summed size of all the files inside the crates.io package for this release.
  • Documentation size: 4.58 MB This is the summed size of all files generated by rustdoc for all configured targets
  • Ø build duration
  • this release: 58s Average build duration of successful builds.
  • all releases: 58s Average build duration of successful builds in releases after 2024-10-23.
  • Links
  • williamwutq/bblock
    1 0 0
  • crates.io
  • Dependencies
  • Versions
  • Owners
  • williamwutq

bblock

bblock wraps bstack allocators to provide persistent, checksummed blocks. Every allocation carries a 4-byte CRC32 trailer; verify() tells you at any time whether the stored bytes match the checksum.

Crates.io Docs.rs License: MIT


Features

  • Transparent checksumming — allocate as usual; the 4-byte CRC32 trailer is managed automatically by the safe API.
  • Sub-range viewsBBlockView::subview(start, end) lets you operate on a named field of a record; writes still update the full-block checksum.
  • Cursor-based I/OBBlockReader and BBlockWriter implement io::Read/io::Write/io::Seek with the same checksum guarantees.
  • Allocator-agnosticBBlockAllocator<A> works with any A: BStackAllocator; no concrete allocator is imported by this crate.

Quick start

Add to Cargo.toml:

[dependencies]
bblock = "0.1"
bstack = { version = ">=0.1.6", features = ["alloc", "set"] }
use bstack::{BStack, LinearBStackAllocator};
use bblock::BBlockAllocator;

// Open (or create) a bstack file and wrap the allocator.
let stack = BStack::open("data.bstk").unwrap();
let alloc = BBlockAllocator::new(LinearBStackAllocator::new(stack));

// Allocate a 16-byte block.  On disk it occupies 20 bytes (16 + 4 checksum).
let block = alloc.alloc(16).unwrap();
let view = block.view();

view.write(b"hello, bblock!!!").unwrap();
assert!(view.verify().unwrap()); // checksum is valid

// Sub-range views update the full-block checksum automatically.
view.subview(0, 5).write(b"world").unwrap();
assert!(block.verify().unwrap()); // still valid; full block was re-checksummed

// Cursor-based writer.
use std::io::Write;
let mut w = block.writer();
w.write_all(b"cursor  write!!!").unwrap();
assert!(block.verify().unwrap());

API overview

Type Description
BBlockAllocator<A> Wraps A: BStackAllocator; alloc, realloc, dealloc
BBlock<'a, A> Checksummed block handle; Copy; source of views and cursors
BBlockView<'a, A> Safe read/write window; supports subview
BBlockReader<'a, A> io::Read + io::Seek over the view's data range
BBlockWriter<'a, A> io::Write + io::Seek; recomputes checksum after every write
CHECKSUM_LENGTH 4 — the CRC32 trailer size in bytes

BBlock<'a, A>

impl<'a, A: BStackAllocator> BBlock<'a, A> {
    // Dimensions
    pub fn len(&self) -> u64;
    pub fn is_empty(&self) -> bool;

    // Serialisation
    pub fn to_bytes(&self) -> [u8; 16];
    pub fn from_bytes(allocator: &'a A, bytes: [u8; 16]) -> Self;

    // Integrity
    pub fn checksum(&self) -> io::Result<u32>;
    pub fn verify(&self) -> io::Result<bool>;

    // Safe access
    pub fn view(&self) -> BBlockView<'a, A>;
    pub fn reader(&self) -> BBlockReader<'a, A>;
    pub fn reader_at(&self, offset: u64) -> BBlockReader<'a, A>;
    pub fn writer(&self) -> BBlockWriter<'a, A>;
    pub fn writer_at(&self, offset: u64) -> BBlockWriter<'a, A>;

    // Unsafe escape hatch — checksum is no longer tracked
    pub unsafe fn into_slice(self) -> BStackSlice<'a, A>;
}

BBlockView<'a, A>

impl<'a, A: BStackAllocator> BBlockView<'a, A> {
    pub fn new(block: &BBlock<'a, A>) -> Self;

    // Dimensions (relative to this view's range)
    pub fn len(&self) -> u64;
    pub fn is_empty(&self) -> bool;

    // Sub-range — coordinates are relative to this view's start
    pub fn subview(&self, start: u64, end: u64) -> Self;

    // Read (from this view's range)
    pub fn read(&self) -> io::Result<Vec<u8>>;
    pub fn read_into(&self, buf: &mut [u8]) -> io::Result<()>;
    pub fn read_range_into(&self, start: u64, buf: &mut [u8]) -> io::Result<()>;

    // Write (to this view's range; always recomputes the full-block checksum)
    pub fn write(&self, data: &[u8]) -> io::Result<()>;
    pub fn write_range(&self, start: u64, data: &[u8]) -> io::Result<()>;
    pub fn zero(&self) -> io::Result<()>;
    pub fn zero_range(&self, start: u64, n: u64) -> io::Result<()>;

    // Integrity (always over the full block, not just this view's range)
    pub fn checksum(&self) -> io::Result<u32>;
    pub fn verify(&self) -> io::Result<bool>;

    // Cursors
    pub fn reader(&self) -> BBlockReader<'a, A>;
    pub fn reader_at(&self, offset: u64) -> BBlockReader<'a, A>;
    pub fn writer(&self) -> BBlockWriter<'a, A>;
    pub fn writer_at(&self, offset: u64) -> BBlockWriter<'a, A>;
}

Limitations and caveats

This crate detects corruption; it does not repair it.
verify() returning false means the data should not be trusted, but bblock provides no mechanism to restore a previous good value.

Checksumming is not part of the allocator's recovery strategy.
bstack's crash recovery operates on committed-length metadata, independently of bblock's checksums. The recovery strategies of different allocators vary. If you need checksum-based recovery baked into the allocator itself, use an allocator that natively supports it.

unsafe code, direct bstack writes, and buggy allocators are not covered.
The checksum is maintained only when you write through the safe API (BBlockView, BBlockWriter). Writing through a raw BStackSlice from BBlock::into_slice, using bstack directly on the same region, or relying on a buggy allocator will all produce stale or incorrect checksums.

If the checksum itself is corrupted, verify() cannot help you.
CRC32 catches the vast majority of real-world corruption scenarios, but it is not a cryptographic guarantee. If both data and checksum are overwritten consistently (e.g., a device returning all-zeros), verify() may return true for corrupted data. For applications requiring strong consistency guarantees, checksums are a useful building block but are not a substitute for write-ahead logs, copy-on-write, two-phase commit, or other proper recovery strategies.

Avoid double-wrapping small blocks.
Embedding a serialised BBlock reference (16 bytes) inside another BBlock is valid, but the 4-byte checksum overhead is proportionally significant for small payloads. Prefer coarser-grained checksumming for small structures.


License

MIT — see LICENSE.