1use core::any::type_name;
8
9use crc32fast::Hasher;
10use serde::{Deserialize, Serialize};
11
12pub(crate) const MAGIC: u32 = 0x424C_4B53;
14
15pub(crate) const HEADER_SIZE: usize = 10;
17
18pub(crate) const CRC_SIZE: usize = 4;
20
21#[derive(Debug)]
23pub enum FlashBlockError<E> {
24 Io(E),
26 FormatError,
28 StorageCorrupted,
30}
31
32pub trait FlashBlock {
116 type Error;
118
119 fn load<T>(&mut self) -> Result<Option<T>, Self::Error>
125 where
126 T: Serialize + for<'de> Deserialize<'de>;
127
128 fn save<T>(&mut self, value: &T) -> Result<(), Self::Error>
132 where
133 T: Serialize + for<'de> Deserialize<'de>;
134
135 fn clear(&mut self) -> Result<(), Self::Error>;
139}
140
141#[doc(hidden)]
147pub trait FlashDevice {
148 type Error;
150
151 fn read(&mut self, offset: u32, bytes: &mut [u8]) -> Result<(), Self::Error>;
153
154 fn write(&mut self, offset: u32, bytes: &[u8]) -> Result<(), Self::Error>;
156
157 fn erase(&mut self, from: u32, to: u32) -> Result<(), Self::Error>;
159}
160
161#[must_use]
163#[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#[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#[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#[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
275pub(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
291pub(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}