Onboard iPhone password-store clone

This commit is contained in:
2026-08-11 16:33:08 +02:00
parent db898152bf
commit daf122aff3
11 changed files with 2770 additions and 38 deletions

View File

@@ -3,11 +3,16 @@
//! Mechanical UniFFI exports for Apple presentation code.
use std::{error::Error, fmt};
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!();
@@ -92,6 +97,146 @@ pub struct MobileShell {
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 {
@@ -156,9 +301,39 @@ pub fn set_selected_mobile_tab(tab: MobileTab) -> Result<(), MobilePreferenceErr
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::{MobileShellState, MobileTab};
use super::{MobileOnboardingErrorKind, MobileOnboardingFfiError, MobileShellState, MobileTab};
#[test]
fn bridge_reads_product_name_from_storage_crate() {
@@ -180,4 +355,23 @@ mod tests {
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"));
}
}
}
}