nu_path/
trailing_slash.rs

1use std::{
2    borrow::Cow,
3    path::{Path, PathBuf},
4};
5
6/// Strip any trailing slashes from a non-root path. This is required in some contexts, for example
7/// for the `PWD` environment variable.
8pub fn strip_trailing_slash(path: &Path) -> Cow<'_, Path> {
9    if has_trailing_slash(path) {
10        // If there are, the safest thing to do is have Rust parse the path for us and build it
11        // again. This will correctly handle a root directory, but it won't add the trailing slash.
12        let mut out = PathBuf::with_capacity(path.as_os_str().len());
13        out.extend(path.components());
14        Cow::Owned(out)
15    } else {
16        // The path is safe and doesn't contain any trailing slashes.
17        Cow::Borrowed(path)
18    }
19}
20
21/// `true` if the path has a trailing slash, including if it's the root directory.
22#[cfg(windows)]
23pub fn has_trailing_slash(path: &Path) -> bool {
24    use std::os::windows::ffi::OsStrExt;
25
26    let last = path.as_os_str().encode_wide().last();
27    last == Some(b'\\' as u16) || last == Some(b'/' as u16)
28}
29
30/// `true` if the path has a trailing slash, including if it's the root directory.
31#[cfg(unix)]
32pub fn has_trailing_slash(path: &Path) -> bool {
33    use std::os::unix::ffi::OsStrExt;
34
35    let last = path.as_os_str().as_bytes().last();
36    last == Some(&b'/')
37}
38
39/// `true` if the path has a trailing slash, including if it's the root directory.
40#[cfg(target_arch = "wasm32")]
41pub fn has_trailing_slash(path: &Path) -> bool {
42    // in the web paths are often just URLs, they are separated by forward slashes
43    path.to_str().is_some_and(|s| s.ends_with('/'))
44}
45
46#[cfg(test)]
47mod tests {
48    use super::*;
49
50    #[cfg_attr(not(unix), ignore = "only for Unix")]
51    #[test]
52    fn strip_root_unix() {
53        assert_eq!(Path::new("/"), strip_trailing_slash(Path::new("/")));
54    }
55
56    #[cfg_attr(not(unix), ignore = "only for Unix")]
57    #[test]
58    fn strip_non_trailing_unix() {
59        assert_eq!(
60            Path::new("/foo/bar"),
61            strip_trailing_slash(Path::new("/foo/bar"))
62        );
63    }
64
65    #[cfg_attr(not(unix), ignore = "only for Unix")]
66    #[test]
67    fn strip_trailing_unix() {
68        assert_eq!(
69            Path::new("/foo/bar"),
70            strip_trailing_slash(Path::new("/foo/bar/"))
71        );
72    }
73
74    #[cfg_attr(not(windows), ignore = "only for Windows")]
75    #[test]
76    fn strip_root_windows() {
77        assert_eq!(Path::new(r"C:\"), strip_trailing_slash(Path::new(r"C:\")));
78    }
79
80    #[cfg_attr(not(windows), ignore = "only for Windows")]
81    #[test]
82    fn strip_non_trailing_windows() {
83        assert_eq!(
84            Path::new(r"C:\foo\bar"),
85            strip_trailing_slash(Path::new(r"C:\foo\bar"))
86        );
87    }
88
89    #[cfg_attr(not(windows), ignore = "only for Windows")]
90    #[test]
91    fn strip_non_trailing_windows_unc() {
92        assert_eq!(
93            Path::new(r"\\foo\bar"),
94            strip_trailing_slash(Path::new(r"\\foo\bar"))
95        );
96    }
97
98    #[cfg_attr(not(windows), ignore = "only for Windows")]
99    #[test]
100    fn strip_trailing_windows() {
101        assert_eq!(
102            Path::new(r"C:\foo\bar"),
103            strip_trailing_slash(Path::new(r"C:\foo\bar\"))
104        );
105    }
106
107    #[cfg_attr(not(windows), ignore = "only for Windows")]
108    #[test]
109    fn strip_trailing_windows_unc() {
110        assert_eq!(
111            Path::new(r"\\foo\bar"),
112            strip_trailing_slash(Path::new(r"\\foo\bar\"))
113        );
114    }
115
116    #[cfg_attr(not(windows), ignore = "only for Windows")]
117    #[test]
118    fn strip_trailing_windows_device() {
119        assert_eq!(
120            Path::new(r"\\.\foo"),
121            strip_trailing_slash(Path::new(r"\\.\foo\"))
122        );
123    }
124}