//! 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::open(&self.save, &self.rom, SaveDevice::Flash128) } fn image(&self, value: u8) -> Vec { 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(); }