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}