1use crate::OperationRejection;
4use coven_crypto::{custody::KeyError, CryptoError, MaterialError};
5use coven_database::DbError;
6use coven_foundation::files::{FileError, ObservationError, StoreLockError};
7use coven_foundation::id_source::KeyId;
8use coven_merge::Audience;
9use coven_storage::{StorageError, StorageFailure};
10use std::sync::Arc;
11
12#[derive(Clone, Copy, Debug, PartialEq, Eq)]
15pub enum CutOffRefusal {
16 DeviceNotGone,
20 NotStoreOwner,
24 NoAccountShared,
27}
28
29#[derive(Debug, thiserror::Error)]
31pub enum SyncError {
32 #[error("pending: {0:?}")]
34 Pending(Box<crate::PendingRecord>),
35 #[error("{failure:?}: {source}")]
38 Failed {
39 failure: crate::LocalFailure,
41 #[source]
43 source: Arc<dyn std::error::Error + Send + Sync>,
44 },
45 #[error("{operation}; then: {cleanup}")]
48 Cleanup {
49 operation: Box<SyncError>,
51 #[source]
53 cleanup: Box<SyncError>,
54 },
55 #[error("storage is not connected")]
57 NoStorage,
58 #[error("the store owner cannot be removed or leave the account that holds the store")]
63 StoreOwner,
64 #[error("invitation or join request is no longer current")]
66 InvitationChanged,
67 #[error("operation {0:?} is not pending")]
69 NotPending(crate::OperationId),
70 #[error("snapshot reload {0:?} must finish before publishing entries or snapshots")]
72 ReloadPending(crate::OperationId),
73 #[error("snapshot {0:?} must finish before stored files are deleted")]
76 SnapshotPending(crate::OperationId),
77 #[error("invalid operation data: {0}")]
79 OperationData(#[from] serde_json::Error),
80 #[error("the store sequence is exhausted")]
83 SequenceExhausted,
84 #[error(transparent)]
86 Storage(StorageError),
87 #[error(transparent)]
89 Database(DbError),
90 #[error(transparent)]
92 SecureStorage(KeyError),
93 #[error(transparent)]
95 Crypto(CryptoError),
96 #[error(transparent)]
98 Format(coven_format::Error),
99 #[error(transparent)]
101 Key(#[from] MaterialError),
102 #[error("key {key} is not available yet")]
104 KeyUnavailable {
105 audience: Audience,
107 key: KeyId,
109 },
110 #[error("administrative change rejected: {0:?}")]
112 Rejected(OperationRejection),
113 #[error("the device cut-off was refused: {0:?}")]
115 CutOffRefused(CutOffRefusal),
116 #[error(transparent)]
118 Damaged(#[from] crate::DamagedObject),
119 #[error("this device or its member was removed")]
121 Removed,
122 #[error("the admission of this device was refused")]
127 NotAdmitted,
128 #[error("the replacement by S3 key {access_key_id} is still under way")]
133 ReplacementPending {
134 access_key_id: String,
136 },
137 #[error("another copy uses this device's id: {0:?}")]
141 DeviceIdInUse(crate::DeviceIdEvidence),
142 #[error("member {member:?} has newer keys; held credentials cannot send")]
146 MemberKeysReplaced {
147 member: coven_foundation::id_source::MemberId,
149 },
150 #[error("an update is required: {0:?}")]
152 UpdateRequired(crate::RequiredUpdate),
153 #[error(transparent)]
155 Code(#[from] crate::CodeError),
156 #[error(transparent)]
161 Shutdown(#[from] coven_foundation::task::Shutdown),
162 #[error(transparent)]
165 Bug(#[from] coven_foundation::task::Bug),
166 #[error("{0}; it has stopped")]
171 WorkerFailed(coven_foundation::task::Bug),
172}
173
174impl From<CutOffRefusal> for SyncError {
175 fn from(refusal: CutOffRefusal) -> Self {
176 Self::CutOffRefused(refusal)
177 }
178}
179
180impl coven_foundation::task::CarriesBug for SyncError {
181 fn bug(&self) -> Option<&coven_foundation::task::Bug> {
182 match self {
183 Self::Bug(bug) => Some(bug),
184 Self::WorkerFailed(_) => None,
186 Self::Database(error) => error.bug(),
187 Self::SecureStorage(error) => error.bug(),
188 Self::Storage(error) => error.bug(),
189 Self::Cleanup { operation, cleanup } => operation.bug().or_else(|| cleanup.bug()),
190 Self::Failed { source, .. } => {
192 source.downcast_ref::<Self>().and_then(coven_foundation::task::CarriesBug::bug)
193 }
194 _ => None,
195 }
196 }
197}
198
199impl From<coven_foundation::task::TaskFailed> for SyncError {
200 fn from(failed: coven_foundation::task::TaskFailed) -> Self {
201 failed.into_error()
202 }
203}
204
205impl From<coven_database::SqliteError> for SyncError {
206 fn from(error: coven_database::SqliteError) -> Self {
207 DbError::from(error).into()
208 }
209}
210
211impl coven_database::WriteFailure for SyncError {
215 fn with_rollback(self, rollback: coven_database::SqliteError) -> Self {
216 match self {
217 Self::Database(error) => Self::Database(error.with_rollback(rollback)),
218 error => Self::cleanup(error, rollback.into()),
219 }
220 }
221
222 fn with_cleanup(write: Self, failures: Vec<DbError>) -> Self {
223 match write {
224 Self::Database(error) => Self::Database(DbError::with_cleanup(error, failures)),
225 error => failures
226 .into_iter()
227 .fold(error, |error, failure| Self::cleanup(error, failure.into())),
228 }
229 }
230}
231
232impl From<DbError> for SyncError {
233 fn from(error: DbError) -> Self {
234 match error {
235 error @ DbError::OperationChanged(_) => {
238 Self::Failed { failure: crate::LocalFailure::Database, source: Arc::new(error) }
239 }
240 error @ DbError::Invariant { .. } => {
242 Self::Failed { failure: crate::LocalFailure::Bug, source: Arc::new(error) }
243 }
244 DbError::Shutdown(shutdown) => Self::Shutdown(shutdown),
246 DbError::Bug(bug) => Self::Bug(bug),
247 DbError::StagingCleanup { operation, cleanup } => {
248 Self::cleanup((*operation).into(), (*cleanup).into())
249 }
250 error => Self::Database(error),
251 }
252 }
253}
254
255impl SyncError {
256 pub fn cleanup(operation: SyncError, cleanup: SyncError) -> Self {
259 Self::Cleanup { operation: Box::new(operation), cleanup: Box::new(cleanup) }
260 }
261
262 #[deny(clippy::wildcard_enum_match_arm)]
265 pub fn pending_reason(&self, provider: coven_storage::CloudProvider) -> crate::PendingReason {
266 use crate::PendingReason;
267 let error = self;
268 match error {
269 SyncError::Pending(record) => record.reason.clone(),
270 SyncError::Storage(error) => PendingReason::Storage {
271 provider,
272 failure: error.failure(),
273 answered: error.answered(),
274 },
275 SyncError::Damaged(object) => PendingReason::Refused(object.failure.clone()),
276 SyncError::UpdateRequired(update) => PendingReason::UpdateRequired(*update),
277 SyncError::Removed => PendingReason::Removed,
278 SyncError::NotAdmitted => PendingReason::NotAdmitted,
279 SyncError::MemberKeysReplaced { member } => {
280 PendingReason::MemberKeysReplaced { member: *member }
281 }
282 SyncError::KeyUnavailable { audience, key } => {
283 PendingReason::KeyUnavailable { audience: audience.clone(), key: *key }
284 }
285 SyncError::Rejected(reason) => PendingReason::Rejected(reason.clone()),
286 SyncError::Cleanup { operation, cleanup } => PendingReason::Cleanup {
287 operation: Box::new(operation.pending_reason(provider)),
288 cleanup: Box::new(cleanup.pending_reason(provider)),
289 },
290 SyncError::Failed { .. }
291 | SyncError::NoStorage
292 | SyncError::StoreOwner
293 | SyncError::InvitationChanged
294 | SyncError::CutOffRefused(_)
295 | SyncError::ReplacementPending { .. }
296 | SyncError::NotPending(_)
297 | SyncError::ReloadPending(_)
298 | SyncError::SnapshotPending(_)
299 | SyncError::OperationData(_)
300 | SyncError::SequenceExhausted
301 | SyncError::Database(_)
302 | SyncError::SecureStorage(_)
303 | SyncError::Crypto(_)
304 | SyncError::Format(_)
305 | SyncError::Key(_)
306 | SyncError::DeviceIdInUse(_)
307 | SyncError::Code(_)
308 | SyncError::Shutdown(_)
309 | SyncError::Bug(_)
310 | SyncError::WorkerFailed(_) => PendingReason::Failed(error.local_failure()),
311 }
312 }
313
314 pub(crate) fn disk(error: impl std::error::Error + Send + Sync + 'static) -> Self {
316 Self::Failed { failure: crate::LocalFailure::of_disk(&error), source: Arc::new(error) }
317 }
318
319 #[deny(clippy::wildcard_enum_match_arm)]
321 pub(crate) fn local_failure(&self) -> crate::LocalFailure {
322 match self {
323 Self::Failed { failure, .. } => *failure,
324 Self::Database(_) => crate::LocalFailure::Database,
325 Self::SecureStorage(_) => crate::LocalFailure::KeyCustody,
326 Self::Crypto(_) | Self::Key(_) => crate::LocalFailure::Crypto,
327 Self::Cleanup { operation, .. } => operation.local_failure(),
328 Self::Pending(_)
329 | Self::NoStorage
330 | Self::StoreOwner
331 | Self::InvitationChanged
332 | Self::ReplacementPending { .. }
333 | Self::NotPending(_)
334 | Self::ReloadPending(_)
335 | Self::SnapshotPending(_)
336 | Self::OperationData(_)
337 | Self::SequenceExhausted
338 | Self::Storage(_)
339 | Self::Format(_)
340 | Self::KeyUnavailable { .. }
341 | Self::Rejected(_)
342 | Self::CutOffRefused(_)
343 | Self::Damaged(_)
344 | Self::Removed
345 | Self::NotAdmitted
346 | Self::DeviceIdInUse(_)
347 | Self::MemberKeysReplaced { .. }
348 | Self::UpdateRequired(_)
349 | Self::Code(_) => crate::LocalFailure::Operation,
350 Self::Shutdown(_) => crate::LocalFailure::Shutdown,
351 Self::Bug(_) | Self::WorkerFailed(_) => crate::LocalFailure::Bug,
352 }
353 }
354
355 pub(crate) fn local_failure_clears_by_itself(&self) -> bool {
360 matches!(
361 self,
362 Self::Database(_)
363 | Self::SecureStorage(_)
364 | Self::Failed { .. }
365 | Self::Shutdown(_)
366 | Self::Bug(_)
367 | Self::WorkerFailed(_)
368 ) && crate::PendingReason::Failed(self.local_failure()).clears()
369 == Some(crate::pending::Clears::ByItself)
370 }
371
372 pub(crate) fn cause(&self) -> &(dyn std::error::Error + 'static) {
375 match self {
376 Self::Failed { source, .. } => source.as_ref(),
377 error => error,
378 }
379 }
380
381 pub fn into_shared(self) -> (crate::LocalFailure, Arc<dyn std::error::Error + Send + Sync>) {
384 match self {
385 Self::Failed { failure, source } => (failure, source),
386 error => (error.local_failure(), Arc::new(error)),
387 }
388 }
389}
390
391impl From<StorageError> for SyncError {
392 fn from(error: StorageError) -> Self {
393 match error {
394 StorageError::Bug(bug) => Self::Bug(bug),
395 error => Self::Storage(error),
396 }
397 }
398}
399
400impl From<KeyError> for SyncError {
401 fn from(error: KeyError) -> Self {
402 match error {
403 KeyError::Bug(bug) => Self::Bug(bug),
404 error => Self::SecureStorage(error),
405 }
406 }
407}
408
409impl From<FileError> for SyncError {
410 fn from(error: FileError) -> Self {
411 match error {
412 FileError::Bug(bug) => Self::Bug(bug),
413 error => Self::disk(error),
414 }
415 }
416}
417
418impl From<ObservationError> for SyncError {
419 fn from(error: ObservationError) -> Self {
420 Self::disk(error)
421 }
422}
423
424impl From<coven_foundation::files::WriterError> for SyncError {
425 fn from(error: coven_foundation::files::WriterError) -> Self {
426 match error {
427 coven_foundation::files::WriterError::Lock(error) => error.into(),
428 coven_foundation::files::WriterError::File(error) => error.into(),
429 }
430 }
431}
432
433impl From<StoreLockError> for SyncError {
434 fn from(error: StoreLockError) -> Self {
435 match error {
436 StoreLockError::Bug(bug) => Self::Bug(bug),
437 error => Self::disk(error),
438 }
439 }
440}
441
442#[derive(Clone, Debug, thiserror::Error)]
444pub enum SyncFailure {
445 #[error("an update is required")]
447 UpdateRequired(crate::RequiredUpdate),
448 #[error("this device or its member was removed")]
450 Removed,
451 #[error("the admission of this device was refused")]
453 NotAdmitted,
454 #[error("another copy uses this device's id: {0:?}")]
456 DeviceIdInUse(crate::DeviceIdEvidence),
457 #[error("storage failed: {0}")]
459 Storage(#[source] Arc<StorageError>),
460 #[error("coven's own work failed its last attempt: {0}")]
464 Operation(Box<crate::PendingRecord>),
465 #[error("sync failed: {0}")]
467 Other(#[source] Arc<dyn std::error::Error + Send + Sync>),
468}
469
470impl From<OperationRejection> for SyncError {
471 fn from(reason: OperationRejection) -> Self {
472 match reason {
473 OperationRejection::StoreOwner => Self::StoreOwner,
474 reason => Self::Rejected(reason),
475 }
476 }
477}
478
479impl From<StorageFailure> for SyncError {
480 fn from(failure: StorageFailure) -> Self {
481 Self::Storage(failure.into())
482 }
483}
484
485impl From<SyncError> for SyncFailure {
486 fn from(error: SyncError) -> Self {
487 match error {
488 SyncError::Removed => Self::Removed,
489 SyncError::NotAdmitted => Self::NotAdmitted,
490 SyncError::DeviceIdInUse(evidence) => Self::DeviceIdInUse(evidence),
491 SyncError::UpdateRequired(update) => Self::UpdateRequired(update),
492 SyncError::Storage(error) => Self::Storage(Arc::new(error)),
493 error => Self::Other(Arc::new(error)),
494 }
495 }
496}
497
498impl From<coven_format::Error> for SyncError {
499 fn from(error: coven_format::Error) -> Self {
500 match required_format(&error) {
501 Some(update) => Self::UpdateRequired(update),
502 None => Self::Format(error),
503 }
504 }
505}
506
507impl From<CryptoError> for SyncError {
508 fn from(error: CryptoError) -> Self {
509 Self::Crypto(error)
510 }
511}
512
513#[derive(Clone, Debug, thiserror::Error)]
515#[error("damaged object at {path}: {failure}")]
516pub struct DamagedObject {
517 pub path: String,
519 #[source]
521 pub failure: crate::Refusal,
522}
523
524pub(crate) fn required_format(
525 error: &(dyn std::error::Error + 'static),
526) -> Option<crate::RequiredUpdate> {
527 let version = match error.downcast_ref::<coven_format::Error>() {
528 Some(coven_format::Error::UnsupportedVersion(version)) => Some(*version),
529 _ => None,
530 }?;
531 (version > coven_format::FORMAT_VERSION)
532 .then_some(crate::RequiredUpdate::CovenFormat { version })
533}
534
535pub(crate) fn newer_format(error: &(dyn std::error::Error + 'static)) -> bool {
537 required_format(error).is_some()
538}
539
540#[cfg(test)]
541#[path = "error_tests.rs"]
542mod tests;