Skip to main content

nu_protocol/errors/shell_error/
io.rs

1#[cfg(doc)] // allow mentioning this in doc comments
2use super::ShellError;
3use miette::{Diagnostic, LabeledSpan, SourceSpan};
4use std::{
5    error::Error as StdError,
6    fmt::{self, Display, Formatter},
7    panic::Location,
8    path::{Path, PathBuf},
9};
10use thiserror::Error;
11
12use crate::Span;
13
14/// Alias for a `Result` with the error type [`ErrorKind`] by default.
15///
16/// This may be used in all situations that would usually return an [`std::io::Error`] but are
17/// already part of the [`nu_protocol`](crate) crate and can therefore interact with
18/// [`shell_error::io`](self) directly.
19///
20/// To make programming inside this module easier, you can pass the `E` type with another error.
21/// This avoids the annoyance of having a shadowed `Result`.
22pub type Result<T, E = ErrorKind> = std::result::Result<T, E>;
23
24/// Represents an I/O error in the [`ShellError::Io`] variant.
25///
26/// This is the central I/O error for the [`ShellError::Io`] variant.
27/// It represents all I/O errors by encapsulating [`ErrorKind`], an extension of
28/// [`std::io::ErrorKind`].
29/// The `span` indicates where the error occurred in user-provided code.
30/// If the error is not tied to user-provided code, the `location` refers to the precise point in
31/// the Rust code where the error originated.
32/// The optional `path` provides the file or directory involved in the error.
33/// If [`ErrorKind`] alone doesn't provide enough detail, additional context can be added to clarify
34/// the issue.
35///
36/// For handling user input errors (e.g., commands), prefer using [`new`](Self::new).
37/// Alternatively, use the [`factory`](Self::factory) method to simplify error creation in repeated
38/// contexts.
39/// For internal errors, use [`new_internal`](Self::new_internal) to include the location in Rust
40/// code where the error originated.
41///
42/// # Examples
43///
44/// ## User Input Error
45/// ```rust
46/// # use nu_protocol::shell_error::io::{IoError, ErrorKind};
47/// # use nu_protocol::Span;
48/// use std::path::PathBuf;
49///
50/// # let span = Span::test_data();
51/// let path = PathBuf::from("/some/missing/file");
52/// let error = IoError::new(ErrorKind::FileNotFound, span, path);
53/// println!("Error: {:?}", error);
54/// ```
55///
56/// ## Internal Error
57/// ```rust
58/// # use nu_protocol::shell_error::io::{IoError, ErrorKind};
59//  #
60/// let error = IoError::new_internal(
61///     ErrorKind::from_std(std::io::ErrorKind::UnexpectedEof),
62///     "Failed to read data from buffer",
63/// );
64/// println!("Error: {:?}", error);
65/// ```
66///
67/// ## Using the Factory Method
68/// ```rust
69/// # use nu_protocol::shell_error::io::{IoError, ErrorKind};
70/// # use nu_protocol::{Span, ShellError};
71/// use std::path::PathBuf;
72///
73/// # fn should_return_err() -> Result<(), ShellError> {
74/// # let span = Span::new(50, 60);
75/// let path = PathBuf::from("/some/file");
76/// let from_io_error = IoError::factory(span, Some(path.as_path()));
77///
78/// let content = std::fs::read_to_string(&path).map_err(from_io_error)?;
79/// # Ok(())
80/// # }
81/// #
82/// # assert!(should_return_err().is_err());
83/// ```
84///
85/// # ShellErrorBridge
86///
87/// The [`ShellErrorBridge`](super::bridge::ShellErrorBridge) struct is used to contain a
88/// [`ShellError`] inside a [`std::io::Error`].
89/// This allows seamless transfer of `ShellError` instances where `std::io::Error` is expected.
90/// When a `ShellError` needs to be packed into an I/O context, use this bridge.
91/// Similarly, when handling an I/O error that is expected to contain a `ShellError`,
92/// use the bridge to unpack it.
93///
94/// This approach ensures clarity about where such container transfers occur.
95/// All other I/O errors should be handled using the provided constructors for `IoError`.
96/// This way, the code explicitly indicates when and where a `ShellError` transfer might happen.
97#[derive(Debug, Eq, Clone, PartialEq)]
98#[non_exhaustive]
99pub struct IoError {
100    /// The type of the underlying I/O error.
101    ///
102    /// [`std::io::ErrorKind`] provides detailed context about the type of I/O error that occurred
103    /// and is part of [`std::io::Error`].
104    /// If a kind cannot be represented by it, consider adding a new variant to [`ErrorKind`].
105    ///
106    /// Only in very rare cases should [`std::io::Error::other()`] be used, make sure you provide
107    /// `additional_context` to get useful errors in these cases.
108    pub kind: ErrorKind,
109
110    /// The source location of the error.
111    pub span: Span,
112
113    /// The path related to the I/O error, if applicable.
114    ///
115    /// Many I/O errors involve a file or directory path, but operating system error messages
116    /// often don't include the specific path.
117    /// Setting this to [`Some`] allows users to see which path caused the error.
118    pub path: Option<PathBuf>,
119
120    /// Additional details to provide more context about the error.
121    ///
122    /// Only set this field if it adds meaningful context.
123    /// If [`ErrorKind`] already contains all the necessary information, leave this as [`None`].
124    pub additional_context: Option<AdditionalContext>,
125
126    /// The precise location in the Rust code where the error originated.
127    ///
128    /// This field is particularly useful for debugging errors that stem from the Rust
129    /// implementation rather than user-provided Nushell code.
130    /// The original [`Location`] is converted to a string to more easily report the error
131    /// attributing the location.
132    ///
133    /// This value is only used if `span` is [`Span::unknown()`] as most of the time we want to
134    /// refer to user code than the Rust code.
135    pub location: Option<String>,
136}
137
138/// Prevents other crates from constructing certain enum variants directly.
139///
140/// This type is only used to block construction while still allowing pattern matching.
141/// It's not meant to be used for anything else.
142#[derive(Debug, Clone, Copy, PartialEq, Eq)]
143struct Sealed;
144
145#[derive(Debug, Clone, Copy, PartialEq, Eq, Diagnostic)]
146pub enum ErrorKind {
147    /// [`std::io::ErrorKind`] from the standard library.
148    ///
149    /// This variant wraps a standard library error kind and extends our own error enum with it.
150    /// The hidden field prevents other crates, even our own, from constructing this directly.
151    /// Most of the time, you already have a full [`std::io::Error`], so just pass that directly to
152    /// [`IoError::new`] or [`IoError::new_with_additional_context`].
153    /// This allows us to inspect the raw os error of `std::io::Error`s.
154    ///
155    /// Matching is still easy:
156    ///
157    /// ```rust
158    /// # use nu_protocol::shell_error::io::ErrorKind;
159    /// #
160    /// # let err_kind = ErrorKind::from_std(std::io::ErrorKind::NotFound);
161    /// match err_kind {
162    ///     ErrorKind::Std(std::io::ErrorKind::NotFound, ..) => { /* ... */ }
163    ///     _ => {}
164    /// }
165    /// ```
166    ///
167    /// If you want to provide an [`std::io::ErrorKind`] manually, use [`ErrorKind::from_std`].
168    #[allow(private_interfaces)]
169    Std(std::io::ErrorKind, Sealed),
170
171    /// Killing a job process failed.
172    ///
173    /// This error is part [`ShellError::Io`](super::ShellError::Io) instead of
174    /// [`ShellError::Job`](super::ShellError::Job) as this error occurs because some I/O operation
175    /// failed on the OS side.
176    /// And not part of our logic.
177    KillJobProcess,
178
179    NotAFile,
180
181    /// The file or directory is in use by another program.
182    ///
183    /// On Windows, this maps to
184    /// [`ERROR_SHARING_VIOLATION`](::windows::Win32::Foundation::ERROR_SHARING_VIOLATION) and
185    /// prevents access like deletion or modification.
186    #[cfg_attr(not(windows), allow(rustdoc::broken_intra_doc_links))]
187    AlreadyInUse,
188
189    // use these variants in cases where we know precisely whether a file or directory was expected
190    FileNotFound,
191    DirectoryNotFound,
192}
193
194impl ErrorKind {
195    /// Construct an [`ErrorKind`] from a [`std::io::ErrorKind`] without a full [`std::io::Error`].
196    ///
197    /// In most cases, you should use [`IoError::new`] and pass the full [`std::io::Error`] instead.
198    /// This method is only meant for cases where we provide our own io error kinds.
199    pub fn from_std(kind: std::io::ErrorKind) -> Self {
200        Self::Std(kind, Sealed)
201    }
202}
203
204#[derive(Debug, Clone, PartialEq, Eq, Error, Diagnostic)]
205#[error("{0}")]
206pub struct AdditionalContext(String);
207
208impl From<String> for AdditionalContext {
209    fn from(value: String) -> Self {
210        AdditionalContext(value)
211    }
212}
213
214impl IoError {
215    /// Creates a new [`IoError`] with the given kind, span, and optional path.
216    ///
217    /// This constructor should be used in all cases where the combination of the error kind, span,
218    /// and path provides enough information to describe the error clearly.
219    /// For example, errors like "File not found" or "Permission denied" are typically
220    /// self-explanatory when paired with the file path and the location in user-provided
221    /// Nushell code (`span`).
222    ///
223    /// # Constraints
224    /// If `span` is unknown, use:
225    /// - `new_internal` if no path is available.
226    /// - `new_internal_with_path` if a path is available.
227    pub fn new(kind: impl Into<ErrorKind>, span: Span, path: impl Into<Option<PathBuf>>) -> Self {
228        let path = path.into();
229
230        if span == Span::unknown() {
231            debug_assert!(
232                path.is_some(),
233                "for unknown spans with paths, use `new_internal_with_path`"
234            );
235            debug_assert!(
236                path.is_none(),
237                "for unknown spans without paths, use `new_internal`"
238            );
239        }
240
241        Self {
242            kind: kind.into(),
243            span,
244            path,
245            additional_context: None,
246            location: None,
247        }
248    }
249
250    /// Creates a new [`IoError`] with additional context.
251    ///
252    /// Use this constructor when the error kind, span, and path are not sufficient to fully
253    /// explain the error, and additional context can provide meaningful details.
254    /// Avoid redundant context (e.g., "Permission denied" for an error kind of
255    /// [`ErrorKind::PermissionDenied`](std::io::ErrorKind::PermissionDenied)).
256    ///
257    /// # Constraints
258    /// If `span` is unknown, use:
259    /// - `new_internal` if no path is available.
260    /// - `new_internal_with_path` if a path is available.
261    pub fn new_with_additional_context(
262        kind: impl Into<ErrorKind>,
263        span: Span,
264        path: impl Into<Option<PathBuf>>,
265        additional_context: impl ToString,
266    ) -> Self {
267        let path = path.into();
268
269        if span == Span::unknown() {
270            debug_assert!(
271                path.is_some(),
272                "for unknown spans with paths, use `new_internal_with_path`"
273            );
274            debug_assert!(
275                path.is_none(),
276                "for unknown spans without paths, use `new_internal`"
277            );
278        }
279
280        Self {
281            kind: kind.into(),
282            span,
283            path,
284            additional_context: Some(additional_context.to_string().into()),
285            location: None,
286        }
287    }
288
289    /// Creates a new [`IoError`] for internal I/O errors without a user-provided span or path.
290    ///
291    /// This constructor is intended for internal errors in the Rust implementation that still need
292    /// to be reported to the end user.
293    /// Since these errors are not tied to user-provided Nushell code, they generally have no
294    /// meaningful span or path.
295    ///
296    /// Instead, these errors provide:
297    /// - `additional_context`:
298    ///   Details about what went wrong internally.
299    /// - `location`:
300    ///   The location in the Rust code where the error occurred, allowing us to trace and debug
301    ///   the issue.
302    ///
303    /// # Examples
304    /// ```rust
305    /// use nu_protocol::shell_error::{self, io::IoError};
306    ///
307    /// let error = IoError::new_internal(
308    ///     shell_error::io::ErrorKind::from_std(std::io::ErrorKind::UnexpectedEof),
309    ///     "Failed to read from buffer",
310    /// );
311    /// ```
312    #[track_caller]
313    pub fn new_internal(kind: impl Into<ErrorKind>, additional_context: impl ToString) -> Self {
314        Self {
315            kind: kind.into(),
316            span: Span::unknown(),
317            path: None,
318            additional_context: Some(additional_context.to_string().into()),
319            location: Some(Location::caller().to_string()),
320        }
321    }
322
323    /// Creates a new [`IoError`] for internal I/O errors with an explicit caller location.
324    ///
325    /// Use this when you already have a [`Location`] (for example, from a helper) and want to
326    /// attach it instead of relying on `#[track_caller]`.
327    /// This is otherwise equivalent to [`new_internal`](Self::new_internal).
328    pub fn new_internal_with_location(
329        kind: impl Into<ErrorKind>,
330        additional_context: impl ToString,
331        location: &Location<'_>,
332    ) -> Self {
333        Self {
334            kind: kind.into(),
335            span: Span::unknown(),
336            path: None,
337            additional_context: Some(additional_context.to_string().into()),
338            location: Some(location.to_string()),
339        }
340    }
341
342    /// Creates a new `IoError` for internal I/O errors with a specific path.
343    ///
344    /// This constructor is similar to [`new_internal`](Self::new_internal) but also includes a
345    /// file or directory path relevant to the error.
346    /// Use this function in rare cases where an internal error involves a specific path, and the
347    /// combination of path and additional context is helpful.
348    ///
349    /// # Examples
350    /// ```rust
351    /// use nu_protocol::shell_error::{self, io::IoError};
352    /// use std::path::PathBuf;
353    ///
354    /// let error = IoError::new_internal_with_path(
355    ///     shell_error::io::ErrorKind::FileNotFound,
356    ///     "Could not find special file",
357    ///     PathBuf::from("/some/file"),
358    /// );
359    /// ```
360    #[track_caller]
361    pub fn new_internal_with_path(
362        kind: impl Into<ErrorKind>,
363        additional_context: impl ToString,
364        path: PathBuf,
365    ) -> Self {
366        Self {
367            kind: kind.into(),
368            span: Span::unknown(),
369            path: path.into(),
370            additional_context: Some(additional_context.to_string().into()),
371            location: Some(Location::caller().to_string()),
372        }
373    }
374
375    /// Creates a new [`IoError`] for internal I/O errors with a path and explicit location.
376    ///
377    /// Use this variant when the error relates to a specific path and you already have a
378    /// [`Location`] you want to record, rather than relying on `#[track_caller]`.
379    /// This is otherwise equivalent to [`new_internal_with_path`](Self::new_internal_with_path).
380    pub fn new_internal_with_path_and_location(
381        kind: impl Into<ErrorKind>,
382        additional_context: impl ToString,
383        path: PathBuf,
384        location: &Location<'_>,
385    ) -> Self {
386        Self {
387            kind: kind.into(),
388            span: Span::unknown(),
389            path: path.into(),
390            additional_context: Some(additional_context.to_string().into()),
391            location: Some(location.to_string()),
392        }
393    }
394
395    /// Creates a factory closure for constructing [`IoError`] instances from [`std::io::Error`] values.
396    ///
397    /// This method is particularly useful when you need to handle multiple I/O errors which all
398    /// take the same span and path.
399    /// Instead of calling `.map_err(|err| IoError::new(err, span, path))` every time, you
400    /// can create the factory closure once and pass that into `.map_err`.
401    pub fn factory<'p, P>(span: Span, path: P) -> impl Fn(std::io::Error) -> Self + use<'p, P>
402    where
403        P: Into<Option<&'p Path>>,
404    {
405        let path = path.into();
406        move |err: std::io::Error| IoError::new(err, span, path.map(PathBuf::from))
407    }
408}
409
410impl From<std::io::Error> for ErrorKind {
411    fn from(err: std::io::Error) -> Self {
412        (&err).into()
413    }
414}
415
416impl From<&std::io::Error> for ErrorKind {
417    fn from(err: &std::io::Error) -> Self {
418        #[cfg(windows)]
419        if let Some(raw_os_error) = err.raw_os_error() {
420            use windows::Win32::Foundation;
421
422            #[allow(clippy::single_match, reason = "in the future we can expand here")]
423            match Foundation::WIN32_ERROR(raw_os_error as u32) {
424                Foundation::ERROR_SHARING_VIOLATION => return ErrorKind::AlreadyInUse,
425                _ => {}
426            }
427        }
428
429        #[cfg(debug_assertions)]
430        if err.kind() == std::io::ErrorKind::Other {
431            panic!(
432                "\
433suspicious conversion:
434    tried to convert `std::io::Error` with `std::io::ErrorKind::Other`
435    into `nu_protocol::shell_error::io::ErrorKind`
436
437I/O errors should always be specific, provide more context
438
439{err:#?}\
440            "
441            )
442        }
443
444        ErrorKind::Std(err.kind(), Sealed)
445    }
446}
447
448impl From<nu_system::KillByPidError> for ErrorKind {
449    fn from(value: nu_system::KillByPidError) -> Self {
450        match value {
451            nu_system::KillByPidError::Output(error) => error.into(),
452            nu_system::KillByPidError::KillProcess => ErrorKind::KillJobProcess,
453        }
454    }
455}
456
457impl StdError for IoError {}
458impl Display for IoError {
459    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
460        match self.kind {
461            ErrorKind::Std(std::io::ErrorKind::NotFound, _) => write!(f, "Not found"),
462            ErrorKind::FileNotFound => write!(f, "File not found"),
463            ErrorKind::DirectoryNotFound => write!(f, "Directory not found"),
464            _ => write!(f, "I/O error"),
465        }
466    }
467}
468
469impl Display for ErrorKind {
470    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
471        match self {
472            ErrorKind::Std(std::io::ErrorKind::NotFound, _) => write!(f, "Not found"),
473            ErrorKind::Std(error_kind, _) => {
474                let msg = error_kind.to_string();
475                let (first, rest) = msg.split_at(1);
476                write!(f, "{}{}", first.to_uppercase(), rest)
477            }
478            ErrorKind::KillJobProcess => write!(f, "Killing job process failed"),
479            ErrorKind::NotAFile => write!(f, "Not a file"),
480            ErrorKind::AlreadyInUse => write!(f, "Already in use"),
481            ErrorKind::FileNotFound => write!(f, "File not found"),
482            ErrorKind::DirectoryNotFound => write!(f, "Directory not found"),
483        }
484    }
485}
486
487impl std::error::Error for ErrorKind {}
488
489impl Diagnostic for IoError {
490    fn code<'a>(&'a self) -> Option<Box<dyn std::fmt::Display + 'a>> {
491        let mut code = String::from("nu::shell::io::");
492        match self.kind {
493            ErrorKind::Std(error_kind, _) => match error_kind {
494                std::io::ErrorKind::NotFound => code.push_str("not_found"),
495                std::io::ErrorKind::PermissionDenied => code.push_str("permission_denied"),
496                std::io::ErrorKind::ConnectionRefused => code.push_str("connection_refused"),
497                std::io::ErrorKind::ConnectionReset => code.push_str("connection_reset"),
498                std::io::ErrorKind::ConnectionAborted => code.push_str("connection_aborted"),
499                std::io::ErrorKind::NotConnected => code.push_str("not_connected"),
500                std::io::ErrorKind::AddrInUse => code.push_str("addr_in_use"),
501                std::io::ErrorKind::AddrNotAvailable => code.push_str("addr_not_available"),
502                std::io::ErrorKind::BrokenPipe => code.push_str("broken_pipe"),
503                std::io::ErrorKind::AlreadyExists => code.push_str("already_exists"),
504                std::io::ErrorKind::WouldBlock => code.push_str("would_block"),
505                std::io::ErrorKind::InvalidInput => code.push_str("invalid_input"),
506                std::io::ErrorKind::InvalidData => code.push_str("invalid_data"),
507                std::io::ErrorKind::TimedOut => code.push_str("timed_out"),
508                std::io::ErrorKind::WriteZero => code.push_str("write_zero"),
509                std::io::ErrorKind::Interrupted => code.push_str("interrupted"),
510                std::io::ErrorKind::Unsupported => code.push_str("unsupported"),
511                std::io::ErrorKind::UnexpectedEof => code.push_str("unexpected_eof"),
512                std::io::ErrorKind::OutOfMemory => code.push_str("out_of_memory"),
513                std::io::ErrorKind::Other => code.push_str("other"),
514                kind => code.push_str(&kind.to_string().to_lowercase().replace(" ", "_")),
515            },
516            ErrorKind::KillJobProcess => code.push_str("kill_job_process"),
517            ErrorKind::NotAFile => code.push_str("not_a_file"),
518            ErrorKind::AlreadyInUse => code.push_str("already_in_use"),
519            ErrorKind::FileNotFound => code.push_str("file_not_found"),
520            ErrorKind::DirectoryNotFound => code.push_str("directory_not_found"),
521        }
522
523        Some(Box::new(code))
524    }
525
526    fn help<'a>(&'a self) -> Option<Box<dyn std::fmt::Display + 'a>> {
527        let make_msg = |path: &Path| {
528            let path = format!("'{}'", path.display());
529            match self.kind {
530                ErrorKind::NotAFile => format!("{path} is not a file"),
531                ErrorKind::AlreadyInUse => {
532                    format!("{path} is already being used by another program")
533                }
534                ErrorKind::Std(std::io::ErrorKind::NotFound, _)
535                | ErrorKind::FileNotFound
536                | ErrorKind::DirectoryNotFound => format!("{path} does not exist"),
537                _ => format!("The error occurred at {path}"),
538            }
539        };
540
541        self.path
542            .as_ref()
543            .map(|path| make_msg(path))
544            .map(|s| Box::new(s) as Box<dyn std::fmt::Display>)
545    }
546
547    fn labels(&self) -> Option<Box<dyn Iterator<Item = miette::LabeledSpan> + '_>> {
548        let span_is_unknown = self.span == Span::unknown();
549        let span = match (span_is_unknown, self.location.as_ref()) {
550            (true, None) => return None,
551            (false, _) => SourceSpan::from(self.span),
552            (true, Some(location)) => SourceSpan::new(0.into(), location.len()),
553        };
554
555        let label = LabeledSpan::new_with_span(Some(self.kind.to_string()), span);
556        Some(Box::new(std::iter::once(label)))
557    }
558
559    fn diagnostic_source(&self) -> Option<&dyn Diagnostic> {
560        self.additional_context
561            .as_ref()
562            .map(|ctx| ctx as &dyn Diagnostic)
563    }
564
565    fn source_code(&self) -> Option<&dyn miette::SourceCode> {
566        let span_is_unknown = self.span == Span::unknown();
567        match (span_is_unknown, self.location.as_ref()) {
568            (true, None) | (false, _) => None,
569            (true, Some(location)) => Some(location as &dyn miette::SourceCode),
570        }
571    }
572}
573
574impl From<IoError> for std::io::Error {
575    fn from(value: IoError) -> Self {
576        Self::new(value.kind.into(), value)
577    }
578}
579
580impl From<ErrorKind> for std::io::ErrorKind {
581    fn from(value: ErrorKind) -> Self {
582        match value {
583            ErrorKind::Std(error_kind, _) => error_kind,
584            _ => std::io::ErrorKind::Other,
585        }
586    }
587}
588
589/// More specific variants of [`NotFound`](std::io::ErrorKind).
590///
591/// Use these to define how a `NotFound` error maps to our custom [`ErrorKind`].
592pub enum NotFound {
593    /// Map into [`FileNotFound`](ErrorKind::FileNotFound).
594    File,
595    /// Map into [`DirectoryNotFound`](ErrorKind::DirectoryNotFound).
596    Directory,
597}
598
599/// Extension trait for working with [`std::io::Error`].
600pub trait IoErrorExt {
601    /// Map [`NotFound`](std::io::ErrorKind) variants into more precise variants.
602    ///
603    /// The OS doesn't know when an entity was not found whether it was meant to be a file or a
604    /// directory or something else.
605    /// But sometimes we, the application, know what we expect and with this method, we can further
606    /// specify it.
607    ///
608    /// # Examples
609    /// Reading a file.
610    /// If the file isn't found, return [`FileNotFound`](ErrorKind::FileNotFound).
611    /// ```rust
612    /// # use nu_protocol::{
613    /// #     shell_error::io::{ErrorKind, IoErrorExt, IoError, NotFound},
614    /// #     ShellError, Span,
615    /// # };
616    /// # use std::{fs, path::PathBuf};
617    /// #
618    /// # fn example() -> Result<(), ShellError> {
619    /// #     let span = Span::test_data();
620    /// let a_file = PathBuf::from("scripts/ellie.nu");
621    /// let ellie = fs::read_to_string(&a_file).map_err(|err| {
622    ///     ShellError::Io(IoError::new(
623    ///         err.not_found_as(NotFound::File),
624    ///         span,
625    ///         a_file,
626    ///     ))
627    /// })?;
628    /// #     Ok(())
629    /// # }
630    /// #
631    /// # assert!(matches!(
632    /// #     example(),
633    /// #     Err(ShellError::Io(IoError {
634    /// #         kind: ErrorKind::FileNotFound,
635    /// #         ..
636    /// #     }))
637    /// # ));
638    /// ```
639    fn not_found_as(self, kind: NotFound) -> ErrorKind;
640}
641
642impl IoErrorExt for ErrorKind {
643    fn not_found_as(self, kind: NotFound) -> ErrorKind {
644        match (kind, self) {
645            (NotFound::File, Self::Std(std::io::ErrorKind::NotFound, _)) => ErrorKind::FileNotFound,
646            (NotFound::Directory, Self::Std(std::io::ErrorKind::NotFound, _)) => {
647                ErrorKind::DirectoryNotFound
648            }
649            _ => self,
650        }
651    }
652}
653
654impl IoErrorExt for std::io::Error {
655    fn not_found_as(self, kind: NotFound) -> ErrorKind {
656        ErrorKind::from(self).not_found_as(kind)
657    }
658}
659
660impl IoErrorExt for &std::io::Error {
661    fn not_found_as(self, kind: NotFound) -> ErrorKind {
662        ErrorKind::from(self).not_found_as(kind)
663    }
664}
665
666#[cfg(test)]
667mod assert_not_impl {
668    use super::*;
669
670    /// Assertion that `ErrorKind` does not implement `From<std::io::ErrorKind>`.
671    ///
672    /// This implementation exists only in tests to make sure that no crate,
673    /// including ours, accidentally adds a `From<std::io::ErrorKind>` impl for `ErrorKind`.
674    /// If someone tries, it will fail due to conflicting implementations.
675    ///
676    /// We want to force usage of [`IoError::new`] with a full [`std::io::Error`] instead of
677    /// allowing conversion from just an [`std::io::ErrorKind`].
678    /// That way, we can properly inspect and classify uncategorized I/O errors.
679    impl From<std::io::ErrorKind> for ErrorKind {
680        fn from(_: std::io::ErrorKind) -> Self {
681            unimplemented!("ErrorKind should not implement From<std::io::ErrorKind>")
682        }
683    }
684}