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
9use 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        // Verbatim paths treat '/' as a literal character, so only '\' separates body components.
306        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        // Windows prefixes, in precedence order: \\?\UNC\, \\?\ (verbatim), \\.\ (device),
322        // \\ (UNC), drive letter. The device prefix must be matched before the generic UNC
323        // check because it also starts with two separators.
324        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                // A drive letter only counts as a prefix when followed by a separator or end
344                // of path; otherwise (e.g. `\\?\C:foo`) the whole first component is the prefix.
345                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                // A UNC prefix requires a share name; `\\server` without one is treated as a
361                // relative path, so its prefix is empty.
362                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        // Uses is_separator rather than is_body_separator: after a verbatim drive prefix,
377        // '/' still counts as the root separator (e.g. `\\?\C:/foo`) even though '/' does
378        // not separate body components.
379        let has_root = path_bytes
380            .get(prefix_end)
381            .is_some_and(|byte| is_separator(*byte));
382        // The leading `.`/`./` is absorbed into the body (e.g. `./a` → parent `.`).
383        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        // Loop because trimming a trailing '.' (e.g. `foo/.`) can expose a separator to trim,
391        // and vice versa (`foo/./`). Verbatim paths never treat '.' as a current-directory
392        // component, hence the !is_verbatim check below.
393        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        // An empty body means the path is root- or prefix-only and has no parent; only a
417        // bare current-directory component (`.`/`./`) has `""` as its parent.
418        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        // Match behavior of std::path::Path, which is case-insensitive for drive letters (e.g., "C:" == "c:")
448        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
484/// Normalizes a path by resolving `.` and `..` components without
485/// requiring the path to exist on disk (unlike `canonicalize`).
486pub 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}