tracexec-core 1.0.0-rc.1

Core crate of tracexec [Internal implementation! DO NOT DEPEND ON!]
Documentation
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]",
    }
  }
}

// we need to implement custom Display so Result and Either do not fit.
#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub enum OutputMsg {
  Ok(ArcStr),
  // Part of the message contains error
  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,
    }
  }

  /// Join two paths with a '/', preserving the semantics of [`OutputMsg`]
  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)))),
    }
  }

  /// Escape the content for bash shell if it is not error
  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)),
      ),
    }
  }

  /// Escape the content for bash shell if it is not error
  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> {
    // We (ab)use Rust's Debug feature to escape our string.
    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]")
    );
  }
}