Skip to main content

bblock/checksum/
crc.rs

1//! CRC32-checksummed persistent blocks built on top of [`bstack`](https://docs.rs/bstack).
2//!
3//! # Overview
4//!
5//! This module wraps any [`BStackAllocator`] and appends a 4-byte CRC32 checksum
6//! to every allocation. You verify integrity at any time with
7//! [`BCrcBlock::verify`] or [`BCrcBlockView::verify`]; if the stored checksum does
8//! not match the current data, the block has been corrupted since it was last
9//! written through the safe API.
10//!
11//! The main types:
12//!
13//! | Type                | Role                                                            |
14//! |---------------------|-----------------------------------------------------------------|
15//! | [`BCrcBlockAllocator`] | Wraps a `BStackAllocator`; produces [`BCrcBlock`]s                 |
16//! | [`BCrcBlock`]          | Checksummed block handle; source of views, readers, and writers |
17//! | [`BCrcBlockView`]      | Safe read/write window with `subview` support                   |
18//! | [`BCrcBlockReader`]    | Cursor-based `io::Read + io::Seek` over a view's data           |
19//! | [`BCrcBlockWriter`]    | Cursor-based `io::Write + io::Seek` that maintains the checksum |
20//!
21//! # What this crate protects you from
22//!
23//! **Undetected silent corruption** — bit rot, partial writes, and other
24//! storage anomalies that change bytes without signalling an error. `verify()`
25//! catches these as long as the checksum bytes themselves are intact.
26//!
27//! # What this crate does *not* protect you from
28//!
29//! * **`unsafe` code bypassing checksum tracking.** Writing through a raw
30//!   [`BStackSlice`] obtained via [`BCrcBlock::into_slice`] leaves the checksum
31//!   stale. The safe API ([`BCrcBlockView`], [`BCrcBlockWriter`]) always recomputes it.
32//! * **A buggy or malicious allocator.** If the underlying [`BStackAllocator`]
33//!   writes to the wrong offsets or lengths, checksums cannot compensate.
34//! * **Direct use of `bstack`.** Writing to the same region through a
35//!   `bstack` handle updates the data but not the checksum.
36//!
37//! # Detection, not recovery
38//!
39//! This module only **detects** corruption — it does not repair, revert, or
40//! reconstruct. `verify()` returning `false` means the data must not be trusted,
41//! but there is no mechanism to restore a previous good value.
42//!
43//! # Limitations and caveats
44//!
45//! * **If the checksum bytes are also corrupted**, `verify()` may return `true`
46//!   for corrupt data or `false` for intact data. CRC32 catches the vast majority
47//!   of real-world corruption but is not a cryptographic guarantee.
48//! * **Avoid double-wrapping small blocks.** The 4-byte checksum overhead is
49//!   proportionally large for tiny payloads.
50//!
51//! # Write cost
52//!
53//! Every write recomputes CRC32 over the **full block data**, regardless of how
54//! many bytes were changed. For large blocks with many small writes, prefer the
55//! [`xor`](crate::xor) types which update the checksum incrementally.
56//!
57//! # Composability
58//!
59//! [`BCrcBlockAllocator`] implements [`BStackAllocator`], so it can be passed to
60//! any generic API that accepts `T: BStackAllocator`. In particular, this is
61//! what allows [`BCrcBlock`] to implement [`bstack::BStackGuardedSlice`] without
62//! requiring the stricter `BStackSliceAllocator` bound. It also means the
63//! wrappers can be stacked: `BXorBlockAllocator<BCrcBlockAllocator<A>>` and
64//! `BCrcBlockAllocator<BXorBlockAllocator<A>>` both compile.
65//!
66//! # bstack `guarded` feature
67//!
68//! [`BCrcBlock`] and [`BCrcBlockView`] implement [`bstack::BStackGuardedSlice`].
69//! `as_slice()` exposes only the data region (for views, the view's
70//! sub-range). `write()` and `zero()` recompute the CRC32 checksum over the
71//! full block after each mutation. [`BCrcBlockView`] additionally implements
72//! [`bstack::BStackGuardedSliceSubview`], enabling its use in generic
73//! guarded-I/O contexts.
74//!
75//! `len()` and `is_empty()` on [`BCrcBlock`], and `len()`, `is_empty()`,
76//! `read()`, `write()`, and `zero()` on [`BCrcBlockView`] are provided by the
77//! trait — callers must `use bstack::BStackGuardedSlice`.
78
79use crate::{BStackRawAllocator, BlockStart};
80use bstack::{
81    BStack, BStackAllocator, BStackGuardedSlice, BStackGuardedSliceSubview, BStackSlice,
82    BStackSliceReader, BStackSliceWriter,
83};
84use std::cmp::Ordering;
85use std::fmt;
86use std::hash::{Hash, Hasher};
87use std::io;
88
89/// Number of bytes appended to every allocation for the CRC32 checksum.
90///
91/// CRC32 produces a 32-bit (4-byte) value stored in little-endian order
92/// immediately after the usable data in each block.
93pub const CHECKSUM_LENGTH: u64 = 4;
94
95/// Generic wrapper over any [`BStackAllocator`] that transparently appends a
96/// 4-byte CRC32 checksum to every allocation.
97///
98/// `BCrcBlockAllocator<A>` mirrors the allocation interface of the inner `A` but
99/// returns [`BCrcBlock`]s instead of raw [`BStackSlice`]s. Each [`BCrcBlock`] has
100/// `CHECKSUM_LENGTH` (4) extra bytes appended, so an `alloc(n)` call allocates
101/// `n + 4` bytes in the underlying stack.
102///
103/// The wrapper holds ownership of the inner allocator and exposes it via
104/// [`inner`](BCrcBlockAllocator::inner) and
105/// [`into_inner`](BCrcBlockAllocator::into_inner) for cases where direct access
106/// to the underlying stack is needed — for example to reconstruct a
107/// [`BCrcBlock`] from a serialised reference via [`BCrcBlock::from_bytes`].
108///
109/// No concrete allocator type is imported; the crate is intentionally
110/// allocator-agnostic and works with any type that satisfies
111/// `A: BStackAllocator`.
112///
113/// ## `BStackAllocator` impl
114///
115/// `BCrcBlockAllocator<A>` itself implements [`BStackAllocator`] with
116/// `Allocated<'_> = BCrcBlock<'_, BCrcBlockAllocator<A>>`. This means it can be
117/// used as the inner allocator for another wrapper, allowing checksum layers
118/// to be stacked.
119pub struct BCrcBlockAllocator<A: BStackAllocator> {
120    inner: A,
121}
122
123impl<A: BStackAllocator> BCrcBlockAllocator<A> {
124    /// Create a new `BCrcBlockAllocator` wrapping `inner`.
125    pub fn new(inner: A) -> Self {
126        Self { inner }
127    }
128
129    /// Return a shared reference to the inner allocator.
130    pub fn inner(&self) -> &A {
131        &self.inner
132    }
133
134    /// Consume the wrapper and return the inner allocator.
135    pub fn into_inner(self) -> A {
136        self.inner
137    }
138}
139
140/// A handle to a checksummed block allocated by a [`BCrcBlockAllocator`].
141///
142/// **Backing layout:** `[data: len bytes][crc32: 4 bytes LE]`
143///
144/// The first `len` bytes are the usable payload. The last 4 bytes hold the
145/// CRC32 checksum of that payload in little-endian order.
146///
147/// `BCrcBlock` is `Copy`: every copy refers to the same physical region in the
148/// underlying file, so mutations through one copy are immediately visible
149/// through any other copy (or a derived [`BCrcBlockView`]).
150///
151/// ## Safe path
152///
153/// Use [`view`](BCrcBlock::view) to get a [`BCrcBlockView`], then call its read and
154/// write methods. Every write recomputes and stores the checksum. Call
155/// [`verify`](BCrcBlock::verify) at any time to confirm integrity.
156///
157/// [`reader`](BCrcBlock::reader) and [`writer`](BCrcBlock::writer) provide
158/// cursor-based `io::Read`/`io::Write` access with the same checksum guarantees.
159///
160/// ## `BStackGuardedSlice`
161///
162/// `BCrcBlock` implements [`bstack::BStackGuardedSlice`] (requires the bstack
163/// `guarded` feature, enabled by default in this crate). `as_slice()` returns
164/// only the data region, hiding the checksum trailer. `write()` and `zero()`
165/// recompute the CRC32 checksum after each mutation. `len()` and `is_empty()`
166/// are provided by this trait; callers must `use bstack::BStackGuardedSlice`.
167///
168/// ## Unsafe escape hatch
169///
170/// [`into_slice`](BCrcBlock::into_slice) consumes the block and returns the raw
171/// [`BStackSlice`] (including the checksum trailer). Any mutation through that
172/// slice bypasses checksum tracking; use it only when you specifically need
173/// to operate outside the checksum layer.
174#[derive(Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
175pub struct BCrcBlock<'a, A: BStackAllocator> {
176    slice: BStackSlice<'a, A>,
177    len: u64,
178}
179
180impl<'a, A: BStackAllocator> Copy for BCrcBlock<'a, A> {}
181
182impl<'a, A: BStackAllocator> Clone for BCrcBlock<'a, A> {
183    fn clone(&self) -> Self {
184        *self
185    }
186}
187
188impl<'a, A: BStackAllocator> BCrcBlock<'a, A> {
189    /// Serialize this block reference as a 16-byte array.
190    ///
191    /// The format is `[offset: u64 LE | usable_len: u64 LE]`. Reconstruct
192    /// with [`BCrcBlock::from_bytes`].
193    pub fn to_bytes(&self) -> [u8; 16] {
194        let mut out = [0u8; 16];
195        out[..8].copy_from_slice(&self.slice.start().to_le_bytes());
196        out[8..].copy_from_slice(&self.len.to_le_bytes());
197        out
198    }
199
200    /// Reconstruct a block reference from a 16-byte array produced by
201    /// [`BCrcBlock::to_bytes`].
202    pub fn from_bytes(allocator: &'a A, bytes: [u8; 16]) -> Self {
203        let offset = u64::from_le_bytes(bytes[..8].try_into().unwrap());
204        let len = u64::from_le_bytes(bytes[8..].try_into().unwrap());
205        BCrcBlock {
206            slice: unsafe { BStackSlice::from_raw_parts(allocator, offset, len + CHECKSUM_LENGTH) },
207            len,
208        }
209    }
210
211    /// Read the stored CRC32 checksum from the backing store.
212    pub fn checksum(&self) -> io::Result<u32> {
213        let mut buf = [0u8; 4];
214        unsafe { self.checksum_slice() }.read_into(&mut buf)?;
215        Ok(u32::from_le_bytes(buf))
216    }
217
218    /// Return `true` if the stored checksum matches a freshly computed CRC32
219    /// of the current data bytes.
220    pub fn verify(&self) -> io::Result<bool> {
221        let data = unsafe { self.data_slice() }.read()?;
222        let stored = self.checksum()?;
223        Ok(crc32fast::hash(&data) == stored)
224    }
225
226    /// Return a [`BCrcBlockView`] covering the full usable data region.
227    ///
228    /// The view shares the same backing region as this block; both remain
229    /// independently usable because [`BStackSlice`] is `Copy`.
230    pub fn view(&self) -> BCrcBlockView<'a, A> {
231        BCrcBlockView {
232            slice: self.slice,
233            full_len: self.len,
234            start: 0,
235            end: self.len,
236        }
237    }
238
239    /// Return a cursor-based reader positioned at the start of the usable data.
240    pub fn reader(&self) -> BCrcBlockReader<'a, A> {
241        BCrcBlockReader {
242            inner: unsafe { self.data_slice() }.reader(),
243        }
244    }
245
246    /// Return a cursor-based reader positioned at `offset` within the usable data.
247    pub fn reader_at(&self, offset: u64) -> BCrcBlockReader<'a, A> {
248        BCrcBlockReader {
249            inner: unsafe { self.data_slice() }.reader_at(offset),
250        }
251    }
252
253    /// Return a cursor-based writer positioned at the start of the usable data.
254    ///
255    /// Every write through the returned [`BCrcBlockWriter`] automatically
256    /// recomputes and persists the CRC32 checksum over the full data region.
257    pub fn writer(&self) -> BCrcBlockWriter<'a, A> {
258        let full_data = unsafe { self.data_slice() };
259        BCrcBlockWriter {
260            inner: full_data.writer(),
261            full_data,
262            checksum: unsafe { self.checksum_slice() },
263        }
264    }
265
266    /// Return a cursor-based writer positioned at `offset` within the usable data.
267    ///
268    /// Every write through the returned [`BCrcBlockWriter`] automatically
269    /// recomputes and persists the CRC32 checksum over the full data region.
270    pub fn writer_at(&self, offset: u64) -> BCrcBlockWriter<'a, A> {
271        let full_data = unsafe { self.data_slice() };
272        BCrcBlockWriter {
273            inner: full_data.writer_at(offset),
274            full_data,
275            checksum: unsafe { self.checksum_slice() },
276        }
277    }
278
279    /// Consume the block and return the raw underlying [`BStackSlice`].
280    ///
281    /// # Safety
282    ///
283    /// Any mutation of the returned slice bypasses checksum tracking. After
284    /// calling this function the caller is responsible for maintaining or
285    /// ignoring checksum integrity.
286    pub unsafe fn into_slice(self) -> BStackSlice<'a, A> {
287        self.slice
288    }
289
290    unsafe fn data_slice(&self) -> BStackSlice<'a, A> {
291        self.slice.subslice(0, self.len)
292    }
293
294    unsafe fn checksum_slice(&self) -> BStackSlice<'a, A> {
295        self.slice.subslice(self.len, self.len + CHECKSUM_LENGTH)
296    }
297}
298
299/// A safe read/write window into a sub-range of a [`BCrcBlock`]'s usable data.
300///
301/// A full-block view is obtained via [`BCrcBlock::view`];
302/// a sub-range view via [`BCrcBlockView::subview`]. Sub-range coordinates are
303/// always **relative** to the current view's start, mirroring the convention
304/// used by `BStackSlice::subslice`.
305///
306/// ## Checksum scope
307///
308/// All write operations — including those through a sub-range view — recompute
309/// the CRC32 over the **full block data**, not just the bytes the view covers.
310/// Likewise, [`verify`](BCrcBlockView::verify) always checks the full block,
311/// regardless of how narrow the view is.
312///
313/// This means a corrupted byte outside the view's range will still be caught
314/// by `verify()`, and a write inside the view will not leave the rest of the
315/// block's checksum stale.
316///
317/// ## What sub-views are for
318///
319/// Sub-views are a convenience for operating on a named field or section of a
320/// larger record without having to track absolute offsets. They do not create
321/// independent integrity domains: there is still one checksum per block, and
322/// all views share it.
323///
324/// ## `BStackGuardedSlice` and `BStackGuardedSliceSubview`
325///
326/// `BCrcBlockView` implements [`bstack::BStackGuardedSlice`]: `as_slice()`
327/// returns the bytes covered by this view; `write()` and `zero()` recompute
328/// the CRC32 over the full block. `len()`, `is_empty()`, `read()`, `write()`,
329/// and `zero()` are provided by this trait; callers must
330/// `use bstack::BStackGuardedSlice`.
331///
332/// `BCrcBlockView` also implements [`bstack::BStackGuardedSliceSubview`],
333/// enabling its use where a `T: BStackGuardedSliceSubview` bound is required.
334/// The inherent [`subview`](BCrcBlockView::subview) method returns the concrete
335/// `BCrcBlockView` type and is preferred for direct calls.
336#[derive(Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
337pub struct BCrcBlockView<'a, A: BStackAllocator> {
338    /// The full block allocation: `[data: full_len bytes][checksum: 4 bytes]`.
339    slice: BStackSlice<'a, A>,
340    /// Length of the full usable data region (used for checksum recomputation).
341    full_len: u64,
342    /// Inclusive start of this view within the data region.
343    start: u64,
344    /// Exclusive end of this view within the data region.
345    end: u64,
346}
347
348impl<'a, A: BStackAllocator> Copy for BCrcBlockView<'a, A> {}
349
350impl<'a, A: BStackAllocator> Clone for BCrcBlockView<'a, A> {
351    fn clone(&self) -> Self {
352        *self
353    }
354}
355
356impl<'a, A: BStackAllocator> From<BCrcBlock<'a, A>> for [u8; 16] {
357    fn from(block: BCrcBlock<'a, A>) -> [u8; 16] {
358        block.to_bytes()
359    }
360}
361
362impl<'a, A: BStackAllocator> BCrcBlockView<'a, A> {
363    /// Create a full-block view from an existing [`BCrcBlock`].
364    pub fn new(block: &BCrcBlock<'a, A>) -> Self {
365        Self {
366            slice: block.slice,
367            full_len: block.len,
368            start: 0,
369            end: block.len,
370        }
371    }
372
373    /// Return a view covering `[start, end)` within this view's coordinate space.
374    ///
375    /// Coordinates are relative: `subview(0, 3)` on a view that itself starts at
376    /// byte 5 of the block produces a view covering bytes 5–7 of the block.
377    ///
378    /// Writes through the returned view update the **full block checksum**.
379    pub fn subview(&self, start: u64, end: u64) -> Self {
380        BCrcBlockView {
381            slice: self.slice,
382            full_len: self.full_len,
383            start: self.start + start,
384            end: self.start + end,
385        }
386    }
387
388    /// Read all bytes in this view into `buf`.
389    pub fn read_into(&self, buf: &mut [u8]) -> io::Result<()> {
390        unsafe { self.data_slice() }.read_into(buf)
391    }
392
393    /// Read bytes starting at `start` within this view into `buf`.
394    pub fn read_range_into(&self, start: u64, buf: &mut [u8]) -> io::Result<()> {
395        unsafe { self.data_slice() }.read_range_into(start, buf)
396    }
397
398    /// Read the stored CRC32 checksum of the containing block.
399    pub fn checksum(&self) -> io::Result<u32> {
400        let mut buf = [0u8; 4];
401        unsafe { self.checksum_slice() }.read_into(&mut buf)?;
402        Ok(u32::from_le_bytes(buf))
403    }
404
405    /// Return `true` if the containing block's stored checksum matches its
406    /// current full data.
407    ///
408    /// This always verifies the **full block**, regardless of whether this is
409    /// a subview.
410    pub fn verify(&self) -> io::Result<bool> {
411        let data = unsafe { self.full_data_slice() }.read()?;
412        let stored = self.checksum()?;
413        Ok(crc32fast::hash(&data) == stored)
414    }
415
416    /// Overwrite bytes starting at `start` within this view with `data` and
417    /// recompute the block checksum.
418    pub fn write_range(&self, start: u64, data: &[u8]) -> io::Result<()> {
419        unsafe { self.data_slice() }.write_range(start, data)?;
420        self.update_checksum()
421    }
422
423    /// Zero `n` bytes starting at `start` within this view and recompute the
424    /// block checksum.
425    pub fn zero_range(&self, start: u64, n: u64) -> io::Result<()> {
426        unsafe { self.data_slice() }.zero_range(start, n)?;
427        self.update_checksum()
428    }
429
430    /// Return a cursor-based reader positioned at the start of this view.
431    pub fn reader(&self) -> BCrcBlockReader<'a, A> {
432        BCrcBlockReader {
433            inner: unsafe { self.data_slice() }.reader(),
434        }
435    }
436
437    /// Return a cursor-based reader positioned at `offset` within this view.
438    pub fn reader_at(&self, offset: u64) -> BCrcBlockReader<'a, A> {
439        BCrcBlockReader {
440            inner: unsafe { self.data_slice() }.reader_at(offset),
441        }
442    }
443
444    /// Return a cursor-based writer positioned at the start of this view.
445    ///
446    /// Every write automatically recomputes the full block checksum.
447    pub fn writer(&self) -> BCrcBlockWriter<'a, A> {
448        BCrcBlockWriter {
449            inner: unsafe { self.data_slice() }.writer(),
450            full_data: unsafe { self.full_data_slice() },
451            checksum: unsafe { self.checksum_slice() },
452        }
453    }
454
455    /// Return a cursor-based writer positioned at `offset` within this view.
456    ///
457    /// Every write automatically recomputes the full block checksum.
458    pub fn writer_at(&self, offset: u64) -> BCrcBlockWriter<'a, A> {
459        BCrcBlockWriter {
460            inner: unsafe { self.data_slice() }.writer_at(offset),
461            full_data: unsafe { self.full_data_slice() },
462            checksum: unsafe { self.checksum_slice() },
463        }
464    }
465
466    /// # Safety
467    ///
468    /// Returns only the bytes covered by this view. Any write bypasses checksum
469    /// tracking; callers must call `update_checksum` or accept stale checksums.
470    unsafe fn data_slice(&self) -> BStackSlice<'a, A> {
471        self.slice.subslice(self.start, self.end)
472    }
473
474    /// # Safety
475    ///
476    /// Returns the full block data region. Intended for checksum recomputation.
477    unsafe fn full_data_slice(&self) -> BStackSlice<'a, A> {
478        self.slice.subslice(0, self.full_len)
479    }
480
481    /// # Safety
482    ///
483    /// Returns the raw checksum bytes. Writing an incorrect value forges the
484    /// checksum; callers must ensure it reflects the full data region.
485    unsafe fn checksum_slice(&self) -> BStackSlice<'a, A> {
486        self.slice
487            .subslice(self.full_len, self.full_len + CHECKSUM_LENGTH)
488    }
489
490    fn update_checksum(&self) -> io::Result<()> {
491        let data = unsafe { self.full_data_slice() }.read()?;
492        let crc = crc32fast::hash(&data);
493        unsafe { self.checksum_slice() }.write(crc.to_le_bytes())
494    }
495}
496
497/// A cursor-based reader over the bytes covered by a [`BCrcBlockView`].
498///
499/// Implements [`io::Read`] and [`io::Seek`] within the coordinate space of the
500/// view (position 0 = first byte of the view). Constructed via
501/// [`BCrcBlock::reader`], [`BCrcBlock::reader_at`], [`BCrcBlockView::reader`], or
502/// [`BCrcBlockView::reader_at`].
503#[derive(Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
504pub struct BCrcBlockReader<'a, A: BStackAllocator> {
505    inner: BStackSliceReader<'a, A>,
506}
507
508impl<'a, A: BStackAllocator> fmt::Debug for BCrcBlockReader<'a, A> {
509    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
510        f.debug_struct("BCrcBlockReader")
511            .field("start", &self.inner.slice().start())
512            .field("end", &self.inner.slice().end())
513            .field("len", &self.inner.slice().len())
514            .field("cursor", &self.inner.position())
515            .finish_non_exhaustive()
516    }
517}
518
519impl<'a, A: BStackAllocator> BCrcBlockReader<'a, A> {
520    /// Return the current cursor position within the view's coordinate space.
521    pub fn position(&self) -> u64 {
522        self.inner.position()
523    }
524}
525
526/// Two readers compare equal when their active slice and cursor position match.
527impl<'a, A: BStackAllocator> PartialEq<BCrcBlockWriter<'a, A>> for BCrcBlockReader<'a, A> {
528    fn eq(&self, other: &BCrcBlockWriter<'a, A>) -> bool {
529        self.inner.slice() == other.inner.slice() && self.inner.position() == other.inner.position()
530    }
531}
532
533/// Ordered by absolute payload position, then by active length.
534impl<'a, A: BStackAllocator> PartialOrd<BCrcBlockWriter<'a, A>> for BCrcBlockReader<'a, A> {
535    fn partial_cmp(&self, other: &BCrcBlockWriter<'a, A>) -> Option<Ordering> {
536        let self_pos = self.inner.slice().start() + self.inner.position();
537        let other_pos = other.inner.slice().start() + other.inner.position();
538        Some(
539            self_pos
540                .cmp(&other_pos)
541                .then(self.inner.slice().len().cmp(&other.inner.slice().len())),
542        )
543    }
544}
545
546impl<'a, A: BStackAllocator> io::Read for BCrcBlockReader<'a, A> {
547    fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
548        self.inner.read(buf)
549    }
550}
551
552impl<'a, A: BStackAllocator> io::Seek for BCrcBlockReader<'a, A> {
553    fn seek(&mut self, pos: io::SeekFrom) -> io::Result<u64> {
554        self.inner.seek(pos)
555    }
556}
557
558/// A cursor-based writer over the bytes covered by a [`BCrcBlockView`].
559///
560/// Implements [`io::Write`] and [`io::Seek`] within the coordinate space of the
561/// view. Every write automatically recomputes the CRC32 checksum over the
562/// **full block data** (not just the active view range), keeping the block's
563/// integrity invariant intact. Constructed via [`BCrcBlock::writer`],
564/// [`BCrcBlock::writer_at`], [`BCrcBlockView::writer`], or [`BCrcBlockView::writer_at`].
565#[derive(Clone)]
566pub struct BCrcBlockWriter<'a, A: BStackAllocator> {
567    /// Cursor writer scoped to the view's active range.
568    inner: BStackSliceWriter<'a, A>,
569    /// Full block data region — read to recompute the checksum after each write.
570    full_data: BStackSlice<'a, A>,
571    /// Checksum slot — written after each mutation.
572    checksum: BStackSlice<'a, A>,
573}
574
575impl<'a, A: BStackAllocator> fmt::Debug for BCrcBlockWriter<'a, A> {
576    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
577        f.debug_struct("BCrcBlockWriter")
578            .field("start", &self.inner.slice().start())
579            .field("end", &self.inner.slice().end())
580            .field("len", &self.inner.slice().len())
581            .field("cursor", &self.inner.position())
582            .finish_non_exhaustive()
583    }
584}
585
586impl<'a, A: BStackAllocator> BCrcBlockWriter<'a, A> {
587    /// Return the current cursor position within the view's coordinate space.
588    pub fn position(&self) -> u64 {
589        self.inner.position()
590    }
591
592    fn update_checksum(&self) -> io::Result<()> {
593        let data = self.full_data.read()?;
594        let crc = crc32fast::hash(&data);
595        self.checksum.write(crc.to_le_bytes())
596    }
597}
598
599/// Two writers compare equal when their active slice and cursor position match.
600impl<'a, A: BStackAllocator> PartialEq for BCrcBlockWriter<'a, A> {
601    fn eq(&self, other: &Self) -> bool {
602        self.inner.slice() == other.inner.slice() && self.inner.position() == other.inner.position()
603    }
604}
605
606impl<'a, A: BStackAllocator> Eq for BCrcBlockWriter<'a, A> {}
607
608impl<'a, A: BStackAllocator> Hash for BCrcBlockWriter<'a, A> {
609    fn hash<H: Hasher>(&self, state: &mut H) {
610        self.inner.slice().hash(state);
611        self.inner.position().hash(state);
612    }
613}
614
615/// Ordered by absolute payload position, then by active length.
616impl<'a, A: BStackAllocator> PartialOrd for BCrcBlockWriter<'a, A> {
617    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
618        Some(self.cmp(other))
619    }
620}
621
622impl<'a, A: BStackAllocator> Ord for BCrcBlockWriter<'a, A> {
623    fn cmp(&self, other: &Self) -> Ordering {
624        let self_pos = self.inner.slice().start() + self.inner.position();
625        let other_pos = other.inner.slice().start() + other.inner.position();
626        self_pos
627            .cmp(&other_pos)
628            .then(self.inner.slice().len().cmp(&other.inner.slice().len()))
629    }
630}
631
632impl<'a, A: BStackAllocator> PartialEq<BCrcBlockReader<'a, A>> for BCrcBlockWriter<'a, A> {
633    fn eq(&self, other: &BCrcBlockReader<'a, A>) -> bool {
634        other == self
635    }
636}
637
638impl<'a, A: BStackAllocator> PartialOrd<BCrcBlockReader<'a, A>> for BCrcBlockWriter<'a, A> {
639    fn partial_cmp(&self, other: &BCrcBlockReader<'a, A>) -> Option<Ordering> {
640        other.partial_cmp(self).map(|o| o.reverse())
641    }
642}
643
644impl<'a, A: BStackAllocator> io::Write for BCrcBlockWriter<'a, A> {
645    fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
646        let n = self.inner.write(buf)?;
647        if n > 0 {
648            self.update_checksum()?;
649        }
650        Ok(n)
651    }
652
653    fn flush(&mut self) -> io::Result<()> {
654        self.inner.flush()
655    }
656}
657
658impl<'a, A: BStackAllocator> io::Seek for BCrcBlockWriter<'a, A> {
659    fn seek(&mut self, pos: io::SeekFrom) -> io::Result<u64> {
660        self.inner.seek(pos)
661    }
662}
663
664/// Implements [`BStackAllocator`] for [`BCrcBlockAllocator`], exposing it as a
665/// composable allocator layer.
666///
667/// `Allocated<'_>` is `BCrcBlock<'_, BCrcBlockAllocator<A>>`. Inner allocated
668/// handles are reconstructed via `BStackRawAllocator::from_raw` for
669/// `realloc` and `dealloc`, so no back-conversion from offset alone is needed.
670impl<A> BStackAllocator for BCrcBlockAllocator<A>
671where
672    A: BStackAllocator<Error = io::Error> + BStackRawAllocator,
673    for<'a> A::Allocated<'a>: BlockStart + Copy,
674{
675    type Error = io::Error;
676    type Allocated<'a>
677        = BCrcBlock<'a, BCrcBlockAllocator<A>>
678    where
679        A: 'a;
680
681    fn stack(&self) -> &BStack {
682        self.inner.stack()
683    }
684
685    fn into_stack(self) -> BStack {
686        self.inner.into_stack()
687    }
688
689    fn alloc(&self, len: u64) -> io::Result<BCrcBlock<'_, BCrcBlockAllocator<A>>> {
690        let inner = self.inner.alloc(len + CHECKSUM_LENGTH)?;
691        let offset = inner.block_start();
692        let slice = unsafe { BStackSlice::from_raw_parts(self, offset, len + CHECKSUM_LENGTH) };
693        Ok(BCrcBlock { slice, len })
694    }
695
696    fn realloc<'a>(
697        &'a self,
698        block: BCrcBlock<'a, BCrcBlockAllocator<A>>,
699        new_len: u64,
700    ) -> io::Result<BCrcBlock<'a, BCrcBlockAllocator<A>>> {
701        let offset = block.slice.start();
702        let inner_old_slice = unsafe {
703            BStackSlice::from_raw_parts(&self.inner, offset, block.len + CHECKSUM_LENGTH)
704        };
705        let inner_old: A::Allocated<'_> = unsafe { A::from_raw(inner_old_slice) };
706        let inner_new = self.inner.realloc(inner_old, new_len + CHECKSUM_LENGTH)?;
707        let new_offset = inner_new.block_start();
708        let slice =
709            unsafe { BStackSlice::from_raw_parts(self, new_offset, new_len + CHECKSUM_LENGTH) };
710        Ok(BCrcBlock {
711            slice,
712            len: new_len,
713        })
714    }
715
716    fn dealloc(&self, block: BCrcBlock<'_, BCrcBlockAllocator<A>>) -> io::Result<()> {
717        let offset = block.slice.start();
718        let inner_slice = unsafe {
719            BStackSlice::from_raw_parts(&self.inner, offset, block.len + CHECKSUM_LENGTH)
720        };
721        let inner: A::Allocated<'_> = unsafe { A::from_raw(inner_slice) };
722        self.inner.dealloc(inner)
723    }
724}
725
726/// Satisfies the `Allocated<'a>: TryInto<BStackSlice<'a, Self>>` bound
727/// required by [`BStackAllocator`]. The conversion is infallible: the raw
728/// backing slice is returned as-is.
729impl<'a, A> TryInto<BStackSlice<'a, BCrcBlockAllocator<A>>> for BCrcBlock<'a, BCrcBlockAllocator<A>>
730where
731    A: BStackAllocator<Error = io::Error> + BStackRawAllocator,
732    for<'b> A::Allocated<'b>: BlockStart + Copy,
733{
734    type Error = std::convert::Infallible;
735
736    fn try_into(self) -> Result<BStackSlice<'a, BCrcBlockAllocator<A>>, Self::Error> {
737        Ok(self.slice)
738    }
739}
740
741impl<'a, A: BStackAllocator> BlockStart for BCrcBlock<'a, A> {
742    fn block_start(&self) -> u64 {
743        self.slice.start()
744    }
745}
746
747unsafe impl<A> BStackRawAllocator for BCrcBlockAllocator<A>
748where
749    A: BStackAllocator<Error = io::Error> + BStackRawAllocator,
750    for<'a> A::Allocated<'a>: BlockStart + Copy,
751{
752    unsafe fn from_raw<'a>(
753        slice: BStackSlice<'a, BCrcBlockAllocator<A>>,
754    ) -> BCrcBlock<'a, BCrcBlockAllocator<A>> {
755        let len = slice.len() - CHECKSUM_LENGTH;
756        BCrcBlock { slice, len }
757    }
758}
759
760/// Implements [`BStackGuardedSlice`] for [`BCrcBlock`].
761///
762/// * `as_slice()` returns the data region only (excludes the 4-byte checksum
763///   trailer), so callers read and write only usable payload bytes.
764/// * `write()` writes to the data region and recomputes the CRC32 checksum
765///   over the entire data region.
766/// * `zero()` zeroes the data region and recomputes the CRC32 checksum.
767///
768/// Both `write` and `zero` are overridden directly (rather than using the
769/// `post_write` hook) so the checksum is always consistent after the call.
770impl<'a, A: BStackAllocator + 'a> BStackGuardedSlice<'a, A> for BCrcBlock<'a, A> {
771    fn len(&self) -> u64 {
772        self.len
773    }
774
775    unsafe fn raw_block(&self) -> BStackSlice<'a, A> {
776        self.slice
777    }
778
779    fn as_slice(&self) -> io::Result<BStackSlice<'a, A>> {
780        Ok(unsafe { self.data_slice() })
781    }
782
783    fn write(&self, data: impl AsRef<[u8]>) -> io::Result<()> {
784        unsafe { self.data_slice() }.write(data.as_ref())?;
785        let full = unsafe { self.data_slice() }.read()?;
786        let crc = crc32fast::hash(&full);
787        unsafe { self.checksum_slice() }.write(crc.to_le_bytes())
788    }
789
790    fn zero(&self) -> io::Result<()> {
791        unsafe { self.data_slice() }.zero()?;
792        let zeros = vec![0u8; self.len as usize];
793        let crc = crc32fast::hash(&zeros);
794        unsafe { self.checksum_slice() }.write(crc.to_le_bytes())
795    }
796}
797
798impl<'a, A: BStackAllocator + 'a> BStackGuardedSliceSubview<'a, A> for BCrcBlockView<'a, A> {
799    fn subview(&self, start: u64, end: u64) -> impl BStackGuardedSliceSubview<'a, A> + '_ {
800        BCrcBlockView {
801            slice: self.slice,
802            full_len: self.full_len,
803            start: self.start + start,
804            end: self.start + end,
805        }
806    }
807}
808
809/// Implements [`BStackGuardedSlice`] for [`BCrcBlockView`].
810///
811/// * `as_slice()` returns the bytes covered by this view.
812/// * `write()` overwrites the view range and recomputes the CRC32 over the
813///   full block.
814/// * `zero()` zeroes the view range and recomputes the CRC32 over the full
815///   block.
816impl<'a, A: BStackAllocator + 'a> BStackGuardedSlice<'a, A> for BCrcBlockView<'a, A> {
817    fn len(&self) -> u64 {
818        self.end - self.start
819    }
820
821    unsafe fn raw_block(&self) -> BStackSlice<'a, A> {
822        unsafe { self.data_slice() }
823    }
824
825    fn as_slice(&self) -> io::Result<BStackSlice<'a, A>> {
826        Ok(unsafe { self.data_slice() })
827    }
828
829    fn write(&self, data: impl AsRef<[u8]>) -> io::Result<()> {
830        unsafe { self.data_slice() }.write(data.as_ref())?;
831        self.update_checksum()
832    }
833
834    fn zero(&self) -> io::Result<()> {
835        unsafe { self.data_slice() }.zero()?;
836        self.update_checksum()
837    }
838}
839
840#[cfg(test)]
841mod tests {
842    use super::*;
843    use bstack::{BStack, BStackGuardedSlice, LinearBStackAllocator};
844    use tempfile::NamedTempFile;
845
846    fn make_allocator() -> (BCrcBlockAllocator<LinearBStackAllocator>, NamedTempFile) {
847        let file = NamedTempFile::new().unwrap();
848        let stack = BStack::open(file.path()).unwrap();
849        let allocator = BCrcBlockAllocator::new(LinearBStackAllocator::new(stack));
850        (allocator, file)
851    }
852
853    #[test]
854    fn test_alloc_len() {
855        let (alloc, _f) = make_allocator();
856        let block = alloc.alloc(30).unwrap();
857        assert_eq!(block.len(), 30);
858        let raw_len = unsafe { block.into_slice().len() };
859        assert_eq!(raw_len, 34);
860    }
861
862    #[test]
863    fn test_write_and_verify() {
864        let (alloc, _f) = make_allocator();
865        let block = alloc.alloc(5).unwrap();
866        let view = block.view();
867        view.write(b"hello").unwrap();
868        assert!(view.verify().unwrap());
869        assert_eq!(view.read().unwrap(), b"hello");
870    }
871
872    #[test]
873    fn test_verify_fails_after_raw_write() {
874        let (alloc, _f) = make_allocator();
875        let block = alloc.alloc(5).unwrap();
876        let view = block.view();
877        view.write(b"hello").unwrap();
878        unsafe {
879            block.into_slice().write(b"world").unwrap();
880        }
881        assert!(!view.verify().unwrap());
882    }
883
884    #[test]
885    fn test_realloc() {
886        let (alloc, _f) = make_allocator();
887        let block = alloc.alloc(4).unwrap();
888        block.view().write(b"abcd").unwrap();
889        let block2 = alloc.realloc(block, 8).unwrap();
890        assert_eq!(block2.len(), 8);
891        let raw_len = unsafe { block2.into_slice().len() };
892        assert_eq!(raw_len, 12);
893    }
894
895    #[test]
896    fn test_to_from_bytes() {
897        let (alloc, _f) = make_allocator();
898        let block = alloc.alloc(8).unwrap();
899        block.view().write(&b"rustacean"[..8]).unwrap();
900        let bytes: [u8; 16] = block.into();
901        let block2 = BCrcBlock::from_bytes(alloc.inner(), bytes);
902        assert_eq!(block2.len(), 8);
903        assert!(block2.verify().unwrap());
904        assert_eq!(block2.view().read().unwrap(), &b"rustacean"[..8]);
905    }
906
907    #[test]
908    fn test_zero_clears_and_valid() {
909        let (alloc, _f) = make_allocator();
910        let block = alloc.alloc(6).unwrap();
911        let view = block.view();
912        view.write(b"foobar").unwrap();
913        view.zero().unwrap();
914        assert_eq!(view.read().unwrap(), vec![0u8; 6]);
915        assert!(view.verify().unwrap());
916    }
917
918    #[test]
919    fn test_reader() {
920        use std::io::Read;
921        let (alloc, _f) = make_allocator();
922        let block = alloc.alloc(4).unwrap();
923        block.view().write(b"abcd").unwrap();
924        let mut buf = [0u8; 4];
925        block.reader().read_exact(&mut buf).unwrap();
926        assert_eq!(&buf, b"abcd");
927    }
928
929    #[test]
930    fn test_writer_maintains_checksum() {
931        use std::io::Write;
932        let (alloc, _f) = make_allocator();
933        let block = alloc.alloc(4).unwrap();
934        block.writer().write_all(b"WXYZ").unwrap();
935        assert!(block.verify().unwrap());
936        assert_eq!(block.view().read().unwrap(), b"WXYZ");
937    }
938
939    #[test]
940    fn test_writer_seek_and_overwrite() {
941        use std::io::{Seek, SeekFrom, Write};
942        let (alloc, _f) = make_allocator();
943        let block = alloc.alloc(4).unwrap();
944        let mut w = block.writer();
945        w.write_all(b"abcd").unwrap();
946        w.seek(SeekFrom::Start(2)).unwrap();
947        w.write_all(b"XY").unwrap();
948        assert!(block.verify().unwrap());
949        assert_eq!(block.view().read().unwrap(), b"abXY");
950    }
951
952    #[test]
953    fn test_reader_writer_cmp() {
954        let (alloc, _f) = make_allocator();
955        let block = alloc.alloc(4).unwrap();
956        let r = block.reader();
957        let w = block.writer();
958        assert_eq!(r, w);
959        assert_eq!(w, r);
960    }
961
962    #[test]
963    fn test_subview_read() {
964        let (alloc, _f) = make_allocator();
965        let block = alloc.alloc(8).unwrap();
966        block.view().write(b"hello!!!").unwrap();
967        let sub = block.view().subview(0, 5);
968        assert_eq!(sub.len(), 5);
969        assert_eq!(sub.read().unwrap(), b"hello");
970    }
971
972    #[test]
973    fn test_subview_write_updates_full_checksum() {
974        let (alloc, _f) = make_allocator();
975        let block = alloc.alloc(8).unwrap();
976        block.view().write(b"hello!!!").unwrap();
977        let sub = block.view().subview(0, 5);
978        sub.write(b"world").unwrap();
979        assert!(block.verify().unwrap());
980        assert_eq!(block.view().read().unwrap(), b"world!!!");
981    }
982
983    #[test]
984    fn test_subview_writer_updates_full_checksum() {
985        use std::io::Write;
986        let (alloc, _f) = make_allocator();
987        let block = alloc.alloc(8).unwrap();
988        block.view().write(b"hello!!!").unwrap();
989        block
990            .view()
991            .subview(0, 5)
992            .writer()
993            .write_all(b"world")
994            .unwrap();
995        assert!(block.verify().unwrap());
996        assert_eq!(block.view().read().unwrap(), b"world!!!");
997    }
998
999    #[test]
1000    fn test_subview_nested() {
1001        let (alloc, _f) = make_allocator();
1002        let block = alloc.alloc(8).unwrap();
1003        block.view().write(b"abcdefgh").unwrap();
1004        // subview [2, 6) then subview [1, 3) of that → block bytes [3, 5)
1005        let sub = block.view().subview(2, 6).subview(1, 3);
1006        assert_eq!(sub.len(), 2);
1007        assert_eq!(sub.read().unwrap(), b"de");
1008        sub.write(b"XY").unwrap();
1009        assert!(block.verify().unwrap());
1010        assert_eq!(block.view().read().unwrap(), b"abcXYfgh");
1011    }
1012}