1use crate::*;
19
20pub fn build_abs_path(root: &str, path: &str) -> String {
27 debug_assert!(root.starts_with('/'), "root must start with /");
28 debug_assert!(root.ends_with('/'), "root must end with /");
29
30 let p = root[1..].to_string();
31
32 if path == "/" {
33 p
34 } else {
35 debug_assert!(!path.starts_with('/'), "path must not start with /");
36 p + path
37 }
38}
39
40pub fn build_rooted_abs_path(root: &str, path: &str) -> String {
47 debug_assert!(root.starts_with('/'), "root must start with /");
48 debug_assert!(root.ends_with('/'), "root must end with /");
49
50 let p = root.to_string();
51
52 if path == "/" {
53 p
54 } else {
55 debug_assert!(!path.starts_with('/'), "path must not start with /");
56 p + path
57 }
58}
59
60pub fn build_rel_path(root: &str, path: &str) -> String {
68 debug_assert!(root != path, "get rel path with root is invalid");
69
70 if path.starts_with('/') {
71 debug_assert!(
72 path.starts_with(root),
73 "path {path} doesn't start with root {root}"
74 );
75 path[root.len()..].to_string()
76 } else {
77 debug_assert!(
78 path.starts_with(&root[1..]),
79 "path {path} doesn't start with root {root}"
80 );
81 path[root.len() - 1..].to_string()
82 }
83}
84
85pub fn normalize_path(path: &str) -> String {
97 let path = path.trim().trim_start_matches('/');
100
101 if path.is_empty() {
103 return "/".to_string();
104 }
105
106 let has_trailing = path.ends_with('/');
107
108 let mut p = path
109 .split('/')
110 .filter(|v| !v.is_empty())
111 .collect::<Vec<&str>>()
112 .join("/");
113
114 if has_trailing {
116 p.push('/');
117 }
118
119 p
120}
121
122pub fn normalize_root(v: &str) -> String {
135 let mut v = v
136 .split('/')
137 .filter(|v| !v.is_empty())
138 .collect::<Vec<&str>>()
139 .join("/");
140 if !v.starts_with('/') {
141 v.insert(0, '/');
142 }
143 if !v.ends_with('/') {
144 v.push('/')
145 }
146 v
147}
148
149pub fn get_basename(path: &str) -> &str {
151 if path == "/" {
153 return "/";
154 }
155
156 if !path.ends_with('/') {
158 return path
159 .split('/')
160 .next_back()
161 .expect("file path without name is invalid");
162 }
163
164 let idx = path[..path.len() - 1].rfind('/').map(|v| v + 1);
168
169 match idx {
170 Some(v) => {
171 let (_, name) = path.split_at(v);
172 name
173 }
174 None => path,
175 }
176}
177
178pub fn get_parent(path: &str) -> &str {
180 if path == "/" {
181 return "/";
182 }
183
184 if !path.ends_with('/') {
185 let idx = path.rfind('/');
189
190 return match idx {
191 Some(v) => {
192 let (parent, _) = path.split_at(v + 1);
193 parent
194 }
195 None => "/",
196 };
197 }
198
199 let idx = path[..path.len() - 1].rfind('/').map(|v| v + 1);
203
204 match idx {
205 Some(v) => {
206 let (parent, _) = path.split_at(v);
207 parent
208 }
209 None => "/",
210 }
211}
212
213#[inline]
215pub fn validate_path(path: &str, mode: EntryMode) -> bool {
216 debug_assert!(!path.is_empty(), "input path should not be empty");
217
218 match mode {
219 EntryMode::FILE => !path.ends_with('/'),
220 EntryMode::DIR => path.ends_with('/'),
221 EntryMode::Unknown => false,
222 }
223}
224
225#[cfg(test)]
226mod tests {
227 use super::*;
228
229 #[test]
230 fn test_normalize_path() {
231 let cases = vec![
232 ("file path", "abc", "abc"),
233 ("dir path", "abc/", "abc/"),
234 ("empty path", "", "/"),
235 ("root path", "/", "/"),
236 ("root path with extra /", "///", "/"),
237 ("abs file path", "/abc/def", "abc/def"),
238 ("abs dir path", "/abc/def/", "abc/def/"),
239 ("abs file path with extra /", "///abc/def", "abc/def"),
240 ("abs dir path with extra /", "///abc/def/", "abc/def/"),
241 ("file path contains ///", "abc///def", "abc/def"),
242 ("dir path contains ///", "abc///def///", "abc/def/"),
243 ("file with whitespace", "abc/def ", "abc/def"),
244 ];
245
246 for (name, input, expect) in cases {
247 assert_eq!(normalize_path(input), expect, "{name}")
248 }
249 }
250
251 #[test]
252 fn test_normalize_root() {
253 let cases = vec![
254 ("dir path", "abc/", "/abc/"),
255 ("empty path", "", "/"),
256 ("root path", "/", "/"),
257 ("root path with extra /", "///", "/"),
258 ("abs dir path", "/abc/def/", "/abc/def/"),
259 ("abs file path with extra /", "///abc/def", "/abc/def/"),
260 ("abs dir path with extra /", "///abc/def/", "/abc/def/"),
261 ("dir path contains ///", "abc///def///", "/abc/def/"),
262 ];
263
264 for (name, input, expect) in cases {
265 assert_eq!(normalize_root(input), expect, "{name}")
266 }
267 }
268
269 #[test]
270 fn test_get_basename() {
271 let cases = vec![
272 ("file abs path", "foo/bar/baz.txt", "baz.txt"),
273 ("file rel path", "bar/baz.txt", "baz.txt"),
274 ("file walk", "foo/bar/baz", "baz"),
275 ("dir rel path", "bar/baz/", "baz/"),
276 ("dir root", "/", "/"),
277 ("dir walk", "foo/bar/baz/", "baz/"),
278 ];
279
280 for (name, input, expect) in cases {
281 let actual = get_basename(input);
282 assert_eq!(actual, expect, "{name}")
283 }
284 }
285
286 #[test]
287 fn test_get_parent() {
288 let cases = vec![
289 ("file abs path", "foo/bar/baz.txt", "foo/bar/"),
290 ("file rel path", "bar/baz.txt", "bar/"),
291 ("file walk", "foo/bar/baz", "foo/bar/"),
292 ("dir rel path", "bar/baz/", "bar/"),
293 ("dir root", "/", "/"),
294 ("dir abs path", "/foo/bar/", "/foo/"),
295 ("dir walk", "foo/bar/baz/", "foo/bar/"),
296 ];
297
298 for (name, input, expect) in cases {
299 let actual = get_parent(input);
300 assert_eq!(actual, expect, "{name}")
301 }
302 }
303
304 #[test]
305 fn test_build_abs_path() {
306 let cases = vec![
307 ("input abs file", "/abc/", "/", "abc/"),
308 ("input dir", "/abc/", "def/", "abc/def/"),
309 ("input file", "/abc/", "def", "abc/def"),
310 ("input abs file with root /", "/", "/", ""),
311 ("input empty with root /", "/", "", ""),
312 ("input dir with root /", "/", "def/", "def/"),
313 ("input file with root /", "/", "def", "def"),
314 ];
315
316 for (name, root, input, expect) in cases {
317 let actual = build_abs_path(root, input);
318 assert_eq!(actual, expect, "{name}")
319 }
320 }
321
322 #[test]
323 fn test_build_rooted_abs_path() {
324 let cases = vec![
325 ("input abs file", "/abc/", "/", "/abc/"),
326 ("input dir", "/abc/", "def/", "/abc/def/"),
327 ("input file", "/abc/", "def", "/abc/def"),
328 ("input abs file with root /", "/", "/", "/"),
329 ("input dir with root /", "/", "def/", "/def/"),
330 ("input file with root /", "/", "def", "/def"),
331 ];
332
333 for (name, root, input, expect) in cases {
334 let actual = build_rooted_abs_path(root, input);
335 assert_eq!(actual, expect, "{name}")
336 }
337 }
338
339 #[test]
340 fn test_build_rel_path() {
341 let cases = vec![
342 ("input abs file", "/abc/", "/abc/def", "def"),
343 ("input dir", "/abc/", "/abc/def/", "def/"),
344 ("input file", "/abc/", "abc/def", "def"),
345 ("input dir with root /", "/", "def/", "def/"),
346 ("input file with root /", "/", "def", "def"),
347 ];
348
349 for (name, root, input, expect) in cases {
350 let actual = build_rel_path(root, input);
351 assert_eq!(actual, expect, "{name}")
352 }
353 }
354
355 #[test]
356 fn test_validate_path() {
357 let cases = vec![
358 ("input file with mode file", "abc", EntryMode::FILE, true),
359 ("input file with mode dir", "abc", EntryMode::DIR, false),
360 ("input dir with mode file", "abc/", EntryMode::FILE, false),
361 ("input dir with mode dir", "abc/", EntryMode::DIR, true),
362 ("root with mode dir", "/", EntryMode::DIR, true),
363 (
364 "input file with mode unknown",
365 "abc",
366 EntryMode::Unknown,
367 false,
368 ),
369 (
370 "input dir with mode unknown",
371 "abc/",
372 EntryMode::Unknown,
373 false,
374 ),
375 ];
376
377 for (name, path, mode, expect) in cases {
378 let actual = validate_path(path, mode);
379 assert_eq!(actual, expect, "{name}")
380 }
381 }
382}