From e7b58c4f4b1090deebaaf5ab3b477b8c441eef96 Mon Sep 17 00:00:00 2001 From: Alex Mikhalev Date: Wed, 28 Dec 2016 11:50:49 -0700 Subject: [PATCH] Added FromStr impl and tests for HostAndPort --- src/host.rs | 26 +++++++++++++++++++++++++- src/parser.rs | 1 + tests/unit.rs | 39 ++++++++++++++++++++++++++++++++++++++- 3 files changed, 64 insertions(+), 2 deletions(-) diff --git a/src/host.rs b/src/host.rs index 4da5848c..23321c7c 100644 --- a/src/host.rs +++ b/src/host.rs @@ -11,6 +11,7 @@ use std::cmp; use std::fmt::{self, Formatter}; use std::io; use std::net::{Ipv4Addr, Ipv6Addr, SocketAddr, SocketAddrV4, SocketAddrV6, ToSocketAddrs}; +use std::str::FromStr; use std::vec; use parser::{ParseResult, ParseError}; use percent_encoding::percent_decode; @@ -120,7 +121,7 @@ impl> fmt::Display for Host { /// This mostly exists because coherence rules don’t allow us to implement /// `ToSocketAddrs for (Host, u16)`. -#[derive(Clone)] +#[derive(Clone, PartialEq, Eq, Debug)] pub struct HostAndPort { pub host: Host, pub port: u16, @@ -136,6 +137,29 @@ impl<'a> HostAndPort<&'a str> { } } +impl FromStr for HostAndPort { + type Err = ParseError; + fn from_str(s: &str) -> Result { + // uses rfind because there can be colons in ipv6 addresses + let split_idx = match s.rfind(':') { + Some(idx) => idx, + None => return Err(ParseError::HostAndPortWithoutPort), + }; + let (host_str, port_str) = s.split_at(split_idx); + let host = try!(Host::parse(host_str)); + if port_str.len() == 0 { + return Err(ParseError::InvalidPort); + } + let port_str = &port_str[1..]; // to remove the : + let port: u16 = try!(port_str.parse() + .map_err(|_| ParseError::InvalidPort)); + Ok(HostAndPort { + host: host, + port: port, + }) + } +} + impl> ToSocketAddrs for HostAndPort { type Iter = SocketAddrs; diff --git a/src/parser.rs b/src/parser.rs index 92f27979..961b268b 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -52,6 +52,7 @@ simple_enum_error! { InvalidIpv6Address => "invalid IPv6 address", InvalidDomainCharacter => "invalid domain character", RelativeUrlWithoutBase => "relative URL without a base", + HostAndPortWithoutPort => "port number unspecified", RelativeUrlWithCannotBeABaseBase => "relative URL with a cannot-be-a-base base", SetHostOnCannotBeABaseUrl => "a cannot-be-a-base URL doesn’t have a host to set", Overflow => "URLs more than 4 GB are not supported", diff --git a/tests/unit.rs b/tests/unit.rs index b4d1992d..25b039b8 100644 --- a/tests/unit.rs +++ b/tests/unit.rs @@ -13,7 +13,7 @@ extern crate url; use std::borrow::Cow; use std::net::{Ipv4Addr, Ipv6Addr}; use std::path::{Path, PathBuf}; -use url::{Host, Url, form_urlencoded}; +use url::{Host, Url, form_urlencoded, HostAndPort, ParseError}; #[test] fn size() { @@ -245,6 +245,43 @@ fn test_form_serialize() { assert_eq!(encoded, "foo=%C3%A9%26&bar=&foo=%23"); } +#[test] +fn test_hostandport_from_str() { + use std::str::FromStr; + let cases: Vec<(&str, HostAndPort<_>)> = vec![ + ("localhost:1234", HostAndPort { host: Host::Domain("localhost"), port: 1234 }), + ("192.168.0.1:54321", HostAndPort { + host: Host::Ipv4(Ipv4Addr::new(192, 168, 0, 1)), + port: 54321, + }), + ("[::1]:9999", HostAndPort { + host: Host::Ipv6(Ipv6Addr::new(0, 0, 0, 0, 0, 0, 0, 1)), + port: 9999, + }), + (":0", HostAndPort { + host: Host::Domain(""), + port: 0, + }), + ]; + + for (s, expected_result) in cases { + let result: HostAndPort = s.parse().unwrap(); + assert_eq!(result, expected_result.to_owned()); + } + + let error_cases: Vec<(&str, ParseError)> = vec![ + ("localhost", ParseError::HostAndPortWithoutPort), + ("localhost:", ParseError::InvalidPort), + ("hello:world", ParseError::InvalidPort), + ("hello::8", ParseError::InvalidDomainCharacter), + ]; + + for (s, expected_error) in error_cases { + let error = HostAndPort::from_str(s).unwrap_err(); + assert_eq!(error, expected_error); + } +} + #[test] /// https://github.com/servo/rust-url/issues/25 fn issue_25() {