umsh_journal_store/
record.rs

1//! The shared journal record engine.
2//!
3//! Every journal in this crate writes fixed-size records into two
4//! alternating flash pages. A record becomes visible only when its final
5//! commit word is written, so a mount after power loss always recovers
6//! the previous committed record, never a partial write.
7
8/// Flash page size shared by every backend the journals run on (nRF52840
9/// NVMC and ESP32-S3 SPI flash are both 4 KiB-erase parts).
10pub const PAGE_SIZE: u32 = 4096;
11
12/// Wraparound-safe generation comparison: a record numbered 0 supersedes
13/// one numbered `u32::MAX`.
14pub fn generation_is_newer(candidate: u32, current: u32) -> bool {
15    candidate != current && candidate.wrapping_sub(current) < (1 << 31)
16}
17
18/// Flash writer used by the journal's two-stage record commit.
19#[allow(async_fn_in_trait)]
20pub trait RecordWriter {
21    type Error;
22
23    async fn write_record(&mut self, address: u32, bytes: &[u8]) -> Result<(), Self::Error>;
24}
25
26#[allow(async_fn_in_trait)]
27pub trait PageEraser {
28    type Error;
29
30    async fn erase_page(&mut self, start: u32, end: u32) -> Result<(), Self::Error>;
31}
32
33/// Flash reader used by a journal mount and by the boot path's
34/// walk-back scan.
35///
36/// Synchronous, unlike the writer and eraser: every backend this runs on
37/// reads either from a memory-mapped window or from a cached SPI read,
38/// and a mount scan issues one call per slot across two pages — making
39/// it async would put an await in that loop for no backend that needs
40/// one.
41pub trait RecordReader {
42    type Error;
43
44    fn read_record(&mut self, address: u32, bytes: &mut [u8]) -> Result<(), Self::Error>;
45}
46
47#[derive(Clone, Copy, Debug, PartialEq, Eq)]
48pub enum CommitError<E> {
49    Body(E),
50    Commit(E),
51}
52
53/// Write a record body first and its visibility marker last. The
54/// record's last four bytes are the commit word (shared convention
55/// across every journal in this crate).
56///
57/// The caller must not publish the corresponding in-RAM snapshot until this
58/// returns `Ok(())`. A mount ignores either failure shape because the commit
59/// word remains incomplete.
60pub async fn write_committed_record<W: RecordWriter, const SLOT: usize>(
61    writer: &mut W,
62    target: u32,
63    bytes: &[u8; SLOT],
64) -> Result<(), CommitError<W::Error>> {
65    let commit_offset = SLOT - 4;
66    writer
67        .write_record(target, &bytes[..commit_offset])
68        .await
69        .map_err(CommitError::Body)?;
70    writer
71        .write_record(target + commit_offset as u32, &[0; 4])
72        .await
73        .map_err(CommitError::Commit)
74}
75
76pub async fn erase_journal_page<E: PageEraser>(eraser: &mut E, page: u32) -> Result<(), E::Error> {
77    eraser.erase_page(page, page + PAGE_SIZE).await
78}
79
80/// CRC32 (reflected, polynomial 0xEDB88320) over a record body; the
81/// journals store it immediately before the commit word.
82pub fn crc32(bytes: &[u8]) -> u32 {
83    let mut crc = 0xffff_ffffu32;
84    for &byte in bytes {
85        crc ^= u32::from(byte);
86        for _ in 0..8 {
87            crc = (crc >> 1) ^ (0xedb8_8320 & 0u32.wrapping_sub(crc & 1));
88        }
89    }
90    !crc
91}
92
93#[cfg(test)]
94mod tests {
95    use super::*;
96
97    #[test]
98    fn generation_comparison_handles_wraparound() {
99        assert!(generation_is_newer(0, u32::MAX));
100        assert!(!generation_is_newer(u32::MAX, 0));
101    }
102}