From fa3e0334c1dcbbc91f63375906aaab72e5bafe59 Mon Sep 17 00:00:00 2001 From: Sergio Benitez Date: Wed, 19 May 2021 18:20:40 -0700 Subject: [PATCH] Overhaul URI types, parsers, 'uri!' macro. This commit entirely rewrites Rocket's URI parsing routines and overhauls the 'uri!' macro resolving all known issues and removing any potential limitations for compile-time URI creation. This commit: * Introduces a new 'Reference' URI variant for URI-references. * Modifies 'Redirect' to accept 'TryFrom'. * Introduces a new 'Asterisk' URI variant for parity. * Allows creation of any URI type from a string literal via 'uri!'. * Enables dynamic/static prefixing/suffixing of route URIs in 'uri!'. * Unifies 'Segments' and 'QuerySegments' into one generic 'Segments'. * Consolidates URI formatting types/traits into a 'uri::fmt' module. * Makes APIs more symmetric across URI types. It also includes the following less-relevant changes: * Implements 'FromParam' for a single-segment 'PathBuf'. * Adds 'FileName::is_safe()'. * No longer reparses upstream request URIs. Resolves #842. Resolves #853. Resolves #998. --- contrib/lib/src/helmet/mod.rs | 10 +- contrib/lib/src/serve.rs | 4 +- contrib/lib/tests/helmet.rs | 22 +- core/codegen/src/attribute/param/guard.rs | 4 +- core/codegen/src/attribute/param/parse.rs | 12 +- core/codegen/src/attribute/route/parse.rs | 6 +- core/codegen/src/bang/mod.rs | 8 +- core/codegen/src/bang/uri.rs | 132 ++--- core/codegen/src/bang/uri_parsing.rs | 427 ++++++++++---- core/codegen/src/derive/uri_display.rs | 35 +- core/codegen/src/exports.rs | 1 + core/codegen/src/http_codegen.rs | 85 ++- core/codegen/src/lib.rs | 290 +++++++-- core/codegen/src/proc_macro_ext.rs | 7 + core/codegen/tests/route-raw.rs | 8 +- core/codegen/tests/route.rs | 8 +- core/codegen/tests/typed-uris.rs | 387 ++++++------ .../ui-fail-nightly/typed-uri-bad-type.stderr | 192 ++++-- .../typed-uris-bad-params.stderr | 258 ++++---- .../typed-uris-invalid-syntax.stderr | 180 ++++-- .../uri_display_type_errors.stderr | 48 +- .../ui-fail-stable/typed-uri-bad-type.stderr | 192 ++++-- .../typed-uris-bad-params.stderr | 257 ++++---- .../typed-uris-invalid-syntax.stderr | 183 ++++-- .../uri_display_type_errors.stderr | 48 +- .../tests/ui-fail/typed-uri-bad-type.rs | 40 +- .../tests/ui-fail/typed-uris-bad-params.rs | 51 +- .../ui-fail/typed-uris-invalid-syntax.rs | 25 +- core/codegen/tests/uri_display.rs | 2 +- core/http/Cargo.toml | 2 +- core/http/src/parse/indexed.rs | 21 +- core/http/src/parse/uri/error.rs | 1 + core/http/src/parse/uri/mod.rs | 13 +- core/http/src/parse/uri/parser.rs | 300 +++++----- core/http/src/parse/uri/spec.txt | 39 +- core/http/src/parse/uri/tables.rs | 37 +- core/http/src/parse/uri/tests.rs | 272 ++++----- core/http/src/raw_str.rs | 2 +- core/http/src/uri/absolute.rs | 457 ++++++++++----- core/http/src/uri/asterisk.rs | 9 + core/http/src/uri/authority.rs | 138 ++--- core/http/src/uri/error.rs | 30 + core/http/src/uri/{ => fmt}/encoding.rs | 9 +- core/http/src/uri/{ => fmt}/formatter.rs | 303 +++++++--- core/http/src/uri/{ => fmt}/from_uri_param.rs | 77 +-- core/http/src/uri/fmt/mod.rs | 14 + core/http/src/uri/fmt/part.rs | 84 +++ core/http/src/uri/{ => fmt}/uri_display.rs | 97 ++-- core/http/src/uri/mod.rs | 101 +--- core/http/src/uri/origin.rs | 505 ++++++---------- core/http/src/uri/path_query.rs | 387 ++++++++++++ core/http/src/uri/reference.rs | 549 ++++++++++++++++++ core/http/src/uri/segments.rs | 249 +++++--- core/http/src/uri/uri.rs | 337 ++++++----- core/lib/src/catcher/catcher.rs | 2 +- core/lib/src/form/lenient.rs | 2 +- core/lib/src/form/name/file_name.rs | 33 +- core/lib/src/form/strict.rs | 2 +- core/lib/src/local/asynchronous/request.rs | 3 +- core/lib/src/request/from_param.rs | 26 +- core/lib/src/request/request.rs | 26 +- core/lib/src/response/redirect.rs | 92 +-- core/lib/src/rocket.rs | 4 +- core/lib/src/route/uri.rs | 24 +- core/lib/src/router/collider.rs | 15 +- core/lib/src/router/router.rs | 4 +- core/lib/src/server.rs | 2 +- .../lib/tests/absolute-uris-okay-issue-443.rs | 14 +- core/lib/tests/scoped-uri.rs | 4 +- core/lib/tests/segments-issues-41-86.rs | 12 +- .../tests/uri-percent-encoding-issue-808.rs | 10 +- examples/pastebin/src/main.rs | 12 +- examples/pastebin/src/paste_id.rs | 4 +- examples/pastebin/src/tests.rs | 4 +- examples/responders/src/tests.rs | 18 +- examples/templating/src/hbs.rs | 4 +- examples/templating/src/tera.rs | 4 +- site/guide/5-responses.md | 40 +- 78 files changed, 4767 insertions(+), 2549 deletions(-) create mode 100644 core/http/src/uri/asterisk.rs create mode 100644 core/http/src/uri/error.rs rename core/http/src/uri/{ => fmt}/encoding.rs (90%) rename core/http/src/uri/{ => fmt}/formatter.rs (65%) rename core/http/src/uri/{ => fmt}/from_uri_param.rs (84%) create mode 100644 core/http/src/uri/fmt/mod.rs create mode 100644 core/http/src/uri/fmt/part.rs rename core/http/src/uri/{ => fmt}/uri_display.rs (87%) create mode 100644 core/http/src/uri/path_query.rs create mode 100644 core/http/src/uri/reference.rs diff --git a/contrib/lib/src/helmet/mod.rs b/contrib/lib/src/helmet/mod.rs index 312c5493..d911445e 100644 --- a/contrib/lib/src/helmet/mod.rs +++ b/contrib/lib/src/helmet/mod.rs @@ -76,17 +76,17 @@ //! `SpaceHelmet`: //! //! ```rust -//! # extern crate rocket; +//! # #[macro_use] extern crate rocket; //! # extern crate rocket_contrib; //! use rocket::http::uri::Uri; //! use rocket_contrib::helmet::{SpaceHelmet, Frame, XssFilter, Hsts, NoSniff}; //! -//! let site_uri = Uri::parse("https://mysite.example.com").unwrap(); -//! let report_uri = Uri::parse("https://report.example.com").unwrap(); +//! let site_uri = uri!("https://mysite.example.com"); +//! let report_uri = uri!("https://report.example.com"); //! let helmet = SpaceHelmet::default() //! .enable(Hsts::default()) -//! .enable(Frame::AllowFrom(site_uri)) -//! .enable(XssFilter::EnableReport(report_uri)) +//! .enable(Frame::AllowFrom(site_uri.into())) +//! .enable(XssFilter::EnableReport(report_uri.into())) //! .disable::(); //! ``` //! diff --git a/contrib/lib/src/serve.rs b/contrib/lib/src/serve.rs index a48b7d1f..f57912c4 100644 --- a/contrib/lib/src/serve.rs +++ b/contrib/lib/src/serve.rs @@ -357,10 +357,12 @@ impl Into> for StaticFiles { #[rocket::async_trait] impl Handler for StaticFiles { async fn handle<'r>(&self, req: &'r Request<'_>, data: Data) -> Outcome<'r> { + use rocket::http::uri::fmt::Path; + // Get the segments as a `PathBuf`, allowing dotfiles requested. let options = self.options; let allow_dotfiles = options.contains(Options::DotFiles); - let path = req.segments::>(0..).ok() + let path = req.segments::>(0..).ok() .and_then(|segments| segments.to_path_buf(allow_dotfiles).ok()) .map(|path| self.root.join(path)); diff --git a/contrib/lib/tests/helmet.rs b/contrib/lib/tests/helmet.rs index 10713488..7631e108 100644 --- a/contrib/lib/tests/helmet.rs +++ b/contrib/lib/tests/helmet.rs @@ -4,7 +4,7 @@ extern crate rocket; #[cfg(feature = "helmet")] mod helmet_tests { - use rocket::http::{Status, uri::Uri}; + use rocket::http::Status; use rocket::local::blocking::{Client, LocalResponse}; use rocket_contrib::helmet::*; @@ -108,24 +108,22 @@ mod helmet_tests { #[test] fn uri_test() { - let allow_uri = Uri::parse("https://www.google.com").unwrap(); - let report_uri = Uri::parse("https://www.google.com").unwrap(); - let enforce_uri = Uri::parse("https://www.google.com").unwrap(); + let allow_uri = uri!("https://rocket.rs"); + let report_uri = uri!("https://rocket.rs"); + let enforce_uri = uri!("https://rocket.rs"); let helmet = SpaceHelmet::default() - .enable(Frame::AllowFrom(allow_uri)) - .enable(XssFilter::EnableReport(report_uri)) - .enable(ExpectCt::ReportAndEnforce(Duration::seconds(30), enforce_uri)); + .enable(Frame::AllowFrom(allow_uri.into())) + .enable(XssFilter::EnableReport(report_uri.into())) + .enable(ExpectCt::ReportAndEnforce(Duration::seconds(30), enforce_uri.into())); dispatch!(helmet, |response: LocalResponse<'_>| { - assert_header!(response, "X-Frame-Options", - "ALLOW-FROM https://www.google.com"); + assert_header!(response, "X-Frame-Options", "ALLOW-FROM https://rocket.rs"); - assert_header!(response, "X-XSS-Protection", - "1; report=https://www.google.com"); + assert_header!(response, "X-XSS-Protection", "1; report=https://rocket.rs"); assert_header!(response, "Expect-CT", - "max-age=30, enforce, report-uri=\"https://www.google.com\""); + "max-age=30, enforce, report-uri=\"https://rocket.rs\""); }); } diff --git a/core/codegen/src/attribute/param/guard.rs b/core/codegen/src/attribute/param/guard.rs index 9a7db661..53396320 100644 --- a/core/codegen/src/attribute/param/guard.rs +++ b/core/codegen/src/attribute/param/guard.rs @@ -19,8 +19,8 @@ impl FromMeta for Dynamic { let string = StringLit::from_meta(meta)?; let span = string.subspan(1..string.len() + 1); - // We don't allow `_`. We abuse `uri::Query` to enforce this. - Ok(Dynamic::parse::(&string, span)?) + // We don't allow `_`. We abuse `fmt::Query` to enforce this. + Ok(Dynamic::parse::(&string, span)?) } } diff --git a/core/codegen/src/attribute/param/parse.rs b/core/codegen/src/attribute/param/parse.rs index 2fb933c7..54eba8c4 100644 --- a/core/codegen/src/attribute/param/parse.rs +++ b/core/codegen/src/attribute/param/parse.rs @@ -2,10 +2,10 @@ use unicode_xid::UnicodeXID; use devise::{Diagnostic, ext::SpanDiagnosticExt}; use crate::name::Name; -use crate::http::uri::{self, UriPart}; use crate::proc_macro_ext::StringLit; use crate::proc_macro2::Span; use crate::attribute::param::{Parameter, Dynamic}; +use crate::http::uri::fmt::{Part, Kind, Path}; #[derive(Debug)] pub struct Error<'a> { @@ -27,7 +27,7 @@ pub enum ErrorKind { } impl Dynamic { - pub fn parse( + pub fn parse( segment: &str, span: Span, ) -> Result> { @@ -40,7 +40,7 @@ impl Dynamic { } impl Parameter { - pub fn parse( + pub fn parse( segment: &str, source_span: Span, ) -> Result> { @@ -62,7 +62,7 @@ impl Parameter { } let dynamic = Dynamic { name: Name::new(name, span), trailing, index: 0 }; - if dynamic.is_wild() && P::KIND != uri::Kind::Path { + if dynamic.is_wild() && P::KIND != Kind::Path { return Err(Error::new(name, span, ErrorKind::Ignored)); } else if dynamic.is_wild() { return Ok(Parameter::Ignored(dynamic)); @@ -84,7 +84,7 @@ impl Parameter { Ok(Parameter::Static(Name::new(segment, source_span))) } - pub fn parse_many( + pub fn parse_many( source: &str, source_span: Span, ) -> impl Iterator>> { @@ -182,7 +182,7 @@ impl devise::FromMeta for Dynamic { fn from_meta(meta: &devise::MetaItem) -> devise::Result { let string = StringLit::from_meta(meta)?; let span = string.subspan(1..string.len() + 1); - let param = Dynamic::parse::(&string, span)?; + let param = Dynamic::parse::(&string, span)?; if param.is_wild() { return Err(Error::new(&string, span, ErrorKind::Ignored).into()); diff --git a/core/codegen/src/attribute/route/parse.rs b/core/codegen/src/attribute/route/parse.rs index 0c83c261..fadf7428 100644 --- a/core/codegen/src/attribute/route/parse.rs +++ b/core/codegen/src/attribute/route/parse.rs @@ -10,7 +10,7 @@ use crate::syn_ext::FnArgExt; use crate::name::Name; use crate::proc_macro2::Span; use crate::http::ext::IntoOwned; -use crate::http::uri::{self, Origin}; +use crate::http::uri::{Origin, fmt}; /// This structure represents the parsed `route` attribute and associated items. #[derive(Debug)] @@ -159,14 +159,14 @@ impl Route { // Parse and collect the path parameters. let (source, span) = (attr.uri.path(), attr.uri.path_span); - let path_params = Parameter::parse_many::(source.as_str(), span) + let path_params = Parameter::parse_many::(source.as_str(), span) .map(|p| Route::upgrade_param(p?, &arguments)) .filter_map(|p| p.map_err(|e| diags.push(e.into())).ok()) .collect::>(); // Parse and collect the query parameters. let query_params = match (attr.uri.query(), attr.uri.query_span) { - (Some(r), Some(span)) => Parameter::parse_many::(r.as_str(), span) + (Some(q), Some(span)) => Parameter::parse_many::(q.as_str(), span) .map(|p| Route::upgrade_param(p?, &arguments)) .filter_map(|p| p.map_err(|e| diags.push(e.into())).ok()) .collect::>(), diff --git a/core/codegen/src/bang/mod.rs b/core/codegen/src/bang/mod.rs index 756f7527..7dc7e35c 100644 --- a/core/codegen/src/bang/mod.rs +++ b/core/codegen/src/bang/mod.rs @@ -46,19 +46,19 @@ pub fn catchers_macro(input: proc_macro::TokenStream) -> TokenStream { pub fn uri_macro(input: proc_macro::TokenStream) -> TokenStream { uri::_uri_macro(input.into()) .unwrap_or_else(|diag| diag.emit_as_expr_tokens_or(quote! { - rocket::http::uri::Origin::dummy() + rocket::http::uri::Origin::ROOT })) } pub fn uri_internal_macro(input: proc_macro::TokenStream) -> TokenStream { - // FIXME: Ideally we would generate an `Origin::dummy()` so that we don't + // TODO: Ideally we would generate a perfect `Origin::ROOT` so that we don't // assist in propoagate further errors. Alas, we can't set the span to the // invocation of `uri!` without access to `span.parent()`, and // `Span::call_site()` here points to the `#[route]`, immediate caller, - // generate a rather confusing error message when there's a type-mismatch. + // generating a rather confusing error message when there's a type-mismatch. uri::_uri_internal_macro(input.into()) .unwrap_or_else(|diag| diag.emit_as_expr_tokens_or(quote! { - rocket::http::uri::Origin::dummy() + rocket::http::uri::Origin::ROOT })) } diff --git a/core/codegen/src/bang/uri.rs b/core/codegen/src/bang/uri.rs index 6c300d13..a128679a 100644 --- a/core/codegen/src/bang/uri.rs +++ b/core/codegen/src/bang/uri.rs @@ -3,15 +3,14 @@ use std::fmt::Display; use devise::{syn, Result}; use devise::ext::{SpanDiagnosticExt, quote_respanned}; -use crate::http::uri; -use crate::syn::{Expr, Ident, Type, spanned::Spanned}; +use crate::http::uri::fmt; use crate::http_codegen::Optional; +use crate::syn::{Expr, Ident, Type, spanned::Spanned}; use crate::syn_ext::IdentExt; use crate::bang::uri_parsing::*; use crate::proc_macro2::TokenStream; use crate::attribute::param::Parameter; -use crate::exports::_uri; - +use crate::exports::*; use crate::URI_MACRO_PREFIX; macro_rules! p { @@ -31,11 +30,14 @@ pub fn prefix_last_segment(path: &mut syn::Path, prefix: &str) { pub fn _uri_macro(input: TokenStream) -> Result { let input2: TokenStream = input.clone().into(); - let mut params = syn::parse2::(input)?; - prefix_last_segment(&mut params.route_path, URI_MACRO_PREFIX); - - let path = ¶ms.route_path; - Ok(quote!(#path!(#input2))) + match syn::parse2::(input)? { + UriMacro::Routed(ref mut mac) => { + prefix_last_segment(&mut mac.route.path, URI_MACRO_PREFIX); + let path = &mac.route.path; + Ok(quote!(#path!(#input2))) + }, + UriMacro::Literal(uri) => Ok(quote!(#uri)), + } } fn extract_exprs<'a>(internal: &'a InternalUriParams) -> Result<( @@ -44,7 +46,7 @@ fn extract_exprs<'a>(internal: &'a InternalUriParams) -> Result<( impl Iterator, // types for both path || query )> { - let route_name = &internal.uri_params.route_path; + let route_name = &internal.uri_mac.route.path; match internal.validate() { Validation::Ok(exprs) => { let path_params = internal.dynamic_path_params(); @@ -64,7 +66,7 @@ fn extract_exprs<'a>(internal: &'a InternalUriParams) -> Result<( let mut route_name = quote!(#route_name).to_string(); route_name.retain(|c| !c.is_whitespace()); - let diag = internal.uri_params.args_span() + let diag = internal.uri_mac.args_span() .error("expected unnamed arguments due to ignored parameters") .note(format!("uri for route `{}` ignores path parameters: \"{}\"", route_name, internal.route_uri)); @@ -75,17 +77,16 @@ fn extract_exprs<'a>(internal: &'a InternalUriParams) -> Result<( let mut route_name = quote!(#route_name).to_string(); route_name.retain(|c| !c.is_whitespace()); - let diag = internal.uri_params.args_span() - .error(format!("expected {} but {} supplied", + let diag = internal.uri_mac.args_span() + .error(format!("route expects {} but {} supplied", p!(expected, "parameter"), p!(actual, "was"))) - .note(format!("route `{}` has uri \"{}\"", - route_name, internal.route_uri)); + .note(format!("route `{}` has uri \"{}\"", route_name, internal.route_uri)); Err(diag) } Validation::Named(missing, extra, dup) => { let e = format!("invalid parameters for `{}` route uri", quote!(#route_name)); - let mut diag = internal.uri_params.args_span().error(e) + let mut diag = internal.uri_mac.args_span().error(e) .note(format!("uri parameters are: {}", internal.fn_args_str())); fn join>(iter: T) -> (&'static str, String) { @@ -116,11 +117,11 @@ fn extract_exprs<'a>(internal: &'a InternalUriParams) -> Result<( } } -fn add_binding(to: &mut Vec, ident: &Ident, ty: &Type, expr: &Expr) { +fn add_binding(to: &mut Vec, ident: &Ident, ty: &Type, expr: &Expr) { let span = expr.span(); let part = match P::KIND { - uri::Kind::Path => quote_spanned!(span => #_uri::Path), - uri::Kind::Query => quote_spanned!(span => #_uri::Query), + fmt::Kind::Path => quote_spanned!(span => #_fmt::Path), + fmt::Kind::Query => quote_spanned!(span => #_fmt::Query), }; let tmp_ident = ident.clone().with_span(expr.span()); @@ -128,7 +129,7 @@ fn add_binding(to: &mut Vec, ident: &Ident, ty: &T to.push(quote_spanned!(span => #[allow(non_snake_case)] #let_stmt; - let #ident = <#ty as #_uri::FromUriParam<#part, _>>::from_uri_param(#tmp_ident); + let #ident = <#ty as #_fmt::FromUriParam<#part, _>>::from_uri_param(#tmp_ident); )); } @@ -139,41 +140,30 @@ fn explode_path<'a>( mut args: impl Iterator, ) -> TokenStream { if internal.dynamic_path_params().count() == 0 { - let route_uri = &internal.route_uri; - if let Some(ref mount) = internal.uri_params.mount_point { - let full_uri = route_uri.map_path(|p| format!("{}/{}", mount, p)) - .expect("origin from path") - .into_normalized(); + let path = internal.route_uri.path().as_str(); + quote!(#_fmt::UriArgumentsKind::Static(#path)) + } else { + let uri_display = quote!(#_fmt::UriDisplay<#_fmt::Path>); + let dyn_exprs = internal.path_params.iter().map(|param| { + match param { + Parameter::Static(name) => { + quote!(&#name as &dyn #uri_display) + }, + Parameter::Dynamic(_) | Parameter::Guard(_) => { + let (ident, ty) = args.next().expect("ident/ty for non-ignored"); + let expr = exprs.next().expect("one expr per dynamic arg"); + add_binding::(bindings, &ident, &ty, &expr); + quote_spanned!(expr.span() => &#ident as &dyn #uri_display) + } + Parameter::Ignored(_) => { + let expr = exprs.next().expect("one expr per dynamic arg"); + quote_spanned!(expr.span() => &#expr as &dyn #uri_display) + } + } + }); - let path = full_uri.path().as_str(); - return quote!(#_uri::UriArgumentsKind::Static(#path)); - } else { - let path = route_uri.path().as_str(); - return quote!(#_uri::UriArgumentsKind::Static(#path)); - } + quote!(#_fmt::UriArgumentsKind::Dynamic(&[#(#dyn_exprs),*])) } - - let uri_display = quote!(#_uri::UriDisplay<#_uri::Path>); - let all_path_params = internal.mount_params.iter().chain(internal.path_params.iter()); - let dyn_exprs = all_path_params.map(|param| { - match param { - Parameter::Static(name) => { - quote!(&#name as &dyn #uri_display) - }, - Parameter::Dynamic(_) | Parameter::Guard(_) => { - let (ident, ty) = args.next().expect("ident/ty for non-ignored"); - let expr = exprs.next().expect("one expr per dynamic arg"); - add_binding::(bindings, &ident, &ty, &expr); - quote_spanned!(expr.span() => &#ident as &dyn #uri_display) - } - Parameter::Ignored(_) => { - let expr = exprs.next().expect("one expr per dynamic arg"); - quote_spanned!(expr.span() => &#expr as &dyn #uri_display) - } - } - }); - - quote!(#_uri::UriArgumentsKind::Dynamic(&[#(#dyn_exprs),*])) } fn explode_query<'a>( @@ -184,11 +174,11 @@ fn explode_query<'a>( ) -> Option { let query = internal.route_uri.query()?.as_str(); if internal.dynamic_query_params().count() == 0 { - return Some(quote!(#_uri::UriArgumentsKind::Static(#query))); + return Some(quote!(#_fmt::UriArgumentsKind::Static(#query))); } - let query_arg = quote!(#_uri::UriQueryArgument); - let uri_display = quote!(#_uri::UriDisplay<#_uri::Query>); + let query_arg = quote!(#_fmt::UriQueryArgument); + let uri_display = quote!(#_fmt::UriDisplay<#_fmt::Query>); let dyn_exprs = internal.query_params.iter().filter_map(|param| { if let Parameter::Static(source) = param { return Some(quote!(#query_arg::Raw(#source))); @@ -208,10 +198,9 @@ fn explode_query<'a>( let expr = match arg_expr.as_expr() { Some(expr) => expr, None => { - // Force a typecheck for the `Ignoreable` trait. Note that write - // out the path to `is_ignorable` to get the right span. + // Force a typecheck for the `Ignoreable` trait. bindings.push(quote_respanned! { arg_expr.span() => - rocket::http::uri::assert_ignorable::<#_uri::Query, #ty>(); + #_fmt::assert_ignorable::<#_fmt::Query, #ty>(); }); return None; @@ -219,7 +208,7 @@ fn explode_query<'a>( }; let name = &dynamic.name; - add_binding::(bindings, &ident, &ty, &expr); + add_binding::(bindings, &ident, &ty, &expr); Some(match dynamic.trailing { false => quote_spanned! { expr.span() => #query_arg::NameValue(#name, &#ident as &dyn #uri_display) @@ -230,7 +219,7 @@ fn explode_query<'a>( }) }); - Some(quote!(#_uri::UriArgumentsKind::Dynamic(&[#(#dyn_exprs),*]))) + Some(quote!(#_fmt::UriArgumentsKind::Dynamic(&[#(#dyn_exprs),*]))) } pub fn _uri_internal_macro(input: TokenStream) -> Result { @@ -239,13 +228,20 @@ pub fn _uri_internal_macro(input: TokenStream) -> Result { let (path_exprs, query_exprs, mut fn_args) = extract_exprs(&internal)?; let mut bindings = vec![]; - let uri_mod = quote!(rocket::http::uri); let path = explode_path(&internal, &mut bindings, path_exprs, &mut fn_args); - let query = explode_query(&internal, &mut bindings, query_exprs, fn_args); - let query = Optional(query); + let query = Optional(explode_query(&internal, &mut bindings, query_exprs, fn_args)); - Ok(quote!({ - #(#bindings)* - #uri_mod::UriArguments { path: #path, query: #query, }.into_origin() - })) + let prefix = internal.uri_mac.prefix.as_ref() + .map(|prefix| quote_spanned!(prefix.span() => .with_prefix(#prefix))); + + let suffix = internal.uri_mac.suffix.as_ref() + .map(|suffix| quote_spanned!(suffix.span() => .with_suffix(#suffix))); + + Ok(quote_spanned!(internal.uri_mac.route.path.span() => + #[allow(unused_braces)] { + #(#bindings)* + let __builder = #_fmt::RouteUriBuilder::new(#path, #query); + __builder #prefix #suffix .render() + } + )) } diff --git a/core/codegen/src/bang/uri_parsing.rs b/core/codegen/src/bang/uri_parsing.rs index 9b941275..88debbe0 100644 --- a/core/codegen/src/bang/uri_parsing.rs +++ b/core/codegen/src/bang/uri_parsing.rs @@ -1,13 +1,17 @@ +use std::ops::Deref; + use indexmap::IndexMap; use devise::{Spanned, ext::TypeExt}; -use quote::ToTokens; +use quote::{ToTokens, TokenStreamExt}; +use rocket_http::uri::{Error, Reference}; -use crate::syn::{self, Expr, Ident, LitStr, Path, Token, Type}; -use crate::syn::parse::{self, Parse, ParseStream}; +use crate::{http_codegen, syn::{self, Expr, Ident, LitStr, Path, Token, Type}}; +use crate::syn::parse::{self, Parse, ParseStream, Parser}; use crate::syn::punctuated::Punctuated; -use crate::http::{uri, uri::Origin, ext::IntoOwned}; -use crate::proc_macro2::{TokenStream, Span}; +use crate::http::uri::{Uri, Origin, Absolute, fmt}; +use crate::http::ext::IntoOwned; +use crate::proc_macro2::{TokenStream, TokenTree, Span}; use crate::proc_macro_ext::StringLit; use crate::attribute::param::{Parameter, Dynamic}; use crate::name::Name; @@ -32,24 +36,54 @@ pub enum Args { Named(Punctuated), } -// 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 +/// A string literal parsed as a URI. #[derive(Debug)] -pub struct UriParams { - pub mount_point: Option>, - pub route_path: Path, - pub arguments: Args, +pub struct UriLit(Uri<'static>, Span); + +/// An expression in a URI slot (prefix, suffix, or literal). +#[derive(Debug)] +pub enum UriExpr { + /// A string literal parsed as a URI. + Uri(UriLit), + /// An expression that will be typechecked to be some URI kind. + Expr(Expr), +} + +/// See `UriMacro` for what each field represents. +#[derive(Debug)] +pub struct RouteInvocation { + pub path: Path, + pub args: Args, +} + +/// See `UriMacro` for what each field represents. +#[derive(Debug)] +pub struct RoutedUri { + pub prefix: Option, + pub route: RouteInvocation, + pub suffix: Option, +} + +// The macro can be invoked with 1, 2, or 3 arguments. +// +// As a `Literal`, with a single argument: +// uri!("/mount/point"); +// ^-------------| +// literal.0 +// +// As `Routed`, with 1, 2, or 3 arguments: prefix/suffix optional. +// uri!("/mount/point", this::route(e1, e2, e3), "?some#suffix"); +// ^-------------| ^---------|^----------| |-----|------| +// routed.prefix | | routed.suffix +// | route.route.args +// routed.route.path +#[derive(Debug)] +pub enum UriMacro { + Literal(UriLit), + Routed(RoutedUri), } #[derive(Debug)] -pub struct FnArg { - pub ident: Ident, - pub ty: Type, -} - pub enum Validation<'a> { // Parameters that were ignored in a named argument setting. NamedIgnored(Vec<&'a Dynamic>), @@ -63,27 +97,38 @@ pub enum Validation<'a> { // This is invoked by Rocket itself. The `uri!` macro expands to a call to a // route-specific macro which in-turn expands to a call to `internal_uri!`, -// passing along the user's parameters (`uri_params`) from the original `uri!` +// passing along the user's invocation (`uri_mac`) from the original `uri!` // call. This is necessary so that we can converge the type information in the // route (from the route-specific macro) with the user's parameters (by // forwarding them to the internal_uri! call). // -// `fn_args` are the URI arguments (excluding request guards and ignored path -// parts) from the original handler in the order they were declared in the URI -// (`/`). `route_uri` is the URI itself. +// `fn_args` are the URI arguments (excluding request guards) from the original +// handler in the order they were declared in the URI (`/`). +// `route_uri` is the full route URI itself. +// +// The syntax of `uri_mac` is that of `UriMacro`. // // internal_uri!("//<_>?lang=en&", (one: ty, two: ty), $($tt)*); // ^----/----^ ^-----\-----^ ^-------/------^ ^-----| -// path_params query_params fn_args uri_params +// path_params query_params fn_args uri_mac // ^------ route_uri ------^ #[derive(Debug)] pub struct InternalUriParams { pub route_uri: Origin<'static>, - pub mount_params: Vec, pub path_params: Vec, pub query_params: Vec, pub fn_args: Vec, - pub uri_params: UriParams, + pub uri_mac: RoutedUri, +} + +#[derive(Debug)] +pub struct FnArg { + pub ident: Ident, + pub ty: Type, +} + +fn err>(span: Span, s: S) -> parse::Result { + Err(parse::Error::new(span.into(), s.as_ref())) } impl Parse for ArgExpr { @@ -111,77 +156,139 @@ impl Parse for Arg { } } -fn err>(span: Span, s: S) -> parse::Result { - Err(parse::Error::new(span.into(), s.as_ref())) -} - -impl Parse for UriParams { - // Parses the mount point, if any, route identifier, and arguments. - fn parse(input: ParseStream<'_>) -> parse::Result { - if input.is_empty() { - return Err(input.error("call to `uri!` cannot be empty")); - } - - // Parse the mount point and suffixing ',', if any. - let mount_point = if input.peek(LitStr) { - let string = input.parse::()?; - let mount_point = Origin::parse_owned(string.value()) - .map(|m| m.into_normalized()) - .map_err(|_| { - // TODO(proc_macro): use error, add example as a help - parse::Error::new(string.span(), "invalid mount point; \ - mount points must be static, absolute URIs: `/example`") - })?; - - if !input.peek(Token![,]) && input.cursor().eof() { - return err(string.span(), "unexpected end of input: \ - expected ',' followed by route path"); - } - - input.parse::()?; - Some(mount_point) - } else { - None - }; - - // Parse the route identifier, which must always exist. - let route_path = input.parse::()?; - +impl Parse for Args { + fn parse(input: ParseStream<'_>) -> syn::Result { // If there are no arguments, finish early. - if !input.peek(Token![:]) && input.cursor().eof() { - let arguments = Args::Unnamed(Punctuated::new()); - return Ok(Self { mount_point, route_path, arguments }); + if input.cursor().eof() { + return Ok(Args::Unnamed(Punctuated::new())); } - // Parse arguments - let colon = input.parse::()?; - let arguments: Punctuated = input.parse_terminated(Arg::parse)?; - - // A 'colon' was used but there are no arguments. - if arguments.is_empty() { - return err(colon.span(), "expected argument list after `:`"); - } - - // Ensure that both types of arguments were not used at once. - let (mut homogeneous_args, mut prev_named) = (true, None); - for arg in &arguments { - match prev_named { - Some(prev_named) => homogeneous_args = prev_named == arg.is_named(), - None => prev_named = Some(arg.is_named()), + // Parse arguments. Ensure both types of args were not used at once. + let args: Punctuated = input.parse_terminated(Arg::parse)?; + let mut first_is_named = None; + for arg in &args { + if let Some(first_is_named) = first_is_named { + if first_is_named != arg.is_named() { + return err(args.span(), "named and unnamed parameters cannot be mixed"); + } + } else { + first_is_named = Some(arg.is_named()); } } - if !homogeneous_args { - return err(arguments.span(), "named and unnamed parameters cannot be mixed"); - } - // Create the `Args` enum, which properly record one-kind-of-argument-ness. - let arguments = match prev_named { - Some(true) => Args::Named(arguments), - _ => Args::Unnamed(arguments) + match first_is_named { + Some(true) => Ok(Args::Named(args)), + _ => Ok(Args::Unnamed(args)) + } + } +} + +impl Parse for RouteInvocation { + fn parse(input: ParseStream<'_>) -> syn::Result { + let path = input.parse()?; + let args = if input.peek(syn::token::Paren) { + let args; + syn::parenthesized!(args in input); + args.parse()? + } else { + Args::Unnamed(Punctuated::new()) }; - Ok(Self { mount_point, route_path, arguments }) + Ok(RouteInvocation { path, args }) + } +} + +impl Parse for UriLit { + fn parse(input: ParseStream<'_>) -> syn::Result { + let string = input.parse::()?; + let uri = match Uri::parse_any(&string) { + Ok(uri) => uri.into_owned(), + Err(e) => { + let span = string.subspan(e.index() + 1..(e.index() + 2)); + return err(span, format!("invalid URI: {}", e)); + } + }; + + Ok(UriLit(uri, string.span())) + } +} + +impl UriMacro { + fn unary(input: ParseStream<'_>) -> parse::Result { + if input.peek(LitStr) { + Ok(UriMacro::Literal(input.parse()?)) + } else { + Ok(UriMacro::Routed(RoutedUri { + prefix: None, + route: input.parse()?, + suffix: None, + })) + } + } + + fn binary(prefix: TokenStream, middle: TokenStream) -> parse::Result { + Ok(UriMacro::Routed(RoutedUri { + prefix: UriExpr::parse_prefix.parse2(prefix)?, + route: syn::parse2(middle)?, + suffix: None, + })) + } + + fn ternary(prefix: TokenStream, mid: TokenStream, suffix: TokenStream) -> parse::Result { + Ok(UriMacro::Routed(RoutedUri { + prefix: UriExpr::parse_prefix.parse2(prefix)?, + route: syn::parse2(mid)?, + suffix: UriExpr::parse_suffix.parse2(suffix)? + })) + } +} + +impl Parse for UriMacro { + fn parse(input: ParseStream<'_>) -> parse::Result { + use syn::buffer::Cursor; + use parse::{StepCursor, Result}; + + fn stream<'c>(cursor: StepCursor<'c, '_>) -> Result<(Option, Cursor<'c>)> { + let mut stream = TokenStream::new(); + let mut cursor = *cursor; + while let Some((tt, next)) = cursor.token_tree() { + cursor = next; + match tt { + TokenTree::Punct(p) if p.as_char() == ',' => break, + _ => stream.append(tt) + } + } + + stream.is_empty() + .then(|| Ok((None, cursor))) + .unwrap_or_else(|| Ok((Some(stream), cursor))) + } + + let mut args = vec![]; + while let Some(tokens) = input.step(stream)? { + args.push(tokens); + } + + let (arg_count, mut iter) = (args.len(), args.into_iter()); + let mut next = || iter.next().unwrap(); + match arg_count { + 0 => err(Span::call_site(), "expected at least 1 argument, found none"), + 1 => UriMacro::unary.parse2(next()), + 2 => UriMacro::binary(next(), next()), + 3 => UriMacro::ternary(next(), next(), next()), + n => err(iter.skip(3).next().unwrap().span(), + format!("expected 1, 2, or 3 arguments, found {}", n)) + } + } +} + +impl Parse for RoutedUri { + fn parse(input: ParseStream<'_>) -> syn::Result { + match UriMacro::parse(input)? { + UriMacro::Routed(route) => Ok(route), + UriMacro::Literal(uri) => err(uri.span(), "expected route URI, found literal") + } } } @@ -197,12 +304,11 @@ impl Parse for FnArg { impl Parse for InternalUriParams { fn parse(input: ParseStream<'_>) -> parse::Result { - let route_uri_str = input.parse::()?; + let route_uri_str = input.parse::()?; input.parse::()?; // Validation should always succeed since this macro can only be called // if the route attribute succeeded, implying a valid route URI. - let route_uri_str = StringLit::new(route_uri_str.value(), route_uri_str.span()); let route_uri = Origin::parse_route(&route_uri_str) .map(|o| o.into_normalized().into_owned()) .map_err(|_| input.error("internal error: invalid route URI"))?; @@ -213,39 +319,28 @@ impl Parse for InternalUriParams { let fn_args = fn_args.into_iter().collect(); input.parse::()?; - let uri_params = input.parse::()?; + let uri_params = input.parse::()?; - // This span isn't right...we don't have the original span. let span = route_uri_str.subspan(1..route_uri.path().len() + 1); - let mount_params = match uri_params.mount_point.as_ref() { - Some(mount) => Parameter::parse_many::(mount.path().as_str(), span) - .map(|p| p.expect("internal error: invalid path parameter")) - .collect::>(), - None => vec![] - }; - - let path_params = Parameter::parse_many::(route_uri.path().as_str(), span) + let path_params = Parameter::parse_many::(route_uri.path().as_str(), span) .map(|p| p.expect("internal error: invalid path parameter")) .collect::>(); - let query_params = match route_uri.query() { - Some(query) => { - let i = route_uri.path().len() + 2; - let span = route_uri_str.subspan(i..(i + query.len())); - Parameter::parse_many::(query.as_str(), span) - .map(|p| p.expect("internal error: invalid query parameter")) - .collect::>() - } - None => vec![] - }; + let query = route_uri.query(); + let query_params = query.map(|query| { + let i = route_uri.path().len() + 2; + let span = route_uri_str.subspan(i..(i + query.len())); + Parameter::parse_many::(query.as_str(), span) + .map(|p| p.expect("internal error: invalid query parameter")) + .collect::>() + }).unwrap_or_default(); Ok(InternalUriParams { route_uri, - mount_params, path_params, query_params, fn_args, - uri_params + uri_mac: uri_params }) } } @@ -273,7 +368,7 @@ impl InternalUriParams { } pub fn validate(&self) -> Validation<'_> { - let args = &self.uri_params.arguments; + let args = &self.uri_mac.route.args; let all_params = self.dynamic_path_params().chain(self.dynamic_query_params()); match args { Args::Unnamed(args) => { @@ -321,12 +416,12 @@ impl InternalUriParams { } } -impl UriParams { +impl RoutedUri { /// The Span to use when referring to all of the arguments. pub fn args_span(&self) -> Span { - match self.arguments.num() { - 0 => self.route_path.span(), - _ => self.arguments.span() + match self.route.args.num() { + 0 => self.route.path.span(), + _ => self.route.args.span() } } } @@ -378,6 +473,104 @@ impl ArgExpr { } } +fn uri_err(lit: &StringLit, error: Error<'_>) -> parse::Result { + let span = lit.subspan(error.index() + 1..(error.index() + 2)); + err(span, format!("invalid URI: {}", error)) +} + +impl UriExpr { + fn parse_prefix(input: ParseStream<'_>) -> syn::Result> { + if let Ok(_) = input.parse::() { + return Ok(None); + } + + if !input.peek(LitStr) { + return input.parse::().map(|e| Some(UriExpr::Expr(e))); + } + + let lit = input.parse::()?; + let uri = Uri::parse::>(&lit) + .or_else(|e| Uri::parse::>(&lit).map_err(|e2| (e, e2))) + .map_err(|(e1, e2)| lit.starts_with('/').then(|| e1).unwrap_or_else(|| e2)) + .or_else(|e| uri_err(&lit, e))?; + + if matches!(&uri, Uri::Origin(o) if o.query().is_some()) + || matches!(&uri, Uri::Absolute(a) if a.query().is_some()) + { + return err(lit.span(), "URI prefix cannot contain query part"); + } + + Ok(Some(UriExpr::Uri(UriLit(uri.into_owned(), lit.span())))) + } + + fn parse_suffix(input: ParseStream<'_>) -> syn::Result> { + if let Ok(_) = input.parse::() { + return Ok(None); + } + + if !input.peek(LitStr) { + return input.parse::().map(|e| Some(UriExpr::Expr(e))); + } + + let lit = input.parse::()?; + let uri = Reference::parse(&lit).or_else(|e| uri_err(&lit, e))?; + if uri.scheme().is_some() || uri.authority().is_some() || !uri.path().is_empty() { + return err(lit.span(), "URI suffix must contain only query and/or fragment"); + } + + // This is a bit of finagling to get the types to match up how we'd + // like. A URI like `?foo` will parse as a `Reference`, since that's + // what it is. But if we left this as is, we'd convert Origins and + // Absolutes to References on suffix appendage when we don't need to. + // This is because anything + a Reference _must_ result in a Reference + // since the resulting URI could have a fragment. Since here we know + // that's not the case, we lie and say it's Absolute since an Absolute + // can't contain a fragment, so an Origin + Absolute suffix is still an + // Origin, and likewise for an Absolute. + let uri = match uri.fragment() { + None => { + let query = uri.query().map(|q| q.as_str()); + Uri::Absolute(Absolute::const_new("", None, "", query)) + } + Some(_) => Uri::Reference(uri) + }; + + Ok(Some(UriExpr::Uri(UriLit(uri.into_owned(), lit.span())))) + } +} + +impl Deref for UriLit { + type Target = Uri<'static>; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl ToTokens for UriLit { + fn to_tokens(&self, t: &mut TokenStream) { + use http_codegen::*; + + let (uri, span) = (&self.0, self.1); + match uri { + Uri::Origin(o) => Origin(o, span).to_tokens(t), + Uri::Absolute(o) => Absolute(o, span).to_tokens(t), + Uri::Authority(o) => Authority(o, span).to_tokens(t), + Uri::Reference(r) => Reference(r, span).to_tokens(t), + Uri::Asterisk(a) => Asterisk(*a, span).to_tokens(t), + } + } +} + +impl ToTokens for UriExpr { + fn to_tokens(&self, t: &mut TokenStream) { + match self { + UriExpr::Uri(uri) => uri.to_tokens(t), + UriExpr::Expr(e) => e.to_tokens(t), + } + } +} + impl ToTokens for ArgExpr { fn to_tokens(&self, tokens: &mut TokenStream) { match self { diff --git a/core/codegen/src/derive/uri_display.rs b/core/codegen/src/derive/uri_display.rs index 19227d0c..671b54dc 100644 --- a/core/codegen/src/derive/uri_display.rs +++ b/core/codegen/src/derive/uri_display.rs @@ -1,10 +1,10 @@ use devise::{*, ext::SpanDiagnosticExt}; -use rocket_http::uri; use crate::exports::*; use crate::derive::form_field::{FieldExt, VariantExt}; use crate::proc_macro2::TokenStream; +use crate::http::uri::fmt; const NO_EMPTY_FIELDS: &str = "fieldless structs are not supported"; const NO_NULLARY: &str = "nullary items are not supported"; @@ -13,10 +13,9 @@ const ONLY_ONE_UNNAMED: &str = "tuple structs or variants must have exactly one const EXACTLY_ONE_FIELD: &str = "struct must have exactly one field"; pub fn derive_uri_display_query(input: proc_macro::TokenStream) -> TokenStream { - use crate::http::uri::Query; - const URI_DISPLAY: StaticTokens = quote_static!(#_uri::UriDisplay<#_uri::Query>); - const FORMATTER: StaticTokens = quote_static!(#_uri::Formatter<#_uri::Query>); + const URI_DISPLAY: StaticTokens = quote_static!(#_fmt::UriDisplay<#_fmt::Query>); + const FORMATTER: StaticTokens = quote_static!(#_fmt::Formatter<#_fmt::Query>); let uri_display = DeriveGenerator::build_for(input.clone(), quote!(impl #URI_DISPLAY)) .support(Support::Struct | Support::Enum | Support::Type | Support::Lifetime) @@ -84,9 +83,9 @@ pub fn derive_uri_display_query(input: proc_macro::TokenStream) -> TokenStream { Err(diag) => return diag.emit_as_item_tokens() }; - let from_self = from_uri_param::(input.clone(), quote!(Self)); - let from_ref = from_uri_param::(input.clone(), quote!(&'__r Self)); - let from_mut = from_uri_param::(input.clone(), quote!(&'__r mut Self)); + let from_self = from_uri_param::(input.clone(), quote!(Self)); + let from_ref = from_uri_param::(input.clone(), quote!(&'__r Self)); + let from_mut = from_uri_param::(input.clone(), quote!(&'__r mut Self)); let mut ts = TokenStream::from(uri_display); ts.extend(TokenStream::from(from_self)); @@ -97,10 +96,8 @@ pub fn derive_uri_display_query(input: proc_macro::TokenStream) -> TokenStream { #[allow(non_snake_case)] pub fn derive_uri_display_path(input: proc_macro::TokenStream) -> TokenStream { - use crate::http::uri::Path; - - const URI_DISPLAY: StaticTokens = quote_static!(#_uri::UriDisplay<#_uri::Path>); - const FORMATTER: StaticTokens = quote_static!(#_uri::Formatter<#_uri::Path>); + const URI_DISPLAY: StaticTokens = quote_static!(#_fmt::UriDisplay<#_fmt::Path>); + const FORMATTER: StaticTokens = quote_static!(#_fmt::Formatter<#_fmt::Path>); let uri_display = DeriveGenerator::build_for(input.clone(), quote!(impl #URI_DISPLAY)) .support(Support::TupleStruct | Support::Type | Support::Lifetime) @@ -130,9 +127,9 @@ pub fn derive_uri_display_path(input: proc_macro::TokenStream) -> TokenStream { Err(diag) => return diag.emit_as_item_tokens() }; - let from_self = from_uri_param::(input.clone(), quote!(Self)); - let from_ref = from_uri_param::(input.clone(), quote!(&'__r Self)); - let from_mut = from_uri_param::(input.clone(), quote!(&'__r mut Self)); + let from_self = from_uri_param::(input.clone(), quote!(Self)); + let from_ref = from_uri_param::(input.clone(), quote!(&'__r Self)); + let from_mut = from_uri_param::(input.clone(), quote!(&'__r mut Self)); let mut ts = TokenStream::from(uri_display); ts.extend(TokenStream::from(from_self)); @@ -141,10 +138,10 @@ pub fn derive_uri_display_path(input: proc_macro::TokenStream) -> TokenStream { ts.into() } -fn from_uri_param(input: proc_macro::TokenStream, ty: TokenStream) -> TokenStream { +fn from_uri_param(input: proc_macro::TokenStream, ty: TokenStream) -> TokenStream { let part = match P::KIND { - uri::Kind::Path => quote!(#_uri::Path), - uri::Kind::Query => quote!(#_uri::Query), + fmt::Kind::Path => quote!(#_fmt::Path), + fmt::Kind::Query => quote!(#_fmt::Query), }; let ty: syn::Type = syn::parse2(ty).expect("valid type"); @@ -153,10 +150,10 @@ fn from_uri_param(input: proc_macro::TokenStream, ty: TokenStre _ => None }; - let param_trait = quote!(impl #gen #_uri::FromUriParam<#part, #ty>); + let param_trait = quote!(impl #gen #_fmt::FromUriParam<#part, #ty>); DeriveGenerator::build_for(input, param_trait) .support(Support::All) - .type_bound(quote!(#_uri::UriDisplay<#part>)) + .type_bound(quote!(#_fmt::UriDisplay<#part>)) .inner_mapper(MapperBuild::new() .with_output(move |_, _| quote! { type Target = #ty; diff --git a/core/codegen/src/exports.rs b/core/codegen/src/exports.rs index 6f2ec05a..4aa292fb 100644 --- a/core/codegen/src/exports.rs +++ b/core/codegen/src/exports.rs @@ -76,6 +76,7 @@ define_exported_paths! { _form => ::rocket::form::prelude, _http => ::rocket::http, _uri => ::rocket::http::uri, + _fmt => ::rocket::http::uri::fmt, _Option => ::std::option::Option, _Result => ::std::result::Result, _Some => ::std::option::Option::Some, diff --git a/core/codegen/src/http_codegen.rs b/core/codegen/src/http_codegen.rs index a479a527..adb3a6c3 100644 --- a/core/codegen/src/http_codegen.rs +++ b/core/codegen/src/http_codegen.rs @@ -1,8 +1,8 @@ use quote::ToTokens; use devise::{FromMeta, MetaItem, Result, ext::{Split2, PathExt, SpanDiagnosticExt}}; -use crate::proc_macro2::TokenStream; use crate::http; +use crate::proc_macro2::{TokenStream, Span}; #[derive(Debug)] pub struct ContentType(pub http::ContentType); @@ -19,6 +19,21 @@ pub struct Method(pub http::Method); #[derive(Clone, Debug)] pub struct Optional(pub Option); +#[derive(Debug)] +pub struct Origin<'a>(pub &'a http::uri::Origin<'a>, pub Span); + +#[derive(Debug)] +pub struct Absolute<'a>(pub &'a http::uri::Absolute<'a>, pub Span); + +#[derive(Debug)] +pub struct Authority<'a>(pub &'a http::uri::Authority<'a>, pub Span); + +#[derive(Debug)] +pub struct Reference<'a>(pub &'a http::uri::Reference<'a>, pub Span); + +#[derive(Debug)] +pub struct Asterisk(pub http::uri::Asterisk, pub Span); + impl FromMeta for Status { fn from_meta(meta: &MetaItem) -> Result { let num = usize::from_meta(meta)?; @@ -145,3 +160,71 @@ impl ToTokens for Optional { tokens.extend(opt_tokens); } } + +impl ToTokens for Origin<'_> { + fn to_tokens(&self, tokens: &mut TokenStream) { + let (origin, span) = (self.0, self.1); + let origin = origin.clone().into_normalized(); + define_spanned_export!(span => _uri); + + let path = origin.path().as_str(); + let query = Optional(origin.query().map(|q| q.as_str())); + tokens.extend(quote_spanned! { span => + #_uri::Origin::const_new(#path, #query) + }); + } +} + +impl ToTokens for Absolute<'_> { + fn to_tokens(&self, tokens: &mut TokenStream) { + let (absolute, span) = (self.0, self.1); + define_spanned_export!(span => _uri); + let absolute = absolute.clone().into_normalized(); + + let scheme = absolute.scheme(); + let auth = Optional(absolute.authority().map(|a| Authority(a, span))); + let path = absolute.path().as_str(); + let query = Optional(absolute.query().map(|q| q.as_str())); + tokens.extend(quote_spanned! { span => + #_uri::Absolute::const_new(#scheme, #auth, #path, #query) + }); + } +} + +impl ToTokens for Authority<'_> { + fn to_tokens(&self, tokens: &mut TokenStream) { + let (authority, span) = (self.0, self.1); + define_spanned_export!(span => _uri); + + let user_info = Optional(authority.user_info()); + let host = authority.host(); + let port = Optional(authority.port()); + tokens.extend(quote_spanned! { span => + #_uri::Authority::const_new(#user_info, #host, #port) + }); + } +} + +impl ToTokens for Reference<'_> { + fn to_tokens(&self, tokens: &mut TokenStream) { + let (reference, span) = (self.0, self.1); + define_spanned_export!(span => _uri); + let reference = reference.clone().into_normalized(); + + let scheme = Optional(reference.scheme()); + let auth = Optional(reference.authority().map(|a| Authority(a, span))); + let path = reference.path().as_str(); + let query = Optional(reference.query().map(|q| q.as_str())); + let frag = Optional(reference.fragment().map(|f| f.as_str())); + tokens.extend(quote_spanned! { span => + #_uri::Reference::const_new(#scheme, #auth, #path, #query, #frag) + }); + } +} + +impl ToTokens for Asterisk { + fn to_tokens(&self, tokens: &mut TokenStream) { + define_spanned_export!(self.1 => _uri); + tokens.extend(quote_spanned!(self.1 => #_uri::Asterisk)); + } +} diff --git a/core/codegen/src/lib.rs b/core/codegen/src/lib.rs index a1529025..f55e2ed5 100644 --- a/core/codegen/src/lib.rs +++ b/core/codegen/src/lib.rs @@ -1028,77 +1028,221 @@ pub fn catchers(input: TokenStream) -> TokenStream { emit!(bang::catchers_macro(input)) } -/// Type-safe, URI-safe generation of an [`Origin`] URI from a route. +/// Type-safe, encoding-safe route and non-route URI generation. /// -/// The `uri!` macro creates a type-safe, URL-safe URI given a route and values -/// for the 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 and query. +/// The `uri!` macro creates type-safe, URL-safe URIs given a route and concrete +/// parameters for its URI or a URI string literal. /// -/// For example, for the following route: +/// # String Literal Parsing /// -/// ```rust -/// # #[macro_use] extern crate rocket; -/// # -/// #[get("/person/?")] -/// fn person(name: String, age: Option) -> String { -/// # "".into() /* -/// ... -/// # */ -/// } +/// Given a string literal as input, `uri!` parses the string using +/// [`Uri::parse_any()`] and emits a `'static`, `const` value whose type is one +/// of [`Asterisk`], [`Origin`], [`Authority`], [`Absolute`], or [`Reference`], +/// reflecting the parsed value. If the type allows normalization, the value is +/// normalized before being emitted. Parse errors are caught and emitted at +/// compile-time. +/// +/// The grammar for this variant of `uri!` is: +/// +/// ```text +/// uri := STRING +/// +/// STRING := an uncooked string literal, as defined by Rust (example: `"/hi"`) /// ``` /// -/// A URI can be created as follows: +/// `STRING` is expected to be an undecoded URI of any variant. +/// +/// ## Examples +/// +/// ```rust +/// # #[macro_use] extern crate rocket; +/// use rocket::http::uri::Absolute; +/// +/// // Values returned from `uri!` are `const` and `'static`. +/// const ROOT_CONST: Absolute<'static> = uri!("https://rocket.rs"); +/// static ROOT_STATIC: Absolute<'static> = uri!("https://rocket.rs?root"); +/// +/// // Any variant can be parsed, but beware of ambiguities. +/// let asterisk = uri!("*"); +/// let origin = uri!("/foo/bar/baz"); +/// let authority = uri!("rocket.rs:443"); +/// let absolute = uri!("https://rocket.rs:443"); +/// let reference = uri!("foo?bar#baz"); +/// +/// # use rocket::http::uri::{Asterisk, Origin, Authority, Reference}; +/// # // Ensure we get the types we expect. +/// # let asterisk: Asterisk = asterisk; +/// # let origin: Origin<'static> = origin; +/// # let authority: Authority<'static> = authority; +/// # let absolute: Absolute<'static> = absolute; +/// # let reference: Reference<'static> = reference; +/// ``` +/// +/// # Type-Safe Route URIs +/// +/// A URI to a route name `foo` is generated using `uri!(foo(v1, v2, v3))` or +/// `uri!(foo(a = v1, b = v2, c = v3))`, where `v1`, `v2`, `v3` are the values +/// to fill in for route parameters named `a`, `b`, and `c`. If the named +/// parameter sytnax is used (`a = v1`, etc.), parameters can appear in any +/// order. +/// +/// More concretely, for the route `person` defined below: +/// +/// ```rust +/// # #[macro_use] extern crate rocket; +/// #[get("/person/?")] +/// fn person(name: &str, age: Option) { } +/// ``` +/// +/// ...a URI can be created as follows: /// /// ```rust /// # #[macro_use] extern crate rocket; -/// # /// # #[get("/person/?")] -/// # fn person(name: String, age: Option) { } -/// # +/// # fn person(name: &str, age: Option) { } /// // with unnamed parameters, in route path declaration order -/// let mike = uri!(person: "Mike Smith", Some(28)); +/// let mike = uri!(person("Mike Smith", Some(28))); /// assert_eq!(mike.to_string(), "/person/Mike%20Smith?age=28"); /// /// // with named parameters, order irrelevant -/// let mike = uri!(person: name = "Mike", age = Some(28)); -/// let mike = uri!(person: age = Some(28), name = "Mike"); +/// let mike = uri!(person(name = "Mike", age = Some(28))); +/// let mike = uri!(person(age = Some(28), name = "Mike")); /// assert_eq!(mike.to_string(), "/person/Mike?age=28"); /// -/// // with a specific mount-point -/// let mike = uri!("/api", person: name = "Mike", age = Some(28)); -/// assert_eq!(mike.to_string(), "/api/person/Mike?age=28"); -/// -/// // with unnamed values ignored -/// let mike = uri!(person: "Mike", _); -/// assert_eq!(mike.to_string(), "/person/Mike"); -/// /// // with unnamed values, explicitly `None`. -/// let option: Option = None; -/// let mike = uri!(person: "Mike", option); -/// assert_eq!(mike.to_string(), "/person/Mike"); -/// -/// // with named values ignored -/// let mike = uri!(person: name = "Mike", age = _); +/// let mike = uri!(person("Mike", None::)); /// assert_eq!(mike.to_string(), "/person/Mike"); /// /// // with named values, explicitly `None` /// let option: Option = None; -/// let mike = uri!(person: name = "Mike", age = option); +/// let mike = uri!(person(name = "Mike", age = None::)); /// assert_eq!(mike.to_string(), "/person/Mike"); /// ``` /// +/// For optional query parameters, those of type `Option` or `Result`, a `_` can +/// be used in-place of `None` or `Err`: +/// +/// ```rust +/// # #[macro_use] extern crate rocket; +/// # #[get("/person/?")] +/// # fn person(name: &str, age: Option) { } +/// // with named values ignored +/// let mike = uri!(person(name = "Mike", age = _)); +/// assert_eq!(mike.to_string(), "/person/Mike"); +/// +/// // with named values ignored +/// let mike = uri!(person(age = _, name = "Mike")); +/// assert_eq!(mike.to_string(), "/person/Mike"); +/// +/// // with unnamed values ignored +/// let mike = uri!(person("Mike", _)); +/// assert_eq!(mike.to_string(), "/person/Mike"); +/// ``` +/// +/// It is a type error to attempt to ignore query parameters that are neither +/// `Option` or `Result`. Path parameters can never be ignored. A path parameter +/// of type `Option` or `Result` must be filled by a value that can +/// target a type of `T`: +/// +/// ```rust +/// # #[macro_use] extern crate rocket; +/// #[get("/person/")] +/// fn maybe(name: Option<&str>) { } +/// +/// let bob1 = uri!(maybe(name = "Bob")); +/// let bob2 = uri!(maybe("Bob Smith")); +/// assert_eq!(bob1.to_string(), "/person/Bob"); +/// assert_eq!(bob2.to_string(), "/person/Bob%20Smith"); +/// +/// #[get("/person/")] +/// fn ok(age: Result) { } +/// +/// let kid1 = uri!(ok(age = 10)); +/// let kid2 = uri!(ok(12)); +/// assert_eq!(kid1.to_string(), "/person/10"); +/// assert_eq!(kid2.to_string(), "/person/12"); +/// ``` +/// +/// Values for ignored route segments can be of any type as long as the type +/// implements [`UriDisplay`] for the appropriate URI part. If a route URI +/// contains ignored segments, the route URI invocation cannot use named +/// arguments. +/// +/// ```rust +/// # #[macro_use] extern crate rocket; +/// #[get("/ignore/<_>/")] +/// fn ignore(other: &str) { } +/// +/// let bob = uri!(ignore("Bob Hope", "hello")); +/// let life = uri!(ignore(42, "cat&dog")); +/// assert_eq!(bob.to_string(), "/ignore/Bob%20Hope/hello"); +/// assert_eq!(life.to_string(), "/ignore/42/cat%26dog"); +/// ``` +/// +/// ## Prefixes and Suffixes +/// +/// A route URI can be be optionally prefixed and/or suffixed by a URI generated +/// from a string literal or an arbitrary expression. This takes the form +/// `uri!(prefix, foo(v1, v2, v3), suffix)`, where both `prefix` and `suffix` +/// are optional, and either `prefix` or `suffix` may be `_` to specify the +/// value as empty. +/// +/// ```rust +/// # #[macro_use] extern crate rocket; +/// #[get("/person/?")] +/// fn person(name: &str, age: Option) { } +/// +/// // with a specific mount-point of `/api`. +/// let bob = uri!("/api", person("Bob", Some(28))); +/// assert_eq!(bob.to_string(), "/api/person/Bob?age=28"); +/// +/// // with an absolute URI as a prefix +/// let bob = uri!("https://rocket.rs", person("Bob", Some(28))); +/// assert_eq!(bob.to_string(), "https://rocket.rs/person/Bob?age=28"); +/// +/// // with another absolute URI as a prefix +/// let bob = uri!("https://rocket.rs/foo", person("Bob", Some(28))); +/// assert_eq!(bob.to_string(), "https://rocket.rs/foo/person/Bob?age=28"); +/// +/// // with an expression as a prefix +/// let host = uri!("http://bob.me"); +/// let bob = uri!(host, person("Bob", Some(28))); +/// assert_eq!(bob.to_string(), "http://bob.me/person/Bob?age=28"); +/// +/// // with a suffix but no prefix +/// let bob = uri!(_, person("Bob", Some(28)), "#baz"); +/// assert_eq!(bob.to_string(), "/person/Bob?age=28#baz"); +/// +/// // with both a prefix and suffix +/// let bob = uri!("https://rocket.rs/", person("Bob", Some(28)), "#woo"); +/// assert_eq!(bob.to_string(), "https://rocket.rs/person/Bob?age=28#woo"); +/// +/// // with an expression suffix. if the route URI already has a query, the +/// // query part is ignored. otherwise it is added. +/// let suffix = uri!("?woo#bam"); +/// let bob = uri!(_, person("Bob", Some(28)), suffix.clone()); +/// assert_eq!(bob.to_string(), "/person/Bob?age=28#bam"); +/// +/// let bob = uri!(_, person("Bob", None::), suffix.clone()); +/// assert_eq!(bob.to_string(), "/person/Bob?woo#bam"); +/// ``` +/// /// ## Grammar /// -/// The grammar for the `uri!` macro is: +/// The grammar for this variant of the `uri!` macro is: /// /// ```text -/// uri := (mount ',')? PATH (':' params)? +/// uri := (prefix ',')? route +/// | prefix ',' route ',' suffix +/// +/// prefix := STRING | expr ; `Origin` or `Absolute` +/// suffix := STRING | expr ; `Reference` or `Absolute` +/// +/// route := PATH '(' (named | unnamed) ')' +/// +/// named := IDENT = expr (',' named)? ','? +/// unnamed := expr (',' unnamed)? ','? /// -/// mount = STRING -/// params := unnamed | named -/// unnamed := expr (',' expr)* -/// named := IDENT = expr (',' named)? /// expr := EXPR | '_' /// /// EXPR := a valid Rust expression (examples: `foo()`, `12`, `"hey"`) @@ -1107,37 +1251,55 @@ pub fn catchers(input: TokenStream) -> TokenStream { /// PATH := a path, as defined by Rust (examples: `route`, `my_mod::route`) /// ``` /// -/// ## Semantics +/// ## Dynamic Semantics /// -/// The `uri!` macro returns an [`Origin`] structure with the URI of the -/// supplied route interpolated with the given values. Note that `Origin` -/// implements `Into` (and by extension, `TryInto`), so it can be -/// converted into a [`Uri`] using `.into()` as needed. +/// The returned value is that of the prefix (minus any query part) concatenated +/// with the route URI concatenated with the query (if the route has no query +/// part) and fragment parts of the suffix. The route URI is generated by +/// interpolating the declared route URI with the URL-safe version of the route +/// values in `uri!()`. The generated URI is guaranteed to be URI-safe. /// -/// 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, -/// after conversion with [`FromUriParam`], or if a value is ignored using `_` -/// and the corresponding route type implements [`Ignorable`]. +/// Each route value is rendered in its appropriate place in the URI using the +/// [`UriDisplay`] implementation for the value's type. The `UriDisplay` +/// implementation ensures that the rendered value is URL-safe. /// -/// Each value passed into `uri!` is rendered in its appropriate place in the -/// URI using the [`UriDisplay`] implementation for the value's type. The -/// `UriDisplay` implementation ensures that the rendered value is URI-safe. +/// A `uri!()` invocation allocated at-most once. /// -/// If a mount-point is provided, the mount-point is prepended to the route's -/// URI. +/// ## Static Semantics +/// +/// The `uri!` macro returns one of [`Origin`], [`Absolute`], or [`Reference`], +/// depending on the types of the prefix and suffix, if any. The table below +/// specifies all combinations: +/// +/// | Prefix | Suffix | Output | +/// |------------|-------------|-------------| +/// | None | None | `Origin` | +/// | None | `Absolute` | `Origin` | +/// | None | `Reference` | `Reference` | +/// | `Origin` | None | `Origin` | +/// | `Origin` | `Absolute` | `Origin` | +/// | `Origin` | `Reference` | `Reference` | +/// | `Absolute` | None | `Absolute` | +/// | `Absolute` | `Absolute` | `Absolute` | +/// | `Absolute` | `Reference` | `Reference` | +/// +/// A `uri!` invocation only typechecks if the type of every route URI value in +/// the invocation matches the type declared for the parameter in the given +/// route, after conversion with [`FromUriParam`], or if a value is ignored +/// using `_` and the corresponding route type implements [`Ignorable`]. /// /// ### Conversion /// /// The [`FromUriParam`] trait is used to typecheck and perform a conversion for -/// each value passed to `uri!`. If a `FromUriParam` implementation exists -/// for a type `T` for part URI part `P`, then a value of type `S` can be used -/// in `uri!` macro for a route URI parameter declared with a type of `T` in -/// part `P`. For example, the following implementation, provided by Rocket, +/// each value passed to `uri!`. If a `FromUriParam for T` implementation +/// exists for a type `T` for part URI part `P`, then a value of type `S` can be +/// used in `uri!` macro for a route URI parameter declared with a type of `T` +/// in part `P`. For example, the following implementation, provided by Rocket, /// allows an `&str` to be used in a `uri!` invocation for route URI parameters /// declared as `String`: /// /// ```rust,ignore -/// impl FromUriParam for String { .. } +/// impl FromUriParam for String { .. } /// ``` /// /// ### Ignorables @@ -1149,6 +1311,10 @@ pub fn catchers(input: TokenStream) -> TokenStream { /// /// [`Uri`]: ../rocket/http/uri/enum.Uri.html /// [`Origin`]: ../rocket/http/uri/struct.Origin.html +/// [`Asterisk`]: ../rocket/http/uri/struct.Asterisk.html +/// [`Authority`]: ../rocket/http/uri/struct.Authority.html +/// [`Absolute`]: ../rocket/http/uri/struct.Absolute.html +/// [`Reference`]: ../rocket/http/uri/struct.Reference.html /// [`FromUriParam`]: ../rocket/http/uri/trait.FromUriParam.html /// [`UriDisplay`]: ../rocket/http/uri/trait.UriDisplay.html /// [`Ignorable`]: ../rocket/http/uri/trait.Ignorable.html diff --git a/core/codegen/src/proc_macro_ext.rs b/core/codegen/src/proc_macro_ext.rs index 26967256..32c13365 100644 --- a/core/codegen/src/proc_macro_ext.rs +++ b/core/codegen/src/proc_macro_ext.rs @@ -70,6 +70,13 @@ impl StringLit { } } +impl crate::syn::parse::Parse for StringLit { + fn parse(input: devise::syn::parse::ParseStream<'_>) -> devise::syn::Result { + let lit = input.parse::()?; + Ok(StringLit::new(lit.value(), lit.span())) + } +} + impl devise::FromMeta for StringLit { fn from_meta(meta: &devise::MetaItem) -> devise::Result { Ok(StringLit::new(String::from_meta(meta)?, meta.value_span())) diff --git a/core/codegen/tests/route-raw.rs b/core/codegen/tests/route-raw.rs index e89c594f..22f608bd 100644 --- a/core/codegen/tests/route-raw.rs +++ b/core/codegen/tests/route-raw.rs @@ -30,14 +30,14 @@ fn test_raw_ident() { let response = client.get("/example?type=1").dispatch(); assert_eq!(response.into_string().unwrap(), "example is 1"); - let uri_named = uri!(get: r#enum = "test_named", r#type = 1); + let uri_named = uri!(get(r#enum = "test_named", r#type = 1)); assert_eq!(uri_named.to_string(), "/test_named?type=1"); - let uri_unnamed = uri!(get: "test_unnamed", 2); + let uri_unnamed = uri!(get("test_unnamed", 2)); assert_eq!(uri_unnamed.to_string(), "/test_unnamed?type=2"); - let uri_raws = uri!(swap: r#raw = "1", r#bare = "2"); + let uri_raws = uri!(swap(r#raw = "1", r#bare = "2")); assert_eq!(uri_raws.to_string(), "/swap/1/2"); - let uri_bare = uri!(swap: raw = "1", bare = "2"); + let uri_bare = uri!(swap(raw = "1", bare = "2")); assert_eq!(uri_bare.to_string(), "/swap/1/2"); } diff --git a/core/codegen/tests/route.rs b/core/codegen/tests/route.rs index f02d285c..36517406 100644 --- a/core/codegen/tests/route.rs +++ b/core/codegen/tests/route.rs @@ -11,7 +11,7 @@ use rocket::request::Request; use rocket::http::ext::Normalize; use rocket::local::blocking::Client; use rocket::data::{self, Data, FromData}; -use rocket::http::{Status, RawStr, ContentType}; +use rocket::http::{Status, RawStr, ContentType, uri::fmt::Path}; // Use all of the code generation available at once. @@ -48,7 +48,7 @@ fn post1( let string = format!("{}, {}, {}, {}, {}, {}", sky, name, a, query.field, path.normalized_str(), simple.0); - let uri = uri!(post1: a, name, path, sky, query); + let uri = uri!(post1(a, name, path, sky, query)); format!("({}) ({})", string, uri.to_string()) } @@ -71,7 +71,7 @@ fn post2( let string = format!("{}, {}, {}, {}, {}, {}", sky, name, a, query.field, path.normalized_str(), simple.0); - let uri = uri!(post2: a, name, path, sky, query); + let uri = uri!(post2(a, name, path, sky, query)); format!("({}) ({})", string, uri.to_string()) } @@ -307,7 +307,7 @@ struct PathString(String); impl FromSegments<'_> for PathString { type Error = std::convert::Infallible; - fn from_segments(segments: Segments<'_>) -> Result { + fn from_segments(segments: Segments<'_, Path>) -> Result { Ok(PathString(segments.collect::>().join("/"))) } diff --git a/core/codegen/tests/typed-uris.rs b/core/codegen/tests/typed-uris.rs index b510129a..4e8cdcb7 100644 --- a/core/codegen/tests/typed-uris.rs +++ b/core/codegen/tests/typed-uris.rs @@ -5,16 +5,17 @@ use std::path::PathBuf; use rocket::http::CookieJar; -use rocket::http::uri::{FromUriParam, Query}; +use rocket::http::uri::fmt::{FromUriParam, Query}; use rocket::form::{Form, error::{Errors, ErrorKind}}; macro_rules! assert_uri_eq { ($($uri:expr => $expected:expr,)+) => { $( let actual = $uri; - let expected = rocket::http::uri::Origin::parse($expected).expect("valid origin URI"); + let expected = rocket::http::uri::Uri::parse_any($expected).expect("valid URI"); if actual != expected { - panic!("URI mismatch: got {}, expected {}", actual, expected); + panic!("URI mismatch: got {}, expected {}\nGot) {:?}\nExpected) {:?}", + actual, expected, actual, expected); } )+ }; @@ -41,6 +42,9 @@ struct Second { nickname: String, } +#[post("/")] +fn index() { } + #[post("/")] fn simple(id: i32) { } @@ -96,187 +100,244 @@ fn guarded_segments(cookies: &CookieJar<'_>, path: PathBuf, id: usize) { } #[test] fn check_simple_unnamed() { assert_uri_eq! { - uri!(simple: 100) => "/100", - uri!(simple: -23) => "/-23", - uri!(unused_param: 1, 2) => "/1/2", + uri!(simple(100)) => "/100", + uri!(simple(-23)) => "/-23", + uri!(unused_param(1, 2)) => "/1/2", } // The "flipped" test ensures that the order of parameters depends on the // route's URI, not on the order in the function signature. assert_uri_eq! { - uri!(simple2: 100, "hello".to_string()) => "/100/hello", - uri!(simple2: 1349, "hey".to_string()) => "/1349/hey", - uri!(simple2_flipped: 100, "hello".to_string()) => "/100/hello", + uri!(simple2(100, "hello".to_string())) => "/100/hello", + uri!(simple2(1349, "hey".to_string())) => "/1349/hey", + uri!(simple2_flipped(100, "hello".to_string())) => "/100/hello", } // Ensure that `.from_uri_param()` is called. assert_uri_eq! { - uri!(simple2: 100, "hello") => "/100/hello", - uri!(simple2_flipped: 1349, "hey") => "/1349/hey", + uri!(simple2(100, "hello")) => "/100/hello", + uri!(simple2_flipped(1349, "hey")) => "/1349/hey", } // Ensure that the `UriDisplay` trait is being used. assert_uri_eq! { - uri!(simple2: 100, "hello there") => "/100/hello%20there", - uri!(simple2_flipped: 100, "hello there") => "/100/hello%20there", + uri!(simple2(100, "hello there")) => "/100/hello%20there", + uri!(simple2_flipped(100, "hello there")) => "/100/hello%20there", } // Ensure that query parameters are handled properly. assert_uri_eq! { - uri!(simple3: 100) => "/?id=100", - uri!(simple3: 1349) => "/?id=1349", - uri!(simple4: 100, "bob") => "/?id=100&name=bob", - uri!(simple4: 1349, "Bob Anderson") => "/?id=1349&name=Bob%20Anderson", - uri!(simple4: -2, "@M+s&OU=") => "/?id=-2&name=@M%2Bs%26OU%3D", - uri!(simple4_flipped: 100, "bob") => "/?id=100&name=bob", - uri!(simple4_flipped: 1349, "Bob Anderson") => "/?id=1349&name=Bob%20Anderson", + uri!(simple3(100)) => "/?id=100", + uri!(simple3(1349)) => "/?id=1349", + uri!(simple4(100, "bob")) => "/?id=100&name=bob", + uri!(simple4(1349, "Bob Anderson")) => "/?id=1349&name=Bob%20Anderson", + uri!(simple4(-2, "@M+s&OU=")) => "/?id=-2&name=@M%2Bs%26OU%3D", + uri!(simple4_flipped(100, "bob")) => "/?id=100&name=bob", + uri!(simple4_flipped(1349, "Bob Anderson")) => "/?id=1349&name=Bob%20Anderson", } } #[test] fn check_simple_named() { assert_uri_eq! { - uri!(simple: id = 100) => "/100", - uri!(simple: id = -23) => "/-23", - uri!(unused_param: used = 1, _unused = 2) => "/1/2", + uri!(simple(id = 100)) => "/100", + uri!(simple(id = -23)) => "/-23", + uri!(unused_param(used = 1, _unused = 2)) => "/1/2", } assert_uri_eq! { - uri!(simple2: id = 100, name = "hello".to_string()) => "/100/hello", - uri!(simple2: name = "hi".to_string(), id = 123) => "/123/hi", - uri!(simple2_flipped: id = 1349, name = "hey".to_string()) => "/1349/hey", - uri!(simple2_flipped: name = "hello".to_string(), id = 100) => "/100/hello", + uri!(simple2(id = 100, name = "hello".to_string())) => "/100/hello", + uri!(simple2(name = "hi".to_string(), id = 123)) => "/123/hi", + uri!(simple2_flipped(id = 1349, name = "hey".to_string())) => "/1349/hey", + uri!(simple2_flipped(name = "hello".to_string(), id = 100)) => "/100/hello", } // Ensure that `.from_uri_param()` is called. assert_uri_eq! { - uri!(simple2: id = 100, name = "hello") => "/100/hello", - uri!(simple2: id = 100, name = "hi") => "/100/hi", - uri!(simple2: id = 1349, name = "hey") => "/1349/hey", - uri!(simple2: name = "hello", id = 100) => "/100/hello", - uri!(simple2: name = "hi", id = 100) => "/100/hi", - uri!(simple2_flipped: id = 1349, name = "hey") => "/1349/hey", + uri!(simple2(id = 100, name = "hello")) => "/100/hello", + uri!(simple2(id = 100, name = "hi")) => "/100/hi", + uri!(simple2(id = 1349, name = "hey")) => "/1349/hey", + uri!(simple2(name = "hello", id = 100)) => "/100/hello", + uri!(simple2(name = "hi", id = 100)) => "/100/hi", + uri!(simple2_flipped(id = 1349, name = "hey")) => "/1349/hey", } // Ensure that the `UriDisplay` trait is being used. assert_uri_eq! { - uri!(simple2: id = 100, name = "hello there") => "/100/hello%20there", - uri!(simple2: name = "hello there", id = 100) => "/100/hello%20there", - uri!(simple2_flipped: id = 100, name = "hello there") => "/100/hello%20there", - uri!(simple2_flipped: name = "hello there", id = 100) => "/100/hello%20there", + uri!(simple2(id = 100, name = "hello there")) => "/100/hello%20there", + uri!(simple2(name = "hello there", id = 100)) => "/100/hello%20there", + uri!(simple2_flipped(id = 100, name = "hello there")) => "/100/hello%20there", + uri!(simple2_flipped(name = "hello there", id = 100)) => "/100/hello%20there", } // Ensure that query parameters are handled properly. assert_uri_eq! { - uri!(simple3: id = 100) => "/?id=100", - uri!(simple3: id = 1349) => "/?id=1349", - uri!(simple4: id = 100, name = "bob") => "/?id=100&name=bob", - uri!(simple4: id = 1349, name = "Bob A") => "/?id=1349&name=Bob%20A", - uri!(simple4: name = "Bob A", id = 1349) => "/?id=1349&name=Bob%20A", - uri!(simple4_flipped: id = 1349, name = "Bob A") => "/?id=1349&name=Bob%20A", - uri!(simple4_flipped: name = "Bob A", id = 1349) => "/?id=1349&name=Bob%20A", + uri!(simple3(id = 100)) => "/?id=100", + uri!(simple3(id = 1349)) => "/?id=1349", + uri!(simple4(id = 100, name = "bob")) => "/?id=100&name=bob", + uri!(simple4(id = 1349, name = "Bob A")) => "/?id=1349&name=Bob%20A", + uri!(simple4(name = "Bob A", id = 1349)) => "/?id=1349&name=Bob%20A", + uri!(simple4_flipped(id = 1349, name = "Bob A")) => "/?id=1349&name=Bob%20A", + uri!(simple4_flipped(name = "Bob A", id = 1349)) => "/?id=1349&name=Bob%20A", } } #[test] -fn check_mount_point() { +fn check_route_prefix_suffix() { assert_uri_eq! { - uri!("/mount", simple: 100) => "/mount/100", - uri!("/mount", simple: id = 23) => "/mount/23", - uri!("/another", simple: 100) => "/another/100", - uri!("/another", simple: id = 23) => "/another/23", + uri!(index) => "/", + uri!("/", index) => "/", + uri!("/hi", index) => "/hi", + uri!("/", simple3(10)) => "/?id=10", + uri!("/hi", simple3(11)) => "/hi?id=11", + uri!("/mount", simple(100)) => "/mount/100", + uri!("/mount", simple(id = 23)) => "/mount/23", + uri!("/another", simple(100)) => "/another/100", + uri!("/another", simple(id = 23)) => "/another/23", } assert_uri_eq! { - uri!("/a", simple2: 100, "hey") => "/a/100/hey", - uri!("/b", simple2: id = 23, name = "hey") => "/b/23/hey", + uri!("http://rocket.rs", index) => "http://rocket.rs", + uri!("http://rocket.rs/", index) => "http://rocket.rs", + uri!("http://rocket.rs", index) => "http://rocket.rs", + uri!("http://", index) => "http://", + uri!("ftp:", index) => "ftp:/", + } + + assert_uri_eq! { + uri!("http://rocket.rs", index, "?foo") => "http://rocket.rs?foo", + uri!("http://rocket.rs/", index, "#bar") => "http://rocket.rs#bar", + uri!("http://rocket.rs", index, "?bar#baz") => "http://rocket.rs?bar#baz", + uri!("http://rocket.rs/", index, "?bar#baz") => "http://rocket.rs?bar#baz", + uri!("http://", index, "?foo") => "http://?foo", + uri!("http://rocket.rs", simple3(id = 100), "?foo") => "http://rocket.rs?id=100", + uri!("http://rocket.rs", simple3(id = 100), "?foo#bar") => "http://rocket.rs?id=100#bar", + uri!(_, simple3(id = 100), "?foo#bar") => "/?id=100#bar", + } + + let dyn_origin = uri!("/a/b/c"); + let dyn_origin2 = uri!("/a/b/c?foo-bar"); + assert_uri_eq! { + uri!(dyn_origin.clone(), index) => "/a/b/c", + uri!(dyn_origin2.clone(), index) => "/a/b/c", + uri!(dyn_origin.clone(), simple3(10)) => "/a/b/c?id=10", + uri!(dyn_origin2.clone(), simple3(10)) => "/a/b/c?id=10", + uri!(dyn_origin.clone(), simple(100)) => "/a/b/c/100", + uri!(dyn_origin2.clone(), simple(100)) => "/a/b/c/100", + uri!(dyn_origin.clone(), simple2(100, "hey")) => "/a/b/c/100/hey", + uri!(dyn_origin2.clone(), simple2(100, "hey")) => "/a/b/c/100/hey", + uri!(dyn_origin.clone(), simple2(id = 23, name = "hey")) => "/a/b/c/23/hey", + uri!(dyn_origin2.clone(), simple2(id = 23, name = "hey")) => "/a/b/c/23/hey", + } + + let dyn_absolute = uri!("http://rocket.rs"); + assert_uri_eq! { + uri!(dyn_absolute.clone(), index) => "http://rocket.rs", + uri!(uri!("http://rocket.rs/a/b"), index) => "http://rocket.rs/a/b", + } + + let dyn_abs = uri!("http://rocket.rs?foo"); + assert_uri_eq! { + uri!(_, index, dyn_abs.clone()) => "/?foo", + uri!("http://rocket.rs/", index, dyn_abs.clone()) => "http://rocket.rs?foo", + uri!("http://rocket.rs", index, dyn_abs.clone()) => "http://rocket.rs?foo", + uri!("http://", index, dyn_abs.clone()) => "http://?foo", + uri!(_, simple3(id = 123), dyn_abs) => "/?id=123", + } + + let dyn_ref = uri!("?foo#bar"); + assert_uri_eq! { + uri!(_, index, dyn_ref.clone()) => "/?foo#bar", + uri!("http://rocket.rs/", index, dyn_ref.clone()) => "http://rocket.rs?foo#bar", + uri!("http://rocket.rs", index, dyn_ref.clone()) => "http://rocket.rs?foo#bar", + uri!("http://", index, dyn_ref.clone()) => "http://?foo#bar", + uri!(_, simple3(id = 123), dyn_ref) => "/?id=123#bar", } } #[test] fn check_guards_ignored() { assert_uri_eq! { - uri!("/mount", guard_1: 100) => "/mount/100", - uri!("/mount", guard_2: 2938, "boo") => "/mount/2938/boo", - uri!("/mount", guard_3: 340, "Bob") => "/mount/a/340/hi/Bob/hey", - uri!(guard_1: 100) => "/100", - uri!(guard_2: 2938, "boo") => "/2938/boo", - uri!(guard_3: 340, "Bob") => "/a/340/hi/Bob/hey", - uri!("/mount", guard_1: id = 100) => "/mount/100", - uri!("/mount", guard_2: id = 2938, name = "boo") => "/mount/2938/boo", - uri!("/mount", guard_3: id = 340, name = "Bob") => "/mount/a/340/hi/Bob/hey", - uri!(guard_1: id = 100) => "/100", - uri!(guard_2: name = "boo", id = 2938) => "/2938/boo", - uri!(guard_3: name = "Bob", id = 340) => "/a/340/hi/Bob/hey", + uri!("/mount", guard_1(100)) => "/mount/100", + uri!("/mount", guard_2(2938, "boo")) => "/mount/2938/boo", + uri!("/mount", guard_3(340, "Bob")) => "/mount/a/340/hi/Bob/hey", + uri!(guard_1(100)) => "/100", + uri!(guard_2(2938, "boo")) => "/2938/boo", + uri!(guard_3(340, "Bob")) => "/a/340/hi/Bob/hey", + uri!("/mount", guard_1(id = 100)) => "/mount/100", + uri!("/mount", guard_2(id = 2938, name = "boo")) => "/mount/2938/boo", + uri!("/mount", guard_3(id = 340, name = "Bob")) => "/mount/a/340/hi/Bob/hey", + uri!(guard_1(id = 100)) => "/100", + uri!(guard_2(name = "boo", id = 2938)) => "/2938/boo", + uri!(guard_3(name = "Bob", id = 340)) => "/a/340/hi/Bob/hey", } } #[test] fn check_with_segments() { assert_uri_eq! { - uri!(segments: PathBuf::from("one/two/three")) => "/a/one/two/three", - uri!(segments: path = PathBuf::from("one/two/three")) => "/a/one/two/three", - uri!("/c", segments: PathBuf::from("one/tw o/")) => "/c/a/one/tw%20o", - uri!("/c", segments: path = PathBuf::from("one/tw o/")) => "/c/a/one/tw%20o", - uri!(segments: PathBuf::from("one/ tw?o/")) => "/a/one/%20tw%3Fo", - uri!(param_and_segments: 10, PathBuf::from("a/b")) => "/a/10/then/a/b", - uri!(param_and_segments: id = 10, path = PathBuf::from("a/b")) - => "/a/10/then/a/b", - uri!(guarded_segments: 10, PathBuf::from("a/b")) => "/a/10/then/a/b", - uri!(guarded_segments: id = 10, path = PathBuf::from("a/b")) - => "/a/10/then/a/b", + uri!(segments(PathBuf::from("one/two/three"))) => "/a/one/two/three", + uri!(segments(path = PathBuf::from("one/two/three"))) => "/a/one/two/three", + uri!("/c", segments(PathBuf::from("one/tw o/"))) => "/c/a/one/tw%20o", + uri!("/c", segments(path = PathBuf::from("one/tw o/"))) => "/c/a/one/tw%20o", + uri!(segments(PathBuf::from("one/ tw?o/"))) => "/a/one/%20tw%3Fo", + uri!(param_and_segments(10, PathBuf::from("a/b"))) => "/a/10/then/a/b", + uri!(param_and_segments(id = 10, path = PathBuf::from("a/b"))) => "/a/10/then/a/b", + uri!(guarded_segments(10, PathBuf::from("a/b"))) => "/a/10/then/a/b", + uri!(guarded_segments(id = 10, path = PathBuf::from("a/b"))) => "/a/10/then/a/b", } // Now check the `from_uri_param()` conversions for `PathBuf`. assert_uri_eq! { - uri!(segments: "one/two/three") => "/a/one/two/three", - uri!("/oh", segments: path = "one/two/three") => "/oh/a/one/two/three", - uri!(segments: "one/ tw?o/") => "/a/one/%20tw%3Fo", - uri!(param_and_segments: id = 10, path = "a/b") => "/a/10/then/a/b", - uri!(guarded_segments: 10, "a/b") => "/a/10/then/a/b", - uri!(guarded_segments: id = 10, path = "a/b") => "/a/10/then/a/b", + uri!(segments("one/two/three")) => "/a/one/two/three", + uri!("/", segments(path = "one/two/three")) => "/a/one/two/three", + uri!("/oh", segments(path = "one/two/three")) => "/oh/a/one/two/three", + uri!(segments("one/ tw?o/")) => "/a/one/%20tw%3Fo", + uri!(param_and_segments(id = 10, path = "a/b")) => "/a/10/then/a/b", + uri!(guarded_segments(10, "a/b")) => "/a/10/then/a/b", + uri!(guarded_segments(id = 10, path = "a/b")) => "/a/10/then/a/b", } } #[test] fn check_complex() { assert_uri_eq! { - uri!(complex: "no idea", 10, "high", ("A B C", "a c")) => + uri!(complex("no idea", 10, "high", ("A B C", "a c"))) => "/name/no%20idea?foo=10&type=10&type=high&name=A%20B%20C&nickname=a%20c", - uri!(complex: "Bob", 248, "?", User { name: "Robert".into(), nickname: "Bob".into() }) => + uri!(complex("Bob", 248, "?", User { name: "Robert".into(), nickname: "Bob".into() })) => "/name/Bob?foo=248&type=10&type=%3F&name=Robert&nickname=Bob", - uri!(complex: "Bob", 248, "a a", &User { name: "Robert".into(), nickname: "B".into() }) => + uri!(complex("Bob", 248, "a a", &User { name: "Robert".into(), nickname: "B".into() })) => "/name/Bob?foo=248&type=10&type=a%20a&name=Robert&nickname=B", - uri!(complex: "no idea", 248, "", &User { name: "A B".into(), nickname: "A".into() }) => + uri!(complex("no idea", 248, "", &User { name: "A B".into(), nickname: "A".into() })) => "/name/no%20idea?foo=248&type=10&type=&name=A%20B&nickname=A", - uri!(complex: "hi", 3, "b", &User { name: "A B C".into(), nickname: "a b".into() }) => + uri!(complex("hi", 3, "b", &User { name: "A B C".into(), nickname: "a b".into() })) => "/name/hi?foo=3&type=10&type=b&name=A%20B%20C&nickname=a%20b", - uri!(complex: name = "no idea", foo = 10, r#type = "high", query = ("A B C", "a c")) => + uri!(complex(name = "no idea", foo = 10, r#type = "high", query = ("A B C", "a c"))) => "/name/no%20idea?foo=10&type=10&type=high&name=A%20B%20C&nickname=a%20c", - uri!(complex: foo = 10, name = "no idea", r#type = "high", query = ("A B C", "a c")) => + uri!(complex(foo = 10, name = "no idea", r#type = "high", query = ("A B C", "a c"))) => "/name/no%20idea?foo=10&type=10&type=high&name=A%20B%20C&nickname=a%20c", - uri!(complex: query = ("A B C", "a c"), foo = 10, name = "no idea", r#type = "high", ) => + uri!(complex(query = ("A B C", "a c"), foo = 10, name = "no idea", r#type = "high", )) => "/name/no%20idea?foo=10&type=10&type=high&name=A%20B%20C&nickname=a%20c", - uri!(complex: query = ("A B C", "a c"), foo = 10, name = "no idea", r#type = "high") => + uri!(complex(query = ("A B C", "a c"), foo = 10, name = "no idea", r#type = "high")) => "/name/no%20idea?foo=10&type=10&type=high&name=A%20B%20C&nickname=a%20c", - uri!(complex: query = *&("A B C", "a c"), foo = 10, name = "no idea", r#type = "high") => + uri!(complex(query = *&("A B C", "a c"), foo = 10, name = "no idea", r#type = "high")) => "/name/no%20idea?foo=10&type=10&type=high&name=A%20B%20C&nickname=a%20c", - uri!(complex: foo = 3, name = "hi", r#type = "b", - query = &User { name: "A B C".into(), nickname: "a b".into() }) => + uri!(complex(foo = 3, name = "hi", r#type = "b", + query = &User { name: "A B C".into(), nickname: "a b".into() })) => "/name/hi?foo=3&type=10&type=b&name=A%20B%20C&nickname=a%20b", - uri!(complex: query = &User { name: "A B C".into(), nickname: "a b".into() }, - foo = 3, name = "hi", r#type = "b") => + uri!(complex(query = &User { name: "A B C".into(), nickname: "a b".into() }, + foo = 3, name = "hi", r#type = "b")) => "/name/hi?foo=3&type=10&type=b&name=A%20B%20C&nickname=a%20b", } // Ensure variables are correctly processed. let user = User { name: "Robert".into(), nickname: "Bob".into() }; assert_uri_eq! { - uri!(complex: "complex", 0, "high", &user) => + uri!(complex("complex", 0, "high", &user)) => "/name/complex?foo=0&type=10&type=high&name=Robert&nickname=Bob", - uri!(complex: "complex", 0, "high", &user) => + uri!(complex("complex", 0, "high", &user)) => "/name/complex?foo=0&type=10&type=high&name=Robert&nickname=Bob", - uri!(complex: "complex", 0, "high", user) => + uri!(complex("complex", 0, "high", user)) => "/name/complex?foo=0&type=10&type=high&name=Robert&nickname=Bob", } } @@ -290,42 +351,42 @@ fn check_location_promotion() { let s2 = S2 { name: "Bob".into() }; assert_uri_eq! { - uri!(simple2: 1, &S1("A".into()).0) => "/1/A", - uri!(simple2: 1, &mut S1("A".into()).0) => "/1/A", - uri!(simple2: 1, S1("A".into()).0) => "/1/A", - uri!(simple2: 1, &S2 { name: "A".into() }.name) => "/1/A", - uri!(simple2: 1, &mut S2 { name: "A".into() }.name) => "/1/A", - uri!(simple2: 1, S2 { name: "A".into() }.name) => "/1/A", - uri!(simple2: 1, &s1.0) => "/1/Bob", - uri!(simple2: 1, &s2.name) => "/1/Bob", - uri!(simple2: 2, &s1.0) => "/2/Bob", - uri!(simple2: 2, &s2.name) => "/2/Bob", - uri!(simple2: 2, s1.0) => "/2/Bob", - uri!(simple2: 2, s2.name) => "/2/Bob", + uri!(simple2(1, &S1("A".into()).0)) => "/1/A", + uri!(simple2(1, &mut S1("A".into()).0)) => "/1/A", + uri!(simple2(1, S1("A".into()).0)) => "/1/A", + uri!(simple2(1, &S2 { name: "A".into() }.name)) => "/1/A", + uri!(simple2(1, &mut S2 { name: "A".into() }.name)) => "/1/A", + uri!(simple2(1, S2 { name: "A".into() }.name)) => "/1/A", + uri!(simple2(1, &s1.0)) => "/1/Bob", + uri!(simple2(1, &s2.name)) => "/1/Bob", + uri!(simple2(2, &s1.0)) => "/2/Bob", + uri!(simple2(2, &s2.name)) => "/2/Bob", + uri!(simple2(2, s1.0)) => "/2/Bob", + uri!(simple2(2, s2.name)) => "/2/Bob", } let mut s1 = S1("Bob".into()); let mut s2 = S2 { name: "Bob".into() }; assert_uri_eq! { - uri!(simple2: 1, &mut S1("A".into()).0) => "/1/A", - uri!(simple2: 1, S1("A".into()).0) => "/1/A", - uri!(simple2: 1, &mut S2 { name: "A".into() }.name) => "/1/A", - uri!(simple2: 1, S2 { name: "A".into() }.name) => "/1/A", - uri!(simple2: 1, &mut s1.0) => "/1/Bob", - uri!(simple2: 1, &mut s2.name) => "/1/Bob", - uri!(simple2: 2, &mut s1.0) => "/2/Bob", - uri!(simple2: 2, &mut s2.name) => "/2/Bob", - uri!(simple2: 2, s1.0) => "/2/Bob", - uri!(simple2: 2, s2.name) => "/2/Bob", + uri!(simple2(1, &mut S1("A".into()).0)) => "/1/A", + uri!(simple2(1, S1("A".into()).0)) => "/1/A", + uri!(simple2(1, &mut S2 { name: "A".into() }.name)) => "/1/A", + uri!(simple2(1, S2 { name: "A".into() }.name)) => "/1/A", + uri!(simple2(1, &mut s1.0)) => "/1/Bob", + uri!(simple2(1, &mut s2.name)) => "/1/Bob", + uri!(simple2(2, &mut s1.0)) => "/2/Bob", + uri!(simple2(2, &mut s2.name)) => "/2/Bob", + uri!(simple2(2, s1.0)) => "/2/Bob", + uri!(simple2(2, s2.name)) => "/2/Bob", } } #[test] fn check_scoped() { assert_uri_eq!{ - uri!(typed_uris::simple: 100) => "/typed_uris/100", - uri!(typed_uris::simple: id = 100) => "/typed_uris/100", - uri!(typed_uris::deeper::simple: 100) => "/typed_uris/deeper/100", + uri!(typed_uris::simple(100)) => "/typed_uris/100", + uri!(typed_uris::simple(id = 100)) => "/typed_uris/100", + uri!(typed_uris::deeper::simple(100)) => "/typed_uris/deeper/100", } } @@ -336,10 +397,10 @@ mod typed_uris { #[test] fn check_simple_scoped() { assert_uri_eq! { - uri!(simple: id = 100) => "/typed_uris/100", - uri!(crate::simple: id = 100) => "/100", - uri!("/mount", crate::simple: id = 100) => "/mount/100", - uri!(crate::simple2: id = 100, name = "hello") => "/100/hello", + uri!(simple(id = 100)) => "/typed_uris/100", + uri!(crate::simple(id = 100)) => "/100", + uri!("/mount", crate::simple(id = 100)) => "/mount/100", + uri!(crate::simple2(id = 100, name = "hello")) => "/100/hello", } } @@ -350,8 +411,8 @@ mod typed_uris { #[test] fn check_deep_scoped() { assert_uri_eq! { - uri!(super::simple: id = 100) => "/typed_uris/100", - uri!(crate::simple: id = 100) => "/100", + uri!(super::simple(id = 100)) => "/typed_uris/100", + uri!(crate::simple(id = 100)) => "/100", } } } @@ -376,68 +437,68 @@ fn test_optional_uri_parameters() { let mut some_10 = Some(10); let mut third = Third { one: "hi there".into(), two: "a b".into() }; assert_uri_eq! { - uri!(optionals: + uri!(optionals( foo = 10, bar = &"hi there", q1 = Some(10), - rest = Some(Third { one: "hi there".into(), two: "a b".into() }) - ) => "/10/hi%20there?q1=10&one=hi%20there&two=a%20b", + rest = Some(Third { one: "hi there".into(), two: "a b".into() }), + )) => "/10/hi%20there?q1=10&one=hi%20there&two=a%20b", - uri!(optionals: + uri!(optionals( foo = &10, bar = &"hi there", q1 = Some(&10), - rest = Some(&third) - ) => "/10/hi%20there?q1=10&one=hi%20there&two=a%20b", + rest = Some(&third), + )) => "/10/hi%20there?q1=10&one=hi%20there&two=a%20b", - uri!(optionals: + uri!(optionals( foo = &mut 10, bar = &mut "hi there", q1 = some_10.as_mut(), - rest = Some(&mut third) - ) => "/10/hi%20there?q1=10&one=hi%20there&two=a%20b", + rest = Some(&mut third), + )) => "/10/hi%20there?q1=10&one=hi%20there&two=a%20b", - uri!(optionals: + uri!(optionals( foo = 10, bar = &"hi there", q1 = _, - rest = Some(Third { one: "hi there".into(), two: "a b".into() }) - ) => "/10/hi%20there?one=hi%20there&two=a%20b", + rest = Some(Third { one: "hi there".into(), two: "a b".into() }), + )) => "/10/hi%20there?one=hi%20there&two=a%20b", - uri!(optionals: + uri!(optionals( foo = 10, bar = &"hi there", q1 = Some(10), - rest = _ - ) => "/10/hi%20there?q1=10", + rest = _, + )) => "/10/hi%20there?q1=10", - uri!(optionals: + uri!(optionals( foo = 10, bar = &"hi there", q1 = Err(ErrorKind::Missing.into()) as Result, - rest = _ - ) => "/10/hi%20there", + rest = _, + )) => "/10/hi%20there", - uri!(optionals: + uri!(optionals( foo = 10, bar = &"hi there", q1 = None as Option, rest = _ - ) => "/10/hi%20there", + )) => "/10/hi%20there", - uri!(optionals: + uri!(optionals( foo = 10, bar = &"hi there", q1 = _, rest = None as Option>, - ) => "/10/hi%20there", + )) => "/10/hi%20there", - uri!(optionals: + uri!(optionals( foo = 10, bar = &"hi there", q1 = _, rest = _, - ) => "/10/hi%20there", + )) => "/10/hi%20there", } } @@ -445,15 +506,15 @@ fn test_optional_uri_parameters() { fn test_simple_ignored() { #[get("/<_>")] fn ignore_one() { } assert_uri_eq! { - uri!(ignore_one: 100) => "/100", - uri!(ignore_one: "hello") => "/hello", - uri!(ignore_one: "cats r us") => "/cats%20r%20us", + uri!(ignore_one(100)) => "/100", + uri!(ignore_one("hello")) => "/hello", + uri!(ignore_one("cats r us")) => "/cats%20r%20us", } #[get("/<_>/<_>")] fn ignore_two() { } assert_uri_eq! { - uri!(ignore_two: 100, "boop") => "/100/boop", - uri!(ignore_two: &"hi", "bop") => "/hi/bop", + uri!(ignore_two(100, "boop")) => "/100/boop", + uri!(ignore_two(&"hi", "bop")) => "/hi/bop", } #[get("/<_>/foo/<_>")] fn ignore_inner_two() { } @@ -461,9 +522,9 @@ fn test_simple_ignored() { #[get("/hey/hi/<_>/foo/<_>")] fn ignore_inner_two_b() { } assert_uri_eq! { - uri!(ignore_inner_two: 100, "boop") => "/100/foo/boop", - uri!(ignore_inner_one_a: "!?") => "/hi/!%3F/foo", - uri!(ignore_inner_two_b: &mut 5, "boo") => "/hey/hi/5/foo/boo", + uri!(ignore_inner_two(100, "boop")) => "/100/foo/boop", + uri!(ignore_inner_one_a("!?")) => "/hi/!%3F/foo", + uri!(ignore_inner_two_b(&mut 5, "boo")) => "/hey/hi/5/foo/boo", } #[get("/<_>/foo/<_>?hi")] fn ignore_with_q() { } @@ -471,8 +532,8 @@ fn test_simple_ignored() { #[get("/hi/<_>/foo/<_>?&")] fn ignore_with_q3(hi: &str, hey: &str) { } assert_uri_eq! { - uri!(ignore_with_q: 100, "boop") => "/100/foo/boop?hi", - uri!(ignore_with_q2: "!?", "bop", Some(3usize)) => "/hi/!%3F/foo/bop?hi&hey=3", - uri!(ignore_with_q3: &mut 5, "boo", "hi b", "ho") => "/hi/5/foo/boo?hi=hi%20b&hey=ho", + uri!(ignore_with_q(100, "boop")) => "/100/foo/boop?hi", + uri!(ignore_with_q2("!?", "bop", Some(3usize))) => "/hi/!%3F/foo/bop?hi&hey=3", + uri!(ignore_with_q3(&mut 5, "boo", "hi b", "ho")) => "/hi/5/foo/boo?hi=hi%20b&hey=ho", } } diff --git a/core/codegen/tests/ui-fail-nightly/typed-uri-bad-type.stderr b/core/codegen/tests/ui-fail-nightly/typed-uri-bad-type.stderr index 19ebe0e8..80f0077c 100644 --- a/core/codegen/tests/ui-fail-nightly/typed-uri-bad-type.stderr +++ b/core/codegen/tests/ui-fail-nightly/typed-uri-bad-type.stderr @@ -1,8 +1,8 @@ -error[E0277]: the trait bound `usize: FromUriParam` is not satisfied - --> $DIR/typed-uri-bad-type.rs:45:23 +error[E0277]: the trait bound `usize: FromUriParam` is not satisfied + --> $DIR/typed-uri-bad-type.rs:45:22 | -45 | uri!(simple: id = "hi"); - | ^^^^ the trait `FromUriParam` is not implemented for `usize` +45 | uri!(simple(id = "hi")); + | ^^^^ the trait `FromUriParam` is not implemented for `usize` | = help: the following implementations were found: > @@ -10,11 +10,11 @@ error[E0277]: the trait bound `usize: FromUriParam> = note: required by `from_uri_param` -error[E0277]: the trait bound `usize: FromUriParam` is not satisfied - --> $DIR/typed-uri-bad-type.rs:47:18 +error[E0277]: the trait bound `usize: FromUriParam` is not satisfied + --> $DIR/typed-uri-bad-type.rs:47:17 | -47 | uri!(simple: "hello"); - | ^^^^^^^ the trait `FromUriParam` is not implemented for `usize` +47 | uri!(simple("hello")); + | ^^^^^^^ the trait `FromUriParam` is not implemented for `usize` | = help: the following implementations were found: > @@ -22,11 +22,11 @@ error[E0277]: the trait bound `usize: FromUriParam> = note: required by `from_uri_param` -error[E0277]: the trait bound `usize: FromUriParam` is not satisfied - --> $DIR/typed-uri-bad-type.rs:49:23 +error[E0277]: the trait bound `usize: FromUriParam` is not satisfied + --> $DIR/typed-uri-bad-type.rs:49:22 | -49 | uri!(simple: id = 239239i64); - | ^^^^^^^^^ the trait `FromUriParam` is not implemented for `usize` +49 | uri!(simple(id = 239239i64)); + | ^^^^^^^^^ the trait `FromUriParam` is not implemented for `usize` | = help: the following implementations were found: > @@ -34,32 +34,32 @@ error[E0277]: the trait bound `usize: FromUriParam > = note: required by `from_uri_param` -error[E0277]: the trait bound `S: FromUriParam` is not satisfied - --> $DIR/typed-uri-bad-type.rs:51:31 +error[E0277]: the trait bound `S: FromUriParam` is not satisfied + --> $DIR/typed-uri-bad-type.rs:51:30 | -51 | uri!(not_uri_display: 10, S); - | ^ the trait `FromUriParam` is not implemented for `S` +51 | uri!(not_uri_display(10, S)); + | ^ the trait `FromUriParam` is not implemented for `S` | = note: required by `from_uri_param` -error[E0277]: the trait bound `i32: FromUriParam>` is not satisfied - --> $DIR/typed-uri-bad-type.rs:56:26 +error[E0277]: the trait bound `i32: FromUriParam>` is not satisfied + --> $DIR/typed-uri-bad-type.rs:56:25 | -56 | uri!(optionals: id = Some(10), name = Ok("bob".into())); - | ^^^^^^^^ the trait `FromUriParam>` is not implemented for `i32` +56 | uri!(optionals(id = Some(10), name = Ok("bob".into()))); + | ^^^^^^^^ the trait `FromUriParam>` is not implemented for `i32` | = help: the following implementations were found: > > > - = note: required because of the requirements on the impl of `FromUriParam>` for `std::option::Option` + = note: required because of the requirements on the impl of `FromUriParam>` for `std::option::Option` = note: required by `from_uri_param` -error[E0277]: the trait bound `std::string::String: FromUriParam>` is not satisfied - --> $DIR/typed-uri-bad-type.rs:56:43 +error[E0277]: the trait bound `std::string::String: FromUriParam>` is not satisfied + --> $DIR/typed-uri-bad-type.rs:56:42 | -56 | uri!(optionals: id = Some(10), name = Ok("bob".into())); - | ^^^^^^^^^^^^^^^^ the trait `FromUriParam>` is not implemented for `std::string::String` +56 | uri!(optionals(id = Some(10), name = Ok("bob".into()))); + | ^^^^^^^^^^^^^^^^ the trait `FromUriParam>` is not implemented for `std::string::String` | = help: the following implementations were found: > @@ -67,14 +67,14 @@ error[E0277]: the trait bound `std::string::String: FromUriParam> > and 2 others - = note: required because of the requirements on the impl of `FromUriParam>` for `Result` + = note: required because of the requirements on the impl of `FromUriParam>` for `Result` = note: required by `from_uri_param` -error[E0277]: the trait bound `isize: FromUriParam` is not satisfied - --> $DIR/typed-uri-bad-type.rs:58:20 +error[E0277]: the trait bound `isize: FromUriParam` is not satisfied + --> $DIR/typed-uri-bad-type.rs:58:19 | -58 | uri!(simple_q: "hi"); - | ^^^^ the trait `FromUriParam` is not implemented for `isize` +58 | uri!(simple_q("hi")); + | ^^^^ the trait `FromUriParam` is not implemented for `isize` | = help: the following implementations were found: > @@ -82,11 +82,11 @@ error[E0277]: the trait bound `isize: FromUriParam> = note: required by `from_uri_param` -error[E0277]: the trait bound `isize: FromUriParam` is not satisfied - --> $DIR/typed-uri-bad-type.rs:60:25 +error[E0277]: the trait bound `isize: FromUriParam` is not satisfied + --> $DIR/typed-uri-bad-type.rs:60:24 | -60 | uri!(simple_q: id = "hi"); - | ^^^^ the trait `FromUriParam` is not implemented for `isize` +60 | uri!(simple_q(id = "hi")); + | ^^^^ the trait `FromUriParam` is not implemented for `isize` | = help: the following implementations were found: > @@ -94,48 +94,120 @@ error[E0277]: the trait bound `isize: FromUriParam> = note: required by `from_uri_param` -error[E0277]: the trait bound `S: FromUriParam` is not satisfied - --> $DIR/typed-uri-bad-type.rs:62:24 +error[E0277]: the trait bound `S: FromUriParam` is not satisfied + --> $DIR/typed-uri-bad-type.rs:62:23 | -62 | uri!(other_q: 100, S); - | ^ the trait `FromUriParam` is not implemented for `S` +62 | uri!(other_q(100, S)); + | ^ the trait `FromUriParam` is not implemented for `S` | = note: required by `from_uri_param` -error[E0277]: the trait bound `S: FromUriParam` is not satisfied - --> $DIR/typed-uri-bad-type.rs:64:26 +error[E0277]: the trait bound `S: FromUriParam` is not satisfied + --> $DIR/typed-uri-bad-type.rs:64:25 | -64 | uri!(other_q: rest = S, id = 100); - | ^ the trait `FromUriParam` is not implemented for `S` +64 | uri!(other_q(rest = S, id = 100)); + | ^ the trait `FromUriParam` is not implemented for `S` | = note: required by `from_uri_param` -error[E0277]: the trait bound `S: Ignorable` is not satisfied - --> $DIR/typed-uri-bad-type.rs:66:26 +error[E0277]: the trait bound `S: Ignorable` is not satisfied + --> $DIR/typed-uri-bad-type.rs:66:25 | -66 | uri!(other_q: rest = _, id = 100); - | ^ the trait `Ignorable` is not implemented for `S` +66 | uri!(other_q(rest = _, id = 100)); + | ^ the trait `Ignorable` is not implemented for `S` | - ::: $WORKSPACE/core/http/src/uri/uri_display.rs + ::: $WORKSPACE/core/http/src/uri/fmt/uri_display.rs | - | pub fn assert_ignorable>() { } - | ------------ required by this bound in `assert_ignorable` + | pub fn assert_ignorable>() { } + | ------------ required by this bound in `assert_ignorable` -error[E0277]: the trait bound `usize: Ignorable` is not satisfied - --> $DIR/typed-uri-bad-type.rs:68:34 +error[E0277]: the trait bound `usize: Ignorable` is not satisfied + --> $DIR/typed-uri-bad-type.rs:68:33 | -68 | uri!(other_q: rest = S, id = _); - | ^ the trait `Ignorable` is not implemented for `usize` +68 | uri!(other_q(rest = S, id = _)); + | ^ the trait `Ignorable` is not implemented for `usize` | - ::: $WORKSPACE/core/http/src/uri/uri_display.rs + ::: $WORKSPACE/core/http/src/uri/fmt/uri_display.rs | - | pub fn assert_ignorable>() { } - | ------------ required by this bound in `assert_ignorable` + | pub fn assert_ignorable>() { } + | ------------ required by this bound in `assert_ignorable` -error[E0277]: the trait bound `S: FromUriParam` is not satisfied - --> $DIR/typed-uri-bad-type.rs:68:26 +error[E0277]: the trait bound `S: FromUriParam` is not satisfied + --> $DIR/typed-uri-bad-type.rs:68:25 | -68 | uri!(other_q: rest = S, id = _); - | ^ the trait `FromUriParam` is not implemented for `S` +68 | uri!(other_q(rest = S, id = _)); + | ^ the trait `FromUriParam` is not implemented for `S` | = note: required by `from_uri_param` + +error[E0277]: the trait bound `usize: FromUriParam` is not satisfied + --> $DIR/typed-uri-bad-type.rs:77:40 + | +77 | uri!(uri!("?foo#bar"), simple(id = "hi")); + | ^^^^ the trait `FromUriParam` is not implemented for `usize` + | + = help: the following implementations were found: + > + > + > + = note: required by `from_uri_param` + +error[E0277]: the trait bound `rocket::http::uri::Reference<'_>: ValidRoutePrefix` is not satisfied + --> $DIR/typed-uri-bad-type.rs:77:15 + | +77 | uri!(uri!("?foo#bar"), simple(id = "hi")); + | ^^^^^^^^^^ the trait `ValidRoutePrefix` is not implemented for `rocket::http::uri::Reference<'_>` + +error[E0277]: the trait bound `usize: FromUriParam` is not satisfied + --> $DIR/typed-uri-bad-type.rs:78:33 + | +78 | uri!(uri!("*"), simple(id = "hi")); + | ^^^^ the trait `FromUriParam` is not implemented for `usize` + | + = help: the following implementations were found: + > + > + > + = note: required by `from_uri_param` + +error[E0277]: the trait bound `rocket::http::uri::Asterisk: ValidRoutePrefix` is not satisfied + --> $DIR/typed-uri-bad-type.rs:78:15 + | +78 | uri!(uri!("*"), simple(id = "hi")); + | ^^^ the trait `ValidRoutePrefix` is not implemented for `rocket::http::uri::Asterisk` + +error[E0277]: the trait bound `usize: FromUriParam` is not satisfied + --> $DIR/typed-uri-bad-type.rs:81:25 + | +81 | uri!(_, simple(id = "hi"), uri!("*")); + | ^^^^ the trait `FromUriParam` is not implemented for `usize` + | + = help: the following implementations were found: + > + > + > + = note: required by `from_uri_param` + +error[E0277]: the trait bound `rocket::http::uri::Asterisk: ValidRouteSuffix>` is not satisfied + --> $DIR/typed-uri-bad-type.rs:81:37 + | +81 | uri!(_, simple(id = "hi"), uri!("*")); + | ^^^ the trait `ValidRouteSuffix>` is not implemented for `rocket::http::uri::Asterisk` + +error[E0277]: the trait bound `usize: FromUriParam` is not satisfied + --> $DIR/typed-uri-bad-type.rs:82:25 + | +82 | uri!(_, simple(id = "hi"), uri!("/foo/bar")); + | ^^^^ the trait `FromUriParam` is not implemented for `usize` + | + = help: the following implementations were found: + > + > + > + = note: required by `from_uri_param` + +error[E0277]: the trait bound `rocket::http::uri::Origin<'_>: ValidRouteSuffix>` is not satisfied + --> $DIR/typed-uri-bad-type.rs:82:37 + | +82 | uri!(_, simple(id = "hi"), uri!("/foo/bar")); + | ^^^^^^^^^^ the trait `ValidRouteSuffix>` is not implemented for `rocket::http::uri::Origin<'_>` diff --git a/core/codegen/tests/ui-fail-nightly/typed-uris-bad-params.stderr b/core/codegen/tests/ui-fail-nightly/typed-uris-bad-params.stderr index 238e58ee..59ebe538 100644 --- a/core/codegen/tests/ui-fail-nightly/typed-uris-bad-params.stderr +++ b/core/codegen/tests/ui-fail-nightly/typed-uris-bad-params.stderr @@ -1,271 +1,279 @@ error: expected identifier - --> $DIR/typed-uris-bad-params.rs:62:19 + --> $DIR/typed-uris-bad-params.rs:63:18 | -62 | uri!(ignored: _ = 10); - | ^ +63 | uri!(ignored(_ = 10)); + | ^ -error: expected 1 parameter but 2 were supplied - --> $DIR/typed-uris-bad-params.rs:68:19 +error: route expects 1 parameter but 2 were supplied + --> $DIR/typed-uris-bad-params.rs:69:18 | -68 | uri!(ignored: 10, "10"); - | ^^^^^^^^ +69 | uri!(ignored(10, "10")); + | ^^^^^^^^ | = note: route `ignored` has uri "/<_>" error: expected unnamed arguments due to ignored parameters - --> $DIR/typed-uris-bad-params.rs:66:19 + --> $DIR/typed-uris-bad-params.rs:67:18 | -66 | uri!(ignored: num = 10); - | ^^^^^^^^ +67 | uri!(ignored(num = 10)); + | ^^^^^^^^ | = note: uri for route `ignored` ignores path parameters: "/<_>" -error: expected 1 parameter but 2 were supplied - --> $DIR/typed-uris-bad-params.rs:64:19 +error: route expects 1 parameter but 2 were supplied + --> $DIR/typed-uris-bad-params.rs:65:18 | -64 | uri!(ignored: 10, 20); - | ^^^^^^ +65 | uri!(ignored(10, 20)); + | ^^^^^^ | = note: route `ignored` has uri "/<_>" error: path parameters cannot be ignored - --> $DIR/typed-uris-bad-params.rs:60:19 + --> $DIR/typed-uris-bad-params.rs:61:18 | -60 | uri!(ignored: _); - | ^ +61 | uri!(ignored(_)); + | ^ error: path parameters cannot be ignored - --> $DIR/typed-uris-bad-params.rs:58:37 + --> $DIR/typed-uris-bad-params.rs:59:36 | -58 | uri!(optionals: id = 10, name = _); - | ^ +59 | uri!(optionals(id = 10, name = _)); + | ^ error: path parameters cannot be ignored - --> $DIR/typed-uris-bad-params.rs:56:26 + --> $DIR/typed-uris-bad-params.rs:57:25 | -56 | uri!(optionals: id = _, name = "bob".into()); - | ^ +57 | uri!(optionals(id = _, name = "bob".into())); + | ^ error: invalid parameters for `has_two` route uri - --> $DIR/typed-uris-bad-params.rs:54:19 + --> $DIR/typed-uris-bad-params.rs:55:18 | -54 | uri!(has_two: id = 100, cookies = "hi"); - | ^^^^^^^^^^^^^^^^^^^^^^^^ +55 | uri!(has_two(id = 100, cookies = "hi")); + | ^^^^^^^^^^^^^^^^^^^^^^^^ | = note: uri parameters are: id: i32, name: String = help: missing parameter: `name` help: unknown parameter: `cookies` - --> $DIR/typed-uris-bad-params.rs:54:29 + --> $DIR/typed-uris-bad-params.rs:55:28 | -54 | uri!(has_two: id = 100, cookies = "hi"); - | ^^^^^^^ +55 | uri!(has_two(id = 100, cookies = "hi")); + | ^^^^^^^ error: invalid parameters for `has_two` route uri - --> $DIR/typed-uris-bad-params.rs:52:19 + --> $DIR/typed-uris-bad-params.rs:53:18 | -52 | uri!(has_two: cookies = "hi", id = 100, id = 10, id = 10); - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +53 | uri!(has_two(cookies = "hi", id = 100, id = 10, id = 10)); + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ | = note: uri parameters are: id: i32, name: String = help: missing parameter: `name` help: unknown parameter: `cookies` - --> $DIR/typed-uris-bad-params.rs:52:19 + --> $DIR/typed-uris-bad-params.rs:53:18 | -52 | uri!(has_two: cookies = "hi", id = 100, id = 10, id = 10); - | ^^^^^^^ +53 | uri!(has_two(cookies = "hi", id = 100, id = 10, id = 10)); + | ^^^^^^^ help: duplicate parameter: `id` - --> $DIR/typed-uris-bad-params.rs:52:45 + --> $DIR/typed-uris-bad-params.rs:53:44 | -52 | uri!(has_two: cookies = "hi", id = 100, id = 10, id = 10); - | ^^ ^^ +53 | uri!(has_two(cookies = "hi", id = 100, id = 10, id = 10)); + | ^^ ^^ error: invalid parameters for `has_two` route uri - --> $DIR/typed-uris-bad-params.rs:50:19 + --> $DIR/typed-uris-bad-params.rs:51:18 | -50 | uri!(has_two: name = "hi"); - | ^^^^^^^^^^^ +51 | uri!(has_two(name = "hi")); + | ^^^^^^^^^^^ | = note: uri parameters are: id: i32, name: String = help: missing parameter: `id` error: invalid parameters for `has_two` route uri - --> $DIR/typed-uris-bad-params.rs:48:19 + --> $DIR/typed-uris-bad-params.rs:49:18 | -48 | uri!(has_two: id = 100, id = 100, ); - | ^^^^^^^^^^^^^^^^^^^ +49 | uri!(has_two(id = 100, id = 100, )); + | ^^^^^^^^^^^^^^^^^^^ | = note: uri parameters are: id: i32, name: String = help: missing parameter: `name` help: duplicate parameter: `id` - --> $DIR/typed-uris-bad-params.rs:48:29 + --> $DIR/typed-uris-bad-params.rs:49:28 | -48 | uri!(has_two: id = 100, id = 100, ); - | ^^ +49 | uri!(has_two(id = 100, id = 100, )); + | ^^ error: invalid parameters for `has_one_guarded` route uri - --> $DIR/typed-uris-bad-params.rs:46:27 + --> $DIR/typed-uris-bad-params.rs:47:26 | -46 | uri!(has_one_guarded: id = 100, cookies = "hi"); - | ^^^^^^^^^^^^^^^^^^^^^^^^ +47 | uri!(has_one_guarded(id = 100, cookies = "hi")); + | ^^^^^^^^^^^^^^^^^^^^^^^^ | = note: uri parameters are: id: i32 help: unknown parameter: `cookies` - --> $DIR/typed-uris-bad-params.rs:46:37 + --> $DIR/typed-uris-bad-params.rs:47:36 | -46 | uri!(has_one_guarded: id = 100, cookies = "hi"); - | ^^^^^^^ +47 | uri!(has_one_guarded(id = 100, cookies = "hi")); + | ^^^^^^^ error: invalid parameters for `has_one_guarded` route uri - --> $DIR/typed-uris-bad-params.rs:44:27 + --> $DIR/typed-uris-bad-params.rs:45:26 | -44 | uri!(has_one_guarded: cookies = "hi", id = 100); - | ^^^^^^^^^^^^^^^^^^^^^^^^ +45 | uri!(has_one_guarded(cookies = "hi", id = 100)); + | ^^^^^^^^^^^^^^^^^^^^^^^^ | = note: uri parameters are: id: i32 help: unknown parameter: `cookies` - --> $DIR/typed-uris-bad-params.rs:44:27 + --> $DIR/typed-uris-bad-params.rs:45:26 | -44 | uri!(has_one_guarded: cookies = "hi", id = 100); - | ^^^^^^^ +45 | uri!(has_one_guarded(cookies = "hi", id = 100)); + | ^^^^^^^ error: invalid parameters for `has_one` route uri - --> $DIR/typed-uris-bad-params.rs:42:19 + --> $DIR/typed-uris-bad-params.rs:43:18 | -42 | uri!(has_one: name = "hi"); - | ^^^^^^^^^^^ +43 | uri!(has_one(name = "hi")); + | ^^^^^^^^^^^ | = note: uri parameters are: id: i32 = help: missing parameter: `id` help: unknown parameter: `name` - --> $DIR/typed-uris-bad-params.rs:42:19 + --> $DIR/typed-uris-bad-params.rs:43:18 | -42 | uri!(has_one: name = "hi"); - | ^^^^ +43 | uri!(has_one(name = "hi")); + | ^^^^ error: invalid parameters for `has_one` route uri - --> $DIR/typed-uris-bad-params.rs:40:19 + --> $DIR/typed-uris-bad-params.rs:41:18 | -40 | uri!(has_one: id = 100, id = 100, ); - | ^^^^^^^^^^^^^^^^^^^ +41 | uri!(has_one(id = 100, id = 100, )); + | ^^^^^^^^^^^^^^^^^^^ | = note: uri parameters are: id: i32 help: duplicate parameter: `id` - --> $DIR/typed-uris-bad-params.rs:40:29 + --> $DIR/typed-uris-bad-params.rs:41:28 | -40 | uri!(has_one: id = 100, id = 100, ); - | ^^ +41 | uri!(has_one(id = 100, id = 100, )); + | ^^ error: invalid parameters for `has_one` route uri - --> $DIR/typed-uris-bad-params.rs:38:19 + --> $DIR/typed-uris-bad-params.rs:39:18 | -38 | uri!(has_one: id = 100, id = 100); - | ^^^^^^^^^^^^^^^^^^ +39 | uri!(has_one(id = 100, id = 100)); + | ^^^^^^^^^^^^^^^^^^ | = note: uri parameters are: id: i32 help: duplicate parameter: `id` - --> $DIR/typed-uris-bad-params.rs:38:29 + --> $DIR/typed-uris-bad-params.rs:39:28 | -38 | uri!(has_one: id = 100, id = 100); - | ^^ +39 | uri!(has_one(id = 100, id = 100)); + | ^^ error: invalid parameters for `has_one` route uri - --> $DIR/typed-uris-bad-params.rs:36:19 + --> $DIR/typed-uris-bad-params.rs:37:18 | -36 | uri!(has_one: name = 100, age = 50, id = 100, id = 50); - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +37 | uri!(has_one(name = 100, age = 50, id = 100, id = 50)); + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ | = note: uri parameters are: id: i32 help: unknown parameters: `name`, `age` - --> $DIR/typed-uris-bad-params.rs:36:19 + --> $DIR/typed-uris-bad-params.rs:37:18 | -36 | uri!(has_one: name = 100, age = 50, id = 100, id = 50); - | ^^^^ ^^^ +37 | uri!(has_one(name = 100, age = 50, id = 100, id = 50)); + | ^^^^ ^^^ help: duplicate parameter: `id` - --> $DIR/typed-uris-bad-params.rs:36:51 + --> $DIR/typed-uris-bad-params.rs:37:50 | -36 | uri!(has_one: name = 100, age = 50, id = 100, id = 50); - | ^^ +37 | uri!(has_one(name = 100, age = 50, id = 100, id = 50)); + | ^^ error: invalid parameters for `has_one` route uri - --> $DIR/typed-uris-bad-params.rs:34:19 + --> $DIR/typed-uris-bad-params.rs:35:18 | -34 | uri!(has_one: name = 100, age = 50, id = 100); - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +35 | uri!(has_one(name = 100, age = 50, id = 100)); + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ | = note: uri parameters are: id: i32 help: unknown parameters: `name`, `age` - --> $DIR/typed-uris-bad-params.rs:34:19 + --> $DIR/typed-uris-bad-params.rs:35:18 | -34 | uri!(has_one: name = 100, age = 50, id = 100); - | ^^^^ ^^^ +35 | uri!(has_one(name = 100, age = 50, id = 100)); + | ^^^^ ^^^ error: invalid parameters for `has_one` route uri - --> $DIR/typed-uris-bad-params.rs:32:19 + --> $DIR/typed-uris-bad-params.rs:33:18 | -32 | uri!(has_one: name = 100, id = 100); - | ^^^^^^^^^^^^^^^^^^^^ +33 | uri!(has_one(name = 100, id = 100)); + | ^^^^^^^^^^^^^^^^^^^^ | = note: uri parameters are: id: i32 help: unknown parameter: `name` - --> $DIR/typed-uris-bad-params.rs:32:19 + --> $DIR/typed-uris-bad-params.rs:33:18 | -32 | uri!(has_one: name = 100, id = 100); - | ^^^^ +33 | uri!(has_one(name = 100, id = 100)); + | ^^^^ error: invalid parameters for `has_one` route uri - --> $DIR/typed-uris-bad-params.rs:30:19 + --> $DIR/typed-uris-bad-params.rs:31:18 | -30 | uri!(has_one: id = 100, name = "hi"); - | ^^^^^^^^^^^^^^^^^^^^^ +31 | uri!(has_one(id = 100, name = "hi")); + | ^^^^^^^^^^^^^^^^^^^^^ | = note: uri parameters are: id: i32 help: unknown parameter: `name` - --> $DIR/typed-uris-bad-params.rs:30:29 + --> $DIR/typed-uris-bad-params.rs:31:28 | -30 | uri!(has_one: id = 100, name = "hi"); - | ^^^^ +31 | uri!(has_one(id = 100, name = "hi")); + | ^^^^ -error: expected 2 parameters but 1 was supplied - --> $DIR/typed-uris-bad-params.rs:28:19 +error: route expects 2 parameters but 1 was supplied + --> $DIR/typed-uris-bad-params.rs:29:18 | -28 | uri!(has_two: 10); - | ^^ +29 | uri!(has_two(10)); + | ^^ | = note: route `has_two` has uri "/?" -error: expected 2 parameters but 3 were supplied - --> $DIR/typed-uris-bad-params.rs:27:19 +error: route expects 2 parameters but 3 were supplied + --> $DIR/typed-uris-bad-params.rs:28:18 | -27 | uri!(has_two: 10, "hi", "there"); - | ^^^^^^^^^^^^^^^^^ +28 | uri!(has_two(10, "hi", "there")); + | ^^^^^^^^^^^^^^^^^ | = note: route `has_two` has uri "/?" -error: expected 1 parameter but 2 were supplied - --> $DIR/typed-uris-bad-params.rs:25:27 +error: route expects 1 parameter but 2 were supplied + --> $DIR/typed-uris-bad-params.rs:26:26 | -25 | uri!(has_one_guarded: "hi", 100); - | ^^^^^^^^^ +26 | uri!(has_one_guarded("hi", 100)); + | ^^^^^^^^^ | = note: route `has_one_guarded` has uri "/" -error: expected 1 parameter but 2 were supplied - --> $DIR/typed-uris-bad-params.rs:24:19 +error: route expects 1 parameter but 2 were supplied + --> $DIR/typed-uris-bad-params.rs:25:18 | -24 | uri!(has_one: "Hello", 23, ); - | ^^^^^^^^^^^^ +25 | uri!(has_one("Hello", 23, )); + | ^^^^^^^^^^^^ | = note: route `has_one` has uri "/" -error: expected 1 parameter but 2 were supplied - --> $DIR/typed-uris-bad-params.rs:23:19 +error: route expects 1 parameter but 2 were supplied + --> $DIR/typed-uris-bad-params.rs:24:18 | -23 | uri!(has_one: 1, 23); - | ^^^^^ +24 | uri!(has_one(1, 23)); + | ^^^^^ | = note: route `has_one` has uri "/" -error: expected 1 parameter but 0 were supplied +error: route expects 1 parameter but 0 were supplied + --> $DIR/typed-uris-bad-params.rs:22:10 + | +22 | uri!(has_one()); + | ^^^^^^^ + | + = note: route `has_one` has uri "/" + +error: route expects 1 parameter but 0 were supplied --> $DIR/typed-uris-bad-params.rs:21:10 | 21 | uri!(has_one); diff --git a/core/codegen/tests/ui-fail-nightly/typed-uris-invalid-syntax.stderr b/core/codegen/tests/ui-fail-nightly/typed-uris-invalid-syntax.stderr index 27eb8aa0..c0cd9ecc 100644 --- a/core/codegen/tests/ui-fail-nightly/typed-uris-invalid-syntax.stderr +++ b/core/codegen/tests/ui-fail-nightly/typed-uris-invalid-syntax.stderr @@ -1,65 +1,153 @@ -error: named and unnamed parameters cannot be mixed - --> $DIR/typed-uris-invalid-syntax.rs:7:18 - | -7 | uri!(simple: id = 100, "Hello"); - | ^^^^^^^^^^^^^^^^^ - -error: named and unnamed parameters cannot be mixed - --> $DIR/typed-uris-invalid-syntax.rs:8:18 - | -8 | uri!(simple: "Hello", id = 100); - | ^^^^^^^^^^^^^^^^^ - -error: expected `:` - --> $DIR/typed-uris-invalid-syntax.rs:9:16 - | -9 | uri!(simple,); - | ^ - -error: expected argument list after `:` - --> $DIR/typed-uris-invalid-syntax.rs:10:16 +error: expected identifier + --> $DIR/typed-uris-invalid-syntax.rs:10:28 | -10 | uri!(simple:); +10 | uri!(simple: id = 100, "Hello"); + | ^^^^^^^ + +error: named and unnamed parameters cannot be mixed + --> $DIR/typed-uris-invalid-syntax.rs:11:17 + | +11 | uri!(simple(id = 100, "Hello")); + | ^^^^^^^^^^^^^^^^^ + +error: named and unnamed parameters cannot be mixed + --> $DIR/typed-uris-invalid-syntax.rs:12:17 + | +12 | uri!(simple("Hello", id = 100)); + | ^^^^^^^^^^^^^^^^^ + +error: unexpected token + --> $DIR/typed-uris-invalid-syntax.rs:14:16 + | +14 | uri!(simple:); | ^ -error: unexpected end of input: expected ',' followed by route path - --> $DIR/typed-uris-invalid-syntax.rs:11:10 +error: invalid URI: unexpected EOF: expected token ':' at index 5 + --> $DIR/typed-uris-invalid-syntax.rs:16:16 | -11 | uri!("/mount"); - | ^^^^^^^^ +16 | uri!("mount", simple); + | ^ -error: unexpected end of input, expected identifier - --> $DIR/typed-uris-invalid-syntax.rs:12:5 +error: invalid URI: unexpected EOF: expected token ':' at index 5 + --> $DIR/typed-uris-invalid-syntax.rs:17:16 | -12 | uri!("/mount",); - | ^^^^^^^^^^^^^^^^ - | - = note: this error originates in the macro `uri` (in Nightly builds, run with -Z macro-backtrace for more info) +17 | uri!("mount", simple, "http://"); + | ^ -error: invalid mount point; mount points must be static, absolute URIs: `/example` - --> $DIR/typed-uris-invalid-syntax.rs:13:10 +error: URI suffix must contain only query and/or fragment + --> $DIR/typed-uris-invalid-syntax.rs:18:28 | -13 | uri!("mount", simple); - | ^^^^^^^ +18 | uri!("/mount", simple, "http://"); + | ^^^^^^^^^ -error: invalid mount point; mount points must be static, absolute URIs: `/example` - --> $DIR/typed-uris-invalid-syntax.rs:14:10 +error: expected 1, 2, or 3 arguments, found 4 + --> $DIR/typed-uris-invalid-syntax.rs:19:36 | -14 | uri!("/mount/", simple); - | ^^^^^^^^^^^^^ +19 | uri!("/mount", simple, "#foo", "?foo"); + | ^^^^^^ -error: unexpected end of input, call to `uri!` cannot be empty - --> $DIR/typed-uris-invalid-syntax.rs:15:5 +error: invalid URI: unexpected EOF: expected token ':' at index 5 + --> $DIR/typed-uris-invalid-syntax.rs:20:16 | -15 | uri!(); +20 | uri!("mount", simple(10, "hi"), "http://"); + | ^ + +error: URI suffix must contain only query and/or fragment + --> $DIR/typed-uris-invalid-syntax.rs:21:38 + | +21 | uri!("/mount", simple(10, "hi"), "http://"); + | ^^^^^^^^^ + +error: URI prefix cannot contain query part + --> $DIR/typed-uris-invalid-syntax.rs:22:10 + | +22 | uri!("/mount?foo", simple(10, "hi"), "foo/bar?foo#bar"); + | ^^^^^^^^^^^^ + +error: URI suffix must contain only query and/or fragment + --> $DIR/typed-uris-invalid-syntax.rs:23:38 + | +23 | uri!("/mount", simple(10, "hi"), "a/b"); + | ^^^^^ + +error: expected 1, 2, or 3 arguments, found 4 + --> $DIR/typed-uris-invalid-syntax.rs:24:46 + | +24 | uri!("/mount", simple(10, "hi"), "#foo", "?foo"); + | ^^^^^^ + +error: invalid URI: unexpected token '<' at index 7 + --> $DIR/typed-uris-invalid-syntax.rs:25:18 + | +25 | uri!("/mount/", simple); + | ^ + +error: expected at least 1 argument, found none + --> $DIR/typed-uris-invalid-syntax.rs:26:5 + | +26 | uri!(); | ^^^^^^^ | = note: this error originates in the macro `uri` (in Nightly builds, run with -Z macro-backtrace for more info) +error: unexpected token + --> $DIR/typed-uris-invalid-syntax.rs:27:16 + | +27 | uri!(simple: id = ); + | ^ + error: unexpected end of input, expected expression - --> $DIR/typed-uris-invalid-syntax.rs:16:5 + --> $DIR/typed-uris-invalid-syntax.rs:28:22 | -16 | uri!(simple: id = ); - | ^^^^^^^^^^^^^^^^^^^^ +28 | uri!(simple(id = )); + | ^ + +error: invalid URI: unexpected EOF: expected some token at index 0 + --> $DIR/typed-uris-invalid-syntax.rs:29:11 | - = note: this error originates in the macro `uri` (in Nightly builds, run with -Z macro-backtrace for more info) +29 | uri!("*", simple(10), "hi"); + | ^ + +error: URI suffix must contain only query and/or fragment + --> $DIR/typed-uris-invalid-syntax.rs:30:40 + | +30 | uri!("some.host:8088", simple(10), "hi"); + | ^^^^ + +error: expected identifier + --> $DIR/typed-uris-invalid-syntax.rs:33:18 + | +33 | uri!("/foo", "bar"); + | ^^^^^ + +error: unexpected token + --> $DIR/typed-uris-invalid-syntax.rs:34:17 + | +34 | uri!("/foo" ("bar")); + | ^^^^^^^ + +error: URI prefix cannot contain query part + --> $DIR/typed-uris-invalid-syntax.rs:35:10 + | +35 | uri!("ftp:?", index); + | ^^^^^^^ + +error: URI suffix must contain only query and/or fragment + --> $DIR/typed-uris-invalid-syntax.rs:36:25 + | +36 | uri!("ftp:", index, "foo#bar"); + | ^^^^^^^^^ + +error: URI suffix must contain only query and/or fragment + --> $DIR/typed-uris-invalid-syntax.rs:37:25 + | +37 | uri!("ftp:", index, "foo?bar"); + | ^^^^^^^^^ + +error: route expects 2 parameters but 0 were supplied + --> $DIR/typed-uris-invalid-syntax.rs:13:10 + | +13 | uri!(simple,); + | ^^^^^^ + | + = note: route `simple` has uri "//" diff --git a/core/codegen/tests/ui-fail-nightly/uri_display_type_errors.stderr b/core/codegen/tests/ui-fail-nightly/uri_display_type_errors.stderr index 1114a845..13d31426 100644 --- a/core/codegen/tests/ui-fail-nightly/uri_display_type_errors.stderr +++ b/core/codegen/tests/ui-fail-nightly/uri_display_type_errors.stderr @@ -1,61 +1,61 @@ -error[E0277]: the trait bound `BadType: UriDisplay` is not satisfied +error[E0277]: the trait bound `BadType: UriDisplay` is not satisfied --> $DIR/uri_display_type_errors.rs:6:13 | 6 | struct Bar1(BadType); - | ^^^^^^^ the trait `UriDisplay` is not implemented for `BadType` + | ^^^^^^^ the trait `UriDisplay` is not implemented for `BadType` | - = note: required because of the requirements on the impl of `UriDisplay` for `&BadType` + = note: required because of the requirements on the impl of `UriDisplay` for `&BadType` -error[E0277]: the trait bound `BadType: UriDisplay` is not satisfied +error[E0277]: the trait bound `BadType: UriDisplay` is not satisfied --> $DIR/uri_display_type_errors.rs:10:5 | 10 | field: BadType, - | ^^^^^^^^^^^^^^ the trait `UriDisplay` is not implemented for `BadType` + | ^^^^^^^^^^^^^^ the trait `UriDisplay` is not implemented for `BadType` | - = note: required because of the requirements on the impl of `UriDisplay` for `&BadType` + = note: required because of the requirements on the impl of `UriDisplay` for `&BadType` -error[E0277]: the trait bound `BadType: UriDisplay` is not satisfied +error[E0277]: the trait bound `BadType: UriDisplay` is not satisfied --> $DIR/uri_display_type_errors.rs:16:5 | 16 | bad: BadType, - | ^^^^^^^^^^^^ the trait `UriDisplay` is not implemented for `BadType` + | ^^^^^^^^^^^^ the trait `UriDisplay` is not implemented for `BadType` | - = note: required because of the requirements on the impl of `UriDisplay` for `&BadType` + = note: required because of the requirements on the impl of `UriDisplay` for `&BadType` -error[E0277]: the trait bound `BadType: UriDisplay` is not satisfied +error[E0277]: the trait bound `BadType: UriDisplay` is not satisfied --> $DIR/uri_display_type_errors.rs:21:11 | 21 | Inner(BadType), - | ^^^^^^^ the trait `UriDisplay` is not implemented for `BadType` + | ^^^^^^^ the trait `UriDisplay` is not implemented for `BadType` | - = note: required because of the requirements on the impl of `UriDisplay` for `&BadType` + = note: required because of the requirements on the impl of `UriDisplay` for `&BadType` = note: 1 redundant requirements hidden - = note: required because of the requirements on the impl of `UriDisplay` for `&&BadType` + = note: required because of the requirements on the impl of `UriDisplay` for `&&BadType` -error[E0277]: the trait bound `BadType: UriDisplay` is not satisfied +error[E0277]: the trait bound `BadType: UriDisplay` is not satisfied --> $DIR/uri_display_type_errors.rs:27:9 | 27 | field: BadType, - | ^^^^^^^^^^^^^^ the trait `UriDisplay` is not implemented for `BadType` + | ^^^^^^^^^^^^^^ the trait `UriDisplay` is not implemented for `BadType` | - = note: required because of the requirements on the impl of `UriDisplay` for `&BadType` + = note: required because of the requirements on the impl of `UriDisplay` for `&BadType` = note: 1 redundant requirements hidden - = note: required because of the requirements on the impl of `UriDisplay` for `&&BadType` + = note: required because of the requirements on the impl of `UriDisplay` for `&&BadType` -error[E0277]: the trait bound `BadType: UriDisplay` is not satisfied +error[E0277]: the trait bound `BadType: UriDisplay` is not satisfied --> $DIR/uri_display_type_errors.rs:35:9 | 35 | other: BadType, - | ^^^^^^^^^^^^^^ the trait `UriDisplay` is not implemented for `BadType` + | ^^^^^^^^^^^^^^ the trait `UriDisplay` is not implemented for `BadType` | - = note: required because of the requirements on the impl of `UriDisplay` for `&BadType` + = note: required because of the requirements on the impl of `UriDisplay` for `&BadType` = note: 1 redundant requirements hidden - = note: required because of the requirements on the impl of `UriDisplay` for `&&BadType` + = note: required because of the requirements on the impl of `UriDisplay` for `&&BadType` -error[E0277]: the trait bound `BadType: UriDisplay` is not satisfied +error[E0277]: the trait bound `BadType: UriDisplay` is not satisfied --> $DIR/uri_display_type_errors.rs:40:12 | 40 | struct Baz(BadType); - | ^^^^^^^ the trait `UriDisplay` is not implemented for `BadType` + | ^^^^^^^ the trait `UriDisplay` is not implemented for `BadType` | - = note: required because of the requirements on the impl of `UriDisplay` for `&BadType` + = note: required because of the requirements on the impl of `UriDisplay` for `&BadType` diff --git a/core/codegen/tests/ui-fail-stable/typed-uri-bad-type.stderr b/core/codegen/tests/ui-fail-stable/typed-uri-bad-type.stderr index 07202503..c80c51b5 100644 --- a/core/codegen/tests/ui-fail-stable/typed-uri-bad-type.stderr +++ b/core/codegen/tests/ui-fail-stable/typed-uri-bad-type.stderr @@ -1,8 +1,8 @@ -error[E0277]: the trait bound `usize: FromUriParam` is not satisfied - --> $DIR/typed-uri-bad-type.rs:45:23 +error[E0277]: the trait bound `usize: FromUriParam` is not satisfied + --> $DIR/typed-uri-bad-type.rs:45:22 | -45 | uri!(simple: id = "hi"); - | ^^^^ the trait `FromUriParam` is not implemented for `usize` +45 | uri!(simple(id = "hi")); + | ^^^^ the trait `FromUriParam` is not implemented for `usize` | = help: the following implementations were found: > @@ -10,11 +10,11 @@ error[E0277]: the trait bound `usize: FromUriParam> = note: required by `from_uri_param` -error[E0277]: the trait bound `usize: FromUriParam` is not satisfied - --> $DIR/typed-uri-bad-type.rs:47:18 +error[E0277]: the trait bound `usize: FromUriParam` is not satisfied + --> $DIR/typed-uri-bad-type.rs:47:17 | -47 | uri!(simple: "hello"); - | ^^^^^^^ the trait `FromUriParam` is not implemented for `usize` +47 | uri!(simple("hello")); + | ^^^^^^^ the trait `FromUriParam` is not implemented for `usize` | = help: the following implementations were found: > @@ -22,11 +22,11 @@ error[E0277]: the trait bound `usize: FromUriParam> = note: required by `from_uri_param` -error[E0277]: the trait bound `usize: FromUriParam` is not satisfied - --> $DIR/typed-uri-bad-type.rs:49:23 +error[E0277]: the trait bound `usize: FromUriParam` is not satisfied + --> $DIR/typed-uri-bad-type.rs:49:22 | -49 | uri!(simple: id = 239239i64); - | ^^^^^^^^^ the trait `FromUriParam` is not implemented for `usize` +49 | uri!(simple(id = 239239i64)); + | ^^^^^^^^^ the trait `FromUriParam` is not implemented for `usize` | = help: the following implementations were found: > @@ -34,32 +34,32 @@ error[E0277]: the trait bound `usize: FromUriParam > = note: required by `from_uri_param` -error[E0277]: the trait bound `S: FromUriParam` is not satisfied - --> $DIR/typed-uri-bad-type.rs:51:31 +error[E0277]: the trait bound `S: FromUriParam` is not satisfied + --> $DIR/typed-uri-bad-type.rs:51:30 | -51 | uri!(not_uri_display: 10, S); - | ^ the trait `FromUriParam` is not implemented for `S` +51 | uri!(not_uri_display(10, S)); + | ^ the trait `FromUriParam` is not implemented for `S` | = note: required by `from_uri_param` -error[E0277]: the trait bound `i32: FromUriParam>` is not satisfied - --> $DIR/typed-uri-bad-type.rs:56:26 +error[E0277]: the trait bound `i32: FromUriParam>` is not satisfied + --> $DIR/typed-uri-bad-type.rs:56:25 | -56 | uri!(optionals: id = Some(10), name = Ok("bob".into())); - | ^^^^ the trait `FromUriParam>` is not implemented for `i32` +56 | uri!(optionals(id = Some(10), name = Ok("bob".into()))); + | ^^^^ the trait `FromUriParam>` is not implemented for `i32` | = help: the following implementations were found: > > > - = note: required because of the requirements on the impl of `FromUriParam>` for `std::option::Option` + = note: required because of the requirements on the impl of `FromUriParam>` for `std::option::Option` = note: required by `from_uri_param` -error[E0277]: the trait bound `std::string::String: FromUriParam>` is not satisfied - --> $DIR/typed-uri-bad-type.rs:56:43 +error[E0277]: the trait bound `std::string::String: FromUriParam>` is not satisfied + --> $DIR/typed-uri-bad-type.rs:56:42 | -56 | uri!(optionals: id = Some(10), name = Ok("bob".into())); - | ^^ the trait `FromUriParam>` is not implemented for `std::string::String` +56 | uri!(optionals(id = Some(10), name = Ok("bob".into()))); + | ^^ the trait `FromUriParam>` is not implemented for `std::string::String` | = help: the following implementations were found: > @@ -67,14 +67,14 @@ error[E0277]: the trait bound `std::string::String: FromUriParam> > and 2 others - = note: required because of the requirements on the impl of `FromUriParam>` for `Result` + = note: required because of the requirements on the impl of `FromUriParam>` for `Result` = note: required by `from_uri_param` -error[E0277]: the trait bound `isize: FromUriParam` is not satisfied - --> $DIR/typed-uri-bad-type.rs:58:20 +error[E0277]: the trait bound `isize: FromUriParam` is not satisfied + --> $DIR/typed-uri-bad-type.rs:58:19 | -58 | uri!(simple_q: "hi"); - | ^^^^ the trait `FromUriParam` is not implemented for `isize` +58 | uri!(simple_q("hi")); + | ^^^^ the trait `FromUriParam` is not implemented for `isize` | = help: the following implementations were found: > @@ -82,11 +82,11 @@ error[E0277]: the trait bound `isize: FromUriParam> = note: required by `from_uri_param` -error[E0277]: the trait bound `isize: FromUriParam` is not satisfied - --> $DIR/typed-uri-bad-type.rs:60:25 +error[E0277]: the trait bound `isize: FromUriParam` is not satisfied + --> $DIR/typed-uri-bad-type.rs:60:24 | -60 | uri!(simple_q: id = "hi"); - | ^^^^ the trait `FromUriParam` is not implemented for `isize` +60 | uri!(simple_q(id = "hi")); + | ^^^^ the trait `FromUriParam` is not implemented for `isize` | = help: the following implementations were found: > @@ -94,48 +94,120 @@ error[E0277]: the trait bound `isize: FromUriParam> = note: required by `from_uri_param` -error[E0277]: the trait bound `S: FromUriParam` is not satisfied - --> $DIR/typed-uri-bad-type.rs:62:24 +error[E0277]: the trait bound `S: FromUriParam` is not satisfied + --> $DIR/typed-uri-bad-type.rs:62:23 | -62 | uri!(other_q: 100, S); - | ^ the trait `FromUriParam` is not implemented for `S` +62 | uri!(other_q(100, S)); + | ^ the trait `FromUriParam` is not implemented for `S` | = note: required by `from_uri_param` -error[E0277]: the trait bound `S: FromUriParam` is not satisfied - --> $DIR/typed-uri-bad-type.rs:64:26 +error[E0277]: the trait bound `S: FromUriParam` is not satisfied + --> $DIR/typed-uri-bad-type.rs:64:25 | -64 | uri!(other_q: rest = S, id = 100); - | ^ the trait `FromUriParam` is not implemented for `S` +64 | uri!(other_q(rest = S, id = 100)); + | ^ the trait `FromUriParam` is not implemented for `S` | = note: required by `from_uri_param` -error[E0277]: the trait bound `S: Ignorable` is not satisfied - --> $DIR/typed-uri-bad-type.rs:66:26 +error[E0277]: the trait bound `S: Ignorable` is not satisfied + --> $DIR/typed-uri-bad-type.rs:66:25 | -66 | uri!(other_q: rest = _, id = 100); - | ^ the trait `Ignorable` is not implemented for `S` +66 | uri!(other_q(rest = _, id = 100)); + | ^ the trait `Ignorable` is not implemented for `S` | - ::: $WORKSPACE/core/http/src/uri/uri_display.rs + ::: $WORKSPACE/core/http/src/uri/fmt/uri_display.rs | - | pub fn assert_ignorable>() { } - | ------------ required by this bound in `assert_ignorable` + | pub fn assert_ignorable>() { } + | ------------ required by this bound in `assert_ignorable` -error[E0277]: the trait bound `usize: Ignorable` is not satisfied - --> $DIR/typed-uri-bad-type.rs:68:34 +error[E0277]: the trait bound `usize: Ignorable` is not satisfied + --> $DIR/typed-uri-bad-type.rs:68:33 | -68 | uri!(other_q: rest = S, id = _); - | ^ the trait `Ignorable` is not implemented for `usize` +68 | uri!(other_q(rest = S, id = _)); + | ^ the trait `Ignorable` is not implemented for `usize` | - ::: $WORKSPACE/core/http/src/uri/uri_display.rs + ::: $WORKSPACE/core/http/src/uri/fmt/uri_display.rs | - | pub fn assert_ignorable>() { } - | ------------ required by this bound in `assert_ignorable` + | pub fn assert_ignorable>() { } + | ------------ required by this bound in `assert_ignorable` -error[E0277]: the trait bound `S: FromUriParam` is not satisfied - --> $DIR/typed-uri-bad-type.rs:68:26 +error[E0277]: the trait bound `S: FromUriParam` is not satisfied + --> $DIR/typed-uri-bad-type.rs:68:25 | -68 | uri!(other_q: rest = S, id = _); - | ^ the trait `FromUriParam` is not implemented for `S` +68 | uri!(other_q(rest = S, id = _)); + | ^ the trait `FromUriParam` is not implemented for `S` | = note: required by `from_uri_param` + +error[E0277]: the trait bound `usize: FromUriParam` is not satisfied + --> $DIR/typed-uri-bad-type.rs:77:40 + | +77 | uri!(uri!("?foo#bar"), simple(id = "hi")); + | ^^^^ the trait `FromUriParam` is not implemented for `usize` + | + = help: the following implementations were found: + > + > + > + = note: required by `from_uri_param` + +error[E0277]: the trait bound `rocket::http::uri::Reference<'_>: ValidRoutePrefix` is not satisfied + --> $DIR/typed-uri-bad-type.rs:77:15 + | +77 | uri!(uri!("?foo#bar"), simple(id = "hi")); + | ^^^^^^^^^^ the trait `ValidRoutePrefix` is not implemented for `rocket::http::uri::Reference<'_>` + +error[E0277]: the trait bound `usize: FromUriParam` is not satisfied + --> $DIR/typed-uri-bad-type.rs:78:33 + | +78 | uri!(uri!("*"), simple(id = "hi")); + | ^^^^ the trait `FromUriParam` is not implemented for `usize` + | + = help: the following implementations were found: + > + > + > + = note: required by `from_uri_param` + +error[E0277]: the trait bound `rocket::http::uri::Asterisk: ValidRoutePrefix` is not satisfied + --> $DIR/typed-uri-bad-type.rs:78:15 + | +78 | uri!(uri!("*"), simple(id = "hi")); + | ^^^ the trait `ValidRoutePrefix` is not implemented for `rocket::http::uri::Asterisk` + +error[E0277]: the trait bound `usize: FromUriParam` is not satisfied + --> $DIR/typed-uri-bad-type.rs:81:25 + | +81 | uri!(_, simple(id = "hi"), uri!("*")); + | ^^^^ the trait `FromUriParam` is not implemented for `usize` + | + = help: the following implementations were found: + > + > + > + = note: required by `from_uri_param` + +error[E0277]: the trait bound `rocket::http::uri::Asterisk: ValidRouteSuffix>` is not satisfied + --> $DIR/typed-uri-bad-type.rs:81:37 + | +81 | uri!(_, simple(id = "hi"), uri!("*")); + | ^^^ the trait `ValidRouteSuffix>` is not implemented for `rocket::http::uri::Asterisk` + +error[E0277]: the trait bound `usize: FromUriParam` is not satisfied + --> $DIR/typed-uri-bad-type.rs:82:25 + | +82 | uri!(_, simple(id = "hi"), uri!("/foo/bar")); + | ^^^^ the trait `FromUriParam` is not implemented for `usize` + | + = help: the following implementations were found: + > + > + > + = note: required by `from_uri_param` + +error[E0277]: the trait bound `rocket::http::uri::Origin<'_>: ValidRouteSuffix>` is not satisfied + --> $DIR/typed-uri-bad-type.rs:82:37 + | +82 | uri!(_, simple(id = "hi"), uri!("/foo/bar")); + | ^^^^^^^^^^ the trait `ValidRouteSuffix>` is not implemented for `rocket::http::uri::Origin<'_>` diff --git a/core/codegen/tests/ui-fail-stable/typed-uris-bad-params.stderr b/core/codegen/tests/ui-fail-stable/typed-uris-bad-params.stderr index 83c203a0..ae95f4f0 100644 --- a/core/codegen/tests/ui-fail-stable/typed-uris-bad-params.stderr +++ b/core/codegen/tests/ui-fail-stable/typed-uris-bad-params.stderr @@ -1,264 +1,271 @@ error: expected identifier - --> $DIR/typed-uris-bad-params.rs:62:19 + --> $DIR/typed-uris-bad-params.rs:63:18 | -62 | uri!(ignored: _ = 10); - | ^ +63 | uri!(ignored(_ = 10)); + | ^ -error: expected 1 parameter but 2 were supplied +error: route expects 1 parameter but 2 were supplied --- note: route `ignored` has uri "/<_>" - --> $DIR/typed-uris-bad-params.rs:68:19 + --> $DIR/typed-uris-bad-params.rs:69:18 | -68 | uri!(ignored: 10, "10"); - | ^^ +69 | uri!(ignored(10, "10")); + | ^^ error: expected unnamed arguments due to ignored parameters --- note: uri for route `ignored` ignores path parameters: "/<_>" - --> $DIR/typed-uris-bad-params.rs:66:19 + --> $DIR/typed-uris-bad-params.rs:67:18 | -66 | uri!(ignored: num = 10); - | ^^^ +67 | uri!(ignored(num = 10)); + | ^^^ -error: expected 1 parameter but 2 were supplied +error: route expects 1 parameter but 2 were supplied --- note: route `ignored` has uri "/<_>" - --> $DIR/typed-uris-bad-params.rs:64:19 + --> $DIR/typed-uris-bad-params.rs:65:18 | -64 | uri!(ignored: 10, 20); - | ^^ +65 | uri!(ignored(10, 20)); + | ^^ error: path parameters cannot be ignored - --> $DIR/typed-uris-bad-params.rs:60:19 + --> $DIR/typed-uris-bad-params.rs:61:18 | -60 | uri!(ignored: _); - | ^ +61 | uri!(ignored(_)); + | ^ error: path parameters cannot be ignored - --> $DIR/typed-uris-bad-params.rs:58:37 + --> $DIR/typed-uris-bad-params.rs:59:36 | -58 | uri!(optionals: id = 10, name = _); - | ^ +59 | uri!(optionals(id = 10, name = _)); + | ^ error: path parameters cannot be ignored - --> $DIR/typed-uris-bad-params.rs:56:26 + --> $DIR/typed-uris-bad-params.rs:57:25 | -56 | uri!(optionals: id = _, name = "bob".into()); - | ^ +57 | uri!(optionals(id = _, name = "bob".into())); + | ^ error: invalid parameters for `has_two` route uri --- note: uri parameters are: id: i32, name: String --- help: missing parameter: `name` - --> $DIR/typed-uris-bad-params.rs:54:19 + --> $DIR/typed-uris-bad-params.rs:55:18 | -54 | uri!(has_two: id = 100, cookies = "hi"); - | ^^ +55 | uri!(has_two(id = 100, cookies = "hi")); + | ^^ error: [help] unknown parameter: `cookies` - --> $DIR/typed-uris-bad-params.rs:54:29 + --> $DIR/typed-uris-bad-params.rs:55:28 | -54 | uri!(has_two: id = 100, cookies = "hi"); - | ^^^^^^^ +55 | uri!(has_two(id = 100, cookies = "hi")); + | ^^^^^^^ error: invalid parameters for `has_two` route uri --- note: uri parameters are: id: i32, name: String --- help: missing parameter: `name` - --> $DIR/typed-uris-bad-params.rs:52:19 + --> $DIR/typed-uris-bad-params.rs:53:18 | -52 | uri!(has_two: cookies = "hi", id = 100, id = 10, id = 10); - | ^^^^^^^ +53 | uri!(has_two(cookies = "hi", id = 100, id = 10, id = 10)); + | ^^^^^^^ error: [help] unknown parameter: `cookies` - --> $DIR/typed-uris-bad-params.rs:52:19 + --> $DIR/typed-uris-bad-params.rs:53:18 | -52 | uri!(has_two: cookies = "hi", id = 100, id = 10, id = 10); - | ^^^^^^^ +53 | uri!(has_two(cookies = "hi", id = 100, id = 10, id = 10)); + | ^^^^^^^ error: [help] duplicate parameter: `id` - --> $DIR/typed-uris-bad-params.rs:52:45 + --> $DIR/typed-uris-bad-params.rs:53:44 | -52 | uri!(has_two: cookies = "hi", id = 100, id = 10, id = 10); - | ^^ +53 | uri!(has_two(cookies = "hi", id = 100, id = 10, id = 10)); + | ^^ error: invalid parameters for `has_two` route uri --- note: uri parameters are: id: i32, name: String --- help: missing parameter: `id` - --> $DIR/typed-uris-bad-params.rs:50:19 + --> $DIR/typed-uris-bad-params.rs:51:18 | -50 | uri!(has_two: name = "hi"); - | ^^^^ +51 | uri!(has_two(name = "hi")); + | ^^^^ error: invalid parameters for `has_two` route uri --- note: uri parameters are: id: i32, name: String --- help: missing parameter: `name` - --> $DIR/typed-uris-bad-params.rs:48:19 + --> $DIR/typed-uris-bad-params.rs:49:18 | -48 | uri!(has_two: id = 100, id = 100, ); - | ^^ +49 | uri!(has_two(id = 100, id = 100, )); + | ^^ error: [help] duplicate parameter: `id` - --> $DIR/typed-uris-bad-params.rs:48:29 + --> $DIR/typed-uris-bad-params.rs:49:28 | -48 | uri!(has_two: id = 100, id = 100, ); - | ^^ +49 | uri!(has_two(id = 100, id = 100, )); + | ^^ error: invalid parameters for `has_one_guarded` route uri --- note: uri parameters are: id: i32 - --> $DIR/typed-uris-bad-params.rs:46:27 + --> $DIR/typed-uris-bad-params.rs:47:26 | -46 | uri!(has_one_guarded: id = 100, cookies = "hi"); - | ^^ +47 | uri!(has_one_guarded(id = 100, cookies = "hi")); + | ^^ error: [help] unknown parameter: `cookies` - --> $DIR/typed-uris-bad-params.rs:46:37 + --> $DIR/typed-uris-bad-params.rs:47:36 | -46 | uri!(has_one_guarded: id = 100, cookies = "hi"); - | ^^^^^^^ +47 | uri!(has_one_guarded(id = 100, cookies = "hi")); + | ^^^^^^^ error: invalid parameters for `has_one_guarded` route uri --- note: uri parameters are: id: i32 - --> $DIR/typed-uris-bad-params.rs:44:27 + --> $DIR/typed-uris-bad-params.rs:45:26 | -44 | uri!(has_one_guarded: cookies = "hi", id = 100); - | ^^^^^^^ +45 | uri!(has_one_guarded(cookies = "hi", id = 100)); + | ^^^^^^^ error: [help] unknown parameter: `cookies` - --> $DIR/typed-uris-bad-params.rs:44:27 + --> $DIR/typed-uris-bad-params.rs:45:26 | -44 | uri!(has_one_guarded: cookies = "hi", id = 100); - | ^^^^^^^ +45 | uri!(has_one_guarded(cookies = "hi", id = 100)); + | ^^^^^^^ error: invalid parameters for `has_one` route uri --- note: uri parameters are: id: i32 --- help: missing parameter: `id` - --> $DIR/typed-uris-bad-params.rs:42:19 + --> $DIR/typed-uris-bad-params.rs:43:18 | -42 | uri!(has_one: name = "hi"); - | ^^^^ +43 | uri!(has_one(name = "hi")); + | ^^^^ error: [help] unknown parameter: `name` - --> $DIR/typed-uris-bad-params.rs:42:19 + --> $DIR/typed-uris-bad-params.rs:43:18 | -42 | uri!(has_one: name = "hi"); - | ^^^^ +43 | uri!(has_one(name = "hi")); + | ^^^^ error: invalid parameters for `has_one` route uri --- note: uri parameters are: id: i32 - --> $DIR/typed-uris-bad-params.rs:40:19 + --> $DIR/typed-uris-bad-params.rs:41:18 | -40 | uri!(has_one: id = 100, id = 100, ); - | ^^ +41 | uri!(has_one(id = 100, id = 100, )); + | ^^ error: [help] duplicate parameter: `id` - --> $DIR/typed-uris-bad-params.rs:40:29 + --> $DIR/typed-uris-bad-params.rs:41:28 | -40 | uri!(has_one: id = 100, id = 100, ); - | ^^ +41 | uri!(has_one(id = 100, id = 100, )); + | ^^ error: invalid parameters for `has_one` route uri --- note: uri parameters are: id: i32 - --> $DIR/typed-uris-bad-params.rs:38:19 + --> $DIR/typed-uris-bad-params.rs:39:18 | -38 | uri!(has_one: id = 100, id = 100); - | ^^ +39 | uri!(has_one(id = 100, id = 100)); + | ^^ error: [help] duplicate parameter: `id` - --> $DIR/typed-uris-bad-params.rs:38:29 + --> $DIR/typed-uris-bad-params.rs:39:28 | -38 | uri!(has_one: id = 100, id = 100); - | ^^ +39 | uri!(has_one(id = 100, id = 100)); + | ^^ error: invalid parameters for `has_one` route uri --- note: uri parameters are: id: i32 - --> $DIR/typed-uris-bad-params.rs:36:19 + --> $DIR/typed-uris-bad-params.rs:37:18 | -36 | uri!(has_one: name = 100, age = 50, id = 100, id = 50); - | ^^^^ +37 | uri!(has_one(name = 100, age = 50, id = 100, id = 50)); + | ^^^^ error: [help] unknown parameters: `name`, `age` - --> $DIR/typed-uris-bad-params.rs:36:19 + --> $DIR/typed-uris-bad-params.rs:37:18 | -36 | uri!(has_one: name = 100, age = 50, id = 100, id = 50); - | ^^^^ +37 | uri!(has_one(name = 100, age = 50, id = 100, id = 50)); + | ^^^^ error: [help] duplicate parameter: `id` - --> $DIR/typed-uris-bad-params.rs:36:51 + --> $DIR/typed-uris-bad-params.rs:37:50 | -36 | uri!(has_one: name = 100, age = 50, id = 100, id = 50); - | ^^ +37 | uri!(has_one(name = 100, age = 50, id = 100, id = 50)); + | ^^ error: invalid parameters for `has_one` route uri --- note: uri parameters are: id: i32 - --> $DIR/typed-uris-bad-params.rs:34:19 + --> $DIR/typed-uris-bad-params.rs:35:18 | -34 | uri!(has_one: name = 100, age = 50, id = 100); - | ^^^^ +35 | uri!(has_one(name = 100, age = 50, id = 100)); + | ^^^^ error: [help] unknown parameters: `name`, `age` - --> $DIR/typed-uris-bad-params.rs:34:19 + --> $DIR/typed-uris-bad-params.rs:35:18 | -34 | uri!(has_one: name = 100, age = 50, id = 100); - | ^^^^ +35 | uri!(has_one(name = 100, age = 50, id = 100)); + | ^^^^ error: invalid parameters for `has_one` route uri --- note: uri parameters are: id: i32 - --> $DIR/typed-uris-bad-params.rs:32:19 + --> $DIR/typed-uris-bad-params.rs:33:18 | -32 | uri!(has_one: name = 100, id = 100); - | ^^^^ +33 | uri!(has_one(name = 100, id = 100)); + | ^^^^ error: [help] unknown parameter: `name` - --> $DIR/typed-uris-bad-params.rs:32:19 + --> $DIR/typed-uris-bad-params.rs:33:18 | -32 | uri!(has_one: name = 100, id = 100); - | ^^^^ +33 | uri!(has_one(name = 100, id = 100)); + | ^^^^ error: invalid parameters for `has_one` route uri --- note: uri parameters are: id: i32 - --> $DIR/typed-uris-bad-params.rs:30:19 + --> $DIR/typed-uris-bad-params.rs:31:18 | -30 | uri!(has_one: id = 100, name = "hi"); - | ^^ +31 | uri!(has_one(id = 100, name = "hi")); + | ^^ error: [help] unknown parameter: `name` - --> $DIR/typed-uris-bad-params.rs:30:29 + --> $DIR/typed-uris-bad-params.rs:31:28 | -30 | uri!(has_one: id = 100, name = "hi"); - | ^^^^ +31 | uri!(has_one(id = 100, name = "hi")); + | ^^^^ -error: expected 2 parameters but 1 was supplied +error: route expects 2 parameters but 1 was supplied --- note: route `has_two` has uri "/?" - --> $DIR/typed-uris-bad-params.rs:28:19 + --> $DIR/typed-uris-bad-params.rs:29:18 | -28 | uri!(has_two: 10); - | ^^ +29 | uri!(has_two(10)); + | ^^ -error: expected 2 parameters but 3 were supplied +error: route expects 2 parameters but 3 were supplied --- note: route `has_two` has uri "/?" - --> $DIR/typed-uris-bad-params.rs:27:19 + --> $DIR/typed-uris-bad-params.rs:28:18 | -27 | uri!(has_two: 10, "hi", "there"); - | ^^ +28 | uri!(has_two(10, "hi", "there")); + | ^^ -error: expected 1 parameter but 2 were supplied +error: route expects 1 parameter but 2 were supplied --- note: route `has_one_guarded` has uri "/" - --> $DIR/typed-uris-bad-params.rs:25:27 + --> $DIR/typed-uris-bad-params.rs:26:26 | -25 | uri!(has_one_guarded: "hi", 100); - | ^^^^ +26 | uri!(has_one_guarded("hi", 100)); + | ^^^^ -error: expected 1 parameter but 2 were supplied +error: route expects 1 parameter but 2 were supplied --- note: route `has_one` has uri "/" - --> $DIR/typed-uris-bad-params.rs:24:19 + --> $DIR/typed-uris-bad-params.rs:25:18 | -24 | uri!(has_one: "Hello", 23, ); - | ^^^^^^^ +25 | uri!(has_one("Hello", 23, )); + | ^^^^^^^ -error: expected 1 parameter but 2 were supplied +error: route expects 1 parameter but 2 were supplied --- note: route `has_one` has uri "/" - --> $DIR/typed-uris-bad-params.rs:23:19 + --> $DIR/typed-uris-bad-params.rs:24:18 | -23 | uri!(has_one: 1, 23); - | ^ +24 | uri!(has_one(1, 23)); + | ^ -error: expected 1 parameter but 0 were supplied +error: route expects 1 parameter but 0 were supplied + --- note: route `has_one` has uri "/" + --> $DIR/typed-uris-bad-params.rs:22:10 + | +22 | uri!(has_one()); + | ^^^^^^^ + +error: route expects 1 parameter but 0 were supplied --- note: route `has_one` has uri "/" --> $DIR/typed-uris-bad-params.rs:21:10 | diff --git a/core/codegen/tests/ui-fail-stable/typed-uris-invalid-syntax.stderr b/core/codegen/tests/ui-fail-stable/typed-uris-invalid-syntax.stderr index a024d086..6f44c995 100644 --- a/core/codegen/tests/ui-fail-stable/typed-uris-invalid-syntax.stderr +++ b/core/codegen/tests/ui-fail-stable/typed-uris-invalid-syntax.stderr @@ -1,65 +1,152 @@ -error: named and unnamed parameters cannot be mixed - --> $DIR/typed-uris-invalid-syntax.rs:7:18 - | -7 | uri!(simple: id = 100, "Hello"); - | ^^ - -error: named and unnamed parameters cannot be mixed - --> $DIR/typed-uris-invalid-syntax.rs:8:18 - | -8 | uri!(simple: "Hello", id = 100); - | ^^^^^^^ - -error: expected `:` - --> $DIR/typed-uris-invalid-syntax.rs:9:16 - | -9 | uri!(simple,); - | ^ - -error: expected argument list after `:` - --> $DIR/typed-uris-invalid-syntax.rs:10:16 +error: expected identifier + --> $DIR/typed-uris-invalid-syntax.rs:10:28 | -10 | uri!(simple:); +10 | uri!(simple: id = 100, "Hello"); + | ^^^^^^^ + +error: named and unnamed parameters cannot be mixed + --> $DIR/typed-uris-invalid-syntax.rs:11:17 + | +11 | uri!(simple(id = 100, "Hello")); + | ^^ + +error: named and unnamed parameters cannot be mixed + --> $DIR/typed-uris-invalid-syntax.rs:12:17 + | +12 | uri!(simple("Hello", id = 100)); + | ^^^^^^^ + +error: unexpected token + --> $DIR/typed-uris-invalid-syntax.rs:14:16 + | +14 | uri!(simple:); | ^ -error: unexpected end of input: expected ',' followed by route path - --> $DIR/typed-uris-invalid-syntax.rs:11:10 +error: invalid URI: unexpected EOF: expected token ':' at index 5 + --> $DIR/typed-uris-invalid-syntax.rs:16:10 | -11 | uri!("/mount"); - | ^^^^^^^^ - -error: unexpected end of input, expected identifier - --> $DIR/typed-uris-invalid-syntax.rs:12:5 - | -12 | uri!("/mount",); - | ^^^^^^^^^^^^^^^^ - | - = note: this error originates in a macro (in Nightly builds, run with -Z macro-backtrace for more info) - -error: invalid mount point; mount points must be static, absolute URIs: `/example` - --> $DIR/typed-uris-invalid-syntax.rs:13:10 - | -13 | uri!("mount", simple); +16 | uri!("mount", simple); | ^^^^^^^ -error: invalid mount point; mount points must be static, absolute URIs: `/example` - --> $DIR/typed-uris-invalid-syntax.rs:14:10 +error: invalid URI: unexpected EOF: expected token ':' at index 5 + --> $DIR/typed-uris-invalid-syntax.rs:17:10 | -14 | uri!("/mount/", simple); +17 | uri!("mount", simple, "http://"); + | ^^^^^^^ + +error: URI suffix must contain only query and/or fragment + --> $DIR/typed-uris-invalid-syntax.rs:18:28 + | +18 | uri!("/mount", simple, "http://"); + | ^^^^^^^^^ + +error: expected 1, 2, or 3 arguments, found 4 + --> $DIR/typed-uris-invalid-syntax.rs:19:36 + | +19 | uri!("/mount", simple, "#foo", "?foo"); + | ^^^^^^ + +error: invalid URI: unexpected EOF: expected token ':' at index 5 + --> $DIR/typed-uris-invalid-syntax.rs:20:10 + | +20 | uri!("mount", simple(10, "hi"), "http://"); + | ^^^^^^^ + +error: URI suffix must contain only query and/or fragment + --> $DIR/typed-uris-invalid-syntax.rs:21:38 + | +21 | uri!("/mount", simple(10, "hi"), "http://"); + | ^^^^^^^^^ + +error: URI prefix cannot contain query part + --> $DIR/typed-uris-invalid-syntax.rs:22:10 + | +22 | uri!("/mount?foo", simple(10, "hi"), "foo/bar?foo#bar"); + | ^^^^^^^^^^^^ + +error: URI suffix must contain only query and/or fragment + --> $DIR/typed-uris-invalid-syntax.rs:23:38 + | +23 | uri!("/mount", simple(10, "hi"), "a/b"); + | ^^^^^ + +error: expected 1, 2, or 3 arguments, found 4 + --> $DIR/typed-uris-invalid-syntax.rs:24:46 + | +24 | uri!("/mount", simple(10, "hi"), "#foo", "?foo"); + | ^^^^^^ + +error: invalid URI: unexpected token '<' at index 7 + --> $DIR/typed-uris-invalid-syntax.rs:25:10 + | +25 | uri!("/mount/", simple); | ^^^^^^^^^^^^^ -error: unexpected end of input, call to `uri!` cannot be empty - --> $DIR/typed-uris-invalid-syntax.rs:15:5 +error: expected at least 1 argument, found none + --> $DIR/typed-uris-invalid-syntax.rs:26:5 | -15 | uri!(); +26 | uri!(); | ^^^^^^^ | = note: this error originates in a macro (in Nightly builds, run with -Z macro-backtrace for more info) +error: unexpected token + --> $DIR/typed-uris-invalid-syntax.rs:27:16 + | +27 | uri!(simple: id = ); + | ^ + error: unexpected end of input, expected expression - --> $DIR/typed-uris-invalid-syntax.rs:16:5 + --> $DIR/typed-uris-invalid-syntax.rs:28:16 | -16 | uri!(simple: id = ); - | ^^^^^^^^^^^^^^^^^^^^ +28 | uri!(simple(id = )); + | ^^^^^^^ + +error: invalid URI: unexpected EOF: expected some token at index 0 + --> $DIR/typed-uris-invalid-syntax.rs:29:10 | - = note: this error originates in a macro (in Nightly builds, run with -Z macro-backtrace for more info) +29 | uri!("*", simple(10), "hi"); + | ^^^ + +error: URI suffix must contain only query and/or fragment + --> $DIR/typed-uris-invalid-syntax.rs:30:40 + | +30 | uri!("some.host:8088", simple(10), "hi"); + | ^^^^ + +error: expected identifier + --> $DIR/typed-uris-invalid-syntax.rs:33:18 + | +33 | uri!("/foo", "bar"); + | ^^^^^ + +error: unexpected token + --> $DIR/typed-uris-invalid-syntax.rs:34:17 + | +34 | uri!("/foo" ("bar")); + | ^^^^^^^ + +error: URI prefix cannot contain query part + --> $DIR/typed-uris-invalid-syntax.rs:35:10 + | +35 | uri!("ftp:?", index); + | ^^^^^^^ + +error: URI suffix must contain only query and/or fragment + --> $DIR/typed-uris-invalid-syntax.rs:36:25 + | +36 | uri!("ftp:", index, "foo#bar"); + | ^^^^^^^^^ + +error: URI suffix must contain only query and/or fragment + --> $DIR/typed-uris-invalid-syntax.rs:37:25 + | +37 | uri!("ftp:", index, "foo?bar"); + | ^^^^^^^^^ + +error: route expects 2 parameters but 0 were supplied + --- note: route `simple` has uri "//" + --> $DIR/typed-uris-invalid-syntax.rs:13:10 + | +13 | uri!(simple,); + | ^^^^^^ diff --git a/core/codegen/tests/ui-fail-stable/uri_display_type_errors.stderr b/core/codegen/tests/ui-fail-stable/uri_display_type_errors.stderr index 45f0b8f9..234b8595 100644 --- a/core/codegen/tests/ui-fail-stable/uri_display_type_errors.stderr +++ b/core/codegen/tests/ui-fail-stable/uri_display_type_errors.stderr @@ -1,61 +1,61 @@ -error[E0277]: the trait bound `BadType: UriDisplay` is not satisfied +error[E0277]: the trait bound `BadType: UriDisplay` is not satisfied --> $DIR/uri_display_type_errors.rs:6:13 | 6 | struct Bar1(BadType); - | ^^^^^^^ the trait `UriDisplay` is not implemented for `BadType` + | ^^^^^^^ the trait `UriDisplay` is not implemented for `BadType` | - = note: required because of the requirements on the impl of `UriDisplay` for `&BadType` + = note: required because of the requirements on the impl of `UriDisplay` for `&BadType` -error[E0277]: the trait bound `BadType: UriDisplay` is not satisfied +error[E0277]: the trait bound `BadType: UriDisplay` is not satisfied --> $DIR/uri_display_type_errors.rs:10:5 | 10 | field: BadType, - | ^^^^^ the trait `UriDisplay` is not implemented for `BadType` + | ^^^^^ the trait `UriDisplay` is not implemented for `BadType` | - = note: required because of the requirements on the impl of `UriDisplay` for `&BadType` + = note: required because of the requirements on the impl of `UriDisplay` for `&BadType` -error[E0277]: the trait bound `BadType: UriDisplay` is not satisfied +error[E0277]: the trait bound `BadType: UriDisplay` is not satisfied --> $DIR/uri_display_type_errors.rs:16:5 | 16 | bad: BadType, - | ^^^ the trait `UriDisplay` is not implemented for `BadType` + | ^^^ the trait `UriDisplay` is not implemented for `BadType` | - = note: required because of the requirements on the impl of `UriDisplay` for `&BadType` + = note: required because of the requirements on the impl of `UriDisplay` for `&BadType` -error[E0277]: the trait bound `BadType: UriDisplay` is not satisfied +error[E0277]: the trait bound `BadType: UriDisplay` is not satisfied --> $DIR/uri_display_type_errors.rs:21:11 | 21 | Inner(BadType), - | ^^^^^^^ the trait `UriDisplay` is not implemented for `BadType` + | ^^^^^^^ the trait `UriDisplay` is not implemented for `BadType` | - = note: required because of the requirements on the impl of `UriDisplay` for `&BadType` + = note: required because of the requirements on the impl of `UriDisplay` for `&BadType` = note: 1 redundant requirements hidden - = note: required because of the requirements on the impl of `UriDisplay` for `&&BadType` + = note: required because of the requirements on the impl of `UriDisplay` for `&&BadType` -error[E0277]: the trait bound `BadType: UriDisplay` is not satisfied +error[E0277]: the trait bound `BadType: UriDisplay` is not satisfied --> $DIR/uri_display_type_errors.rs:27:9 | 27 | field: BadType, - | ^^^^^ the trait `UriDisplay` is not implemented for `BadType` + | ^^^^^ the trait `UriDisplay` is not implemented for `BadType` | - = note: required because of the requirements on the impl of `UriDisplay` for `&BadType` + = note: required because of the requirements on the impl of `UriDisplay` for `&BadType` = note: 1 redundant requirements hidden - = note: required because of the requirements on the impl of `UriDisplay` for `&&BadType` + = note: required because of the requirements on the impl of `UriDisplay` for `&&BadType` -error[E0277]: the trait bound `BadType: UriDisplay` is not satisfied +error[E0277]: the trait bound `BadType: UriDisplay` is not satisfied --> $DIR/uri_display_type_errors.rs:35:9 | 35 | other: BadType, - | ^^^^^ the trait `UriDisplay` is not implemented for `BadType` + | ^^^^^ the trait `UriDisplay` is not implemented for `BadType` | - = note: required because of the requirements on the impl of `UriDisplay` for `&BadType` + = note: required because of the requirements on the impl of `UriDisplay` for `&BadType` = note: 1 redundant requirements hidden - = note: required because of the requirements on the impl of `UriDisplay` for `&&BadType` + = note: required because of the requirements on the impl of `UriDisplay` for `&&BadType` -error[E0277]: the trait bound `BadType: UriDisplay` is not satisfied +error[E0277]: the trait bound `BadType: UriDisplay` is not satisfied --> $DIR/uri_display_type_errors.rs:40:12 | 40 | struct Baz(BadType); - | ^^^^^^^ the trait `UriDisplay` is not implemented for `BadType` + | ^^^^^^^ the trait `UriDisplay` is not implemented for `BadType` | - = note: required because of the requirements on the impl of `UriDisplay` for `&BadType` + = note: required because of the requirements on the impl of `UriDisplay` for `&BadType` diff --git a/core/codegen/tests/ui-fail/typed-uri-bad-type.rs b/core/codegen/tests/ui-fail/typed-uri-bad-type.rs index 4379561b..5d4cd568 100644 --- a/core/codegen/tests/ui-fail/typed-uri-bad-type.rs +++ b/core/codegen/tests/ui-fail/typed-uri-bad-type.rs @@ -42,34 +42,42 @@ fn other_q(id: usize, rest: S) { } fn optionals_q(id: Option, name: Result>) { } fn main() { - uri!(simple: id = "hi"); + uri!(simple(id = "hi")); - uri!(simple: "hello"); + uri!(simple("hello")); - uri!(simple: id = 239239i64); + uri!(simple(id = 239239i64)); - uri!(not_uri_display: 10, S); + uri!(not_uri_display(10, S)); // This one is okay. In paths, a value _must_ be supplied. - uri!(optionals: id = 10, name = "bob".to_string()); + uri!(optionals(id = 10, name = "bob".to_string())); - uri!(optionals: id = Some(10), name = Ok("bob".into())); + uri!(optionals(id = Some(10), name = Ok("bob".into()))); - uri!(simple_q: "hi"); + uri!(simple_q("hi")); - uri!(simple_q: id = "hi"); + uri!(simple_q(id = "hi")); - uri!(other_q: 100, S); + uri!(other_q(100, S)); - uri!(other_q: rest = S, id = 100); + uri!(other_q(rest = S, id = 100)); - uri!(other_q: rest = _, id = 100); + uri!(other_q(rest = _, id = 100)); - uri!(other_q: rest = S, id = _); + uri!(other_q(rest = S, id = _)); // These are all okay. - uri!(optionals_q: _, _); - uri!(optionals_q: id = Some(10), name = Some("Bob".to_string())); - uri!(optionals_q: _, Some("Bob".into())); - uri!(optionals_q: id = _, name = _); + uri!(optionals_q(_, _)); + uri!(optionals_q(id = Some(10), name = Some("Bob".to_string()))); + uri!(optionals_q(_, Some("Bob".into()))); + uri!(optionals_q(id = _, name = _)); + + // Invalid prefixes. + uri!(uri!("?foo#bar"), simple(id = "hi")); + uri!(uri!("*"), simple(id = "hi")); + + // Invalid suffix. + uri!(_, simple(id = "hi"), uri!("*")); + uri!(_, simple(id = "hi"), uri!("/foo/bar")); } diff --git a/core/codegen/tests/ui-fail/typed-uris-bad-params.rs b/core/codegen/tests/ui-fail/typed-uris-bad-params.rs index 8ffe0f97..68464338 100644 --- a/core/codegen/tests/ui-fail/typed-uris-bad-params.rs +++ b/core/codegen/tests/ui-fail/typed-uris-bad-params.rs @@ -19,51 +19,52 @@ fn ignored() { } fn main() { uri!(has_one); + uri!(has_one()); - uri!(has_one: 1, 23); - uri!(has_one: "Hello", 23, ); - uri!(has_one_guarded: "hi", 100); + uri!(has_one(1, 23)); + uri!(has_one("Hello", 23, )); + uri!(has_one_guarded("hi", 100)); - uri!(has_two: 10, "hi", "there"); - uri!(has_two: 10); + uri!(has_two(10, "hi", "there")); + uri!(has_two(10)); - uri!(has_one: id = 100, name = "hi"); + uri!(has_one(id = 100, name = "hi")); - uri!(has_one: name = 100, id = 100); + uri!(has_one(name = 100, id = 100)); - uri!(has_one: name = 100, age = 50, id = 100); + uri!(has_one(name = 100, age = 50, id = 100)); - uri!(has_one: name = 100, age = 50, id = 100, id = 50); + uri!(has_one(name = 100, age = 50, id = 100, id = 50)); - uri!(has_one: id = 100, id = 100); + uri!(has_one(id = 100, id = 100)); - uri!(has_one: id = 100, id = 100, ); + uri!(has_one(id = 100, id = 100, )); - uri!(has_one: name = "hi"); + uri!(has_one(name = "hi")); - uri!(has_one_guarded: cookies = "hi", id = 100); + uri!(has_one_guarded(cookies = "hi", id = 100)); - uri!(has_one_guarded: id = 100, cookies = "hi"); + uri!(has_one_guarded(id = 100, cookies = "hi")); - uri!(has_two: id = 100, id = 100, ); + uri!(has_two(id = 100, id = 100, )); - uri!(has_two: name = "hi"); + uri!(has_two(name = "hi")); - uri!(has_two: cookies = "hi", id = 100, id = 10, id = 10); + uri!(has_two(cookies = "hi", id = 100, id = 10, id = 10)); - uri!(has_two: id = 100, cookies = "hi"); + uri!(has_two(id = 100, cookies = "hi")); - uri!(optionals: id = _, name = "bob".into()); + uri!(optionals(id = _, name = "bob".into())); - uri!(optionals: id = 10, name = _); + uri!(optionals(id = 10, name = _)); - uri!(ignored: _); + uri!(ignored(_)); - uri!(ignored: _ = 10); + uri!(ignored(_ = 10)); - uri!(ignored: 10, 20); + uri!(ignored(10, 20)); - uri!(ignored: num = 10); + uri!(ignored(num = 10)); - uri!(ignored: 10, "10"); + uri!(ignored(10, "10")); } diff --git a/core/codegen/tests/ui-fail/typed-uris-invalid-syntax.rs b/core/codegen/tests/ui-fail/typed-uris-invalid-syntax.rs index fe8384ba..2d0105b1 100644 --- a/core/codegen/tests/ui-fail/typed-uris-invalid-syntax.rs +++ b/core/codegen/tests/ui-fail/typed-uris-invalid-syntax.rs @@ -1,17 +1,38 @@ #[macro_use] extern crate rocket; +#[get("/")] +fn index() { } + #[post("//")] fn simple(id: i32, name: String) -> &'static str { "" } fn main() { uri!(simple: id = 100, "Hello"); - uri!(simple: "Hello", id = 100); + uri!(simple(id = 100, "Hello")); + uri!(simple("Hello", id = 100)); uri!(simple,); uri!(simple:); - uri!("/mount"); uri!("/mount",); uri!("mount", simple); + uri!("mount", simple, "http://"); + uri!("/mount", simple, "http://"); + uri!("/mount", simple, "#foo", "?foo"); + uri!("mount", simple(10, "hi"), "http://"); + uri!("/mount", simple(10, "hi"), "http://"); + uri!("/mount?foo", simple(10, "hi"), "foo/bar?foo#bar"); + uri!("/mount", simple(10, "hi"), "a/b"); + uri!("/mount", simple(10, "hi"), "#foo", "?foo"); uri!("/mount/", simple); uri!(); uri!(simple: id = ); + uri!(simple(id = )); + uri!("*", simple(10), "hi"); + uri!("some.host:8088", simple(10), "hi"); + uri!("?foo"); + uri!(""); + uri!("/foo", "bar"); + uri!("/foo" ("bar")); + uri!("ftp:?", index); + uri!("ftp:", index, "foo#bar"); + uri!("ftp:", index, "foo?bar"); } diff --git a/core/codegen/tests/uri_display.rs b/core/codegen/tests/uri_display.rs index e3748a5e..95593935 100644 --- a/core/codegen/tests/uri_display.rs +++ b/core/codegen/tests/uri_display.rs @@ -1,6 +1,6 @@ #[macro_use] extern crate rocket; -use rocket::http::uri::{UriDisplay, Query, Path}; +use rocket::http::uri::fmt::{UriDisplay, Query, Path}; macro_rules! assert_uri_display_query { ($v:expr, $s:expr) => ( diff --git a/core/http/Cargo.toml b/core/http/Cargo.toml index 7d2f3771..dd790853 100644 --- a/core/http/Cargo.toml +++ b/core/http/Cargo.toml @@ -34,7 +34,7 @@ ref-cast = "1.0" uncased = "0.9.6" parking_lot = "0.11" either = "1" -pear = "0.2.1" +pear = "0.2.3" pin-project-lite = "0.2" memchr = "2" stable-pattern = "0.1" diff --git a/core/http/src/parse/indexed.rs b/core/http/src/parse/indexed.rs index 1195aafe..3d1a13e0 100644 --- a/core/http/src/parse/indexed.rs +++ b/core/http/src/parse/indexed.rs @@ -164,17 +164,32 @@ impl<'a, T: ?Sized + ToOwned + 'a> Indexed<'a, T> self.len() == 0 } + /// Make `self` concrete by allocating if indexed. + /// + /// # Panics + /// + /// Panics if `self` is an indexed string and `source` is None. + pub fn into_concrete(self, source: &Option>) -> Cow<'a, T> { + if self.is_indexed() && source.is_none() { + panic!("cannot concretize indexed str to str without base string!") + } + + match self { + Indexed::Indexed(i, j) => Cow::Owned(source.as_ref().unwrap()[i..j].to_owned()), + Indexed::Concrete(string) => string, + } + } + /// Retrieves the string `self` corresponds to. If `self` is derived from /// indexes, the corresponding subslice of `source` is returned. Otherwise, /// the concrete string is returned. /// /// # Panics /// - /// Panics if `self` is an indexed string and `string` is None. - // pub fn to_source(&self, source: Option<&'a T>) -> &T { + /// Panics if `self` is an indexed string and `source` is None. pub fn from_cow_source<'s>(&'s self, source: &'s Option>) -> &'s T { if self.is_indexed() && source.is_none() { - panic!("Cannot convert indexed str to str without base string!") + panic!("cannot convert indexed str to str without base string!") } match *self { diff --git a/core/http/src/parse/uri/error.rs b/core/http/src/parse/uri/error.rs index 4e215ed2..d5565689 100644 --- a/core/http/src/parse/uri/error.rs +++ b/core/http/src/parse/uri/error.rs @@ -18,6 +18,7 @@ pub struct Error<'a> { pub(crate) index: usize, } +#[doc(hidden)] impl<'a> From>> for Error<'a> { fn from(inner: ParseError>) -> Self { let expected = inner.error.map(|t| t.into(), |v| v.values.into()); diff --git a/core/http/src/parse/uri/mod.rs b/core/http/src/parse/uri/mod.rs index 6e6e6fd3..d8029e5f 100644 --- a/core/http/src/parse/uri/mod.rs +++ b/core/http/src/parse/uri/mod.rs @@ -4,9 +4,9 @@ pub(crate) mod tables; #[cfg(test)] mod tests; -use crate::uri::{Uri, Origin, Absolute, Authority}; +use crate::uri::{Uri, Origin, Absolute, Authority, Reference}; -use self::parser::{uri, origin, authority_only, absolute_only}; +use self::parser::*; pub use self::error::Error; @@ -24,10 +24,15 @@ pub fn origin_from_str(s: &str) -> Result, Error<'_>> { #[inline] pub fn authority_from_str(s: &str) -> Result, Error<'_>> { - Ok(parse!(authority_only: RawInput::new(s.as_bytes()))?) + Ok(parse!(authority: RawInput::new(s.as_bytes()))?) } #[inline] pub fn absolute_from_str(s: &str) -> Result, Error<'_>> { - Ok(parse!(absolute_only: RawInput::new(s.as_bytes()))?) + Ok(parse!(absolute: RawInput::new(s.as_bytes()))?) +} + +#[inline] +pub fn reference_from_str(s: &str) -> Result, Error<'_>> { + Ok(parse!(reference: RawInput::new(s.as_bytes()))?) } diff --git a/core/http/src/parse/uri/parser.rs b/core/http/src/parse/uri/parser.rs index 13458327..68b4712e 100644 --- a/core/http/src/parse/uri/parser.rs +++ b/core/http/src/parse/uri/parser.rs @@ -1,67 +1,168 @@ use pear::parsers::*; -use pear::input::{Extent, Rewind}; -use pear::macros::{parser, switch, parse_current_marker, parse_error, parse_try}; +use pear::combinators::*; +use pear::input::{self, Pear, Extent, Rewind, Input}; +use pear::macros::{parser, switch, parse_error, parse_try}; -use crate::uri::{Uri, Origin, Authority, Absolute, Host}; -use crate::parse::uri::tables::{is_reg_name_char, is_pchar, is_qchar}; +use crate::uri::{Uri, Origin, Authority, Absolute, Reference, Asterisk}; +use crate::parse::uri::tables::*; use crate::parse::uri::RawInput; type Result<'a, T> = pear::input::Result>; +// SAFETY: Every `unsafe` here comes from bytes -> &str conversions. Since all +// bytes are checked against tables in `tables.rs`, all of which allow only +// ASCII characters, these are all safe. + +// TODO: We should cap the source we pass into `raw` to the bytes we've actually +// checked. Otherwise, we could have good bytes followed by unchecked bad bytes +// since eof() may not called. However, note that we only actually expose these +// parsers via `parse!()`, which _does_ call `eof()`, so we're externally okay. + +#[parser(rewind)] +pub fn complete(input: &mut Pear, p: P) -> input::Result + where I: Input + Rewind, P: FnOnce(&mut Pear) -> input::Result +{ + (p()?, eof()?).0 +} + +/// TODO: Have a way to ask for for preference in ambiguity resolution. +/// * An ordered [Preference] is probably best. +/// * Need to filter/uniqueify. See `uri-pref`. +/// Once we have this, we should probably set the default so that `authority` is +/// preferred over `absolute`, otherwise something like `foo:3122` is absolute. #[parser] pub fn uri<'a>(input: &mut RawInput<'a>) -> Result<'a, Uri<'a>> { - match input.items.len() { - 0 => parse_error!("empty URI")?, - 1 => switch! { - eat(b'*') => Uri::Asterisk, - eat(b'/') => Uri::Origin(Origin::new::<_, &str>("/", None)), - eat(b'%') => parse_error!("'%' is not a valid URI")?, - _ => 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.start.into(), None, host, None)) - } - }, - // NOTE: We accept '%' even when it isn't followed by two hex digits. - _ => switch! { - peek(b'/') => Uri::Origin(origin()?), - _ => absolute_or_authority()? - } + // To resolve all ambiguities with preference, we might need to look at the + // complete string twice: origin/ref, asterisk/ref, authority/absolute. + switch! { + complete(|i| eat(i, b'*')) => Uri::Asterisk(Asterisk), + origin@complete(origin) => Uri::Origin(origin), + authority@complete(authority) => Uri::Authority(authority), + absolute@complete(absolute) => Uri::Absolute(absolute), + _ => Uri::Reference(reference()?) } } #[parser] pub fn origin<'a>(input: &mut RawInput<'a>) -> Result<'a, Origin<'a>> { - (peek(b'/')?, path_and_query(is_pchar, is_qchar)?).1 + let (_, path, query) = (peek(b'/')?, path()?, query()?); + unsafe { Origin::raw(input.start.into(), path.into(), query) } } #[parser] -fn path_and_query<'a, F, Q>( - input: &mut RawInput<'a>, - is_path_char: F, - is_query_char: Q -) -> Result<'a, Origin<'a>> - where F: Fn(&u8) -> bool + Copy, Q: Fn(&u8) -> bool + Copy -{ - let path = take_while(is_path_char)?; - let query = parse_try!(eat(b'?') => take_while(is_query_char)?); +pub fn authority<'a>(input: &mut RawInput<'a>) -> Result<'a, Authority<'a>> { + let prefix = take_while(is_reg_name_char)?; + let (user_info, host, port) = switch! { + peek(b'[') if prefix.is_empty() => (None, host()?, port()?), + eat(b':') => { + let suffix = take_while(is_reg_name_char)?; + switch! { + peek(b':') => { + let end = (take_while(is_user_info_char)?, eat(b'@')?).0; + (input.span(prefix, end), host()?, port()?) + }, + eat(b'@') => (input.span(prefix, suffix), host()?, port()?), + // FIXME: Rewind to just after prefix to get the right context + // to be able to call `port()`. Then remove `maybe_port()`. + _ => (None, prefix, maybe_port(&suffix)?) + } + }, + eat(b'@') => (Some(prefix), host()?, port()?), + _ => (None, prefix, None), + }; - if path.is_empty() && query.is_none() { - parse_error!("expected path or query, found neither")? - } else { - // We know the string is ASCII because of the `is_char` checks above. - Ok(unsafe { Origin::raw(input.start.into(), path.into(), query.map(|q| q.into())) }) + unsafe { Authority::raw(input.start.into(), user_info, host, port) } +} + +#[parser] +pub fn absolute<'a>(input: &mut RawInput<'a>) -> Result<'a, Absolute<'a>> { + let scheme = take_some_while(is_scheme_char)?; + if !scheme.get(0).map_or(false, |b| b.is_ascii_alphabetic()) { + parse_error!("invalid scheme")?; + } + + let (_, (authority, path), query) = (eat(b':')?, hier_part()?, query()?); + unsafe { Absolute::raw(input.start.into(), scheme, authority, path, query) } +} + +#[parser] +pub fn reference<'a>( + input: &mut RawInput<'a>, +) -> Result<'a, Reference<'a>> { + let prefix = take_while(is_scheme_char)?; + let (scheme, authority, path) = switch! { + peek(b':') if prefix.is_empty() => parse_error!("missing scheme")?, + eat(b':') => { + if !prefix.get(0).map_or(false, |b| b.is_ascii_alphabetic()) { + parse_error!("invalid scheme")?; + } + + let (authority, path) = hier_part()?; + (Some(prefix), authority, path) + }, + peek_slice(b"//") if prefix.is_empty() => { + let (authority, path) = hier_part()?; + (None, authority, path) + }, + _ => { + let path = path()?; + let full_path = input.span(prefix, path).unwrap_or(none()?); + (None, None, full_path) + }, + }; + + let (source, query, fragment) = (input.start.into(), query()?, fragment()?); + unsafe { Reference::raw(source, scheme, authority, path, query, fragment) } +} + +#[parser] +pub fn hier_part<'a>( + input: &mut RawInput<'a> +) -> Result<'a, (Option>, Extent<&'a [u8]>)> { + switch! { + eat_slice(b"//") => { + let authority = authority()?; + let path = parse_try!(peek(b'/') => path()? => || none()?); + (Some(authority), path) + }, + _ => (None, path()?) } } #[parser] -fn port_from<'a>(input: &mut RawInput<'a>, bytes: &[u8]) -> Result<'a, u16> { +fn host<'a>( + input: &mut RawInput<'a>, +) -> Result<'a, Extent<&'a [u8]>> { + switch! { + peek(b'[') => enclosed(b'[', is_pchar, b']')?, + _ => take_while(is_reg_name_char)? + } +} + +#[parser] +fn port<'a>( + input: &mut RawInput<'a>, +) -> Result<'a, Option> { + if !succeeds(input, |i| eat(i, b':')) { + return Ok(None); + } + + let bytes = take_n_while(5, |c| c.is_ascii_digit())?; + maybe_port(&bytes)? +} + +// FIXME: The context here is wrong since it's empty. We should reset to +// current - bytes.len(). Or something like that. +#[parser] +fn maybe_port<'a>(input: &mut RawInput<'a>, bytes: &[u8]) -> Result<'a, Option> { + if bytes.len() > 5 { + parse_error!("port len is out of range")?; + } else if !bytes.iter().all(|b| b.is_ascii_digit()) { + parse_error!("invalid port bytes")?; + } + let mut port_num: u32 = 0; for (b, i) in bytes.iter().rev().zip(&[1, 10, 100, 1000, 10000]) { - if !b.is_ascii_digit() { - parse_error!("port byte is out of range")?; - } - port_num += (b - b'0') as u32 * i; } @@ -69,123 +170,20 @@ fn port_from<'a>(input: &mut RawInput<'a>, bytes: &[u8]) -> Result<'a, u16> { parse_error!("port out of range: {}", port_num)?; } - Ok(port_num as u16) + Some(port_num as u16) } #[parser] -fn port<'a>(input: &mut RawInput<'a>) -> Result<'a, u16> { - let port = take_n_while(5, |c| c.is_ascii_digit())?; - port_from(&port)? +fn path<'a>(input: &mut RawInput<'a>) -> Result<'a, Extent<&'a [u8]>> { + take_while(is_pchar)? } #[parser] -fn authority<'a>( - input: &mut RawInput<'a>, - user_info: Option> -) -> 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 = parse_try!(eat(b':') => port()?); - unsafe { Authority::raw(input.start.into(), user_info, host, port) } -} - -// Callers must ensure that `scheme` is actually ASCII. -#[parser] -fn absolute<'a>( - input: &mut RawInput<'a>, - scheme: Extent<&'a [u8]> -) -> Result<'a, Absolute<'a>> { - let (authority, path_and_query) = switch! { - eat_slice(b"://") => { - let before_auth = parse_current_marker!(); - let left = take_while(|c| is_reg_name_char(c) || *c == b':')?; - let authority = switch! { - eat(b'@') => authority(Some(left))?, - _ => { - input.rewind_to(before_auth); - authority(None)? - } - }; - - let path_and_query = parse_try!(path_and_query(is_pchar, is_qchar)); - (Some(authority), path_and_query) - }, - eat(b':') => (None, Some(path_and_query(is_pchar, is_qchar)?)), - _ => parse_error!("expected ':' but none was found")? - }; - - // `authority` and `path_and_query` parsers ensure ASCII. - unsafe { Absolute::raw(input.start.into(), scheme, authority, path_and_query) } +fn query<'a>(input: &mut RawInput<'a>) -> Result<'a, Option>> { + parse_try!(eat(b'?') => take_while(is_qchar)?) } #[parser] -pub fn authority_only<'a>(input: &mut RawInput<'a>) -> Result<'a, Authority<'a>> { - if let Uri::Authority(authority) = absolute_or_authority()? { - Ok(authority) - } else { - parse_error!("expected authority URI but found absolute URI")? - } -} - -#[parser] -pub fn absolute_only<'a>(input: &mut RawInput<'a>) -> Result<'a, Absolute<'a>> { - if let Uri::Absolute(absolute) = absolute_or_authority()? { - Ok(absolute) - } else { - parse_error!("expected absolute URI but found authority URI")? - } -} - -#[parser] -fn absolute_or_authority<'a>(input: &mut RawInput<'a>) -> Result<'a, Uri<'a>> { - // If the URI begins with `:`, it must follow with a `port`. - switch! { - peek(b':') => return Ok(Uri::Authority(authority(None)?)), - } - - let start = parse_current_marker!(); - let left = take_while(is_reg_name_char)?; - let mark_at_left = parse_current_marker!(); - switch! { - peek_slice(b":/") => Uri::Absolute(absolute(left)?), - eat(b'@') => Uri::Authority(authority(Some(left))?), - 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':')?; - let authority_here = parse_context!(); - switch! { - eat(b'@') => Uri::Authority(authority(Some(authority_here))?), - peek(b'/') => { - input.rewind_to(mark_at_left); - 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 = parse_try!(eat(b'?') => take_while(is_pchar)?); - if query.is_some() || rest.is_empty() || rest.len() > 5 { - Uri::raw_absolute(input.start.into(), left, rest, query) - } else if let Ok(port) = port_from(input, &rest) { - let host = Host::Raw(left); - let source = input.start.into(); - let port = Some(port); - Uri::Authority(Authority::raw(source, None, host, port)) - } else { - Uri::raw_absolute(input.start.into(), left, rest, query) - } - } - } - }, - _ => { - input.rewind_to(start); - Uri::Authority(authority(None)?) - } - } +fn fragment<'a>(input: &mut RawInput<'a>) -> Result<'a, Option>> { + parse_try!(eat(b'#') => take_while(is_qchar)?) } diff --git a/core/http/src/parse/uri/spec.txt b/core/http/src/parse/uri/spec.txt index a72624ab..b8d75958 100644 --- a/core/http/src/parse/uri/spec.txt +++ b/core/http/src/parse/uri/spec.txt @@ -1,4 +1,26 @@ -RFC 7230 URI Grammar +RFC 7230 Uri-Reference Grammar + +URI-reference = URI / relative-ref + +URI = absolute-URI [ "#" fragment ] + +relative-ref = relative-part [ "?" query ] [ "#" fragment ] + +; like hier-part - rootless + noscheme +relative-part = "//" authority path-abempty + / path-absolute + / path-noscheme ; like rootless but first seg can't have ':' + / path-empty + +fragment = *( pchar / "/" / "?" ) ;; = query + +# unrolled form of `URI-reference` above, for comparison +URI-reference = scheme ":" hier-part [ "?" query ] [ "#" fragment ] + / relative-part [ "?" query ] [ "#" fragment ] + +-------------------------------------------------------------------------------- + +RFC 7230 Target Grammar request-target = origin-form / absolute-form / authority-form / asterisk-form @@ -18,19 +40,6 @@ 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 ] @@ -60,6 +69,8 @@ path-empty = 0 segment = *pchar segment-nz = 1*pchar +segment-nz-nc = 1*( unreserved / pct-encoded / sub-delims / "@" ) + ; non-zero-length segment without any colon ":" pchar = unreserved / pct-encoded / sub-delims / ":" / "@" diff --git a/core/http/src/parse/uri/tables.rs b/core/http/src/parse/uri/tables.rs index 5d9db6c0..dda74401 100644 --- a/core/http/src/parse/uri/tables.rs +++ b/core/http/src/parse/uri/tables.rs @@ -20,13 +20,16 @@ const fn char_table(sets: &[&[u8]]) -> [u8; 256] { table } -const UNRESERVED: &[u8] = &[ +const ALPHA: &[u8] = &[ b'A', b'B', b'C', b'D', b'E', b'F', b'G', b'H', b'I', b'J', b'K', b'L', b'M', b'N', b'O', b'P', b'Q', b'R', b'S', b'T', b'U', b'V', b'W', b'X', b'Y', b'Z', b'a', b'b', b'c', b'd', b'e', b'f', b'g', b'h', b'i', b'j', b'k', b'l', b'm', b'n', b'o', b'p', b'q', b'r', b's', b't', b'u', b'v', - b'w', b'x', b'y', b'z', b'0', b'1', b'2', b'3', b'4', b'5', b'6', b'7', - b'8', b'9', b'-', b'.', b'_', b'~', + b'w', b'x', b'y', b'z' +]; + +const DIGIT: &[u8] = &[ + b'0', b'1', b'2', b'3', b'4', b'5', b'6', b'7', b'8', b'9' ]; const PCT_ENCODED: &[u8] = &[ @@ -38,8 +41,24 @@ const SUB_DELIMS: &[u8] = &[ b'!', b'$', b'&', b'\'', b'(', b')', b'*', b'+', b',', b';', b'=' ]; +const SCHEME_CHAR: [u8; 256] = char_table(&[ + ALPHA, DIGIT, &[b'+', b'-', b'.'] +]); + +const UNRESERVED: [u8; 256] = char_table(&[ + ALPHA, DIGIT, &[b'-', b'.', b'_', b'~'] +]); + +const REG_NAME_CHARS: [u8; 256] = char_table(&[ + &UNRESERVED, PCT_ENCODED, SUB_DELIMS +]); + +const USER_INFO_CHARS: [u8; 256] = char_table(&[ + ®_NAME_CHARS, &[b':'] +]); + pub const PATH_CHARS: [u8; 256] = char_table(&[ - UNRESERVED, PCT_ENCODED, SUB_DELIMS, &[b':', b'@', b'/'] + ®_NAME_CHARS, &[b':', b'@', b'/'] ]); const QUERY_CHARS: [u8; 256] = char_table(&[ @@ -50,13 +69,15 @@ const QUERY_CHARS: [u8; 256] = char_table(&[ &[b'{', b'}', b'[', b']', b'\\', b'^', b'`', b'|'], ]); -const REG_NAME_CHARS: [u8; 256] = char_table(&[ - UNRESERVED, PCT_ENCODED, SUB_DELIMS -]); - #[inline(always)] pub const fn is_pchar(&c: &u8) -> bool { PATH_CHARS[c as usize] != 0 } +#[inline(always)] +pub const fn is_scheme_char(&c: &u8) -> bool { SCHEME_CHAR[c as usize] != 0 } + +#[inline(always)] +pub const fn is_user_info_char(&c: &u8) -> bool { USER_INFO_CHARS[c as usize] != 0 } + #[inline(always)] pub const fn is_qchar(&c: &u8) -> bool { QUERY_CHARS[c as usize] != 0 } diff --git a/core/http/src/parse/uri/tests.rs b/core/http/src/parse/uri/tests.rs index 4ddf9426..26ac8ca6 100644 --- a/core/http/src/parse/uri/tests.rs +++ b/core/http/src/parse/uri/tests.rs @@ -1,16 +1,15 @@ -use crate::uri::{Uri, Origin, Authority, Absolute}; +use crate::uri::{Origin, Authority, Absolute, Asterisk}; use crate::parse::uri::*; -use crate::uri::Host::*; macro_rules! assert_parse_eq { - ($($from:expr => $to:expr),+) => ( + ($($from:expr => $to:expr),+ $(,)?) => ( $( - let expected = $to.into(); + let expected = $to; match from_str($from) { Ok(output) => { if output != expected { println!("Failure on: {:?}", $from); - assert_eq!(output, expected); + assert_eq!(output, expected, "{} != {}", output, expected); } } Err(e) => { @@ -20,12 +19,10 @@ macro_rules! assert_parse_eq { } )+ ); - - ($($from:expr => $to:expr),+,) => (assert_parse_eq!($($from => $to),+)) } macro_rules! assert_no_parse { - ($($from:expr),+) => ( + ($($from:expr),+ $(,)?) => ( $( if let Ok(uri) = from_str($from) { println!("{:?} parsed unexpectedly!", $from); @@ -38,7 +35,7 @@ macro_rules! assert_no_parse { } macro_rules! assert_parse { - ($($from:expr),+) => ( + ($($from:expr),+ $(,)?) => ( $( if let Err(e) = from_str($from) { println!("{:?} failed to parse", $from); @@ -46,12 +43,10 @@ macro_rules! assert_parse { } )+ ); - - ($($from:expr),+,) => (assert_parse!($($from),+)) } macro_rules! assert_displays_eq { - ($($string:expr),+) => ( + ($($string:expr),+ $(,)?) => ( $( let string = $string.into(); match from_str(string) { @@ -75,20 +70,19 @@ macro_rules! assert_displays_eq { ($($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)); + assert_parse_eq!("*" => Origin::path_only("*")); } #[test] #[should_panic] fn test_assert_parse_eq_consecutive() { - assert_parse_eq!("/" => uri_origin("/", None), "/" => Uri::Asterisk); + assert_parse_eq! { + "/" => Origin::ROOT, + "/" => Asterisk + }; } #[test] @@ -103,13 +97,14 @@ fn bad_parses() { "://z7:77777777777777777777777777777`77777777777", // from #1621 - "://@example.com/test", - "://example.com:/test", - "://@example.com:/test", - "://example.com/test?", - "://example.com:/test?", - "://@example.com/test?", - "://@example.com:/test?" + ":/", + + // almost URIs + ":/", + "://", + "::", + ":::", + "a://a::", }; } @@ -129,144 +124,153 @@ fn test_parse_issue_924_samples() { "/rocket/?query=%3E5", ); - assert_no_parse!("/rocket/?query=>5", "/?#foo"); + assert_no_parse!("/rocket/?query=>5"); } #[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), + "*" => Asterisk, + "/" => Origin::ROOT, + "." => Authority::new(None, ".", None), + "_" => Authority::new(None, "_", None), + "1" => Authority::new(None, "1", None), + "b" => Authority::new(None, "b", None), + "%" => Authority::new(None, "%", None), + "?" => Reference::new(None, None, "", "", None), + "#" => Reference::new(None, None, "", None, ""), + ":" => Authority::new(None, "", 0), + "@" => Authority::new("", "", None), ); - assert_no_parse!("?", "#", "%"); + 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")), - "/?t=[rocket|is\\here^`]&{ok}" => uri_origin("/", Some("t=[rocket|is\\here^`]&{ok}")), + "/a/b/c" => Origin::path_only("/a/b/c"), + "//" => Origin::path_only("//"), + "///" => Origin::path_only("///"), + "////" => Origin::path_only("////"), + "/a/b/c?" => Origin::new("/a/b/c", Some("")), + "/a/b/c?abc" => Origin::new("/a/b/c", Some("abc")), + "/a/b/c???" => Origin::new("/a/b/c", Some("??")), + "/a/b/c?a?b?" => Origin::new("/a/b/c", Some("a?b?")), + "/a/b/c?a?b?/c" => Origin::new("/a/b/c", Some("a?b?/c")), + "/?abc" => Origin::new("/", Some("abc")), + "/hi%20there?a=b&c=d" => Origin::new("/hi%20there", Some("a=b&c=d")), + "/c/d/fa/b/c?abc" => Origin::new("/c/d/fa/b/c", Some("abc")), + "/xn--ls8h?emoji=poop" => Origin::new("/xn--ls8h", Some("emoji=poop")), + "/?t=[rocket|is\\here^`]&{ok}" => Origin::new("/", Some("t=[rocket|is\\here^`]&{ok}")), ); } #[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)), + "@:" => Authority::new("", "", 0), + "abc" => Authority::new(None, "abc", None), + "@abc" => Authority::new("", "abc", None), + "a@b" => Authority::new("a", "b", None), + "a@" => Authority::new("a", "", None), + ":@" => Authority::new(":", "", None), + ":@:" => Authority::new(":", "", 0), + "sergio:benitez@spark" => Authority::new("sergio:benitez", "spark", None), + "a:b:c@1.2.3:12121" => Authority::new("a:b:c", "1.2.3", 12121), + "sergio@spark" => Authority::new("sergio", "spark", None), + "sergio@spark:230" => Authority::new("sergio", "spark", 230), + "sergio@[1::]:230" => Authority::new("sergio", "[1::]", 230), + "rocket.rs:8000" => Authority::new(None, "rocket.rs", 8000), + "[1::2::3]:80" => Authority::new(None, "[1::2::3]", 80), + "bar:" => Authority::new(None, "bar", 0), // could be absolute too ); } #[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"))), - ), + "http:/" => Absolute::new("http", None, "/", None), + "http://" => Absolute::new("http", Authority::new(None, "", None), "", None), + "http:///" => Absolute::new("http", Authority::new(None, "", None), "/", None), + "http://a.com:8000" => Absolute::new("http", Authority::new(None, "a.com", 8000), "", None), + "http://foo:8000" => Absolute::new("http", Authority::new(None, "foo", 8000), "", None), + "foo:bar" => Absolute::new("foo", None, "bar", None), + "ftp:::" => Absolute::new("ftp", None, "::", None), + "ftp:::?bar" => Absolute::new("ftp", None, "::", "bar"), + "http://:::@a.b.c.:8000" => + Absolute::new("http", Authority::new(":::", "a.b.c.", 8000), "", None), + "http://sergio:pass@foo.com:8000" => + Absolute::new("http", Authority::new("sergio:pass", "foo.com", 8000), "", None), + "foo:/sergio/pass?hi" => Absolute::new("foo", None, "/sergio/pass", "hi"), + "foo:?hi" => Absolute::new("foo", None, "", "hi"), + "foo:a/b" => Absolute::new("foo", None, "a/b", None), + "foo:a/b?" => Absolute::new("foo", None, "a/b", ""), + "foo:a/b?hi" => Absolute::new("foo", None, "a/b", "hi"), + "foo:/a/b" => Absolute::new("foo", None, "/a/b", None), + "abc://u:p@foo.com:123/a/b?key=value&key2=value2" => + Absolute::new("abc", + Authority::new("u:p", "foo.com", 123), + "/a/b", "key=value&key2=value2"), + "ftp://foo.com:21/abc" => + Absolute::new("ftp", Authority::new(None, "foo.com", 21), "/abc", None), + "http://rocket.rs/abc" => + Absolute::new("http", Authority::new(None, "rocket.rs", None), "/abc", None), + "http://s:b@rocket.rs/abc" => + Absolute::new("http", Authority::new("s:b", "rocket.rs", None), "/abc", None), + "http://rocket.rs/abc?q" => + Absolute::new("http", Authority::new(None, "rocket.rs", None), "/abc", "q"), + "http://rocket.rs" => + Absolute::new("http", Authority::new(None, "rocket.rs", None), "", None), + "git://s::@rocket.rs:443/abc?q" => + Absolute::new("git", Authority::new("s::", "rocket.rs", 443), "/abc", "q"), + "git://:@rocket.rs:443/abc?q" => + Absolute::new("git", Authority::new(":", "rocket.rs", 443), "/abc", "q"), + "a://b?test" => Absolute::new("a", Authority::new(None, "b", None), "", "test"), + "a://b:?test" => Absolute::new("a", Authority::new(None, "b", 0), "", "test"), + "a://b:1?test" => Absolute::new("a", Authority::new(None, "b", 1), "", "test"), }; } +#[test] +fn reference() { + assert_parse_eq!( + "*#" => Reference::new(None, None, "*", None, ""), + "*#h" => Reference::new(None, None, "*", None, "h"), + "@/" => Reference::new(None, None, "@/", None, None), + "@?" => Reference::new(None, None, "@", "", None), + "@?#" => Reference::new(None, None, "@", "", ""), + "@#foo" => Reference::new(None, None, "@", None, "foo"), + "foo/bar" => Reference::new(None, None, "foo/bar", None, None), + "foo/bar?baz" => Reference::new(None, None, "foo/bar", "baz", None), + "foo/bar?baz#cat" => Reference::new(None, None, "foo/bar", "baz", "cat"), + "a?b#c" => Reference::new(None, None, "a", "b", "c"), + "?#" => Reference::new(None, None, "", "", ""), + "ftp:foo/bar?baz#" => Reference::new("ftp", None, "foo/bar", "baz", ""), + "ftp:bar#" => Reference::new("ftp", None, "bar", None, ""), + "ftp:?bar#" => Reference::new("ftp", None, "", "bar", ""), + "ftp:::?bar#" => Reference::new("ftp", None, "::", "bar", ""), + "#foo" => Reference::new(None, None, "", None, "foo"), + "a:/#" => Reference::new("a", None, "/", None, ""), + "a:/?a#" => Reference::new("a", None, "/", "a", ""), + "a:/?a#b" => Reference::new("a", None, "/", "a", "b"), + "a:?a#b" => Reference::new("a", None, "", "a", "b"), + "a://?a#b" => Reference::new("a", Authority::new(None, "", None), "", "a", "b"), + "a://:?a#b" => Reference::new("a", Authority::new(None, "", 0), "", "a", "b"), + "a://:2000?a#b" => Reference::new("a", Authority::new(None, "", 2000), "", "a", "b"), + "a://a:2000?a#b" => Reference::new("a", Authority::new(None, "a", 2000), "", "a", "b"), + "a://a:@2000?a#b" => Reference::new("a", Authority::new("a:", "2000", None), "", "a", "b"), + "a://a:@:80?a#b" => Reference::new("a", Authority::new("a:", "", 80), "", "a", "b"), + "a://a:@b:80?a#b" => Reference::new("a", Authority::new("a:", "b", 80), "", "a", "b"), + ); +} + #[test] fn display() { assert_displays_eq! { - "abc", "@):0", "[a]" + "abc", "@):0", "[a]", + "http://rocket.rs", "http://a:b@rocket.rs", "git://a@b:800/foo?bar", + "git://a@b:800/foo?bar#baz", + "a:b", "a@b", "a?b", "a?b#c", } } diff --git a/core/http/src/raw_str.rs b/core/http/src/raw_str.rs index 3daccd55..cc9ec9e5 100644 --- a/core/http/src/raw_str.rs +++ b/core/http/src/raw_str.rs @@ -6,7 +6,7 @@ use std::fmt; use ref_cast::RefCast; use stable_pattern::{Pattern, Searcher, ReverseSearcher, Split, SplitInternal}; -use crate::uri::encoding::{percent_encode, DEFAULT_ENCODE_SET}; +use crate::uri::fmt::{percent_encode, DEFAULT_ENCODE_SET}; use crate::uncased::UncasedStr; diff --git a/core/http/src/uri/absolute.rs b/core/http/src/uri/absolute.rs index 8d3db5c0..2ec3aad6 100644 --- a/core/http/src/uri/absolute.rs +++ b/core/http/src/uri/absolute.rs @@ -1,12 +1,11 @@ use std::borrow::Cow; -use std::fmt::{self, Display}; +use std::convert::TryFrom; use crate::ext::IntoOwned; use crate::parse::{Extent, IndexedStr}; -use crate::uri::{Authority, Origin, Error, as_utf8_unchecked}; +use crate::uri::{Authority, Path, Query, Data, Error, as_utf8_unchecked, fmt}; -/// A URI with a scheme, authority, path, and query: -/// `http://user:pass@domain.com:4444/path?query`. +/// A URI with a scheme, authority, path, and query. /// /// # Structure /// @@ -14,19 +13,64 @@ use crate::uri::{Authority, Origin, Error, as_utf8_unchecked}; /// URI with all optional parts: /// /// ```text -/// http://user:pass@domain.com:4444/path?query -/// |--| |-----------------------||---------| -/// scheme authority origin +/// http://user:pass@domain.com:4444/foo/bar?some=query +/// |--| |------------------------||------| |--------| +/// scheme authority path query /// ``` /// -/// The scheme part of the absolute URI and at least one of authority or origin -/// are required. +/// Only the scheme part of the URI is required. +/// +/// # Normalization +/// +/// Rocket prefers _normalized_ absolute URIs, an absolute URI with the +/// following properties: +/// +/// * The path and query, if any, are normalized with no empty segments. +/// * If there is an authority, the path is empty or absolute with more than +/// one character. +/// +/// The [`Absolute::is_normalized()`] method checks for normalization while +/// [`Absolute::into_normalized()`] normalizes any absolute URI. +/// +/// As an example, the following URIs are all valid, normalized URIs: +/// +/// ```rust +/// # extern crate rocket; +/// # use rocket::http::uri::Absolute; +/// # let valid_uris = [ +/// "http://rocket.rs", +/// "scheme:/foo/bar", +/// "scheme:/foo/bar?abc", +/// # ]; +/// # for uri in &valid_uris { +/// # let uri = Absolute::parse(uri).unwrap(); +/// # assert!(uri.is_normalized(), "{} non-normal?", uri); +/// # } +/// ``` +/// +/// By contrast, the following are valid but non-normal URIs: +/// +/// ```rust +/// # extern crate rocket; +/// # use rocket::http::uri::Absolute; +/// # let invalid = [ +/// "http://rocket.rs/", // trailing '/' +/// "ftp:/a/b/", // trailing empty segment +/// "ftp:/a//c//d", // two empty segments +/// "ftp:/a/b/?", // empty path segment +/// "ftp:/?foo&", // trailing empty query segment +/// # ]; +/// # for uri in &invalid { +/// # assert!(!Absolute::parse(uri).unwrap().is_normalized()); +/// # } +/// ``` #[derive(Debug, Clone)] pub struct Absolute<'a> { - source: Option>, - scheme: IndexedStr<'a>, - authority: Option>, - origin: Option>, + pub(crate) source: Option>, + pub(crate) scheme: IndexedStr<'a>, + pub(crate) authority: Option>, + pub(crate) path: Data<'a, fmt::Path>, + pub(crate) query: Option>, } impl IntoOwned for Absolute<'_> { @@ -37,53 +81,35 @@ impl IntoOwned for Absolute<'_> { source: self.source.into_owned(), scheme: self.scheme.into_owned(), authority: self.authority.into_owned(), - origin: self.origin.into_owned(), + path: self.path.into_owned(), + query: self.query.into_owned(), } } } impl<'a> Absolute<'a> { - #[inline] - pub(crate) unsafe fn raw( - source: Cow<'a, [u8]>, - scheme: Extent<&'a [u8]>, - authority: Option>, - origin: Option>, - ) -> Absolute<'a> { - Absolute { - authority, origin, - source: Some(as_utf8_unchecked(source)), - scheme: scheme.into(), - } - } - - #[cfg(test)] - pub(crate) fn new( - scheme: &'a str, - authority: Option>, - origin: Option> - ) -> Absolute<'a> { - Absolute { - authority, origin, - source: None, - scheme: Cow::Borrowed(scheme).into(), - } - } - /// 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; + /// # #[macro_use] 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); + /// let uri = Absolute::parse("https://rocket.rs").expect("valid URI"); + /// assert_eq!(uri.scheme(), "https"); + /// assert_eq!(uri.authority().unwrap().host(), "rocket.rs"); + /// assert_eq!(uri.path(), ""); + /// assert!(uri.query().is_none()); + /// + /// // Prefer to use `uri!()` when the input is statically known: + /// let uri = uri!("https://rocket.rs"); + /// assert_eq!(uri.scheme(), "https"); + /// assert_eq!(uri.authority().unwrap().host(), "rocket.rs"); + /// assert_eq!(uri.path(), ""); + /// assert!(uri.query().is_none()); /// ``` pub fn parse(string: &'a str) -> Result, Error<'a>> { crate::parse::uri::absolute_from_str(string) @@ -94,10 +120,8 @@ impl<'a> Absolute<'a> { /// # Example /// /// ```rust - /// # extern crate rocket; - /// use rocket::http::uri::Absolute; - /// - /// let uri = Absolute::parse("ftp://127.0.0.1").expect("valid URI"); + /// # #[macro_use] extern crate rocket; + /// let uri = uri!("ftp://127.0.0.1"); /// assert_eq!(uri.scheme(), "ftp"); /// ``` #[inline(always)] @@ -110,16 +134,14 @@ impl<'a> Absolute<'a> { /// # Example /// /// ```rust - /// # extern crate rocket; - /// use rocket::http::uri::Absolute; - /// - /// let uri = Absolute::parse("https://rocket.rs:80").expect("valid URI"); + /// # #[macro_use] extern crate rocket; + /// let uri = uri!("https://rocket.rs:80"); /// 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"); + /// let uri = uri!("file:/web/home"); /// assert_eq!(uri.authority(), None); /// ``` #[inline(always)] @@ -127,50 +149,154 @@ impl<'a> Absolute<'a> { self.authority.as_ref() } - /// Returns the origin part of the absolute URI, if there is one. + /// Returns the path part. May be empty. /// /// # Example /// /// ```rust - /// # extern crate rocket; - /// use rocket::http::uri::Absolute; + /// # #[macro_use] extern crate rocket; + /// let uri = uri!("ftp://rocket.rs/foo/bar"); + /// assert_eq!(uri.path(), "/foo/bar"); /// - /// 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().unwrap(), "new"); - /// - /// let uri = Absolute::parse("https://rocket.rs").expect("valid URI"); - /// assert_eq!(uri.origin(), None); + /// let uri = uri!("ftp://rocket.rs"); + /// assert!(uri.path().is_empty()); /// ``` #[inline(always)] - pub fn origin(&self) -> Option<&Origin<'a>> { - self.origin.as_ref() + pub fn path(&self) -> Path<'_> { + Path { source: &self.source, data: &self.path } } - /// Sets the authority in `self` to `authority` and returns `self`. + /// Returns the query part with the leading `?`. May be empty. /// /// # Example /// /// ```rust - /// # extern crate rocket; - /// use rocket::http::uri::{Absolute, Authority}; + /// # #[macro_use] extern crate rocket; + /// let uri = uri!("ftp://rocket.rs/foo?bar"); + /// assert_eq!(uri.query().unwrap(), "bar"); /// - /// let uri = Absolute::parse("https://rocket.rs:80").expect("valid URI"); - /// let authority = uri.authority().unwrap(); - /// assert_eq!(authority.host(), "rocket.rs"); - /// assert_eq!(authority.port(), Some(80)); - /// - /// let new_authority = Authority::parse("google.com").unwrap(); - /// let uri = uri.with_authority(new_authority); - /// let authority = uri.authority().unwrap(); - /// assert_eq!(authority.host(), "google.com"); - /// assert_eq!(authority.port(), None); + /// let uri = uri!("ftp://rocket.rs"); + /// assert!(uri.query().is_none()); /// ``` #[inline(always)] - pub fn with_authority(mut self, authority: Authority<'a>) -> Self { - self.set_authority(authority); + pub fn query(&self) -> Option> { + self.query.as_ref().map(|data| Query { source: &self.source, data }) + } + + /// Removes the query part of this URI, if there is any. + /// + /// # Example + /// + /// ```rust + /// # #[macro_use] extern crate rocket; + /// let mut uri = uri!("ftp://rocket.rs/foo?bar"); + /// assert_eq!(uri.query().unwrap(), "bar"); + /// + /// uri.clear_query(); + /// assert!(uri.query().is_none()); + /// ``` + #[inline(always)] + pub fn clear_query(&mut self) { + self.set_query(None); + } + + /// Returns `true` if `self` is normalized. Otherwise, returns `false`. + /// + /// See [Normalization](#normalization) for more information on what it + /// means for an absolute URI to be normalized. Note that `uri!()` always + /// returns a normalized version of its static input. + /// + /// # Example + /// + /// ```rust + /// # #[macro_use] extern crate rocket; + /// use rocket::http::uri::Absolute; + /// + /// assert!(uri!("http://rocket.rs").is_normalized()); + /// assert!(uri!("http://rocket.rs///foo////bar").is_normalized()); + /// + /// assert!(Absolute::parse("http:/").unwrap().is_normalized()); + /// assert!(Absolute::parse("http://").unwrap().is_normalized()); + /// assert!(Absolute::parse("http://foo.rs/foo/bar").unwrap().is_normalized()); + /// assert!(Absolute::parse("foo:bar").unwrap().is_normalized()); + /// + /// assert!(!Absolute::parse("git://rocket.rs/").unwrap().is_normalized()); + /// assert!(!Absolute::parse("http:/foo//bar").unwrap().is_normalized()); + /// assert!(!Absolute::parse("foo:bar?baz&&bop").unwrap().is_normalized()); + /// ``` + pub fn is_normalized(&self) -> bool { + let normalized_query = self.query().map_or(true, |q| q.is_normalized()); + if self.authority().is_some() && !self.path().is_empty() { + self.path().is_normalized(true) + && self.path() != "/" + && normalized_query + } else { + self.path().is_normalized(false) && normalized_query + } + } + + /// Normalizes `self` in-place. Does nothing if `self` is already + /// normalized. + /// + /// # Example + /// + /// ```rust + /// use rocket::http::uri::Absolute; + /// + /// let mut uri = Absolute::parse("git://rocket.rs/").unwrap(); + /// assert!(!uri.is_normalized()); + /// uri.normalize(); + /// assert!(uri.is_normalized()); + /// + /// let mut uri = Absolute::parse("http:/foo//bar").unwrap(); + /// assert!(!uri.is_normalized()); + /// uri.normalize(); + /// assert!(uri.is_normalized()); + /// + /// let mut uri = Absolute::parse("foo:bar?baz&&bop").unwrap(); + /// assert!(!uri.is_normalized()); + /// uri.normalize(); + /// assert!(uri.is_normalized()); + /// ``` + pub fn normalize(&mut self) { + if self.authority().is_some() && !self.path().is_empty() { + if self.path() == "/" { + self.set_path(""); + } else if !self.path().is_normalized(true) { + self.path = self.path().to_normalized(true); + } + } else { + self.path = self.path().to_normalized(false); + } + + if let Some(query) = self.query() { + if !query.is_normalized() { + self.query = query.to_normalized(); + } + } + } + + /// Normalizes `self`. This is a no-op if `self` is already normalized. + /// + /// # Example + /// + /// ```rust + /// use rocket::http::uri::Absolute; + /// + /// let mut uri = Absolute::parse("git://rocket.rs/").unwrap(); + /// assert!(!uri.is_normalized()); + /// assert!(uri.into_normalized().is_normalized()); + /// + /// let mut uri = Absolute::parse("http:/foo//bar").unwrap(); + /// assert!(!uri.is_normalized()); + /// assert!(uri.into_normalized().is_normalized()); + /// + /// let mut uri = Absolute::parse("foo:bar?baz&&bop").unwrap(); + /// assert!(!uri.is_normalized()); + /// assert!(uri.into_normalized().is_normalized()); + /// ``` + pub fn into_normalized(mut self) -> Self { + self.normalize(); self } @@ -179,18 +305,16 @@ impl<'a> Absolute<'a> { /// # Example /// /// ```rust - /// # extern crate rocket; - /// use rocket::http::uri::{Absolute, Authority}; - /// - /// let mut uri = Absolute::parse("https://rocket.rs:80").expect("valid URI"); + /// # #[macro_use] extern crate rocket; + /// let mut uri = uri!("https://rocket.rs:80"); /// let authority = uri.authority().unwrap(); /// assert_eq!(authority.host(), "rocket.rs"); /// assert_eq!(authority.port(), Some(80)); /// - /// let new_authority = Authority::parse("google.com:443").unwrap(); + /// let new_authority = uri!("rocket.rs:443"); /// uri.set_authority(new_authority); /// let authority = uri.authority().unwrap(); - /// assert_eq!(authority.host(), "google.com"); + /// assert_eq!(authority.host(), "rocket.rs"); /// assert_eq!(authority.port(), Some(443)); /// ``` #[inline(always)] @@ -198,53 +322,117 @@ impl<'a> Absolute<'a> { self.authority = Some(authority); } - /// Sets the origin in `self` to `origin` and returns `self`. + /// Sets the authority in `self` to `authority` and returns `self`. /// /// # Example /// /// ```rust - /// # extern crate rocket; - /// use rocket::http::uri::{Absolute, Origin}; + /// # #[macro_use] extern crate rocket; + /// let uri = uri!("https://rocket.rs:80"); + /// let authority = uri.authority().unwrap(); + /// assert_eq!(authority.host(), "rocket.rs"); + /// assert_eq!(authority.port(), Some(80)); /// - /// let mut uri = Absolute::parse("http://rocket.rs/web/?new").unwrap(); - /// let origin = uri.origin().unwrap(); - /// assert_eq!(origin.path(), "/web/"); - /// assert_eq!(origin.query().unwrap(), "new"); - /// - /// let new_origin = Origin::parse("/launch").unwrap(); - /// let uri = uri.with_origin(new_origin); - /// let origin = uri.origin().unwrap(); - /// assert_eq!(origin.path(), "/launch"); - /// assert_eq!(origin.query(), None); + /// let new_authority = uri!("rocket.rs"); + /// let uri = uri.with_authority(new_authority); + /// let authority = uri.authority().unwrap(); + /// assert_eq!(authority.host(), "rocket.rs"); + /// assert_eq!(authority.port(), None); /// ``` #[inline(always)] - pub fn with_origin(mut self, origin: Origin<'a>) -> Self { - self.set_origin(origin); + pub fn with_authority(mut self, authority: Authority<'a>) -> Self { + self.set_authority(authority); self } +} - /// Sets the origin in `self` to `origin`. +/// PRIVATE API. +#[doc(hidden)] +impl<'a> Absolute<'a> { + /// PRIVATE. Used by parser. /// - /// # Example - /// - /// ```rust - /// # extern crate rocket; - /// use rocket::http::uri::{Absolute, Origin}; - /// - /// let mut uri = Absolute::parse("http://rocket.rs/web/?new").unwrap(); - /// let origin = uri.origin().unwrap(); - /// assert_eq!(origin.path(), "/web/"); - /// assert_eq!(origin.query().unwrap(), "new"); - /// - /// let new_origin = Origin::parse("/launch?when=now").unwrap(); - /// uri.set_origin(new_origin); - /// let origin = uri.origin().unwrap(); - /// assert_eq!(origin.path(), "/launch"); - /// assert_eq!(origin.query().unwrap(), "when=now"); - /// ``` - #[inline(always)] - pub fn set_origin(&mut self, origin: Origin<'a>) { - self.origin = Some(origin); + /// SAFETY: `source` must be valid UTF-8. + /// CORRECTNESS: `scheme` must be non-empty. + #[inline] + pub(crate) unsafe fn raw( + source: Cow<'a, [u8]>, + scheme: Extent<&'a [u8]>, + authority: Option>, + path: Extent<&'a [u8]>, + query: Option>, + ) -> Absolute<'a> { + Absolute { + source: Some(as_utf8_unchecked(source)), + scheme: scheme.into(), + authority, + path: Data::raw(path), + query: query.map(Data::raw) + } + } + + /// PRIVATE. Used by tests. + #[cfg(test)] + pub fn new( + scheme: &'a str, + authority: impl Into>>, + path: &'a str, + query: impl Into>, + ) -> Absolute<'a> { + assert!(!scheme.is_empty()); + Absolute::const_new(scheme, authority.into(), path, query.into()) + } + + /// PRIVATE. Used by codegen. + pub const fn const_new( + scheme: &'a str, + authority: Option>, + path: &'a str, + query: Option<&'a str>, + ) -> Absolute<'a> { + Absolute { + source: None, + scheme: IndexedStr::Concrete(Cow::Borrowed(scheme)), + authority, + path: Data { + value: IndexedStr::Concrete(Cow::Borrowed(path)), + decoded_segments: state::Storage::new(), + }, + query: match query { + Some(query) => Some(Data { + value: IndexedStr::Concrete(Cow::Borrowed(query)), + decoded_segments: state::Storage::new(), + }), + None => None, + }, + } + } + + // TODO: Have a way to get a validated `path` to do this. See `Path`? + pub(crate) fn set_path

(&mut self, path: P) + where P: Into> + { + self.path = Data::new(path.into()); + } + + // TODO: Have a way to get a validated `query` to do this. See `Query`? + pub(crate) fn set_query>>>(&mut self, query: Q) { + self.query = query.into().map(Data::new); + } +} + +impl<'a> TryFrom<&'a String> for Absolute<'a> { + type Error = Error<'a>; + + fn try_from(value: &'a String) -> Result { + Absolute::parse(value.as_str()) + } +} + +impl<'a> TryFrom<&'a str> for Absolute<'a> { + type Error = Error<'a>; + + fn try_from(value: &'a str) -> Result { + Absolute::parse(value) } } @@ -252,20 +440,21 @@ impl<'a, 'b> PartialEq> for Absolute<'a> { fn eq(&self, other: &Absolute<'b>) -> bool { self.scheme() == other.scheme() && self.authority() == other.authority() - && self.origin() == other.origin() + && self.path() == other.path() + && self.query() == other.query() } } -impl Display for Absolute<'_> { - 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, ":")? +impl std::fmt::Display for Absolute<'_> { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}:", self.scheme())?; + if let Some(authority) = self.authority() { + write!(f, "//{}", authority)?; } - if let Some(ref origin) = self.origin { - write!(f, "{}", origin)?; + write!(f, "{}", self.path())?; + if let Some(query) = self.query() { + write!(f, "?{}", query)?; } Ok(()) diff --git a/core/http/src/uri/asterisk.rs b/core/http/src/uri/asterisk.rs new file mode 100644 index 00000000..2edadd49 --- /dev/null +++ b/core/http/src/uri/asterisk.rs @@ -0,0 +1,9 @@ +/// The literal `*` URI. +#[derive(Debug, PartialEq, Eq, Hash, Copy, Clone)] +pub struct Asterisk; + +impl std::fmt::Display for Asterisk { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + "*".fmt(f) + } +} diff --git a/core/http/src/uri/authority.rs b/core/http/src/uri/authority.rs index a01d8e6e..d6bbf2d7 100644 --- a/core/http/src/uri/authority.rs +++ b/core/http/src/uri/authority.rs @@ -1,9 +1,10 @@ use std::fmt::{self, Display}; +use std::convert::TryFrom; use std::borrow::Cow; use crate::ext::IntoOwned; use crate::parse::{Extent, IndexedStr}; -use crate::uri::{as_utf8_unchecked, Error}; +use crate::uri::{as_utf8_unchecked, error::Error}; /// A URI with an authority only: `user:pass@host:8000`. /// @@ -21,26 +22,12 @@ use crate::uri::{as_utf8_unchecked, Error}; /// Only the host part of the URI is required. #[derive(Debug, Clone)] pub struct Authority<'a> { - source: Option>, + pub(crate) source: Option>, user_info: Option>, - host: Host>, + host: IndexedStr<'a>, port: Option, } -#[derive(Debug, Clone)] -pub(crate) enum Host { - Bracketed(T), - Raw(T) -} - -impl IntoOwned for Host { - type Owned = Host; - - fn into_owned(self) -> Self::Owned { - self.map_inner(IntoOwned::into_owned) - } -} - impl IntoOwned for Authority<'_> { type Owned = Authority<'static>; @@ -55,31 +42,42 @@ impl IntoOwned for Authority<'_> { } impl<'a> Authority<'a> { + // SAFETY: `source` must be valid UTF-8. + // CORRECTNESS: `host` must be non-empty. pub(crate) unsafe fn raw( source: Cow<'a, [u8]>, user_info: Option>, - host: Host>, + host: Extent<&'a [u8]>, port: Option ) -> Authority<'a> { Authority { source: Some(as_utf8_unchecked(source)), user_info: user_info.map(IndexedStr::from), - host: host.map_inner(IndexedStr::from), - port: port + host: IndexedStr::from(host), + port, } } #[cfg(test)] - pub(crate) fn new( - user_info: Option<&'a str>, - host: Host<&'a str>, - port: Option - ) -> Authority<'a> { + pub fn new( + user_info: impl Into>, + host: &'a str, + port: impl Into>, + ) -> Self { + Authority::const_new(user_info.into(), host, port.into()) + } + + /// PRIVATE. Used by codegen. + #[doc(hidden)] + pub const fn const_new(user_info: Option<&'a str>, host: &'a str, port: Option) -> Self { Authority { source: None, - user_info: user_info.map(|u| Cow::Borrowed(u).into()), - host: host.map_inner(|inner| Cow::Borrowed(inner).into()), - port: port + user_info: match user_info { + Some(info) => Some(IndexedStr::Concrete(Cow::Borrowed(info))), + None => None + }, + host: IndexedStr::Concrete(Cow::Borrowed(host)), + port, } } @@ -89,7 +87,7 @@ impl<'a> Authority<'a> { /// # Example /// /// ```rust - /// # extern crate rocket; + /// # #[macro_use] extern crate rocket; /// use rocket::http::uri::Authority; /// /// // Parse a valid authority URI. @@ -99,7 +97,13 @@ impl<'a> Authority<'a> { /// assert_eq!(uri.port(), None); /// /// // Invalid authority URIs fail to parse. - /// Authority::parse("http://google.com").expect_err("invalid authority"); + /// Authority::parse("https://rocket.rs").expect_err("invalid authority"); + /// + /// // Prefer to use `uri!()` when the input is statically known: + /// let uri = uri!("user:pass@host"); + /// assert_eq!(uri.user_info(), Some("user:pass")); + /// assert_eq!(uri.host(), "host"); + /// assert_eq!(uri.port(), None); /// ``` pub fn parse(string: &'a str) -> Result, Error<'a>> { crate::parse::uri::authority_from_str(string) @@ -108,12 +112,9 @@ impl<'a> Authority<'a> { /// 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(); + /// # #[macro_use] extern crate rocket; + /// let uri = uri!("username:password@host"); /// assert_eq!(uri.user_info(), Some("username:password")); /// ``` pub fn user_info(&self) -> Option<&str> { @@ -127,23 +128,21 @@ impl<'a> Authority<'a> { /// brackets will not be part of the returned string. /// /// # Example - /// /// ```rust - /// # extern crate rocket; - /// use rocket::http::uri::Authority; + /// # #[macro_use] extern crate rocket; /// - /// let uri = Authority::parse("domain.com:123").unwrap(); + /// let uri = uri!("domain.com:123"); /// assert_eq!(uri.host(), "domain.com"); /// - /// let uri = Authority::parse("username:password@host:123").unwrap(); + /// let uri = uri!("username:password@host:123"); /// assert_eq!(uri.host(), "host"); /// - /// let uri = Authority::parse("username:password@[1::2]:123").unwrap(); - /// assert_eq!(uri.host(), "1::2"); + /// let uri = uri!("username:password@[1::2]:123"); + /// assert_eq!(uri.host(), "[1::2]"); /// ``` #[inline(always)] pub fn host(&self) -> &str { - self.host.inner().from_cow_source(&self.source) + self.host.from_cow_source(&self.source) } /// Returns the port part of the authority URI, if there is one. @@ -151,18 +150,16 @@ impl<'a> Authority<'a> { /// # Example /// /// ```rust - /// # extern crate rocket; - /// use rocket::http::uri::Authority; - /// + /// # #[macro_use] extern crate rocket; /// // With a port. - /// let uri = Authority::parse("username:password@host:123").unwrap(); + /// let uri = uri!("username:password@host:123"); /// assert_eq!(uri.port(), Some(123)); /// - /// let uri = Authority::parse("domain.com:8181").unwrap(); + /// let uri = uri!("domain.com:8181"); /// assert_eq!(uri.port(), Some(8181)); /// /// // Without a port. - /// let uri = Authority::parse("username:password@host").unwrap(); + /// let uri = uri!("username:password@host"); /// assert_eq!(uri.port(), None); /// ``` #[inline(always)] @@ -175,7 +172,6 @@ impl<'b> PartialEq> for Authority<'_> { 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() } } @@ -186,11 +182,7 @@ impl Display for Authority<'_> { write!(f, "{}@", user_info)?; } - match self.host { - Host::Bracketed(_) => write!(f, "[{}]", self.host())?, - Host::Raw(_) => write!(f, "{}", self.host())? - } - + self.host().fmt(f)?; if let Some(port) = self.port { write!(f, ":{}", port)?; } @@ -199,29 +191,19 @@ impl Display for Authority<'_> { } } -impl Host { - #[inline] - fn inner(&self) -> &T { - match *self { - Host::Bracketed(ref inner) | Host::Raw(ref inner) => inner - } - } +// Because inference doesn't take `&String` to `&str`. +impl<'a> TryFrom<&'a String> for Authority<'a> { + type Error = Error<'a>; - #[inline] - fn is_bracketed(&self) -> bool { - match *self { - Host::Bracketed(_) => true, - _ => false - } - } - - #[inline] - fn map_inner(self, f: F) -> Host - where F: FnOnce(T) -> U - { - match self { - Host::Bracketed(inner) => Host::Bracketed(f(inner)), - Host::Raw(inner) => Host::Raw(f(inner)) - } + fn try_from(value: &'a String) -> Result { + Authority::parse(value.as_str()) + } +} + +impl<'a> TryFrom<&'a str> for Authority<'a> { + type Error = Error<'a>; + + fn try_from(value: &'a str) -> Result { + Authority::parse(value) } } diff --git a/core/http/src/uri/error.rs b/core/http/src/uri/error.rs new file mode 100644 index 00000000..3b976c61 --- /dev/null +++ b/core/http/src/uri/error.rs @@ -0,0 +1,30 @@ +//! Errors arising from parsing invalid URIs. + +use std::fmt; + +pub use crate::parse::uri::Error; + +/// The error type returned when a URI conversion fails. +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +pub struct TryFromUriError(pub(crate) ()); + +impl fmt::Display for TryFromUriError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + "invalid conversion from general to specific URI variant".fmt(f) + } +} + +/// An error interpreting a segment as a [`PathBuf`] component in +/// [`Segments::to_path_buf()`]. +/// +/// [`PathBuf`]: std::path::PathBuf +/// [`Segments::to_path_buf()`]: crate::uri::Segments::to_path_buf() +#[derive(Debug, PartialEq, Eq, Clone)] +pub enum PathError { + /// 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), +} diff --git a/core/http/src/uri/encoding.rs b/core/http/src/uri/fmt/encoding.rs similarity index 90% rename from core/http/src/uri/encoding.rs rename to core/http/src/uri/fmt/encoding.rs index a7bbdede..dcd08b89 100644 --- a/core/http/src/uri/encoding.rs +++ b/core/http/src/uri/fmt/encoding.rs @@ -4,12 +4,13 @@ use std::borrow::Cow; use percent_encoding::{AsciiSet, utf8_percent_encode}; use crate::RawStr; -use crate::uri::{UriPart, Path, Query}; +use crate::uri::fmt::{Part, Path, Query}; use crate::parse::uri::tables::PATH_CHARS; #[derive(Clone, Copy)] #[allow(non_camel_case_types)] -pub struct UNSAFE_ENCODE_SET(PhantomData

); +pub struct UNSAFE_ENCODE_SET(PhantomData

); + pub trait EncodeSet { const SET: AsciiSet; } @@ -32,7 +33,7 @@ const fn set_from_table(table: &'static [u8; 256]) -> AsciiSet { const PATH_SET: AsciiSet = set_from_table(&PATH_CHARS); -impl Default for UNSAFE_ENCODE_SET

{ +impl Default for UNSAFE_ENCODE_SET

{ #[inline(always)] fn default() -> Self { UNSAFE_ENCODE_SET(PhantomData) } } @@ -51,7 +52,7 @@ impl EncodeSet for UNSAFE_ENCODE_SET { #[derive(Clone, Copy)] #[allow(non_camel_case_types)] -pub struct ENCODE_SET(PhantomData

); +pub struct ENCODE_SET(PhantomData

); impl EncodeSet for ENCODE_SET { const SET: AsciiSet = >::SET diff --git a/core/http/src/uri/formatter.rs b/core/http/src/uri/fmt/formatter.rs similarity index 65% rename from core/http/src/uri/formatter.rs rename to core/http/src/uri/fmt/formatter.rs index ef5f0bc4..1622279f 100644 --- a/core/http/src/uri/formatter.rs +++ b/core/http/src/uri/fmt/formatter.rs @@ -1,25 +1,23 @@ use std::fmt::{self, Write}; use std::marker::PhantomData; +use std::borrow::Cow; use smallvec::SmallVec; -use crate::uri::{UriPart, Path, Query, UriDisplay, Origin, Kind}; +use crate::uri::{Absolute, Origin, Reference}; +use crate::uri::fmt::{UriDisplay, Part, Path, Query, Kind}; /// A struct used to format strings for [`UriDisplay`]. /// /// # Marker Generic: `Formatter` vs. `Formatter` /// -/// Like [`UriDisplay`], the [`UriPart`] parameter `P` in `Formatter

` must be +/// Like [`UriDisplay`], the [`Part`] parameter `P` in `Formatter

` must be /// either [`Path`] or [`Query`] resulting in either `Formatter` or /// `Formatter`. The `Path` version is used when formatting parameters /// in the path part of the URI while the `Query` version is used when /// formatting parameters in the query part of the URI. The /// [`write_named_value()`] method is only available to `UriDisplay`. /// -/// [`UriPart`]: crate::uri::UriPart -/// [`Path`]: crate::uri::Path -/// [`Query`]: crate::uri::Query -/// /// # Overview /// /// A mutable version of this struct is passed to [`UriDisplay::fmt()`]. This @@ -39,10 +37,6 @@ use crate::uri::{UriPart, Path, Query, UriDisplay, Origin, Kind}; /// calls `write_named_vlaue()`, the nested names are joined by a `.`, /// written out followed by a `=`, followed by the value. /// -/// [`UriDisplay`]: crate::uri::UriDisplay -/// [`UriDisplay::fmt()`]: crate::uri::UriDisplay::fmt() -/// [`write_named_value()`]: crate::uri::Formatter::write_named_value() -/// /// # Usage /// /// Usage is fairly straightforward: @@ -59,8 +53,6 @@ use crate::uri::{UriPart, Path, Query, UriDisplay, Origin, Kind}; /// called, after a call to `write_named_value` or `write_value`, or after a /// call to [`refresh()`]. /// -/// [`refresh()`]: crate::uri::Formatter::refresh() -/// /// # Example /// /// The following example uses all of the `write` methods in a varied order to @@ -72,7 +64,7 @@ use crate::uri::{UriPart, Path, Query, UriDisplay, Origin, Kind}; /// # extern crate rocket; /// use std::fmt; /// -/// use rocket::http::uri::{Formatter, UriDisplay, Query}; +/// use rocket::http::uri::fmt::{Formatter, UriDisplay, Query}; /// /// struct Outer { /// value: Inner, @@ -105,7 +97,7 @@ use crate::uri::{UriPart, Path, Query, UriDisplay, Origin, Kind}; /// /// let inner = Inner { value: 0, extra: 1 }; /// let outer = Outer { value: inner, another: 2, extra: 3 }; -/// let uri_string = format!("{}", &outer as &UriDisplay); +/// let uri_string = format!("{}", &outer as &dyn UriDisplay); /// assert_eq!(uri_string, "outer_field.inner_field=0&\ /// outer_field=1&\ /// outer_field=inside&\ @@ -123,17 +115,17 @@ use crate::uri::{UriPart, Path, Query, UriDisplay, Origin, Kind}; /// # #[macro_use] extern crate rocket; /// use std::fmt::{self, Write}; /// -/// use rocket::http::uri::{UriDisplay, Formatter, UriPart, Path, Query}; +/// use rocket::http::uri::fmt::{UriDisplay, Formatter, Part, Path, Query}; /// /// pub struct Complex(u8, u8); /// -/// impl UriDisplay

for Complex { +/// impl UriDisplay

for Complex { /// fn fmt(&self, f: &mut Formatter

) -> fmt::Result { /// write!(f, "{}+{}", self.0, self.1) /// } /// } /// -/// let uri_string = format!("{}", &Complex(42, 231) as &UriDisplay); +/// let uri_string = format!("{}", &Complex(42, 231) as &dyn UriDisplay); /// assert_eq!(uri_string, "42+231"); /// /// #[derive(UriDisplayQuery)] @@ -142,13 +134,15 @@ use crate::uri::{UriPart, Path, Query, UriDisplay, Origin, Kind}; /// } /// /// let message = Message { number: Complex(42, 47) }; -/// let uri_string = format!("{}", &message as &UriDisplay); +/// let uri_string = format!("{}", &message as &dyn UriDisplay); /// assert_eq!(uri_string, "number=42+47"); /// ``` /// -/// [`write_value()`]: crate::uri::Formatter::write_value() -/// [`write_raw()`]: crate::uri::Formatter::write_raw() -pub struct Formatter<'i, P: UriPart> { +/// [`write_named_value()`]: Formatter::write_value() +/// [`write_value()`]: Formatter::write_value() +/// [`write_raw()`]: Formatter::write_raw() +/// [`refresh()`]: Formatter::refresh() +pub struct Formatter<'i, P: Part> { prefixes: SmallVec<[&'static str; 3]>, inner: &'i mut (dyn Write + 'i), previous: bool, @@ -156,7 +150,7 @@ pub struct Formatter<'i, P: UriPart> { _marker: PhantomData

, } -impl<'i, P: UriPart> Formatter<'i, P> { +impl<'i, P: Part> Formatter<'i, P> { #[inline(always)] pub(crate) fn new(inner: &'i mut (dyn Write + 'i)) -> Self { Formatter { @@ -191,11 +185,11 @@ impl<'i, P: UriPart> Formatter<'i, P> { /// # extern crate rocket; /// use std::fmt; /// - /// use rocket::http::uri::{Formatter, UriDisplay, UriPart, Path}; + /// use rocket::http::uri::fmt::{Formatter, UriDisplay, Part, Path}; /// /// struct Foo; /// - /// impl UriDisplay

for Foo { + /// impl UriDisplay

for Foo { /// fn fmt(&self, f: &mut Formatter

) -> fmt::Result { /// f.write_raw("f")?; /// f.write_raw("o")?; @@ -204,7 +198,7 @@ impl<'i, P: UriPart> Formatter<'i, P> { /// } /// /// let foo = Foo; - /// let uri_string = format!("{}", &foo as &UriDisplay); + /// let uri_string = format!("{}", &foo as &dyn UriDisplay); /// assert_eq!(uri_string, "foo"); /// ``` pub fn write_raw>(&mut self, string: S) -> fmt::Result { @@ -247,11 +241,11 @@ impl<'i, P: UriPart> Formatter<'i, P> { /// # extern crate rocket; /// use std::fmt; /// - /// use rocket::http::uri::{Formatter, UriDisplay, UriPart, Path, Query}; + /// use rocket::http::uri::fmt::{Formatter, UriDisplay, Part, Path, Query}; /// /// struct Foo(usize); /// - /// impl UriDisplay

for Foo { + /// impl UriDisplay

for Foo { /// fn fmt(&self, f: &mut Formatter

) -> fmt::Result { /// f.write_value(&self.0) /// } @@ -259,10 +253,10 @@ impl<'i, P: UriPart> Formatter<'i, P> { /// /// let foo = Foo(123); /// - /// let uri_string = format!("{}", &foo as &UriDisplay); + /// let uri_string = format!("{}", &foo as &dyn UriDisplay); /// assert_eq!(uri_string, "123"); /// - /// let uri_string = format!("{}", &foo as &UriDisplay); + /// let uri_string = format!("{}", &foo as &dyn UriDisplay); /// assert_eq!(uri_string, "123"); /// ``` #[inline] @@ -283,7 +277,7 @@ impl<'i, P: UriPart> Formatter<'i, P> { /// # #[macro_use] extern crate rocket; /// use std::fmt; /// - /// use rocket::http::uri::{Formatter, UriDisplay, Query, Path}; + /// use rocket::http::uri::fmt::{Formatter, UriDisplay, Query, Path}; /// /// struct Foo; /// @@ -296,18 +290,9 @@ impl<'i, P: UriPart> Formatter<'i, P> { /// } /// } /// - /// let uri_string = format!("{}", &Foo as &UriDisplay); + /// let uri_string = format!("{}", &Foo as &dyn UriDisplay); /// assert_eq!(uri_string, "araw&format"); /// - ///// #[derive(UriDisplayQuery)] - ///// struct Message { - ///// inner: Foo, - ///// } - ///// - ///// let msg = Message { inner: Foo }; - ///// let uri_string = format!("{}", &msg as &UriDisplay); - ///// assert_eq!(uri_string, "inner=araw&inner=format"); - /// /// impl UriDisplay for Foo { /// fn fmt(&self, f: &mut Formatter) -> fmt::Result { /// f.write_raw("a")?; @@ -317,8 +302,17 @@ impl<'i, P: UriPart> Formatter<'i, P> { /// } /// } /// - /// let uri_string = format!("{}", &Foo as &UriDisplay); + /// let uri_string = format!("{}", &Foo as &dyn UriDisplay); /// assert_eq!(uri_string, "araw/format"); + /// + /// #[derive(UriDisplayQuery)] + /// struct Message { + /// inner: Foo, + /// } + /// + /// let msg = Message { inner: Foo }; + /// let uri_string = format!("{}", &msg as &dyn UriDisplay); + /// assert_eq!(uri_string, "inner=araw&inner=format"); /// ``` #[inline(always)] pub fn refresh(&mut self) { @@ -380,7 +374,7 @@ impl Formatter<'_, Query> { /// # extern crate rocket; /// use std::fmt; /// - /// use rocket::http::uri::{Formatter, UriDisplay, Query}; + /// use rocket::http::uri::fmt::{Formatter, UriDisplay, Query}; /// /// struct Foo { /// name: usize @@ -395,7 +389,7 @@ impl Formatter<'_, Query> { /// } /// /// let foo = Foo { name: 123 }; - /// let uri_string = format!("{}", &foo as &UriDisplay); + /// let uri_string = format!("{}", &foo as &dyn UriDisplay); /// assert_eq!(uri_string, "name=123"); /// ``` #[inline] @@ -404,7 +398,7 @@ impl Formatter<'_, Query> { } } -impl fmt::Write for Formatter<'_, P> { +impl fmt::Write for Formatter<'_, P> { fn write_str(&mut self, s: &str) -> fmt::Result { self.write_raw(s) } @@ -425,66 +419,221 @@ pub enum UriQueryArgument<'a> { Value(&'a dyn UriDisplay) } +/// No prefix at all. +#[doc(hidden)] +pub struct Void; + // Used by code generation. #[doc(hidden)] -pub struct UriArguments<'a> { - pub path: UriArgumentsKind<&'a [&'a dyn UriDisplay]>, - pub query: Option]>>, +pub trait ValidRoutePrefix { + type Output; + + fn append(self, path: Cow<'static, str>, query: Option>) -> Self::Output; +} + +impl<'a> ValidRoutePrefix for Origin<'a> { + type Output = Self; + + fn append(self, path: Cow<'static, str>, query: Option>) -> Self::Output { + // No-op if `self` is already normalzied. + let mut prefix = self.into_normalized(); + prefix.clear_query(); + + if prefix.path() == "/" { + // Avoid a double `//` to start. + return Origin::new(path, query); + } else if path == "/" { + // Appending path to `/` is a no-op, but append any query. + prefix.set_query(query); + return prefix; + } + + Origin::new(format!("{}{}", prefix.path(), path), query) + } +} + +impl<'a> ValidRoutePrefix for Absolute<'a> { + type Output = Self; + + fn append(self, path: Cow<'static, str>, query: Option>) -> Self::Output { + // No-op if `self` is already normalzied. + let mut prefix = self.into_normalized(); + prefix.clear_query(); + + if prefix.authority().is_some() { + // The prefix is normalized. Appending a `/` is a no-op. + if path == "/" { + prefix.set_query(query); + return prefix; + } + } + + // In these cases, appending `path` would be a no-op or worse. + if prefix.path().is_empty() || prefix.path() == "/" { + prefix.set_path(path); + prefix.set_query(query); + return prefix; + } + + if path == "/" { + prefix.set_query(query); + return prefix; + } + + prefix.set_path(format!("{}{}", prefix.path(), path)); + prefix.set_query(query); + prefix + } +} + +// `Self` is a valid suffix for `T`. +#[doc(hidden)] +pub trait ValidRouteSuffix { + type Output; + + fn prepend(self, prefix: T) -> Self::Output; +} + +impl<'a> ValidRouteSuffix> for Reference<'a> { + type Output = Self; + + fn prepend(self, prefix: Origin<'a>) -> Self::Output { + Reference::from(prefix).with_query_fragment_of(self) + } +} + +impl<'a> ValidRouteSuffix> for Reference<'a> { + type Output = Self; + + fn prepend(self, prefix: Absolute<'a>) -> Self::Output { + Reference::from(prefix).with_query_fragment_of(self) + } +} + +impl<'a> ValidRouteSuffix> for Absolute<'a> { + type Output = Origin<'a>; + + fn prepend(self, mut prefix: Origin<'a>) -> Self::Output { + if let Some(query) = self.query { + if prefix.query().is_none() { + prefix.set_query(query.value.into_concrete(&self.source)); + } + } + + prefix + } +} + +impl<'a> ValidRouteSuffix> for Absolute<'a> { + type Output = Self; + + fn prepend(self, mut prefix: Absolute<'a>) -> Self::Output { + if let Some(query) = self.query { + if prefix.query().is_none() { + prefix.set_query(query.value.into_concrete(&self.source)); + } + } + + prefix + } } // Used by code generation. -impl UriArguments<'_> { - #[doc(hidden)] - pub fn into_origin(self) -> Origin<'static> { - use std::borrow::Cow; +#[doc(hidden)] +pub struct RouteUriBuilder { + pub path: Cow<'static, str>, + pub query: Option>, +} + +// Used by code generation. +#[doc(hidden)] +pub struct PrefixedRouteUri(T); + +// Used by code generation. +#[doc(hidden)] +pub struct SuffixedRouteUri(T); + +// Used by code generation. +#[doc(hidden)] +impl RouteUriBuilder { + /// Create a new `RouteUriBuilder` with the given path/query args. + pub fn new( + path_args: UriArgumentsKind<&[&dyn UriDisplay]>, + query_args: Option]>>, + ) -> Self { use self::{UriArgumentsKind::*, UriQueryArgument::*}; - let path: Cow<'static, str> = match self.path { + let path: Cow<'static, str> = match path_args { Static(path) => path.into(), Dynamic(args) => { let mut string = String::from("/"); - { - let mut formatter = Formatter::::new(&mut string); - for value in args { - let _ = formatter.write_value(value); - } + let mut formatter = Formatter::::new(&mut string); + for value in args { + let _ = formatter.write_value(value); } string.into() } }; - let query: Option> = self.query.and_then(|q| match q { - Static(query) => Some(query.into()), - Dynamic(args) if args.is_empty() => None, - Dynamic(args) => { + let query: Option> = match query_args { + None => None, + Some(Static(query)) => Some(query.into()), + Some(Dynamic(args)) => { let mut string = String::new(); - { - let mut f = Formatter::::new(&mut string); - for arg in args { - let _ = match arg { - Raw(v) => f.write_raw(v), - NameValue(n, v) => f.write_named_value(n, v), - Value(v) => f.write_value(v), - }; - } + let mut f = Formatter::::new(&mut string); + for arg in args { + let _ = match arg { + Raw(v) => f.write_raw(v), + NameValue(n, v) => f.write_named_value(n, v), + Value(v) => f.write_value(v), + }; } - match string.is_empty() { - false => Some(string.into()), - true => None, - } + (!string.is_empty()).then(|| string.into()) } - }); + }; - Origin::new(path, query) + RouteUriBuilder { path, query } + } + + pub fn with_prefix(self, p: P) -> PrefixedRouteUri { + PrefixedRouteUri(p.append(self.path, self.query)) + } + + pub fn with_suffix(self, suffix: S) -> SuffixedRouteUri + where S: ValidRouteSuffix> + { + SuffixedRouteUri(suffix.prepend(self.render())) + } + + pub fn render(self) -> Origin<'static> { + Origin::new(self.path, self.query) + } +} + +#[doc(hidden)] +impl PrefixedRouteUri { + pub fn with_suffix>(self, suffix: S) -> SuffixedRouteUri { + SuffixedRouteUri(suffix.prepend(self.0)) + } + + pub fn render(self) -> T { + self.0 + } +} + +#[doc(hidden)] +impl SuffixedRouteUri { + pub fn render(self) -> T { + self.0 } } // See https://github.com/SergioBenitez/Rocket/issues/1534. #[cfg(test)] mod prefix_soundness_test { - use crate::uri::{Formatter, Query, UriDisplay}; + use crate::uri::fmt::{Formatter, UriDisplay, Query}; struct MyValue; diff --git a/core/http/src/uri/from_uri_param.rs b/core/http/src/uri/fmt/from_uri_param.rs similarity index 84% rename from core/http/src/uri/from_uri_param.rs rename to core/http/src/uri/fmt/from_uri_param.rs index 65a62e19..51cd53da 100644 --- a/core/http/src/uri/from_uri_param.rs +++ b/core/http/src/uri/fmt/from_uri_param.rs @@ -1,6 +1,7 @@ use std::path::{Path, PathBuf}; -use crate::uri::{self, UriPart, UriDisplay}; +use crate::uri::fmt::UriDisplay; +use crate::uri::fmt::{self, Part}; /// Conversion trait for parameters used in [`uri!`] invocations. /// @@ -17,9 +18,9 @@ use crate::uri::{self, UriPart, UriDisplay}; /// be automated. Rocket provides [`impl_from_uri_param_identity`] to generate /// the _identity_ implementations automatically. For a type `T`, these are: /// -/// * `impl FromUriParam for T` -/// * `impl<'x, P: UriPart> FromUriParam for T` -/// * `impl<'x, P: UriPart> FromUriParam for T` +/// * `impl FromUriParam for T` +/// * `impl<'x, P: Part> FromUriParam for T` +/// * `impl<'x, P: Part> FromUriParam for T` /// /// See [`impl_from_uri_param_identity`] for usage details. /// @@ -40,10 +41,10 @@ use crate::uri::{self, UriPart, UriDisplay}; /// /// ```rust /// # extern crate rocket; -/// # use rocket::http::uri::{FromUriParam, UriPart}; +/// # use rocket::http::uri::fmt::{FromUriParam, Part}; /// # struct S; /// # type String = S; -/// impl<'a, P: UriPart> FromUriParam for String { +/// impl<'a, P: Part> FromUriParam for String { /// type Target = &'a str; /// # fn from_uri_param(s: &'a str) -> Self::Target { "hi" } /// } @@ -99,7 +100,7 @@ use crate::uri::{self, UriPart, UriDisplay}; /// invocation. For instance, if the route has a type of `T` and you'd like to /// use a type of `S` in a `uri!` invocation, you'd implement `FromUriParam for S` where `P` is `Path` for conversions valid in the path part of a -/// URI, `Uri` for conversions valid in the query part of a URI, or `P: UriPart` +/// URI, `Uri` for conversions valid in the query part of a URI, or `P: Part` /// when a conversion is valid in either case. /// /// This is typically only warranted for owned-value types with corresponding @@ -121,7 +122,7 @@ use crate::uri::{self, UriPart, UriDisplay}; /// # #[macro_use] extern crate rocket; /// use std::fmt; /// -/// use rocket::http::uri::{Formatter, UriDisplay, FromUriParam, Query}; +/// use rocket::http::uri::fmt::{Formatter, UriDisplay, FromUriParam, Query}; /// /// #[derive(FromForm)] /// struct User<'a> { @@ -150,7 +151,7 @@ use crate::uri::{self, UriPart, UriDisplay}; /// ```rust /// # #[macro_use] extern crate rocket; /// # use std::fmt; -/// # use rocket::http::uri::{Formatter, UriDisplay, FromUriParam, Query}; +/// # use rocket::http::uri::fmt::{Formatter, UriDisplay, FromUriParam, Query}; /// # /// # #[derive(FromForm)] /// # struct User<'a> { name: &'a str, nickname: String, } @@ -172,22 +173,22 @@ use crate::uri::{self, UriPart, UriDisplay}; /// #[post("/?")] /// fn some_route(name: &str, user: User<'_>) { /* .. */ } /// -/// let uri = uri!(some_route: name = "hey", user = ("Robert Mike", "Bob")); +/// let uri = uri!(some_route(name = "hey", user = ("Robert Mike", "Bob"))); /// assert_eq!(uri.path(), "/hey"); /// assert_eq!(uri.query().unwrap(), "name=Robert%20Mike&nickname=Bob"); /// ``` /// -/// [`uri!`]: crate::uri -/// [`UriDisplay`]: crate::uri::UriDisplay -/// [`FromUriParam::Target`]: crate::uri::FromUriParam::Target -/// [`Path`]: crate::uri::Path -pub trait FromUriParam { +/// [`uri!`]: rocket::uri +/// [`FromUriParam::Target`]: crate::uri::fmt::FromUriParam::Target +/// [`Path`]: crate::uri::fmt::Path +/// [`Query`]: crate::uri::fmt::Query +pub trait FromUriParam { /// The resulting type of this conversion. type Target: UriDisplay

; /// Converts a value of type `T` into a value of type `Self::Target`. The /// resulting value of type `Self::Target` will be rendered into a URI using - /// its [`UriDisplay`](crate::uri::UriDisplay) implementation. + /// its [`UriDisplay`] implementation. fn from_uri_param(param: T) -> Self::Target; } @@ -200,7 +201,7 @@ macro_rules! impl_conversion_ref { ($($A:ty => $B:ty),*) => ( impl_conversion_ref!(@_ $(() $A => $B),*); ); (@_ $(($($l:tt)*) $A:ty => $B:ty),*) => ($( - impl_conversion_ref!([P] ($($l)* P: $crate::uri::UriPart) $A => $B); + impl_conversion_ref!([P] ($($l)* P: $crate::uri::fmt::Part) $A => $B); )*); ($([$P:ty] ($($l:tt)*) $A:ty => $B:ty),*) => ($( @@ -212,7 +213,7 @@ macro_rules! impl_conversion_ref { ($([$P:ty] $A:ty => $B:ty),*) => ( impl_conversion_ref!($([$P] () $A => $B),*);); (@_ [$P:ty] ($($l:tt)*) $A:ty => $B:ty) => ( - impl<$($l)*> $crate::uri::FromUriParam<$P, $A> for $B { + impl<$($l)*> $crate::uri::fmt::FromUriParam<$P, $A> for $B { type Target = $A; #[inline(always)] fn from_uri_param(param: $A) -> $A { param } } @@ -224,13 +225,13 @@ macro_rules! impl_conversion_ref { /// /// For a type `T`, the _identity_ implementations of `FromUriParam` are: /// -/// * `impl FromUriParam for T` +/// * `impl FromUriParam for T` /// * `impl<'x> FromUriParam for T` /// * `impl<'x> FromUriParam for T` /// /// where `P` is one of: /// -/// * `P: UriPart` (the generic `P`) +/// * `P: Part` (the generic `P`) /// * [`Path`] /// * [`Query`] /// @@ -241,7 +242,7 @@ macro_rules! impl_conversion_ref { /// Generates the three _identity_ implementations for the generic `P`. /// /// * Example: `impl_from_uri_param_identity!(MyType);` -/// * Generates: `impl FromUriParam for MyType { ... }` +/// * Generates: `impl FromUriParam for MyType { ... }` /// /// 2. `impl_from_uri_param_identity!((generics*) Type);` /// @@ -250,11 +251,11 @@ macro_rules! impl_conversion_ref { /// implementation. /// /// * Example: `impl_from_uri_param_identity!(('a) MyType<'a>);` -/// * Generates: `impl<'a, P: UriPart> FromUriParam for MyType<'a> { ... }` +/// * Generates: `impl<'a, P: Part> FromUriParam for MyType<'a> { ... }` /// /// 3. `impl_from_uri_param_identity!([Part] Type);` /// -/// Generates the three _identity_ implementations for the `UriPart` +/// Generates the three _identity_ implementations for the `Part` /// `Part`, where `Part` is a path to [`Path`] or [`Query`]. /// /// * Example: `impl_from_uri_param_identity!([Path] MyType);` @@ -267,9 +268,9 @@ macro_rules! impl_conversion_ref { /// * Example: `impl_from_uri_param_identity!([Path] ('a) MyType<'a>);` /// * Generates: `impl<'a> FromUriParam for MyType<'a> { ... }` /// -/// [`FromUriParam`]: crate::uri::FromUriParam -/// [`Path`]: crate::uri::Path -/// [`Query`]: crate::uri::Query +/// [`FromUriParam`]: crate::uri::fmt::FromUriParam +/// [`Path`]: crate::uri::fmt::Path +/// [`Query`]: crate::uri::fmt::Query #[macro_export(local_inner_macros)] macro_rules! impl_from_uri_param_identity { ($(($($l:tt)*) $T:ty),*) => ($( impl_conversion_ref!(($($l)*) $T => $T); )*); @@ -297,16 +298,16 @@ impl_conversion_ref! { ('a) String => &'a str } -impl_from_uri_param_identity!([uri::Path] ('a) &'a Path); -impl_from_uri_param_identity!([uri::Path] PathBuf); +impl_from_uri_param_identity!([fmt::Path] ('a) &'a Path); +impl_from_uri_param_identity!([fmt::Path] PathBuf); impl_conversion_ref! { - [uri::Path] ('a) &'a Path => PathBuf, - [uri::Path] ('a) PathBuf => &'a Path + [fmt::Path] ('a) &'a Path => PathBuf, + [fmt::Path] ('a) PathBuf => &'a Path } /// A no cost conversion allowing an `&str` to be used in place of a `PathBuf`. -impl<'a> FromUriParam for PathBuf { +impl<'a> FromUriParam for PathBuf { type Target = &'a Path; #[inline(always)] @@ -316,7 +317,7 @@ impl<'a> FromUriParam for PathBuf { } /// A no cost conversion allowing an `&&str` to be used in place of a `PathBuf`. -impl<'a, 'b> FromUriParam for PathBuf { +impl<'a, 'b> FromUriParam for PathBuf { type Target = &'b Path; #[inline(always)] @@ -326,7 +327,7 @@ impl<'a, 'b> FromUriParam for PathBuf { } /// A no cost conversion allowing any `T` to be used in place of an `Option`. -impl> FromUriParam for Option { +impl> FromUriParam for Option { type Target = T::Target; #[inline(always)] @@ -336,7 +337,7 @@ impl> FromUriParam for Option } /// A no cost conversion allowing `T` to be used in place of an `Result`. -impl> FromUriParam for Result { +impl> FromUriParam for Result { type Target = T::Target; #[inline(always)] @@ -345,7 +346,7 @@ impl> FromUriParam for Result< } } -impl> FromUriParam> for Option { +impl> FromUriParam> for Option { type Target = Option; #[inline(always)] @@ -354,7 +355,7 @@ impl> FromUriParam> for } } -impl> FromUriParam> for Result { +impl> FromUriParam> for Result { type Target = Option; #[inline(always)] @@ -363,7 +364,7 @@ impl> FromUriParam> f } } -impl> FromUriParam> for Result { +impl> FromUriParam> for Result { type Target = Result; #[inline(always)] @@ -372,7 +373,7 @@ impl> FromUriParam } } -impl> FromUriParam> for Option { +impl> FromUriParam> for Option { type Target = Result; #[inline(always)] diff --git a/core/http/src/uri/fmt/mod.rs b/core/http/src/uri/fmt/mod.rs new file mode 100644 index 00000000..64feabb7 --- /dev/null +++ b/core/http/src/uri/fmt/mod.rs @@ -0,0 +1,14 @@ +//! Type safe and URI safe formatting types and traits. + +mod uri_display; +mod formatter; +mod from_uri_param; +mod encoding; +mod part; + +pub use self::formatter::*; +pub use self::uri_display::*; +pub use self::from_uri_param::*; +pub use self::part::*; + +pub(crate) use self::encoding::*; diff --git a/core/http/src/uri/fmt/part.rs b/core/http/src/uri/fmt/part.rs new file mode 100644 index 00000000..a952407d --- /dev/null +++ b/core/http/src/uri/fmt/part.rs @@ -0,0 +1,84 @@ +use crate::parse::IndexedStr; + +/// Marker trait for types that mark a part of a URI. +/// +/// This trait exists solely to categorize types that mark a part of the URI, +/// currently [`Path`] and [`Query`]. Said another way, types that implement +/// this trait are marker types that represent a part of a URI at the +/// type-level. +/// +/// This trait is _sealed_: it cannot be implemented outside of Rocket. +/// +/// # Usage +/// +/// You will find this trait in traits like [`UriDisplay`] or structs like +/// [`Formatter`] as the bound on a generic parameter: `P: Part`. Because the +/// trait is sealed, the generic type is guaranteed to be instantiated as one of +/// [`Query`] or [`Path`], effectively creating two instances of the generic +/// items: `UriDisplay` and `UriDisplay`, and `Formatter` +/// and `Formatter`. Unlike having two distinct, non-generic traits, this +/// approach enables succinct, type-checked generic implementations of these +/// items. +/// +/// [`UriDisplay`]: crate::uri::fmt::UriDisplay +/// [`Formatter`]: crate::uri::fmt::Formatter +pub trait Part: private::Sealed { + /// The dynamic version of `Self`. + #[doc(hidden)] + const KIND: Kind; + + /// The delimiter used to separate components of this URI part. + /// Specifically, `/` for `Path` and `&` for `Query`. + #[doc(hidden)] + const DELIMITER: char; + + /// The raw form of a segment in this part. + #[doc(hidden)] + type Raw: Send + Sync + 'static; +} + +mod private { + pub trait Sealed {} + impl Sealed for super::Path {} + impl Sealed for super::Query {} +} + +/// Dynamic version of the `Path` and `Query` parts. +#[doc(hidden)] +#[derive(Debug, PartialEq, Eq, Copy, Clone)] +pub enum Kind { Path, Query } + +/// Marker type indicating use of a type for the path [`Part`] of a URI. +/// +/// In route URIs, this corresponds to all of the text before a `?`, if any, or +/// all of the text in the URI otherwise: +/// +/// ```text +/// #[get("/home//?")] +/// ^------------------ Path +/// ``` +#[derive(Debug, Clone, Copy)] +pub enum Path { } + +/// Marker type indicating use of a type for the query [`Part`] of a URI. +/// +/// In route URIs, this corresponds to all of the text after a `?`, if any. +/// +/// ```text +/// #[get("/home//?&")] +/// ^-------------- Query +/// ``` +#[derive(Debug, Clone, Copy)] +pub enum Query { } + +impl Part for Path { + const KIND: Kind = Kind::Path; + const DELIMITER: char = '/'; + type Raw = IndexedStr<'static>; +} + +impl Part for Query { + const KIND: Kind = Kind::Query; + const DELIMITER: char = '&'; + type Raw = (IndexedStr<'static>, IndexedStr<'static>); +} diff --git a/core/http/src/uri/uri_display.rs b/core/http/src/uri/fmt/uri_display.rs similarity index 87% rename from core/http/src/uri/uri_display.rs rename to core/http/src/uri/fmt/uri_display.rs index b2028149..469915bc 100644 --- a/core/http/src/uri/uri_display.rs +++ b/core/http/src/uri/fmt/uri_display.rs @@ -1,7 +1,8 @@ use std::{fmt, path}; use std::borrow::Cow; -use crate::uri::{Uri, UriPart, Path, Query, Formatter}; +use crate::RawStr; +use crate::uri::fmt::{Part, Path, Query, Formatter}; /// Trait implemented by types that can be displayed as part of a URI in /// [`uri!`]. @@ -14,8 +15,8 @@ use crate::uri::{Uri, UriPart, Path, Query, Formatter}; /// /// # Marker Generic: `Path`, `Query` /// -/// The [`UriPart`] parameter `P` in `UriDisplay

` must be either [`Path`] or -/// [`Query`] (see the [`UriPart`] documentation for how this is enforced), +/// The [`Part`] parameter `P` in `UriDisplay

` must be either [`Path`] or +/// [`Query`] (see the [`Part`] documentation for how this is enforced), /// resulting in either `UriDisplay` or `UriDisplay`. /// /// As the names might imply, the `Path` version of the trait is used when @@ -37,16 +38,12 @@ use crate::uri::{Uri, UriPart, Path, Query, Formatter}; /// ```rust /// # extern crate rocket; /// # use std::fmt; -/// # use rocket::http::uri::{UriPart, UriDisplay, Formatter}; +/// # use rocket::http::uri::fmt::{Part, UriDisplay, Formatter}; /// # struct SomeType; -/// impl UriDisplay

for SomeType +/// impl UriDisplay

for SomeType /// # { fn fmt(&self, f: &mut Formatter

) -> fmt::Result { Ok(()) } } /// ``` /// -/// [`UriPart`]: crate::uri::UriPart -/// [`Path`]: crate::uri::Path -/// [`Query`]: crate::uri::Query -/// /// # Code Generation /// /// When the [`uri!`] macro is used to generate a URI for a route, the types for @@ -74,18 +71,18 @@ use crate::uri::{Uri, UriPart, Path, Query, Formatter}; /// # fn get_item(id: i32, track: Option) { /* .. */ } /// # /// // With unnamed parameters. -/// uri!(get_item: 100, Some("inbound")); +/// uri!(get_item(100, Some("inbound"))); /// /// // With named parameters. -/// uri!(get_item: id = 100, track = Some("inbound")); -/// uri!(get_item: track = Some("inbound"), id = 100); +/// uri!(get_item(id = 100, track = Some("inbound"))); +/// uri!(get_item(track = Some("inbound"), id = 100)); /// /// // Ignoring `track`. -/// uri!(get_item: 100, _); -/// uri!(get_item: 100, None as Option); -/// uri!(get_item: id = 100, track = _); -/// uri!(get_item: track = _, id = 100); -/// uri!(get_item: id = 100, track = None as Option<&str>); +/// uri!(get_item(100, _)); +/// uri!(get_item(100, None as Option)); +/// uri!(get_item(id = 100, track = _)); +/// uri!(get_item(track = _, id = 100)); +/// uri!(get_item(id = 100, track = None as Option<&str>)); /// ``` /// /// After verifying parameters and their types, Rocket will generate code @@ -93,10 +90,11 @@ use crate::uri::{Uri, UriPart, Path, Query, Formatter}; /// /// ```rust /// # extern crate rocket; -/// # use rocket::http::uri::{UriDisplay, Path, Query, Origin}; +/// # use rocket::http::uri::Origin; +/// # use rocket::http::uri::fmt::{UriDisplay, Path, Query}; /// # /// Origin::parse(&format!("/item/{}?track={}", -/// &100 as &UriDisplay, &"inbound" as &UriDisplay)); +/// &100 as &dyn UriDisplay, &"inbound" as &dyn UriDisplay)); /// ``` /// /// For this expression to typecheck, `i32` must implement `UriDisplay` @@ -105,11 +103,11 @@ use crate::uri::{Uri, UriPart, Path, Query, Formatter}; /// seen, the implementations will be used to display the value in a URI-safe /// manner. /// -/// [`uri!`]: ../../../rocket/macro.uri.html +/// [`uri!`]: rocket::uri /// /// # Provided Implementations /// -/// Rocket implements `UriDisplay

` for all `P: UriPart` for several built-in +/// Rocket implements `UriDisplay

` for all `P: Part` for several built-in /// types. /// /// * **i8, i16, i32, i64, i128, isize, u8, u16, u32, u64, u128, usize, f32, @@ -167,7 +165,7 @@ use crate::uri::{Uri, UriPart, Path, Query, Formatter}; /// If the `Result` is `Ok`, uses the implementation of `UriDisplay` for /// `T`. Otherwise, nothing is rendered. /// -/// [`FromUriParam`]: crate::uri::FromUriParam +/// [`FromUriParam`]: crate::uri::fmt::FromUriParam /// /// # Deriving /// @@ -176,7 +174,7 @@ use crate::uri::{Uri, UriPart, Path, Query, Formatter}; /// /// ```rust /// # #[macro_use] extern crate rocket; -/// # use rocket::http::uri::{UriDisplay, Query, Path}; +/// # use rocket::http::uri::fmt::{UriDisplay, Query, Path}; /// // Derives `UriDisplay` /// #[derive(UriDisplayQuery)] /// struct User { @@ -185,7 +183,7 @@ use crate::uri::{Uri, UriPart, Path, Query, Formatter}; /// } /// /// let user = User { name: "Michael Smith".into(), age: 31 }; -/// let uri_string = format!("{}", &user as &UriDisplay); +/// let uri_string = format!("{}", &user as &dyn UriDisplay); /// assert_eq!(uri_string, "name=Michael%20Smith&age=31"); /// /// // Derives `UriDisplay` @@ -193,7 +191,7 @@ use crate::uri::{Uri, UriPart, Path, Query, Formatter}; /// struct Name(String); /// /// let name = Name("Bob Smith".into()); -/// let uri_string = format!("{}", &name as &UriDisplay); +/// let uri_string = format!("{}", &name as &dyn UriDisplay); /// assert_eq!(uri_string, "Bob%20Smith"); /// ``` /// @@ -204,11 +202,9 @@ use crate::uri::{Uri, UriPart, Path, Query, Formatter}; /// [`UriDisplay`] and [`UriDisplay`] derive documentation for full /// details. /// -/// [`Ignorable`]: crate::uri::Ignorable +/// [`Ignorable`]: crate::uri::fmt::Ignorable /// [`UriDisplay`]: ../../derive.UriDisplayPath.html /// [`UriDisplay`]: ../../derive.UriDisplayQuery.html -/// [`Formatter::write_named_value()`]: crate::uri::Formatter::write_named_value() -/// [`Formatter::write_value()`]: crate::uri::Formatter::write_value() /// /// # Implementing /// @@ -259,7 +255,7 @@ use crate::uri::{Uri, UriPart, Path, Query, Formatter}; /// /// use std::fmt; /// use rocket::http::impl_from_uri_param_identity; -/// use rocket::http::uri::{Formatter, FromUriParam, UriDisplay, Path}; +/// use rocket::http::uri::fmt::{Formatter, FromUriParam, UriDisplay, Path}; /// use rocket::response::Redirect; /// /// impl UriDisplay for Name<'_> { @@ -276,7 +272,7 @@ use crate::uri::{Uri, UriPart, Path, Query, Formatter}; /// /// #[get("/name/")] /// fn redirector(name: Name<'_>) -> Redirect { -/// Redirect::to(uri!(real: name)) +/// Redirect::to(uri!(real(name))) /// } /// /// #[get("/")] @@ -284,15 +280,15 @@ use crate::uri::{Uri, UriPart, Path, Query, Formatter}; /// format!("Hello, {}!", name.0) /// } /// -/// let uri = uri!(real: Name("Mike Smith".into())); +/// let uri = uri!(real(Name("Mike Smith".into()))); /// assert_eq!(uri.path(), "/name:Mike%20Smith"); /// ``` -pub trait UriDisplay { +pub trait UriDisplay { /// Formats `self` in a URI-safe manner using the given formatter. fn fmt(&self, f: &mut Formatter<'_, P>) -> fmt::Result; } -impl fmt::Display for &dyn UriDisplay

{ +impl fmt::Display for &dyn UriDisplay

{ #[inline(always)] fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { UriDisplay::fmt(*self, &mut >::new(f)) @@ -302,10 +298,10 @@ impl fmt::Display for &dyn UriDisplay

{ // Direct implementations: these are the leaves of a call to `UriDisplay::fmt`. /// Percent-encodes the raw string. -impl UriDisplay

for str { +impl UriDisplay

for str { #[inline(always)] fn fmt(&self, f: &mut Formatter<'_, P>) -> fmt::Result { - f.write_raw(&Uri::percent_encode(self)) + f.write_raw(RawStr::new(self).percent_encode().as_str()) } } @@ -328,7 +324,7 @@ impl UriDisplay for path::Path { macro_rules! impl_with_display { ($($T:ty),+) => {$( /// This implementation is identical to the `Display` implementation. - impl UriDisplay

for $T { + impl UriDisplay

for $T { #[inline(always)] fn fmt(&self, f: &mut Formatter<'_, P>) -> fmt::Result { use std::fmt::Write; @@ -351,7 +347,7 @@ impl_with_display! { // implementation. /// Percent-encodes the raw string. Defers to `str`. -impl UriDisplay

for String { +impl UriDisplay

for String { #[inline(always)] fn fmt(&self, f: &mut Formatter<'_, P>) -> fmt::Result { self.as_str().fmt(f) @@ -359,7 +355,7 @@ impl UriDisplay

for String { } /// Percent-encodes the raw string. Defers to `str`. -impl UriDisplay

for Cow<'_, str> { +impl UriDisplay

for Cow<'_, str> { #[inline(always)] fn fmt(&self, f: &mut Formatter<'_, P>) -> fmt::Result { self.as_ref().fmt(f) @@ -375,7 +371,7 @@ impl UriDisplay for path::PathBuf { } /// Defers to the `UriDisplay

` implementation for `T`. -impl + ?Sized> UriDisplay

for &T { +impl + ?Sized> UriDisplay

for &T { #[inline(always)] fn fmt(&self, f: &mut Formatter<'_, P>) -> fmt::Result { UriDisplay::fmt(*self, f) @@ -383,7 +379,7 @@ impl + ?Sized> UriDisplay

for &T { } /// Defers to the `UriDisplay

` implementation for `T`. -impl + ?Sized> UriDisplay

for &mut T { +impl + ?Sized> UriDisplay

for &mut T { #[inline(always)] fn fmt(&self, f: &mut Formatter<'_, P>) -> fmt::Result { UriDisplay::fmt(*self, f) @@ -419,7 +415,7 @@ impl, E> UriDisplay for Result { /// /// When a parameter is explicitly ignored in `uri!` by supplying `_` as the /// parameter's value, that parameter's type is required to implement this -/// trait for the corresponding `UriPart`. +/// trait for the corresponding `Part`. /// /// ```rust /// # #[macro_use] extern crate rocket; @@ -427,12 +423,12 @@ impl, E> UriDisplay for Result { /// fn get_item(id: i32, track: Option) { /* .. */ } /// /// // Ignore the `track` parameter: `Option` must be `Ignorable`. -/// uri!(get_item: 100, _); -/// uri!(get_item: id = 100, track = _); +/// uri!(get_item(100, _)); +/// uri!(get_item(id = 100, track = _)); /// /// // Provide a value for `track`. -/// uri!(get_item: 100, Some(4)); -/// uri!(get_item: id = 100, track = Some(4)); +/// uri!(get_item(100, Some(4))); +/// uri!(get_item(id = 100, track = Some(4))); /// ``` /// /// # Implementations @@ -442,23 +438,24 @@ impl, E> UriDisplay for Result { /// /// ```rust /// # #[macro_use] extern crate rocket; -/// use rocket::http::uri::{Ignorable, Query}; +/// use rocket::http::uri::fmt::{Ignorable, Query}; /// /// # struct MyType; /// impl Ignorable for MyType { } /// ``` -pub trait Ignorable { } +pub trait Ignorable { } impl Ignorable for Option { } impl Ignorable for Result { } #[doc(hidden)] -pub fn assert_ignorable>() { } +pub fn assert_ignorable>() { } #[cfg(test)] mod uri_display_tests { use std::path; - use crate::uri::{FromUriParam, UriDisplay, Query, Path}; + use crate::uri::fmt::{FromUriParam, UriDisplay}; + use crate::uri::fmt::{Query, Path}; macro_rules! uri_display { (<$P:ident, $Target:ty> $source:expr) => ({ @@ -566,7 +563,7 @@ mod uri_display_tests { #[test] fn check_ignorables() { - use crate::uri::assert_ignorable; + use crate::uri::fmt::assert_ignorable; assert_ignorable::>(); assert_ignorable::>>(); diff --git a/core/http/src/uri/mod.rs b/core/http/src/uri/mod.rs index f7e786a0..9211412f 100644 --- a/core/http/src/uri/mod.rs +++ b/core/http/src/uri/mod.rs @@ -1,106 +1,25 @@ //! Types for URIs and traits for rendering URI components. mod uri; -mod uri_display; -mod formatter; -mod from_uri_param; mod origin; +mod reference; mod authority; mod absolute; mod segments; +mod path_query; +mod asterisk; -pub(crate) mod encoding; +pub mod error; +pub mod fmt; -pub use crate::parse::uri::Error; +#[doc(inline)] +pub use self::error::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::formatter::*; -pub use self::from_uri_param::*; pub use self::segments::*; - -mod private { - pub trait Sealed {} - impl Sealed for super::Path {} - impl Sealed for super::Query {} -} - -/// Marker trait for types that mark a part of a URI. -/// -/// This trait exists solely to categorize types that mark a part of the URI, -/// currently [`Path`] and [`Query`]. Said another way, types that implement -/// this trait are marker types that represent a part of a URI at the -/// type-level. -/// -/// This trait is _sealed_: it cannot be implemented outside of Rocket. -/// -/// # Usage -/// -/// You will find this trait in traits like [`UriDisplay`] or structs like -/// [`Formatter`] as the bound on a generic parameter: `P: UriPart`. Because the -/// trait is sealed, the generic type is guaranteed to be instantiated as one of -/// [`Query`] or [`Path`], effectively creating two instances of the generic -/// items: `UriDisplay` and `UriDisplay`, and `Formatter` -/// and `Formatter`. Unlike having two distinct, non-generic traits, this -/// approach enables succinct, type-checked generic implementations of these -/// items. -/// -/// [`Query`]: crate::uri::Query -/// [`Path`]: crate::uri::Path -/// [`UriDisplay`]: crate::uri::UriDisplay -/// [`Formatter`]: crate::uri::Formatter -pub trait UriPart: private::Sealed { - /// The dynamic version of `Self`. - #[doc(hidden)] - const KIND: Kind; - - /// The delimiter used to separate components of this URI part. - /// Specifically, `/` for `Path` and `&` for `Query`. - #[doc(hidden)] - const DELIMITER: char; -} - -/// Dynamic version of the `Path` and `Query` parts. -#[doc(hidden)] -#[derive(Debug, PartialEq, Eq, Copy, Clone)] -pub enum Kind { Path, Query } - -/// Marker type indicating use of a type for the path [`UriPart`] of a URI. -/// -/// In route URIs, this corresponds to all of the text before a `?`, if any, or -/// all of the text in the URI otherwise: -/// -/// ```text -/// #[get("/home//?")] -/// ^------------------ Path -/// ``` -/// -/// [`UriPart`]: crate::uri::UriPart -#[derive(Debug, Clone, Copy)] -pub enum Path { } - -/// Marker type indicating use of a type for the query [`UriPart`] of a URI. -/// -/// In route URIs, this corresponds to all of the text after a `?`, if any. -/// -/// ```text -/// #[get("/home//?&")] -/// ^-------------- Query -/// ``` -/// -/// [`UriPart`]: crate::uri::UriPart -#[derive(Debug, Clone, Copy)] -pub enum Query { } - -impl UriPart for Path { - const KIND: Kind = Kind::Path; - const DELIMITER: char = '/'; -} - -impl UriPart for Query { - const KIND: Kind = Kind::Query; - const DELIMITER: char = '&'; -} +pub use self::reference::*; +pub use self::path_query::*; +pub use self::asterisk::*; diff --git a/core/http/src/uri/origin.rs b/core/http/src/uri/origin.rs index 8a8a9453..3c80c98d 100644 --- a/core/http/src/uri/origin.rs +++ b/core/http/src/uri/origin.rs @@ -1,16 +1,12 @@ use std::borrow::Cow; use std::convert::TryFrom; -use std::fmt::{self, Display}; use std::hash::Hash; use crate::ext::IntoOwned; -use crate::parse::{Indexed, Extent, IndexedStr, uri::tables::is_pchar}; -use crate::uri::{self, UriPart, Query, Path}; -use crate::uri::{Error, Segments, QuerySegments, as_utf8_unchecked}; +use crate::parse::{Extent, IndexedStr, uri::tables::is_pchar}; +use crate::uri::{Error, Path, Query, Data, as_utf8_unchecked, fmt}; use crate::{RawStr, RawStrBuf}; -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. @@ -53,7 +49,7 @@ use state::Storage; /// # } /// ``` /// -/// By contrast, the following are valid but _abnormal_ URIs: +/// By contrast, the following are valid but _non-normal_ URIs: /// /// ```rust /// # extern crate rocket; @@ -77,7 +73,7 @@ use state::Storage; /// # extern crate rocket; /// # use rocket::http::uri::Origin; /// # let invalid = [ -/// // abnormal versions +/// // non-normal versions /// "//", "/a/b/", "/a/ab//c//d", "/a?a&&b&", /// /// // normalized versions @@ -89,14 +85,11 @@ use state::Storage; /// # assert_eq!(abnormal.into_normalized(), expected); /// # } /// ``` -#[derive(Clone, Debug)] +#[derive(Debug, Clone)] pub struct Origin<'a> { pub(crate) source: Option>, - pub(crate) path: IndexedStr<'a>, - pub(crate) query: Option>, - - pub(crate) decoded_path_segs: Storage>>, - pub(crate) decoded_query_segs: Storage, IndexedStr<'static>)>>, + pub(crate) path: Data<'a, fmt::Path>, + pub(crate) query: Option>, } impl Hash for Origin<'_> { @@ -117,7 +110,7 @@ impl Eq for Origin<'_> { } impl PartialEq for Origin<'_> { fn eq(&self, other: &str) -> bool { let (path, query) = RawStr::new(other).split_at_byte(b'?'); - self.path() == path && self.query().unwrap_or("".into()) == query + self.path() == path && self.query().map_or("", |q| q.as_str()) == query } } @@ -141,32 +134,15 @@ impl IntoOwned for Origin<'_> { source: self.source.into_owned(), path: self.path.into_owned(), query: self.query.into_owned(), - decoded_path_segs: self.decoded_path_segs.map(|v| v.into_owned()), - decoded_query_segs: self.decoded_query_segs.map(|v| v.into_owned()), } } } -fn decode_to_indexed_str( - value: &RawStr, - (indexed, source): (&IndexedStr<'_>, &RawStr) -) -> IndexedStr<'static> { - let decoded = match P::KIND { - uri::Kind::Path => value.percent_decode_lossy(), - uri::Kind::Query => value.url_decode_lossy(), - }; - - match decoded { - Cow::Borrowed(b) if indexed.is_indexed() => { - let indexed = IndexedStr::checked_from(b, source.as_str()); - debug_assert!(indexed.is_some()); - indexed.unwrap_or(IndexedStr::from(Cow::Borrowed(""))) - } - cow => IndexedStr::from(Cow::Owned(cow.into_owned())), - } -} - impl<'a> Origin<'a> { + /// The root: `'/'`. + #[doc(hidden)] + pub const ROOT: Origin<'static> = Origin::const_new("/", None); + /// SAFETY: `source` must be UTF-8. #[inline] pub(crate) unsafe fn raw( @@ -176,11 +152,8 @@ impl<'a> Origin<'a> { ) -> Origin<'a> { Origin { source: Some(as_utf8_unchecked(source)), - path: path.into(), - query: query.map(|q| q.into()), - - decoded_path_segs: Storage::new(), - decoded_query_segs: Storage::new(), + path: Data::raw(path), + query: query.map(Data::raw) } } @@ -193,19 +166,42 @@ impl<'a> Origin<'a> { { Origin { source: None, - path: Indexed::from(path.into()), - query: query.map(|q| Indexed::from(q.into())), - decoded_path_segs: Storage::new(), - decoded_query_segs: Storage::new(), + path: Data::new(path.into()), + query: query.map(Data::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 harmless. + // 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` are not guaranteed to be valid origin URIs! #[doc(hidden)] - pub fn dummy() -> Origin<'static> { - Origin::new::<&'static str, &'static str>("/", None) + pub fn path_only>>(path: P) -> Origin<'a> { + Origin::new(path, None::<&'a str>) + } + + // 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` are not guaranteed to be valid origin URIs! + #[doc(hidden)] + pub const fn const_new(path: &'a str, query: Option<&'a str>) -> Origin<'a> { + Origin { + source: None, + path: Data { + value: IndexedStr::Concrete(Cow::Borrowed(path)), + decoded_segments: state::Storage::new(), + }, + query: match query { + Some(query) => Some(Data { + value: IndexedStr::Concrete(Cow::Borrowed(query)), + decoded_segments: state::Storage::new(), + }), + None => None, + }, + } + } + + pub(crate) fn set_query>>>(&mut self, query: Q) { + self.query = query.into().map(Data::new); } /// Parses the string `string` into an `Origin`. Parsing will never @@ -214,7 +210,7 @@ impl<'a> Origin<'a> { /// # Example /// /// ```rust - /// # extern crate rocket; + /// # #[macro_use] extern crate rocket; /// use rocket::http::uri::Origin; /// /// // Parse a valid origin URI. @@ -224,6 +220,11 @@ impl<'a> Origin<'a> { /// /// // Invalid URIs fail to parse. /// Origin::parse("foo bar").expect_err("invalid URI"); + /// + /// // Prefer to use `uri!()` when the input is statically known: + /// let uri = uri!("/a/b/c?query"); + /// assert_eq!(uri.path(), "/a/b/c"); + /// assert_eq!(uri.query().unwrap(), "query"); /// ``` pub fn parse(string: &'a str) -> Result, Error<'a>> { crate::parse::uri::origin_from_str(string) @@ -243,19 +244,14 @@ impl<'a> Origin<'a> { } let (path, query) = RawStr::new(string).split_at_byte(b'?'); - let query = match query.is_empty() { - false => Some(query.as_str()), - true => None, - }; - + let query = (!query.is_empty()).then(|| query.as_str()); Ok(Origin::new(path.as_str(), query)) } /// Parses the string `string` into an `Origin`. Parsing will never - /// allocate. This method should be used instead of - /// [`Origin::parse()`](crate::uri::Origin::parse()) when the source URI is - /// already a `String`. Returns an `Error` if `string` is not a valid origin - /// URI. + /// 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 /// @@ -266,7 +262,7 @@ impl<'a> Origin<'a> { /// 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); + /// assert!(uri.query().is_none()); /// ``` pub fn parse_owned(string: String) -> Result, Error<'static>> { // We create a copy of a pointer to `string` to escape the borrow @@ -280,7 +276,6 @@ impl<'a> Origin<'a> { // These two facts can be easily verified. An `&mut` can't 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 copy_of_str = unsafe { &*(string.as_str() as *const str) }; let origin = Origin::parse(copy_of_str)?; debug_assert!(origin.source.is_some(), "Origin source parsed w/o source"); @@ -288,8 +283,6 @@ impl<'a> Origin<'a> { let origin = Origin { path: origin.path.into_owned(), query: origin.query.into_owned(), - decoded_path_segs: origin.decoded_path_segs.into_owned(), - decoded_query_segs: origin.decoded_query_segs.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 @@ -300,118 +293,39 @@ impl<'a> Origin<'a> { Ok(origin) } - /// 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 normal = Origin::parse("/a/b/c?a=b&c").unwrap(); - /// assert!(normal.is_normalized()); - /// - /// let abnormal = Origin::parse("/a/b/c//d").unwrap(); - /// assert!(!abnormal.is_normalized()); - /// - /// let abnormal = Origin::parse("/a?q&&b").unwrap(); - /// assert!(!abnormal.is_normalized()); - /// ``` - pub fn is_normalized(&self) -> bool { - self.path().starts_with('/') - && self.raw_path_segments().all(|s| !s.is_empty()) - && self.raw_query_segments().all(|s| !s.is_empty()) - } - - /// 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.into_normalized(); - /// assert!(normalized.is_normalized()); - /// assert_eq!(normalized, Origin::parse("/a/b/c/d").unwrap()); - /// ``` - pub fn into_normalized(mut self) -> Self { - use std::fmt::Write; - - if self.is_normalized() { - self - } else { - let mut new_path = String::with_capacity(self.path().len()); - for seg in self.raw_path_segments().filter(|s| !s.is_empty()) { - let _ = write!(new_path, "/{}", seg); - } - - if new_path.is_empty() { - new_path.push('/'); - } - - self.path = Indexed::from(Cow::Owned(new_path)); - - if let Some(q) = self.query() { - let mut new_query = String::with_capacity(q.len()); - let raw_segments = self.raw_query_segments() - .filter(|s| !s.is_empty()) - .enumerate(); - - for (i, seg) in raw_segments { - if i != 0 { new_query.push('&'); } - let _ = write!(new_query, "{}", seg); - } - - self.query = Some(Indexed::from(Cow::Owned(new_query))); - } - - // Note: normalization preserves segments! - self - } - } - /// Returns the path part of this URI. /// - /// ### Examples - /// - /// A URI with only a path: + /// # Example /// /// ```rust - /// # extern crate rocket; - /// use rocket::http::uri::Origin; - /// - /// let uri = Origin::parse("/a/b/c").unwrap(); + /// # #[macro_use] extern crate rocket; + /// let uri = uri!("/a/b/c"); /// 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(); + /// let uri = uri!("/a/b/c?name=bob"); /// assert_eq!(uri.path(), "/a/b/c"); /// ``` #[inline] - pub fn path(&self) -> &RawStr { - self.path.from_cow_source(&self.source).into() + pub fn path(&self) -> Path<'_> { + Path { source: &self.source, data: &self.path } + } + + /// Returns the query part of this URI without the question mark, if there + /// is any. + /// + /// # Example + /// + /// ```rust + /// # #[macro_use] extern crate rocket; + /// let uri = uri!("/a/b/c?alphabet=true"); + /// assert_eq!(uri.query().unwrap(), "alphabet=true"); + /// + /// let uri = uri!("/a/b/c"); + /// assert!(uri.query().is_none()); + /// ``` + #[inline] + pub fn query(&self) -> Option> { + self.query.as_ref().map(|data| Query { source: &self.source, data }) } /// Applies the function `f` to the internal `path` and returns a new @@ -424,94 +338,88 @@ impl<'a> Origin<'a> { /// Affix a trailing slash if one isn't present. /// /// ```rust - /// # extern crate rocket; - /// use rocket::http::uri::Origin; + /// # #[macro_use] extern crate rocket; + /// let uri = uri!("/a/b/c"); + /// let expected_uri = uri!("/a/b/c/d"); + /// assert_eq!(uri.map_path(|p| format!("{}/d", p)), Some(expected_uri)); /// - /// let old_uri = Origin::parse("/a/b/c").unwrap(); - /// let expected_uri = Origin::parse("/a/b/c/").unwrap(); - /// assert_eq!(old_uri.map_path(|p| format!("{}/", p)), Some(expected_uri)); + /// let uri = uri!("/a/b/c"); + /// let abnormal_map = uri.map_path(|p| format!("{}///d", p)); + /// assert_eq!(abnormal_map.unwrap(), "/a/b/c///d"); /// - /// let old_uri = Origin::parse("/a/b/c/").unwrap(); - /// let expected_uri = Origin::parse("/a/b/c//").unwrap(); - /// assert_eq!(old_uri.map_path(|p| format!("{}/", p)), Some(expected_uri)); + /// let uri = uri!("/a/b/c"); + /// let expected = uri!("/b/c"); + /// let mapped = uri.map_path(|p| p.strip_prefix("/a").unwrap_or(p)); + /// assert_eq!(mapped, Some(expected)); /// - /// let old_uri = Origin::parse("/a/b/c/").unwrap(); - /// let expected = Origin::parse("/b/c/").unwrap(); - /// assert_eq!(old_uri.map_path(|p| p.strip_prefix("/a").unwrap_or(p)), Some(expected)); + /// let uri = uri!("/a"); + /// assert_eq!(uri.map_path(|p| p.strip_prefix("/a").unwrap_or(p)), None); /// - /// let old_uri = Origin::parse("/a").unwrap(); - /// assert_eq!(old_uri.map_path(|p| p.strip_prefix("/a").unwrap_or(p)), None); - /// - /// let old_uri = Origin::parse("/a/b/c/").unwrap(); - /// assert_eq!(old_uri.map_path(|p| format!("hi/{}", p)), None); + /// let uri = uri!("/a/b/c"); + /// assert_eq!(uri.map_path(|p| format!("hi/{}", p)), None); /// ``` #[inline] pub fn map_path<'s, F, P>(&'s self, f: F) -> Option where F: FnOnce(&'s RawStr) -> P, P: Into + 's { - let path = f(self.path()).into(); + let path = f(self.path().raw()).into(); if !path.starts_with('/') || !path.as_bytes().iter().all(|b| is_pchar(&b)) { return None; } Some(Origin { source: self.source.clone(), - path: Cow::from(path.into_string()).into(), + path: Data::new(Cow::from(path.into_string())), query: self.query.clone(), - decoded_path_segs: Storage::new(), - decoded_query_segs: Storage::new(), }) } - /// 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().unwrap(), "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<&RawStr> { - self.query.as_ref().map(|q| q.from_cow_source(&self.source).into()) - } - /// 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(); + /// # #[macro_use] extern crate rocket; + /// let mut uri = uri!("/a/b/c?query=some"); /// assert_eq!(uri.query().unwrap(), "query=some"); /// /// uri.clear_query(); - /// assert_eq!(uri.query(), None); + /// assert!(uri.query().is_none()); /// ``` pub fn clear_query(&mut self) { - self.query = None; + self.set_query(None); } - /// Returns a (smart) iterator over the non-empty, percent-decoded segments - /// of the path of this 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. Note that `uri!()` always + /// normalizes static input. + /// + /// # Example + /// + /// ```rust + /// # #[macro_use] extern crate rocket; + /// use rocket::http::uri::Origin; + /// + /// assert!(Origin::parse("/").unwrap().is_normalized()); + /// assert!(Origin::parse("/a/b/c").unwrap().is_normalized()); + /// assert!(Origin::parse("/a/b/c?a=b&c").unwrap().is_normalized()); + /// + /// assert!(!Origin::parse("/a/b/c//d").unwrap().is_normalized()); + /// assert!(!Origin::parse("/a?q&&b").unwrap().is_normalized()); + /// + /// assert!(uri!("/a/b/c//d").is_normalized()); + /// assert!(uri!("/a?q&&b").is_normalized()); + /// ``` + pub fn is_normalized(&self) -> bool { + self.path().is_normalized(true) && self.query().map_or(true, |q| q.is_normalized()) + } + + /// Normalizes `self`. This is a no-op if `self` is already normalized. + /// + /// See [Normalization](#normalization) for more information on what it + /// means for an origin URI to be normalized. /// /// # Example /// @@ -519,25 +427,28 @@ impl<'a> Origin<'a> { /// # extern crate rocket; /// use rocket::http::uri::Origin; /// - /// let uri = Origin::parse("/a%20b/b%2Fc/d//e?query=some").unwrap(); - /// let path_segs: Vec<&str> = uri.path_segments().collect(); - /// assert_eq!(path_segs, &["a b", "b/c", "d", "e"]); + /// let mut abnormal = Origin::parse("/a/b/c//d").unwrap(); + /// assert!(!abnormal.is_normalized()); + /// abnormal.normalize(); + /// assert!(abnormal.is_normalized()); /// ``` - pub fn path_segments(&self) -> Segments<'_> { - let cached = self.decoded_path_segs.get_or_set(|| { - let (indexed, path) = (&self.path, self.path()); - self.raw_path_segments() - .filter(|r| !r.is_empty()) - .map(|s| decode_to_indexed_str::(s, (indexed, path))) - .collect() - }); + pub fn normalize(&mut self) { + if !self.path().is_normalized(true) { + self.path = self.path().to_normalized(true); + } - Segments { source: self.path(), segments: cached, pos: 0 } + if let Some(query) = self.query() { + if !query.is_normalized() { + self.query = query.to_normalized(); + } + } } - /// Returns a (smart) iterator over the non-empty, url-decoded `(name, - /// value)` pairs of the query of this URI. If there is no query, the - /// iterator is empty. + /// Consumes `self` and returns a normalized version. + /// + /// This is a no-op if `self` is already normalized. See + /// [Normalization](#normalization) for more information on what it means + /// for an origin URI to be normalized. /// /// # Example /// @@ -545,103 +456,13 @@ impl<'a> Origin<'a> { /// # extern crate rocket; /// use rocket::http::uri::Origin; /// - /// let uri = Origin::parse("/").unwrap(); - /// let query_segs: Vec<_> = uri.query_segments().collect(); - /// assert!(query_segs.is_empty()); - /// - /// let uri = Origin::parse("/foo/bar?a+b%2F=some+one%40gmail.com&&%26%3D2").unwrap(); - /// let query_segs: Vec<_> = uri.query_segments().collect(); - /// assert_eq!(query_segs, &[("a b/", "some one@gmail.com"), ("&=2", "")]); + /// let abnormal = Origin::parse("/a/b/c//d").unwrap(); + /// assert!(!abnormal.is_normalized()); + /// assert!(abnormal.into_normalized().is_normalized()); /// ``` - pub fn query_segments(&self) -> QuerySegments<'_> { - let cached = self.decoded_query_segs.get_or_set(|| { - let (indexed, query) = match (self.query.as_ref(), self.query()) { - (Some(i), Some(q)) => (i, q), - _ => return vec![], - }; - - self.raw_query_segments() - .filter(|s| !s.is_empty()) - .map(|s| s.split_at_byte(b'=')) - .map(|(name, val)| { - let name = decode_to_indexed_str::(name, (indexed, query)); - let val = decode_to_indexed_str::(val, (indexed, query)); - (name, val) - }) - .collect() - }); - - QuerySegments { source: self.query(), segments: cached, pos: 0 } - } - - /// Returns an iterator over the raw, undecoded segments of the path in this - /// URI. Segments may be empty. - /// - /// ### Example - /// - /// ```rust - /// # extern crate rocket; - /// use rocket::http::uri::Origin; - /// - /// let uri = Origin::parse("/").unwrap(); - /// let segments: Vec<_> = uri.raw_path_segments().collect(); - /// assert!(segments.is_empty()); - /// - /// let uri = Origin::parse("//").unwrap(); - /// let segments: Vec<_> = uri.raw_path_segments().collect(); - /// assert_eq!(segments, &["", ""]); - /// - /// let uri = Origin::parse("/a").unwrap(); - /// let segments: Vec<_> = uri.raw_path_segments().collect(); - /// assert_eq!(segments, &["a"]); - /// - /// let uri = Origin::parse("/a//b///c/d?query¶m").unwrap(); - /// let segments: Vec<_> = uri.raw_path_segments().collect(); - /// assert_eq!(segments, &["a", "", "b", "", "", "c", "d"]); - /// ``` - #[inline(always)] - pub fn raw_path_segments(&self) -> impl Iterator { - let path = match self.path() { - p if p == "/" => None, - p if p.starts_with('/') => Some(&p[1..]), - p => Some(p) - }; - - path.map(|p| p.split(Path::DELIMITER)) - .into_iter() - .flatten() - } - - /// Returns an iterator over the non-empty, non-url-decoded `(name, value)` - /// pairs of the query of this URI. If there is no query, the iterator is - /// empty. Segments may be empty. - /// - /// # Example - /// - /// ```rust - /// # extern crate rocket; - /// use rocket::http::uri::Origin; - /// - /// let uri = Origin::parse("/").unwrap(); - /// assert!(uri.raw_query_segments().next().is_none()); - /// - /// let uri = Origin::parse("/?a=b&dog").unwrap(); - /// let query_segs: Vec<_> = uri.raw_query_segments().collect(); - /// assert_eq!(query_segs, &["a=b", "dog"]); - /// - /// let uri = Origin::parse("/?&").unwrap(); - /// let query_segs: Vec<_> = uri.raw_query_segments().collect(); - /// assert_eq!(query_segs, &["", ""]); - /// - /// let uri = Origin::parse("/foo/bar?a+b%2F=some+one%40gmail.com&&%26%3D2").unwrap(); - /// let query_segs: Vec<_> = uri.raw_query_segments().collect(); - /// assert_eq!(query_segs, &["a+b%2F=some+one%40gmail.com", "", "%26%3D2"]); - /// ``` - #[inline] - pub fn raw_query_segments(&self) -> impl Iterator { - self.query() - .into_iter() - .flat_map(|q| q.split(Query::DELIMITER)) + pub fn into_normalized(mut self) -> Self { + self.normalize(); + self } } @@ -670,11 +491,11 @@ impl<'a> TryFrom<&'a str> for Origin<'a> { } } -impl Display for Origin<'_> { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { +impl std::fmt::Display for Origin<'_> { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!(f, "{}", self.path())?; - if let Some(q) = self.query() { - write!(f, "?{}", q)?; + if let Some(query) = self.query() { + write!(f, "?{}", query)?; } Ok(()) @@ -687,7 +508,7 @@ mod tests { fn seg_count(path: &str, expected: usize) -> bool { let origin = Origin::parse(path).unwrap(); - let segments = origin.path_segments(); + let segments = origin.path().segments(); let actual = segments.len(); if actual != expected { eprintln!("Count mismatch: expected {}, got {}.", expected, actual); @@ -707,7 +528,7 @@ mod tests { Err(e) => panic!("failed to parse {}: {}", path, e) }; - let actual: Vec<&str> = uri.path_segments().collect(); + let actual: Vec<&str> = uri.path().segments().collect(); actual == expected } @@ -792,7 +613,7 @@ mod tests { fn test_query(uri: &str, query: Option<&str>) { let uri = Origin::parse(uri).unwrap(); - assert_eq!(uri.query().map(|s| s.as_str()), query); + assert_eq!(uri.query().map(|q| q.as_str()), query); } #[test] diff --git a/core/http/src/uri/path_query.rs b/core/http/src/uri/path_query.rs new file mode 100644 index 00000000..93287ffa --- /dev/null +++ b/core/http/src/uri/path_query.rs @@ -0,0 +1,387 @@ +use std::hash::Hash; +use std::borrow::Cow; +use std::fmt::Write; + +use state::Storage; + +use crate::{RawStr, ext::IntoOwned}; +use crate::uri::Segments; +use crate::uri::fmt::{self, Part}; +use crate::parse::{IndexedStr, Extent}; + +// INTERNAL DATA STRUCTURE. +#[doc(hidden)] +#[derive(Debug, Clone)] +pub struct Data<'a, P: Part> { + pub(crate) value: IndexedStr<'a>, + pub(crate) decoded_segments: Storage>, +} + +impl<'a, P: Part> Data<'a, P> { + pub(crate) fn raw(value: Extent<&'a [u8]>) -> Self { + Data { value: value.into(), decoded_segments: Storage::new() } + } + + // INTERNAL METHOD. + #[doc(hidden)] + pub fn new>>(value: S) -> Self { + Data { + value: IndexedStr::from(value.into()), + decoded_segments: Storage::new(), + } + } +} + +/// A URI path: `/foo/bar`, `foo/bar`, etc. +#[derive(Debug, Clone, Copy)] +pub struct Path<'a> { + pub(crate) source: &'a Option>, + pub(crate) data: &'a Data<'a, fmt::Path>, +} + +/// A URI query: `?foo&bar`. +#[derive(Debug, Clone, Copy)] +pub struct Query<'a> { + pub(crate) source: &'a Option>, + pub(crate) data: &'a Data<'a, fmt::Query>, +} + +fn decode_to_indexed_str( + value: &RawStr, + (indexed, source): (&IndexedStr<'_>, &RawStr) +) -> IndexedStr<'static> { + let decoded = match P::KIND { + fmt::Kind::Path => value.percent_decode_lossy(), + fmt::Kind::Query => value.url_decode_lossy(), + }; + + match decoded { + Cow::Borrowed(b) if indexed.is_indexed() => { + let indexed = IndexedStr::checked_from(b, source.as_str()); + debug_assert!(indexed.is_some()); + indexed.unwrap_or(IndexedStr::from(Cow::Borrowed(""))) + } + cow => IndexedStr::from(Cow::Owned(cow.into_owned())), + } +} + +impl<'a> Path<'a> { + /// Returns the raw path value. + /// + /// # Example + /// + /// ```rust + /// # #[macro_use] extern crate rocket; + /// let uri = uri!("/foo%20bar%2dbaz"); + /// assert_eq!(uri.path(), "/foo%20bar%2dbaz"); + /// assert_eq!(uri.path().raw(), "/foo%20bar%2dbaz"); + /// ``` + pub fn raw(&self) -> &'a RawStr { + self.data.value.from_cow_source(&self.source).into() + } + + /// Returns the raw, undecoded path value as an `&str`. + /// + /// # Example + /// + /// ```rust + /// # #[macro_use] extern crate rocket; + /// let uri = uri!("/foo%20bar%2dbaz"); + /// assert_eq!(uri.path(), "/foo%20bar%2dbaz"); + /// assert_eq!(uri.path().as_str(), "/foo%20bar%2dbaz"); + /// ``` + pub fn as_str(&self) -> &'a str { + self.raw().as_str() + } + + /// Whether `self` is normalized, i.e, it has no empty segments. + /// + /// If `absolute`, then a starting `/` is required. + pub(crate) fn is_normalized(&self, absolute: bool) -> bool { + (!absolute || self.raw().starts_with('/')) + && self.raw_segments().all(|s| !s.is_empty()) + } + + /// Normalizes `self`. If `absolute`, a starting `/` is required. + pub(crate) fn to_normalized(&self, absolute: bool) -> Data<'static, fmt::Path> { + let mut path = String::with_capacity(self.raw().len()); + let absolute = absolute || self.raw().starts_with('/'); + for (i, seg) in self.raw_segments().filter(|s| !s.is_empty()).enumerate() { + if absolute || i != 0 { path.push('/'); } + let _ = write!(path, "{}", seg); + } + + if path.is_empty() && absolute { + path.push('/'); + } + + Data { + value: IndexedStr::from(Cow::Owned(path)), + decoded_segments: Storage::new(), + } + } + + /// Returns an iterator over the raw, undecoded segments. Segments may be + /// empty. + /// + /// ### Example + /// + /// ```rust + /// # #[macro_use] extern crate rocket; + /// use rocket::http::uri::Origin; + /// + /// let uri = Origin::parse("/").unwrap(); + /// assert_eq!(uri.path().raw_segments().count(), 0); + /// + /// let uri = Origin::parse("//").unwrap(); + /// let segments: Vec<_> = uri.path().raw_segments().collect(); + /// assert_eq!(segments, &["", ""]); + /// + /// // Recall that `uri!()` normalizes static inputs. + /// let uri = uri!("//"); + /// assert_eq!(uri.path().raw_segments().count(), 0); + /// + /// let uri = Origin::parse("/a").unwrap(); + /// let segments: Vec<_> = uri.path().raw_segments().collect(); + /// assert_eq!(segments, &["a"]); + /// + /// let uri = Origin::parse("/a//b///c/d?query¶m").unwrap(); + /// let segments: Vec<_> = uri.path().raw_segments().collect(); + /// assert_eq!(segments, &["a", "", "b", "", "", "c", "d"]); + /// ``` + #[inline(always)] + pub fn raw_segments(&self) -> impl Iterator { + let path = match self.raw() { + p if p.is_empty() || p == "/" => None, + p if p.starts_with(fmt::Path::DELIMITER) => Some(&p[1..]), + p => Some(p) + }; + + path.map(|p| p.split(fmt::Path::DELIMITER)) + .into_iter() + .flatten() + } + + /// Returns a (smart) iterator over the non-empty, percent-decoded segments. + /// + /// # Example + /// + /// ```rust + /// # #[macro_use] extern crate rocket; + /// use rocket::http::uri::Origin; + /// + /// let uri = Origin::parse("/a%20b/b%2Fc/d//e?query=some").unwrap(); + /// let path_segs: Vec<&str> = uri.path().segments().collect(); + /// assert_eq!(path_segs, &["a b", "b/c", "d", "e"]); + /// ``` + pub fn segments(&self) -> Segments<'a, fmt::Path> { + let cached = self.data.decoded_segments.get_or_set(|| { + let (indexed, path) = (&self.data.value, self.raw()); + self.raw_segments() + .filter(|r| !r.is_empty()) + .map(|s| decode_to_indexed_str::(s, (indexed, path))) + .collect() + }); + + Segments::new(self.raw(), cached) + } +} + +impl<'a> Query<'a> { + /// Returns the raw, undecoded query value. + /// + /// # Example + /// + /// ```rust + /// # #[macro_use] extern crate rocket; + /// let uri = uri!("/foo?baz+bar"); + /// assert_eq!(uri.query().unwrap(), "baz+bar"); + /// assert_eq!(uri.query().unwrap().raw(), "baz+bar"); + /// ``` + pub fn raw(&self) -> &'a RawStr { + self.data.value.from_cow_source(&self.source).into() + } + + /// Returns the raw, undecoded query value as an `&str`. + /// + /// # Example + /// + /// ```rust + /// # #[macro_use] extern crate rocket; + /// let uri = uri!("/foo/bar?baz+bar"); + /// assert_eq!(uri.query().unwrap(), "baz+bar"); + /// assert_eq!(uri.query().unwrap().as_str(), "baz+bar"); + /// ``` + pub fn as_str(&self) -> &'a str { + self.raw().as_str() + } + + /// Whether `self` is normalized, i.e, it has no empty segments. + pub(crate) fn is_normalized(&self) -> bool { + !self.is_empty() && self.raw_segments().all(|s| !s.is_empty()) + } + + /// Normalizes `self`. + pub(crate) fn to_normalized(&self) -> Option> { + let mut query = String::with_capacity(self.raw().len()); + for (i, seg) in self.raw_segments().filter(|s| !s.is_empty()).enumerate() { + if i != 0 { query.push('&'); } + let _ = write!(query, "{}", seg); + } + + if query.is_empty() { + return None; + } + + Some(Data { + value: IndexedStr::from(Cow::Owned(query)), + decoded_segments: Storage::new(), + }) + } + + /// Returns an iterator over the non-empty, undecoded `(name, value)` pairs + /// of this query. If there is no query, the iterator is empty. Segments may + /// be empty. + /// + /// # Example + /// + /// ```rust + /// # #[macro_use] extern crate rocket; + /// use rocket::http::uri::Origin; + /// + /// let uri = Origin::parse("/").unwrap(); + /// assert!(uri.query().is_none()); + /// + /// let uri = Origin::parse("/?a=b&dog").unwrap(); + /// let query_segs: Vec<_> = uri.query().unwrap().raw_segments().collect(); + /// assert_eq!(query_segs, &["a=b", "dog"]); + /// + /// // This is not normalized, so the query is `""`, the empty string. + /// let uri = Origin::parse("/?&").unwrap(); + /// let query_segs: Vec<_> = uri.query().unwrap().raw_segments().collect(); + /// assert_eq!(query_segs, &["", ""]); + /// + /// // Recall that `uri!()` normalizes. + /// let uri = uri!("/?&"); + /// assert!(uri.query().is_none()); + /// + /// // These are raw and undecoded. Use `segments()` for decoded variant. + /// let uri = Origin::parse("/foo/bar?a+b%2F=some+one%40gmail.com&&%26%3D2").unwrap(); + /// let query_segs: Vec<_> = uri.query().unwrap().raw_segments().collect(); + /// assert_eq!(query_segs, &["a+b%2F=some+one%40gmail.com", "", "%26%3D2"]); + /// ``` + #[inline] + pub fn raw_segments(&self) -> impl Iterator { + let query = match self.raw() { + q if q.is_empty() => None, + q => Some(q) + }; + + query.map(|p| p.split(fmt::Query::DELIMITER)) + .into_iter() + .flatten() + } + + /// Returns a (smart) iterator over the non-empty, url-decoded `(name, + /// value)` pairs of this query. If there is no query, the iterator is + /// empty. + /// + /// # Example + /// + /// ```rust + /// # #[macro_use] extern crate rocket; + /// use rocket::http::uri::Origin; + /// + /// let uri = Origin::parse("/").unwrap(); + /// assert!(uri.query().is_none()); + /// + /// let uri = Origin::parse("/foo/bar?a+b%2F=some+one%40gmail.com&&%26%3D2").unwrap(); + /// let query_segs: Vec<_> = uri.query().unwrap().segments().collect(); + /// assert_eq!(query_segs, &[("a b/", "some one@gmail.com"), ("&=2", "")]); + /// ``` + pub fn segments(&self) -> Segments<'a, fmt::Query> { + let cached = self.data.decoded_segments.get_or_set(|| { + let (indexed, query) = (&self.data.value, self.raw()); + self.raw_segments() + .filter(|s| !s.is_empty()) + .map(|s| s.split_at_byte(b'=')) + .map(|(k, v)| { + let key = decode_to_indexed_str::(k, (indexed, query)); + let val = decode_to_indexed_str::(v, (indexed, query)); + (key, val) + }) + .collect() + }); + + Segments::new(self.raw(), cached) + } +} + +macro_rules! impl_partial_eq { + ($A:ty = $B:ty) => ( + impl PartialEq<$A> for $B { + #[inline(always)] + fn eq(&self, other: &$A) -> bool { + let left: &RawStr = self.as_ref(); + let right: &RawStr = other.as_ref(); + left == right + } + } + ) +} + +macro_rules! impl_traits { + ($T:ident) => ( + impl Hash for $T<'_> { + fn hash(&self, state: &mut H) { + self.raw().hash(state); + } + } + + impl Eq for $T<'_> { } + + impl IntoOwned for Data<'_, fmt::$T> { + type Owned = Data<'static, fmt::$T>; + + fn into_owned(self) -> Self::Owned { + Data { + value: self.value.into_owned(), + decoded_segments: self.decoded_segments.map(|v| v.into_owned()), + } + } + } + + impl std::ops::Deref for $T<'_> { + type Target = RawStr; + + fn deref(&self) -> &Self::Target { + self.raw() + } + } + + impl AsRef for $T<'_> { + fn as_ref(&self) -> &RawStr { + self.raw() + } + } + + impl std::fmt::Display for $T<'_> { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.raw()) + } + } + + impl_partial_eq!($T<'_> = $T<'_>); + impl_partial_eq!(str = $T<'_>); + impl_partial_eq!(&str = $T<'_>); + impl_partial_eq!($T<'_> = str); + impl_partial_eq!($T<'_> = &str); + impl_partial_eq!(RawStr = $T<'_>); + impl_partial_eq!(&RawStr = $T<'_>); + impl_partial_eq!($T<'_> = RawStr); + impl_partial_eq!($T<'_> = &RawStr); + ) +} + +impl_traits!(Path); +impl_traits!(Query); diff --git a/core/http/src/uri/reference.rs b/core/http/src/uri/reference.rs new file mode 100644 index 00000000..4dfb1cda --- /dev/null +++ b/core/http/src/uri/reference.rs @@ -0,0 +1,549 @@ +use std::{borrow::Cow, convert::TryFrom}; + +use crate::RawStr; +use crate::ext::IntoOwned; +use crate::uri::{Authority, Data, Origin, Absolute, Asterisk}; +use crate::uri::{Path, Query, Error, as_utf8_unchecked, fmt}; +use crate::parse::{Extent, IndexedStr}; + +/// A URI-reference with optional scheme, authority, relative path, query, and +/// fragment parts. +/// +/// # Structure +/// +/// The following diagram illustrates the syntactic structure of a URI reference +/// with all optional parts: +/// +/// ```text +/// http://user:pass@domain.com:4444/foo/bar?some=query#and-fragment +/// |--| |------------------------||------| |--------| |----------| +/// scheme authority path query fragment +/// ``` +/// +/// All parts are optional. When a scheme and authority are not present, the +/// path may be relative: `foo/bar?baz#cat`. +/// +/// # Conversion +/// +/// All other URI types ([`Origin`], [`Absolute`], and so on) are valid URI +/// references. As such, conversion between the types is lossless: +/// +/// ```rust +/// # #[macro_use] extern crate rocket; +/// use rocket::http::uri::Reference; +/// +/// let absolute = uri!("http://rocket.rs"); +/// let reference: Reference = absolute.into(); +/// assert_eq!(reference.scheme(), Some("http")); +/// assert_eq!(reference.authority().unwrap().host(), "rocket.rs"); +/// +/// let origin = uri!("/foo/bar"); +/// let reference: Reference = origin.into(); +/// assert_eq!(reference.path(), "/foo/bar"); +/// ``` +/// +/// Note that `uri!()` macro _always_ prefers the more specific URI variant to +/// `Reference` when possible, as is demonstrated above for `absolute` and +/// `origin`. +#[derive(Debug, Clone)] +pub struct Reference<'a> { + source: Option>, + scheme: Option>, + authority: Option>, + path: Data<'a, fmt::Path>, + query: Option>, + fragment: Option>, +} + +impl<'a> Reference<'a> { + #[inline] + pub(crate) unsafe fn raw( + source: Cow<'a, [u8]>, + scheme: Option>, + authority: Option>, + path: Extent<&'a [u8]>, + query: Option>, + fragment: Option>, + ) -> Reference<'a> { + Reference { + source: Some(as_utf8_unchecked(source)), + scheme: scheme.map(|s| s.into()), + authority: authority.map(|s| s.into()), + path: Data::raw(path), + query: query.map(Data::raw), + fragment: fragment.map(|f| f.into()), + } + } + + /// PRIVATE. Used during test. + #[cfg(test)] + pub fn new( + scheme: impl Into>, + auth: impl Into>>, + path: &'a str, + query: impl Into>, + frag: impl Into>, + ) -> Reference<'a> { + Reference::const_new(scheme.into(), auth.into(), path, query.into(), frag.into()) + } + + /// PRIVATE. Used by codegen. + #[doc(hidden)] + pub const fn const_new( + scheme: Option<&'a str>, + authority: Option>, + path: &'a str, + query: Option<&'a str>, + fragment: Option<&'a str>, + ) -> Reference<'a> { + Reference { + source: None, + scheme: match scheme { + Some(scheme) => Some(IndexedStr::Concrete(Cow::Borrowed(scheme))), + None => None + }, + authority, + path: Data { + value: IndexedStr::Concrete(Cow::Borrowed(path)), + decoded_segments: state::Storage::new(), + }, + query: match query { + Some(query) => Some(Data { + value: IndexedStr::Concrete(Cow::Borrowed(query)), + decoded_segments: state::Storage::new(), + }), + None => None, + }, + fragment: match fragment { + Some(frag) => Some(IndexedStr::Concrete(Cow::Borrowed(frag))), + None => None, + }, + } + } + + /// Parses the string `string` into an `Reference`. Parsing will never + /// allocate. Returns an `Error` if `string` is not a valid origin URI. + /// + /// # Example + /// + /// ```rust + /// # #[macro_use] extern crate rocket; + /// use rocket::http::uri::Reference; + /// + /// // Parse a valid URI reference. + /// let uri = Reference::parse("/a/b/c?query").expect("valid URI"); + /// assert_eq!(uri.path(), "/a/b/c"); + /// assert_eq!(uri.query().unwrap(), "query"); + /// + /// // Invalid URIs fail to parse. + /// Reference::parse("foo bar").expect_err("invalid URI"); + /// + /// // Prefer to use `uri!()` when the input is statically known: + /// let uri = uri!("/a/b/c?query#fragment"); + /// assert_eq!(uri.path(), "/a/b/c"); + /// assert_eq!(uri.query().unwrap(), "query"); + /// assert_eq!(uri.fragment().unwrap(), "fragment"); + /// ``` + pub fn parse(string: &'a str) -> Result> { + crate::parse::uri::reference_from_str(string) + } + + /// Parses the string `string` into a `Reference`. Never allocates. + /// + /// This method should be used instead of [`Reference::parse()`] when the + /// source URI is already a `String`. Returns an `Error` if `string` is not + /// a valid URI reference. + /// + /// # Example + /// + /// ```rust + /// # extern crate rocket; + /// use rocket::http::uri::Reference; + /// + /// let source = format!("/foo?{}#3", 2); + /// let uri = Reference::parse_owned(source).unwrap(); + /// assert_eq!(uri.path(), "/foo"); + /// assert_eq!(uri.query().unwrap(), "2"); + /// assert_eq!(uri.fragment().unwrap(), "3"); + /// ``` + pub fn parse_owned(string: String) -> Result> { + // 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 while `copy_of_str` is live. + // + // These two facts can be easily verified. An `&mut` can't 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 uri_ref = Reference::parse(copy_of_str)?; + debug_assert!(uri_ref.source.is_some(), "UriRef parsed w/o source"); + + let uri_ref = Reference { + scheme: uri_ref.scheme.into_owned(), + authority: uri_ref.authority.into_owned(), + path: uri_ref.path.into_owned(), + query: uri_ref.query.into_owned(), + fragment: uri_ref.fragment.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)), + }; + + Ok(uri_ref) + } + + /// Returns the scheme. If `Some`, is non-empty. + /// + /// # Example + /// + /// ```rust + /// # #[macro_use] extern crate rocket; + /// let uri = uri!("http://rocket.rs?foo#bar"); + /// assert_eq!(uri.scheme(), Some("http")); + /// + /// let uri = uri!("ftp:/?foo#bar"); + /// assert_eq!(uri.scheme(), Some("ftp")); + /// + /// let uri = uri!("?foo#bar"); + /// assert_eq!(uri.scheme(), None); + /// ``` + #[inline] + pub fn scheme(&self) -> Option<&str> { + self.scheme.as_ref().map(|s| s.from_cow_source(&self.source)) + } + + /// Returns the authority part. + /// + /// # Example + /// + /// ```rust + /// # #[macro_use] extern crate rocket; + /// let uri = uri!("http://rocket.rs:4444?foo#bar"); + /// let auth = uri!("rocket.rs:4444"); + /// assert_eq!(uri.authority().unwrap(), &auth); + /// + /// let uri = uri!("?foo#bar"); + /// assert_eq!(uri.authority(), None); + /// ``` + #[inline(always)] + pub fn authority(&self) -> Option<&Authority<'a>> { + self.authority.as_ref() + } + + /// Returns the path part. May be empty. + /// + /// # Example + /// + /// ```rust + /// # #[macro_use] extern crate rocket; + /// let uri = uri!("http://rocket.rs/guide?foo#bar"); + /// assert_eq!(uri.path(), "/guide"); + /// ``` + #[inline(always)] + pub fn path(&self) -> Path<'_> { + Path { source: &self.source, data: &self.path } + } + + /// Returns the query part. May be empty. + /// + /// # Example + /// + /// ```rust + /// # #[macro_use] extern crate rocket; + /// let uri = uri!("http://rocket.rs/guide?foo#bar"); + /// assert_eq!(uri.query().unwrap(), "foo"); + /// + /// let uri = uri!("http://rocket.rs/guide?q=bar"); + /// assert_eq!(uri.query().unwrap(), "q=bar"); + /// + /// // Empty query parts are normalized away by `uri!()`. + /// let uri = uri!("http://rocket.rs/guide?#bar"); + /// assert!(uri.query().is_none()); + /// ``` + #[inline(always)] + pub fn query(&self) -> Option> { + self.query.as_ref().map(|data| Query { source: &self.source, data }) + } + + /// Returns the fragment part, if any. + /// + /// # Example + /// + /// ```rust + /// # #[macro_use] extern crate rocket; + /// let uri = uri!("http://rocket.rs/guide?foo#bar"); + /// assert_eq!(uri.fragment().unwrap(), "bar"); + /// + /// // Fragment parts aren't normalized away, unlike query parts. + /// let uri = uri!("http://rocket.rs/guide?foo#"); + /// assert_eq!(uri.fragment().unwrap(), ""); + /// ``` + #[inline(always)] + pub fn fragment(&self) -> Option<&RawStr> { + self.fragment.as_ref() + .map(|frag| frag.from_cow_source(&self.source).into()) + } + + /// Returns `true` if `self` is normalized. Otherwise, returns `false`. + /// + /// Normalization for a URI reference is equivalent to normalization for an + /// absolute URI. See [`Absolute#normalization`] for more information on + /// what it means for an absolute URI to be normalized. + /// + /// # Example + /// + /// ```rust + /// # #[macro_use] extern crate rocket; + /// use rocket::http::uri::Reference; + /// + /// assert!(Reference::parse("foo/bar").unwrap().is_normalized()); + /// assert!(Reference::parse("foo/bar#").unwrap().is_normalized()); + /// assert!(Reference::parse("http://").unwrap().is_normalized()); + /// assert!(Reference::parse("http://foo.rs/foo/bar").unwrap().is_normalized()); + /// assert!(Reference::parse("foo:bar#baz").unwrap().is_normalized()); + /// assert!(Reference::parse("http://rocket.rs#foo").unwrap().is_normalized()); + /// + /// assert!(!Reference::parse("http://?").unwrap().is_normalized()); + /// assert!(!Reference::parse("git://rocket.rs/").unwrap().is_normalized()); + /// assert!(!Reference::parse("http:/foo//bar").unwrap().is_normalized()); + /// assert!(!Reference::parse("foo:bar?baz&&bop#c").unwrap().is_normalized()); + /// assert!(!Reference::parse("http://rocket.rs?#foo").unwrap().is_normalized()); + /// + /// // Recall that `uri!()` normalizes static input. + /// assert!(uri!("http://rocket.rs#foo").is_normalized()); + /// assert!(uri!("http://rocket.rs///foo////bar#cat").is_normalized()); + /// ``` + pub fn is_normalized(&self) -> bool { + let normalized_query = self.query().map_or(true, |q| q.is_normalized()); + if self.authority().is_some() && !self.path().is_empty() { + self.path().is_normalized(true) + && self.path() != "/" + && normalized_query + } else { + self.path().is_normalized(false) && normalized_query + } + } + + /// Normalizes `self` in-place. Does nothing if `self` is already + /// normalized. + /// + /// # Example + /// + /// ```rust + /// use rocket::http::uri::Reference; + /// + /// let mut uri = Reference::parse("git://rocket.rs/").unwrap(); + /// assert!(!uri.is_normalized()); + /// uri.normalize(); + /// assert!(uri.is_normalized()); + /// + /// let mut uri = Reference::parse("http:/foo//bar?baz&&#cat").unwrap(); + /// assert!(!uri.is_normalized()); + /// uri.normalize(); + /// assert!(uri.is_normalized()); + /// + /// let mut uri = Reference::parse("foo:bar?baz&&bop").unwrap(); + /// assert!(!uri.is_normalized()); + /// uri.normalize(); + /// assert!(uri.is_normalized()); + /// ``` + pub fn normalize(&mut self) { + if self.authority().is_some() && !self.path().is_empty() { + if self.path() == "/" { + self.set_path(""); + } else if !self.path().is_normalized(true) { + self.path = self.path().to_normalized(true); + } + } else { + self.path = self.path().to_normalized(false); + } + + if let Some(query) = self.query() { + if !query.is_normalized() { + self.query = query.to_normalized(); + } + } + } + + /// Normalizes `self`. This is a no-op if `self` is already normalized. + /// + /// # Example + /// + /// ```rust + /// use rocket::http::uri::Reference; + /// + /// let mut uri = Reference::parse("git://rocket.rs/").unwrap(); + /// assert!(!uri.is_normalized()); + /// assert!(uri.into_normalized().is_normalized()); + /// + /// let mut uri = Reference::parse("http:/foo//bar?baz&&#cat").unwrap(); + /// assert!(!uri.is_normalized()); + /// assert!(uri.into_normalized().is_normalized()); + /// + /// let mut uri = Reference::parse("foo:bar?baz&&bop").unwrap(); + /// assert!(!uri.is_normalized()); + /// assert!(uri.into_normalized().is_normalized()); + /// ``` + pub fn into_normalized(mut self) -> Self { + self.normalize(); + self + } + + pub(crate) fn set_path

(&mut self, path: P) + where P: Into> + { + self.path = Data::new(path.into()); + } + + /// Returns the conrete path and query. + pub(crate) fn with_query_fragment_of(mut self, other: Reference<'a>) -> Self { + if let Some(query) = other.query { + if self.query().is_none() { + self.query = Some(Data::new(query.value.into_concrete(&self.source))); + } + } + + if let Some(frag) = other.fragment { + if self.fragment().is_none() { + self.fragment = Some(IndexedStr::from(frag.into_concrete(&self.source))); + } + } + + self + } +} + +impl PartialEq> for Reference<'_> { + fn eq(&self, other: &Reference<'_>) -> bool { + self.scheme() == other.scheme() + && self.authority() == other.authority() + && self.path() == other.path() + && self.query() == other.query() + && self.fragment() == other.fragment() + } +} + +impl IntoOwned for Reference<'_> { + type Owned = Reference<'static>; + + fn into_owned(self) -> Self::Owned { + Reference { + source: self.source.into_owned(), + scheme: self.scheme.into_owned(), + authority: self.authority.into_owned(), + path: self.path.into_owned(), + query: self.query.into_owned(), + fragment: self.fragment.into_owned(), + } + } +} + +impl<'a> From> for Reference<'a> { + fn from(absolute: Absolute<'a>) -> Self { + Reference { + source: absolute.source, + scheme: Some(absolute.scheme), + authority: absolute.authority, + path: absolute.path, + query: absolute.query, + fragment: None, + } + } +} + +impl<'a> From> for Reference<'a> { + fn from(origin: Origin<'a>) -> Self { + Reference { + source: origin.source, + scheme: None, + authority: None, + path: origin.path, + query: origin.query, + fragment: None, + } + } +} + +impl<'a> From> for Reference<'a> { + fn from(authority: Authority<'a>) -> Self { + Reference { + source: match authority.source { + Some(Cow::Borrowed(b)) => Some(Cow::Borrowed(b)), + _ => None + }, + authority: Some(authority), + scheme: None, + path: Data::new(""), + query: None, + fragment: None, + } + } +} + +impl From for Reference<'_> { + fn from(_: Asterisk) -> Self { + Reference { + source: None, + authority: None, + scheme: None, + path: Data::new("*"), + query: None, + fragment: None, + } + } +} + +impl<'a> TryFrom<&'a str> for Reference<'a> { + type Error = Error<'a>; + + fn try_from(value: &'a str) -> Result { + Reference::parse(value) + } +} + +impl TryFrom for Reference<'static> { + type Error = Error<'static>; + + fn try_from(value: String) -> Result { + Reference::parse_owned(value) + } +} + +// Because inference doesn't take `&String` to `&str`. +impl<'a> TryFrom<&'a String> for Reference<'a> { + type Error = Error<'a>; + + fn try_from(value: &'a String) -> Result { + Reference::parse(value.as_str()) + } +} + +impl std::fmt::Display for Reference<'_> { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + if let Some(scheme) = self.scheme() { + write!(f, "{}:", scheme)?; + } + + if let Some(authority) = self.authority() { + write!(f, "//{}", authority)?; + } + + write!(f, "{}", self.path())?; + + if let Some(query) = self.query() { + write!(f, "?{}", query)?; + } + + if let Some(frag) = self.fragment() { + write!(f, "#{}", frag)?; + } + + Ok(()) + } +} diff --git a/core/http/src/uri/segments.rs b/core/http/src/uri/segments.rs index cab8148b..f5402089 100644 --- a/core/http/src/uri/segments.rs +++ b/core/http/src/uri/segments.rs @@ -1,11 +1,15 @@ use std::path::PathBuf; use crate::RawStr; -use crate::parse::IndexedStr; +use crate::uri::fmt::{Part, Path, Query}; +use crate::uri::error::PathError; -/// Iterator over the non-empty, percent-decoded segments of a URI path. +/// Iterator over the non-empty, percent-decoded segments of a URI component. /// -/// Returned by [`Origin::path_segments()`]. +/// Returned by [`Path::segments()`] and [`Query::segments()`]. +/// +/// [`Path::segments()`]: crate::uri::Path::segments() +/// [`Query::segments()`]: crate::uri::Query::segments() /// /// ### Examples /// @@ -14,7 +18,7 @@ use crate::parse::IndexedStr; /// use rocket::http::uri::Origin; /// /// let uri = Origin::parse("/a%20z/////b/c////////d").unwrap(); -/// let segments = uri.path_segments(); +/// let segments = uri.path().segments(); /// for (i, segment) in segments.enumerate() { /// match i { /// 0 => assert_eq!(segment, "a z"), @@ -24,31 +28,44 @@ use crate::parse::IndexedStr; /// _ => panic!("only four segments") /// } /// } -/// # assert_eq!(uri.path_segments().len(), 4); -/// # assert_eq!(uri.path_segments().count(), 4); -/// # assert_eq!(uri.path_segments().next(), Some("a z")); +/// # assert_eq!(uri.path().segments().len(), 4); +/// # assert_eq!(uri.path().segments().count(), 4); +/// # assert_eq!(uri.path().segments().next(), Some("a z")); /// ``` #[derive(Debug, Clone, Copy)] -pub struct Segments<'o> { - pub(super) source: &'o RawStr, - pub(super) segments: &'o [IndexedStr<'static>], +pub struct Segments<'a, P: Part> { + pub(super) source: &'a RawStr, + pub(super) segments: &'a [P::Raw], pub(super) pos: usize, } -/// An error interpreting a segment as a [`PathBuf`] component in -/// [`Segments::to_path_buf()`]. -#[derive(Debug, PartialEq, Eq, Clone)] -pub enum PathError { - /// 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), -} +impl Segments<'_, P> { + #[doc(hidden)] + #[inline(always)] + pub fn new<'a>(source: &'a RawStr, segments: &'a [P::Raw]) -> Segments<'a, P> { + Segments { source, segments, pos: 0, } + } -impl<'o> Segments<'o> { /// Returns the number of path segments left. + /// + /// # Example + /// + /// ```rust + /// # #[macro_use] extern crate rocket; + /// let uri = uri!("/foo/bar?baz&cat&car"); + /// + /// let mut segments = uri.path().segments(); + /// assert_eq!(segments.len(), 2); + /// + /// segments.next(); + /// assert_eq!(segments.len(), 1); + /// + /// segments.next(); + /// assert_eq!(segments.len(), 0); + /// + /// segments.next(); + /// assert_eq!(segments.len(), 0); + /// ``` #[inline] pub fn len(&self) -> usize { let max_pos = std::cmp::min(self.pos, self.segments.len()); @@ -56,28 +73,88 @@ impl<'o> Segments<'o> { } /// Returns `true` if there are no segments left. + /// + /// # Example + /// + /// ```rust + /// # #[macro_use] extern crate rocket; + /// let uri = uri!("/foo/bar?baz&cat&car"); + /// + /// let mut segments = uri.path().segments(); + /// assert!(!segments.is_empty()); + /// + /// segments.next(); + /// segments.next(); + /// assert!(segments.is_empty()); + /// ``` #[inline] pub fn is_empty(&self) -> bool { self.len() == 0 } - /// Skips `n` segments. + /// Returns a new `Segments` with `n` segments skipped. + /// + /// # Example + /// + /// ```rust + /// # #[macro_use] extern crate rocket; + /// let uri = uri!("/foo/bar/baz/cat"); + /// + /// let mut segments = uri.path().segments(); + /// assert_eq!(segments.len(), 4); + /// assert_eq!(segments.next(), Some("foo")); + /// + /// let mut segments = segments.skip(2); + /// assert_eq!(segments.len(), 1); + /// assert_eq!(segments.next(), Some("cat")); + /// ``` #[inline] pub fn skip(mut self, n: usize) -> Self { self.pos = std::cmp::min(self.pos + n, self.segments.len()); self } +} - /// Get the `n`th segment from the current position. +impl<'a> Segments<'a, Path> { + /// Returns the `n`th segment, 0-indexed, from the current position. + /// + /// # Example + /// + /// ```rust + /// # #[macro_use] extern crate rocket; + /// let uri = uri!("/foo/bar/baaaz/cat"); + /// + /// let segments = uri.path().segments(); + /// assert_eq!(segments.get(0), Some("foo")); + /// assert_eq!(segments.get(1), Some("bar")); + /// assert_eq!(segments.get(2), Some("baaaz")); + /// assert_eq!(segments.get(3), Some("cat")); + /// assert_eq!(segments.get(4), None); + /// ``` #[inline] - pub fn get(&self, n: usize) -> Option<&'o str> { + pub fn get(&self, n: usize) -> Option<&'a str> { self.segments.get(self.pos + n) .map(|i| i.from_source(Some(self.source.as_str()))) } /// Returns `true` if `self` is a prefix of `other`. + /// + /// # Example + /// + /// ```rust + /// # #[macro_use] extern crate rocket; + /// let a = uri!("/foo/bar/baaaz/cat"); + /// let b = uri!("/foo/bar"); + /// + /// assert!(b.path().segments().prefix_of(a.path().segments())); + /// assert!(!a.path().segments().prefix_of(b.path().segments())); + /// + /// let a = uri!("/foo/bar/baaaz/cat"); + /// let b = uri!("/dog/foo/bar"); + /// assert!(b.path().segments().skip(1).prefix_of(a.path().segments())); + /// ``` #[inline] - pub fn prefix_of<'b>(self, other: Segments<'b>) -> bool { + pub fn prefix_of<'b>(self, other: Segments<'b, Path>) -> bool { if self.len() > other.len() { return false; } @@ -86,17 +163,17 @@ impl<'o> Segments<'o> { } /// Creates a `PathBuf` from `self`. The returned `PathBuf` is - /// percent-decoded. If a segment is equal to "..", the previous segment (if + /// percent-decoded. If a segment is equal to `..`, the previous segment (if /// any) is skipped. /// /// For security purposes, if a segment meets any of the following /// conditions, an `Err` is returned indicating the condition met: /// - /// * Decoded segment starts with any of: '*' + /// * Decoded segment starts with any of: `*` /// * Decoded segment ends with any of: `:`, `>`, `<` /// * Decoded segment contains any of: `/` /// * On Windows, decoded segment contains any of: `\` - /// * Percent-encoding results in invalid UTF8. + /// * Percent-encoding results in invalid UTF-8. /// /// Additionally, if `allow_dotfiles` is `false`, an `Err` is returned if /// the following condition is met: @@ -106,6 +183,21 @@ impl<'o> Segments<'o> { /// As a result of these conditions, a `PathBuf` derived via `FromSegments` /// is safe to interpolate within, or use as a suffix of, a path without /// additional checks. + /// + /// # Example + /// + /// ```rust + /// # #[macro_use] extern crate rocket; + /// use std::path::Path; + /// + /// let uri = uri!("/a/b/c/d/.pass"); + /// + /// let path = uri.path().segments().to_path_buf(true); + /// assert_eq!(path.unwrap(), Path::new("a/b/c/d/.pass")); + /// + /// let path = uri.path().segments().to_path_buf(false); + /// assert!(path.is_err()); + /// ``` pub fn to_path_buf(&self, allow_dotfiles: bool) -> Result { let mut buf = PathBuf::new(); for segment in self.clone() { @@ -130,74 +222,63 @@ impl<'o> Segments<'o> { } } + // TODO: Should we check the filename against the list in `FileName`? + // That list is mostly for writing, while this is likely to be read. + // TODO: Add an option to allow/disallow shell characters? + Ok(buf) } } -impl<'o> Iterator for Segments<'o> { - type Item = &'o str; - - fn next(&mut self) -> Option { - let item = self.get(0)?; - self.pos += 1; - Some(item) - } - - fn size_hint(&self) -> (usize, Option) { - (self.len(), Some(self.len())) - } - - fn count(self) -> usize { - self.len() - } -} - -/// Decoded query segments iterator. -#[derive(Debug, Clone, Copy)] -pub struct QuerySegments<'o> { - pub(super) source: Option<&'o RawStr>, - pub(super) segments: &'o [(IndexedStr<'static>, IndexedStr<'static>)], - pub(super) pos: usize, -} - -impl<'o> QuerySegments<'o> { - /// Returns the number of query segments left. - pub fn len(&self) -> usize { - let max_pos = std::cmp::min(self.pos, self.segments.len()); - self.segments.len() - max_pos - } - - /// Skip `n` segments. - pub fn skip(mut self, n: usize) -> Self { - self.pos = std::cmp::min(self.pos + n, self.segments.len()); - self - } - - /// Get the `n`th segment from the current position. +impl<'a> Segments<'a, Query> { + /// Returns the `n`th segment, 0-indexed, from the current position. + /// + /// # Example + /// + /// ```rust + /// # #[macro_use] extern crate rocket; + /// let uri = uri!("/?foo=1&bar=hi+there&baaaz&cat=dog&=oh"); + /// + /// let segments = uri.query().unwrap().segments(); + /// assert_eq!(segments.get(0), Some(("foo", "1"))); + /// assert_eq!(segments.get(1), Some(("bar", "hi there"))); + /// assert_eq!(segments.get(2), Some(("baaaz", ""))); + /// assert_eq!(segments.get(3), Some(("cat", "dog"))); + /// assert_eq!(segments.get(4), Some(("", "oh"))); + /// assert_eq!(segments.get(5), None); + /// ``` #[inline] - pub fn get(&self, n: usize) -> Option<(&'o str, &'o str)> { + pub fn get(&self, n: usize) -> Option<(&'a str, &'a str)> { let (name, val) = self.segments.get(self.pos + n)?; - let source = self.source.map(|s| s.as_str()); + let source = Some(self.source.as_str()); let name = name.from_source(source); let val = val.from_source(source); Some((name, val)) } } -impl<'o> Iterator for QuerySegments<'o> { - type Item = (&'o str, &'o str); +macro_rules! impl_iterator { + ($T:ty => $I:ty) => ( + impl<'a> Iterator for Segments<'a, $T> { + type Item = $I; - fn next(&mut self) -> Option { - let item = self.get(0)?; - self.pos += 1; - Some(item) - } + fn next(&mut self) -> Option { + let item = self.get(0)?; + self.pos += 1; + Some(item) + } - fn size_hint(&self) -> (usize, Option) { - (self.len(), Some(self.len())) - } + fn size_hint(&self) -> (usize, Option) { + (self.len(), Some(self.len())) + } - fn count(self) -> usize { - self.len() - } + fn count(self) -> usize { + self.len() + } + } + + ) } + +impl_iterator!(Path => &'a str); +impl_iterator!(Query => (&'a str, &'a str)); diff --git a/core/http/src/uri/uri.rs b/core/http/src/uri/uri.rs index 421d4d2b..aeccf37f 100644 --- a/core/http/src/uri/uri.rs +++ b/core/http/src/uri/uri.rs @@ -1,14 +1,10 @@ use std::fmt::{self, Display}; -use std::convert::From; -use std::borrow::Cow; -use std::str::Utf8Error; use std::convert::TryFrom; +use std::borrow::Cow; -use crate::RawStr; use crate::ext::IntoOwned; -use crate::parse::Extent; -use crate::uri::{Origin, Authority, Absolute, Error}; -use crate::uri::encoding::{percent_encode, DEFAULT_ENCODE_SET}; +use crate::uri::{Origin, Authority, Absolute, Reference, Asterisk}; +use crate::uri::error::{Error, TryFromUriError}; /// An `enum` encapsulating any of the possible URI variants. /// @@ -16,15 +12,7 @@ use crate::uri::encoding::{percent_encode, DEFAULT_ENCODE_SET}; /// /// 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` (for instance, in [`Redirect`](rocket::response::Redirect) -/// methods). This means that you can provide any type `T` that implements -/// `TryInto`, or, equivalently, any type `U` for which `Uri` implements -/// `TryFrom` or `From`. These include `&str` and `String`, [`Origin`], -/// [`Authority`], and [`Absolute`]. +/// incoming requests accepred by Rocket contain URIs in origin-form. /// /// ## Parsing /// @@ -32,8 +20,7 @@ use crate::uri::encoding::{percent_encode, DEFAULT_ENCODE_SET}; /// compliant "request target" parser with limited liberties for real-world /// deviations. In particular, the parser deviates as follows: /// -/// * It accepts `%` characters without two trailing hex-digits unless it is -/// the only character in the URI. +/// * It accepts `%` characters without two trailing hex-digits. /// /// * It accepts the following additional unencoded characters in query parts, /// to match real-world browser behavior: @@ -46,63 +33,102 @@ use crate::uri::encoding::{percent_encode, DEFAULT_ENCODE_SET}; /// 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()`]. -/// -/// [`Origin`]: crate::uri::Origin -/// [`Authority`]: crate::uri::Authority -/// [`Absolute`]: crate::uri::Absolute -/// [`Uri::parse()`]: crate::uri::Uri::parse() -/// [`Uri::percent_encode()`]: crate::uri::Uri::percent_encode() -/// [`Uri::percent_decode()`]: crate::uri::Uri::percent_decode() -/// [`Uri::percent_decode_lossy()`]: crate::uri::Uri::percent_decode_lossy() #[derive(Debug, PartialEq, Clone)] pub enum Uri<'a> { + /// An asterisk: exactly `*`. + Asterisk(Asterisk), /// An origin URI. Origin(Origin<'a>), /// An authority URI. Authority(Authority<'a>), /// An absolute URI. Absolute(Absolute<'a>), - /// An asterisk: exactly `*`. - Asterisk, + /// A URI reference. + Reference(Reference<'a>), } impl<'a> Uri<'a> { - #[inline] - pub(crate) unsafe fn raw_absolute( - source: Cow<'a, [u8]>, - scheme: Extent<&'a [u8]>, - path: Extent<&'a [u8]>, - query: Option>, - ) -> Uri<'a> { - let origin = Origin::raw(source.clone(), path, query); - Uri::Absolute(Absolute::raw(source.clone(), scheme, None, Some(origin))) - } - - /// Parses the string `string` into a `Uri`. Parsing will never allocate. - /// Returns an `Error` if `string` is not a valid URI. + /// Parses the string `string` into a `Uri` of kind `T`. + /// + /// This is identical to `T::parse(string).map(Uri::from)`. + /// + /// `T` is typically one of [`Asterisk`], [`Origin`], [`Authority`], + /// [`Absolute`], or [`Reference`]. Parsing never allocates. Returns an + /// `Error` if `string` is not a valid URI of kind `T`. + /// + /// To perform an ambgiuous parse into _any_ valid URI type, use + /// [`Uri::parse_any()`]. /// /// # Example /// /// ```rust - /// # extern crate rocket; - /// use rocket::http::uri::Uri; + /// # #[macro_use] extern crate rocket; + /// use rocket::http::uri::{Uri, Origin}; /// /// // Parse a valid origin URI (note: in practice, use `Origin::parse()`). - /// let uri = Uri::parse("/a/b/c?query").expect("valid URI"); + /// 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().unwrap(), "query"); /// + /// // Prefer to use the `uri!()` macro for static inputs. The return value + /// // is of the specific type, not `Uri`. + /// let origin = uri!("/a/b/c?query"); + /// assert_eq!(origin.path(), "/a/b/c"); + /// assert_eq!(origin.query().unwrap(), "query"); + /// /// // Invalid URIs fail to parse. - /// Uri::parse("foo bar").expect_err("invalid URI"); + /// Uri::parse::("foo bar").expect_err("invalid URI"); /// ``` - pub fn parse(string: &'a str) -> Result, Error<'_>> { + pub fn parse(string: &'a str) -> Result, Error<'_>> + where T: Into> + TryFrom<&'a str, Error = Error<'a>> + { + T::try_from(string).map(|v| v.into()) + } + + /// Parse `string` into a the "best fit" URI type. + /// + /// Always prefer to use `uri!()` for statically known inputs. + /// + /// Because URI parsing is ambgious (that is, there isn't a one-to-one + /// mapping between strings and a URI type), the internal type returned by + /// this method _may_ not be the desired type. This method chooses the "best + /// fit" type for a given string by preferring to parse in the following + /// order: + /// + /// * `Asterisk` + /// * `Origin` + /// * `Authority` + /// * `Absolute` + /// * `Reference` + /// + /// Thus, even though `*` is a valid `Asterisk` _and_ `Reference` URI, it + /// will parse as an `Asterisk`. + /// + /// # Example + /// + /// ```rust + /// # #[macro_use] extern crate rocket; + /// use rocket::http::uri::{Uri, Origin, Reference}; + /// + /// // An absolute path is an origin _unless_ it contains a fragment. + /// let uri = Uri::parse_any("/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().unwrap(), "query"); + /// + /// let uri = Uri::parse_any("/a/b/c?query#fragment").expect("valid URI"); + /// let reference = uri.reference().expect("reference URI"); + /// assert_eq!(reference.path(), "/a/b/c"); + /// assert_eq!(reference.query().unwrap(), "query"); + /// assert_eq!(reference.fragment().unwrap(), "fragment"); + /// + /// // Prefer to use the `uri!()` macro for static inputs. The return type + /// // is the internal type, not `Uri`. The explicit type is not required. + /// let uri: Origin = uri!("/a/b/c?query"); + /// let uri: Reference = uri!("/a/b/c?query#fragment"); + /// ``` + pub fn parse_any(string: &'a str) -> Result, Error<'_>> { crate::parse::uri::from_str(string) } @@ -112,13 +138,19 @@ impl<'a> Uri<'a> { /// # Example /// /// ```rust - /// # extern crate rocket; - /// use rocket::http::uri::Uri; + /// # #[macro_use] extern crate rocket; + /// use rocket::http::uri::{Uri, Absolute, Origin}; /// - /// let uri = Uri::parse("/a/b/c?query").expect("valid URI"); + /// let uri = Uri::parse::("/a/b/c?query").expect("valid URI"); /// assert!(uri.origin().is_some()); /// - /// let uri = Uri::parse("http://google.com").expect("valid URI"); + /// let uri = Uri::from(uri!("/a/b/c?query")); + /// assert!(uri.origin().is_some()); + /// + /// let uri = Uri::parse::("https://rocket.rs").expect("valid URI"); + /// assert!(uri.origin().is_none()); + /// + /// let uri = Uri::from(uri!("https://rocket.rs")); /// assert!(uri.origin().is_none()); /// ``` pub fn origin(&self) -> Option<&Origin<'a>> { @@ -134,13 +166,19 @@ impl<'a> Uri<'a> { /// # Example /// /// ```rust - /// # extern crate rocket; - /// use rocket::http::uri::Uri; + /// # #[macro_use] extern crate rocket; + /// use rocket::http::uri::{Uri, Absolute, Authority}; /// - /// let uri = Uri::parse("user:pass@domain.com").expect("valid URI"); + /// let uri = Uri::parse::("user:pass@domain.com").expect("valid URI"); /// assert!(uri.authority().is_some()); /// - /// let uri = Uri::parse("http://google.com").expect("valid URI"); + /// let uri = Uri::from(uri!("user:pass@domain.com")); + /// assert!(uri.authority().is_some()); + /// + /// let uri = Uri::parse::("https://rocket.rs").expect("valid URI"); + /// assert!(uri.authority().is_none()); + /// + /// let uri = Uri::from(uri!("https://rocket.rs")); /// assert!(uri.authority().is_none()); /// ``` pub fn authority(&self) -> Option<&Authority<'a>> { @@ -156,13 +194,19 @@ impl<'a> Uri<'a> { /// # Example /// /// ```rust - /// # extern crate rocket; - /// use rocket::http::uri::Uri; + /// # #[macro_use] extern crate rocket; + /// use rocket::http::uri::{Uri, Absolute, Origin}; /// - /// let uri = Uri::parse("http://google.com").expect("valid URI"); + /// let uri = Uri::parse::("http://rocket.rs").expect("valid URI"); /// assert!(uri.absolute().is_some()); /// - /// let uri = Uri::parse("/path").expect("valid URI"); + /// let uri = Uri::from(uri!("http://rocket.rs")); + /// assert!(uri.absolute().is_some()); + /// + /// let uri = Uri::parse::("/path").expect("valid URI"); + /// assert!(uri.absolute().is_none()); + /// + /// let uri = Uri::from(uri!("/path")); /// assert!(uri.absolute().is_none()); /// ``` pub fn absolute(&self) -> Option<&Absolute<'a>> { @@ -172,61 +216,32 @@ impl<'a> Uri<'a> { } } - /// Returns a URL-encoded version of the string. Any reserved characters are - /// percent-encoded. + /// Returns the internal instance of `Reference` if `self` is a + /// `Uri::Reference`. Otherwise, returns `None`. /// - /// # Examples + /// # Example /// /// ```rust - /// # extern crate rocket; - /// use rocket::http::uri::Uri; + /// # #[macro_use] extern crate rocket; + /// use rocket::http::uri::{Uri, Absolute, Reference}; /// - /// let encoded = Uri::percent_encode("hello?a=hi"); - /// assert_eq!(encoded, "hello%3Fa%3D%3Cb%3Ehi%3C%2Fb%3E"); + /// let uri = Uri::parse::("foo/bar").expect("valid URI"); + /// assert!(uri.reference().is_some()); + /// + /// let uri = Uri::from(uri!("foo/bar")); + /// assert!(uri.reference().is_some()); + /// + /// let uri = Uri::parse::("https://rocket.rs").expect("valid URI"); + /// assert!(uri.reference().is_none()); + /// + /// let uri = Uri::from(uri!("https://rocket.rs")); + /// assert!(uri.reference().is_none()); /// ``` - pub fn percent_encode(string: &S) -> Cow<'_, str> - where S: AsRef + ?Sized - { - percent_encode::(RawStr::new(string)) - } - - /// Returns a URL-decoded version of the string. If the percent encoded - /// values are not valid UTF-8, an `Err` is returned. - /// - /// # Examples - /// - /// ```rust - /// # extern crate rocket; - /// use rocket::http::uri::Uri; - /// - /// let decoded = Uri::percent_decode("/Hello%2C%20world%21".as_bytes()); - /// assert_eq!(decoded.unwrap(), "/Hello, world!"); - /// ``` - pub fn percent_decode(bytes: &S) -> Result, Utf8Error> - where S: AsRef<[u8]> + ?Sized - { - let decoder = percent_encoding::percent_decode(bytes.as_ref()); - decoder.decode_utf8() - } - - /// Returns a URL-decoded version of the path. Any invalid UTF-8 - /// percent-encoded byte sequences will be replaced � U+FFFD, the - /// replacement character. - /// - /// # Examples - /// - /// ```rust - /// # extern crate rocket; - /// use rocket::http::uri::Uri; - /// - /// let decoded = Uri::percent_decode_lossy("/Hello%2C%20world%21".as_bytes()); - /// assert_eq!(decoded, "/Hello, world!"); - /// ``` - pub fn percent_decode_lossy(bytes: &S) -> Cow<'_, str> - where S: AsRef<[u8]> + ?Sized - { - let decoder = percent_encoding::percent_decode(bytes.as_ref()); - decoder.decode_utf8_lossy() + pub fn reference(&self) -> Option<&Reference<'a>> { + match self { + Uri::Reference(ref inner) => Some(inner), + _ => None + } } } @@ -237,26 +252,26 @@ pub(crate) unsafe fn as_utf8_unchecked(input: Cow<'_, [u8]>) -> Cow<'_, str> { } } -impl<'a> TryFrom<&'a str> for Uri<'a> { - type Error = Error<'a>; - - #[inline] - fn try_from(string: &'a str) -> Result, Self::Error> { - Uri::parse(string) - } -} - -impl TryFrom for Uri<'static> { - type Error = Error<'static>; - - #[inline] - fn try_from(string: String) -> Result, 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> TryFrom<&'a str> for Uri<'a> { +// type Error = Error<'a>; +// +// #[inline] +// fn try_from(string: &'a str) -> Result, Self::Error> { +// Uri::parse(string) +// } +// } +// +// impl TryFrom for Uri<'static> { +// type Error = Error<'static>; +// +// #[inline] +// fn try_from(string: String) -> Result, 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 IntoOwned for Uri<'_> { type Owned = Uri<'static>; @@ -266,7 +281,8 @@ impl IntoOwned for Uri<'_> { 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 + Uri::Reference(reference) => Uri::Reference(reference.into_owned()), + Uri::Asterisk(asterisk) => Uri::Asterisk(asterisk) } } } @@ -277,42 +293,53 @@ impl Display for Uri<'_> { 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, "*") + Uri::Reference(ref reference) => write!(f, "{}", reference), + Uri::Asterisk(ref asterisk) => write!(f, "{}", asterisk) } } } -/// The error type returned when a URI conversion fails. -#[derive(Debug, Copy, Clone, PartialEq, Eq)] -pub struct TryFromUriError(()); - -impl fmt::Display for TryFromUriError { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - "invalid conversion from general to specific URI variant".fmt(f) - } -} - 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) + ($T:ident $(<$lt:lifetime>)?) => ( + impl<'a> From<$T $(<$lt>)?> for Uri<'a> { + fn from(other: $T $(<$lt>)?) -> Uri<'a> { + Uri::$T(other) } } - impl<'a> TryFrom> for $type<'a> { + impl<'a> TryFrom> for $T $(<$lt>)? { type Error = TryFromUriError; fn try_from(uri: Uri<'a>) -> Result { match uri { - Uri::$type(inner) => Ok(inner), + Uri::$T(inner) => Ok(inner), _ => Err(TryFromUriError(())) } } } + + impl<'b, $($lt)?> PartialEq<$T $(<$lt>)?> for Uri<'b> { + fn eq(&self, other: &$T $(<$lt>)?) -> bool { + match self { + Uri::$T(inner) => inner == other, + _ => false + } + } + } + + impl<'b, $($lt)?> PartialEq> for $T $(<$lt>)? { + fn eq(&self, other: &Uri<'b>) -> bool { + match other { + Uri::$T(inner) => inner == self, + _ => false + } + } + } ) } -impl_uri_from!(Origin); -impl_uri_from!(Authority); -impl_uri_from!(Absolute); +impl_uri_from!(Origin<'a>); +impl_uri_from!(Authority<'a>); +impl_uri_from!(Absolute<'a>); +impl_uri_from!(Reference<'a>); +impl_uri_from!(Asterisk); diff --git a/core/lib/src/catcher/catcher.rs b/core/lib/src/catcher/catcher.rs index af90dba9..90b45f21 100644 --- a/core/lib/src/catcher/catcher.rs +++ b/core/lib/src/catcher/catcher.rs @@ -164,7 +164,7 @@ impl Catcher { Catcher { name: None, - base: uri::Origin::new("/", None::<&str>), + base: uri::Origin::ROOT, handler: Box::new(handler), code, } diff --git a/core/lib/src/form/lenient.rs b/core/lib/src/form/lenient.rs index 3dbfb951..e5397629 100644 --- a/core/lib/src/form/lenient.rs +++ b/core/lib/src/form/lenient.rs @@ -1,7 +1,7 @@ use std::ops::{Deref, DerefMut}; use crate::form::prelude::*; -use crate::http::uri::{Query, FromUriParam}; +use crate::http::uri::fmt::{Query, FromUriParam}; /// A form guard for parsing form types leniently. /// diff --git a/core/lib/src/form/name/file_name.rs b/core/lib/src/form/name/file_name.rs index bcbf9319..8fc61ea9 100644 --- a/core/lib/src/form/name/file_name.rs +++ b/core/lib/src/form/name/file_name.rs @@ -58,7 +58,7 @@ impl FileName { /// '(', ')', '&', ';', '#', '?', '*'`. /// /// On Windows (and non-Unix OSs), these are the characters `'.', '<', '>', - /// ':', '"', '/', '\\', '|', '?', '*', ',', ';', '=', '(', ')', '&', '#'`, + /// ':', '"', '/', '\', '|', '?', '*', ',', ';', '=', '(', ')', '&', '#'`, /// and the reserved names `"CON", "PRN", "AUX", "NUL", "COM1", "COM2", /// "COM3", "COM4", "COM5", "COM6", "COM7", "COM8", "COM9", "LPT1", "LPT2", /// "LPT3", "LPT4", "LPT5", "LPT6", "LPT7", "LPT8", "LPT9"`. @@ -164,6 +164,33 @@ impl FileName { Some(file_name) } + /// Returns `true` if the _complete_ raw file name is safe. + /// + /// Note that `.as_str()` returns a safe _subset_ of the raw file name, if + /// there is one. If this method returns `true`, then that subset is the + /// complete raw file name. + /// + /// This method should be use sparingly. In particular, there is no + /// advantage to calling `is_safe()` prior to calling `as_str()`; simply + /// call `as_str()`. + /// + /// # Example + /// + /// ```rust + /// use rocket::form::name::FileName; + /// + /// let name = FileName::new("some-file.txt"); + /// assert_eq!(name.as_str(), Some("some-file")); + /// assert!(!name.is_safe()); + /// + /// let name = FileName::new("some-file"); + /// assert_eq!(name.as_str(), Some("some-file")); + /// assert!(name.is_safe()); + /// ``` + pub fn is_safe(&self) -> bool { + self.as_str().map_or(false, |s| s == &self.0) + } + /// The raw, unsanitized, potentially unsafe file name. Prefer to use /// [`FileName::as_str()`], always. /// @@ -171,8 +198,8 @@ impl FileName { /// /// This method returns the file name exactly as it was specified by the /// client. You should **_not_** use this name _unless_ you require the - /// originally specified `filename` _and_ it is known to contain special, - /// potentially dangerous characters, _and_: + /// originally specified `filename` _and_ it is known not to contain + /// special, potentially dangerous characters, _and_: /// /// 1. All clients are known to be trusted, perhaps because the server /// only runs locally, serving known, local requests, or... diff --git a/core/lib/src/form/strict.rs b/core/lib/src/form/strict.rs index cfc942a4..ddd80c8d 100644 --- a/core/lib/src/form/strict.rs +++ b/core/lib/src/form/strict.rs @@ -1,7 +1,7 @@ use std::ops::{Deref, DerefMut}; use crate::form::prelude::*; -use crate::http::uri::{Query, FromUriParam}; +use crate::http::uri::fmt::{Query, FromUriParam}; /// A form guard for parsing form types strictly. /// diff --git a/core/lib/src/local/asynchronous/request.rs b/core/lib/src/local/asynchronous/request.rs index e30b5c58..0a71c997 100644 --- a/core/lib/src/local/asynchronous/request.rs +++ b/core/lib/src/local/asynchronous/request.rs @@ -45,8 +45,7 @@ impl<'c> LocalRequest<'c> { { // Try to parse `uri` into an `Origin`, storing whether it's good. let uri_str = uri.to_string(); - let try_origin = uri.try_into() - .map_err(|_| Origin::new::<_, &'static str>(uri_str, None)); + let try_origin = uri.try_into().map_err(|_| Origin::path_only(uri_str)); // Create a request. We'll handle bad URIs later, in `_dispatch`. let origin = try_origin.clone().unwrap_or_else(|bad| bad); diff --git a/core/lib/src/request/from_param.rs b/core/lib/src/request/from_param.rs index d7f4b573..3cd8fc55 100644 --- a/core/lib/src/request/from_param.rs +++ b/core/lib/src/request/from_param.rs @@ -1,7 +1,7 @@ use std::str::FromStr; use std::path::PathBuf; -use crate::http::uri::{Segments, PathError}; +use crate::http::uri::{Segments, error::PathError, fmt::Path}; /// Trait to convert a dynamic path segment string to a concrete value. /// @@ -228,6 +228,18 @@ impl_with_fromstr! { bool, IpAddr, Ipv4Addr, Ipv6Addr, SocketAddrV4, SocketAddrV6, SocketAddr } +impl<'a> FromParam<'a> for PathBuf { + type Error = PathError; + + #[inline] + fn from_param(param: &'a str) -> Result { + use crate::http::private::Indexed; + + let segments = &[Indexed::Indexed(0, param.len())]; + Segments::new(param.into(), segments).to_path_buf(false) + } +} + impl<'a, T: FromParam<'a>> FromParam<'a> for Result { type Error = std::convert::Infallible; @@ -276,14 +288,14 @@ pub trait FromSegments<'r>: Sized { /// Parses an instance of `Self` from many dynamic path parameter strings or /// returns an `Error` if one cannot be parsed. - fn from_segments(segments: Segments<'r>) -> Result; + fn from_segments(segments: Segments<'r, Path>) -> Result; } -impl<'r> FromSegments<'r> for Segments<'r> { +impl<'r> FromSegments<'r> for Segments<'r, Path> { type Error = std::convert::Infallible; #[inline(always)] - fn from_segments(segments: Segments<'r>) -> Result, Self::Error> { + fn from_segments(segments: Self) -> Result { Ok(segments) } } @@ -307,7 +319,7 @@ impl<'r> FromSegments<'r> for Segments<'r> { impl FromSegments<'_> for PathBuf { type Error = PathError; - fn from_segments(segments: Segments<'_>) -> Result { + fn from_segments(segments: Segments<'_, Path>) -> Result { segments.to_path_buf(false) } } @@ -316,7 +328,7 @@ impl<'r, T: FromSegments<'r>> FromSegments<'r> for Result { type Error = std::convert::Infallible; #[inline] - fn from_segments(segments: Segments<'r>) -> Result, Self::Error> { + fn from_segments(segments: Segments<'r, Path>) -> Result, Self::Error> { match T::from_segments(segments) { Ok(val) => Ok(Ok(val)), Err(e) => Ok(Err(e)), @@ -328,7 +340,7 @@ impl<'r, T: FromSegments<'r>> FromSegments<'r> for Option { type Error = std::convert::Infallible; #[inline] - fn from_segments(segments: Segments<'r>) -> Result, Self::Error> { + fn from_segments(segments: Segments<'r, Path>) -> Result, Self::Error> { match T::from_segments(segments) { Ok(val) => Ok(Some(val)), Err(_) => Ok(None) diff --git a/core/lib/src/request/request.rs b/core/lib/src/request/request.rs index 8424f63d..f0ce039c 100644 --- a/core/lib/src/request/request.rs +++ b/core/lib/src/request/request.rs @@ -1,8 +1,7 @@ -use std::{ops::RangeFrom, sync::Arc}; -use std::net::{IpAddr, SocketAddr}; -use std::future::Future; use std::fmt; -use std::str; +use std::ops::RangeFrom; +use std::{future::Future, borrow::Cow, sync::Arc}; +use std::net::{IpAddr, SocketAddr}; use yansi::Paint; use state::{Container, Storage}; @@ -14,7 +13,7 @@ use crate::request::{FromParam, FromSegments, FromRequest, Outcome}; use crate::form::{self, ValueField, FromForm}; use crate::{Rocket, Route, Orbit}; -use crate::http::{hyper, uri::{Origin, Segments}, uncased::UncasedStr}; +use crate::http::{hyper, uri::{Origin, Segments, fmt::Path}, uncased::UncasedStr}; use crate::http::{Method, Header, HeaderMap}; use crate::http::{ContentType, Accept, MediaType, CookieJar, Cookie}; use crate::data::Limits; @@ -794,18 +793,21 @@ impl<'r> Request<'r> { /// Get the segments beginning at the `n`th, 0-indexed, after the mount /// point for the currently matched route, if they exist. Used by codegen. #[inline] - pub fn routed_segments(&self, n: RangeFrom) -> Segments<'_> { + pub fn routed_segments(&self, n: RangeFrom) -> Segments<'_, Path> { let mount_segments = self.route() .map(|r| r.uri.metadata.base_segs.len()) .unwrap_or(0); - self.uri().path_segments().skip(mount_segments + n.start) + self.uri().path().segments().skip(mount_segments + n.start) } // Retrieves the pre-parsed query items. Used by matching and codegen. #[inline] pub fn query_fields(&self) -> impl Iterator> { - self.uri().query_segments().map(ValueField::from) + self.uri().query() + .map(|q| q.segments().map(ValueField::from)) + .into_iter() + .flatten() } /// Set `self`'s parameters given that the route used to reach this request @@ -836,7 +838,7 @@ impl<'r> Request<'r> { ) -> Result, Error<'r>> { // Get a copy of the URI (only supports path-and-query) for later use. let uri = match (h_uri.scheme(), h_uri.authority(), h_uri.path_and_query()) { - (None, None, Some(path_query)) => path_query.as_str(), + (None, None, Some(path_query)) => path_query, _ => return Err(Error::InvalidUri(h_uri)), }; @@ -846,8 +848,10 @@ impl<'r> Request<'r> { None => return Err(Error::BadMethod(h_method)) }; - // We need to re-parse the URI since we don't trust Hyper... :( - let uri = Origin::parse(uri)?; + // In debug, make sure we agree with Hyper. Otherwise, cross our fingers + // and trust that it only gives us valid URIs like it's supposed to. + debug_assert!(Origin::parse(uri.as_str()).is_ok()); + let uri = Origin::new(uri.path(), uri.query().map(Cow::Borrowed)); // Construct the request object. let mut request = Request::new(rocket, method, uri); diff --git a/core/lib/src/response/redirect.rs b/core/lib/src/response/redirect.rs index 49dbbc24..efd31d76 100644 --- a/core/lib/src/response/redirect.rs +++ b/core/lib/src/response/redirect.rs @@ -2,7 +2,7 @@ use std::convert::TryInto; use crate::request::Request; use crate::response::{self, Response, Responder}; -use crate::http::uri::Uri; +use crate::http::uri::Reference; use crate::http::Status; /// An empty redirect response to a given URL. @@ -11,19 +11,19 @@ use crate::http::Status; /// /// # Usage /// -/// All constructors accept a generic type of `T: TryInto>`. Among -/// the candidate types are: +/// All constructors accept a generic type of `T: TryInto>`. +/// Among the candidate types are: /// -/// * `String` -/// * `&'static str` +/// * `String`, `&'static str` /// * [`Origin`](crate::http::uri::Origin) /// * [`Authority`](crate::http::uri::Authority) /// * [`Absolute`](crate::http::uri::Absolute) -/// * [`Uri`](crate::http::uri::Uri) +/// * [`Reference`](crate::http::uri::Reference) /// /// Any non-`'static` strings must first be allocated using `.to_string()` or /// similar before being passed to a `Redirect` constructor. When redirecting to -/// a route, _always_ use [`uri!`] to construct a valid [`Origin`]: +/// a route, or any URI containing a route, _always_ use [`uri!`] to construct a +/// valid URI: /// /// ```rust /// # #[macro_use] extern crate rocket; @@ -36,14 +36,19 @@ use crate::http::Status; /// /// #[get("/hi//")] /// fn hi(name: String, age: u8) -> Redirect { -/// Redirect::to(uri!(hello: name, age)) +/// Redirect::to(uri!(hello(name, age))) +/// } +/// +/// #[get("/bye//")] +/// fn bye(name: String, age: u8) -> Redirect { +/// Redirect::to(uri!("https://rocket.rs/bye", hello(name, age), "?bye#now")) /// } /// ``` /// /// [`Origin`]: crate::http::uri::Origin /// [`uri!`]: ../macro.uri.html #[derive(Debug)] -pub struct Redirect(Status, Option>); +pub struct Redirect(Status, Option>); impl Redirect { /// Construct a temporary "see other" (303) redirect response. This is the @@ -54,34 +59,35 @@ impl Redirect { /// # Examples /// /// ```rust + /// # #[macro_use] extern crate rocket; /// use rocket::response::Redirect; /// - /// # let query = "foo"; - /// let redirect = Redirect::to("/other_url"); - /// let redirect = Redirect::to(format!("https://google.com/{}", query)); + /// let redirect = Redirect::to(uri!("/foo/bar")); + /// let redirect = Redirect::to(uri!("https://domain.com#foo")); /// ``` - pub fn to>>(uri: U) -> Redirect { + pub fn to>>(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; - /// - /// # let query = "foo"; - /// let redirect = Redirect::temporary("/other_url"); - /// let redirect = Redirect::temporary(format!("https://google.com/{}", query)); - /// ``` - pub fn temporary>>(uri: U) -> Redirect { - Redirect(Status::TemporaryRedirect, 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 + /// # #[macro_use] extern crate rocket; + /// use rocket::response::Redirect; + /// + /// let redirect = Redirect::temporary(uri!("some/other/path")); + /// let redirect = Redirect::temporary(uri!("https://rocket.rs?foo")); + /// let redirect = Redirect::temporary(format!("some-{}-thing", "crazy")); + /// ``` + pub fn temporary>>(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 @@ -93,13 +99,13 @@ impl Redirect { /// # Examples /// /// ```rust + /// # #[macro_use] extern crate rocket; /// use rocket::response::Redirect; /// - /// # let query = "foo"; - /// let redirect = Redirect::permanent("/other_url"); - /// let redirect = Redirect::permanent(format!("https://google.com/{}", query)); + /// let redirect = Redirect::permanent(uri!("/other_url")); + /// let redirect = Redirect::permanent(format!("some-{}-thing", "crazy")); /// ``` - pub fn permanent>>(uri: U) -> Redirect { + pub fn permanent>>(uri: U) -> Redirect { Redirect(Status::PermanentRedirect, uri.try_into().ok()) } @@ -113,13 +119,13 @@ impl Redirect { /// # Examples /// /// ```rust + /// # #[macro_use] extern crate rocket; /// use rocket::response::Redirect; /// - /// # let query = "foo"; - /// let redirect = Redirect::found("/other_url"); - /// let redirect = Redirect::found(format!("https://google.com/{}", query)); + /// let redirect = Redirect::found(uri!("/other_url")); + /// let redirect = Redirect::found(format!("some-{}-thing", "crazy")); /// ``` - pub fn found>>(uri: U) -> Redirect { + pub fn found>>(uri: U) -> Redirect { Redirect(Status::Found, uri.try_into().ok()) } @@ -131,13 +137,13 @@ impl Redirect { /// # Examples /// /// ```rust + /// # #[macro_use] extern crate rocket; /// use rocket::response::Redirect; /// - /// # let query = "foo"; - /// let redirect = Redirect::moved("/other_url"); - /// let redirect = Redirect::moved(format!("https://google.com/{}", query)); + /// let redirect = Redirect::moved(uri!("here")); + /// let redirect = Redirect::moved(format!("some-{}-thing", "crazy")); /// ``` - pub fn moved>>(uri: U) -> Redirect { + pub fn moved>>(uri: U) -> Redirect { Redirect(Status::MovedPermanently, uri.try_into().ok()) } } diff --git a/core/lib/src/rocket.rs b/core/lib/src/rocket.rs index 7bae39b2..ef7ed8b1 100644 --- a/core/lib/src/rocket.rs +++ b/core/lib/src/rocket.rs @@ -529,9 +529,9 @@ fn log_items(e: &str, t: &str, items: I, base: B, origin: O) } items.sort_by_key(|i| origin(i).path().as_str().chars().count()); - items.sort_by_key(|i| origin(i).path_segments().len()); + items.sort_by_key(|i| origin(i).path().segments().len()); items.sort_by_key(|i| base(i).path().as_str().chars().count()); - items.sort_by_key(|i| base(i).path_segments().len()); + items.sort_by_key(|i| base(i).path().segments().len()); items.iter().for_each(|i| launch_info_!("{}", i)); } diff --git a/core/lib/src/route/uri.rs b/core/lib/src/route/uri.rs index be6f647b..656f0f4c 100644 --- a/core/lib/src/route/uri.rs +++ b/core/lib/src/route/uri.rs @@ -171,7 +171,7 @@ impl<'a> RouteUri<'a> { self.origin.path().as_str() } - /// The query part of this route URI as an `Option<&str>`. + /// The query part of this route URI, if there is one. /// /// # Example /// @@ -180,10 +180,18 @@ impl<'a> RouteUri<'a> { /// use rocket::http::Method; /// # use rocket::route::dummy_handler as handler; /// + /// let index = Route::new(Method::Get, "/foo/bar", handler); + /// assert!(index.uri.query().is_none()); + /// + /// // Normalization clears the empty '?'. + /// let index = Route::new(Method::Get, "/foo/bar?", handler); + /// assert!(index.uri.query().is_none()); + /// /// let index = Route::new(Method::Get, "/foo/bar?a=1", handler); - /// assert_eq!(index.uri.query(), Some("a=1")); + /// assert_eq!(index.uri.query().unwrap(), "a=1"); + /// /// let index = index.map_base(|base| format!("{}{}", "/boo", base)).unwrap(); - /// assert_eq!(index.uri.query(), Some("a=1")); + /// assert_eq!(index.uri.query().unwrap(), "a=1"); /// ``` #[inline(always)] pub fn query(&self) -> Option<&str> { @@ -241,17 +249,17 @@ impl<'a> RouteUri<'a> { impl Metadata { fn from(base: &Origin<'_>, origin: &Origin<'_>) -> Self { - let base_segs = base.raw_path_segments() + let base_segs = base.path().raw_segments() .map(Segment::from) .collect::>(); - let path_segs = origin.raw_path_segments() + let path_segs = origin.path().raw_segments() .map(Segment::from) .collect::>(); - let query_segs = origin.raw_query_segments() - .map(Segment::from) - .collect::>(); + let query_segs = origin.query() + .map(|q| q.raw_segments().map(Segment::from).collect::>()) + .unwrap_or_default(); let static_query_fields = query_segs.iter().filter(|s| !s.dynamic) .map(|s| ValueField::parse(&s.value)) diff --git a/core/lib/src/router/collider.rs b/core/lib/src/router/collider.rs index 1b6d82b6..6a155578 100644 --- a/core/lib/src/router/collider.rs +++ b/core/lib/src/router/collider.rs @@ -106,7 +106,7 @@ impl Route { fn paths_match(route: &Route, req: &Request<'_>) -> bool { let route_segments = &route.uri.metadata.path_segs; - let req_segments = req.uri().path_segments(); + let req_segments = req.uri().path().segments(); if route.uri.metadata.trailing_path { // The last route segment can be trailing, which is allowed to be empty. @@ -145,8 +145,13 @@ fn queries_match(route: &Route, req: &Request<'_>) -> bool { .map(|(k, v)| (k.as_str(), v.as_str())); for route_seg in route_query_fields { - if !req.uri().query_segments().any(|req_seg| req_seg == route_seg) { - trace_!("request {} missing static query {:?}", req, route_seg); + if let Some(query) = req.uri().query() { + if !query.segments().any(|req_seg| req_seg == route_seg) { + trace_!("request {} missing static query {:?}", req, route_seg); + return false; + } + } else { + trace_!("query-less request {} missing static query {:?}", req, route_seg); return false; } } @@ -181,7 +186,7 @@ impl Collide for Catcher { /// * Have the same status code or are both defaults. fn collides_with(&self, other: &Self) -> bool { self.code == other.code - && self.base.path_segments().eq(other.base.path_segments()) + && self.base.path().segments().eq(other.base.path().segments()) } } @@ -193,7 +198,7 @@ impl Catcher { /// * Its base is a prefix of the normalized/decoded `req.path()`. pub(crate) fn matches(&self, status: Status, req: &Request<'_>) -> bool { self.code.map_or(true, |code| code == status.code) - && self.base.path_segments().prefix_of(req.uri().path_segments()) + && self.base.path().segments().prefix_of(req.uri().path().segments()) } } diff --git a/core/lib/src/router/router.rs b/core/lib/src/router/router.rs index 57d8c9d7..08408a4f 100644 --- a/core/lib/src/router/router.rs +++ b/core/lib/src/router/router.rs @@ -32,7 +32,7 @@ impl Router { pub fn add_catcher(&mut self, catcher: Catcher) { let catchers = self.catchers.entry(catcher.code).or_default(); catchers.push(catcher); - catchers.sort_by(|a, b| b.base.path_segments().len().cmp(&a.base.path_segments().len())) + catchers.sort_by(|a, b| b.base.path().segments().len().cmp(&a.base.path().segments().len())) } #[inline] @@ -68,7 +68,7 @@ impl Router { (None, None) => None, (None, c@Some(_)) | (c@Some(_), None) => c, (Some(a), Some(b)) => { - if b.base.path_segments().len() > a.base.path_segments().len() { + if b.base.path().segments().len() > a.base.path().segments().len() { Some(b) } else { Some(a) diff --git a/core/lib/src/server.rs b/core/lib/src/server.rs index bd42f4f6..3dd6820c 100644 --- a/core/lib/src/server.rs +++ b/core/lib/src/server.rs @@ -87,7 +87,7 @@ async fn hyper_service_fn( // 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(&rocket, Method::Get, Origin::dummy()); + let dummy = Request::new(&rocket, Method::Get, Origin::ROOT); let r = rocket.handle_error(Status::BadRequest, &dummy).await; return rocket.send_response(r, tx).await; } diff --git a/core/lib/tests/absolute-uris-okay-issue-443.rs b/core/lib/tests/absolute-uris-okay-issue-443.rs index 56b348b8..c526a9a4 100644 --- a/core/lib/tests/absolute-uris-okay-issue-443.rs +++ b/core/lib/tests/absolute-uris-okay-issue-443.rs @@ -2,9 +2,9 @@ use rocket::response::Redirect; -#[get("/google")] -fn google() -> Redirect { - Redirect::to("https://www.google.com") +#[get("/http")] +fn http() -> Redirect { + Redirect::to(uri!("http://rocket.rs")) } #[get("/rocket")] @@ -18,13 +18,13 @@ mod test_absolute_uris_okay { #[test] fn redirect_works() { - let client = Client::debug_with(routes![google, redirect]).unwrap(); + let client = Client::debug_with(routes![http, redirect]).unwrap(); - let response = client.get("/google").dispatch(); + let response = client.get(uri!(http)).dispatch(); let location = response.headers().get_one("Location"); - assert_eq!(location, Some("https://www.google.com")); + assert_eq!(location, Some("http://rocket.rs")); - let response = client.get("/rocket").dispatch(); + let response = client.get(uri!(redirect)).dispatch(); let location = response.headers().get_one("Location"); assert_eq!(location, Some("https://rocket.rs:80")); } diff --git a/core/lib/tests/scoped-uri.rs b/core/lib/tests/scoped-uri.rs index 97ab7ead..0df913b0 100644 --- a/core/lib/tests/scoped-uri.rs +++ b/core/lib/tests/scoped-uri.rs @@ -6,13 +6,13 @@ mod inner { #[rocket::get("/")] pub fn hello() -> String { - format!("Hello! Try {}.", uri!(super::hello_name: "Rust 2018")) + format!("Hello! Try {}.", uri!(super::hello_name("Rust 2018"))) } } #[rocket::get("/")] fn hello_name(name: String) -> String { - format!("Hello, {}! This is {}.", name, rocket::uri!(hello_name: &name)) + format!("Hello, {}! This is {}.", name, rocket::uri!(hello_name(&name))) } fn rocket() -> Rocket { diff --git a/core/lib/tests/segments-issues-41-86.rs b/core/lib/tests/segments-issues-41-86.rs index 7ff1a6ee..f7286b0d 100644 --- a/core/lib/tests/segments-issues-41-86.rs +++ b/core/lib/tests/segments-issues-41-86.rs @@ -1,29 +1,29 @@ #[macro_use] extern crate rocket; -use rocket::http::uri::Segments; +use rocket::http::uri::{Segments, fmt::Path}; #[get("/test/")] -fn test(path: Segments<'_>) -> String { +fn test(path: Segments<'_, Path>) -> String { path.collect::>().join("/") } #[get("/two/")] -fn two(path: Segments<'_>) -> String { +fn two(path: Segments<'_, Path>) -> String { path.collect::>().join("/") } #[get("/one/two/")] -fn one_two(path: Segments<'_>) -> String { +fn one_two(path: Segments<'_, Path>) -> String { path.collect::>().join("/") } #[get("/", rank = 2)] -fn none(path: Segments<'_>) -> String { +fn none(path: Segments<'_, Path>) -> String { path.collect::>().join("/") } #[get("/static//is/")] -fn dual(user: String, path: Segments<'_>) -> String { +fn dual(user: String, path: Segments<'_, Path>) -> String { user + "/is/" + &path.collect::>().join("/") } diff --git a/core/lib/tests/uri-percent-encoding-issue-808.rs b/core/lib/tests/uri-percent-encoding-issue-808.rs index e215defe..9cffdfa6 100644 --- a/core/lib/tests/uri-percent-encoding-issue-808.rs +++ b/core/lib/tests/uri-percent-encoding-issue-808.rs @@ -2,7 +2,6 @@ use rocket::{Rocket, Build}; use rocket::response::Redirect; -use rocket::http::uri::Uri; const NAME: &str = "John[]|\\%@^"; @@ -13,12 +12,12 @@ fn hello(name: String) -> String { #[get("/raw")] fn raw_redirect() -> Redirect { - Redirect::to(format!("/hello/{}", Uri::percent_encode(NAME))) + Redirect::to(uri!(hello(NAME))) } #[get("/uri")] fn uri_redirect() -> Redirect { - Redirect::to(uri!(hello: NAME)) + Redirect::to(uri!(hello(NAME))) } fn rocket() -> Rocket { @@ -28,7 +27,7 @@ fn rocket() -> Rocket { mod tests { use super::*; use rocket::local::blocking::Client; - use rocket::http::{Status, uri::Uri}; + use rocket::http::Status; #[test] fn uri_percent_encoding_redirect() { @@ -49,8 +48,7 @@ mod tests { #[test] fn uri_percent_encoding_get() { let client = Client::debug(rocket()).unwrap(); - let name = Uri::percent_encode(NAME); - let response = client.get(format!("/hello/{}", name)).dispatch(); + let response = client.get(uri!(hello(NAME))).dispatch(); assert_eq!(response.status(), Status::Ok); assert_eq!(response.into_string().unwrap(), format!("Hello, {}!", NAME)); } diff --git a/examples/pastebin/src/main.rs b/examples/pastebin/src/main.rs index ac883e9c..6e0c67b4 100644 --- a/examples/pastebin/src/main.rs +++ b/examples/pastebin/src/main.rs @@ -5,7 +5,6 @@ mod paste_id; use std::io; -use rocket::State; use rocket::data::{Data, ToByteUnit}; use rocket::http::uri::Absolute; use rocket::response::content::Plain; @@ -13,17 +12,15 @@ use rocket::tokio::fs::{self, File}; use crate::paste_id::PasteId; -const HOST: &str = "http://localhost:8000"; +const HOST: Absolute<'static> = uri!("http://localhost:8000"); + const ID_LENGTH: usize = 3; #[post("/", data = "")] -async fn upload(paste: Data, host: &State>) -> io::Result { +async fn upload(paste: Data) -> io::Result { let id = PasteId::new(ID_LENGTH); paste.open(128.kibibytes()).into_file(id.file_path()).await?; - - // TODO: Ok(uri!(HOST, retrieve: id)) - let host = host.inner().clone(); - Ok(host.with_origin(uri!(retrieve: id)).to_string()) + Ok(uri!(HOST, retrieve(id)).to_string()) } #[get("/")] @@ -57,6 +54,5 @@ fn index() -> &'static str { #[launch] fn rocket() -> _ { rocket::build() - .manage(Absolute::parse(HOST).expect("valid host")) .mount("/", routes![index, upload, delete, retrieve]) } diff --git a/examples/pastebin/src/paste_id.rs b/examples/pastebin/src/paste_id.rs index 1eee006f..4764929c 100644 --- a/examples/pastebin/src/paste_id.rs +++ b/examples/pastebin/src/paste_id.rs @@ -1,7 +1,7 @@ use std::borrow::Cow; use std::path::{Path, PathBuf}; -use rocket::http::uri::{self, FromUriParam}; +use rocket::http::uri::fmt; use rocket::request::FromParam; use rand::{self, Rng}; @@ -46,7 +46,7 @@ impl<'a> FromParam<'a> for PasteId<'a> { } } -impl<'a> FromUriParam for PasteId<'_> { +impl<'a> fmt::FromUriParam for PasteId<'_> { type Target = PasteId<'a>; fn from_uri_param(param: &'a str) -> Self::Target { diff --git a/examples/pastebin/src/tests.rs b/examples/pastebin/src/tests.rs index bf77ce6f..ae9b6bc2 100644 --- a/examples/pastebin/src/tests.rs +++ b/examples/pastebin/src/tests.rs @@ -25,7 +25,7 @@ fn upload_paste(client: &Client, body: &str) -> String { } fn download_paste(client: &Client, id: &str) -> Option { - let response = client.get(uri!(super::retrieve: id)).dispatch(); + let response = client.get(uri!(super::retrieve(id))).dispatch(); if response.status().class().is_success() { Some(response.into_string().unwrap()) } else { @@ -34,7 +34,7 @@ fn download_paste(client: &Client, id: &str) -> Option { } fn delete_paste(client: &Client, id: &str) { - let response = client.delete(uri!(super::delete: id)).dispatch(); + let response = client.delete(uri!(super::delete(id))).dispatch(); assert_eq!(response.status(), Status::Ok); } diff --git a/examples/responders/src/tests.rs b/examples/responders/src/tests.rs index 3e3beae0..a084b1be 100644 --- a/examples/responders/src/tests.rs +++ b/examples/responders/src/tests.rs @@ -50,7 +50,7 @@ async fn test_one_hi_per_second() { // Listen for 1 second at 1 `hi` per 250ms, see if we get ~4 `hi`'s, then // send a shutdown() signal, meaning we should get a `goodbye`. let client = Client::tracked(super::rocket()).await.unwrap(); - let response = client.get(uri!(super::one_hi_per_ms: 250)).dispatch().await; + let response = client.get(uri!(super::one_hi_per_ms(250))).dispatch().await; let response = response.into_string(); let timer = time::sleep(Duration::from_secs(1)); @@ -100,11 +100,11 @@ fn test_login() { assert_eq!(r.into_string().unwrap(), "Hi! Please log in before continuing."); for name in &["Bob", "Charley", "Joe Roger"] { - let r = client.get(uri!(super::maybe_redir: name)).dispatch(); + let r = client.get(uri!(super::maybe_redir(name))).dispatch(); assert_eq!(r.status(), Status::SeeOther); } - let r = client.get(uri!(super::maybe_redir: "Sergio")).dispatch(); + let r = client.get(uri!(super::maybe_redir("Sergio"))).dispatch(); assert_eq!(r.status(), Status::Ok); assert_eq!(r.into_string().unwrap(), "Hello, Sergio!"); } @@ -139,11 +139,11 @@ fn test_xml() { #[test] fn test_either() { let client = Client::tracked(super::rocket()).unwrap(); - let r = client.get(uri!(super::json_or_msgpack: "json")).dispatch(); + let r = client.get(uri!(super::json_or_msgpack("json"))).dispatch(); assert_eq!(r.content_type().unwrap(), ContentType::JSON); assert_eq!(r.into_string().unwrap(), "\"hi\""); - let r = client.get(uri!(super::json_or_msgpack: "msgpack")).dispatch(); + let r = client.get(uri!(super::json_or_msgpack("msgpack"))).dispatch(); assert_eq!(r.content_type().unwrap(), ContentType::MsgPack); assert_eq!(r.into_bytes().unwrap(), &[162, 104, 105]); } @@ -155,13 +155,13 @@ use super::Kind; #[test] fn test_custom() { let client = Client::tracked(super::rocket()).unwrap(); - let r = client.get(uri!(super::custom: Some(Kind::String))).dispatch(); + let r = client.get(uri!(super::custom(Some(Kind::String)))).dispatch(); assert_eq!(r.into_string().unwrap(), "Hey, I'm some data."); - let r = client.get(uri!(super::custom: Some(Kind::Bytes))).dispatch(); + let r = client.get(uri!(super::custom(Some(Kind::Bytes)))).dispatch(); assert_eq!(r.into_string().unwrap(), "Hi"); - let r = client.get(uri!(super::custom: None as Option)).dispatch(); + let r = client.get(uri!(super::custom(_))).dispatch(); assert_eq!(r.status(), Status::Unauthorized); assert_eq!(r.content_type().unwrap(), ContentType::HTML); assert_eq!(r.into_string().unwrap(), "No no no!"); @@ -175,7 +175,7 @@ fn test_custom() { assert_eq!(response.status(), Status::Ok); // Fetch it using `custom`. - let r = client.get(uri!(super::custom: Some(Kind::File))).dispatch(); + let r = client.get(uri!(super::custom(Some(Kind::File)))).dispatch(); assert_eq!(r.into_string(), Some(CONTENTS.into())); // Delete it. diff --git a/examples/templating/src/hbs.rs b/examples/templating/src/hbs.rs index 2b92541a..212445fa 100644 --- a/examples/templating/src/hbs.rs +++ b/examples/templating/src/hbs.rs @@ -14,7 +14,7 @@ struct TemplateContext<'r> { #[get("/")] pub fn index() -> Redirect { - Redirect::to(uri!("/hbs", hello: name = "Your Name")) + Redirect::to(uri!("/hbs", hello(name = "Your Name"))) } #[get("/hello/")] @@ -38,7 +38,7 @@ pub fn about() -> Template { #[catch(404)] pub fn not_found(req: &Request<'_>) -> Template { let mut map = std::collections::HashMap::new(); - map.insert("path", req.uri().path()); + map.insert("path", req.uri().path().raw()); Template::render("hbs/error/404", &map) } diff --git a/examples/templating/src/tera.rs b/examples/templating/src/tera.rs index 54409f73..572d9d1b 100644 --- a/examples/templating/src/tera.rs +++ b/examples/templating/src/tera.rs @@ -13,7 +13,7 @@ struct TemplateContext<'r> { #[get("/")] pub fn index() -> Redirect { - Redirect::to(uri!("/tera", hello: name = "Your Name")) + Redirect::to(uri!("/tera", hello(name = "Your Name"))) } #[get("/hello/")] @@ -35,7 +35,7 @@ pub fn about() -> Template { #[catch(404)] pub fn not_found(req: &Request<'_>) -> Template { let mut map = HashMap::new(); - map.insert("path", req.uri().path()); + map.insert("path", req.uri().path().raw()); Template::render("tera/error/404", &map) } diff --git a/site/guide/5-responses.md b/site/guide/5-responses.md index fae83ad8..ea173167 100644 --- a/site/guide/5-responses.md +++ b/site/guide/5-responses.md @@ -492,23 +492,23 @@ URIs to `person` can be created as follows: # fn person(id: Option, name: &str, age: Option) { /* .. */ } // with unnamed parameters, in route path declaration order -let mike = uri!(person: 101, "Mike Smith", Some(28)); +let mike = uri!(person(101, "Mike Smith", Some(28))); assert_eq!(mike.to_string(), "/101/Mike%20Smith?age=28"); // with named parameters, order irrelevant -let mike = uri!(person: name = "Mike", id = 101, age = Some(28)); +let mike = uri!(person(name = "Mike", id = 101, age = Some(28))); assert_eq!(mike.to_string(), "/101/Mike?age=28"); -let mike = uri!(person: id = 101, age = Some(28), name = "Mike"); +let mike = uri!(person(id = 101, age = Some(28), name = "Mike")); assert_eq!(mike.to_string(), "/101/Mike?age=28"); // with a specific mount-point -let mike = uri!("/api", person: id = 101, name = "Mike", age = Some(28)); +let mike = uri!("/api", person(id = 101, name = "Mike", age = Some(28))); assert_eq!(mike.to_string(), "/api/101/Mike?age=28"); // with optional (defaultable) query parameters ignored -let mike = uri!(person: 101, "Mike", _); +let mike = uri!(person(101, "Mike", _)); assert_eq!(mike.to_string(), "/101/Mike"); -let mike = uri!(person: id = 101, name = "Mike", age = _); +let mike = uri!(person(id = 101, name = "Mike", age = _)); assert_eq!(mike.to_string(), "/101/Mike"); ``` @@ -518,8 +518,8 @@ Rocket informs you of any mismatched parameters at compile-time: error: `person` route uri expects 3 parameters but 1 was supplied --> examples/uri/main.rs:7:26 | -7 | let x = uri!(person: "Mike Smith"); - | ^^^^^^^^^^^^ +7 | let x = uri!(person("Mike Smith")); + | ^^^^^^^^^^^^ | = note: expected parameters: id: Option , name: &str, age: Option ``` @@ -529,8 +529,8 @@ Rocket also informs you of any type errors at compile-time: ```rust,ignore --> examples/uri/src/main.rs:7:31 | -7 | let x = uri!(person: id = "10", name = "Mike Smith", age = Some(10)); - | ^^^^ `FromUriParam` is not implemented for `usize` +7 | let x = uri!(person(id = "10", name = "Mike Smith", age = Some(10))); + | ^^^^ `FromUriParam` is not implemented for `usize` ``` We recommend that you use `uri!` exclusively when constructing URIs to your @@ -585,17 +585,17 @@ automatically generated, allowing for URIs to `add_user` to be generated using # #[post("/user/?")] # fn add_user(id: usize, details: UserDetails) { /* .. */ } -let link = uri!(add_user: 120, UserDetails { age: Some(20), nickname: "Bob".into() }); +let link = uri!(add_user(120, UserDetails { age: Some(20), nickname: "Bob".into() })); assert_eq!(link.to_string(), "/user/120?age=20&nickname=Bob"); ``` ### Typed URI Parts -The [`UriPart`] trait categorizes types that mark a part of the URI as either a -[`Path`] or a [`Query`]. Said another way, types that implement `UriPart` are +The [`Part`] trait categorizes types that mark a part of the URI as either a +[`Path`] or a [`Query`]. Said another way, types that implement `Part` are marker types that represent a part of a URI at the type-level. Traits such as -[`UriDisplay`] and [`FromUriParam`] bound a generic parameter by `UriPart`: `P: -UriPart`. This creates two instances of each trait: `UriDisplay` and +[`UriDisplay`] and [`FromUriParam`] bound a generic parameter by `Part`: `P: +Part`. This creates two instances of each trait: `UriDisplay` and `UriDisplay`, and `FromUriParam` and `FromUriParam`. As the names might imply, the `Path` version of the traits is used when @@ -624,7 +624,7 @@ generated. /// Note that `id` is `Option` in the route, but `id` in `uri!` _cannot_ /// be an `Option`. `age`, on the other hand, _must_ be an `Option` (or `Result` /// or `_`) as its in the query part and is allowed to be ignored. -let mike = uri!(person: id = 101, name = "Mike", age = Some(28)); +let mike = uri!(person(id = 101, name = "Mike", age = Some(28))); assert_eq!(mike.to_string(), "/101/Mike?age=28"); ``` @@ -639,10 +639,10 @@ Rocket, allows an `&str` to be used in a `uri!` invocation for route URI parameters declared as `String`: ```rust -# use rocket::http::uri::{FromUriParam, UriPart}; +# use rocket::http::uri::fmt::{FromUriParam, Part}; # struct S; # type String = S; -impl<'a, P: UriPart> FromUriParam for String { +impl<'a, P: Part> FromUriParam for String { type Target = &'a str; # fn from_uri_param(s: &'a str) -> Self::Target { "hi" } } @@ -679,13 +679,13 @@ use std::path::PathBuf; #[get("/person//")] fn person(id: usize, details: Option) { /* .. */ } -uri!(person: id = 100, details = "a/b/c"); +uri!(person(id = 100, details = "a/b/c")); ``` See the [`FromUriParam`] documentation for further details. [`Origin`]: @api/rocket/http/uri/struct.Origin.html -[`UriPart`]: @api/rocket/http/uri/trait.UriPart.html +[`Part`]: @api/rocket/http/uri/trait.Part.html [`Uri`]: @api/rocket/http/uri/enum.Uri.html [`Redirect::to()`]: @api/rocket/response/struct.Redirect.html#method.to [`uri!`]: @api/rocket/macro.uri.html