1use std::{backtrace::Backtrace, fmt, io};
2
3pub type PassFuResult<T> = Result<T, PassFuError>;
4
5pub trait Errors {
6 type Contextual;
7 fn ctx(self, context: impl fmt::Display) -> Self::Contextual;
8}
9impl<T, E> Errors for Result<T, E>
10where
11 E: Into<PassFuError>,
12{
13 type Contextual = Result<T, PassFuError>;
14 fn ctx(self, context: impl fmt::Display) -> Self::Contextual {
15 self.map_err(|e| e.into().ctx(context))
16 }
17}
18
19#[derive(Debug)]
20pub struct PassFuError {
21 msg: String,
22 trouble: Trouble,
23 backtrace: Backtrace,
24}
25impl std::error::Error for PassFuError {
26 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
27 Some(&self.trouble)
28 }
29}
30impl fmt::Display for PassFuError {
31 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
32 let Self { msg, trouble, .. } = self;
33 write!(f, "{msg} - {trouble}")
34 }
35}
36
37impl PassFuError {
38 #[inline]
39 fn new(trouble: impl Into<Trouble>) -> Self {
40 Self {
41 msg: String::default(),
42 trouble: trouble.into(),
43 backtrace: Backtrace::capture(),
44 }
45 }
46 fn ctx(mut self, context: impl fmt::Display) -> Self {
47 let context = context.to_string();
48 if !self.msg.is_empty() {
49 self.msg.insert_str(0, " - ");
50 }
51 self.msg.insert_str(0, &context.to_string());
52 self
53 }
54 pub fn code(&self) -> i32 {
55 match self.trouble {
56 Trouble::Logic(_) => 255,
57 Trouble::ChildIo(_) => 253,
58 Trouble::Totp(_) => 252,
59 Trouble::TotpTime(_) => 251,
60 Trouble::Child(ChildExitStatus(code)) => code,
61 }
62 }
63}
64impl<T> From<T> for PassFuError
65where
66 Trouble: From<T>,
67{
68 fn from(err: T) -> Self {
69 Self::new(Trouble::from(err))
70 }
71}
72
73#[derive(Debug, thiserror::Error)]
74enum Trouble {
75 #[error(transparent)]
76 Logic(#[from] LogicError),
77 #[error(transparent)]
78 Child(#[from] ChildExitStatus),
79 #[error(transparent)]
80 ChildIo(#[from] ChildIoError),
81 #[error("TOTP code generation failed: {0:?}")]
82 Totp(#[from] totp_rs::TotpUrlError),
83 #[error("TOTP code timing failed: {0:?}")]
84 TotpTime(#[from] std::time::SystemTimeError),
85}
86#[derive(Debug, thiserror::Error)]
87#[error("child process returned {0}")]
88pub(crate) struct ChildExitStatus(i32);
89impl From<i32> for ChildExitStatus {
90 fn from(exit_status: i32) -> Self {
91 ChildExitStatus(exit_status)
92 }
93}
94#[derive(Debug, thiserror::Error)]
95#[error("{0}")]
96pub(crate) struct LogicError(String);
97impl LogicError {
98 pub fn new(message: impl ToString) -> Self {
99 LogicError(message.to_string())
100 }
101}
102#[derive(Debug, thiserror::Error)]
103#[error("child process io failed: {0:?}")]
104pub(crate) struct ChildIoError(#[from] pub io::Error);
105
106pub fn assert_child_io<T>(cmd: impl fmt::Debug, result: io::Result<T>) -> PassFuResult<T> {
107 result
108 .map_err(ChildIoError)
109 .ctx(&format!("{cmd:?}"))
110 .ctx("running child")
111}
112
113pub fn assert_child_status(
114 cmd: impl fmt::Debug,
115 status: io::Result<std::process::ExitStatus>,
116) -> PassFuResult<()> {
117 let code = status
118 .map_err(ChildIoError)
119 .ctx(&format!("{cmd:?}"))
120 .ctx("running child")?;
121
122 if code.success() {
123 Ok(())
124 } else {
125 Err(ChildExitStatus(code.code().unwrap_or(255)))
126 .ctx(&format!("{cmd:?}"))
127 .ctx("running child")
128 }
129}