edit/
path.rs

1// Copyright (c) Microsoft Corporation.
2// Licensed under the MIT License.
3
4//! Path related helpers.
5
6use std::ffi::{OsStr, OsString};
7use std::path::{Component, MAIN_SEPARATOR_STR, Path, PathBuf};
8
9/// Normalizes a given path by removing redundant components.
10/// The given path must be absolute (e.g. by joining it with the current working directory).
11pub fn normalize(path: &Path) -> PathBuf {
12    let mut res = PathBuf::with_capacity(path.as_os_str().as_encoded_bytes().len());
13    let mut root_len = 0;
14
15    for component in path.components() {
16        match component {
17            Component::Prefix(p) => res.push(p.as_os_str()),
18            Component::RootDir => {
19                res.push(OsStr::new(MAIN_SEPARATOR_STR));
20                root_len = res.as_os_str().as_encoded_bytes().len();
21            }
22            Component::CurDir => {}
23            Component::ParentDir => {
24                // Get the length up to the parent directory
25                if let Some(len) = res
26                    .parent()
27                    .map(|p| p.as_os_str().as_encoded_bytes().len())
28                    // Ensure we don't pop the root directory
29                    && len >= root_len
30                {
31                    // Pop the last component from `res`.
32                    //
33                    // This can be replaced with a plain `res.as_mut_os_string().truncate(len)`
34                    // once `os_string_truncate` is stabilized (#133262).
35                    let mut bytes = res.into_os_string().into_encoded_bytes();
36                    bytes.truncate(len);
37                    res = PathBuf::from(unsafe { OsString::from_encoded_bytes_unchecked(bytes) });
38                }
39            }
40            Component::Normal(p) => res.push(p),
41        }
42    }
43
44    res
45}
46
47#[cfg(test)]
48mod tests {
49    use std::ffi::OsString;
50    use std::path::Path;
51
52    use super::*;
53
54    fn norm(s: &str) -> OsString {
55        normalize(Path::new(s)).into_os_string()
56    }
57
58    #[cfg(unix)]
59    #[test]
60    fn test_unix() {
61        assert_eq!(norm("/a/b/c"), "/a/b/c");
62        assert_eq!(norm("/a/b/c/"), "/a/b/c");
63        assert_eq!(norm("/a/./b"), "/a/b");
64        assert_eq!(norm("/a/b/../c"), "/a/c");
65        assert_eq!(norm("/../../a"), "/a");
66        assert_eq!(norm("/../"), "/");
67        assert_eq!(norm("/a//b/c"), "/a/b/c");
68        assert_eq!(norm("/a/b/c/../../../../d"), "/d");
69        assert_eq!(norm("//"), "/");
70    }
71
72    #[cfg(windows)]
73    #[test]
74    fn test_windows() {
75        assert_eq!(norm(r"C:\a\b\c"), r"C:\a\b\c");
76        assert_eq!(norm(r"C:\a\b\c\"), r"C:\a\b\c");
77        assert_eq!(norm(r"C:\a\.\b"), r"C:\a\b");
78        assert_eq!(norm(r"C:\a\b\..\c"), r"C:\a\c");
79        assert_eq!(norm(r"C:\..\..\a"), r"C:\a");
80        assert_eq!(norm(r"C:\..\"), r"C:\");
81        assert_eq!(norm(r"C:\a\\b\c"), r"C:\a\b\c");
82        assert_eq!(norm(r"C:/a\b/c"), r"C:\a\b\c");
83        assert_eq!(norm(r"C:\a\b\c\..\..\..\..\d"), r"C:\d");
84        assert_eq!(norm(r"\\server\share\path"), r"\\server\share\path");
85    }
86}