simple_fs/reshape/
collapser.rs1use camino::{Utf8Component, Utf8Path, Utf8PathBuf};
7
8pub fn into_collapsed(path: impl Into<Utf8PathBuf>) -> Utf8PathBuf {
25 let path_buf = path.into();
26
27 if path_buf.as_str().is_empty() {
29 return path_buf;
30 }
31
32 if is_collapsed(&path_buf) {
34 return path_buf;
35 }
36
37 let mut components = Vec::new();
38 let mut normal_seen = false;
39
40 for component in path_buf.components() {
42 match component {
43 Utf8Component::Prefix(prefix) => {
44 components.push(Utf8Component::Prefix(prefix));
45 }
46 Utf8Component::RootDir => {
47 components.push(Utf8Component::RootDir);
48 normal_seen = false; }
50 Utf8Component::CurDir => {
51 if components.is_empty() {
53 components.push(component);
54 }
55 }
57 Utf8Component::ParentDir => {
58 if normal_seen && !components.is_empty() {
61 match components.last() {
62 Some(Utf8Component::Normal(_)) => {
63 components.pop();
64 normal_seen = components.iter().any(|c| matches!(c, Utf8Component::Normal(_)));
65 continue;
66 }
67 Some(Utf8Component::ParentDir) => {}
68 Some(Utf8Component::RootDir) | Some(Utf8Component::Prefix(_)) => {
69 continue;
72 }
73 _ => {}
74 }
75 }
76 components.push(component);
77 }
78 Utf8Component::Normal(name) => {
79 components.push(Utf8Component::Normal(name));
80 normal_seen = true;
81 }
82 }
83 }
84
85 if components.is_empty() {
87 if path_buf.as_str().starts_with("./") {
88 return Utf8PathBuf::from(".");
89 } else {
90 return Utf8PathBuf::from("");
91 }
92 }
93
94 let mut result = Utf8PathBuf::new();
96 for component in components {
97 result.push(component.as_str());
98 }
99
100 result
101}
102
103pub fn try_into_collapsed(path: impl Into<Utf8PathBuf>) -> Option<Utf8PathBuf> {
109 let path_buf = path.into();
110
111 if is_collapsed(&path_buf) && !contains_problematic_components(&path_buf) {
114 return Some(path_buf);
115 }
116
117 let mut components = Vec::new();
118 let mut normal_seen = false;
119 let mut parent_count = 0;
120
121 for component in path_buf.components() {
123 match component {
124 Utf8Component::Prefix(_) => {
125 return None;
127 }
128 Utf8Component::RootDir => {
129 return None;
131 }
132 Utf8Component::CurDir => {
133 if components.is_empty() {
135 components.push(component);
136 }
137 }
139 Utf8Component::ParentDir => {
140 if normal_seen {
141 if let Some(Utf8Component::Normal(_)) = components.last() {
143 components.pop();
144 normal_seen = components.iter().any(|c| matches!(c, Utf8Component::Normal(_)));
145 continue;
146 }
147 } else {
148 parent_count += 1;
150 }
151 components.push(component);
152 }
153 Utf8Component::Normal(name) => {
154 components.push(Utf8Component::Normal(name));
155 normal_seen = true;
156 }
157 }
158 }
159
160 if parent_count > 0 && components.iter().filter(|c| matches!(c, Utf8Component::Normal(_))).count() < parent_count {
163 return None;
164 }
165
166 if components.is_empty() {
168 if path_buf.as_str().starts_with("./") {
169 return Some(Utf8PathBuf::from("."));
170 } else {
171 return Some(Utf8PathBuf::from(""));
172 }
173 }
174
175 let mut result = Utf8PathBuf::new();
177 for component in components {
178 result.push(component.as_str());
179 }
180
181 Some(result)
182}
183
184pub fn is_collapsed(path: impl AsRef<Utf8Path>) -> bool {
191 let path = path.as_ref();
192 let mut components = path.components().peekable();
193 let mut is_absolute = false;
194 let mut previous_was_normal = false;
195
196 while let Some(component) = components.next() {
197 match component {
198 Utf8Component::Prefix(_) | Utf8Component::RootDir => {
199 is_absolute = true;
200 }
201 Utf8Component::CurDir => {
202 if previous_was_normal || is_absolute || components.peek().is_some() {
204 return false;
205 }
206 }
207 Utf8Component::ParentDir => {
208 if is_absolute {
210 return false;
211 }
212 if previous_was_normal {
214 return false;
215 }
216 }
217 Utf8Component::Normal(_) => {
218 previous_was_normal = true;
219 }
220 }
221 }
222
223 true
224}
225
226fn contains_problematic_components(path: &Utf8Path) -> bool {
228 let mut has_parent_after_normal = false;
229 let mut has_prefix_or_root = false;
230 let mut normal_seen = false;
231
232 for component in path.components() {
233 match component {
234 Utf8Component::Prefix(_) | Utf8Component::RootDir => {
235 has_prefix_or_root = true;
236 }
237 Utf8Component::ParentDir => {
238 if normal_seen {
239 has_parent_after_normal = true;
240 }
241 }
242 Utf8Component::Normal(_) => {
243 normal_seen = true;
244 }
245 _ => {}
246 }
247 }
248
249 has_prefix_or_root || has_parent_after_normal
250}
251
252#[cfg(test)]
255mod tests {
256 type Result<T> = core::result::Result<T, Box<dyn std::error::Error>>; use super::*;
259
260 #[test]
263 fn test_reshape_collapser_into_collapsed_simple() -> Result<()> {
264 let data = &[
266 ("a/b/c", "a/b/c"),
268 ("a/./b", "a/b"),
269 ("./a/b", "./a/b"),
270 ("./a/b", "./a/b"),
271 ("a/./b/.", "a/b"),
272 ("/a/./b/.", "/a/b"),
273 ("a/../b", "b"),
274 ("../a/b", "../a/b"), ("../a/b/..", "../a"), ("../a/b/../../..", "../.."), ("a/b/..", "a"),
278 ("a/b/../..", ""), ("../../a/b", "../../a/b"), (".", "."), ("..", ".."), ];
283
284 for (input, expected) in data {
286 let input_path = Utf8PathBuf::from(input);
287 let result_path = into_collapsed(input_path);
288 let expected_path = Utf8PathBuf::from(expected);
289 assert_eq!(
290 result_path, expected_path,
291 "Input: '{input}', Expected: '{expected}', Got: '{result_path}'"
292 );
293 }
294
295 Ok(())
296 }
297}
298
299