Skip to main content

lean_ctx/core/
pathutil.rs

1use std::path::{Path, PathBuf};
2
3/// Canonicalize a path and strip the Windows verbatim/extended-length prefix (`\\?\`)
4/// that `std::fs::canonicalize` adds on Windows. This prefix breaks many tools and
5/// string-based path comparisons.
6///
7/// On non-Windows platforms this is equivalent to `std::fs::canonicalize`.
8pub fn safe_canonicalize(path: &Path) -> std::io::Result<PathBuf> {
9    let canon = std::fs::canonicalize(path)?;
10    Ok(strip_verbatim(canon))
11}
12
13/// Like `safe_canonicalize` but returns the original path on failure.
14pub fn safe_canonicalize_or_self(path: &Path) -> PathBuf {
15    safe_canonicalize(path).unwrap_or_else(|_| path.to_path_buf())
16}
17
18/// Remove the `\\?\` / `//?/` verbatim prefix from a `PathBuf`.
19/// Handles both regular verbatim (`\\?\C:\...`) and UNC verbatim (`\\?\UNC\...`).
20pub 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
29/// Remove the `\\?\` / `//?/` verbatim prefix from a path string.
30/// Returns `Some(cleaned)` if a prefix was found, `None` otherwise.
31pub 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
43#[cfg(test)]
44mod tests {
45    use super::*;
46
47    #[test]
48    fn strip_regular_verbatim() {
49        let p = PathBuf::from(r"\\?\C:\Users\dev\project");
50        let result = strip_verbatim(p);
51        assert_eq!(result, PathBuf::from("C:/Users/dev/project"));
52    }
53
54    #[test]
55    fn strip_unc_verbatim() {
56        let p = PathBuf::from(r"\\?\UNC\server\share\dir");
57        let result = strip_verbatim(p);
58        assert_eq!(result, PathBuf::from("//server/share/dir"));
59    }
60
61    #[test]
62    fn no_prefix_unchanged() {
63        let p = PathBuf::from("/home/user/project");
64        let result = strip_verbatim(p.clone());
65        assert_eq!(result, p);
66    }
67
68    #[test]
69    fn windows_drive_unchanged() {
70        let p = PathBuf::from("C:/Users/dev");
71        let result = strip_verbatim(p.clone());
72        assert_eq!(result, p);
73    }
74
75    #[test]
76    fn strip_str_regular() {
77        assert_eq!(
78            strip_verbatim_str(r"\\?\E:\code\lean-ctx"),
79            Some("E:/code/lean-ctx".to_string())
80        );
81    }
82
83    #[test]
84    fn strip_str_unc() {
85        assert_eq!(
86            strip_verbatim_str(r"\\?\UNC\myserver\data"),
87            Some("//myserver/data".to_string())
88        );
89    }
90
91    #[test]
92    fn strip_str_forward_slash_variant() {
93        assert_eq!(
94            strip_verbatim_str("//?/C:/Users/dev"),
95            Some("C:/Users/dev".to_string())
96        );
97    }
98
99    #[test]
100    fn strip_str_no_prefix() {
101        assert_eq!(strip_verbatim_str("/home/user"), None);
102    }
103
104    #[test]
105    fn safe_canonicalize_or_self_nonexistent() {
106        let p = Path::new("/this/path/should/not/exist/xyzzy");
107        let result = safe_canonicalize_or_self(p);
108        assert_eq!(result, p.to_path_buf());
109    }
110}