Skip to main content

localauthentication/
la_error.rs

1//! Errors produced by the `LocalAuthentication` bridge.
2
3use core::ffi::c_char;
4use core::fmt;
5
6use libc::free;
7
8use crate::ffi;
9
10/// `LocalAuthentication`'s `NSError` domain string.
11pub const LA_ERROR_DOMAIN: &str = "com.apple.LocalAuthentication";
12
13/// Convenient result alias used throughout this crate.
14pub type Result<T, E = LAError> = std::result::Result<T, E>;
15
16/// Top-level error type returned by this crate.
17#[derive(Debug, Clone, PartialEq, Eq)]
18#[non_exhaustive]
19pub enum LAError {
20    /// Invalid input crossed the FFI boundary.
21    InvalidArgument(String),
22    /// The framework callback did not complete before the bridge timeout elapsed.
23    TimedOut(String),
24    /// The Swift bridge failed before reaching the framework call.
25    BridgeFailed(String),
26    /// Authentication was not successful because valid credentials were not provided.
27    AuthenticationFailed(String),
28    /// Authentication was cancelled by the user.
29    UserCancel(String),
30    /// Authentication was cancelled because the fallback button was tapped.
31    UserFallback(String),
32    /// Authentication was cancelled by the system.
33    SystemCancel(String),
34    /// Authentication cannot start because no device passcode is configured.
35    PasscodeNotSet(String),
36    /// Authentication cannot start because biometry is unavailable.
37    BiometryNotAvailable(String),
38    /// Authentication cannot start because no biometric identities are enrolled.
39    BiometryNotEnrolled(String),
40    /// Authentication cannot start because biometry is locked.
41    BiometryLockout(String),
42    /// Authentication was cancelled by the application.
43    AppCancel(String),
44    /// The `LAContext` has already been invalidated.
45    InvalidContext(String),
46    /// Authentication would require UI while interaction is disallowed.
47    NotInteractive(String),
48    /// Authentication cannot start because no companion device is nearby.
49    CompanionNotAvailable(String),
50    /// Authentication cannot start because the paired biometric accessory is unavailable.
51    BiometryNotPaired(String),
52    /// Authentication cannot start because the paired biometric accessory is disconnected.
53    BiometryDisconnected(String),
54    /// Authentication cannot start because an embedded UI size is invalid.
55    InvalidDimensions(String),
56    /// Catch-all for unmapped framework or bridge status codes.
57    Other { code: i32, message: String },
58}
59
60/// Backward-compatible alias for the v0.1.x error type name.
61pub type LocalAuthenticationError = LAError;
62
63impl LAError {
64    /// Numeric bridge, `LAError`, or `OSStatus` code reported by the bridge.
65    #[must_use]
66    pub const fn code(&self) -> i32 {
67        match self {
68            Self::InvalidArgument(_) => ffi::status::INVALID_ARGUMENT,
69            Self::TimedOut(_) => ffi::status::TIMED_OUT,
70            Self::BridgeFailed(_) => ffi::status::BRIDGE_FAILED,
71            Self::AuthenticationFailed(_) => ffi::la_error::AUTHENTICATION_FAILED,
72            Self::UserCancel(_) => ffi::la_error::USER_CANCEL,
73            Self::UserFallback(_) => ffi::la_error::USER_FALLBACK,
74            Self::SystemCancel(_) => ffi::la_error::SYSTEM_CANCEL,
75            Self::PasscodeNotSet(_) => ffi::la_error::PASSCODE_NOT_SET,
76            Self::BiometryNotAvailable(_) => ffi::la_error::BIOMETRY_NOT_AVAILABLE,
77            Self::BiometryNotEnrolled(_) => ffi::la_error::BIOMETRY_NOT_ENROLLED,
78            Self::BiometryLockout(_) => ffi::la_error::BIOMETRY_LOCKOUT,
79            Self::AppCancel(_) => ffi::la_error::APP_CANCEL,
80            Self::InvalidContext(_) => ffi::la_error::INVALID_CONTEXT,
81            Self::NotInteractive(_) => ffi::la_error::NOT_INTERACTIVE,
82            Self::CompanionNotAvailable(_) => ffi::la_error::COMPANION_NOT_AVAILABLE,
83            Self::BiometryNotPaired(_) => ffi::la_error::BIOMETRY_NOT_PAIRED,
84            Self::BiometryDisconnected(_) => ffi::la_error::BIOMETRY_DISCONNECTED,
85            Self::InvalidDimensions(_) => ffi::la_error::INVALID_DIMENSIONS,
86            Self::Other { code, .. } => *code,
87        }
88    }
89
90    /// Human-readable description returned by the Swift bridge.
91    #[must_use]
92    pub fn message(&self) -> &str {
93        match self {
94            Self::InvalidArgument(message)
95            | Self::TimedOut(message)
96            | Self::BridgeFailed(message)
97            | Self::AuthenticationFailed(message)
98            | Self::UserCancel(message)
99            | Self::UserFallback(message)
100            | Self::SystemCancel(message)
101            | Self::PasscodeNotSet(message)
102            | Self::BiometryNotAvailable(message)
103            | Self::BiometryNotEnrolled(message)
104            | Self::BiometryLockout(message)
105            | Self::AppCancel(message)
106            | Self::InvalidContext(message)
107            | Self::NotInteractive(message)
108            | Self::CompanionNotAvailable(message)
109            | Self::BiometryNotPaired(message)
110            | Self::BiometryDisconnected(message)
111            | Self::InvalidDimensions(message)
112            | Self::Other { message, .. } => message,
113        }
114    }
115
116    /// `LocalAuthentication`'s `NSError` domain string.
117    #[must_use]
118    pub const fn domain() -> &'static str {
119        LA_ERROR_DOMAIN
120    }
121
122    /// Build an error value from a numeric code and message.
123    #[must_use]
124    pub fn from_code_message(code: i32, message: impl Into<String>) -> Self {
125        from_status_message(code, message.into())
126    }
127}
128
129impl fmt::Display for LAError {
130    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
131        write!(f, "{} (code {})", self.message(), self.code())
132    }
133}
134
135impl std::error::Error for LAError {}
136
137/// Take ownership of a Swift-allocated C string and free it with `libc::free`.
138pub(crate) fn take_owned_c_string(ptr: *mut c_char) -> String {
139    if ptr.is_null() {
140        return String::new();
141    }
142
143    let string = unsafe { core::ffi::CStr::from_ptr(ptr) }
144        .to_string_lossy()
145        .into_owned();
146    unsafe { free(ptr.cast()) };
147    string
148}
149
150/// Take ownership of a Swift-allocated byte buffer and free it with `libc::free`.
151pub(crate) fn take_owned_buffer(ptr: *mut u8, len: usize) -> Vec<u8> {
152    if ptr.is_null() || len == 0 {
153        if !ptr.is_null() {
154            unsafe { free(ptr.cast()) };
155        }
156        return Vec::new();
157    }
158
159    let bytes = unsafe { std::slice::from_raw_parts(ptr, len) }.to_vec();
160    unsafe { free(ptr.cast()) };
161    bytes
162}
163
164/// Build an `LAError` from a status code and optional message.
165pub(crate) fn from_status(status: i32, error_str: *mut c_char) -> LAError {
166    let message = take_owned_c_string(error_str);
167    from_status_message(status, message)
168}
169
170/// Build an `LAError` from a status code and message generated in Rust.
171pub(crate) const fn from_status_message(status: i32, message: String) -> LAError {
172    match status {
173        ffi::status::INVALID_ARGUMENT => LAError::InvalidArgument(message),
174        ffi::status::TIMED_OUT => LAError::TimedOut(message),
175        ffi::status::BRIDGE_FAILED => LAError::BridgeFailed(message),
176        ffi::la_error::AUTHENTICATION_FAILED => LAError::AuthenticationFailed(message),
177        ffi::la_error::USER_CANCEL => LAError::UserCancel(message),
178        ffi::la_error::USER_FALLBACK => LAError::UserFallback(message),
179        ffi::la_error::SYSTEM_CANCEL => LAError::SystemCancel(message),
180        ffi::la_error::PASSCODE_NOT_SET => LAError::PasscodeNotSet(message),
181        ffi::la_error::BIOMETRY_NOT_AVAILABLE => LAError::BiometryNotAvailable(message),
182        ffi::la_error::BIOMETRY_NOT_ENROLLED => LAError::BiometryNotEnrolled(message),
183        ffi::la_error::BIOMETRY_LOCKOUT => LAError::BiometryLockout(message),
184        ffi::la_error::APP_CANCEL => LAError::AppCancel(message),
185        ffi::la_error::INVALID_CONTEXT => LAError::InvalidContext(message),
186        ffi::la_error::NOT_INTERACTIVE => LAError::NotInteractive(message),
187        ffi::la_error::COMPANION_NOT_AVAILABLE => LAError::CompanionNotAvailable(message),
188        ffi::la_error::BIOMETRY_NOT_PAIRED => LAError::BiometryNotPaired(message),
189        ffi::la_error::BIOMETRY_DISCONNECTED => LAError::BiometryDisconnected(message),
190        ffi::la_error::INVALID_DIMENSIONS => LAError::InvalidDimensions(message),
191        code => LAError::Other { code, message },
192    }
193}
194
195#[cfg(test)]
196mod tests {
197    use super::LAError;
198    use crate::ffi;
199
200    #[test]
201    fn maps_common_la_error_codes() {
202        let error = LAError::from_code_message(
203            ffi::la_error::BIOMETRY_LOCKOUT,
204            "biometry is locked".to_owned(),
205        );
206        assert!(matches!(
207            error,
208            LAError::BiometryLockout(message)
209            if message == "biometry is locked"
210        ));
211    }
212
213    #[test]
214    fn maps_bridge_status_codes() {
215        let error =
216            LAError::from_code_message(ffi::status::TIMED_OUT, "operation timed out".to_owned());
217        assert!(matches!(
218            error,
219            LAError::TimedOut(message)
220            if message == "operation timed out"
221        ));
222    }
223}