Skip to main content

fallow_types/
path_util.rs

1//! Cross-platform path classification helpers.
2//!
3//! Rust's [`std::path::Path::is_absolute`] uses platform-specific semantics:
4//! on Unix a path starting with `/` is absolute, while on Windows a path needs
5//! a drive prefix (`C:\foo`) or a UNC root (`\\?\C:\foo`). A POSIX-style
6//! absolute path like `/project/foo.ts` returns `false` from `is_absolute()`
7//! on Windows, which breaks code that conditionally joins relative paths
8//! against a root.
9//!
10//! Use these helpers whenever input paths may originate from user-supplied
11//! data shared across CI runners, config files, source maps, or diff output.
12
13use std::path::{Component, Path};
14
15/// Returns `true` if `path` is anchored under either platform's path
16/// conventions.
17///
18/// Recognises host-absolute paths, POSIX-style rooted paths (`/foo`), and
19/// Windows drive-prefixed paths (`C:\foo`, `c:/foo`) regardless of the host OS.
20pub fn is_absolute_path_any_platform(path: &Path) -> bool {
21    if path.is_absolute() {
22        return true;
23    }
24    if matches!(path.components().next(), Some(Component::RootDir)) {
25        return true;
26    }
27    looks_like_windows_drive_absolute(path.as_os_str().as_encoded_bytes())
28}
29
30/// Returns `true` if `value` looks like a Windows-style absolute path
31/// with a drive letter, colon, and path separator.
32///
33/// This string-shaped variant is useful before constructing a [`Path`].
34pub fn looks_like_windows_absolute_path(value: &str) -> bool {
35    looks_like_windows_drive_absolute(value.as_bytes())
36}
37
38/// Renders `path` relative to `root` with forward slashes.
39///
40/// A path outside `root` keeps its full form. A path equal to `root` gives an
41/// empty string. The slash normalisation keeps the output the same on all
42/// platforms, so every message and report that shows a root-relative path must
43/// use this helper.
44#[must_use]
45pub fn display_relative(root: &Path, path: &Path) -> String {
46    path.strip_prefix(root)
47        .unwrap_or(path)
48        .display()
49        .to_string()
50        .replace('\\', "/")
51}
52
53fn looks_like_windows_drive_absolute(bytes: &[u8]) -> bool {
54    bytes.len() >= 3
55        && bytes[0].is_ascii_alphabetic()
56        && bytes[1] == b':'
57        && matches!(bytes[2], b'/' | b'\\')
58}
59
60#[cfg(test)]
61mod tests {
62    use super::*;
63    use std::path::PathBuf;
64
65    #[test]
66    fn display_relative_strips_root_prefix() {
67        assert_eq!(
68            display_relative(Path::new("/project"), Path::new("/project/src/a.ts")),
69            "src/a.ts"
70        );
71    }
72
73    #[test]
74    fn display_relative_keeps_path_outside_root() {
75        assert_eq!(
76            display_relative(Path::new("/project"), Path::new("/other/a.ts")),
77            "/other/a.ts"
78        );
79    }
80
81    #[test]
82    fn display_relative_normalises_backslashes() {
83        assert_eq!(
84            display_relative(Path::new("/project"), Path::new(r"packages\ui\a.ts")),
85            "packages/ui/a.ts"
86        );
87    }
88
89    #[test]
90    fn display_relative_of_root_is_empty() {
91        assert_eq!(
92            display_relative(Path::new("/project"), Path::new("/project")),
93            ""
94        );
95    }
96
97    #[test]
98    fn posix_style_root_is_absolute_on_any_platform() {
99        assert!(is_absolute_path_any_platform(Path::new(
100            "/project/src/a.ts"
101        )));
102        assert!(is_absolute_path_any_platform(Path::new("/foo")));
103        assert!(is_absolute_path_any_platform(Path::new("/")));
104    }
105
106    #[test]
107    fn windows_drive_letter_is_absolute_on_any_platform() {
108        assert!(is_absolute_path_any_platform(Path::new(
109            "C:\\project\\src\\a.ts"
110        )));
111        assert!(is_absolute_path_any_platform(Path::new(
112            "C:/project/src/a.ts"
113        )));
114        assert!(is_absolute_path_any_platform(Path::new("d:/foo")));
115    }
116
117    #[test]
118    fn relative_paths_return_false() {
119        assert!(!is_absolute_path_any_platform(Path::new("src/a.ts")));
120        assert!(!is_absolute_path_any_platform(Path::new("./src/a.ts")));
121        assert!(!is_absolute_path_any_platform(Path::new("../parent/a.ts")));
122        assert!(!is_absolute_path_any_platform(Path::new("a.ts")));
123        assert!(!is_absolute_path_any_platform(Path::new("")));
124    }
125
126    #[cfg_attr(miri, ignore)]
127    #[test]
128    fn host_absolute_works_through_is_absolute() {
129        let cwd = std::env::current_dir().expect("current_dir");
130        assert!(is_absolute_path_any_platform(&cwd));
131    }
132
133    #[test]
134    fn looks_like_windows_absolute_path_recognises_drive_shapes() {
135        assert!(looks_like_windows_absolute_path("C:\\foo"));
136        assert!(looks_like_windows_absolute_path("c:/foo"));
137        assert!(looks_like_windows_absolute_path("Z:/very/deep/path.ts"));
138        assert!(!looks_like_windows_absolute_path("/foo"));
139        assert!(!looks_like_windows_absolute_path("src/foo"));
140        assert!(!looks_like_windows_absolute_path("C:"));
141        assert!(!looks_like_windows_absolute_path("CC:/foo"));
142        assert!(!looks_like_windows_absolute_path(""));
143    }
144
145    #[test]
146    fn drive_prefix_path_string_is_absolute_via_os_str_bytes() {
147        let p = PathBuf::from("E:/source/map.js");
148        assert!(is_absolute_path_any_platform(&p));
149    }
150}