//! Storage-owned service boundary for the Iced desktop presentation adapter. use std::{error::Error, fmt, path::Path}; use crate::{ authentication::{NativeAuthenticationHandle, NativeAuthenticationSession}, config::Config, crypto::{KeyInfo, KeyStore, SecretProvider}, document::{DocumentError, EntryDocument, EntryDocumentService}, git::{AutomaticEntryCommitter, GitIdentity}, presentation::ClipboardTimeout, read::{TreeModel, VaultReader}, repository::{DirectoryPath, Repository}, secret_store::SecretProtectionPolicy, write::{WriteError, WriteOutcome}, }; #[derive(Clone, Copy, Debug, Eq, PartialEq)] pub enum DesktopErrorKind { Configuration, Authentication, KeyMaterial, Repository, Read, Document, Git, Conflict, Unchanged, MissingDefaultKey, } #[derive(Debug)] pub struct DesktopError { kind: DesktopErrorKind, message: String, } impl DesktopError { pub fn kind(&self) -> DesktopErrorKind { self.kind } fn new(kind: DesktopErrorKind, error: impl fmt::Display) -> Self { Self { kind, message: error.to_string(), } } fn document(error: DocumentError) -> Self { let kind = match &error { DocumentError::Write(WriteError::ConcurrentModification { .. }) => { DesktopErrorKind::Conflict } DocumentError::Write(WriteError::Unchanged) => DesktopErrorKind::Unchanged, _ => DesktopErrorKind::Document, }; Self::new(kind, error) } } impl fmt::Display for DesktopError { fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { formatter.write_str(&self.message) } } impl Error for DesktopError {} #[derive(Clone, Debug)] pub struct DesktopStorage { config: Config, } impl DesktopStorage { pub fn load(explicit: Option<&Path>) -> Result { Config::load(explicit) .map(|config| Self { config }) .map_err(|error| DesktopError::new(DesktopErrorKind::Configuration, error)) } pub fn system() -> Result { let storage = Self::load(None)?; let keys = KeyStore::load(storage.config.key_material()) .map_err(|error| DesktopError::new(DesktopErrorKind::KeyMaterial, error))?; let handle = keys .resolve(storage.config.default_key().as_str()) .map_err(|error| DesktopError::new(DesktopErrorKind::KeyMaterial, error))?; let key = keys .infos() .find(|key| key.fingerprint() == handle.fingerprint()) .ok_or_else(|| { DesktopError::new( DesktopErrorKind::MissingDefaultKey, "the configured GPG key is unavailable", ) })?; let authentication = NativeAuthenticationSession::system( SecretProtectionPolicy::default(), storage.config.authentication_timeout(), ) .map_err(|error| DesktopError::new(DesktopErrorKind::Authentication, error))?; Ok(DesktopBootstrap { storage, authentication, key, }) } pub fn clipboard_timeout(&self) -> ClipboardTimeout { self.config.clipboard_timeout() } pub fn tree(&self) -> Result { let repository = self.repository()?; let keys = self.keys()?; VaultReader::new(&repository, &keys) .list(&DirectoryPath::root()) .map_err(|error| DesktopError::new(DesktopErrorKind::Read, error)) } pub fn open_document( &self, entry: &str, secrets: &mut impl SecretProvider, ) -> Result { let repository = self.repository()?; let keys = self.keys()?; EntryDocumentService::new(&repository, &keys) .open(entry, secrets) .map_err(DesktopError::document) } pub fn save_document(&self, document: &EntryDocument) -> Result { let repository = self.repository()?; let keys = self.keys()?; let entry = document.path().to_string(); let mut committer = AutomaticEntryCommitter::for_entry(&repository, &entry, GitIdentity::ironstorage()) .map_err(|error| DesktopError::new(DesktopErrorKind::Git, error))?; EntryDocumentService::new(&repository, &keys) .save_recoverable(document, None, &mut committer) .map_err(DesktopError::document) } pub fn save_active_document( &self, handle: &NativeAuthenticationHandle, document: &EntryDocument, ) -> Result { handle .ensure_active() .map_err(|error| DesktopError::new(DesktopErrorKind::Authentication, error))?; self.save_document(document) } fn repository(&self) -> Result { Repository::open(self.config.vault()) .map_err(|error| DesktopError::new(DesktopErrorKind::Repository, error)) } fn keys(&self) -> Result { KeyStore::load(self.config.key_material()) .map_err(|error| DesktopError::new(DesktopErrorKind::KeyMaterial, error)) } } pub struct DesktopBootstrap { storage: DesktopStorage, authentication: NativeAuthenticationSession, key: KeyInfo, } impl DesktopBootstrap { pub fn into_parts(self) -> (DesktopStorage, NativeAuthenticationSession, KeyInfo) { (self.storage, self.authentication, self.key) } }