clean_path/lib.rs
1#![forbid(unsafe_code)]
2
3//! `clean-path` is a safe fork of the
4//! [`path-clean`](https://crates.io/crates/path-clean) crate.
5//!
6//! # About
7//!
8//! This fork aims to provide the same utility as
9//! [`path-clean`](https://crates.io/crates/path-clean), without using unsafe. Additionally, the api
10//! is improved ([`clean`] takes `AsRef<Path>` instead of just `&str`) and `Clean` is implemented on
11//! `Path` in addition to just `PathBuf`.
12//!
13//! The main cleaning procedure is implemented using the methods provided by `PathBuf`, thus it should
14//! bring portability benefits over [`path-clean`](https://crates.io/crates/path-clean) w.r.t. correctly
15//! handling cross-platform filepaths. However, the current implementation is not highly-optimized, so
16//! if performance is top-priority, consider using [`path-clean`](https://crates.io/crates/path-clean)
17//! instead.
18//!
19//! # Specification
20//!
21//! The cleaning works as follows:
22//! 1. Reduce multiple slashes to a single slash.
23//! 2. Eliminate `.` path name elements (the current directory).
24//! 3. Eliminate `..` path name elements (the parent directory) and the non-`.` non-`..`, element that precedes them.
25//! 4. Eliminate `..` elements that begin a rooted path, that is, replace `/..` by `/` at the beginning of a path.
26//! 5. Leave intact `..` elements that begin a non-rooted path.
27//!
28//! If the result of this process is an empty string, return the
29//! string `"."`, representing the current directory.
30//!
31//! This transformation is performed lexically, without touching the filesystem. Therefore it doesn't do
32//! any symlink resolution or absolute path resolution. For more information you can see ["Getting
33//! Dot-Dot Right"](https://9p.io/sys/doc/lexnames.html).
34//!
35//! This functionality is exposed in the [`clean`] function and [`Clean`] trait implemented for
36//! [`std::path::PathBuf`] and [`std::path::Path`].
37//!
38//!
39//! # Example
40//!
41//! ```rust
42//! use std::path::{Path, PathBuf};
43//! use clean_path::{clean, Clean};
44//!
45//! assert_eq!(clean("foo/../../bar"), PathBuf::from("../bar"));
46//! assert_eq!(Path::new("hello/world/..").clean(), PathBuf::from("hello"));
47//! assert_eq!(
48//! PathBuf::from("/test/../path/").clean(),
49//! PathBuf::from("/path")
50//! );
51//! ```
52
53use std::path::{Path, PathBuf};
54
55/// The Clean trait implements the `clean` method.
56pub trait Clean {
57 fn clean(&self) -> PathBuf;
58}
59
60/// Clean implemented for PathBuf
61impl Clean for PathBuf {
62 fn clean(&self) -> PathBuf {
63 clean(self)
64 }
65}
66
67/// Clean implemented for PathBuf
68impl Clean for Path {
69 fn clean(&self) -> PathBuf {
70 clean(self)
71 }
72}
73
74/**
75Clean the given path to according to a set of rules:
761. Reduce multiple slashes to a single slash.
772. Eliminate `.` path name elements (the current directory).
783. Eliminate `..` path name elements (the parent directory) and the non-`.` non-`..`, element that precedes them.
794. Eliminate `..` elements that begin a rooted path, that is, replace `/..` by `/` at the beginning of a path.
805. Leave intact `..` elements that begin a non-rooted path.
81
82If the result of this process is an empty string, return the string `"."`, representing the current directory.
83
84Note that symlinks and absolute paths are not resolved.
85
86# Example
87
88```rust
89# use std::path::PathBuf;
90# use clean_path::{clean, Clean};
91assert_eq!(clean("foo/../../bar"), PathBuf::from("../bar"));
92```
93*/
94pub fn clean<P: AsRef<Path>>(path: P) -> PathBuf {
95 let path = path.as_ref();
96 clean_internal(path)
97}
98
99/// The core implementation.
100fn clean_internal(path: &Path) -> PathBuf {
101 // based off of github.com/rust-lang/cargo/blob/fede83/src/cargo/util/paths.rs#L61
102 use std::path::Component;
103
104 let mut components = path.components().peekable();
105 let mut cleaned = if let Some(c @ Component::Prefix(..)) = components.peek().cloned() {
106 components.next();
107 PathBuf::from(c.as_os_str())
108 } else {
109 PathBuf::new()
110 };
111
112 // amount of leading parentdir components in `cleaned`
113 let mut dotdots = 0;
114 // amount of components in `cleaned`
115 // invariant: component_count >= dotdots
116 let mut component_count = 0;
117
118 for component in components {
119 match component {
120 Component::Prefix(..) => unreachable!(),
121 Component::RootDir => {
122 cleaned.push(component.as_os_str());
123 component_count += 1;
124 }
125 Component::CurDir => {}
126 Component::ParentDir if component_count == 1 && cleaned.is_absolute() => {}
127 Component::ParentDir if component_count == dotdots => {
128 cleaned.push("..");
129 dotdots += 1;
130 component_count += 1;
131 }
132 Component::ParentDir => {
133 cleaned.pop();
134 component_count -= 1;
135 }
136 Component::Normal(c) => {
137 cleaned.push(c);
138 component_count += 1;
139 }
140 }
141 }
142
143 if component_count == 0 {
144 cleaned.push(".");
145 }
146
147 cleaned
148}
149
150#[cfg(test)]
151mod tests {
152 use super::{clean, Clean};
153 use std::path::PathBuf;
154
155 #[test]
156 fn test_empty_path_is_current_dir() {
157 assert_eq!(clean(""), PathBuf::from("."));
158 }
159
160 #[test]
161 fn test_clean_paths_dont_change() {
162 let tests = vec![(".", "."), ("..", ".."), ("/", "/")];
163
164 for test in tests {
165 assert_eq!(
166 clean(test.0),
167 PathBuf::from(test.1),
168 "clean({}) == {}",
169 test.0,
170 test.1
171 );
172 }
173 }
174
175 #[test]
176 fn test_replace_multiple_slashes() {
177 let tests = vec![
178 ("/", "/"),
179 ("//", "/"),
180 ("///", "/"),
181 (".//", "."),
182 ("//..", "/"),
183 ("..//", ".."),
184 ("/..//", "/"),
185 ("/.//./", "/"),
186 ("././/./", "."),
187 ("path//to///thing", "path/to/thing"),
188 ("/path//to///thing", "/path/to/thing"),
189 ];
190
191 for test in tests {
192 assert_eq!(
193 clean(test.0),
194 PathBuf::from(test.1),
195 "clean({}) == {}",
196 test.0,
197 test.1
198 );
199 }
200 }
201
202 #[test]
203 fn test_eliminate_current_dir() {
204 let tests = vec![
205 ("./", "."),
206 ("/./", "/"),
207 ("./test", "test"),
208 ("./test/./path", "test/path"),
209 ("/test/./path/", "/test/path"),
210 ("test/path/.", "test/path"),
211 ];
212
213 for test in tests {
214 assert_eq!(
215 clean(test.0),
216 PathBuf::from(test.1),
217 "clean({}) == {}",
218 test.0,
219 test.1
220 );
221 }
222 }
223
224 #[test]
225 fn test_eliminate_parent_dir() {
226 let tests = vec![
227 ("/..", "/"),
228 ("/../test", "/test"),
229 ("test/..", "."),
230 ("test/path/..", "test"),
231 ("test/../path", "path"),
232 ("/test/../path", "/path"),
233 ("test/path/../../", "."),
234 ("test/path/../../..", ".."),
235 ("/test/path/../../..", "/"),
236 ("/test/path/../../../..", "/"),
237 ("test/path/../../../..", "../.."),
238 ("test/path/../../another/path", "another/path"),
239 ("test/path/../../another/path/..", "another"),
240 ("../test", "../test"),
241 ("../test/", "../test"),
242 ("../test/path", "../test/path"),
243 ("../test/..", ".."),
244 ];
245
246 for test in tests {
247 assert_eq!(
248 clean(test.0),
249 PathBuf::from(test.1),
250 "clean({}) == {}",
251 test.0,
252 test.1
253 );
254 }
255 }
256
257 #[test]
258 fn test_trait() {
259 assert_eq!(
260 PathBuf::from("/test/../path/").clean(),
261 PathBuf::from("/path")
262 );
263 }
264}