Skip to main content

coven/
error.rs

1//! Failures composing the store's directory, storage and custody capabilities.
2
3use crate::{
4    CryptoError, FileError, KeyError, OAuthError, StorageCheckError, StorageError, StoreLockError,
5    SyncError,
6};
7
8/// A failed setup commits no local storage settings or credentials.
9#[derive(Debug, thiserror::Error)]
10pub enum StorageSetupError {
11    /// A provider or S3 settings check refused the location.
12    #[error(transparent)]
13    Check(StorageCheckError),
14    /// Reconnecting cannot move an established store to another location.
15    #[error("storage location change is unsupported")]
16    LocationChangeUnsupported,
17    /// A classified storage failure.
18    #[error(transparent)]
19    Storage(StorageError),
20    /// Sign-in failed or was cancelled before setup could connect.
21    #[error(transparent)]
22    OAuth(#[from] OAuthError),
23    /// The facade could not commit to key custody.
24    #[error("secure storage: {0}")]
25    SecureStorage(KeyError),
26    /// This device or its member was removed from the store (§14.1).
27    #[error("this device or its member was removed")]
28    Removed,
29    /// This member's account holds the store, and setup signed in with
30    /// another account. Their access stays on the account that holds it:
31    /// the person signs in with that one (§10.1, §20.9).
32    #[error("the store's owner must sign in with the account that holds it")]
33    StoreOwner,
34    /// The store needs a newer app schema or coven format (§18).
35    #[error("an update is required: {0:?}")]
36    UpdateRequired(coven_format::RequiredUpdate),
37    /// The app cancelled setup before its point of no return, and what it
38    /// had written was undone (§11.6).
39    #[error("storage setup was cancelled and undone")]
40    Cancelled,
41    /// A setup on this device is past its point of no return and still
42    /// finishing, so another can't start; starting sync finishes it (§11.6).
43    #[error("a storage setup is still finishing")]
44    SetupRunning,
45    /// A local setup step failed, retaining its cause.
46    #[error("storage setup: {0}")]
47    Internal(#[source] Box<dyn std::error::Error + Send + Sync>),
48    /// Setup's runtime is shutting down and dropped it partway, so how much
49    /// of it was saved is unknown (E15).
50    #[error(transparent)]
51    Shutdown(#[from] coven_foundation::task::Shutdown),
52    /// Setup panicked: a bug, which the caller reports (E15). How much of it
53    /// was saved is unknown.
54    #[error(transparent)]
55    Bug(#[from] coven_foundation::task::Bug),
56}
57
58impl coven_foundation::task::CarriesBug for StorageSetupError {
59    fn bug(&self) -> Option<&coven_foundation::task::Bug> {
60        match self {
61            Self::Bug(bug) => Some(bug),
62            Self::SecureStorage(error) => error.bug(),
63            Self::Storage(error) => error.bug(),
64            Self::Check(error) => error.bug(),
65            Self::OAuth(error) => error.bug(),
66            _ => None,
67        }
68    }
69}
70
71impl From<StorageError> for StorageSetupError {
72    fn from(error: StorageError) -> Self {
73        match error {
74            StorageError::Bug(bug) => Self::Bug(bug),
75            error => Self::Storage(error),
76        }
77    }
78}
79
80/// A storage failure keeps its one spelling, `Storage`; the other refusals
81/// are checks.
82impl From<StorageCheckError> for StorageSetupError {
83    fn from(error: StorageCheckError) -> Self {
84        match error {
85            StorageCheckError::Storage(error) => Self::from(error),
86            error => Self::Check(error),
87        }
88    }
89}
90
91impl From<KeyError> for StorageSetupError {
92    fn from(error: KeyError) -> Self {
93        match error {
94            KeyError::Bug(bug) => Self::Bug(bug),
95            error => Self::SecureStorage(error),
96        }
97    }
98}
99
100impl From<coven_foundation::task::TaskFailed> for StorageSetupError {
101    fn from(failed: coven_foundation::task::TaskFailed) -> Self {
102        failed.into_error()
103    }
104}
105
106/// Creating a store failed, with publication and rollback made explicit (E1).
107#[derive(Debug, thiserror::Error)]
108pub enum StoreCreationError {
109    /// Preparing, publishing or removing the hidden directory failed.
110    #[error(transparent)]
111    Directory(coven_foundation::files::BootstrapDirectoryError),
112    /// Opening the database or custody failed.
113    #[error(transparent)]
114    Store(crate::CovenError),
115    /// The device is locked, and the protection class the app chose keeps
116    /// the new store's files from being made until it unlocks (api.md E1).
117    /// No store was made; the app waits for the unlock and creates it again.
118    #[error("{}: the device is locked, and the store's files are protected until it unlocks", path.display())]
119    DeviceLocked {
120        /// The first protected file the lock refused.
121        path: std::path::PathBuf,
122    },
123    /// Creating or persisting the member identity failed.
124    #[error(transparent)]
125    Identity(crate::IdentityError),
126    /// Closing the database or its workers failed.
127    #[error(transparent)]
128    Sync(SyncError),
129    /// Closing or rollback also failed; both causes remain available.
130    #[error("{operation}; cleanup failed: {cleanup}")]
131    Cleanup {
132        /// The original failure.
133        operation: Box<StoreCreationError>,
134        /// A cleanup failure.
135        #[source]
136        cleanup: Box<StoreCreationError>,
137    },
138    /// The complete store is visible and closed, but its directory sync failed.
139    #[error("store {store} was published but its directory sync failed: {source}")]
140    PublishedButNotSynced {
141        /// The published store; reopen it with `CovenBuilder::open`.
142        store: crate::StoreId,
143        /// The directory-sync failure, retaining any close failure as `Cleanup`.
144        #[source]
145        source: Box<StoreCreationError>,
146    },
147    /// Creation's runtime is shutting down and dropped its work (E15).
148    /// Creation runs as its own task, so with `ShutdownOutcome::Unknown` it
149    /// may have stopped anywhere, before or after publishing the store;
150    /// listing the stores shows which. A step on the blocking pool that the
151    /// runtime dropped is `NotRun`.
152    #[error(transparent)]
153    Shutdown(#[from] crate::task::Shutdown),
154    /// Creation panicked: a bug, which the caller reports (E15). As with
155    /// `Shutdown`, listing the stores shows whether it published the store.
156    #[error(transparent)]
157    Bug(#[from] crate::task::Bug),
158}
159
160impl From<coven_foundation::files::BootstrapDirectoryError> for StoreCreationError {
161    fn from(error: coven_foundation::files::BootstrapDirectoryError) -> Self {
162        if let coven_foundation::files::BootstrapDirectoryError::Bug(bug) = error {
163            return Self::Bug(bug);
164        }
165        match error.device_locked() {
166            Some(path) => Self::DeviceLocked { path: path.to_owned() },
167            None => Self::Directory(error),
168        }
169    }
170}
171
172impl From<crate::CovenError> for StoreCreationError {
173    fn from(error: crate::CovenError) -> Self {
174        match error {
175            crate::CovenError::DeviceLocked { path } => Self::DeviceLocked { path },
176            crate::CovenError::Bug(bug) => Self::Bug(bug),
177            error => Self::Store(error),
178        }
179    }
180}
181
182/// Opening the database fails as the store's open does.
183impl From<coven_database::OpenError> for StoreCreationError {
184    fn from(error: coven_database::OpenError) -> Self {
185        crate::CovenError::from(error).into()
186    }
187}
188
189impl From<crate::IdentityError> for StoreCreationError {
190    fn from(error: crate::IdentityError) -> Self {
191        match error {
192            crate::IdentityError::Bug(bug) => Self::Bug(bug),
193            error => Self::Identity(error),
194        }
195    }
196}
197
198impl From<SyncError> for StoreCreationError {
199    fn from(error: SyncError) -> Self {
200        match error {
201            SyncError::Bug(bug) => Self::Bug(bug),
202            error => Self::Sync(error),
203        }
204    }
205}
206
207impl crate::task::CarriesBug for StoreCreationError {
208    fn bug(&self) -> Option<&crate::task::Bug> {
209        match self {
210            Self::Bug(bug) => Some(bug),
211            Self::Directory(error) => error.bug(),
212            Self::Store(error) => error.bug(),
213            Self::Identity(error) => error.bug(),
214            Self::Sync(error) => error.bug(),
215            Self::Cleanup { operation, cleanup } => operation.bug().or_else(|| cleanup.bug()),
216            Self::PublishedButNotSynced { source, .. } => source.bug(),
217            _ => None,
218        }
219    }
220}
221
222impl From<crate::task::TaskFailed> for StoreCreationError {
223    fn from(failed: crate::task::TaskFailed) -> Self {
224        failed.into_error()
225    }
226}
227
228impl StoreCreationError {
229    pub(crate) fn with_cleanup(self, cleanup: Self) -> Self {
230        Self::Cleanup { operation: Box::new(self), cleanup: Box::new(cleanup) }
231    }
232}
233
234/// Deleting the local store stopped at a step the caller may retry (E1).
235#[derive(Debug, thiserror::Error)]
236pub enum StoreDeletionError {
237    /// The store is open or its lock could not be taken.
238    #[error(transparent)]
239    Lock(StoreLockError),
240    /// Writes or files wait to upload; nothing was deleted.
241    #[error("{} writes and {} files wait to upload", .0.writes, .0.files)]
242    Unsent(coven_database::UnsentWork),
243    /// Reading the queue, connecting, or journaling this device's removal
244    /// failed before the removal was journaled: nothing was deleted, nothing
245    /// in storage changed, and the store is as it was.
246    #[error(transparent)]
247    Sync(crate::SyncError),
248    /// A location is recorded for the store and this device holds no
249    /// credentials for it, so it can't publish its removal: the person
250    /// signs in to `provider`, then deletes again. Before the removal was
251    /// journaled nothing changed; after, the deletion stays recorded for
252    /// `CovenBuilder::resume_deletion`.
253    #[error("sign in to {0:?} to delete the store from this device")]
254    SignInRequired(crate::CloudProvider),
255    /// This device's removal is journaled, and the deletion failed before it
256    /// applied, with `0`'s cause. The deletion only goes forward: the handle
257    /// is closed, the store lists as being deleted and refuses opens, and
258    /// `CovenBuilder::resume_deletion` finishes it.
259    #[error("the store is being deleted from this device; finishing it failed: {0}")]
260    Unfinished(#[source] crate::SyncError),
261    /// This device's removal applied, or no storage was set up, and closing
262    /// the handle failed; nothing was deleted. A recorded deletion is
263    /// finished by `CovenBuilder::resume_deletion`, and either by
264    /// `Coven::discard_store`.
265    #[error("closing the store failed after this device left it: {0}")]
266    Closing(#[source] crate::SyncError),
267    /// Opening the store to finish its recorded deletion failed.
268    #[error(transparent)]
269    Open(crate::CovenError),
270    /// `resume_deletion` found no deletion of the store recorded on this
271    /// device.
272    #[error("no deletion of store {0} is under way")]
273    NoUnfinishedDeletion(crate::StoreId),
274    /// Removing a keychain entry failed.
275    #[error(transparent)]
276    Key(KeyError),
277    /// Removing the store directory failed.
278    #[error(transparent)]
279    File(FileError),
280    /// The deletion's runtime is shutting down and dropped it before it
281    /// started, so nothing was deleted (E15).
282    #[error(transparent)]
283    Shutdown(#[from] crate::task::Shutdown),
284    /// The deletion panicked: a bug, which the caller reports (E15).
285    /// Retrying finishes a deletion that stopped partway.
286    #[error(transparent)]
287    Bug(#[from] crate::task::Bug),
288}
289
290impl crate::task::CarriesBug for StoreDeletionError {
291    fn bug(&self) -> Option<&crate::task::Bug> {
292        match self {
293            Self::Bug(bug) => Some(bug),
294            Self::Lock(error) => error.bug(),
295            Self::Sync(error) | Self::Closing(error) | Self::Unfinished(error) => error.bug(),
296            Self::Open(error) => error.bug(),
297            Self::Key(error) => error.bug(),
298            Self::File(error) => error.bug(),
299            _ => None,
300        }
301    }
302}
303
304impl From<crate::SyncError> for StoreDeletionError {
305    fn from(error: crate::SyncError) -> Self {
306        match error {
307            crate::SyncError::Bug(bug) => Self::Bug(bug),
308            error => Self::Sync(error),
309        }
310    }
311}
312
313impl From<KeyError> for StoreDeletionError {
314    fn from(error: KeyError) -> Self {
315        match error {
316            KeyError::Bug(bug) => Self::Bug(bug),
317            error => Self::Key(error),
318        }
319    }
320}
321
322impl From<FileError> for StoreDeletionError {
323    fn from(error: FileError) -> Self {
324        match error {
325            FileError::Bug(bug) => Self::Bug(bug),
326            error => Self::File(error),
327        }
328    }
329}
330
331impl From<StoreLockError> for StoreDeletionError {
332    fn from(error: StoreLockError) -> Self {
333        match error {
334            StoreLockError::Bug(bug) => Self::Bug(bug),
335            error => Self::Lock(error),
336        }
337    }
338}
339
340impl From<crate::task::TaskFailed> for StoreDeletionError {
341    fn from(failed: crate::task::TaskFailed) -> Self {
342        failed.into_error()
343    }
344}
345
346/// Opening a read-only store or its saved file-storage connection failed.
347#[derive(Debug, thiserror::Error)]
348pub enum ReadOnlyOpenError {
349    /// The database or store directory could not be opened.
350    #[error(transparent)]
351    Local(crate::CovenError),
352    /// Saved settings, credential custody or provider construction failed.
353    #[error(transparent)]
354    Storage(crate::SyncError),
355    /// Opening panicked: a bug, which the caller reports (E15).
356    #[error(transparent)]
357    Bug(#[from] crate::task::Bug),
358}
359
360impl From<crate::CovenError> for ReadOnlyOpenError {
361    fn from(error: crate::CovenError) -> Self {
362        match error {
363            crate::CovenError::Bug(bug) => Self::Bug(bug),
364            error => Self::Local(error),
365        }
366    }
367}
368
369/// A database call's or opening's failure is the store's.
370impl From<crate::DbError> for ReadOnlyOpenError {
371    fn from(error: crate::DbError) -> Self {
372        crate::CovenError::from(error).into()
373    }
374}
375
376impl From<coven_database::OpenError> for ReadOnlyOpenError {
377    fn from(error: coven_database::OpenError) -> Self {
378        crate::CovenError::from(error).into()
379    }
380}
381
382impl From<crate::SyncError> for ReadOnlyOpenError {
383    fn from(error: crate::SyncError) -> Self {
384        match error {
385            crate::SyncError::Bug(bug) => Self::Bug(bug),
386            error => Self::Storage(error),
387        }
388    }
389}
390
391impl crate::task::CarriesBug for ReadOnlyOpenError {
392    fn bug(&self) -> Option<&crate::task::Bug> {
393        match self {
394            Self::Bug(bug) => Some(bug),
395            Self::Local(error) => error.bug(),
396            Self::Storage(error) => error.bug(),
397        }
398    }
399}
400
401/// Opening this member's sealed store keys failed.
402#[derive(Debug, thiserror::Error)]
403pub enum StoreKeyUnlockError {
404    /// No stored provider configuration or credentials are available.
405    #[error("no storage configured")]
406    NoStorage,
407    /// The provider refused or failed a request.
408    #[error(transparent)]
409    Storage(StorageError),
410    /// Unlocking connected while a storage setup past its point of no
411    /// return was still finishing, so it finished that setup (§11.6), and
412    /// making a key or sealing a key copy for an operation settled there
413    /// failed: the device's random source failed, or the recipient's
414    /// sealing key is of low order.
415    #[error(transparent)]
416    Crypto(CryptoError),
417    /// Custody refused to read or persist keys.
418    #[error(transparent)]
419    SecureStorage(KeyError),
420    /// This device or its member was removed from the store (§14.1).
421    #[error("this device or its member was removed")]
422    Removed,
423    /// The store needs a newer app schema or coven format (§18).
424    #[error("an update is required: {0:?}")]
425    UpdateRequired(crate::RequiredUpdate),
426    /// Sequence validation or local work failed, preserving its cause.
427    #[error("opening store keys: {0}")]
428    Other(#[source] Box<dyn std::error::Error + Send + Sync>),
429    /// Unlocking's runtime is shutting down and dropped it partway, so
430    /// whether it saved the keys it opened is unknown (E15).
431    #[error(transparent)]
432    Shutdown(#[from] crate::task::Shutdown),
433    /// Unlocking panicked: a bug, which the caller reports (E15). Whether
434    /// it saved the keys it opened is unknown.
435    #[error(transparent)]
436    Bug(#[from] crate::task::Bug),
437}
438
439impl crate::task::CarriesBug for StoreKeyUnlockError {
440    fn bug(&self) -> Option<&crate::task::Bug> {
441        match self {
442            Self::Bug(bug) => Some(bug),
443            Self::Storage(error) => error.bug(),
444            Self::SecureStorage(error) => error.bug(),
445            _ => None,
446        }
447    }
448}
449
450impl From<StorageError> for StoreKeyUnlockError {
451    fn from(error: StorageError) -> Self {
452        match error {
453            StorageError::Bug(bug) => Self::Bug(bug),
454            error => Self::Storage(error),
455        }
456    }
457}
458
459impl From<KeyError> for StoreKeyUnlockError {
460    fn from(error: KeyError) -> Self {
461        match error {
462            KeyError::Bug(bug) => Self::Bug(bug),
463            error => Self::SecureStorage(error),
464        }
465    }
466}
467
468impl From<crate::task::TaskFailed> for StoreKeyUnlockError {
469    fn from(failed: crate::task::TaskFailed) -> Self {
470        failed.into_error()
471    }
472}
473
474/// What setup and unlock report by name: a removal or a required update,
475/// whether the pass returned it or stopped at a record of it.
476enum Stopped {
477    Removed,
478    UpdateRequired(crate::RequiredUpdate),
479}
480
481fn stopped(error: &SyncError) -> Option<Stopped> {
482    let reason = match error {
483        // A joined store whose admission was refused never became a member:
484        // shown as a removal is (§13.2).
485        SyncError::Removed | SyncError::NotAdmitted => return Some(Stopped::Removed),
486        SyncError::UpdateRequired(update) => return Some(Stopped::UpdateRequired(*update)),
487        SyncError::Pending(record) => &record.reason,
488        _ => return None,
489    };
490    match reason {
491        crate::PendingReason::Removed | crate::PendingReason::NotAdmitted => Some(Stopped::Removed),
492        crate::PendingReason::UpdateRequired(update) => Some(Stopped::UpdateRequired(*update)),
493        _ => None,
494    }
495}
496
497pub(crate) fn setup_error(error: SyncError) -> StorageSetupError {
498    match stopped(&error) {
499        Some(Stopped::Removed) => return StorageSetupError::Removed,
500        Some(Stopped::UpdateRequired(update)) => return StorageSetupError::UpdateRequired(update),
501        None => {}
502    }
503    match error {
504        SyncError::SecureStorage(error) => error.into(),
505        SyncError::Storage(error) => error.into(),
506        SyncError::StoreOwner => StorageSetupError::StoreOwner,
507        SyncError::Shutdown(shutdown) => StorageSetupError::Shutdown(shutdown),
508        SyncError::Bug(bug) => StorageSetupError::Bug(bug),
509        error => StorageSetupError::Internal(Box::new(error)),
510    }
511}
512
513pub(crate) fn unlock_error(error: SyncError) -> StoreKeyUnlockError {
514    match stopped(&error) {
515        Some(Stopped::Removed) => return StoreKeyUnlockError::Removed,
516        Some(Stopped::UpdateRequired(update)) => {
517            return StoreKeyUnlockError::UpdateRequired(update)
518        }
519        None => {}
520    }
521    match error {
522        SyncError::NoStorage => StoreKeyUnlockError::NoStorage,
523        SyncError::SecureStorage(error) => error.into(),
524        SyncError::Crypto(error) => StoreKeyUnlockError::Crypto(error),
525        SyncError::Storage(error) => error.into(),
526        SyncError::Shutdown(shutdown) => StoreKeyUnlockError::Shutdown(shutdown),
527        SyncError::Bug(bug) => StoreKeyUnlockError::Bug(bug),
528        error => StoreKeyUnlockError::Other(Box::new(error)),
529    }
530}
531
532#[cfg(test)]
533#[path = "error_tests.rs"]
534mod tests;