use thiserror::Error;
#[derive(Error, Debug)]
pub enum AntQuicError {
#[error("Transport error: {0}")]
Transport(#[from] crate::transport_error::Error),
#[error("Connection error: {0}")]
Connection(#[from] crate::connection::ConnectionError),
#[error("Discovery error: {0}")]
Discovery(#[from] crate::candidate_discovery::DiscoveryError),
#[error("NAT traversal error: {0}")]
NatTraversal(#[from] crate::nat_traversal_api::NatTraversalError),
#[error("Configuration error: {0}")]
Config(String),
#[error("I/O error: {0}")]
Io(#[from] std::io::Error),
#[error("Crypto error: {0}")]
Crypto(String),
#[error("PQC error: {0}")]
Pqc(#[from] crate::crypto::pqc::types::PqcError),
#[error("Operation timed out: {0}")]
Timeout(String),
#[error("Resource exhausted: {0}")]
ResourceExhausted(String),
#[error("Invalid parameter: {0}")]
InvalidParameter(String),
#[error("Internal error: {0}")]
Internal(String),
}
pub type Result<T> = std::result::Result<T, AntQuicError>;
pub mod utils {
use super::*;
use tracing::{debug, error, info, warn};
pub fn log_error(error: &(dyn std::error::Error + 'static), context: &str) {
let error_msg = format!("{}: {}", context, error);
match error.downcast_ref::<AntQuicError>() {
Some(AntQuicError::Internal(_)) => error!("{}", error_msg),
Some(AntQuicError::Transport(_)) => warn!("{}", error_msg),
Some(AntQuicError::Connection(_)) => warn!("{}", error_msg),
Some(AntQuicError::Timeout(_)) => info!("{}", error_msg),
Some(AntQuicError::InvalidParameter(_)) => debug!("{}", error_msg),
_ => warn!("{}", error_msg),
}
}
pub fn to_user_message(error: &(dyn std::error::Error + 'static)) -> String {
match error.downcast_ref::<AntQuicError>() {
Some(AntQuicError::Transport(_)) => {
"Network connection error. Please check your internet connection.".to_string()
}
Some(AntQuicError::Connection(_)) => {
"Failed to establish connection. The remote peer may be unreachable.".to_string()
}
Some(AntQuicError::Discovery(_)) => {
"Failed to discover network configuration. Please check your network settings."
.to_string()
}
Some(AntQuicError::NatTraversal(_)) => {
"NAT traversal failed. This may be due to restrictive network policies.".to_string()
}
Some(AntQuicError::Timeout(_)) => "Operation timed out. Please try again.".to_string(),
Some(AntQuicError::Config(_)) => {
"Configuration error. Please check your settings.".to_string()
}
Some(AntQuicError::Io(_)) => {
"System I/O error. Please check file permissions and disk space.".to_string()
}
Some(AntQuicError::Crypto(_)) => {
"Cryptographic operation failed. This may indicate a security issue.".to_string()
}
Some(AntQuicError::Pqc(_)) => {
"Post-quantum cryptographic operation failed.".to_string()
}
Some(AntQuicError::ResourceExhausted(_)) => {
"System resources exhausted. Please close some applications and try again."
.to_string()
}
Some(AntQuicError::InvalidParameter(_)) => {
"Invalid input parameters provided.".to_string()
}
Some(AntQuicError::Internal(_)) => {
"An internal error occurred. Please report this issue.".to_string()
}
_ => format!("An unexpected error occurred: {}", error),
}
}
pub fn is_recoverable(error: &(dyn std::error::Error + 'static)) -> bool {
match error.downcast_ref::<AntQuicError>() {
Some(AntQuicError::Timeout(_)) => true,
Some(AntQuicError::Connection(_)) => true,
Some(AntQuicError::Discovery(_)) => true,
Some(AntQuicError::NatTraversal(_)) => true,
Some(AntQuicError::Io(io_err)) => {
matches!(
io_err.kind(),
std::io::ErrorKind::TimedOut | std::io::ErrorKind::Interrupted
)
}
_ => false,
}
}
pub fn get_retry_delay(
error: &(dyn std::error::Error + 'static),
) -> Option<std::time::Duration> {
match error.downcast_ref::<AntQuicError>() {
Some(AntQuicError::Timeout(_)) => Some(std::time::Duration::from_millis(100)),
Some(AntQuicError::Connection(_)) => Some(std::time::Duration::from_millis(500)),
Some(AntQuicError::Discovery(_)) => Some(std::time::Duration::from_secs(1)),
Some(AntQuicError::NatTraversal(_)) => Some(std::time::Duration::from_secs(2)),
Some(AntQuicError::Io(io_err)) => match io_err.kind() {
std::io::ErrorKind::TimedOut => Some(std::time::Duration::from_millis(100)),
std::io::ErrorKind::Interrupted => Some(std::time::Duration::from_millis(10)),
_ => None,
},
_ => None,
}
}
}
#[macro_export]
macro_rules! ensure {
($condition:expr, $error:expr) => {
if !($condition) {
return Err($error.into());
}
};
}
#[macro_export]
macro_rules! bail {
($error:expr) => {
return Err($error.into());
};
}
#[macro_export]
macro_rules! context {
($result:expr, $context:expr) => {
$result.map_err(|e| AntQuicError::Internal(format!("{}: {}", $context, e)))
};
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn error_transport_display() {
let err = AntQuicError::Transport(crate::transport_error::Error::INTERNAL_ERROR("test"));
let display = format!("{err}");
assert!(display.contains("Transport error"));
}
#[test]
fn error_connection_display() {
let err = AntQuicError::Connection(crate::connection::ConnectionError::LocallyClosed);
let display = format!("{err}");
assert!(display.contains("Connection error"));
}
#[test]
fn error_config_display() {
let err = AntQuicError::Config("bad config".to_string());
let display = format!("{err}");
assert!(display.contains("Configuration error"));
assert!(display.contains("bad config"));
}
#[test]
fn error_crypto_display() {
let err = AntQuicError::Crypto("key error".to_string());
let display = format!("{err}");
assert!(display.contains("Crypto error"));
}
#[test]
fn error_timeout_display() {
let err = AntQuicError::Timeout("timed out".to_string());
let display = format!("{err}");
assert!(display.contains("timed out"));
}
#[test]
fn error_resource_exhausted_display() {
let err = AntQuicError::ResourceExhausted("no mem".to_string());
let display = format!("{err}");
assert!(display.contains("Resource exhausted"));
}
#[test]
fn error_invalid_parameter_display() {
let err = AntQuicError::InvalidParameter("bad param".to_string());
let display = format!("{err}");
assert!(display.contains("Invalid parameter"));
}
#[test]
fn error_internal_display() {
let err = AntQuicError::Internal("bug".to_string());
let display = format!("{err}");
assert!(display.contains("Internal error"));
}
#[test]
fn error_debug_format() {
let err = AntQuicError::Internal("test".to_string());
let debug = format!("{err:?}");
assert!(debug.contains("Internal"));
}
#[test]
fn error_from_invalid_parameter() {
let s = "bad".to_string();
let err: AntQuicError = AntQuicError::InvalidParameter(s);
assert!(format!("{err}").contains("Invalid parameter"));
}
#[test]
fn error_io_into() {
let io_err = std::io::Error::new(std::io::ErrorKind::NotFound, "file not found");
let err: AntQuicError = io_err.into();
assert!(matches!(err, AntQuicError::Io(_)));
}
#[test]
fn log_error_internal() {
let err = AntQuicError::Internal("test internal".to_string());
utils::log_error(&err, "test context");
}
#[test]
fn log_error_transport() {
let err = AntQuicError::Transport(crate::transport_error::Error::INTERNAL_ERROR("x"));
utils::log_error(&err, "transport");
}
#[test]
fn log_error_generic() {
let err = std::io::Error::other("generic");
utils::log_error(&err, "generic context");
}
#[test]
fn user_message_transport() {
let err = AntQuicError::Transport(crate::transport_error::Error::INTERNAL_ERROR(""));
let msg = utils::to_user_message(&err);
assert!(msg.contains("Network connection error"));
}
#[test]
fn user_message_connection() {
let err = AntQuicError::Connection(crate::connection::ConnectionError::LocallyClosed);
let msg = utils::to_user_message(&err);
assert!(msg.contains("Failed to establish connection"));
}
#[test]
fn user_message_timeout() {
let err = AntQuicError::Timeout("x".to_string());
let msg = utils::to_user_message(&err);
assert!(msg.contains("timed out"));
}
#[test]
fn user_message_crypto() {
let err = AntQuicError::Crypto("x".to_string());
let msg = utils::to_user_message(&err);
assert!(msg.contains("Cryptographic operation failed"));
}
#[test]
fn user_message_internal() {
let err = AntQuicError::Internal("x".to_string());
let msg = utils::to_user_message(&err);
assert!(msg.contains("internal error"));
}
#[test]
fn user_message_config() {
let err = AntQuicError::Config("x".to_string());
let msg = utils::to_user_message(&err);
assert!(msg.contains("Configuration error"));
}
#[test]
fn user_message_discovery() {
let err = std::io::Error::other("weird");
let msg = utils::to_user_message(&err);
assert!(msg.contains("unexpected error"));
}
#[test]
fn user_message_io() {
let err = AntQuicError::Io(std::io::Error::new(
std::io::ErrorKind::PermissionDenied,
"denied",
));
let msg = utils::to_user_message(&err);
assert!(msg.contains("I/O error"));
}
#[test]
fn user_message_invalid_parameter() {
let err = AntQuicError::InvalidParameter("x".to_string());
let msg = utils::to_user_message(&err);
assert!(msg.contains("Invalid input"));
}
#[test]
fn user_message_resource_exhausted() {
let err = AntQuicError::ResourceExhausted("x".to_string());
let msg = utils::to_user_message(&err);
assert!(msg.contains("resources exhausted"));
}
#[test]
fn user_message_pqc() {
use crate::crypto::pqc::types::PqcError;
let err = AntQuicError::Pqc(PqcError::KeyGenerationFailed("test".to_string()));
let msg = utils::to_user_message(&err);
assert!(msg.contains("Post-quantum"));
}
#[test]
fn user_message_nat_traversal() {
use crate::nat_traversal_api::NatTraversalError;
let err = AntQuicError::NatTraversal(NatTraversalError::HolePunchingFailed);
let msg = utils::to_user_message(&err);
assert!(msg.contains("NAT traversal failed"));
}
#[test]
fn timeout_is_recoverable() {
let err = AntQuicError::Timeout("x".to_string());
assert!(utils::is_recoverable(&err));
}
#[test]
fn connection_error_is_recoverable() {
let err = AntQuicError::Connection(crate::connection::ConnectionError::LocallyClosed);
assert!(utils::is_recoverable(&err));
}
#[test]
fn internal_error_is_not_recoverable() {
let err = AntQuicError::Internal("x".to_string());
assert!(!utils::is_recoverable(&err));
}
#[test]
fn config_error_is_not_recoverable() {
let err = AntQuicError::Config("x".to_string());
assert!(!utils::is_recoverable(&err));
}
#[test]
fn io_timeout_is_recoverable() {
let err = AntQuicError::Io(std::io::Error::new(std::io::ErrorKind::TimedOut, "timeout"));
assert!(utils::is_recoverable(&err));
}
#[test]
fn io_interrupted_is_recoverable() {
let err = AntQuicError::Io(std::io::Error::new(
std::io::ErrorKind::Interrupted,
"interrupted",
));
assert!(utils::is_recoverable(&err));
}
#[test]
fn io_permission_denied_is_not_recoverable() {
let err = AntQuicError::Io(std::io::Error::new(
std::io::ErrorKind::PermissionDenied,
"denied",
));
assert!(!utils::is_recoverable(&err));
}
#[test]
fn generic_error_is_not_recoverable() {
let err = std::io::Error::other("other");
assert!(!utils::is_recoverable(&err));
}
#[test]
fn crypto_error_is_not_recoverable() {
let err = AntQuicError::Crypto("x".to_string());
assert!(!utils::is_recoverable(&err));
}
#[test]
fn timeout_has_retry_delay() {
let err = AntQuicError::Timeout("x".to_string());
let delay = utils::get_retry_delay(&err);
assert!(delay.is_some());
assert_eq!(delay.unwrap(), std::time::Duration::from_millis(100));
}
#[test]
fn connection_has_retry_delay() {
let err = AntQuicError::Connection(crate::connection::ConnectionError::LocallyClosed);
let delay = utils::get_retry_delay(&err);
assert!(delay.is_some());
assert_eq!(delay.unwrap(), std::time::Duration::from_millis(500));
}
#[test]
fn discovery_has_retry_delay() {
use crate::candidate_discovery::DiscoveryError;
let err = AntQuicError::Discovery(DiscoveryError::NoLocalInterfaces);
let delay = utils::get_retry_delay(&err);
assert!(delay.is_some());
}
#[test]
fn internal_has_no_retry_delay() {
let err = AntQuicError::Internal("x".to_string());
assert!(utils::get_retry_delay(&err).is_none());
}
#[test]
fn generic_error_has_no_retry_delay() {
let err = std::io::Error::other("other");
assert!(utils::get_retry_delay(&err).is_none());
}
#[test]
fn io_timeout_has_retry_delay() {
let err = AntQuicError::Io(std::io::Error::new(std::io::ErrorKind::TimedOut, "timeout"));
let delay = utils::get_retry_delay(&err);
assert!(delay.is_some());
assert_eq!(delay.unwrap(), std::time::Duration::from_millis(100));
}
#[test]
fn io_interrupted_has_retry_delay() {
let err = AntQuicError::Io(std::io::Error::new(std::io::ErrorKind::Interrupted, "int"));
let delay = utils::get_retry_delay(&err);
assert!(delay.is_some());
assert_eq!(delay.unwrap(), std::time::Duration::from_millis(10));
}
#[test]
fn ensure_passes_when_true() {
let result: Result<()> = (|| {
ensure!(
true,
AntQuicError::Internal("should not happen".to_string())
);
Ok(())
})();
assert!(result.is_ok());
}
#[test]
fn ensure_fails_when_false() {
let result: Result<()> = (|| {
ensure!(false, AntQuicError::InvalidParameter("bad".to_string()));
Ok(())
})();
assert!(result.is_err());
assert!(matches!(
result.unwrap_err(),
AntQuicError::InvalidParameter(_)
));
}
#[test]
fn bail_returns_error() {
let result: Result<()> = (|| {
bail!(AntQuicError::Internal("bailed".to_string()));
})();
assert!(result.is_err());
assert!(format!("{}", result.unwrap_err()).contains("bailed"));
}
#[test]
fn bail_with_type_conversion() {
let result: Result<()> = (|| {
let io_err = std::io::Error::new(std::io::ErrorKind::NotFound, "missing");
bail!(io_err);
})();
assert!(result.is_err());
assert!(matches!(result.unwrap_err(), AntQuicError::Io(_)));
}
#[test]
fn error_source_chain_transport() {
let err = AntQuicError::Transport(crate::transport_error::Error::INTERNAL_ERROR("chain"));
let source = std::error::Error::source(&err);
assert!(source.is_some());
}
}