1use crate::path_mapping::PathFormat;
12
13fn is_sep(c: char, fmt: PathFormat) -> bool {
15 match fmt {
16 PathFormat::Windows => c == '\\' || c == '/',
17 PathFormat::Posix | PathFormat::Uri => c == '/',
18 }
19}
20
21pub fn sep(fmt: PathFormat) -> char {
23 match fmt {
24 PathFormat::Windows => '\\',
25 _ => '/',
26 }
27}
28
29fn normalize(path: &str, fmt: PathFormat) -> &str {
32 if path.is_empty() || path == "." {
33 return path;
34 }
35 let anchor_len = anchor_len(path, fmt);
36 let mut end = path.len();
37 while end > anchor_len && is_sep(path.as_bytes()[end - 1] as char, fmt) {
38 end -= 1;
39 }
40 &path[..end]
41}
42
43fn anchor_len(path: &str, fmt: PathFormat) -> usize {
45 let bytes = path.as_bytes();
46 match fmt {
47 PathFormat::Windows => {
48 if bytes.len() >= 2 && bytes[0].is_ascii_alphabetic() && bytes[1] == b':' {
49 if bytes.len() > 2 && is_sep(bytes[2] as char, fmt) {
51 3
52 } else {
53 2
54 }
55 } else if bytes.len() >= 2
56 && is_sep(bytes[0] as char, fmt)
57 && is_sep(bytes[1] as char, fmt)
58 {
59 let rest = &path[2..];
61 let server_end = rest.find(|c: char| is_sep(c, fmt)).unwrap_or(rest.len());
62 let after_server = 2 + server_end;
63 if after_server < path.len() {
64 let share_rest = &path[after_server + 1..];
66 let share_end = share_rest
67 .find(|c: char| is_sep(c, fmt))
68 .unwrap_or(share_rest.len());
69 let end = after_server + 1 + share_end;
70 if end < path.len() {
74 end + 1
75 } else {
76 end
77 }
78 } else {
79 after_server
81 }
82 } else if !bytes.is_empty() && is_sep(bytes[0] as char, fmt) {
83 1
84 } else {
85 0
86 }
87 }
88 PathFormat::Posix | PathFormat::Uri => {
89 if bytes.len() >= 2
90 && bytes[0] == b'/'
91 && bytes[1] == b'/'
92 && (bytes.len() < 3 || bytes[2] != b'/')
93 {
94 2 } else if !bytes.is_empty() && bytes[0] == b'/' {
96 1
97 } else {
98 0
99 }
100 }
101 }
102}
103
104pub fn split(path: &str, fmt: PathFormat) -> (&str, &str) {
106 let path = normalize(path, fmt);
107 if path == "." {
108 return (".", "");
109 }
110 let anchor = anchor_len(path, fmt);
111 let anchor = anchor.min(path.len());
113
114 if path.len() <= anchor {
116 return (path, "");
117 }
118
119 let last_sep = path[anchor..].rfind(|c: char| is_sep(c, fmt));
120 match last_sep {
121 Some(i) => {
122 let sep_pos = anchor + i;
123 let parent = &path[..sep_pos];
124 let name = &path[sep_pos + 1..];
125 if parent.len() < anchor {
127 (&path[..anchor], name)
128 } else {
129 (parent, name)
130 }
131 }
132 None => {
133 (&path[..anchor], &path[anchor..])
135 }
136 }
137}
138
139pub fn file_name(path: &str, fmt: PathFormat) -> &str {
141 split(path, fmt).1
142}
143
144pub fn parent(path: &str, fmt: PathFormat) -> String {
147 let (p, _name) = split(path, fmt);
148 let base = p;
149 if fmt == PathFormat::Windows {
151 let s = base.replace('/', "\\");
152 if s.starts_with("\\\\") && s.matches('\\').count() >= 3 && !s.ends_with('\\') {
153 return format!("{}\\", s);
154 }
155 return s;
156 }
157 base.to_string()
158}
159
160pub fn file_stem(path: &str, fmt: PathFormat) -> &str {
162 let name = file_name(path, fmt);
163 match name.rfind('.') {
164 Some(0) | None => name, Some(i) => &name[..i],
166 }
167}
168
169pub fn extension(path: &str, fmt: PathFormat) -> &str {
171 let name = file_name(path, fmt);
172 match name.rfind('.') {
173 Some(0) | None => "", Some(i) => &name[i..],
175 }
176}
177
178pub fn extension_no_dot(path: &str, fmt: PathFormat) -> &str {
180 let ext = extension(path, fmt);
181 ext.strip_prefix('.').unwrap_or("")
182}
183
184pub fn parts(path: &str, fmt: PathFormat) -> Vec<String> {
190 let path = normalize(path, fmt);
191 if path.is_empty() || path == "." {
192 return Vec::new();
193 }
194
195 let mut result = Vec::new();
196 let anchor = anchor_len(path, fmt).min(path.len());
197
198 if anchor > 0 {
199 let mut anchor_str = path[..anchor].to_string();
200 if fmt == PathFormat::Windows {
202 anchor_str = anchor_str.replace('/', "\\");
203 if anchor_str.starts_with("\\\\")
205 && anchor_str.matches('\\').count() >= 3
206 && !anchor_str.ends_with('\\')
207 {
208 anchor_str.push('\\');
209 }
210 }
211 result.push(anchor_str);
212 }
213
214 let remaining = &path[anchor..];
215 for part in remaining.split(|c: char| is_sep(c, fmt)) {
216 if !part.is_empty() {
217 result.push(part.to_string());
218 }
219 }
220
221 result
222}
223
224pub fn suffixes(path: &str, fmt: PathFormat) -> Vec<String> {
227 let name = file_name(path, fmt);
228 let mut result = Vec::new();
229 if let Some(first_dot) = name.find('.') {
230 if first_dot == 0 {
231 if let Some(second_dot) = name[1..].find('.') {
233 let mut remaining = &name[1 + second_dot..];
234 while let Some(dot_pos) = remaining[1..].find('.') {
235 result.push(remaining[..dot_pos + 1].to_string());
236 remaining = &remaining[dot_pos + 1..];
237 }
238 result.push(remaining.to_string());
239 }
240 } else {
242 let mut remaining = &name[first_dot..];
243 while let Some(dot_pos) = remaining[1..].find('.') {
244 result.push(remaining[..dot_pos + 1].to_string());
245 remaining = &remaining[dot_pos + 1..];
246 }
247 result.push(remaining.to_string());
248 }
249 }
250 result
251}
252
253pub fn join_pathlib(parts: &[String], fmt: PathFormat) -> String {
261 match fmt {
262 PathFormat::Posix | PathFormat::Uri => join_pathlib_posix(parts),
263 PathFormat::Windows => join_pathlib_windows(parts),
264 }
265}
266
267fn join_pathlib_posix(parts: &[String]) -> String {
268 let mut segments: Vec<&str> = Vec::new();
269 let mut is_absolute = false;
270
271 for part in parts {
272 if part.is_empty() {
273 continue;
274 }
275 let sub_components: Vec<&str> = part.split('/').collect();
276 for (i, c) in sub_components.iter().enumerate() {
277 if c.is_empty() && i == 0 {
278 is_absolute = true;
280 segments.clear();
281 } else if *c == "." {
282 } else if !c.is_empty() {
284 segments.push(c);
285 }
286 }
287 }
288
289 if is_absolute {
290 if segments.is_empty() {
291 "/".to_string()
292 } else {
293 format!("/{}", segments.join("/"))
294 }
295 } else if segments.is_empty() {
296 ".".to_string()
297 } else {
298 segments.join("/")
299 }
300}
301
302fn win_parse_drive(s: &str) -> (&str, &str) {
305 let bytes = s.as_bytes();
306 if bytes.len() >= 2 && bytes[0].is_ascii_alphabetic() && bytes[1] == b':' {
307 (&s[..2], &s[2..])
308 } else if bytes.len() >= 2
309 && is_sep(bytes[0] as char, PathFormat::Windows)
310 && is_sep(bytes[1] as char, PathFormat::Windows)
311 {
312 let rest = &s[2..];
314 let server_end = rest
315 .find(|c: char| is_sep(c, PathFormat::Windows))
316 .unwrap_or(rest.len());
317 let after_server = 2 + server_end;
318 if after_server < s.len() {
319 let share_rest = &s[after_server + 1..];
320 let share_end = share_rest
321 .find(|c: char| is_sep(c, PathFormat::Windows))
322 .unwrap_or(share_rest.len());
323 let end = after_server + 1 + share_end;
324 (&s[..end], &s[end..])
325 } else {
326 (s, "")
327 }
328 } else {
329 ("", s)
330 }
331}
332
333fn join_pathlib_windows(parts: &[String]) -> String {
334 let mut drive = String::new();
337 let mut root = String::new();
338 let mut segments: Vec<String> = Vec::new();
339
340 for part in parts {
341 let (new_drive, after_drive) = win_parse_drive(part);
342 let has_root = !after_drive.is_empty()
343 && is_sep(after_drive.as_bytes()[0] as char, PathFormat::Windows);
344 let new_root = if has_root { "\\" } else { "" };
345 let rel = if has_root {
346 &after_drive[1..]
347 } else {
348 after_drive
349 };
350
351 if !new_drive.is_empty() {
352 if !new_drive.is_empty() && !drive.is_empty() && !new_drive.eq_ignore_ascii_case(&drive)
353 {
354 drive = new_drive.to_string();
356 root = new_root.to_string();
357 segments.clear();
358 } else {
359 drive = new_drive.to_string();
361 if !new_root.is_empty() {
362 root = new_root.to_string();
363 segments.clear();
364 }
365 }
366 } else if !new_root.is_empty() {
367 root = new_root.to_string();
369 segments.clear();
370 }
371 for c in rel.split(|c: char| is_sep(c, PathFormat::Windows)) {
373 if c == "." || c.is_empty() {
374 continue;
375 }
376 segments.push(c.to_string());
377 }
378 }
379
380 let mut result = format!("{}{}", drive, root);
382 if !segments.is_empty() {
383 if !result.is_empty() && !result.ends_with('\\') && !result.ends_with(':') {
384 result.push('\\');
385 }
386 result.push_str(&segments.join("\\"));
387 }
388
389 if result.is_empty() {
390 ".".to_string()
391 } else {
392 result
393 }
394}
395
396#[cfg(test)]
397mod tests {
398 use super::*;
399
400 #[test]
403 fn posix_parts_absolute() {
404 assert_eq!(
405 parts("/mnt/renders/scene.exr", PathFormat::Posix),
406 vec!["/", "mnt", "renders", "scene.exr"]
407 );
408 }
409
410 #[test]
411 fn posix_parts_relative() {
412 assert_eq!(
413 parts("sub/file.exr", PathFormat::Posix),
414 vec!["sub", "file.exr"]
415 );
416 }
417
418 #[test]
419 fn posix_parts_root() {
420 assert_eq!(parts("/", PathFormat::Posix), vec!["/"]);
421 }
422
423 #[test]
424 fn windows_parts_drive() {
425 assert_eq!(
426 parts(r"C:\mnt\file.txt", PathFormat::Windows),
427 vec![r"C:\", "mnt", "file.txt"]
428 );
429 }
430
431 #[test]
432 fn windows_parts_root_backslash() {
433 assert_eq!(
434 parts(r"\mnt\data\file.txt", PathFormat::Windows),
435 vec![r"\", "mnt", "data", "file.txt"]
436 );
437 }
438
439 #[test]
440 fn windows_parts_unc() {
441 assert_eq!(
442 parts(r"\\server\share\dir", PathFormat::Windows),
443 vec![r"\\server\share\", "dir"]
444 );
445 }
446
447 #[test]
448 fn posix_file_name() {
449 assert_eq!(
450 file_name("/mnt/renders/scene.exr", PathFormat::Posix),
451 "scene.exr"
452 );
453 }
454
455 #[test]
456 fn posix_parent() {
457 assert_eq!(
458 parent("/mnt/renders/scene.exr", PathFormat::Posix),
459 "/mnt/renders"
460 );
461 }
462
463 #[test]
464 fn posix_parent_root() {
465 assert_eq!(parent("/", PathFormat::Posix), "/");
466 }
467
468 #[test]
469 fn posix_file_stem() {
470 assert_eq!(
471 file_stem("/mnt/renders/scene.exr", PathFormat::Posix),
472 "scene"
473 );
474 }
475
476 #[test]
477 fn posix_extension() {
478 assert_eq!(
479 extension("/mnt/renders/scene.exr", PathFormat::Posix),
480 ".exr"
481 );
482 }
483
484 #[test]
485 fn no_extension() {
486 assert_eq!(extension("/mnt/renders/Makefile", PathFormat::Posix), "");
487 }
488
489 #[test]
490 fn posix_suffixes_single() {
491 assert_eq!(suffixes("scene.exr", PathFormat::Posix), vec![".exr"]);
492 }
493
494 #[test]
495 fn posix_suffixes_compound() {
496 assert_eq!(
497 suffixes("archive.tar.gz", PathFormat::Posix),
498 vec![".tar", ".gz"]
499 );
500 }
501
502 #[test]
503 fn posix_suffixes_none() {
504 assert_eq!(
505 suffixes("Makefile", PathFormat::Posix),
506 Vec::<String>::new()
507 );
508 }
509
510 #[test]
511 fn windows_parent_backslash() {
512 assert_eq!(
513 parent(r"\mnt\renders\scene.exr", PathFormat::Windows),
514 r"\mnt\renders"
515 );
516 }
517
518 #[test]
519 fn windows_file_name_mixed_sep() {
520 assert_eq!(
521 file_name(r"C:\mnt/renders\scene.exr", PathFormat::Windows),
522 "scene.exr"
523 );
524 }
525
526 #[test]
529 fn posix_parts_single_component() {
530 assert_eq!(parts("/mnt", PathFormat::Posix), vec!["/", "mnt"]);
531 }
532
533 #[test]
534 fn posix_parts_dot() {
535 let empty: Vec<String> = vec![];
537 assert_eq!(parts(".", PathFormat::Posix), empty);
538 }
539
540 #[test]
541 fn posix_parts_dotdot() {
542 assert_eq!(parts("..", PathFormat::Posix), vec![".."]);
543 }
544
545 #[test]
546 fn posix_parts_dotdot_foo() {
547 assert_eq!(parts("../foo", PathFormat::Posix), vec!["..", "foo"]);
548 }
549
550 #[test]
551 fn posix_parts_repeated_separators() {
552 assert_eq!(
554 parts("/mnt//renders///scene.exr", PathFormat::Posix),
555 vec!["/", "mnt", "renders", "scene.exr"]
556 );
557 }
558
559 #[test]
560 fn posix_parts_double_slash_root() {
561 assert_eq!(
563 parts("//mnt/file", PathFormat::Posix),
564 vec!["//", "mnt", "file"]
565 );
566 }
567
568 #[test]
569 fn posix_parts_trailing_slash() {
570 assert_eq!(
572 parts("/mnt/renders/", PathFormat::Posix),
573 vec!["/", "mnt", "renders"]
574 );
575 }
576
577 #[test]
578 fn posix_parts_deep() {
579 assert_eq!(
580 parts("/a/b/c/d/e", PathFormat::Posix),
581 vec!["/", "a", "b", "c", "d", "e"]
582 );
583 }
584
585 #[test]
586 fn posix_parts_bare_file() {
587 assert_eq!(parts("file.txt", PathFormat::Posix), vec!["file.txt"]);
588 }
589
590 #[test]
591 fn posix_parts_empty() {
592 let empty: Vec<String> = vec![];
593 assert_eq!(parts("", PathFormat::Posix), empty);
594 }
595
596 #[test]
599 fn posix_dot_name() {
600 assert_eq!(file_name(".", PathFormat::Posix), "");
601 }
602
603 #[test]
604 fn posix_dot_stem() {
605 assert_eq!(file_stem(".", PathFormat::Posix), "");
606 }
607
608 #[test]
609 fn posix_dot_suffix() {
610 assert_eq!(extension(".", PathFormat::Posix), "");
611 }
612
613 #[test]
614 fn posix_dot_parent() {
615 assert_eq!(parent(".", PathFormat::Posix), ".");
616 }
617
618 #[test]
619 fn posix_trailing_slash_name() {
620 assert_eq!(file_name("/mnt/renders/", PathFormat::Posix), "renders");
622 }
623
624 #[test]
625 fn posix_trailing_slash_stem() {
626 assert_eq!(file_stem("/mnt/renders/", PathFormat::Posix), "renders");
627 }
628
629 #[test]
630 fn posix_trailing_slash_suffix() {
631 assert_eq!(extension("/mnt/renders/", PathFormat::Posix), "");
632 }
633
634 #[test]
635 fn posix_trailing_slash_parent() {
636 assert_eq!(parent("/mnt/renders/", PathFormat::Posix), "/mnt");
638 }
639
640 #[test]
641 fn posix_hidden_tar_gz_suffixes() {
642 assert_eq!(
643 suffixes(".hidden.tar.gz", PathFormat::Posix),
644 vec![".tar", ".gz"]
645 );
646 }
647
648 #[test]
649 fn posix_hidden_tar_gz_stem() {
650 assert_eq!(
651 file_stem(".hidden.tar.gz", PathFormat::Posix),
652 ".hidden.tar"
653 );
654 }
655
656 #[test]
657 fn posix_hidden_tar_gz_suffix() {
658 assert_eq!(extension(".hidden.tar.gz", PathFormat::Posix), ".gz");
659 }
660
661 #[test]
664 fn windows_parts_drive_root() {
665 assert_eq!(parts(r"C:\", PathFormat::Windows), vec![r"C:\"]);
667 }
668
669 #[test]
670 fn windows_parts_drive_file() {
671 assert_eq!(
672 parts(r"C:\mnt\file.txt", PathFormat::Windows),
673 vec![r"C:\", "mnt", "file.txt"]
674 );
675 }
676
677 #[test]
678 fn windows_parts_forward_slash() {
679 assert_eq!(
681 parts("C:/path/to/file", PathFormat::Windows),
682 vec![r"C:\", "path", "to", "file"]
683 );
684 }
685
686 #[test]
687 fn windows_parts_repeated_separators() {
688 assert_eq!(
689 parts("C:/path//to///file", PathFormat::Windows),
690 vec![r"C:\", "path", "to", "file"]
691 );
692 }
693
694 #[test]
695 fn windows_parts_unc_root() {
696 assert_eq!(
698 parts(r"\\server\share", PathFormat::Windows),
699 vec![r"\\server\share\"]
700 );
701 }
702
703 #[test]
704 fn windows_parts_unc_dir() {
705 assert_eq!(
706 parts(r"\\server\share\dir", PathFormat::Windows),
707 vec![r"\\server\share\", "dir"]
708 );
709 }
710
711 #[test]
712 fn windows_parts_unc_dir_file() {
713 assert_eq!(
714 parts(r"\\server\share\dir\file.txt", PathFormat::Windows),
715 vec![r"\\server\share\", "dir", "file.txt"]
716 );
717 }
718
719 #[test]
720 fn windows_parts_root_only() {
721 assert_eq!(
723 parts(r"\mnt\data\file.txt", PathFormat::Windows),
724 vec![r"\", "mnt", "data", "file.txt"]
725 );
726 }
727
728 #[test]
729 fn windows_parts_unc_no_share() {
730 assert_eq!(parts(r"\\server", PathFormat::Windows), vec![r"\\server"]);
733 }
734
735 #[test]
736 fn windows_parts_relative_drive() {
737 assert_eq!(parts("C:", PathFormat::Windows), vec!["C:"]);
739 }
740
741 #[test]
742 fn windows_parts_trailing_slash() {
743 assert_eq!(parts(r"C:\mnt\", PathFormat::Windows), vec![r"C:\", "mnt"]);
745 }
746
747 #[test]
748 fn windows_parts_unc_trailing_slash() {
749 assert_eq!(
751 parts(r"\\server\share\", PathFormat::Windows),
752 vec![r"\\server\share\"]
753 );
754 }
755
756 #[test]
759 fn windows_drive_root_name() {
760 assert_eq!(file_name(r"C:\", PathFormat::Windows), "");
762 }
763
764 #[test]
765 fn windows_drive_root_parent() {
766 assert_eq!(parent(r"C:\", PathFormat::Windows), r"C:\");
768 }
769
770 #[test]
771 fn windows_unc_name() {
772 assert_eq!(file_name(r"\\server\share", PathFormat::Windows), "");
774 }
775
776 #[test]
777 fn windows_unc_parent() {
778 assert_eq!(
781 parent(r"\\server\share", PathFormat::Windows),
782 r"\\server\share\"
783 );
784 }
785
786 #[test]
787 fn windows_unc_dir_name() {
788 assert_eq!(file_name(r"\\server\share\dir", PathFormat::Windows), "dir");
790 }
791
792 #[test]
793 fn windows_unc_dir_parent() {
794 assert_eq!(
796 parent(r"\\server\share\dir", PathFormat::Windows),
797 r"\\server\share\"
798 );
799 }
800
801 #[test]
806 fn windows_forward_slash_file_name() {
807 assert_eq!(
808 file_name("/input/scene.exr", PathFormat::Windows),
809 "scene.exr"
810 );
811 }
812
813 #[test]
814 fn windows_forward_slash_file_stem() {
815 assert_eq!(file_stem("/input/scene.exr", PathFormat::Windows), "scene");
816 }
817
818 #[test]
819 fn windows_forward_slash_extension() {
820 assert_eq!(extension("/input/scene.exr", PathFormat::Windows), ".exr");
821 }
822
823 #[test]
824 fn windows_forward_slash_parent() {
825 assert_eq!(parent("/input/scene.exr", PathFormat::Windows), r"\input");
826 }
827
828 #[test]
829 fn windows_forward_slash_parts() {
830 assert_eq!(
831 parts("/input/scene.exr", PathFormat::Windows),
832 vec![r"\", "input", "scene.exr"]
833 );
834 }
835}