nu_protocol/errors/shell_error/
io.rs

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