#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
#[non_exhaustive]
pub enum ErrorPolicy {
Skip,
#[default]
Fail,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
#[non_exhaustive]
pub enum ErrorClass {
Retryable,
RecordLevel,
Fatal,
}
#[derive(Debug, thiserror::Error)]
#[error("fatal error in {component}: {reason}")]
pub struct FatalError {
pub component: String,
pub reason: String,
}
#[derive(Debug, thiserror::Error)]
#[non_exhaustive]
pub enum DeserError {
#[error("malformed payload: {reason}")]
Malformed {
reason: String,
},
#[error("schema unavailable: {reason}")]
SchemaUnavailable {
reason: String,
},
#[error("not ready: {reason}")]
NotReady {
reason: String,
},
#[error("deserializer failed: {reason}")]
Fatal {
reason: String,
},
}
#[derive(Debug, thiserror::Error)]
#[non_exhaustive]
pub enum SourceError {
#[error("source error ({class:?}): {reason}")]
Client {
class: ErrorClass,
reason: String,
},
}
#[derive(Debug, thiserror::Error)]
#[non_exhaustive]
pub enum SinkError {
#[error("sink error ({class:?}): {reason}")]
Client {
class: ErrorClass,
reason: String,
},
}
pub const TLS_REJECTION_ALERTS: &[u8] = &[
40, 42, 43, 44, 45, 46, 48, 49, 51, 70, 71, 109, 110, 112, 116, 120, ];
#[must_use]
pub fn find_source<'a, T: std::error::Error + 'static>(
err: &'a (dyn std::error::Error + 'static),
) -> Option<&'a T> {
let mut pending = vec![err];
while let Some(e) = pending.pop() {
if let Some(found) = e.downcast_ref() {
return Some(found);
}
if let Some(inner) = e
.downcast_ref::<std::io::Error>()
.and_then(|io| io.get_ref())
{
pending.push(inner);
}
pending.extend(e.source());
}
None
}
#[macro_export]
macro_rules! tls_rejection {
($($rustls:ident)::+, $err:expr) => {
$crate::error::find_source::<$($rustls)::+::Error>($err).filter(|tls| match tls {
$($rustls)::+::Error::InvalidCertificate(_)
| $($rustls)::+::Error::PeerIncompatible(_) => true,
$($rustls)::+::Error::AlertReceived(alert) => {
$crate::error::TLS_REJECTION_ALERTS.contains(&u8::from(*alert))
}
_ => false,
})
};
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn tls_rejection_matches_the_rejection_classes() {
use rustls::{AlertDescription as A, CertificateError, Error as Tls};
for (tls, rejected) in [
(
Tls::InvalidCertificate(CertificateError::UnknownIssuer),
true,
),
(
Tls::PeerIncompatible(rustls::PeerIncompatible::NoCipherSuitesInCommon),
true,
),
(Tls::AlertReceived(A::HandshakeFailure), true),
(Tls::AlertReceived(A::CertificateRequired), true),
(Tls::AlertReceived(A::DecodeError), false),
(Tls::AlertReceived(A::InternalError), false),
(Tls::General("reset".into()), false),
] {
let name = format!("{tls:?}");
let err = std::io::Error::new(std::io::ErrorKind::InvalidData, tls);
assert_eq!(
crate::tls_rejection!(rustls, &err).is_some(),
rejected,
"{name}"
);
}
}
#[test]
fn defaults_match_documented_policy() {
assert_eq!(ErrorPolicy::default(), ErrorPolicy::Fail);
}
#[test]
fn errors_render_reasons() {
let e = DeserError::Malformed {
reason: "truncated header".into(),
};
assert!(e.to_string().contains("truncated header"));
}
#[derive(Debug, thiserror::Error)]
#[error("target {0}")]
struct Target(u8);
#[derive(Debug, thiserror::Error)]
#[error("wrapper")]
struct Wrapper(#[source] std::io::Error);
#[test]
fn find_source_reaches_the_error_an_io_error_wraps() {
let err = Wrapper(std::io::Error::other(std::io::Error::new(
std::io::ErrorKind::InvalidData,
Target(7),
)));
assert_eq!(find_source::<Target>(&err).map(|t| t.0), Some(7));
assert!(find_source::<FatalError>(&err).is_none());
}
}