Skip to main content

pacenotes_installer\restore/
plan.rs

1/// One package the restore has to put back.
2#[derive(Clone, Debug, PartialEq, Eq)]
3pub struct PutBack {
4    pub key: String,
5    /// The digest the snapshot recorded, which is why restore cannot reuse
6    /// `engine::plan`: the cache index holds only the newest version of a key,
7    /// while the older payload is still on disk under its own digest.
8    pub sha256: String,
9    pub version: String,
10}
11
12/// What restoring to a point would do, shown as the preview and then carried
13/// out unchanged.
14#[derive(Clone, Debug, Default, PartialEq, Eq)]
15pub struct Plan {
16    /// Installed now and not wanted at that point, or wanted in another
17    /// version, in which case it is also in `put_back`.
18    pub remove: Vec<String>,
19    /// Installed from the cache, in key order.
20    pub put_back: Vec<PutBack>,
21    /// Wanted at that point but no longer in the cache: neither removed nor
22    /// reinstalled, and named in the preview before the user commits.
23    pub unavailable: Vec<PutBack>,
24    /// Already as the point had it.
25    pub keep: Vec<String>,
26}
27
28impl Plan {
29    /// The folder already matches the point.
30    pub fn is_empty(&self) -> bool {
31        self.remove.is_empty() && self.put_back.is_empty()
32    }
33
34    /// Some of what the point held cannot come back.
35    pub fn is_partial(&self) -> bool {
36        !self.unavailable.is_empty()
37    }
38}
39
40/// What it takes to make `live` look like `target`.
41///
42/// `payload_present` says whether a digest is in the cache, so the decision is
43/// testable without one. An empty `target` is a complete uninstall: the same
44/// operation, deliberately not a second code path.
45pub fn plan(
46    target: &super::Snapshot,
47    live: &super::Snapshot,
48    payload_present: &dyn Fn(&str) -> bool,
49) -> Plan {
50    let mut plan = Plan::default();
51    for (key, now) in live {
52        let Some(then) = target.get(key) else {
53            plan.remove.push(key.clone());
54            continue;
55        };
56        // A half-written entry is never already correct: its files may be partial.
57        if then.sha256 == now.sha256 && now.state == crate::ledger::EntryState::Installed {
58            plan.keep.push(key.clone());
59        } else if payload_present(&then.sha256) {
60            plan.remove.push(key.clone());
61            plan.put_back.push(put_back(key, then));
62        } else {
63            plan.unavailable.push(put_back(key, then));
64        }
65    }
66    for (key, then) in target.iter().filter(|(key, _)| !live.contains_key(*key)) {
67        if payload_present(&then.sha256) {
68            plan.put_back.push(put_back(key, then));
69        } else {
70            plan.unavailable.push(put_back(key, then));
71        }
72    }
73    plan
74}
75
76fn put_back(key: &str, then: &crate::ledger::LedgerEntry) -> PutBack {
77    PutBack {
78        key: key.to_string(),
79        sha256: then.sha256.clone(),
80        version: then.version.clone(),
81    }
82}
83
84/// The ledger once `plan` has been carried out, `failed` naming what could
85/// not be put back after all.
86///
87/// A restored package gets the snapshot's entry, because its per-file `prior`
88/// records what the user had before this installer first wrote there; a fresh
89/// install would record `absent` and a later uninstall would delete the
90/// user's file. A package left alone keeps its live entry, which describes
91/// what is actually on disk.
92pub fn ledger_after(
93    target: &super::Snapshot,
94    live: &super::Snapshot,
95    plan: &Plan,
96    failed: &[String],
97) -> super::Snapshot {
98    let restored = plan
99        .put_back
100        .iter()
101        .filter(|wanted| !failed.contains(&wanted.key))
102        .filter_map(|wanted| Some((wanted.key.clone(), target.get(&wanted.key)?.clone())));
103    let untouched = plan
104        .keep
105        .iter()
106        .chain(plan.unavailable.iter().map(|wanted| &wanted.key))
107        .filter_map(|key| Some((key.clone(), live.get(key)?.clone())));
108    untouched.chain(restored).collect()
109}
110
111/// Per single-pick category, the package the restored state holds in it, which
112/// is what the game's configuration step needs to make it the user's pick again.
113pub fn picks(
114    target: &super::Snapshot,
115    choice_categories: &[&str],
116) -> std::collections::BTreeMap<String, String> {
117    choice_categories
118        .iter()
119        .filter_map(|category| {
120            let key = target
121                .keys()
122                .find(|key| pacenotes_shared::category_of_download_path(key) == Some(category))?;
123            Some((category.to_string(), key.clone()))
124        })
125        .collect()
126}