Skip to main content

path/
path.rs

1//! Relative path types for deltadb.
2//!
3//! Provides [`RelPath`] and [`RelPathBuf`] — path types that are guaranteed to be
4//! relative, normalized, and valid unicode. Internally stored in POSIX (`/`-delimited)
5//! format regardless of host platform.
6//!
7//! Adapted from Zed's `util::rel_path` module.
8
9#[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                // Safety: bytes are valid WTF-8 sequence.
49                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        // Match behavior of std::path::Path, which is case-insensitive for drive letters (e.g., "C:" == "c:")
341        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
364// This part contains the methods adopted from the std lib
365// to make the pub api of `PathStyle` compatible with `Path`
366impl 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
422/// Normalizes a path by resolving `.` and `..` components without
423/// requiring the path to exist on disk (unlike `canonicalize`).
424pub 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}