Files
IronStorage/crates/apple/src/lib.rs

378 lines
11 KiB
Rust

#![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<StorageTab> 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<MobileTab> 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<StorageShellState> 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<MobileShellState> 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<MobilePage>,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq, uniffi::Enum)]
pub enum MobileOnboardingPhase {
Validating,
Authenticating,
Receiving,
Integrating,
Finishing,
}
impl From<StorageOnboardingPhase> 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<String>,
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<StorageOnboardingErrorKind> 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<StorageOnboardingError> 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<MobileOnboardingDiscovery, MobileOnboardingFfiError> {
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<MobileOnboardingOutcome, MobileOnboardingFfiError> {
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<mobile::MobileShell> 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<ConfigError> 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<Arc<MobileOnboardingOperation>, 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"));
}
}
}
}