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