Skip to main content

camino_anchored/
errors.rs

1// Copyright (c) The camino-anchored Contributors
2// SPDX-License-Identifier: MIT OR Apache-2.0
3
4use camino::{FromPathBufError, Utf8Path, Utf8PathBuf};
5use std::{error::Error, fmt, io};
6
7/// An error indicating that a path was not a well-formed absolute path.
8///
9/// Returned by [`AbsUtf8PathBuf::new`](crate::AbsUtf8PathBuf::new).
10#[derive(Clone, Debug, PartialEq, Eq)]
11pub struct AbsUtf8PathError {
12    /// The path that was rejected.
13    path: Utf8PathBuf,
14
15    /// The reason the path was rejected.
16    kind: AbsUtf8PathErrorKind,
17}
18
19impl AbsUtf8PathError {
20    pub(crate) fn new(path: Utf8PathBuf, kind: AbsUtf8PathErrorKind) -> Self {
21        Self { path, kind }
22    }
23
24    /// Returns the path that was rejected.
25    #[must_use]
26    pub fn path(&self) -> &Utf8Path {
27        &self.path
28    }
29
30    /// Consumes the error, returning the path that was rejected.
31    #[must_use]
32    pub fn into_path(self) -> Utf8PathBuf {
33        self.path
34    }
35
36    /// Returns the reason the path was rejected.
37    #[must_use]
38    pub fn kind(&self) -> AbsUtf8PathErrorKind {
39        self.kind
40    }
41}
42
43impl fmt::Display for AbsUtf8PathError {
44    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
45        let path = &self.path;
46        match self.kind {
47            AbsUtf8PathErrorKind::Empty => {
48                write!(f, "expected an absolute path, but the path is empty")
49            }
50            AbsUtf8PathErrorKind::ContainsNul => {
51                write!(
52                    f,
53                    "expected an absolute path, but {path:?} contains a NUL byte"
54                )
55            }
56            AbsUtf8PathErrorKind::NotAbsolute => {
57                write!(f, "expected an absolute path, got `{path}`")
58            }
59            AbsUtf8PathErrorKind::RootRelative => write!(
60                f,
61                "expected an absolute path, but `{path}` is relative to the root \
62                 of the current drive"
63            ),
64            AbsUtf8PathErrorKind::DriveRelative => write!(
65                f,
66                "expected an absolute path, but `{path}` is relative to the \
67                 current directory of its drive"
68            ),
69        }
70    }
71}
72
73impl Error for AbsUtf8PathError {}
74
75/// The reason a path is not a well-formed absolute path.
76///
77/// Part of [`AbsUtf8PathError`].
78#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
79#[non_exhaustive]
80pub enum AbsUtf8PathErrorKind {
81    /// The path is empty.
82    Empty,
83
84    /// The path contains a NUL byte.
85    ///
86    /// No supported platform allows NUL bytes in paths.
87    ContainsNul,
88
89    /// The path is not absolute, e.g. `foo/bar`.
90    NotAbsolute,
91
92    /// The path is root-relative, e.g. `\foo` on Windows.
93    RootRelative,
94
95    /// The path is drive-relative, e.g. `C:foo` or `C:` on Windows.
96    DriveRelative,
97}
98
99/// An error indicating that a path was not a well-formed relative path.
100///
101/// Returned by [`RelUtf8PathBuf::new`](crate::RelUtf8PathBuf::new).
102#[derive(Clone, Debug, PartialEq, Eq)]
103pub struct RelUtf8PathError {
104    /// The non-well-formed relative path.
105    path: Utf8PathBuf,
106
107    /// The reason the path was rejected.
108    kind: RelUtf8PathErrorKind,
109}
110
111impl RelUtf8PathError {
112    pub(crate) fn new(path: Utf8PathBuf, kind: RelUtf8PathErrorKind) -> Self {
113        Self { path, kind }
114    }
115
116    /// Returns the path that was rejected.
117    #[must_use]
118    pub fn path(&self) -> &Utf8Path {
119        &self.path
120    }
121
122    /// Consumes the error, returning the path that was rejected.
123    #[must_use]
124    pub fn into_path(self) -> Utf8PathBuf {
125        self.path
126    }
127
128    /// Returns the reason the path was rejected.
129    #[must_use]
130    pub fn kind(&self) -> RelUtf8PathErrorKind {
131        self.kind
132    }
133}
134
135impl fmt::Display for RelUtf8PathError {
136    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
137        let path = &self.path;
138        match self.kind {
139            RelUtf8PathErrorKind::Empty => {
140                write!(f, "expected a relative path, but the path is empty")
141            }
142            RelUtf8PathErrorKind::ContainsNul => {
143                write!(
144                    f,
145                    "expected a relative path, but {path:?} contains a NUL byte"
146                )
147            }
148            RelUtf8PathErrorKind::Absolute => {
149                write!(f, "expected a relative path, but `{path}` is absolute")
150            }
151            RelUtf8PathErrorKind::RootRelative => write!(
152                f,
153                "expected a relative path, but `{path}` is relative to the root \
154                 of the current drive (remove the leading separator to make it \
155                 relative)"
156            ),
157            RelUtf8PathErrorKind::DriveRelative => write!(
158                f,
159                "expected a relative path, but `{path}` is relative to the \
160                 current directory of its drive (remove the drive prefix to \
161                 make it relative)"
162            ),
163        }
164    }
165}
166
167impl Error for RelUtf8PathError {}
168
169/// The reason a path is not a well-formed relative path.
170///
171/// Part of [`RelUtf8PathError`].
172#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
173#[non_exhaustive]
174pub enum RelUtf8PathErrorKind {
175    /// The path is empty.
176    Empty,
177
178    /// The path contains a NUL byte.
179    ///
180    /// No supported platform allows NUL bytes in paths.
181    ContainsNul,
182
183    /// The path is absolute, e.g. `/foo` on Unix or `C:\foo` on Windows.
184    Absolute,
185
186    /// The path is root-relative, e.g. `\foo` on Windows.
187    RootRelative,
188
189    /// The path is drive-relative, e.g. `C:foo` or `C:` on Windows.
190    DriveRelative,
191}
192
193/// An error that occurs if an input path could not be resolved.
194///
195/// Returned by
196/// [`AbsUtf8PathBuf::resolve_against_current_dir`](crate::AbsUtf8PathBuf::resolve_against_current_dir)
197/// and [`PathAnchor::resolve_input`](crate::PathAnchor::resolve_input).
198#[derive(Debug)]
199pub struct ResolvePathError {
200    input: Utf8PathBuf,
201    kind: ResolvePathErrorKind,
202}
203
204impl ResolvePathError {
205    pub(crate) fn new(input: Utf8PathBuf, kind: ResolvePathErrorKind) -> Self {
206        Self { input, kind }
207    }
208
209    /// The input path that could not be resolved.
210    #[must_use]
211    pub fn input(&self) -> &Utf8Path {
212        &self.input
213    }
214
215    /// Consumes the error, returning the input path that could not be resolved.
216    #[must_use]
217    pub fn into_input(self) -> Utf8PathBuf {
218        self.input
219    }
220
221    /// Returns the reason resolution failed.
222    #[must_use]
223    pub fn kind(&self) -> &ResolvePathErrorKind {
224        &self.kind
225    }
226}
227
228impl fmt::Display for ResolvePathError {
229    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
230        let input = &self.input;
231        match &self.kind {
232            ResolvePathErrorKind::Empty => write!(f, "cannot resolve an empty path"),
233            ResolvePathErrorKind::ContainsNul => {
234                write!(f, "cannot resolve {input:?}: path contains a NUL byte")
235            }
236            ResolvePathErrorKind::Native(NativePathErrorKind::Io(_)) => {
237                write!(f, "failed to resolve `{input}` to an absolute path")
238            }
239            ResolvePathErrorKind::Native(NativePathErrorKind::NonUtf8(error)) => write!(
240                f,
241                "resolved `{input}` to `{}`, which is not valid UTF-8",
242                error.as_path().display()
243            ),
244            ResolvePathErrorKind::Native(NativePathErrorKind::Invalid(error)) => {
245                match error.kind() {
246                    AbsUtf8PathErrorKind::Empty | AbsUtf8PathErrorKind::ContainsNul => {
247                        write!(f, "resolved `{input}` to an invalid path")
248                    }
249                    AbsUtf8PathErrorKind::NotAbsolute
250                    | AbsUtf8PathErrorKind::RootRelative
251                    | AbsUtf8PathErrorKind::DriveRelative => write!(
252                        f,
253                        "resolved `{input}` to an invalid path \
254                         (the current directory may be unreachable)"
255                    ),
256                }
257            }
258        }
259    }
260}
261
262impl Error for ResolvePathError {
263    fn source(&self) -> Option<&(dyn Error + 'static)> {
264        match &self.kind {
265            ResolvePathErrorKind::Empty | ResolvePathErrorKind::ContainsNul => None,
266            ResolvePathErrorKind::Native(kind) => kind.source(),
267        }
268    }
269}
270
271/// The reason an input path could not be resolved.
272///
273/// Part of [`ResolvePathError`].
274#[derive(Debug)]
275#[non_exhaustive]
276pub enum ResolvePathErrorKind {
277    /// The input path is empty.
278    Empty,
279
280    /// The input path contains a NUL byte.
281    ContainsNul,
282
283    /// Native resolution failed, or its result could not be used.
284    Native(NativePathErrorKind),
285}
286
287/// An error that occurred while obtaining the current directory.
288///
289/// Returned by
290/// [`AbsUtf8PathBuf::current_dir`](crate::AbsUtf8PathBuf::current_dir) and
291/// [`PathAnchor::current_dir`](crate::PathAnchor::current_dir).
292#[derive(Debug)]
293pub struct CurrentDirError {
294    kind: NativePathErrorKind,
295}
296
297impl CurrentDirError {
298    pub(crate) fn new(kind: NativePathErrorKind) -> Self {
299        Self { kind }
300    }
301
302    /// Returns information about the kind of error that occurred.
303    #[must_use]
304    pub fn kind(&self) -> &NativePathErrorKind {
305        &self.kind
306    }
307}
308
309impl fmt::Display for CurrentDirError {
310    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
311        match &self.kind {
312            NativePathErrorKind::Io(_) => write!(f, "failed to read the current directory"),
313            NativePathErrorKind::NonUtf8(error) => write!(
314                f,
315                "current directory `{}` is not valid UTF-8",
316                error.as_path().display()
317            ),
318            NativePathErrorKind::Invalid(error) => match error.kind() {
319                AbsUtf8PathErrorKind::Empty | AbsUtf8PathErrorKind::ContainsNul => {
320                    write!(f, "current directory is invalid")
321                }
322                AbsUtf8PathErrorKind::NotAbsolute
323                | AbsUtf8PathErrorKind::RootRelative
324                | AbsUtf8PathErrorKind::DriveRelative => {
325                    write!(f, "current directory is invalid (it may be unreachable)")
326                }
327            },
328        }
329    }
330}
331
332impl Error for CurrentDirError {
333    fn source(&self) -> Option<&(dyn Error + 'static)> {
334        self.kind.source()
335    }
336}
337
338/// A platform-level error that occurred while resolving a path.
339///
340/// Part of [`CurrentDirError`] and [`ResolvePathErrorKind::Native`].
341#[derive(Debug)]
342#[non_exhaustive]
343pub enum NativePathErrorKind {
344    /// A platform-level resolution failure occurred (e.g. the current directory
345    /// is not readable).
346    Io(io::Error),
347
348    /// The resolved path is not valid UTF-8.
349    NonUtf8(FromPathBufError),
350
351    /// The resolved path is not a well-formed absolute path.
352    ///
353    /// This should not occur in typical use. On Linux systems with glibc older
354    /// than 2.27, this can happen if the current directory is outside the
355    /// process's root directory (for example, after a `chroot` operation):
356    /// `getcwd` returns a path starting with `(unreachable)`. See
357    /// [glibc bug 22679](https://sourceware.org/bugzilla/show_bug.cgi?id=22679).
358    Invalid(AbsUtf8PathError),
359}
360
361impl NativePathErrorKind {
362    fn source(&self) -> Option<&(dyn Error + 'static)> {
363        match self {
364            NativePathErrorKind::Io(error) => Some(error),
365            NativePathErrorKind::Invalid(error) => Some(error),
366            NativePathErrorKind::NonUtf8(error) => Some(error),
367        }
368    }
369}
370
371#[derive(Clone, Copy, Debug)]
372pub(crate) enum MalformedPathKind {
373    Empty,
374    ContainsNul,
375}
376
377impl From<MalformedPathKind> for AbsUtf8PathErrorKind {
378    fn from(kind: MalformedPathKind) -> Self {
379        match kind {
380            MalformedPathKind::Empty => Self::Empty,
381            MalformedPathKind::ContainsNul => Self::ContainsNul,
382        }
383    }
384}
385
386impl From<MalformedPathKind> for RelUtf8PathErrorKind {
387    fn from(kind: MalformedPathKind) -> Self {
388        match kind {
389            MalformedPathKind::Empty => Self::Empty,
390            MalformedPathKind::ContainsNul => Self::ContainsNul,
391        }
392    }
393}
394
395impl From<MalformedPathKind> for ResolvePathErrorKind {
396    fn from(kind: MalformedPathKind) -> Self {
397        match kind {
398            MalformedPathKind::Empty => Self::Empty,
399            MalformedPathKind::ContainsNul => Self::ContainsNul,
400        }
401    }
402}
403
404/// An error converting a [`PathBuf`](std::path::PathBuf) to an
405/// [`AbsUtf8PathBuf`](crate::AbsUtf8PathBuf).
406#[derive(Clone, Debug, PartialEq, Eq)]
407#[non_exhaustive]
408pub enum TryFromPathBufError {
409    /// The path was not valid UTF-8.
410    NonUtf8(FromPathBufError),
411
412    /// The path was UTF-8, but was rejected by
413    /// [`AbsUtf8PathBuf::new`](crate::AbsUtf8PathBuf::new).
414    Invalid(AbsUtf8PathError),
415}
416
417impl fmt::Display for TryFromPathBufError {
418    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
419        match self {
420            TryFromPathBufError::NonUtf8(error) => fmt::Display::fmt(error, f),
421            TryFromPathBufError::Invalid(error) => fmt::Display::fmt(error, f),
422        }
423    }
424}
425
426impl Error for TryFromPathBufError {
427    fn source(&self) -> Option<&(dyn Error + 'static)> {
428        match self {
429            TryFromPathBufError::NonUtf8(error) => error.source(),
430            TryFromPathBufError::Invalid(error) => error.source(),
431        }
432    }
433}
434
435#[cfg(test)]
436mod tests {
437    use super::*;
438
439    #[cfg(unix)]
440    fn non_utf8_error(prefix: &str) -> FromPathBufError {
441        use std::{ffi::OsStr, os::unix::ffi::OsStrExt, path::PathBuf};
442
443        let mut bytes = prefix.as_bytes().to_vec();
444        bytes.push(0xff);
445        Utf8PathBuf::try_from(PathBuf::from(OsStr::from_bytes(&bytes)))
446            .expect_err("path with an invalid byte is not UTF-8")
447    }
448
449    #[cfg(windows)]
450    fn non_utf8_error(prefix: &str) -> FromPathBufError {
451        use std::{ffi::OsString, os::windows::ffi::OsStringExt, path::PathBuf};
452
453        const LONE_SURROGATE: u16 = 0xD800;
454        let mut wide: Vec<u16> = prefix.encode_utf16().collect();
455        wide.push(LONE_SURROGATE);
456        Utf8PathBuf::try_from(PathBuf::from(OsString::from_wide(&wide)))
457            .expect_err("path with a lone surrogate is not UTF-8")
458    }
459
460    #[test]
461    fn absolute_path_error_display() {
462        for (path, kind, expected) in [
463            (
464                "",
465                AbsUtf8PathErrorKind::Empty,
466                "expected an absolute path, but the path is empty",
467            ),
468            (
469                "/a\0b",
470                AbsUtf8PathErrorKind::ContainsNul,
471                r#"expected an absolute path, but "/a\0b" contains a NUL byte"#,
472            ),
473            (
474                "foo/bar",
475                AbsUtf8PathErrorKind::NotAbsolute,
476                "expected an absolute path, got `foo/bar`",
477            ),
478            (
479                r"\foo",
480                AbsUtf8PathErrorKind::RootRelative,
481                r"expected an absolute path, but `\foo` is relative to the root of the current drive",
482            ),
483            (
484                "C:foo",
485                AbsUtf8PathErrorKind::DriveRelative,
486                "expected an absolute path, but `C:foo` is relative to the current directory of its drive",
487            ),
488        ] {
489            let error = AbsUtf8PathError::new(Utf8PathBuf::from(path), kind);
490            assert_eq!(error.to_string(), expected, "{kind:?}");
491            assert!(error.source().is_none(), "{kind:?}");
492        }
493    }
494
495    #[test]
496    fn relative_path_error_display() {
497        for (path, kind, expected) in [
498            (
499                "",
500                RelUtf8PathErrorKind::Empty,
501                "expected a relative path, but the path is empty",
502            ),
503            (
504                "a\0b",
505                RelUtf8PathErrorKind::ContainsNul,
506                r#"expected a relative path, but "a\0b" contains a NUL byte"#,
507            ),
508            (
509                "/foo",
510                RelUtf8PathErrorKind::Absolute,
511                "expected a relative path, but `/foo` is absolute",
512            ),
513            (
514                r"\foo",
515                RelUtf8PathErrorKind::RootRelative,
516                r"expected a relative path, but `\foo` is relative to the root of the current drive (remove the leading separator to make it relative)",
517            ),
518            (
519                "C:foo",
520                RelUtf8PathErrorKind::DriveRelative,
521                "expected a relative path, but `C:foo` is relative to the current directory of its drive (remove the drive prefix to make it relative)",
522            ),
523        ] {
524            let error = RelUtf8PathError::new(Utf8PathBuf::from(path), kind);
525            assert_eq!(error.to_string(), expected, "{kind:?}");
526            assert!(error.source().is_none(), "{kind:?}");
527        }
528    }
529
530    #[cfg(any(unix, windows))]
531    #[test]
532    fn resolve_path_error_display() {
533        let not_found = io::Error::from(io::ErrorKind::NotFound).to_string();
534        for (input, kind, expected, expected_source) in [
535            (
536                "",
537                ResolvePathErrorKind::Empty,
538                "cannot resolve an empty path",
539                None,
540            ),
541            (
542                "a\0b",
543                ResolvePathErrorKind::ContainsNul,
544                r#"cannot resolve "a\0b": path contains a NUL byte"#,
545                None,
546            ),
547            (
548                "config.toml",
549                ResolvePathErrorKind::Native(NativePathErrorKind::Io(io::Error::from(
550                    io::ErrorKind::NotFound,
551                ))),
552                "failed to resolve `config.toml` to an absolute path",
553                Some(not_found.as_str()),
554            ),
555            (
556                "config.toml",
557                ResolvePathErrorKind::Native(NativePathErrorKind::NonUtf8(non_utf8_error(
558                    "/repo/config.toml",
559                ))),
560                "resolved `config.toml` to `/repo/config.toml\u{fffd}`, which is not valid UTF-8",
561                Some("PathBuf contains invalid UTF-8: /repo/config.toml\u{fffd}"),
562            ),
563            (
564                "config.toml",
565                native_invalid(
566                    "(unreachable)/repo/config.toml",
567                    AbsUtf8PathErrorKind::NotAbsolute,
568                ),
569                "resolved `config.toml` to an invalid path (the current directory may be unreachable)",
570                Some("expected an absolute path, got `(unreachable)/repo/config.toml`"),
571            ),
572            (
573                "config.toml",
574                native_invalid("", AbsUtf8PathErrorKind::Empty),
575                "resolved `config.toml` to an invalid path",
576                Some("expected an absolute path, but the path is empty"),
577            ),
578            (
579                "config.toml",
580                native_invalid("/repo/a\0b", AbsUtf8PathErrorKind::ContainsNul),
581                "resolved `config.toml` to an invalid path",
582                Some(r#"expected an absolute path, but "/repo/a\0b" contains a NUL byte"#),
583            ),
584        ] {
585            let error = ResolvePathError::new(Utf8PathBuf::from(input), kind);
586            assert_eq!(error.to_string(), expected, "{error:?}");
587            assert_eq!(
588                error.source().map(|source| source.to_string()).as_deref(),
589                expected_source,
590                "{error:?}"
591            );
592        }
593    }
594
595    fn native_invalid(path: &str, kind: AbsUtf8PathErrorKind) -> ResolvePathErrorKind {
596        ResolvePathErrorKind::Native(NativePathErrorKind::Invalid(AbsUtf8PathError::new(
597            Utf8PathBuf::from(path),
598            kind,
599        )))
600    }
601
602    #[cfg(any(unix, windows))]
603    #[test]
604    fn current_dir_error_display() {
605        let invalid = |path: &str, kind| {
606            CurrentDirError::new(NativePathErrorKind::Invalid(AbsUtf8PathError::new(
607                Utf8PathBuf::from(path),
608                kind,
609            )))
610        };
611        let not_found = io::Error::from(io::ErrorKind::NotFound).to_string();
612        for (error, expected, expected_source) in [
613            (
614                CurrentDirError::new(NativePathErrorKind::Io(io::Error::from(
615                    io::ErrorKind::NotFound,
616                ))),
617                "failed to read the current directory",
618                Some(not_found.as_str()),
619            ),
620            (
621                CurrentDirError::new(NativePathErrorKind::NonUtf8(non_utf8_error("/repo/"))),
622                "current directory `/repo/\u{fffd}` is not valid UTF-8",
623                Some("PathBuf contains invalid UTF-8: /repo/\u{fffd}"),
624            ),
625            (
626                invalid("(unreachable)/repo", AbsUtf8PathErrorKind::NotAbsolute),
627                "current directory is invalid (it may be unreachable)",
628                Some("expected an absolute path, got `(unreachable)/repo`"),
629            ),
630            (
631                invalid(r"\repo", AbsUtf8PathErrorKind::RootRelative),
632                "current directory is invalid (it may be unreachable)",
633                Some(
634                    r"expected an absolute path, but `\repo` is relative to the root of the current drive",
635                ),
636            ),
637            (
638                invalid("", AbsUtf8PathErrorKind::Empty),
639                "current directory is invalid",
640                Some("expected an absolute path, but the path is empty"),
641            ),
642            (
643                invalid("/a\0b", AbsUtf8PathErrorKind::ContainsNul),
644                "current directory is invalid",
645                Some(r#"expected an absolute path, but "/a\0b" contains a NUL byte"#),
646            ),
647        ] {
648            assert_eq!(error.to_string(), expected, "{error:?}");
649            assert_eq!(
650                error.source().map(|source| source.to_string()).as_deref(),
651                expected_source,
652                "{error:?}"
653            );
654        }
655    }
656}