save.rsannotatedsave.rssource285 lines · 8.8 KB · raw
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

244fn current_dir_with_fallback() -> PathBuf {
245    env::current_dir().unwrap_or_else(|err| {
246        log::error!(
247            "Unable to determine current working directory, defaulting to temp directory: {err}"
248        );
249        env::temp_dir()
250    })
251}
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}