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    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                // Safety: bytes are valid WTF-8 sequence.
48                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
336// This part contains the methods adopted from the std lib
337// to make the pub api of `PathStyle` compatible with `Path`
338impl 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
381/// Normalizes a path by resolving `.` and `..` components without
382/// requiring the path to exist on disk (unlike `canonicalize`).
383pub 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}