#![forbid(unsafe_code)] #![deny(clippy::disallowed_types)] //! Mechanical UniFFI exports for Apple presentation code. use std::{error::Error, fmt, sync::Arc}; use ironstorage::{ config::ConfigError, mobile::{self, MobileShellState as StorageShellState, MobileTab as StorageTab}, mobile_onboarding::{ self, MobileOnboardingError as StorageOnboardingError, MobileOnboardingErrorKind as StorageOnboardingErrorKind, MobileOnboardingPhase as StorageOnboardingPhase, }, }; uniffi::setup_scaffolding!(); #[derive(Clone, Copy, Debug, Eq, PartialEq, uniffi::Enum)] pub enum MobileTab { Home, Passwords, Totp, Preferences, } impl From for MobileTab { fn from(tab: StorageTab) -> Self { match tab { StorageTab::Home => Self::Home, StorageTab::Passwords => Self::Passwords, StorageTab::Totp => Self::Totp, StorageTab::Preferences => Self::Preferences, } } } impl From for StorageTab { fn from(tab: MobileTab) -> Self { match tab { MobileTab::Home => Self::Home, MobileTab::Passwords => Self::Passwords, MobileTab::Totp => Self::Totp, MobileTab::Preferences => Self::Preferences, } } } #[derive(Clone, Copy, Debug, Eq, PartialEq, uniffi::Enum)] pub enum MobileShellState { Loading, Empty, Ready, Locked, Error, } impl From for MobileShellState { fn from(state: StorageShellState) -> Self { match state { StorageShellState::Loading => Self::Loading, StorageShellState::Empty => Self::Empty, StorageShellState::Ready => Self::Ready, StorageShellState::Locked => Self::Locked, StorageShellState::Error => Self::Error, } } } impl From for StorageShellState { fn from(state: MobileShellState) -> Self { match state { MobileShellState::Loading => Self::Loading, MobileShellState::Empty => Self::Empty, MobileShellState::Ready => Self::Ready, MobileShellState::Locked => Self::Locked, MobileShellState::Error => Self::Error, } } } #[derive(Clone, uniffi::Record)] pub struct MobilePage { pub tab: MobileTab, pub title: String, pub system_image: String, pub selected_system_image: String, pub state: MobileShellState, pub state_title: String, pub state_detail: String, } #[derive(Clone, uniffi::Record)] pub struct MobileShell { pub selected_tab: MobileTab, pub pages: Vec, } #[derive(Clone, Copy, Debug, Eq, PartialEq, uniffi::Enum)] pub enum MobileOnboardingPhase { Validating, Authenticating, Receiving, Integrating, Finishing, } impl From for MobileOnboardingPhase { fn from(phase: StorageOnboardingPhase) -> Self { match phase { StorageOnboardingPhase::Validating => Self::Validating, StorageOnboardingPhase::Authenticating => Self::Authenticating, StorageOnboardingPhase::Receiving => Self::Receiving, StorageOnboardingPhase::Integrating => Self::Integrating, StorageOnboardingPhase::Finishing => Self::Finishing, } } } #[derive(Clone, uniffi::Record)] pub struct MobileOnboardingProgress { pub phase: MobileOnboardingPhase, pub title: String, pub detail: String, } #[derive(Clone, uniffi::Record)] pub struct MobileOnboardingDiscovery { pub branches: Vec, pub selected_branch: u32, } #[derive(Clone, uniffi::Record)] pub struct MobileOnboardingOutcome { pub title: String, pub detail: String, } #[derive(Clone, Copy, Debug, Eq, PartialEq, uniffi::Enum)] pub enum MobileOnboardingErrorKind { InvalidInput, UnsupportedRemote, Authentication, Repository, ExistingClone, Interrupted, SecureStorage, Configuration, AlreadyConfigured, } impl From for MobileOnboardingErrorKind { fn from(kind: StorageOnboardingErrorKind) -> Self { match kind { StorageOnboardingErrorKind::InvalidInput => Self::InvalidInput, StorageOnboardingErrorKind::UnsupportedRemote => Self::UnsupportedRemote, StorageOnboardingErrorKind::Authentication => Self::Authentication, StorageOnboardingErrorKind::Repository => Self::Repository, StorageOnboardingErrorKind::ExistingClone => Self::ExistingClone, StorageOnboardingErrorKind::Interrupted => Self::Interrupted, StorageOnboardingErrorKind::SecureStorage => Self::SecureStorage, StorageOnboardingErrorKind::Configuration => Self::Configuration, StorageOnboardingErrorKind::AlreadyConfigured => Self::AlreadyConfigured, } } } #[derive(Debug, uniffi::Error)] pub enum MobileOnboardingFfiError { Failed { kind: MobileOnboardingErrorKind, title: String, detail: String, }, } impl fmt::Display for MobileOnboardingFfiError { fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { match self { Self::Failed { title, detail, .. } => write!(formatter, "{title}: {detail}"), } } } impl Error for MobileOnboardingFfiError {} impl From for MobileOnboardingFfiError { fn from(error: StorageOnboardingError) -> Self { Self::Failed { kind: error.kind().into(), title: error.title().to_owned(), detail: error.detail().to_owned(), } } } #[derive(uniffi::Object)] pub struct MobileOnboardingOperation { operation: mobile_onboarding::MobileOnboardingOperation, request: mobile_onboarding::MobileOnboardingRequest, } #[uniffi::export] impl MobileOnboardingOperation { pub fn progress(&self) -> MobileOnboardingProgress { let progress = self.operation.progress(); MobileOnboardingProgress { phase: progress.phase().into(), title: progress.title().to_owned(), detail: progress.detail().to_owned(), } } pub fn discover(&self) -> Result { let discovery = self.operation.discover(&self.request)?; Ok(MobileOnboardingDiscovery { branches: discovery.branches().to_vec(), selected_branch: u32::try_from(discovery.selected_branch()).unwrap_or_default(), }) } pub fn setup( &self, branch: String, use_existing: bool, ) -> Result { let outcome = self.operation.setup(&self.request, &branch, use_existing)?; Ok(MobileOnboardingOutcome { title: outcome.title().to_owned(), detail: outcome.detail().to_owned(), }) } pub fn cancel(&self) { self.operation.cancel(); } } impl From for MobileShell { fn from(shell: mobile::MobileShell) -> Self { Self { selected_tab: shell.selected_tab().into(), pages: shell .pages() .iter() .map(|page| MobilePage { tab: page.tab().into(), title: page.title().to_owned(), system_image: page.system_image().to_owned(), selected_system_image: page.selected_system_image().to_owned(), state: page.state().into(), state_title: page.state_title().to_owned(), state_detail: page.state_detail().to_owned(), }) .collect(), } } } #[derive(Debug, uniffi::Error)] pub enum MobilePreferenceError { Configuration { message: String }, } impl fmt::Display for MobilePreferenceError { fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { match self { Self::Configuration { message } => formatter.write_str(message), } } } impl Error for MobilePreferenceError {} impl From for MobilePreferenceError { fn from(error: ConfigError) -> Self { Self::Configuration { message: error.to_string(), } } } #[uniffi::export] pub fn product_name() -> String { ironstorage::PRODUCT_NAME.to_owned() } #[uniffi::export] pub fn mobile_shell() -> MobileShell { mobile::MobileShell::load().into() } #[uniffi::export] pub fn mobile_shell_fixture(state: MobileShellState) -> MobileShell { mobile::MobileShell::fixture(state.into()).into() } #[uniffi::export] pub fn set_selected_mobile_tab(tab: MobileTab) -> Result<(), MobilePreferenceError> { mobile::store_selected_tab(tab.into()).map_err(Into::into) } #[uniffi::export] pub fn mobile_onboarding_operation( server_url: String, account: String, repository_path: String, application_token: String, ) -> Result, MobileOnboardingFfiError> { Ok(Arc::new(MobileOnboardingOperation { operation: mobile_onboarding::MobileOnboardingOperation::default(), request: mobile_onboarding::MobileOnboardingRequest::new( server_url, account, repository_path, application_token.into_bytes(), )?, })) } #[uniffi::export] pub fn replace_configured_mobile_application_token( account: String, application_token: String, ) -> Result<(), MobileOnboardingFfiError> { mobile_onboarding::replace_configured_application_token( account, application_token.into_bytes(), )?; Ok(()) } #[cfg(test)] mod tests { use super::{MobileOnboardingErrorKind, MobileOnboardingFfiError, MobileShellState, MobileTab}; #[test] fn bridge_reads_product_name_from_storage_crate() { assert_eq!(super::product_name(), ironstorage::PRODUCT_NAME); } #[test] fn bridge_exposes_every_view_ready_shell_fixture() { for state in [ MobileShellState::Loading, MobileShellState::Empty, MobileShellState::Ready, MobileShellState::Locked, MobileShellState::Error, ] { let shell = super::mobile_shell_fixture(state); assert_eq!(shell.selected_tab, MobileTab::Home); assert_eq!(shell.pages.len(), 4); assert!(shell.pages.iter().all(|page| page.state == state)); } } #[test] fn bridge_rejects_non_https_before_creating_an_operation() { let error = match super::mobile_onboarding_operation( "ssh://example.test".to_owned(), "alice".to_owned(), "team/passwords".to_owned(), "DO-NOT-RENDER".to_owned(), ) { Ok(_) => panic!("SSH must be rejected"), Err(error) => error, }; match error { MobileOnboardingFfiError::Failed { kind, detail, .. } => { assert_eq!(kind, MobileOnboardingErrorKind::UnsupportedRemote); assert!(!detail.contains("DO-NOT-RENDER")); } } } }