feat(lib): remove extern Url type usage

BREAKING CHANGE: The `Url` type is no longer used. Any instance in the
  `Client` API has had it replaced with `hyper::Uri`.

  This also means `Error::Uri` has changed types to
  `hyper::error::UriError`.

  The type `hyper::header::parsing::HTTP_VALUE` has been made private,
  as an implementation detail. The function `http_percent_encoding`
  should be used instead.
This commit is contained in:
Sean McArthur
2017-03-21 10:35:31 -07:00
parent e81184e53c
commit 4fb7e6ebc6
12 changed files with 192 additions and 155 deletions

View File

@@ -28,7 +28,7 @@ fn get_one_at_a_time(b: &mut test::Bencher) {
let client = hyper::Client::new(&handle);
let url: hyper::Url = format!("http://{}/get", addr).parse().unwrap();
let url: hyper::Uri = format!("http://{}/get", addr).parse().unwrap();
b.bytes = 160 * 2 + PHRASE.len() as u64;
b.iter(move || {
@@ -51,7 +51,7 @@ fn post_one_at_a_time(b: &mut test::Bencher) {
let client = hyper::Client::new(&handle);
let url: hyper::Url = format!("http://{}/get", addr).parse().unwrap();
let url: hyper::Uri = format!("http://{}/get", addr).parse().unwrap();
let post = "foo bar baz quux";
b.bytes = 180 * 2 + post.len() as u64 + PHRASE.len() as u64;

View File

@@ -24,8 +24,8 @@ fn main() {
}
};
let url = hyper::Url::parse(&url).unwrap();
if url.scheme() != "http" {
let url = url.parse::<hyper::Uri>().unwrap();
if url.scheme() != Some("http") {
println!("This example only works with 'http' URLs.");
return;
}

View File

@@ -7,7 +7,7 @@ use tokio_io::{AsyncRead, AsyncWrite};
use tokio::reactor::Handle;
use tokio::net::{TcpStream, TcpStreamNew};
use tokio_service::Service;
use Url;
use Uri;
use super::dns;
@@ -15,25 +15,25 @@ use super::dns;
///
/// This trait is not implemented directly, and only exists to make
/// the intent clearer. A connector should implement `Service` with
/// `Request=Url` and `Response: Io` instead.
pub trait Connect: Service<Request=Url, Error=io::Error> + 'static {
/// `Request=Uri` and `Response: Io` instead.
pub trait Connect: Service<Request=Uri, Error=io::Error> + 'static {
/// The connected Io Stream.
type Output: AsyncRead + AsyncWrite + 'static;
/// A Future that will resolve to the connected Stream.
type Future: Future<Item=Self::Output, Error=io::Error> + 'static;
/// Connect to a remote address.
fn connect(&self, Url) -> <Self as Connect>::Future;
fn connect(&self, Uri) -> <Self as Connect>::Future;
}
impl<T> Connect for T
where T: Service<Request=Url, Error=io::Error> + 'static,
where T: Service<Request=Uri, Error=io::Error> + 'static,
T::Response: AsyncRead + AsyncWrite,
T::Future: Future<Error=io::Error>,
{
type Output = T::Response;
type Future = T::Future;
fn connect(&self, url: Url) -> <Self as Connect>::Future {
fn connect(&self, url: Uri) -> <Self as Connect>::Future {
self.call(url)
}
}
@@ -66,21 +66,21 @@ impl fmt::Debug for HttpConnector {
}
impl Service for HttpConnector {
type Request = Url;
type Request = Uri;
type Response = TcpStream;
type Error = io::Error;
type Future = HttpConnecting;
fn call(&self, url: Url) -> Self::Future {
fn call(&self, url: Uri) -> Self::Future {
debug!("Http::connect({:?})", url);
let host = match url.host_str() {
let host = match url.host() {
Some(s) => s,
None => return HttpConnecting {
state: State::Error(Some(io::Error::new(io::ErrorKind::InvalidInput, "invalid url"))),
handle: self.handle.clone(),
},
};
let port = url.port_or_known_default().unwrap_or(80);
let port = url.port().unwrap_or(80);
HttpConnecting {
state: State::Resolving(self.dns.resolve(host.into(), port)),
@@ -185,13 +185,12 @@ impl<S: SslClient> HttpsConnector<S> {
mod tests {
use std::io;
use tokio::reactor::Core;
use Url;
use super::{Connect, HttpConnector};
#[test]
fn test_non_http_url() {
let mut core = Core::new().unwrap();
let url = Url::parse("file:///home/sean/foo.txt").unwrap();
let url = "/foo/bar?baz".parse().unwrap();
let connector = HttpConnector::new(1, &core.handle());
assert_eq!(core.run(connector.connect(url)).unwrap_err().kind(), io::ErrorKind::InvalidInput);

View File

@@ -10,7 +10,7 @@ use std::marker::PhantomData;
use std::rc::Rc;
use std::time::Duration;
use futures::{Poll, Async, Future, Stream};
use futures::{future, Poll, Async, Future, Stream};
use futures::unsync::oneshot;
use tokio_io::{AsyncRead, AsyncWrite};
use tokio::reactor::Handle;
@@ -24,7 +24,7 @@ use header::{Headers, Host};
use http::{self, TokioBody};
use method::Method;
use self::pool::{Pool, Pooled};
use Url;
use uri::{self, Uri};
pub use self::connect::{HttpConnector, Connect};
pub use self::request::Request;
@@ -95,7 +95,7 @@ where C: Connect,
{
/// Send a GET Request using this Client.
#[inline]
pub fn get(&self, url: Url) -> FutureResponse {
pub fn get(&self, url: Uri) -> FutureResponse {
self.request(Request::new(Method::Get, url))
}
@@ -135,18 +135,30 @@ where C: Connect,
type Future = FutureResponse;
fn call(&self, req: Self::Request) -> Self::Future {
let url = req.url().clone();
let url = req.uri().clone();
let domain = match uri::scheme_and_authority(&url) {
Some(uri) => uri,
None => {
return FutureResponse(Box::new(future::err(::Error::Io(
io::Error::new(
io::ErrorKind::InvalidInput,
"invalid URI for Client Request"
)
))));
}
};
let host = Host::new(domain.host().expect("authority implies host").to_owned(), domain.port());
let (mut head, body) = request::split(req);
let mut headers = Headers::new();
headers.set(Host::new(url.host_str().unwrap().to_owned(), url.port()));
headers.set(host);
headers.extend(head.headers.iter());
head.headers = headers;
let checkout = self.pool.checkout(&url[..::url::Position::BeforePath]);
let checkout = self.pool.checkout(domain.as_ref());
let connect = {
let handle = self.handle.clone();
let pool = self.pool.clone();
let pool_key = Rc::new(url[..::url::Position::BeforePath].to_owned());
let pool_key = Rc::new(domain.to_string());
self.connector.connect(url)
.map(move |io| {
let (tx, rx) = oneshot::channel();

View File

@@ -1,18 +1,15 @@
use std::fmt;
use Url;
use header::Headers;
use http::{Body, RequestHead};
use method::Method;
use uri::Uri;
use uri::{self, Uri};
use version::HttpVersion;
use std::str::FromStr;
/// A client request to a remote server.
pub struct Request<B = Body> {
method: Method,
url: Url,
uri: Uri,
version: HttpVersion,
headers: Headers,
body: Option<B>,
@@ -22,10 +19,10 @@ pub struct Request<B = Body> {
impl<B> Request<B> {
/// Construct a new Request.
#[inline]
pub fn new(method: Method, url: Url) -> Request<B> {
pub fn new(method: Method, uri: Uri) -> Request<B> {
Request {
method: method,
url: url,
uri: uri,
version: HttpVersion::default(),
headers: Headers::new(),
body: None,
@@ -33,9 +30,9 @@ impl<B> Request<B> {
}
}
/// Read the Request Url.
/// Read the Request Uri.
#[inline]
pub fn url(&self) -> &Url { &self.url }
pub fn uri(&self) -> &Uri { &self.uri }
/// Read the Request Version.
#[inline]
@@ -57,9 +54,9 @@ impl<B> Request<B> {
#[inline]
pub fn headers_mut(&mut self) -> &mut Headers { &mut self.headers }
/// Set the `Url` of this request.
/// Set the `Uri` of this request.
#[inline]
pub fn set_url(&mut self, url: Url) { self.url = url; }
pub fn set_uri(&mut self, uri: Uri) { self.uri = uri; }
/// Set the `HttpVersion` of this request.
#[inline]
@@ -81,7 +78,7 @@ impl<B> fmt::Debug for Request<B> {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
f.debug_struct("Request")
.field("method", &self.method)
.field("url", &self.url)
.field("uri", &self.uri)
.field("version", &self.version)
.field("headers", &self.headers)
.finish()
@@ -90,9 +87,9 @@ impl<B> fmt::Debug for Request<B> {
pub fn split<B>(req: Request<B>) -> (RequestHead, Option<B>) {
let uri = if req.is_proxy {
Uri::from(req.url)
req.uri
} else {
Uri::from_str(&req.url[::url::Position::BeforePath..::url::Position::AfterQuery]).expect("url is not uri")
uri::origin_form(&req.uri)
};
let head = RequestHead {
subject: ::http::RequestLine(req.method, uri),

View File

@@ -6,7 +6,8 @@ use std::str::Utf8Error;
use std::string::FromUtf8Error;
use httparse;
use url;
pub use uri::UriError;
use self::Error::{
Method,
@@ -21,8 +22,6 @@ use self::Error::{
Utf8
};
pub use url::ParseError;
/// Result type often returned from methods that can have hyper `Error`s.
pub type Result<T> = ::std::result::Result<T, Error>;
@@ -32,7 +31,7 @@ pub enum Error {
/// An invalid `Method`, such as `GE,T`.
Method,
/// An invalid `Uri`, such as `exam ple.domain`.
Uri(url::ParseError),
Uri(UriError),
/// An invalid `HttpVersion`, such as `HTP/1.1`
Version,
/// An invalid `Header`.
@@ -102,15 +101,15 @@ impl StdError for Error {
}
}
impl From<IoError> for Error {
fn from(err: IoError) -> Error {
Io(err)
impl From<UriError> for Error {
fn from(err: UriError) -> Error {
Uri(err)
}
}
impl From<url::ParseError> for Error {
fn from(err: url::ParseError) -> Error {
Uri(err)
impl From<IoError> for Error {
fn from(err: IoError) -> Error {
Io(err)
}
}
@@ -145,7 +144,6 @@ mod tests {
use std::error::Error as StdError;
use std::io;
use httparse;
use url;
use super::Error;
use super::Error::*;
@@ -185,7 +183,6 @@ mod tests {
fn test_from() {
from_and_cause!(io::Error::new(io::ErrorKind::Other, "other") => Io(..));
from_and_cause!(url::ParseError::EmptyHost => Uri(..));
from!(httparse::Error::HeaderName => Header);
from!(httparse::Error::HeaderName => Header);
@@ -196,14 +193,4 @@ mod tests {
from!(httparse::Error::TooManyHeaders => TooLarge);
from!(httparse::Error::Version => Version);
}
#[cfg(feature = "openssl")]
#[test]
fn test_from_ssl() {
use openssl::ssl::error::SslError;
from!(SslError::StreamError(
io::Error::new(io::ErrorKind::Other, "ssl negotiation")) => Io(..));
from_and_cause!(SslError::SslSessionClosed => Ssl(..));
}
}

View File

@@ -9,10 +9,9 @@
use language_tags::LanguageTag;
use std::fmt;
use unicase::UniCase;
use url::percent_encoding;
use header::{Header, Raw, parsing};
use header::parsing::{parse_extended_value, HTTP_VALUE};
use header::parsing::{parse_extended_value, http_percent_encode};
use header::shared::Charset;
/// The implied disposition of the content of the HTTP body
@@ -182,8 +181,7 @@ impl fmt::Display for ContentDisposition {
try!(write!(f, "{}", lang));
};
try!(write!(f, "'"));
try!(f.write_str(
&percent_encoding::percent_encode(bytes, HTTP_VALUE).to_string()))
try!(http_percent_encode(f, bytes))
}
},
DispositionParam::Ext(ref k, ref v) => try!(write!(f, "; {}=\"{}\"", k, v)),

View File

@@ -9,6 +9,7 @@ use url::percent_encoding;
use header::Raw;
use header::shared::Charset;
/// Reads a single raw string when parsing a header.
pub fn from_one_raw_str<T: str::FromStr>(raw: &Raw) -> ::Result<T> {
if let Some(line) = raw.one() {
@@ -132,25 +133,11 @@ pub fn parse_extended_value(val: &str) -> ::Result<ExtendedValue> {
})
}
define_encode_set! {
/// This encode set is used for HTTP header values and is defined at
/// https://tools.ietf.org/html/rfc5987#section-3.2
pub HTTP_VALUE = [percent_encoding::SIMPLE_ENCODE_SET] | {
' ', '"', '%', '\'', '(', ')', '*', ',', '/', ':', ';', '<', '-', '>', '?',
'[', '\\', ']', '{', '}'
}
}
impl fmt::Debug for HTTP_VALUE {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
f.pad("HTTP_VALUE")
}
}
impl Display for ExtendedValue {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
let encoded_value =
percent_encoding::percent_encode(&self.value[..], HTTP_VALUE);
percent_encoding::percent_encode(&self.value[..], self::percent_encoding_http::HTTP_VALUE);
if let Some(ref lang) = self.language_tag {
write!(f, "{}'{}'{}", self.charset, lang, encoded_value)
} else {
@@ -159,6 +146,33 @@ impl Display for ExtendedValue {
}
}
/// Percent encode a sequence of bytes with a character set defined in
/// https://tools.ietf.org/html/rfc5987#section-3.2
pub fn http_percent_encode(f: &mut fmt::Formatter, bytes: &[u8]) -> fmt::Result {
let encoded = percent_encoding::percent_encode(bytes, self::percent_encoding_http::HTTP_VALUE);
fmt::Display::fmt(&encoded, f)
}
mod percent_encoding_http {
use std::fmt;
use url::percent_encoding;
define_encode_set! {
/// This encode set is used for HTTP header values and is defined at
/// https://tools.ietf.org/html/rfc5987#section-3.2
pub HTTP_VALUE = [percent_encoding::SIMPLE_ENCODE_SET] | {
' ', '"', '%', '\'', '(', ')', '*', ',', '/', ':', ';', '<', '-', '>', '?',
'[', '\\', ']', '{', '}'
}
}
impl fmt::Debug for HTTP_VALUE {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
f.pad("HTTP_VALUE")
}
}
}
#[cfg(test)]
mod tests {
use header::shared::Charset;

View File

@@ -61,7 +61,7 @@ impl Http1Transaction for ServerTransaction {
let path = unsafe { ByteStr::from_utf8_unchecked(path) };
let subject = RequestLine(
method,
try!(::uri::from_mem_str(path)),
try!(::uri::from_byte_str(path)),
);
headers.extend(HeadersAsBytesIter {

View File

@@ -1,8 +1,9 @@
use std::ops::Deref;
use std::str;
use bytes::Bytes;
#[derive(Debug, Clone, PartialEq, Eq)]
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct ByteStr(Bytes);
impl ByteStr {
@@ -10,7 +11,35 @@ impl ByteStr {
ByteStr(slice)
}
pub fn from_static(s: &'static str) -> ByteStr {
ByteStr(Bytes::from_static(s.as_bytes()))
}
pub fn slice(&self, from: usize, to: usize) -> ByteStr {
assert!(self.as_str().is_char_boundary(from));
assert!(self.as_str().is_char_boundary(to));
ByteStr(self.0.slice(from, to))
}
pub fn slice_to(&self, idx: usize) -> ByteStr {
assert!(self.as_str().is_char_boundary(idx));
ByteStr(self.0.slice_to(idx))
}
pub fn as_str(&self) -> &str {
unsafe { str::from_utf8_unchecked(self.0.as_ref()) }
}
}
impl Deref for ByteStr {
type Target = str;
fn deref(&self) -> &str {
self.as_str()
}
}
impl<'a> From<&'a str> for ByteStr {
fn from(s: &'a str) -> ByteStr {
ByteStr(Bytes::from(s))
}
}

View File

@@ -1,6 +1,6 @@
#![doc(html_root_url = "https://hyperium.github.io/hyper/")]
#![deny(missing_docs)]
//#![deny(warnings)]
#![deny(warnings)]
#![deny(missing_debug_implementations)]
#![cfg_attr(all(test, feature = "nightly"), feature(test))]
@@ -35,7 +35,6 @@ extern crate test;
pub use uri::Uri;
pub use url::Url;
pub use client::Client;
pub use error::{Result, Error};
pub use header::Headers;

View File

@@ -1,13 +1,8 @@
use std::borrow::Cow;
use std::error::Error as StdError;
use std::fmt::{Display, self};
use std::ops::Deref;
use std::str::{self, FromStr};
use http::ByteStr;
use Url;
use url::ParseError as UrlError;
use Error;
/// The Request-URI of a Request's StartLine.
///
@@ -32,9 +27,9 @@ use Error;
/// | | | | |
/// scheme authority path query fragment
/// ```
#[derive(Clone)]
#[derive(Clone, Hash)]
pub struct Uri {
source: InternalUri,
source: ByteStr,
scheme_end: Option<usize>,
authority_end: Option<usize>,
query: Option<usize>,
@@ -43,20 +38,23 @@ pub struct Uri {
impl Uri {
/// Parse a string into a `Uri`.
fn new(s: InternalUri) -> Result<Uri, Error> {
fn new(s: ByteStr) -> Result<Uri, UriError> {
if s.len() == 0 {
Err(Error::Uri(UrlError::RelativeUrlWithoutBase))
Err(UriError(ErrorKind::Empty))
} else if s.as_bytes() == b"*" {
// asterisk-form
Ok(Uri {
source: InternalUri::Cow("*".into()),
source: ByteStr::from_static("*"),
scheme_end: None,
authority_end: None,
query: None,
fragment: None,
})
} else if s.as_bytes() == b"/" {
// shortcut for '/'
Ok(Uri::default())
} else if s.as_bytes()[0] == b'/' {
// origin-form
let query = parse_query(&s);
let fragment = parse_fragment(&s);
Ok(Uri {
@@ -67,22 +65,14 @@ impl Uri {
fragment: fragment,
})
} else if s.contains("://") {
// absolute-form
let scheme = parse_scheme(&s);
let auth = parse_authority(&s);
if let Some(end) = scheme {
match &s[..end] {
"ftp" | "gopher" | "http" | "https" | "ws" | "wss" => {},
"blob" | "file" => return Err(Error::Method),
_ => return Err(Error::Method),
}
match auth {
Some(a) => {
if (end + 3) == a {
return Err(Error::Method);
}
},
None => return Err(Error::Method),
}
let scheme_end = scheme.expect("just checked for ':' above");
let auth_end = auth.expect("just checked for ://");
if scheme_end + 3 == auth_end {
// authority was empty
return Err(UriError(ErrorKind::MissingAuthority));
}
let query = parse_query(&s);
let fragment = parse_fragment(&s);
@@ -94,8 +84,10 @@ impl Uri {
fragment: fragment,
})
} else if (s.contains("/") || s.contains("?")) && !s.contains("://") {
return Err(Error::Method)
// last possibility is authority-form, above are illegal characters
return Err(UriError(ErrorKind::Malformed))
} else {
// authority-form
let len = s.len();
Ok(Uri {
source: s,
@@ -220,25 +212,12 @@ fn parse_fragment(s: &str) -> Option<usize> {
}
impl FromStr for Uri {
type Err = Error;
type Err = UriError;
fn from_str(s: &str) -> Result<Uri, Error> {
fn from_str(s: &str) -> Result<Uri, UriError> {
//TODO: refactor such that the to_owned() is only required at the end
//of successful parsing, so an Err doesn't needlessly clone the string.
Uri::new(InternalUri::Cow(Cow::Owned(s.to_owned())))
}
}
impl From<Url> for Uri {
fn from(url: Url) -> Uri {
let s = InternalUri::Cow(Cow::Owned(url.into_string()));
// TODO: we could build the indices with some simple subtraction,
// instead of re-parsing an already parsed string.
// For example:
// let scheme_end = url[..url::Position::AfterScheme].as_ptr() as usize
// - url.as_str().as_ptr() as usize;
// etc
Uri::new(s).expect("Uri::From<Url> failed")
Uri::new(ByteStr::from(s))
}
}
@@ -259,7 +238,7 @@ impl AsRef<str> for Uri {
impl Default for Uri {
fn default() -> Uri {
Uri {
source: InternalUri::Cow("/".into()),
source: ByteStr::from_static("/"),
scheme_end: None,
authority_end: None,
query: None,
@@ -280,34 +259,67 @@ impl Display for Uri {
}
}
pub fn from_mem_str(s: ByteStr) -> Result<Uri, Error> {
Uri::new(InternalUri::Shared(s))
pub fn from_byte_str(s: ByteStr) -> Result<Uri, UriError> {
Uri::new(s)
}
#[derive(Clone)]
enum InternalUri {
Cow(Cow<'static, str>),
Shared(ByteStr),
pub fn scheme_and_authority(uri: &Uri) -> Option<Uri> {
if uri.scheme_end.is_some() {
Some(Uri {
source: uri.source.slice_to(uri.authority_end.expect("scheme without authority")),
scheme_end: uri.scheme_end,
authority_end: uri.authority_end,
query: None,
fragment: None,
})
} else {
None
}
}
impl InternalUri {
fn as_str(&self) -> &str {
match *self {
InternalUri::Cow(ref s) => s.as_ref(),
InternalUri::Shared(ref m) => m.as_str(),
pub fn origin_form(uri: &Uri) -> Uri {
let start = uri.authority_end.unwrap_or(uri.scheme_end.unwrap_or(0));
let end = if let Some(f) = uri.fragment {
uri.source.len() - f - 1
} else {
uri.source.len()
};
Uri {
source: uri.source.slice(start, end),
scheme_end: None,
authority_end: None,
query: uri.query,
fragment: None,
}
}
/// An error parsing a `Uri`.
#[derive(Clone, Debug)]
pub struct UriError(ErrorKind);
#[derive(Clone, Debug)]
enum ErrorKind {
Empty,
Malformed,
MissingAuthority,
}
impl fmt::Display for UriError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
f.pad(self.description())
}
}
impl StdError for UriError {
fn description(&self) -> &str {
match self.0 {
ErrorKind::Empty => "empty Uri string",
ErrorKind::Malformed => "invalid character in Uri authority",
ErrorKind::MissingAuthority => "absolute Uri missing authority segment",
}
}
}
impl Deref for InternalUri {
type Target = str;
fn deref(&self) -> &str {
self.as_str()
}
}
macro_rules! test_parse {
(
$test_name:ident,
@@ -443,13 +455,3 @@ fn test_uri_parse_error() {
err("localhost/");
err("localhost?key=val");
}
#[test]
fn test_uri_from_url() {
let uri = Uri::from(Url::parse("http://test.com/nazghul?test=3").unwrap());
assert_eq!(uri.path(), "/nazghul");
assert_eq!(uri.authority(), Some("test.com"));
assert_eq!(uri.scheme(), Some("http"));
assert_eq!(uri.query(), Some("test=3"));
assert_eq!(uri.as_ref(), "http://test.com/nazghul?test=3");
}