lean_ctx/core/
pathutil.rs1use std::path::{Path, PathBuf};
2
3pub fn safe_canonicalize(path: &Path) -> std::io::Result<PathBuf> {
9 let canon = std::fs::canonicalize(path)?;
10 Ok(strip_verbatim(canon))
11}
12
13pub fn safe_canonicalize_or_self(path: &Path) -> PathBuf {
15 safe_canonicalize(path).unwrap_or_else(|_| path.to_path_buf())
16}
17
18pub fn safe_canonicalize_bounded(path: &Path, timeout_ms: u64) -> PathBuf {
22 #[cfg(windows)]
23 {
24 let path_owned = path.to_path_buf();
25 let (tx, rx) = std::sync::mpsc::channel();
26 let _ = std::thread::Builder::new()
27 .name("canonicalize-bounded".into())
28 .spawn(move || {
29 let result = safe_canonicalize(&path_owned).unwrap_or_else(|_| path_owned);
30 let _ = tx.send(result);
31 });
32 match rx.recv_timeout(std::time::Duration::from_millis(timeout_ms)) {
33 Ok(canonical) => canonical,
34 Err(_) => {
35 tracing::debug!(
36 "canonicalize timed out ({}ms) for {}; using original path",
37 timeout_ms,
38 path.display()
39 );
40 path.to_path_buf()
41 }
42 }
43 }
44 #[cfg(not(windows))]
45 {
46 let _ = timeout_ms;
47 safe_canonicalize_or_self(path)
48 }
49}
50
51pub fn strip_verbatim(path: PathBuf) -> PathBuf {
54 let s = path.to_string_lossy();
55 if let Some(stripped) = strip_verbatim_str(&s) {
56 PathBuf::from(stripped)
57 } else {
58 path
59 }
60}
61
62pub fn strip_verbatim_str(path: &str) -> Option<String> {
65 let normalized = path.replace('\\', "/");
66
67 if let Some(rest) = normalized.strip_prefix("//?/UNC/") {
68 Some(format!("//{rest}"))
69 } else {
70 normalized
71 .strip_prefix("//?/")
72 .map(std::string::ToString::to_string)
73 }
74}
75
76pub fn normalize_tool_path(path: &str) -> String {
80 let mut p = match strip_verbatim_str(path) {
81 Some(stripped) => stripped,
82 None => path.to_string(),
83 };
84
85 if p.len() >= 3
87 && p.starts_with('/')
88 && p.as_bytes()[1].is_ascii_alphabetic()
89 && p.as_bytes()[2] == b'/'
90 {
91 let drive = p.as_bytes()[1].to_ascii_uppercase() as char;
92 p = format!("{drive}:{}", &p[2..]);
93 }
94
95 p = p.replace('\\', "/");
96
97 while p.contains("//") && !p.starts_with("//") {
99 p = p.replace("//", "/");
100 }
101
102 if p.len() > 1 && p.ends_with('/') && !p.ends_with(":/") {
104 p.pop();
105 }
106
107 p
108}
109
110#[cfg(test)]
111mod tests {
112 use super::*;
113
114 #[test]
115 fn strip_regular_verbatim() {
116 let p = PathBuf::from(r"\\?\C:\Users\dev\project");
117 let result = strip_verbatim(p);
118 assert_eq!(result, PathBuf::from("C:/Users/dev/project"));
119 }
120
121 #[test]
122 fn strip_unc_verbatim() {
123 let p = PathBuf::from(r"\\?\UNC\server\share\dir");
124 let result = strip_verbatim(p);
125 assert_eq!(result, PathBuf::from("//server/share/dir"));
126 }
127
128 #[test]
129 fn no_prefix_unchanged() {
130 let p = PathBuf::from("/home/user/project");
131 let result = strip_verbatim(p.clone());
132 assert_eq!(result, p);
133 }
134
135 #[test]
136 fn windows_drive_unchanged() {
137 let p = PathBuf::from("C:/Users/dev");
138 let result = strip_verbatim(p.clone());
139 assert_eq!(result, p);
140 }
141
142 #[test]
143 fn strip_str_regular() {
144 assert_eq!(
145 strip_verbatim_str(r"\\?\E:\code\lean-ctx"),
146 Some("E:/code/lean-ctx".to_string())
147 );
148 }
149
150 #[test]
151 fn strip_str_unc() {
152 assert_eq!(
153 strip_verbatim_str(r"\\?\UNC\myserver\data"),
154 Some("//myserver/data".to_string())
155 );
156 }
157
158 #[test]
159 fn strip_str_forward_slash_variant() {
160 assert_eq!(
161 strip_verbatim_str("//?/C:/Users/dev"),
162 Some("C:/Users/dev".to_string())
163 );
164 }
165
166 #[test]
167 fn strip_str_no_prefix() {
168 assert_eq!(strip_verbatim_str("/home/user"), None);
169 }
170
171 #[test]
172 fn safe_canonicalize_or_self_nonexistent() {
173 let p = Path::new("/this/path/should/not/exist/xyzzy");
174 let result = safe_canonicalize_or_self(p);
175 assert_eq!(result, p.to_path_buf());
176 }
177
178 #[test]
179 fn normalize_msys_path_to_native() {
180 assert_eq!(
181 normalize_tool_path("/c/Users/ABC/AppData/lean-ctx"),
182 "C:/Users/ABC/AppData/lean-ctx"
183 );
184 }
185
186 #[test]
187 fn normalize_msys_uppercase_drive() {
188 assert_eq!(
189 normalize_tool_path("/D/Program Files/lean-ctx.exe"),
190 "D:/Program Files/lean-ctx.exe"
191 );
192 }
193
194 #[test]
195 fn normalize_native_windows_path_unchanged() {
196 assert_eq!(
197 normalize_tool_path("C:/Users/ABC/lean-ctx.exe"),
198 "C:/Users/ABC/lean-ctx.exe"
199 );
200 }
201
202 #[test]
203 fn normalize_backslash_windows_path() {
204 assert_eq!(
205 normalize_tool_path(r"C:\Users\ABC\lean-ctx.exe"),
206 "C:/Users/ABC/lean-ctx.exe"
207 );
208 }
209
210 #[test]
211 fn normalize_unix_path_unchanged() {
212 assert_eq!(
213 normalize_tool_path("/usr/local/bin/lean-ctx"),
214 "/usr/local/bin/lean-ctx"
215 );
216 }
217
218 #[test]
219 fn normalize_double_slashes() {
220 assert_eq!(
221 normalize_tool_path("C:/Users//ABC//lean-ctx"),
222 "C:/Users/ABC/lean-ctx"
223 );
224 }
225
226 #[test]
227 fn normalize_trailing_slash_removed() {
228 assert_eq!(normalize_tool_path("/c/Users/ABC/"), "C:/Users/ABC");
229 }
230
231 #[test]
232 fn normalize_root_slash_preserved() {
233 assert_eq!(normalize_tool_path("/"), "/");
234 }
235
236 #[test]
237 fn normalize_drive_root_preserved() {
238 assert_eq!(normalize_tool_path("C:/"), "C:/");
239 }
240
241 #[test]
242 fn normalize_verbatim_with_msys() {
243 assert_eq!(normalize_tool_path(r"\\?\C:\Users\dev"), "C:/Users/dev");
244 }
245}