1#[cfg(windows)]
10use anyhow::Context;
11#[cfg(all(unix, not(target_family = "wasm")))]
12use std::os::unix::prelude::OsStrExt;
13use std::{
14 borrow::Cow,
15 ffi::OsStr,
16 path::{Path, PathBuf},
17};
18#[cfg(windows)]
19use tendril::fmt::{Format, WTF8};
20
21use crate::path_component::{Component, Components, Prefix, PrefixExt, iter_after};
22use crate::rel_path::RelPath;
23
24pub mod abs_path;
25mod path_component;
26pub mod rel_path;
27mod windows_prefix;
28
29pub fn try_from_bytes<'a, T>(bytes: &'a [u8]) -> anyhow::Result<T>
30where
31 T: From<&'a Path>,
32{
33 #[cfg(target_family = "wasm")]
34 {
35 std::str::from_utf8(bytes)
36 .map(Path::new)
37 .map(Into::into)
38 .map_err(Into::into)
39 }
40 #[cfg(all(unix, not(target_family = "wasm")))]
41 {
42 Ok(T::from(Path::new(OsStr::from_bytes(bytes))))
43 }
44 #[cfg(windows)]
45 {
46 WTF8::validate(bytes)
47 .then(|| {
48 T::from(Path::new(unsafe {
50 OsStr::from_encoded_bytes_unchecked(bytes)
51 }))
52 })
53 .with_context(|| format!("Invalid WTF-8 sequence: {bytes:?}"))
54 }
55}
56
57pub trait PathExt {
58 fn to_rel_path_buf(&self) -> anyhow::Result<rel_path::RelPathBuf>;
59}
60
61impl<T: AsRef<Path> + ?Sized> PathExt for T {
62 fn to_rel_path_buf(&self) -> anyhow::Result<rel_path::RelPathBuf> {
63 Ok(RelPath::new(self.as_ref(), PathStyle::local())?.into_owned())
64 }
65}
66
67#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
68pub enum PathStyle {
69 Unix,
70 Windows,
71}
72
73impl PathStyle {
74 #[cfg(target_os = "windows")]
75 pub const fn local() -> Self {
76 PathStyle::Windows
77 }
78
79 #[cfg(not(target_os = "windows"))]
80 pub const fn local() -> Self {
81 PathStyle::Unix
82 }
83
84 #[inline]
85 pub fn primary_separator(&self) -> &'static str {
86 match self {
87 PathStyle::Unix => "/",
88 PathStyle::Windows => "\\",
89 }
90 }
91
92 pub fn separators(&self) -> &'static [&'static str] {
93 match self {
94 PathStyle::Unix => &["/"],
95 PathStyle::Windows => &["\\", "/"],
96 }
97 }
98
99 pub fn separators_ch(&self) -> &'static [char] {
100 match self {
101 PathStyle::Unix => &['/'],
102 PathStyle::Windows => &['\\', '/'],
103 }
104 }
105
106 pub fn is_absolute(&self, path_like: &str) -> bool {
107 path_like.starts_with('/')
108 || *self == PathStyle::Windows
109 && (path_like.starts_with('\\')
110 || path_like
111 .chars()
112 .next()
113 .is_some_and(|c| c.is_ascii_alphabetic())
114 && path_like[1..]
115 .strip_prefix(':')
116 .is_some_and(|path| path.starts_with('/') || path.starts_with('\\')))
117 }
118
119 pub fn is_windows(&self) -> bool {
120 *self == PathStyle::Windows
121 }
122
123 pub fn is_posix(&self) -> bool {
124 *self == PathStyle::Unix
125 }
126
127 pub fn join(self, left: impl AsRef<Path>, right: impl AsRef<Path>) -> Option<String> {
128 let right = right.as_ref().to_str()?;
129 if is_absolute(right, self) {
130 return None;
131 }
132 let left = left.as_ref().to_str()?;
133 if left.is_empty() {
134 Some(right.into())
135 } else {
136 Some(format!(
137 "{left}{}{right}",
138 if left.ends_with(self.primary_separator()) {
139 ""
140 } else {
141 self.primary_separator()
142 }
143 ))
144 }
145 }
146
147 pub fn join_path(
148 self,
149 left: impl AsRef<Path>,
150 right: impl AsRef<Path>,
151 ) -> anyhow::Result<PathBuf> {
152 let left = left
153 .as_ref()
154 .to_str()
155 .ok_or_else(|| anyhow::anyhow!("Path contains invalid UTF-8"))?;
156 let right = right.as_ref();
157 let right_string = right
158 .to_str()
159 .ok_or_else(|| anyhow::anyhow!("Path contains invalid UTF-8"))?;
160 let joined = self
161 .join(left, right_string)
162 .ok_or_else(|| anyhow::anyhow!("Path must be relative: {right:?}"))?;
163 Ok(PathBuf::from(self.normalize(&joined)))
164 }
165
166 pub fn join_path_preserving_components(
167 self,
168 left: impl AsRef<Path>,
169 right: impl AsRef<Path>,
170 ) -> anyhow::Result<PathBuf> {
171 let left = left
172 .as_ref()
173 .to_str()
174 .ok_or_else(|| anyhow::anyhow!("Path contains invalid UTF-8"))?;
175 let right = right.as_ref();
176 let right_string = right
177 .to_str()
178 .ok_or_else(|| anyhow::anyhow!("Path contains invalid UTF-8"))?;
179 let joined = self
180 .join(left, right_string)
181 .ok_or_else(|| anyhow::anyhow!("Path must be relative: {right:?}"))?;
182 Ok(PathBuf::from(&joined))
183 }
184
185 pub fn normalize(self, path_like: &str) -> String {
186 match self {
187 PathStyle::Windows => {
188 let drive_and_remainder = path_like.split_once(':').filter(|(drive, _)| {
189 let mut characters = drive.chars();
190 characters
191 .next()
192 .is_some_and(|character| character.is_ascii_alphabetic())
193 && characters.next().is_none()
194 });
195 let unc_remainder = path_like
196 .strip_prefix("\\\\")
197 .or_else(|| path_like.strip_prefix("//"));
198
199 let (prefix, remainder) = if let Some((drive, remainder)) = drive_and_remainder {
200 if let Some(remainder) = remainder
201 .strip_prefix('\\')
202 .or_else(|| remainder.strip_prefix('/'))
203 {
204 (format!("{drive}:\\"), remainder)
205 } else {
206 (format!("{drive}:"), remainder)
207 }
208 } else if let Some(remainder) = unc_remainder {
209 let (server, remainder) = match remainder.split_once(['\\', '/']) {
210 Some(parts) => parts,
211 None => return path_like.to_string(),
212 };
213 let (share, remainder) = match remainder.split_once(['\\', '/']) {
214 Some(parts) => parts,
215 None => return format!("\\\\{server}\\{remainder}"),
216 };
217 (format!("\\\\{server}\\{share}\\"), remainder)
218 } else if let Some(remainder) = path_like
219 .strip_prefix('\\')
220 .or_else(|| path_like.strip_prefix('/'))
221 {
222 ("\\".to_string(), remainder)
223 } else {
224 (String::new(), path_like)
225 };
226
227 let mut components: Vec<&str> = Vec::new();
228 for component in remainder.split(['\\', '/']) {
229 match component {
230 "" | "." => {}
231 ".." => {
232 if components.last().is_some_and(|c| *c != "..") {
233 components.pop();
234 } else if prefix.is_empty() {
235 components.push(component);
236 }
237 }
238 component => components.push(component),
239 }
240 }
241
242 let normalized = components.join("\\");
243 if prefix.is_empty() {
244 normalized
245 } else {
246 format!("{prefix}{normalized}")
247 }
248 }
249 PathStyle::Unix => {
250 let is_absolute = path_like.starts_with('/');
251 let remainder = if is_absolute {
252 path_like.trim_start_matches('/')
253 } else {
254 path_like
255 };
256
257 let mut components = Vec::new();
258 for component in remainder.split(self.separators_ch()) {
259 match component {
260 "" | "." => {}
261 ".." => {
262 if components
263 .last()
264 .is_some_and(|component| *component != "..")
265 {
266 components.pop();
267 } else if !is_absolute {
268 components.push(component);
269 }
270 }
271 component => components.push(component),
272 }
273 }
274
275 let normalized = components.join(self.primary_separator());
276 if is_absolute && normalized.is_empty() {
277 "/".to_string()
278 } else if is_absolute {
279 format!("/{normalized}")
280 } else {
281 normalized
282 }
283 }
284 }
285 }
286
287 pub fn split(self, path_like: &str) -> (Option<&str>, &str) {
288 let Some(pos) = path_like.rfind(self.primary_separator()) else {
289 return (None, path_like);
290 };
291 let filename_start = pos + self.primary_separator().len();
292 (
293 Some(&path_like[..filename_start]),
294 &path_like[filename_start..],
295 )
296 }
297
298 pub fn file_name<'a>(&self, path: &'a Path) -> Option<&'a OsStr> {
299 self.components(path).next_back().and_then(|p| match p {
300 Component::Normal(p) => Some(p),
301 _ => None,
302 })
303 }
304
305 pub fn parent<'a>(&self, path: &'a Path) -> Option<&'a Path> {
306 let mut comps = self.components(path);
307 let comp = comps.next_back();
308 comp.and_then(|p| match p {
309 Component::Normal(_) | Component::CurDir | Component::ParentDir => {
310 Some(comps.as_path())
311 }
312 _ => None,
313 })
314 }
315
316 pub fn starts_with<P: AsRef<Path>>(&self, path: P, prefix: P) -> bool {
317 iter_after(
318 self.components(path.as_ref()),
319 self.components(prefix.as_ref()),
320 )
321 .is_some()
322 }
323
324 pub fn strip_prefix<'a>(
325 &self,
326 child: &'a Path,
327 parent: &'a Path,
328 ) -> Option<std::borrow::Cow<'a, RelPath>> {
329 let parent = parent.to_str()?;
330 if parent.is_empty() {
331 return RelPath::new(child, *self).ok();
332 }
333 let parent = self
334 .separators()
335 .iter()
336 .find_map(|sep| parent.strip_suffix(sep))
337 .unwrap_or(parent);
338 let child = child.to_str()?;
339
340 let stripped = if self.is_windows()
342 && child.as_bytes().get(1) == Some(&b':')
343 && parent.as_bytes().get(1) == Some(&b':')
344 && child.as_bytes()[0].eq_ignore_ascii_case(&parent.as_bytes()[0])
345 {
346 child[2..].strip_prefix(&parent[2..])?
347 } else {
348 child.strip_prefix(parent)?
349 };
350 if let Some(relative) = self
351 .separators()
352 .iter()
353 .find_map(|sep| stripped.strip_prefix(sep))
354 {
355 RelPath::new(relative.as_ref(), *self).ok()
356 } else if stripped.is_empty() {
357 Some(Cow::Borrowed(RelPath::empty()))
358 } else {
359 None
360 }
361 }
362}
363
364impl PathStyle {
367 fn components<'a>(&self, path: &'a Path) -> Components<'a> {
368 Components::new(*self, path)
369 }
370
371 fn parse_prefix<'a>(&self, path: &'a OsStr) -> Option<Prefix<'a>> {
372 match self {
373 PathStyle::Unix => None,
374 PathStyle::Windows => windows_prefix::parse_prefix(path),
375 }
376 }
377
378 fn has_prefixes(&self) -> bool {
379 match self {
380 PathStyle::Unix => false,
381 PathStyle::Windows => true,
382 }
383 }
384
385 fn is_sep_byte(&self, b: u8) -> bool {
386 match self {
387 Self::Unix => b == b'/',
388 Self::Windows => b == b'\\' || b == b'/',
389 }
390 }
391
392 fn is_verbatim_sep(&self, b: u8) -> bool {
393 match self {
394 Self::Unix => self.is_sep_byte(b),
395 Self::Windows => b == b'\\',
396 }
397 }
398
399 fn has_physical_root(&self, s: &[u8], prefix: Option<Prefix<'_>>) -> bool {
400 let path = if let Some(p) = prefix {
401 &s[p.len()..]
402 } else {
403 s
404 };
405 !path.is_empty() && self.is_sep_byte(path[0])
406 }
407}
408
409fn is_absolute(path_like: &str, path_style: PathStyle) -> bool {
410 path_like.starts_with('/')
411 || path_style == PathStyle::Windows
412 && (path_like.starts_with('\\')
413 || path_like
414 .chars()
415 .next()
416 .is_some_and(|c| c.is_ascii_alphabetic())
417 && path_like[1..]
418 .strip_prefix(':')
419 .is_some_and(|path| path.starts_with('/') || path.starts_with('\\')))
420}
421
422pub fn normalize_path(path: &Path) -> PathBuf {
425 use std::path::Component;
426 let mut components = path.components().peekable();
427 let mut ret = if let Some(c @ Component::Prefix(..)) = components.peek().cloned() {
428 components.next();
429 PathBuf::from(c.as_os_str())
430 } else {
431 PathBuf::new()
432 };
433
434 for component in components {
435 match component {
436 Component::Prefix(..) => unreachable!(),
437 Component::RootDir => {
438 ret.push(component.as_os_str());
439 }
440 Component::CurDir => {}
441 Component::ParentDir => {
442 ret.pop();
443 }
444 Component::Normal(c) => {
445 ret.push(c);
446 }
447 }
448 }
449 ret
450}
451
452#[cfg(test)]
453mod tests {
454 use super::*;
455 use crate::rel_path::rel_path;
456
457 #[test]
458 fn test_join_path_uses_path_style_separator() {
459 let posix_path = PathStyle::Unix
460 .join_path(Path::new("/home/user/dev"), "worktrees")
461 .unwrap();
462 let windows_path = PathStyle::Windows
463 .join_path(Path::new("C:\\Users\\user\\dev"), "worktrees")
464 .unwrap();
465
466 assert_eq!(posix_path, PathBuf::from("/home/user/dev/worktrees"));
467 assert_eq!(
468 windows_path.to_string_lossy(),
469 "C:\\Users\\user\\dev\\worktrees"
470 );
471 }
472
473 #[test]
474 fn test_join_path_preserving_components() {
475 let posix_path = PathStyle::Unix
476 .join_path_preserving_components(Path::new("/home/user/symlink"), "../worktrees")
477 .unwrap();
478 let windows_path = PathStyle::Windows
479 .join_path_preserving_components(Path::new(r"C:\Users\user\symlink"), r"..\worktrees")
480 .unwrap();
481
482 assert_eq!(posix_path, PathBuf::from("/home/user/symlink/../worktrees"));
483 assert_eq!(
484 windows_path.to_string_lossy(),
485 r"C:\Users\user\symlink\..\worktrees"
486 );
487 }
488
489 #[test]
490 fn test_normalize_uses_path_style_separator() {
491 assert_eq!(
492 PathStyle::Unix.normalize("/home/user/dev/../worktrees/./zed"),
493 "/home/user/worktrees/zed"
494 );
495 assert_eq!(
496 PathStyle::Windows.normalize("C:\\Users\\user\\dev\\worktrees"),
497 "C:\\Users\\user\\dev\\worktrees"
498 );
499 }
500
501 #[test]
502 fn test_normalize_windows_path_regardless_of_host_platform() {
503 assert_eq!(
504 PathStyle::Windows.normalize(r"C:\Users\user\dev\..\worktrees"),
505 r"C:\Users\user\worktrees"
506 );
507 assert_eq!(
508 PathStyle::Windows.normalize(r"C:\Users\.\worktrees"),
509 r"C:\Users\worktrees"
510 );
511 assert_eq!(
512 PathStyle::Windows.normalize(r"C:\Users\user\dev\sub\..\..\worktrees"),
513 r"C:\Users\user\worktrees"
514 );
515 assert_eq!(
516 PathStyle::Windows.normalize("C:/Users/user/dev/../worktrees"),
517 r"C:\Users\user\worktrees"
518 );
519 assert_eq!(
520 PathStyle::Windows.normalize(r"C:/Users\user/dev\..\worktrees"),
521 r"C:\Users\user\worktrees"
522 );
523 assert_eq!(
524 PathStyle::Windows.normalize(r"C:\Users/user\.\worktrees"),
525 r"C:\Users\user\worktrees"
526 );
527 assert_eq!(
528 PathStyle::Windows.normalize(r"\\server\share\dev\..\worktrees"),
529 r"\\server\share\worktrees"
530 );
531 assert_eq!(
532 PathStyle::Windows.normalize(r"//server\share/dev\..\worktrees"),
533 r"\\server\share\worktrees"
534 );
535 assert_eq!(
536 PathStyle::Windows.normalize(r"\dev\..\worktrees"),
537 r"\worktrees"
538 );
539 assert_eq!(
540 PathStyle::Windows.normalize(r"dev\..\worktrees"),
541 r"worktrees"
542 );
543 assert_eq!(
544 PathStyle::Windows.normalize(r"C:\..\worktrees"),
545 r"C:\worktrees"
546 );
547 }
548
549 #[test]
550 fn test_strip_prefix() {
551 let expected = [
552 (
553 PathStyle::Unix,
554 "/a/b/c",
555 "/a/b",
556 Some(rel_path("c").into_arc()),
557 ),
558 (
559 PathStyle::Unix,
560 "/a/b/c",
561 "/a/b/",
562 Some(rel_path("c").into_arc()),
563 ),
564 (
565 PathStyle::Unix,
566 "/a/b/c",
567 "/",
568 Some(rel_path("a/b/c").into_arc()),
569 ),
570 (PathStyle::Unix, "/a/b/c", "", None),
571 (PathStyle::Unix, "/a/b//c", "/a/b/", None),
572 (PathStyle::Unix, "/a/bc", "/a/b", None),
573 (
574 PathStyle::Unix,
575 "/a/b/c",
576 "/a/b/c",
577 Some(rel_path("").into_arc()),
578 ),
579 (
580 PathStyle::Windows,
581 "C:\\a\\b\\c",
582 "C:\\a\\b",
583 Some(rel_path("c").into_arc()),
584 ),
585 (
586 PathStyle::Windows,
587 "C:\\a\\b\\c",
588 "C:\\a\\b\\",
589 Some(rel_path("c").into_arc()),
590 ),
591 (
592 PathStyle::Windows,
593 "C:\\a\\b\\c",
594 "C:\\",
595 Some(rel_path("a/b/c").into_arc()),
596 ),
597 (PathStyle::Windows, "C:\\a\\b\\c", "", None),
598 (PathStyle::Windows, "C:\\a\\b\\\\c", "C:\\a\\b\\", None),
599 (PathStyle::Windows, "C:\\a\\bc", "C:\\a\\b", None),
600 (
601 PathStyle::Windows,
602 "C:\\a\\b/c",
603 "C:\\a\\b",
604 Some(rel_path("c").into_arc()),
605 ),
606 (
607 PathStyle::Windows,
608 "C:\\a\\b/c",
609 "C:\\a\\b\\",
610 Some(rel_path("c").into_arc()),
611 ),
612 (
613 PathStyle::Windows,
614 "C:\\a\\b/c",
615 "C:\\a\\b/",
616 Some(rel_path("c").into_arc()),
617 ),
618 ];
619 let actual = expected.clone().map(|(style, child, parent, _)| {
620 (
621 style,
622 child,
623 parent,
624 style
625 .strip_prefix(child.as_ref(), parent.as_ref())
626 .map(|rel_path| rel_path.into_arc()),
627 )
628 });
629 pretty_assertions::assert_eq!(actual, expected);
630 }
631
632 #[test]
633 fn test_unix_path_style_starts_with() {
634 let unix_paths_and_prefixes = [
635 ("", "", true),
636 ("foo", "", true),
637 ("", "foo", false),
638 ("foo", "foo", true),
639 ("foo/", "foo", true),
640 ("foo//", "foo", true),
641 ("foo///", "foo", true),
642 ("foo/.", "foo", true),
643 ("foo/./bar", "foo/bar", true),
644 ("foo/.//bar", "foo/bar", true),
645 ("foo//./bar", "foo/bar", true),
646 ("foo/bar", "foo", true),
647 ("foo/bar", "foobar", false),
648 ("foo/bar/baz", "foo/bar", true),
649 ("foo/bar", "foo/bar/baz", false),
650 ("./foo/bar/", ".", true),
651 ("/etc/passwd", "/etc", true),
652 ("/etc/passwd", "/etc/", true),
653 ("/etc/passwd", "/etc/passwd", true),
654 ("/etc/passwd", "/etc/passwd/", true),
655 ("/etc/passwd", "/etc/passwd///", true),
656 ("/etc/passwd", "/e", false),
657 ("/etc/passwd", "/etc/passwd.txt", false),
658 ("/etc/foo.rs", "/etc/foo", false),
659 ];
660
661 for (path, prefix, expected) in unix_paths_and_prefixes {
662 assert_eq!(
663 PathStyle::Unix.starts_with(Path::new(path), Path::new(prefix)),
664 expected,
665 "{path:?}.starts_with({prefix:?})"
666 );
667 }
668 }
669
670 #[test]
671 fn test_windows_path_style_starts_with() {
672 let windows_paths_and_prefixes = [
673 (
674 r"C:\src\rust\cargo-test\test\Cargo.toml",
675 r"c:\src\rust\cargo-test\test",
676 true,
677 ),
678 (r"C:\foo\.\bar.txt", r"C:\foo\bar.txt", true),
679 (r"C:\foo\.", r"C:\foo", true),
680 (r"\\server\share", r"\\server", false),
681 (r"\\server\share\foo.txt", r"\\server", false),
682 (r"\\server\share\foo.txt", r"\\server\share", true),
683 (r"\\server\share\foo.txt", r"\\server\share\", true),
684 (r"\\server\share\foo.txt", r"\\server\share/", true),
685 (r"\\server\share\foo.txt", r"\\server\other", false),
686 (r"\\server\share\foo.txt", r"\\SERVER\share", false),
687 (r"\\?\C:\foo\.\bar.txt", r"\\?\C:\foo\bar.txt", false),
688 (r"\\?\C:\foo.txt", r"\\?\C:\", true),
689 (
690 r"\\?\UNC\server\share\foo.txt",
691 r"\\?\UNC\server\share",
692 true,
693 ),
694 (r"\\?\UNC\server\share\foo.txt", r"\\?\UNC\server\", false),
695 (r"\\?\bar\foo.txt", r"\\?\bar", true),
696 (r"\\?\C:\foo/bar.txt", r"\\?\C:\foo", false),
697 ];
698
699 for (path, prefix, expected) in windows_paths_and_prefixes {
700 assert_eq!(
701 PathStyle::Windows.starts_with(Path::new(path), Path::new(prefix)),
702 expected,
703 "{path:?}.starts_with({prefix:?})"
704 );
705 }
706 }
707
708 #[test]
709 fn test_unix_path_style_file_name() {
710 let unix_paths_and_filenames = [
711 (Path::new(""), None),
712 (Path::new("/usr/bin/"), Some("bin")),
713 (Path::new("tmp/foo.txt"), Some("foo.txt")),
714 (Path::new("."), None),
715 (Path::new("./"), None),
716 (Path::new("foo.txt/."), Some("foo.txt")),
717 (Path::new("foo.txt/.//"), Some("foo.txt")),
718 (Path::new("foo/./bar"), Some("bar")),
719 (Path::new("foo.txt/.."), None),
720 (Path::new("/.."), None),
721 (Path::new("/"), None),
722 ];
723
724 for (path, filename) in unix_paths_and_filenames {
725 assert_eq!(PathStyle::Unix.file_name(path), filename.map(OsStr::new));
726 }
727 }
728
729 #[test]
730 fn test_windows_path_style_file_name() {
731 let windows_paths_and_filenames = [
732 (Path::new(""), None),
733 (Path::new("."), None),
734 (Path::new(r"C:\usr\bin\"), Some("bin")),
735 (Path::new(r"C:"), None),
736 (Path::new(r"tmp\foo.txt"), Some("foo.txt")),
737 (Path::new("tmp/foo.txt"), Some("foo.txt")),
738 (Path::new(r"foo.txt\."), Some("foo.txt")),
739 (Path::new(r"foo.txt\.\"), Some("foo.txt")),
740 (Path::new(r"foo.txt\.."), None),
741 (Path::new(r"C:\"), None),
742 (Path::new(r"\\server\share"), None),
743 (Path::new(r"\\server\share\foo.txt"), Some("foo.txt")),
744 (Path::new("//server/share"), None),
745 (Path::new("//server/share/foo.txt"), Some("foo.txt")),
746 (Path::new(r"\\?\bar"), None),
747 (Path::new(r"\\?\bar\foo.txt"), Some("foo.txt")),
748 (Path::new(r"\\?\C:/foo/bar"), None),
749 (Path::new(r"\\.\device"), None),
750 (Path::new(r"\\.\device\foo.txt"), Some("foo.txt")),
751 ];
752
753 for (path, filename) in windows_paths_and_filenames {
754 assert_eq!(PathStyle::Windows.file_name(path), filename.map(OsStr::new));
755 }
756 }
757
758 #[test]
759 fn test_unix_path_style_parent() {
760 let unix_paths_and_parents = [
761 ("", None),
762 ("/foo/bar", Some("/foo")),
763 ("/foo", Some("/")),
764 ("/", None),
765 ("///foo///", Some("/")),
766 ("///foo///bar", Some("///foo")),
767 ("foo/bar", Some("foo")),
768 ("foo", Some("")),
769 ("foo/.", Some("")),
770 ("foo/./bar", Some("foo")),
771 ("foo/../bar", Some("foo/..")),
772 ("./a", Some(".")),
773 ("./.", Some("")),
774 ("/..", Some("/")),
775 (".", Some("")),
776 ("..", Some("")),
777 ];
778
779 for (path, parent) in unix_paths_and_parents {
780 assert_eq!(
781 PathStyle::Unix.parent(Path::new(path)),
782 parent.map(Path::new)
783 );
784 }
785 }
786
787 #[test]
788 fn test_windows_path_style_parent() {
789 let windows_paths_and_parents = [
790 ("", None),
791 (r"C:\foo\bar", Some(r"C:\foo")),
792 (r"C:\foo", Some(r"C:\")),
793 (r"C:\", None),
794 (r"C:foo", Some("C:")),
795 (r"C:", None),
796 (r"\foo", Some(r"\")),
797 (r"\", None),
798 (r"foo\bar", Some("foo")),
799 (r"foo\\bar", Some("foo")),
800 ("foo/bar", Some("foo")),
801 ("foo", Some("")),
802 (r"foo\.", Some("")),
803 (r"foo\.\bar", Some("foo")),
804 (r"foo\..\bar", Some(r"foo\..")),
805 (r".\a", Some(".")),
806 (r".\.", Some("")),
807 (r"\\server\share", None),
808 (r"\\server\share\foo.txt", Some(r"\\server\share\")),
809 ("//server/share", None),
810 ("//server/share/foo.txt", Some("//server/share/")),
811 (r"\\?\bar", None),
812 (r"\\?\bar\foo.txt", Some(r"\\?\bar\")),
813 (
814 r"\\?\UNC\server\share\foo.txt",
815 Some(r"\\?\UNC\server\share\"),
816 ),
817 (r"\\?\C:\foo.txt", Some(r"\\?\C:\")),
818 (r"\\?\C:/foo/bar", None),
819 (r"\\.\device", None),
820 (r"\\.\device\foo.txt", Some(r"\\.\device\")),
821 (".", Some("")),
822 ("..", Some("")),
823 ];
824
825 for (path, parent) in windows_paths_and_parents {
826 assert_eq!(
827 PathStyle::Windows.parent(Path::new(path)),
828 parent.map(Path::new)
829 );
830 }
831 }
832
833 #[cfg(unix)]
834 #[test]
835 fn test_local_path_style_non_utf8() {
836 use std::ffi::OsStr;
837 use std::os::unix::ffi::OsStrExt;
838
839 let path = Path::new(OsStr::from_bytes(b"/home/user/\xff\xfe/repo/file.txt"));
840 assert_eq!(
841 PathStyle::Unix.file_name(path),
842 Some(OsStr::new("file.txt"))
843 );
844 assert_eq!(
845 PathStyle::Unix.parent(path),
846 Some(Path::new(OsStr::from_bytes(b"/home/user/\xff\xfe/repo")))
847 );
848
849 let another_path = Path::new(OsStr::from_bytes(b"/home/user/repo/\xff\xfe.txt"));
850 assert_eq!(
851 PathStyle::Unix.file_name(another_path),
852 Some(OsStr::from_bytes(b"\xff\xfe.txt"))
853 );
854 assert_eq!(
855 PathStyle::Unix.parent(another_path),
856 Some(Path::new("/home/user/repo"))
857 );
858 }
859
860 #[cfg(windows)]
861 #[test]
862 fn test_local_path_style_non_utf8() {
863 use std::ffi::OsString;
864 use std::os::windows::ffi::OsStringExt;
865
866 let wide: Vec<u16> = "C:\\invalid_"
867 .encode_utf16()
868 .chain(Some(0xD800))
869 .chain(r"\repo\file.txt".encode_utf16())
870 .collect();
871 let os_str = OsString::from_wide(&wide);
872 let path = Path::new(&os_str);
873
874 assert_eq!(
875 PathStyle::Windows.file_name(path),
876 Some(OsStr::new("file.txt"))
877 );
878 let parent = PathStyle::Windows.parent(path).unwrap();
879 assert_eq!(
880 PathStyle::Windows.file_name(parent),
881 Some(OsStr::new("repo"))
882 );
883 }
884}