Skip to main content

repame_shell/
store.rs

1use std::path::{Path, PathBuf};
2
3pub fn save_file_path(env_var: &str, file_name: &str) -> PathBuf {
4    if let Ok(p) = std::env::var(env_var)
5        && !p.is_empty()
6    {
7        return PathBuf::from(p);
8    }
9    std::env::current_dir()
10        .unwrap_or_else(|_| PathBuf::from("."))
11        .join(file_name)
12}
13
14pub fn store_json(text: &str, path: &Path) -> Result<(), String> {
15    if let Some(parent) = path.parent()
16        && !parent.as_os_str().is_empty()
17    {
18        std::fs::create_dir_all(parent).map_err(|e| e.to_string())?;
19    }
20    // Crash-safe: temp + rename, so a mid-write kill leaves either the old
21    // file or the new one, never a torn half-write. Same-volume `rename`
22    // replaces atomically on POSIX and on Windows (std sets
23    // `MOVEFILE_REPLACE_EXISTING`); the temp lives next to the target so no
24    // cross-volume copy. No `fsync`: an OS crash (not just a process kill)
25    // can still lose the rename; games needing power-loss durability must
26    // fsync the temp and the directory game-side.
27    let tmp = path.with_extension("tmp");
28    std::fs::write(&tmp, text).map_err(|e| e.to_string())?;
29    std::fs::rename(&tmp, path).map_err(|e| e.to_string())
30}
31
32pub fn load_json(path: &Path) -> Result<String, String> {
33    std::fs::read_to_string(path).map_err(|e| e.to_string())
34}
35
36#[cfg(test)]
37mod tests {
38    use super::*;
39
40    #[test]
41    fn env_override_wins() {
42        let _lock = ENV_LOCK.lock().unwrap();
43        unsafe { std::env::set_var("REPAME_SAVE_TEST_PATH", "/tmp/x.json") };
44        assert_eq!(
45            save_file_path("REPAME_SAVE_TEST_PATH", "s.json"),
46            PathBuf::from("/tmp/x.json")
47        );
48        unsafe { std::env::remove_var("REPAME_SAVE_TEST_PATH") };
49    }
50
51    #[test]
52    fn round_trip_leaves_no_tmp() {
53        let dir = std::env::temp_dir().join(format!("repame-store-{}", std::process::id()));
54        let path = dir.join("save.json");
55        let _ = std::fs::remove_dir_all(&dir);
56        store_json("{\"a\":1}", &path).expect("write");
57        assert_eq!(load_json(&path).expect("read"), "{\"a\":1}");
58        assert!(
59            !path.with_extension("tmp").exists(),
60            "temp renamed away, not left behind"
61        );
62        let _ = std::fs::remove_dir_all(&dir);
63    }
64
65    static ENV_LOCK: std::sync::Mutex<()> = std::sync::Mutex::new(());
66}