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