use std::fmt;
use std::io;
#[derive(Debug, Clone)]
pub struct ValidationError {
pub field: String,
pub message: String,
}
impl ValidationError {
pub fn new(field: impl Into<String>, message: impl Into<String>) -> Self {
Self {
field: field.into(),
message: message.into(),
}
}
pub fn empty(field: impl Into<String>) -> Self {
let field = field.into();
Self {
message: format!("{field} cannot be empty"),
field,
}
}
pub fn too_long(field: impl Into<String>, max_length: usize) -> Self {
let field = field.into();
Self {
message: format!("{field} exceeds maximum length of {max_length}"),
field,
}
}
pub fn invalid_characters(field: impl Into<String>) -> Self {
let field = field.into();
Self {
message: format!("{field} contains invalid characters"),
field,
}
}
}
impl fmt::Display for ValidationError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "Validation error for '{}': {}", self.field, self.message)
}
}
impl std::error::Error for ValidationError {}
#[derive(Debug)]
pub enum AuthError {
InvalidCredentials,
MethodNotSupported(String),
UserNotAllowed(String),
AccountLocked(String),
TooManyAttempts,
Timeout,
AgentNotAvailable,
NoIdentities,
KeyFileError(String),
KeyInvalid(String),
PassphraseRequired,
PassphraseIncorrect,
ServerRejected(String),
Internal(String),
}
impl fmt::Display for AuthError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
AuthError::InvalidCredentials => write!(f, "Invalid credentials"),
AuthError::MethodNotSupported(method) => {
write!(f, "Authentication method '{method}' not supported")
}
AuthError::UserNotAllowed(_) => {
write!(f, "User is not allowed to connect")
}
AuthError::AccountLocked(_) => write!(f, "Account is locked"),
AuthError::TooManyAttempts => write!(f, "Too many authentication attempts"),
AuthError::Timeout => write!(f, "Authentication timed out"),
AuthError::AgentNotAvailable => write!(f, "SSH agent not available"),
AuthError::NoIdentities => write!(f, "No identities available in SSH agent"),
AuthError::KeyFileError(path) => write!(f, "Cannot read key file: {path}"),
AuthError::KeyInvalid(reason) => write!(f, "Invalid key: {reason}"),
AuthError::PassphraseRequired => write!(f, "Passphrase required for key"),
AuthError::PassphraseIncorrect => write!(f, "Incorrect passphrase"),
AuthError::ServerRejected(reason) => write!(f, "Server rejected: {reason}"),
AuthError::Internal(reason) => write!(f, "Internal authentication error: {reason}"),
}
}
}
impl std::error::Error for AuthError {}
#[derive(Debug)]
pub enum ConnectionError {
DnsResolutionFailed(String),
ConnectionRefused(String),
Timeout(String),
NetworkUnreachable(String),
HostUnreachable(String),
ConnectionClosed(String),
ProtocolMismatch(String),
HostKeyVerificationFailed(String),
RateLimited(String),
TlsError(String),
Io(io::Error),
Other(String),
}
impl fmt::Display for ConnectionError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
ConnectionError::DnsResolutionFailed(host) => {
write!(f, "Could not resolve hostname: {host}")
}
ConnectionError::ConnectionRefused(host) => {
write!(f, "Connection refused by {host}")
}
ConnectionError::Timeout(msg) => write!(f, "Connection timed out: {msg}"),
ConnectionError::NetworkUnreachable(msg) => write!(f, "Network unreachable: {msg}"),
ConnectionError::HostUnreachable(host) => write!(f, "Host unreachable: {host}"),
ConnectionError::ConnectionClosed(msg) => write!(f, "Connection closed: {msg}"),
ConnectionError::ProtocolMismatch(msg) => write!(f, "Protocol mismatch: {msg}"),
ConnectionError::HostKeyVerificationFailed(msg) => {
write!(f, "Host key verification failed: {msg}")
}
ConnectionError::RateLimited(msg) => write!(f, "Rate limited: {msg}"),
ConnectionError::TlsError(msg) => write!(f, "TLS error: {msg}"),
ConnectionError::Io(err) => write!(f, "IO error: {err}"),
ConnectionError::Other(msg) => write!(f, "Connection error: {msg}"),
}
}
}
impl std::error::Error for ConnectionError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match self {
ConnectionError::Io(err) => Some(err),
_ => None,
}
}
}
impl From<io::Error> for ConnectionError {
fn from(err: io::Error) -> Self {
ConnectionError::Io(err)
}
}
#[derive(Debug, Clone)]
pub struct RateLimitError {
pub identifier: String,
pub wait_seconds: f64,
}
impl RateLimitError {
pub fn new(identifier: impl Into<String>, wait_seconds: f64) -> Self {
Self {
identifier: identifier.into(),
wait_seconds,
}
}
}
impl fmt::Display for RateLimitError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"Rate limit exceeded for '{}'. Please wait {:.1} seconds before retrying.",
self.identifier, self.wait_seconds
)
}
}
impl std::error::Error for RateLimitError {}
pub type SharedResult<T> = Result<T, Box<dyn std::error::Error + Send + Sync>>;
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_validation_error() {
let err = ValidationError::new("username", "contains spaces");
assert_eq!(err.field, "username");
assert!(err.to_string().contains("Validation error"));
let empty_err = ValidationError::empty("password");
assert!(empty_err.message.contains("cannot be empty"));
let long_err = ValidationError::too_long("hostname", 253);
assert!(long_err.message.contains("253"));
}
#[test]
fn test_auth_error_display() {
let err = AuthError::InvalidCredentials;
assert!(err.to_string().contains("Invalid credentials"));
let method_err = AuthError::MethodNotSupported("kerberos".to_string());
assert!(method_err.to_string().contains("kerberos"));
}
#[test]
fn test_connection_error() {
let err = ConnectionError::ConnectionRefused("example.com:22".to_string());
assert!(err.to_string().contains("Connection refused"));
let io_err = ConnectionError::from(io::Error::new(io::ErrorKind::NotFound, "test"));
assert!(io_err.to_string().contains("IO error"));
}
#[test]
fn test_rate_limit_error() {
let err = RateLimitError::new("192.168.1.1", 5.5);
assert!(err.to_string().contains("192.168.1.1"));
assert!(err.to_string().contains("5.5"));
}
}