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}