use super::utils::{deserialize_from_str, serialize_to_str}; use serde::{de::Deserializer, ser::Serializer, Deserialize, Serialize}; use std::{fmt, hash::Hash, str::FromStr}; mod host; mod parser; pub use host::{Host, HostParseError}; /// `distant` connects and logs into the specified destination, which may be specified as either /// `hostname:port` where an attempt to connect to a **distant** server will be made, or a URI of /// one of the following forms: /// /// * `distant://hostname:port` - connect to a distant server /// * `ssh://[user@]hostname[:port]` - connect to an SSH server /// /// **Note:** Due to the limitations of a URI, an IPv6 address is not supported. #[derive(Clone, Debug, Hash, PartialEq, Eq)] pub struct Destination { /// Sequence of characters beginning with a letter and followed by any combination of letters, /// digits, plus (+), period (.), or hyphen (-) representing a scheme associated with a /// destination pub scheme: Option, /// Sequence of alphanumeric characters representing a username tied to a destination pub username: Option, /// Sequence of alphanumeric characters representing a password tied to a destination pub password: Option, /// Consisting of either a registered name (including but not limited to a hostname) or an IP /// address. IPv4 addresses must be in dot-decimal notation, and IPv6 addresses must be /// enclosed in brackets ([]) pub host: Host, /// Port tied to a destination pub port: Option, } impl Destination { /// Returns true if the destination's scheme represents the specified (case-insensitive). pub fn scheme_eq(&self, s: &str) -> bool { match self.scheme.as_ref() { Some(scheme) => scheme.eq_ignore_ascii_case(s), None => false, } } } impl AsRef for &Destination { fn as_ref(&self) -> &Destination { self } } impl AsMut for &mut Destination { fn as_mut(&mut self) -> &mut Destination { self } } impl fmt::Display for Destination { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { if let Some(scheme) = self.scheme.as_ref() { write!(f, "{scheme}://")?; } if let Some(username) = self.username.as_ref() { write!(f, "{username}")?; } if let Some(password) = self.password.as_ref() { write!(f, ":{password}")?; } if self.username.is_some() || self.password.is_some() { write!(f, "@")?; } // For host, if we have a port and are IPv6, we need to wrap in [{}] match &self.host { Host::Ipv6(x) if self.port.is_some() => write!(f, "[{}]", x)?, x => write!(f, "{}", x)?, } if let Some(port) = self.port { write!(f, ":{port}")?; } Ok(()) } } impl FromStr for Destination { type Err = &'static str; /// Parses a destination in the form `[scheme://][[username][:password]@]host[:port]` fn from_str(s: &str) -> Result { parser::parse(s) } } impl FromStr for Box { type Err = &'static str; fn from_str(s: &str) -> Result { let destination = s.parse::()?; Ok(Box::new(destination)) } } impl<'a> PartialEq<&'a str> for Destination { #[allow(clippy::cmp_owned)] fn eq(&self, other: &&'a str) -> bool { self.to_string() == *other } } impl Serialize for Destination { fn serialize(&self, serializer: S) -> Result where S: Serializer, { serialize_to_str(self, serializer) } } impl<'de> Deserialize<'de> for Destination { fn deserialize(deserializer: D) -> Result where D: Deserializer<'de>, { deserialize_from_str(deserializer) } } #[cfg(test)] mod tests { use super::*; #[test] fn display_should_output_using_available_components() { let destination = Destination { scheme: None, username: None, password: None, host: Host::Name("example.com".to_string()), port: None, }; assert_eq!(destination, "example.com"); } #[test] fn display_should_not_wrap_ipv6_in_square_brackets_if_has_no_port() { let destination = Destination { scheme: None, username: None, password: None, host: Host::Ipv6("::1".parse().unwrap()), port: None, }; assert_eq!(destination, "::1"); } #[test] fn display_should_wrap_ipv6_in_square_brackets_if_has_port() { let destination = Destination { scheme: None, username: None, password: None, host: Host::Ipv6("::1".parse().unwrap()), port: Some(12345), }; assert_eq!(destination, "[::1]:12345"); } }