ichen_openprotocol/
address.rs

1use super::TextID;
2use derive_more::*;
3use lazy_static::*;
4use regex::Regex;
5use serde::{Deserialize, Deserializer, Serialize, Serializer};
6use std::convert::{TryFrom, TryInto};
7use std::net::Ipv4Addr;
8use std::num::{NonZeroU16, NonZeroU8};
9use std::str::FromStr;
10
11lazy_static! {
12    static ref IP_REGEX: Regex =
13        Regex::new(r#"^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}:\d{1,5}$"#).unwrap();
14    static ref TTY_REGEX: Regex = Regex::new(r#"^tty\w+$"#).unwrap();
15}
16
17/// A data structure holding a controller's physical address.
18///
19#[derive(Debug, Display, PartialEq, Eq, Hash, Clone)]
20pub enum Address<'a> {
21    /// Address unknown.
22    #[display(fmt = "0.0.0.0:0")]
23    Unknown,
24    //
25    /// An IP v.4 address plus port.
26    #[display(fmt = "{}:{}", _0, _1)]
27    IPv4(Ipv4Addr, NonZeroU16),
28    //
29    /// A Windows COM port.
30    #[display(fmt = "COM{}", _0)]
31    ComPort(NonZeroU8),
32    //
33    /// A UNIX-style tty serial port device.
34    #[display(fmt = "{}", _0)]
35    TtyDevice(TextID<'a>),
36}
37
38impl<'a> Address<'a> {
39    /// Create a new `Address::IPv4` from an IP address string and port number.
40    ///
41    /// The IP address cannot be unspecified (e.g. `0.0.0.0`).
42    /// The IP port cannot be zero.
43    ///
44    /// # Errors
45    ///
46    /// Returns `Err(String)` if:
47    /// * The IP address string is invalid,
48    /// * The IP address is unspecified (e.g. `0.0.0.0`),
49    /// * The IP port is zero.
50    ///
51    /// ## Error Examples
52    ///
53    /// ~~~
54    /// # use ichen_openprotocol::*;
55    /// assert_eq!(Err("invalid IP address: [hello]".into()), Address::new_ipv4("hello", 123));
56    /// assert_eq!(Err("IP port cannot be zero".into()), Address::new_ipv4("1.02.003.004", 0));
57    /// assert_eq!(Err("invalid null IP address".into()), Address::new_ipv4("0.00.000.0", 123));
58    /// ~~~
59    ///
60    /// # Examples
61    ///
62    /// ~~~
63    /// # use ichen_openprotocol::*;
64    /// # use std::str::FromStr;
65    /// # use std::net::Ipv4Addr;
66    /// # use std::num::NonZeroU16;
67    /// # fn main() -> std::result::Result<(), String> {
68    /// assert_eq!(
69    ///     Address::IPv4(Ipv4Addr::from_str("1.2.3.4").unwrap(), NonZeroU16::new(5).unwrap()),
70    ///     Address::new_ipv4("1.02.003.004", 5)?
71    /// );
72    /// # Ok(())
73    /// # }
74    /// ~~~
75    pub fn new_ipv4(addr: &str, port: u16) -> Result<Self, String> {
76        let addr =
77            Ipv4Addr::from_str(addr).map_err(|_| format!("invalid IP address: [{}]", addr))?;
78
79        if !addr.is_unspecified() {
80            Ok(Self::IPv4(addr, NonZeroU16::new(port).ok_or("IP port cannot be zero")?))
81        } else {
82            Err("invalid null IP address".into())
83        }
84    }
85
86    /// Create a new `Address::ComPort` from a Windows serial port number.
87    ///
88    /// The COM port number cannot be zero.
89    ///
90    /// # Errors
91    ///
92    /// Returns `Err(String)` if the COM port number is zero.
93    ///
94    /// ## Error Examples
95    ///
96    /// ~~~
97    /// # use ichen_openprotocol::*;
98    /// assert_eq!(Err("COM port cannot be zero".into()), Address::new_com_port(0));
99    /// ~~~
100    ///
101    /// # Examples
102    ///
103    /// ~~~
104    /// # use ichen_openprotocol::*;
105    /// # use std::num::NonZeroU8;
106    /// # fn main() -> std::result::Result<(), String> {
107    /// assert_eq!(
108    ///     Address::ComPort(NonZeroU8::new(5).unwrap()),
109    ///     Address::new_com_port(5)?
110    /// );
111    /// # Ok(())
112    /// # }
113    /// ~~~
114    pub fn new_com_port(port: u8) -> Result<Self, String> {
115        Ok(Self::ComPort(NonZeroU8::new(port).ok_or("COM port cannot be zero")?))
116    }
117
118    /// Create a new `Address::TtyDevice` from a UNIX-style tty device name.
119    ///
120    /// The device name should start with `tty`.
121    ///
122    /// # Errors
123    ///
124    /// Returns `Err(String)` if the device name is not valid for a tty.
125    ///
126    /// ## Error Examples
127    ///
128    /// ~~~
129    /// # use ichen_openprotocol::*;
130    /// assert_eq!(Err("invalid tty device: [hello]".into()), Address::new_tty_device("hello"));
131    /// ~~~
132    ///
133    /// # Examples
134    ///
135    /// ~~~
136    /// # use ichen_openprotocol::*;
137    /// # fn main() -> std::result::Result<(), String> {
138    /// # use std::borrow::Cow;
139    /// assert_eq!(
140    ///     Address::TtyDevice(TextID::new("ttyHello").unwrap()),
141    ///     Address::new_tty_device("ttyHello")?
142    /// );
143    /// # Ok(())
144    /// # }
145    /// ~~~
146    pub fn new_tty_device(device: &'a str) -> Result<Self, String> {
147        if TTY_REGEX.is_match(device) {
148            Ok(Address::TtyDevice(device.try_into()?))
149        } else {
150            Err(format!("invalid tty device: [{}]", device))
151        }
152    }
153}
154
155impl<'a> TryFrom<&'a str> for Address<'a> {
156    type Error = String;
157
158    /// Parse a text string into an `Address`.
159    ///
160    /// # Errors
161    ///
162    /// Returns `Err(String)` if the input string is not recognized as a valid address.
163    ///
164    /// ## Error Examples
165    ///
166    /// ~~~
167    /// # use ichen_openprotocol::*;
168    /// # use std::convert::TryFrom;
169    /// // The following should error because port cannot be zero if IP address is not zero
170    /// assert_eq!(
171    ///     Err("IP port cannot be zero".into()),
172    ///     Address::try_from("1.02.003.004:0")
173    /// );
174    ///
175    /// // The following should error because port must be zero if IP address is zero
176    /// assert_eq!(
177    ///     Err("null IP must have zero port number".into()),
178    ///     Address::try_from("0.0.0.0:123")
179    /// );
180    /// ~~~
181    ///
182    /// # Examples
183    ///
184    /// ~~~
185    /// # use ichen_openprotocol::*;
186    /// # use std::convert::TryFrom;
187    /// # use std::borrow::Cow;
188    /// # use std::str::FromStr;
189    /// # use std::num::{NonZeroU16, NonZeroU8};
190    /// # use std::net::Ipv4Addr;
191    /// # fn main() -> std::result::Result<(), String> {
192    /// assert_eq!(
193    ///     Address::IPv4(Ipv4Addr::from_str("1.2.3.4").unwrap(), NonZeroU16::new(5).unwrap()),
194    ///     Address::try_from("1.02.003.004:05")?
195    /// );
196    ///
197    /// // 0.0.0.0:0 is OK because both IP address and port are zero
198    /// assert_eq!(Address::Unknown, Address::try_from("0.0.0.0:0")?);
199    ///
200    /// assert_eq!(
201    ///     Address::ComPort(NonZeroU8::new(123).unwrap()),
202    ///     Address::try_from("COM123")?
203    /// );
204    ///
205    /// assert_eq!(
206    ///     Address::TtyDevice(TextID::new("ttyABC").unwrap()),
207    ///     Address::try_from("ttyABC")?
208    /// );
209    /// # Ok(())
210    /// # }
211    /// ~~~
212    fn try_from(item: &'a str) -> std::result::Result<Self, Self::Error> {
213        const PREFIX_COM: &str = "COM";
214
215        Ok(match item {
216            // Match COM port syntax
217            text if text.starts_with(PREFIX_COM) => {
218                let port = &text[PREFIX_COM.len()..];
219                let port =
220                    u8::from_str(port).map_err(|_| format!("invalid COM port: [{}]", port))?;
221                Address::new_com_port(port)?
222            }
223            //
224            // Match tty syntax
225            text if TTY_REGEX.is_match(text) => Address::new_tty_device(text)?,
226            //
227            // Match IP:port syntax
228            text if IP_REGEX.is_match(text) => {
229                // Check IP address validity
230                let (address, port) = text.split_at(text.find(':').unwrap());
231
232                let address = Ipv4Addr::from_str(address).map_err(|_| "invalid IP address")?;
233
234                // Check port
235                let port = &port[1..];
236
237                match u16::from_str(port) {
238                    // Allow port 0 on unspecified addresses only
239                    Ok(0) => {
240                        if !address.is_unspecified() {
241                            return Err("IP port cannot be zero".into());
242                        } else {
243                            Address::Unknown
244                        }
245                    }
246                    // Port must be 0 on unspecified addresses
247                    Ok(p) => {
248                        if address.is_unspecified() {
249                            return Err("null IP must have zero port number".into());
250                        } else {
251                            Address::IPv4(address, NonZeroU16::new(p).unwrap())
252                        }
253                    }
254                    // Other errors
255                    Err(_) => return Err(format!("invalid IP port: [{}]", port)),
256                }
257            }
258            // Failed to match any address type
259            _ => return Err(format!("invalid address: [{}]", item)),
260        })
261    }
262}
263
264impl Serialize for Address<'_> {
265    fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
266        Serialize::serialize(&self.to_string(), serializer)
267    }
268}
269
270impl<'a, 'de: 'a> Deserialize<'de> for Address<'a> {
271    fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
272        let s: &str = Deserialize::deserialize(deserializer)?;
273        Address::try_from(s).map_err(|err| serde::de::Error::custom(format!("{}: [{}]", err, s)))
274    }
275}