1use crate::journal::{JournalFlash, SharedFlash, journal_write_target};
5use embassy_sync::blocking_mutex::raw::RawMutex;
6use umsh_journal_store::{
7 proto,
8 record::{self, PAGE_SIZE},
9};
10
11pub const SLOT_SIZE: usize = PAGE_SIZE as usize;
12pub const MAX_PAYLOAD: usize = proto::max_payload(SLOT_SIZE);
14pub type BootPayload = heapless::Vec<u8, MAX_PAYLOAD>;
15
16pub struct WifiStore<M: RawMutex + 'static, F: JournalFlash + 'static> {
17 flash: &'static SharedFlash<M, F>,
18 page0: u32,
19 generation: u32,
20 slot: Option<u32>,
21 walked_back_to: u32,
22}
23
24#[cfg(test)]
25mod tests {
26 use super::*;
27 use embassy_sync::{blocking_mutex::raw::NoopRawMutex, mutex::Mutex};
28 use record::{PageEraser, RecordReader, RecordWriter};
29 use std::{boxed::Box, vec, vec::Vec};
30
31 struct Flash {
32 bytes: Vec<u8>,
33 budget: usize,
34 }
35 impl RecordReader for Flash {
36 type Error = ();
37 fn read_record(&mut self, address: u32, out: &mut [u8]) -> Result<(), ()> {
38 out.copy_from_slice(&self.bytes[address as usize..address as usize + out.len()]);
39 Ok(())
40 }
41 }
42 impl RecordWriter for Flash {
43 type Error = ();
44 async fn write_record(&mut self, address: u32, bytes: &[u8]) -> Result<(), ()> {
45 for (i, byte) in bytes.iter().enumerate() {
46 if self.budget == 0 {
47 return Err(());
48 }
49 self.budget -= 1;
50 self.bytes[address as usize + i] &= byte;
51 }
52 Ok(())
53 }
54 }
55 impl PageEraser for Flash {
56 type Error = ();
57 async fn erase_page(&mut self, start: u32, end: u32) -> Result<(), ()> {
58 self.bytes[start as usize..end as usize].fill(255);
59 Ok(())
60 }
61 }
62 fn run<T>(future: impl core::future::Future<Output = T>) -> T {
63 let mut future = core::pin::pin!(future);
64 let mut cx = core::task::Context::from_waker(core::task::Waker::noop());
65 match future.as_mut().poll(&mut cx) {
66 core::task::Poll::Ready(value) => value,
67 _ => panic!("mock flash must not suspend"),
68 }
69 }
70
71 #[test]
72 fn legacy_upgrade_and_interrupted_commits_preserve_one_complete_snapshot() {
73 let old = b"legacy configuration";
74 let new = [0x42; 2560];
75 for cut in (0..24).chain((24..4092).step_by(137)).chain(4092..=4096) {
76 let mut bytes = vec![255; 8192];
77 let mut record = proto::encode_record(7, proto::RecordRef::Snapshot(old));
78 record[proto::COMMIT_OFFSET..].fill(0);
79 bytes[2048..4096].copy_from_slice(&record);
81 let flash = Box::leak(Box::new(Mutex::<NoopRawMutex, _>::new(Flash {
82 bytes,
83 budget: cut,
84 })));
85 let (mut store, mounted) = run(WifiStore::mount(flash, 0));
86 assert_eq!(mounted.as_deref(), Some(old.as_slice()));
87 let saved = run(store.persist(&new));
88 let (_, mounted) = run(WifiStore::mount(flash, 0));
89 assert_eq!(
90 mounted.as_deref(),
91 Some(if saved.is_ok() { &new } else { old.as_slice() })
92 );
93 }
94 }
95
96 #[test]
97 fn clear_is_committed_and_an_invalid_payload_can_walk_back() {
98 let flash = Box::leak(Box::new(Mutex::<NoopRawMutex, _>::new(Flash {
99 bytes: vec![255; 8192],
100 budget: usize::MAX,
101 })));
102 let (mut store, _) = run(WifiStore::mount(flash, 0));
103 run(store.persist(b"first")).unwrap();
104 run(store.persist(b"invalid protocol payload")).unwrap();
105 let (mut store, _) = run(WifiStore::mount(flash, 0));
106 let mut out = [0; 64];
107 let len = run(store.older_snapshot(&mut out)).unwrap();
108 assert_eq!(&out[..len], b"first");
109 run(store.clear()).unwrap();
110 assert!(run(WifiStore::mount(flash, 0)).1.is_none());
111 }
112}
113
114fn newest<F: JournalFlash>(
118 flash: &mut F,
119 page0: u32,
120 before: Option<u32>,
121 buf: &mut [u8; SLOT_SIZE],
122) -> Option<(u32, usize, u32)> {
123 let mut winner: Option<(u32, usize, u32)> = None;
124 for page in [page0, page0 + PAGE_SIZE] {
125 if flash.read_record(page, buf).is_err() {
126 continue;
127 }
128 let large = proto::probe_record(buf).is_some();
129 let width = if large { SLOT_SIZE } else { proto::SLOT_SIZE };
130 for offset in (0..SLOT_SIZE).step_by(width) {
131 let Some(generation) = proto::probe_record(&buf[offset..offset + width]) else {
132 continue;
133 };
134 if before.is_some_and(|before| !record::generation_is_newer(before, generation)) {
135 continue;
136 }
137 if winner.is_none_or(|(_, _, old)| record::generation_is_newer(generation, old)) {
138 winner = Some((page + offset as u32, width, generation));
139 }
140 }
141 }
142 winner
143}
144
145impl<M: RawMutex + 'static, F: JournalFlash + 'static> WifiStore<M, F> {
146 pub const MAX_PAYLOAD: usize = MAX_PAYLOAD;
148
149 pub async fn mount(
150 flash: &'static SharedFlash<M, F>,
151 page0: u32,
152 ) -> (Self, Option<BootPayload>) {
153 let mut locked = flash.lock().await;
154 let mut buf = [0; SLOT_SIZE];
155 let found = newest(&mut *locked, page0, None, &mut buf);
156 let payload = found.and_then(|(address, size, _)| {
157 locked.read_record(address, &mut buf[..size]).ok()?;
158 let (_, payload) = proto::payload_bytes(&buf[..size])?;
159 BootPayload::from_slice(payload?).ok()
160 });
161 let generation = found.map_or(0, |(_, _, generation)| generation);
162 (
163 Self {
164 flash,
165 page0,
166 generation,
167 slot: found.map(|(address, _, _)| address),
168 walked_back_to: generation,
169 },
170 payload,
171 )
172 }
173
174 pub async fn older_snapshot(&mut self, out: &mut [u8]) -> Option<usize> {
175 let mut flash = self.flash.lock().await;
176 let mut buf = [0; SLOT_SIZE];
177 let (address, size, generation) =
178 newest(&mut *flash, self.page0, Some(self.walked_back_to), &mut buf)?;
179 self.walked_back_to = generation;
180 flash.read_record(address, &mut buf[..size]).ok()?;
181 let (_, payload) = proto::payload_bytes(&buf[..size])?;
182 let payload = payload?;
183 out.get_mut(..payload.len())?.copy_from_slice(payload);
184 Some(payload.len())
185 }
186
187 pub async fn persist(&mut self, payload: &[u8]) -> Result<(), ()> {
188 if payload.len() > Self::MAX_PAYLOAD {
189 return Err(());
190 }
191 self.write(proto::RecordRef::Snapshot(payload)).await
192 }
193
194 pub async fn clear(&mut self) -> Result<(), ()> {
195 self.write(proto::RecordRef::Cleared).await
196 }
197
198 async fn write(&mut self, record: proto::RecordRef<'_>) -> Result<(), ()> {
199 let mut flash = self.flash.lock().await;
200 let current_page = self
202 .slot
203 .map(|address| self.page0 + ((address - self.page0) / PAGE_SIZE) * PAGE_SIZE);
204 let target = journal_write_target(&mut *flash, current_page, self.page0, SLOT_SIZE).await?;
205 let generation = self.generation.wrapping_add(1);
206 let bytes = proto::encode_record_sized::<SLOT_SIZE>(generation, record);
207 record::write_committed_record(&mut *flash, target, &bytes)
208 .await
209 .map_err(|_| ())?;
210 self.generation = generation;
211 self.slot = Some(target);
212 Ok(())
213 }
214}