Skip to main content

pacenotes_installer\ledger/
document.rs

1/// Bumped when the shape changes in a way an older installer must not read.
2const LEDGER_VERSION: u32 = 1;
3
4#[derive(serde::Serialize, serde::Deserialize, Default)]
5pub(super) struct Document {
6    #[serde(default)]
7    version: u32,
8    #[serde(default)]
9    pub(super) packages: std::collections::BTreeMap<String, super::LedgerEntry>,
10}
11
12/// How `installed.json` was found; only `Missing` means the folder is untouched.
13#[derive(Clone, Copy, PartialEq, Eq, Debug)]
14pub enum LedgerState {
15    Missing,
16    /// Unparseable, or written by a newer build: read as empty, never written over.
17    Unreadable,
18    Loaded,
19}
20
21pub struct Ledger {
22    dir: std::path::PathBuf,
23    pub(super) document: Document,
24    state: LedgerState,
25}
26
27impl Ledger {
28    pub fn open(game_dir: impl AsRef<std::path::Path>) -> Ledger {
29        let dir = game_dir.as_ref().join(super::STATE_DIR);
30        let (document, state) = match std::fs::read(dir.join(super::LEDGER_FILE)) {
31            Err(_) => (Document::default(), LedgerState::Missing),
32            Ok(bytes) => match serde_json::from_slice::<Document>(&bytes) {
33                Ok(document) if document.version <= LEDGER_VERSION => {
34                    (document, LedgerState::Loaded)
35                }
36                Ok(_) | Err(_) => (Document::default(), LedgerState::Unreadable),
37            },
38        };
39        Ledger {
40            dir,
41            document,
42            state,
43        }
44    }
45
46    pub fn state(&self) -> LedgerState {
47        self.state
48    }
49
50    /// `.pacenotes` inside the game folder.
51    pub fn state_dir(&self) -> &std::path::Path {
52        &self.dir
53    }
54
55    pub fn put(&mut self, key: &str, entry: super::LedgerEntry) -> std::io::Result<()> {
56        self.document.packages.insert(key.to_string(), entry);
57        self.save()
58    }
59
60    /// Separate from `put`, so a write-ahead entry and a finished one cannot be confused.
61    pub fn mark_installed(&mut self, key: &str) -> std::io::Result<()> {
62        if let Some(entry) = self.document.packages.get_mut(key) {
63            entry.state = super::EntryState::Installed;
64        }
65        self.save()
66    }
67
68    pub fn remove(&mut self, key: &str) -> std::io::Result<()> {
69        self.document.packages.remove(key);
70        self.save()
71    }
72
73    /// What restoring a point does.
74    pub fn replace_all(
75        &mut self,
76        packages: std::collections::BTreeMap<String, super::LedgerEntry>,
77    ) -> std::io::Result<()> {
78        self.document.packages = packages;
79        self.save()
80    }
81
82    /// The packages alone, as `replace_all` takes them back.
83    pub fn save_snapshot(&self, path: &std::path::Path) -> std::io::Result<()> {
84        let json = serde_json::to_vec_pretty(&self.document.packages)
85            .map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e))?;
86        std::fs::write(path, json)
87    }
88
89    /// What `save_snapshot` wrote, or `None` when it is missing or unreadable.
90    pub fn read_snapshot(
91        path: &std::path::Path,
92    ) -> Option<std::collections::BTreeMap<String, super::LedgerEntry>> {
93        let bytes = std::fs::read(path).ok()?;
94        serde_json::from_slice(&bytes).ok()
95    }
96
97    /// After each package, as a temp file renamed over the old one, so a run
98    /// that dies leaves a ledger that still describes the disk. Refuses to
99    /// replace a ledger it could not read, which may record a newer build's packages.
100    pub fn save(&mut self) -> std::io::Result<()> {
101        if self.state == LedgerState::Unreadable {
102            return Err(std::io::Error::new(
103                std::io::ErrorKind::InvalidData,
104                "this game folder has an installation record this version cannot read; \
105                 it was probably written by a newer installer",
106            ));
107        }
108        std::fs::create_dir_all(&self.dir)?;
109        self.document.version = LEDGER_VERSION;
110        let json = serde_json::to_vec_pretty(&self.document)
111            .map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e))?;
112
113        let temp = self.dir.join(format!("{}.tmp", super::LEDGER_FILE));
114        std::fs::write(&temp, &json)?;
115        std::fs::rename(&temp, self.dir.join(super::LEDGER_FILE))?;
116        self.state = LedgerState::Loaded;
117        Ok(())
118    }
119}