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
275 .borrow()
276 .get(slot)
277 .map(|s| s.is_some())
278 .unwrap_or(false)
279 }
280
281 fn delete_slot(&self, slot: usize) -> Result<(), SaveError> {
282 let mut slots = self.slots.borrow_mut();
283 if slot >= slots.len() {
284 return Err(SaveError::IoError(format!(
285 "slot index {} out of range (max {})",
286 slot,
287 slots.len() - 1
288 )));
289 }
290 slots[slot] = None;
291 Ok(())
292 }
293}
294
295#[cfg(test)]
300mod tests {
301 use super::*;
302
303 #[derive(Debug, Clone, PartialEq, Eq)]
308 struct MockSave {
309 player_name: String,
310 level: u8,
311 gold: u32,
312 }
313
314 impl MockSave {
315 const NAME_LEN: usize = 16;
316 }
317
318 impl SaveData for MockSave {
319 fn serialize(&self) -> Vec<u8> {
320 let mut v = Vec::with_capacity(Self::NAME_LEN + 1 + 4);
321 let name_bytes = self.player_name.as_bytes();
322 let copy_len = name_bytes.len().min(Self::NAME_LEN);
323 v.extend_from_slice(&name_bytes[..copy_len]);
324 v.resize(Self::NAME_LEN, 0);
326 v.push(self.level);
327 v.extend_from_slice(&self.gold.to_le_bytes());
328 v
329 }
330
331 fn deserialize(data: &[u8]) -> Result<Self, SaveError> {
332 if data.len() < Self::NAME_LEN + 1 + 4 {
333 return Err(SaveError::InvalidData);
334 }
335 let name_bytes = &data[..Self::NAME_LEN];
336 let name_end = name_bytes
338 .iter()
339 .position(|&b| b == 0)
340 .unwrap_or(Self::NAME_LEN);
341 let player_name = String::from_utf8(name_bytes[..name_end].to_vec())
342 .map_err(|_| SaveError::InvalidData)?;
343 let level = data[Self::NAME_LEN];
344 let gold_start = Self::NAME_LEN + 1;
345 let gold = u32::from_le_bytes([
346 data[gold_start],
347 data[gold_start + 1],
348 data[gold_start + 2],
349 data[gold_start + 3],
350 ]);
351 Ok(MockSave {
352 player_name,
353 level,
354 gold,
355 })
356 }
357
358 fn save_size() -> usize {
359 Self::NAME_LEN + 1 + 4 + 2
361 }
362 }
363
364 #[test]
369 fn test_crc16_known_value() {
370 let data = b"123456789";
372 assert_eq!(crc16_xmodem(data), 0x31C3);
373 }
374
375 #[test]
376 fn test_crc16_empty() {
377 assert_eq!(crc16_xmodem(b""), 0x0000);
378 }
379
380 #[test]
385 fn test_save_and_load_roundtrip() {
386 let storage = Box::new(InMemoryStorage::new());
387 let manager = SaveManager::<MockSave>::new(storage);
388
389 let original = MockSave {
390 player_name: "Ash".to_string(),
391 level: 42,
392 gold: 9999,
393 };
394
395 manager
397 .save(SaveSlot::Slot1, &original)
398 .expect("save should succeed");
399
400 let loaded = manager.load(SaveSlot::Slot1).expect("load should succeed");
402
403 assert_eq!(loaded, original, "loaded data should match original");
404 }
405
406 #[test]
407 fn test_save_and_load_multiple_slots() {
408 let storage = Box::new(InMemoryStorage::new());
409 let manager = SaveManager::<MockSave>::new(storage);
410
411 let save1 = MockSave {
412 player_name: "Red".to_string(),
413 level: 50,
414 gold: 5000,
415 };
416 let save2 = MockSave {
417 player_name: "Blue".to_string(),
418 level: 48,
419 gold: 4800,
420 };
421 let save3 = MockSave {
422 player_name: "Green".to_string(),
423 level: 55,
424 gold: 5500,
425 };
426
427 manager.save(SaveSlot::Slot1, &save1).unwrap();
428 manager.save(SaveSlot::Slot2, &save2).unwrap();
429 manager.save(SaveSlot::Slot3, &save3).unwrap();
430
431 assert_eq!(manager.load(SaveSlot::Slot1).unwrap(), save1);
432 assert_eq!(manager.load(SaveSlot::Slot2).unwrap(), save2);
433 assert_eq!(manager.load(SaveSlot::Slot3).unwrap(), save3);
434 }
435
436 #[test]
441 fn test_load_empty_slot_returns_error() {
442 let storage = Box::new(InMemoryStorage::new());
443 let manager = SaveManager::<MockSave>::new(storage);
444
445 let result = manager.load(SaveSlot::Slot1);
446 assert!(result.is_err());
447 assert_eq!(result.unwrap_err(), SaveError::SlotEmpty);
448 }
449
450 #[test]
455 fn test_corrupted_data_returns_checksum_error() {
456 let storage = Box::new(InMemoryStorage::new());
457
458 let mock = MockSave {
460 player_name: "Ash".to_string(),
461 level: 10,
462 gold: 100,
463 };
464 let raw = mock.serialize();
465 let mut payload = raw.clone();
466 let wrong_cksum: u16 = 0xFFFF;
468 payload.extend_from_slice(&wrong_cksum.to_le_bytes());
469 storage.write(0, &payload).unwrap();
470
471 let manager = SaveManager::<MockSave>::new(storage);
472 let result = manager.load(SaveSlot::Slot1);
473 assert!(result.is_err());
474 assert_eq!(result.unwrap_err(), SaveError::InvalidChecksum);
475 }
476
477 #[test]
482 fn test_list_slots() {
483 let storage = Box::new(InMemoryStorage::new());
484 let manager = SaveManager::<MockSave>::new(storage);
485
486 let slots = manager.list_slots();
487 assert_eq!(slots.len(), 3);
488 for (_slot, has_data) in &slots {
489 assert!(!has_data, "all slots should be empty initially");
490 }
491
492 let mock = MockSave {
493 player_name: "Test".to_string(),
494 level: 1,
495 gold: 0,
496 };
497 manager.save(SaveSlot::Slot2, &mock).unwrap();
498
499 let slots = manager.list_slots();
500 assert!(!slots[0].1, "slot 1 should still be empty");
501 assert!(slots[1].1, "slot 2 should have data");
502 assert!(!slots[2].1, "slot 3 should still be empty");
503 }
504
505 #[test]
510 fn test_delete_slot() {
511 let storage = Box::new(InMemoryStorage::new());
512 let manager = SaveManager::<MockSave>::new(storage);
513
514 let mock = MockSave {
515 player_name: "Del".to_string(),
516 level: 7,
517 gold: 77,
518 };
519 manager.save(SaveSlot::Slot1, &mock).unwrap();
520 assert!(manager.list_slots()[0].1, "slot 1 should have data");
521
522 manager.delete(SaveSlot::Slot1).unwrap();
523 assert!(
524 !manager.list_slots()[0].1,
525 "slot 1 should be empty after delete"
526 );
527
528 let result = manager.load(SaveSlot::Slot1);
529 assert_eq!(result.unwrap_err(), SaveError::SlotEmpty);
530 }
531
532 #[test]
537 fn test_save_slot_indices() {
538 assert_eq!(SaveSlot::Slot1.index(), 0);
539 assert_eq!(SaveSlot::Slot2.index(), 1);
540 assert_eq!(SaveSlot::Slot3.index(), 2);
541 }
542
543 #[test]
544 fn test_save_slot_all() {
545 let all = SaveSlot::all();
546 assert_eq!(all.len(), 3);
547 assert_eq!(all[0], SaveSlot::Slot1);
548 assert_eq!(all[1], SaveSlot::Slot2);
549 assert_eq!(all[2], SaveSlot::Slot3);
550 }
551
552 #[test]
557 fn test_overwrite_slot() {
558 let storage = Box::new(InMemoryStorage::new());
559 let manager = SaveManager::<MockSave>::new(storage);
560
561 let first = MockSave {
562 player_name: "First".to_string(),
563 level: 10,
564 gold: 100,
565 };
566 manager.save(SaveSlot::Slot1, &first).unwrap();
567
568 let second = MockSave {
569 player_name: "Second".to_string(),
570 level: 20,
571 gold: 200,
572 };
573 manager.save(SaveSlot::Slot1, &second).unwrap();
574
575 let loaded = manager.load(SaveSlot::Slot1).unwrap();
576 assert_eq!(loaded, second, "overwritten slot should return new data");
577 assert_ne!(loaded, first);
578 }
579}