Implement iPhone password swipe actions

This commit is contained in:
hugo committed 2026-08-11 22:13:29 +02:00
1 parent a748744425
commit 6edcb5fc86
7 files changed
+1816 -6

No files matched your search

+165 -1
View File
@@ -9,12 +9,16 @@ use crate::{
config::{Config, ConfigError},
crypto::{CryptoError, KeyInfo, KeyStore, SecretProvider, SecretProviderError},
document::{DocumentError, EntryDocument, EntryDocumentService, EntryFieldId},
git::{AutomaticEntryCommitter, GitIdentity},
git::{AutomaticEntryCommitter, AutomaticTreeCommitter, GitError, GitIdentity},
mobile_entry::{
MobileEntryDraft, MobileEntryEditorError, MobileEntryEditorFieldKind,
MobileEntryEditorInput, MobileEntryEditorPage, MobileEntryEditorSession, MobileEntryPage,
MobileEntryValueError, field_value,
},
mobile_mutation::{
MobileMutationAction, MobileMutationError, MobileMutationOutcome, MobileMutationPlan,
MobileMutationRequest, MobileMutationService,
},
mobile_totp::{MobileTotpDetail, MobileTotpError, MobileTotpPage, MobileTotpService},
recipient::RecipientPolicyManager,
repository::{
@@ -162,6 +166,7 @@ struct MobileAuthenticationStatus {
active: Option<ActiveMobileLease>,
next_editor_id: u64,
editors: BTreeMap<u64, MobileEntryDraft>,
mutation_active: bool,
watch_shared_totp_entries: std::collections::BTreeSet<EntryPath>,
}
@@ -196,6 +201,7 @@ impl MobileAuthentication {
active: None,
next_editor_id: 0,
editors: BTreeMap::new(),
mutation_active: false,
watch_shared_totp_entries: config.watch_shared_totp_entries().clone(),
}),
config,
@@ -399,6 +405,66 @@ impl MobileAuthentication {
})
}
pub fn prepare_entry_mutation(
&self,
path: &str,
action: MobileMutationAction,
) -> Result<MobileMutationPlan, MobileAuthenticationError> {
let (has_open_editor, has_dirty_editor) = self.editor_state(path)?;
MobileMutationService::new(&self.repository)
.prepare(path, action, has_open_editor, has_dirty_editor)
.map_err(mutation_error)
}
pub fn perform_entry_mutation(
&self,
request: MobileMutationRequest,
) -> Result<MobileMutationOutcome, MobileAuthenticationError> {
self.ensure_active()?;
self.reserve_entry_mutation()?;
let result = self.perform_reserved_entry_mutation(request);
self.release_entry_mutation();
result
}
fn perform_reserved_entry_mutation(
&self,
request: MobileMutationRequest,
) -> Result<MobileMutationOutcome, MobileAuthenticationError> {
let (handle, key) = {
let status = self.status()?;
let active = status.active.as_ref().ok_or_else(locked_error)?;
(active.handle.clone(), active.key.clone())
};
let mut committer = AutomaticTreeCommitter::for_source(
&self.repository,
&request.source,
GitIdentity::ironstorage(),
)
.map_err(git_mutation_error)?;
let has_open_editor = self.editor_state(&request.source)?.0;
let editors = if has_open_editor && request.discard_editor {
self.take_entry_editors(&request.source)?
} else {
Vec::new()
};
let mut provider = KeyOnlyProvider::new(handle, &key);
let result = MobileMutationService::new(&self.repository).perform(
&request,
has_open_editor && editors.is_empty(),
&self.keys,
&mut provider,
&mut committer,
);
match result {
Ok(outcome) => Ok(outcome),
Err(error) => {
self.restore_entry_editors(editors)?;
Err(mutation_error(error))
}
}
}
pub fn unlock_totp(
&self,
passphrase: Option<SecretBytes>,
@@ -766,6 +832,12 @@ impl MobileAuthentication {
draft: MobileEntryDraft,
) -> Result<MobileEntryEditorSession, MobileAuthenticationError> {
let mut status = self.status()?;
if status.mutation_active {
return Err(entry_detail(
"Password Action In Progress",
"wait for the current move, copy, or delete action to finish",
));
}
let id = status.next_editor_id;
status.next_editor_id = status.next_editor_id.checked_add(1).ok_or_else(|| {
entry_detail(
@@ -778,6 +850,66 @@ impl MobileAuthentication {
Ok(MobileEntryEditorSession::new(id, page))
}
fn reserve_entry_mutation(&self) -> Result<(), MobileAuthenticationError> {
let mut status = self.status()?;
// ponytail: serialize mobile mutations; use per-path reservations if concurrent UI needs it.
if status.mutation_active {
return Err(MobileAuthenticationError::new(
MobileAuthenticationErrorKind::Conflict,
"Password Action In Progress",
"Wait for the current move, copy, or delete action to finish.",
));
}
status.mutation_active = true;
Ok(())
}
fn release_entry_mutation(&self) {
if let Ok(mut status) = self.status.lock() {
status.mutation_active = false;
}
}
fn editor_state(&self, path: &str) -> Result<(bool, bool), MobileAuthenticationError> {
let status = self.status()?;
let mut matching = status
.editors
.values()
.filter(|draft| draft.document().path().to_string() == path);
let Some(first) = matching.next() else {
return Ok((false, false));
};
Ok((
true,
first.document().is_modified() || matching.any(|draft| draft.document().is_modified()),
))
}
fn take_entry_editors(
&self,
path: &str,
) -> Result<Vec<(u64, MobileEntryDraft)>, MobileAuthenticationError> {
let mut status = self.status()?;
let ids = status
.editors
.iter()
.filter(|(_, draft)| draft.document().path().to_string() == path)
.map(|(id, _)| *id)
.collect::<Vec<_>>();
Ok(ids
.into_iter()
.filter_map(|id| status.editors.remove(&id).map(|draft| (id, draft)))
.collect())
}
fn restore_entry_editors(
&self,
editors: Vec<(u64, MobileEntryDraft)>,
) -> Result<(), MobileAuthenticationError> {
self.status()?.editors.extend(editors);
Ok(())
}
fn restore_editor(
&self,
editor: u64,
@@ -870,6 +1002,38 @@ fn document_error(error: DocumentError) -> MobileAuthenticationError {
MobileAuthenticationError::new(kind, "Password Entry Could Not Be Saved", error.to_string())
}
fn mutation_error(error: MobileMutationError) -> MobileAuthenticationError {
MobileAuthenticationError::new(
if error.is_conflict() {
MobileAuthenticationErrorKind::Conflict
} else {
MobileAuthenticationErrorKind::Entry
},
error.title(),
error.to_string(),
)
}
fn git_mutation_error(error: GitError) -> MobileAuthenticationError {
let conflict = matches!(
&error,
GitError::DirtyWorktree | GitError::MergeConflicts { .. }
);
MobileAuthenticationError::new(
if conflict {
MobileAuthenticationErrorKind::Conflict
} else {
MobileAuthenticationErrorKind::Entry
},
if conflict {
"Password Action Blocked by Git"
} else {
"Password Action Failed"
},
error.to_string(),
)
}
fn value_error(error: MobileEntryValueError) -> MobileAuthenticationError {
entry_detail("Field Value Is Unavailable", error)
}