use std::borrow::Borrow; use std::borrow::Cow; use std::error; use std::error::Error as _; use std::fmt::Debug; use std::fmt::Display; use std::fmt::Formatter; use std::fmt::Result as FmtResult; use std::io; use std::mem::transmute; use std::ops::Deref; use std::result; /// A result type using our [`Error`] by default. pub type Result = result::Result; #[allow(clippy::wildcard_imports)] mod private { use super::*; pub trait Sealed {} impl Sealed for Option {} impl Sealed for Result {} impl Sealed for &'static str {} impl Sealed for String {} impl Sealed for Error {} impl Sealed for io::Error {} } /// A `str` replacement whose owned representation is a `Box` and /// not a `String`. #[derive(Debug)] #[repr(transparent)] #[doc(hidden)] pub struct Str(str); impl ToOwned for Str { type Owned = Box; #[inline] fn to_owned(&self) -> Self::Owned { self.0.to_string().into_boxed_str() } } impl Borrow for Box { #[inline] fn borrow(&self) -> &Str { // SAFETY: `Str` is `repr(transparent)` and so `&str` and `&Str` // can trivially be converted into each other. unsafe { transmute::<&str, &Str>(self.deref()) } } } impl Deref for Str { type Target = str; fn deref(&self) -> &Self::Target { &self.0 } } // For convenient use in `format!`, for example. impl Display for Str { #[inline] fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult { Display::fmt(&self.0, f) } } /// A helper trait to abstracting over various string types, allowing /// for conversion into a `Cow<'static, Str>`. This is the `Cow` enabled /// equivalent of `ToString`. pub trait IntoCowStr: private::Sealed { fn into_cow_str(self) -> Cow<'static, Str>; } impl IntoCowStr for &'static str { fn into_cow_str(self) -> Cow<'static, Str> { // SAFETY: `Str` is `repr(transparent)` and so `&str` and `&Str` // can trivially be converted into each other. let other = unsafe { transmute::<&str, &Str>(self) }; Cow::Borrowed(other) } } impl IntoCowStr for String { fn into_cow_str(self) -> Cow<'static, Str> { Cow::Owned(self.into_boxed_str()) } } // TODO: We may want to support optionally storing a backtrace in // terminal variants. enum ErrorImpl { Io(io::Error), // Unfortunately, if we just had a single `Context` variant that // contains a `Cow`, this inner `Cow` would cause an overall enum // size increase by a machine word, because currently `rustc` // seemingly does not fold the necessary bits into the outer enum. // We have two variants to work around that until `rustc` is smart // enough. ContextOwned { context: Box, source: Box, }, ContextStatic { context: &'static str, source: Box, }, } impl ErrorImpl { fn kind(&self) -> ErrorKind { match self { Self::Io(error) => match error.kind() { io::ErrorKind::NotFound => ErrorKind::NotFound, io::ErrorKind::PermissionDenied => ErrorKind::PermissionDenied, io::ErrorKind::AlreadyExists => ErrorKind::AlreadyExists, io::ErrorKind::WouldBlock => ErrorKind::WouldBlock, io::ErrorKind::InvalidInput => ErrorKind::InvalidInput, io::ErrorKind::InvalidData => ErrorKind::InvalidData, io::ErrorKind::TimedOut => ErrorKind::TimedOut, io::ErrorKind::WriteZero => ErrorKind::WriteZero, io::ErrorKind::Interrupted => ErrorKind::Interrupted, io::ErrorKind::Unsupported => ErrorKind::Unsupported, io::ErrorKind::UnexpectedEof => ErrorKind::UnexpectedEof, io::ErrorKind::OutOfMemory => ErrorKind::OutOfMemory, _ => ErrorKind::Other, }, Self::ContextOwned { source, .. } | Self::ContextStatic { source, .. } => { source.deref().kind() } } } #[cfg(test)] fn is_owned(&self) -> Option { match self { Self::ContextOwned { .. } => Some(true), Self::ContextStatic { .. } => Some(false), _ => None, } } } impl Debug for ErrorImpl { // We try to mirror roughly how anyhow's Error is behaving, because // that makes the most sense. fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult { if f.alternate() { let mut dbg; match self { Self::Io(io) => { dbg = f.debug_tuple(stringify!(Io)); dbg.field(io) } Self::ContextOwned { context, .. } => { dbg = f.debug_tuple(stringify!(ContextOwned)); dbg.field(context) } Self::ContextStatic { context, .. } => { dbg = f.debug_tuple(stringify!(ContextStatic)); dbg.field(context) } } .finish() } else { let () = match self { Self::Io(error) => write!(f, "Error: {error}")?, Self::ContextOwned { context, .. } => write!(f, "Error: {context}")?, Self::ContextStatic { context, .. } => write!(f, "Error: {context}")?, }; if let Some(source) = self.source() { let () = f.write_str("\n\nCaused by:")?; let mut error = Some(source); while let Some(err) = error { let () = write!(f, "\n {err:}")?; error = err.source(); } } Ok(()) } } } impl Display for ErrorImpl { fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult { let () = match self { Self::Io(error) => Display::fmt(error, f)?, Self::ContextOwned { context, .. } => Display::fmt(context, f)?, Self::ContextStatic { context, .. } => Display::fmt(context, f)?, }; if f.alternate() { let mut error = self.source(); while let Some(err) = error { let () = write!(f, ": {err}")?; error = err.source(); } } Ok(()) } } impl error::Error for ErrorImpl { fn source(&self) -> Option<&(dyn error::Error + 'static)> { match self { Self::Io(error) => error.source(), Self::ContextOwned { source, .. } | Self::ContextStatic { source, .. } => Some(source), } } } /// An enum providing a rough classification of errors. /// /// The variants of this type partly resemble those of /// [`std::io::Error`], because these are the most common sources of /// error that the crate concerns itself with. #[derive(Clone, Copy, Debug, PartialEq)] #[non_exhaustive] pub enum ErrorKind { /// An entity was not found, often a file. NotFound, /// The operation lacked the necessary privileges to complete. PermissionDenied, /// An entity already exists, often a file. AlreadyExists, /// The operation needs to block to complete, but the blocking /// operation was requested to not occur. WouldBlock, /// A parameter was incorrect. InvalidInput, /// Data not valid for the operation were encountered. InvalidData, /// The I/O operation's timeout expired, causing it to be canceled. TimedOut, /// An error returned when an operation could not be completed /// because a call to [`write`] returned [`Ok(0)`]. WriteZero, /// This operation was interrupted. /// /// Interrupted operations can typically be retried. Interrupted, /// This operation is unsupported on this platform. Unsupported, /// An error returned when an operation could not be completed /// because an "end of file" was reached prematurely. UnexpectedEof, /// An operation could not be completed, because it failed /// to allocate enough memory. OutOfMemory, /// A custom error that does not fall under any other I/O error /// kind. Other, } /// The error type used by the library. /// /// Errors generally form a chain, with higher-level errors typically /// providing additional context for lower level ones. E.g., an IO error /// such as file-not-found could be reported by a system level API (such /// as [`std::fs::File::open`]) and may be contextualized with the path /// to the file attempted to be opened. /// /// ``` /// use std::fs::File; /// use std::error::Error as _; /// # use libbpf_rs::ErrorExt as _; /// /// let path = "/does-not-exist"; /// let result = File::open(path).with_context(|| format!("failed to open {path}")); /// /// let err = result.unwrap_err(); /// assert_eq!(err.to_string(), "failed to open /does-not-exist"); /// /// // Retrieve the underlying error. /// let inner_err = err.source().unwrap(); /// assert!(inner_err.to_string().starts_with("No such file or directory")); /// ``` /// /// For convenient reporting, the [`Display`][std::fmt::Display] /// representation takes care of reporting the complete error chain when /// the alternate flag is set: /// ``` /// # use std::fs::File; /// # use std::error::Error as _; /// # use libbpf_rs::ErrorExt as _; /// # let path = "/does-not-exist"; /// # let result = File::open(path).with_context(|| format!("failed to open {path}")); /// # let err = result.unwrap_err(); /// // > failed to open /does-not-exist: No such file or directory (os error 2) /// println!("{err:#}"); /// ``` /// /// The [`Debug`][std::fmt::Debug] representation similarly will print /// the entire error chain, but will do so in a multi-line format: /// ``` /// # use std::fs::File; /// # use std::error::Error as _; /// # use libbpf_rs::ErrorExt as _; /// # let path = "/does-not-exist"; /// # let result = File::open(path).with_context(|| format!("failed to open {path}")); /// # let err = result.unwrap_err(); /// // > Error: failed to open /does-not-exist /// // > /// // > Caused by: /// // > No such file or directory (os error 2) /// println!("{err:?}"); /// ``` // Representation is optimized for fast copying (a single machine word), // not so much for fast creation (as it is heap allocated). We generally // expect errors to be exceptional, though a lot of functionality is // fallible (i.e., returns a `Result` which would be penalized // by a large `Err` variant). #[repr(transparent)] pub struct Error { /// The top-most error of the chain. error: Box, } impl Error { /// Create an [`Error`] from an OS error code (typically `errno`). /// /// # Notes /// An OS error code should always be positive. #[inline] pub fn from_raw_os_error(code: i32) -> Self { debug_assert!( code > 0, "OS error code should be positive integer; got: {code}" ); Self::from(io::Error::from_raw_os_error(code)) } #[inline] pub(crate) fn with_io_error(kind: io::ErrorKind, error: E) -> Self where E: ToString, { Self::from(io::Error::new(kind, error.to_string())) } #[inline] pub(crate) fn with_invalid_data(error: E) -> Self where E: ToString, { Self::with_io_error(io::ErrorKind::InvalidData, error) } /// Retrieve a rough error classification in the form of an /// [`ErrorKind`]. #[inline] pub fn kind(&self) -> ErrorKind { self.error.kind() } /// Layer the provided context on top of this `Error`, creating a /// new one in the process. fn layer_context(self, context: Cow<'static, Str>) -> Self { match context { Cow::Owned(context) => Self { error: Box::new(ErrorImpl::ContextOwned { context, source: self.error, }), }, Cow::Borrowed(context) => Self { error: Box::new(ErrorImpl::ContextStatic { context, source: self.error, }), }, } } } impl Debug for Error { #[inline] fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult { Debug::fmt(&self.error, f) } } impl Display for Error { #[inline] fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult { Display::fmt(&self.error, f) } } impl error::Error for Error { #[inline] fn source(&self) -> Option<&(dyn error::Error + 'static)> { self.error.source() } } impl From for Error { fn from(other: io::Error) -> Self { Self { error: Box::new(ErrorImpl::Io(other)), } } } /// A trait providing ergonomic chaining capabilities to [`Error`]. pub trait ErrorExt: private::Sealed { /// The output type produced by [`context`](Self::context) and /// [`with_context`](Self::with_context). type Output; /// Add context to this error. // If we had specialization of sorts we could be more lenient as to // what we can accept, but for now this method always works with // static strings and nothing else. fn context(self, context: C) -> Self::Output where C: IntoCowStr; /// Add context to this error, using a closure for lazy evaluation. fn with_context(self, f: F) -> Self::Output where C: IntoCowStr, F: FnOnce() -> C; } impl ErrorExt for Error { type Output = Error; fn context(self, context: C) -> Self::Output where C: IntoCowStr, { self.layer_context(context.into_cow_str()) } fn with_context(self, f: F) -> Self::Output where C: IntoCowStr, F: FnOnce() -> C, { self.layer_context(f().into_cow_str()) } } impl ErrorExt for Result where E: ErrorExt, { type Output = Result; fn context(self, context: C) -> Self::Output where C: IntoCowStr, { match self { Ok(val) => Ok(val), Err(err) => Err(err.context(context)), } } fn with_context(self, f: F) -> Self::Output where C: IntoCowStr, F: FnOnce() -> C, { match self { Ok(val) => Ok(val), Err(err) => Err(err.with_context(f)), } } } impl ErrorExt for io::Error { type Output = Error; fn context(self, context: C) -> Self::Output where C: IntoCowStr, { Error::from(self).context(context) } fn with_context(self, f: F) -> Self::Output where C: IntoCowStr, F: FnOnce() -> C, { Error::from(self).with_context(f) } } /// A trait providing conversion shortcuts for creating `Error` /// instances. pub trait IntoError: private::Sealed where Self: Sized, { fn ok_or_error(self, kind: io::ErrorKind, f: F) -> Result where C: ToString, F: FnOnce() -> C; #[inline] fn ok_or_invalid_data(self, f: F) -> Result where C: ToString, F: FnOnce() -> C, { self.ok_or_error(io::ErrorKind::InvalidData, f) } } impl IntoError for Option { #[inline] fn ok_or_error(self, kind: io::ErrorKind, f: F) -> Result where C: ToString, F: FnOnce() -> C, { self.ok_or_else(|| Error::with_io_error(kind, f().to_string())) } } #[cfg(test)] mod tests { use super::*; use std::mem::size_of; /// Check various features of our `Str` wrapper type. #[test] fn str_wrapper() { let b = "test string".to_string().into_boxed_str(); let s: &Str = b.borrow(); let _b: Box = s.to_owned(); assert_eq!(s.to_string(), b.deref()); assert_eq!(format!("{s:?}"), "Str(\"test string\")"); } /// Check that our `Error` type's size is as expected. #[test] fn error_size() { assert_eq!(size_of::(), size_of::()); assert_eq!(size_of::(), 4 * size_of::()); } /// Check that we can format errors as expected. #[test] fn error_formatting() { let err = io::Error::new(io::ErrorKind::InvalidData, "some invalid data"); let err = Error::from(err); let src = err.source(); assert!(src.is_none(), "{src:?}"); assert!(err.error.is_owned().is_none()); assert_eq!(err.kind(), ErrorKind::InvalidData); assert_eq!(format!("{err}"), "some invalid data"); assert_eq!(format!("{err:#}"), "some invalid data"); assert_eq!(format!("{err:?}"), "Error: some invalid data"); // TODO: The inner format may not actually be all that stable. let expected = r#"Io( Custom { kind: InvalidData, error: "some invalid data", }, )"#; assert_eq!(format!("{err:#?}"), expected); let err = err.context("inner context"); let src = err.source(); assert!(src.is_some(), "{src:?}"); assert!(!err.error.is_owned().unwrap()); assert_eq!(err.kind(), ErrorKind::InvalidData); assert_eq!(format!("{err}"), "inner context"); assert_eq!(format!("{err:#}"), "inner context: some invalid data"); let expected = r#"Error: inner context Caused by: some invalid data"#; assert_eq!(format!("{err:?}"), expected); // Nope, not going to bother. assert_ne!(format!("{err:#?}"), ""); let err = err.context("outer context".to_string()); let src = err.source(); assert!(src.is_some(), "{src:?}"); assert!(err.error.is_owned().unwrap()); assert_eq!(err.kind(), ErrorKind::InvalidData); assert_eq!(format!("{err}"), "outer context"); assert_eq!( format!("{err:#}"), "outer context: inner context: some invalid data" ); let expected = r#"Error: outer context Caused by: inner context some invalid data"#; assert_eq!(format!("{err:?}"), expected); assert_ne!(format!("{err:#?}"), ""); } }