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