Reviewed-on: #10 Co-authored-by: bQUARKz <bquarkz@gmail.com> Co-committed-by: bQUARKz <bquarkz@gmail.com>
432 lines
13 KiB
Rust
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);
|
|
}
|
|
}
|