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}