1use crate::*;
19use std::hash::{BuildHasher, Hasher};
20
21pub fn build_abs_path(root: &str, path: &str) -> String {
28 debug_assert!(root.starts_with('/'), "root must start with /");
29 debug_assert!(root.ends_with('/'), "root must end with /");
30
31 let p = root[1..].to_string();
32
33 if path == "/" {
34 p
35 } else {
36 debug_assert!(!path.starts_with('/'), "path must not start with /");
37 p + path
38 }
39}
40
41pub fn build_rooted_abs_path(root: &str, path: &str) -> String {
48 debug_assert!(root.starts_with('/'), "root must start with /");
49 debug_assert!(root.ends_with('/'), "root must end with /");
50
51 let p = root.to_string();
52
53 if path == "/" {
54 p
55 } else {
56 debug_assert!(!path.starts_with('/'), "path must not start with /");
57 p + path
58 }
59}
60
61pub fn build_rel_path(root: &str, path: &str) -> String {
69 debug_assert!(root != path, "get rel path with root is invalid");
70
71 if path.starts_with('/') {
72 debug_assert!(
73 path.starts_with(root),
74 "path {path} doesn't start with root {root}"
75 );
76 path[root.len()..].to_string()
77 } else {
78 debug_assert!(
79 path.starts_with(&root[1..]),
80 "path {path} doesn't start with root {root}"
81 );
82 path[root.len() - 1..].to_string()
83 }
84}
85
86pub fn normalize_path(path: &str) -> String {
98 let path = path.trim().trim_start_matches('/');
101
102 if path.is_empty() {
104 return "/".to_string();
105 }
106
107 let has_trailing = path.ends_with('/');
108
109 let mut p = path
110 .split('/')
111 .filter(|v| !v.is_empty())
112 .collect::<Vec<&str>>()
113 .join("/");
114
115 if has_trailing {
117 p.push('/');
118 }
119
120 p
121}
122
123pub fn normalize_root(v: &str) -> String {
136 let mut v = v
137 .split('/')
138 .filter(|v| !v.is_empty())
139 .collect::<Vec<&str>>()
140 .join("/");
141 if !v.starts_with('/') {
142 v.insert(0, '/');
143 }
144 if !v.ends_with('/') {
145 v.push('/')
146 }
147 v
148}
149
150pub fn get_basename(path: &str) -> &str {
152 if path == "/" {
154 return "/";
155 }
156
157 if !path.ends_with('/') {
159 return path
160 .split('/')
161 .next_back()
162 .expect("file path without name is invalid");
163 }
164
165 let idx = path[..path.len() - 1].rfind('/').map(|v| v + 1);
169
170 match idx {
171 Some(v) => {
172 let (_, name) = path.split_at(v);
173 name
174 }
175 None => path,
176 }
177}
178
179pub fn get_parent(path: &str) -> &str {
181 if path == "/" {
182 return "/";
183 }
184
185 if !path.ends_with('/') {
186 let idx = path.rfind('/');
190
191 return match idx {
192 Some(v) => {
193 let (parent, _) = path.split_at(v + 1);
194 parent
195 }
196 None => "/",
197 };
198 }
199
200 let idx = path[..path.len() - 1].rfind('/').map(|v| v + 1);
204
205 match idx {
206 Some(v) => {
207 let (parent, _) = path.split_at(v);
208 parent
209 }
210 None => "/",
211 }
212}
213
214const RANDOM_TMP_PATH_POSTFIX_LENGTH: usize = 8;
216const CHARS: &[u8] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
218const CHARS_LENGTH: u64 = CHARS.len() as u64;
219
220#[inline]
225pub fn build_tmp_path_of(path: &str) -> String {
226 let name = get_basename(path);
227 let mut buf = String::with_capacity(name.len() + RANDOM_TMP_PATH_POSTFIX_LENGTH);
228 buf.push_str(name);
229 buf.push('.');
230
231 for _ in 0..RANDOM_TMP_PATH_POSTFIX_LENGTH {
247 let random = std::collections::hash_map::RandomState::new()
248 .build_hasher()
249 .finish();
250 let choice: usize = (random % CHARS_LENGTH).try_into().unwrap();
251 buf.push(CHARS[choice] as char);
252 }
253
254 buf
255}
256
257#[inline]
259pub fn validate_path(path: &str, mode: EntryMode) -> bool {
260 debug_assert!(!path.is_empty(), "input path should not be empty");
261
262 match mode {
263 EntryMode::FILE => !path.ends_with('/'),
264 EntryMode::DIR => path.ends_with('/'),
265 EntryMode::Unknown => false,
266 }
267}
268
269#[cfg(test)]
270mod tests {
271 use super::*;
272
273 #[test]
274 fn test_normalize_path() {
275 let cases = vec![
276 ("file path", "abc", "abc"),
277 ("dir path", "abc/", "abc/"),
278 ("empty path", "", "/"),
279 ("root path", "/", "/"),
280 ("root path with extra /", "///", "/"),
281 ("abs file path", "/abc/def", "abc/def"),
282 ("abs dir path", "/abc/def/", "abc/def/"),
283 ("abs file path with extra /", "///abc/def", "abc/def"),
284 ("abs dir path with extra /", "///abc/def/", "abc/def/"),
285 ("file path contains ///", "abc///def", "abc/def"),
286 ("dir path contains ///", "abc///def///", "abc/def/"),
287 ("file with whitespace", "abc/def ", "abc/def"),
288 ];
289
290 for (name, input, expect) in cases {
291 assert_eq!(normalize_path(input), expect, "{name}")
292 }
293 }
294
295 #[test]
296 fn test_normalize_root() {
297 let cases = vec![
298 ("dir path", "abc/", "/abc/"),
299 ("empty path", "", "/"),
300 ("root path", "/", "/"),
301 ("root path with extra /", "///", "/"),
302 ("abs dir path", "/abc/def/", "/abc/def/"),
303 ("abs file path with extra /", "///abc/def", "/abc/def/"),
304 ("abs dir path with extra /", "///abc/def/", "/abc/def/"),
305 ("dir path contains ///", "abc///def///", "/abc/def/"),
306 ];
307
308 for (name, input, expect) in cases {
309 assert_eq!(normalize_root(input), expect, "{name}")
310 }
311 }
312
313 #[test]
314 fn test_get_basename() {
315 let cases = vec![
316 ("file abs path", "foo/bar/baz.txt", "baz.txt"),
317 ("file rel path", "bar/baz.txt", "baz.txt"),
318 ("file walk", "foo/bar/baz", "baz"),
319 ("dir rel path", "bar/baz/", "baz/"),
320 ("dir root", "/", "/"),
321 ("dir walk", "foo/bar/baz/", "baz/"),
322 ];
323
324 for (name, input, expect) in cases {
325 let actual = get_basename(input);
326 assert_eq!(actual, expect, "{name}")
327 }
328 }
329
330 #[test]
331 fn test_get_parent() {
332 let cases = vec![
333 ("file abs path", "foo/bar/baz.txt", "foo/bar/"),
334 ("file rel path", "bar/baz.txt", "bar/"),
335 ("file walk", "foo/bar/baz", "foo/bar/"),
336 ("dir rel path", "bar/baz/", "bar/"),
337 ("dir root", "/", "/"),
338 ("dir abs path", "/foo/bar/", "/foo/"),
339 ("dir walk", "foo/bar/baz/", "foo/bar/"),
340 ];
341
342 for (name, input, expect) in cases {
343 let actual = get_parent(input);
344 assert_eq!(actual, expect, "{name}")
345 }
346 }
347
348 #[test]
349 fn test_build_abs_path() {
350 let cases = vec![
351 ("input abs file", "/abc/", "/", "abc/"),
352 ("input dir", "/abc/", "def/", "abc/def/"),
353 ("input file", "/abc/", "def", "abc/def"),
354 ("input abs file with root /", "/", "/", ""),
355 ("input empty with root /", "/", "", ""),
356 ("input dir with root /", "/", "def/", "def/"),
357 ("input file with root /", "/", "def", "def"),
358 ];
359
360 for (name, root, input, expect) in cases {
361 let actual = build_abs_path(root, input);
362 assert_eq!(actual, expect, "{name}")
363 }
364 }
365
366 #[test]
367 fn test_build_rooted_abs_path() {
368 let cases = vec![
369 ("input abs file", "/abc/", "/", "/abc/"),
370 ("input dir", "/abc/", "def/", "/abc/def/"),
371 ("input file", "/abc/", "def", "/abc/def"),
372 ("input abs file with root /", "/", "/", "/"),
373 ("input dir with root /", "/", "def/", "/def/"),
374 ("input file with root /", "/", "def", "/def"),
375 ];
376
377 for (name, root, input, expect) in cases {
378 let actual = build_rooted_abs_path(root, input);
379 assert_eq!(actual, expect, "{name}")
380 }
381 }
382
383 #[test]
384 fn test_build_rel_path() {
385 let cases = vec![
386 ("input abs file", "/abc/", "/abc/def", "def"),
387 ("input dir", "/abc/", "/abc/def/", "def/"),
388 ("input file", "/abc/", "abc/def", "def"),
389 ("input dir with root /", "/", "def/", "def/"),
390 ("input file with root /", "/", "def", "def"),
391 ];
392
393 for (name, root, input, expect) in cases {
394 let actual = build_rel_path(root, input);
395 assert_eq!(actual, expect, "{name}")
396 }
397 }
398
399 #[test]
400 fn test_validate_path() {
401 let cases = vec![
402 ("input file with mode file", "abc", EntryMode::FILE, true),
403 ("input file with mode dir", "abc", EntryMode::DIR, false),
404 ("input dir with mode file", "abc/", EntryMode::FILE, false),
405 ("input dir with mode dir", "abc/", EntryMode::DIR, true),
406 ("root with mode dir", "/", EntryMode::DIR, true),
407 (
408 "input file with mode unknown",
409 "abc",
410 EntryMode::Unknown,
411 false,
412 ),
413 (
414 "input dir with mode unknown",
415 "abc/",
416 EntryMode::Unknown,
417 false,
418 ),
419 ];
420
421 for (name, path, mode, expect) in cases {
422 let actual = validate_path(path, mode);
423 assert_eq!(actual, expect, "{name}")
424 }
425 }
426
427 #[test]
428 fn test_build_tmp_path_of() {
429 let cases = vec![
430 ("a file path", "example.txt", "example.txt."),
431 (
432 "a file path in a directory",
433 "folder/example.txt",
434 "example.txt.",
435 ),
436 ];
437
438 for (name, path, expect_starts_with) in cases {
439 let actual = build_tmp_path_of(path);
440 assert!(
441 actual.starts_with(expect_starts_with),
442 "{name}: got `{actual}`, but expect `{expect_starts_with}`"
443 );
444 assert_eq!(
445 actual.len(),
446 expect_starts_with.len() + 8, "{name}: got `{actual}`, but expect `{expect_starts_with}`"
448 )
449 }
450 }
451}