Rocket

Unnamed repository; edit this file 'description' to name the repository.
Log | Files | Refs | README

commit 56c6a96f6a94ab844a0f571c303d741c7ec668d3
parent c04655f290c25055d4e3011be0034a2e5d3acbd0
Author: Sergio Benitez <sb@sergio.bz>
Date:   Sat, 28 Jul 2018 18:26:15 -0700

Overhaul URI types.

This is fairly large commit with several entangled logical changes.

The primary change in this commit is to completely overhaul how URI
handling in Rocket works. Prior to this commit, the `Uri` type acted as
an origin API. Its parser was minimal and lenient, allowing URIs that
were invalid according to RFC 7230. By contrast, the new `Uri` type
brings with it a strict RFC 7230 compliant parser. The `Uri` type now
represents any kind of valid URI, not simply `Origin` types. Three new
URI types were introduced:

  * `Origin` - represents valid origin URIs
  * `Absolute` - represents valid absolute URIs
  * `Authority` - represents valid authority URIs

The `Origin` type replaces `Uri` in many cases:

  * As fields and method inputs of `Route`
  * The `&Uri` request guard is now `&Origin`
  * The `uri!` macro produces an `Origin` instead of a `Uri`

The strict nature of URI parsing cascaded into the following changes:

  * Several `Route` methods now `panic!` on invalid URIs
  * The `Rocket::mount()` method is (correctly) stricter with URIs
  * The `Redirect` constructors take a `TryInto<Uri>` type
  * Dispatching of a `LocalRequest` correctly validates URIs

Overall, URIs are now properly and uniformly handled throughout Rocket's
codebase, resulting in a more reliable and correct system.

In addition to these URI changes, the following changes are also part of
this commit:

  * The `LocalRequest::cloned_dispatch()` method was removed in favor of
    chaining `.clone().dispatch()`.
  * The entire Rocket codebase uses `crate` instead of `pub(crate)` as a
    visibility modifier.
  * Rocket uses the `crate_visibility_modifier` and `try_from` features.

A note on unsafety: this commit introduces many uses of `unsafe` in the
URI parser. All of these uses are a result of unsafely transforming byte
slices (`&[u8]` or similar) into strings (`&str`). The parser ensures
that these casts are safe, but of course, we must label their use
`unsafe`. The parser was written to be as generic and efficient as
possible and thus can parse directly from byte sources. Rocket, however,
does not make use of this fact and so would be able to remove all uses
of `unsafe` by parsing from an existing `&str`. This should be
considered in the future.

Fixes #443.
Resolves #263.

Diffstat:
Mcontrib/lib/src/lib.rs | 1+
Mcontrib/lib/src/templates/engine.rs | 6+++---
Mcore/codegen/src/decorators/route.rs | 4++--
Mcore/codegen/src/lib.rs | 29+++++++++++++++++------------
Mcore/codegen/src/macros/uri.rs | 141+++++++++++++++++++++++++++++++++++++++++++++++++------------------------------
Mcore/codegen/src/parser/route.rs | 13+++++++------
Mcore/codegen/src/parser/uri.rs | 55++++++++++++++++++++++++++++++++-----------------------
Mcore/codegen/src/parser/uri_macro.rs | 16+++++++++-------
Mcore/codegen/tests/compile-fail/absolute-mount-paths.rs | 6+++---
Mcore/codegen/tests/segments.rs | 2+-
Mcore/codegen/tests/typed-uris.rs | 4++--
Mcore/codegen_next/src/ext.rs | 2+-
Mcore/http/Cargo.toml | 1+
Mcore/http/src/accept.rs | 2+-
Mcore/http/src/ext.rs | 36++++++++++++++++++++++++++++++++++++
Mcore/http/src/lib.rs | 18++++++++++++------
Mcore/http/src/parse/accept.rs | 4+++-
Mcore/http/src/parse/indexed.rs | 23++++++++++++++++++-----
Mcore/http/src/parse/media_type.rs | 9++++++---
Mcore/http/src/parse/mod.rs | 10+++++-----
Acore/http/src/parse/uri/error.rs | 92+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Acore/http/src/parse/uri/mod.rs | 43+++++++++++++++++++++++++++++++++++++++++++
Acore/http/src/parse/uri/parser.rs | 190+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Acore/http/src/parse/uri/spec.txt | 95+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Acore/http/src/parse/uri/tables.rs | 92+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Acore/http/src/parse/uri/tests.rs | 228+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Acore/http/src/uri/absolute.rs | 175+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Acore/http/src/uri/authority.rs | 227+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mcore/http/src/uri/mod.rs | 10+++++++++-
Acore/http/src/uri/origin.rs | 653+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mcore/http/src/uri/uri.rs | 653+++++++++++++++++++------------------------------------------------------------
Mcore/http/src/uri/uri_display.rs | 7+++----
Mcore/lib/src/catcher.rs | 4++--
Mcore/lib/src/config/config.rs | 12++++++------
Mcore/lib/src/config/custom_values.rs | 6+++---
Mcore/lib/src/config/environment.rs | 4++--
Mcore/lib/src/config/mod.rs | 4++--
Mcore/lib/src/config/toml_ext.rs | 2+-
Mcore/lib/src/data/data.rs | 6+++---
Mcore/lib/src/data/data_stream.rs | 2+-
Mcore/lib/src/error.rs | 2+-
Mcore/lib/src/fairing/mod.rs | 2+-
Mcore/lib/src/lib.rs | 3++-
Mcore/lib/src/local/client.rs | 55+++++++++++++++----------------------------------------
Mcore/lib/src/local/request.rs | 161+++++++++++++++++++++++++++++++++++++++++++++++++++----------------------------
Mcore/lib/src/logger.rs | 6+++---
Mcore/lib/src/request/form/form.rs | 2+-
Mcore/lib/src/request/from_request.rs | 11+++++------
Mcore/lib/src/request/mod.rs | 2+-
Mcore/lib/src/request/param.rs | 19++++++++++++++++---
Mcore/lib/src/request/request.rs | 63+++++++++++++++++++++++++++++++++++----------------------------
Mcore/lib/src/response/mod.rs | 2+-
Mcore/lib/src/response/redirect.rs | 159+++++++++++++++++++++++++++++++++++++++++--------------------------------------
Mcore/lib/src/response/response.rs | 2+-
Mcore/lib/src/rocket.rs | 65++++++++++++++++++++++++++++++++++++++++++++++++-----------------
Mcore/lib/src/router/collider.rs | 27++++++++++++++++++---------
Mcore/lib/src/router/mod.rs | 112+++++++++++++++++++++++++++++++++++++++++++++----------------------------------
Mcore/lib/src/router/route.rs | 99++++++++++++++++++++++++++++++++++++++++---------------------------------------
Acore/lib/tests/absolute-uris-okay-issue-443.rs | 35+++++++++++++++++++++++++++++++++++
Mcore/lib/tests/local-request-content-type-issue-505.rs | 8++++----
Mexamples/handlebars_templates/src/main.rs | 2+-
Mexamples/manual_routes/src/main.rs | 2+-
Mexamples/manual_routes/src/tests.rs | 5++---
Mexamples/tera_templates/src/main.rs | 2+-
64 files changed, 2727 insertions(+), 1006 deletions(-)

