Skip to content

Commit

Permalink
Overhaul URI types.
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
SergioBenitez committed Jul 29, 2018
1 parent c04655f commit 56c6a96
Show file tree
Hide file tree
Showing 64 changed files with 2,727 additions and 1,006 deletions.
1 change: 1 addition & 0 deletions contrib/lib/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
#![feature(use_extern_macros)]
#![feature(crate_visibility_modifier)]

// TODO: Version URLs.
#![doc(html_root_url = "https://api.rocket.rs")]
Expand Down
6 changes: 3 additions & 3 deletions contrib/lib/src/templates/engine.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -91,7 +91,7 @@ impl Engines {
})
}

pub(crate) fn render<C: Serialize>(
crate fn render<C: Serialize>(
&self,
name: &str,
info: &TemplateInfo,
Expand Down
4 changes: 2 additions & 2 deletions core/codegen/src/decorators/route.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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));
Expand Down
29 changes: 17 additions & 12 deletions core/codegen/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 '"'
Expand All @@ -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
Expand All @@ -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:
//!
Expand All @@ -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");
//!
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -221,7 +227,6 @@
//! ROCKET_CODEGEN_DEBUG=1 cargo build
//! ```

extern crate syntax;
extern crate syntax_ext;
extern crate syntax_pos;
Expand Down
141 changes: 88 additions & 53 deletions core/codegen/src/macros/uri.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,17 +3,22 @@ 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;
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,
Expand Down Expand Up @@ -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
Expand All @@ -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);
}
}
Expand All @@ -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)
Expand Down
13 changes: 7 additions & 6 deletions core/codegen/src/parser/route.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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.
///
Expand All @@ -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>>,
Expand Down Expand Up @@ -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" {
Expand All @@ -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>>
Expand Down
Loading

0 comments on commit 56c6a96

Please sign in to comment.