feat(desktop): add safe explicit Flash save-file persistence
This commit is contained in:
parent
8ded9bc90e
commit
86f29018fd
17 changed files with 1032 additions and 133 deletions
201
crates/desktop/src/cartridge/save_file/tests.rs
Normal file
201
crates/desktop/src/cartridge/save_file/tests.rs
Normal file
|
|
@ -0,0 +1,201 @@
|
|||
//! File tests use generated bytes in private temporary directories, never user saves.
|
||||
use super::*;
|
||||
use gba_core::cartridge::{FLASH_PROGRAM_CYCLES, SAVE_START};
|
||||
|
||||
struct Fixture {
|
||||
dir: PathBuf,
|
||||
rom: PathBuf,
|
||||
save: PathBuf,
|
||||
}
|
||||
impl Fixture {
|
||||
fn new() -> Self {
|
||||
static NEXT: AtomicU64 = AtomicU64::new(0);
|
||||
let dir = std::env::temp_dir().join(format!(
|
||||
"gba-save-{}-{}",
|
||||
std::process::id(),
|
||||
NEXT.fetch_add(1, Ordering::Relaxed)
|
||||
));
|
||||
fs::create_dir(&dir).unwrap();
|
||||
let rom = dir.join("original.gba");
|
||||
fs::write(&rom, [0xfe, 0xff, 0xff, 0xea]).unwrap();
|
||||
let save = dir.join("original.sav");
|
||||
Self { dir, rom, save }
|
||||
}
|
||||
fn open(&self) -> io::Result<SaveFile> {
|
||||
SaveFile::open(&self.save, &self.rom, SaveDevice::Flash128)
|
||||
}
|
||||
fn image(&self, value: u8) -> Vec<u8> {
|
||||
let bytes = vec![value; SaveDevice::Flash128.capacity()];
|
||||
fs::write(&self.save, &bytes).unwrap();
|
||||
bytes
|
||||
}
|
||||
}
|
||||
impl Drop for Fixture {
|
||||
fn drop(&mut self) {
|
||||
let _ = fs::remove_dir_all(&self.dir);
|
||||
}
|
||||
}
|
||||
fn memory() -> Memory {
|
||||
let mut m = Memory::new(vec![]).unwrap();
|
||||
m.set_save_device(SaveDevice::Flash128);
|
||||
m
|
||||
}
|
||||
fn start_program(m: &mut Memory, value: u8) {
|
||||
for (offset, byte) in [(0x5555, 0xaa), (0x2aaa, 0x55), (0x5555, 0xa0), (0, value)] {
|
||||
m.write8(SAVE_START + offset, byte).unwrap();
|
||||
}
|
||||
}
|
||||
fn program(m: &mut Memory, value: u8) {
|
||||
start_program(m, value);
|
||||
m.advance_cycles(FLASH_PROGRAM_CYCLES);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fresh_image_is_volatile_until_modified_then_reloads_with_unique_backups() {
|
||||
let f = Fixture::new();
|
||||
let mut m = memory();
|
||||
let mut save = f.open().unwrap();
|
||||
assert!(!save.loaded());
|
||||
save.initialize(&mut m).unwrap();
|
||||
assert!(save.persist(&m).unwrap().is_none());
|
||||
assert!(!f.save.exists());
|
||||
program(&mut m, 0xa5);
|
||||
assert!(save.persist(&m).unwrap().unwrap().backup.is_none());
|
||||
assert_eq!(fs::read(&f.save).unwrap(), m.save_image().unwrap());
|
||||
assert!(save.persist(&m).unwrap().is_none());
|
||||
drop(save);
|
||||
assert!(!sibling(&f.save, ".lock").exists());
|
||||
let mut save = f.open().unwrap();
|
||||
assert!(save.loaded());
|
||||
let mut m = memory();
|
||||
save.initialize(&mut m).unwrap();
|
||||
assert!(!m.save_modified());
|
||||
assert_eq!(m.read8(SAVE_START).unwrap(), 0xa5);
|
||||
program(&mut m, 0xa0);
|
||||
let first = save.persist(&m).unwrap().unwrap().backup.unwrap();
|
||||
let original = fs::read(&first).unwrap();
|
||||
assert_eq!(original[0], 0xa5);
|
||||
program(&mut m, 0x80);
|
||||
let second = save.persist(&m).unwrap().unwrap().backup.unwrap();
|
||||
assert_ne!(first, second);
|
||||
assert_eq!(fs::read(&first).unwrap(), original);
|
||||
assert_eq!(fs::read(second).unwrap()[0], 0xa0);
|
||||
assert_eq!(fs::read(&f.save).unwrap()[0], 0x80);
|
||||
assert!(!fs::read_dir(&f.dir).unwrap().any(|p| p
|
||||
.unwrap()
|
||||
.file_name()
|
||||
.to_string_lossy()
|
||||
.contains(".tmp.")));
|
||||
assert_eq!(fs::read(&f.rom).unwrap(), [0xfe, 0xff, 0xff, 0xea]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn incomplete_commands_and_busy_operations_preserve_the_existing_image() {
|
||||
let f = Fixture::new();
|
||||
let original = f.image(255);
|
||||
let mut save = f.open().unwrap();
|
||||
let mut m = memory();
|
||||
save.initialize(&mut m).unwrap();
|
||||
m.write8(SAVE_START + 0x5555, 0xaa).unwrap();
|
||||
assert!(save.persist(&m).is_err());
|
||||
assert_eq!(fs::read(&f.save).unwrap(), original);
|
||||
m.load_save_image(&original).unwrap();
|
||||
start_program(&mut m, 0x12);
|
||||
assert!(save.persist(&m).is_err());
|
||||
assert_eq!(fs::read(&f.save).unwrap(), original);
|
||||
m.advance_cycles(FLASH_PROGRAM_CYCLES);
|
||||
assert!(save.persist(&m).unwrap().is_some());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn lock_excludes_other_sessions_and_does_not_delete_replaced_locks() {
|
||||
let f = Fixture::new();
|
||||
let first = f.open().unwrap();
|
||||
assert!(f.open().is_err());
|
||||
let lock = sibling(&f.save, ".lock");
|
||||
fs::rename(&lock, f.dir.join("owned-lock")).unwrap();
|
||||
fs::write(&lock, b"another owner").unwrap();
|
||||
drop(first);
|
||||
assert_eq!(fs::read(&lock).unwrap(), b"another owner");
|
||||
assert!(f.open().is_err());
|
||||
fs::remove_file(&lock).unwrap();
|
||||
drop(f.open().unwrap());
|
||||
assert!(!lock.exists());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn size_and_device_errors_leave_no_lock_and_no_changed_bytes() {
|
||||
let f = Fixture::new();
|
||||
for length in [0, 1, 65536, 131071, 131073] {
|
||||
let bytes = vec![0x5a; length];
|
||||
fs::write(&f.save, &bytes).unwrap();
|
||||
assert!(f.open().is_err());
|
||||
assert_eq!(fs::read(&f.save).unwrap(), bytes);
|
||||
assert!(!sibling(&f.save, ".lock").exists());
|
||||
}
|
||||
assert!(SaveFile::open(&f.save, &f.rom, SaveDevice::None).is_err());
|
||||
let bytes = vec![255; 65536];
|
||||
fs::write(&f.save, &bytes).unwrap();
|
||||
let save = SaveFile::open(&f.save, &f.rom, SaveDevice::Flash64).unwrap();
|
||||
let mut m = Memory::new(vec![]).unwrap();
|
||||
m.set_save_device(SaveDevice::Flash64);
|
||||
save.initialize(&mut m).unwrap();
|
||||
assert_eq!(m.save_image().unwrap(), bytes);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn external_changes_and_new_files_are_never_overwritten() {
|
||||
for existing in [false, true] {
|
||||
let f = Fixture::new();
|
||||
if existing {
|
||||
f.image(255);
|
||||
}
|
||||
let mut save = f.open().unwrap();
|
||||
let mut m = memory();
|
||||
save.initialize(&mut m).unwrap();
|
||||
program(&mut m, 0x12);
|
||||
let external = f.image(0x5a);
|
||||
assert!(save
|
||||
.persist(&m)
|
||||
.unwrap_err()
|
||||
.to_string()
|
||||
.contains("changed externally"));
|
||||
assert_eq!(fs::read(&f.save).unwrap(), external);
|
||||
assert_eq!(fs::read_dir(&f.dir).unwrap().count(), 3); // ROM, image, owned lock only.
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
#[test]
|
||||
fn reject_rom_aliases_symlinks_directories_and_readonly_images() {
|
||||
use std::os::unix::fs::{symlink, PermissionsExt};
|
||||
let f = Fixture::new();
|
||||
assert!(SaveFile::open(&f.rom, &f.rom, SaveDevice::Flash128).is_err());
|
||||
fs::hard_link(&f.rom, &f.save).unwrap();
|
||||
assert!(f.open().is_err());
|
||||
fs::remove_file(&f.save).unwrap();
|
||||
let target = f.dir.join("target");
|
||||
fs::write(&target, vec![255; 131072]).unwrap();
|
||||
symlink(&target, &f.save).unwrap();
|
||||
assert!(f.open().is_err());
|
||||
fs::remove_file(&f.save).unwrap();
|
||||
symlink(f.dir.join("absent"), &f.save).unwrap();
|
||||
assert!(f.open().is_err());
|
||||
fs::remove_file(&f.save).unwrap();
|
||||
fs::create_dir(&f.save).unwrap();
|
||||
assert!(f.open().is_err());
|
||||
fs::remove_dir(&f.save).unwrap();
|
||||
let image = f.image(255);
|
||||
fs::set_permissions(&f.save, fs::Permissions::from_mode(0o400)).unwrap();
|
||||
assert!(f.open().is_err());
|
||||
assert_eq!(fs::read(&f.save).unwrap(), image);
|
||||
fs::set_permissions(&f.save, fs::Permissions::from_mode(0o600)).unwrap();
|
||||
let mut save = f.open().unwrap();
|
||||
let mut m = memory();
|
||||
save.initialize(&mut m).unwrap();
|
||||
program(&mut m, 0x12);
|
||||
fs::set_permissions(&f.save, fs::Permissions::from_mode(0o400)).unwrap();
|
||||
assert!(save.persist(&m).is_err());
|
||||
assert_eq!(fs::read(&f.save).unwrap(), image);
|
||||
fs::set_permissions(&f.save, fs::Permissions::from_mode(0o600)).unwrap();
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue