tokio_tar/
fs.rs

1use std::path::{Component, Path, PathBuf};
2
3/// Normalize a path, like Python's `os.path.normpath`.
4///
5/// Adapted from <https://github.com/rust-lang/cargo/blob/fede83ccf973457de319ba6fa0e36ead454d2e20/src/cargo/util/paths.rs#L61>.
6pub(crate) fn normalize(path: &Path) -> Option<PathBuf> {
7    let mut components = path.components().peekable();
8    let mut ret = if let Some(c @ Component::Prefix(..)) = components.peek() {
9        let buf = PathBuf::from(c.as_os_str());
10        components.next();
11        buf
12    } else {
13        PathBuf::new()
14    };
15    let mut has_root = false;
16
17    for component in components {
18        match component {
19            Component::Prefix(..) => unreachable!(),
20            Component::RootDir => {
21                ret.push(component.as_os_str());
22                has_root = true;
23            }
24            Component::CurDir => {}
25            Component::ParentDir => {
26                // Preserve leading `..` components.
27                if ret
28                    .components()
29                    .next_back()
30                    .is_some_and(|component| component == Component::ParentDir)
31                {
32                    ret.push(component.as_os_str());
33                } else if ret.pop() {
34                    // We successfully removed a component.
35                } else if has_root {
36                    // An absolute path tried to go above the root.
37                    return None;
38                } else {
39                    // If we don't have a root, we can just push the `..` component.
40                    ret.push(component.as_os_str());
41                }
42            }
43            Component::Normal(c) => {
44                ret.push(c);
45            }
46        }
47    }
48
49    Some(ret)
50}
51
52#[cfg(test)]
53mod tests {
54    #[cfg(unix)]
55    use std::path::{Path, PathBuf};
56
57    #[test]
58    #[cfg(unix)]
59    fn test_normalize() {
60        // Basic relative path.
61        assert_eq!(
62            crate::fs::normalize(Path::new("a/b/c")),
63            Some(PathBuf::from("a/b/c"))
64        );
65
66        // Path with `..`, should remove `b`.
67        assert_eq!(
68            crate::fs::normalize(Path::new("a/b/../c")),
69            Some(PathBuf::from("a/c"))
70        );
71
72        // Path with `.` should be ignored.
73        assert_eq!(
74            crate::fs::normalize(Path::new("./a/b")),
75            Some(PathBuf::from("a/b"))
76        );
77
78        // Path with no relative components should be unchanged.
79        assert_eq!(
80            crate::fs::normalize(Path::new("outside")),
81            Some(PathBuf::from("outside"))
82        );
83
84        // Excessive `..` should be ignored.
85        assert_eq!(
86            crate::fs::normalize(Path::new("../../../../")),
87            Some(PathBuf::from("../../../../"),)
88        );
89
90        // Multiple `..` should stack.
91        assert_eq!(
92            crate::fs::normalize(Path::new("a/b/../../c")),
93            Some(PathBuf::from("c"))
94        );
95
96        // Rooted absolute path, `..` should not go above root.
97        assert_eq!(crate::fs::normalize(Path::new("/a/../..")), None);
98
99        // Root with dot and parent.
100        assert_eq!(
101            crate::fs::normalize(Path::new("/./a/../b")),
102            Some(PathBuf::from("/b"))
103        );
104
105        // Trailing slash should be ignored.
106        assert_eq!(
107            crate::fs::normalize(Path::new("a/b/c/")),
108            Some(PathBuf::from("a/b/c"))
109        );
110
111        // Trailing `/.` should be dropped.
112        assert_eq!(
113            crate::fs::normalize(Path::new("a/b/.")),
114            Some(PathBuf::from("a/b"))
115        );
116
117        // Trailing `/..` should pop last component.
118        assert_eq!(
119            crate::fs::normalize(Path::new("a/b/..")),
120            Some(PathBuf::from("a"))
121        );
122
123        // Leading `..` in a relative path should be preserved.
124        assert_eq!(
125            crate::fs::normalize(Path::new("../x/y")),
126            Some(PathBuf::from("../x/y"))
127        );
128
129        // Mix of preserved leading `..` and collapsed internals.
130        assert_eq!(
131            crate::fs::normalize(Path::new("../../a/b/../c")),
132            Some(PathBuf::from("../../a/c"))
133        );
134
135        // Windows drive absolute: C:\a\..\b
136        #[cfg(windows)]
137        assert_eq!(
138            crate::fs::normalize(Path::new(r"C:\a\..\b")),
139            Some(PathBuf::from(r"C:\b"))
140        );
141
142        // Windows drive-relative (no backslash): C:..\a
143        // should preserve the `..`
144        #[cfg(windows)]
145        assert_eq!(
146            crate::fs::normalize(Path::new(r"C:..\a")),
147            Some(PathBuf::from(r"C:..\a"))
148        );
149
150        // Root-only should normalize to root.
151        assert_eq!(
152            crate::fs::normalize(Path::new("/")),
153            Some(PathBuf::from("/"))
154        );
155
156        // Just `..` should normalize to `..`
157        assert_eq!(
158            crate::fs::normalize(Path::new("..")),
159            Some(PathBuf::from(".."))
160        );
161    }
162}