agent_runtime/
live_surface.rs1use crate::install::link_map::{EntryKind, LinkMap};
7use std::collections::BTreeSet;
8use std::fs;
9use std::path::{Component, Path, PathBuf};
10
11pub fn expected_live_paths(source_root: &Path, link_map: &LinkMap) -> BTreeSet<PathBuf> {
12 let mut out = BTreeSet::new();
13 for entry in &link_map.entries {
14 let Some(dest) = clean_rel_path(&entry.destination) else {
15 continue;
16 };
17 match entry.kind {
18 EntryKind::SymlinkedFile if entry.recursive => {
19 let Some(source) = entry.source.as_deref().and_then(clean_rel_path) else {
20 continue;
21 };
22 let source_abs = source_root.join(source);
23 if source_abs.is_dir() {
24 for rel in source_files(&source_abs) {
25 out.insert(dest.join(rel));
26 }
27 } else if source_abs.exists() {
28 out.insert(dest);
29 }
30 }
31 EntryKind::SymlinkedFile
32 | EntryKind::PluginManifestCopy
33 | EntryKind::BackedUpOnReplace
34 | EntryKind::ManagedBlock => {
35 out.insert(dest);
36 }
37 }
38 }
39 out
40}
41
42pub fn scan_roots(link_map: &LinkMap) -> BTreeSet<PathBuf> {
43 let mut out = BTreeSet::new();
44 for entry in &link_map.entries {
45 let Some(dest) = clean_rel_path(&entry.destination) else {
46 continue;
47 };
48 match entry.kind {
49 EntryKind::SymlinkedFile if entry.recursive => {
50 out.insert(dest);
51 }
52 EntryKind::SymlinkedFile
53 | EntryKind::PluginManifestCopy
54 | EntryKind::BackedUpOnReplace => {
55 if let Some(parent) = dest.parent()
56 && !parent.as_os_str().is_empty()
57 {
58 out.insert(parent.to_path_buf());
59 }
60 }
61 EntryKind::ManagedBlock => {}
62 }
63 }
64 out
65}
66
67pub fn live_files_under_roots(
68 live_home: &Path,
69 roots: &BTreeSet<PathBuf>,
70) -> Result<BTreeSet<PathBuf>, std::io::Error> {
71 let mut out = BTreeSet::new();
72 for rel_root in roots {
73 collect_live_files(live_home, rel_root, &mut out)?;
74 }
75 Ok(out)
76}
77
78pub fn clean_rel_path(path: impl AsRef<str>) -> Option<PathBuf> {
79 let path = Path::new(path.as_ref());
80 if path.as_os_str().is_empty() || path.is_absolute() {
81 return None;
82 }
83 if path
84 .components()
85 .any(|component| !matches!(component, Component::Normal(_)))
86 {
87 return None;
88 }
89 Some(path.to_path_buf())
90}
91
92pub fn ignored_live_file(path: &Path) -> bool {
93 path.file_name().and_then(|name| name.to_str()) == Some(".DS_Store")
94}
95
96fn source_files(root: &Path) -> BTreeSet<PathBuf> {
97 let mut out = BTreeSet::new();
98 collect_source_files(root, root, &mut out);
99 out
100}
101
102fn collect_source_files(root: &Path, dir: &Path, out: &mut BTreeSet<PathBuf>) {
103 let Ok(entries) = fs::read_dir(dir) else {
104 return;
105 };
106 for entry in entries.flatten() {
107 let path = entry.path();
108 let Ok(kind) = entry.file_type() else {
109 continue;
110 };
111 if kind.is_dir() {
112 collect_source_files(root, &path, out);
113 } else if let Ok(rel) = path.strip_prefix(root) {
114 out.insert(rel.to_path_buf());
115 }
116 }
117}
118
119fn collect_live_files(
120 live_home: &Path,
121 rel: &Path,
122 out: &mut BTreeSet<PathBuf>,
123) -> Result<(), std::io::Error> {
124 let path = live_home.join(rel);
125 let Ok(meta) = fs::symlink_metadata(&path) else {
126 return Ok(());
127 };
128 if meta.file_type().is_symlink() || meta.is_file() {
129 out.insert(rel.to_path_buf());
130 return Ok(());
131 }
132 if !meta.is_dir() {
133 return Ok(());
134 }
135
136 let entries = fs::read_dir(&path)?;
137 for entry in entries {
138 let entry = entry?;
139 collect_live_files(live_home, &rel.join(entry.file_name()), out)?;
140 }
141 Ok(())
142}