use core::fmt;
use apple_cf::CFError;
pub type Result<T, E = SecurityError> = std::result::Result<T, E>;
pub type OsStatus = i32;
pub mod status {
use super::OsStatus;
pub const SUCCESS: OsStatus = 0;
pub const DUPLICATE_ITEM: OsStatus = -25_299;
pub const ITEM_NOT_FOUND: OsStatus = -25_300;
pub const INTERACTION_NOT_ALLOWED: OsStatus = -25_308;
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct StatusError {
pub operation: &'static str,
pub status: OsStatus,
pub message: String,
}
impl fmt::Display for StatusError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"{} failed with OSStatus {}: {}",
self.operation, self.status, self.message
)
}
}
impl std::error::Error for StatusError {}
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub enum SecurityError {
InvalidArgument(String),
ItemNotFound(String),
DuplicateItem(String),
InteractionNotAllowed(String),
TrustEvaluationFailed(String),
UnexpectedType {
operation: &'static str,
expected: &'static str,
},
Serialization(String),
CoreFoundation(CFError),
Status(StatusError),
}
impl SecurityError {
#[must_use]
pub const fn code(&self) -> Option<OsStatus> {
match self {
Self::ItemNotFound(_) => Some(status::ITEM_NOT_FOUND),
Self::DuplicateItem(_) => Some(status::DUPLICATE_ITEM),
Self::InteractionNotAllowed(_) => Some(status::INTERACTION_NOT_ALLOWED),
Self::Status(error) => Some(error.status),
_ => None,
}
}
pub(crate) fn from_status(operation: &'static str, status: OsStatus, message: String) -> Self {
match status {
status::ITEM_NOT_FOUND => Self::ItemNotFound(message),
status::DUPLICATE_ITEM => Self::DuplicateItem(message),
status::INTERACTION_NOT_ALLOWED => Self::InteractionNotAllowed(message),
_ => Self::Status(StatusError {
operation,
status,
message,
}),
}
}
}
impl fmt::Display for SecurityError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::InvalidArgument(message) => write!(f, "invalid argument: {message}"),
Self::ItemNotFound(message) => write!(f, "item not found: {message}"),
Self::DuplicateItem(message) => write!(f, "duplicate item: {message}"),
Self::InteractionNotAllowed(message) => write!(f, "interaction not allowed: {message}"),
Self::TrustEvaluationFailed(message) => {
write!(f, "trust evaluation failed: {message}")
}
Self::UnexpectedType {
operation,
expected,
} => write!(
f,
"{operation} returned an unexpected value (expected {expected})"
),
Self::Serialization(message) => write!(f, "serialization error: {message}"),
Self::CoreFoundation(error) => write!(f, "{error}"),
Self::Status(error) => write!(f, "{error}"),
}
}
}
impl std::error::Error for SecurityError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match self {
Self::CoreFoundation(error) => Some(error),
Self::Status(error) => Some(error),
_ => None,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn status_error_display_includes_operation_status_and_message() {
let error = StatusError {
operation: "security_op",
status: -50,
message: "bad input".to_owned(),
};
assert_eq!(
error.to_string(),
"security_op failed with OSStatus -50: bad input"
);
}
#[test]
fn from_status_maps_known_codes_to_specialized_variants() {
assert_eq!(
SecurityError::from_status("op", status::ITEM_NOT_FOUND, "missing".to_owned()),
SecurityError::ItemNotFound("missing".to_owned())
);
assert_eq!(
SecurityError::from_status("op", status::DUPLICATE_ITEM, "duplicate".to_owned()),
SecurityError::DuplicateItem("duplicate".to_owned())
);
assert_eq!(
SecurityError::from_status(
"op",
status::INTERACTION_NOT_ALLOWED,
"suppressed".to_owned(),
),
SecurityError::InteractionNotAllowed("suppressed".to_owned())
);
}
#[test]
fn from_status_wraps_unknown_codes_in_status_error() {
let error = SecurityError::from_status("security_op", -1_234, "unexpected".to_owned());
assert_eq!(
error,
SecurityError::Status(StatusError {
operation: "security_op",
status: -1_234,
message: "unexpected".to_owned(),
})
);
assert_eq!(error.code(), Some(-1_234));
}
#[test]
fn code_reports_expected_status_values() {
let status_error = SecurityError::Status(StatusError {
operation: "security_op",
status: -42,
message: "boom".to_owned(),
});
assert_eq!(
SecurityError::ItemNotFound("missing".to_owned()).code(),
Some(status::ITEM_NOT_FOUND)
);
assert_eq!(
SecurityError::DuplicateItem("duplicate".to_owned()).code(),
Some(status::DUPLICATE_ITEM)
);
assert_eq!(
SecurityError::InteractionNotAllowed("suppressed".to_owned()).code(),
Some(status::INTERACTION_NOT_ALLOWED)
);
assert_eq!(status_error.code(), Some(-42));
assert_eq!(
SecurityError::InvalidArgument("bad".to_owned()).code(),
None
);
}
#[test]
fn security_error_display_and_source_follow_wrapped_status() {
let wrapped = StatusError {
operation: "security_op",
status: -42,
message: "boom".to_owned(),
};
let error = SecurityError::Status(wrapped.clone());
let serialization = SecurityError::Serialization("invalid json".to_owned());
assert_eq!(error.to_string(), wrapped.to_string());
assert_eq!(
std::error::Error::source(&error).unwrap().to_string(),
wrapped.to_string()
);
assert!(std::error::Error::source(&serialization).is_none());
}
}