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.strip_prefix("//?/").map(|rest| rest.to_string())
38    }
39}
40
41#[cfg(test)]
42mod tests {
43    use super::*;
44
45    #[test]
46    fn strip_regular_verbatim() {
47        let p = PathBuf::from(r"\\?\C:\Users\dev\project");
48        let result = strip_verbatim(p);
49        assert_eq!(result, PathBuf::from("C:/Users/dev/project"));
50    }
51
52    #[test]
53    fn strip_unc_verbatim() {
54        let p = PathBuf::from(r"\\?\UNC\server\share\dir");
55        let result = strip_verbatim(p);
56        assert_eq!(result, PathBuf::from("//server/share/dir"));
57    }
58
59    #[test]
60    fn no_prefix_unchanged() {
61        let p = PathBuf::from("/home/user/project");
62        let result = strip_verbatim(p.clone());
63        assert_eq!(result, p);
64    }
65
66    #[test]
67    fn windows_drive_unchanged() {
68        let p = PathBuf::from("C:/Users/dev");
69        let result = strip_verbatim(p.clone());
70        assert_eq!(result, p);
71    }
72
73    #[test]
74    fn strip_str_regular() {
75        assert_eq!(
76            strip_verbatim_str(r"\\?\E:\code\lean-ctx"),
77            Some("E:/code/lean-ctx".to_string())
78        );
79    }
80
81    #[test]
82    fn strip_str_unc() {
83        assert_eq!(
84            strip_verbatim_str(r"\\?\UNC\myserver\data"),
85            Some("//myserver/data".to_string())
86        );
87    }
88
89    #[test]
90    fn strip_str_forward_slash_variant() {
91        assert_eq!(
92            strip_verbatim_str("//?/C:/Users/dev"),
93            Some("C:/Users/dev".to_string())
94        );
95    }
96
97    #[test]
98    fn strip_str_no_prefix() {
99        assert_eq!(strip_verbatim_str("/home/user"), None);
100    }
101
102    #[test]
103    fn safe_canonicalize_or_self_nonexistent() {
104        let p = Path::new("/this/path/should/not/exist/xyzzy");
105        let result = safe_canonicalize_or_self(p);
106        assert_eq!(result, p.to_path_buf());
107    }
108}