Skip to main content

device_envoy_core/
flash_block.rs

1//! Shared low-level flash block protocol for type-safe persistent storage.
2//!
3//! This module provides the platform-independent protocol layer for
4//! platform crates. See your platform crate's `flash_block` module for
5//! constructors, hardware wiring, and usage examples.
6
7use core::any::type_name;
8
9use crc32fast::Hasher;
10use serde::{Deserialize, Serialize};
11
12/// Magic number identifying a valid flash block: `'BLKS'`.
13pub(crate) const MAGIC: u32 = 0x424C_4B53;
14
15/// Number of bytes in the block header: magic(4) + type\_hash(4) + payload\_len(2).
16pub(crate) const HEADER_SIZE: usize = 10;
17
18/// Number of bytes used by the CRC trailer.
19pub(crate) const CRC_SIZE: usize = 4;
20
21/// Errors returned by [`save_block`], [`load_block`], and [`clear_block`].
22#[derive(Debug)]
23pub enum FlashBlockError<E> {
24    /// An I/O operation on the underlying flash device failed.
25    Io(E),
26    /// Serialization or deserialization failed.
27    FormatError,
28    /// The stored data is corrupt (bad CRC or invalid length).
29    StorageCorrupted,
30}
31
32/// Operations on blocks of flash memory.
33///
34/// Platform crates implement this trait on their concrete flash block handle
35/// types.
36///
37/// Constructors and hardware wiring remain platform-specific; this trait
38/// defines the shared operation surface used by higher-level abstractions.
39///
40/// # Features
41///
42/// - Type safety: hash-based type checking prevents reading data written under a
43///   different Rust type name. Trying to read a different type returns `Ok(None)`.
44/// - Postcard serialization: compact, `no_std`-friendly binary format.
45///
46/// This example increments a persisted boot counter and clears a separate
47/// scratch block in the same helper.
48///
49/// # Example
50///
51/// ```rust,no_run
52/// use core::convert::Infallible;
53/// use device_envoy_core::flash_block::FlashBlock;
54///
55/// #[derive(serde::Serialize, serde::Deserialize, Debug, Clone, Copy)]
56/// struct BootCounter(u8);
57///
58/// impl BootCounter {
59///     const fn new(value: u8) -> Self {
60///         Self(value)
61///     }
62///
63///     fn increment(self) -> Self {
64///         Self((self.0 + 1) % 10)
65///     }
66/// }
67///
68/// fn update_boot_counter_and_clear_scratch(
69///     boot_counter_flash_block: &mut impl FlashBlock<Error = Infallible>,
70///     scratch_flash_block: &mut impl FlashBlock<Error = Infallible>,
71/// ) -> Result<BootCounter, Infallible> {
72///     // Load the typed value, defaulting to 0 when the block is empty.
73///     let boot_counter = boot_counter_flash_block
74///         .load()?
75///         .unwrap_or(BootCounter::new(0))
76///         .increment();
77///
78///     // Save the updated value back to flash.
79///     boot_counter_flash_block.save(&boot_counter)?;
80///
81///     // Clear the extra scratch block.
82///     scratch_flash_block.clear()?;
83///
84///     Ok(boot_counter)
85/// }
86///
87/// # struct DemoFlashBlock;
88/// # impl FlashBlock for DemoFlashBlock {
89/// #     type Error = Infallible;
90/// #     fn load<T>(&mut self) -> Result<Option<T>, Self::Error>
91/// #     where
92/// #         T: serde::Serialize + for<'de> serde::Deserialize<'de>,
93/// #     {
94/// #         Ok(None)
95/// #     }
96/// #     fn save<T>(&mut self, _value: &T) -> Result<(), Self::Error>
97/// #     where
98/// #         T: serde::Serialize + for<'de> serde::Deserialize<'de>,
99/// #     {
100/// #         Ok(())
101/// #     }
102/// #     fn clear(&mut self) -> Result<(), Self::Error> {
103/// #         Ok(())
104/// #     }
105/// # }
106/// # fn main() {
107/// #     let mut boot_counter_flash_block = DemoFlashBlock;
108/// #     let mut scratch_flash_block = DemoFlashBlock;
109/// #     let _ = update_boot_counter_and_clear_scratch(
110/// #         &mut boot_counter_flash_block,
111/// #         &mut scratch_flash_block,
112/// #     );
113/// # }
114/// ```
115pub trait FlashBlock {
116    /// Error returned by block operations.
117    type Error;
118
119    /// Load a typed value from this block.
120    ///
121    /// Returns `Ok(None)` when the block is empty or contains a different type.
122    ///
123    /// See the [FlashBlock trait documentation](Self) for usage examples.
124    fn load<T>(&mut self) -> Result<Option<T>, Self::Error>
125    where
126        T: Serialize + for<'de> Deserialize<'de>;
127
128    /// Save a typed value to this block.
129    ///
130    /// See the [FlashBlock trait documentation](Self) for usage examples.
131    fn save<T>(&mut self, value: &T) -> Result<(), Self::Error>
132    where
133        T: Serialize + for<'de> Deserialize<'de>;
134
135    /// Clear this block.
136    ///
137    /// See the [FlashBlock trait documentation](Self) for usage examples.
138    fn clear(&mut self) -> Result<(), Self::Error>;
139}
140
141/// Low-level read/write/erase interface for a flash device.
142///
143/// Implement this trait in the platform crate to connect the shared block
144/// protocol to the hardware driver.
145// Public for cross-crate platform plumbing; hidden from end-user docs.
146#[doc(hidden)]
147pub trait FlashDevice {
148    /// The error type returned by I/O operations.
149    type Error;
150
151    /// Read `bytes.len()` bytes starting at `offset`.
152    fn read(&mut self, offset: u32, bytes: &mut [u8]) -> Result<(), Self::Error>;
153
154    /// Write `bytes` starting at `offset`.
155    fn write(&mut self, offset: u32, bytes: &[u8]) -> Result<(), Self::Error>;
156
157    /// Erase flash from `from` (inclusive) to `to` (exclusive), in bytes.
158    fn erase(&mut self, from: u32, to: u32) -> Result<(), Self::Error>;
159}
160
161/// Maximum payload bytes for a flash block size.
162#[must_use]
163// Public for cross-crate platform plumbing; hidden from end-user docs.
164#[doc(hidden)]
165pub const fn max_payload_size(block_size: usize) -> usize {
166    assert!(block_size > HEADER_SIZE + CRC_SIZE, "block_size too small");
167    block_size - HEADER_SIZE - CRC_SIZE
168}
169
170/// Serialize `value` and write it into the block starting at `block_offset`.
171///
172/// The block is erased before writing. On success the block contains:
173/// magic + type hash + payload length + serialized payload + CRC32.
174// Public for cross-crate platform plumbing; hidden from end-user docs.
175#[doc(hidden)]
176pub fn save_block<const BLOCK_SIZE: usize, T, F>(
177    flash: &mut F,
178    block_offset: u32,
179    value: &T,
180) -> Result<(), FlashBlockError<F::Error>>
181where
182    T: Serialize + for<'de> Deserialize<'de>,
183    F: FlashDevice,
184{
185    let max_payload_size = max_payload_size(BLOCK_SIZE);
186    let mut payload_buffer = [0u8; BLOCK_SIZE];
187    let payload = postcard::to_slice(value, &mut payload_buffer[..max_payload_size])
188        .map_err(|_| FlashBlockError::FormatError)?;
189    let payload_len = payload.len();
190
191    let mut block_bytes = [0xFFu8; BLOCK_SIZE];
192    block_bytes[0..4].copy_from_slice(&MAGIC.to_le_bytes());
193    block_bytes[4..8].copy_from_slice(&compute_type_hash::<T>().to_le_bytes());
194    block_bytes[8..10].copy_from_slice(&(payload_len as u16).to_le_bytes());
195    block_bytes[HEADER_SIZE..HEADER_SIZE + payload_len].copy_from_slice(payload);
196
197    let crc_offset = HEADER_SIZE + payload_len;
198    let crc = compute_crc(&block_bytes[..crc_offset]);
199    block_bytes[crc_offset..crc_offset + CRC_SIZE].copy_from_slice(&crc.to_le_bytes());
200
201    let block_size_u32 = u32::try_from(BLOCK_SIZE).expect("block size must fit in u32");
202    flash
203        .erase(block_offset, block_offset + block_size_u32)
204        .map_err(FlashBlockError::Io)?;
205    flash
206        .write(block_offset, &block_bytes)
207        .map_err(FlashBlockError::Io)?;
208    Ok(())
209}
210
211/// Read the block at `block_offset`.
212///
213/// Returns `Ok(None)` when the block has no recognized magic or the stored
214/// type hash does not match `T`. Returns `Err` when the data is corrupt.
215// Public for cross-crate platform plumbing; hidden from end-user docs.
216#[doc(hidden)]
217pub fn load_block<const BLOCK_SIZE: usize, T, F>(
218    flash: &mut F,
219    block_offset: u32,
220) -> Result<Option<T>, FlashBlockError<F::Error>>
221where
222    T: Serialize + for<'de> Deserialize<'de>,
223    F: FlashDevice,
224{
225    let mut block_bytes = [0u8; BLOCK_SIZE];
226    flash
227        .read(block_offset, &mut block_bytes)
228        .map_err(FlashBlockError::Io)?;
229
230    let magic = u32::from_le_bytes(block_bytes[0..4].try_into().expect("4-byte slice"));
231    if magic != MAGIC {
232        return Ok(None);
233    }
234
235    let stored_type_hash = u32::from_le_bytes(block_bytes[4..8].try_into().expect("4-byte slice"));
236    if stored_type_hash != compute_type_hash::<T>() {
237        return Ok(None);
238    }
239
240    let payload_len =
241        u16::from_le_bytes(block_bytes[8..10].try_into().expect("2-byte slice")) as usize;
242    if payload_len > max_payload_size(BLOCK_SIZE) {
243        return Err(FlashBlockError::StorageCorrupted);
244    }
245
246    let crc_offset = HEADER_SIZE + payload_len;
247    let stored_crc = u32::from_le_bytes(
248        block_bytes[crc_offset..crc_offset + CRC_SIZE]
249            .try_into()
250            .expect("4-byte slice"),
251    );
252    if stored_crc != compute_crc(&block_bytes[..crc_offset]) {
253        return Err(FlashBlockError::StorageCorrupted);
254    }
255
256    let payload = &block_bytes[HEADER_SIZE..HEADER_SIZE + payload_len];
257    postcard::from_bytes(payload)
258        .map(Some)
259        .map_err(|_| FlashBlockError::StorageCorrupted)
260}
261
262/// Erase the block at `block_offset`.
263// Public for cross-crate platform plumbing; hidden from end-user docs.
264#[doc(hidden)]
265pub fn clear_block<const BLOCK_SIZE: usize, F: FlashDevice>(
266    flash: &mut F,
267    block_offset: u32,
268) -> Result<(), FlashBlockError<F::Error>> {
269    let block_size_u32 = u32::try_from(BLOCK_SIZE).expect("block size must fit in u32");
270    flash
271        .erase(block_offset, block_offset + block_size_u32)
272        .map_err(FlashBlockError::Io)
273}
274
275/// FNV-1a hash of `T`'s fully-qualified type name.
276///
277/// Used as a type-safety tag stored alongside serialized data so that an attempt
278/// to load the wrong type returns `Ok(None)` rather than corrupt data.
279pub(crate) fn compute_type_hash<T>() -> u32 {
280    const FNV_OFFSET: u32 = 2_166_136_261;
281    const FNV_PRIME: u32 = 16_777_619;
282
283    let mut hash = FNV_OFFSET;
284    for byte in type_name::<T>().bytes() {
285        hash ^= u32::from(byte);
286        hash = hash.wrapping_mul(FNV_PRIME);
287    }
288    hash
289}
290
291/// CRC32 checksum.
292pub(crate) fn compute_crc(bytes: &[u8]) -> u32 {
293    let mut hasher = Hasher::new();
294    hasher.update(bytes);
295    hasher.finalize()
296}
297
298#[cfg(test)]
299mod tests {
300    use super::{
301        FlashBlockError, FlashDevice, HEADER_SIZE, clear_block, load_block, max_payload_size,
302        save_block,
303    };
304
305    const TEST_FLASH_BLOCK_SIZE: usize = 4096;
306    const TEST_FLASH_SIZE: usize = TEST_FLASH_BLOCK_SIZE * 4;
307
308    struct MemoryFlashDevice {
309        bytes: [u8; TEST_FLASH_SIZE],
310    }
311
312    impl MemoryFlashDevice {
313        fn new() -> Self {
314            Self {
315                bytes: [0xFF; TEST_FLASH_SIZE],
316            }
317        }
318    }
319
320    impl FlashDevice for MemoryFlashDevice {
321        type Error = ();
322
323        fn read(&mut self, offset: u32, bytes: &mut [u8]) -> Result<(), ()> {
324            let offset = offset as usize;
325            bytes.copy_from_slice(&self.bytes[offset..offset + bytes.len()]);
326            Ok(())
327        }
328
329        fn write(&mut self, offset: u32, bytes: &[u8]) -> Result<(), ()> {
330            let offset = offset as usize;
331            self.bytes[offset..offset + bytes.len()].copy_from_slice(bytes);
332            Ok(())
333        }
334
335        fn erase(&mut self, from: u32, to: u32) -> Result<(), ()> {
336            self.bytes[from as usize..to as usize].fill(0xFF);
337            Ok(())
338        }
339    }
340
341    #[derive(Clone, Debug, Eq, PartialEq, serde::Serialize, serde::Deserialize)]
342    struct WifiPersistedState {
343        ssid: heapless::String<32>,
344        password: heapless::String<64>,
345        timezone_offset_minutes: i32,
346    }
347
348    #[derive(Clone, Debug, Eq, PartialEq, serde::Serialize, serde::Deserialize)]
349    struct OtherState {
350        timezone_offset_minutes: i32,
351    }
352
353    #[test]
354    fn save_load_clear_round_trip() {
355        let mut device = MemoryFlashDevice::new();
356        let state = WifiPersistedState {
357            ssid: heapless::String::try_from("demo-net").expect("ssid fits"),
358            password: heapless::String::try_from("password123").expect("password fits"),
359            timezone_offset_minutes: -300,
360        };
361
362        save_block::<TEST_FLASH_BLOCK_SIZE, _, _>(&mut device, 0, &state).expect("save succeeds");
363        let loaded = load_block::<TEST_FLASH_BLOCK_SIZE, WifiPersistedState, _>(&mut device, 0)
364            .expect("load succeeds")
365            .expect("value exists");
366        assert_eq!(loaded, state);
367
368        clear_block::<TEST_FLASH_BLOCK_SIZE, _>(&mut device, 0).expect("clear succeeds");
369        let cleared = load_block::<TEST_FLASH_BLOCK_SIZE, WifiPersistedState, _>(&mut device, 0)
370            .expect("load succeeds");
371        assert!(cleared.is_none());
372    }
373
374    #[test]
375    fn type_mismatch_returns_none() {
376        let mut device = MemoryFlashDevice::new();
377        let other = OtherState {
378            timezone_offset_minutes: 60,
379        };
380        save_block::<TEST_FLASH_BLOCK_SIZE, _, _>(&mut device, 0, &other).expect("save succeeds");
381        let result = load_block::<TEST_FLASH_BLOCK_SIZE, WifiPersistedState, _>(&mut device, 0)
382            .expect("load succeeds");
383        assert!(result.is_none());
384    }
385
386    #[test]
387    fn corrupted_crc_returns_error() {
388        let mut device = MemoryFlashDevice::new();
389        let state = WifiPersistedState {
390            ssid: heapless::String::new(),
391            password: heapless::String::new(),
392            timezone_offset_minutes: 0,
393        };
394        save_block::<TEST_FLASH_BLOCK_SIZE, _, _>(&mut device, 0, &state).expect("save succeeds");
395        device.bytes[HEADER_SIZE + 1] ^= 0x5A;
396
397        let error = load_block::<TEST_FLASH_BLOCK_SIZE, WifiPersistedState, _>(&mut device, 0)
398            .expect_err("crc mismatch should fail");
399        assert!(matches!(error, FlashBlockError::<()>::StorageCorrupted));
400    }
401
402    #[test]
403    fn max_payload_size_is_header_and_crc_aware() {
404        assert_eq!(
405            max_payload_size(TEST_FLASH_BLOCK_SIZE),
406            TEST_FLASH_BLOCK_SIZE - 14
407        );
408    }
409}