distant_net/common/
destination.rs

1use std::fmt;
2use std::hash::Hash;
3use std::str::FromStr;
4
5use serde::de::Deserializer;
6use serde::ser::Serializer;
7use serde::{Deserialize, Serialize};
8
9use super::utils::{deserialize_from_str, serialize_to_str};
10
11mod host;
12mod parser;
13
14pub use host::{Host, HostParseError};
15
16/// `distant` connects and logs into the specified destination, which may be specified as either
17/// `hostname:port` where an attempt to connect to a **distant** server will be made, or a URI of
18/// one of the following forms:
19///
20/// * `distant://hostname:port` - connect to a distant server
21/// * `ssh://[user@]hostname[:port]` - connect to an SSH server
22///
23/// **Note:** Due to the limitations of a URI, an IPv6 address is not supported.
24#[derive(Clone, Debug, Hash, PartialEq, Eq)]
25pub struct Destination {
26    /// Sequence of characters beginning with a letter and followed by any combination of letters,
27    /// digits, plus (+), period (.), or hyphen (-) representing a scheme associated with a
28    /// destination
29    pub scheme: Option<String>,
30
31    /// Sequence of alphanumeric characters representing a username tied to a destination
32    pub username: Option<String>,
33
34    /// Sequence of alphanumeric characters representing a password tied to a destination
35    pub password: Option<String>,
36
37    /// Consisting of either a registered name (including but not limited to a hostname) or an IP
38    /// address. IPv4 addresses must be in dot-decimal notation, and IPv6 addresses must be
39    /// enclosed in brackets ([])
40    pub host: Host,
41
42    /// Port tied to a destination
43    pub port: Option<u16>,
44}
45
46impl Destination {
47    /// Returns true if the destination's scheme represents the specified (case-insensitive).
48    pub fn scheme_eq(&self, s: &str) -> bool {
49        match self.scheme.as_ref() {
50            Some(scheme) => scheme.eq_ignore_ascii_case(s),
51            None => false,
52        }
53    }
54}
55
56impl AsRef<Destination> for &Destination {
57    fn as_ref(&self) -> &Destination {
58        self
59    }
60}
61
62impl AsMut<Destination> for &mut Destination {
63    fn as_mut(&mut self) -> &mut Destination {
64        self
65    }
66}
67
68impl fmt::Display for Destination {
69    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
70        if let Some(scheme) = self.scheme.as_ref() {
71            write!(f, "{scheme}://")?;
72        }
73
74        if let Some(username) = self.username.as_ref() {
75            write!(f, "{username}")?;
76        }
77
78        if let Some(password) = self.password.as_ref() {
79            write!(f, ":{password}")?;
80        }
81
82        if self.username.is_some() || self.password.is_some() {
83            write!(f, "@")?;
84        }
85
86        // For host, if we have a port and are IPv6, we need to wrap in [{}]
87        match &self.host {
88            Host::Ipv6(x) if self.port.is_some() => write!(f, "[{x}]")?,
89            x => write!(f, "{x}")?,
90        }
91
92        if let Some(port) = self.port {
93            write!(f, ":{port}")?;
94        }
95
96        Ok(())
97    }
98}
99
100impl FromStr for Destination {
101    type Err = &'static str;
102
103    /// Parses a destination in the form `[scheme://][[username][:password]@]host[:port]`
104    fn from_str(s: &str) -> Result<Self, Self::Err> {
105        parser::parse(s)
106    }
107}
108
109impl FromStr for Box<Destination> {
110    type Err = &'static str;
111
112    fn from_str(s: &str) -> Result<Self, Self::Err> {
113        let destination = s.parse::<Destination>()?;
114        Ok(Box::new(destination))
115    }
116}
117
118impl<'a> PartialEq<&'a str> for Destination {
119    #[allow(clippy::cmp_owned)]
120    fn eq(&self, other: &&'a str) -> bool {
121        self.to_string() == *other
122    }
123}
124
125impl Serialize for Destination {
126    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
127    where
128        S: Serializer,
129    {
130        serialize_to_str(self, serializer)
131    }
132}
133
134impl<'de> Deserialize<'de> for Destination {
135    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
136    where
137        D: Deserializer<'de>,
138    {
139        deserialize_from_str(deserializer)
140    }
141}
142
143#[cfg(test)]
144mod tests {
145    use super::*;
146
147    #[test]
148    fn display_should_output_using_available_components() {
149        let destination = Destination {
150            scheme: None,
151            username: None,
152            password: None,
153            host: Host::Name("example.com".to_string()),
154            port: None,
155        };
156        assert_eq!(destination, "example.com");
157    }
158
159    #[test]
160    fn display_should_not_wrap_ipv6_in_square_brackets_if_has_no_port() {
161        let destination = Destination {
162            scheme: None,
163            username: None,
164            password: None,
165            host: Host::Ipv6("::1".parse().unwrap()),
166            port: None,
167        };
168        assert_eq!(destination, "::1");
169    }
170
171    #[test]
172    fn display_should_wrap_ipv6_in_square_brackets_if_has_port() {
173        let destination = Destination {
174            scheme: None,
175            username: None,
176            password: None,
177            host: Host::Ipv6("::1".parse().unwrap()),
178            port: Some(12345),
179        };
180        assert_eq!(destination, "[::1]:12345");
181    }
182}