1use std::borrow::Cow;
2use std::ffi::OsString;
3use std::path::{Component, Path, PathBuf, Prefix};
4use std::sync::LazyLock;
5
6use either::Either;
7use path_slash::PathExt;
8
9#[expect(clippy::print_stderr)]
11pub static CWD: LazyLock<PathBuf> = LazyLock::new(|| {
12 std::env::current_dir().unwrap_or_else(|_e| {
13 eprintln!("Current directory does not exist");
14 std::process::exit(1);
15 })
16});
17
18pub trait Simplified {
19 fn simplified(&self) -> &Path;
23
24 fn simplified_display(&self) -> impl std::fmt::Display;
29
30 fn simple_canonicalize(&self) -> std::io::Result<PathBuf>;
32
33 fn user_display(&self) -> impl std::fmt::Display;
37
38 fn user_display_from(&self, base: impl AsRef<Path>) -> impl std::fmt::Display;
43
44 fn portable_display(&self) -> impl std::fmt::Display;
48}
49
50impl<T: AsRef<Path>> Simplified for T {
51 fn simplified(&self) -> &Path {
52 dunce::simplified(self.as_ref())
53 }
54
55 fn simplified_display(&self) -> impl std::fmt::Display {
56 dunce::simplified(self.as_ref()).display()
57 }
58
59 fn simple_canonicalize(&self) -> std::io::Result<PathBuf> {
60 dunce::canonicalize(self.as_ref())
61 }
62
63 fn user_display(&self) -> impl std::fmt::Display {
64 let path = dunce::simplified(self.as_ref());
65
66 if CWD.ancestors().nth(1).is_none() {
68 return path.display();
69 }
70
71 let path = path.strip_prefix(CWD.simplified()).unwrap_or(path);
74
75 if path.as_os_str() == "" {
76 return Path::new(".").display();
78 }
79
80 path.display()
81 }
82
83 fn user_display_from(&self, base: impl AsRef<Path>) -> impl std::fmt::Display {
84 let path = dunce::simplified(self.as_ref());
85
86 if CWD.ancestors().nth(1).is_none() {
88 return path.display();
89 }
90
91 let path = path
94 .strip_prefix(base.as_ref())
95 .unwrap_or_else(|_| path.strip_prefix(CWD.simplified()).unwrap_or(path));
96
97 if path.as_os_str() == "" {
98 return Path::new(".").display();
100 }
101
102 path.display()
103 }
104
105 fn portable_display(&self) -> impl std::fmt::Display {
106 let path = dunce::simplified(self.as_ref());
107
108 let path = path.strip_prefix(CWD.simplified()).unwrap_or(path);
111
112 path.to_slash()
114 .map(Either::Left)
115 .unwrap_or_else(|| Either::Right(path.display()))
116 }
117}
118
119pub trait PythonExt {
120 fn escape_for_python(&self) -> String;
129}
130
131impl<T: AsRef<Path>> PythonExt for T {
132 fn escape_for_python(&self) -> String {
133 escape_path_for_python(self)
134 }
135}
136
137#[cfg(unix)]
142fn escape_path_for_python<P: AsRef<Path>>(path: P) -> String {
143 use std::os::unix::ffi::OsStrExt;
144 format!(
145 r#"__import__("os").fsdecode(b"{}")"#,
146 path.as_ref().as_os_str().as_bytes().escape_ascii()
147 )
148}
149
150#[cfg(windows)]
152fn escape_path_for_python<P: AsRef<Path>>(path: P) -> String {
153 use std::fmt::Write;
154 use std::os::windows::ffi::OsStrExt;
155
156 let mut literal = String::new();
157 literal.push('"');
158 for character in char::decode_utf16(path.as_ref().as_os_str().encode_wide()) {
159 match character {
160 Ok(character) if character.is_ascii() => {
161 literal.extend((character as u8).escape_ascii().map(char::from));
162 }
163 Ok(character) if character.is_control() => {
165 let _ = write!(literal, r"\u{:04x}", u32::from(character));
166 }
167 Ok(character) => literal.push(character),
168 Err(error) => {
169 let _ = write!(literal, r"\u{:04x}", error.unpaired_surrogate());
170 }
171 }
172 }
173 literal.push('"');
174 literal
175}
176
177pub fn normalize_url_path(path: &str) -> Cow<'_, str> {
184 let path = percent_encoding::percent_decode_str(path)
186 .decode_utf8()
187 .unwrap_or(Cow::Borrowed(path));
188
189 if cfg!(windows) {
191 Cow::Owned(
192 path.strip_prefix('/')
193 .unwrap_or(&path)
194 .replace('/', std::path::MAIN_SEPARATOR_STR),
195 )
196 } else {
197 path
198 }
199}
200
201pub fn is_windows_absolute_path(path: &str) -> bool {
203 let [drive, b':', separator, ..] = path.as_bytes() else {
204 return false;
205 };
206 drive.is_ascii_alphabetic() && matches!(separator, b'/' | b'\\')
207}
208
209pub fn normalize_absolute_path(path: &Path) -> Result<PathBuf, std::io::Error> {
226 let mut components = path.components().peekable();
227 let mut ret = components
228 .next_if_map_mut(|component| match component {
229 Component::Prefix(..) => Some(PathBuf::from(component.as_os_str())),
230 _ => None,
231 })
232 .unwrap_or_default();
233
234 for component in components {
235 match component {
236 Component::Prefix(..) => unreachable!(),
237 Component::RootDir => {
238 ret.push(component.as_os_str());
239 }
240 Component::CurDir => {}
241 Component::ParentDir => {
242 if !ret.pop() {
243 return Err(std::io::Error::new(
244 std::io::ErrorKind::InvalidInput,
245 format!(
246 "cannot normalize a relative path beyond the base directory: {}",
247 path.display()
248 ),
249 ));
250 }
251 }
252 Component::Normal(c) => {
253 ret.push(c);
254 }
255 }
256 }
257 Ok(ret)
258}
259
260fn path_equals_components(path: &Path) -> bool {
267 let mut expected_len = 0;
270 let mut next_needs_separator = false;
271 for component in path.components() {
272 let bytes = component.as_os_str().as_encoded_bytes();
273 if next_needs_separator && !matches!(component, Component::RootDir) {
276 expected_len += Path::new("/").as_os_str().as_encoded_bytes().len();
278 }
279 expected_len += bytes.len();
280 next_needs_separator = match component {
281 Component::RootDir => false,
283 Component::Prefix(_) => false,
285 _ => true,
286 };
287 }
288 expected_len == path.as_os_str().as_encoded_bytes().len()
289}
290
291pub fn normalize_path<'path>(path: impl Into<Cow<'path, Path>>) -> Cow<'path, Path> {
297 let path = path.into();
298 if path
300 .components()
301 .any(|component| matches!(component, Component::ParentDir | Component::CurDir))
302 {
303 return Cow::Owned(normalized(&path));
304 }
305
306 if !path_equals_components(&path) {
309 return Cow::Owned(normalized(&path));
310 }
311
312 path
314}
315
316pub fn normalize_path_under(path: impl AsRef<Path>, root: impl AsRef<Path>) -> Option<PathBuf> {
321 let path = normalize_path(path.as_ref()).into_owned();
322 let root = normalize_path(root.as_ref());
323
324 if root.as_os_str().is_empty() || path.as_path() == root.as_ref() {
325 None
326 } else {
327 path.starts_with(root.as_ref()).then_some(path)
328 }
329}
330
331fn normalized(path: &Path) -> PathBuf {
349 let mut normalized = PathBuf::new();
350 for component in path.components() {
351 match component {
352 Component::Prefix(_) | Component::RootDir | Component::Normal(_) => {
353 normalized.push(component);
355 }
356 Component::ParentDir => {
357 match normalized.components().next_back() {
358 None | Some(Component::ParentDir | Component::RootDir) => {
359 normalized.push(component);
361 }
362 Some(Component::Normal(_) | Component::Prefix(_) | Component::CurDir) => {
363 normalized.pop();
365 }
366 }
367 }
368 Component::CurDir => {
369 }
371 }
372 }
373 normalized
374}
375
376pub fn relative_to(
385 path: impl AsRef<Path>,
386 base: impl AsRef<Path>,
387) -> Result<PathBuf, std::io::Error> {
388 let path = normalize_path(path.as_ref());
390 let base = normalize_path(base.as_ref());
391
392 let (stripped, common_prefix) = base
394 .ancestors()
395 .find_map(|ancestor| {
396 dunce::simplified(&path)
398 .strip_prefix(dunce::simplified(ancestor))
399 .ok()
400 .map(|stripped| (stripped, ancestor))
401 })
402 .ok_or_else(|| {
403 std::io::Error::other(format!(
404 "Trivial strip failed: {} vs. {}",
405 path.simplified_display(),
406 base.simplified_display()
407 ))
408 })?;
409
410 let levels_up = base.components().count() - common_prefix.components().count();
412 let up = std::iter::repeat_n("..", levels_up).collect::<PathBuf>();
413
414 Ok(up.join(stripped))
415}
416
417pub fn find_git_repository_root(path: &Path) -> Option<&Path> {
422 path.ancestors()
424 .find(|ancestor| ancestor.join(".git").exists())
425}
426
427pub fn try_relative_to_if(
430 path: impl AsRef<Path>,
431 base: impl AsRef<Path>,
432 should_relativize: bool,
433) -> Result<PathBuf, std::io::Error> {
434 if should_relativize {
435 relative_to(&path, &base).or_else(|_| std::path::absolute(path.as_ref()))
436 } else {
437 std::path::absolute(path.as_ref())
438 }
439}
440
441pub fn verbatim_path(path: &Path) -> Cow<'_, Path> {
468 if !cfg!(windows) {
469 return Cow::Borrowed(path);
470 }
471
472 let resolved_path = if path.is_relative() {
476 Cow::Owned(CWD.join(path))
477 } else {
478 Cow::Borrowed(path)
479 };
480
481 if let Some(Component::Prefix(prefix)) = resolved_path.components().next() {
483 match prefix.kind() {
484 Prefix::UNC(..) | Prefix::Disk(_) => {},
485 Prefix::DeviceNS(_)
487 | Prefix::Verbatim(_)
489 | Prefix::VerbatimDisk(_)
490 | Prefix::VerbatimUNC(..) => return Cow::Borrowed(path)
491 }
492 }
493
494 let normalized_path = normalized(&resolved_path);
496
497 let mut components = normalized_path.components();
498 let Some(Component::Prefix(prefix)) = components.next() else {
499 return Cow::Borrowed(path);
500 };
501
502 match prefix.kind() {
503 Prefix::Disk(_) => {
505 let mut result = OsString::from(r"\\?\");
506 result.push(normalized_path.as_os_str()); Cow::Owned(PathBuf::from(result))
508 }
509 Prefix::UNC(server, share) => {
511 let mut result = OsString::from(r"\\?\UNC\");
512 result.push(server);
513 result.push(r"\");
514 result.push(share);
515 for component in components {
516 match component {
517 Component::RootDir => {} Component::Prefix(_) => {
519 debug_assert!(false, "prefix already consumed");
520 }
521 Component::CurDir | Component::ParentDir => {
522 debug_assert!(false, "path already normalized");
523 }
524 Component::Normal(_) => {
525 result.push(r"\");
526 result.push(component.as_os_str());
527 }
528 }
529 }
530 Cow::Owned(PathBuf::from(result))
531 }
532 Prefix::DeviceNS(_)
533 | Prefix::Verbatim(_)
534 | Prefix::VerbatimDisk(_)
535 | Prefix::VerbatimUNC(..) => {
536 debug_assert!(false, "skipped via fast path");
537 Cow::Borrowed(path)
538 }
539 }
540}
541
542#[derive(Debug, Clone, PartialEq, Eq)]
547pub struct PortablePath<'a>(&'a Path);
548
549#[derive(Debug, Clone, PartialEq, Eq)]
550pub struct PortablePathBuf(Box<Path>);
551
552#[cfg(feature = "schemars")]
553impl schemars::JsonSchema for PortablePathBuf {
554 fn schema_name() -> Cow<'static, str> {
555 Cow::Borrowed("PortablePathBuf")
556 }
557
558 fn json_schema(_gen: &mut schemars::generate::SchemaGenerator) -> schemars::Schema {
559 PathBuf::json_schema(_gen)
560 }
561}
562
563impl AsRef<Path> for PortablePath<'_> {
564 fn as_ref(&self) -> &Path {
565 self.0
566 }
567}
568
569impl<'a, T> From<&'a T> for PortablePath<'a>
570where
571 T: AsRef<Path> + ?Sized,
572{
573 fn from(path: &'a T) -> Self {
574 PortablePath(path.as_ref())
575 }
576}
577
578impl std::fmt::Display for PortablePath<'_> {
579 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
580 let path = self.0.to_slash_lossy();
581 if path.is_empty() {
582 write!(f, ".")
583 } else {
584 write!(f, "{path}")
585 }
586 }
587}
588
589impl std::fmt::Display for PortablePathBuf {
590 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
591 let path = self.0.to_slash_lossy();
592 if path.is_empty() {
593 write!(f, ".")
594 } else {
595 write!(f, "{path}")
596 }
597 }
598}
599
600impl From<&str> for PortablePathBuf {
601 fn from(path: &str) -> Self {
602 if path == "." {
603 Self(PathBuf::new().into_boxed_path())
604 } else {
605 Self(PathBuf::from(path).into_boxed_path())
606 }
607 }
608}
609
610impl From<PortablePathBuf> for Box<Path> {
611 fn from(portable: PortablePathBuf) -> Self {
612 portable.0
613 }
614}
615
616impl From<Box<Path>> for PortablePathBuf {
617 fn from(path: Box<Path>) -> Self {
618 Self(path)
619 }
620}
621
622impl<'a> From<&'a Path> for PortablePathBuf {
623 fn from(path: &'a Path) -> Self {
624 Box::<Path>::from(path).into()
625 }
626}
627
628#[cfg(feature = "serde")]
629impl serde::Serialize for PortablePathBuf {
630 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
631 where
632 S: serde::ser::Serializer,
633 {
634 self.to_string().serialize(serializer)
635 }
636}
637
638#[cfg(feature = "serde")]
639impl serde::Serialize for PortablePath<'_> {
640 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
641 where
642 S: serde::ser::Serializer,
643 {
644 self.to_string().serialize(serializer)
645 }
646}
647
648#[cfg(feature = "serde")]
649impl<'de> serde::de::Deserialize<'de> for PortablePathBuf {
650 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
651 where
652 D: serde::de::Deserializer<'de>,
653 {
654 let s = <Cow<'_, str>>::deserialize(deserializer)?;
655 if s == "." {
656 Ok(Self(PathBuf::new().into_boxed_path()))
657 } else {
658 Ok(Self(PathBuf::from(s.as_ref()).into_boxed_path()))
659 }
660 }
661}
662
663impl AsRef<Path> for PortablePathBuf {
664 fn as_ref(&self) -> &Path {
665 &self.0
666 }
667}
668
669#[cfg(test)]
670mod tests {
671 use std::assert_matches;
672
673 use super::*;
674
675 #[test]
676 fn test_find_git_repository_root() -> std::io::Result<()> {
677 let temp_dir = tempfile::tempdir()?;
678
679 let repository = temp_dir.path().join("repository");
680 let nested = repository.join("packages/project");
681 fs_err::create_dir_all(repository.join(".git"))?;
682 fs_err::create_dir_all(&nested)?;
683 assert_eq!(
684 find_git_repository_root(&nested),
685 Some(repository.as_path())
686 );
687
688 let worktree = temp_dir.path().join("worktree");
689 let nested = worktree.join("packages/project");
690 fs_err::create_dir_all(&nested)?;
691 fs_err::write(worktree.join(".git"), "gitdir: ../repository/.git")?;
692 assert_eq!(find_git_repository_root(&nested), Some(worktree.as_path()));
693
694 Ok(())
695 }
696
697 #[test]
698 fn test_normalize_url() {
699 if cfg!(windows) {
700 assert_eq!(
701 normalize_url_path("/C:/Users/ferris/wheel-0.42.0.tar.gz"),
702 "C:\\Users\\ferris\\wheel-0.42.0.tar.gz"
703 );
704 } else {
705 assert_eq!(
706 normalize_url_path("/C:/Users/ferris/wheel-0.42.0.tar.gz"),
707 "/C:/Users/ferris/wheel-0.42.0.tar.gz"
708 );
709 }
710
711 if cfg!(windows) {
712 assert_eq!(
713 normalize_url_path("./ferris/wheel-0.42.0.tar.gz"),
714 ".\\ferris\\wheel-0.42.0.tar.gz"
715 );
716 } else {
717 assert_eq!(
718 normalize_url_path("./ferris/wheel-0.42.0.tar.gz"),
719 "./ferris/wheel-0.42.0.tar.gz"
720 );
721 }
722
723 if cfg!(windows) {
724 assert_eq!(
725 normalize_url_path("./wheel%20cache/wheel-0.42.0.tar.gz"),
726 ".\\wheel cache\\wheel-0.42.0.tar.gz"
727 );
728 } else {
729 assert_eq!(
730 normalize_url_path("./wheel%20cache/wheel-0.42.0.tar.gz"),
731 "./wheel cache/wheel-0.42.0.tar.gz"
732 );
733 }
734 }
735
736 #[test]
737 fn test_normalize_path() {
738 let path = Path::new("/a/b/../c/./d");
739 let normalized = normalize_absolute_path(path).unwrap();
740 assert_eq!(normalized, Path::new("/a/c/d"));
741
742 let path = Path::new("/a/../c/./d");
743 let normalized = normalize_absolute_path(path).unwrap();
744 assert_eq!(normalized, Path::new("/c/d"));
745
746 let path = Path::new("/a/../../c/./d");
748 let err = normalize_absolute_path(path).unwrap_err();
749 assert_eq!(err.kind(), std::io::ErrorKind::InvalidInput);
750 }
751
752 #[test]
753 fn test_relative_to() {
754 assert_eq!(
755 relative_to(
756 Path::new("/home/ferris/carcinization/lib/python/site-packages/foo/__init__.py"),
757 Path::new("/home/ferris/carcinization/lib/python/site-packages"),
758 )
759 .unwrap(),
760 Path::new("foo/__init__.py")
761 );
762 assert_eq!(
763 relative_to(
764 Path::new("/home/ferris/carcinization/lib/marker.txt"),
765 Path::new("/home/ferris/carcinization/lib/python/site-packages"),
766 )
767 .unwrap(),
768 Path::new("../../marker.txt")
769 );
770 assert_eq!(
771 relative_to(
772 Path::new("/home/ferris/carcinization/bin/foo_launcher"),
773 Path::new("/home/ferris/carcinization/lib/python/site-packages"),
774 )
775 .unwrap(),
776 Path::new("../../../bin/foo_launcher")
777 );
778 }
779
780 #[test]
781 fn test_normalize_relative() {
782 let cases = [
783 (
784 "../../workspace-git-path-dep-test/packages/c/../../packages/d",
785 "../../workspace-git-path-dep-test/packages/d",
786 ),
787 (
788 "workspace-git-path-dep-test/packages/c/../../packages/d",
789 "workspace-git-path-dep-test/packages/d",
790 ),
791 ("./a/../../b", "../b"),
792 ("/usr/../../foo", "/../foo"),
793 ("foo/./bar", "foo/bar"),
795 ("/a/./b/./c", "/a/b/c"),
796 ("./foo/bar", "foo/bar"),
797 (".", ""),
798 ("./.", ""),
799 ("foo/.", "foo"),
800 ("foo//bar", "foo/bar"),
802 ("/a///b//c", "/a/b/c"),
803 ("foo/./../bar", "bar"),
805 ("foo/bar/./../baz", "foo/baz"),
806 ("foo/bar", "foo/bar"),
808 ("/a/b/c", "/a/b/c"),
809 ("", ""),
810 ];
811 for (input, expected) in cases {
812 assert_eq!(
813 normalize_path(Path::new(input)),
814 Path::new(expected),
815 "input: {input:?}"
816 );
817 }
818
819 for already_normalized in ["foo/bar", "/a/b/c", "foo", "/", ""] {
821 let path = Path::new(already_normalized);
822 assert_matches!(
823 normalize_path(path),
824 Cow::Borrowed(_),
825 "expected borrowed for {already_normalized:?}"
826 );
827 }
828 }
829
830 #[test]
831 fn test_normalize_path_under() {
832 assert_eq!(
833 normalize_path_under("scripts/script", "scripts"),
834 Some(PathBuf::from("scripts/script"))
835 );
836 assert_eq!(
837 normalize_path_under("/scripts/script", "/scripts"),
838 Some(PathBuf::from("/scripts/script"))
839 );
840 assert_eq!(
841 normalize_path_under("scripts/nested/../script", "scripts"),
842 Some(PathBuf::from("scripts/script"))
843 );
844 assert_eq!(
845 normalize_path_under("/scripts/nested/../script", "/scripts"),
846 Some(PathBuf::from("/scripts/script"))
847 );
848 assert_eq!(normalize_path_under("scripts/.", "scripts"), None);
849 assert_eq!(normalize_path_under("/scripts/.", "/scripts"), None);
850 assert_eq!(normalize_path_under("scripts/../script", "scripts"), None);
851 assert_eq!(normalize_path_under("/scripts/../script", "/scripts"), None);
852 assert_eq!(normalize_path_under("scripts/script", "."), None);
853 assert_eq!(normalize_path_under("scripts/script", ""), None);
854 }
855
856 #[test]
857 fn test_normalize_trailing_path_separator() {
858 let cases = [
859 (
860 "/home/ferris/projects/python/",
861 "/home/ferris/projects/python",
862 ),
863 ("python/", "python"),
864 ("/", "/"),
865 ("foo/bar/", "foo/bar"),
866 ("foo//", "foo"),
867 ];
868 for (input, expected) in cases {
869 assert_eq!(normalize_path(Path::new(input)), Path::new(expected));
870 }
871 }
872
873 #[test]
874 #[cfg(windows)]
875 fn test_normalize_windows() {
876 let cases = [
877 (
878 r"C:\Users\Ferris\projects\python\",
879 r"C:\Users\Ferris\projects\python",
880 ),
881 (r"C:\foo\.\bar", r"C:\foo\bar"),
882 (r"C:\foo\\bar", r"C:\foo\bar"),
883 (r"C:\foo\bar\..\baz", r"C:\foo\baz"),
884 (r"foo\.\bar", r"foo\bar"),
885 (r"C:foo", r"C:foo"),
886 (r"C:\foo", r"C:\foo"),
887 (r"C:\\foo", r"C:\foo"),
888 (r"\\?\C:foo", r"\\?\C:foo"),
889 (r"\\?\C:\foo", r"\\?\C:\foo"),
890 (r"\\?\C:\\foo", r"\\?\C:\foo"),
891 (r"\\server\share\foo", r"\\server\share\foo"),
892 ];
893 for (input, expected) in cases {
894 assert_eq!(normalize_path(Path::new(input)), Path::new(expected));
895 }
896 }
897
898 #[cfg(windows)]
899 #[test]
900 fn test_verbatim_path() {
901 let relative_path = format!(r"\\?\{}\path\to\logging.", CWD.simplified_display());
902 let relative_root = format!(
903 r"\\?\{}\path\to\logging.",
904 CWD.components()
905 .next()
906 .expect("expected a drive letter prefix")
907 .simplified_display()
908 );
909 let cases = [
910 (r"C:\path\to\logging.", r"\\?\C:\path\to\logging."),
912 (r"C:\path\to\.\logging.", r"\\?\C:\path\to\logging."),
913 (r"C:\path\to\..\to\logging.", r"\\?\C:\path\to\logging."),
914 (r"C:/path/to/../to/./logging.", r"\\?\C:\path\to\logging."),
915 (r"C:path\to\..\to\logging.", r"\\?\C:path\to\logging."), (r".\path\to\.\logging.", relative_path.as_str()),
917 (r"path\to\..\to\logging.", relative_path.as_str()),
918 (r"./path/to/logging.", relative_path.as_str()),
919 (r"\path\to\logging.", relative_root.as_str()),
920 (
922 r"\\127.0.0.1\c$\path\to\logging.",
923 r"\\?\UNC\127.0.0.1\c$\path\to\logging.",
924 ),
925 (
926 r"\\127.0.0.1\c$\path\to\.\logging.",
927 r"\\?\UNC\127.0.0.1\c$\path\to\logging.",
928 ),
929 (
930 r"\\127.0.0.1\c$\path\to\..\to\logging.",
931 r"\\?\UNC\127.0.0.1\c$\path\to\logging.",
932 ),
933 (
934 r"//127.0.0.1/c$/path/to/../to/./logging.",
935 r"\\?\UNC\127.0.0.1\c$\path\to\logging.",
936 ),
937 (r"\\?\C:\path\to\logging.", r"\\?\C:\path\to\logging."),
939 (
941 r"\\?\UNC\127.0.0.1\c$\path\to\logging.",
942 r"\\?\UNC\127.0.0.1\c$\path\to\logging.",
943 ),
944 (r"\\.\PhysicalDrive0", r"\\.\PhysicalDrive0"),
946 (r"\\.\NUL", r"\\.\NUL"),
947 ];
948
949 for (input, expected) in cases {
950 assert_eq!(verbatim_path(Path::new(input)), Path::new(expected));
951 }
952 }
953
954 #[test]
955 #[cfg(unix)]
956 fn test_path_escape_for_python() {
957 use std::ffi::OsStr;
958 use std::os::unix::ffi::OsStrExt;
959
960 for (range, expected) in [
963 (
964 1..=0x5e,
965 r##"__import__("os").fsdecode(b"/foo/\x01\x02\x03\x04\x05\x06\x07\x08\t\n\x0b\x0c\r\x0e\x0f\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19\x1a\x1b\x1c\x1d\x1e\x1f !\"#$%&\'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^/bar")"##,
966 ),
967 (
968 0x5f..=0xa3,
969 r#"__import__("os").fsdecode(b"/foo/_`abcdefghijklmnopqrstuvwxyz{|}~\x7f\x80\x81\x82\x83\x84\x85\x86\x87\x88\x89\x8a\x8b\x8c\x8d\x8e\x8f\x90\x91\x92\x93\x94\x95\x96\x97\x98\x99\x9a\x9b\x9c\x9d\x9e\x9f\xa0\xa1\xa2\xa3/bar")"#,
970 ),
971 (
972 0xa4..=0xd1,
973 r#"__import__("os").fsdecode(b"/foo/\xa4\xa5\xa6\xa7\xa8\xa9\xaa\xab\xac\xad\xae\xaf\xb0\xb1\xb2\xb3\xb4\xb5\xb6\xb7\xb8\xb9\xba\xbb\xbc\xbd\xbe\xbf\xc0\xc1\xc2\xc3\xc4\xc5\xc6\xc7\xc8\xc9\xca\xcb\xcc\xcd\xce\xcf\xd0\xd1/bar")"#,
974 ),
975 (
976 0xd2..=u8::MAX,
977 r#"__import__("os").fsdecode(b"/foo/\xd2\xd3\xd4\xd5\xd6\xd7\xd8\xd9\xda\xdb\xdc\xdd\xde\xdf\xe0\xe1\xe2\xe3\xe4\xe5\xe6\xe7\xe8\xe9\xea\xeb\xec\xed\xee\xef\xf0\xf1\xf2\xf3\xf4\xf5\xf6\xf7\xf8\xf9\xfa\xfb\xfc\xfd\xfe\xff/bar")"#,
978 ),
979 ] {
980 let bytes = b"/foo/"
981 .iter()
982 .copied()
983 .chain(range)
984 .chain(b"/bar".iter().copied())
985 .collect::<Box<[_]>>();
986 let path = Path::new(OsStr::from_bytes(&bytes));
987 assert_eq!(path.escape_for_python(), expected);
988 }
989 }
990
991 #[cfg(windows)]
992 #[test]
993 fn test_path_escape_for_python() {
994 use std::os::windows::ffi::OsStringExt;
995
996 let path_osstr = OsString::from_wide(&[
999 0x5c, 0x5c, 0x3f, 0x5c, 0x43, 0x3a, 0x5c, 0x63, 0x61, 0x66, 0x00e9, 0x5c, 0xd83d,
1000 0xde00, 0x5c, 0xd800, 0x78, 0xdc00, 0x22, 0x09, 0x0a, 0x0d,
1001 ]);
1002 let path = Path::new(&path_osstr);
1003 assert_eq!(
1005 path.escape_for_python(),
1006 r#""\\\\?\\C:\\café\\😀\\\ud800x\udc00\"\t\n\r""#
1007 );
1008 }
1009}