sos-protocol 0.17.1

Networking and sync protocol types for the Save Our Secrets SDK.
Documentation
//! Error type for the wire protocol.
use http::StatusCode;
use serde::{Deserialize, Serialize};
use serde_json::Value;
use sos_sync::{MaybeConflict, SyncStatus};
use std::error::Error as StdError;
use thiserror::Error;

/// Trait for error implementations that
/// support a conflict error.
pub trait AsConflict {
    /// Determine if this is a conflict error.
    fn is_conflict(&self) -> bool;

    /// Determine if this is a hard conflict error.
    fn is_hard_conflict(&self) -> bool;

    /// Take an underlying conflict error.
    fn take_conflict(self) -> Option<ConflictError>;
}

/// Errors generated by the wire protocol.
#[derive(Debug, Error)]
pub enum Error {
    /// Reached EOF decoding a relay packet.
    #[error("relay packet end of file")]
    EndOfFile,

    /// Error generated when a conflict is detected.
    #[error(transparent)]
    Conflict(#[from] ConflictError),

    /// Error generated by the IO module.
    #[error(transparent)]
    Io(#[from] std::io::Error),

    /// Error generated converting from a slice.
    #[error(transparent)]
    TryFromSlice(#[from] std::array::TryFromSliceError),

    /// Error generated by the protobuf library when encoding.
    #[error(transparent)]
    ProtoBufEncode(#[from] prost::EncodeError),

    /// Error generated by the protobuf library when decoding.
    #[error(transparent)]
    ProtoBufDecode(#[from] prost::DecodeError),

    /// Error generated by the protobuf library when converting enums.
    #[error(transparent)]
    ProtoEnum(#[from] prost::UnknownEnumValue),

    /// Error generated by the core library.
    #[error(transparent)]
    Core(#[from] sos_core::Error),

    /// Error generated by the backend library.
    #[error(transparent)]
    Backend(#[from] sos_backend::Error),

    /// Error generated by the backendcstorage.
    #[error(transparent)]
    BackendStorage(#[from] sos_backend::StorageError),

    /// Error generated by the signer library.
    #[error(transparent)]
    Signer(#[from] sos_signer::Error),

    /// Error generated by the sync library.
    #[error(transparent)]
    Sync(#[from] sos_sync::Error),

    /// Error generated by the merkle tree library.
    #[error(transparent)]
    Merkle(#[from] rs_merkle::Error),

    /// Error generated converting time types.
    #[error(transparent)]
    Time(#[from] time::error::ComponentRange),

    /// Error generated parsing URLs.
    #[error(transparent)]
    UrlParse(#[from] url::ParseError),

    /// Error generated by the HTTP library.
    #[error(transparent)]
    Http(#[from] http::Error),

    /// Error generated by the HTTP library.
    #[error(transparent)]
    StatusCode(#[from] http::status::InvalidStatusCode),

    /// Error generated by the JSON library.
    #[error(transparent)]
    Json(#[from] serde_json::Error),

    /// Error generated by network communication.
    #[error(transparent)]
    Network(#[from] NetworkError),

    /// Error generated joining a task.
    #[error(transparent)]
    Join(#[from] tokio::task::JoinError),

    #[cfg(feature = "network-client")]
    /// Error generated converting a header to a string.
    #[error(transparent)]
    ToStr(#[from] reqwest::header::ToStrError),

    #[cfg(feature = "network-client")]
    /// Error generated by the HTTP request library.
    #[error(transparent)]
    Request(#[from] reqwest::Error),

    #[cfg(feature = "network-client")]
    /// Error generated decoding a base58 string.
    #[error(transparent)]
    Base58Decode(#[from] bs58::decode::Error),

    #[cfg(feature = "network-client")]
    /// Error generated when a downloaded file checksum does not
    /// match the expected checksum.
    #[error("file download checksum mismatch; expected '{0}' but got '{1}'")]
    FileChecksumMismatch(String, String),

    #[cfg(feature = "network-client")]
    /// Error generated when a file transfer is canceled.
    ///
    /// The boolean flag indicates whether the cancellation was
    /// triggered by the user.
    #[error("file transfer canceled")]
    TransferCanceled(crate::transfer::CancelReason),

    #[cfg(feature = "network-client")]
    /// Overflow error calculating the retry exponential factor.
    #[error("retry overflow")]
    RetryOverflow,

    #[cfg(feature = "network-client")]
    /// Network retry was canceled possibly by the user.
    #[error("network retry was canceled")]
    RetryCanceled(crate::transfer::CancelReason),

    #[cfg(feature = "listen")]
    /// Error generated when a websocket message is not binary.
    #[error("not binary message type on websocket")]
    NotBinaryWebsocketMessageType,

    /// Error generated by the websocket client.
    #[cfg(feature = "listen")]
    #[error(transparent)]
    WebSocket(#[from] tokio_tungstenite::tungstenite::Error),
}

#[cfg(feature = "network-client")]
impl Error {
    /// Determine if this is a canceled error and
    /// whether the cancellation was triggered by the user.
    pub fn cancellation_reason(
        &self,
    ) -> Option<&crate::transfer::CancelReason> {
        let source = source_error(self);
        if let Some(err) = source.downcast_ref::<Error>() {
            if let Error::TransferCanceled(reason) = err {
                Some(reason)
            } else {
                None
            }
        } else {
            None
        }
    }
}

pub(crate) fn source_error<'a>(
    error: &'a (dyn StdError + 'static),
) -> &'a (dyn StdError + 'static) {
    let mut source = error;
    while let Some(next_source) = source.source() {
        source = next_source;
    }
    source
}

/// Error created communicating over the network.
#[derive(Debug, Error)]
pub enum NetworkError {
    /// Error generated when an unexpected response code is received.
    #[error("unexpected response status code {0}")]
    ResponseCode(StatusCode),

    /// Error generated when an unexpected response code is received.
    #[error("unexpected response {1} (code: {0})")]
    ResponseJson(StatusCode, Value),

    /// Error generated when an unexpected content type is returend.
    #[error("unexpected content type {0}, expected: {1}")]
    ContentType(String, String),
}

/// Error reply.
#[derive(Default, Serialize, Deserialize)]
#[serde(default)]
pub struct ErrorReply {
    /// Status code.
    code: u16,
    /// Data value.
    #[serde(skip_serializing_if = "Option::is_none")]
    value: Option<Value>,
    /// Error message.
    #[serde(skip_serializing_if = "Option::is_none")]
    message: Option<String>,
}

impl ErrorReply {
    /// New error reply with a message.
    pub fn new_message(
        status: StatusCode,
        message: impl std::fmt::Display,
    ) -> Self {
        Self {
            code: status.into(),
            message: Some(message.to_string()),
            ..Default::default()
        }
    }
}

impl From<NetworkError> for ErrorReply {
    fn from(value: NetworkError) -> Self {
        match value {
            NetworkError::ResponseCode(status) => ErrorReply {
                code: status.into(),
                ..Default::default()
            },
            NetworkError::ResponseJson(status, value) => ErrorReply {
                code: status.into(),
                value: Some(value),
                ..Default::default()
            },
            NetworkError::ContentType(_, _) => ErrorReply {
                code: StatusCode::BAD_REQUEST.into(),
                ..Default::default()
            },
        }
    }
}

/// Error created whan a conflict is detected.
#[derive(Debug, Error)]
pub enum ConflictError {
    /// Error generated when a soft conflict was detected.
    ///
    /// A soft conflict may be resolved by searching for a
    /// common ancestor commit and merging changes since
    /// the common ancestor commit.
    #[error("soft conflict")]
    Soft {
        /// Conflict information.
        conflict: MaybeConflict,
        /// Local information sent to the remote.
        local: SyncStatus,
        /// Remote information in the server reply.
        remote: SyncStatus,
    },

    /// Error generated when a hard conflict was detected.
    ///
    /// A hard conflict is triggered after a soft conflict
    /// attempted to scan proofs on a remote and was unable
    /// to find a common ancestor commit.
    #[error("hard conflict")]
    Hard,
}

impl AsConflict for Error {
    fn is_conflict(&self) -> bool {
        matches!(self, Error::Conflict(_))
    }

    fn is_hard_conflict(&self) -> bool {
        matches!(self, Error::Conflict(ConflictError::Hard))
    }

    fn take_conflict(self) -> Option<ConflictError> {
        match self {
            Self::Conflict(err) => Some(err),
            _ => None,
        }
    }
}