agentmux_srv/backend/remote/
wsl_fs.rs1pub const SHARE_HOST: &str = "wsl.localhost";
17const LEGACY_SHARE_HOST: &str = "wsl$";
18
19#[derive(Debug, Clone, PartialEq, Eq)]
21pub struct SharePath {
22 pub distro: String,
23 pub linux: String,
25}
26
27pub fn share_of(path: &std::path::Path) -> Option<SharePath> {
32 if cfg!(windows) {
33 split_share(&path.to_string_lossy())
34 } else {
35 None
36 }
37}
38
39pub fn split_share(path: &str) -> Option<SharePath> {
44 let rest = strip_unc_prefix(path)?;
45 let mut parts = rest.split(['\\', '/']).filter(|p| !p.is_empty());
46 let host = parts.next()?;
47 if !host.eq_ignore_ascii_case(SHARE_HOST) && !host.eq_ignore_ascii_case(LEGACY_SHARE_HOST) {
48 return None;
49 }
50 let distro = parts
51 .next()
52 .filter(|d| *d != "." && *d != "..")?
53 .to_string();
54 let mut linux: Vec<&str> = Vec::new();
55 for part in parts {
56 match part {
57 "." => {}
58 ".." => {
59 linux.pop();
60 }
61 p => linux.push(p),
62 }
63 }
64 Some(SharePath {
65 distro,
66 linux: format!("/{}", linux.join("/")),
67 })
68}
69
70fn strip_unc_prefix(path: &str) -> Option<&str> {
71 [r"\\?\UNC\", "//?/UNC/", r"\\", "//"]
72 .into_iter()
73 .find(|prefix| {
74 path.get(..prefix.len())
75 .is_some_and(|p| p.eq_ignore_ascii_case(prefix))
76 })
77 .map(|prefix| &path[prefix.len()..])
78}
79
80pub fn share_path(distro: &str, linux: &str) -> String {
82 let mut out = format!(r"\\{SHARE_HOST}\{distro}\");
83 out.push_str(&linux.trim_start_matches('/').replace('/', "\\"));
84 out
85}
86
87const SYSTEM_DIRS: &[&str] = &[
91 "/usr", "/bin", "/sbin", "/etc", "/var", "/boot", "/lib", "/lib32", "/lib64", "/proc", "/sys",
92 "/dev", "/mnt", "/init", "/run",
93];
94
95pub fn is_system_path(linux: &str) -> bool {
97 linux == "/"
98 || SYSTEM_DIRS
99 .iter()
100 .any(|d| linux == *d || linux.starts_with(&format!("{d}/")))
101}
102
103pub fn drive_path(linux: &str) -> Option<String> {
106 let rest = linux.strip_prefix("/mnt/")?;
107 let (letter, tail) = rest.split_once('/').unwrap_or((rest, ""));
108 let mut chars = letter.chars();
109 let c = chars.next().filter(|c| c.is_ascii_alphabetic())?;
110 if chars.next().is_some() {
111 return None;
112 }
113 Some(format!(
114 "{}:\\{}",
115 c.to_ascii_uppercase(),
116 tail.replace('/', "\\")
117 ))
118}
119
120pub fn is_home(linux: &str) -> bool {
122 linux == "/root"
123 || linux
124 .strip_prefix("/home/")
125 .is_some_and(|u| !u.is_empty() && !u.contains('/'))
126}
127
128pub fn in_home(linux: &str) -> bool {
131 is_home(linux)
132 || linux.starts_with("/root/")
133 || linux
134 .strip_prefix("/home/")
135 .is_some_and(|u| u.split_once('/').is_some_and(|(user, _)| !user.is_empty()))
136}
137
138#[cfg(test)]
139mod tests {
140 use super::*;
141
142 fn sp(distro: &str, linux: &str) -> Option<SharePath> {
143 Some(SharePath {
144 distro: distro.to_string(),
145 linux: linux.to_string(),
146 })
147 }
148
149 #[test]
150 fn every_spelling_of_a_share_path_splits_the_same_way() {
151 for path in [
152 r"\\wsl.localhost\Ubuntu\home\u\proj",
153 r"\\WSL.LOCALHOST\Ubuntu\home\u\proj\",
154 r"\\wsl$\Ubuntu\home\u\proj",
155 r"\\?\UNC\wsl.localhost\Ubuntu\home\u\proj",
156 "//wsl.localhost/Ubuntu/home/u/proj",
157 r"\\wsl.localhost\Ubuntu\home\u\.\x\..\proj",
158 ] {
159 assert_eq!(split_share(path), sp("Ubuntu", "/home/u/proj"), "{path}");
160 }
161 assert_eq!(split_share(r"\\wsl.localhost\Ubuntu"), sp("Ubuntu", "/"));
162 assert_eq!(
163 split_share(r"\\wsl.localhost\Ubuntu\..\..\x"),
164 sp("Ubuntu", "/x")
165 );
166 }
167
168 #[test]
169 fn other_paths_are_not_share_paths() {
170 for path in [
171 r"C:\Users\u",
172 r"\\?\C:\Users\u",
173 r"\\server\share\x",
174 r"\\wsl.localhost",
175 r"\\wsl.localhost\..\x",
176 "/home/u",
177 "",
178 ] {
179 assert_eq!(split_share(path), None, "{path}");
180 }
181 }
182
183 #[test]
185 fn a_share_path_is_one_only_on_windows() {
186 let p = std::path::Path::new("//wsl.localhost/Ubuntu/home/u/x");
187 assert_eq!(share_of(p).is_some(), cfg!(windows));
188 }
189
190 #[test]
191 fn share_paths_round_trip() {
192 assert_eq!(share_path("Ubuntu", "/"), r"\\wsl.localhost\Ubuntu\");
193 let p = share_path("Ubuntu", "/home/u/proj");
194 assert_eq!(p, r"\\wsl.localhost\Ubuntu\home\u\proj");
195 assert_eq!(split_share(&p), sp("Ubuntu", "/home/u/proj"));
196 }
197
198 #[test]
199 fn a_distro_has_system_folders_and_homes() {
200 for p in [
201 "/",
202 "/usr",
203 "/usr/bin/ls",
204 "/etc/passwd",
205 "/mnt",
206 "/mnt/wsl",
207 "/init",
208 ] {
209 assert!(is_system_path(p), "{p}");
210 }
211 for p in [
212 "/home/u",
213 "/root/x",
214 "/srv/app",
215 "/usrlocal",
216 "/tmp/x",
217 "/opt/tool",
218 ] {
219 assert!(!is_system_path(p), "{p}");
220 }
221 assert!(is_home("/home/u") && is_home("/root"));
222 assert!(!is_home("/home") && !is_home("/home/u/x"));
223 assert!(in_home("/home/u") && in_home("/home/u/proj/a.rs") && in_home("/root/x"));
224 assert!(
225 !in_home("/home")
226 && !in_home("/srv/app")
227 && !in_home("/rootx")
228 && !in_home("/mnt/c/Users/u")
229 );
230 }
231
232 #[test]
233 fn a_drive_under_mnt_is_its_windows_path() {
234 assert_eq!(drive_path("/mnt/c"), Some(r"C:\".to_string()));
235 assert_eq!(
236 drive_path("/mnt/c/Windows/System32"),
237 Some(r"C:\Windows\System32".to_string())
238 );
239 assert_eq!(drive_path("/mnt/d/x"), Some(r"D:\x".to_string()));
240 assert_eq!(drive_path("/mnt/wsl"), None);
241 assert_eq!(drive_path("/mnt"), None);
242 assert_eq!(drive_path("/home/u"), None);
243 }
244}