1use crate::record::{self, CommitError, RecordWriter};
18
19pub const IDENTITY_PAYLOAD_LEN: usize = 64;
22
23pub fn encode_identity(secret: &[u8; 32], public: &[u8; 32]) -> [u8; IDENTITY_PAYLOAD_LEN] {
24 let mut payload = [0u8; IDENTITY_PAYLOAD_LEN];
25 payload[..32].copy_from_slice(secret);
26 payload[32..].copy_from_slice(public);
27 payload
28}
29
30pub fn decode_identity(payload: &[u8]) -> Option<([u8; 32], [u8; 32])> {
33 if payload.len() != IDENTITY_PAYLOAD_LEN {
34 return None;
35 }
36 Some((
37 payload[..32].try_into().expect("length checked"),
38 payload[32..].try_into().expect("length checked"),
39 ))
40}
41
42pub const SLOT_SIZE: usize = 2048;
45pub const COMMIT_OFFSET: usize = SLOT_SIZE - 4;
46const CRC_OFFSET: usize = COMMIT_OFFSET - 4;
47const MAGIC: [u8; 4] = *b"UPRS";
48const KIND_SNAPSHOT: u8 = 0;
49const KIND_CLEARED: u8 = 1;
50const HEADER_LEN: usize = 4 + 4 + 1 + 2;
51pub const MAX_PAYLOAD: usize = CRC_OFFSET - HEADER_LEN;
53
54#[derive(Clone, Debug, PartialEq, Eq)]
56pub enum Record {
57 Snapshot(heapless::Vec<u8, MAX_PAYLOAD>),
59 Cleared,
65}
66
67#[derive(Clone, Copy, Debug)]
72pub enum RecordRef<'a> {
73 Snapshot(&'a [u8]),
74 Cleared,
75}
76
77#[derive(Clone, Debug, PartialEq, Eq)]
80pub struct Stored {
81 pub generation: u32,
82 pub record: Record,
83}
84
85pub fn encode_record(generation: u32, record: RecordRef<'_>) -> [u8; SLOT_SIZE] {
90 let mut bytes = [0xFFu8; SLOT_SIZE];
91 bytes[..4].copy_from_slice(&MAGIC);
92 bytes[4..8].copy_from_slice(&generation.to_le_bytes());
93 let (kind, payload): (u8, &[u8]) = match record {
94 RecordRef::Snapshot(payload) => (KIND_SNAPSHOT, payload),
95 RecordRef::Cleared => (KIND_CLEARED, &[]),
96 };
97 bytes[8] = kind;
98 bytes[9..11].copy_from_slice(&(payload.len() as u16).to_le_bytes());
99 bytes[HEADER_LEN..HEADER_LEN + payload.len()].copy_from_slice(payload);
100 let crc = record::crc32(&bytes[..CRC_OFFSET]);
101 bytes[CRC_OFFSET..COMMIT_OFFSET].copy_from_slice(&crc.to_le_bytes());
102 bytes
103}
104
105impl Stored {
106 #[cfg(test)]
110 pub fn encode(&self) -> [u8; SLOT_SIZE] {
111 let record = match &self.record {
112 Record::Snapshot(payload) => RecordRef::Snapshot(payload),
113 Record::Cleared => RecordRef::Cleared,
114 };
115 encode_record(self.generation, record)
116 }
117
118 pub fn decode(bytes: &[u8; SLOT_SIZE]) -> Option<Self> {
119 let generation = probe_record(bytes)?;
120 let len = usize::from(u16::from_le_bytes(bytes[9..11].try_into().ok()?));
121 let record = match bytes[8] {
122 KIND_SNAPSHOT => {
123 let mut payload = heapless::Vec::new();
124 payload
125 .extend_from_slice(&bytes[HEADER_LEN..HEADER_LEN + len])
126 .ok()?;
127 Record::Snapshot(payload)
128 }
129 _ => Record::Cleared,
131 };
132 Some(Self { generation, record })
133 }
134}
135
136pub fn probe_record(bytes: &[u8; SLOT_SIZE]) -> Option<u32> {
146 if bytes[..4] != MAGIC {
147 return None;
148 }
149 if bytes[COMMIT_OFFSET..] != [0; 4] {
150 return None;
151 }
152 let crc = u32::from_le_bytes(bytes[CRC_OFFSET..COMMIT_OFFSET].try_into().ok()?);
153 if crc != record::crc32(&bytes[..CRC_OFFSET]) {
154 return None;
155 }
156 let len = usize::from(u16::from_le_bytes(bytes[9..11].try_into().ok()?));
157 if len > MAX_PAYLOAD {
158 return None;
159 }
160 match bytes[8] {
161 KIND_SNAPSHOT => {}
162 KIND_CLEARED if len == 0 => {}
163 _ => return None,
164 }
165 Some(u32::from_le_bytes(bytes[4..8].try_into().ok()?))
166}
167
168pub fn payload_bytes(bytes: &[u8; SLOT_SIZE]) -> Option<(u32, Option<&[u8]>)> {
177 let generation = probe_record(bytes)?;
178 let len = usize::from(u16::from_le_bytes(bytes[9..11].try_into().ok()?));
179 let payload = match bytes[8] {
180 KIND_SNAPSHOT => Some(&bytes[HEADER_LEN..HEADER_LEN + len]),
181 _ => None,
183 };
184 Some((generation, payload))
185}
186
187pub fn consider_slot(current: &mut Option<(u32, u32)>, address: u32, bytes: &[u8; SLOT_SIZE]) {
191 let Some(generation) = probe_record(bytes) else {
192 return;
193 };
194 if current.is_none_or(|(_, newest)| record::generation_is_newer(generation, newest)) {
195 *current = Some((address, generation));
196 }
197}
198
199pub fn consider_older_slot(
212 current: &mut Option<(u32, u32)>,
213 address: u32,
214 bytes: &[u8; SLOT_SIZE],
215 newer_than: u32,
216) {
217 let Some(generation) = probe_record(bytes) else {
218 return;
219 };
220 if !record::generation_is_newer(newer_than, generation) {
221 return;
222 }
223 if current.is_none_or(|(_, newest)| record::generation_is_newer(generation, newest)) {
224 *current = Some((address, generation));
225 }
226}
227
228pub async fn write_record<W: RecordWriter>(
232 writer: &mut W,
233 target: u32,
234 generation: u32,
235 record: RecordRef<'_>,
236) -> Result<(), CommitError<W::Error>> {
237 let bytes = encode_record(generation, record);
238 crate::record::write_committed_record(writer, target, &bytes).await
239}
240
241#[cfg(test)]
242mod tests {
243 use super::*;
244 use crate::record::{PAGE_SIZE, PageEraser};
245 use core::future::Future;
246 use core::task::{Context, Poll, Waker};
247
248 const PAGE0: u32 = 0x000E_8000;
251 const PAGE1: u32 = PAGE0 + PAGE_SIZE;
252
253 fn block_on<F: Future>(future: F) -> F::Output {
254 let mut future = core::pin::pin!(future);
255 let waker = Waker::noop();
256 let mut context = Context::from_waker(&waker);
257 loop {
258 if let Poll::Ready(output) = future.as_mut().poll(&mut context) {
259 return output;
260 }
261 }
262 }
263
264 struct MockFlash {
267 bytes: std::collections::BTreeMap<u32, u8>,
268 budget: Option<usize>,
269 }
270
271 impl MockFlash {
272 fn new() -> Self {
273 Self {
274 bytes: std::collections::BTreeMap::new(),
275 budget: None,
276 }
277 }
278
279 fn slot(&self, address: u32) -> [u8; SLOT_SIZE] {
280 let mut out = [0xFFu8; SLOT_SIZE];
281 for (offset, byte) in out.iter_mut().enumerate() {
282 if let Some(value) = self.bytes.get(&(address + offset as u32)) {
283 *byte = *value;
284 }
285 }
286 out
287 }
288
289 fn mount(&self) -> Option<(u32, Stored)> {
290 let mut latest = None;
291 for page in [PAGE0, PAGE1] {
292 let mut address = page;
293 while address < page + PAGE_SIZE {
294 consider_slot(&mut latest, address, &self.slot(address));
295 address += SLOT_SIZE as u32;
296 }
297 }
298 let (slot, _) = latest?;
299 Stored::decode(&self.slot(slot)).map(|stored| (slot, stored))
300 }
301
302 fn mounted_snapshot(&self) -> Option<Stored> {
306 match self.mount() {
307 Some((_, stored)) if matches!(stored.record, Record::Snapshot(_)) => Some(stored),
308 _ => None,
309 }
310 }
311 }
312
313 impl RecordWriter for MockFlash {
314 type Error = ();
315
316 async fn write_record(&mut self, address: u32, bytes: &[u8]) -> Result<(), Self::Error> {
317 for (offset, byte) in bytes.iter().enumerate() {
318 if let Some(budget) = &mut self.budget {
319 if *budget == 0 {
320 return Err(());
321 }
322 *budget -= 1;
323 }
324 self.bytes.insert(address + offset as u32, *byte);
325 }
326 Ok(())
327 }
328 }
329
330 impl PageEraser for MockFlash {
331 type Error = ();
332
333 async fn erase_page(&mut self, start: u32, end: u32) -> Result<(), Self::Error> {
334 self.bytes
335 .retain(|address, _| *address < start || *address >= end);
336 Ok(())
337 }
338 }
339
340 fn record(generation: u32, fill: u8, len: usize) -> Stored {
341 let mut payload = heapless::Vec::new();
342 payload.resize(len, fill).unwrap();
343 Stored {
344 generation,
345 record: Record::Snapshot(payload),
346 }
347 }
348
349 fn tombstone(generation: u32) -> Stored {
350 Stored {
351 generation,
352 record: Record::Cleared,
353 }
354 }
355
356 async fn write_record<W: RecordWriter>(
360 writer: &mut W,
361 target: u32,
362 stored: &Stored,
363 ) -> Result<(), CommitError<W::Error>> {
364 let record = match &stored.record {
365 Record::Snapshot(payload) => RecordRef::Snapshot(payload),
366 Record::Cleared => RecordRef::Cleared,
367 };
368 super::write_record(writer, target, stored.generation, record).await
369 }
370
371 #[test]
372 fn committed_record_round_trips_and_newest_generation_wins() {
373 let mut flash = MockFlash::new();
374 block_on(write_record(&mut flash, PAGE0, &record(1, 0xAA, 900))).unwrap();
375 block_on(write_record(
376 &mut flash,
377 PAGE0 + SLOT_SIZE as u32,
378 &record(2, 0xBB, 3),
379 ))
380 .unwrap();
381 let (address, mounted) = flash.mount().unwrap();
382 assert_eq!(address, PAGE0 + SLOT_SIZE as u32);
383 assert_eq!(mounted, record(2, 0xBB, 3));
384 }
385
386 #[test]
387 fn uncommitted_corrupt_and_oversize_records_are_ignored() {
388 let mut flash = MockFlash::new();
389 let bytes = record(1, 0xAA, 16).encode();
391 block_on(RecordWriter::write_record(
392 &mut flash,
393 PAGE0,
394 &bytes[..COMMIT_OFFSET],
395 ))
396 .unwrap();
397 assert!(flash.mount().is_none());
398
399 block_on(write_record(&mut flash, PAGE0, &record(1, 0xAA, 16))).unwrap();
401 flash.bytes.insert(PAGE0 + 12, 0x00);
402 assert!(flash.mount().is_none());
403
404 for (offset, bad) in [(9u32, (MAX_PAYLOAD as u16 + 1).to_le_bytes()), (8, [2, 0])] {
406 let mut bytes = record(1, 0xAA, 16).encode();
407 let at = offset as usize;
408 bytes[at..at + 2].copy_from_slice(&bad);
409 let crc = crate::record::crc32(&bytes[..CRC_OFFSET]);
410 bytes[CRC_OFFSET..COMMIT_OFFSET].copy_from_slice(&crc.to_le_bytes());
411 let mut flash = MockFlash::new();
412 block_on(RecordWriter::write_record(
413 &mut flash,
414 PAGE1,
415 &bytes[..COMMIT_OFFSET],
416 ))
417 .unwrap();
418 block_on(RecordWriter::write_record(
419 &mut flash,
420 PAGE1 + COMMIT_OFFSET as u32,
421 &[0; 4],
422 ))
423 .unwrap();
424 assert!(flash.mount().is_none());
425 }
426 }
427
428 #[test]
433 fn tombstone_clears_and_survives_interruption() {
434 let mut flash = MockFlash::new();
436 block_on(write_record(&mut flash, PAGE0, &record(1, 0xAA, 40))).unwrap();
437 block_on(write_record(&mut flash, PAGE1, &record(2, 0xBB, 40))).unwrap();
438 assert_eq!(flash.mounted_snapshot().unwrap(), record(2, 0xBB, 40));
439
440 let total = COMMIT_OFFSET + 4;
447 for cut in (0..=HEADER_LEN + 1)
448 .chain((HEADER_LEN..CRC_OFFSET).step_by(89))
449 .chain(CRC_OFFSET - 1..total)
450 {
451 let mut flash = flash_with_two_snapshots();
452 flash.budget = Some(cut);
453 let target = PAGE0 + SLOT_SIZE as u32;
454 assert!(block_on(write_record(&mut flash, target, &tombstone(3))).is_err());
455 flash.budget = None;
456 assert_eq!(
457 flash.mounted_snapshot().expect("snapshot must survive"),
458 record(2, 0xBB, 40),
459 "cut at {cut} lost or replaced the committed snapshot"
460 );
461 }
462
463 let mut flash = flash_with_two_snapshots();
466 block_on(write_record(
467 &mut flash,
468 PAGE0 + SLOT_SIZE as u32,
469 &tombstone(3),
470 ))
471 .unwrap();
472 assert!(flash.mounted_snapshot().is_none());
473 assert_eq!(flash.mount().unwrap().1, tombstone(3));
474
475 block_on(write_record(
477 &mut flash,
478 PAGE1 + SLOT_SIZE as u32,
479 &tombstone(4),
480 ))
481 .unwrap();
482 assert!(flash.mounted_snapshot().is_none());
483 block_on(write_record(&mut flash, PAGE0, &record(5, 0xCC, 8))).unwrap();
484 assert_eq!(flash.mounted_snapshot().unwrap(), record(5, 0xCC, 8));
485 }
486
487 fn flash_with_two_snapshots() -> MockFlash {
488 let mut flash = MockFlash::new();
489 block_on(write_record(&mut flash, PAGE0, &record(1, 0xAA, 40))).unwrap();
490 block_on(write_record(&mut flash, PAGE1, &record(2, 0xBB, 40))).unwrap();
491 flash
492 }
493
494 #[test]
499 fn walk_back_finds_successively_older_committed_records() {
500 let mut flash = MockFlash::new();
501 block_on(write_record(&mut flash, PAGE0, &record(1, 0xAA, 40))).unwrap();
502 block_on(write_record(
503 &mut flash,
504 PAGE0 + SLOT_SIZE as u32,
505 &record(2, 0xBB, 40),
506 ))
507 .unwrap();
508 block_on(write_record(&mut flash, PAGE1, &record(3, 0xCC, 40))).unwrap();
509
510 let older = |newer_than: u32| {
511 let mut latest = None;
512 for page in [PAGE0, PAGE1] {
513 let mut address = page;
514 while address < page + PAGE_SIZE {
515 consider_older_slot(&mut latest, address, &flash.slot(address), newer_than);
516 address += SLOT_SIZE as u32;
517 }
518 }
519 let (slot, _) = latest?;
520 Stored::decode(&flash.slot(slot))
521 };
522
523 assert_eq!(flash.mount().unwrap().1, record(3, 0xCC, 40));
524 assert_eq!(older(3), Some(record(2, 0xBB, 40)));
525 assert_eq!(older(2), Some(record(1, 0xAA, 40)));
526 assert_eq!(older(1), None);
527 }
528
529 #[test]
530 fn identity_payload_round_trips() {
531 let payload = encode_identity(&[0x11; 32], &[0x22; 32]);
532 assert_eq!(decode_identity(&payload), Some(([0x11; 32], [0x22; 32])));
533 assert_eq!(decode_identity(&payload[..63]), None);
534 assert_eq!(decode_identity(&[]), None);
535 }
536
537 #[test]
540 fn generation_wraparound_selects_the_newer_record() {
541 let mut flash = MockFlash::new();
542 block_on(write_record(&mut flash, PAGE0, &record(u32::MAX, 0xAA, 8))).unwrap();
543 block_on(write_record(
544 &mut flash,
545 PAGE0 + SLOT_SIZE as u32,
546 &tombstone(0),
547 ))
548 .unwrap();
549 assert!(flash.mounted_snapshot().is_none());
550 assert_eq!(flash.mount().unwrap().1, tombstone(0));
551 }
552
553 #[test]
556 fn power_cut_at_every_byte_never_replaces_the_committed_record() {
557 let old = record(7, 0x11, 700);
558 let new = record(8, 0x22, 700);
559 let total = COMMIT_OFFSET + 4;
561 for cut in 0..total {
562 let mut flash = MockFlash::new();
563 block_on(write_record(&mut flash, PAGE0, &old)).unwrap();
564 flash.budget = Some(cut);
565 let result = block_on(write_record(&mut flash, PAGE0 + SLOT_SIZE as u32, &new));
566 assert!(result.is_err(), "cut at {cut} must fail the write");
567 flash.budget = None;
568 let (_, mounted) = flash.mount().expect("old record must survive");
569 assert_eq!(mounted, old, "cut at {cut} corrupted the mount");
570 }
571 let mut flash = MockFlash::new();
573 block_on(write_record(&mut flash, PAGE0, &old)).unwrap();
574 block_on(write_record(&mut flash, PAGE0 + SLOT_SIZE as u32, &new)).unwrap();
575 assert_eq!(flash.mount().unwrap().1, new);
576 }
577}