Skip to main content

security/
error.rs

1//! Errors returned by the `security-rs` bindings.
2
3use core::fmt;
4
5use apple_cf::CFError;
6
7/// Convenient result alias used throughout this crate.
8pub type Result<T, E = SecurityError> = std::result::Result<T, E>;
9
10/// Raw `OSStatus` code returned by Security.framework.
11pub type OsStatus = i32;
12
13/// Common status-code constants surfaced by the safe bridge.
14pub mod status {
15    use super::OsStatus;
16
17    /// Mirrors a common `OSStatus` value from Security.framework.
18    pub const SUCCESS: OsStatus = 0;
19    /// Mirrors a common `OSStatus` value from Security.framework.
20    pub const DUPLICATE_ITEM: OsStatus = -25_299;
21    /// Mirrors a common `OSStatus` value from Security.framework.
22    pub const ITEM_NOT_FOUND: OsStatus = -25_300;
23    /// Mirrors a common `OSStatus` value from Security.framework.
24    pub const INTERACTION_NOT_ALLOWED: OsStatus = -25_308;
25}
26
27/// Structured `OSStatus` error returned by `Security.framework`.
28#[derive(Debug, Clone, PartialEq, Eq)]
29pub struct StatusError {
30    /// API name that returned the status code.
31    pub operation: &'static str,
32    /// Raw `OSStatus` numeric code.
33    pub status: OsStatus,
34    /// Human-readable description when available.
35    pub message: String,
36}
37
38impl fmt::Display for StatusError {
39    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
40        write!(
41            f,
42            "{} failed with OSStatus {}: {}",
43            self.operation, self.status, self.message
44        )
45    }
46}
47
48impl std::error::Error for StatusError {}
49
50/// Top-level error type returned by this crate.
51#[derive(Debug, Clone, PartialEq, Eq)]
52#[non_exhaustive]
53pub enum SecurityError {
54    /// Invalid input crossed the FFI boundary.
55    InvalidArgument(String),
56    /// A requested keychain item was missing.
57    ItemNotFound(String),
58    /// A duplicate keychain item already existed.
59    DuplicateItem(String),
60    /// Authentication UI was suppressed or otherwise unavailable.
61    InteractionNotAllowed(String),
62    /// Trust evaluation failed and Security.framework provided a reason.
63    TrustEvaluationFailed(String),
64    /// Security.framework returned an unexpected Core Foundation type.
65    UnexpectedType {
66        /// API name being decoded.
67        operation: &'static str,
68        /// Expected Core Foundation family.
69        expected: &'static str,
70    },
71    /// JSON serialization or deserialization failed.
72    Serialization(String),
73    /// A Core Foundation creation call returned a null pointer.
74    CoreFoundation(CFError),
75    /// Security.framework returned an unexpected `OSStatus`.
76    Status(StatusError),
77}
78
79impl SecurityError {
80    #[must_use]
81    /// Returns the underlying `OSStatus` when Security.framework provided one.
82    pub const fn code(&self) -> Option<OsStatus> {
83        match self {
84            Self::ItemNotFound(_) => Some(status::ITEM_NOT_FOUND),
85            Self::DuplicateItem(_) => Some(status::DUPLICATE_ITEM),
86            Self::InteractionNotAllowed(_) => Some(status::INTERACTION_NOT_ALLOWED),
87            Self::Status(error) => Some(error.status),
88            _ => None,
89        }
90    }
91
92    pub(crate) fn from_status(operation: &'static str, status: OsStatus, message: String) -> Self {
93        match status {
94            status::ITEM_NOT_FOUND => Self::ItemNotFound(message),
95            status::DUPLICATE_ITEM => Self::DuplicateItem(message),
96            status::INTERACTION_NOT_ALLOWED => Self::InteractionNotAllowed(message),
97            _ => Self::Status(StatusError {
98                operation,
99                status,
100                message,
101            }),
102        }
103    }
104}
105
106impl fmt::Display for SecurityError {
107    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
108        match self {
109            Self::InvalidArgument(message) => write!(f, "invalid argument: {message}"),
110            Self::ItemNotFound(message) => write!(f, "item not found: {message}"),
111            Self::DuplicateItem(message) => write!(f, "duplicate item: {message}"),
112            Self::InteractionNotAllowed(message) => write!(f, "interaction not allowed: {message}"),
113            Self::TrustEvaluationFailed(message) => {
114                write!(f, "trust evaluation failed: {message}")
115            }
116            Self::UnexpectedType {
117                operation,
118                expected,
119            } => write!(
120                f,
121                "{operation} returned an unexpected value (expected {expected})"
122            ),
123            Self::Serialization(message) => write!(f, "serialization error: {message}"),
124            Self::CoreFoundation(error) => write!(f, "{error}"),
125            Self::Status(error) => write!(f, "{error}"),
126        }
127    }
128}
129
130impl std::error::Error for SecurityError {
131    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
132        match self {
133            Self::CoreFoundation(error) => Some(error),
134            Self::Status(error) => Some(error),
135            _ => None,
136        }
137    }
138}
139
140#[cfg(test)]
141mod tests {
142    use super::*;
143
144    #[test]
145    fn status_error_display_includes_operation_status_and_message() {
146        let error = StatusError {
147            operation: "security_op",
148            status: -50,
149            message: "bad input".to_owned(),
150        };
151
152        assert_eq!(
153            error.to_string(),
154            "security_op failed with OSStatus -50: bad input"
155        );
156    }
157
158    #[test]
159    fn from_status_maps_known_codes_to_specialized_variants() {
160        assert_eq!(
161            SecurityError::from_status("op", status::ITEM_NOT_FOUND, "missing".to_owned()),
162            SecurityError::ItemNotFound("missing".to_owned())
163        );
164        assert_eq!(
165            SecurityError::from_status("op", status::DUPLICATE_ITEM, "duplicate".to_owned()),
166            SecurityError::DuplicateItem("duplicate".to_owned())
167        );
168        assert_eq!(
169            SecurityError::from_status(
170                "op",
171                status::INTERACTION_NOT_ALLOWED,
172                "suppressed".to_owned(),
173            ),
174            SecurityError::InteractionNotAllowed("suppressed".to_owned())
175        );
176    }
177
178    #[test]
179    fn from_status_wraps_unknown_codes_in_status_error() {
180        let error = SecurityError::from_status("security_op", -1_234, "unexpected".to_owned());
181
182        assert_eq!(
183            error,
184            SecurityError::Status(StatusError {
185                operation: "security_op",
186                status: -1_234,
187                message: "unexpected".to_owned(),
188            })
189        );
190        assert_eq!(error.code(), Some(-1_234));
191    }
192
193    #[test]
194    fn code_reports_expected_status_values() {
195        let status_error = SecurityError::Status(StatusError {
196            operation: "security_op",
197            status: -42,
198            message: "boom".to_owned(),
199        });
200
201        assert_eq!(
202            SecurityError::ItemNotFound("missing".to_owned()).code(),
203            Some(status::ITEM_NOT_FOUND)
204        );
205        assert_eq!(
206            SecurityError::DuplicateItem("duplicate".to_owned()).code(),
207            Some(status::DUPLICATE_ITEM)
208        );
209        assert_eq!(
210            SecurityError::InteractionNotAllowed("suppressed".to_owned()).code(),
211            Some(status::INTERACTION_NOT_ALLOWED)
212        );
213        assert_eq!(status_error.code(), Some(-42));
214        assert_eq!(
215            SecurityError::InvalidArgument("bad".to_owned()).code(),
216            None
217        );
218    }
219
220    #[test]
221    fn security_error_display_and_source_follow_wrapped_status() {
222        let wrapped = StatusError {
223            operation: "security_op",
224            status: -42,
225            message: "boom".to_owned(),
226        };
227        let error = SecurityError::Status(wrapped.clone());
228        let serialization = SecurityError::Serialization("invalid json".to_owned());
229
230        assert_eq!(error.to_string(), wrapped.to_string());
231        assert_eq!(
232            std::error::Error::source(&error).unwrap().to_string(),
233            wrapped.to_string()
234        );
235        assert!(std::error::Error::source(&serialization).is_none());
236    }
237}