1use camino::{FromPathBufError, Utf8Path, Utf8PathBuf};
5use std::{error::Error, fmt, io};
6
7#[derive(Clone, Debug, PartialEq, Eq)]
11pub struct AbsUtf8PathError {
12 path: Utf8PathBuf,
14
15 kind: AbsUtf8PathErrorKind,
17}
18
19impl AbsUtf8PathError {
20 pub(crate) fn new(path: Utf8PathBuf, kind: AbsUtf8PathErrorKind) -> Self {
21 Self { path, kind }
22 }
23
24 #[must_use]
26 pub fn path(&self) -> &Utf8Path {
27 &self.path
28 }
29
30 #[must_use]
32 pub fn into_path(self) -> Utf8PathBuf {
33 self.path
34 }
35
36 #[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#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
79#[non_exhaustive]
80pub enum AbsUtf8PathErrorKind {
81 Empty,
83
84 ContainsNul,
88
89 NotAbsolute,
91
92 RootRelative,
94
95 DriveRelative,
97}
98
99#[derive(Clone, Debug, PartialEq, Eq)]
103pub struct RelUtf8PathError {
104 path: Utf8PathBuf,
106
107 kind: RelUtf8PathErrorKind,
109}
110
111impl RelUtf8PathError {
112 pub(crate) fn new(path: Utf8PathBuf, kind: RelUtf8PathErrorKind) -> Self {
113 Self { path, kind }
114 }
115
116 #[must_use]
118 pub fn path(&self) -> &Utf8Path {
119 &self.path
120 }
121
122 #[must_use]
124 pub fn into_path(self) -> Utf8PathBuf {
125 self.path
126 }
127
128 #[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#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
173#[non_exhaustive]
174pub enum RelUtf8PathErrorKind {
175 Empty,
177
178 ContainsNul,
182
183 Absolute,
185
186 RootRelative,
188
189 DriveRelative,
191}
192
193#[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 #[must_use]
211 pub fn input(&self) -> &Utf8Path {
212 &self.input
213 }
214
215 #[must_use]
217 pub fn into_input(self) -> Utf8PathBuf {
218 self.input
219 }
220
221 #[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#[derive(Debug)]
275#[non_exhaustive]
276pub enum ResolvePathErrorKind {
277 Empty,
279
280 ContainsNul,
282
283 Native(NativePathErrorKind),
285}
286
287#[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 #[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#[derive(Debug)]
342#[non_exhaustive]
343pub enum NativePathErrorKind {
344 Io(io::Error),
347
348 NonUtf8(FromPathBufError),
350
351 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#[derive(Clone, Debug, PartialEq, Eq)]
407#[non_exhaustive]
408pub enum TryFromPathBufError {
409 NonUtf8(FromPathBufError),
411
412 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}