diff --git a/Cargo.toml b/Cargo.toml index 98b7e25ba..1299f2c05 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -2,7 +2,7 @@ name = "url" # When updating version, also modify html_root_url in the lib.rs -version = "2.0.0" +version = "2.1.0" authors = ["The rust-url developers"] description = "URL library for Rust, based on the WHATWG URL Standard" diff --git a/idna/tests/punycode.rs b/idna/tests/punycode.rs index 1851a32cf..aac0307bc 100644 --- a/idna/tests/punycode.rs +++ b/idna/tests/punycode.rs @@ -7,8 +7,8 @@ // except according to those terms. use idna::punycode::{decode, encode_str}; -use serde_json::Value; use serde_json::map::Map; +use serde_json::Value; use std::str::FromStr; use test::TestFn; diff --git a/idna/tests/tests.rs b/idna/tests/tests.rs index 995bbadd6..15d381b64 100644 --- a/idna/tests/tests.rs +++ b/idna/tests/tests.rs @@ -1,6 +1,6 @@ extern crate idna; -extern crate serde_json; extern crate rustc_test as test; +extern crate serde_json; mod punycode; mod uts46; diff --git a/src/lib.rs b/src/lib.rs index 355eebd1f..d60935c29 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -123,8 +123,9 @@ use std::cmp; use std::error::Error; use std::fmt::{self, Write}; use std::hash; +use std::io; use std::mem; -use std::net::IpAddr; +use std::net::{IpAddr, SocketAddr, ToSocketAddrs}; use std::ops::{Range, RangeFrom, RangeTo}; use std::path::{Path, PathBuf}; use std::str; @@ -945,6 +946,61 @@ impl Url { self.port.or_else(|| parser::default_port(self.scheme())) } + /// Resolve a URL’s host and port number to `SocketAddr`. + /// + /// If the URL has the default port number of a scheme that is unknown to this library, + /// `default_port_number` provides an opportunity to provide the actual port number. + /// In non-example code this should be implemented either simply as `|| None`, + /// or by matching on the URL’s `.scheme()`. + /// + /// If the host is a domain, it is resolved using the standard library’s DNS support. + /// + /// # Examples + /// + /// ```no_run + /// let url = url::Url::parse("https://example.net/").unwrap(); + /// let addrs = url.socket_addrs(|| None).unwrap(); + /// std::net::TcpStream::connect(&*addrs) + /// # ; + /// ``` + /// + /// ``` + /// /// With application-specific known default port numbers + /// fn socket_addrs(url: url::Url) -> std::io::Result> { + /// url.socket_addrs(|| match url.scheme() { + /// "socks5" | "socks5h" => Some(1080), + /// _ => None, + /// }) + /// } + /// ``` + pub fn socket_addrs( + &self, + default_port_number: impl Fn() -> Option, + ) -> io::Result> { + // Note: trying to avoid the Vec allocation by returning `impl AsRef<[SocketAddr]>` + // causes borrowck issues because the return value borrows `default_port_number`: + // + // https://github.com/rust-lang/rfcs/blob/master/text/1951-expand-impl-trait.md#scoping-for-type-and-lifetime-parameters + // + // > This RFC proposes that *all* type parameters are considered in scope + // > for `impl Trait` in return position + + fn io_result(opt: Option, message: &str) -> io::Result { + opt.ok_or_else(|| io::Error::new(io::ErrorKind::InvalidData, message)) + } + + let host = io_result(self.host(), "No host name in the URL")?; + let port = io_result( + self.port_or_known_default().or_else(default_port_number), + "No port number in the URL", + )?; + Ok(match host { + Host::Domain(domain) => (domain, port).to_socket_addrs()?.collect(), + Host::Ipv4(ip) => vec![(ip, port).into()], + Host::Ipv6(ip) => vec![(ip, port).into()], + }) + } + /// Return the path for this URL, as a percent-encoded ASCII string. /// For cannot-be-a-base URLs, this is an arbitrary string that doesn’t start with '/'. /// For other URLs, this starts with a '/' slash @@ -2296,9 +2352,8 @@ impl<'de> serde::Deserialize<'de> for Url { where E: Error, { - Url::parse(s).map_err(|err| { - Error::invalid_value(Unexpected::Str(s), &err.description()) - }) + Url::parse(s) + .map_err(|err| Error::invalid_value(Unexpected::Str(s), &err.description())) } }