//! Production-ready error types for the mesh P2P layer. //! //! This module provides structured error types with context for debugging //! and recovery. Errors are categorized by subsystem for easier handling. use std::fmt; use thiserror::Error; use crate::address::MeshAddress; use crate::transport::TransportAddr; /// Top-level mesh error type. #[derive(Debug, Error)] pub enum MeshError { /// Transport layer errors. #[error("transport error: {0}")] Transport(#[from] TransportError), /// Routing errors. #[error("routing error: {0}")] Routing(#[from] RoutingError), /// Crypto/encryption errors. #[error("crypto error: {0}")] Crypto(#[from] CryptoError), /// Protocol errors (malformed messages, version mismatch). #[error("protocol error: {0}")] Protocol(#[from] ProtocolError), /// Store/cache errors. #[error("store error: {0}")] Store(#[from] StoreError), /// Configuration errors. #[error("config error: {0}")] Config(#[from] ConfigError), /// Internal errors (bugs, invariant violations). #[error("internal error: {0}")] Internal(String), } /// Transport layer errors. #[derive(Debug, Error)] pub enum TransportError { /// Failed to send data. #[error("send failed to {dest}: {reason}")] SendFailed { dest: String, reason: String }, /// Failed to receive data. #[error("receive failed: {0}")] ReceiveFailed(String), /// Connection failed or lost. #[error("connection to {dest} failed: {reason}")] ConnectionFailed { dest: String, reason: String }, /// Transport not available. #[error("transport '{name}' not available")] NotAvailable { name: String }, /// No transports registered. #[error("no transports registered")] NoTransports, /// MTU exceeded. #[error("payload {size} bytes exceeds MTU {mtu} bytes")] MtuExceeded { size: usize, mtu: usize }, /// Duty cycle limit reached. #[error("duty cycle limit reached: {used_ms}ms used of {limit_ms}ms allowed")] DutyCycleExceeded { used_ms: u64, limit_ms: u64 }, /// Timeout waiting for response. #[error("timeout waiting for response from {dest}")] Timeout { dest: String }, /// I/O error. #[error("I/O error: {0}")] Io(#[from] std::io::Error), } /// Routing errors. #[derive(Debug, Error)] pub enum RoutingError { /// No route to destination. #[error("no route to {0}")] NoRoute(String), /// Route expired. #[error("route to {dest} expired (last seen {age_secs}s ago)")] RouteExpired { dest: String, age_secs: u64 }, /// Too many hops. #[error("max hops ({max}) exceeded for message to {dest}")] MaxHopsExceeded { dest: String, max: u8 }, /// Message expired. #[error("message expired (TTL {ttl_secs}s, age {age_secs}s)")] MessageExpired { ttl_secs: u32, age_secs: u64 }, /// Duplicate message (dedup). #[error("duplicate message ID {0}")] Duplicate(String), /// Routing table full. #[error("routing table full ({capacity} entries)")] TableFull { capacity: usize }, } /// Crypto/encryption errors. #[derive(Debug, Error)] pub enum CryptoError { /// Signature verification failed. #[error("signature verification failed for {context}")] SignatureInvalid { context: String }, /// Decryption failed. #[error("decryption failed: {0}")] DecryptionFailed(String), /// Key not found. #[error("key not found for {0}")] KeyNotFound(String), /// KeyPackage invalid or expired. #[error("KeyPackage invalid: {0}")] KeyPackageInvalid(String), /// Replay attack detected. #[error("replay detected: sequence {seq} already seen from {sender}")] ReplayDetected { sender: String, seq: u32 }, /// Wrong epoch. #[error("wrong epoch: expected {expected}, got {got}")] WrongEpoch { expected: u16, got: u16 }, /// MLS error (from openmls). #[error("MLS error: {0}")] Mls(String), } /// Protocol errors. #[derive(Debug, Error)] pub enum ProtocolError { /// Unknown message type. #[error("unknown message type: 0x{0:02x}")] UnknownMessageType(u8), /// Invalid message format. #[error("invalid message format: {0}")] InvalidFormat(String), /// Version mismatch. #[error("protocol version mismatch: expected {expected}, got {got}")] VersionMismatch { expected: u8, got: u8 }, /// Required field missing. #[error("required field missing: {0}")] MissingField(String), /// CBOR decode error. #[error("CBOR decode error: {0}")] CborDecode(String), /// CBOR encode error. #[error("CBOR encode error: {0}")] CborEncode(String), /// Message too large. #[error("message too large: {size} bytes (max {max})")] MessageTooLarge { size: usize, max: usize }, } /// Store/cache errors. #[derive(Debug, Error)] pub enum StoreError { /// Store is full. #[error("store full: {current}/{capacity} items")] Full { current: usize, capacity: usize }, /// Item not found. #[error("item not found: {0}")] NotFound(String), /// Persistence error. #[error("persistence error: {0}")] Persistence(String), /// Serialization error. #[error("serialization error: {0}")] Serialization(String), } /// Configuration errors. #[derive(Debug, Error)] pub enum ConfigError { /// Invalid configuration value. #[error("invalid config value for '{key}': {reason}")] InvalidValue { key: String, reason: String }, /// Missing required configuration. #[error("missing required config: {0}")] Missing(String), /// Configuration parse error. #[error("config parse error: {0}")] Parse(String), } /// Result type alias for mesh operations. pub type MeshResult = Result; /// Error context extension trait for adding context to errors. pub trait ErrorContext { /// Add context to an error. fn context(self, context: impl Into) -> MeshResult; /// Add context with a closure (lazy evaluation). fn with_context(self, f: F) -> MeshResult where F: FnOnce() -> String; } impl> ErrorContext for Result { fn context(self, context: impl Into) -> MeshResult { self.map_err(|e| { let err = e.into(); MeshError::Internal(format!("{}: {}", context.into(), err)) }) } fn with_context(self, f: F) -> MeshResult where F: FnOnce() -> String, { self.map_err(|e| { let err = e.into(); MeshError::Internal(format!("{}: {}", f(), err)) }) } } /// Convert anyhow errors to MeshError. impl From for MeshError { fn from(e: anyhow::Error) -> Self { MeshError::Internal(e.to_string()) } } /// Helper to create transport send errors. impl TransportError { pub fn send_failed(dest: &TransportAddr, reason: impl Into) -> Self { Self::SendFailed { dest: dest.to_string(), reason: reason.into(), } } pub fn connection_failed(dest: &TransportAddr, reason: impl Into) -> Self { Self::ConnectionFailed { dest: dest.to_string(), reason: reason.into(), } } } /// Helper to create routing errors. impl RoutingError { pub fn no_route(addr: &MeshAddress) -> Self { Self::NoRoute(format!("{}", addr)) } pub fn no_route_bytes(addr: &[u8]) -> Self { Self::NoRoute(hex::encode(&addr[..8.min(addr.len())])) } } /// Helper to create crypto errors. impl CryptoError { pub fn signature_invalid(context: impl Into) -> Self { Self::SignatureInvalid { context: context.into(), } } pub fn replay(sender: &MeshAddress, seq: u32) -> Self { Self::ReplayDetected { sender: format!("{}", sender), seq, } } } /// Helper to create protocol errors. impl ProtocolError { pub fn cbor_decode(e: impl fmt::Display) -> Self { Self::CborDecode(e.to_string()) } pub fn cbor_encode(e: impl fmt::Display) -> Self { Self::CborEncode(e.to_string()) } } #[cfg(test)] mod tests { use super::*; #[test] fn error_display() { let err = TransportError::SendFailed { dest: "tcp:127.0.0.1:8080".to_string(), reason: "connection refused".to_string(), }; assert!(err.to_string().contains("tcp:127.0.0.1:8080")); assert!(err.to_string().contains("connection refused")); } #[test] fn error_conversion() { let transport_err = TransportError::NoTransports; let mesh_err: MeshError = transport_err.into(); assert!(matches!(mesh_err, MeshError::Transport(_))); } #[test] fn routing_error_helpers() { let addr = MeshAddress::from_bytes([0xAB; 16]); let err = RoutingError::no_route(&addr); assert!(err.to_string().contains("no route")); } #[test] fn crypto_error_helpers() { let addr = MeshAddress::from_bytes([0xCD; 16]); let err = CryptoError::replay(&addr, 42); assert!(err.to_string().contains("42")); } #[test] fn context_extension() { fn fallible() -> Result<(), TransportError> { Err(TransportError::NoTransports) } let result: MeshResult<()> = fallible().context("during startup"); assert!(result.is_err()); let err_str = result.unwrap_err().to_string(); assert!(err_str.contains("during startup")); } }