use std::fmt;
pub type Result<T> = std::result::Result<T, Error>;
#[derive(Debug)]
pub enum Error {
Runtime(String),
InvalidAddress(String),
InvalidEndpointPath,
InvalidFingerprint(String),
AlreadyRegistered,
NotConnected,
ConnectionLost(LossCause),
Negotiation(String),
Protocol(String),
Rejected,
UnknownEndpoint,
Unsupported,
NoParkedConnection,
NoReply,
Canceled,
Indeterminate,
LimitExceeded,
Tls(String),
Untrusted(crate::identity::Fingerprint),
Io(std::io::Error),
Transport(String),
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum LossCause {
IdleTimeout,
PeerClosed,
LocallyClosed,
Reset,
TransportError,
}
impl fmt::Display for LossCause {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(match self {
LossCause::IdleTimeout => "idle timeout",
LossCause::PeerClosed => "closed by the peer",
LossCause::LocallyClosed => "closed locally",
LossCause::Reset => "stateless reset",
LossCause::TransportError => "transport error",
})
}
}
impl fmt::Display for Error {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Error::Runtime(m) => write!(f, "runtime error: {m}"),
Error::InvalidAddress(m) => write!(f, "invalid address: {m}"),
Error::InvalidEndpointPath => f.write_str(
"invalid endpoint path: must start with '/', be 1..=512 bytes and contain no control bytes",
),
Error::InvalidFingerprint(m) => {
write!(f, "invalid fingerprint: expected sha256:<64 hex digits>, got {m:?}")
}
Error::AlreadyRegistered => f.write_str("endpoint path already registered"),
Error::NotConnected => f.write_str("endpoint is not connected to any peer"),
Error::ConnectionLost(cause) => {
write!(f, "connection lost before the transfer completed: {cause}")
}
Error::Negotiation(m) => write!(f, "negotiation failed: {m}"),
Error::Protocol(m) => write!(f, "protocol violation: {m}"),
Error::Rejected => f.write_str("peer rejected the transfer"),
Error::UnknownEndpoint => f.write_str("peer has no such endpoint"),
Error::Unsupported => f.write_str("peer does not support the requested feature"),
Error::NoParkedConnection => {
f.write_str("peer has no parked connection for a stream toward it")
}
Error::NoReply => f.write_str("peer accepted the request but sent no reply"),
Error::Canceled => f.write_str("transfer canceled"),
Error::Indeterminate => {
f.write_str("outcome indeterminate: the transfer may or may not have been accepted")
}
Error::LimitExceeded => f.write_str("resource limit exceeded"),
Error::Tls(m) => write!(f, "tls error: {m}"),
Error::Untrusted(fp) => write!(f, "peer identity {fp} is not trusted"),
Error::Io(e) => write!(f, "io error: {e}"),
Error::Transport(m) => write!(f, "transport error: {m}"),
}
}
}
impl std::error::Error for Error {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match self {
Error::Io(e) => Some(e),
_ => None,
}
}
}
impl From<std::io::Error> for Error {
fn from(e: std::io::Error) -> Self {
Error::Io(e)
}
}
impl Error {
pub fn is_definite_failure(&self) -> bool {
matches!(
self,
Error::ConnectionLost(_)
| Error::Rejected
| Error::UnknownEndpoint
| Error::Unsupported
| Error::NoParkedConnection
| Error::Canceled
| Error::NotConnected
| Error::LimitExceeded
| Error::Untrusted(_)
)
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub enum ErrorCode {
UnknownEndpoint,
Rejected,
Unsupported,
Internal,
NoReply,
}
impl ErrorCode {
pub const fn to_wire(self) -> u64 {
match self {
ErrorCode::UnknownEndpoint => 1,
ErrorCode::Rejected => 2,
ErrorCode::Unsupported => 3,
ErrorCode::Internal => 4,
ErrorCode::NoReply => 5,
}
}
pub const fn from_wire(code: u64) -> Option<ErrorCode> {
match code {
1 => Some(ErrorCode::UnknownEndpoint),
2 => Some(ErrorCode::Rejected),
3 => Some(ErrorCode::Unsupported),
4 => Some(ErrorCode::Internal),
5 => Some(ErrorCode::NoReply),
_ => None,
}
}
}
impl From<ErrorCode> for Error {
fn from(code: ErrorCode) -> Error {
match code {
ErrorCode::UnknownEndpoint => Error::UnknownEndpoint,
ErrorCode::Rejected => Error::Rejected,
ErrorCode::Unsupported => Error::Unsupported,
ErrorCode::Internal => Error::Transport("peer reported an internal error".into()),
ErrorCode::NoReply => Error::NoReply,
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub enum StopReason {
Rejected,
Canceled,
UnknownEndpoint,
Unsupported,
LimitExceeded,
ShuttingDown,
Other(u64),
}
impl From<StopReason> for Error {
fn from(reason: StopReason) -> Error {
match reason {
StopReason::Rejected => Error::Rejected,
StopReason::Canceled => Error::Canceled,
StopReason::UnknownEndpoint => Error::UnknownEndpoint,
StopReason::Unsupported => Error::Unsupported,
StopReason::LimitExceeded => Error::LimitExceeded,
StopReason::ShuttingDown => Error::Rejected,
StopReason::Other(code) => {
Error::Transport(format!("peer stopped receiving with code {code}"))
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn display_is_non_empty_for_every_variant() {
let variants = [
Error::Runtime("x".into()),
Error::InvalidAddress("x".into()),
Error::InvalidEndpointPath,
Error::AlreadyRegistered,
Error::NotConnected,
Error::ConnectionLost(LossCause::IdleTimeout),
Error::Negotiation("x".into()),
Error::Protocol("x".into()),
Error::Rejected,
Error::UnknownEndpoint,
Error::Unsupported,
Error::NoReply,
Error::Canceled,
Error::Indeterminate,
Error::LimitExceeded,
Error::Tls("x".into()),
Error::Io(std::io::Error::other("x")),
Error::Transport("x".into()),
];
for v in &variants {
assert!(!v.to_string().is_empty(), "{v:?}");
}
}
#[test]
fn definite_failures_exclude_the_unknowable_ones() {
for definite in [
Error::ConnectionLost(LossCause::PeerClosed),
Error::Rejected,
Error::UnknownEndpoint,
Error::Unsupported,
Error::Canceled,
Error::NotConnected,
Error::LimitExceeded,
] {
assert!(definite.is_definite_failure(), "{definite:?}");
}
assert!(!Error::Indeterminate.is_definite_failure());
assert!(!Error::NoReply.is_definite_failure());
}
#[test]
fn io_error_is_the_source() {
use std::error::Error as _;
let e = Error::Io(std::io::Error::other("boom"));
assert!(e.source().is_some());
assert!(Error::Canceled.source().is_none());
}
}