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:
@@ -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(×tamp_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)
|
||||
|
||||
Reference in New Issue
Block a user