corundum_framework_update/
replace.rs1use std::ffi::OsString;
4use std::io;
5use std::path::{Path, PathBuf};
6
7use crate::Error;
8use crate::Result;
9use crate::install::stage;
10
11pub(crate) trait Fs {
14 fn rename(&self, from: &Path, to: &Path) -> io::Result<()>;
15 fn remove(&self, path: &Path) -> io::Result<()>;
16 fn exists(&self, path: &Path) -> bool;
17}
18
19pub(crate) struct RealFs;
20
21impl Fs for RealFs {
22 fn rename(&self, from: &Path, to: &Path) -> io::Result<()> {
23 std::fs::rename(from, to)
24 }
25 fn remove(&self, path: &Path) -> io::Result<()> {
26 std::fs::remove_file(path)
27 }
28 fn exists(&self, path: &Path) -> bool {
29 path.exists()
30 }
31}
32
33pub fn replace_file(path: &Path, bytes: &[u8]) -> Result<()> {
40 let dir = path
41 .parent()
42 .filter(|dir| !dir.as_os_str().is_empty())
43 .unwrap_or(Path::new("."));
44 let staged = stage(dir, bytes)?.into_temp_path();
47 if cfg!(windows) {
48 replace_windows(&RealFs, path, &staged)
49 } else {
50 replace_unix(&RealFs, path, &staged)
51 }
52 .map_err(Error::Io)
53}
54
55pub(crate) fn replace_unix(fs: &dyn Fs, path: &Path, staged: &Path) -> io::Result<()> {
57 fs.rename(staged, path)
58}
59
60fn old_name(path: &Path, n: u32) -> PathBuf {
62 let mut name = OsString::from(path.as_os_str());
63 name.push(".old");
64 if n > 0 {
65 name.push(format!(".{n}"));
66 }
67 PathBuf::from(name)
68}
69
70pub(crate) fn replace_windows(fs: &dyn Fs, path: &Path, staged: &Path) -> io::Result<()> {
77 let mut n = 0;
80 while fs.exists(&old_name(path, n)) {
81 let _ = fs.remove(&old_name(path, n));
83 n += 1;
84 }
85 let old = (0..)
86 .map(|n| old_name(path, n))
87 .find(|old| !fs.exists(old))
88 .expect("an unbounded range has a free name");
89 fs.rename(path, &old)?;
90 if let Err(e) = fs.rename(staged, path) {
91 let _ = fs.rename(&old, path);
92 return Err(e);
93 }
94 Ok(())
95}
96
97#[cfg(test)]
98mod tests {
99 use super::*;
100 use std::cell::RefCell;
101 use std::collections::HashSet;
102
103 #[derive(Default)]
105 struct RecordingFs {
106 calls: RefCell<Vec<String>>,
107 present: RefCell<HashSet<PathBuf>>,
108 fail: Option<String>,
109 }
110
111 impl RecordingFs {
112 fn with(present: &[&str]) -> Self {
113 Self {
114 present: RefCell::new(present.iter().map(PathBuf::from).collect()),
115 ..Self::default()
116 }
117 }
118 fn failing(mut self, call: &str) -> Self {
119 self.fail = Some(call.to_owned());
120 self
121 }
122 fn record(&self, call: String) -> io::Result<()> {
123 self.calls.borrow_mut().push(call.clone());
124 if self.fail.as_deref() == Some(call.as_str()) {
125 return Err(io::Error::other("injected"));
126 }
127 Ok(())
128 }
129 fn calls(&self) -> Vec<String> {
130 self.calls.borrow().clone()
131 }
132 }
133
134 impl Fs for RecordingFs {
135 fn rename(&self, from: &Path, to: &Path) -> io::Result<()> {
136 self.record(format!("rename {} {}", from.display(), to.display()))?;
137 let mut present = self.present.borrow_mut();
138 present.remove(from);
139 present.insert(to.to_owned());
140 Ok(())
141 }
142 fn remove(&self, path: &Path) -> io::Result<()> {
143 self.record(format!("remove {}", path.display()))?;
144 self.present.borrow_mut().remove(path);
145 Ok(())
146 }
147 fn exists(&self, path: &Path) -> bool {
148 self.present.borrow().contains(path)
149 }
150 }
151
152 #[test]
153 fn unix_renames_over_the_target() {
154 let fs = RecordingFs::default();
155 replace_unix(&fs, Path::new("/d/app"), Path::new("/d/.update-x")).unwrap();
156 assert_eq!(fs.calls(), ["rename /d/.update-x /d/app"]);
157 }
158
159 #[test]
160 fn windows_sets_the_running_exe_aside() {
161 let fs = RecordingFs::with(&["/d/app", "/d/.update-x"]);
162 replace_windows(&fs, Path::new("/d/app"), Path::new("/d/.update-x")).unwrap();
163 assert_eq!(
164 fs.calls(),
165 ["rename /d/app /d/app.old", "rename /d/.update-x /d/app"]
166 );
167 }
168
169 #[test]
170 fn windows_removes_an_earlier_leftover_first() {
171 let fs = RecordingFs::with(&["/d/app", "/d/app.old", "/d/app.old.1"]);
172 replace_windows(&fs, Path::new("/d/app"), Path::new("/d/.update-x")).unwrap();
173 assert_eq!(
174 fs.calls(),
175 [
176 "remove /d/app.old",
177 "remove /d/app.old.1",
178 "rename /d/app /d/app.old",
179 "rename /d/.update-x /d/app"
180 ]
181 );
182 }
183
184 #[test]
185 fn windows_goes_on_when_the_leftover_is_locked() {
186 let fs = RecordingFs::with(&["/d/app", "/d/app.old"]).failing("remove /d/app.old");
187 replace_windows(&fs, Path::new("/d/app"), Path::new("/d/.update-x")).unwrap();
188 assert_eq!(
189 fs.calls(),
190 [
191 "remove /d/app.old",
192 "rename /d/app /d/app.old.1",
193 "rename /d/.update-x /d/app"
194 ]
195 );
196 }
197
198 #[test]
199 fn windows_puts_the_old_one_back_when_the_move_fails() {
200 let fs = RecordingFs::with(&["/d/app"]).failing("rename /d/.update-x /d/app");
201 let result = replace_windows(&fs, Path::new("/d/app"), Path::new("/d/.update-x"));
202 assert!(result.is_err());
203 assert_eq!(fs.calls().last().unwrap(), "rename /d/app.old /d/app");
204 }
205
206 #[test]
207 fn the_real_thing_replaces_content_and_keeps_it_executable() {
208 let tmp = tempfile::tempdir().unwrap();
209 let f = tmp.path().join("f");
210 std::fs::write(&f, b"old").unwrap();
211 #[cfg(unix)]
212 {
213 use std::os::unix::fs::PermissionsExt;
214 std::fs::set_permissions(&f, std::fs::Permissions::from_mode(0o755)).unwrap();
215 }
216 replace_file(&f, b"new").unwrap();
217 assert_eq!(std::fs::read(&f).unwrap(), b"new");
218 #[cfg(unix)]
219 {
220 use std::os::unix::fs::PermissionsExt;
221 assert_eq!(
222 std::fs::metadata(&f).unwrap().permissions().mode() & 0o777,
223 0o755
224 );
225 }
226 let left = std::fs::read_dir(tmp.path()).unwrap().count();
228 assert_eq!(left, if cfg!(windows) { 2 } else { 1 });
229 }
230
231 #[test]
232 #[cfg(unix)]
233 fn a_failed_replace_leaves_no_staging_file() {
234 let tmp = tempfile::tempdir().unwrap();
235 let dir = tmp.path().join("d");
237 std::fs::create_dir(&dir).unwrap();
238 assert!(replace_file(&dir, b"new").is_err());
239 let left: Vec<_> = std::fs::read_dir(tmp.path()).unwrap().collect();
240 assert_eq!(left.len(), 1);
241 assert_eq!(left[0].as_ref().unwrap().path(), dir);
242 }
243}