1pub type FrozenResult<T> = Result<T, FrozenError>;
36
37#[derive(Clone)]
39pub struct FrozenError {
40 pub module: u8,
42
43 pub domain: u8,
45
46 pub reason: u8,
48
49 pub context: Box<str>,
51
52 pub suppressed: Option<Box<Vec<FrozenError>>>,
54}
55
56impl FrozenError {
57 #[inline(always)]
74 pub fn new(module: u8, domain: u8, code: ErrCode, errmsg: &str) -> Self {
75 Self {
76 module,
77 domain,
78 reason: code.reason,
79 context: format!("[{}] {}", code.detail, errmsg).into_boxed_str(),
80 suppressed: None,
81 }
82 }
83
84 #[inline(always)]
102 pub fn new_raw<E: std::fmt::Display>(module: u8, domain: u8, code: ErrCode, err: E) -> Self {
103 Self {
104 domain,
105 module,
106 reason: code.reason,
107 context: format!("[{}] {}", code.detail, err).into_boxed_str(),
108 suppressed: None,
109 }
110 }
111
112 pub fn add_suppressed(&mut self, err: FrozenError) {
114 let list = self.suppressed.get_or_insert_with(|| Box::new(Vec::new()));
115 list.push(err);
116 }
117
118 pub fn with_suppressed(mut self, err: FrozenError) -> Self {
120 self.add_suppressed(err);
121 self
122 }
123
124 #[inline]
126 pub fn suppressed(&self) -> &[FrozenError] {
127 match &self.suppressed {
128 Some(list) => list.as_slice(),
129 None => &[],
130 }
131 }
132
133 pub const UNBOUND_MODULE: u8 = 0x00;
135
136 #[inline(always)]
149 pub fn unbound(domain: u8, code: ErrCode, errmsg: &str) -> Self {
150 Self::new(Self::UNBOUND_MODULE, domain, code, errmsg)
151 }
152
153 #[inline(always)]
167 pub fn unbound_raw<E: std::fmt::Display>(domain: u8, code: ErrCode, err: E) -> Self {
168 Self::new_raw(Self::UNBOUND_MODULE, domain, code, err)
169 }
170
171 #[must_use]
185 pub fn with_module(mut self, module: u8) -> Self {
186 self.module = module;
187 if let Some(suppressed) = &mut self.suppressed {
188 for sub in suppressed.iter_mut() {
189 *sub = sub.clone().with_module(module);
190 }
191 }
192
193 self
194 }
195}
196
197impl FrozenError {
198 fn fmt_tree(
199 &self,
200 f: &mut std::fmt::Formatter<'_>,
201 prefix: &str,
202 is_tail: bool,
203 is_root: bool,
204 ) -> std::fmt::Result {
205 if is_root {
206 writeln!(
207 f,
208 "[{:#04x}:{:#04x}:{:#04x}] {}",
209 self.module, self.domain, self.reason, self.context
210 )?;
211 } else {
212 let branch = if is_tail { "└── " } else { "├── " };
213 writeln!(
214 f,
215 "{}{}[{:#04x}:{:#04x}:{:#04x}] {}",
216 prefix, branch, self.module, self.domain, self.reason, self.context
217 )?;
218 }
219
220 if let Some(suppressed) = &self.suppressed {
221 let next_prefix = if is_root {
222 String::new()
223 } else if is_tail {
224 format!("{prefix} ")
225 } else {
226 format!("{prefix}│ ")
227 };
228
229 let count = suppressed.len();
230 for (idx, sub_err) in suppressed.iter().enumerate() {
231 let is_last = idx + 1 == count;
232 sub_err.fmt_tree(f, &next_prefix, is_last, false)?;
233 }
234 }
235
236 Ok(())
237 }
238}
239
240impl std::fmt::Debug for FrozenError {
241 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
242 match &self.suppressed {
243 None => write!(
244 f,
245 "FrozenError {{ module: {:#04x}, domain: {:#04x}, reason: {:#04x}, context: {:?} }}",
246 self.module, self.domain, self.reason, self.context
247 ),
248 Some(suppressed) => write!(
249 f,
250 "FrozenError {{ module: {:#04x}, domain: {:#04x}, reason: {:#04x}, context: {:?}, suppressed: {:?} }}",
251 self.module, self.domain, self.reason, self.context, suppressed
252 ),
253 }
254 }
255}
256
257impl std::fmt::Display for FrozenError {
258 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
259 if self.suppressed.is_none() {
260 write!(
261 f,
262 "[{:#04x}:{:#04x}:{:#04x}] {}",
263 self.module, self.domain, self.reason, self.context
264 )
265 } else {
266 let mut formatted = String::new();
267
268 struct TreeHelper<'a>(&'a FrozenError);
269
270 impl std::fmt::Display for TreeHelper<'_> {
271 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
272 self.0.fmt_tree(f, "", true, true)
273 }
274 }
275
276 use std::fmt::Write as _;
277 let _ = write!(formatted, "{}", TreeHelper(self));
278
279 if formatted.ends_with('\n') {
280 formatted.pop();
281 }
282
283 write!(f, "{formatted}")
284 }
285 }
286}
287
288impl std::error::Error for FrozenError {}
289
290impl PartialEq for FrozenError {
291 fn eq(&self, other: &Self) -> bool {
292 (self.module == other.module)
293 && (self.domain == other.domain)
294 && (self.reason == other.reason)
295 }
296}
297
298#[derive(Debug, Clone)]
311pub struct ErrCode {
312 pub reason: u8,
314
315 pub detail: &'static str,
317}
318
319impl ErrCode {
320 #[inline]
336 pub const fn new(reason: u8, detail: &'static str) -> Self {
337 Self { reason, detail }
338 }
339}
340
341pub trait BindModule<T> {
356 fn with_module(self, module_id: u8) -> FrozenResult<T>;
358}
359
360impl<T> BindModule<T> for FrozenResult<T> {
361 #[inline(always)]
362 fn with_module(self, module_id: u8) -> FrozenResult<T> {
363 self.map_err(|e| e.with_module(module_id))
364 }
365}
366
367#[cfg(test)]
368mod tests {
369 use super::*;
370
371 #[test]
372 fn ok_context_exact_format() {
373 let err = FrozenError::new(1, 2, ErrCode::new(3, "io"), "failure");
374 assert_eq!(err.context, "[io] failure".into());
375 }
376
377 #[test]
378 fn ok_empty_message() {
379 let err = FrozenError::new(1, 2, ErrCode::new(3, "io"), "");
380 assert_eq!(err.context, "[io] ".into());
381 }
382
383 #[test]
384 fn ok_empty_detail() {
385 let err = FrozenError::new(1, 2, ErrCode::new(3, ""), "failure");
386 assert_eq!(err.context, "[] failure".into());
387 }
388
389 #[test]
390 fn ok_new_and_new_raw_same_id() {
391 let e1 = FrozenError::new(1, 2, ErrCode::new(3, "io"), "fail");
392
393 let io_err = std::io::Error::other("fail");
394 let e2 = FrozenError::new_raw(1, 2, ErrCode::new(3, "io"), io_err);
395
396 assert_eq!(e1, e2);
397 }
398
399 #[test]
400 fn ok_context_formatting() {
401 let err = FrozenError::new(1, 1, ErrCode::new(1, "io"), "disk failure");
402 assert_eq!(err.context, "[io] disk failure".into());
403 }
404
405 #[test]
406 fn ok_new_raw_uses_display() {
407 let io_err = std::io::Error::new(std::io::ErrorKind::PermissionDenied, "access denied");
408 let err = FrozenError::new_raw(1, 2, ErrCode::new(3, "io"), io_err);
409
410 assert!(err.context.contains("[io]"));
411 assert!(err.context.contains("access denied"));
412 }
413
414 #[test]
415 fn ok_compare_same_id_different_context() {
416 let e1 = FrozenError::new(1, 2, ErrCode::new(3, "io"), "a");
417 let e2 = FrozenError::new(1, 2, ErrCode::new(3, "io"), "b");
418
419 assert_eq!(e1, e2);
420 }
421
422 #[test]
423 fn ok_compare_different_id() {
424 let e1 = FrozenError::new(1, 2, ErrCode::new(3, "io"), "a");
425 let e2 = FrozenError::new(1, 2, ErrCode::new(4, "io"), "a");
426
427 assert_ne!(e1, e2);
428 }
429
430 #[test]
431 fn ok_display_format() {
432 let err = FrozenError::new(0x01, 0x11, ErrCode::new(0x1C, "lock"), "file locked");
433 assert_eq!(format!("{err}"), "[0x01:0x11:0x1c] [lock] file locked");
434 }
435
436 #[test]
437 fn ok_debug_format_includes_context() {
438 let err = FrozenError::new(0x01, 0x11, ErrCode::new(0x1C, "lock"), "file locked");
439 let dbg = format!("{err:?}");
440 assert!(dbg.contains("0x01"));
441 assert!(dbg.contains("0x11"));
442 assert!(dbg.contains("0x1c"));
443 assert!(dbg.contains("[lock] file locked"));
444 }
445
446 #[test]
447 fn ok_is_std_error() {
448 fn assert_is_error<E: std::error::Error>(_: &E) {}
453 let err = FrozenError::new(1, 2, ErrCode::new(3, "io"), "fail");
454 assert_is_error(&err);
455 }
456
457 #[test]
458 fn ok_suppressed_tree_display() {
459 let mut root = FrozenError::new(0x01, 0x10, ErrCode::new(0x20, "posix"), "f_advise failed");
460 let sub1 = FrozenError::new(0x01, 0x10, ErrCode::new(0x30, "posix"), "close failed");
461 let mut sub2 = FrozenError::new(0x01, 0x10, ErrCode::new(0x40, "posix"), "flush failed");
462 let sub2_nested = FrozenError::new(0x01, 0x10, ErrCode::new(0x50, "posix"), "sync failed");
463
464 sub2.add_suppressed(sub2_nested);
465 root.add_suppressed(sub1);
466 root.add_suppressed(sub2);
467
468 assert_eq!(root.suppressed().len(), 2);
469
470 let displayed = format!("{root}");
471 let expected = "\
472[0x01:0x10:0x20] [posix] f_advise failed
473├── [0x01:0x10:0x30] [posix] close failed
474└── [0x01:0x10:0x40] [posix] flush failed
475 └── [0x01:0x10:0x50] [posix] sync failed";
476
477 assert_eq!(displayed, expected);
478 }
479
480 #[test]
481 fn ok_with_suppressed_builder() {
482 let err = FrozenError::new(1, 2, ErrCode::new(3, "io"), "parent")
483 .with_suppressed(FrozenError::new(1, 2, ErrCode::new(4, "io"), "cleanup"));
484
485 assert_eq!(err.suppressed().len(), 1);
486 assert_eq!(err.suppressed()[0].reason, 4);
487 }
488
489 #[test]
490 fn ok_unbound_and_with_module() {
491 let mut err = FrozenError::unbound(0x08, ErrCode::new(0x10, "inv"), "unbound err");
492 assert_eq!(err.module, FrozenError::UNBOUND_MODULE);
493 assert_eq!(err.domain, 0x08);
494 assert_eq!(err.reason, 0x10);
495
496 let suppressed = FrozenError::unbound(0x08, ErrCode::new(0x20, "close"), "suppressed err");
497 err.add_suppressed(suppressed);
498
499 let bound = err.with_module(0x42);
500 assert_eq!(bound.module, 0x42);
501 assert_eq!(bound.domain, 0x08);
502 assert_eq!(bound.reason, 0x10);
503 assert_eq!(bound.suppressed()[0].module, 0x42);
504 }
505
506 #[test]
507 fn ok_bind_module_trait() {
508 let res: FrozenResult<()> =
509 Err(FrozenError::unbound(0x08, ErrCode::new(0x10, "inv"), "fail"));
510 let bound_res = res.with_module(0x07);
511 assert_eq!(bound_res.unwrap_err().module, 0x07);
512
513 let ok_res: FrozenResult<i32> = Ok(0x0A);
514 assert_eq!(ok_res.with_module(0x07).unwrap(), 0x0A);
515 }
516}