Skip to main content

entracte_lib/
pause_store.rs

1use std::io;
2use std::path::Path;
3
4use log::error;
5use serde::{Deserialize, Serialize};
6
7use crate::secure_io::{read_capped, write_user_only};
8
9/// PauseSnapshot is two scalars. Anything bigger than 4 KiB on disk
10/// is either junk or attacker-controlled — refuse to even parse it.
11const MAX_PAUSE_BYTES: u64 = 4 * 1024;
12
13#[derive(Debug, Clone, Default, Serialize, Deserialize)]
14#[serde(default)]
15pub struct PauseSnapshot {
16    pub paused: bool,
17    pub until_epoch_secs: Option<u64>,
18}
19
20pub fn load(path: &Path) -> PauseSnapshot {
21    match read_capped(path, MAX_PAUSE_BYTES) {
22        Ok(text) => serde_json::from_str(&text).unwrap_or_else(|e| {
23            error!(
24                "pause_store: failed to parse {}: {e} — using defaults",
25                path.display()
26            );
27            PauseSnapshot::default()
28        }),
29        Err(e) if e.kind() == io::ErrorKind::NotFound => PauseSnapshot::default(),
30        Err(e) => {
31            error!(
32                "pause_store: failed to read {}: {e} — using defaults",
33                path.display()
34            );
35            PauseSnapshot::default()
36        }
37    }
38}
39
40pub fn save(path: &Path, snapshot: &PauseSnapshot) -> io::Result<()> {
41    let body = serde_json::to_string_pretty(snapshot).map_err(io::Error::other)?;
42    write_user_only(path, body.as_bytes())
43}
44
45#[cfg(test)]
46mod tests {
47    use super::*;
48    use crate::test_support::{temp_dir, TempDir};
49
50    fn temp_pause_file() -> (TempDir, std::path::PathBuf) {
51        let dir = temp_dir();
52        let path = dir.path().join("pause.json");
53        (dir, path)
54    }
55
56    #[test]
57    fn load_missing_returns_default() {
58        let (_dir, path) = temp_pause_file();
59        let s = load(&path);
60        assert!(!s.paused);
61        assert!(s.until_epoch_secs.is_none());
62    }
63
64    #[test]
65    fn save_and_load_round_trip_indefinite() {
66        let (_dir, path) = temp_pause_file();
67        let snap = PauseSnapshot {
68            paused: true,
69            until_epoch_secs: None,
70        };
71        save(&path, &snap).unwrap();
72        let loaded = load(&path);
73        assert!(loaded.paused);
74        assert!(loaded.until_epoch_secs.is_none());
75    }
76
77    #[test]
78    fn save_and_load_round_trip_until() {
79        let (_dir, path) = temp_pause_file();
80        let snap = PauseSnapshot {
81            paused: true,
82            until_epoch_secs: Some(1_700_000_000),
83        };
84        save(&path, &snap).unwrap();
85        let loaded = load(&path);
86        assert!(loaded.paused);
87        assert_eq!(loaded.until_epoch_secs, Some(1_700_000_000));
88    }
89
90    #[test]
91    fn save_and_load_round_trip_running() {
92        let (_dir, path) = temp_pause_file();
93        let snap = PauseSnapshot {
94            paused: false,
95            until_epoch_secs: None,
96        };
97        save(&path, &snap).unwrap();
98        let loaded = load(&path);
99        assert!(!loaded.paused);
100        assert!(loaded.until_epoch_secs.is_none());
101    }
102
103    #[test]
104    fn load_corrupt_returns_default() {
105        let (_dir, path) = temp_pause_file();
106        std::fs::write(&path, "{not valid json").unwrap();
107        let loaded = load(&path);
108        assert!(!loaded.paused);
109        assert!(loaded.until_epoch_secs.is_none());
110    }
111
112    #[test]
113    fn save_creates_parent_dirs() {
114        let dir = temp_dir();
115        let path = dir.path().join("a").join("b").join("pause.json");
116        save(&path, &PauseSnapshot::default()).unwrap();
117        assert!(path.exists());
118    }
119}