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 normalize_absolute_path(path: &Path) -> Result<PathBuf, std::io::Error> {
218 let mut components = path.components().peekable();
219 let mut ret = components
220 .next_if_map_mut(|component| match component {
221 Component::Prefix(..) => Some(PathBuf::from(component.as_os_str())),
222 _ => None,
223 })
224 .unwrap_or_default();
225
226 for component in components {
227 match component {
228 Component::Prefix(..) => unreachable!(),
229 Component::RootDir => {
230 ret.push(component.as_os_str());
231 }
232 Component::CurDir => {}
233 Component::ParentDir => {
234 if !ret.pop() {
235 return Err(std::io::Error::new(
236 std::io::ErrorKind::InvalidInput,
237 format!(
238 "cannot normalize a relative path beyond the base directory: {}",
239 path.display()
240 ),
241 ));
242 }
243 }
244 Component::Normal(c) => {
245 ret.push(c);
246 }
247 }
248 }
249 Ok(ret)
250}
251
252fn path_equals_components(path: &Path) -> bool {
259 let mut expected_len = 0;
262 let mut next_needs_separator = false;
263 for component in path.components() {
264 let bytes = component.as_os_str().as_encoded_bytes();
265 if next_needs_separator && !matches!(component, Component::RootDir) {
268 expected_len += Path::new("/").as_os_str().as_encoded_bytes().len();
270 }
271 expected_len += bytes.len();
272 next_needs_separator = match component {
273 Component::RootDir => false,
275 Component::Prefix(_) => false,
277 _ => true,
278 };
279 }
280 expected_len == path.as_os_str().as_encoded_bytes().len()
281}
282
283pub fn normalize_path<'path>(path: impl Into<Cow<'path, Path>>) -> Cow<'path, Path> {
289 let path = path.into();
290 if path
292 .components()
293 .any(|component| matches!(component, Component::ParentDir | Component::CurDir))
294 {
295 return Cow::Owned(normalized(&path));
296 }
297
298 if !path_equals_components(&path) {
301 return Cow::Owned(normalized(&path));
302 }
303
304 path
306}
307
308pub fn normalize_path_under(path: impl AsRef<Path>, root: impl AsRef<Path>) -> Option<PathBuf> {
313 let path = normalize_path(path.as_ref()).into_owned();
314 let root = normalize_path(root.as_ref());
315
316 if root.as_os_str().is_empty() || path.as_path() == root.as_ref() {
317 None
318 } else {
319 path.starts_with(root.as_ref()).then_some(path)
320 }
321}
322
323fn normalized(path: &Path) -> PathBuf {
341 let mut normalized = PathBuf::new();
342 for component in path.components() {
343 match component {
344 Component::Prefix(_) | Component::RootDir | Component::Normal(_) => {
345 normalized.push(component);
347 }
348 Component::ParentDir => {
349 match normalized.components().next_back() {
350 None | Some(Component::ParentDir | Component::RootDir) => {
351 normalized.push(component);
353 }
354 Some(Component::Normal(_) | Component::Prefix(_) | Component::CurDir) => {
355 normalized.pop();
357 }
358 }
359 }
360 Component::CurDir => {
361 }
363 }
364 }
365 normalized
366}
367
368pub fn relative_to(
377 path: impl AsRef<Path>,
378 base: impl AsRef<Path>,
379) -> Result<PathBuf, std::io::Error> {
380 let path = normalize_path(path.as_ref());
382 let base = normalize_path(base.as_ref());
383
384 let (stripped, common_prefix) = base
386 .ancestors()
387 .find_map(|ancestor| {
388 dunce::simplified(&path)
390 .strip_prefix(dunce::simplified(ancestor))
391 .ok()
392 .map(|stripped| (stripped, ancestor))
393 })
394 .ok_or_else(|| {
395 std::io::Error::other(format!(
396 "Trivial strip failed: {} vs. {}",
397 path.simplified_display(),
398 base.simplified_display()
399 ))
400 })?;
401
402 let levels_up = base.components().count() - common_prefix.components().count();
404 let up = std::iter::repeat_n("..", levels_up).collect::<PathBuf>();
405
406 Ok(up.join(stripped))
407}
408
409pub fn find_git_repository_root(path: &Path) -> Option<&Path> {
414 path.ancestors()
416 .find(|ancestor| ancestor.join(".git").exists())
417}
418
419pub fn try_relative_to_if(
422 path: impl AsRef<Path>,
423 base: impl AsRef<Path>,
424 should_relativize: bool,
425) -> Result<PathBuf, std::io::Error> {
426 if should_relativize {
427 relative_to(&path, &base).or_else(|_| std::path::absolute(path.as_ref()))
428 } else {
429 std::path::absolute(path.as_ref())
430 }
431}
432
433pub fn verbatim_path(path: &Path) -> Cow<'_, Path> {
460 if !cfg!(windows) {
461 return Cow::Borrowed(path);
462 }
463
464 let resolved_path = if path.is_relative() {
468 Cow::Owned(CWD.join(path))
469 } else {
470 Cow::Borrowed(path)
471 };
472
473 if let Some(Component::Prefix(prefix)) = resolved_path.components().next() {
475 match prefix.kind() {
476 Prefix::UNC(..) | Prefix::Disk(_) => {},
477 Prefix::DeviceNS(_)
479 | Prefix::Verbatim(_)
481 | Prefix::VerbatimDisk(_)
482 | Prefix::VerbatimUNC(..) => return Cow::Borrowed(path)
483 }
484 }
485
486 let normalized_path = normalized(&resolved_path);
488
489 let mut components = normalized_path.components();
490 let Some(Component::Prefix(prefix)) = components.next() else {
491 return Cow::Borrowed(path);
492 };
493
494 match prefix.kind() {
495 Prefix::Disk(_) => {
497 let mut result = OsString::from(r"\\?\");
498 result.push(normalized_path.as_os_str()); Cow::Owned(PathBuf::from(result))
500 }
501 Prefix::UNC(server, share) => {
503 let mut result = OsString::from(r"\\?\UNC\");
504 result.push(server);
505 result.push(r"\");
506 result.push(share);
507 for component in components {
508 match component {
509 Component::RootDir => {} Component::Prefix(_) => {
511 debug_assert!(false, "prefix already consumed");
512 }
513 Component::CurDir | Component::ParentDir => {
514 debug_assert!(false, "path already normalized");
515 }
516 Component::Normal(_) => {
517 result.push(r"\");
518 result.push(component.as_os_str());
519 }
520 }
521 }
522 Cow::Owned(PathBuf::from(result))
523 }
524 Prefix::DeviceNS(_)
525 | Prefix::Verbatim(_)
526 | Prefix::VerbatimDisk(_)
527 | Prefix::VerbatimUNC(..) => {
528 debug_assert!(false, "skipped via fast path");
529 Cow::Borrowed(path)
530 }
531 }
532}
533
534#[derive(Debug, Clone, PartialEq, Eq)]
539pub struct PortablePath<'a>(&'a Path);
540
541#[derive(Debug, Clone, PartialEq, Eq)]
542pub struct PortablePathBuf(Box<Path>);
543
544#[cfg(feature = "schemars")]
545impl schemars::JsonSchema for PortablePathBuf {
546 fn schema_name() -> Cow<'static, str> {
547 Cow::Borrowed("PortablePathBuf")
548 }
549
550 fn json_schema(_gen: &mut schemars::generate::SchemaGenerator) -> schemars::Schema {
551 PathBuf::json_schema(_gen)
552 }
553}
554
555impl AsRef<Path> for PortablePath<'_> {
556 fn as_ref(&self) -> &Path {
557 self.0
558 }
559}
560
561impl<'a, T> From<&'a T> for PortablePath<'a>
562where
563 T: AsRef<Path> + ?Sized,
564{
565 fn from(path: &'a T) -> Self {
566 PortablePath(path.as_ref())
567 }
568}
569
570impl std::fmt::Display for PortablePath<'_> {
571 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
572 let path = self.0.to_slash_lossy();
573 if path.is_empty() {
574 write!(f, ".")
575 } else {
576 write!(f, "{path}")
577 }
578 }
579}
580
581impl std::fmt::Display for PortablePathBuf {
582 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
583 let path = self.0.to_slash_lossy();
584 if path.is_empty() {
585 write!(f, ".")
586 } else {
587 write!(f, "{path}")
588 }
589 }
590}
591
592impl From<&str> for PortablePathBuf {
593 fn from(path: &str) -> Self {
594 if path == "." {
595 Self(PathBuf::new().into_boxed_path())
596 } else {
597 Self(PathBuf::from(path).into_boxed_path())
598 }
599 }
600}
601
602impl From<PortablePathBuf> for Box<Path> {
603 fn from(portable: PortablePathBuf) -> Self {
604 portable.0
605 }
606}
607
608impl From<Box<Path>> for PortablePathBuf {
609 fn from(path: Box<Path>) -> Self {
610 Self(path)
611 }
612}
613
614impl<'a> From<&'a Path> for PortablePathBuf {
615 fn from(path: &'a Path) -> Self {
616 Box::<Path>::from(path).into()
617 }
618}
619
620#[cfg(feature = "serde")]
621impl serde::Serialize for PortablePathBuf {
622 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
623 where
624 S: serde::ser::Serializer,
625 {
626 self.to_string().serialize(serializer)
627 }
628}
629
630#[cfg(feature = "serde")]
631impl serde::Serialize for PortablePath<'_> {
632 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
633 where
634 S: serde::ser::Serializer,
635 {
636 self.to_string().serialize(serializer)
637 }
638}
639
640#[cfg(feature = "serde")]
641impl<'de> serde::de::Deserialize<'de> for PortablePathBuf {
642 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
643 where
644 D: serde::de::Deserializer<'de>,
645 {
646 let s = <Cow<'_, str>>::deserialize(deserializer)?;
647 if s == "." {
648 Ok(Self(PathBuf::new().into_boxed_path()))
649 } else {
650 Ok(Self(PathBuf::from(s.as_ref()).into_boxed_path()))
651 }
652 }
653}
654
655impl AsRef<Path> for PortablePathBuf {
656 fn as_ref(&self) -> &Path {
657 &self.0
658 }
659}
660
661#[cfg(test)]
662mod tests {
663 use super::*;
664
665 #[test]
666 fn test_find_git_repository_root() -> std::io::Result<()> {
667 let temp_dir = tempfile::tempdir()?;
668
669 let repository = temp_dir.path().join("repository");
670 let nested = repository.join("packages/project");
671 fs_err::create_dir_all(repository.join(".git"))?;
672 fs_err::create_dir_all(&nested)?;
673 assert_eq!(
674 find_git_repository_root(&nested),
675 Some(repository.as_path())
676 );
677
678 let worktree = temp_dir.path().join("worktree");
679 let nested = worktree.join("packages/project");
680 fs_err::create_dir_all(&nested)?;
681 fs_err::write(worktree.join(".git"), "gitdir: ../repository/.git")?;
682 assert_eq!(find_git_repository_root(&nested), Some(worktree.as_path()));
683
684 Ok(())
685 }
686
687 #[test]
688 fn test_normalize_url() {
689 if cfg!(windows) {
690 assert_eq!(
691 normalize_url_path("/C:/Users/ferris/wheel-0.42.0.tar.gz"),
692 "C:\\Users\\ferris\\wheel-0.42.0.tar.gz"
693 );
694 } else {
695 assert_eq!(
696 normalize_url_path("/C:/Users/ferris/wheel-0.42.0.tar.gz"),
697 "/C:/Users/ferris/wheel-0.42.0.tar.gz"
698 );
699 }
700
701 if cfg!(windows) {
702 assert_eq!(
703 normalize_url_path("./ferris/wheel-0.42.0.tar.gz"),
704 ".\\ferris\\wheel-0.42.0.tar.gz"
705 );
706 } else {
707 assert_eq!(
708 normalize_url_path("./ferris/wheel-0.42.0.tar.gz"),
709 "./ferris/wheel-0.42.0.tar.gz"
710 );
711 }
712
713 if cfg!(windows) {
714 assert_eq!(
715 normalize_url_path("./wheel%20cache/wheel-0.42.0.tar.gz"),
716 ".\\wheel cache\\wheel-0.42.0.tar.gz"
717 );
718 } else {
719 assert_eq!(
720 normalize_url_path("./wheel%20cache/wheel-0.42.0.tar.gz"),
721 "./wheel cache/wheel-0.42.0.tar.gz"
722 );
723 }
724 }
725
726 #[test]
727 fn test_normalize_path() {
728 let path = Path::new("/a/b/../c/./d");
729 let normalized = normalize_absolute_path(path).unwrap();
730 assert_eq!(normalized, Path::new("/a/c/d"));
731
732 let path = Path::new("/a/../c/./d");
733 let normalized = normalize_absolute_path(path).unwrap();
734 assert_eq!(normalized, Path::new("/c/d"));
735
736 let path = Path::new("/a/../../c/./d");
738 let err = normalize_absolute_path(path).unwrap_err();
739 assert_eq!(err.kind(), std::io::ErrorKind::InvalidInput);
740 }
741
742 #[test]
743 fn test_relative_to() {
744 assert_eq!(
745 relative_to(
746 Path::new("/home/ferris/carcinization/lib/python/site-packages/foo/__init__.py"),
747 Path::new("/home/ferris/carcinization/lib/python/site-packages"),
748 )
749 .unwrap(),
750 Path::new("foo/__init__.py")
751 );
752 assert_eq!(
753 relative_to(
754 Path::new("/home/ferris/carcinization/lib/marker.txt"),
755 Path::new("/home/ferris/carcinization/lib/python/site-packages"),
756 )
757 .unwrap(),
758 Path::new("../../marker.txt")
759 );
760 assert_eq!(
761 relative_to(
762 Path::new("/home/ferris/carcinization/bin/foo_launcher"),
763 Path::new("/home/ferris/carcinization/lib/python/site-packages"),
764 )
765 .unwrap(),
766 Path::new("../../../bin/foo_launcher")
767 );
768 }
769
770 #[test]
771 fn test_normalize_relative() {
772 let cases = [
773 (
774 "../../workspace-git-path-dep-test/packages/c/../../packages/d",
775 "../../workspace-git-path-dep-test/packages/d",
776 ),
777 (
778 "workspace-git-path-dep-test/packages/c/../../packages/d",
779 "workspace-git-path-dep-test/packages/d",
780 ),
781 ("./a/../../b", "../b"),
782 ("/usr/../../foo", "/../foo"),
783 ("foo/./bar", "foo/bar"),
785 ("/a/./b/./c", "/a/b/c"),
786 ("./foo/bar", "foo/bar"),
787 (".", ""),
788 ("./.", ""),
789 ("foo/.", "foo"),
790 ("foo//bar", "foo/bar"),
792 ("/a///b//c", "/a/b/c"),
793 ("foo/./../bar", "bar"),
795 ("foo/bar/./../baz", "foo/baz"),
796 ("foo/bar", "foo/bar"),
798 ("/a/b/c", "/a/b/c"),
799 ("", ""),
800 ];
801 for (input, expected) in cases {
802 assert_eq!(
803 normalize_path(Path::new(input)),
804 Path::new(expected),
805 "input: {input:?}"
806 );
807 }
808
809 for already_normalized in ["foo/bar", "/a/b/c", "foo", "/", ""] {
811 let path = Path::new(already_normalized);
812 assert!(
813 matches!(normalize_path(path), Cow::Borrowed(_)),
814 "expected borrowed for {already_normalized:?}"
815 );
816 }
817 }
818
819 #[test]
820 fn test_normalize_path_under() {
821 assert_eq!(
822 normalize_path_under("scripts/script", "scripts"),
823 Some(PathBuf::from("scripts/script"))
824 );
825 assert_eq!(
826 normalize_path_under("/scripts/script", "/scripts"),
827 Some(PathBuf::from("/scripts/script"))
828 );
829 assert_eq!(
830 normalize_path_under("scripts/nested/../script", "scripts"),
831 Some(PathBuf::from("scripts/script"))
832 );
833 assert_eq!(
834 normalize_path_under("/scripts/nested/../script", "/scripts"),
835 Some(PathBuf::from("/scripts/script"))
836 );
837 assert_eq!(normalize_path_under("scripts/.", "scripts"), None);
838 assert_eq!(normalize_path_under("/scripts/.", "/scripts"), None);
839 assert_eq!(normalize_path_under("scripts/../script", "scripts"), None);
840 assert_eq!(normalize_path_under("/scripts/../script", "/scripts"), None);
841 assert_eq!(normalize_path_under("scripts/script", "."), None);
842 assert_eq!(normalize_path_under("scripts/script", ""), None);
843 }
844
845 #[test]
846 fn test_normalize_trailing_path_separator() {
847 let cases = [
848 (
849 "/home/ferris/projects/python/",
850 "/home/ferris/projects/python",
851 ),
852 ("python/", "python"),
853 ("/", "/"),
854 ("foo/bar/", "foo/bar"),
855 ("foo//", "foo"),
856 ];
857 for (input, expected) in cases {
858 assert_eq!(normalize_path(Path::new(input)), Path::new(expected));
859 }
860 }
861
862 #[test]
863 #[cfg(windows)]
864 fn test_normalize_windows() {
865 let cases = [
866 (
867 r"C:\Users\Ferris\projects\python\",
868 r"C:\Users\Ferris\projects\python",
869 ),
870 (r"C:\foo\.\bar", r"C:\foo\bar"),
871 (r"C:\foo\\bar", r"C:\foo\bar"),
872 (r"C:\foo\bar\..\baz", r"C:\foo\baz"),
873 (r"foo\.\bar", r"foo\bar"),
874 (r"C:foo", r"C:foo"),
875 (r"C:\foo", r"C:\foo"),
876 (r"C:\\foo", r"C:\foo"),
877 (r"\\?\C:foo", r"\\?\C:foo"),
878 (r"\\?\C:\foo", r"\\?\C:\foo"),
879 (r"\\?\C:\\foo", r"\\?\C:\foo"),
880 (r"\\server\share\foo", r"\\server\share\foo"),
881 ];
882 for (input, expected) in cases {
883 assert_eq!(normalize_path(Path::new(input)), Path::new(expected));
884 }
885 }
886
887 #[cfg(windows)]
888 #[test]
889 fn test_verbatim_path() {
890 let relative_path = format!(r"\\?\{}\path\to\logging.", CWD.simplified_display());
891 let relative_root = format!(
892 r"\\?\{}\path\to\logging.",
893 CWD.components()
894 .next()
895 .expect("expected a drive letter prefix")
896 .simplified_display()
897 );
898 let cases = [
899 (r"C:\path\to\logging.", r"\\?\C:\path\to\logging."),
901 (r"C:\path\to\.\logging.", r"\\?\C:\path\to\logging."),
902 (r"C:\path\to\..\to\logging.", r"\\?\C:\path\to\logging."),
903 (r"C:/path/to/../to/./logging.", r"\\?\C:\path\to\logging."),
904 (r"C:path\to\..\to\logging.", r"\\?\C:path\to\logging."), (r".\path\to\.\logging.", relative_path.as_str()),
906 (r"path\to\..\to\logging.", relative_path.as_str()),
907 (r"./path/to/logging.", relative_path.as_str()),
908 (r"\path\to\logging.", relative_root.as_str()),
909 (
911 r"\\127.0.0.1\c$\path\to\logging.",
912 r"\\?\UNC\127.0.0.1\c$\path\to\logging.",
913 ),
914 (
915 r"\\127.0.0.1\c$\path\to\.\logging.",
916 r"\\?\UNC\127.0.0.1\c$\path\to\logging.",
917 ),
918 (
919 r"\\127.0.0.1\c$\path\to\..\to\logging.",
920 r"\\?\UNC\127.0.0.1\c$\path\to\logging.",
921 ),
922 (
923 r"//127.0.0.1/c$/path/to/../to/./logging.",
924 r"\\?\UNC\127.0.0.1\c$\path\to\logging.",
925 ),
926 (r"\\?\C:\path\to\logging.", r"\\?\C:\path\to\logging."),
928 (
930 r"\\?\UNC\127.0.0.1\c$\path\to\logging.",
931 r"\\?\UNC\127.0.0.1\c$\path\to\logging.",
932 ),
933 (r"\\.\PhysicalDrive0", r"\\.\PhysicalDrive0"),
935 (r"\\.\NUL", r"\\.\NUL"),
936 ];
937
938 for (input, expected) in cases {
939 assert_eq!(verbatim_path(Path::new(input)), Path::new(expected));
940 }
941 }
942
943 #[test]
944 #[cfg(unix)]
945 fn test_path_escape_for_python() {
946 use std::ffi::OsStr;
947 use std::os::unix::ffi::OsStrExt;
948
949 for (range, expected) in [
952 (
953 1..=0x5e,
954 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")"##,
955 ),
956 (
957 0x5f..=0xa3,
958 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")"#,
959 ),
960 (
961 0xa4..=0xd1,
962 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")"#,
963 ),
964 (
965 0xd2..=u8::MAX,
966 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")"#,
967 ),
968 ] {
969 let bytes = b"/foo/"
970 .iter()
971 .copied()
972 .chain(range)
973 .chain(b"/bar".iter().copied())
974 .collect::<Box<[_]>>();
975 let path = Path::new(OsStr::from_bytes(&bytes));
976 assert_eq!(path.escape_for_python(), expected);
977 }
978 }
979
980 #[cfg(windows)]
981 #[test]
982 fn test_path_escape_for_python() {
983 use std::os::windows::ffi::OsStringExt;
984
985 let path_osstr = OsString::from_wide(&[
988 0x5c, 0x5c, 0x3f, 0x5c, 0x43, 0x3a, 0x5c, 0x63, 0x61, 0x66, 0x00e9, 0x5c, 0xd83d,
989 0xde00, 0x5c, 0xd800, 0x78, 0xdc00, 0x22, 0x09, 0x0a, 0x0d,
990 ]);
991 let path = Path::new(&path_osstr);
992 assert_eq!(
994 path.escape_for_python(),
995 r#""\\\\?\\C:\\café\\😀\\\ud800x\udc00\"\t\n\r""#
996 );
997 }
998}