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#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
87pub enum OutputMsg {
88 Ok(ArcStr),
89 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 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 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 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 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}