tracexec_core/event/
message.rs

1use std::{
2  borrow::Cow,
3  fmt::{
4    Debug,
5    Display,
6  },
7  hash::Hash,
8};
9
10use either::Either;
11use nix::errno::Errno;
12use owo_colors::OwoColorize;
13use serde::Serialize;
14
15use crate::{
16  cache::ArcStr,
17  cli,
18  proc::cached_string,
19};
20
21#[cfg(feature = "ebpf")]
22#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
23#[repr(u8)]
24pub enum BpfError {
25  Dropped,
26  Flags,
27}
28
29#[derive(Debug, Clone, Copy, PartialEq, Eq)]
30#[repr(u64)]
31pub enum FriendlyError {
32  InspectError(Errno),
33  #[cfg(feature = "ebpf")]
34  Bpf(BpfError),
35}
36
37impl PartialOrd for FriendlyError {
38  fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
39    Some(Ord::cmp(self, other))
40  }
41}
42
43impl Ord for FriendlyError {
44  fn cmp(&self, other: &Self) -> std::cmp::Ordering {
45    match (self, other) {
46      (Self::InspectError(a), Self::InspectError(b)) => (*a as i32).cmp(&(*b as i32)),
47      #[cfg(feature = "ebpf")]
48      (Self::Bpf(a), Self::Bpf(b)) => a.cmp(b),
49      #[cfg(feature = "ebpf")]
50      (Self::InspectError(_), Self::Bpf(_)) => std::cmp::Ordering::Less,
51      #[cfg(feature = "ebpf")]
52      (Self::Bpf(_), Self::InspectError(_)) => std::cmp::Ordering::Greater,
53    }
54  }
55}
56
57impl Hash for FriendlyError {
58  fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
59    core::mem::discriminant(self).hash(state);
60    match self {
61      Self::InspectError(e) => (*e as i32).hash(state),
62      #[cfg(feature = "ebpf")]
63      Self::Bpf(e) => e.hash(state),
64    }
65  }
66}
67
68#[cfg(feature = "ebpf")]
69impl From<BpfError> for FriendlyError {
70  fn from(value: BpfError) -> Self {
71    Self::Bpf(value)
72  }
73}
74
75impl From<&FriendlyError> for &'static str {
76  fn from(value: &FriendlyError) -> Self {
77    match value {
78      FriendlyError::InspectError(_) => "[err: failed to inspect]",
79      #[cfg(feature = "ebpf")]
80      FriendlyError::Bpf(_) => "[err: bpf error]",
81    }
82  }
83}
84
85// we need to implement custom Display so Result and Either do not fit.
86#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
87pub enum OutputMsg {
88  Ok(ArcStr),
89  // Part of the message contains error
90  PartialOk(ArcStr),
91  Err(FriendlyError),
92}
93
94impl AsRef<str> for OutputMsg {
95  fn as_ref(&self) -> &str {
96    match self {
97      Self::Ok(s) => s.as_ref(),
98      Self::PartialOk(s) => s.as_ref(),
99      Self::Err(e) => <&'static str>::from(e),
100    }
101  }
102}
103
104impl Serialize for OutputMsg {
105  fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
106  where
107    S: serde::Serializer,
108  {
109    match self {
110      Self::Ok(s) => s.serialize(serializer),
111      Self::PartialOk(s) => s.serialize(serializer),
112      Self::Err(e) => <&'static str>::from(e).serialize(serializer),
113    }
114  }
115}
116
117impl Display for OutputMsg {
118  fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
119    match self {
120      Self::Ok(msg) => write!(f, "{msg:?}"),
121      Self::PartialOk(msg) => write!(f, "{:?}", cli::theme::THEME.inline_error.style(msg)),
122      Self::Err(e) => Display::fmt(&cli::theme::THEME.inline_error.style(&e), f),
123    }
124  }
125}
126
127impl Display for FriendlyError {
128  fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
129    write!(f, "{}", <&'static str>::from(self))
130  }
131}
132
133impl From<ArcStr> for OutputMsg {
134  fn from(value: ArcStr) -> Self {
135    Self::Ok(value)
136  }
137}
138
139impl OutputMsg {
140  pub fn not_ok(&self) -> bool {
141    !matches!(self, Self::Ok(_))
142  }
143
144  pub fn is_ok_and(&self, predicate: impl FnOnce(&str) -> bool) -> bool {
145    match self {
146      Self::Ok(s) => predicate(s),
147      Self::PartialOk(_) => false,
148      Self::Err(_) => false,
149    }
150  }
151
152  pub fn is_err_or(&self, predicate: impl FnOnce(&str) -> bool) -> bool {
153    match self {
154      Self::Ok(s) => predicate(s),
155      Self::PartialOk(_) => true,
156      Self::Err(_) => true,
157    }
158  }
159
160  /// Join two paths with a '/', preserving the semantics of [`OutputMsg`]
161  pub fn join(&self, path: impl AsRef<str>) -> Self {
162    let path = path.as_ref();
163    match self {
164      Self::Ok(s) => Self::Ok(cached_string(format!("{s}/{path}"))),
165      Self::PartialOk(s) => Self::PartialOk(cached_string(format!("{s}/{path}"))),
166      Self::Err(s) => Self::PartialOk(cached_string(format!("{}/{path}", <&'static str>::from(s)))),
167    }
168  }
169
170  /// Escape the content for bash shell if it is not error
171  pub fn cli_bash_escaped_with_style(
172    &self,
173    style: owo_colors::Style,
174  ) -> Either<impl Display, impl Display> {
175    match self {
176      Self::Ok(s) => Either::Left(style.style(shell_quote::QuoteRefExt::<String>::quoted(
177        s.as_str(),
178        shell_quote::Bash,
179      ))),
180      Self::PartialOk(s) => Either::Left(cli::theme::THEME.inline_error.style(
181        shell_quote::QuoteRefExt::<String>::quoted(s.as_str(), shell_quote::Bash),
182      )),
183      Self::Err(e) => Either::Right(
184        cli::theme::THEME
185          .inline_error
186          .style(<&'static str>::from(e)),
187      ),
188    }
189  }
190
191  /// Escape the content for bash shell if it is not error
192  pub fn bash_escaped(&self) -> Cow<'static, str> {
193    match self {
194      Self::Ok(s) | Self::PartialOk(s) => Cow::Owned(shell_quote::QuoteRefExt::quoted(
195        s.as_str(),
196        shell_quote::Bash,
197      )),
198      Self::Err(e) => Cow::Borrowed(<&'static str>::from(e)),
199    }
200  }
201
202  pub fn cli_styled(&self, style: owo_colors::Style) -> Either<impl Display + '_, impl Display> {
203    match self {
204      Self::Ok(s) => Either::Left(s.style(style)),
205      Self::PartialOk(s) => Either::Left(s.style(cli::theme::THEME.inline_error)),
206      Self::Err(e) => Either::Right(
207        cli::theme::THEME
208          .inline_error
209          .style(<&'static str>::from(e)),
210      ),
211    }
212  }
213
214  pub fn cli_escaped_styled(
215    &self,
216    style: owo_colors::Style,
217  ) -> Either<impl Display + '_, impl Display> {
218    // We (ab)use Rust's Debug feature to escape our string.
219    struct DebugAsDisplay<T: Debug>(T);
220    impl<T: Debug> Display for DebugAsDisplay<T> {
221      fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
222        self.0.fmt(f)
223      }
224    }
225    match self {
226      Self::Ok(s) => Either::Left(style.style(DebugAsDisplay(s))),
227      Self::PartialOk(s) => Either::Left(cli::theme::THEME.inline_error.style(DebugAsDisplay(s))),
228      Self::Err(e) => Either::Right(
229        cli::theme::THEME
230          .inline_error
231          .style(<&'static str>::from(e)),
232      ),
233    }
234  }
235}
236
237#[cfg(test)]
238mod tests {
239  use std::{
240    collections::hash_map::DefaultHasher,
241    hash::{
242      Hash,
243      Hasher,
244    },
245  };
246
247  use nix::errno::Errno;
248
249  use super::*;
250  use crate::cache::ArcStr;
251
252  #[test]
253  fn test_friendly_error_display() {
254    let e = FriendlyError::InspectError(Errno::EINVAL);
255    assert_eq!(format!("{}", e), "[err: failed to inspect]");
256  }
257
258  #[cfg(feature = "ebpf")]
259  #[test]
260  fn test_friendly_error_bpf_display() {
261    let e = FriendlyError::Bpf(BpfError::Dropped);
262    assert_eq!(format!("{}", e), "[err: bpf error]");
263  }
264
265  #[test]
266  fn test_output_msg_as_ref() {
267    let ok = OutputMsg::Ok(ArcStr::from("hello"));
268    let partial = OutputMsg::PartialOk(ArcStr::from("partial"));
269    let err = OutputMsg::Err(FriendlyError::InspectError(Errno::EACCES));
270
271    assert_eq!(ok.as_ref(), "hello");
272    assert_eq!(partial.as_ref(), "partial");
273    assert_eq!(err.as_ref(), "[err: failed to inspect]");
274  }
275
276  #[test]
277  fn test_not_ok() {
278    let ok = OutputMsg::Ok(ArcStr::from("ok"));
279    let partial = OutputMsg::PartialOk(ArcStr::from("partial"));
280    let err = OutputMsg::Err(FriendlyError::InspectError(Errno::EPERM));
281
282    assert!(!ok.not_ok());
283    assert!(partial.not_ok());
284    assert!(err.not_ok());
285  }
286
287  #[test]
288  fn test_is_ok_and_is_err_or() {
289    let ok = OutputMsg::Ok(ArcStr::from("matchme"));
290    let partial = OutputMsg::PartialOk(ArcStr::from("partial"));
291    let err = OutputMsg::Err(FriendlyError::InspectError(Errno::EPERM));
292
293    assert!(ok.is_ok_and(|s| s.contains("match")));
294    assert!(!partial.is_ok_and(|_| true));
295    assert!(!err.is_ok_and(|_| true));
296
297    assert!(!ok.is_err_or(|s| s.contains("ok")));
298    assert!(partial.is_err_or(|_| false));
299    assert!(err.is_err_or(|_| false));
300  }
301
302  #[test]
303  fn test_join() {
304    let ok = OutputMsg::Ok(ArcStr::from("base"));
305    let partial = OutputMsg::PartialOk(ArcStr::from("part"));
306    let err = OutputMsg::Err(FriendlyError::InspectError(Errno::EPERM));
307
308    assert_eq!(ok.join("path").as_ref(), "base/path");
309    assert_eq!(partial.join("p").as_ref(), "part/p");
310    assert_eq!(err.join("x").as_ref(), "[err: failed to inspect]/x");
311  }
312
313  #[test]
314  fn test_bash_escaped() {
315    let ok = OutputMsg::Ok(ArcStr::from("a b"));
316    let err = OutputMsg::Err(FriendlyError::InspectError(Errno::EPERM));
317
318    assert_eq!(ok.bash_escaped(), "$'a b'");
319    assert_eq!(err.bash_escaped(), "[err: failed to inspect]");
320  }
321
322  #[test]
323  fn test_hash_eq_ord() {
324    let a = FriendlyError::InspectError(Errno::EINVAL);
325    let b = FriendlyError::InspectError(Errno::EACCES);
326
327    assert!(a != b);
328    assert!(a < b || a > b);
329
330    let mut hasher = DefaultHasher::new();
331    a.hash(&mut hasher);
332    let _hash_val = hasher.finish();
333  }
334
335  #[test]
336  fn test_from_arcstr() {
337    let s: ArcStr = ArcStr::from("hello");
338    let msg: OutputMsg = s.clone().into();
339    assert_eq!(msg.as_ref(), "hello");
340  }
341
342  #[test]
343  fn test_display_debug_formats() {
344    let ok = OutputMsg::Ok(ArcStr::from("ok"));
345    let partial = OutputMsg::PartialOk(ArcStr::from("partial"));
346    let err = OutputMsg::Err(FriendlyError::InspectError(Errno::EINVAL));
347
348    assert!(format!("{}", ok).contains("ok"));
349    assert!(format!("{}", partial).contains("partial"));
350    assert!(format!("{}", err).contains("[err: failed to inspect]"));
351  }
352}