use crate::fs::{FsError, VirtualFS}; use std::collections::HashMap; pub const MEMCARD_SLOT_COUNT: usize = 32; pub const MEMCARD_SLOT_CAPACITY_BYTES: usize = 32 * 1024; const SLOT_FILE_MAGIC: &[u8; 4] = b"PMMS"; const SLOT_FILE_VERSION: u8 = 1; const SLOT_HEADER_SIZE: usize = 4 + 1 + 16 + 8 + 4 + 4; #[derive(Debug, Clone, Copy, PartialEq, Eq)] #[repr(i32)] pub enum MemcardStatus { Ok = 0, Empty = 1, NotFound = 2, NoSpace = 3, AccessDenied = 4, Corrupt = 5, Conflict = 6, Unavailable = 7, InvalidState = 8, } #[derive(Debug, Clone, Copy, PartialEq, Eq)] #[repr(i32)] pub enum MemcardSlotState { Empty = 0, Staged = 1, Committed = 2, Corrupt = 3, } #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub struct MemcardSlotStat { pub state: MemcardSlotState, pub used_bytes: u32, pub generation: u64, pub checksum: u32, } pub struct MemcardReadResult { pub status: MemcardStatus, pub bytes: Vec, pub bytes_read: u32, } pub struct MemcardWriteResult { pub status: MemcardStatus, pub bytes_written: u32, } #[derive(Debug, Clone)] struct SlotImage { payload: Vec, generation: u64, checksum: u32, save_uuid: [u8; 16], } #[derive(Default)] pub struct MemcardService { staged: HashMap<(u32, u8), Vec>, } impl MemcardService { pub fn new() -> Self { Self::default() } pub fn clear_all_staging(&mut self) { self.staged.clear(); } pub fn slot_count(&self) -> usize { MEMCARD_SLOT_COUNT } pub fn slot_stat(&self, fs: &VirtualFS, app_id: u32, slot: u8) -> MemcardSlotStat { if let Some(staged_payload) = self.staged.get(&(app_id, slot)) { let generation = match self.load_committed(fs, app_id, slot) { Ok(Some(committed)) => committed.generation.saturating_add(1), _ => 1, }; return MemcardSlotStat { state: MemcardSlotState::Staged, used_bytes: staged_payload.len() as u32, generation, checksum: checksum32(staged_payload), }; } match self.load_committed(fs, app_id, slot) { Ok(Some(committed)) => MemcardSlotStat { state: MemcardSlotState::Committed, used_bytes: committed.payload.len() as u32, generation: committed.generation, checksum: committed.checksum, }, Ok(None) => MemcardSlotStat { state: MemcardSlotState::Empty, used_bytes: 0, generation: 0, checksum: 0, }, Err(MemcardStatus::Corrupt) => MemcardSlotStat { state: MemcardSlotState::Corrupt, used_bytes: 0, generation: 0, checksum: 0, }, Err(_) => MemcardSlotStat { state: MemcardSlotState::Empty, used_bytes: 0, generation: 0, checksum: 0, }, } } pub fn slot_read( &self, fs: &VirtualFS, app_id: u32, slot: u8, offset: usize, max_bytes: usize, ) -> MemcardReadResult { if let Some(staged_payload) = self.staged.get(&(app_id, slot)) { return Self::slice_payload(staged_payload, offset, max_bytes); } match self.load_committed(fs, app_id, slot) { Ok(Some(committed)) => Self::slice_payload(&committed.payload, offset, max_bytes), Ok(None) => { MemcardReadResult { status: MemcardStatus::Empty, bytes: Vec::new(), bytes_read: 0 } } Err(status) => MemcardReadResult { status, bytes: Vec::new(), bytes_read: 0 }, } } pub fn slot_write( &mut self, fs: &VirtualFS, app_id: u32, slot: u8, offset: usize, data: &[u8], ) -> MemcardWriteResult { let end = match offset.checked_add(data.len()) { Some(v) => v, None => { return MemcardWriteResult { status: MemcardStatus::NoSpace, bytes_written: 0 }; } }; if end > MEMCARD_SLOT_CAPACITY_BYTES { return MemcardWriteResult { status: MemcardStatus::NoSpace, bytes_written: 0 }; } let mut payload = if let Some(staged_payload) = self.staged.get(&(app_id, slot)) { staged_payload.clone() } else { match self.load_committed(fs, app_id, slot) { Ok(Some(committed)) => committed.payload, Ok(None) => Vec::new(), Err(status) => { return MemcardWriteResult { status, bytes_written: 0 }; } } }; if offset > payload.len() { payload.resize(offset, 0); } if end > payload.len() { payload.resize(end, 0); } payload[offset..end].copy_from_slice(data); self.staged.insert((app_id, slot), payload); MemcardWriteResult { status: MemcardStatus::Ok, bytes_written: data.len() as u32 } } pub fn slot_commit(&mut self, fs: &mut VirtualFS, app_id: u32, slot: u8) -> MemcardStatus { let Some(staged_payload) = self.staged.get(&(app_id, slot)).cloned() else { return MemcardStatus::InvalidState; }; let (save_uuid, generation) = match self.load_committed(fs, app_id, slot) { Ok(Some(committed)) => (committed.save_uuid, committed.generation.saturating_add(1)), Ok(None) => (make_save_uuid(app_id, slot), 1), Err(status) => return status, }; let checksum = checksum32(&staged_payload); let encoded = encode_slot_file(SlotImage { payload: staged_payload, generation, checksum, save_uuid, }); let path = slot_path(app_id, slot); match fs.write_file(&path, &encoded) { Ok(()) => { self.staged.remove(&(app_id, slot)); MemcardStatus::Ok } Err(err) => map_fs_error(err), } } pub fn slot_clear(&mut self, fs: &mut VirtualFS, app_id: u32, slot: u8) -> MemcardStatus { self.staged.remove(&(app_id, slot)); let path = slot_path(app_id, slot); match fs.delete(&path) { Ok(()) => MemcardStatus::Ok, Err(FsError::NotFound) => MemcardStatus::Empty, Err(err) => map_fs_error(err), } } fn slice_payload(payload: &[u8], offset: usize, max_bytes: usize) -> MemcardReadResult { if offset >= payload.len() || max_bytes == 0 { return MemcardReadResult { status: MemcardStatus::Ok, bytes: Vec::new(), bytes_read: 0, }; } let end = payload.len().min(offset.saturating_add(max_bytes)); let bytes = payload[offset..end].to_vec(); MemcardReadResult { status: MemcardStatus::Ok, bytes_read: bytes.len() as u32, bytes } } fn load_committed( &self, fs: &VirtualFS, app_id: u32, slot: u8, ) -> Result, MemcardStatus> { let path = slot_path(app_id, slot); match fs.read_file(&path) { Ok(bytes) => decode_slot_file(&bytes).map(Some), Err(FsError::NotFound) => Ok(None), Err(err) => Err(map_fs_error(err)), } } } fn slot_path(app_id: u32, slot: u8) -> String { format!("/user/games/{}/memcard/slot_{:02}.pmem", app_id, slot) } fn map_fs_error(err: FsError) -> MemcardStatus { match err { FsError::NotFound => MemcardStatus::NotFound, FsError::AlreadyExists => MemcardStatus::Conflict, FsError::PermissionDenied => MemcardStatus::AccessDenied, FsError::NotMounted | FsError::IOError(_) | FsError::Other(_) | FsError::InvalidPath(_) => { MemcardStatus::Unavailable } } } fn checksum32(data: &[u8]) -> u32 { let mut a: u32 = 1; let mut b: u32 = 0; const MOD: u32 = 65521; for &byte in data { a = (a + byte as u32) % MOD; b = (b + a) % MOD; } (b << 16) | a } fn make_save_uuid(app_id: u32, slot: u8) -> [u8; 16] { let mut out = [0u8; 16]; out[0..4].copy_from_slice(&app_id.to_le_bytes()); out[4] = slot; out[5..13].copy_from_slice( &(std::time::SystemTime::now() .duration_since(std::time::UNIX_EPOCH) .map(|d| d.as_nanos() as u64) .unwrap_or(0)) .to_le_bytes(), ); out[13] = 0x50; out[14] = 0x4D; out[15] = 0x31; out } fn encode_slot_file(slot: SlotImage) -> Vec { let mut out = Vec::with_capacity(SLOT_HEADER_SIZE + slot.payload.len()); out.extend_from_slice(SLOT_FILE_MAGIC); out.push(SLOT_FILE_VERSION); out.extend_from_slice(&slot.save_uuid); out.extend_from_slice(&slot.generation.to_le_bytes()); out.extend_from_slice(&slot.checksum.to_le_bytes()); out.extend_from_slice(&(slot.payload.len() as u32).to_le_bytes()); out.extend_from_slice(&slot.payload); out } fn decode_slot_file(bytes: &[u8]) -> Result { if bytes.len() < SLOT_HEADER_SIZE { return Err(MemcardStatus::Corrupt); } if &bytes[0..4] != SLOT_FILE_MAGIC { return Err(MemcardStatus::Corrupt); } if bytes[4] != SLOT_FILE_VERSION { return Err(MemcardStatus::Corrupt); } let mut save_uuid = [0u8; 16]; save_uuid.copy_from_slice(&bytes[5..21]); let generation = u64::from_le_bytes(bytes[21..29].try_into().map_err(|_| MemcardStatus::Corrupt)?); let checksum = u32::from_le_bytes(bytes[29..33].try_into().map_err(|_| MemcardStatus::Corrupt)?); let payload_size = u32::from_le_bytes(bytes[33..37].try_into().map_err(|_| MemcardStatus::Corrupt)?) as usize; if payload_size > MEMCARD_SLOT_CAPACITY_BYTES { return Err(MemcardStatus::Corrupt); } if bytes.len() != SLOT_HEADER_SIZE + payload_size { return Err(MemcardStatus::Corrupt); } let payload = bytes[SLOT_HEADER_SIZE..].to_vec(); if checksum32(&payload) != checksum { return Err(MemcardStatus::Corrupt); } Ok(SlotImage { payload, generation, checksum, save_uuid }) } #[cfg(test)] mod tests { use super::*; use crate::fs::{FsBackend, FsEntry}; use std::collections::HashMap; struct MockBackend { files: HashMap>, } impl MockBackend { fn new() -> Self { Self { files: HashMap::new() } } } impl FsBackend for MockBackend { fn mount(&mut self) -> Result<(), FsError> { Ok(()) } fn unmount(&mut self) {} fn list_dir(&self, _path: &str) -> Result, FsError> { Ok(Vec::new()) } fn read_file(&self, path: &str) -> Result, FsError> { self.files.get(path).cloned().ok_or(FsError::NotFound) } fn write_file(&mut self, path: &str, data: &[u8]) -> Result<(), FsError> { self.files.insert(path.to_string(), data.to_vec()); Ok(()) } fn delete(&mut self, path: &str) -> Result<(), FsError> { self.files.remove(path).map(|_| ()).ok_or(FsError::NotFound) } fn exists(&self, path: &str) -> bool { self.files.contains_key(path) } fn is_healthy(&self) -> bool { true } } #[test] fn slot_roundtrip_commit_and_generation() { let mut fs = VirtualFS::new(); fs.mount(Box::new(MockBackend::new())).expect("mount"); let mut mem = MemcardService::new(); let write = mem.slot_write(&fs, 10, 0, 0, b"hello"); assert_eq!(write.status, MemcardStatus::Ok); assert_eq!(write.bytes_written, 5); let staged = mem.slot_stat(&fs, 10, 0); assert_eq!(staged.state, MemcardSlotState::Staged); assert_eq!(staged.generation, 1); assert_eq!(mem.slot_commit(&mut fs, 10, 0), MemcardStatus::Ok); let committed = mem.slot_stat(&fs, 10, 0); assert_eq!(committed.state, MemcardSlotState::Committed); assert_eq!(committed.generation, 1); let write2 = mem.slot_write(&fs, 10, 0, 5, b"!"); assert_eq!(write2.status, MemcardStatus::Ok); assert_eq!(mem.slot_commit(&mut fs, 10, 0), MemcardStatus::Ok); let committed2 = mem.slot_stat(&fs, 10, 0); assert_eq!(committed2.generation, 2); let read = mem.slot_read(&fs, 10, 0, 0, 10); assert_eq!(read.status, MemcardStatus::Ok); assert_eq!(read.bytes, b"hello!"); assert_eq!(read.bytes_read, 6); } #[test] fn slot_clear_missing_is_empty() { let mut fs = VirtualFS::new(); fs.mount(Box::new(MockBackend::new())).expect("mount"); let mut mem = MemcardService::new(); assert_eq!(mem.slot_clear(&mut fs, 1, 1), MemcardStatus::Empty); } #[test] fn slot_stat_reports_corruption() { let mut fs = VirtualFS::new(); let mut backend = MockBackend::new(); backend.files.insert(slot_path(7, 2), vec![0, 1, 2, 3, 4]); fs.mount(Box::new(backend)).expect("mount"); let mem = MemcardService::new(); let stat = mem.slot_stat(&fs, 7, 2); assert_eq!(stat.state, MemcardSlotState::Corrupt); } }