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