1use std::cell::RefCell;
31
32#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
34pub enum SaveError {
35 #[error("I/O error: {0}")]
37 IoError(String),
38
39 #[error("invalid checksum — data may be corrupted")]
41 InvalidChecksum,
42
43 #[error("invalid data")]
45 InvalidData,
46
47 #[error("slot is empty")]
49 SlotEmpty,
50
51 #[error("slot is full")]
53 SlotFull,
54}
55
56#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
60pub enum SaveSlot {
61 Slot1,
62 Slot2,
63 Slot3,
64}
65
66impl SaveSlot {
67 pub fn index(&self) -> usize {
69 match self {
70 SaveSlot::Slot1 => 0,
71 SaveSlot::Slot2 => 1,
72 SaveSlot::Slot3 => 2,
73 }
74 }
75
76 pub fn all() -> Vec<SaveSlot> {
78 vec![SaveSlot::Slot1, SaveSlot::Slot2, SaveSlot::Slot3]
79 }
80}
81
82pub trait SaveStorage {
88 fn write(&self, slot: usize, data: &[u8]) -> Result<(), SaveError>;
90
91 fn read(&self, slot: usize) -> Result<Vec<u8>, SaveError>;
95
96 fn slot_exists(&self, slot: usize) -> bool;
98
99 fn delete_slot(&self, slot: usize) -> Result<(), SaveError>;
101}
102
103pub trait SaveData: Sized {
108 fn serialize(&self) -> Vec<u8>;
113
114 fn deserialize(data: &[u8]) -> Result<Self, SaveError>;
119
120 fn checksum(data: &[u8]) -> u16 {
125 crc16_xmodem(data)
126 }
127
128 fn save_size() -> usize;
132
133 fn validate(data: &[u8]) -> bool {
138 if data.len() < 2 {
139 return false;
140 }
141 let (game_data, cksum_bytes) = data.split_at(data.len() - 2);
142 let stored_checksum = u16::from_le_bytes([cksum_bytes[0], cksum_bytes[1]]);
143 Self::checksum(game_data) == stored_checksum
144 }
145}
146
147pub struct SaveManager<S: SaveData> {
151 storage: Box<dyn SaveStorage>,
152 _phantom: std::marker::PhantomData<S>,
153}
154
155impl<S: SaveData> SaveManager<S> {
156 pub fn new(storage: Box<dyn SaveStorage>) -> Self {
158 Self {
159 storage,
160 _phantom: std::marker::PhantomData,
161 }
162 }
163
164 pub fn save(&self, slot: SaveSlot, data: &S) -> Result<(), SaveError> {
168 let raw = data.serialize();
169 let cksum = S::checksum(&raw);
170 let mut payload = raw;
171 payload.extend_from_slice(&cksum.to_le_bytes());
172 self.storage.write(slot.index(), &payload)
173 }
174
175 pub fn load(&self, slot: SaveSlot) -> Result<S, SaveError> {
179 let payload = self.storage.read(slot.index())?;
180 if !S::validate(&payload) {
181 return Err(SaveError::InvalidChecksum);
182 }
183 let game_data = &payload[..payload.len() - 2];
184 S::deserialize(game_data)
185 }
186
187 pub fn list_slots(&self) -> Vec<(SaveSlot, bool)> {
189 SaveSlot::all()
190 .into_iter()
191 .map(|slot| (slot, self.storage.slot_exists(slot.index())))
192 .collect()
193 }
194
195 pub fn delete(&self, slot: SaveSlot) -> Result<(), SaveError> {
197 self.storage.delete_slot(slot.index())
198 }
199}
200
201pub fn crc16_xmodem(data: &[u8]) -> u16 {
207 let mut crc: u16 = 0;
208 for &byte in data {
209 crc ^= (byte as u16) << 8;
210 for _ in 0..8 {
211 if crc & 0x8000 != 0 {
212 crc = (crc << 1) ^ 0x1021;
213 } else {
214 crc <<= 1;
215 }
216 }
217 }
218 crc
219}
220
221pub struct InMemoryStorage {
229 slots: RefCell<Vec<Option<Vec<u8>>>>,
230}
231
232impl InMemoryStorage {
233 pub fn new() -> Self {
235 Self {
236 slots: RefCell::new(vec![None, None, None]),
237 }
238 }
239}
240
241impl Default for InMemoryStorage {
242 fn default() -> Self {
243 Self::new()
244 }
245}
246
247impl SaveStorage for InMemoryStorage {
248 fn write(&self, slot: usize, data: &[u8]) -> Result<(), SaveError> {
249 let mut slots = self.slots.borrow_mut();
250 if slot >= slots.len() {
251 return Err(SaveError::IoError(format!(
252 "slot index {} out of range (max {})",
253 slot,
254 slots.len() - 1
255 )));
256 }
257 slots[slot] = Some(data.to_vec());
258 Ok(())
259 }
260
261 fn read(&self, slot: usize) -> Result<Vec<u8>, SaveError> {
262 let slots = self.slots.borrow();
263 if slot >= slots.len() {
264 return Err(SaveError::IoError(format!(
265 "slot index {} out of range (max {})",
266 slot,
267 slots.len() - 1
268 )));
269 }
270 slots[slot].clone().ok_or(SaveError::SlotEmpty)
271 }
272
273 fn slot_exists(&self, slot: usize) -> bool {
274 self.slots.borrow().get(slot).map(|s| s.is_some()).unwrap_or(false)
275 }
276
277 fn delete_slot(&self, slot: usize) -> Result<(), SaveError> {
278 let mut slots = self.slots.borrow_mut();
279 if slot >= slots.len() {
280 return Err(SaveError::IoError(format!(
281 "slot index {} out of range (max {})",
282 slot,
283 slots.len() - 1
284 )));
285 }
286 slots[slot] = None;
287 Ok(())
288 }
289}
290
291#[cfg(test)]
296mod tests {
297 use super::*;
298
299 #[derive(Debug, Clone, PartialEq, Eq)]
304 struct MockSave {
305 player_name: String,
306 level: u8,
307 gold: u32,
308 }
309
310 impl MockSave {
311 const NAME_LEN: usize = 16;
312 }
313
314 impl SaveData for MockSave {
315 fn serialize(&self) -> Vec<u8> {
316 let mut v = Vec::with_capacity(Self::NAME_LEN + 1 + 4);
317 let name_bytes = self.player_name.as_bytes();
318 let copy_len = name_bytes.len().min(Self::NAME_LEN);
319 v.extend_from_slice(&name_bytes[..copy_len]);
320 v.resize(Self::NAME_LEN, 0);
322 v.push(self.level);
323 v.extend_from_slice(&self.gold.to_le_bytes());
324 v
325 }
326
327 fn deserialize(data: &[u8]) -> Result<Self, SaveError> {
328 if data.len() < Self::NAME_LEN + 1 + 4 {
329 return Err(SaveError::InvalidData);
330 }
331 let name_bytes = &data[..Self::NAME_LEN];
332 let name_end = name_bytes.iter().position(|&b| b == 0).unwrap_or(Self::NAME_LEN);
334 let player_name = String::from_utf8(name_bytes[..name_end].to_vec())
335 .map_err(|_| SaveError::InvalidData)?;
336 let level = data[Self::NAME_LEN];
337 let gold_start = Self::NAME_LEN + 1;
338 let gold = u32::from_le_bytes([
339 data[gold_start],
340 data[gold_start + 1],
341 data[gold_start + 2],
342 data[gold_start + 3],
343 ]);
344 Ok(MockSave { player_name, level, gold })
345 }
346
347 fn save_size() -> usize {
348 Self::NAME_LEN + 1 + 4 + 2
350 }
351 }
352
353 #[test]
358 fn test_crc16_known_value() {
359 let data = b"123456789";
361 assert_eq!(crc16_xmodem(data), 0x31C3);
362 }
363
364 #[test]
365 fn test_crc16_empty() {
366 assert_eq!(crc16_xmodem(b""), 0x0000);
367 }
368
369 #[test]
374 fn test_save_and_load_roundtrip() {
375 let storage = Box::new(InMemoryStorage::new());
376 let manager = SaveManager::<MockSave>::new(storage);
377
378 let original = MockSave {
379 player_name: "Ash".to_string(),
380 level: 42,
381 gold: 9999,
382 };
383
384 manager.save(SaveSlot::Slot1, &original).expect("save should succeed");
386
387 let loaded = manager.load(SaveSlot::Slot1).expect("load should succeed");
389
390 assert_eq!(loaded, original, "loaded data should match original");
391 }
392
393 #[test]
394 fn test_save_and_load_multiple_slots() {
395 let storage = Box::new(InMemoryStorage::new());
396 let manager = SaveManager::<MockSave>::new(storage);
397
398 let save1 = MockSave { player_name: "Red".to_string(), level: 50, gold: 5000 };
399 let save2 = MockSave { player_name: "Blue".to_string(), level: 48, gold: 4800 };
400 let save3 = MockSave { player_name: "Green".to_string(), level: 55, gold: 5500 };
401
402 manager.save(SaveSlot::Slot1, &save1).unwrap();
403 manager.save(SaveSlot::Slot2, &save2).unwrap();
404 manager.save(SaveSlot::Slot3, &save3).unwrap();
405
406 assert_eq!(manager.load(SaveSlot::Slot1).unwrap(), save1);
407 assert_eq!(manager.load(SaveSlot::Slot2).unwrap(), save2);
408 assert_eq!(manager.load(SaveSlot::Slot3).unwrap(), save3);
409 }
410
411 #[test]
416 fn test_load_empty_slot_returns_error() {
417 let storage = Box::new(InMemoryStorage::new());
418 let manager = SaveManager::<MockSave>::new(storage);
419
420 let result = manager.load(SaveSlot::Slot1);
421 assert!(result.is_err());
422 assert_eq!(result.unwrap_err(), SaveError::SlotEmpty);
423 }
424
425 #[test]
430 fn test_corrupted_data_returns_checksum_error() {
431 let storage = Box::new(InMemoryStorage::new());
432
433 let mock = MockSave { player_name: "Ash".to_string(), level: 10, gold: 100 };
435 let raw = mock.serialize();
436 let mut payload = raw.clone();
437 let wrong_cksum: u16 = 0xFFFF;
439 payload.extend_from_slice(&wrong_cksum.to_le_bytes());
440 storage.write(0, &payload).unwrap();
441
442 let manager = SaveManager::<MockSave>::new(storage);
443 let result = manager.load(SaveSlot::Slot1);
444 assert!(result.is_err());
445 assert_eq!(result.unwrap_err(), SaveError::InvalidChecksum);
446 }
447
448 #[test]
453 fn test_list_slots() {
454 let storage = Box::new(InMemoryStorage::new());
455 let manager = SaveManager::<MockSave>::new(storage);
456
457 let slots = manager.list_slots();
458 assert_eq!(slots.len(), 3);
459 for (_slot, has_data) in &slots {
460 assert!(!has_data, "all slots should be empty initially");
461 }
462
463 let mock = MockSave { player_name: "Test".to_string(), level: 1, gold: 0 };
464 manager.save(SaveSlot::Slot2, &mock).unwrap();
465
466 let slots = manager.list_slots();
467 assert!(!slots[0].1, "slot 1 should still be empty");
468 assert!(slots[1].1, "slot 2 should have data");
469 assert!(!slots[2].1, "slot 3 should still be empty");
470 }
471
472 #[test]
477 fn test_delete_slot() {
478 let storage = Box::new(InMemoryStorage::new());
479 let manager = SaveManager::<MockSave>::new(storage);
480
481 let mock = MockSave { player_name: "Del".to_string(), level: 7, gold: 77 };
482 manager.save(SaveSlot::Slot1, &mock).unwrap();
483 assert!(manager.list_slots()[0].1, "slot 1 should have data");
484
485 manager.delete(SaveSlot::Slot1).unwrap();
486 assert!(!manager.list_slots()[0].1, "slot 1 should be empty after delete");
487
488 let result = manager.load(SaveSlot::Slot1);
489 assert_eq!(result.unwrap_err(), SaveError::SlotEmpty);
490 }
491
492 #[test]
497 fn test_save_slot_indices() {
498 assert_eq!(SaveSlot::Slot1.index(), 0);
499 assert_eq!(SaveSlot::Slot2.index(), 1);
500 assert_eq!(SaveSlot::Slot3.index(), 2);
501 }
502
503 #[test]
504 fn test_save_slot_all() {
505 let all = SaveSlot::all();
506 assert_eq!(all.len(), 3);
507 assert_eq!(all[0], SaveSlot::Slot1);
508 assert_eq!(all[1], SaveSlot::Slot2);
509 assert_eq!(all[2], SaveSlot::Slot3);
510 }
511
512 #[test]
517 fn test_overwrite_slot() {
518 let storage = Box::new(InMemoryStorage::new());
519 let manager = SaveManager::<MockSave>::new(storage);
520
521 let first = MockSave { player_name: "First".to_string(), level: 10, gold: 100 };
522 manager.save(SaveSlot::Slot1, &first).unwrap();
523
524 let second = MockSave { player_name: "Second".to_string(), level: 20, gold: 200 };
525 manager.save(SaveSlot::Slot1, &second).unwrap();
526
527 let loaded = manager.load(SaveSlot::Slot1).unwrap();
528 assert_eq!(loaded, second, "overwritten slot should return new data");
529 assert_ne!(loaded, first);
530 }
531}