Switch savegame serialization format from Bincode to custom parsing

- Replaced Bincode-based serialization/deserialization with a custom save format for better control.
- Introduced save header with magic bytes, timestamp handling, and UTF-8 validation.
- Enhanced error handling for save parsing and increased format flexibility.
- Removed
This commit is contained in:
2026-04-26 20:31:56 +02:00
parent fc0e18da56
commit 097aff5360

View File

@@ -1,5 +1,4 @@
use alloc::vec::Vec;
use bincode::{Decode, Encode};
use embedded_savegame::storage::{Flash, Storage};
use embedded_storage::nor_flash::{NorFlash, ReadNorFlash};
use esp_bootloader_esp_idf::partitions::{Error as PartitionError, FlashRegion};
@@ -25,13 +24,44 @@ pub struct SaveInfo {
pub created_at: Option<alloc::string::String>,
}
/// Wrapper that includes both the config data and metadata like creation timestamp.
#[derive(Serialize, Debug, Encode, Decode)]
struct SaveWrapper {
/// UTC timestamp in RFC3339 format
created_at: alloc::string::String,
/// Raw config JSON data
data: Vec<u8>,
const SAVE_MAGIC: [u8; 4] = *b"SGM1";
const SAVE_HEADER_LEN: usize = 6; // magic (4) + timestamp_len (u16)
struct ParsedSave<'a> {
created_at: &'a str,
data: &'a [u8],
}
fn parse_save(data: &[u8]) -> FatResult<ParsedSave<'_>> {
if data.len() < SAVE_HEADER_LEN {
return Err(FatError::String {
error: "Save payload too short".into(),
});
}
if data[..4] != SAVE_MAGIC {
return Err(FatError::String {
error: "Save payload has invalid magic".into(),
});
}
let timestamp_len = u16::from_le_bytes([data[4], data[5]]) as usize;
let timestamp_end = SAVE_HEADER_LEN + timestamp_len;
if timestamp_end > data.len() {
return Err(FatError::String {
error: "Save payload timestamp length exceeds data".into(),
});
}
let created_at = core::str::from_utf8(&data[SAVE_HEADER_LEN..timestamp_end]).map_err(|e| {
FatError::String {
error: alloc::format!("Save payload contains invalid timestamp UTF-8: {e:?}"),
}
})?;
Ok(ParsedSave {
created_at,
data: &data[timestamp_end..],
})
}
// ── Flash adapter ──────────────────────────────────────────────────────────────
@@ -134,11 +164,21 @@ impl SavegameManager {
/// slot before `append()` writes to the next free one.
/// Both operations are performed atomically on the same Storage instance.
pub fn save(&mut self, data: &[u8], timestamp: &str) -> FatResult<()> {
let wrapper = SaveWrapper {
created_at: alloc::string::String::from(timestamp),
data: data.to_vec(),
};
let mut serialized = bincode::encode_to_vec(&wrapper, bincode::config::standard())?;
let timestamp_bytes = timestamp.as_bytes();
let timestamp_len: u16 =
timestamp_bytes
.len()
.try_into()
.map_err(|_| FatError::String {
error: "Timestamp too long for save header".into(),
})?;
let mut serialized =
Vec::with_capacity(SAVE_HEADER_LEN + timestamp_bytes.len() + data.len());
serialized.extend_from_slice(&SAVE_MAGIC);
serialized.extend_from_slice(&timestamp_len.to_le_bytes());
serialized.extend_from_slice(timestamp_bytes);
serialized.extend_from_slice(data);
// Flash storage often requires length to be a multiple of 4.
let padding = (4 - (serialized.len() % 4)) % 4;
@@ -169,12 +209,8 @@ impl SavegameManager {
match self.storage.read(idx, &mut buf)? {
None => Ok(None),
Some(data) => {
// Try to deserialize as SaveWrapper (new Bincode format)
let (wrapper, _) = bincode::decode_from_slice::<SaveWrapper, _>(
data,
bincode::config::standard(),
)?;
Ok(Some(wrapper.data))
let parsed = parse_save(data)?;
Ok(Some(parsed.data.to_vec()))
}
}
}
@@ -192,17 +228,22 @@ impl SavegameManager {
let mut buf = alloc::vec![0u8; SAVEGAME_SLOT_SIZE];
for idx in 0..SAVEGAME_SLOT_COUNT {
if let Some(data) = self.storage.read(idx, &mut buf)? {
// Try to deserialize as SaveWrapper (new Bincode format)
let (wrapper, _) = bincode::decode_from_slice::<SaveWrapper, _>(
data,
bincode::config::standard(),
)?;
saves.push(SaveInfo {
idx,
len: wrapper.data.len() as u32,
created_at: Some(wrapper.created_at),
});
match parse_save(data) {
Ok(save) => {
saves.push(SaveInfo {
idx,
len: save.data.len() as u32,
created_at: Some(alloc::string::String::from(save.created_at)),
});
}
Err(err) => {
saves.push(SaveInfo {
idx,
len: 0,
created_at: None,
});
}
}
}
}
Ok(saves)