From 097aff5360171f0824a22fffb3d302ba619d7074 Mon Sep 17 00:00:00 2001 From: Empire Phoenix Date: Sun, 26 Apr 2026 20:31:56 +0200 Subject: [PATCH] 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 --- .../rust/src/hal/savegame_manager.rs | 101 ++++++++++++------ 1 file changed, 71 insertions(+), 30 deletions(-) diff --git a/Software/MainBoard/rust/src/hal/savegame_manager.rs b/Software/MainBoard/rust/src/hal/savegame_manager.rs index 92ff03f..04aeb15 100644 --- a/Software/MainBoard/rust/src/hal/savegame_manager.rs +++ b/Software/MainBoard/rust/src/hal/savegame_manager.rs @@ -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, } -/// 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, +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> { + 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::( - 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::( - 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)