path_clean/
lib.rs

1//! `path-clean` is a Rust port of the the `cleanname` procedure from the Plan 9 C library, and is similar to
2//! [`path.Clean`](https://golang.org/pkg/path/#Clean) from the Go standard library. It works as follows:
3//!
4//! 1. Reduce multiple slashes to a single slash.
5//! 2. Eliminate `.` path name elements (the current directory).
6//! 3. Eliminate `..` path name elements (the parent directory) and the non-`.` non-`..`, element that precedes them.
7//! 4. Eliminate `..` elements that begin a rooted path, that is, replace `/..` by `/` at the beginning of a path.
8//! 5. Leave intact `..` elements that begin a non-rooted path.
9//!
10//! If the result of this process is an empty string, return the string `"."`, representing the current directory.
11//!
12//! It performs this transform lexically, without touching the filesystem. Therefore it doesn't do
13//! any symlink resolution or absolute path resolution. For more information you can see ["Getting Dot-Dot
14//! Right"](https://9p.io/sys/doc/lexnames.html).
15//!
16//! For convenience, the [`PathClean`] trait is exposed and comes implemented for [`std::path::{Path, PathBuf}`].
17//!
18//! ```rust
19//! use std::path::PathBuf;
20//! use path_clean::{clean, PathClean};
21//! assert_eq!(clean("hello/world/.."), PathBuf::from("hello"));
22//! assert_eq!(
23//!     PathBuf::from("/test/../path/").clean(),
24//!     PathBuf::from("/path")
25//! );
26//! ```
27#![forbid(unsafe_code)]
28
29use std::path::{Component, Path, PathBuf};
30
31/// The Clean trait implements a `clean` method.
32pub trait PathClean {
33    fn clean(&self) -> PathBuf;
34}
35
36/// PathClean implemented for `Path`
37impl PathClean for Path {
38    fn clean(&self) -> PathBuf {
39        clean(self)
40    }
41}
42
43/// PathClean implemented for `PathBuf`
44impl PathClean for PathBuf {
45    fn clean(&self) -> PathBuf {
46        clean(self)
47    }
48}
49
50/// The core implementation. It performs the following, lexically:
51/// 1. Reduce multiple slashes to a single slash.
52/// 2. Eliminate `.` path name elements (the current directory).
53/// 3. Eliminate `..` path name elements (the parent directory) and the non-`.` non-`..`, element that precedes them.
54/// 4. Eliminate `..` elements that begin a rooted path, that is, replace `/..` by `/` at the beginning of a path.
55/// 5. Leave intact `..` elements that begin a non-rooted path.
56///
57/// If the result of this process is an empty string, return the string `"."`, representing the current directory.
58pub fn clean<P>(path: P) -> PathBuf
59where
60    P: AsRef<Path>,
61{
62    let mut out = Vec::new();
63
64    for comp in path.as_ref().components() {
65        match comp {
66            Component::CurDir => (),
67            Component::ParentDir => match out.last() {
68                Some(Component::RootDir) => (),
69                Some(Component::Normal(_)) => {
70                    out.pop();
71                }
72                None
73                | Some(Component::CurDir)
74                | Some(Component::ParentDir)
75                | Some(Component::Prefix(_)) => out.push(comp),
76            },
77            comp => out.push(comp),
78        }
79    }
80
81    if !out.is_empty() {
82        out.iter().collect()
83    } else {
84        PathBuf::from(".")
85    }
86}
87
88#[cfg(test)]
89mod tests {
90    use super::{clean, PathClean};
91    use std::path::{Path, PathBuf};
92
93    #[test]
94    fn test_empty_path_is_current_dir() {
95        assert_eq!(clean(""), PathBuf::from("."));
96    }
97
98    #[test]
99    fn test_clean_paths_dont_change() {
100        let tests = vec![(".", "."), ("..", ".."), ("/", "/")];
101
102        for test in tests {
103            assert_eq!(clean(test.0), PathBuf::from(test.1));
104        }
105    }
106
107    #[test]
108    fn test_replace_multiple_slashes() {
109        let tests = vec![
110            ("/", "/"),
111            ("//", "/"),
112            ("///", "/"),
113            (".//", "."),
114            ("//..", "/"),
115            ("..//", ".."),
116            ("/..//", "/"),
117            ("/.//./", "/"),
118            ("././/./", "."),
119            ("path//to///thing", "path/to/thing"),
120            ("/path//to///thing", "/path/to/thing"),
121        ];
122
123        for test in tests {
124            assert_eq!(clean(test.0), PathBuf::from(test.1));
125        }
126    }
127
128    #[test]
129    fn test_eliminate_current_dir() {
130        let tests = vec![
131            ("./", "."),
132            ("/./", "/"),
133            ("./test", "test"),
134            ("./test/./path", "test/path"),
135            ("/test/./path/", "/test/path"),
136            ("test/path/.", "test/path"),
137        ];
138
139        for test in tests {
140            assert_eq!(clean(test.0), PathBuf::from(test.1));
141        }
142    }
143
144    #[test]
145    fn test_eliminate_parent_dir() {
146        let tests = vec![
147            ("/..", "/"),
148            ("/../test", "/test"),
149            ("test/..", "."),
150            ("test/path/..", "test"),
151            ("test/../path", "path"),
152            ("/test/../path", "/path"),
153            ("test/path/../../", "."),
154            ("test/path/../../..", ".."),
155            ("/test/path/../../..", "/"),
156            ("/test/path/../../../..", "/"),
157            ("test/path/../../../..", "../.."),
158            ("test/path/../../another/path", "another/path"),
159            ("test/path/../../another/path/..", "another"),
160            ("../test", "../test"),
161            ("../test/", "../test"),
162            ("../test/path", "../test/path"),
163            ("../test/..", ".."),
164        ];
165
166        for test in tests {
167            assert_eq!(clean(test.0), PathBuf::from(test.1));
168        }
169    }
170
171    #[test]
172    fn test_pathbuf_trait() {
173        assert_eq!(
174            PathBuf::from("/test/../path/").clean(),
175            PathBuf::from("/path")
176        );
177    }
178
179    #[test]
180    fn test_path_trait() {
181        assert_eq!(Path::new("/test/../path/").clean(), PathBuf::from("/path"));
182    }
183
184    #[test]
185    #[cfg(target_os = "windows")]
186    fn test_windows_paths() {
187        let tests = vec![
188            ("\\..", "\\"),
189            ("\\..\\test", "\\test"),
190            ("test\\..", "."),
191            ("test\\path\\..\\..\\..", ".."),
192            ("test\\path/..\\../another\\path", "another\\path"), // Mixed
193            ("test\\path\\my/path", "test\\path\\my\\path"),      // Mixed 2
194            ("/dir\\../otherDir/test.json", "/otherDir/test.json"), // User example
195            ("c:\\test\\..", "c:\\"),                             // issue #12
196            ("c:/test/..", "c:/"),                                // issue #12
197        ];
198
199        for test in tests {
200            assert_eq!(clean(test.0), PathBuf::from(test.1));
201        }
202    }
203}