use std::fmt;
use snafu::prelude::*;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum GrpcControlBoundaryKind {
QueueSaturated,
StreamClosed,
TransportLost,
}
impl fmt::Display for GrpcControlBoundaryKind {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter.write_str(match self {
Self::QueueSaturated => "queue_saturated",
Self::StreamClosed => "stream_closed",
Self::TransportLost => "transport_lost",
})
}
}
#[derive(Snafu)]
#[snafu(visibility(pub))]
pub enum AnytypeGrpcError {
#[snafu(display("gRPC authentication failed (details redacted)"))]
Auth { source: AuthError },
#[snafu(display("gRPC configuration failed (details redacted)"))]
Config { source: ConfigError },
#[snafu(display("gRPC view operation failed (details redacted)"))]
View { source: ViewError },
#[snafu(display("gRPC backup operation failed (details redacted)"))]
Backup { source: BackupError },
#[snafu(display("gRPC transport failed (details redacted)"))]
Transport {
#[snafu(source(false))]
source: tonic::transport::Error,
},
#[snafu(
context(suffix(GrpcTimeoutConfigSnafu)),
display("gRPC deadline configuration error: {source}")
)]
TimeoutConfig {
source: crate::deadline::GrpcTimeoutConfigError,
},
#[snafu(context(suffix(GrpcDeadlineSnafu)), display("{source}"))]
Deadline {
source: crate::deadline::GrpcDeadlineError,
},
#[snafu(display("gRPC control boundary kind={kind} outcome={outcome}"))]
ControlBoundary {
kind: GrpcControlBoundaryKind,
outcome: crate::deadline::GrpcTimeoutOutcome,
},
}
#[derive(Snafu)]
#[snafu(visibility(pub))]
pub enum AuthError {
#[snafu(display("gRPC auth request failed with code {}", source.code()))]
Status {
#[snafu(source(false))]
source: tonic::Status,
},
#[snafu(display("Anytype auth API error ({code}; description redacted)"))]
Api { code: i32, description: String },
#[snafu(display("Create session returned empty token"))]
EmptyToken,
#[snafu(display("invalid authentication metadata (details redacted)"))]
InvalidMetadata {
#[snafu(source(false))]
source: tonic::metadata::errors::InvalidMetadataValue,
},
#[snafu(
context(suffix(AuthTimeoutConfigSnafu)),
display("gRPC deadline configuration error: {source}")
)]
TimeoutConfig {
source: crate::deadline::GrpcTimeoutConfigError,
},
#[snafu(context(suffix(AuthDeadlineSnafu)), display("{source}"))]
Deadline {
source: crate::deadline::GrpcDeadlineError,
},
}
#[derive(Snafu)]
#[snafu(visibility(pub))]
pub enum ConfigError {
#[snafu(display("configuration I/O failed (details redacted)"))]
Io {
#[snafu(source(false))]
source: std::io::Error,
},
#[snafu(display("configuration parsing failed (details redacted)"))]
Parse {
#[snafu(source(false))]
source: serde_json::Error,
},
#[snafu(display("home directory environment variable not set"))]
MissingHome,
}
#[derive(Snafu)]
#[snafu(visibility(pub))]
pub enum ViewError {
#[snafu(
context(suffix(ViewSnafu)),
display("view authentication failed (details redacted)")
)]
Auth { source: AuthError },
#[snafu(display("view gRPC request failed with code {}", source.code()))]
Rpc {
#[snafu(source(false))]
source: tonic::Status,
},
#[snafu(display("Anytype view API error ({code}; description redacted)"))]
ApiResponse { code: i32, description: String },
#[snafu(display("Object view missing in response"))]
MissingObjectView,
#[snafu(display("dataview block not found (view id redacted)"))]
MissingDataviewBlock { view_id: String },
#[snafu(display("view not found (id redacted)"))]
MissingView { view_id: String },
#[snafu(display("view is not supported (id redacted; type {actual})"))]
NotSupportedView { view_id: String, actual: i32 },
}
#[derive(Snafu)]
#[snafu(visibility(pub))]
pub enum BackupError {
#[snafu(display("backup gRPC request failed with code {}", source.code()))]
BackupRpc {
#[snafu(source(false))]
source: tonic::Status,
},
#[snafu(display("Anytype backup API error ({code}; description redacted)"))]
BackupApiResponse { code: i32, description: String },
#[snafu(display("backup authentication failed (details redacted)"))]
BackupAuth { source: AuthError },
#[snafu(display("invalid backup options (details redacted)"))]
InvalidOptions { message: String },
#[snafu(display("failed to resolve backup space name (details redacted)"))]
SpaceNameLookup { space_id: String, message: String },
#[snafu(display("Backup response missing export path"))]
MissingExportPath,
#[snafu(display("backup I/O failed (details redacted)"))]
BackupIo {
path: std::path::PathBuf,
#[snafu(source(false))]
source: std::io::Error,
},
#[snafu(display("moving backup output failed (details redacted)"))]
BackupMove {
from: std::path::PathBuf,
to: std::path::PathBuf,
#[snafu(source(false))]
source: std::io::Error,
},
#[snafu(context(suffix(BackupDeadlineSnafu)), display("{source}"))]
Deadline {
source: crate::deadline::GrpcDeadlineError,
},
}
macro_rules! impl_redacted_debug {
($($error:ty),+ $(,)?) => {
$(
impl fmt::Debug for $error {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
fmt::Display::fmt(self, formatter)
}
}
)+
};
}
impl_redacted_debug!(
AnytypeGrpcError,
AuthError,
ConfigError,
ViewError,
BackupError,
);
impl From<tonic::Status> for AuthError {
fn from(source: tonic::Status) -> Self {
AuthError::Status { source }
}
}
impl From<tonic::metadata::errors::InvalidMetadataValue> for AuthError {
fn from(source: tonic::metadata::errors::InvalidMetadataValue) -> Self {
AuthError::InvalidMetadata { source }
}
}
impl From<crate::deadline::GrpcTimeoutConfigError> for AuthError {
fn from(source: crate::deadline::GrpcTimeoutConfigError) -> Self {
AuthError::TimeoutConfig { source }
}
}
impl From<std::io::Error> for ConfigError {
fn from(source: std::io::Error) -> Self {
ConfigError::Io { source }
}
}
impl From<serde_json::Error> for ConfigError {
fn from(source: serde_json::Error) -> Self {
ConfigError::Parse { source }
}
}
impl From<AuthError> for ViewError {
fn from(source: AuthError) -> Self {
ViewError::Auth { source }
}
}
impl From<tonic::Status> for ViewError {
fn from(source: tonic::Status) -> Self {
ViewError::Rpc { source }
}
}
impl From<tonic::Status> for BackupError {
fn from(source: tonic::Status) -> Self {
BackupError::BackupRpc { source }
}
}
impl From<AuthError> for BackupError {
fn from(source: AuthError) -> Self {
BackupError::BackupAuth { source }
}
}
impl From<AuthError> for AnytypeGrpcError {
fn from(source: AuthError) -> Self {
match source {
AuthError::TimeoutConfig { source } => AnytypeGrpcError::TimeoutConfig { source },
AuthError::Deadline { source } => AnytypeGrpcError::Deadline { source },
source => AnytypeGrpcError::Auth { source },
}
}
}
impl From<ConfigError> for AnytypeGrpcError {
fn from(source: ConfigError) -> Self {
AnytypeGrpcError::Config { source }
}
}
impl From<ViewError> for AnytypeGrpcError {
fn from(source: ViewError) -> Self {
AnytypeGrpcError::View { source }
}
}
impl From<BackupError> for AnytypeGrpcError {
fn from(source: BackupError) -> Self {
match source {
BackupError::Deadline { source } => AnytypeGrpcError::Deadline { source },
source => AnytypeGrpcError::Backup { source },
}
}
}
impl From<tonic::transport::Error> for AnytypeGrpcError {
fn from(source: tonic::transport::Error) -> Self {
AnytypeGrpcError::Transport { source }
}
}
impl From<crate::deadline::GrpcTimeoutConfigError> for AnytypeGrpcError {
fn from(source: crate::deadline::GrpcTimeoutConfigError) -> Self {
AnytypeGrpcError::TimeoutConfig { source }
}
}
impl From<crate::deadline::GrpcDeadlineError> for AnytypeGrpcError {
fn from(source: crate::deadline::GrpcDeadlineError) -> Self {
AnytypeGrpcError::Deadline { source }
}
}
#[cfg(test)]
mod tests {
use super::*;
const SECRET: &str = "HOSTILE_RPC_PEER_SECRET";
fn assert_redacted<E>(error: &E)
where
E: std::error::Error,
{
assert!(!error.to_string().contains(SECRET));
assert!(!format!("{error:?}").contains(SECRET));
let mut source = error.source();
while let Some(current) = source {
assert!(!current.to_string().contains(SECRET));
assert!(!format!("{current:?}").contains(SECRET));
source = current.source();
}
}
#[test]
fn public_rpc_error_families_redact_peer_controlled_details_and_sources() {
let auth_status = AuthError::Status {
source: tonic::Status::internal(SECRET),
};
assert_redacted(&auth_status);
assert_redacted(&AuthError::Api {
code: 500,
description: SECRET.to_owned(),
});
let config = ConfigError::Io {
source: std::io::Error::other(SECRET),
};
assert_redacted(&config);
let view_status = ViewError::Rpc {
source: tonic::Status::invalid_argument(SECRET),
};
assert_redacted(&view_status);
assert_redacted(&ViewError::ApiResponse {
code: 400,
description: SECRET.to_owned(),
});
assert_redacted(&ViewError::MissingView {
view_id: SECRET.to_owned(),
});
let backup_status = BackupError::BackupRpc {
source: tonic::Status::unavailable(SECRET),
};
assert_redacted(&backup_status);
assert_redacted(&BackupError::BackupApiResponse {
code: 503,
description: SECRET.to_owned(),
});
assert_redacted(&BackupError::SpaceNameLookup {
space_id: SECRET.to_owned(),
message: SECRET.to_owned(),
});
assert_redacted(&AnytypeGrpcError::Auth {
source: AuthError::Api {
code: 500,
description: SECRET.to_owned(),
},
});
let transport = tonic::transport::Endpoint::from_shared(format!("not {SECRET}"))
.expect_err("hostile endpoint is invalid");
assert_redacted(&AnytypeGrpcError::Transport { source: transport });
assert_redacted(&AnytypeGrpcError::ControlBoundary {
kind: GrpcControlBoundaryKind::QueueSaturated,
outcome: crate::deadline::GrpcTimeoutOutcome::MutationIndeterminate,
});
}
}