Skip to main content

max / synckit

8.3 KB · 254 lines History Blame Raw
1 //! Error types for the SyncKit client SDK.
2
3 use thiserror::Error;
4
5 /// All errors that can occur in the SyncKit client.
6 ///
7 /// `#[non_exhaustive]`: new variants may be added in a minor release, so callers
8 /// must include a wildcard arm when matching. This keeps a new error kind from
9 /// being a breaking change across the consuming apps.
10 #[derive(Debug, Error)]
11 #[non_exhaustive]
12 pub enum SyncKitError {
13 /// Network-level failure: connection refused, timeout, DNS resolution, TLS handshake.
14 #[error("HTTP request failed: {0}")]
15 Http(#[from] reqwest::Error),
16
17 /// Server returned a non-success HTTP status (4xx or 5xx). Status and message
18 /// extracted from response.
19 #[error("Server returned {status}: {message}")]
20 Server {
21 status: u16,
22 message: String,
23 /// Parsed `Retry-After` header value in seconds (429 responses only).
24 /// Hidden from public API, used internally by the retry loop.
25 #[doc(hidden)]
26 retry_after_secs: Option<u64>,
27 },
28
29 /// Response body could not be parsed as expected JSON type.
30 #[error("JSON serialization error: {0}")]
31 Json(#[from] serde_json::Error),
32
33 /// Encryption method called before `setup_encryption_new` or
34 /// `setup_encryption_existing`.
35 #[error("Encryption not initialized, call setup_encryption first")]
36 NoMasterKey,
37
38 /// `unwrap_master_key` or `decrypt_data`/`decrypt_bytes` failed. Wrong
39 /// password or corrupted ciphertext.
40 #[error("Wrong password or corrupted key envelope")]
41 DecryptionFailed,
42
43 /// A downloaded blob decrypted cleanly but did not hash to its requested
44 /// content address, the server served a different-but-authentic or
45 /// rolled-back ciphertext under this hash. The bytes are discarded.
46 #[error("Blob integrity check failed: expected {expected}, got {actual}")]
47 IntegrityFailed { expected: String, actual: String },
48
49 /// Key envelope JSON has an unrecognized version or missing fields.
50 #[error("Invalid key envelope: {0}")]
51 InvalidEnvelope(String),
52
53 /// Argon2 key derivation or AEAD encryption/decryption failed (corrupt
54 /// data, wrong parameters).
55 #[error("Encryption error: {0}")]
56 Crypto(String),
57
58 /// Base64 decoding of encrypted payloads failed.
59 #[error("Base64 decode error: {0}")]
60 Base64(#[from] base64::DecodeError),
61
62 /// API method called before `authenticate` or `restore_session`.
63 #[error("Not authenticated, call authenticate first")]
64 NotAuthenticated,
65
66 /// JWT `exp` claim is within 30 seconds of current time. Caller should
67 /// re-authenticate.
68 #[error("Token expired, re-authenticate to continue syncing")]
69 TokenExpired,
70
71 /// A caller passed an invalid argument (e.g. a negative size). Distinct from
72 /// [`Self::Internal`], which signals an unexpected internal state.
73 #[error("Invalid argument: {0}")]
74 InvalidArgument(String),
75
76 /// Internal error that should not occur in normal operation.
77 #[error("Internal error: {0}")]
78 Internal(String),
79
80 /// OS keychain operation failed (store, load, or delete). Platform-specific.
81 #[cfg(feature = "keychain")]
82 #[error("Keychain error: {0}")]
83 Keychain(String),
84
85 /// A SQLite operation in the `SyncStore` engine failed.
86 #[cfg(feature = "store")]
87 #[error("Database error: {0}")]
88 Database(String),
89 }
90
91 #[cfg(feature = "keychain")]
92 impl From<keyring::Error> for SyncKitError {
93 fn from(e: keyring::Error) -> Self {
94 SyncKitError::Keychain(e.to_string())
95 }
96 }
97
98 #[cfg(feature = "store")]
99 impl From<rusqlite::Error> for SyncKitError {
100 fn from(e: rusqlite::Error) -> Self {
101 SyncKitError::Database(e.to_string())
102 }
103 }
104
105 /// Convenience alias.
106 pub type Result<T> = std::result::Result<T, SyncKitError>;
107
108 #[cfg(test)]
109 mod tests {
110 use super::*;
111 use std::error::Error;
112
113 #[test]
114 fn error_is_send_and_sync() {
115 fn assert_send_sync<T: Send + Sync>() {}
116 assert_send_sync::<SyncKitError>();
117 }
118
119 #[test]
120 fn display_all_variants() {
121 let cases: Vec<(SyncKitError, &str)> = vec![
122 (
123 SyncKitError::Server {
124 status: 500,
125 message: "boom".into(),
126 retry_after_secs: None,
127 },
128 "Server returned 500: boom",
129 ),
130 (SyncKitError::NoMasterKey, "Encryption not initialized"),
131 (SyncKitError::DecryptionFailed, "Wrong password"),
132 (
133 SyncKitError::InvalidEnvelope("bad".into()),
134 "Invalid key envelope: bad",
135 ),
136 (
137 SyncKitError::Crypto("aead".into()),
138 "Encryption error: aead",
139 ),
140 (SyncKitError::NotAuthenticated, "Not authenticated"),
141 (SyncKitError::TokenExpired, "Token expired"),
142 (
143 SyncKitError::Internal("oops".into()),
144 "Internal error: oops",
145 ),
146 ];
147 for (err, expected_substring) in cases {
148 let msg = err.to_string();
149 assert!(
150 msg.contains(expected_substring),
151 "Expected '{expected_substring}' in '{msg}'"
152 );
153 }
154 }
155
156 #[test]
157 fn debug_format_no_panic() {
158 let variants: Vec<SyncKitError> = vec![
159 SyncKitError::Server {
160 status: 500,
161 message: "err".into(),
162 retry_after_secs: None,
163 },
164 SyncKitError::NoMasterKey,
165 SyncKitError::DecryptionFailed,
166 SyncKitError::InvalidEnvelope("v".into()),
167 SyncKitError::Crypto("c".into()),
168 SyncKitError::NotAuthenticated,
169 SyncKitError::TokenExpired,
170 SyncKitError::Internal("i".into()),
171 ];
172 for v in variants {
173 let debug = format!("{v:?}");
174 assert!(!debug.is_empty());
175 }
176 }
177
178 #[test]
179 fn source_json_error() {
180 let inner = serde_json::from_str::<serde_json::Value>("bad").unwrap_err();
181 let err = SyncKitError::Json(inner);
182 assert!(err.source().is_some(), "Json variant should chain source");
183 }
184
185 #[test]
186 fn source_base64_error() {
187 use base64::Engine;
188 let inner = base64::engine::general_purpose::STANDARD
189 .decode("!!!invalid!!!")
190 .unwrap_err();
191 let err = SyncKitError::Base64(inner);
192 assert!(err.source().is_some(), "Base64 variant should chain source");
193 }
194
195 #[test]
196 fn source_none_for_leaf_variants() {
197 assert!(SyncKitError::NoMasterKey.source().is_none());
198 assert!(SyncKitError::DecryptionFailed.source().is_none());
199 assert!(SyncKitError::NotAuthenticated.source().is_none());
200 assert!(SyncKitError::TokenExpired.source().is_none());
201 assert!(SyncKitError::InvalidEnvelope("x".into()).source().is_none());
202 assert!(SyncKitError::Crypto("x".into()).source().is_none());
203 assert!(SyncKitError::Internal("x".into()).source().is_none());
204 let server = SyncKitError::Server {
205 status: 500,
206 message: "x".into(),
207 retry_after_secs: None,
208 };
209 assert!(server.source().is_none());
210 }
211
212 #[test]
213 fn server_error_empty_message() {
214 let err = SyncKitError::Server {
215 status: 503,
216 message: String::new(),
217 retry_after_secs: None,
218 };
219 let msg = err.to_string();
220 assert!(msg.contains("503"));
221 assert!(
222 msg.contains(": "),
223 "Should have colon separator even with empty message"
224 );
225 }
226
227 #[test]
228 fn server_error_very_long_message() {
229 let long = "x".repeat(1_000_000);
230 let err = SyncKitError::Server {
231 status: 500,
232 message: long,
233 retry_after_secs: None,
234 };
235 let msg = err.to_string();
236 assert!(msg.contains("500"));
237 assert!(msg.len() > 1_000_000);
238 }
239
240 #[test]
241 fn invalid_envelope_preserves_detail() {
242 let detail = "unsupported version 99";
243 let err = SyncKitError::InvalidEnvelope(detail.into());
244 assert!(err.to_string().contains(detail));
245 }
246
247 #[test]
248 fn internal_preserves_detail() {
249 let detail = "unexpected state in push handler";
250 let err = SyncKitError::Internal(detail.into());
251 assert!(err.to_string().contains(detail));
252 }
253 }
254