Skip to main content

xtask_todo_lib/devshell/vfs/
path.rs

1//! Logical path normalization and resolution against cwd.
2
3/// Resolve `path` against `cwd` to an absolute logical path (same rules as [`super::Vfs::resolve_to_absolute`]).
4#[must_use]
5pub fn resolve_path_with_cwd(cwd: &str, path: &str) -> String {
6    let path = path.trim();
7    let path_normalized = normalize_path(path);
8    // 绝对路径:直接归一化后返回
9    if path_normalized.starts_with('/') {
10        return path_normalized;
11    }
12    if path_normalized == "/" {
13        return cwd.to_string();
14    }
15    // 相对路径:先与 cwd 拼接再归一化,避免单独 ".." 被归一成 "." 导致无法退回根目录
16    let base = cwd.trim_end_matches('/');
17    let p = path.trim_start_matches('/');
18    let combined = if base.is_empty() {
19        format!("/{p}")
20    } else {
21        format!("{base}/{p}")
22    };
23    let result = normalize_path(&combined);
24    if result.is_empty() || result == "." {
25        "/".to_string()
26    } else if result.starts_with('/') {
27        result
28    } else {
29        format!("/{result}")
30    }
31}
32
33/// Normalize a path to Unix style: backslash -> slash, strip Windows drive,
34/// resolve . and .., preserve absolute vs relative.
35#[must_use]
36pub fn normalize_path(input: &str) -> String {
37    let s = input.replace('\\', "/");
38
39    // Strip Windows drive letter prefix (e.g. C:) and treat as absolute.
40    let (rest, absolute) = if s.len() >= 2
41        && s.chars().next().is_some_and(|c| c.is_ascii_alphabetic())
42        && s.chars().nth(1) == Some(':')
43    {
44        (&s[2..], true)
45    } else {
46        (s.as_str(), s.starts_with('/'))
47    };
48
49    let rest = rest.trim_start_matches('/');
50    let mut out: Vec<&str> = Vec::new();
51    for p in rest.split('/') {
52        match p {
53            "" | "." => {}
54            ".." => {
55                out.pop();
56            }
57            _ => out.push(p),
58        }
59    }
60
61    if absolute {
62        "/".to_string() + &out.join("/")
63    } else if out.is_empty() {
64        ".".to_string()
65    } else {
66        out.join("/")
67    }
68}