201 lines
7 KiB
Rust
201 lines
7 KiB
Rust
//! 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();
|
|
}
|