Skip to main content

coven_storage/
error.rs

1use crate::CloudProvider;
2
3/// Failures the app can distinguish and act on (Appendix F.5).
4#[derive(Clone, Copy, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
5pub enum StorageFailure {
6    /// The platform or OS version cannot use this built-in provider.
7    ProviderUnavailable,
8    /// The app did not register the access required by this operation.
9    AccessNotRegistered {
10        /// Access the app must register before this operation is available.
11        required: crate::OAuthAccess,
12    },
13    /// The authorization did not grant the required access.
14    AccessNotGranted {
15        /// Access the account must authorize.
16        required: crate::OAuthAccess,
17    },
18    /// An App-folder store cannot be shared without relocating it.
19    DropboxAppFolderNotShareable,
20    /// The installation must complete membership and key-copy catch-up before sending.
21    CatchUpRequired,
22    /// No route to the provider, a timeout, an interrupted response, or a
23    /// server failure the provider answered with (§6.7).
24    Network,
25    /// Two complete reads of one immutable object returned different bytes:
26    /// a fault on the way to the device changed one of them (§19.1).
27    Corrupted,
28    /// Credentials were refused or must be refreshed.
29    Authentication,
30    /// The account is signed in but cannot perform this operation.
31    PermissionDenied,
32    /// The requested object is absent.
33    NotFound,
34    /// A create-once path is occupied.
35    AlreadyExists,
36    /// A concurrent storage mutation defeated a conditional request.
37    Conflict,
38    /// The bucket, folder or zone is absent.
39    ContainerNotFound,
40    /// The S3 endpoint or signing region is wrong.
41    RegionMismatch,
42    /// Setting up a store that never had storage found another installation's
43    /// creation slot: another installation of the store claimed this
44    /// location first (§10.3). Restore or join it instead.
45    StoreExists,
46    /// S3 rejected the device clock after one retry with the offset its
47    /// response stated (§20.7).
48    ClockSkew,
49    /// Every member reaches the whole location (a Dropbox shared folder, a
50    /// OneDrive folder, or an S3 bucket), so a store there needs it to
51    /// itself, and it holds something besides this store (§20.3). At first
52    /// setup: an object or folder outside the store's own, or a location
53    /// marker that doesn't name it. At a reconnect, restore or join: a marker
54    /// that is missing, malformed or names another store.
55    OwnLocationRequired,
56    /// The provider has no space left.
57    QuotaExceeded,
58    /// The provider requests a later retry.
59    RateLimited,
60    /// The request's configuration is invalid.
61    InvalidConfiguration,
62    /// The provider refused the request for another reason.
63    Refused,
64    /// The response or recorded session is malformed.
65    Protocol,
66    /// An object path is outside the store's layout.
67    InvalidPath,
68    /// A range is empty, reversed or beyond the object's end.
69    InvalidRange,
70    /// Coven changes sharing only from the account holding the store. That is
71    /// coven's rule, not every provider's limit (§13.2).
72    NotStoreOwner,
73    /// The invited account is the one holding the store (§13.2).
74    InviteeIsStoreOwner,
75    /// The provider refused to share the store for a reason only a person
76    /// can fix, the invited address's or the inviting account's, so no retry
77    /// changes the answer (§13.2).
78    SharingRefused(coven_foundation::sharing::SharingRefusal),
79    /// Dropbox cannot upgrade a pending viewer without the recipient's account id.
80    AccountIdUnavailable,
81    /// The invite belongs to another provider location.
82    InvitationMismatch,
83    /// The session belongs to another provider or location.
84    SessionMismatch,
85    /// The recorded upload expired or a publication conflict invalidated it.
86    SessionExpired,
87    /// The native id of an unresolved S3 initiation cannot be proven to belong to this session.
88    SessionIdentityUnknown,
89    /// A part disagrees with the session's offset, size or alignment.
90    InvalidPart,
91    /// A buffered floor replacement exceeds this provider's single-request limit.
92    /// Recorded replacements use `Storage::begin_replace` instead.
93    SingleRequestTooLarge {
94        /// Encrypted body length in bytes.
95        size: u64,
96        /// The adapter's single-request limit in bytes.
97        limit: u64,
98    },
99    /// An S3 object published by `CopyObject` exceeds its single-copy limit,
100    /// so this profile refuses it (§20.6 says why).
101    CopyTooLarge {
102        /// Encrypted body length in bytes.
103        size: u64,
104        /// The single-copy limit in bytes.
105        limit: u64,
106    },
107    /// Parsing recorded provider data failed.
108    Encoding,
109    /// Local file access or persisting provider settings or upload progress failed.
110    File,
111    /// This installation has no member keys in custody.
112    MemberKeysMissing,
113    /// Coven's work for the request panicked: a bug, which the caller
114    /// reports (E15). `StorageError::Bug` carries it.
115    Bug,
116}
117
118/// A storage error preserves its cause without converting it into text.
119#[derive(Debug, thiserror::Error)]
120pub enum StorageError {
121    /// A classified failure with no underlying native error.
122    #[error("{0:?}")]
123    Failure(StorageFailure),
124    /// A classified local failure with its diagnostic or native cause.
125    #[error("{failure:?}: {source}")]
126    Caused {
127        /// The failure the app can act on.
128        failure: StorageFailure,
129        /// The diagnostic or original error.
130        #[source]
131        source: Box<dyn std::error::Error + Send + Sync>,
132    },
133    /// A provider failure, classified for the app, with its original cause.
134    #[error("{provider:?}: {failure:?}: {source}")]
135    Provider {
136        /// Provider that failed.
137        provider: CloudProvider,
138        /// Actionable classification.
139        failure: StorageFailure,
140        /// Original transport, SDK or framework error.
141        #[source]
142        source: Box<dyn std::error::Error + Send + Sync>,
143    },
144    /// Cleanup failed too; neither cause is hidden.
145    #[error("{operation}; cleanup also failed: {cleanup}")]
146    Cleanup {
147        /// The original failure.
148        #[source]
149        operation: Box<StorageError>,
150        /// Failure removing temporary data or recording the failed upload state.
151        cleanup: Box<StorageError>,
152    },
153    /// Coven's work for the request panicked: a bug, which the caller
154    /// reports (E15). Its failure is `StorageFailure::Bug`.
155    #[error(transparent)]
156    Bug(#[from] coven_foundation::task::Bug),
157}
158
159impl coven_foundation::task::CarriesBug for StorageError {
160    fn bug(&self) -> Option<&coven_foundation::task::Bug> {
161        match self {
162            Self::Bug(bug) => Some(bug),
163            Self::Cleanup { operation, cleanup } => operation.bug().or_else(|| cleanup.bug()),
164            Self::Failure(_) | Self::Caused { .. } | Self::Provider { .. } => None,
165        }
166    }
167}
168
169impl StorageError {
170    /// Native destination conflicts mean a failed condition during replacement.
171    pub(crate) fn replacement(mut self) -> Self {
172        match &mut self {
173            Self::Failure(failure)
174            | Self::Caused { failure, .. }
175            | Self::Provider { failure, .. }
176                if *failure == StorageFailure::AlreadyExists =>
177            {
178                *failure = StorageFailure::Conflict
179            }
180            _ => {}
181        }
182        self
183    }
184
185    /// What the app can do about this failure.
186    pub fn failure(&self) -> StorageFailure {
187        match self {
188            Self::Provider { failure, .. } => *failure,
189            Self::Failure(failure) | Self::Caused { failure, .. } => *failure,
190            Self::Cleanup { operation, .. } => operation.failure(),
191            Self::Bug(_) => StorageFailure::Bug,
192        }
193    }
194
195    /// The failure when the request alone failed: none when its cleanup
196    /// failed too, which leaves state behind that a caller handling the
197    /// failure itself, such as an occupied path or a throttled request,
198    /// doesn't know of.
199    pub fn own_failure(&self) -> Option<StorageFailure> {
200        match self {
201            Self::Cleanup { .. } => None,
202            _ => Some(self.failure()),
203        }
204    }
205
206    /// Whether the provider answered the request behind this failure, even
207    /// with an error (E5). `Network` alone cannot say: a server's error
208    /// response is an answer; a refused connection or a timeout is not.
209    pub fn answered(&self) -> bool {
210        match self {
211            Self::Provider { source, .. } => crate::providers::response_arrived(source.as_ref()),
212            Self::Cleanup { operation, .. } => operation.answered(),
213            Self::Failure(_) | Self::Caused { .. } | Self::Bug(_) => false,
214        }
215    }
216
217    /// Whether the provider answered this request with a refusal, so it
218    /// stored nothing. A timeout, or a lost, malformed or server-failure
219    /// reply, leaves the outcome unknown. The clients resend a request only
220    /// after a refusal (a rejected token, a skewed clock), so no earlier
221    /// attempt of it stored anything either.
222    pub fn refused(&self) -> bool {
223        self.answered()
224            && !matches!(self.failure(), StorageFailure::Network | StorageFailure::Protocol)
225    }
226
227    /// The wait a provider response asked every consumer to observe, from
228    /// its `Retry-After`, Dropbox's `retry_after` or CloudKit's
229    /// `CKErrorRetryAfterKey`.
230    pub(crate) fn retry_after(&self) -> Option<crate::RetryAfter> {
231        match self {
232            Self::Provider { provider, source, .. } => {
233                crate::providers::retry_after(*provider, source.as_ref())
234            }
235            Self::Cleanup { operation, .. } => operation.retry_after(),
236            Self::Failure(_) | Self::Caused { .. } | Self::Bug(_) => None,
237        }
238    }
239
240    /// Transport interruptions and provider throttling invite retry.
241    pub fn retryable(&self) -> bool {
242        matches!(self.failure(), StorageFailure::Network | StorageFailure::RateLimited)
243    }
244}
245
246/// The provider operation checked before setup commits anything (E5).
247#[derive(Clone, Copy, Debug, PartialEq, Eq)]
248pub enum StorageCheck {
249    /// Create a fresh sealed object.
250    Create,
251    /// Refuse a second create at the same path.
252    CreateOnce,
253    /// Replace the observed object and refuse its stale revision without changing bytes.
254    ConditionalReplace,
255    /// Read the original bytes in full.
256    Read,
257    /// Read exactly an interior byte range.
258    ReadRange,
259    /// Run the scoped query and check returned probe metadata; fresh objects may be omitted.
260    List,
261    /// Delete the object and confirm it is absent.
262    Delete,
263    /// S3: multipart publication refuses an occupied name with its bytes and
264    /// time unchanged, and conditional completion hides unfinished uploads.
265    MultipartCreateOnce,
266    /// S3: multipart objects are dated by complete publication, as recorded.
267    PublicationTime,
268    /// S3: another setting agrees with the service's observed behavior.
269    Behavior,
270}
271
272/// A provider or S3 settings check refused the location (§20.7, §20.8).
273#[derive(Debug, thiserror::Error)]
274pub enum StorageCheckError {
275    /// This profile has no certification with bucket versioning enabled.
276    #[error("S3 bucket versioning {state:?} is not certified for this profile")]
277    S3VersioningUnsupported {
278        /// The bucket's observed versioning state.
279        state: crate::S3BucketVersioning,
280    },
281    /// The bucket state is unknown and this profile has no versioning certification.
282    #[error("S3 bucket versioning is unknown; this profile requires a successful GetBucketVersioning (s3:GetBucketVersioning; GCS storage.buckets.get): {source}")]
283    S3VersioningUnknown {
284        /// The refused versioning read, retaining its native cause.
285        #[source]
286        source: std::sync::Arc<StorageError>,
287    },
288    /// A provider check failed; cleanup failures retain both causes.
289    #[error("storage check {check:?}: {source}")]
290    ProviderCheck {
291        /// The operation that failed.
292        check: StorageCheck,
293        /// The original failure, including cleanup if that also failed.
294        #[source]
295        source: StorageError,
296    },
297    /// A classified storage failure.
298    #[error(transparent)]
299    Storage(StorageError),
300}
301
302impl coven_foundation::task::CarriesBug for StorageCheckError {
303    fn bug(&self) -> Option<&coven_foundation::task::Bug> {
304        match self {
305            Self::Storage(error) | Self::ProviderCheck { source: error, .. } => error.bug(),
306            Self::S3VersioningUnknown { source } => source.bug(),
307            Self::S3VersioningUnsupported { .. } => None,
308        }
309    }
310}
311
312impl From<StorageError> for StorageCheckError {
313    fn from(error: StorageError) -> Self {
314        Self::Storage(error)
315    }
316}
317
318impl StorageFailure {
319    /// Retain the diagnostic or native cause alongside this classification.
320    pub fn with_source(
321        self,
322        source: impl Into<Box<dyn std::error::Error + Send + Sync>>,
323    ) -> StorageError {
324        StorageError::Caused { failure: self, source: source.into() }
325    }
326}
327
328impl From<StorageFailure> for StorageError {
329    fn from(failure: StorageFailure) -> Self {
330        Self::Failure(failure)
331    }
332}
333
334impl From<coven_foundation::files::FileError> for StorageError {
335    fn from(error: coven_foundation::files::FileError) -> Self {
336        match error {
337            coven_foundation::files::FileError::Bug(bug) => Self::Bug(bug),
338            error => StorageFailure::File.with_source(error),
339        }
340    }
341}
342
343#[cfg(test)]
344#[path = "error_tests.rs"]
345mod tests;
346
347/// A code's storage part that the format's primitives refuse: truncated,
348/// trailing bytes, an unknown tag or a length past its bound (format.md D13).
349impl From<coven_format::Error> for StorageError {
350    fn from(error: coven_format::Error) -> Self {
351        StorageFailure::Encoding.with_source(error)
352    }
353}
354
355impl From<coven_format::path::PathError> for StorageError {
356    fn from(error: coven_format::path::PathError) -> Self {
357        StorageFailure::InvalidPath.with_source(error)
358    }
359}