use miette::{Diagnostic, LabeledSpan, SourceSpan}; use std::{ fmt::Display, path::{Path, PathBuf}, }; use thiserror::Error; use crate::Span; use super::{location::Location, ShellError}; /// Represents an I/O error in the [`ShellError::Io`] variant. /// /// This is the central I/O error for the [`ShellError::Io`] variant. /// It represents all I/O errors by encapsulating [`ErrorKind`], an extension of /// [`std::io::ErrorKind`]. /// The `span` indicates where the error occurred in user-provided code. /// If the error is not tied to user-provided code, the `location` refers to the precise point in /// the Rust code where the error originated. /// The optional `path` provides the file or directory involved in the error. /// If [`ErrorKind`] alone doesn't provide enough detail, additional context can be added to clarify /// the issue. /// /// For handling user input errors (e.g., commands), prefer using [`new`](Self::new). /// Alternatively, use the [`factory`](Self::factory) method to simplify error creation in repeated /// contexts. /// For internal errors, use [`new_internal`](Self::new_internal) to include the location in Rust /// code where the error originated. /// /// # Examples /// /// ## User Input Error /// ```rust /// # use nu_protocol::shell_error::io::{IoError, ErrorKind}; /// # use nu_protocol::Span; /// use std::path::PathBuf; /// /// # let span = Span::test_data(); /// let path = PathBuf::from("/some/missing/file"); /// let error = IoError::new( /// std::io::ErrorKind::NotFound, /// span, /// path /// ); /// println!("Error: {:?}", error); /// ``` /// /// ## Internal Error /// ```rust /// # use nu_protocol::shell_error::io::{IoError, ErrorKind}; // # /// let error = IoError::new_internal( /// std::io::ErrorKind::UnexpectedEof, /// "Failed to read data from buffer", /// nu_protocol::location!() /// ); /// println!("Error: {:?}", error); /// ``` /// /// ## Using the Factory Method /// ```rust /// # use nu_protocol::shell_error::io::{IoError, ErrorKind}; /// # use nu_protocol::{Span, ShellError}; /// use std::path::PathBuf; /// /// # fn should_return_err() -> Result<(), ShellError> { /// # let span = Span::new(50, 60); /// let path = PathBuf::from("/some/file"); /// let from_io_error = IoError::factory(span, Some(path.as_path())); /// /// let content = std::fs::read_to_string(&path).map_err(from_io_error)?; /// # Ok(()) /// # } /// # /// # assert!(should_return_err().is_err()); /// ``` /// /// # ShellErrorBridge /// /// The [`ShellErrorBridge`](super::bridge::ShellErrorBridge) struct is used to contain a /// [`ShellError`] inside a [`std::io::Error`]. /// This allows seamless transfer of `ShellError` instances where `std::io::Error` is expected. /// When a `ShellError` needs to be packed into an I/O context, use this bridge. /// Similarly, when handling an I/O error that is expected to contain a `ShellError`, /// use the bridge to unpack it. /// /// This approach ensures clarity about where such container transfers occur. /// All other I/O errors should be handled using the provided constructors for `IoError`. /// This way, the code explicitly indicates when and where a `ShellError` transfer might happen. #[derive(Debug, Clone, Error, PartialEq)] #[non_exhaustive] #[error("I/O error")] pub struct IoError { /// The type of the underlying I/O error. /// /// [`std::io::ErrorKind`] provides detailed context about the type of I/O error that occurred /// and is part of [`std::io::Error`]. /// If a kind cannot be represented by it, consider adding a new variant to [`ErrorKind`]. /// /// Only in very rare cases should [`std::io::ErrorKind::Other`] be used, make sure you provide /// `additional_context` to get useful errors in these cases. pub kind: ErrorKind, /// The source location of the error. pub span: Span, /// The path related to the I/O error, if applicable. /// /// Many I/O errors involve a file or directory path, but operating system error messages /// often don't include the specific path. /// Setting this to [`Some`] allows users to see which path caused the error. pub path: Option, /// Additional details to provide more context about the error. /// /// Only set this field if it adds meaningful context. /// If [`ErrorKind`] already contains all the necessary information, leave this as [`None`]. pub additional_context: Option, /// The precise location in the Rust code where the error originated. /// /// This field is particularly useful for debugging errors that stem from the Rust /// implementation rather than user-provided Nushell code. /// The original [`Location`] is converted to a string to more easily report the error /// attributing the location. /// /// This value is only used if `span` is [`Span::unknown()`] as most of the time we want to /// refer to user code than the Rust code. pub location: Option, } #[derive(Debug, Clone, Copy, PartialEq, Eq, Diagnostic)] pub enum ErrorKind { Std(std::io::ErrorKind), // TODO: in Rust 1.83 this can be std::io::ErrorKind::NotADirectory NotADirectory, NotAFile, // TODO: in Rust 1.83 this can be std::io::ErrorKind::IsADirectory IsADirectory, } impl IoError { /// Creates a new [`IoError`] with the given kind, span, and optional path. /// /// This constructor should be used in all cases where the combination of the error kind, span, /// and path provides enough information to describe the error clearly. /// For example, errors like "File not found" or "Permission denied" are typically /// self-explanatory when paired with the file path and the location in user-provided /// Nushell code (`span`). /// /// # Constraints /// If `span` is unknown, use: /// - `new_internal` if no path is available. /// - `new_internal_with_path` if a path is available. pub fn new(kind: impl Into, span: Span, path: impl Into>) -> Self { let path = path.into(); if span == Span::unknown() { debug_assert!( path.is_some(), "for unknown spans with paths, use `new_internal_with_path`" ); debug_assert!( path.is_none(), "for unknown spans without paths, use `new_internal`" ); } Self { kind: kind.into(), span, path, additional_context: None, location: None, } } /// Creates a new [`IoError`] with additional context. /// /// Use this constructor when the error kind, span, and path are not sufficient to fully /// explain the error, and additional context can provide meaningful details. /// Avoid redundant context (e.g., "Permission denied" for an error kind of /// [`ErrorKind::PermissionDenied`](std::io::ErrorKind::PermissionDenied)). /// /// # Constraints /// If `span` is unknown, use: /// - `new_internal` if no path is available. /// - `new_internal_with_path` if a path is available. pub fn new_with_additional_context( kind: impl Into, span: Span, path: impl Into>, additional_context: impl ToString, ) -> Self { let path = path.into(); if span == Span::unknown() { debug_assert!( path.is_some(), "for unknown spans with paths, use `new_internal_with_path`" ); debug_assert!( path.is_none(), "for unknown spans without paths, use `new_internal`" ); } Self { kind: kind.into(), span, path, additional_context: Some(additional_context.to_string()), location: None, } } /// Creates a new [`IoError`] for internal I/O errors without a user-provided span or path. /// /// This constructor is intended for internal errors in the Rust implementation that still need /// to be reported to the end user. /// Since these errors are not tied to user-provided Nushell code, they generally have no /// meaningful span or path. /// /// Instead, these errors provide: /// - `additional_context`: /// Details about what went wrong internally. /// - `location`: /// The location in the Rust code where the error occurred, allowing us to trace and debug /// the issue. /// Use the [`nu_protocol::location!`](crate::location) macro to generate the location /// information. /// /// # Examples /// ```rust /// use nu_protocol::shell_error::io::IoError; /// /// let error = IoError::new_internal( /// std::io::ErrorKind::UnexpectedEof, /// "Failed to read from buffer", /// nu_protocol::location!(), /// ); /// ``` pub fn new_internal( kind: impl Into, additional_context: impl ToString, location: Location, ) -> Self { Self { kind: kind.into(), span: Span::unknown(), path: None, additional_context: Some(additional_context.to_string()), location: Some(location.to_string()), } } /// Creates a new `IoError` for internal I/O errors with a specific path. /// /// This constructor is similar to [`new_internal`] but also includes a file or directory /// path relevant to the error. Use this function in rare cases where an internal error /// involves a specific path, and the combination of path and additional context is helpful. /// /// # Examples /// ```rust /// use std::path::PathBuf; /// use nu_protocol::shell_error::io::IoError; /// /// let error = IoError::new_internal_with_path( /// std::io::ErrorKind::NotFound, /// "Could not find special file", /// nu_protocol::location!(), /// PathBuf::from("/some/file"), /// ); /// ``` pub fn new_internal_with_path( kind: impl Into, additional_context: impl ToString, location: Location, path: PathBuf, ) -> Self { Self { kind: kind.into(), span: Span::unknown(), path: path.into(), additional_context: Some(additional_context.to_string()), location: Some(location.to_string()), } } /// Creates a factory closure for constructing [`IoError`] instances from [`std::io::Error`] values. /// /// This method is particularly useful when you need to handle multiple I/O errors which all /// take the same span and path. /// Instead of calling `.map_err(|err| IoError::new(err.kind(), span, path))` every time, you /// can create the factory closure once and pass that into `.map_err`. pub fn factory<'p, P>(span: Span, path: P) -> impl Fn(std::io::Error) -> Self + use<'p, P> where P: Into>, { let path = path.into(); move |err: std::io::Error| IoError::new(err.kind(), span, path.map(PathBuf::from)) } } impl Display for ErrorKind { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { ErrorKind::Std(error_kind) => { let msg = error_kind.to_string(); let (first, rest) = msg.split_at(1); write!(f, "{}{}", first.to_uppercase(), rest) } ErrorKind::NotADirectory => write!(f, "Not a directory"), ErrorKind::NotAFile => write!(f, "Not a file"), ErrorKind::IsADirectory => write!(f, "Is a directory"), } } } impl std::error::Error for ErrorKind {} impl Diagnostic for IoError { fn code<'a>(&'a self) -> Option> { let mut code = String::from("nu::shell::io::"); match self.kind { ErrorKind::Std(error_kind) => match error_kind { std::io::ErrorKind::NotFound => code.push_str("not_found"), std::io::ErrorKind::PermissionDenied => code.push_str("permission_denied"), std::io::ErrorKind::ConnectionRefused => code.push_str("connection_refused"), std::io::ErrorKind::ConnectionReset => code.push_str("connection_reset"), std::io::ErrorKind::ConnectionAborted => code.push_str("connection_aborted"), std::io::ErrorKind::NotConnected => code.push_str("not_connected"), std::io::ErrorKind::AddrInUse => code.push_str("addr_in_use"), std::io::ErrorKind::AddrNotAvailable => code.push_str("addr_not_available"), std::io::ErrorKind::BrokenPipe => code.push_str("broken_pipe"), std::io::ErrorKind::AlreadyExists => code.push_str("already_exists"), std::io::ErrorKind::WouldBlock => code.push_str("would_block"), std::io::ErrorKind::InvalidInput => code.push_str("invalid_input"), std::io::ErrorKind::InvalidData => code.push_str("invalid_data"), std::io::ErrorKind::TimedOut => code.push_str("timed_out"), std::io::ErrorKind::WriteZero => code.push_str("write_zero"), std::io::ErrorKind::Interrupted => code.push_str("interrupted"), std::io::ErrorKind::Unsupported => code.push_str("unsupported"), std::io::ErrorKind::UnexpectedEof => code.push_str("unexpected_eof"), std::io::ErrorKind::OutOfMemory => code.push_str("out_of_memory"), std::io::ErrorKind::Other => code.push_str("other"), kind => code.push_str(&kind.to_string().to_lowercase().replace(" ", "_")), }, ErrorKind::NotADirectory => code.push_str("not_a_directory"), ErrorKind::NotAFile => code.push_str("not_a_file"), ErrorKind::IsADirectory => code.push_str("is_a_directory"), } Some(Box::new(code)) } fn help<'a>(&'a self) -> Option> { self.path .as_ref() .map(|path| format!("The error occurred at '{}'", path.display())) .map(|s| Box::new(s) as Box) } fn labels(&self) -> Option + '_>> { let span_is_unknown = self.span == Span::unknown(); let span = match (span_is_unknown, self.location.as_ref()) { (true, None) => return None, (false, _) => SourceSpan::from(self.span), (true, Some(location)) => SourceSpan::new(0.into(), location.len()), }; let label = match self.additional_context.as_ref() { Some(ctx) => format!("{ctx}\n{}", self.kind), None => self.kind.to_string(), }; let label = LabeledSpan::new_with_span(Some(label), span); Some(Box::new(std::iter::once(label))) } fn diagnostic_source(&self) -> Option<&dyn Diagnostic> { Some(&self.kind as &dyn Diagnostic) } fn source_code(&self) -> Option<&dyn miette::SourceCode> { let span_is_unknown = self.span == Span::unknown(); match (span_is_unknown, self.location.as_ref()) { (true, None) | (false, _) => None, (true, Some(location)) => Some(location as &dyn miette::SourceCode), } } } impl From for ShellError { fn from(value: IoError) -> Self { ShellError::Io(value) } } impl From for std::io::Error { fn from(value: IoError) -> Self { Self::new(value.kind.into(), value) } } impl From for ErrorKind { fn from(value: std::io::ErrorKind) -> Self { ErrorKind::Std(value) } } impl From for std::io::ErrorKind { fn from(value: ErrorKind) -> Self { match value { ErrorKind::Std(error_kind) => error_kind, _ => std::io::ErrorKind::Other, } } }