atomcode_core/uninstall/
scan.rs1use std::fs;
4use std::path::{Path, PathBuf};
5
6use anyhow::Result;
7
8use super::paths::uninstall_manifest;
9use super::{Group, Item};
10
11#[derive(Debug)]
12pub struct Plan {
13 pub items: Vec<Item>,
14 pub binary_path: PathBuf,
15 pub atomcode_dir: PathBuf,
16}
17
18pub fn scan(binary_path: &Path, atomcode_dir: &Path) -> Result<Plan> {
21 let mut items = Vec::new();
22
23 if binary_path.exists() {
25 items.push(item(Group::Binary, binary_path.to_path_buf(), "binary")?);
26 }
27 if let Some(dir) = binary_path.parent() {
28 let bak_name = {
30 let mut s = binary_path.file_name().unwrap_or_default().to_os_string();
31 s.push(".bak");
32 s
33 };
34 let p = dir.join(&bak_name);
35 if p.exists() {
36 items.push(item(Group::Binary, p, "self-update backup")?);
37 }
38
39 for (name, note) in [
40 (".atomcode.rolling", "self-update rename slot"),
41 (".atomcode.download", "self-update partial download"),
42 (".atomcode.writable-probe", "self-update probe leftover"),
43 ] {
44 let p = dir.join(name);
45 if p.exists() {
46 items.push(item(Group::Binary, p, note)?);
47 }
48 }
49 }
50
51 let m = uninstall_manifest();
53 for fname in m.credential_files {
54 let p = atomcode_dir.join(fname);
55 if p.exists() {
56 items.push(item(Group::Credentials, p, fname)?);
57 }
58 }
59
60 for fname in m.state_files {
62 let p = atomcode_dir.join(fname);
63 if p.exists() {
64 items.push(item(Group::State, p, fname)?);
65 }
66 }
67 for dname in m.state_dirs {
68 let p = atomcode_dir.join(dname);
69 if p.exists() {
70 items.push(item(Group::State, p, dname)?);
71 }
72 }
73 if atomcode_dir.exists() {
74 for entry in fs::read_dir(atomcode_dir)? {
75 let entry = entry?;
76 let name = entry.file_name();
77 let name_str = name.to_string_lossy();
78 for prefix in m.state_prefixes {
79 if name_str.starts_with(prefix) {
80 items.push(item(Group::State, entry.path(), "notice marker")?);
81 break;
82 }
83 }
84 }
85 }
86
87 Ok(Plan {
88 items,
89 binary_path: binary_path.to_path_buf(),
90 atomcode_dir: atomcode_dir.to_path_buf(),
91 })
92}
93
94fn item(group: Group, path: PathBuf, note: &'static str) -> Result<Item> {
95 let size = if path.is_dir() {
96 dir_size(&path).unwrap_or(0)
97 } else {
98 fs::metadata(&path).map(|m| m.len()).unwrap_or(0)
99 };
100 let needs_privilege = needs_privilege_to_remove(&path);
101 Ok(Item {
102 group,
103 path,
104 size_bytes: size,
105 note,
106 needs_privilege,
107 })
108}
109
110fn dir_size(p: &Path) -> Result<u64> {
111 let mut total = 0u64;
112 for entry in fs::read_dir(p)? {
113 let entry = entry?;
114 let md = entry.metadata()?;
115 if md.is_dir() {
116 total = total.saturating_add(dir_size(&entry.path()).unwrap_or(0));
117 } else {
118 total = total.saturating_add(md.len());
119 }
120 }
121 Ok(total)
122}
123
124#[cfg(unix)]
125fn needs_privilege_to_remove(p: &Path) -> bool {
126 use std::os::unix::ffi::OsStrExt;
127 let parent = match p.parent() {
128 Some(parent) => parent,
129 None => return false,
130 };
131 let c_path = match std::ffi::CString::new(parent.as_os_str().as_bytes()) {
132 Ok(s) => s,
133 Err(_) => return true, };
135 unsafe { libc::access(c_path.as_ptr(), libc::W_OK) != 0 }
137}
138
139#[cfg(not(unix))]
140fn needs_privilege_to_remove(_p: &Path) -> bool {
141 false
142}
143
144#[cfg(test)]
145mod tests {
146 use super::*;
147 use std::fs;
148 use tempfile::TempDir;
149
150 fn make_fake_install(tmp: &TempDir) -> (std::path::PathBuf, std::path::PathBuf) {
151 let bin_dir = tmp.path().join("bin");
152 fs::create_dir(&bin_dir).unwrap();
153 let exe = bin_dir.join("atomcode");
154 fs::write(&exe, b"\x7fELF......").unwrap();
155 fs::write(bin_dir.join("atomcode.bak"), b"old").unwrap();
157 fs::write(bin_dir.join(".atomcode.rolling"), b"r").unwrap();
158
159 let data = tmp.path().join(".atomcode");
160 fs::create_dir(&data).unwrap();
161 fs::write(data.join("auth.toml"), b"k=1").unwrap();
162 fs::write(data.join("config.toml"), b"x=1").unwrap();
163 fs::write(data.join("history"), b"hi").unwrap();
164 fs::create_dir(data.join("plugins")).unwrap();
165 fs::write(data.join("plugins/.gitkeep"), b"").unwrap();
166 fs::create_dir(data.join("staged")).unwrap();
167 (exe, data)
168 }
169
170 #[test]
171 fn scan_finds_binary_and_artifacts() {
172 let tmp = TempDir::new().unwrap();
173 let (exe, data) = make_fake_install(&tmp);
174 let plan = scan(&exe, &data).unwrap();
175 let bin_paths: Vec<_> = plan
176 .items
177 .iter()
178 .filter(|i| i.group == Group::Binary)
179 .map(|i| i.path.clone())
180 .collect();
181 assert!(bin_paths.contains(&exe));
182 assert!(bin_paths.contains(&exe.with_file_name("atomcode.bak")));
183 assert!(bin_paths.contains(&exe.with_file_name(".atomcode.rolling")));
184 }
185
186 #[test]
187 fn scan_classifies_credentials_and_state() {
188 let tmp = TempDir::new().unwrap();
189 let (exe, data) = make_fake_install(&tmp);
190 let plan = scan(&exe, &data).unwrap();
191 let creds: Vec<_> = plan
192 .items
193 .iter()
194 .filter(|i| i.group == Group::Credentials)
195 .map(|i| i.path.clone())
196 .collect();
197 assert!(creds.contains(&data.join("auth.toml")));
198 assert!(creds.contains(&data.join("config.toml")));
199
200 let state: Vec<_> = plan
201 .items
202 .iter()
203 .filter(|i| i.group == Group::State)
204 .map(|i| i.path.clone())
205 .collect();
206 assert!(state.contains(&data.join("history")));
207 assert!(state.contains(&data.join("plugins")));
208 assert!(state.contains(&data.join("staged")));
209 }
210
211 #[test]
212 fn scan_skips_missing_files_silently() {
213 let tmp = TempDir::new().unwrap();
214 let exe = tmp.path().join("atomcode");
215 std::fs::write(&exe, b"x").unwrap();
216 let data = tmp.path().join("nonexistent");
217 let plan = scan(&exe, &data).unwrap();
218 assert_eq!(
220 plan.items
221 .iter()
222 .filter(|i| i.group == Group::Binary)
223 .count(),
224 1
225 );
226 assert_eq!(
227 plan.items
228 .iter()
229 .filter(|i| i.group != Group::Binary)
230 .count(),
231 0
232 );
233 }
234
235 #[test]
236 #[cfg(unix)]
237 fn needs_privilege_false_for_user_writable_tempdir() {
238 let tmp = TempDir::new().unwrap();
239 let p = tmp.path().join("file");
240 std::fs::write(&p, b"x").unwrap();
241 assert!(!needs_privilege_to_remove(&p));
243 }
244
245 #[test]
246 #[cfg(unix)]
247 fn needs_privilege_true_for_path_with_no_parent() {
248 let p = std::path::Path::new("/");
250 let _ = needs_privilege_to_remove(p);
252 }
253
254 #[test]
255 fn scan_returns_items_in_group_order() {
256 let tmp = TempDir::new().unwrap();
257 let (exe, data) = make_fake_install(&tmp);
258 let plan = scan(&exe, &data).unwrap();
259 let groups: Vec<_> = plan.items.iter().map(|i| i.group).collect();
260 let first_cred = groups.iter().position(|g| *g == Group::Credentials);
261 let first_state = groups.iter().position(|g| *g == Group::State);
262 let last_bin = groups.iter().rposition(|g| *g == Group::Binary);
263 if let (Some(lb), Some(fc)) = (last_bin, first_cred) {
264 assert!(lb < fc);
265 }
266 if let (Some(fc), Some(fs)) = (first_cred, first_state) {
267 assert!(fc < fs);
268 }
269 }
270}