#[cfg(doc)] use super::ShellError;
use miette::{Diagnostic, LabeledSpan, SourceSpan};
use std::{
error::Error as StdError,
fmt::{self, Display, Formatter},
panic::Location,
path::{Path, PathBuf},
};
use thiserror::Error;
use crate::Span;
pub type Result<T, E = ErrorKind> = std::result::Result<T, E>;
#[derive(Debug, Eq, Clone, PartialEq)]
#[non_exhaustive]
pub struct IoError {
pub kind: ErrorKind,
pub span: Span,
pub path: Option<PathBuf>,
pub additional_context: Option<AdditionalContext>,
pub location: Option<String>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
struct Sealed;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Diagnostic)]
pub enum ErrorKind {
#[allow(private_interfaces)]
Std(std::io::ErrorKind, Sealed),
KillJobProcess,
NotAFile,
#[cfg_attr(not(windows), allow(rustdoc::broken_intra_doc_links))]
AlreadyInUse,
FileNotFound,
DirectoryNotFound,
}
impl ErrorKind {
pub fn from_std(kind: std::io::ErrorKind) -> Self {
Self::Std(kind, Sealed)
}
}
#[derive(Debug, Clone, PartialEq, Eq, Error, Diagnostic)]
#[error("{0}")]
pub struct AdditionalContext(String);
impl From<String> for AdditionalContext {
fn from(value: String) -> Self {
AdditionalContext(value)
}
}
impl IoError {
pub fn new(kind: impl Into<ErrorKind>, span: Span, path: impl Into<Option<PathBuf>>) -> 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,
}
}
pub fn new_with_additional_context(
kind: impl Into<ErrorKind>,
span: Span,
path: impl Into<Option<PathBuf>>,
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().into()),
location: None,
}
}
#[track_caller]
pub fn new_internal(kind: impl Into<ErrorKind>, additional_context: impl ToString) -> Self {
Self {
kind: kind.into(),
span: Span::unknown(),
path: None,
additional_context: Some(additional_context.to_string().into()),
location: Some(Location::caller().to_string()),
}
}
pub fn new_internal_with_location(
kind: impl Into<ErrorKind>,
additional_context: impl ToString,
location: &Location<'_>,
) -> Self {
Self {
kind: kind.into(),
span: Span::unknown(),
path: None,
additional_context: Some(additional_context.to_string().into()),
location: Some(location.to_string()),
}
}
#[track_caller]
pub fn new_internal_with_path(
kind: impl Into<ErrorKind>,
additional_context: impl ToString,
path: PathBuf,
) -> Self {
Self {
kind: kind.into(),
span: Span::unknown(),
path: path.into(),
additional_context: Some(additional_context.to_string().into()),
location: Some(Location::caller().to_string()),
}
}
pub fn new_internal_with_path_and_location(
kind: impl Into<ErrorKind>,
additional_context: impl ToString,
path: PathBuf,
location: &Location<'_>,
) -> Self {
Self {
kind: kind.into(),
span: Span::unknown(),
path: path.into(),
additional_context: Some(additional_context.to_string().into()),
location: Some(location.to_string()),
}
}
pub fn factory<'p, P>(span: Span, path: P) -> impl Fn(std::io::Error) -> Self + use<'p, P>
where
P: Into<Option<&'p Path>>,
{
let path = path.into();
move |err: std::io::Error| IoError::new(err, span, path.map(PathBuf::from))
}
}
impl From<std::io::Error> for ErrorKind {
fn from(err: std::io::Error) -> Self {
(&err).into()
}
}
impl From<&std::io::Error> for ErrorKind {
fn from(err: &std::io::Error) -> Self {
#[cfg(windows)]
if let Some(raw_os_error) = err.raw_os_error() {
use windows::Win32::Foundation;
#[allow(clippy::single_match, reason = "in the future we can expand here")]
match Foundation::WIN32_ERROR(raw_os_error as u32) {
Foundation::ERROR_SHARING_VIOLATION => return ErrorKind::AlreadyInUse,
_ => {}
}
}
#[cfg(debug_assertions)]
if err.kind() == std::io::ErrorKind::Other {
panic!(
"\
suspicious conversion:
tried to convert `std::io::Error` with `std::io::ErrorKind::Other`
into `nu_protocol::shell_error::io::ErrorKind`
I/O errors should always be specific, provide more context
{err:#?}\
"
)
}
ErrorKind::Std(err.kind(), Sealed)
}
}
impl From<nu_system::KillByPidError> for ErrorKind {
fn from(value: nu_system::KillByPidError) -> Self {
match value {
nu_system::KillByPidError::Output(error) => error.into(),
nu_system::KillByPidError::KillProcess => ErrorKind::KillJobProcess,
}
}
}
impl StdError for IoError {}
impl Display for IoError {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
match self.kind {
ErrorKind::Std(std::io::ErrorKind::NotFound, _) => write!(f, "Not found"),
ErrorKind::FileNotFound => write!(f, "File not found"),
ErrorKind::DirectoryNotFound => write!(f, "Directory not found"),
_ => write!(f, "I/O error"),
}
}
}
impl Display for ErrorKind {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
match self {
ErrorKind::Std(std::io::ErrorKind::NotFound, _) => write!(f, "Not found"),
ErrorKind::Std(error_kind, _) => {
let msg = error_kind.to_string();
let (first, rest) = msg.split_at(1);
write!(f, "{}{}", first.to_uppercase(), rest)
}
ErrorKind::KillJobProcess => write!(f, "Killing job process failed"),
ErrorKind::NotAFile => write!(f, "Not a file"),
ErrorKind::AlreadyInUse => write!(f, "Already in use"),
ErrorKind::FileNotFound => write!(f, "File not found"),
ErrorKind::DirectoryNotFound => write!(f, "Directory not found"),
}
}
}
impl std::error::Error for ErrorKind {}
impl Diagnostic for IoError {
fn code<'a>(&'a self) -> Option<Box<dyn std::fmt::Display + 'a>> {
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::KillJobProcess => code.push_str("kill_job_process"),
ErrorKind::NotAFile => code.push_str("not_a_file"),
ErrorKind::AlreadyInUse => code.push_str("already_in_use"),
ErrorKind::FileNotFound => code.push_str("file_not_found"),
ErrorKind::DirectoryNotFound => code.push_str("directory_not_found"),
}
Some(Box::new(code))
}
fn help<'a>(&'a self) -> Option<Box<dyn std::fmt::Display + 'a>> {
let make_msg = |path: &Path| {
let path = format!("'{}'", path.display());
match self.kind {
ErrorKind::NotAFile => format!("{path} is not a file"),
ErrorKind::AlreadyInUse => {
format!("{path} is already being used by another program")
}
ErrorKind::Std(std::io::ErrorKind::NotFound, _)
| ErrorKind::FileNotFound
| ErrorKind::DirectoryNotFound => format!("{path} does not exist"),
_ => format!("The error occurred at {path}"),
}
};
self.path
.as_ref()
.map(|path| make_msg(path))
.map(|s| Box::new(s) as Box<dyn std::fmt::Display>)
}
fn labels(&self) -> Option<Box<dyn Iterator<Item = miette::LabeledSpan> + '_>> {
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 = LabeledSpan::new_with_span(Some(self.kind.to_string()), span);
Some(Box::new(std::iter::once(label)))
}
fn diagnostic_source(&self) -> Option<&dyn Diagnostic> {
self.additional_context
.as_ref()
.map(|ctx| ctx 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<IoError> for std::io::Error {
fn from(value: IoError) -> Self {
Self::new(value.kind.into(), value)
}
}
impl From<ErrorKind> for std::io::ErrorKind {
fn from(value: ErrorKind) -> Self {
match value {
ErrorKind::Std(error_kind, _) => error_kind,
_ => std::io::ErrorKind::Other,
}
}
}
pub enum NotFound {
File,
Directory,
}
pub trait IoErrorExt {
fn not_found_as(self, kind: NotFound) -> ErrorKind;
}
impl IoErrorExt for ErrorKind {
fn not_found_as(self, kind: NotFound) -> ErrorKind {
match (kind, self) {
(NotFound::File, Self::Std(std::io::ErrorKind::NotFound, _)) => ErrorKind::FileNotFound,
(NotFound::Directory, Self::Std(std::io::ErrorKind::NotFound, _)) => {
ErrorKind::DirectoryNotFound
}
_ => self,
}
}
}
impl IoErrorExt for std::io::Error {
fn not_found_as(self, kind: NotFound) -> ErrorKind {
ErrorKind::from(self).not_found_as(kind)
}
}
impl IoErrorExt for &std::io::Error {
fn not_found_as(self, kind: NotFound) -> ErrorKind {
ErrorKind::from(self).not_found_as(kind)
}
}
#[cfg(test)]
mod assert_not_impl {
use super::*;
impl From<std::io::ErrorKind> for ErrorKind {
fn from(_: std::io::ErrorKind) -> Self {
unimplemented!("ErrorKind should not implement From<std::io::ErrorKind>")
}
}
}