pass_fu/
err.rs

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}