#![forbid(unsafe_code)] #![deny(clippy::disallowed_types)] //! Mechanical UniFFI exports for the minimal watchOS Rust runtime. use std::{ error::Error, fmt, sync::{Arc, Mutex}, }; use ironstorage::mobile_watch::{ WatchPersistenceAction as StoragePersistenceAction, WatchRuntime as StorageWatchRuntime, WatchSnapshotApply as StorageSnapshotApply, WatchSnapshotError as StorageWatchError, WatchSnapshotUpdate as StorageSnapshotUpdate, WatchTotpRecord as StorageTotpRecord, }; uniffi::setup_scaffolding!(); #[derive(Clone, Copy, Debug, Eq, PartialEq, uniffi::Enum)] pub enum WatchSnapshotApply { Replaced, Revoked, Duplicate, Stale, PairingChanged, } impl From for WatchSnapshotApply { fn from(value: StorageSnapshotApply) -> Self { match value { StorageSnapshotApply::Replaced => Self::Replaced, StorageSnapshotApply::Revoked => Self::Revoked, StorageSnapshotApply::Duplicate => Self::Duplicate, StorageSnapshotApply::Stale => Self::Stale, StorageSnapshotApply::PairingChanged => Self::PairingChanged, } } } #[derive(Clone, Copy, Debug, Eq, PartialEq, uniffi::Enum)] pub enum WatchPersistenceAction { Keep, Replace, Delete, } impl From for WatchPersistenceAction { fn from(value: StoragePersistenceAction) -> Self { match value { StoragePersistenceAction::Keep => Self::Keep, StoragePersistenceAction::Replace => Self::Replace, StoragePersistenceAction::Delete => Self::Delete, } } } #[derive(Clone, Debug, Eq, PartialEq, uniffi::Record)] pub struct WatchSnapshotUpdate { pub apply: WatchSnapshotApply, pub persistence: WatchPersistenceAction, pub revision: Option, pub selected_entries: u32, pub receipt: Vec, } impl From for WatchSnapshotUpdate { fn from(value: StorageSnapshotUpdate) -> Self { Self { apply: value.apply().into(), persistence: value.persistence().into(), revision: value.revision(), selected_entries: value.selected_entries(), receipt: value.receipt().to_vec(), } } } #[derive(Clone, Debug, Eq, PartialEq, uniffi::Record)] pub struct WatchTotpRecord { pub path: String, pub issuer: Option, pub account: String, pub code: String, pub period: u64, pub valid_until: u64, } impl From for WatchTotpRecord { fn from(value: StorageTotpRecord) -> Self { Self { path: value.path().to_owned(), issuer: value.issuer().map(str::to_owned), account: value.account().to_owned(), code: String::from_utf8(value.code().expose().to_vec()) .expect("storage-generated TOTP codes are ASCII"), period: value.period(), valid_until: value.valid_until(), } } } #[derive(Debug, uniffi::Error)] pub enum WatchFfiError { Failed { message: String }, } impl fmt::Display for WatchFfiError { fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { match self { Self::Failed { message } => formatter.write_str(message), } } } impl Error for WatchFfiError {} impl From for WatchFfiError { fn from(value: StorageWatchError) -> Self { Self::Failed { message: value.to_string(), } } } #[derive(uniffi::Object)] pub struct WatchCore { runtime: Mutex, } #[uniffi::export] impl WatchCore { pub fn apply_snapshot(&self, snapshot: Vec) -> Result { self.runtime .lock() .map_err(|_| lock_error())? .apply_snapshot(snapshot) .map(Into::into) .map_err(Into::into) } pub fn records_at(&self, unix_seconds: u64) -> Result, WatchFfiError> { self.runtime .lock() .map_err(|_| lock_error())? .records_at(unix_seconds) .map(|records| records.into_iter().map(Into::into).collect()) .map_err(Into::into) } pub fn protected_data_unavailable(&self) -> Result<(), WatchFfiError> { self.runtime .lock() .map_err(|_| lock_error())? .protected_data_unavailable(); Ok(()) } pub fn no_persisted_snapshot(&self) -> Result<(), WatchFfiError> { self.runtime .lock() .map_err(|_| lock_error())? .no_persisted_snapshot(); Ok(()) } } fn lock_error() -> WatchFfiError { WatchFfiError::Failed { message: "Apple Watch TOTP state is unavailable".to_owned(), } } #[uniffi::export] pub fn watch_core() -> Arc { Arc::new(WatchCore { runtime: Mutex::new(StorageWatchRuntime::default()), }) } #[cfg(test)] mod tests { #[test] fn bridge_masks_records_when_protected_data_is_unavailable() { let core = super::watch_core(); core.no_persisted_snapshot().expect("available Keychain"); assert!(core.records_at(59).expect("empty snapshot").is_empty()); core.protected_data_unavailable().expect("lock transition"); assert!(core.records_at(59).is_err()); } }