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 std::assert_matches;
664
665 use super::*;
666
667 #[test]
668 fn test_find_git_repository_root() -> std::io::Result<()> {
669 let temp_dir = tempfile::tempdir()?;
670
671 let repository = temp_dir.path().join("repository");
672 let nested = repository.join("packages/project");
673 fs_err::create_dir_all(repository.join(".git"))?;
674 fs_err::create_dir_all(&nested)?;
675 assert_eq!(
676 find_git_repository_root(&nested),
677 Some(repository.as_path())
678 );
679
680 let worktree = temp_dir.path().join("worktree");
681 let nested = worktree.join("packages/project");
682 fs_err::create_dir_all(&nested)?;
683 fs_err::write(worktree.join(".git"), "gitdir: ../repository/.git")?;
684 assert_eq!(find_git_repository_root(&nested), Some(worktree.as_path()));
685
686 Ok(())
687 }
688
689 #[test]
690 fn test_normalize_url() {
691 if cfg!(windows) {
692 assert_eq!(
693 normalize_url_path("/C:/Users/ferris/wheel-0.42.0.tar.gz"),
694 "C:\\Users\\ferris\\wheel-0.42.0.tar.gz"
695 );
696 } else {
697 assert_eq!(
698 normalize_url_path("/C:/Users/ferris/wheel-0.42.0.tar.gz"),
699 "/C:/Users/ferris/wheel-0.42.0.tar.gz"
700 );
701 }
702
703 if cfg!(windows) {
704 assert_eq!(
705 normalize_url_path("./ferris/wheel-0.42.0.tar.gz"),
706 ".\\ferris\\wheel-0.42.0.tar.gz"
707 );
708 } else {
709 assert_eq!(
710 normalize_url_path("./ferris/wheel-0.42.0.tar.gz"),
711 "./ferris/wheel-0.42.0.tar.gz"
712 );
713 }
714
715 if cfg!(windows) {
716 assert_eq!(
717 normalize_url_path("./wheel%20cache/wheel-0.42.0.tar.gz"),
718 ".\\wheel cache\\wheel-0.42.0.tar.gz"
719 );
720 } else {
721 assert_eq!(
722 normalize_url_path("./wheel%20cache/wheel-0.42.0.tar.gz"),
723 "./wheel cache/wheel-0.42.0.tar.gz"
724 );
725 }
726 }
727
728 #[test]
729 fn test_normalize_path() {
730 let path = Path::new("/a/b/../c/./d");
731 let normalized = normalize_absolute_path(path).unwrap();
732 assert_eq!(normalized, Path::new("/a/c/d"));
733
734 let path = Path::new("/a/../c/./d");
735 let normalized = normalize_absolute_path(path).unwrap();
736 assert_eq!(normalized, Path::new("/c/d"));
737
738 let path = Path::new("/a/../../c/./d");
740 let err = normalize_absolute_path(path).unwrap_err();
741 assert_eq!(err.kind(), std::io::ErrorKind::InvalidInput);
742 }
743
744 #[test]
745 fn test_relative_to() {
746 assert_eq!(
747 relative_to(
748 Path::new("/home/ferris/carcinization/lib/python/site-packages/foo/__init__.py"),
749 Path::new("/home/ferris/carcinization/lib/python/site-packages"),
750 )
751 .unwrap(),
752 Path::new("foo/__init__.py")
753 );
754 assert_eq!(
755 relative_to(
756 Path::new("/home/ferris/carcinization/lib/marker.txt"),
757 Path::new("/home/ferris/carcinization/lib/python/site-packages"),
758 )
759 .unwrap(),
760 Path::new("../../marker.txt")
761 );
762 assert_eq!(
763 relative_to(
764 Path::new("/home/ferris/carcinization/bin/foo_launcher"),
765 Path::new("/home/ferris/carcinization/lib/python/site-packages"),
766 )
767 .unwrap(),
768 Path::new("../../../bin/foo_launcher")
769 );
770 }
771
772 #[test]
773 fn test_normalize_relative() {
774 let cases = [
775 (
776 "../../workspace-git-path-dep-test/packages/c/../../packages/d",
777 "../../workspace-git-path-dep-test/packages/d",
778 ),
779 (
780 "workspace-git-path-dep-test/packages/c/../../packages/d",
781 "workspace-git-path-dep-test/packages/d",
782 ),
783 ("./a/../../b", "../b"),
784 ("/usr/../../foo", "/../foo"),
785 ("foo/./bar", "foo/bar"),
787 ("/a/./b/./c", "/a/b/c"),
788 ("./foo/bar", "foo/bar"),
789 (".", ""),
790 ("./.", ""),
791 ("foo/.", "foo"),
792 ("foo//bar", "foo/bar"),
794 ("/a///b//c", "/a/b/c"),
795 ("foo/./../bar", "bar"),
797 ("foo/bar/./../baz", "foo/baz"),
798 ("foo/bar", "foo/bar"),
800 ("/a/b/c", "/a/b/c"),
801 ("", ""),
802 ];
803 for (input, expected) in cases {
804 assert_eq!(
805 normalize_path(Path::new(input)),
806 Path::new(expected),
807 "input: {input:?}"
808 );
809 }
810
811 for already_normalized in ["foo/bar", "/a/b/c", "foo", "/", ""] {
813 let path = Path::new(already_normalized);
814 assert_matches!(
815 normalize_path(path),
816 Cow::Borrowed(_),
817 "expected borrowed for {already_normalized:?}"
818 );
819 }
820 }
821
822 #[test]
823 fn test_normalize_path_under() {
824 assert_eq!(
825 normalize_path_under("scripts/script", "scripts"),
826 Some(PathBuf::from("scripts/script"))
827 );
828 assert_eq!(
829 normalize_path_under("/scripts/script", "/scripts"),
830 Some(PathBuf::from("/scripts/script"))
831 );
832 assert_eq!(
833 normalize_path_under("scripts/nested/../script", "scripts"),
834 Some(PathBuf::from("scripts/script"))
835 );
836 assert_eq!(
837 normalize_path_under("/scripts/nested/../script", "/scripts"),
838 Some(PathBuf::from("/scripts/script"))
839 );
840 assert_eq!(normalize_path_under("scripts/.", "scripts"), None);
841 assert_eq!(normalize_path_under("/scripts/.", "/scripts"), None);
842 assert_eq!(normalize_path_under("scripts/../script", "scripts"), None);
843 assert_eq!(normalize_path_under("/scripts/../script", "/scripts"), None);
844 assert_eq!(normalize_path_under("scripts/script", "."), None);
845 assert_eq!(normalize_path_under("scripts/script", ""), None);
846 }
847
848 #[test]
849 fn test_normalize_trailing_path_separator() {
850 let cases = [
851 (
852 "/home/ferris/projects/python/",
853 "/home/ferris/projects/python",
854 ),
855 ("python/", "python"),
856 ("/", "/"),
857 ("foo/bar/", "foo/bar"),
858 ("foo//", "foo"),
859 ];
860 for (input, expected) in cases {
861 assert_eq!(normalize_path(Path::new(input)), Path::new(expected));
862 }
863 }
864
865 #[test]
866 #[cfg(windows)]
867 fn test_normalize_windows() {
868 let cases = [
869 (
870 r"C:\Users\Ferris\projects\python\",
871 r"C:\Users\Ferris\projects\python",
872 ),
873 (r"C:\foo\.\bar", r"C:\foo\bar"),
874 (r"C:\foo\\bar", r"C:\foo\bar"),
875 (r"C:\foo\bar\..\baz", r"C:\foo\baz"),
876 (r"foo\.\bar", r"foo\bar"),
877 (r"C:foo", r"C:foo"),
878 (r"C:\foo", r"C:\foo"),
879 (r"C:\\foo", r"C:\foo"),
880 (r"\\?\C:foo", r"\\?\C:foo"),
881 (r"\\?\C:\foo", r"\\?\C:\foo"),
882 (r"\\?\C:\\foo", r"\\?\C:\foo"),
883 (r"\\server\share\foo", r"\\server\share\foo"),
884 ];
885 for (input, expected) in cases {
886 assert_eq!(normalize_path(Path::new(input)), Path::new(expected));
887 }
888 }
889
890 #[cfg(windows)]
891 #[test]
892 fn test_verbatim_path() {
893 let relative_path = format!(r"\\?\{}\path\to\logging.", CWD.simplified_display());
894 let relative_root = format!(
895 r"\\?\{}\path\to\logging.",
896 CWD.components()
897 .next()
898 .expect("expected a drive letter prefix")
899 .simplified_display()
900 );
901 let cases = [
902 (r"C:\path\to\logging.", r"\\?\C:\path\to\logging."),
904 (r"C:\path\to\.\logging.", r"\\?\C:\path\to\logging."),
905 (r"C:\path\to\..\to\logging.", r"\\?\C:\path\to\logging."),
906 (r"C:/path/to/../to/./logging.", r"\\?\C:\path\to\logging."),
907 (r"C:path\to\..\to\logging.", r"\\?\C:path\to\logging."), (r".\path\to\.\logging.", relative_path.as_str()),
909 (r"path\to\..\to\logging.", relative_path.as_str()),
910 (r"./path/to/logging.", relative_path.as_str()),
911 (r"\path\to\logging.", relative_root.as_str()),
912 (
914 r"\\127.0.0.1\c$\path\to\logging.",
915 r"\\?\UNC\127.0.0.1\c$\path\to\logging.",
916 ),
917 (
918 r"\\127.0.0.1\c$\path\to\.\logging.",
919 r"\\?\UNC\127.0.0.1\c$\path\to\logging.",
920 ),
921 (
922 r"\\127.0.0.1\c$\path\to\..\to\logging.",
923 r"\\?\UNC\127.0.0.1\c$\path\to\logging.",
924 ),
925 (
926 r"//127.0.0.1/c$/path/to/../to/./logging.",
927 r"\\?\UNC\127.0.0.1\c$\path\to\logging.",
928 ),
929 (r"\\?\C:\path\to\logging.", r"\\?\C:\path\to\logging."),
931 (
933 r"\\?\UNC\127.0.0.1\c$\path\to\logging.",
934 r"\\?\UNC\127.0.0.1\c$\path\to\logging.",
935 ),
936 (r"\\.\PhysicalDrive0", r"\\.\PhysicalDrive0"),
938 (r"\\.\NUL", r"\\.\NUL"),
939 ];
940
941 for (input, expected) in cases {
942 assert_eq!(verbatim_path(Path::new(input)), Path::new(expected));
943 }
944 }
945
946 #[test]
947 #[cfg(unix)]
948 fn test_path_escape_for_python() {
949 use std::ffi::OsStr;
950 use std::os::unix::ffi::OsStrExt;
951
952 for (range, expected) in [
955 (
956 1..=0x5e,
957 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")"##,
958 ),
959 (
960 0x5f..=0xa3,
961 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")"#,
962 ),
963 (
964 0xa4..=0xd1,
965 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")"#,
966 ),
967 (
968 0xd2..=u8::MAX,
969 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")"#,
970 ),
971 ] {
972 let bytes = b"/foo/"
973 .iter()
974 .copied()
975 .chain(range)
976 .chain(b"/bar".iter().copied())
977 .collect::<Box<[_]>>();
978 let path = Path::new(OsStr::from_bytes(&bytes));
979 assert_eq!(path.escape_for_python(), expected);
980 }
981 }
982
983 #[cfg(windows)]
984 #[test]
985 fn test_path_escape_for_python() {
986 use std::os::windows::ffi::OsStringExt;
987
988 let path_osstr = OsString::from_wide(&[
991 0x5c, 0x5c, 0x3f, 0x5c, 0x43, 0x3a, 0x5c, 0x63, 0x61, 0x66, 0x00e9, 0x5c, 0xd83d,
992 0xde00, 0x5c, 0xd800, 0x78, 0xdc00, 0x22, 0x09, 0x0a, 0x0d,
993 ]);
994 let path = Path::new(&path_osstr);
995 assert_eq!(
997 path.escape_for_python(),
998 r#""\\\\?\\C:\\café\\😀\\\ud800x\udc00\"\t\n\r""#
999 );
1000 }
1001}