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}