1use std::path::PathBuf;
29
30pub trait PathClean<T> {
33 fn clean(&self) -> T;
34}
35
36impl PathClean<PathBuf> for PathBuf {
38 fn clean(&self) -> PathBuf {
39 PathBuf::from(clean(self.to_str().unwrap_or("")))
40 }
41}
42
43pub 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}