dittolive_ditto/
error.rs

1//! Ditto Error Types
2//!
3//! Modeled after [`::std::io::Error`].
4//!
5//! [`DittoError`] is the main error type used in the SDK.
6
7use std::{convert::Infallible, error::Error as ErrorTrait, fmt};
8
9use crate::{
10    auth::AuthenticationClientFeedback,
11    ffi_sdk::{self, ffi_utils::repr_c},
12};
13
14/// Custom result type for Ditto
15pub type Result<Ok, Err = DittoError> = ::core::result::Result<Ok, Err>;
16
17// FIXME(Daniel & Ham): properly clean up the errors of the Rust SDK in the next
18// breaking version, _e.g._, V5.
19#[doc(inline)]
20pub use ffi_sdk::FfiErrorCode as CoreApiErrorKind;
21
22/// Custom error for Ditto.
23///
24/// ### Recoverable errors
25///
26/// Use its [`.kind()`][Self::kind()] method to be able to `match` on a bunch of
27/// specific known [`ErrorKind`]s.
28///
29///   - ⚠️ be aware that the list of possible kinds is **not exhaustive**.
30///
31/// ### Unexpected/unrecoverable errors
32///
33/// Can be `{}`-[`Display`][::core::fmt::Display]ed for a human-friendly message
34/// and representation.
35pub struct DittoError {
36    repr: Repr,
37}
38
39pub(crate) struct FfiError {
40    pub(crate) code: ::ffi_sdk::FfiErrorCode,
41    raw: repr_c::Box<::ffi_sdk::FfiError>,
42}
43
44impl ::core::fmt::Debug for FfiError {
45    fn fmt(&self, f: &mut ::core::fmt::Formatter<'_>) -> ::core::fmt::Result {
46        f.debug_struct("FfiError")
47            .field("code", &self.code)
48            .field(
49                "description",
50                &::ffi_sdk::dittoffi_error_description(&self.raw),
51            )
52            .finish_non_exhaustive()
53    }
54}
55
56impl ::core::fmt::Display for FfiError {
57    fn fmt(&self, f: &mut ::core::fmt::Formatter<'_>) -> ::core::fmt::Result {
58        let Self { code, raw } = self;
59        write!(
60            f,
61            "{code:?}: {}",
62            ::ffi_sdk::dittoffi_error_description(raw)
63        )?;
64        Ok(())
65    }
66}
67
68impl ::std::error::Error for FfiError {}
69
70#[derive(Debug)]
71enum Repr {
72    Simple(ErrorKind),
73    Authentication(AuthenticationClientFeedback),
74    Ffi(FfiError),
75    FfiLegacy(legacy::FfiError),
76    Rust(RustError),
77    License(LicenseTokenError),
78}
79
80#[derive(Debug)]
81/// Error related to License
82pub(crate) enum LicenseTokenError {
83    VerificationFailed { message: String },
84    Expired { message: String },
85    UnsupportedFutureVersion { message: String },
86}
87
88impl LicenseTokenError {
89    /// Returns the internal error message for more details
90    pub fn message(&self) -> &String {
91        match self {
92            LicenseTokenError::VerificationFailed { message } => message,
93            LicenseTokenError::Expired { message } => message,
94            LicenseTokenError::UnsupportedFutureVersion { message } => message,
95        }
96    }
97}
98
99#[derive(Clone, Copy, Debug, PartialEq)]
100#[non_exhaustive]
101/// General kind of an error
102pub enum ErrorKind {
103    /// Configured Authentication provider was not found or failed to authenticate
104    Authentication,
105
106    /// Required configuration was missing or invalid
107    Config, // use to distinguish misconfigured from invalid query/document/etc.
108
109    /// Error originated in the FFI layer
110    FfiLegacy, // use this when you can't re-map into a Rust Error
111
112    /// An error occurred in Ditto's internals
113    Internal,
114
115    /// Provided parameters or data are not valid
116    InvalidInput, // use when user input fails validation
117
118    /// Error with the local file system or networking
119    IO, // re-maps std::io::error types (Filesystem and Network), including missing local files
120
121    /// The Ditto license is missing, expired, or otherwise invalid
122    License, // specifically when the License is not valid
123
124    /// Ditto sync was started without a valid license
125    NotActivated, // When Sync started before license validation
126
127    /// The requested resource *no longer* exists (i.e. lost or destroyed)
128    /// Used typically for empty query results
129    NonExtant, // when a requested resource doesn't exist
130
131    /// The requested Ditto instance has already been dropped
132    ReleasedDittoInstance,
133
134    /// An error occurred with one of the core APIs of Ditto.
135    CoreApi(CoreApiErrorKind),
136
137    /// Deprecation error
138    Deprecation,
139}
140
141/// A datastructure representing an error from the FFI using the legacy mechanisms
142/// (thread-local storage and integer error code).
143mod legacy {
144    #[derive(Debug)]
145    /// Errors originating in FFI code.
146    ///
147    /// Shadows the FFI's CError type which doesn't have a C Repr.
148    pub(crate) struct FfiError {
149        pub(crate) code: i32,
150        pub(crate) msg: String,
151    }
152}
153
154/// Errors originating in Rust Code
155pub(crate) struct RustError {
156    pub(crate) kind: ErrorKind,
157    pub(crate) error: Box<dyn ErrorTrait + Send + Sync>,
158}
159
160impl fmt::Debug for RustError {
161    fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> fmt::Result {
162        // Display both the ErrorKind Varriend but also the message text
163        let msg = format!("{:?} - {}", &self.kind, self.kind);
164        fmt.debug_struct("RustError")
165            .field("kind", &msg)
166            .field("error", &self.error) // Then also display underlying cause info
167            .finish()
168    }
169}
170
171impl ErrorKind {
172    fn as_str(&self) -> &'static str {
173        match *self {
174            ErrorKind::Authentication => "Unable to authenticate Ditto",
175            ErrorKind::Config => "Required configuration values are missing or invalid",
176            ErrorKind::FfiLegacy => "Unmapped Ditto Error",
177            ErrorKind::IO => "There is a problem with the underlying file, directory, or network socket",
178            ErrorKind::Internal => "Ditto encountered an internal error",
179            ErrorKind::InvalidInput => "Invalid client input provided",
180            ErrorKind::License => "License token error",
181            ErrorKind::NotActivated => "Sync could not be started because Ditto has not yet been activated. This can be achieved with a successful call to `set_license_token`. If you need to obtain a license token then please visit https://portal.ditto.live.",
182            ErrorKind::NonExtant => "The target entity can no longer be found",
183            ErrorKind::ReleasedDittoInstance => "The related Ditto instance has been closed",
184            ErrorKind::CoreApi(_) => "\
185                an error occurred from core Ditto functionality. \
186                Please use the `Display` implementation for more info.\
187            ",
188            ErrorKind::Deprecation => "Deprecated method called",
189        }
190    }
191}
192
193impl ::core::fmt::Display for ErrorKind {
194    fn fmt(&self, f: &mut ::core::fmt::Formatter<'_>) -> ::core::fmt::Result {
195        match self {
196            ErrorKind::CoreApi(ffi_error_code) => write!(f, "{ffi_error_code:?}"),
197            _ => self.as_str().fmt(f),
198        }
199    }
200}
201
202impl From<ErrorKind> for DittoError {
203    fn from(kind: ErrorKind) -> DittoError {
204        DittoError {
205            repr: Repr::Simple(kind),
206        }
207    }
208}
209
210impl fmt::Debug for DittoError {
211    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
212        fmt::Debug::fmt(&self.repr, f)
213    }
214}
215
216impl ErrorTrait for DittoError {}
217
218impl DittoError {
219    /// Construct an error from an existing Error creating an error chain
220    pub(crate) fn new<E>(kind: ErrorKind, rust_err: E) -> Self
221    where
222        E: Into<Box<dyn ErrorTrait + Send + Sync>>,
223    {
224        DittoError {
225            repr: Repr::Rust(RustError {
226                kind,
227                error: rust_err.into(),
228            }),
229        }
230    }
231}
232
233impl DittoError {
234    /// Construct an error with a specific message from a string
235    pub(crate) fn from_str(kind: ErrorKind, msg: impl Into<String>) -> DittoError {
236        let msg: String = msg.into();
237        DittoError {
238            repr: Repr::Rust(RustError {
239                kind,
240                error: msg.into(),
241            }),
242        }
243    }
244
245    /// Construct an error from a small peer info error code
246    pub(crate) fn from_small_peer_info_error_code(error_code: i32) -> DittoError {
247        match error_code {
248            -1 => Self::from_str(
249                ErrorKind::InvalidInput,
250                "The observability subsystem is unavailable",
251            ),
252            1 => Self::from_str(
253                ErrorKind::InvalidInput,
254                "The amount of data is too large according to our self-imposed limits.",
255            ),
256            2 => Self::from_str(
257                ErrorKind::InvalidInput,
258                "The amount of JSON data is too nested acccording to our self-imposed limits, or \
259                 if the data cannot be parsed to determine the depth.",
260            ),
261            3 => Self::from_str(
262                ErrorKind::InvalidInput,
263                "The data cannot be parsed as a Map<String, Value>.",
264            ),
265            _ => Self::from_str(ErrorKind::FfiLegacy, "Unmapped error"),
266        }
267    }
268
269    /// Construct an error from a LicenseTokenError
270    pub(crate) fn license(err: LicenseTokenError) -> Self {
271        DittoError {
272            repr: Repr::License(err),
273        }
274    }
275
276    /// Manually specify the Error Kind, but fetch the message from the FFI.
277    ///
278    /// The result is returned as a Rust style error with the cause of `<String as ErrorTrait>`
279    pub(crate) fn from_ffi(kind: ErrorKind) -> Self {
280        let msg = match ffi_sdk::ditto_error_message() {
281            Some(c_msg) => c_msg.into_string(),
282            None => "no message".into(),
283        };
284        DittoError::new(kind, msg)
285    }
286
287    /// Return the kind of the Error
288    pub fn kind(&self) -> ErrorKind {
289        match &self.repr {
290            Repr::Simple(kind) => *kind,
291            Repr::Rust(e) => e.kind,
292            Repr::FfiLegacy(_c) => ErrorKind::FfiLegacy, // could implement uniform code to kind
293            // mapping in future
294            Repr::License(_) => ErrorKind::License,
295            Repr::Authentication(_) => ErrorKind::Authentication,
296            Repr::Ffi(ffi) => ErrorKind::CoreApi(ffi.code),
297        }
298    }
299
300    /// If this error originated from the store's encryption layer, returns the
301    /// specific [`CoreApiErrorKind`] describing it; otherwise `None`.
302    ///
303    /// This groups the encryption-related [`CoreApiErrorKind`] variants so that
304    /// callers can react to encryption problems without matching each variant
305    /// individually. The variants covered are:
306    ///
307    ///   - [`CoreApiErrorKind::EncryptionKeyRequired`]
308    ///   - [`CoreApiErrorKind::EncryptionKeyUnexpected`]
309    ///   - [`CoreApiErrorKind::EncryptionKeyInvalid`]
310    ///   - [`CoreApiErrorKind::EncryptionUnsupportedScheme`]
311    ///   - [`CoreApiErrorKind::EncryptionMetadataCorrupt`]
312    ///
313    /// An encrypted store whose contents are corrupt fails key validation the
314    /// same way, so severe corruption of an encrypted store can also surface
315    /// as [`CoreApiErrorKind::EncryptionKeyInvalid`].
316    pub fn encryption_error_kind(&self) -> Option<CoreApiErrorKind> {
317        match self.kind() {
318            ErrorKind::CoreApi(
319                code @ (CoreApiErrorKind::EncryptionKeyRequired
320                | CoreApiErrorKind::EncryptionKeyUnexpected
321                | CoreApiErrorKind::EncryptionKeyInvalid
322                | CoreApiErrorKind::EncryptionUnsupportedScheme
323                | CoreApiErrorKind::EncryptionMetadataCorrupt),
324            ) => Some(code),
325            _ => None,
326        }
327    }
328
329    /// Returns `true` if this error originated from the store's encryption
330    /// layer (a missing, unexpected, or invalid encryption key, an unsupported
331    /// on-disk encryption scheme, or corrupt encryption metadata).
332    ///
333    /// See [`Self::encryption_error_kind()`] to recover the specific kind.
334    pub fn is_encryption_error(&self) -> bool {
335        self.encryption_error_kind().is_some()
336    }
337
338    /// If this is an Authentication error, get feedback from the auth provider.
339    ///
340    /// The returned value may contain arbitrary JSON from the auth service
341    /// with details describing the success or rejection of the authentication request.
342    pub fn get_authentication_client_feedback(&self) -> Option<AuthenticationClientFeedback> {
343        if let Repr::Authentication(ref feedback) = self.repr {
344            Some(feedback.clone())
345        } else {
346            None
347        }
348    }
349
350    // We may want to evolve how this is constructed, so let's not expose it to customers yet
351    pub(crate) fn from_authentication_feedback(feedback: AuthenticationClientFeedback) -> Self {
352        DittoError {
353            repr: Repr::Authentication(feedback),
354        }
355    }
356}
357
358impl fmt::Display for DittoError {
359    fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> fmt::Result {
360        match self.repr {
361            Repr::Simple(kind) => write!(fmt, "{}", kind),
362            Repr::Rust(ref e) => e.error.fmt(fmt), // forward to cause error
363            Repr::FfiLegacy(ref c) => write!(fmt, "{} (code {})", c.msg, c.code),
364            Repr::License(ref e) => write!(fmt, "{}", e.message()),
365            Repr::Authentication(ref feedback) => match feedback.feedback {
366                Some(ref feedback) => {
367                    write!(fmt, "Authentication Error with feedback: {}", feedback)
368                }
369                None => {
370                    write!(fmt, "Authentication Error")
371                }
372            },
373            Repr::Ffi(ref ffi_error) => ffi_error.fmt(fmt),
374        }
375    }
376}
377
378error_from_i32! {
379    i32, ::core::num::NonZeroI32
380}
381#[rustfmt::skip]
382macro_rules! error_from_i32 {(
383    $( $i32:ty ),* $(,)?
384) => (
385    $(
386        impl From<$i32> for legacy::FfiError {
387            fn from(code: $i32) -> legacy::FfiError {
388                let code: i32 = code.into();
389                debug_assert_ne!(code, 0);
390                match ffi_sdk::ditto_error_message() {
391                    Some(c_msg) => {
392                        let msg = c_msg.into_string();
393                        legacy::FfiError { code, msg }
394                    }
395                    None => legacy::FfiError {
396                        msg: "No Message".to_owned(),
397                        code,
398                    },
399                }
400            }
401        }
402
403        impl From<$i32> for DittoError {
404            fn from(code: $i32) -> Self {
405                DittoError { repr: Repr::FfiLegacy(code.into()) }
406            }
407        }
408    )*
409)}
410use error_from_i32;
411
412impl From<::serde_cbor::Error> for DittoError {
413    fn from(err: ::serde_cbor::Error) -> Self {
414        DittoError::new(ErrorKind::InvalidInput, err)
415    }
416}
417
418impl From<::serde_json::Error> for DittoError {
419    fn from(err: ::serde_json::Error) -> Self {
420        DittoError::new(ErrorKind::InvalidInput, err)
421    }
422}
423
424impl From<::std::io::Error> for DittoError {
425    fn from(err: ::std::io::Error) -> Self {
426        DittoError::new(ErrorKind::IO, err)
427    }
428}
429
430impl From<Infallible> for DittoError {
431    fn from(err: Infallible) -> Self {
432        DittoError::new(ErrorKind::Internal, err)
433    }
434}
435
436impl From<repr_c::Box_<ffi_sdk::FfiError>> for DittoError {
437    fn from(raw: repr_c::Box<ffi_sdk::FfiError>) -> Self {
438        DittoError {
439            repr: Repr::Ffi(FfiError {
440                code: ::ffi_sdk::dittoffi_error_code(&*raw),
441                raw,
442            }),
443        }
444    }
445}
446
447impl From<::tokio::task::JoinError> for DittoError {
448    fn from(err: ::tokio::task::JoinError) -> Self {
449        DittoError::new(ErrorKind::Internal, err)
450    }
451}
452
453#[cfg(test)]
454mod tests {
455    use super::*;
456
457    /// The full set of encryption-layer `CoreApiErrorKind`s that the grouping
458    /// helpers must recognize.
459    const ENCRYPTION_KINDS: &[CoreApiErrorKind] = &[
460        CoreApiErrorKind::EncryptionKeyRequired,
461        CoreApiErrorKind::EncryptionKeyUnexpected,
462        CoreApiErrorKind::EncryptionKeyInvalid,
463        CoreApiErrorKind::EncryptionUnsupportedScheme,
464        CoreApiErrorKind::EncryptionMetadataCorrupt,
465    ];
466
467    #[test]
468    fn encryption_helpers_match_every_encryption_kind() {
469        for &code in ENCRYPTION_KINDS {
470            let err = DittoError::from(ErrorKind::CoreApi(code));
471            assert!(
472                err.is_encryption_error(),
473                "is_encryption_error() should be true for {code:?}"
474            );
475            assert_eq!(
476                err.encryption_error_kind(),
477                Some(code),
478                "encryption_error_kind() should return the same kind for {code:?}"
479            );
480        }
481    }
482
483    #[test]
484    fn encryption_helpers_reject_non_encryption_kinds() {
485        // A non-`CoreApi` kind and a `CoreApi` kind that is not encryption-related
486        // must both be rejected, so the predicate does not simply match `CoreApi(_)`.
487        let non_encryption: &[ErrorKind] = &[
488            ErrorKind::IO,
489            ErrorKind::CoreApi(CoreApiErrorKind::StoreQuery),
490        ];
491        for &kind in non_encryption {
492            let err = DittoError::from(kind);
493            assert!(
494                !err.is_encryption_error(),
495                "is_encryption_error() should be false for {kind:?}"
496            );
497            assert_eq!(
498                err.encryption_error_kind(),
499                None,
500                "encryption_error_kind() should be None for {kind:?}"
501            );
502        }
503    }
504}