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, ShellError};
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(std::io::ErrorKind::NotFound, 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///     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#[derive(Debug, Clone, Copy, PartialEq, Eq, Diagnostic)]
129pub enum ErrorKind {
130    Std(std::io::ErrorKind),
131    NotAFile,
132    // use these variants in cases where we know precisely whether a file or directory was expected
133    FileNotFound,
134    DirectoryNotFound,
135}
136
137#[derive(Debug, Clone, PartialEq, Eq, Error, Diagnostic)]
138#[error("{0}")]
139pub struct AdditionalContext(String);
140
141impl From<String> for AdditionalContext {
142    fn from(value: String) -> Self {
143        AdditionalContext(value)
144    }
145}
146
147impl IoError {
148    /// Creates a new [`IoError`] with the given kind, span, and optional path.
149    ///
150    /// This constructor should be used in all cases where the combination of the error kind, span,
151    /// and path provides enough information to describe the error clearly.
152    /// For example, errors like "File not found" or "Permission denied" are typically
153    /// self-explanatory when paired with the file path and the location in user-provided
154    /// Nushell code (`span`).
155    ///
156    /// # Constraints
157    /// If `span` is unknown, use:
158    /// - `new_internal` if no path is available.
159    /// - `new_internal_with_path` if a path is available.
160    pub fn new(kind: impl Into<ErrorKind>, span: Span, path: impl Into<Option<PathBuf>>) -> Self {
161        let path = path.into();
162
163        if span == Span::unknown() {
164            debug_assert!(
165                path.is_some(),
166                "for unknown spans with paths, use `new_internal_with_path`"
167            );
168            debug_assert!(
169                path.is_none(),
170                "for unknown spans without paths, use `new_internal`"
171            );
172        }
173
174        Self {
175            kind: kind.into(),
176            span,
177            path,
178            additional_context: None,
179            location: None,
180        }
181    }
182
183    /// Creates a new [`IoError`] with additional context.
184    ///
185    /// Use this constructor when the error kind, span, and path are not sufficient to fully
186    /// explain the error, and additional context can provide meaningful details.
187    /// Avoid redundant context (e.g., "Permission denied" for an error kind of
188    /// [`ErrorKind::PermissionDenied`](std::io::ErrorKind::PermissionDenied)).
189    ///
190    /// # Constraints
191    /// If `span` is unknown, use:
192    /// - `new_internal` if no path is available.
193    /// - `new_internal_with_path` if a path is available.
194    pub fn new_with_additional_context(
195        kind: impl Into<ErrorKind>,
196        span: Span,
197        path: impl Into<Option<PathBuf>>,
198        additional_context: impl ToString,
199    ) -> Self {
200        let path = path.into();
201
202        if span == Span::unknown() {
203            debug_assert!(
204                path.is_some(),
205                "for unknown spans with paths, use `new_internal_with_path`"
206            );
207            debug_assert!(
208                path.is_none(),
209                "for unknown spans without paths, use `new_internal`"
210            );
211        }
212
213        Self {
214            kind: kind.into(),
215            span,
216            path,
217            additional_context: Some(additional_context.to_string().into()),
218            location: None,
219        }
220    }
221
222    /// Creates a new [`IoError`] for internal I/O errors without a user-provided span or path.
223    ///
224    /// This constructor is intended for internal errors in the Rust implementation that still need
225    /// to be reported to the end user.
226    /// Since these errors are not tied to user-provided Nushell code, they generally have no
227    /// meaningful span or path.
228    ///
229    /// Instead, these errors provide:
230    /// - `additional_context`:
231    ///   Details about what went wrong internally.
232    /// - `location`:
233    ///   The location in the Rust code where the error occurred, allowing us to trace and debug
234    ///   the issue.
235    ///   Use the [`nu_protocol::location!`](crate::location) macro to generate the location
236    ///   information.
237    ///
238    /// # Examples
239    /// ```rust
240    /// use nu_protocol::shell_error::io::IoError;
241    ///
242    /// let error = IoError::new_internal(
243    ///     std::io::ErrorKind::UnexpectedEof,
244    ///     "Failed to read from buffer",
245    ///     nu_protocol::location!(),
246    /// );
247    /// ```
248    pub fn new_internal(
249        kind: impl Into<ErrorKind>,
250        additional_context: impl ToString,
251        location: Location,
252    ) -> Self {
253        Self {
254            kind: kind.into(),
255            span: Span::unknown(),
256            path: None,
257            additional_context: Some(additional_context.to_string().into()),
258            location: Some(location.to_string()),
259        }
260    }
261
262    /// Creates a new `IoError` for internal I/O errors with a specific path.
263    ///
264    /// This constructor is similar to [`new_internal`](Self::new_internal) but also includes a
265    /// file or directory path relevant to the error.
266    /// Use this function in rare cases where an internal error involves a specific path, and the
267    /// combination of path and additional context is helpful.
268    ///
269    /// # Examples
270    /// ```rust
271    /// use nu_protocol::shell_error::io::IoError;
272    /// use std::path::PathBuf;
273    ///
274    /// let error = IoError::new_internal_with_path(
275    ///     std::io::ErrorKind::NotFound,
276    ///     "Could not find special file",
277    ///     nu_protocol::location!(),
278    ///     PathBuf::from("/some/file"),
279    /// );
280    /// ```
281    pub fn new_internal_with_path(
282        kind: impl Into<ErrorKind>,
283        additional_context: impl ToString,
284        location: Location,
285        path: PathBuf,
286    ) -> Self {
287        Self {
288            kind: kind.into(),
289            span: Span::unknown(),
290            path: path.into(),
291            additional_context: Some(additional_context.to_string().into()),
292            location: Some(location.to_string()),
293        }
294    }
295
296    /// Creates a factory closure for constructing [`IoError`] instances from [`std::io::Error`] values.
297    ///
298    /// This method is particularly useful when you need to handle multiple I/O errors which all
299    /// take the same span and path.
300    /// Instead of calling `.map_err(|err| IoError::new(err.kind(), span, path))` every time, you
301    /// can create the factory closure once and pass that into `.map_err`.
302    pub fn factory<'p, P>(span: Span, path: P) -> impl Fn(std::io::Error) -> Self + use<'p, P>
303    where
304        P: Into<Option<&'p Path>>,
305    {
306        let path = path.into();
307        move |err: std::io::Error| IoError::new(err.kind(), span, path.map(PathBuf::from))
308    }
309}
310
311impl StdError for IoError {}
312impl Display for IoError {
313    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
314        match self.kind {
315            ErrorKind::Std(std::io::ErrorKind::NotFound) => write!(f, "Not found"),
316            ErrorKind::FileNotFound => write!(f, "File not found"),
317            ErrorKind::DirectoryNotFound => write!(f, "Directory not found"),
318            _ => write!(f, "I/O error"),
319        }
320    }
321}
322
323impl Display for ErrorKind {
324    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
325        match self {
326            ErrorKind::Std(std::io::ErrorKind::NotFound) => write!(f, "Not found"),
327            ErrorKind::Std(error_kind) => {
328                let msg = error_kind.to_string();
329                let (first, rest) = msg.split_at(1);
330                write!(f, "{}{}", first.to_uppercase(), rest)
331            }
332            ErrorKind::NotAFile => write!(f, "Not a file"),
333            ErrorKind::FileNotFound => write!(f, "File not found"),
334            ErrorKind::DirectoryNotFound => write!(f, "Directory not found"),
335        }
336    }
337}
338
339impl std::error::Error for ErrorKind {}
340
341impl Diagnostic for IoError {
342    fn code<'a>(&'a self) -> Option<Box<dyn std::fmt::Display + 'a>> {
343        let mut code = String::from("nu::shell::io::");
344        match self.kind {
345            ErrorKind::Std(error_kind) => match error_kind {
346                std::io::ErrorKind::NotFound => code.push_str("not_found"),
347                std::io::ErrorKind::PermissionDenied => code.push_str("permission_denied"),
348                std::io::ErrorKind::ConnectionRefused => code.push_str("connection_refused"),
349                std::io::ErrorKind::ConnectionReset => code.push_str("connection_reset"),
350                std::io::ErrorKind::ConnectionAborted => code.push_str("connection_aborted"),
351                std::io::ErrorKind::NotConnected => code.push_str("not_connected"),
352                std::io::ErrorKind::AddrInUse => code.push_str("addr_in_use"),
353                std::io::ErrorKind::AddrNotAvailable => code.push_str("addr_not_available"),
354                std::io::ErrorKind::BrokenPipe => code.push_str("broken_pipe"),
355                std::io::ErrorKind::AlreadyExists => code.push_str("already_exists"),
356                std::io::ErrorKind::WouldBlock => code.push_str("would_block"),
357                std::io::ErrorKind::InvalidInput => code.push_str("invalid_input"),
358                std::io::ErrorKind::InvalidData => code.push_str("invalid_data"),
359                std::io::ErrorKind::TimedOut => code.push_str("timed_out"),
360                std::io::ErrorKind::WriteZero => code.push_str("write_zero"),
361                std::io::ErrorKind::Interrupted => code.push_str("interrupted"),
362                std::io::ErrorKind::Unsupported => code.push_str("unsupported"),
363                std::io::ErrorKind::UnexpectedEof => code.push_str("unexpected_eof"),
364                std::io::ErrorKind::OutOfMemory => code.push_str("out_of_memory"),
365                std::io::ErrorKind::Other => code.push_str("other"),
366                kind => code.push_str(&kind.to_string().to_lowercase().replace(" ", "_")),
367            },
368            ErrorKind::NotAFile => code.push_str("not_a_file"),
369            ErrorKind::FileNotFound => code.push_str("file_not_found"),
370            ErrorKind::DirectoryNotFound => code.push_str("directory_not_found"),
371        }
372
373        Some(Box::new(code))
374    }
375
376    fn help<'a>(&'a self) -> Option<Box<dyn std::fmt::Display + 'a>> {
377        let make_msg = |path: &Path| {
378            let path = format!("'{}'", path.display());
379            match self.kind {
380                ErrorKind::NotAFile => format!("{path} is not a file"),
381                ErrorKind::Std(std::io::ErrorKind::NotFound)
382                | ErrorKind::FileNotFound
383                | ErrorKind::DirectoryNotFound => format!("{path} does not exist"),
384                _ => format!("The error occurred at {path}"),
385            }
386        };
387
388        self.path
389            .as_ref()
390            .map(|path| make_msg(path))
391            .map(|s| Box::new(s) as Box<dyn std::fmt::Display>)
392    }
393
394    fn labels(&self) -> Option<Box<dyn Iterator<Item = miette::LabeledSpan> + '_>> {
395        let span_is_unknown = self.span == Span::unknown();
396        let span = match (span_is_unknown, self.location.as_ref()) {
397            (true, None) => return None,
398            (false, _) => SourceSpan::from(self.span),
399            (true, Some(location)) => SourceSpan::new(0.into(), location.len()),
400        };
401
402        let label = LabeledSpan::new_with_span(Some(self.kind.to_string()), span);
403        Some(Box::new(std::iter::once(label)))
404    }
405
406    fn diagnostic_source(&self) -> Option<&dyn Diagnostic> {
407        self.additional_context
408            .as_ref()
409            .map(|ctx| ctx as &dyn Diagnostic)
410    }
411
412    fn source_code(&self) -> Option<&dyn miette::SourceCode> {
413        let span_is_unknown = self.span == Span::unknown();
414        match (span_is_unknown, self.location.as_ref()) {
415            (true, None) | (false, _) => None,
416            (true, Some(location)) => Some(location as &dyn miette::SourceCode),
417        }
418    }
419}
420
421impl From<IoError> for ShellError {
422    fn from(value: IoError) -> Self {
423        ShellError::Io(value)
424    }
425}
426
427impl From<IoError> for std::io::Error {
428    fn from(value: IoError) -> Self {
429        Self::new(value.kind.into(), value)
430    }
431}
432
433impl From<std::io::ErrorKind> for ErrorKind {
434    fn from(value: std::io::ErrorKind) -> Self {
435        ErrorKind::Std(value)
436    }
437}
438
439impl From<ErrorKind> for std::io::ErrorKind {
440    fn from(value: ErrorKind) -> Self {
441        match value {
442            ErrorKind::Std(error_kind) => error_kind,
443            _ => std::io::ErrorKind::Other,
444        }
445    }
446}
447
448/// More specific variants of [`NotFound`](std::io::ErrorKind).
449///
450/// Use these to define how a `NotFound` error maps to our custom [`ErrorKind`].
451pub enum NotFound {
452    /// Map into [`FileNotFound`](ErrorKind::FileNotFound).
453    File,
454    /// Map into [`DirectoryNotFound`](ErrorKind::DirectoryNotFound).
455    Directory,
456}
457
458/// Extension trait for working with [`std::io::ErrorKind`].
459pub trait ErrorKindExt {
460    /// Map [`NotFound`](std::io::ErrorKind) variants into more precise variants.
461    ///
462    /// The OS doesn't know when an entity was not found whether it was meant to be a file or a
463    /// directory or something else.
464    /// But sometimes we, the application, know what we expect and with this method, we can further
465    /// specify it.
466    ///
467    /// # Examples
468    /// Reading a file.
469    /// If the file isn't found, return [`FileNotFound`](ErrorKind::FileNotFound).
470    /// ```rust
471    /// # use nu_protocol::{
472    /// #     shell_error::io::{ErrorKind, ErrorKindExt, IoError, NotFound},
473    /// #     ShellError, Span,
474    /// # };
475    /// # use std::{fs, path::PathBuf};
476    /// #
477    /// # fn example() -> Result<(), ShellError> {
478    /// #     let span = Span::test_data();
479    /// let a_file = PathBuf::from("scripts/ellie.nu");
480    /// let ellie = fs::read_to_string(&a_file).map_err(|err| {
481    ///     ShellError::Io(IoError::new(
482    ///         err.kind().not_found_as(NotFound::File),
483    ///         span,
484    ///         a_file,
485    ///     ))
486    /// })?;
487    /// #     Ok(())
488    /// # }
489    /// #
490    /// # assert!(matches!(
491    /// #     example(),
492    /// #     Err(ShellError::Io(IoError {
493    /// #         kind: ErrorKind::FileNotFound,
494    /// #         ..
495    /// #     }))
496    /// # ));
497    /// ```
498    fn not_found_as(self, kind: NotFound) -> ErrorKind;
499}
500
501impl ErrorKindExt for std::io::ErrorKind {
502    fn not_found_as(self, kind: NotFound) -> ErrorKind {
503        match (kind, self) {
504            (NotFound::File, Self::NotFound) => ErrorKind::FileNotFound,
505            (NotFound::Directory, Self::NotFound) => ErrorKind::DirectoryNotFound,
506            _ => ErrorKind::Std(self),
507        }
508    }
509}
510
511impl ErrorKindExt for ErrorKind {
512    fn not_found_as(self, kind: NotFound) -> ErrorKind {
513        match self {
514            Self::Std(std_kind) => std_kind.not_found_as(kind),
515            _ => self,
516        }
517    }
518}