diff --git a/contrib/lib/src/lib.rs b/contrib/lib/src/lib.rs @@ -1,4 +1,5 @@ #![feature(use_extern_macros)] +#![feature(crate_visibility_modifier)] // TODO: Version URLs. #![doc(html_root_url = "https://api.rocket.rs")] diff --git a/contrib/lib/src/templates/engine.rs b/contrib/lib/src/templates/engine.rs @@ -62,12 +62,12 @@ pub struct Engines { } impl Engines { - pub(crate) const ENABLED_EXTENSIONS: &'static [&'static str] = &[ + crate const ENABLED_EXTENSIONS: &'static [&'static str] = &[ #[cfg(feature = "tera_templates")] Tera::EXT, #[cfg(feature = "handlebars_templates")] Handlebars::EXT, ]; - pub(crate) fn init(templates: &HashMap<String, TemplateInfo>) -> Option<Engines> { + crate fn init(templates: &HashMap<String, TemplateInfo>) -> Option<Engines> { fn inner<E: Engine>(templates: &HashMap<String, TemplateInfo>) -> Option<E> { let named_templates = templates.iter() .filter(|&(_, i)| i.extension == E::EXT) @@ -91,7 +91,7 @@ impl Engines { }) } - pub(crate) fn render<C: Serialize>( + crate fn render<C: Serialize>( &self, name: &str, info: &TemplateInfo, diff --git a/core/codegen/src/decorators/route.rs b/core/codegen/src/decorators/route.rs @@ -260,9 +260,9 @@ impl RouteParams { ).expect("consistent uri macro item") } - fn explode(&self, ecx: &ExtCtxt) -> (LocalInternedString, &str, Path, P<Expr>, P<Expr>) { + fn explode(&self, ecx: &ExtCtxt) -> (LocalInternedString, String, Path, P<Expr>, P<Expr>) { let name = self.annotated_fn.ident().name.as_str(); - let path = &self.uri.node.as_str(); + let path = self.uri.node.to_string(); let method = method_to_path(ecx, self.method.node); let format = self.format.as_ref().map(|kv| kv.value().clone()); let media_type = option_as_expr(ecx, &media_type_to_expr(ecx, format)); diff --git a/core/codegen/src/lib.rs b/core/codegen/src/lib.rs @@ -88,8 +88,9 @@ //! other: String //! } //! -//! Each field's type is required to implement [`FromFormValue`]. The derive -//! accepts one field attribute: `form`, with the following syntax: +//! Each field's type is required to implement [`FromFormValue`]. +//! +//! The derive accepts one field attribute: `form`, with the following syntax: //! //! <pre> //! form := 'field' '=' '"' IDENT '"' @@ -113,8 +114,8 @@ //! implementation succeeds only when all of the field parses succeed. //! //! The `form` field attribute can be used to direct that a different incoming -//! field name is expected. In this case, the attribute's field name is used -//! instead of the structure's field name when parsing a form. +//! field name is expected. In this case, the `field` name in the attribute is +//! used instead of the structure's actual field name when parsing a form. //! //! [`FromForm`]: /rocket/request/trait.FromForm.html //! [`FromFormValue`]: /rocket/request/trait.FromFormValue.html @@ -138,7 +139,9 @@ //! ### Typed URIs: `uri!` //! //! The `uri!` macro creates a type-safe URI given a route and values for the -//! route's URI parameters. +//! route's URI parameters. The inputs to the macro are the path to a route, a +//! colon, and one argument for each dynamic parameter (parameters in `<>`) in +//! the route's path. //! //! For example, for the following route: //! @@ -152,10 +155,10 @@ //! A URI can be created as follows: //! //! ```rust,ignore -//! // with unnamed parameters +//! // with unnamed parameters, in route path declaration order //! let mike = uri!(person: "Mike", 28); //! -//! // with named parameters +//! // with named parameters, order irrelevant //! let mike = uri!(person: name = "Mike", age = 28); //! let mike = uri!(person: age = 28, name = "Mike"); //! @@ -183,11 +186,14 @@ //! //! #### Semantics //! -//! The `uri!` macro returns a `Uri` structure with the URI of the supplied -//! route with the given values. A `uri!` invocation only succeeds if the type -//! of every value in the invocation matches the type declared for the parameter -//! in the given route. +//! The `uri!` macro returns an [`Origin`](rocket::uri::Origin) structure with +//! the URI of the supplied route interpolated with the given values. Note that +//! `Origin` implements `Into<Uri>` (and by extension, `TryInto<Uri>`), so it +//! can be converted into a [`Uri`](rocket::uri::Uri) using `.into()` as needed. +//! //! +//! A `uri!` invocation only typechecks if the type of every value in the +//! invocation matches the type declared for the parameter in the given route. //! The [`FromUriParam`] trait is used to typecheck and perform a conversion for //! each value. If a `FromUriParam<S>` implementation exists for a type `T`, //! then a value of type `S` can be used in `uri!` macro for a route URI @@ -221,7 +227,6 @@ //! ROCKET_CODEGEN_DEBUG=1 cargo build //! ``` - extern crate syntax; extern crate syntax_ext; extern crate syntax_pos; diff --git a/core/codegen/src/macros/uri.rs b/core/codegen/src/macros/uri.rs @@ -3,7 +3,7 @@ use std::fmt::Display; use syntax::codemap::Span; use syntax::ext::base::{DummyResult, ExtCtxt, MacEager, MacResult}; use syntax::tokenstream::{TokenStream, TokenTree}; -use syntax::ast::{self, GenericArg, MacDelimiter, Ident}; +use syntax::ast::{self, Expr, GenericArg, MacDelimiter, Ident}; use syntax::symbol::Symbol; use syntax::parse::PResult; use syntax::ext::build::AstBuilder; @@ -11,9 +11,14 @@ use syntax::ptr::P; use URI_INFO_MACRO_PREFIX; use super::prefix_path; -use utils::{IdentExt, split_idents, ExprExt}; +use utils::{IdentExt, split_idents, ExprExt, option_as_expr}; use parser::{UriParams, InternalUriParams, Validation}; +use rocket_http::uri::Origin; +use rocket_http::ext::IntoOwned; + +// What gets called when `uri!` is invoked. This just invokes the internal URI +// macro which calls the `uri_internal` function below. pub fn uri( ecx: &mut ExtCtxt, sp: Span, @@ -89,49 +94,35 @@ fn extract_exprs<'a>( } } -#[allow(unused_imports)] -pub fn uri_internal( - ecx: &mut ExtCtxt, - sp: Span, - tt: &[TokenTree], -) -> Box<MacResult + 'static> { - // Parse the internal invocation and the user's URI param expressions. - let mut parser = ecx.new_parser_from_tts(tt); - let internal = try_parse!(sp, InternalUriParams::parse(ecx, &mut parser)); - let exprs = try_parse!(sp, extract_exprs(ecx, &internal)); - - // Generate the statements to typecheck each parameter. First, the mount. - let mut argument_stmts = vec![]; - let mut format_assign_tokens = vec![]; - let mut fmt_string = internal.uri_fmt_string(); - if let Some(mount_point) = internal.uri_params.mount_point { - // generating: let mount: &str = $mount_string; - let mount_string = mount_point.node; - argument_stmts.push(ecx.stmt_let_typed( - mount_point.span, - false, - Ident::from_str("mount"), - quote_ty!(ecx, &str), - quote_expr!(ecx, $mount_string), - )); - - // generating: format string arg for `mount` - let mut tokens = quote_tokens!(ecx, mount = mount,); - tokens.iter_mut().for_each(|tree| tree.set_span(mount_point.span)); - format_assign_tokens.push(tokens); - - // Ensure the `format!` string contains the `{mount}` parameter. - fmt_string = "{mount}".to_string() + &fmt_string; - } +// Validates the mount path and the URI and returns a single Origin URI with +// both paths concatinated. Validation should always succeed since this macro +// can only be called if the route attribute succeed, which implies that the +// route URI was valid. +fn extract_origin<'a>( + ecx: &ExtCtxt<'a>, + internal: &InternalUriParams, +) -> PResult<'a, Origin<'static>> { + let base_uri = match internal.uri_params.mount_point { + Some(base) => Origin::parse(&base.node) + .map_err(|_| ecx.struct_span_err(base.span, "invalid path URI"))? + .into_owned(), + None => Origin::dummy() + }; + + Origin::parse_route(&format!("{}/{}", base_uri, internal.uri.node)) + .map(|o| o.to_normalized().into_owned()) + .map_err(|_| ecx.struct_span_err(internal.uri.span, "invalid route URI")) +} - // Now the user's parameters. +fn explode<I>(ecx: &ExtCtxt, route_str: &str, items: I) -> P<Expr> + where I: Iterator<Item = (ast::Ident, P<ast::Ty>, P<Expr>)> +{ + // Generate the statements to typecheck each parameter. // Building <$T as ::rocket::http::uri::FromUriParam<_>>::from_uri_param($e). - for (i, &(mut ident, ref ty)) in internal.fn_args.iter().enumerate() { - let (span, mut expr) = (exprs[i].span, exprs[i].clone()); - - // Format argument names cannot begin with `_`, but a function parameter - // might, so we prefix each parameter with the letters `fmt`. - ident.name = Symbol::intern(&format!("fmt{}", ident.name)); + let mut let_bindings = vec![]; + let mut fmt_exprs = vec![]; + for (mut ident, ty, expr) in items { + let (span, mut expr) = (expr.span, expr.clone()); ident.span = span; // path for call: <T as FromUriParam<_>>::from_uri_param @@ -150,7 +141,7 @@ pub fn uri_internal( if !inner.is_location() { let tmp_ident = ident.append("_tmp"); let tmp_stmt = ecx.stmt_let(span, false, tmp_ident, inner); - argument_stmts.push(tmp_stmt); + let_bindings.push(tmp_stmt); expr = ecx.expr_ident(span, tmp_ident); } } @@ -160,19 +151,63 @@ pub fn uri_internal( let call = ecx.expr_call(span, path_expr, vec![expr]); let stmt = ecx.stmt_let(span, false, ident, call); debug!("Emitting URI typecheck statement: {:?}", stmt); - argument_stmts.push(stmt); + let_bindings.push(stmt); - // generating: arg assignment tokens for format string - let uri_display = quote_path!(ecx, ::rocket::http::uri::UriDisplay); - let mut tokens = quote_tokens!(ecx, $ident = &$ident as &$uri_display,); + // generating: arg tokens for format string + let mut tokens = quote_tokens!(ecx, &$ident as &::rocket::http::uri::UriDisplay,); tokens.iter_mut().for_each(|tree| tree.set_span(span)); - format_assign_tokens.push(tokens); + fmt_exprs.push(tokens); + } + + // Convert all of the '<...>' into '{}'. + let mut inside = false; + let fmt_string: String = route_str.chars().filter_map(|c| { + Some(match c { + '<' => { inside = true; '{' } + '>' => { inside = false; '}' } + _ if !inside => c, + _ => return None + }) + }).collect(); + + // Don't allocate if there are no formatting expressions. + if fmt_exprs.is_empty() { + quote_expr!(ecx, $fmt_string.into()) + } else { + quote_expr!(ecx, { $let_bindings format!($fmt_string, $fmt_exprs).into() }) } +} - let expr = quote_expr!(ecx, { - $argument_stmts - ::rocket::http::uri::Uri::from(format!($fmt_string, $format_assign_tokens)) - }); +#[allow(unused_imports)] +pub fn uri_internal( + ecx: &mut ExtCtxt, + sp: Span, + tt: &[TokenTree], +) -> Box<MacResult + 'static> { + // Parse the internal invocation and the user's URI param expressions. + let mut parser = ecx.new_parser_from_tts(tt); + let internal = try_parse!(sp, InternalUriParams::parse(ecx, &mut parser)); + let exprs = try_parse!(sp, extract_exprs(ecx, &internal)); + let origin = try_parse!(sp, extract_origin(ecx, &internal)); + + // Determine how many parameters there are in the URI path. + let path_param_count = origin.path().matches('<').count(); + + // Create an iterator over the `ident`, `ty`, and `expr` triple. + let mut arguments = internal.fn_args + .into_iter() + .zip(exprs.into_iter()) + .map(|((ident, ty), expr)| (ident, ty, expr)); + + // Generate an expression for both the path and query. + let path = explode(ecx, origin.path(), arguments.by_ref().take(path_param_count)); + let query = option_as_expr(ecx, &origin.query().map(|q| explode(ecx, q, arguments))); + + // Generate the final `Origin` expression. + let expr = quote_expr!(ecx, ::rocket::http::uri::Origin::new::< + ::std::borrow::Cow<'static, str>, + ::std::borrow::Cow<'static, str>, + >($path, $query)); debug!("Emitting URI expression: {:?}", expr); MacEager::expr(expr) diff --git a/core/codegen/src/parser/route.rs b/core/codegen/src/parser/route.rs @@ -10,7 +10,7 @@ use super::Function; use super::keyvalue::KVSpanned; use super::uri::validate_uri; use rocket_http::{Method, MediaType}; -use rocket_http::uri::Uri; +use rocket_http::uri::Origin; /// This structure represents the parsed `route` attribute. /// @@ -22,7 +22,7 @@ use rocket_http::uri::Uri; pub struct RouteParams { pub annotated_fn: Function, pub method: Spanned<Method>, - pub uri: Spanned<Uri<'static>>, + pub uri: Spanned<Origin<'static>>, pub data_param: Option<KVSpanned<Ident>>, pub query_param: Option<Spanned<Ident>>, pub format: Option<KVSpanned<MediaType>>, @@ -185,9 +185,10 @@ fn parse_method(ecx: &ExtCtxt, meta_item: &NestedMetaItem) -> Spanned<Method> { dummy_spanned(Method::Get) } -fn parse_path(ecx: &ExtCtxt, - meta_item: &NestedMetaItem) - -> (Spanned<Uri<'static>>, Option<Spanned<Ident>>) { +fn parse_path( + ecx: &ExtCtxt, + meta_item: &NestedMetaItem +) -> (Spanned<Origin<'static>>, Option<Spanned<Ident>>) { let sp = meta_item.span(); if let Some((name, lit)) = meta_item.name_value() { if name != "path" { @@ -207,7 +208,7 @@ fn parse_path(ecx: &ExtCtxt, .emit(); } - (dummy_spanned(Uri::new("")), None) + (dummy_spanned(Origin::dummy()), None) } fn parse_opt<O, T, F>(ecx: &ExtCtxt, kv: &KVSpanned<T>, f: F) -> Option<KVSpanned<O>> diff --git a/core/codegen/src/parser/uri.rs b/core/codegen/src/parser/uri.rs @@ -2,7 +2,8 @@ use syntax::ast::*; use syntax::codemap::{Span, Spanned, dummy_spanned}; use syntax::ext::base::ExtCtxt; -use rocket_http::uri::Uri; +use rocket_http::ext::IntoOwned; +use rocket_http::uri::{Uri, Origin}; use super::route::param_to_ident; use utils::{span, SpanExt, is_valid_ident}; @@ -11,24 +12,18 @@ use utils::{span, SpanExt, is_valid_ident}; // stripped out at runtime. So, to avoid any confusion, we issue an error at // compile-time for empty segments. At the moment, this disallows trailing // slashes as well, since then the last segment is empty. -fn valid_path(ecx: &ExtCtxt, uri: &Uri, sp: Span) -> bool { - let cleaned = uri.to_string(); - if !uri.as_str().starts_with('/') { - ecx.struct_span_err(sp, "route paths must be absolute") - .note(&format!("expected {:?}, found {:?}", cleaned, uri.as_str())) - .emit() - } else if cleaned != uri.as_str() { +fn valid_path(ecx: &ExtCtxt, uri: &Origin, sp: Span) -> bool { + if !uri.is_normalized() { + let normalized = uri.to_normalized(); ecx.struct_span_err(sp, "paths cannot contain empty segments") - .note(&format!("expected {:?}, found {:?}", cleaned, uri.as_str())) - .emit() - } else { - return true; + .note(&format!("expected '{}', found '{}'", normalized, uri)) + .emit(); } - false + uri.is_normalized() } -fn valid_segments(ecx: &ExtCtxt, uri: &Uri, sp: Span) -> bool { +fn valid_segments(ecx: &ExtCtxt, uri: &Origin, sp: Span) -> bool { let mut validated = true; let mut segments_span = None; for segment in uri.segments() { @@ -97,18 +92,32 @@ fn valid_segments(ecx: &ExtCtxt, uri: &Uri, sp: Span) -> bool { validated } -pub fn validate_uri(ecx: &ExtCtxt, - string: &str, - sp: Span) - -> (Spanned<Uri<'static>>, Option<Spanned<Ident>>) { - let uri = Uri::from(string.to_string()); +pub fn validate_uri( + ecx: &ExtCtxt, + string: &str, + sp: Span, +) -> (Spanned<Origin<'static>>, Option<Spanned<Ident>>) { let query_param = string.find('?') .map(|i| span(&string[(i + 1)..], sp.trim_left(i + 1))) .and_then(|spanned_q_param| param_to_ident(ecx, spanned_q_param)); - if valid_segments(ecx, &uri, sp) && valid_path(ecx, &uri, sp) { - (span(uri, sp), query_param) - } else { - (dummy_spanned(Uri::new("")), query_param) + let dummy = (dummy_spanned(Origin::dummy()), query_param); + match Origin::parse_route(string) { + Ok(uri) => { + let uri = uri.into_owned(); + if valid_segments(ecx, &uri, sp) && valid_path(ecx, &uri, sp) { + return (span(uri, sp), query_param) + } + + dummy + } + Err(e) => { + ecx.struct_span_err(sp, &format!("invalid path URI: {}", e)) + .note("expected path URI in origin form") + .help("example: /path/<route>") + .emit(); + + dummy + } } } diff --git a/core/codegen/src/parser/uri_macro.rs b/core/codegen/src/parser/uri_macro.rs @@ -24,6 +24,12 @@ pub enum Args { Named(Vec<(Spanned<Ident>, P<Expr>)>), } +// For an invocation that looks like: +// uri!("/mount/point", this::route: e1, e2, e3); +// ^-------------| ^----------| ^---------| +// uri_params.mount_point | uri_params.arguments +// uri_params.route_path +// #[derive(Debug)] pub struct UriParams { pub mount_point: Option<Spanned<LocalInternedString>>, @@ -31,6 +37,9 @@ pub struct UriParams { pub arguments: Option<Spanned<Args>>, } +// `fn_args` are the URI arguments (excluding guards) from the original route's +// handler in the order they were declared in the URI (`<first>/<second>`). +// `uri` is the full URI used in the origin route's attribute #[derive(Debug)] pub struct InternalUriParams { pub uri: Spanned<String>, @@ -252,11 +261,4 @@ impl InternalUriParams { } } } - - pub fn uri_fmt_string(&self) -> String { - self.uri.node - .replace('<', "{fmt") - .replace("..>", "}") - .replace('>', "}") - } } diff --git a/core/codegen/tests/compile-fail/absolute-mount-paths.rs b/core/codegen/tests/compile-fail/absolute-mount-paths.rs @@ -1,13 +1,13 @@ #![feature(plugin, decl_macro)] #![plugin(rocket_codegen)] -#[get("a")] //~ ERROR absolute +#[get("a")] //~ ERROR invalid fn get() -> &'static str { "hi" } -#[get("")] //~ ERROR absolute +#[get("")] //~ ERROR invalid fn get1(id: usize) -> &'static str { "hi" } -#[get("a/b/c")] //~ ERROR absolute +#[get("a/b/c")] //~ ERROR invalid fn get2(id: usize) -> &'static str { "hi" } fn main() { } diff --git a/core/codegen/tests/segments.rs b/core/codegen/tests/segments.rs @@ -4,7 +4,7 @@ extern crate rocket; use std::path::PathBuf; -use rocket::http::uri::SegmentError; +use rocket::request::SegmentError; #[post("/<a>/<b..>")] fn get(a: String, b: PathBuf) -> String { diff --git a/core/codegen/tests/typed-uris.rs b/core/codegen/tests/typed-uris.rs @@ -8,7 +8,7 @@ use std::fmt; use std::path::PathBuf; use rocket::http::{RawStr, Cookies}; -use rocket::http::uri::{Uri, UriDisplay, FromUriParam}; +use rocket::http::uri::{Origin, UriDisplay, FromUriParam}; use rocket::request::Form; #[derive(FromForm)] @@ -85,7 +85,7 @@ fn param_and_segments(path: PathBuf, id: usize) -> &'static str { "" } fn guarded_segments(cookies: Cookies, path: PathBuf, id: usize) -> &'static str { "" } macro assert_uri_eq($($uri:expr => $expected:expr,)+) { - $(assert_eq!($uri, Uri::from($expected));)+ + $(assert_eq!($uri, Origin::parse($expected).expect("valid origin URI"));)+ } #[test] diff --git a/core/codegen_next/src/ext.rs b/core/codegen_next/src/ext.rs @@ -21,7 +21,7 @@ impl MemberExt for Member { } } -pub(crate) trait FieldsExt { +pub trait FieldsExt { fn len(&self) -> usize; fn is_empty(&self) -> bool; fn named(&self) -> Option<&FieldsNamed>; diff --git a/core/http/Cargo.toml b/core/http/Cargo.toml @@ -24,6 +24,7 @@ hyper = { version = "0.10.13", default-features = false } time = "0.1" indexmap = "1.0" rustls = { version = "0.13", optional = true } +state = "0.4" [dependencies.cookie] git = "https://github.com/alexcrichton/cookie-rs" diff --git a/core/http/src/accept.rs b/core/http/src/accept.rs @@ -163,7 +163,7 @@ impl PartialEq for AcceptParams { /// let response = Response::build().header(Accept::JSON).finalize(); /// ``` #[derive(Debug, Clone, PartialEq)] -pub struct Accept(pub(crate) AcceptParams); +pub struct Accept(crate AcceptParams); macro_rules! accept_constructor { ($($name:ident ($check:ident): $str:expr, $t:expr, diff --git a/core/http/src/ext.rs b/core/http/src/ext.rs @@ -1,9 +1,15 @@ +//! Extension traits implemented by several HTTP types. + use smallvec::{Array, SmallVec}; // TODO: It would be nice if we could somehow have one trait that could give us // either SmallVec or Vec. +/// Trait implemented by types that can be converted into a collection. pub trait IntoCollection<T> { + /// Converts `self` into a collection. fn into_collection<A: Array<Item=T>>(self) -> SmallVec<A>; + + #[doc(hidden)] fn mapped<U, F: FnMut(T) -> U, A: Array<Item=U>>(self, f: F) -> SmallVec<A>; } @@ -62,3 +68,33 @@ impl_for_slice!(; 7); impl_for_slice!(; 8); impl_for_slice!(; 9); impl_for_slice!(; 10); + +use std::borrow::Cow; + +/// Trait implemented by types that can be converted into owned versions of +/// themselves. +pub trait IntoOwned { + /// The owned version of the type. + type Owned: 'static; + + /// Converts `self` into an owned version of itself. + fn into_owned(self) -> Self::Owned; +} + +impl<T: IntoOwned> IntoOwned for Option<T> { + type Owned = Option<T::Owned>; + + #[inline(always)] + fn into_owned(self) -> Self::Owned { + self.map(|inner| inner.into_owned()) + } +} + +impl<'a, B: 'static + ToOwned + ?Sized> IntoOwned for Cow<'a, B> { + type Owned = Cow<'static, B>; + + #[inline(always)] + fn into_owned(self) -> Self::Owned { + Cow::Owned(self.into_owned()) + } +} diff --git a/core/http/src/lib.rs b/core/http/src/lib.rs @@ -1,6 +1,8 @@ #![feature(specialization)] #![feature(proc_macro_non_items, use_extern_macros)] #![feature(const_fn)] +#![feature(try_from)] +#![feature(crate_visibility_modifier)] #![recursion_limit="256"] //! Types that map to concepts in HTTP. @@ -19,10 +21,13 @@ extern crate percent_encoding; extern crate cookie; extern crate time; extern crate indexmap; +extern crate state; pub mod hyper; pub mod uri; +pub mod ext; +#[doc(hidden)] #[cfg(feature = "tls")] pub mod tls; @@ -38,16 +43,20 @@ mod status; mod header; mod accept; mod raw_str; -mod ext; -pub(crate) mod parse; +crate mod parse; + +pub mod uncased; // We need to export these for codegen, but otherwise it's unnecessary. // TODO: Expose a `const fn` from ContentType when possible. (see RFC#1817) -pub mod uncased; +// FIXME(rustc): These show up in the rexported module. #[doc(hidden)] pub use self::parse::Indexed; #[doc(hidden)] pub use self::media_type::{MediaParams, Source}; +// This one we need to expose for core. +#[doc(hidden)] pub use self::cookies::{Key, CookieJar}; + pub use self::method::Method; pub use self::content_type::ContentType; pub use self::accept::{Accept, QMediaType}; @@ -57,6 +66,3 @@ pub use self::raw_str::RawStr; pub use self::media_type::MediaType; pub use self::cookies::{Cookie, SameSite, Cookies}; - -#[doc(hidden)] -pub use self::cookies::{Key, CookieJar}; diff --git a/core/http/src/parse/accept.rs b/core/http/src/parse/accept.rs @@ -2,10 +2,12 @@ use pear::parser; use pear::parsers::*; use {Accept, QMediaType}; -use parse::{Input, Result}; use parse::checkers::is_whitespace; use parse::media_type::media_type; +type Input<'a> = ::parse::IndexedInput<'a, str>; +type Result<'a, T> = ::pear::Result<T, Input<'a>>; + #[parser] fn weighted_media_type<'a>(input: &mut Input<'a>) -> Result<'a, QMediaType> { let media_type = media_type()?; diff --git a/core/http/src/parse/indexed.rs b/core/http/src/parse/indexed.rs @@ -6,7 +6,11 @@ use std::fmt::{self, Debug}; use pear::{Input, Length}; +use ext::IntoOwned; + pub type IndexedString = Indexed<'static, str>; +pub type IndexedStr<'a> = Indexed<'a, str>; +pub type IndexedBytes<'a> = Indexed<'a, [u8]>; pub trait AsPtr { fn as_ptr(&self) -> *const u8; @@ -48,9 +52,7 @@ impl<'a, T: ?Sized + ToOwned + 'a> Indexed<'a, T> { _ => panic!("cannot convert indexed T to U unless indexed") } } -} -impl<'a, T: ?Sized + ToOwned + 'a> Indexed<'a, T> { #[inline(always)] pub fn coerce_lifetime<'b>(self) -> Indexed<'b, T> { match self { @@ -60,6 +62,17 @@ impl<'a, T: ?Sized + ToOwned + 'a> Indexed<'a, T> { } } +impl<'a, T: 'static + ?Sized + ToOwned> IntoOwned for Indexed<'a, T> { + type Owned = Indexed<'static, T>; + + fn into_owned(self) -> Indexed<'static, T> { + match self { + Indexed::Indexed(a, b) => Indexed::Indexed(a, b), + Indexed::Concrete(cow) => Indexed::Concrete(IntoOwned::into_owned(cow)) + } + } +} + use std::ops::Add; impl<'a, T: ?Sized + ToOwned + 'a> Add for Indexed<'a, T> { @@ -303,12 +316,12 @@ pub struct Context { impl ::std::fmt::Display for Context { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { const LIMIT: usize = 7; - write!(f, "{}", self.offset)?; + write!(f, "[{}:]", self.offset)?; if self.string.len() > LIMIT { - write!(f, " ({}..)", &self.string[..LIMIT]) + write!(f, " {}..", &self.string[..LIMIT]) } else if !self.string.is_empty() { - write!(f, " ({})", &self.string) + write!(f, " {}", &self.string) } else { Ok(()) } diff --git a/core/http/src/parse/media_type.rs b/core/http/src/parse/media_type.rs @@ -5,10 +5,13 @@ use pear::parsers::*; use {MediaType, Source}; use parse::checkers::{is_whitespace, is_valid_token}; -use parse::{Input, Slice, Result}; +use parse::IndexedStr; + +type Input<'a> = ::parse::IndexedInput<'a, str>; +type Result<'a, T> = ::pear::Result<T, Input<'a>>; #[parser] -fn quoted_string<'a>(input: &mut Input<'a>) -> Result<'a, Slice<'a>> { +fn quoted_string<'a>(input: &mut Input<'a>) -> Result<'a, IndexedStr<'a>> { eat('"')?; let mut is_escaped = false; @@ -23,7 +26,7 @@ fn quoted_string<'a>(input: &mut Input<'a>) -> Result<'a, Slice<'a>> { } #[parser] -fn media_param<'a>(input: &mut Input<'a>) -> Result<'a, (Slice<'a>, Slice<'a>)> { +fn media_param<'a>(input: &mut Input<'a>) -> Result<'a, (IndexedStr<'a>, IndexedStr<'a>)> { let key = (take_some_while_until(is_valid_token, '=')?, eat('=')?).0; let value = switch! { peek('"') => quoted_string()?, diff --git a/core/http/src/parse/mod.rs b/core/http/src/parse/mod.rs @@ -1,12 +1,12 @@ mod media_type; mod accept; -mod indexed; mod checkers; +mod indexed; -pub use self::indexed::*; pub use self::media_type::*; pub use self::accept::*; -pub type Input<'a> = IndexedInput<'a, str>; -pub type Slice<'a> = Indexed<'a, str>; -pub type Result<'a, T> = ::pear::Result<T, Input<'a>>; +pub mod uri; + +// Exposed for codegen. +#[doc(hidden)] pub use self::indexed::*; diff --git a/core/http/src/parse/uri/error.rs b/core/http/src/parse/uri/error.rs @@ -0,0 +1,92 @@ +use std::fmt; +use std::borrow::Cow; + +use pear::{ParseErr, Expected}; +use parse::indexed::Context; +use parse::uri::RawInput; +use ext::IntoOwned; + +/// Error emitted on URI parse failure. +/// +/// Internally, the type includes information about where the parse error +/// occured (the error's context) and information about what went wrong. +/// Externally, this information can be retrieved (in textual form) through its +/// `Display` implementation. In other words, by printing a value of this type. +#[derive(Debug)] +pub struct Error<'a> { + expected: Expected<Or<char, u8>, Cow<'a, str>, String>, + context: Option<Context> +} + +#[derive(Debug)] +enum Or<L, R> { + A(L), + B(R) +} + +impl<'a> Error<'a> { + crate fn from(src: &'a str, pear_error: ParseErr<RawInput<'a>>) -> Error<'a> { + let new_expected = pear_error.expected.map(|token| { + if token.is_ascii() && !token.is_ascii_control() { + Or::A(token as char) + } else { + Or::B(token) + } + }, String::from_utf8_lossy, |indexed| { + let src = Some(src.as_bytes()); + String::from_utf8_lossy(indexed.from_source(src)).to_string() + }); + + Error { expected: new_expected, context: pear_error.context } + } +} + +impl fmt::Display for Or<char, u8> { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match *self { + Or::A(left) => write!(f, "'{}'", left), + Or::B(right) => write!(f, "non-ASCII byte {}", right), + } + } +} + +impl<'a> fmt::Display for Error<'a> { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + // This relies on specialization of the `Display` impl for `Expected`. + write!(f, "{}", self.expected)?; + + if let Some(ref context) = self.context { + write!(f, " at index {}", context.offset)?; + } + + Ok(()) + } +} + +impl<'a> IntoOwned for Error<'a> { + type Owned = Error<'static>; + + fn into_owned(self) -> Self::Owned { + let expected = self.expected.map(|i| i, IntoOwned::into_owned, |i| i); + Error { expected, context: self.context } + } +} + +#[cfg(test)] +mod tests { + use parse::uri::origin_from_str; + + macro_rules! check_err { + ($url:expr => $error:expr) => {{ + let e = origin_from_str($url).unwrap_err(); + assert_eq!(e.to_string(), $error.to_string()) + }} + } + + #[test] + fn check_display() { + check_err!("a" => "expected token '/' but found 'a' at index 0"); + check_err!("?" => "expected token '/' but found '?' at index 0"); + check_err!("θΏ™" => "expected token '/' but found non-ASCII byte 232 at index 0"); + } +} diff --git a/core/http/src/parse/uri/mod.rs b/core/http/src/parse/uri/mod.rs @@ -0,0 +1,43 @@ +mod parser; +mod error; +mod tables; +#[cfg(test)] +mod tests; + +use uri::{Uri, Origin, Absolute, Authority}; +use parse::indexed::IndexedInput; +use self::parser::{uri, origin, authority_only, absolute_only, rocket_route_origin}; + +pub use self::error::Error; + +type RawInput<'a> = IndexedInput<'a, [u8]>; + +#[inline] +pub fn from_str(string: &str) -> Result<Uri, Error> { + parse!(uri: &mut RawInput::from(string.as_bytes())) + .map_err(|e| Error::from(string, e)) +} + +#[inline] +pub fn origin_from_str(string: &str) -> Result<Origin, Error> { + parse!(origin: &mut RawInput::from(string.as_bytes())) + .map_err(|e| Error::from(string, e)) +} + +#[inline] +pub fn route_origin_from_str(string: &str) -> Result<Origin, Error> { + parse!(rocket_route_origin: &mut RawInput::from(string.as_bytes())) + .map_err(|e| Error::from(string, e)) +} + +#[inline] +pub fn authority_from_str(string: &str) -> Result<Authority, Error> { + parse!(authority_only: &mut RawInput::from(string.as_bytes())) + .map_err(|e| Error::from(string, e)) +} + +#[inline] +pub fn absolute_from_str(string: &str) -> Result<Absolute, Error> { + parse!(absolute_only: &mut RawInput::from(string.as_bytes())) + .map_err(|e| Error::from(string, e)) +} diff --git a/core/http/src/parse/uri/parser.rs b/core/http/src/parse/uri/parser.rs @@ -0,0 +1,190 @@ +use pear::parsers::*; +use pear::{parser, switch}; + +use uri::{Uri, Origin, Authority, Absolute, Host}; +use parse::uri::tables::{is_reg_name_char, is_pchar, is_pchar_or_rchar}; +use parse::uri::RawInput; +use parse::IndexedBytes; + +type Result<'a, T> = ::pear::Result<T, RawInput<'a>>; + +#[parser] +crate fn uri<'a>(input: &mut RawInput<'a>) -> Result<'a, Uri<'a>> { + match input.len() { + 0 => return Err(pear_error!("empty URI")), + 1 => switch! { + eat(b'*') => Uri::Asterisk, + eat(b'/') => Uri::Origin(Origin::new::<_, &str>("/", None)), + _ => unsafe { + // the `is_reg_name_char` guarantees ASCII + let host = Host::Raw(take_n_if(1, is_reg_name_char)?); + Uri::Authority(Authority::raw(input.cow_source(), None, host, None)) + } + }, + _ => switch! { + peek(b'/') => Uri::Origin(origin()?), + _ => absolute_or_authority()? + } + } +} + +#[parser] +crate fn origin<'a>(input: &mut RawInput<'a>) -> Result<'a, Origin<'a>> { + (peek(b'/')?, path_and_query(is_pchar)?).1 +} + +#[parser] +crate fn rocket_route_origin<'a>(input: &mut RawInput<'a>) -> Result<'a, Origin<'a>> { + (peek(b'/')?, path_and_query(is_pchar_or_rchar)?).1 +} + +#[parser] +fn path_and_query<'a, F>(input: &mut RawInput<'a>, is_good_char: F) -> Result<'a, Origin<'a>> + where F: Fn(u8) -> bool + Copy +{ + let path = take_while(is_good_char)?; + + // FIXME(rustc): We should be able to use `pear_try`, but rustc...is broken. + let query = switch! { + eat(b'?') => Some(take_while(|c| is_good_char(c) || c == b'?')?), + _ => None + }; + + if path.is_empty() && query.is_none() { + Err(pear_error!("expected path or query, found neither")) + } else { + // We know the string is ASCII because of the `is_good_char` checks above. + Ok(unsafe { Origin::raw(input.cow_source(), path, query) }) + } +} + +#[parser] +fn port_from<'a>(input: &mut RawInput<'a>, bytes: &IndexedBytes<'a>) -> Result<'a, u16> { + let mut port_num: u32 = 0; + let source = Some(input.cow_source()); + let string = bytes.from_cow_source(&source); + for (&b, i) in string.iter().rev().zip(&[1, 10, 100, 1000, 10000]) { + if b < b'0' || b > b'9' { + return Err(pear_error!("port byte is out of range")); + } + + port_num += (b - b'0') as u32 * i; + } + + if port_num > u16::max_value() as u32 { + return Err(pear_error!("port value out of range: {}", port_num)); + } + + Ok(port_num as u16) +} + +#[parser] +fn port<'a>(input: &mut RawInput<'a>) -> Result<'a, u16> { + let port_str = take_n_while(5, |c| c >= b'0' && c <= b'9')?; + port_from(&port_str)? +} + +#[parser] +fn authority<'a>( + input: &mut RawInput<'a>, + user_info: Option<IndexedBytes<'a>> +) -> Result<'a, Authority<'a>> { + let host = switch! { + peek(b'[') => Host::Bracketed(delimited(b'[', is_pchar, b']')?), + _ => Host::Raw(take_while(is_reg_name_char)?) + }; + + // The `is_pchar`,`is_reg_name_char`, and `port()` functions ensure ASCII. + let port = pear_try!(eat(b':') => port()?); + unsafe { Authority::raw(input.cow_source(), user_info, host, port) } +} + +// Callers must ensure that `scheme` is actually ASCII. +#[parser] +fn absolute<'a>( + input: &mut RawInput<'a>, + scheme: IndexedBytes<'a> +) -> Result<'a, Absolute<'a>> { + let (authority, path_and_query) = switch! { + eat_slice(b"://") => { + let left = take_while(|c| is_reg_name_char(c) || c == b':')?; + let authority = switch! { + eat(b'@') => authority(Some(left))?, + _ => { + input.backtrack(left.len())?; + authority(None)? + } + }; + + let path_and_query = pear_try!(path_and_query(is_pchar)); + (Some(authority), path_and_query) + }, + eat(b':') => (None, Some(path_and_query(is_pchar)?)), + _ => return Err(pear_error!("expected ':' but none was found")) + }; + + // `authority` and `path_and_query` parsers ensure ASCII. + unsafe { Absolute::raw(input.cow_source(), scheme, authority, path_and_query) } +} + +#[parser] +crate fn authority_only<'a>(input: &mut RawInput<'a>) -> Result<'a, Authority<'a>> { + if let Uri::Authority(authority) = absolute_or_authority()? { + Ok(authority) + } else { + Err(pear_error!("expected authority URI but found absolute URI")) + } +} + +#[parser] +crate fn absolute_only<'a>(input: &mut RawInput<'a>) -> Result<'a, Absolute<'a>> { + if let Uri::Absolute(absolute) = absolute_or_authority()? { + Ok(absolute) + } else { + Err(pear_error!("expected absolute URI but found authority URI")) + } +} + +#[parser] +fn absolute_or_authority<'a>( + input: &mut RawInput<'a>, +) -> Result<'a, Uri<'a>> { + let left = take_while(is_reg_name_char)?; + switch! { + peek_slice(b":/") => Uri::Absolute(absolute(left)?), + eat(b'@') => Uri::Authority(authority(Some(left))?), + colon@take_n_if(1, |b| b == b':') => { + // could be authority or an IP with ':' in it + let rest = take_while(|c| is_reg_name_char(c) || c == b':')?; + switch! { + eat(b'@') => Uri::Authority(authority(Some(left + colon + rest))?), + peek(b'/') => { + input.backtrack(rest.len() + 1)?; + Uri::Absolute(absolute(left)?) + }, + _ => unsafe { + // Here we hit an ambiguity: `rest` could be a port in + // host:port or a host in scheme:host. Both are correct + // parses. To settle the ambiguity, we assume that if it + // looks like a port, it's a port. Otherwise a host. Unless + // we have a query, in which case it's definitely a host. + let query = pear_try!(eat(b'?') => take_while(is_pchar)?); + if query.is_some() || rest.is_empty() || rest.len() > 5 { + Uri::raw_absolute(input.cow_source(), left, rest, query) + } else if let Ok(port) = port_from(input, &rest) { + let host = Host::Raw(left); + let source = input.cow_source(); + let port = Some(port); + Uri::Authority(Authority::raw(source, None, host, port)) + } else { + Uri::raw_absolute(input.cow_source(), left, rest, query) + } + } + } + }, + _ => { + input.backtrack(left.len())?; + Uri::Authority(authority(None)?) + } + } +} diff --git a/core/http/src/parse/uri/spec.txt b/core/http/src/parse/uri/spec.txt @@ -0,0 +1,95 @@ +RFC 7230 URI Grammar + +request-target = origin-form / absolute-form / authority-form / asterisk-form + +------------------------------------------------------------------------------- + +asterisk-form = "*" + +------------------------------------------------------------------------------- + +origin-form = absolute-path [ "?" query ] + +absolute-path = 1*( "/" segment ) + +------------------------------------------------------------------------------- + +authority-form = authority + +------------------------------------------------------------------------------- + +1. look for ':', '@', '?' +2. if neither is found, you have an authority, text is `host` +3. if ':' is found, have either 'host', 'scheme', or 'userinfo' + * can only be host if: next four characters are port + * must be host if: text before ':' is empty, requires port + * if next (at most) four characters are numbers, then we have a host/port. + * if next character is '/' or there is none, then scheme + * otherwise try as scheme, fallback to userinfo if find '@' +4. if '?' is found, have either 'host', 'scheme', or 'userinfo' +5. if '@' is found, have 'userinfo' + +------------------------------------------------------------------------------- + +absolute-form = absolute-URI + +absolute-URI = scheme ":" hier-part [ "?" query ] + +scheme = ALPHA *( ALPHA / DIGIT / "+" / "-" / "." ) + +hier-part = "//" authority path-abempty + / path-absolute + / path-rootless + / path-empty + +query = *( pchar / "/" / "?" ) + +authority = [ userinfo "@" ] host [ ":" port ] +userinfo = *( unreserved / pct-encoded / sub-delims / ":" ) +host = IP-literal / IPv4address / reg-name +port = *DIGIT + +reg-name = *( unreserved / pct-encoded / sub-delims ) + +path-abempty = *( "/" segment ) + +path-absolute = "/" [ segment-nz *( "/" segment ) ] +path-noscheme = segment-nz-nc *( "/" segment ) +path-rootless = segment-nz *( "/" segment ) +path-empty = 0<pchar> + +segment = *pchar +segment-nz = 1*pchar + +pchar = unreserved / pct-encoded / sub-delims / ":" / "@" + +unreserved = ALPHA / DIGIT / "-" / "." / "_" / "~" +pct-encoded = "%" HEXDIG HEXDIG +sub-delims = "!" / "$" / "&" / "'" / "(" / ")" + / "*" / "+" / "," / ";" / "=" + +IP-literal = "[" ( IPv6address / IPvFuture ) "]" + +IPvFuture = "v" 1*HEXDIG "." 1*( unreserved / sub-delims / ":" ) + +IPv6address = 6( h16 ":" ) ls32 + / "::" 5( h16 ":" ) ls32 + / [ h16 ] "::" 4( h16 ":" ) ls32 + / [ *1( h16 ":" ) h16 ] "::" 3( h16 ":" ) ls32 + / [ *2( h16 ":" ) h16 ] "::" 2( h16 ":" ) ls32 + / [ *3( h16 ":" ) h16 ] "::" h16 ":" ls32 + / [ *4( h16 ":" ) h16 ] "::" ls32 + / [ *5( h16 ":" ) h16 ] "::" h16 + / [ *6( h16 ":" ) h16 ] "::" + +IPv4address = dec-octet "." dec-octet "." dec-octet "." dec-octet + +dec-octet = DIGIT ; 0-9 + / %x31-39 DIGIT ; 10-99 + / "1" 2DIGIT ; 100-199 + / "2" %x30-34 DIGIT ; 200-249 + / "25" %x30-35 ; 250-255 + +ALPHA = %x41-5A / %x61-7A ; A-Z / a-z +HEXDIG = DIGIT / "A" / "B" / "C" / "D" / "E" / "F" +DIGIT = %x30-39 ; 0-9 diff --git a/core/http/src/parse/uri/tables.rs b/core/http/src/parse/uri/tables.rs @@ -0,0 +1,92 @@ +const PATH_CHARS: [u8; 256] = [ + // 0 1 2 3 4 5 6 7 8 9 + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // x + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 1x + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 2x + 0, 0, 0, b'!', 0, 0, b'$', b'%', b'&', b'\'', // 3x + b'(', b')', b'*', b'+', b',', b'-', b'.', b'/', b'0', b'1', // 4x + b'2', b'3', b'4', b'5', b'6', b'7', b'8', b'9', b':', b';', // 5x + // < > (1 used to indicate these are valid in route URIs only) + 1, b'=', 1, 0, b'@', b'A', b'B', b'C', b'D', b'E', // 6x + b'F', b'G', b'H', b'I', b'J', b'K', b'L', b'M', b'N', b'O', // 7x + b'P', b'Q', b'R', b'S', b'T', b'U', b'V', b'W', b'X', b'Y', // 8x + b'Z', 0, 0, 0, 0, b'_', 0, b'a', b'b', b'c', // 9x + b'd', b'e', b'f', b'g', b'h', b'i', b'j', b'k', b'l', b'm', // 10x + b'n', b'o', b'p', b'q', b'r', b's', b't', b'u', b'v', b'w', // 11x + b'x', b'y', b'z', 0, 0, 0, b'~', 0, 0, 0, // 12x + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 13x + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 14x + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 15x + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 16x + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 17x + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 18x + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 19x + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 20x + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 21x + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 22x + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 23x + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 24x + 0, 0, 0, 0, 0, 0 // 25x +]; + +#[inline(always)] +pub fn is_pchar(c: u8) -> bool { + PATH_CHARS[c as usize] == c +} + +#[inline(always)] +pub fn is_pchar_or_rchar(c: u8) -> bool { + PATH_CHARS[c as usize] != 0 +} + +const REG_CHARS: [u8; 256] = [ + // 0 1 2 3 4 5 6 7 8 9 + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // x + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 1x + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 2x + 0, 0, 0, b'!', 0, 0, b'$', 0, b'&', b'\'', // 3x + b'(', b')', b'*', b'+', b',', b'-', b'.', 0, b'0', b'1', // 4x + b'2', b'3', b'4', b'5', b'6', b'7', b'8', b'9', 0, b';', // 5x + 0, b'=', 0, 0, 0, b'A', b'B', b'C', b'D', b'E', // 6x + b'F', b'G', b'H', b'I', b'J', b'K', b'L', b'M', b'N', b'O', // 7x + b'P', b'Q', b'R', b'S', b'T', b'U', b'V', b'W', b'X', b'Y', // 8x + b'Z', 0, 0, 0, 0, b'_', 0, b'a', b'b', b'c', // 9x + b'd', b'e', b'f', b'g', b'h', b'i', b'j', b'k', b'l', b'm', // 10x + b'n', b'o', b'p', b'q', b'r', b's', b't', b'u', b'v', b'w', // 11x + b'x', b'y', b'z', 0, 0, 0, b'~', 0, 0, 0, // 12x + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 13x + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 14x + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 15x + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 16x + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 17x + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 18x + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 19x + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 20x + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 21x + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 22x + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 23x + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 24x + 0, 0, 0, 0, 0, 0 // 25x +]; + +#[inline(always)] +pub fn is_reg_name_char(c: u8) -> bool { + REG_CHARS[c as usize] != 0 +} + +#[cfg(test)] +mod tests { + fn test_char_table(table: &[u8]) { + for (i, &v) in table.iter().enumerate() { + if v != 0 && v != 1 { + assert_eq!(i, v as usize); + } + } + } + + #[test] + fn check_tables() { + test_char_table(&super::PATH_CHARS[..]); + test_char_table(&super::REG_CHARS[..]); + } +} diff --git a/core/http/src/parse/uri/tests.rs b/core/http/src/parse/uri/tests.rs @@ -0,0 +1,228 @@ +use uri::{Uri, Origin, Authority, Absolute}; +use parse::uri::*; +use uri::Host::*; + +macro_rules! assert_parse_eq { + ($($from:expr => $to:expr),+) => ( + $( + let expected = $to.into(); + match from_str($from) { + Ok(output) => { + if output != expected { + println!("Failure on: {:?}", $from); + assert_eq!(output, expected); + } + } + Err(e) => { + println!("{:?} failed to parse!", $from); + panic!("Error: {}", e); + } + } + )+ + ); + + ($($from:expr => $to:expr),+,) => (assert_parse_eq!($($from => $to),+)) +} + +macro_rules! assert_no_parse { + ($($from:expr),+) => ( + $( + if let Ok(uri) = from_str($from) { + println!("{:?} parsed unexpectedly!", $from); + panic!("Parsed as: {:?}", uri); + } + )+ + ); + + ($($from:expr),+,) => (assert_no_parse!($($from),+)) +} + +macro_rules! assert_displays_eq { + ($($string:expr),+) => ( + $( + let string = $string.into(); + match from_str(string) { + Ok(output) => { + let output_string = output.to_string(); + if output_string != string { + println!("Failure on: {:?}", $string); + println!("Got: {:?}", output_string); + println!("Parsed as: {:?}", output); + panic!("failed"); + } + } + Err(e) => { + println!("{:?} failed to parse!", $string); + panic!("Error: {}", e); + } + } + )+ + ); + + ($($string:expr),+,) => (assert_parse_eq!($($string),+)) +} + +fn uri_origin<'a>(path: &'a str, query: Option<&'a str>) -> Uri<'a> { + Uri::Origin(Origin::new(path, query)) +} + +#[test] +#[should_panic] +fn test_assert_parse_eq() { + assert_parse_eq!("*" => uri_origin("*", None)); +} + +#[test] +#[should_panic] +fn test_assert_parse_eq_consecutive() { + assert_parse_eq!("/" => uri_origin("/", None), "/" => Uri::Asterisk); +} + +#[test] +#[should_panic] +fn test_assert_no_parse() { + assert_no_parse!("/"); +} + +#[test] +fn bad_parses() { + assert_no_parse!("://z7:77777777777777777777777777777`77777777777"); +} + +#[test] +fn single_byte() { + assert_parse_eq!( + "*" => Uri::Asterisk, + "/" => uri_origin("/", None), + "." => Authority::new(None, Raw("."), None), + "_" => Authority::new(None, Raw("_"), None), + "1" => Authority::new(None, Raw("1"), None), + "b" => Authority::new(None, Raw("b"), None), + ); + + assert_no_parse!("?", "#", "%"); +} + +#[test] +fn origin() { + assert_parse_eq!( + "/a/b/c" => uri_origin("/a/b/c", None), + "/a/b/c?" => uri_origin("/a/b/c", Some("")), + "/a/b/c?abc" => uri_origin("/a/b/c", Some("abc")), + "/a/b/c???" => uri_origin("/a/b/c", Some("??")), + "/a/b/c?a?b?" => uri_origin("/a/b/c", Some("a?b?")), + "/a/b/c?a?b?/c" => uri_origin("/a/b/c", Some("a?b?/c")), + "/?abc" => uri_origin("/", Some("abc")), + "/hi%20there?a=b&c=d" => uri_origin("/hi%20there", Some("a=b&c=d")), + "/c/d/fa/b/c?abc" => uri_origin("/c/d/fa/b/c", Some("abc")), + "/xn--ls8h?emoji=poop" => uri_origin("/xn--ls8h", Some("emoji=poop")), + ); +} + +#[test] +fn authority() { + assert_parse_eq!( + "abc" => Authority::new(None, Raw("abc"), None), + "@abc" => Authority::new(Some(""), Raw("abc"), None), + "sergio:benitez@spark" => Authority::new(Some("sergio:benitez"), Raw("spark"), None), + "a:b:c@1.2.3:12121" => Authority::new(Some("a:b:c"), Raw("1.2.3"), Some(12121)), + "sergio@spark" => Authority::new(Some("sergio"), Raw("spark"), None), + "sergio@spark:230" => Authority::new(Some("sergio"), Raw("spark"), Some(230)), + "sergio@[1::]:230" => Authority::new(Some("sergio"), Bracketed("1::"), Some(230)), + "google.com:8000" => Authority::new(None, Raw("google.com"), Some(8000)), + "[1::2::3]:80" => Authority::new(None, Bracketed("1::2::3"), Some(80)), + ); +} + +#[test] +fn absolute() { + assert_parse_eq! { + "http://foo.com:8000" => Absolute::new( + "http", + Some(Authority::new(None, Raw("foo.com"), Some(8000))), + None + ), + "http://foo:8000" => Absolute::new( + "http", + Some(Authority::new(None, Raw("foo"), Some(8000))), + None, + ), + "foo:bar" => Absolute::new( + "foo", + None, + Some(Origin::new::<_, &str>("bar", None)), + ), + "http://sergio:pass@foo.com:8000" => Absolute::new( + "http", + Some(Authority::new(Some("sergio:pass"), Raw("foo.com"), Some(8000))), + None, + ), + "foo:/sergio/pass?hi" => Absolute::new( + "foo", + None, + Some(Origin::new("/sergio/pass", Some("hi"))), + ), + "bar:" => Absolute::new( + "bar", + None, + Some(Origin::new::<_, &str>("", None)), + ), + "foo:?hi" => Absolute::new( + "foo", + None, + Some(Origin::new("", Some("hi"))), + ), + "foo:a/b?hi" => Absolute::new( + "foo", + None, + Some(Origin::new("a/b", Some("hi"))), + ), + "foo:a/b" => Absolute::new( + "foo", + None, + Some(Origin::new::<_, &str>("a/b", None)), + ), + "foo:/a/b" => Absolute::new( + "foo", + None, + Some(Origin::new::<_, &str>("/a/b", None)) + ), + "abc://u:p@foo.com:123/a/b?key=value&key2=value2" => Absolute::new( + "abc", + Some(Authority::new(Some("u:p"), Raw("foo.com"), Some(123))), + Some(Origin::new("/a/b", Some("key=value&key2=value2"))), + ), + "ftp://foo.com:21/abc" => Absolute::new( + "ftp", + Some(Authority::new(None, Raw("foo.com"), Some(21))), + Some(Origin::new::<_, &str>("/abc", None)), + ), + "http://google.com/abc" => Absolute::new( + "http", + Some(Authority::new(None, Raw("google.com"), None)), + Some(Origin::new::<_, &str>("/abc", None)), + ), + "http://google.com" => Absolute::new( + "http", + Some(Authority::new(None, Raw("google.com"), None)), + None + ), + "http://foo.com?test" => Absolute::new( + "http", + Some(Authority::new(None, Raw("foo.com"), None,)), + Some(Origin::new("", Some("test"))), + ), + "http://google.com/abc?hi" => Absolute::new( + "http", + Some(Authority::new(None, Raw("google.com"), None,)), + Some(Origin::new("/abc", Some("hi"))), + ), + }; +} + +#[test] +fn display() { + assert_displays_eq! { + "abc", "@):0", "[a]" + } +} diff --git a/core/http/src/uri/absolute.rs b/core/http/src/uri/absolute.rs @@ -0,0 +1,175 @@ +use std::borrow::Cow; +use std::fmt::{self, Display}; + +use ext::IntoOwned; +use parse::{Indexed, IndexedStr}; +use uri::{Authority, Origin, Error, as_utf8_unchecked}; + +/// A URI with a scheme, authority, path, and query: +/// `http://user:pass@domain.com:4444/path?query`. +/// +/// # Structure +/// +/// The following diagram illustrates the syntactic structure of an absolute +/// URI with all optional parts: +/// +/// ```text +/// http://user:pass@domain.com:4444/path?query +/// |--| |-----------------------||---------| +/// scheme authority origin +/// ``` +/// +/// The scheme part of the absolute URI and at least one of authority or origin +/// are required. +#[derive(Debug)] +pub struct Absolute<'a> { + source: Option<Cow<'a, str>>, + scheme: IndexedStr<'a>, + authority: Option<Authority<'a>>, + origin: Option<Origin<'a>>, +} + +impl<'a> IntoOwned for Absolute<'a> { + type Owned = Absolute<'static>; + + fn into_owned(self) -> Self::Owned { + Absolute { + source: self.source.into_owned(), + scheme: self.scheme.into_owned(), + authority: self.authority.into_owned(), + origin: self.origin.into_owned(), + } + } +} + +impl<'a> Absolute<'a> { + #[inline] + crate unsafe fn raw( + source: Cow<'a, [u8]>, + scheme: Indexed<'a, [u8]>, + authority: Option<Authority<'a>>, + origin: Option<Origin<'a>>, + ) -> Absolute<'a> { + Absolute { + source: Some(as_utf8_unchecked(source)), + scheme: scheme.coerce(), + authority: authority, + origin: origin, + } + } + + #[cfg(test)] + crate fn new( + scheme: &'a str, + authority: Option<Authority<'a>>, + origin: Option<Origin<'a>> + ) -> Absolute<'a> { + Absolute { + source: None, scheme: scheme.into(), authority, origin + } + } + + /// Parses the string `string` into an `Absolute`. Parsing will never + /// allocate. Returns an `Error` if `string` is not a valid absolute URI. + /// + /// # Example + /// + /// ```rust + /// # extern crate rocket; + /// use rocket::http::uri::Absolute; + /// + /// // Parse a valid authority URI. + /// let uri = Absolute::parse("http://google.com").expect("valid URI"); + /// assert_eq!(uri.scheme(), "http"); + /// assert_eq!(uri.authority().unwrap().host(), "google.com"); + /// assert_eq!(uri.origin(), None); + /// ``` + pub fn parse(string: &'a str) -> Result<Absolute<'a>, Error<'a>> { + ::parse::uri::absolute_from_str(string) + } + + /// Returns the scheme part of the absolute URI. + /// + /// # Example + /// + /// ```rust + /// # extern crate rocket; + /// use rocket::http::uri::Absolute; + /// + /// let uri = Absolute::parse("ftp://127.0.0.1").expect("valid URI"); + /// assert_eq!(uri.scheme(), "ftp"); + /// ``` + #[inline(always)] + pub fn scheme(&self) -> &str { + self.scheme.from_cow_source(&self.source) + } + + /// Returns the authority part of the absolute URI, if there is one. + /// + /// # Example + /// + /// ```rust + /// # extern crate rocket; + /// use rocket::http::uri::Absolute; + /// + /// let uri = Absolute::parse("https://rocket.rs:80").expect("valid URI"); + /// assert_eq!(uri.scheme(), "https"); + /// let authority = uri.authority().unwrap(); + /// assert_eq!(authority.host(), "rocket.rs"); + /// assert_eq!(authority.port(), Some(80)); + /// + /// let uri = Absolute::parse("file:/web/home").expect("valid URI"); + /// assert_eq!(uri.authority(), None); + /// ``` + #[inline(always)] + pub fn authority(&self) -> Option<&Authority<'a>> { + self.authority.as_ref() + } + + /// Returns the origin part of the absolute URI, if there is one. + /// + /// # Example + /// + /// ```rust + /// # extern crate rocket; + /// use rocket::http::uri::Absolute; + /// + /// let uri = Absolute::parse("file:/web/home.html?new").expect("valid URI"); + /// assert_eq!(uri.scheme(), "file"); + /// let origin = uri.origin().unwrap(); + /// assert_eq!(origin.path(), "/web/home.html"); + /// assert_eq!(origin.query(), Some("new")); + /// + /// let uri = Absolute::parse("https://rocket.rs").expect("valid URI"); + /// assert_eq!(uri.origin(), None); + /// ``` + #[inline(always)] + pub fn origin(&self) -> Option<&Origin<'a>> { + self.origin.as_ref() + } +} + +impl<'a, 'b> PartialEq<Absolute<'b>> for Absolute<'a> { + fn eq(&self, other: &Absolute<'b>) -> bool { + self.scheme() == other.scheme() + && self.authority() == other.authority() + && self.origin() == other.origin() + } +} + +impl<'a> Display for Absolute<'a> { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "{}", self.scheme())?; + match self.authority { + Some(ref authority) => write!(f, "://{}", authority)?, + None => write!(f, ":")? + } + + if let Some(ref origin) = self.origin { + write!(f, "{}", origin)?; + } + + Ok(()) + } +} + diff --git a/core/http/src/uri/authority.rs b/core/http/src/uri/authority.rs @@ -0,0 +1,227 @@ +use std::fmt::{self, Display}; +use std::borrow::Cow; + +use ext::IntoOwned; +use parse::{Indexed, IndexedStr}; +use uri::{as_utf8_unchecked, Error}; + +/// A URI with an authority only: `user:pass@host:8000`. +/// +/// # Structure +/// +/// The following diagram illustrates the syntactic structure of an authority +/// URI: +/// +/// ```text +/// username:password@some.host:8088 +/// |---------------| |-------| |--| +/// user info host port +/// ``` +/// +/// Only the host part of the URI is required. +#[derive(Debug)] +pub struct Authority<'a> { + source: Option<Cow<'a, str>>, + user_info: Option<IndexedStr<'a>>, + host: Host<IndexedStr<'a>>, + port: Option<u16>, +} + +#[derive(Debug)] +crate enum Host<T> { + Bracketed(T), + Raw(T) +} + +impl<T: IntoOwned> IntoOwned for Host<T> { + type Owned = Host<T::Owned>; + + fn into_owned(self) -> Self::Owned { + self.map_inner(IntoOwned::into_owned) + } +} + +impl<'a> IntoOwned for Authority<'a> { + type Owned = Authority<'static>; + + fn into_owned(self) -> Authority<'static> { + Authority { + source: self.source.into_owned(), + user_info: self.user_info.into_owned(), + host: self.host.into_owned(), + port: self.port + } + } +} + +impl<'a> Authority<'a> { + crate unsafe fn raw( + source: Cow<'a, [u8]>, + user_info: Option<Indexed<'a, [u8]>>, + host: Host<Indexed<'a, [u8]>>, + port: Option<u16> + ) -> Authority<'a> { + Authority { + source: Some(as_utf8_unchecked(source)), + user_info: user_info.map(|u| u.coerce()), + host: host.map_inner(|inner| inner.coerce()), + port: port + } + } + + #[cfg(test)] + crate fn new( + user_info: Option<&'a str>, + host: Host<&'a str>, + port: Option<u16> + ) -> Authority<'a> { + Authority { + source: None, + user_info: user_info.map(|u| u.into()), + host: host.map_inner(|inner| inner.into()), + port: port + } + } + + /// Parses the string `string` into an `Authority`. Parsing will never + /// allocate. Returns an `Error` if `string` is not a valid authority URI. + /// + /// # Example + /// + /// ```rust + /// # extern crate rocket; + /// use rocket::http::uri::Authority; + /// + /// // Parse a valid authority URI. + /// let uri = Authority::parse("user:pass@host").expect("valid URI"); + /// assert_eq!(uri.user_info(), Some("user:pass")); + /// assert_eq!(uri.host(), "host"); + /// assert_eq!(uri.port(), None); + /// + /// // Invalid authority URIs fail to parse. + /// Authority::parse("http://google.com").expect_err("invalid authority"); + /// ``` + pub fn parse(string: &'a str) -> Result<Authority<'a>, Error<'a>> { + ::parse::uri::authority_from_str(string) + } + + /// Returns the user info part of the authority URI, if there is one. + /// + /// # Example + /// + /// ```rust + /// # extern crate rocket; + /// use rocket::http::uri::Authority; + /// + /// let uri = Authority::parse("username:password@host").unwrap(); + /// assert_eq!(uri.user_info(), Some("username:password")); + /// ``` + pub fn user_info(&self) -> Option<&str> { + self.user_info.as_ref().map(|u| u.from_cow_source(&self.source)) + } + + /// Returns the host part of the authority URI. + /// + /// + /// If the host was provided in brackets (such as for IPv6 addresses), the + /// brackets will not be part of the returned string. + /// + /// # Example + /// + /// ```rust + /// # extern crate rocket; + /// use rocket::http::uri::Authority; + /// + /// let uri = Authority::parse("domain.com:123").unwrap(); + /// assert_eq!(uri.host(), "domain.com"); + /// + /// let uri = Authority::parse("username:password@host:123").unwrap(); + /// assert_eq!(uri.host(), "host"); + /// + /// let uri = Authority::parse("username:password@[1::2]:123").unwrap(); + /// assert_eq!(uri.host(), "1::2"); + /// ``` + #[inline(always)] + pub fn host(&self) -> &str { + self.host.inner().from_cow_source(&self.source) + } + + /// Returns the port part of the authority URI, if there is one. + /// + /// # Example + /// + /// ```rust + /// # extern crate rocket; + /// use rocket::http::uri::Authority; + /// + /// // With a port. + /// let uri = Authority::parse("username:password@host:123").unwrap(); + /// assert_eq!(uri.port(), Some(123)); + /// + /// let uri = Authority::parse("domain.com:8181").unwrap(); + /// assert_eq!(uri.port(), Some(8181)); + /// + /// // Without a port. + /// let uri = Authority::parse("username:password@host").unwrap(); + /// assert_eq!(uri.port(), None); + /// ``` + #[inline(always)] + pub fn port(&self) -> Option<u16> { + self.port + } +} + +impl<'a, 'b> PartialEq<Authority<'b>> for Authority<'a> { + fn eq(&self, other: &Authority<'b>) -> bool { + self.user_info() == other.user_info() + && self.host() == other.host() + && self.host.is_bracketed() == other.host.is_bracketed() + && self.port() == other.port() + } +} + +impl<'a> Display for Authority<'a> { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + if let Some(user_info) = self.user_info() { + write!(f, "{}@", user_info)?; + } + + match self.host { + Host::Bracketed(_) => write!(f, "[{}]", self.host())?, + Host::Raw(_) => write!(f, "{}", self.host())? + } + + if let Some(port) = self.port { + write!(f, ":{}", port)?; + } + + Ok(()) + } +} + +impl<T> Host<T> { + #[inline] + fn inner(&self) -> &T { + match *self { + Host::Bracketed(ref inner) | Host::Raw(ref inner) => inner + } + } + + #[inline] + fn is_bracketed(&self) -> bool { + match *self { + Host::Bracketed(_) => true, + _ => false + } + } + + #[inline] + fn map_inner<F, U>(self, f: F) -> Host<U> + where F: FnOnce(T) -> U + { + match self { + Host::Bracketed(inner) => Host::Bracketed(f(inner)), + Host::Raw(inner) => Host::Raw(f(inner)) + } + } +} diff --git a/core/http/src/uri/mod.rs b/core/http/src/uri/mod.rs @@ -1,9 +1,17 @@ -//! Types for absolute URIs and traits for URI display. +//! Types for URIs and traits for rendering URI components. mod uri; mod uri_display; mod from_uri_param; +mod origin; +mod authority; +mod absolute; + +pub use parse::uri::Error; pub use self::uri::*; +pub use self::authority::*; +pub use self::origin::*; +pub use self::absolute::*; pub use self::uri_display::*; pub use self::from_uri_param::*; diff --git a/core/http/src/uri/origin.rs b/core/http/src/uri/origin.rs @@ -0,0 +1,653 @@ +use std::fmt::{self, Display}; +use std::borrow::Cow; + +use ext::IntoOwned; +use parse::{Indexed, IndexedStr}; +use uri::{as_utf8_unchecked, Error}; + +use state::Storage; + +/// A URI with an absolute path and optional query: `/path?query`. +/// +/// Origin URIs are the primary type of URI encountered in Rocket applications. +/// They are also the _simplest_ type of URIs, made up of only a path and an +/// optional query. +/// +/// # Structure +/// +/// The following diagram illustrates the syntactic structure of an origin URI: +/// +/// ```text +/// /first_segment/second_segment/third?optional=query +/// |---------------------------------| |------------| +/// path query +/// ``` +/// +/// The URI must begin with a `/`, can be followed by any number of _segments_, +/// and an optional `?` query separator and query string. +/// +/// # Normalization +/// +/// Rocket prefers, and will sometimes require, origin URIs to be _normalized_. +/// A normalized origin URI is a valid origin URI that contains zero empty +/// segments except when there are no segments. +/// +/// As an example, the following URIs are all valid, normalized URIs: +/// +/// ```rust +/// # extern crate rocket; +/// # use rocket::http::uri::Origin; +/// # let valid_uris = [ +/// "/", +/// "/a/b/c", +/// "/a/b/c?q", +/// "/some%20thing" +/// # ]; +/// # for uri in &valid_uris { +/// # assert!(Origin::parse(uri).unwrap().is_normalized()); +/// # } +/// ``` +/// +/// By contrast, the following are valid but _abnormal_ URIs: +/// +/// ```rust +/// # extern crate rocket; +/// # use rocket::http::uri::Origin; +/// # let invalid = [ +/// "//", // one empty segment +/// "/a/b/", // trailing empty segment +/// "/a/ab//c//d" // two empty segments +/// # ]; +/// # for uri in &invalid { +/// # assert!(!Origin::parse(uri).unwrap().is_normalized()); +/// # } +/// ``` +/// +/// The [`Origin::to_normalized()`] method can be used to normalize any +/// `Origin`: +/// +/// ```rust +/// # extern crate rocket; +/// # use rocket::http::uri::Origin; +/// # let invalid = [ +/// // abnormal versions +/// "//", "/a/b/", "/a/ab//c//d" +/// # , +/// +/// // normalized versions +/// "/", "/a/b", "/a/ab/c/d" +/// # ]; +/// # for i in 0..(invalid.len() / 2) { +/// # let abnormal = Origin::parse(invalid[i]).unwrap(); +/// # let expected = Origin::parse(invalid[i + (invalid.len() / 2)]).unwrap(); +/// # assert_eq!(abnormal.to_normalized(), expected); +/// # } +/// ``` +#[derive(Clone, Debug)] +pub struct Origin<'a> { + crate source: Option<Cow<'a, str>>, + crate path: IndexedStr<'a>, + crate query: Option<IndexedStr<'a>>, + crate segment_count: Storage<usize>, +} + +impl<'a, 'b> PartialEq<Origin<'b>> for Origin<'a> { + fn eq(&self, other: &Origin<'b>) -> bool { + self.path() == other.path() && self.query() == other.query() + } +} + +impl<'a> IntoOwned for Origin<'a> { + type Owned = Origin<'static>; + + fn into_owned(self) -> Origin<'static> { + Origin { + source: self.source.into_owned(), + path: self.path.into_owned(), + query: self.query.into_owned(), + segment_count: self.segment_count + } + } +} + +impl<'a> Origin<'a> { + #[inline] + crate unsafe fn raw( + source: Cow<'a, [u8]>, + path: Indexed<'a, [u8]>, + query: Option<Indexed<'a, [u8]>> + ) -> Origin<'a> { + Origin { + source: Some(as_utf8_unchecked(source)), + path: path.coerce(), + query: query.map(|q| q.coerce()), + segment_count: Storage::new() + } + } + + // Used mostly for testing and to construct known good URIs from other parts + // of Rocket. This should _really_ not be used outside of Rocket because the + // resulting `Origin's` may not be valid origin URIs! + #[doc(hidden)] + pub fn new<P, Q>(path: P, query: Option<Q>) -> Origin<'a> + where P: Into<Cow<'a, str>>, Q: Into<Cow<'a, str>> + { + Origin { + source: None, + path: Indexed::from(path), + query: query.map(Indexed::from), + segment_count: Storage::new() + } + } + + // Used to fabricate URIs in several places. Equivalent to `Origin::new("/", + // None)` or `Origin::parse("/").unwrap()`. Should not be used outside of + // Rocket, though doing so would be less harmful. + #[doc(hidden)] + pub fn dummy() -> Origin<'static> { + Origin::new::<&'static str, &'static str>("/", None) + } + + /// Parses the string `string` into an `Origin`. Parsing will never + /// allocate. Returns an `Error` if `string` is not a valid origin URI. + /// + /// # Example + /// + /// ```rust + /// # extern crate rocket; + /// use rocket::http::uri::Origin; + /// + /// // Parse a valid origin URI. + /// let uri = Origin::parse("/a/b/c?query").expect("valid URI"); + /// assert_eq!(uri.path(), "/a/b/c"); + /// assert_eq!(uri.query(), Some("query")); + /// + /// // Invalid URIs fail to parse. + /// Origin::parse("foo bar").expect_err("invalid URI"); + /// ``` + pub fn parse(string: &'a str) -> Result<Origin<'a>, Error<'a>> { + ::parse::uri::origin_from_str(string) + } + + // Parses an `Origin` that may contain `<` or `>` characters which are + // invalid according to the RFC but used by Rocket's routing URIs Don't use + // this outside of Rocket! + #[doc(hidden)] + pub fn parse_route(string: &'a str) -> Result<Origin<'a>, Error<'a>> { + ::parse::uri::route_origin_from_str(string) + } + + /// Parses the string `string` into an `Origin`. Parsing will never + /// allocate. This method should be used instead of [`Origin::parse()`] when + /// the source URI is already a `String`. Returns an `Error` if `string` is + /// not a valid origin URI. + /// + /// # Example + /// + /// ```rust + /// # extern crate rocket; + /// use rocket::http::uri::Origin; + /// + /// let source = format!("/foo/{}/three", 2); + /// let uri = Origin::parse_owned(source).expect("valid URI"); + /// assert_eq!(uri.path(), "/foo/2/three"); + /// assert_eq!(uri.query(), None); + /// ``` + pub fn parse_owned(string: String) -> Result<Origin<'static>, Error<'static>> { + // We create a copy of a pointer to `string` to escape the borrow + // checker. This is so that we can "move out of the borrow" later. + // + // For this to be correct and safe, we need to ensure that: + // + // 1. No `&mut` references to `string` are created after this line. + // 2. `string` isn't dropped by `copy_of_str` is live. + // + // These two facts can be easily verified. An `&mut` can be created + // because `string` isn't `mut`. Then, `string` is clearly not dropped + // since it's passed in to `source`. + let copy_of_str = unsafe { &*(string.as_str() as *const str) }; + let origin = Origin::parse(copy_of_str)?; + + let uri = match origin { + Origin { source: Some(_), path, query, segment_count } => Origin { + segment_count, + path: path.into_owned(), + query: query.into_owned(), + // At this point, it's impossible for anything to be borrowing + // `string` except for `source`, even though Rust doesn't know + // it. Because we're replacing `source` here, there can't + // possibly be a borrow remaining, it's safe to "move out of the + // borrow". + source: Some(Cow::Owned(string)), + }, + _ => unreachable!("parser always parses with a source") + }; + + Ok(uri) + } + + /// Returns `true` if `self` is normalized. Otherwise, returns `false`. + /// + /// See [Normalization](#normalization) for more information on what it + /// means for an origin URI to be normalized. + /// + /// # Example + /// + /// ```rust + /// # extern crate rocket; + /// use rocket::http::uri::Origin; + /// + /// let normal = Origin::parse("/").unwrap(); + /// assert!(normal.is_normalized()); + /// + /// let normal = Origin::parse("/a/b/c").unwrap(); + /// assert!(normal.is_normalized()); + /// + /// let abnormal = Origin::parse("/a/b/c//d").unwrap(); + /// assert!(!abnormal.is_normalized()); + /// ``` + pub fn is_normalized(&self) -> bool { + self.path().starts_with('/') && + !self.path().contains("//") && + !(self.path().len() > 1 && self.path().ends_with('/')) + } + + /// Normalizes `self`. + /// + /// See [Normalization](#normalization) for more information on what it + /// means for an origin URI to be normalized. + /// + /// # Example + /// + /// ```rust + /// # extern crate rocket; + /// use rocket::http::uri::Origin; + /// + /// let abnormal = Origin::parse("/a/b/c//d").unwrap(); + /// assert!(!abnormal.is_normalized()); + /// + /// let normalized = abnormal.to_normalized(); + /// assert!(normalized.is_normalized()); + /// assert_eq!(normalized, Origin::parse("/a/b/c/d").unwrap()); + /// ``` + pub fn to_normalized(&self) -> Origin { + if self.is_normalized() { + Origin::new(self.path(), self.query()) + } else { + let mut new_path = String::with_capacity(self.path().len()); + for segment in self.segments() { + use std::fmt::Write; + let _ = write!(new_path, "/{}", segment); + } + + if new_path.is_empty() { + new_path.push('/'); + } + + Origin::new(new_path, self.query()) + } + } + + /// Returns the path part of this URI. + /// + /// ### Examples + /// + /// A URI with only a path: + /// + /// ```rust + /// # extern crate rocket; + /// use rocket::http::uri::Origin; + /// + /// let uri = Origin::parse("/a/b/c").unwrap(); + /// assert_eq!(uri.path(), "/a/b/c"); + /// ``` + /// + /// A URI with a query: + /// + /// ```rust + /// # extern crate rocket; + /// use rocket::http::uri::Origin; + /// + /// let uri = Origin::parse("/a/b/c?name=bob").unwrap(); + /// assert_eq!(uri.path(), "/a/b/c"); + /// ``` + #[inline] + pub fn path(&self) -> &str { + self.path.from_cow_source(&self.source) + } + + /// Returns the query part of this URI without the question mark, if there is + /// any. + /// + /// ### Examples + /// + /// A URI with a query part: + /// + /// ```rust + /// # extern crate rocket; + /// use rocket::http::uri::Origin; + /// + /// let uri = Origin::parse("/a/b/c?alphabet=true").unwrap(); + /// assert_eq!(uri.query(), Some("alphabet=true")); + /// ``` + /// + /// A URI without the query part: + /// + /// ```rust + /// # extern crate rocket; + /// use rocket::http::uri::Origin; + /// + /// let uri = Origin::parse("/a/b/c").unwrap(); + /// assert_eq!(uri.query(), None); + /// ``` + #[inline] + pub fn query(&self) -> Option<&str> { + self.query.as_ref().map(|q| q.from_cow_source(&self.source)) + } + + /// Removes the query part of this URI, if there is any. + /// + /// # Example + /// + /// ```rust + /// # extern crate rocket; + /// use rocket::http::uri::Origin; + /// + /// let mut uri = Origin::parse("/a/b/c?query=some").unwrap(); + /// assert_eq!(uri.query(), Some("query=some")); + /// + /// uri.clear_query(); + /// assert_eq!(uri.query(), None); + /// ``` + pub fn clear_query(&mut self) { + self.query = None; + } + + /// Returns an iterator over the segments of the path in this URI. Skips + /// empty segments. + /// + /// ### Examples + /// + /// A valid URI with only non-empty segments: + /// + /// ```rust + /// # extern crate rocket; + /// use rocket::http::uri::Origin; + /// + /// let uri = Origin::parse("/a/b/c?a=true").unwrap(); + /// for (i, segment) in uri.segments().enumerate() { + /// match i { + /// 0 => assert_eq!(segment, "a"), + /// 1 => assert_eq!(segment, "b"), + /// 2 => assert_eq!(segment, "c"), + /// _ => unreachable!("only three segments") + /// } + /// } + /// ``` + /// + /// A URI with empty segments: + /// + /// ```rust + /// # extern crate rocket; + /// use rocket::http::uri::Origin; + /// + /// let uri = Origin::parse("///a//b///c////d?query&param").unwrap(); + /// for (i, segment) in uri.segments().enumerate() { + /// match i { + /// 0 => assert_eq!(segment, "a"), + /// 1 => assert_eq!(segment, "b"), + /// 2 => assert_eq!(segment, "c"), + /// 3 => assert_eq!(segment, "d"), + /// _ => unreachable!("only four segments") + /// } + /// } + /// ``` + #[inline(always)] + pub fn segments(&self) -> Segments { + Segments(self.path()) + } + + /// Returns the number of segments in the URI. Empty segments, which are + /// invalid according to RFC#3986, are not counted. + /// + /// The segment count is cached after the first invocation. As a result, + /// this function is O(1) after the first invocation, and O(n) before. + /// + /// ### Examples + /// + /// A valid URI with only non-empty segments: + /// + /// ```rust + /// # extern crate rocket; + /// use rocket::http::uri::Origin; + /// + /// let uri = Origin::parse("/a/b/c").unwrap(); + /// assert_eq!(uri.segment_count(), 3); + /// ``` + /// + /// A URI with empty segments: + /// + /// ```rust + /// # extern crate rocket; + /// use rocket::http::uri::Origin; + /// + /// let uri = Origin::parse("/a/b//c/d///e").unwrap(); + /// assert_eq!(uri.segment_count(), 5); + /// ``` + #[inline] + pub fn segment_count(&self) -> usize { + *self.segment_count.get_or_set(|| self.segments().count()) + } +} + +impl<'a> Display for Origin<'a> { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "{}", self.path())?; + if let Some(q) = self.query() { + write!(f, "?{}", q)?; + } + + Ok(()) + } +} + +/// Iterator over the segments of an absolute URI path. Skips empty segments. +/// +/// ### Examples +/// +/// ```rust +/// # extern crate rocket; +/// use rocket::http::uri::Origin; +/// +/// let uri = Origin::parse("/a/////b/c////////d").unwrap(); +/// let segments = uri.segments(); +/// for (i, segment) in segments.enumerate() { +/// match i { +/// 0 => assert_eq!(segment, "a"), +/// 1 => assert_eq!(segment, "b"), +/// 2 => assert_eq!(segment, "c"), +/// 3 => assert_eq!(segment, "d"), +/// _ => panic!("only four segments") +/// } +/// } +/// ``` +#[derive(Clone, Debug)] +pub struct Segments<'a>(pub &'a str); + +impl<'a> Iterator for Segments<'a> { + type Item = &'a str; + + #[inline] + fn next(&mut self) -> Option<Self::Item> { + // Find the start of the next segment (first that's not '/'). + let i = self.0.find(|c| c != '/')?; + + // Get the index of the first character that _is_ a '/' after start. + // j = index of first character after i (hence the i +) that's not a '/' + let j = self.0[i..].find('/').map_or(self.0.len(), |j| i + j); + + // Save the result, update the iterator, and return! + let result = Some(&self.0[i..j]); + self.0 = &self.0[j..]; + result + } + + // TODO: Potentially take a second parameter with Option<cached count> and + // return it here if it's Some. The downside is that a decision has to be + // made about -when- to compute and cache that count. A place to do it is in + // the segments() method. But this means that the count will always be + // computed regardless of whether it's needed. Maybe this is ok. We'll see. + // fn count(self) -> usize where Self: Sized { + // self.1.unwrap_or_else(self.fold(0, |cnt, _| cnt + 1)) + // } +} + +#[cfg(test)] +mod tests { + use super::Origin; + + fn seg_count(path: &str, expected: usize) -> bool { + let actual = Origin::parse(path).unwrap().segment_count(); + if actual != expected { + eprintln!("Count mismatch: expected {}, got {}.", expected, actual); + eprintln!("{}", if actual != expected { "lifetime" } else { "buf" }); + eprintln!("Segments (for {}):", path); + for (i, segment) in Origin::parse(path).unwrap().segments().enumerate() { + eprintln!("{}: {}", i, segment); + } + } + + actual == expected + } + + fn eq_segments(path: &str, expected: &[&str]) -> bool { + let uri = match Origin::parse(path) { + Ok(uri) => uri, + Err(e) => panic!("failed to parse {}: {}", path, e) + }; + + let actual: Vec<&str> = uri.segments().collect(); + actual == expected + } + + #[test] + fn send_and_sync() { + fn assert<T: Send + Sync>() {}; + assert::<Origin>(); + } + + #[test] + fn simple_segment_count() { + assert!(seg_count("/", 0)); + assert!(seg_count("/a", 1)); + assert!(seg_count("/a/", 1)); + assert!(seg_count("/a/", 1)); + assert!(seg_count("/a/b", 2)); + assert!(seg_count("/a/b/", 2)); + assert!(seg_count("/a/b/", 2)); + assert!(seg_count("/ab/", 1)); + } + + #[test] + fn segment_count() { + assert!(seg_count("////", 0)); + assert!(seg_count("//a//", 1)); + assert!(seg_count("//abc//", 1)); + assert!(seg_count("//abc/def/", 2)); + assert!(seg_count("//////abc///def//////////", 2)); + assert!(seg_count("/a/b/c/d/e/f/g", 7)); + assert!(seg_count("/a/b/c/d/e/f/g", 7)); + assert!(seg_count("/a/b/c/d/e/f/g/", 7)); + assert!(seg_count("/a/b/cdjflk/d/e/f/g", 7)); + assert!(seg_count("//aaflja/b/cdjflk/d/e/f/g", 7)); + assert!(seg_count("/a/b", 2)); + } + + #[test] + fn single_segments_match() { + assert!(eq_segments("/", &[])); + assert!(eq_segments("/a", &["a"])); + assert!(eq_segments("/a/", &["a"])); + assert!(eq_segments("///a/", &["a"])); + assert!(eq_segments("///a///////", &["a"])); + assert!(eq_segments("/a///////", &["a"])); + assert!(eq_segments("//a", &["a"])); + assert!(eq_segments("/abc", &["abc"])); + assert!(eq_segments("/abc/", &["abc"])); + assert!(eq_segments("///abc/", &["abc"])); + assert!(eq_segments("///abc///////", &["abc"])); + assert!(eq_segments("/abc///////", &["abc"])); + assert!(eq_segments("//abc", &["abc"])); + } + + #[test] + fn multi_segments_match() { + assert!(eq_segments("/a/b/c", &["a", "b", "c"])); + assert!(eq_segments("/a/b", &["a", "b"])); + assert!(eq_segments("/a///b", &["a", "b"])); + assert!(eq_segments("/a/b/c/d", &["a", "b", "c", "d"])); + assert!(eq_segments("///a///////d////c", &["a", "d", "c"])); + assert!(eq_segments("/abc/abc", &["abc", "abc"])); + assert!(eq_segments("/abc/abc/", &["abc", "abc"])); + assert!(eq_segments("///abc///////a", &["abc", "a"])); + assert!(eq_segments("/////abc/b", &["abc", "b"])); + assert!(eq_segments("//abc//c////////d", &["abc", "c", "d"])); + } + + #[test] + fn multi_segments_match_funky_chars() { + assert!(eq_segments("/a/b/c!!!", &["a", "b", "c!!!"])); + } + + #[test] + fn segment_mismatch() { + assert!(!eq_segments("/", &["a"])); + assert!(!eq_segments("/a", &[])); + assert!(!eq_segments("/a/a", &["a"])); + assert!(!eq_segments("/a/b", &["b", "a"])); + assert!(!eq_segments("/a/a/b", &["a", "b"])); + assert!(!eq_segments("///a/", &[])); + } + + fn test_query(uri: &str, query: Option<&str>) { + let uri = Origin::parse(uri).unwrap(); + assert_eq!(uri.query(), query); + } + + #[test] + fn query_does_not_exist() { + test_query("/test", None); + test_query("/a/b/c/d/e", None); + test_query("/////", None); + test_query("//a///", None); + test_query("/a/b/c", None); + test_query("/", None); + } + + #[test] + fn query_exists() { + test_query("/test?abc", Some("abc")); + test_query("/a/b/c?abc", Some("abc")); + test_query("/a/b/c/d/e/f/g/?abc", Some("abc")); + test_query("/?123", Some("123")); + test_query("/?", Some("")); + test_query("/?", Some("")); + test_query("/?hi", Some("hi")); + } + + #[test] + fn normalized() { + let uri_to_string = |s| Origin::parse(s) + .unwrap() + .to_normalized() + .to_string(); + + assert_eq!(uri_to_string("/"), "/".to_string()); + assert_eq!(uri_to_string("//"), "/".to_string()); + assert_eq!(uri_to_string("//////a/"), "/a".to_string()); + assert_eq!(uri_to_string("//ab"), "/ab".to_string()); + assert_eq!(uri_to_string("//a"), "/a".to_string()); + assert_eq!(uri_to_string("/a/b///c"), "/a/b/c".to_string()); + assert_eq!(uri_to_string("/a///b/c/d///"), "/a/b/c/d".to_string()); + } +} diff --git a/core/http/src/uri/uri.rs b/core/http/src/uri/uri.rs @@ -1,218 +1,172 @@ -use std::fmt; +use std::fmt::{self, Display}; use std::convert::From; use std::borrow::Cow; use std::str::Utf8Error; -use std::sync::atomic::{AtomicIsize, Ordering}; +use std::convert::TryFrom; -/// Index (start, end) into a string, to prevent borrowing. -type Index = (usize, usize); +use ext::IntoOwned; +use parse::Indexed; +use uri::{Origin, Authority, Absolute, Error}; -/// Representation of an empty segment count. -const EMPTY: isize = -1; - -// TODO: Reconsider deriving PartialEq and Eq to make "//a/b" == "/a/b". -/// Borrowed string type for absolute URIs. -#[derive(Debug)] -pub struct Uri<'a> { - uri: Cow<'a, str>, - path: Index, - query: Option<Index>, - fragment: Option<Index>, - // The cached segment count. `EMPTY` is used to represent no segment count. - segment_count: AtomicIsize, +/// An `enum` encapsulating any of the possible URI variants. +/// +/// # Usage +/// +/// In Rocket, this type will rarely be used directly. Instead, you will +/// typically encounter URIs via the [`Origin`] type. This is because all +/// incoming requests contain origin-type URIs. +/// +/// Nevertheless, the `Uri` type is typically enountered as a conversion target. +/// In particular, you will likely see generic bounds of the form: `T: +/// TryInto<Uri>` (for instance, in [`Redirect`](rocket::Redirect) methods). +/// This means that you can provide any type `T` that implements `TryInto<Uri>`, +/// or, equivalently, any type `U` for which `Uri` implements `TryFrom<U>` or +/// `From<U>`. These include `&str` and `String`, [`Origin`], [`Authority`], and +/// [`Absolute`]. +/// +/// ## Parsing +/// +/// The `Uri` type implements a full, zero-allocation, zero-copy [RFC 7230] +/// compliant parser. To parse an `&str` into a `Uri`, use the [`Uri::parse()`] +/// method. Alternatively, you may also use the `TryFrom<&str>` and +/// `TryFrom<String>` trait implementation. To inspect the parsed type, match on +/// the resulting `enum` and use the methods of the internal structure. +/// +/// [RFC 7230]: https://tools.ietf.org/html/rfc7230 +/// +/// ## Percent Encoding/Decoding +/// +/// This type also provides the following percent encoding/decoding helper +/// methods: [`Uri::percent_encode`], [`Uri::percent_decode`], and +/// [`Uri::percent_decode_lossy`]. +#[derive(Debug, PartialEq)] +pub enum Uri<'a> { + /// An [`Origin`] URI. + Origin(Origin<'a>), + /// An [`Authority`] URI. + Authority(Authority<'a>), + /// An [`Absolute`] URI. + Absolute(Absolute<'a>), + /// An asterisk: exactly `*`. + Asterisk, } impl<'a> Uri<'a> { - /// Constructs a new URI from a given string. The URI is assumed to be an - /// absolute, well formed URI. - pub fn new<T: Into<Cow<'a, str>>>(uri: T) -> Uri<'a> { - let uri = uri.into(); - let qmark = uri.find('?'); - let hmark = uri.find('#'); - - let end = uri.len(); - let (path, query, fragment) = match (qmark, hmark) { - (Some(i), Some(j)) if i < j => ((0, i), Some((i+1, j)), Some((j+1, end))), - (Some(_i), Some(j)) => ((0, j), None, Some((j+1, end))), - (Some(i), None) => ((0, i), Some((i+1, end)), None), - (None, Some(j)) => ((0, j), None, Some((j+1, end))), - (None, None) => ((0, end), None, None), - }; - - Uri { uri, path, query, fragment, segment_count: AtomicIsize::new(EMPTY) } + #[inline] + crate unsafe fn raw_absolute( + source: Cow<'a, [u8]>, + scheme: Indexed<'a, [u8]>, + path: Indexed<'a, [u8]>, + query: Option<Indexed<'a, [u8]>>, + ) -> Uri<'a> { + let origin = Origin::raw(source.clone(), path, query); + Uri::Absolute(Absolute::raw(source.clone(), scheme, None, Some(origin))) } - /// Returns the number of segments in the URI. Empty segments, which are - /// invalid according to RFC#3986, are not counted. - /// - /// The segment count is cached after the first invocation. As a result, - /// this function is O(1) after the first invocation, and O(n) before. + /// Parses the string `string` into a `Uri`. Parsing will never allocate. + /// Returns an `Error` if `string` is not a valid URI. /// - /// ### Examples - /// - /// A valid URI with only non-empty segments: + /// # Example /// /// ```rust /// # extern crate rocket; /// use rocket::http::uri::Uri; /// - /// let uri = Uri::new("/a/b/c"); - /// assert_eq!(uri.segment_count(), 3); - /// ``` - /// - /// A URI with empty segments: - /// - /// ```rust - /// # extern crate rocket; - /// use rocket::http::uri::Uri; + /// // Parse a valid origin URI (note: in practice, use `Origin::parse()`). + /// let uri = Uri::parse("/a/b/c?query").expect("valid URI"); + /// let origin = uri.origin().expect("origin URI"); + /// assert_eq!(origin.path(), "/a/b/c"); + /// assert_eq!(origin.query(), Some("query")); /// - /// let uri = Uri::new("/a/b//c/d///e"); - /// assert_eq!(uri.segment_count(), 5); + /// // Invalid URIs fail to parse. + /// Uri::parse("foo bar").expect_err("invalid URI"); /// ``` - #[inline(always)] - pub fn segment_count(&self) -> usize { - let count = self.segment_count.load(Ordering::Relaxed); - if count == EMPTY { - let real_count = self.segments().count(); - if real_count <= isize::max_value() as usize { - self.segment_count.store(real_count as isize, Ordering::Relaxed); - } - - real_count - } else { - count as usize - } + pub fn parse(string: &'a str) -> Result<Uri<'a>, Error> { + ::parse::uri::from_str(string) } - /// Returns an iterator over the segments of the path in this URI. Skips - /// empty segments. + /// Returns the internal instance of `Origin` if `self` is a `Uri::Origin`. + /// Otherwise, returns `None`. /// - /// ### Examples - /// - /// A valid URI with only non-empty segments: + /// # Example /// /// ```rust /// # extern crate rocket; /// use rocket::http::uri::Uri; /// - /// let uri = Uri::new("/a/b/c?a=true#done"); - /// for (i, segment) in uri.segments().enumerate() { - /// match i { - /// 0 => assert_eq!(segment, "a"), - /// 1 => assert_eq!(segment, "b"), - /// 2 => assert_eq!(segment, "c"), - /// _ => panic!("only three segments") - /// } - /// } - /// ``` - /// - /// A URI with empty segments: - /// - /// ```rust - /// # extern crate rocket; - /// use rocket::http::uri::Uri; + /// let uri = Uri::parse("/a/b/c?query").expect("valid URI"); + /// assert!(uri.origin().is_some()); /// - /// let uri = Uri::new("///a//b///c////d?#"); - /// for (i, segment) in uri.segments().enumerate() { - /// match i { - /// 0 => assert_eq!(segment, "a"), - /// 1 => assert_eq!(segment, "b"), - /// 2 => assert_eq!(segment, "c"), - /// 3 => assert_eq!(segment, "d"), - /// _ => panic!("only four segments") - /// } - /// } + /// let uri = Uri::parse("http://google.com").expect("valid URI"); + /// assert!(uri.origin().is_none()); /// ``` - #[inline(always)] - pub fn segments(&self) -> Segments { - Segments(self.path()) + pub fn origin(&self) -> Option<&Origin<'a>> { + match self { + Uri::Origin(ref inner) => Some(inner), + _ => None + } } - /// Returns the path part of this URI. - /// - /// ### Examples + /// Returns the internal instance of `Authority` if `self` is a + /// `Uri::Authority`. Otherwise, returns `None`. /// - /// A URI with only a path: + /// # Example /// /// ```rust /// # extern crate rocket; /// use rocket::http::uri::Uri; /// - /// let uri = Uri::new("/a/b/c"); - /// assert_eq!(uri.path(), "/a/b/c"); - /// ``` + /// let uri = Uri::parse("user:pass@domain.com").expect("valid URI"); + /// assert!(uri.authority().is_some()); /// - /// A URI with other components: - /// - /// ```rust - /// # extern crate rocket; - /// use rocket::http::uri::Uri; - /// - /// let uri = Uri::new("/a/b/c?name=bob#done"); - /// assert_eq!(uri.path(), "/a/b/c"); + /// let uri = Uri::parse("http://google.com").expect("valid URI"); + /// assert!(uri.authority().is_none()); /// ``` - #[inline(always)] - pub fn path(&self) -> &str { - let (i, j) = self.path; - &self.uri[i..j] + pub fn authority(&self) -> Option<&Authority<'a>> { + match self { + Uri::Authority(ref inner) => Some(inner), + _ => None + } } - /// Returns the query part of this URI without the question mark, if there is - /// any. + /// Returns the internal instance of `Absolute` if `self` is a + /// `Uri::Absolute`. Otherwise, returns `None`. /// - /// ### Examples - /// - /// A URI with a query part: + /// # Example /// /// ```rust /// # extern crate rocket; /// use rocket::http::uri::Uri; /// - /// let uri = Uri::new("/a/b/c?alphabet=true"); - /// assert_eq!(uri.query(), Some("alphabet=true")); - /// ``` - /// - /// A URI without the query part: - /// - /// ```rust - /// # extern crate rocket; - /// use rocket::http::uri::Uri; + /// let uri = Uri::parse("http://google.com").expect("valid URI"); + /// assert!(uri.absolute().is_some()); /// - /// let uri = Uri::new("/a/b/c"); - /// assert_eq!(uri.query(), None); + /// let uri = Uri::parse("/path").expect("valid URI"); + /// assert!(uri.absolute().is_none()); /// ``` - #[inline(always)] - pub fn query(&self) -> Option<&str> { - self.query.map(|(i, j)| &self.uri[i..j]) + pub fn absolute(&self) -> Option<&Absolute<'a>> { + match self { + Uri::Absolute(ref inner) => Some(inner), + _ => None + } } - /// Returns the fragment part of this URI without the hash mark, if there is - /// any. - /// - /// ### Examples - /// - /// A URI with a fragment part: - /// - /// ```rust - /// # extern crate rocket; - /// use rocket::http::uri::Uri; - /// - /// let uri = Uri::new("/a?alphabet=true#end"); - /// assert_eq!(uri.fragment(), Some("end")); - /// ``` + /// Returns a URL-encoded version of the string. Any characters outside of + /// visible ASCII-range are encoded as well as ' ', '"', '#', '<', '>', '`', + /// '?', '{', '}', '%', and '/'. /// - /// A URI without the fragment part: + /// # Examples /// /// ```rust /// # extern crate rocket; /// use rocket::http::uri::Uri; /// - /// let uri = Uri::new("/a?query=true"); - /// assert_eq!(uri.fragment(), None); + /// let encoded = Uri::percent_encode("hello?a=<b>hi</b>"); + /// assert_eq!(encoded, "hello%3Fa=%3Cb%3Ehi%3C%2Fb%3E"); /// ``` - #[inline(always)] - pub fn fragment(&self) -> Option<&str> { - self.fragment.map(|(i, j)| &self.uri[i..j]) + pub fn percent_encode(string: &str) -> Cow<str> { + let set = ::percent_encoding::PATH_SEGMENT_ENCODE_SET; + ::percent_encoding::utf8_percent_encode(string, set).into() } /// Returns a URL-decoded version of the string. If the percent encoded @@ -224,9 +178,8 @@ impl<'a> Uri<'a> { /// # extern crate rocket; /// use rocket::http::uri::Uri; /// - /// let uri = Uri::new("/Hello%2C%20world%21"); - /// let decoded_path = Uri::percent_decode(uri.path().as_bytes()).expect("decoded"); - /// assert_eq!(decoded_path, "/Hello, world!"); + /// let decoded = Uri::percent_decode("/Hello%2C%20world%21".as_bytes()); + /// assert_eq!(decoded.unwrap(), "/Hello, world!"); /// ``` pub fn percent_decode(string: &[u8]) -> Result<Cow<str>, Utf8Error> { let decoder = ::percent_encoding::percent_decode(string); @@ -243,367 +196,77 @@ impl<'a> Uri<'a> { /// # extern crate rocket; /// use rocket::http::uri::Uri; /// - /// let uri = Uri::new("/Hello%2C%20world%21"); - /// let decoded_path = Uri::percent_decode_lossy(uri.path().as_bytes()); - /// assert_eq!(decoded_path, "/Hello, world!"); + /// let decoded = Uri::percent_decode_lossy("/Hello%2C%20world%21".as_bytes()); + /// assert_eq!(decoded, "/Hello, world!"); /// ``` pub fn percent_decode_lossy(string: &[u8]) -> Cow<str> { let decoder = ::percent_encoding::percent_decode(string); decoder.decode_utf8_lossy() } - - /// Returns a URL-encoded version of the string. Any characters outside of - /// visible ASCII-range are encoded as well as ' ', '"', '#', '<', '>', '`', - /// '?', '{', '}', '%', and '/'. - /// - /// # Examples - /// - /// ```rust - /// # extern crate rocket; - /// use rocket::http::uri::Uri; - /// - /// let encoded = Uri::percent_encode("hello?a=<b>hi</b>"); - /// assert_eq!(encoded, "hello%3Fa=%3Cb%3Ehi%3C%2Fb%3E"); - /// ``` - pub fn percent_encode(string: &str) -> Cow<str> { - let set = ::percent_encoding::PATH_SEGMENT_ENCODE_SET; - ::percent_encoding::utf8_percent_encode(string, set).into() - } - - /// Returns the inner string of this URI. - /// - /// The returned string is in raw form. It contains empty segments. If you'd - /// like a string without empty segments, use `to_string` instead. - /// - /// ### Example - /// - /// ```rust - /// # extern crate rocket; - /// use rocket::http::uri::Uri; - /// - /// let uri = Uri::new("/a/b///c/d/e//f?name=Mike#end"); - /// assert_eq!(uri.as_str(), "/a/b///c/d/e//f?name=Mike#end"); - /// ``` - #[inline(always)] - pub fn as_str(&self) -> &str { - &self.uri - } } -impl<'a> Clone for Uri<'a> { - #[inline(always)] - fn clone(&self) -> Uri<'a> { - Uri { - uri: self.uri.clone(), - path: self.path, - query: self.query, - fragment: self.fragment, - segment_count: AtomicIsize::new(EMPTY), - } +crate unsafe fn as_utf8_unchecked(input: Cow<[u8]>) -> Cow<str> { + match input { + Cow::Borrowed(bytes) => Cow::Borrowed(::std::str::from_utf8_unchecked(bytes)), + Cow::Owned(bytes) => Cow::Owned(String::from_utf8_unchecked(bytes)) } } -impl<'a, 'b> PartialEq<Uri<'b>> for Uri<'a> { +impl<'a> TryFrom<&'a str> for Uri<'a> { + type Error = Error<'a>; + #[inline] - fn eq(&self, other: &Uri<'b>) -> bool { - self.path() == other.path() && - self.query() == other.query() && - self.fragment() == other.fragment() + fn try_from(string: &'a str) -> Result<Uri<'a>, Self::Error> { + Uri::parse(string) } } -impl<'a> Eq for Uri<'a> {} +impl TryFrom<String> for Uri<'static> { + type Error = Error<'static>; -impl<'a> From<&'a str> for Uri<'a> { - #[inline(always)] - fn from(uri: &'a str) -> Uri<'a> { - Uri::new(uri) + #[inline] + fn try_from(string: String) -> Result<Uri<'static>, Self::Error> { + // TODO: Potentially optimize this like `Origin::parse_owned`. + Uri::parse(&string) + .map(|u| u.into_owned()) + .map_err(|e| e.into_owned()) } } -impl<'a> From<Cow<'a, str>> for Uri<'a> { - #[inline(always)] - fn from(uri: Cow<'a, str>) -> Uri<'a> { - Uri::new(uri) - } -} +impl<'a> IntoOwned for Uri<'a> { + type Owned = Uri<'static>; -impl From<String> for Uri<'static> { - #[inline(always)] - fn from(uri: String) -> Uri<'static> { - Uri::new(uri) + fn into_owned(self) -> Uri<'static> { + match self { + Uri::Origin(origin) => Uri::Origin(origin.into_owned()), + Uri::Authority(authority) => Uri::Authority(authority.into_owned()), + Uri::Absolute(absolute) => Uri::Absolute(absolute.into_owned()), + Uri::Asterisk => Uri::Asterisk + } } } -impl<'a> fmt::Display for Uri<'a> { +impl<'a> Display for Uri<'a> { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - // If this is the root path, then there are "zero" segments. - if self.segment_count() == 0 { - write!(f, "/")?; - } else { - for segment in self.segments() { - write!(f, "/{}", segment)?; - } - } - - if let Some(query_str) = self.query() { - write!(f, "?{}", query_str)?; + match *self { + Uri::Origin(ref origin) => write!(f, "{}", origin), + Uri::Authority(ref authority) => write!(f, "{}", authority), + Uri::Absolute(ref absolute) => write!(f, "{}", absolute), + Uri::Asterisk => write!(f, "*") } - - if let Some(fragment_str) = self.fragment() { - write!(f, "#{}", fragment_str)?; - } - - Ok(()) - } -} - -/// Iterator over the segments of an absolute URI path. Skips empty segments. -/// -/// ### Examples -/// -/// ```rust -/// # extern crate rocket; -/// use rocket::http::uri::Uri; -/// -/// let uri = Uri::new("/a/////b/c////////d"); -/// let segments = uri.segments(); -/// for (i, segment) in segments.enumerate() { -/// match i { -/// 0 => assert_eq!(segment, "a"), -/// 1 => assert_eq!(segment, "b"), -/// 2 => assert_eq!(segment, "c"), -/// 3 => assert_eq!(segment, "d"), -/// _ => panic!("only four segments") -/// } -/// } -/// ``` -#[derive(Clone, Debug)] -pub struct Segments<'a>(pub &'a str); - -impl<'a> Iterator for Segments<'a> { - type Item = &'a str; - - #[inline] - fn next(&mut self) -> Option<Self::Item> { - // Find the start of the next segment (first that's not '/'). - let i = match self.0.find(|c| c != '/') { - Some(index) => index, - None => return None, - }; - - // Get the index of the first character that _is_ a '/' after start. - // j = index of first character after i (hence the i +) that's not a '/' - let j = self.0[i..].find('/').map_or(self.0.len(), |j| i + j); - - // Save the result, update the iterator, and return! - let result = Some(&self.0[i..j]); - self.0 = &self.0[j..]; - result } - - // TODO: Potentially take a second parameter with Option<cached count> and - // return it here if it's Some. The downside is that a decision has to be - // made about -when- to compute and cache that count. A place to do it is in - // the segments() method. But this means that the count will always be - // computed regardless of whether it's needed. Maybe this is ok. We'll see. - // fn count(self) -> usize where Self: Sized { - // self.1.unwrap_or_else(self.fold(0, |cnt, _| cnt + 1)) - // } -} - -/// Errors which can occur when attempting to interpret a segment string as a -/// valid path segment. -#[derive(Debug, PartialEq, Eq, Clone)] -pub enum SegmentError { - /// The segment contained invalid UTF8 characters when percent decoded. - Utf8(Utf8Error), - /// The segment started with the wrapped invalid character. - BadStart(char), - /// The segment contained the wrapped invalid character. - BadChar(char), - /// The segment ended with the wrapped invalid character. - BadEnd(char), } -#[cfg(test)] -mod tests { - use super::Uri; - - fn seg_count(path: &str, expected: usize) -> bool { - let actual = Uri::new(path).segment_count(); - if actual != expected { - eprintln!("Count mismatch: expected {}, got {}.", expected, actual); - eprintln!("{}", if actual != expected { "lifetime" } else { "buf" }); - eprintln!("Segments (for {}):", path); - for (i, segment) in Uri::new(path).segments().enumerate() { - eprintln!("{}: {}", i, segment); +macro_rules! impl_uri_from { + ($type:ident) => ( + impl<'a> From<$type<'a>> for Uri<'a> { + fn from(other: $type<'a>) -> Uri<'a> { + Uri::$type(other) } } - - actual == expected - } - - fn eq_segments(path: &str, expected: &[&str]) -> bool { - let uri = Uri::new(path); - let actual: Vec<&str> = uri.segments().collect(); - actual == expected - } - - #[test] - fn send_and_sync() { - fn assert<T: Send + Sync>() {}; - assert::<Uri>(); - } - - #[test] - fn simple_segment_count() { - assert!(seg_count("", 0)); - assert!(seg_count("/", 0)); - assert!(seg_count("a", 1)); - assert!(seg_count("/a", 1)); - assert!(seg_count("a/", 1)); - assert!(seg_count("/a/", 1)); - assert!(seg_count("/a/b", 2)); - assert!(seg_count("/a/b/", 2)); - assert!(seg_count("a/b/", 2)); - assert!(seg_count("ab/", 1)); - } - - #[test] - fn segment_count() { - assert!(seg_count("////", 0)); - assert!(seg_count("//a//", 1)); - assert!(seg_count("//abc//", 1)); - assert!(seg_count("//abc/def/", 2)); - assert!(seg_count("//////abc///def//////////", 2)); - assert!(seg_count("a/b/c/d/e/f/g", 7)); - assert!(seg_count("/a/b/c/d/e/f/g", 7)); - assert!(seg_count("/a/b/c/d/e/f/g/", 7)); - assert!(seg_count("/a/b/cdjflk/d/e/f/g", 7)); - assert!(seg_count("//aaflja/b/cdjflk/d/e/f/g", 7)); - assert!(seg_count("/a /b", 2)); - } - - #[test] - fn single_segments_match() { - assert!(eq_segments("", &[])); - assert!(eq_segments("a", &["a"])); - assert!(eq_segments("/a", &["a"])); - assert!(eq_segments("/a/", &["a"])); - assert!(eq_segments("a/", &["a"])); - assert!(eq_segments("///a/", &["a"])); - assert!(eq_segments("///a///////", &["a"])); - assert!(eq_segments("a///////", &["a"])); - assert!(eq_segments("//a", &["a"])); - assert!(eq_segments("", &[])); - assert!(eq_segments("abc", &["abc"])); - assert!(eq_segments("/a", &["a"])); - assert!(eq_segments("/abc/", &["abc"])); - assert!(eq_segments("abc/", &["abc"])); - assert!(eq_segments("///abc/", &["abc"])); - assert!(eq_segments("///abc///////", &["abc"])); - assert!(eq_segments("abc///////", &["abc"])); - assert!(eq_segments("//abc", &["abc"])); - } - - #[test] - fn multi_segments_match() { - assert!(eq_segments("a/b/c", &["a", "b", "c"])); - assert!(eq_segments("/a/b", &["a", "b"])); - assert!(eq_segments("/a///b", &["a", "b"])); - assert!(eq_segments("a/b/c/d", &["a", "b", "c", "d"])); - assert!(eq_segments("///a///////d////c", &["a", "d", "c"])); - assert!(eq_segments("abc/abc", &["abc", "abc"])); - assert!(eq_segments("abc/abc/", &["abc", "abc"])); - assert!(eq_segments("///abc///////a", &["abc", "a"])); - assert!(eq_segments("/////abc/b", &["abc", "b"])); - assert!(eq_segments("//abc//c////////d", &["abc", "c", "d"])); - } - - #[test] - fn multi_segments_match_funky_chars() { - assert!(eq_segments("a/b/c!!!", &["a", "b", "c!!!"])); - assert!(eq_segments("a /b", &["a ", "b"])); - assert!(eq_segments(" a/b", &[" a", "b"])); - assert!(eq_segments(" a/b ", &[" a", "b "])); - assert!(eq_segments(" a///b ", &[" a", "b "])); - assert!(eq_segments(" ab ", &[" ab "])); - } - - #[test] - fn segment_mismatch() { - assert!(!eq_segments("", &["a"])); - assert!(!eq_segments("a", &[])); - assert!(!eq_segments("/a/a", &["a"])); - assert!(!eq_segments("/a/b", &["b", "a"])); - assert!(!eq_segments("/a/a/b", &["a", "b"])); - assert!(!eq_segments("///a/", &[])); - } - - fn test_query(uri: &str, query: Option<&str>) { - let uri = Uri::new(uri); - assert_eq!(uri.query(), query); - } - - fn test_fragment(uri: &str, fragment: Option<&str>) { - let uri = Uri::new(uri); - assert_eq!(uri.fragment(), fragment); - } - - #[test] - fn query_does_not_exist() { - test_query("/test", None); - test_query("/a/b/c/d/e", None); - test_query("/////", None); - test_query("//a///", None); - test_query("/a/b/c#a?123", None); - test_query("/#", None); - test_query("/#?", None); - } - - #[test] - fn query_exists() { - test_query("/test?abc", Some("abc")); - test_query("/a/b/c?abc", Some("abc")); - test_query("/a/b/c/d/e/f/g/?abc#hijklmnop", Some("abc")); - test_query("?123", Some("123")); - test_query("?", Some("")); - test_query("/?", Some("")); - test_query("?#", Some("")); - test_query("/?hi", Some("hi")); - } - - #[test] - fn fragment_exists() { - test_fragment("/test#abc", Some("abc")); - test_fragment("/#abc", Some("abc")); - test_fragment("/#ab?c", Some("ab?c")); - test_fragment("/a/b/c?123#a", Some("a")); - test_fragment("/a/b/c#a?123", Some("a?123")); - test_fragment("/a/b/c?123#a?b", Some("a?b")); - test_fragment("/#a", Some("a")); - } - - #[test] - fn fragment_does_not_exist() { - test_fragment("/testabc", None); - test_fragment("/abc", None); - test_fragment("/a/b/c?123", None); - test_fragment("/a", None); - } - - #[test] - fn to_string() { - let uri_to_string = |string| Uri::new(string).to_string(); - - assert_eq!(uri_to_string("/"), "/".to_string()); - assert_eq!(uri_to_string("//"), "/".to_string()); - assert_eq!(uri_to_string("//////a/"), "/a".to_string()); - assert_eq!(uri_to_string("//ab"), "/ab".to_string()); - assert_eq!(uri_to_string("//a"), "/a".to_string()); - assert_eq!(uri_to_string("/a/b///c"), "/a/b/c".to_string()); - assert_eq!(uri_to_string("/a///b/c/d///"), "/a/b/c/d".to_string()); - assert_eq!(uri_to_string("/a/b/c#a?123"), "/a/b/c#a?123".to_string()); - } + ) } + +impl_uri_from!(Origin); +impl_uri_from!(Authority); +impl_uri_from!(Absolute); diff --git a/core/http/src/uri/uri_display.rs b/core/http/src/uri/uri_display.rs @@ -74,11 +74,12 @@ use self::priv_encode_set::PATH_ENCODE_SET; /// The implementation of `UriDisplay` for these types is identical to the /// `Display` implementation. /// -/// * **[`&RawStr`](/rocket/http/struct.RawStr.html), String, &str, Cow<str>** +/// * **[`&RawStr`](/rocket/http/struct.RawStr.html), `String`, `&str`, +/// `Cow<str>`** /// /// The string is percent encoded. /// -/// * **&T, &mut T** _where_ **T: UriDisplay** +/// * **`&T`, `&mut T`** _where_ **`T: UriDisplay`** /// /// Uses the implementation of `UriDisplay` for `T`. /// @@ -95,8 +96,6 @@ use self::priv_encode_set::PATH_ENCODE_SET; /// below, for instance, `Name`'s implementation defers to `String`'s /// implementation. To percent-encode a string, use [`Uri::percent_encode()`]. /// -/// [`Uri::percent_encode()`]: https://api.rocket.rs/rocket/http/uri/struct.Uri.html#method.percent_encode -/// /// ## Example /// /// The following snippet consists of a `Name` type that implements both diff --git a/core/lib/src/catcher.rs b/core/lib/src/catcher.rs @@ -99,7 +99,7 @@ impl Catcher { } #[inline(always)] - pub(crate) fn handle<'r>(&self, e: Error, r: &'r Request) -> response::Result<'r> { + crate fn handle<'r>(&self, e: Error, r: &'r Request) -> response::Result<'r> { (self.handler)(e, r) } @@ -109,7 +109,7 @@ impl Catcher { } #[inline(always)] - pub(crate) fn is_default(&self) -> bool { + crate fn is_default(&self) -> bool { self.is_default } } diff --git a/core/lib/src/config/config.rs b/core/lib/src/config/config.rs @@ -52,9 +52,9 @@ pub struct Config { /// How much information to log. pub log_level: LoggingLevel, /// The secret key. - pub(crate) secret_key: SecretKey, + crate secret_key: SecretKey, /// TLS configuration. - pub(crate) tls: Option<TlsConfig>, + crate tls: Option<TlsConfig>, /// Streaming read size limits. pub limits: Limits, /// Extra parameters that aren't part of Rocket's core config. @@ -221,7 +221,7 @@ impl Config { /// # Panics /// /// Panics if randomness cannot be retrieved from the OS. - pub(crate) fn default<P>(env: Environment, path: P) -> Result<Config> + crate fn default<P>(env: Environment, path: P) -> Result<Config> where P: AsRef<Path> { let config_path = path.as_ref().to_path_buf(); @@ -288,7 +288,7 @@ impl Config { /// Constructs a `BadType` error given the entry `name`, the invalid `val` /// at that entry, and the `expect`ed type name. #[inline(always)] - pub(crate) fn bad_type(&self, + crate fn bad_type(&self, name: &str, actual: &'static str, expect: &'static str) -> ConfigError { @@ -312,7 +312,7 @@ impl Config { /// * **log**: String /// * **secret_key**: String (256-bit base64) /// * **tls**: Table (`certs` (path as String), `key` (path as String)) - pub(crate) fn set_raw(&mut self, name: &str, val: &Value) -> Result<()> { + crate fn set_raw(&mut self, name: &str, val: &Value) -> Result<()> { let (id, ok) = (|val| val, |_| Ok(())); config_from_raw!(self, name, val, address => (str, set_address, id), @@ -663,7 +663,7 @@ impl Config { /// Retrieves the secret key from `self`. #[inline] - pub(crate) fn secret_key(&self) -> &Key { + crate fn secret_key(&self) -> &Key { self.secret_key.inner() } diff --git a/core/lib/src/config/custom_values.rs b/core/lib/src/config/custom_values.rs @@ -14,14 +14,14 @@ pub enum SecretKey { impl SecretKey { #[inline] - pub(crate) fn inner(&self) -> &Key { + crate fn inner(&self) -> &Key { match *self { SecretKey::Generated(ref key) | SecretKey::Provided(ref key) => key } } #[inline] - pub(crate) fn is_generated(&self) -> bool { + crate fn is_generated(&self) -> bool { match *self { SecretKey::Generated(_) => true, _ => false @@ -79,7 +79,7 @@ pub struct TlsConfig; #[derive(Debug, Clone)] pub struct Limits { // We cache this internally but don't share that fact in the API. - pub(crate) forms: u64, + crate forms: u64, extra: Vec<(String, u64)> } diff --git a/core/lib/src/config/environment.rs b/core/lib/src/config/environment.rs @@ -40,13 +40,13 @@ impl Environment { } /// Returns a string with a comma-separated list of valid environments. - pub(crate) fn valid() -> &'static str { + crate fn valid() -> &'static str { "development, staging, production" } /// Returns a list of all of the possible environments. #[inline] - pub(crate) fn all() -> [Environment; 3] { + crate fn all() -> [Environment; 3] { [Development, Staging, Production] } diff --git a/core/lib/src/config/mod.rs b/core/lib/src/config/mod.rs @@ -217,7 +217,7 @@ pub use self::environment::Environment; pub use self::config::Config; pub use self::builder::ConfigBuilder; pub use logger::LoggingLevel; -pub(crate) use self::toml_ext::LoggedValue; +crate use self::toml_ext::LoggedValue; use logger; use self::Environment::*; @@ -470,7 +470,7 @@ impl RocketConfig { /// # Panics /// /// If there is a problem, prints a nice error message and bails. -pub(crate) fn init() -> Config { +crate fn init() -> Config { let bail = |e: ConfigError| -> ! { logger::init(LoggingLevel::Debug); e.pretty_print(); diff --git a/core/lib/src/config/toml_ext.rs b/core/lib/src/config/toml_ext.rs @@ -81,7 +81,7 @@ pub fn parse_simple_toml_value(mut input: &str) -> StdResult<Value, String> { /// A simple wrapper over a `Value` reference with a custom implementation of /// `Display`. This is used to log config values at initialization. -pub(crate) struct LoggedValue<'a>(pub &'a Value); +crate struct LoggedValue<'a>(pub &'a Value); impl<'a> fmt::Display for LoggedValue<'a> { #[inline] diff --git a/core/lib/src/data/data.rs b/core/lib/src/data/data.rs @@ -87,7 +87,7 @@ impl Data { } // FIXME: This is absolutely terrible (downcasting!), thanks to Hyper. - pub(crate) fn from_hyp(mut body: HyperBodyReader) -> Result<Data, &'static str> { + crate fn from_hyp(mut body: HyperBodyReader) -> Result<Data, &'static str> { // Steal the internal, undecoded data buffer and net stream from Hyper. let (mut hyper_buf, pos, cap) = body.get_mut().take_buf(); // This is only valid because we know that hyper's `cap` represents the @@ -235,7 +235,7 @@ impl Data { // bytes `vec[pos..cap]` are buffered and unread. The remainder of the data // bytes can be read from `stream`. #[inline(always)] - pub(crate) fn new(mut stream: BodyReader) -> Data { + crate fn new(mut stream: BodyReader) -> Data { trace_!("Date::new({:?})", stream); let mut peek_buf: Vec<u8> = vec![0; PEEK_BYTES]; @@ -269,7 +269,7 @@ impl Data { /// This creates a `data` object from a local data source `data`. #[inline] - pub(crate) fn local(data: Vec<u8>) -> Data { + crate fn local(data: Vec<u8>) -> Data { let empty_stream = Cursor::new(vec![]).chain(NetStream::Empty); Data { diff --git a/core/lib/src/data/data_stream.rs b/core/lib/src/data/data_stream.rs @@ -14,7 +14,7 @@ pub type InnerStream = Chain<Cursor<Vec<u8>>, BodyReader>; /// [Data::open](/rocket/data/struct.Data.html#method.open). The stream contains /// all of the data in the body of the request. It exposes no methods directly. /// Instead, it must be used as an opaque `Read` structure. -pub struct DataStream(pub(crate) InnerStream); +pub struct DataStream(crate InnerStream); // TODO: Have a `BufRead` impl for `DataStream`. At the moment, this isn't // possible since Hyper's `HttpReader` doesn't implement `BufRead`. diff --git a/core/lib/src/error.rs b/core/lib/src/error.rs @@ -98,7 +98,7 @@ pub struct LaunchError { impl LaunchError { #[inline(always)] - pub(crate) fn new(kind: LaunchErrorKind) -> LaunchError { + crate fn new(kind: LaunchErrorKind) -> LaunchError { LaunchError { handled: AtomicBool::new(false), kind: kind } } diff --git a/core/lib/src/fairing/mod.rs b/core/lib/src/fairing/mod.rs @@ -55,7 +55,7 @@ mod fairings; mod ad_hoc; mod info_kind; -pub(crate) use self::fairings::Fairings; +crate use self::fairings::Fairings; pub use self::ad_hoc::AdHoc; pub use self::info_kind::{Info, Kind}; diff --git a/core/lib/src/lib.rs b/core/lib/src/lib.rs @@ -5,6 +5,8 @@ #![feature(fnbox)] #![feature(never_type)] #![feature(proc_macro_non_items, use_extern_macros)] +#![feature(crate_visibility_modifier)] +#![feature(try_from)] #![recursion_limit="256"] @@ -128,7 +130,6 @@ pub mod error; // Reexport of HTTP everything. pub mod http { - // FIXME: This unfortunately doesn't work! See rust-lang/rust#51252. #[doc(inline)] pub use rocket_http::*; } diff --git a/core/lib/src/local/client.rs b/core/lib/src/local/client.rs @@ -1,8 +1,10 @@ -use {Rocket, Request, Response}; +use std::cell::RefCell; +use std::borrow::Cow; + +use Rocket; use local::LocalRequest; -use http::{Method, CookieJar, uri::Uri}; +use http::{Method, CookieJar}; use error::LaunchError; -use std::cell::RefCell; /// A structure to construct requests for local dispatching. /// @@ -53,7 +55,7 @@ use std::cell::RefCell; /// [`post`]: #method.post pub struct Client { rocket: Rocket, - cookies: Option<RefCell<CookieJar>>, + crate cookies: Option<RefCell<CookieJar>>, } impl Client { @@ -150,25 +152,6 @@ impl Client { &self.rocket } - // If `self` is tracking cookies, updates the internal cookie jar with the - // changes reflected by `response`. - pub(crate) fn update_cookies(&self, response: &Response) { - if let Some(ref jar) = self.cookies { - let mut jar = jar.borrow_mut(); - let current_time = ::time::now(); - for cookie in response.cookies() { - if let Some(expires) = cookie.expires() { - if expires <= current_time { - jar.force_remove(cookie); - continue; - } - } - - jar.add(cookie.into_owned()); - } - } - } - /// Create a local `GET` request to the URI `uri`. /// /// When dispatched, the request will be served by the instance of Rocket @@ -186,7 +169,7 @@ impl Client { /// let req = client.get("/hello"); /// ``` #[inline(always)] - pub fn get<'c, 'u: 'c, U: Into<Uri<'u>>>(&'c self, uri: U) -> LocalRequest<'c> { + pub fn get<'c, 'u: 'c, U: Into<Cow<'u, str>>>(&'c self, uri: U) -> LocalRequest<'c> { self.req(Method::Get, uri) } @@ -207,7 +190,7 @@ impl Client { /// let req = client.put("/hello"); /// ``` #[inline(always)] - pub fn put<'c, 'u: 'c, U: Into<Uri<'u>>>(&'c self, uri: U) -> LocalRequest<'c> { + pub fn put<'c, 'u: 'c, U: Into<Cow<'u, str>>>(&'c self, uri: U) -> LocalRequest<'c> { self.req(Method::Put, uri) } @@ -232,7 +215,7 @@ impl Client { /// .header(ContentType::Form); /// ``` #[inline(always)] - pub fn post<'c, 'u: 'c, U: Into<Uri<'u>>>(&'c self, uri: U) -> LocalRequest<'c> { + pub fn post<'c, 'u: 'c, U: Into<Cow<'u, str>>>(&'c self, uri: U) -> LocalRequest<'c> { self.req(Method::Post, uri) } @@ -254,7 +237,7 @@ impl Client { /// ``` #[inline(always)] pub fn delete<'c, 'u: 'c, U>(&'c self, uri: U) -> LocalRequest<'c> - where U: Into<Uri<'u>> + where U: Into<Cow<'u, str>> { self.req(Method::Delete, uri) } @@ -277,7 +260,7 @@ impl Client { /// ``` #[inline(always)] pub fn options<'c, 'u: 'c, U>(&'c self, uri: U) -> LocalRequest<'c> - where U: Into<Uri<'u>> + where U: Into<Cow<'u, str>> { self.req(Method::Options, uri) } @@ -300,7 +283,7 @@ impl Client { /// ``` #[inline(always)] pub fn head<'c, 'u: 'c, U>(&'c self, uri: U) -> LocalRequest<'c> - where U: Into<Uri<'u>> + where U: Into<Cow<'u, str>> { self.req(Method::Head, uri) } @@ -323,7 +306,7 @@ impl Client { /// ``` #[inline(always)] pub fn patch<'c, 'u: 'c, U>(&'c self, uri: U) -> LocalRequest<'c> - where U: Into<Uri<'u>> + where U: Into<Cow<'u, str>> { self.req(Method::Patch, uri) } @@ -347,16 +330,8 @@ impl Client { /// ``` #[inline(always)] pub fn req<'c, 'u: 'c, U>(&'c self, method: Method, uri: U) -> LocalRequest<'c> - where U: Into<Uri<'u>> + where U: Into<Cow<'u, str>> { - let request = Request::new(&self.rocket, method, uri); - - if let Some(ref jar) = self.cookies { - for cookie in jar.borrow().iter() { - request.cookies().add_original(cookie.clone().into_owned()); - } - } - - LocalRequest::new(&self, request) + LocalRequest::new(self, method, uri.into()) } } diff --git a/core/lib/src/local/request.rs b/core/lib/src/local/request.rs @@ -2,10 +2,11 @@ use std::fmt; use std::rc::Rc; use std::net::SocketAddr; use std::ops::{Deref, DerefMut}; +use std::borrow::Cow; use {Request, Response, Data}; +use http::{Status, Method, Header, Cookie, uri::Origin, ext::IntoOwned}; use local::Client; -use http::{Header, Cookie}; /// A structure representing a local request as created by [`Client`]. /// @@ -37,26 +38,24 @@ use http::{Header, Cookie}; /// /// # Dispatching /// -/// A `LocalRequest` can be dispatched in one of three ways: +/// A `LocalRequest` can be dispatched in one of two ways: /// /// 1. [`dispatch`] /// /// This method should always be preferred. The `LocalRequest` is consumed /// and a response is returned. /// -/// 2. [`cloned_dispatch`] -/// -/// This method should be used when one `LocalRequest` will be dispatched -/// many times. This method clones the request and dispatches the clone, so -/// the request _is not_ consumed and can be reused. -/// -/// 3. [`mut_dispatch`] +/// 2. [`mut_dispatch`] /// /// This method should _only_ be used when either it is known that the /// application will not modify the request, or it is desired to see /// modifications to the request. No cloning occurs, and the request is not /// consumed. /// +/// Additionally, note that `LocalRequest` implements `Clone`. As such, if the +/// same request needs to be dispatched multiple times, the request can first be +/// cloned and then dispatched: `request.clone().dispatch()`. +/// /// [`Client`]: /rocket/local/struct.Client.html /// [`header`]: #method.header /// [`add_header`]: #method.add_header @@ -66,7 +65,6 @@ use http::{Header, Cookie}; /// [`set_body`]: #method.set_body /// [`dispatch`]: #method.dispatch /// [`mut_dispatch`]: #method.mut_dispatch -/// [`cloned_dispatch`]: #method.cloned_dispatch pub struct LocalRequest<'c> { client: &'c Client, // This pointer exists to access the `Rc<Request>` mutably inside of @@ -97,15 +95,31 @@ pub struct LocalRequest<'c> { // is converted into its owned counterpart before insertion, ensuring stable // addresses. Together, these properties guarantee the second condition. request: Rc<Request<'c>>, - data: Vec<u8> + data: Vec<u8>, + uri: Cow<'c, str>, } impl<'c> LocalRequest<'c> { #[inline(always)] - pub(crate) fn new(client: &'c Client, request: Request<'c>) -> LocalRequest<'c> { + crate fn new( + client: &'c Client, + method: Method, + uri: Cow<'c, str> + ) -> LocalRequest<'c> { + // We set a dummy string for now and check the user's URI on dispatch. + let request = Request::new(client.rocket(), method, Origin::dummy()); + + // Set up any cookies we know about. + if let Some(ref jar) = client.cookies { + for cookie in jar.borrow().iter() { + request.cookies().add_original(cookie.clone().into_owned()); + } + } + + // See the comments on the structure for what's going on here. let mut request = Rc::new(request); let ptr = Rc::get_mut(&mut request).unwrap() as *mut Request; - LocalRequest { client, ptr, request, data: vec![] } + LocalRequest { client, ptr, request, uri, data: vec![] } } /// Retrieves the inner `Request` as seen by Rocket. @@ -135,8 +149,8 @@ impl<'c> LocalRequest<'c> { fn long_lived_request<'a>(&mut self) -> &'a mut Request<'c> { // See the comments in the structure for the argument of correctness. // Additionally, the caller must ensure that the owned instance of - // `Request` itself remains valid as long as the returned reference can - // be accessed. + // `Rc<Request>` remains valid as long as the returned reference can be + // accessed. unsafe { &mut *self.ptr } } @@ -333,34 +347,8 @@ impl<'c> LocalRequest<'c> { /// ``` #[inline(always)] pub fn dispatch(mut self) -> LocalResponse<'c> { - let req = self.long_lived_request(); - let response = self.client.rocket().dispatch(req, Data::local(self.data)); - self.client.update_cookies(&response); - LocalResponse { _request: self.request, response } - } - - /// Dispatches the request, returning the response. - /// - /// This method _does not_ consume `self`. Instead, it clones `self` and - /// dispatches the clone. As such, `self` can be reused. - /// - /// # Example - /// - /// ```rust - /// use rocket::local::Client; - /// - /// let client = Client::new(rocket::ignite()).unwrap(); - /// - /// let req = client.get("/"); - /// let response_a = req.cloned_dispatch(); - /// let response_b = req.cloned_dispatch(); - /// ``` - #[inline(always)] - pub fn cloned_dispatch(&self) -> LocalResponse<'c> { - let cloned = (*self.request).clone(); - let mut req = LocalRequest::new(self.client, cloned); - req.data = self.data.clone(); - req.dispatch() + let r = self.long_lived_request(); + LocalRequest::_dispatch(self.client, r, self.request, &self.uri, self.data) } /// Dispatches the request, returning the response. @@ -373,11 +361,9 @@ impl<'c> LocalRequest<'c> { /// /// This method should _only_ be used when either it is known that /// the application will not modify the request, or it is desired to see - /// modifications to the request. Prefer to use [`dispatch`] or - /// [`cloned_dispatch`] instead + /// modifications to the request. Prefer to use [`dispatch`] instead. /// /// [`dispatch`]: #method.dispatch - /// [`cloned_dispatch`]: #method.cloned_dispatch /// /// # Example /// @@ -392,11 +378,53 @@ impl<'c> LocalRequest<'c> { /// ``` #[inline(always)] pub fn mut_dispatch(&mut self) -> LocalResponse<'c> { - let data = ::std::mem::replace(&mut self.data, vec![]); let req = self.long_lived_request(); - let response = self.client.rocket().dispatch(req, Data::local(data)); - self.client.update_cookies(&response); - LocalResponse { _request: self.request.clone(), response } + let data = ::std::mem::replace(&mut self.data, vec![]); + let rc_req = self.request.clone(); + LocalRequest::_dispatch(self.client, req, rc_req, &self.uri, data) + } + + // Performs the actual dispatch. + fn _dispatch( + client: &'c Client, + request: &'c mut Request<'c>, + owned_request: Rc<Request<'c>>, + uri: &str, + data: Vec<u8> + ) -> LocalResponse<'c> { + // First, validate the URI, returning an error response (generated from + // an error catcher) immediately if it's invalid. + if let Ok(uri) = Origin::parse(uri) { + request.set_uri(uri.into_owned()); + } else { + let res = client.rocket().handle_error(Status::BadRequest, request); + return LocalResponse { _request: owned_request, response: res }; + } + + // Actually dispatch the request. + let response = client.rocket().dispatch(request, Data::local(data)); + + // If the client is tracking cookies, updates the internal cookie jar + // with the changes reflected by `response`. + if let Some(ref jar) = client.cookies { + let mut jar = jar.borrow_mut(); + let current_time = ::time::now(); + for cookie in response.cookies() { + if let Some(expires) = cookie.expires() { + if expires <= current_time { + jar.force_remove(cookie); + continue; + } + } + + jar.add(cookie.into_owned()); + } + } + + LocalResponse { + _request: owned_request, + response: response + } } } @@ -442,7 +470,19 @@ impl<'c> fmt::Debug for LocalResponse<'c> { } } -#[cfg(test)] +impl<'c> Clone for LocalRequest<'c> { + fn clone(&self) -> LocalRequest<'c> { + LocalRequest { + client: self.client, + ptr: self.ptr, + request: self.request.clone(), + data: self.data.clone(), + uri: self.uri.clone() + } + } +} + +// #[cfg(test)] mod tests { // Someday... @@ -474,8 +514,10 @@ mod tests { // is_send::<::local::LocalResponse>(); // } + // This checks that a response can't outlive the `Client`. + // #[compile_fail] // fn test() { - // use local::Client; + // use {Rocket, local::Client}; // let rocket = Rocket::ignite(); // let res = { @@ -488,8 +530,10 @@ mod tests { // // let res2 = client.get("/").dispatch(); // } + // This checks that a response can't outlive the `Client`. + // #[compile_fail] // fn test() { - // use local::Client; + // use {Rocket, local::Client}; // let rocket = Rocket::ignite(); // let res = { @@ -502,8 +546,11 @@ mod tests { // // let res2 = client.get("/").dispatch(); // } + // This checks that a response can't outlive the `Client`, in this case, by + // moving `client` while it is borrowed. + // #[compile_fail] // fn test() { - // use local::Client; + // use {Rocket, local::Client}; // let rocket = Rocket::ignite(); // let client = Client::new(rocket).unwrap(); @@ -511,13 +558,15 @@ mod tests { // let res = { // let x = client.get("/").dispatch(); // let y = client.get("/").dispatch(); + // (x, y) // }; // let x = client; // } + // #[compile_fail] // fn test() { - // use local::Client; + // use {Rocket, local::Client}; // let rocket1 = Rocket::ignite(); // let rocket2 = Rocket::ignite(); @@ -527,7 +576,7 @@ mod tests { // let res = { // let mut res1 = client1.get("/"); - // res1.set_client(&client2); + // res1.client = &client2; // res1 // }; diff --git a/core/lib/src/logger.rs b/core/lib/src/logger.rs @@ -142,7 +142,7 @@ impl log::Log for RocketLogger { } } -pub(crate) fn try_init(level: LoggingLevel, verbose: bool) -> bool { +crate fn try_init(level: LoggingLevel, verbose: bool) -> bool { if level == LoggingLevel::Off { return false; } @@ -192,13 +192,13 @@ fn usize_to_filter(num: usize) -> log::LevelFilter { } } -pub(crate) fn push_max_level(level: LoggingLevel) { +crate fn push_max_level(level: LoggingLevel) { LAST_LOG_FILTER.store(filter_to_usize(log::max_level()), Ordering::Release); PUSHED.store(true, Ordering::Release); log::set_max_level(level.to_level_filter()); } -pub(crate) fn pop_max_level() { +crate fn pop_max_level() { if PUSHED.load(Ordering::Acquire) { log::set_max_level(usize_to_filter(LAST_LOG_FILTER.load(Ordering::Acquire))); } diff --git a/core/lib/src/request/form/form.rs b/core/lib/src/request/form/form.rs @@ -239,7 +239,7 @@ impl<'f, T: FromForm<'f> + 'f> Form<'f, T> { // caller via `get()` and constrain everything to that lifetime. This is, in // reality a little coarser than necessary, but the user can simply move the // call to right after the creation of a Form object to get the same effect. - pub(crate) fn new(string: String, strict: bool) -> FormResult<Self, T::Error> { + crate fn new(string: String, strict: bool) -> FormResult<Self, T::Error> { let long_lived_string: &'f str = unsafe { ::std::mem::transmute(string.as_str()) }; diff --git a/core/lib/src/request/from_request.rs b/core/lib/src/request/from_request.rs @@ -6,8 +6,7 @@ use request::Request; use outcome::{self, IntoOutcome}; use outcome::Outcome::*; -use http::{Status, ContentType, Accept, Method, Cookies}; -use http::uri::Uri; +use http::{Status, ContentType, Accept, Method, Cookies, uri::Origin}; /// Type alias for the `Outcome` of a `FromRequest` conversion. pub type Outcome<S, E> = outcome::Outcome<S, (Status, E), ()>; @@ -102,10 +101,10 @@ impl<S, E> IntoOutcome<S, (Status, E), ()> for Result<S, E> { /// /// _This implementation always returns successfully._ /// -/// * **&URI** +/// * **&Origin** /// -/// Extracts the [`Uri`](/rocket/http/uri/struct.Uri.html) from the incoming -/// request. +/// Extracts the [`Origin`](/rocket/http/uri/struct.Origin.html) URI from +/// the incoming request. /// /// _This implementation always returns successfully._ /// @@ -229,7 +228,7 @@ impl<'a, 'r> FromRequest<'a, 'r> for Method { } } -impl<'a, 'r> FromRequest<'a, 'r> for &'a Uri<'a> { +impl<'a, 'r> FromRequest<'a, 'r> for &'a Origin<'a> { type Error = (); fn from_request(request: &'a Request<'r>) -> Outcome<Self, Self::Error> { diff --git a/core/lib/src/request/mod.rs b/core/lib/src/request/mod.rs @@ -11,7 +11,7 @@ mod tests; pub use self::request::Request; pub use self::from_request::{FromRequest, Outcome}; -pub use self::param::{FromParam, FromSegments}; +pub use self::param::{FromParam, FromSegments, SegmentError}; pub use self::form::{Form, LenientForm, FromForm, FromFormValue, FormItems}; pub use self::state::State; diff --git a/core/lib/src/request/param.rs b/core/lib/src/request/param.rs @@ -1,10 +1,9 @@ -use std::str::FromStr; +use std::str::{FromStr, Utf8Error}; use std::path::PathBuf; use std::fmt::Debug; use std::borrow::Cow; -use http::uri::{Uri, Segments, SegmentError}; -use http::RawStr; +use http::{RawStr, uri::{Uri, Segments}}; /// Trait to convert a dynamic path segment string to a concrete value. /// @@ -309,6 +308,20 @@ impl<'a> FromSegments<'a> for Segments<'a> { } } +/// Errors which can occur when attempting to interpret a segment string as a +/// valid path segment. +#[derive(Debug, PartialEq, Eq, Clone)] +pub enum SegmentError { + /// The segment contained invalid UTF8 characters when percent decoded. + Utf8(Utf8Error), + /// The segment started with the wrapped invalid character. + BadStart(char), + /// The segment contained the wrapped invalid character. + BadChar(char), + /// The segment ended with the wrapped invalid character. + BadEnd(char), +} + /// Creates a `PathBuf` from a `Segments` iterator. The returned `PathBuf` is /// percent-decoded. If a segment is equal to "..", the previous segment (if /// any) is skipped. diff --git a/core/lib/src/request/request.rs b/core/lib/src/request/request.rs @@ -12,7 +12,7 @@ use super::{FromParam, FromSegments, FromRequest, Outcome}; use rocket::Rocket; use router::Route; use config::{Config, Limits}; -use http::uri::{Uri, Segments}; +use http::uri::{Origin, Segments}; use error::Error; use http::{Method, Header, HeaderMap, Cookies, CookieJar}; use http::{RawStr, ContentType, Accept, MediaType}; @@ -40,25 +40,23 @@ struct RequestState<'r> { #[derive(Clone)] pub struct Request<'r> { method: Cell<Method>, - uri: Uri<'r>, + uri: Origin<'r>, headers: HeaderMap<'r>, remote: Option<SocketAddr>, state: RequestState<'r>, } impl<'r> Request<'r> { - /// Create a new `Request` with the given `method` and `uri`. The `uri` - /// parameter can be of any type that implements `Into<Uri>` including - /// `&str` and `String`; it must be a valid absolute URI. + /// Create a new `Request` with the given `method` and `uri`. #[inline(always)] - pub(crate) fn new<'s: 'r, U: Into<Uri<'s>>>( + crate fn new<'s: 'r>( rocket: &'r Rocket, method: Method, - uri: U + uri: Origin<'s> ) -> Request<'r> { Request { method: Cell::new(method), - uri: uri.into(), + uri: uri, headers: HeaderMap::new(), remote: None, state: RequestState { @@ -74,9 +72,11 @@ impl<'r> Request<'r> { } } + // Only used by doc-tests! #[doc(hidden)] pub fn example<F: Fn(&mut Request)>(method: Method, uri: &str, f: F) { let rocket = Rocket::custom(Config::development().unwrap()); + let uri = Origin::parse(uri).expect("invalid URI in example"); let mut request = Request::new(&rocket, method, uri); f(&mut request); } @@ -150,7 +150,7 @@ impl<'r> Request<'r> { self._set_method(method); } - /// Borrow the URI from `self`, which is guaranteed to be an absolute URI. + /// Borrow the `Origin` URI from `self`. /// /// # Example /// @@ -158,31 +158,34 @@ impl<'r> Request<'r> { /// # use rocket::Request; /// # use rocket::http::Method; /// # Request::example(Method::Get, "/uri", |request| { - /// assert_eq!(request.uri().as_str(), "/uri"); + /// assert_eq!(request.uri().path(), "/uri"); /// # }); /// ``` #[inline(always)] - pub fn uri(&self) -> &Uri { + pub fn uri(&self) -> &Origin { &self.uri } - /// Set the URI in `self`. The `uri` parameter can be of any type that - /// implements `Into<Uri>` including `&str` and `String`; it _must_ be a - /// valid, absolute URI. + /// Set the URI in `self` to `uri`. /// /// # Example /// /// ```rust + /// use rocket::http::uri::Origin; + /// /// # use rocket::Request; /// # use rocket::http::Method; /// # Request::example(Method::Get, "/uri", |mut request| { - /// request.set_uri("/hello/Sergio?type=greeting"); - /// assert_eq!(request.uri().as_str(), "/hello/Sergio?type=greeting"); + /// let uri = Origin::parse("/hello/Sergio?type=greeting").unwrap(); + /// + /// request.set_uri(uri); + /// assert_eq!(request.uri().path(), "/hello/Sergio"); + /// assert_eq!(request.uri().query(), Some("type=greeting")); /// # }); /// ``` #[inline(always)] - pub fn set_uri<'u: 'r, U: Into<Uri<'u>>>(&mut self, uri: U) { - self.uri = uri.into(); + pub fn set_uri<'u: 'r>(&mut self, uri: Origin<'u>) { + self.uri = uri; *self.state.params.borrow_mut() = Vec::new(); } @@ -647,36 +650,37 @@ impl<'r> Request<'r> { /// use may result in out of bounds indexing. /// TODO: Figure out the mount path from here. #[inline] - pub(crate) fn set_route(&self, route: &'r Route) { + crate fn set_route(&self, route: &'r Route) { self.state.route.set(Some(route)); *self.state.params.borrow_mut() = route.get_param_indexes(self.uri()); } /// Set the method of `self`, even when `self` is a shared reference. #[inline(always)] - pub(crate) fn _set_method(&self, method: Method) { + crate fn _set_method(&self, method: Method) { self.method.set(method); } /// Replace all of the cookies in `self` with those in `jar`. #[inline] - pub(crate) fn set_cookies(&mut self, jar: CookieJar) { + crate fn set_cookies(&mut self, jar: CookieJar) { self.state.cookies = RefCell::new(jar); } /// Get the managed state T, if it exists. For internal use only! #[inline(always)] - pub(crate) fn get_state<T: Send + Sync + 'static>(&self) -> Option<&'r T> { + crate fn get_state<T: Send + Sync + 'static>(&self) -> Option<&'r T> { self.state.managed.try_get() } /// Convert from Hyper types into a Rocket Request. - pub(crate) fn from_hyp(rocket: &'r Rocket, - h_method: hyper::Method, - h_headers: hyper::header::Headers, - h_uri: hyper::RequestUri, - h_addr: SocketAddr, - ) -> Result<Request<'r>, String> { + crate fn from_hyp( + rocket: &'r Rocket, + h_method: hyper::Method, + h_headers: hyper::header::Headers, + h_uri: hyper::RequestUri, + h_addr: SocketAddr, + ) -> Result<Request<'r>, String> { // Get a copy of the URI for later use. let uri = match h_uri { hyper::RequestUri::AbsolutePath(s) => s, @@ -689,6 +693,9 @@ impl<'r> Request<'r> { None => return Err(format!("Invalid method: {}", h_method)) }; + // We need to re-parse the URI since we don't trust Hyper... :( + let uri = Origin::parse_owned(uri).map_err(|e| e.to_string())?; + // Construct the request object. let mut request = Request::new(rocket, method, uri); request.set_remote(h_addr); diff --git a/core/lib/src/response/mod.rs b/core/lib/src/response/mod.rs @@ -26,7 +26,7 @@ mod stream; mod response; mod failure; -pub(crate) mod flash; +crate mod flash; pub mod content; pub mod status; diff --git a/core/lib/src/response/redirect.rs b/core/lib/src/response/redirect.rs @@ -1,3 +1,5 @@ +use std::convert::TryInto; + use request::Request; use response::{Response, Responder}; use http::uri::Uri; @@ -7,7 +9,7 @@ use http::Status; /// /// This type simplifies returning a redirect response to the client. #[derive(Debug)] -pub struct Redirect(Status, Uri<'static>); +pub struct Redirect(Status, Option<Uri<'static>>); impl Redirect { /// Construct a temporary "see other" (303) redirect response. This is the @@ -23,82 +25,82 @@ impl Redirect { /// # #[allow(unused_variables)] /// let redirect = Redirect::to("/other_url"); /// ``` - pub fn to<U: Into<Uri<'static>>>(uri: U) -> Redirect { - Redirect(Status::SeeOther, uri.into()) + pub fn to<U: TryInto<Uri<'static>>>(uri: U) -> Redirect { + Redirect(Status::SeeOther, uri.try_into().ok()) } - /// Construct a "temporary" (307) redirect response. This response instructs - /// the client to reissue the current request to a different URL, - /// maintaining the contents of the request identically. This means that, - /// for example, a `POST` request will be resent, contents included, to the - /// requested URL. - /// - /// # Examples - /// - /// ```rust - /// use rocket::response::Redirect; - /// - /// # #[allow(unused_variables)] - /// let redirect = Redirect::temporary("/other_url"); - /// ``` - pub fn temporary<U: Into<Uri<'static>>>(uri: U) -> Redirect { - Redirect(Status::TemporaryRedirect, uri.into()) - } + /// Construct a "temporary" (307) redirect response. This response instructs + /// the client to reissue the current request to a different URL, + /// maintaining the contents of the request identically. This means that, + /// for example, a `POST` request will be resent, contents included, to the + /// requested URL. + /// + /// # Examples + /// + /// ```rust + /// use rocket::response::Redirect; + /// + /// # #[allow(unused_variables)] + /// let redirect = Redirect::temporary("/other_url"); + /// ``` + pub fn temporary<U: TryInto<Uri<'static>>>(uri: U) -> Redirect { + Redirect(Status::TemporaryRedirect, uri.try_into().ok()) + } - /// Construct a "permanent" (308) redirect response. This redirect must only - /// be used for permanent redirects as it is cached by clients. This - /// response instructs the client to reissue requests for the current URL to - /// a different URL, now and in the future, maintaining the contents of the - /// request identically. This means that, for example, a `POST` request will - /// be resent, contents included, to the requested URL. - /// - /// # Examples - /// - /// ```rust - /// use rocket::response::Redirect; - /// - /// # #[allow(unused_variables)] - /// let redirect = Redirect::permanent("/other_url"); - /// ``` - pub fn permanent<U: Into<Uri<'static>>>(uri: U) -> Redirect { - Redirect(Status::PermanentRedirect, uri.into()) - } + /// Construct a "permanent" (308) redirect response. This redirect must only + /// be used for permanent redirects as it is cached by clients. This + /// response instructs the client to reissue requests for the current URL to + /// a different URL, now and in the future, maintaining the contents of the + /// request identically. This means that, for example, a `POST` request will + /// be resent, contents included, to the requested URL. + /// + /// # Examples + /// + /// ```rust + /// use rocket::response::Redirect; + /// + /// # #[allow(unused_variables)] + /// let redirect = Redirect::permanent("/other_url"); + /// ``` + pub fn permanent<U: TryInto<Uri<'static>>>(uri: U) -> Redirect { + Redirect(Status::PermanentRedirect, uri.try_into().ok()) + } - /// Construct a temporary "found" (302) redirect response. This response - /// instructs the client to reissue the current request to a different URL, - /// ideally maintaining the contents of the request identically. - /// Unfortunately, different clients may respond differently to this type of - /// redirect, so `303` or `307` redirects, which disambiguate, are - /// preferred. - /// - /// # Examples - /// - /// ```rust - /// use rocket::response::Redirect; - /// - /// # #[allow(unused_variables)] - /// let redirect = Redirect::found("/other_url"); - /// ``` - pub fn found<U: Into<Uri<'static>>>(uri: U) -> Redirect { - Redirect(Status::Found, uri.into()) - } + /// Construct a temporary "found" (302) redirect response. This response + /// instructs the client to reissue the current request to a different URL, + /// ideally maintaining the contents of the request identically. + /// Unfortunately, different clients may respond differently to this type of + /// redirect, so `303` or `307` redirects, which disambiguate, are + /// preferred. + /// + /// # Examples + /// + /// ```rust + /// use rocket::response::Redirect; + /// + /// # #[allow(unused_variables)] + /// let redirect = Redirect::found("/other_url"); + /// ``` + pub fn found<U: TryInto<Uri<'static>>>(uri: U) -> Redirect { + Redirect(Status::Found, uri.try_into().ok()) + } - /// Construct a permanent "moved" (301) redirect response. This response - /// should only be used for permanent redirects as it can be cached by - /// browsers. Because different clients may respond differently to this type - /// of redirect, a `308` redirect, which disambiguates, is preferred. - /// - /// # Examples - /// - /// ```rust - /// use rocket::response::Redirect; - /// - /// # #[allow(unused_variables)] - /// let redirect = Redirect::moved("/other_url"); - /// ``` - pub fn moved<U: Into<Uri<'static>>>(uri: U) -> Redirect { - Redirect(Status::MovedPermanently, uri.into()) - } + /// Construct a permanent "moved" (301) redirect response. This response + /// should only be used for permanent redirects as it can be cached by + /// browsers. Because different clients may respond differently to this type + /// of redirect, a `308` redirect, which disambiguates, is preferred. + /// + /// # Examples + /// + /// ```rust + /// use rocket::response::Redirect; + /// + /// # #[allow(unused_variables)] + /// let redirect = Redirect::moved("/other_url"); + /// ``` + pub fn moved<U: TryInto<Uri<'static>>>(uri: U) -> Redirect { + Redirect(Status::MovedPermanently, uri.try_into().ok()) + } } /// Constructs a response with the appropriate status code and the given URL in @@ -106,9 +108,14 @@ impl Redirect { /// responder does not fail. impl Responder<'static> for Redirect { fn respond_to(self, _: &Request) -> Result<Response<'static>, Status> { - Response::build() - .status(self.0) - .raw_header("Location", self.1.to_string()) - .ok() + if let Some(uri) = self.1 { + Response::build() + .status(self.0) + .raw_header("Location", uri.to_string()) + .ok() + } else { + error!("Invalid URI used for redirect."); + Err(Status::InternalServerError) + } } } diff --git a/core/lib/src/response/response.rs b/core/lib/src/response/response.rs @@ -976,7 +976,7 @@ impl<'r> Response<'r> { // Makes the `Read`er in the body empty but leaves the size of the body if // it exists. Only meant to be used to handle HEAD requests automatically. #[inline(always)] - pub(crate) fn strip_body(&mut self) { + crate fn strip_body(&mut self) { if let Some(body) = self.take_body() { self.body = match body { Body::Sized(_, n) => Some(Body::Sized(Box::new(io::empty()), n)), diff --git a/core/lib/src/rocket.rs b/core/lib/src/rocket.rs @@ -24,16 +24,16 @@ use fairing::{Fairing, Fairings}; use http::{Method, Status, Header}; use http::hyper::{self, header}; -use http::uri::Uri; +use http::uri::Origin; /// The main `Rocket` type: used to mount routes and catchers and launch the /// application. pub struct Rocket { - pub(crate) config: Config, + crate config: Config, router: Router, default_catchers: HashMap<u16, Catcher>, catchers: HashMap<u16, Catcher>, - pub(crate) state: Container, + crate state: Container, fairings: Fairings, } @@ -58,7 +58,11 @@ impl hyper::Handler for Rocket { Ok(req) => req, Err(e) => { error!("Bad incoming request: {}", e); - let dummy = Request::new(self, Method::Get, Uri::new("<unknown>")); + // TODO: We don't have a request to pass in, so we just + // fabricate one. This is weird. We should let the user know + // that we failed to parse a request (by invoking some special + // handler) instead of doing this. + let dummy = Request::new(self, Method::Get, Origin::dummy()); let r = self.handle_error(Status::BadRequest, &dummy); return self.issue_response(r, res); } @@ -193,7 +197,7 @@ impl Rocket { } #[inline] - pub(crate) fn dispatch<'s, 'r>( + crate fn dispatch<'s, 'r>( &'s self, request: &'r mut Request<'s>, data: Data @@ -269,7 +273,7 @@ impl Rocket { // (ensuring `handler` takes an immutable borrow), any caller to `route` // should be able to supply an `&mut` and retain an `&` after the call. #[inline] - pub(crate) fn route<'s, 'r>( + crate fn route<'s, 'r>( &'s self, request: &'r Request<'s>, mut data: Data, @@ -302,7 +306,11 @@ impl Rocket { // catcher is called. If the catcher fails to return a good response, the // 500 catcher is executed. If there is no registered catcher for `status`, // the default catcher is used. - fn handle_error<'r>(&self, status: Status, req: &'r Request) -> Response<'r> { + crate fn handle_error<'r>( + &self, + status: Status, + req: &'r Request + ) -> Response<'r> { warn_!("Responding with {} catcher.", Paint::red(&status)); // Try to get the active catcher but fallback to user's 500 catcher. @@ -434,8 +442,12 @@ impl Rocket { /// /// # Panics /// - /// The `base` mount point must be a static path. That is, the mount point - /// must _not_ contain dynamic path parameters: `<param>`. + /// Panics if the `base` mount point is not a valid static path: a valid + /// origin URI without dynamic parameters. + /// + /// Panics if any route's URI is not a valid origin URI. This kind of panic + /// is guaranteed not to occur if the routes were generated using Rocket's + /// code generation. /// /// # Examples /// @@ -486,17 +498,36 @@ impl Rocket { Paint::purple("Mounting"), Paint::blue(base)); - if base.contains('<') || !base.starts_with('/') { - error_!("Bad mount point: '{}'.", base); - error_!("Mount points must be static, absolute URIs: `/example`"); - panic!("Bad mount point.") + if base.contains('<') || base.contains('>') { + error_!("Invalid mount point: {}", base); + panic!("Mount points cannot contain dynamic parameters"); } for mut route in routes { - let uri = Uri::new(format!("{}/{}", base, route.uri)); + let base_uri = Origin::parse(base) + .unwrap_or_else(|e| { + error_!("Invalid origin URI used as mount point: {}", base); + panic!("Error: {}", e); + }); + + if base_uri.query().is_some() { + error_!("Mount point cannot contain a query string: {}", base_uri); + panic!("Invalid mount point."); + } + + let complete_uri = format!("{}/{}", base_uri, route.uri); + let uri = Origin::parse_route(&complete_uri) + .unwrap_or_else(|e| { + error_!("Invalid route URI: {}", base); + panic!("Error: {}", e) + }); + + if !uri.is_normalized() { + warn_!("Abnormal URI '{}' will be automatically normalized.", uri); + } - route.set_base(base); - route.set_uri(uri.to_string()); + route.set_base(base_uri); + route.set_uri(uri.to_normalized()); info_!("{}", route); self.router.add(route); @@ -633,7 +664,7 @@ impl Rocket { self } - pub(crate) fn prelaunch_check(&self) -> Option<LaunchError> { + crate fn prelaunch_check(&self) -> Option<LaunchError> { let collisions = self.router.collisions(); if !collisions.is_empty() { let owned = collisions.iter().map(|&(a, b)| (a.clone(), b.clone())); diff --git a/core/lib/src/router/collider.rs b/core/lib/src/router/collider.rs @@ -1,6 +1,6 @@ use super::Route; -use http::uri::Uri; +use http::uri::Origin; use http::MediaType; use request::Request; @@ -38,8 +38,8 @@ impl<'a> Collider<str> for &'a str { } // This _only_ checks the `path` component of the URI. -impl<'a, 'b> Collider<Uri<'b>> for Uri<'a> { - fn collides_with(&self, other: &Uri<'b>) -> bool { +impl<'a, 'b> Collider<Origin<'b>> for Origin<'a> { + fn collides_with(&self, other: &Origin<'b>) -> bool { for (seg_a, seg_b) in self.segments().zip(other.segments()) { if seg_a.ends_with("..>") || seg_b.ends_with("..>") { return true; @@ -123,7 +123,7 @@ mod tests { use handler::Outcome; use router::route::Route; use http::{Method, MediaType, ContentType, Accept}; - use http::uri::Uri; + use http::uri::Origin; use http::Method::*; type SimpleRoute = (Method, &'static str); @@ -139,16 +139,18 @@ mod tests { fn unranked_collide(a: &'static str, b: &'static str) -> bool { let route_a = Route::ranked(0, Get, a.to_string(), dummy_handler); - route_a.collides_with(&Route::ranked(0, Get, b.to_string(), dummy_handler)) + let route_b = Route::ranked(0, Get, b.to_string(), dummy_handler); + eprintln!("Checking {} against {}.", route_a, route_b); + route_a.collides_with(&route_b) } fn s_s_collide(a: &'static str, b: &'static str) -> bool { - Uri::new(a).collides_with(&Uri::new(b)) + Origin::parse_route(a).unwrap() + .collides_with(&Origin::parse_route(b).unwrap()) } #[test] fn simple_collisions() { - assert!(unranked_collide("a", "a")); assert!(unranked_collide("/a", "/a")); assert!(unranked_collide("/hello", "/hello")); assert!(unranked_collide("/hello", "/hello/")); @@ -254,6 +256,13 @@ mod tests { } #[test] + fn query_dependent_non_collisions() { + assert!(!m_collide((Get, "/"), (Get, "/?a"))); + assert!(!m_collide((Get, "/"), (Get, "/?<a>"))); + assert!(!m_collide((Get, "/a/<b>"), (Get, "/a/<b>?d"))); + } + + #[test] fn test_str_non_collisions() { assert!(!s_s_collide("/a", "/b")); assert!(!s_s_collide("/a/b", "/a")); @@ -365,7 +374,7 @@ mod tests { where S1: Into<Option<&'static str>>, S2: Into<Option<&'static str>> { let rocket = Rocket::custom(Config::development().unwrap()); - let mut req = Request::new(&rocket, m, "/"); + let mut req = Request::new(&rocket, m, Origin::dummy()); if let Some(mt_str) = mt1.into() { if m.supports_payload() { req.replace_header(mt_str.parse::<ContentType>().unwrap()); @@ -423,7 +432,7 @@ mod tests { fn req_route_path_collide(a: &'static str, b: &'static str) -> bool { let rocket = Rocket::custom(Config::development().unwrap()); - let req = Request::new(&rocket, Get, a.to_string()); + let req = Request::new(&rocket, Get, Origin::parse(a).expect("valid URI")); let route = Route::ranked(0, Get, b.to_string(), dummy_handler); route.collides_with(&req) } diff --git a/core/lib/src/router/mod.rs b/core/lib/src/router/mod.rs @@ -57,7 +57,6 @@ impl Router { result } - // This is slow. Don't expose this publicly; only for tests. #[cfg(test)] fn has_collisions(&self) -> bool { @@ -78,7 +77,7 @@ mod test { use config::Config; use http::Method; use http::Method::*; - use http::uri::Uri; + use http::uri::Origin; use request::Request; use data::Data; use handler::Outcome; @@ -139,13 +138,29 @@ mod test { assert!(unranked_route_collisions(&["/<a>/b", "/a/<a..>"])); assert!(unranked_route_collisions(&["/a/<b>", "/a/<a..>"])); assert!(unranked_route_collisions(&["/a/b/<c>", "/a/<a..>"])); - assert!(unranked_route_collisions(&["<a..>", "/a/<a..>"])); + assert!(unranked_route_collisions(&["/<a..>", "/a/<a..>"])); assert!(unranked_route_collisions(&["/a/<a..>", "/a/<a..>"])); assert!(unranked_route_collisions(&["/a/b/<a..>", "/a/<a..>"])); assert!(unranked_route_collisions(&["/a/b/c/d", "/a/<a..>"])); } #[test] + fn test_collisions_normalize() { + assert!(unranked_route_collisions(&["/hello/", "/hello"])); + assert!(unranked_route_collisions(&["//hello/", "/hello"])); + assert!(unranked_route_collisions(&["//hello/", "/hello//"])); + assert!(unranked_route_collisions(&["/<a>", "/hello//"])); + assert!(unranked_route_collisions(&["/<a>", "/hello///"])); + assert!(unranked_route_collisions(&["/hello///bob", "/hello/<b>"])); + assert!(unranked_route_collisions(&["/<a..>//", "/a//<a..>"])); + assert!(unranked_route_collisions(&["/a/<a..>//", "/a/<a..>"])); + assert!(unranked_route_collisions(&["/a/<a..>//", "/a/b//c//d/"])); + assert!(unranked_route_collisions(&["/a/<a..>/", "/a/bd/e/"])); + assert!(unranked_route_collisions(&["/a/<a..>//", "/a/b//c//d/e/"])); + assert!(unranked_route_collisions(&["/a//<a..>//", "/a/b//c//d/e/"])); + } + + #[test] fn test_no_collisions() { assert!(!unranked_route_collisions(&["/<a>", "/a/<a..>"])); assert!(!unranked_route_collisions(&["/a/b", "/a/b/c"])); @@ -166,7 +181,7 @@ mod test { fn route<'a>(router: &'a Router, method: Method, uri: &str) -> Option<&'a Route> { let rocket = Rocket::custom(Config::development().unwrap()); - let request = Request::new(&rocket, method, Uri::new(uri)); + let request = Request::new(&rocket, method, Origin::parse(uri).unwrap()); let matches = router.route(&request); if matches.len() > 0 { Some(matches[0]) @@ -177,7 +192,7 @@ mod test { fn matches<'a>(router: &'a Router, method: Method, uri: &str) -> Vec<&'a Route> { let rocket = Rocket::custom(Config::development().unwrap()); - let request = Request::new(&rocket, method, Uri::new(uri)); + let request = Request::new(&rocket, method, Origin::parse(uri).unwrap()); router.route(&request) } @@ -245,8 +260,8 @@ mod test { macro_rules! assert_ranked_routes { ($routes:expr, $to:expr, $want:expr) => ({ let router = router_with_routes($routes); - let route_path = route(&router, Get, $to).unwrap().uri.as_str(); - assert_eq!(route_path as &str, $want as &str); + let route_path = route(&router, Get, $to).unwrap().uri.to_string(); + assert_eq!(route_path, $want.to_string()); }) } @@ -275,21 +290,21 @@ mod test { #[test] fn test_no_manual_ranked_collisions() { - assert!(!ranked_collisions(&[(1, "a/<b>"), (2, "a/<b>")])); - assert!(!ranked_collisions(&[(0, "a/<b>"), (2, "a/<b>")])); - assert!(!ranked_collisions(&[(5, "a/<b>"), (2, "a/<b>")])); - assert!(!ranked_collisions(&[(1, "a/<b>"), (1, "b/<b>")])); - assert!(!ranked_collisions(&[(1, "a/<b..>"), (2, "a/<b..>")])); - assert!(!ranked_collisions(&[(0, "a/<b..>"), (2, "a/<b..>")])); - assert!(!ranked_collisions(&[(5, "a/<b..>"), (2, "a/<b..>")])); - assert!(!ranked_collisions(&[(1, "<a..>"), (2, "<a..>")])); + assert!(!ranked_collisions(&[(1, "/a/<b>"), (2, "/a/<b>")])); + assert!(!ranked_collisions(&[(0, "/a/<b>"), (2, "/a/<b>")])); + assert!(!ranked_collisions(&[(5, "/a/<b>"), (2, "/a/<b>")])); + assert!(!ranked_collisions(&[(1, "/a/<b>"), (1, "/b/<b>")])); + assert!(!ranked_collisions(&[(1, "/a/<b..>"), (2, "/a/<b..>")])); + assert!(!ranked_collisions(&[(0, "/a/<b..>"), (2, "/a/<b..>")])); + assert!(!ranked_collisions(&[(5, "/a/<b..>"), (2, "/a/<b..>")])); + assert!(!ranked_collisions(&[(1, "/<a..>"), (2, "/<a..>")])); } #[test] fn test_ranked_collisions() { - assert!(ranked_collisions(&[(2, "a/<b..>"), (2, "a/<b..>")])); - assert!(ranked_collisions(&[(2, "a/c/<b..>"), (2, "a/<b..>")])); - assert!(ranked_collisions(&[(2, "<b..>"), (2, "a/<b..>")])); + assert!(ranked_collisions(&[(2, "/a/<b..>"), (2, "/a/<b..>")])); + assert!(ranked_collisions(&[(2, "/a/c/<b..>"), (2, "/a/<b..>")])); + assert!(ranked_collisions(&[(2, "/<b..>"), (2, "/a/<b..>")])); } macro_rules! assert_ranked_routing { @@ -300,7 +315,7 @@ mod test { assert!(routed_to.len() == expected.len()); for (got, expected) in routed_to.iter().zip(expected.iter()) { assert_eq!(got.rank, expected.0); - assert_eq!(got.uri.as_str(), expected.1); + assert_eq!(got.uri.to_string(), expected.1.to_string()); } }) } @@ -308,33 +323,33 @@ mod test { #[test] fn test_ranked_routing() { assert_ranked_routing!( - to: "a/b", - with: [(1, "a/<b>"), (2, "a/<b>")], - expect: (1, "a/<b>"), (2, "a/<b>") + to: "/a/b", + with: [(1, "/a/<b>"), (2, "/a/<b>")], + expect: (1, "/a/<b>"), (2, "/a/<b>") ); assert_ranked_routing!( - to: "b/b", - with: [(1, "a/<b>"), (2, "b/<b>"), (3, "b/b")], - expect: (2, "b/<b>"), (3, "b/b") + to: "/b/b", + with: [(1, "/a/<b>"), (2, "/b/<b>"), (3, "/b/b")], + expect: (2, "/b/<b>"), (3, "/b/b") ); assert_ranked_routing!( - to: "b/b", - with: [(2, "b/<b>"), (1, "a/<b>"), (3, "b/b")], - expect: (2, "b/<b>"), (3, "b/b") + to: "/b/b", + with: [(2, "/b/<b>"), (1, "/a/<b>"), (3, "/b/b")], + expect: (2, "/b/<b>"), (3, "/b/b") ); assert_ranked_routing!( - to: "b/b", - with: [(3, "b/b"), (2, "b/<b>"), (1, "a/<b>")], - expect: (2, "b/<b>"), (3, "b/b") + to: "/b/b", + with: [(3, "/b/b"), (2, "/b/<b>"), (1, "/a/<b>")], + expect: (2, "/b/<b>"), (3, "/b/b") ); assert_ranked_routing!( - to: "b/b", - with: [(1, "a/<b>"), (2, "b/<b>"), (0, "b/b")], - expect: (0, "b/b"), (2, "b/<b>") + to: "/b/b", + with: [(1, "/a/<b>"), (2, "/b/<b>"), (0, "/b/b")], + expect: (0, "/b/b"), (2, "/b/<b>") ); assert_ranked_routing!( @@ -369,7 +384,7 @@ mod test { let expected = &[$($want),+]; assert!(routed_to.len() == expected.len()); for (got, expected) in routed_to.iter().zip(expected.iter()) { - assert_eq!(got.uri.as_str(), expected as &str); + assert_eq!(got.uri.to_string(), expected.to_string()); } }) } @@ -377,34 +392,35 @@ mod test { #[test] fn test_default_ranked_routing() { assert_default_ranked_routing!( - to: "a/b?v=1", - with: ["a/<b>", "a/b"], - expect: "a/b", "a/<b>" + to: "/a/b?v=1", + with: ["/a/<b>", "/a/b"], + expect: "/a/b", "/a/<b>" ); assert_default_ranked_routing!( - to: "a/b?v=1", - with: ["a/<b>", "a/b", "a/b?<v>"], - expect: "a/b?<v>", "a/b", "a/<b>" + to: "/a/b?v=1", + with: ["/a/<b>", "/a/b", "/a/b?<v>"], + expect: "/a/b?<v>", "/a/b", "/a/<b>" ); assert_default_ranked_routing!( - to: "a/b?v=1", - with: ["a/<b>", "a/b", "a/b?<v>", "a/<b>?<v>"], - expect: "a/b?<v>", "a/b", "a/<b>?<v>", "a/<b>" + to: "/a/b?v=1", + with: ["/a/<b>", "/a/b", "/a/b?<v>", "/a/<b>?<v>"], + expect: "/a/b?<v>", "/a/b", "/a/<b>?<v>", "/a/<b>" ); assert_default_ranked_routing!( - to: "a/b", - with: ["a/<b>", "a/b", "a/b?<v>", "a/<b>?<v>"], - expect: "a/b", "a/<b>" + to: "/a/b", + with: ["/a/<b>", "/a/b", "/a/b?<v>", "/a/<b>?<v>"], + expect: "/a/b", "/a/<b>" ); } fn match_params(router: &Router, path: &str, expected: &[&str]) -> bool { println!("Testing: {} (expect: {:?})", path, expected); route(router, Get, path).map_or(false, |route| { - let params = route.get_param_indexes(&Uri::new(path)); + let uri = Origin::parse_route(path).unwrap(); + let params = route.get_param_indexes(&uri); if params.len() != expected.len() { return false; } diff --git a/core/lib/src/router/route.rs b/core/lib/src/router/route.rs @@ -6,9 +6,11 @@ use yansi::Color::*; use codegen::StaticRouteInfo; use handler::Handler; use http::{Method, MediaType}; -use http::uri::Uri; +use http::ext::IntoOwned; +use http::uri::Origin; /// A route: a method, its handler, path, rank, and format/media type. +#[derive(Clone)] pub struct Route { /// The name of this route, if one was given. pub name: Option<&'static str>, @@ -17,10 +19,10 @@ pub struct Route { /// The function that should be called when the route matches. pub handler: Handler, /// The base mount point of this `Route`. - pub base: Uri<'static>, - /// The uri (in Rocket format) that should be matched against. This uri - /// already includes the base mount point. - pub uri: Uri<'static>, + pub base: Origin<'static>, + /// The uri (in Rocket's route format) that should be matched against. This + /// URI already includes the base mount point. + pub uri: Origin<'static>, /// The rank of this route. Lower ranks have higher priorities. pub rank: isize, /// The media type this route matches against, if any. @@ -28,10 +30,10 @@ pub struct Route { } #[inline(always)] -fn default_rank(uri: &Uri) -> isize { +fn default_rank(uri: &Origin) -> isize { // static path, query = -4; static path, no query = -3 // dynamic path, query = -2; dynamic path, no query = -1 - match (!uri.path().contains('<'), uri.query().is_some()) { + match (!uri.path().contains('<'), uri.query().is_some()) { (true, true) => -4, (true, false) => -3, (false, true) => -2, @@ -77,19 +79,19 @@ impl Route { /// // this is a rank -1 route matching requests to `GET /<name>` /// let name = Route::new(Method::Get, "/<name>", handler); /// ``` - pub fn new<S>(m: Method, path: S, handler: Handler) -> Route + /// + /// # Panics + /// + /// Panics if `path` is not a valid origin URI. + pub fn new<S>(method: Method, path: S, handler: Handler) -> Route where S: AsRef<str> { - let uri = Uri::from(path.as_ref().to_string()); - Route { - name: None, - method: m, - handler: handler, - rank: default_rank(&uri), - base: Uri::from("/"), - uri: uri, - format: None, - } + let path = path.as_ref(); + let origin = Origin::parse_route(path) + .expect("invalid URI used as route path in `Route::new()`"); + + let rank = default_rank(&origin); + Route::ranked(rank, method, path, handler) } /// Creates a new route with the given rank, method, path, and handler with @@ -109,17 +111,22 @@ impl Route { /// // this is a rank 1 route matching requests to `GET /` /// let index = Route::ranked(1, Method::Get, "/", handler); /// ``` - pub fn ranked<S>(rank: isize, m: Method, uri: S, handler: Handler) -> Route + /// + /// # Panics + /// + /// Panics if `path` is not a valid origin URI. + pub fn ranked<S>(rank: isize, method: Method, path: S, handler: Handler) -> Route where S: AsRef<str> { + let uri = Origin::parse_route(path.as_ref()) + .expect("invalid URI used as route path in `Route::ranked()`") + .into_owned(); + Route { name: None, - method: m, - handler: handler, - base: Uri::from("/"), - uri: Uri::from(uri.as_ref().to_string()), - rank: rank, format: None, + base: Origin::dummy(), + method, handler, rank, uri } } @@ -146,14 +153,14 @@ impl Route { } /// Sets the base mount point of the route. Does not update the rank or any - /// other parameters. + /// other parameters. If `path` contains a query, it is ignored. /// /// # Example /// /// ```rust /// use rocket::{Request, Route, Data}; + /// use rocket::http::{Method, uri::Origin}; /// use rocket::handler::Outcome; - /// use rocket::http::Method; /// /// fn handler<'r>(request: &'r Request, _data: Data) -> Outcome<'r> { /// Outcome::from(request, "Hello, world!") @@ -163,12 +170,13 @@ impl Route { /// assert_eq!(index.base(), "/"); /// assert_eq!(index.base.path(), "/"); /// - /// index.set_base("/hi"); + /// index.set_base(Origin::parse("/hi").unwrap()); /// assert_eq!(index.base(), "/hi"); /// assert_eq!(index.base.path(), "/hi"); /// ``` - pub fn set_base<S>(&mut self, path: S) where S: AsRef<str> { - self.base = Uri::from(path.as_ref().to_string()); + pub fn set_base<'a>(&mut self, path: Origin<'a>) { + self.base = path.into_owned(); + self.base.clear_query(); } /// Sets the path of the route. Does not update the rank or any other @@ -178,8 +186,8 @@ impl Route { /// /// ```rust /// use rocket::{Request, Route, Data}; + /// use rocket::http::{Method, uri::Origin}; /// use rocket::handler::Outcome; - /// use rocket::http::Method; /// /// fn handler<'r>(request: &'r Request, _data: Data) -> Outcome<'r> { /// Outcome::from(request, "Hello, world!") @@ -188,11 +196,11 @@ impl Route { /// let mut index = Route::ranked(1, Method::Get, "/", handler); /// assert_eq!(index.uri.path(), "/"); /// - /// index.set_uri("/hello"); + /// index.set_uri(Origin::parse("/hello").unwrap()); /// assert_eq!(index.uri.path(), "/hello"); /// ``` - pub fn set_uri<S>(&mut self, uri: S) where S: AsRef<str> { - self.uri = Uri::from(uri.as_ref().to_string()); + pub fn set_uri<'a>(&mut self, uri: Origin<'a>) { + self.uri = uri.into_owned(); } // FIXME: Decide whether a component has to be fully variable or not. That @@ -200,7 +208,7 @@ impl Route { // TODO: Don't return a Vec...take in an &mut [&'a str] (no alloc!) /// Given a URI, returns a vector of slices of that URI corresponding to the /// dynamic segments in this route. - pub(crate) fn get_param_indexes(&self, uri: &Uri) -> Vec<(usize, usize)> { + crate fn get_param_indexes(&self, uri: &Origin) -> Vec<(usize, usize)> { let route_segs = self.uri.segments(); let uri_segs = uri.segments(); let start_addr = uri.path().as_ptr() as usize; @@ -221,20 +229,6 @@ impl Route { } } -impl Clone for Route { - fn clone(&self) -> Route { - Route { - name: self.name, - method: self.method, - handler: self.handler, - rank: self.rank, - base: self.base.clone(), - uri: self.uri.clone(), - format: self.format.clone(), - } - } -} - impl fmt::Display for Route { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { write!(f, "{} {}", Green.paint(&self.method), Blue.paint(&self.uri))?; @@ -258,7 +252,14 @@ impl fmt::Display for Route { impl fmt::Debug for Route { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - <Route as fmt::Display>::fmt(self, f) + f.debug_struct("Route") + .field("name", &self.name) + .field("method", &self.method) + .field("base", &self.base) + .field("uri", &self.uri) + .field("rank", &self.rank) + .field("format", &self.format) + .finish() } } diff --git a/core/lib/tests/absolute-uris-okay-issue-443.rs b/core/lib/tests/absolute-uris-okay-issue-443.rs @@ -0,0 +1,35 @@ +#![feature(plugin, decl_macro)] +#![plugin(rocket_codegen)] + +extern crate rocket; + +use rocket::response::Redirect; + +#[get("/google")] +fn google() -> Redirect { + Redirect::to("https://www.google.com") +} + +#[get("/rocket")] +fn rocket() -> Redirect { + Redirect::to("https://rocket.rs:80") +} + +mod test_absolute_uris_okay { + use super::*; + use rocket::local::Client; + + #[test] + fn redirect_works() { + let rocket = rocket::ignite().mount("/", routes![google, rocket]); + let client = Client::new(rocket).unwrap(); + + let response = client.get("/google").dispatch(); + let location = response.headers().get_one("Location"); + assert_eq!(location, Some("https://www.google.com")); + + let response = client.get("/rocket").dispatch(); + let location = response.headers().get_one("Location"); + assert_eq!(location, Some("https://rocket.rs:80")); + } +} diff --git a/core/lib/tests/local-request-content-type-issue-505.rs b/core/lib/tests/local-request-content-type-issue-505.rs @@ -66,12 +66,12 @@ mod local_request_content_type_tests { let client = Client::new(rocket()).unwrap(); let mut req = client.post("/"); - assert_eq!(req.cloned_dispatch().body_string(), Some("Absent".to_string())); + assert_eq!(req.clone().dispatch().body_string(), Some("Absent".to_string())); assert_eq!(req.mut_dispatch().body_string(), Some("Absent".to_string())); assert_eq!(req.dispatch().body_string(), Some("Absent".to_string())); let mut req = client.post("/data"); - assert_eq!(req.cloned_dispatch().body_string(), Some("Data Absent".to_string())); + assert_eq!(req.clone().dispatch().body_string(), Some("Data Absent".to_string())); assert_eq!(req.mut_dispatch().body_string(), Some("Data Absent".to_string())); assert_eq!(req.dispatch().body_string(), Some("Data Absent".to_string())); } @@ -81,12 +81,12 @@ mod local_request_content_type_tests { let client = Client::new(rocket()).unwrap(); let mut req = client.post("/").header(ContentType::JSON); - assert_eq!(req.cloned_dispatch().body_string(), Some("Present".to_string())); + assert_eq!(req.clone().dispatch().body_string(), Some("Present".to_string())); assert_eq!(req.mut_dispatch().body_string(), Some("Present".to_string())); assert_eq!(req.dispatch().body_string(), Some("Present".to_string())); let mut req = client.post("/data").header(ContentType::JSON); - assert_eq!(req.cloned_dispatch().body_string(), Some("Data Present".to_string())); + assert_eq!(req.clone().dispatch().body_string(), Some("Data Present".to_string())); assert_eq!(req.mut_dispatch().body_string(), Some("Data Present".to_string())); assert_eq!(req.dispatch().body_string(), Some("Data Present".to_string())); } diff --git a/examples/handlebars_templates/src/main.rs b/examples/handlebars_templates/src/main.rs @@ -50,7 +50,7 @@ fn about() -> Template { #[catch(404)] fn not_found(req: &Request) -> Template { let mut map = std::collections::HashMap::new(); - map.insert("path", req.uri().as_str()); + map.insert("path", req.uri().path()); Template::render("error/404", &map) } diff --git a/examples/manual_routes/src/main.rs b/examples/manual_routes/src/main.rs @@ -28,7 +28,7 @@ fn name<'a>(req: &'a Request, _: Data) -> Outcome<'a> { fn echo_url(req: &Request, _: Data) -> Outcome<'static> { let param = req.uri() - .as_str() + .path() .split_at(6) .1; diff --git a/examples/manual_routes/src/tests.rs b/examples/manual_routes/src/tests.rs @@ -24,9 +24,8 @@ fn test_name() { #[test] fn test_echo() { - let echo = "echo text"; - let uri = format!("/echo:echo text"); - test(&uri, ContentType::Plain, Status::Ok, echo.to_string()); + let uri = format!("/echo:echo%20text"); + test(&uri, ContentType::Plain, Status::Ok, "echo text".into()); } #[test] diff --git a/examples/tera_templates/src/main.rs b/examples/tera_templates/src/main.rs @@ -34,7 +34,7 @@ fn get(name: String) -> Template { #[catch(404)] fn not_found(req: &Request) -> Template { let mut map = HashMap::new(); - map.insert("path", req.uri().as_str()); + map.insert("path", req.uri().path()); Template::render("error/404", &map) }