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::PathBuf`].
17//!
18//! ```rust
19//! use std::path::PathBuf;
20//! use path_clean::{clean, PathClean};
21//! assert_eq!(clean("hello/world/.."), "hello");
22//! assert_eq!(
23//!     PathBuf::from("/test/../path/").clean(),
24//!     PathBuf::from("/path")
25//! );
26//! ```
27
28use std::path::PathBuf;
29
30/// The Clean trait implements a `clean` method. It's recommended you use the provided [`clean`]
31/// function.
32pub trait PathClean<T> {
33    fn clean(&self) -> T;
34}
35
36/// PathClean implemented for PathBuf
37impl PathClean<PathBuf> for PathBuf {
38    fn clean(&self) -> PathBuf {
39        PathBuf::from(clean(self.to_str().unwrap_or("")))
40    }
41}
42
43/// The core implementation. It performs the following, lexically:
44/// 1. Reduce multiple slashes to a single slash.
45/// 2. Eliminate `.` path name elements (the current directory).
46/// 3. Eliminate `..` path name elements (the parent directory) and the non-`.` non-`..`, element that precedes them.
47/// 4. Eliminate `..` elements that begin a rooted path, that is, replace `/..` by `/` at the beginning of a path.
48/// 5. Leave intact `..` elements that begin a non-rooted path.
49///
50/// If the result of this process is an empty string, return the string `"."`, representing the current directory.
51pub fn clean(path: &str) -> String {
52    match path {
53        "" => return ".".to_string(),
54        "." => return ".".to_string(),
55        ".." => return "..".to_string(),
56        "/" => return "/".to_string(),
57        _ => {}
58    }
59
60    let mut out = vec![];
61    let is_root = path.starts_with("/");
62
63    let path = path.trim_end_matches("/");
64    let num_segments = path.split("/").count();
65
66    for segment in path.split("/") {
67        match segment {
68            "" => continue,
69            "." => {
70                if num_segments == 1 {
71                    out.push(segment);
72                };
73                continue;
74            }
75            ".." => {
76                let previous = out.pop();
77                if previous.is_some() && !can_backtrack(previous.unwrap()) {
78                    out.push(previous.unwrap());
79                    out.push(segment);
80                } else if previous.is_none() && !is_root {
81                    out.push(segment);
82                };
83                continue;
84            }
85            _ => {
86                out.push(segment);
87            }
88        };
89    }
90
91    let mut out_str = out.join("/");
92
93    if is_root {
94        out_str = format!("/{}", out_str);
95    }
96
97    if out_str.len() == 0 {
98        return ".".to_string();
99    }
100
101    out_str
102}
103
104fn can_backtrack(segment: &str) -> bool {
105    match segment {
106        "." => false,
107        ".." => false,
108        _ => true,
109    }
110}
111
112#[cfg(test)]
113mod tests {
114    use super::{clean, PathClean};
115    use std::path::PathBuf;
116
117    #[test]
118    fn test_empty_path_is_current_dir() {
119        assert_eq!(clean(""), ".");
120    }
121
122    #[test]
123    fn test_clean_paths_dont_change() {
124        let tests = vec![(".", "."), ("..", ".."), ("/", "/")];
125
126        for test in tests {
127            assert_eq!(clean(test.0), test.1);
128        }
129    }
130
131    #[test]
132    fn test_replace_multiple_slashes() {
133        let tests = vec![
134            ("/", "/"),
135            ("//", "/"),
136            ("///", "/"),
137            (".//", "."),
138            ("//..", "/"),
139            ("..//", ".."),
140            ("/..//", "/"),
141            ("/.//./", "/"),
142            ("././/./", "."),
143            ("path//to///thing", "path/to/thing"),
144            ("/path//to///thing", "/path/to/thing"),
145        ];
146
147        for test in tests {
148            assert_eq!(clean(test.0), test.1);
149        }
150    }
151
152    #[test]
153    fn test_eliminate_current_dir() {
154        let tests = vec![
155            ("./", "."),
156            ("/./", "/"),
157            ("./test", "test"),
158            ("./test/./path", "test/path"),
159            ("/test/./path/", "/test/path"),
160            ("test/path/.", "test/path"),
161        ];
162
163        for test in tests {
164            assert_eq!(clean(test.0), test.1);
165        }
166    }
167
168    #[test]
169    fn test_eliminate_parent_dir() {
170        let tests = vec![
171            ("/..", "/"),
172            ("/../test", "/test"),
173            ("test/..", "."),
174            ("test/path/..", "test"),
175            ("test/../path", "path"),
176            ("/test/../path", "/path"),
177            ("test/path/../../", "."),
178            ("test/path/../../..", ".."),
179            ("/test/path/../../..", "/"),
180            ("/test/path/../../../..", "/"),
181            ("test/path/../../../..", "../.."),
182            ("test/path/../../another/path", "another/path"),
183            ("test/path/../../another/path/..", "another"),
184            ("../test", "../test"),
185            ("../test/", "../test"),
186            ("../test/path", "../test/path"),
187            ("../test/..", ".."),
188        ];
189
190        for test in tests {
191            assert_eq!(clean(test.0), test.1);
192        }
193    }
194
195    #[test]
196    fn test_pathbuf_trait() {
197        assert_eq!(
198            PathBuf::from("/test/../path/").clean(),
199            PathBuf::from("/path")
200        );
201    }
202}