embedded_savegame/lib.rs
1#![no_std]
2//! # Overview
3//!
4//! This library provides a power-fail safe savegame system for embedded devices with wear leveling.
5//! It manages data storage on flash memory (EEPROM or NOR flash) by distributing writes across
6//! multiple slots to prevent wear-out of specific memory locations.
7//!
8//! # Flash Support
9//!
10//! - `eeprom24x` feature: Support for AT24Cxx EEPROM chips
11//! - `w25q` feature: Support for W25Q NOR flash chips
12//! - `mock` feature: Mock flash implementations for testing
13//!
14//! # Example
15//!
16#![cfg_attr(feature = "mock", doc = r#"```"#)]
17#![cfg_attr(not(feature = "mock"), doc = r#"```rust,compile_fail"#)]
18//! use embedded_savegame::storage::{Storage, Flash};
19//!
20//! // Configure storage with 64-byte slots across 8 total slots
21//! const SLOT_SIZE: usize = 64;
22//! const SLOT_COUNT: usize = 8;
23//!
24//! # fn main() -> Result<(), Box<dyn std::error::Error>> {
25//! # use embedded_savegame::mock::SectorMockFlash;
26//! # let mut flash_device = SectorMockFlash::<SLOT_SIZE, SLOT_COUNT>::new();
27//! let mut storage = Storage::<_, SLOT_SIZE, SLOT_COUNT>::new(flash_device);
28//!
29//! // Scan for existing savegame
30//! if let Some(slot) = storage.scan()? {
31//! let mut buf = [0u8; 256];
32//! if let Some(data) = storage.read(slot.idx, &mut buf)? {
33//! // Process loaded savegame
34//! }
35//! }
36//!
37//! // Write new savegame
38//! let mut save_data = b"game state data".to_vec();
39//! storage.append(&mut save_data)?;
40//! # Ok(())
41//! # }
42//! ```
43//!
44//! # Architecture
45//!
46//! Each slot contains a header with:
47//! - Current savegame checksum
48//! - Data length
49//! - Previous savegame checksum (for chain verification)
50//!
51//! The scanner finds the most recent valid savegame by following the checksum chain.
52
53pub mod chksum;
54#[cfg(feature = "eeprom24x")]
55pub mod eeprom24x;
56#[cfg(any(test, feature = "mock"))]
57pub mod mock;
58pub mod storage;
59#[cfg(feature = "w25q")]
60pub mod w25q;
61
62use crate::chksum::Chksum;
63
64const LENGTH_SIZE: usize = 4;
65
66/// A savegame slot containing metadata about stored data
67///
68/// Each slot represents a savegame header stored in flash memory. Slots form a chain
69/// where each new savegame references the previous one via checksums, enabling the
70/// scanner to find the most recent valid savegame even after power failures.
71///
72/// # Fields
73///
74/// - `idx`: The slot index in flash memory
75/// - `chksum`: Checksum of the savegame data
76/// - `len`: Length of the savegame data in bytes
77/// - `prev`: Checksum of the previous savegame (for chain verification)
78#[derive(Debug, PartialEq)]
79pub struct Slot {
80 pub idx: usize,
81 pub chksum: Chksum,
82 pub len: u32,
83 pub prev: Chksum,
84}
85
86impl Slot {
87 /// Size of the slot header in bytes: two checksums and one length field.
88 /// The first byte of the checksum is also used to indicate if the slot is in use.
89 pub const HEADER_SIZE: usize = Chksum::SIZE * 2 + LENGTH_SIZE;
90
91 /// Create a new slot for the given data
92 ///
93 /// Calculates the checksum for the data and creates a slot that references
94 /// the previous savegame's checksum.
95 ///
96 /// # Arguments
97 ///
98 /// * `idx` - The slot index where this will be stored
99 /// * `prev` - The checksum of the previous savegame (or zero for first savegame)
100 /// * `data` - The savegame data to store
101 pub const fn create(idx: usize, prev: Chksum, data: &[u8]) -> Self {
102 let chksum = Chksum::hash(prev, data);
103 let len = data.len() as u32;
104 Self {
105 idx,
106 chksum,
107 len,
108 prev,
109 }
110 }
111
112 /// Check if this slot has valid checksums
113 ///
114 /// A slot is valid if both its checksum and previous checksum have the correct format
115 /// (most significant bit is zero).
116 pub const fn is_valid(&self) -> bool {
117 self.chksum.is_valid() && self.prev.is_valid()
118 }
119
120 /// Check if this slot is an update to another slot
121 ///
122 /// Returns `true` if this slot's `prev` checksum matches the other slot's checksum,
123 /// indicating this is a newer version of the savegame.
124 pub fn is_update_to(&self, other: &Self) -> bool {
125 self.prev == other.chksum
126 }
127
128 /// Calculate the total number of bytes used by this savegame
129 ///
130 /// Accounts for the header in the first slot and continuation bytes in
131 /// subsequent slots if the savegame spans multiple slots.
132 ///
133 /// # Type Parameters
134 ///
135 /// * `SLOT_SIZE` - The size of each slot in bytes
136 pub fn used_bytes<const SLOT_SIZE: usize>(&self) -> usize {
137 let mut size = Self::HEADER_SIZE;
138 let mut remaining_data = self.len as usize;
139 let mut remaining_space = SLOT_SIZE - Self::HEADER_SIZE;
140
141 loop {
142 let this_round = remaining_space.min(remaining_data);
143 size = size.saturating_add(this_round);
144 remaining_data = remaining_data.saturating_sub(this_round);
145
146 if remaining_data == 0 {
147 break;
148 }
149
150 size = size.saturating_add(1); // for the next slot's header byte
151 remaining_space = SLOT_SIZE - 1;
152 }
153
154 size
155 }
156
157 /// Calculate the index of the next free slot after this savegame
158 ///
159 /// Takes into account how many slots this savegame occupies and wraps around
160 /// using modulo arithmetic.
161 ///
162 /// # Type Parameters
163 ///
164 /// * `SLOT_SIZE` - The size of each slot in bytes
165 /// * `SLOT_COUNT` - The total number of slots available
166 pub fn next_slot<const SLOT_SIZE: usize, const SLOT_COUNT: usize>(&self) -> usize {
167 let used_slots = self.used_bytes::<SLOT_SIZE>().div_ceil(SLOT_SIZE);
168 self.idx.saturating_add(used_slots) % SLOT_COUNT
169 }
170
171 /// Serialize the slot header to bytes for writing to flash
172 ///
173 /// The format is: checksum (4 bytes) + length (4 bytes) + prev checksum (4 bytes)
174 pub fn to_bytes(&self) -> [u8; Self::HEADER_SIZE] {
175 let mut buf = [0u8; Self::HEADER_SIZE];
176
177 let (chksum, len, prev) =
178 arrayref::mut_array_refs![&mut buf, Chksum::SIZE, LENGTH_SIZE, Chksum::SIZE];
179
180 chksum.copy_from_slice(&self.chksum.to_bytes());
181 len.copy_from_slice(&self.len.to_be_bytes());
182 prev.copy_from_slice(&self.prev.to_bytes());
183
184 buf
185 }
186
187 /// Deserialize a slot header from bytes
188 ///
189 /// # Arguments
190 ///
191 /// * `idx` - The slot index where this header was read from
192 /// * `bytes` - The header bytes in the format: checksum + length + prev checksum
193 pub fn from_bytes(idx: usize, bytes: [u8; Self::HEADER_SIZE]) -> Self {
194 let (chksum, len, prev) =
195 arrayref::array_refs![&bytes, Chksum::SIZE, LENGTH_SIZE, Chksum::SIZE];
196
197 Self {
198 idx,
199 chksum: Chksum::from_bytes(*chksum),
200 len: u32::from_be_bytes(*len),
201 prev: Chksum::from_bytes(*prev),
202 }
203 }
204}
205
206#[cfg(test)]
207mod tests {
208 use super::*;
209
210 const SLOT_SIZE: usize = 64;
211 const SLOT_COUNT: usize = 8;
212
213 #[test]
214 fn test_slot_to_bytes() {
215 let slot = Slot::create(0, Chksum::zero(), b"hello");
216 assert_eq!(
217 slot.to_bytes(),
218 [116, 186, 120, 103, 0, 0, 0, 5, 0, 0, 0, 0]
219 );
220
221 let append = Slot::create(1, slot.chksum, b"world");
222 assert_eq!(
223 append.to_bytes(),
224 [21, 165, 57, 22, 0, 0, 0, 5, 116, 186, 120, 103]
225 );
226 }
227
228 #[test]
229 fn test_slot_size_small() {
230 let slot = Slot::create(0, Chksum::zero(), b"ohai!");
231 assert_eq!(slot.used_bytes::<SLOT_SIZE>(), Slot::HEADER_SIZE + 5);
232 assert_eq!(slot.next_slot::<SLOT_SIZE, SLOT_COUNT>(), 1);
233 }
234
235 #[test]
236 fn test_slot_size_full() {
237 let bytes = [b'B'; SLOT_SIZE - Slot::HEADER_SIZE];
238 let slot = Slot::create(0, Chksum::zero(), &bytes);
239 assert_eq!(slot.used_bytes::<SLOT_SIZE>(), SLOT_SIZE);
240 assert_eq!(slot.next_slot::<SLOT_SIZE, SLOT_COUNT>(), 1);
241 }
242
243 #[test]
244 fn test_slot_spill_over() {
245 let bytes = [b'B'; SLOT_SIZE];
246 let slot = Slot::create(0, Chksum::zero(), &bytes);
247 assert_eq!(
248 slot.used_bytes::<SLOT_SIZE>(),
249 // One extra because the continue-header
250 Slot::HEADER_SIZE + SLOT_SIZE + 1,
251 );
252 assert_eq!(slot.next_slot::<SLOT_SIZE, SLOT_COUNT>(), 2);
253 }
254
255 #[test]
256 fn test_slot_spill_over_twice() {
257 let bytes = [b'B'; SLOT_SIZE * 2];
258 let slot = Slot::create(0, Chksum::zero(), &bytes);
259 assert_eq!(
260 slot.used_bytes::<SLOT_SIZE>(),
261 // Two extra because the continue-header
262 Slot::HEADER_SIZE + SLOT_SIZE * 2 + 2,
263 );
264 assert_eq!(slot.next_slot::<SLOT_SIZE, SLOT_COUNT>(), 3);
265 }
266}