Skip to main content

vivacity_core/
jsonfile.rs

1//! One parse per JSON file per process: `installed.json` and the global
2//! `config.json` are read by the scope analysis, the layout, the
3//! transaction, the installer and the autoloader in the same run. The
4//! parsed value is kept by path and validated on each read against the
5//! file's (mtime ns, size) — a file rewritten mid-run (installed.json by
6//! the installer) is parsed again; a file removed comes back as absent.
7
8use serde_json::Value;
9use std::collections::HashMap;
10use std::path::{Path, PathBuf};
11use std::sync::{Arc, Mutex, OnceLock};
12
13type Identity = (u128, u64);
14type Entries = HashMap<PathBuf, (Identity, Option<Arc<Value>>)>;
15
16fn identity(path: &Path) -> Option<Identity> {
17    let meta = std::fs::metadata(path).ok()?;
18    let mtime = meta
19        .modified()
20        .ok()?
21        .duration_since(std::time::UNIX_EPOCH)
22        .ok()?
23        .as_nanos();
24    Some((mtime, meta.len()))
25}
26
27fn cache() -> &'static Mutex<Entries> {
28    static CACHE: OnceLock<Mutex<Entries>> = OnceLock::new();
29    CACHE.get_or_init(|| Mutex::new(HashMap::new()))
30}
31
32/// The parsed JSON of `path`: `None` when the file is absent, unreadable
33/// or not JSON (the same answer every caller gave those cases).
34pub fn read(path: &Path) -> Option<Arc<Value>> {
35    let id = identity(path)?;
36    if let Ok(map) = cache().lock() {
37        if let Some((cached_id, value)) = map.get(path) {
38            if *cached_id == id {
39                return value.clone();
40            }
41        }
42    }
43    let value = std::fs::read_to_string(path)
44        .ok()
45        .and_then(|t| serde_json::from_str::<Value>(&t).ok())
46        .map(Arc::new);
47    if let Ok(mut map) = cache().lock() {
48        map.insert(path.to_path_buf(), (id, value.clone()));
49    }
50    value
51}
52
53#[cfg(test)]
54mod tests {
55    use super::*;
56
57    #[test]
58    fn reparses_when_the_file_changes_and_forgets_when_it_goes() {
59        let dir = tempfile::tempdir().expect("tempdir");
60        let f = dir.path().join("x.json");
61        std::fs::write(&f, r#"{"a":1}"#).expect("write");
62        assert_eq!(read(&f).expect("v")["a"], 1);
63        // Same content length, different bytes, later mtime.
64        std::thread::sleep(std::time::Duration::from_millis(20));
65        std::fs::write(&f, r#"{"a":2}"#).expect("write");
66        assert_eq!(read(&f).expect("v")["a"], 2);
67        std::fs::remove_file(&f).expect("rm");
68        assert!(read(&f).is_none());
69        std::fs::write(&f, "not json").expect("write");
70        assert!(read(&f).is_none());
71    }
72}