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 Error<E> {
24 Io(E),
26 FormatError,
28 StorageCorrupted,
30}
31
32#[cfg_attr(
116 feature = "wasm",
117 doc = "\nBrowser-simulated device: [`crate::wasm::FlashBlockWasm`]."
118)]
119pub trait FlashBlock {
120 type Error;
122
123 fn load<T>(&mut self) -> Result<Option<T>, Self::Error>
129 where
130 T: Serialize + for<'de> Deserialize<'de>;
131
132 fn save<T>(&mut self, value: &T) -> Result<(), Self::Error>
136 where
137 T: Serialize + for<'de> Deserialize<'de>;
138
139 fn clear(&mut self) -> Result<(), Self::Error>;
143}
144
145#[doc(hidden)]
151pub trait FlashDevice {
152 type Error;
154
155 fn read(&mut self, offset: u32, bytes: &mut [u8]) -> Result<(), Self::Error>;
157
158 fn write(&mut self, offset: u32, bytes: &[u8]) -> Result<(), Self::Error>;
160
161 fn erase(&mut self, from: u32, to: u32) -> Result<(), Self::Error>;
163}
164
165#[must_use]
167#[doc(hidden)]
169pub const fn max_payload_size(block_size: usize) -> usize {
170 assert!(block_size > HEADER_SIZE + CRC_SIZE, "block_size too small");
171 block_size - HEADER_SIZE - CRC_SIZE
172}
173
174#[doc(hidden)]
180pub fn save_block<const BLOCK_SIZE: usize, T, F>(
181 flash: &mut F,
182 block_offset: u32,
183 value: &T,
184) -> Result<(), Error<F::Error>>
185where
186 T: Serialize + for<'de> Deserialize<'de>,
187 F: FlashDevice,
188{
189 let max_payload_size = max_payload_size(BLOCK_SIZE);
190 let mut payload_buffer = [0u8; BLOCK_SIZE];
191 let payload = postcard::to_slice(value, &mut payload_buffer[..max_payload_size])
192 .map_err(|_| Error::FormatError)?;
193 let payload_len = payload.len();
194
195 let mut block_bytes = [0xFFu8; BLOCK_SIZE];
196 block_bytes[0..4].copy_from_slice(&MAGIC.to_le_bytes());
197 block_bytes[4..8].copy_from_slice(&compute_type_hash::<T>().to_le_bytes());
198 block_bytes[8..10].copy_from_slice(&(payload_len as u16).to_le_bytes());
199 block_bytes[HEADER_SIZE..HEADER_SIZE + payload_len].copy_from_slice(payload);
200
201 let crc_offset = HEADER_SIZE + payload_len;
202 let crc = compute_crc(&block_bytes[..crc_offset]);
203 block_bytes[crc_offset..crc_offset + CRC_SIZE].copy_from_slice(&crc.to_le_bytes());
204
205 let block_size_u32 = u32::try_from(BLOCK_SIZE).expect("block size must fit in u32");
206 flash
207 .erase(block_offset, block_offset + block_size_u32)
208 .map_err(Error::Io)?;
209 flash.write(block_offset, &block_bytes).map_err(Error::Io)?;
210 Ok(())
211}
212
213#[doc(hidden)]
219pub fn load_block<const BLOCK_SIZE: usize, T, F>(
220 flash: &mut F,
221 block_offset: u32,
222) -> Result<Option<T>, Error<F::Error>>
223where
224 T: Serialize + for<'de> Deserialize<'de>,
225 F: FlashDevice,
226{
227 let mut block_bytes = [0u8; BLOCK_SIZE];
228 flash
229 .read(block_offset, &mut block_bytes)
230 .map_err(Error::Io)?;
231
232 let magic = u32::from_le_bytes(block_bytes[0..4].try_into().expect("4-byte slice"));
233 if magic != MAGIC {
234 return Ok(None);
235 }
236
237 let stored_type_hash = u32::from_le_bytes(block_bytes[4..8].try_into().expect("4-byte slice"));
238 if stored_type_hash != compute_type_hash::<T>() {
239 return Ok(None);
240 }
241
242 let payload_len =
243 u16::from_le_bytes(block_bytes[8..10].try_into().expect("2-byte slice")) as usize;
244 if payload_len > max_payload_size(BLOCK_SIZE) {
245 return Err(Error::StorageCorrupted);
246 }
247
248 let crc_offset = HEADER_SIZE + payload_len;
249 let stored_crc = u32::from_le_bytes(
250 block_bytes[crc_offset..crc_offset + CRC_SIZE]
251 .try_into()
252 .expect("4-byte slice"),
253 );
254 if stored_crc != compute_crc(&block_bytes[..crc_offset]) {
255 return Err(Error::StorageCorrupted);
256 }
257
258 let payload = &block_bytes[HEADER_SIZE..HEADER_SIZE + payload_len];
259 postcard::from_bytes(payload)
260 .map(Some)
261 .map_err(|_| Error::StorageCorrupted)
262}
263
264#[doc(hidden)]
267pub fn clear_block<const BLOCK_SIZE: usize, F: FlashDevice>(
268 flash: &mut F,
269 block_offset: u32,
270) -> Result<(), Error<F::Error>> {
271 let block_size_u32 = u32::try_from(BLOCK_SIZE).expect("block size must fit in u32");
272 flash
273 .erase(block_offset, block_offset + block_size_u32)
274 .map_err(Error::Io)
275}
276
277pub(crate) fn compute_type_hash<T>() -> u32 {
282 const FNV_OFFSET: u32 = 2_166_136_261;
283 const FNV_PRIME: u32 = 16_777_619;
284
285 let mut hash = FNV_OFFSET;
286 for byte in type_name::<T>().bytes() {
287 hash ^= u32::from(byte);
288 hash = hash.wrapping_mul(FNV_PRIME);
289 }
290 hash
291}
292
293pub(crate) fn compute_crc(bytes: &[u8]) -> u32 {
295 let mut hasher = Hasher::new();
296 hasher.update(bytes);
297 hasher.finalize()
298}
299
300#[cfg(test)]
301mod tests {
302 use super::{
303 Error, FlashDevice, HEADER_SIZE, clear_block, load_block, max_payload_size, save_block,
304 };
305
306 const TEST_FLASH_BLOCK_SIZE: usize = 4096;
307 const TEST_FLASH_SIZE: usize = TEST_FLASH_BLOCK_SIZE * 4;
308
309 struct MemoryFlashDevice {
310 bytes: [u8; TEST_FLASH_SIZE],
311 }
312
313 impl MemoryFlashDevice {
314 fn new() -> Self {
315 Self {
316 bytes: [0xFF; TEST_FLASH_SIZE],
317 }
318 }
319 }
320
321 impl FlashDevice for MemoryFlashDevice {
322 type Error = ();
323
324 fn read(&mut self, offset: u32, bytes: &mut [u8]) -> Result<(), ()> {
325 let offset = offset as usize;
326 bytes.copy_from_slice(&self.bytes[offset..offset + bytes.len()]);
327 Ok(())
328 }
329
330 fn write(&mut self, offset: u32, bytes: &[u8]) -> Result<(), ()> {
331 let offset = offset as usize;
332 self.bytes[offset..offset + bytes.len()].copy_from_slice(bytes);
333 Ok(())
334 }
335
336 fn erase(&mut self, from: u32, to: u32) -> Result<(), ()> {
337 self.bytes[from as usize..to as usize].fill(0xFF);
338 Ok(())
339 }
340 }
341
342 #[derive(Clone, Debug, Eq, PartialEq, serde::Serialize, serde::Deserialize)]
343 struct WifiPersistedState {
344 ssid: heapless::String<32>,
345 password: heapless::String<64>,
346 timezone_offset_minutes: i32,
347 }
348
349 #[derive(Clone, Debug, Eq, PartialEq, serde::Serialize, serde::Deserialize)]
350 struct OtherState {
351 timezone_offset_minutes: i32,
352 }
353
354 #[test]
355 fn save_load_clear_round_trip() {
356 let mut device = MemoryFlashDevice::new();
357 let state = WifiPersistedState {
358 ssid: heapless::String::try_from("demo-net").expect("ssid fits"),
359 password: heapless::String::try_from("password123").expect("password fits"),
360 timezone_offset_minutes: -300,
361 };
362
363 save_block::<TEST_FLASH_BLOCK_SIZE, _, _>(&mut device, 0, &state).expect("save succeeds");
364 let loaded = load_block::<TEST_FLASH_BLOCK_SIZE, WifiPersistedState, _>(&mut device, 0)
365 .expect("load succeeds")
366 .expect("value exists");
367 assert_eq!(loaded, state);
368
369 clear_block::<TEST_FLASH_BLOCK_SIZE, _>(&mut device, 0).expect("clear succeeds");
370 let cleared = load_block::<TEST_FLASH_BLOCK_SIZE, WifiPersistedState, _>(&mut device, 0)
371 .expect("load succeeds");
372 assert!(cleared.is_none());
373 }
374
375 #[test]
376 fn type_mismatch_returns_none() {
377 let mut device = MemoryFlashDevice::new();
378 let other = OtherState {
379 timezone_offset_minutes: 60,
380 };
381 save_block::<TEST_FLASH_BLOCK_SIZE, _, _>(&mut device, 0, &other).expect("save succeeds");
382 let result = load_block::<TEST_FLASH_BLOCK_SIZE, WifiPersistedState, _>(&mut device, 0)
383 .expect("load succeeds");
384 assert!(result.is_none());
385 }
386
387 #[test]
388 fn corrupted_crc_returns_error() {
389 let mut device = MemoryFlashDevice::new();
390 let state = WifiPersistedState {
391 ssid: heapless::String::new(),
392 password: heapless::String::new(),
393 timezone_offset_minutes: 0,
394 };
395 save_block::<TEST_FLASH_BLOCK_SIZE, _, _>(&mut device, 0, &state).expect("save succeeds");
396 device.bytes[HEADER_SIZE + 1] ^= 0x5A;
397
398 let error = load_block::<TEST_FLASH_BLOCK_SIZE, WifiPersistedState, _>(&mut device, 0)
399 .expect_err("crc mismatch should fail");
400 assert!(matches!(error, Error::<()>::StorageCorrupted));
401 }
402
403 #[test]
404 fn max_payload_size_is_header_and_crc_aware() {
405 assert_eq!(
406 max_payload_size(TEST_FLASH_BLOCK_SIZE),
407 TEST_FLASH_BLOCK_SIZE - 14
408 );
409 }
410}