1use core::fmt;
4
5use apple_cf::CFError;
6
7pub type Result<T, E = SecurityError> = std::result::Result<T, E>;
9
10pub type OsStatus = i32;
12
13pub mod status {
15 use super::OsStatus;
16
17 pub const SUCCESS: OsStatus = 0;
19 pub const DUPLICATE_ITEM: OsStatus = -25_299;
21 pub const ITEM_NOT_FOUND: OsStatus = -25_300;
23 pub const INTERACTION_NOT_ALLOWED: OsStatus = -25_308;
25}
26
27#[derive(Debug, Clone, PartialEq, Eq)]
29pub struct StatusError {
30 pub operation: &'static str,
32 pub status: OsStatus,
34 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#[derive(Debug, Clone, PartialEq, Eq)]
52#[non_exhaustive]
53pub enum SecurityError {
54 InvalidArgument(String),
56 ItemNotFound(String),
58 DuplicateItem(String),
60 InteractionNotAllowed(String),
62 TrustEvaluationFailed(String),
64 UnexpectedType {
66 operation: &'static str,
68 expected: &'static str,
70 },
71 Serialization(String),
73 CoreFoundation(CFError),
75 Status(StatusError),
77}
78
79impl SecurityError {
80 #[must_use]
81 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}