1#![forbid(unsafe_code)]
28
29use std::path::{Component, Path, PathBuf};
30
31pub trait PathClean {
33 fn clean(&self) -> PathBuf;
34}
35
36impl PathClean for Path {
38 fn clean(&self) -> PathBuf {
39 clean(self)
40 }
41}
42
43impl PathClean for PathBuf {
45 fn clean(&self) -> PathBuf {
46 clean(self)
47 }
48}
49
50pub 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"), ("test\\path\\my/path", "test\\path\\my\\path"), ("/dir\\../otherDir/test.json", "/otherDir/test.json"), ("c:\\test\\..", "c:\\"), ("c:/test/..", "c:/"), ];
198
199 for test in tests {
200 assert_eq!(clean(test.0), PathBuf::from(test.1));
201 }
202 }
203}