1use thiserror::Error;
4
5pub type Result<T> = std::result::Result<T, Error>;
7
8#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
13pub enum EstablishmentState {
14 NoKeyPackage,
16 HaveKeyPackage,
18 SessionPending,
20 SessionConfirmed,
22}
23
24impl EstablishmentState {
25 pub fn as_str(&self) -> &'static str {
27 match self {
28 Self::NoKeyPackage => "NoKeyPackage",
29 Self::HaveKeyPackage => "HaveKeyPackage",
30 Self::SessionPending => "SessionPending",
31 Self::SessionConfirmed => "SessionConfirmed",
32 }
33 }
34}
35
36#[non_exhaustive]
43#[derive(Debug, Clone, Copy, PartialEq, Eq)]
44pub enum SessionStateError {
45 SessionNotReady,
47 GroupNotFound,
49 NotInitialized,
51 TransportFailure,
53 CryptoFailure,
55 SessionDesync,
65 Unknown,
67}
68
69impl SessionStateError {
70 pub fn code(&self) -> &'static str {
72 match self {
73 Self::SessionNotReady => "SESSION_NOT_READY",
74 Self::GroupNotFound => "GROUP_NOT_FOUND",
75 Self::NotInitialized => "NOT_INITIALIZED",
76 Self::TransportFailure => "TRANSPORT_FAILURE",
77 Self::CryptoFailure => "CRYPTO_FAILURE",
78 Self::SessionDesync => "SESSION_DESYNC",
79 Self::Unknown => "UNKNOWN",
80 }
81 }
82
83 pub fn classify(error: &Error) -> Self {
85 match error {
86 Error::SessionNotReady(_) => Self::SessionNotReady,
87 Error::MlsNotInitialized => Self::NotInitialized,
88 Error::Transport(_) => Self::TransportFailure,
89 Error::EncryptFailed(_) => Self::CryptoFailure,
90 Error::GroupNotFound(_) => Self::GroupNotFound,
91 Error::Mls(inner) => Self::from(inner),
92 _ => Self::Unknown,
93 }
94 }
95
96 pub fn to_welcome_reason_code(&self) -> crate::events::WelcomeReasonCode {
98 match self {
99 Self::TransportFailure => crate::events::WelcomeReasonCode::TransportUnavailable,
100 Self::SessionNotReady
101 | Self::GroupNotFound
102 | Self::NotInitialized
103 | Self::CryptoFailure
104 | Self::SessionDesync
105 | Self::Unknown => crate::events::WelcomeReasonCode::InternalError,
106 }
107 }
108}
109
110impl From<&offline_protocol_mls::MlsError> for SessionStateError {
111 fn from(error: &offline_protocol_mls::MlsError) -> Self {
112 match error {
113 offline_protocol_mls::MlsError::GroupNotFound(_)
114 | offline_protocol_mls::MlsError::SessionNotFound(_) => Self::GroupNotFound,
115 offline_protocol_mls::MlsError::NotInitialized => Self::NotInitialized,
116 offline_protocol_mls::MlsError::SessionDesync(_) => Self::SessionDesync,
117 offline_protocol_mls::MlsError::Storage(_)
118 | offline_protocol_mls::MlsError::OpenMls(_)
119 | offline_protocol_mls::MlsError::Deserialization(_)
120 | offline_protocol_mls::MlsError::Serialization(_)
121 | offline_protocol_mls::MlsError::InvalidMessage(_)
122 | offline_protocol_mls::MlsError::Encryption(_)
123 | offline_protocol_mls::MlsError::Decryption(_)
124 | offline_protocol_mls::MlsError::CryptoGeneration(_)
125 | offline_protocol_mls::MlsError::Signing(_)
126 | offline_protocol_mls::MlsError::VerificationFailed(_)
127 | offline_protocol_mls::MlsError::InvalidPublicKey(_) => Self::CryptoFailure,
128 offline_protocol_mls::MlsError::CommitNotAuthorized { .. } => Self::Unknown,
136 offline_protocol_mls::MlsError::SessionIdentityMismatch { .. } => Self::Unknown,
145 offline_protocol_mls::MlsError::LeafAddressMismatch { .. }
160 | offline_protocol_mls::MlsError::UnsupportedSender { .. } => Self::Unknown,
161 _ => Self::Unknown,
162 }
163 }
164}
165
166#[non_exhaustive]
170#[derive(Debug, Error)]
171pub enum Error {
172 #[error("Protocol not started")]
174 NotStarted,
175
176 #[error("Protocol already started")]
178 AlreadyStarted,
179
180 #[error("Invalid configuration: {0}")]
182 InvalidConfiguration(String),
183
184 #[error("Core error: {0}")]
186 Core(#[from] offline_protocol_core::Error),
187
188 #[error("Transport error: {0}")]
190 Transport(#[from] offline_protocol_transport::Error),
191
192 #[error("Router error: {0}")]
194 Router(#[from] offline_protocol_router::Error),
195
196 #[error("Reliability error: {0}")]
198 Reliability(#[from] offline_protocol_reliability::Error),
199
200 #[error("MLS error: {0}")]
202 Mls(#[from] offline_protocol_mls::MlsError),
203
204 #[error("MLS encryption not initialized")]
206 MlsNotInitialized,
207
208 #[error("No key package available for recipient: {0}")]
210 NoKeyPackage(String),
211
212 #[error("Session not ready: {0:?}")]
214 SessionNotReady(EstablishmentState),
215
216 #[error("Failed to encrypt message: {0}")]
218 EncryptFailed(String),
219
220 #[error("Service error: {0}")]
222 Service(#[from] offline_protocol_services::ServiceError),
223
224 #[error("Serialization error: {0}")]
226 Serialization(String),
227
228 #[error("User is blocked: {0}")]
230 UserBlocked(String),
231
232 #[error(
236 "Too many concurrent media transfers to {0}; retry after an active transfer completes"
237 )]
238 MediaTransferLimit(String),
239
240 #[error("Group not found: {0}")]
242 GroupNotFound(String),
243
244 #[error("{0}")]
246 PermissionDenied(String),
247
248 #[error("{0}")]
251 InvalidState(String),
252
253 #[error("{0}")]
255 InvalidArgument(String),
256
257 #[error("Data layer is disabled")]
259 DataDisabled,
260
261 #[error("Data layer has no storage; initialize_mls must run first")]
267 DataStorageUnavailable,
268
269 #[error("Document is {actual} bytes compacted, over the {limit} byte cap")]
276 DocTooLarge {
277 actual: usize,
279 limit: usize,
281 },
282
283 #[error("Document data is corrupt: {0}")]
289 DataCorrupted(String),
290
291 #[error("{0}")]
293 Other(String),
294}
295
296#[cfg(test)]
297mod tests {
298 use super::{Error, SessionStateError};
299 use offline_protocol_mls::MlsError;
300 use offline_protocol_transport::Error as TransportError;
301
302 #[test]
303 fn session_state_code_values_are_stable() {
304 assert_eq!(
305 SessionStateError::SessionNotReady.code(),
306 "SESSION_NOT_READY"
307 );
308 assert_eq!(SessionStateError::GroupNotFound.code(), "GROUP_NOT_FOUND");
309 assert_eq!(SessionStateError::NotInitialized.code(), "NOT_INITIALIZED");
310 assert_eq!(
311 SessionStateError::TransportFailure.code(),
312 "TRANSPORT_FAILURE"
313 );
314 assert_eq!(SessionStateError::CryptoFailure.code(), "CRYPTO_FAILURE");
315 assert_eq!(SessionStateError::SessionDesync.code(), "SESSION_DESYNC");
316 assert_eq!(SessionStateError::Unknown.code(), "UNKNOWN");
317 }
318
319 #[test]
320 fn classify_maps_mls_group_not_found() {
321 let classified = SessionStateError::from(&MlsError::GroupNotFound("g1".to_string()));
322 assert_eq!(classified, SessionStateError::GroupNotFound);
323 }
324
325 #[test]
326 fn classify_maps_protocol_group_not_found() {
327 let classified = SessionStateError::classify(&Error::GroupNotFound("g1".to_string()));
330 assert_eq!(classified, SessionStateError::GroupNotFound);
331 }
332
333 #[test]
334 fn classify_maps_not_initialized() {
335 let classified = SessionStateError::from(&MlsError::NotInitialized);
336 assert_eq!(classified, SessionStateError::NotInitialized);
337 }
338
339 #[test]
340 fn classify_maps_crypto_failures() {
341 let classified = SessionStateError::from(&MlsError::Decryption("failed".to_string()));
342 assert_eq!(classified, SessionStateError::CryptoFailure);
343 }
344
345 #[test]
346 fn classify_maps_session_desync_distinct_from_crypto_failure() {
347 let desync = SessionStateError::from(&MlsError::SessionDesync("wrong epoch".to_string()));
351 assert_eq!(desync, SessionStateError::SessionDesync);
352 let crypto = SessionStateError::from(&MlsError::Decryption("aead".to_string()));
354 assert_eq!(crypto, SessionStateError::CryptoFailure);
355 }
356
357 #[test]
358 fn classify_unknown_fallback_is_safe() {
359 let classified = SessionStateError::classify(&Error::Other("opaque".to_string()));
360 assert_eq!(classified, SessionStateError::Unknown);
361 }
362
363 #[test]
364 fn welcome_reason_mapping_uses_typed_transport_failure() {
365 let classified = SessionStateError::classify(&Error::Transport(
366 TransportError::SendFailed("send failed".to_string()),
367 ));
368 assert_eq!(
369 classified.to_welcome_reason_code(),
370 crate::events::WelcomeReasonCode::TransportUnavailable
371 );
372 }
373
374 #[test]
375 fn welcome_reason_mapping_unknown_defaults_internal_error() {
376 let classified = SessionStateError::classify(&Error::Other("opaque".to_string()));
377 assert_eq!(
378 classified.to_welcome_reason_code(),
379 crate::events::WelcomeReasonCode::InternalError
380 );
381 }
382}