1use crate::NativeEmulatorResult; 2use crate::mainloop::{NativeEmulatorError, bincode_config}; 3use bincode::error::{DecodeError, EncodeError}; 4use bincode::{Decode, Encode}; 5use jgenesis_common::frontend::SaveWriter; 6use jgenesis_native_config::common::SavePath; 7use regex::Regex; 8use std::borrow::Cow; 9use std::collections::HashMap; 10use std::ffi::OsStr; 11use std::fs::File; 12use std::io::{BufReader, BufWriter, Read, Write}; 13use std::path::{Path, PathBuf}; 14use std::sync::LazyLock; 15use std::{env, fs, io}; 16use thiserror::Error; 17 18#[derive(Debug, Error)] 19pub enum SaveWriteError { 20 #[error("Error writing save file to '{path}': {source}")] 21 OpenFile { 22 path: String, 23 #[source] 24 source: io::Error, 25 }, 26 #[error("Error reading save bytes from '{path}': {source}")] 27 ReadFile { 28 path: String, 29 #[source] 30 source: io::Error, 31 }, 32 #[error("Error writing save bytes to '{path}': {source}")] 33 WriteFile { 34 path: String, 35 #[source] 36 source: io::Error, 37 }, 38 #[error("Failed to rename temp file '{temp_path}' to '{path}': {source}")] 39 RenameFile { 40 path: String, 41 temp_path: String, 42 #[source] 43 source: io::Error, 44 }, 45 #[error("Error serializing save data to '{path}': {source}")] 46 Encode { 47 path: String, 48 #[source] 49 source: EncodeError, 50 }, 51 #[error("Error deserializing save data from '{path}': {source}")] 52 Decode { 53 path: String, 54 #[source] 55 source: DecodeError, 56 }, 57} 58 59struct SavePaths { 60 path: PathBuf, 61 temp_path: PathBuf, 62} 63 64pub struct FsSaveWriter { 65 base_path: PathBuf, 66 extension_to_paths: HashMap<String, SavePaths>, 67} 68 69impl FsSaveWriter { 70 pub fn new(path: PathBuf) -> Self { 71 Self { base_path: path, extension_to_paths: HashMap::new() } 72 } 73 74 pub fn update_path(&mut self, path: PathBuf) { 75 if path == self.base_path { 76 return; 77 } 78 79 self.base_path = path; 80 self.extension_to_paths.clear(); 81 } 82 83 fn get_or_insert_paths(&mut self, extension: &str) -> &SavePaths { 84 // Double get necessary to avoid borrow checker issues related to returning a reference 85 if !self.extension_to_paths.contains_key(extension) { 86 let path = self.base_path.with_extension(extension); 87 let temp_path = self.base_path.with_extension(format!("{extension}.tmp")); 88 self.extension_to_paths.insert(extension.into(), SavePaths { path, temp_path }); 89 } 90 91 &self.extension_to_paths[extension] 92 } 93 94 fn read_file<D, F>(&mut self, extension: &str, read_fn: F) -> Result<D, SaveWriteError> 95 where 96 F: FnOnce(BufReader<File>, &PathBuf) -> Result<D, SaveWriteError>, 97 { 98 let SavePaths { path, .. } = self.get_or_insert_paths(extension); 99 100 let file = File::open(path).map_err(|source| SaveWriteError::OpenFile { 101 path: path.display().to_string(), 102 source, 103 })?; 104 let reader = BufReader::new(file); 105 read_fn(reader, path) 106 } 107 108 fn write_file<F>(&mut self, extension: &str, write_fn: F) -> Result<(), SaveWriteError> 109 where 110 F: FnOnce(BufWriter<File>, &PathBuf) -> Result<(), SaveWriteError>, 111 { 112 let SavePaths { path, temp_path } = self.get_or_insert_paths(extension); 113 114 { 115 let temp_file = 116 File::options().write(true).create(true).truncate(true).open(temp_path).map_err( 117 |source| SaveWriteError::OpenFile { 118 path: temp_path.display().to_string(), 119 source, 120 }, 121 )?; 122 let temp_writer = BufWriter::new(temp_file); 123 write_fn(temp_writer, temp_path)?; 124 } 125 126 fs::rename(temp_path, path).map_err(|source| SaveWriteError::RenameFile { 127 path: path.display().to_string(), 128 temp_path: temp_path.display().to_string(), 129 source, 130 })?; 131 132 Ok(()) 133 } 134} 135 136impl SaveWriter for FsSaveWriter { 137 type Err = SaveWriteError; 138 139 fn load_bytes(&mut self, extension: &str) -> Result<Vec<u8>, Self::Err> { 140 self.read_file(extension, |mut reader, path| { 141 let mut bytes = Vec::new(); 142 reader.read_to_end(&mut bytes).map_err(|source| SaveWriteError::ReadFile { 143 path: path.display().to_string(), 144 source, 145 })?; 146 Ok(bytes) 147 }) 148 } 149 150 fn persist_bytes(&mut self, extension: &str, bytes: &[u8]) -> Result<(), Self::Err> { 151 self.write_file(extension, |mut writer, path| { 152 writer.write_all(bytes).map_err(|source| SaveWriteError::WriteFile { 153 path: path.display().to_string(), 154 source, 155 }) 156 }) 157 } 158 159 fn load_serialized<D: Decode<()>>(&mut self, extension: &str) -> Result<D, Self::Err> { 160 self.read_file(extension, |mut reader, path| { 161 bincode::decode_from_std_read(&mut reader, bincode_config!()).map_err(|source| { 162 SaveWriteError::Decode { path: path.display().to_string(), source } 163 }) 164 }) 165 } 166 167 fn persist_serialized<E: Encode>(&mut self, extension: &str, data: E) -> Result<(), Self::Err> { 168 self.write_file(extension, |mut writer, path| { 169 bincode::encode_into_std_write(data, &mut writer, bincode_config!()).map_err( 170 |source| SaveWriteError::Encode { path: path.display().to_string(), source }, 171 )?; 172 173 Ok(()) 174 }) 175 } 176} 177 178#[derive(Debug, Clone, Copy, PartialEq, Eq)] 179enum RemoveDiscFromPath { 180 No, 181 Yes, 182} 183 184fn determine_path( 185 path: &SavePath, 186 rom_path: &Path, 187 rom_extension: &str, 188 save_extension: &str, 189 save_subdir: &str, 190 remove_disc: RemoveDiscFromPath, 191) -> NativeEmulatorResult<PathBuf> { 192 let base_dir = match path { 193 SavePath::RomFolder => { 194 // Return early because this is a path directly to the save file, not the parent directory 195 if remove_disc == RemoveDiscFromPath::Yes 196 && let Some(file_name) = rom_path.file_name().and_then(OsStr::to_str) 197 { 198 let name_without_disc = strip_disc_from_file_name(file_name); 199 return Ok(rom_path 200 .with_file_name(name_without_disc.as_ref()) 201 .with_extension(save_extension)); 202 } 203 return Ok(rom_path.with_extension(save_extension)); 204 } 205 SavePath::EmulatorFolder => { 206 let base_dir = jgenesis_common::determine_emulator_dir().unwrap_or_else(|| { 207 log::error!( 208 "Unable to determine AppImage parent directory, defaulting to current directory" 209 ); 210 current_dir_with_fallback() 211 }); 212 213 base_dir.join(save_subdir).join(rom_extension) 214 } 215 SavePath::Custom(custom_path) => custom_path.join(rom_extension), 216 }; 217 218 if !base_dir.exists() { 219 fs::create_dir_all(&base_dir).map_err(|source| NativeEmulatorError::CreateSaveDir { 220 path: base_dir.to_string_lossy().into(), 221 source, 222 })?; 223 } 224 225 let Some(rom_file_name) = rom_path.file_name().and_then(OsStr::to_str) else { 226 return Err(NativeEmulatorError::ParseFileName(rom_path.to_string_lossy().into())); 227 }; 228 229 let save_file_name = match remove_disc { 230 RemoveDiscFromPath::No => rom_file_name.into(), 231 RemoveDiscFromPath::Yes => strip_disc_from_file_name(rom_file_name), 232 }; 233 234 Ok(base_dir.join(Path::new(save_file_name.as_ref()).with_extension(save_extension))) 235} 236 237fn strip_disc_from_file_name(file_name: &str) -> Cow<'_, str> { 238 static DISC_RE: LazyLock<Regex> = LazyLock::new(|| Regex::new(r" \(Disc \d\)").unwrap()); 239 240 DISC_RE.replace(file_name, "") 241}
Fall back to temp dir if getting the current directory fails
253pub struct DeterminedPaths { 254 pub save_path: PathBuf, 255 pub save_state_path: PathBuf, 256} 257 258pub fn determine_save_paths( 259 base_save_path: &SavePath, 260 base_state_path: &SavePath, 261 rom_path: &Path, 262 rom_extension: &str, 263) -> NativeEmulatorResult<DeterminedPaths> { 264 let save_path = determine_path( 265 base_save_path, 266 rom_path, 267 rom_extension, 268 "sav", 269 SavePath::SAVE_SUBDIR, 270 RemoveDiscFromPath::Yes, 271 )?; 272 let save_state_path = determine_path( 273 base_state_path, 274 rom_path, 275 rom_extension, 276 "jst", 277 SavePath::STATE_SUBDIR, 278 RemoveDiscFromPath::No, 279 )?; 280 281 log::info!("Save file path: '{}'", save_path.display()); 282 log::info!("Base save state path: '{}'", save_state_path.display()); 283 284 Ok(DeterminedPaths { save_path, save_state_path }) 285}