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 strip_verbatim(path: PathBuf) -> PathBuf {
21 let s = path.to_string_lossy();
22 if let Some(stripped) = strip_verbatim_str(&s) {
23 PathBuf::from(stripped)
24 } else {
25 path
26 }
27}
28
29pub fn strip_verbatim_str(path: &str) -> Option<String> {
32 let normalized = path.replace('\\', "/");
33
34 if let Some(rest) = normalized.strip_prefix("//?/UNC/") {
35 Some(format!("//{rest}"))
36 } else {
37 normalized
38 .strip_prefix("//?/")
39 .map(std::string::ToString::to_string)
40 }
41}
42
43pub fn normalize_tool_path(path: &str) -> String {
47 let mut p = match strip_verbatim_str(path) {
48 Some(stripped) => stripped,
49 None => path.to_string(),
50 };
51
52 if p.len() >= 3
54 && p.starts_with('/')
55 && p.as_bytes()[1].is_ascii_alphabetic()
56 && p.as_bytes()[2] == b'/'
57 {
58 let drive = p.as_bytes()[1].to_ascii_uppercase() as char;
59 p = format!("{drive}:{}", &p[2..]);
60 }
61
62 p = p.replace('\\', "/");
63
64 while p.contains("//") && !p.starts_with("//") {
66 p = p.replace("//", "/");
67 }
68
69 if p.len() > 1 && p.ends_with('/') && !p.ends_with(":/") {
71 p.pop();
72 }
73
74 p
75}
76
77#[cfg(test)]
78mod tests {
79 use super::*;
80
81 #[test]
82 fn strip_regular_verbatim() {
83 let p = PathBuf::from(r"\\?\C:\Users\dev\project");
84 let result = strip_verbatim(p);
85 assert_eq!(result, PathBuf::from("C:/Users/dev/project"));
86 }
87
88 #[test]
89 fn strip_unc_verbatim() {
90 let p = PathBuf::from(r"\\?\UNC\server\share\dir");
91 let result = strip_verbatim(p);
92 assert_eq!(result, PathBuf::from("//server/share/dir"));
93 }
94
95 #[test]
96 fn no_prefix_unchanged() {
97 let p = PathBuf::from("/home/user/project");
98 let result = strip_verbatim(p.clone());
99 assert_eq!(result, p);
100 }
101
102 #[test]
103 fn windows_drive_unchanged() {
104 let p = PathBuf::from("C:/Users/dev");
105 let result = strip_verbatim(p.clone());
106 assert_eq!(result, p);
107 }
108
109 #[test]
110 fn strip_str_regular() {
111 assert_eq!(
112 strip_verbatim_str(r"\\?\E:\code\lean-ctx"),
113 Some("E:/code/lean-ctx".to_string())
114 );
115 }
116
117 #[test]
118 fn strip_str_unc() {
119 assert_eq!(
120 strip_verbatim_str(r"\\?\UNC\myserver\data"),
121 Some("//myserver/data".to_string())
122 );
123 }
124
125 #[test]
126 fn strip_str_forward_slash_variant() {
127 assert_eq!(
128 strip_verbatim_str("//?/C:/Users/dev"),
129 Some("C:/Users/dev".to_string())
130 );
131 }
132
133 #[test]
134 fn strip_str_no_prefix() {
135 assert_eq!(strip_verbatim_str("/home/user"), None);
136 }
137
138 #[test]
139 fn safe_canonicalize_or_self_nonexistent() {
140 let p = Path::new("/this/path/should/not/exist/xyzzy");
141 let result = safe_canonicalize_or_self(p);
142 assert_eq!(result, p.to_path_buf());
143 }
144}