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}