378 lines
11 KiB
Rust
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"));
|
|
}
|
|
}
|
|
}
|
|
}
|