use thiserror::Error;
pub type Result<T> = std::result::Result<T, Error>;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum EstablishmentState {
NoKeyPackage,
HaveKeyPackage,
SessionPending,
SessionConfirmed,
}
impl EstablishmentState {
pub fn as_str(&self) -> &'static str {
match self {
Self::NoKeyPackage => "NoKeyPackage",
Self::HaveKeyPackage => "HaveKeyPackage",
Self::SessionPending => "SessionPending",
Self::SessionConfirmed => "SessionConfirmed",
}
}
}
#[non_exhaustive]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SessionStateError {
SessionNotReady,
GroupNotFound,
NotInitialized,
TransportFailure,
CryptoFailure,
SessionDesync,
Unknown,
}
impl SessionStateError {
pub fn code(&self) -> &'static str {
match self {
Self::SessionNotReady => "SESSION_NOT_READY",
Self::GroupNotFound => "GROUP_NOT_FOUND",
Self::NotInitialized => "NOT_INITIALIZED",
Self::TransportFailure => "TRANSPORT_FAILURE",
Self::CryptoFailure => "CRYPTO_FAILURE",
Self::SessionDesync => "SESSION_DESYNC",
Self::Unknown => "UNKNOWN",
}
}
pub fn classify(error: &Error) -> Self {
match error {
Error::SessionNotReady(_) => Self::SessionNotReady,
Error::MlsNotInitialized => Self::NotInitialized,
Error::Transport(_) => Self::TransportFailure,
Error::EncryptFailed(_) => Self::CryptoFailure,
Error::GroupNotFound(_) => Self::GroupNotFound,
Error::Mls(inner) => Self::from(inner),
_ => Self::Unknown,
}
}
pub fn to_welcome_reason_code(&self) -> crate::events::WelcomeReasonCode {
match self {
Self::TransportFailure => crate::events::WelcomeReasonCode::TransportUnavailable,
Self::SessionNotReady
| Self::GroupNotFound
| Self::NotInitialized
| Self::CryptoFailure
| Self::SessionDesync
| Self::Unknown => crate::events::WelcomeReasonCode::InternalError,
}
}
}
impl From<&offline_protocol_mls::MlsError> for SessionStateError {
fn from(error: &offline_protocol_mls::MlsError) -> Self {
match error {
offline_protocol_mls::MlsError::GroupNotFound(_)
| offline_protocol_mls::MlsError::SessionNotFound(_) => Self::GroupNotFound,
offline_protocol_mls::MlsError::NotInitialized => Self::NotInitialized,
offline_protocol_mls::MlsError::SessionDesync(_) => Self::SessionDesync,
offline_protocol_mls::MlsError::Storage(_)
| offline_protocol_mls::MlsError::OpenMls(_)
| offline_protocol_mls::MlsError::Deserialization(_)
| offline_protocol_mls::MlsError::Serialization(_)
| offline_protocol_mls::MlsError::InvalidMessage(_)
| offline_protocol_mls::MlsError::Encryption(_)
| offline_protocol_mls::MlsError::Decryption(_)
| offline_protocol_mls::MlsError::CryptoGeneration(_)
| offline_protocol_mls::MlsError::Signing(_)
| offline_protocol_mls::MlsError::VerificationFailed(_)
| offline_protocol_mls::MlsError::InvalidPublicKey(_) => Self::CryptoFailure,
offline_protocol_mls::MlsError::CommitNotAuthorized { .. } => Self::Unknown,
offline_protocol_mls::MlsError::SessionIdentityMismatch { .. } => Self::Unknown,
offline_protocol_mls::MlsError::LeafAddressMismatch { .. }
| offline_protocol_mls::MlsError::UnsupportedSender { .. } => Self::Unknown,
_ => Self::Unknown,
}
}
}
#[non_exhaustive]
#[derive(Debug, Error)]
pub enum Error {
#[error("Protocol not started")]
NotStarted,
#[error("Protocol already started")]
AlreadyStarted,
#[error("Invalid configuration: {0}")]
InvalidConfiguration(String),
#[error("Core error: {0}")]
Core(#[from] offline_protocol_core::Error),
#[error("Transport error: {0}")]
Transport(#[from] offline_protocol_transport::Error),
#[error("Router error: {0}")]
Router(#[from] offline_protocol_router::Error),
#[error("Reliability error: {0}")]
Reliability(#[from] offline_protocol_reliability::Error),
#[error("MLS error: {0}")]
Mls(#[from] offline_protocol_mls::MlsError),
#[error("MLS encryption not initialized")]
MlsNotInitialized,
#[error("No key package available for recipient: {0}")]
NoKeyPackage(String),
#[error("Session not ready: {0:?}")]
SessionNotReady(EstablishmentState),
#[error("Failed to encrypt message: {0}")]
EncryptFailed(String),
#[error("Service error: {0}")]
Service(#[from] offline_protocol_services::ServiceError),
#[error("Serialization error: {0}")]
Serialization(String),
#[error("User is blocked: {0}")]
UserBlocked(String),
#[error(
"Too many concurrent media transfers to {0}; retry after an active transfer completes"
)]
MediaTransferLimit(String),
#[error("Group not found: {0}")]
GroupNotFound(String),
#[error("{0}")]
PermissionDenied(String),
#[error("{0}")]
InvalidState(String),
#[error("{0}")]
InvalidArgument(String),
#[error("Data layer is disabled")]
DataDisabled,
#[error("Data layer has no storage; initialize_mls must run first")]
DataStorageUnavailable,
#[error("Document is {actual} bytes compacted, over the {limit} byte cap")]
DocTooLarge {
actual: usize,
limit: usize,
},
#[error("Document data is corrupt: {0}")]
DataCorrupted(String),
#[error("telemetry config invalid: {0}")]
TelemetryConfigInvalid(String),
#[error("{0}")]
Other(String),
}
impl From<offline_protocol_sealed::SealedError> for Error {
fn from(err: offline_protocol_sealed::SealedError) -> Self {
Self::Mls(offline_protocol_mls::MlsError::from(err))
}
}
#[cfg(test)]
mod tests {
use super::{Error, SessionStateError};
use offline_protocol_mls::MlsError;
use offline_protocol_transport::Error as TransportError;
#[test]
fn session_state_code_values_are_stable() {
assert_eq!(
SessionStateError::SessionNotReady.code(),
"SESSION_NOT_READY"
);
assert_eq!(SessionStateError::GroupNotFound.code(), "GROUP_NOT_FOUND");
assert_eq!(SessionStateError::NotInitialized.code(), "NOT_INITIALIZED");
assert_eq!(
SessionStateError::TransportFailure.code(),
"TRANSPORT_FAILURE"
);
assert_eq!(SessionStateError::CryptoFailure.code(), "CRYPTO_FAILURE");
assert_eq!(SessionStateError::SessionDesync.code(), "SESSION_DESYNC");
assert_eq!(SessionStateError::Unknown.code(), "UNKNOWN");
}
#[test]
fn classify_maps_mls_group_not_found() {
let classified = SessionStateError::from(&MlsError::GroupNotFound("g1".to_string()));
assert_eq!(classified, SessionStateError::GroupNotFound);
}
#[test]
fn classify_maps_protocol_group_not_found() {
let classified = SessionStateError::classify(&Error::GroupNotFound("g1".to_string()));
assert_eq!(classified, SessionStateError::GroupNotFound);
}
#[test]
fn classify_maps_not_initialized() {
let classified = SessionStateError::from(&MlsError::NotInitialized);
assert_eq!(classified, SessionStateError::NotInitialized);
}
#[test]
fn classify_maps_crypto_failures() {
let classified = SessionStateError::from(&MlsError::Decryption("failed".to_string()));
assert_eq!(classified, SessionStateError::CryptoFailure);
}
#[test]
fn classify_maps_session_desync_distinct_from_crypto_failure() {
let desync = SessionStateError::from(&MlsError::SessionDesync("wrong epoch".to_string()));
assert_eq!(desync, SessionStateError::SessionDesync);
let crypto = SessionStateError::from(&MlsError::Decryption("aead".to_string()));
assert_eq!(crypto, SessionStateError::CryptoFailure);
}
#[test]
fn classify_unknown_fallback_is_safe() {
let classified = SessionStateError::classify(&Error::Other("opaque".to_string()));
assert_eq!(classified, SessionStateError::Unknown);
}
#[test]
fn welcome_reason_mapping_uses_typed_transport_failure() {
let classified = SessionStateError::classify(&Error::Transport(
TransportError::SendFailed("send failed".to_string()),
));
assert_eq!(
classified.to_welcome_reason_code(),
crate::events::WelcomeReasonCode::TransportUnavailable
);
}
#[test]
fn welcome_reason_mapping_unknown_defaults_internal_error() {
let classified = SessionStateError::classify(&Error::Other("opaque".to_string()));
assert_eq!(
classified.to_welcome_reason_code(),
crate::events::WelcomeReasonCode::InternalError
);
}
}