bQUARKz 3453494341
Some checks are pending
Test / Build
Intrepid/Prometeu/Runtime/pipeline/head This commit looks good
dev/jenkinsfile (#10)
Reviewed-on: #10
Co-authored-by: bQUARKz <bquarkz@gmail.com>
Co-committed-by: bQUARKz <bquarkz@gmail.com>
2026-04-08 07:39:33 +00:00

432 lines
13 KiB
Rust

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<u8>,
pub bytes_read: u32,
}
pub struct MemcardWriteResult {
pub status: MemcardStatus,
pub bytes_written: u32,
}
#[derive(Debug, Clone)]
struct SlotImage {
payload: Vec<u8>,
generation: u64,
checksum: u32,
save_uuid: [u8; 16],
}
#[derive(Default)]
pub struct MemcardService {
staged: HashMap<(u32, u8), Vec<u8>>,
}
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<Option<SlotImage>, 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<u8> {
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<SlotImage, MemcardStatus> {
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<String, Vec<u8>>,
}
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<Vec<FsEntry>, FsError> {
Ok(Vec::new())
}
fn read_file(&self, path: &str) -> Result<Vec<u8>, 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);
}
}