use thiserror::Error; use std::time::{Duration, SystemTime, UNIX_EPOCH}; /// Errors returned when the system clock cannot be represented as MTP time. #[derive(Clone, Copy, Debug, Error, PartialEq, Eq)] pub enum TimeError { #[error("system clock is before the Unix epoch")] BeforeUnixEpoch, #[error("Unix epoch milliseconds exceed the u64 range")] OutOfRange, } fn duration_to_unix_time_millis(duration: Duration) -> Result { u64::try_from(duration.as_millis()).map_err(|_| TimeError::OutOfRange) } /// Return the current Unix time in milliseconds. /// /// MTP protocol fields that use `CreatedAt` store this value as an unsigned /// integer. The conversion is centralized here so native writers do not /// accidentally use seconds. pub fn unix_time_millis() -> Result { let duration = SystemTime::now() .duration_since(UNIX_EPOCH) .map_err(|_| TimeError::BeforeUnixEpoch)?; duration_to_unix_time_millis(duration) } #[derive(Clone, Debug, Error, PartialEq, Eq)] pub enum CodecError { #[error("Unknown version")] UnknownVersion, #[error("Unknown communication type: {0}")] UnknownCommunicationType(String), #[error("Unknown data type: {0}")] UnknownDataType(String), #[error("Reserved communication type: {0}")] ReservedCommunicationType(u16), #[error("Invalid encoding")] InvalidEncoding, #[error("Too many entries to encode")] TooManyEntries, #[error("Crypto failed: {0}")] CryptoFailed(String), #[error("Missing required field: {0}")] MissingField(String), } /* ================================ TESTS ================================ */ #[cfg(test)] mod tests { use super::*; #[test] fn unix_time_millis_preserves_subsecond_precision() { let duration = Duration::new(1_786_449_600, 123_000_000); assert_eq!( duration_to_unix_time_millis(duration), Ok(1_786_449_600_123) ); } #[test] fn unix_time_millis_rejects_values_outside_u64() { let duration = Duration::new(u64::MAX, 0); assert_eq!( duration_to_unix_time_millis(duration), Err(TimeError::OutOfRange) ); } #[test] fn test_codec_error_display() { let e = CodecError::InvalidEncoding; assert_eq!(format!("{}", e), "Invalid encoding"); } #[test] fn test_codec_error_unknown_version() { assert_eq!(format!("{}", CodecError::UnknownVersion), "Unknown version"); } #[test] fn test_codec_error_clone_eq() { let a = CodecError::InvalidEncoding; let b = a.clone(); assert_eq!(a, b); } } /* CommunicationError * * On native targets the full variant set (including quinn / wtransport * wrappers) is available. On WASM only the transport-independent subset is * compiled. */ #[derive(Debug, Error, Clone)] pub enum CommunicationError { #[error("Use after Closed")] UseAfterClosed, #[error("Connection closed by local shutdown")] ClosedLocally, #[error("Connection closed by peer")] ClosedByPeer, #[error("Connection terminated unexpectedly")] ConnectionLost, #[error("QUIC error: {0}")] #[cfg(not(target_arch = "wasm32"))] Quinn(#[from] quinn::ConnectionError), #[error("ParseCommunicationValue error")] ParseCommunicationValue, #[error("Encode error")] Encode, #[error("Parse Certificate error")] CertificateParseFailed, #[error("Loading Certificate error")] CertificateLoadFailed, #[error("Parse error: {0}")] ParseError(String), #[error("Connection error: {0}")] #[cfg(not(target_arch = "wasm32"))] ConnectionError(#[from] wtransport::error::ConnectionError), #[error("Connecting error: {0}")] ConnectingError(String), #[error("ReadToEnd error: {0}")] #[cfg(not(target_arch = "wasm32"))] ReadToEndError(#[from] quinn::ReadToEndError), #[error("Write error: {0}")] #[cfg(not(target_arch = "wasm32"))] WriteError(#[from] quinn::WriteError), #[error("Closed error: {0}")] #[cfg(not(target_arch = "wasm32"))] ClosedError(#[from] quinn::ClosedStream), #[error("Message too large")] MessageTooLarge, #[error("ReadExactError: {0}")] #[cfg(not(target_arch = "wasm32"))] ReadExactError(#[from] quinn::ReadExactError), #[error("Stream Closed")] StreamClosed, #[error("Stream Error")] StreamError, #[error("Stream Error: {0}")] #[cfg(not(target_arch = "wasm32"))] StreamWriteError(#[from] wtransport::error::StreamWriteError), #[error("Read Exact Error: {0}")] #[cfg(not(target_arch = "wasm32"))] StreamReadExactError(#[from] wtransport::error::StreamReadExactError), #[error("Crypto Provider Install Error")] CryptoProviderInstallFailed, #[error("Authentication failed: {0}")] AuthenticationFailed(String), #[error("Other: {0}")] Other(String), } // ---- manual PartialEq (quinn / wtransport types don't impl PartialEq) ---- impl PartialEq for CommunicationError { fn eq(&self, other: &Self) -> bool { match (self, other) { (Self::UseAfterClosed, Self::UseAfterClosed) => true, (Self::ClosedLocally, Self::ClosedLocally) => true, (Self::ClosedByPeer, Self::ClosedByPeer) => true, (Self::ConnectionLost, Self::ConnectionLost) => true, #[cfg(not(target_arch = "wasm32"))] (Self::Quinn(_), Self::Quinn(_)) => true, (Self::ParseCommunicationValue, Self::ParseCommunicationValue) => true, (Self::Encode, Self::Encode) => true, (Self::CertificateParseFailed, Self::CertificateParseFailed) => true, (Self::CertificateLoadFailed, Self::CertificateLoadFailed) => true, (Self::ParseError(a), Self::ParseError(b)) => a == b, #[cfg(not(target_arch = "wasm32"))] (Self::ConnectionError(_), Self::ConnectionError(_)) => true, (Self::ConnectingError(a), Self::ConnectingError(b)) => a == b, #[cfg(not(target_arch = "wasm32"))] (Self::ReadToEndError(_), Self::ReadToEndError(_)) => true, #[cfg(not(target_arch = "wasm32"))] (Self::WriteError(_), Self::WriteError(_)) => true, #[cfg(not(target_arch = "wasm32"))] (Self::ClosedError(_), Self::ClosedError(_)) => true, (Self::MessageTooLarge, Self::MessageTooLarge) => true, #[cfg(not(target_arch = "wasm32"))] (Self::ReadExactError(_), Self::ReadExactError(_)) => true, (Self::StreamClosed, Self::StreamClosed) => true, (Self::StreamError, Self::StreamError) => true, #[cfg(not(target_arch = "wasm32"))] (Self::StreamWriteError(_), Self::StreamWriteError(_)) => true, #[cfg(not(target_arch = "wasm32"))] (Self::StreamReadExactError(_), Self::StreamReadExactError(_)) => true, (Self::CryptoProviderInstallFailed, Self::CryptoProviderInstallFailed) => true, (Self::AuthenticationFailed(a), Self::AuthenticationFailed(b)) => a == b, (Self::Other(a), Self::Other(b)) => a == b, _ => false, } } } impl Eq for CommunicationError {} /* ================================ PipeError ================================ */ #[cfg(feature = "pipes")] #[derive(Debug, Clone, PartialEq, Eq)] pub enum PipeError { Rejected, HandshakeTimeout, StreamClosed, IoError(String), ConnectionClosed, } #[cfg(feature = "pipes")] impl std::fmt::Display for PipeError { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { PipeError::Rejected => write!(f, "pipe request was rejected"), PipeError::HandshakeTimeout => write!(f, "pipe handshake timed out"), PipeError::StreamClosed => write!(f, "pipe stream closed unexpectedly"), PipeError::IoError(s) => write!(f, "pipe I/O error: {s}"), PipeError::ConnectionClosed => write!(f, "connection closed"), } } } #[cfg(feature = "pipes")] impl std::error::Error for PipeError {} #[cfg(feature = "pipes")] impl From for PipeError { fn from(e: CommunicationError) -> Self { match e { CommunicationError::StreamClosed => PipeError::StreamClosed, CommunicationError::ConnectionError(_) => PipeError::ConnectionClosed, other => PipeError::IoError(other.to_string()), } } } /* ===================== Handshake Outcome Types ===================== */ #[derive(Debug, Clone, PartialEq, Eq)] pub enum RejectionReason { BadVersion { supported_versions: Vec }, AuthenticationFailed { detail: String }, RateLimited, } impl std::fmt::Display for RejectionReason { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { RejectionReason::BadVersion { supported_versions } => { write!( f, "unsupported protocol version; supported: {}", supported_versions.join(", ") ) } RejectionReason::AuthenticationFailed { detail } => { write!(f, "authentication failed: {detail}") } RejectionReason::RateLimited => write!(f, "rate limited"), } } } #[derive(Debug, Clone, PartialEq, Eq)] pub enum HandshakeOutcome { Accepted { version: String, assigned_id: u64 }, Rejected { reason: RejectionReason }, } impl std::fmt::Display for HandshakeOutcome { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { HandshakeOutcome::Accepted { version, assigned_id, } => { write!(f, "accepted (version={version}, id={assigned_id})") } HandshakeOutcome::Rejected { reason } => write!(f, "rejected: {reason}"), } } } /* ================================ TESTS ================================ */ #[cfg(test)] mod communication_error_tests { use super::*; #[test] fn test_communication_error_display() { assert_eq!( format!("{}", CommunicationError::UseAfterClosed), "Use after Closed" ); assert_eq!( format!("{}", CommunicationError::StreamClosed), "Stream Closed" ); assert_eq!( format!("{}", CommunicationError::StreamError), "Stream Error" ); } #[test] fn test_communication_error_clone() { let a = CommunicationError::UseAfterClosed; let b = a.clone(); assert_eq!(format!("{:?}", a), format!("{:?}", b)); } #[test] fn test_communication_error_authentication_failed() { let e = CommunicationError::AuthenticationFailed("bad key".into()); assert!(format!("{}", e).contains("bad key")); } #[test] fn test_communication_error_other() { let e = CommunicationError::Other("custom error".into()); assert!(format!("{}", e).contains("custom error")); } #[cfg(not(target_arch = "wasm32"))] #[test] fn test_connecting_error_display() { let e = CommunicationError::ConnectingError("refused".into()); assert!(format!("{}", e).contains("refused")); } } /* ================================ PipeError TESTS ================================ */ #[cfg(feature = "pipes")] #[cfg(test)] mod pipe_error_tests { use super::*; #[test] fn test_pipe_error_display() { assert_eq!( format!("{}", PipeError::Rejected), "pipe request was rejected" ); assert_eq!( format!("{}", PipeError::HandshakeTimeout), "pipe handshake timed out" ); assert_eq!( format!("{}", PipeError::StreamClosed), "pipe stream closed unexpectedly" ); assert_eq!( format!("{}", PipeError::ConnectionClosed), "connection closed" ); assert_eq!( format!("{}", PipeError::IoError("boom".into())), "pipe I/O error: boom" ); } #[test] fn test_pipe_error_from_stream_closed() { let pe: PipeError = CommunicationError::StreamClosed.into(); assert_eq!(pe, PipeError::StreamClosed); } #[test] fn test_pipe_error_from_connection_error() { let pe: PipeError = CommunicationError::ConnectionError(wtransport::error::ConnectionError::TimedOut) .into(); assert_eq!(pe, PipeError::ConnectionClosed); } #[test] fn test_pipe_error_from_other() { let pe: PipeError = CommunicationError::StreamError.into(); assert_eq!(pe, PipeError::IoError("Stream Error".into())); } } /* ==================== HandshakeOutcome TESTS ==================== */ #[cfg(test)] mod handshake_outcome_tests { use super::*; #[test] fn test_accepted_display() { let outcome = HandshakeOutcome::Accepted { version: "1.0".into(), assigned_id: 42, }; assert_eq!(format!("{outcome}"), "accepted (version=1.0, id=42)"); } #[test] fn test_rejected_bad_version_display() { let outcome = HandshakeOutcome::Rejected { reason: RejectionReason::BadVersion { supported_versions: vec!["1.0".into(), "2.0".into()], }, }; let msg = format!("{outcome}"); assert!(msg.contains("1.0")); assert!(msg.contains("2.0")); } #[test] fn test_rejected_auth_failed_display() { let outcome = HandshakeOutcome::Rejected { reason: RejectionReason::AuthenticationFailed { detail: "invalid signature".into(), }, }; assert!(format!("{outcome}").contains("invalid signature")); } #[test] fn test_rejected_rate_limited_display() { let outcome = HandshakeOutcome::Rejected { reason: RejectionReason::RateLimited, }; assert_eq!(format!("{outcome}"), "rejected: rate limited"); } #[test] fn test_rejection_reason_display() { assert!( format!( "{}", RejectionReason::BadVersion { supported_versions: vec!["1.0".into()] } ) .contains("1.0") ); assert!( format!( "{}", RejectionReason::AuthenticationFailed { detail: "bad".into() } ) .contains("bad") ); assert_eq!(format!("{}", RejectionReason::RateLimited), "rate limited"); } #[test] fn test_handshake_outcome_clone_eq() { let a = HandshakeOutcome::Accepted { version: "1.0".into(), assigned_id: 1, }; let b = a.clone(); assert_eq!(a, b); } }