Skip to main content

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  use test_that::prelude::*;
249
250  use super::*;
251  use crate::cache::ArcStr;
252
253  #[test]
254  fn test_friendly_error_display() {
255    let e = FriendlyError::InspectError(Errno::EINVAL);
256    assert_eq!(format!("{}", e), "[err: failed to inspect]");
257  }
258
259  #[cfg(feature = "ebpf")]
260  #[test]
261  fn test_friendly_error_bpf_display() {
262    let e = FriendlyError::Bpf(BpfError::Dropped);
263    assert_eq!(format!("{}", e), "[err: bpf error]");
264  }
265
266  #[test]
267  fn test_output_msg_as_ref() {
268    let ok = OutputMsg::Ok(ArcStr::from("hello"));
269    let partial = OutputMsg::PartialOk(ArcStr::from("partial"));
270    let err = OutputMsg::Err(FriendlyError::InspectError(Errno::EACCES));
271
272    assert_eq!(ok.as_ref(), "hello");
273    assert_eq!(partial.as_ref(), "partial");
274    assert_eq!(err.as_ref(), "[err: failed to inspect]");
275  }
276
277  #[test]
278  fn test_not_ok() {
279    let ok = OutputMsg::Ok(ArcStr::from("ok"));
280    let partial = OutputMsg::PartialOk(ArcStr::from("partial"));
281    let err = OutputMsg::Err(FriendlyError::InspectError(Errno::EPERM));
282
283    assert!(!ok.not_ok());
284    assert!(partial.not_ok());
285    assert!(err.not_ok());
286  }
287
288  #[test]
289  fn test_is_ok_and_is_err_or() {
290    let ok = OutputMsg::Ok(ArcStr::from("matchme"));
291    let partial = OutputMsg::PartialOk(ArcStr::from("partial"));
292    let err = OutputMsg::Err(FriendlyError::InspectError(Errno::EPERM));
293
294    assert!(ok.is_ok_and(|s| s.contains("match")));
295    assert!(!partial.is_ok_and(|_| true));
296    assert!(!err.is_ok_and(|_| true));
297
298    assert!(!ok.is_err_or(|s| s.contains("ok")));
299    assert!(partial.is_err_or(|_| false));
300    assert!(err.is_err_or(|_| false));
301  }
302
303  #[test]
304  fn test_join() {
305    let ok = OutputMsg::Ok(ArcStr::from("base"));
306    let partial = OutputMsg::PartialOk(ArcStr::from("part"));
307    let err = OutputMsg::Err(FriendlyError::InspectError(Errno::EPERM));
308
309    assert_eq!(ok.join("path").as_ref(), "base/path");
310    assert_eq!(partial.join("p").as_ref(), "part/p");
311    assert_eq!(err.join("x").as_ref(), "[err: failed to inspect]/x");
312  }
313
314  #[test]
315  fn test_bash_escaped() {
316    let ok = OutputMsg::Ok(ArcStr::from("a b"));
317    let err = OutputMsg::Err(FriendlyError::InspectError(Errno::EPERM));
318
319    assert_eq!(ok.bash_escaped(), "$'a b'");
320    assert_eq!(err.bash_escaped(), "[err: failed to inspect]");
321  }
322
323  #[test]
324  fn test_hash_eq_ord() {
325    let a = FriendlyError::InspectError(Errno::EINVAL);
326    let b = FriendlyError::InspectError(Errno::EACCES);
327
328    assert!(a != b);
329    let gt = a > b;
330    let lt = a < b;
331    assert!(gt || lt);
332
333    let mut hasher = DefaultHasher::new();
334    a.hash(&mut hasher);
335    let _hash_val = hasher.finish();
336  }
337
338  #[test]
339  fn test_from_arcstr() {
340    let s: ArcStr = ArcStr::from("hello");
341    let msg: OutputMsg = s.into();
342    assert_eq!(msg.as_ref(), "hello");
343  }
344
345  #[test]
346  fn test_display_debug_formats() {
347    let ok = OutputMsg::Ok(ArcStr::from("ok"));
348    let partial = OutputMsg::PartialOk(ArcStr::from("partial"));
349    let err = OutputMsg::Err(FriendlyError::InspectError(Errno::EINVAL));
350
351    assert_that!(format!("{}", ok), contains_substring("ok"));
352    assert_that!(format!("{}", partial), contains_substring("partial"));
353    assert_that!(
354      format!("{}", err),
355      contains_substring("[err: failed to inspect]")
356    );
357  }
358}