use std::{
borrow::Cow,
fmt::{
Debug,
Display,
},
hash::Hash,
};
use either::Either;
use nix::errno::Errno;
use owo_colors::OwoColorize;
use serde::Serialize;
use crate::{
cache::ArcStr,
cli,
proc::cached_string,
};
#[cfg(feature = "ebpf")]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
#[repr(u8)]
pub enum BpfError {
Dropped,
Flags,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[repr(u64)]
pub enum FriendlyError {
InspectError(Errno),
#[cfg(feature = "ebpf")]
Bpf(BpfError),
}
impl PartialOrd for FriendlyError {
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
Some(Ord::cmp(self, other))
}
}
impl Ord for FriendlyError {
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
match (self, other) {
(Self::InspectError(a), Self::InspectError(b)) => (*a as i32).cmp(&(*b as i32)),
#[cfg(feature = "ebpf")]
(Self::Bpf(a), Self::Bpf(b)) => a.cmp(b),
#[cfg(feature = "ebpf")]
(Self::InspectError(_), Self::Bpf(_)) => std::cmp::Ordering::Less,
#[cfg(feature = "ebpf")]
(Self::Bpf(_), Self::InspectError(_)) => std::cmp::Ordering::Greater,
}
}
}
impl Hash for FriendlyError {
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
core::mem::discriminant(self).hash(state);
match self {
Self::InspectError(e) => (*e as i32).hash(state),
#[cfg(feature = "ebpf")]
Self::Bpf(e) => e.hash(state),
}
}
}
#[cfg(feature = "ebpf")]
impl From<BpfError> for FriendlyError {
fn from(value: BpfError) -> Self {
Self::Bpf(value)
}
}
impl From<&FriendlyError> for &'static str {
fn from(value: &FriendlyError) -> Self {
match value {
FriendlyError::InspectError(_) => "[err: failed to inspect]",
#[cfg(feature = "ebpf")]
FriendlyError::Bpf(_) => "[err: bpf error]",
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub enum OutputMsg {
Ok(ArcStr),
PartialOk(ArcStr),
Err(FriendlyError),
}
impl AsRef<str> for OutputMsg {
fn as_ref(&self) -> &str {
match self {
Self::Ok(s) => s.as_ref(),
Self::PartialOk(s) => s.as_ref(),
Self::Err(e) => <&'static str>::from(e),
}
}
}
impl Serialize for OutputMsg {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
match self {
Self::Ok(s) => s.serialize(serializer),
Self::PartialOk(s) => s.serialize(serializer),
Self::Err(e) => <&'static str>::from(e).serialize(serializer),
}
}
}
impl Display for OutputMsg {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Ok(msg) => write!(f, "{msg:?}"),
Self::PartialOk(msg) => write!(f, "{:?}", cli::theme::THEME.inline_error.style(msg)),
Self::Err(e) => Display::fmt(&cli::theme::THEME.inline_error.style(&e), f),
}
}
}
impl Display for FriendlyError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", <&'static str>::from(self))
}
}
impl From<ArcStr> for OutputMsg {
fn from(value: ArcStr) -> Self {
Self::Ok(value)
}
}
impl OutputMsg {
pub fn not_ok(&self) -> bool {
!matches!(self, Self::Ok(_))
}
pub fn is_ok_and(&self, predicate: impl FnOnce(&str) -> bool) -> bool {
match self {
Self::Ok(s) => predicate(s),
Self::PartialOk(_) => false,
Self::Err(_) => false,
}
}
pub fn is_err_or(&self, predicate: impl FnOnce(&str) -> bool) -> bool {
match self {
Self::Ok(s) => predicate(s),
Self::PartialOk(_) => true,
Self::Err(_) => true,
}
}
pub fn join(&self, path: impl AsRef<str>) -> Self {
let path = path.as_ref();
match self {
Self::Ok(s) => Self::Ok(cached_string(format!("{s}/{path}"))),
Self::PartialOk(s) => Self::PartialOk(cached_string(format!("{s}/{path}"))),
Self::Err(s) => Self::PartialOk(cached_string(format!("{}/{path}", <&'static str>::from(s)))),
}
}
pub fn cli_bash_escaped_with_style(
&self,
style: owo_colors::Style,
) -> Either<impl Display, impl Display> {
match self {
Self::Ok(s) => Either::Left(style.style(shell_quote::QuoteRefExt::<String>::quoted(
s.as_str(),
shell_quote::Bash,
))),
Self::PartialOk(s) => Either::Left(cli::theme::THEME.inline_error.style(
shell_quote::QuoteRefExt::<String>::quoted(s.as_str(), shell_quote::Bash),
)),
Self::Err(e) => Either::Right(
cli::theme::THEME
.inline_error
.style(<&'static str>::from(e)),
),
}
}
pub fn bash_escaped(&self) -> Cow<'static, str> {
match self {
Self::Ok(s) | Self::PartialOk(s) => Cow::Owned(shell_quote::QuoteRefExt::quoted(
s.as_str(),
shell_quote::Bash,
)),
Self::Err(e) => Cow::Borrowed(<&'static str>::from(e)),
}
}
pub fn cli_styled(&self, style: owo_colors::Style) -> Either<impl Display + '_, impl Display> {
match self {
Self::Ok(s) => Either::Left(s.style(style)),
Self::PartialOk(s) => Either::Left(s.style(cli::theme::THEME.inline_error)),
Self::Err(e) => Either::Right(
cli::theme::THEME
.inline_error
.style(<&'static str>::from(e)),
),
}
}
pub fn cli_escaped_styled(
&self,
style: owo_colors::Style,
) -> Either<impl Display + '_, impl Display> {
struct DebugAsDisplay<T: Debug>(T);
impl<T: Debug> Display for DebugAsDisplay<T> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
self.0.fmt(f)
}
}
match self {
Self::Ok(s) => Either::Left(style.style(DebugAsDisplay(s))),
Self::PartialOk(s) => Either::Left(cli::theme::THEME.inline_error.style(DebugAsDisplay(s))),
Self::Err(e) => Either::Right(
cli::theme::THEME
.inline_error
.style(<&'static str>::from(e)),
),
}
}
}
#[cfg(test)]
mod tests {
use std::{
collections::hash_map::DefaultHasher,
hash::{
Hash,
Hasher,
},
};
use nix::errno::Errno;
use test_that::prelude::*;
use super::*;
use crate::cache::ArcStr;
#[test]
fn test_friendly_error_display() {
let e = FriendlyError::InspectError(Errno::EINVAL);
assert_eq!(format!("{}", e), "[err: failed to inspect]");
}
#[cfg(feature = "ebpf")]
#[test]
fn test_friendly_error_bpf_display() {
let e = FriendlyError::Bpf(BpfError::Dropped);
assert_eq!(format!("{}", e), "[err: bpf error]");
}
#[test]
fn test_output_msg_as_ref() {
let ok = OutputMsg::Ok(ArcStr::from("hello"));
let partial = OutputMsg::PartialOk(ArcStr::from("partial"));
let err = OutputMsg::Err(FriendlyError::InspectError(Errno::EACCES));
assert_eq!(ok.as_ref(), "hello");
assert_eq!(partial.as_ref(), "partial");
assert_eq!(err.as_ref(), "[err: failed to inspect]");
}
#[test]
fn test_not_ok() {
let ok = OutputMsg::Ok(ArcStr::from("ok"));
let partial = OutputMsg::PartialOk(ArcStr::from("partial"));
let err = OutputMsg::Err(FriendlyError::InspectError(Errno::EPERM));
assert!(!ok.not_ok());
assert!(partial.not_ok());
assert!(err.not_ok());
}
#[test]
fn test_is_ok_and_is_err_or() {
let ok = OutputMsg::Ok(ArcStr::from("matchme"));
let partial = OutputMsg::PartialOk(ArcStr::from("partial"));
let err = OutputMsg::Err(FriendlyError::InspectError(Errno::EPERM));
assert!(ok.is_ok_and(|s| s.contains("match")));
assert!(!partial.is_ok_and(|_| true));
assert!(!err.is_ok_and(|_| true));
assert!(!ok.is_err_or(|s| s.contains("ok")));
assert!(partial.is_err_or(|_| false));
assert!(err.is_err_or(|_| false));
}
#[test]
fn test_join() {
let ok = OutputMsg::Ok(ArcStr::from("base"));
let partial = OutputMsg::PartialOk(ArcStr::from("part"));
let err = OutputMsg::Err(FriendlyError::InspectError(Errno::EPERM));
assert_eq!(ok.join("path").as_ref(), "base/path");
assert_eq!(partial.join("p").as_ref(), "part/p");
assert_eq!(err.join("x").as_ref(), "[err: failed to inspect]/x");
}
#[test]
fn test_bash_escaped() {
let ok = OutputMsg::Ok(ArcStr::from("a b"));
let err = OutputMsg::Err(FriendlyError::InspectError(Errno::EPERM));
assert_eq!(ok.bash_escaped(), "$'a b'");
assert_eq!(err.bash_escaped(), "[err: failed to inspect]");
}
#[test]
fn test_hash_eq_ord() {
let a = FriendlyError::InspectError(Errno::EINVAL);
let b = FriendlyError::InspectError(Errno::EACCES);
assert!(a != b);
let gt = a > b;
let lt = a < b;
assert!(gt || lt);
let mut hasher = DefaultHasher::new();
a.hash(&mut hasher);
let _hash_val = hasher.finish();
}
#[test]
fn test_from_arcstr() {
let s: ArcStr = ArcStr::from("hello");
let msg: OutputMsg = s.into();
assert_eq!(msg.as_ref(), "hello");
}
#[test]
fn test_display_debug_formats() {
let ok = OutputMsg::Ok(ArcStr::from("ok"));
let partial = OutputMsg::PartialOk(ArcStr::from("partial"));
let err = OutputMsg::Err(FriendlyError::InspectError(Errno::EINVAL));
assert_that!(format!("{}", ok), contains_substring("ok"));
assert_that!(format!("{}", partial), contains_substring("partial"));
assert_that!(
format!("{}", err),
contains_substring("[err: failed to inspect]")
);
}
}