pacenotes_installer\engine/
preflight.rs1#[derive(Clone, Debug, PartialEq, Eq)]
2pub enum PreflightError {
3 PayloadMissing {
4 package: String,
5 },
6 PayloadCorrupt {
8 package: String,
9 },
10 PayloadUnreadable {
11 package: String,
12 reason: String,
13 },
14 NotEnoughSpace {
16 needed: u64,
17 available: u64,
18 },
19 NotWritable {
20 reason: String,
21 },
22 GameRunning,
24}
25
26impl std::fmt::Display for PreflightError {
27 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
28 match self {
29 PreflightError::PayloadMissing { package } => {
30 write!(formatter, "{package} is not in the download cache")
31 }
32 PreflightError::PayloadCorrupt { package } => write!(
33 formatter,
34 "{package} is damaged in the download cache; delete it and download it again"
35 ),
36 PreflightError::PayloadUnreadable { package, reason } => {
37 write!(formatter, "{package} could not be read: {reason}")
38 }
39 PreflightError::NotEnoughSpace { needed, available } => write!(
40 formatter,
41 "this needs {} and the drive has {}",
42 crate::size::human(*needed),
43 crate::size::human(*available)
44 ),
45 PreflightError::NotWritable { reason } => {
46 write!(formatter, "the game folder cannot be written to: {reason}")
47 }
48 PreflightError::GameRunning => write!(
49 formatter,
50 "the game is running. Close it first — its files are open, and the \
51 installation would stop partway through"
52 ),
53 }
54 }
55}
56
57#[derive(Clone, Copy, Debug, Default)]
59pub struct HostFacts {
60 pub free_bytes: Option<u64>,
62 pub game_running: bool,
63}
64
65#[derive(Debug)]
67pub struct Preflight {
68 pub files: std::collections::BTreeMap<String, Vec<crate::zip::FileEntry>>,
70 pub install_bytes: u64,
71}
72
73impl Preflight {
74 pub fn paths(&self) -> std::collections::BTreeMap<String, Vec<String>> {
76 self.files
77 .iter()
78 .map(|(key, files)| {
79 let paths = files.iter().map(|file| file.path.clone()).collect();
80 (key.clone(), paths)
81 })
82 .collect()
83 }
84}
85
86pub fn preflight(
92 plan: &super::Plan,
93 game_dir: &std::path::Path,
94 cache: &crate::cache::Cache,
95 facts: HostFacts,
96 watcher: &mut dyn super::Watcher,
97) -> Result<Preflight, PreflightError> {
98 if facts.game_running {
99 return Err(PreflightError::GameRunning);
100 }
101 probe_writable(game_dir)?;
102
103 let mut files = std::collections::BTreeMap::new();
104 let mut install_bytes = 0u64;
105 let installs = plan.installs();
106 for (index, key) in installs.iter().enumerate() {
107 watcher.phase(
108 super::Phase::Checking,
109 key,
110 index as u64,
111 installs.len() as u64,
112 );
113 let missing = || PreflightError::PayloadMissing {
114 package: key.to_string(),
115 };
116 let sha256 = &cache.get(key).ok_or_else(missing)?.sha256;
117 let listed = verified_files(cache, key, sha256)?;
118 install_bytes += listed.iter().map(|file| file.size).sum::<u64>();
119 files.insert(key.to_string(), listed);
120 }
121
122 check_space(install_bytes, facts)?;
123 Ok(Preflight {
124 files,
125 install_bytes,
126 })
127}
128
129pub(crate) fn verified_files(
133 cache: &crate::cache::Cache,
134 key: &str,
135 sha256: &str,
136) -> Result<Vec<crate::zip::FileEntry>, PreflightError> {
137 let bytes =
138 std::fs::read(cache.payload_path(sha256)).map_err(|_| PreflightError::PayloadMissing {
139 package: key.to_string(),
140 })?;
141 if crate::sha256::hex_digest(&bytes) != sha256 {
142 return Err(PreflightError::PayloadCorrupt {
143 package: key.to_string(),
144 });
145 }
146 crate::zip::entries(&bytes).map_err(|e| PreflightError::PayloadUnreadable {
147 package: key.to_string(),
148 reason: e.0,
149 })
150}
151
152pub(crate) fn check_space(install_bytes: u64, facts: HostFacts) -> Result<(), PreflightError> {
155 if let Some(available) = facts.free_bytes {
156 let needed = install_bytes.saturating_mul(2);
157 if needed > available {
158 return Err(PreflightError::NotEnoughSpace { needed, available });
159 }
160 }
161 Ok(())
162}
163
164pub fn check_writable(game_dir: &std::path::Path) -> Result<(), PreflightError> {
168 let dir = game_dir.join(crate::ledger::STATE_DIR);
169 let created = !dir.exists();
170 let probed = probe_writable(game_dir);
171 if created {
172 let _ = std::fs::remove_dir(&dir);
173 }
174 probed
175}
176
177pub(crate) fn probe_writable(game_dir: &std::path::Path) -> Result<(), PreflightError> {
179 let not_writable = |e: std::io::Error| PreflightError::NotWritable {
180 reason: e.to_string(),
181 };
182 let dir = game_dir.join(crate::ledger::STATE_DIR);
183 std::fs::create_dir_all(&dir).map_err(not_writable)?;
184 let probe = dir.join(".probe");
185 std::fs::write(&probe, b"pacenotes").map_err(not_writable)?;
186 let _ = std::fs::remove_file(&probe);
187 Ok(())
188}
189
190#[cfg(test)]
191mod tests {
192 #[test]
194 fn a_writable_folder_is_left_as_it_was_found() {
195 let game = crate::scratch_dir::ScratchDir::new("probe-fresh");
196
197 assert_eq!(super::check_writable(&game.0), Ok(()));
198 assert!(
199 !game.0.join(crate::ledger::STATE_DIR).exists(),
200 "the probe left its directory behind"
201 );
202 }
203
204 #[test]
206 fn an_existing_state_directory_survives_the_probe() {
207 let game = crate::scratch_dir::ScratchDir::new("probe-existing");
208 let state = game.0.join(crate::ledger::STATE_DIR);
209 std::fs::create_dir_all(&state).expect("state dir");
210 std::fs::write(state.join("installed.json"), b"{}").expect("ledger");
211
212 assert_eq!(super::check_writable(&game.0), Ok(()));
213 assert!(state.join("installed.json").is_file());
214 assert_eq!(std::fs::read_dir(&state).expect("read").count(), 1);
215 }
216}