diff --git a/Cargo.toml b/Cargo.toml index 3af5843fe4..25cf12741a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,3 @@ -[profile.dev] -codegen-units = 4 - [workspace] members = [ "core/lib/", @@ -11,7 +8,7 @@ members = [ "site/tests", "examples/cookies", "examples/errors", - "examples/form_validation", + "examples/forms", "examples/hello_person", "examples/query_params", "examples/hello_world", @@ -30,7 +27,6 @@ members = [ "examples/msgpack", "examples/handlebars_templates", "examples/tera_templates", - "examples/form_kitchen_sink", "examples/config", "examples/raw_upload", "examples/pastebin", diff --git a/contrib/codegen/Cargo.toml b/contrib/codegen/Cargo.toml index 83f5e1f3d4..3f70e15f6c 100644 --- a/contrib/codegen/Cargo.toml +++ b/contrib/codegen/Cargo.toml @@ -19,7 +19,7 @@ proc-macro = true [dependencies] quote = "1.0" -devise = { git = "https://github.com/SergioBenitez/Devise.git", rev = "3648468" } +devise = { git = "https://github.com/SergioBenitez/Devise.git", rev = "bd221a4" } [dev-dependencies] rocket = { version = "0.5.0-dev", path = "../../core/lib" } diff --git a/contrib/lib/src/json.rs b/contrib/lib/src/json.rs index 2ac5867cb7..7c37a97689 100644 --- a/contrib/lib/src/json.rs +++ b/contrib/lib/src/json.rs @@ -14,32 +14,33 @@ //! features = ["json"] //! ``` -use std::ops::{Deref, DerefMut}; use std::io; +use std::ops::{Deref, DerefMut}; use std::iter::FromIterator; -use rocket::request::Request; -use rocket::outcome::Outcome::*; -use rocket::data::{Data, ByteUnit, Transform::*, Transformed}; -use rocket::data::{FromTransformedData, TransformFuture, FromDataFuture}; -use rocket::http::Status; +use rocket::request::{Request, local_cache}; +use rocket::data::{ByteUnit, Data, FromData, Outcome}; use rocket::response::{self, Responder, content}; +use rocket::http::Status; +use rocket::form::prelude as form; use serde::{Serialize, Serializer}; -use serde::de::{Deserialize, Deserializer}; +use serde::de::{Deserialize, DeserializeOwned, Deserializer}; #[doc(hidden)] pub use serde_json::{json_internal, json_internal_vec}; -/// The JSON type: implements [`FromTransformedData`] and [`Responder`], allowing you to -/// easily consume and respond with JSON. +/// The JSON data guard: easily consume and respond with JSON. /// /// ## Receiving JSON /// -/// If you're receiving JSON data, simply add a `data` parameter to your route -/// arguments and ensure the type of the parameter is a `Json`, where `T` is -/// some type you'd like to parse from JSON. `T` must implement [`Deserialize`] -/// from [`serde`]. The data is parsed from the HTTP request body. +/// `Json` is both a data guard and a form guard. +/// +/// ### Data Guard +/// +/// To parse request body data as JSON , add a `data` route argument with a +/// target type of `Json`, where `T` is some type you'd like to parse from +/// JSON. `T` must implement [`serde::Deserialize`]. /// /// ```rust /// # #[macro_use] extern crate rocket; @@ -47,7 +48,7 @@ pub use serde_json::{json_internal, json_internal_vec}; /// # type User = usize; /// use rocket_contrib::json::Json; /// -/// #[post("/users", format = "json", data = "")] +/// #[post("/user", format = "json", data = "")] /// fn new_user(user: Json) { /// /* ... */ /// } @@ -58,6 +59,30 @@ pub use serde_json::{json_internal, json_internal_vec}; /// "application/json" as its `Content-Type` header value will not be routed to /// the handler. /// +/// ### Form Guard +/// +/// `Json`, as a form guard, accepts value and data fields and parses the +/// data as a `T`. Simple use `Json`: +/// +/// ```rust +/// # #[macro_use] extern crate rocket; +/// # extern crate rocket_contrib; +/// # type Metadata = usize; +/// use rocket::form::{Form, FromForm}; +/// use rocket_contrib::json::Json; +/// +/// #[derive(FromForm)] +/// struct User<'r> { +/// name: &'r str, +/// metadata: Json +/// } +/// +/// #[post("/user", data = "
")] +/// fn new_user(form: Form>) { +/// /* ... */ +/// } +/// ``` +/// /// ## Sending JSON /// /// If you're responding with JSON data, return a `Json` type, where `T` @@ -94,6 +119,22 @@ pub use serde_json::{json_internal, json_internal_vec}; #[derive(Debug)] pub struct Json(pub T); +/// An error returned by the [`Json`] data guard when incoming data fails to +/// serialize as JSON. +#[derive(Debug)] +pub enum JsonError<'a> { + /// An I/O error occurred while reading the incoming request data. + Io(io::Error), + + /// The client's data was received successfully but failed to parse as valid + /// JSON or as the requested type. The `&str` value in `.0` is the raw data + /// received from the user, while the `Error` in `.1` is the deserialization + /// error from `serde`. + Parse(&'a str, serde_json::error::Error), +} + +const DEFAULT_LIMIT: ByteUnit = ByteUnit::Mebibyte(1); + impl Json { /// Consumes the JSON wrapper and returns the wrapped item. /// @@ -110,52 +151,38 @@ impl Json { } } -/// An error returned by the [`Json`] data guard when incoming data fails to -/// serialize as JSON. -#[derive(Debug)] -pub enum JsonError<'a> { - /// An I/O error occurred while reading the incoming request data. - Io(io::Error), - - /// The client's data was received successfully but failed to parse as valid - /// JSON or as the requested type. The `&str` value in `.0` is the raw data - /// received from the user, while the `Error` in `.1` is the deserialization - /// error from `serde`. - Parse(&'a str, serde_json::error::Error), -} +impl<'r, T: Deserialize<'r>> Json { + fn from_str(s: &'r str) -> Result> { + serde_json::from_str(s).map(Json).map_err(|e| JsonError::Parse(s, e)) + } -const DEFAULT_LIMIT: ByteUnit = ByteUnit::Mebibyte(1); + async fn from_data(req: &'r Request<'_>, data: Data) -> Result> { + let size_limit = req.limits().get("json").unwrap_or(DEFAULT_LIMIT); + let string = match data.open(size_limit).into_string().await { + Ok(s) if s.is_complete() => s.into_inner(), + Ok(_) => { + let eof = io::ErrorKind::UnexpectedEof; + return Err(JsonError::Io(io::Error::new(eof, "data limit exceeded"))); + }, + Err(e) => return Err(JsonError::Io(e)), + }; -impl<'a, T: Deserialize<'a>> FromTransformedData<'a> for Json { - type Error = JsonError<'a>; - type Owned = String; - type Borrowed = str; - - fn transform<'r>(r: &'r Request<'_>, d: Data) -> TransformFuture<'r, Self::Owned, Self::Error> { - Box::pin(async move { - let size_limit = r.limits().get("json").unwrap_or(DEFAULT_LIMIT); - match d.open(size_limit).stream_to_string().await { - Ok(s) => Borrowed(Success(s)), - Err(e) => Borrowed(Failure((Status::BadRequest, JsonError::Io(e)))) - } - }) + Self::from_str(local_cache!(req, string)) } +} + +#[rocket::async_trait] +impl<'r, T: Deserialize<'r>> FromData<'r> for Json { + type Error = JsonError<'r>; - fn from_data(_: &'a Request<'_>, o: Transformed<'a, Self>) -> FromDataFuture<'a, Self, Self::Error> { - Box::pin(async move { - let string = try_outcome!(o.borrowed()); - match serde_json::from_str(&string) { - Ok(v) => Success(Json(v)), - Err(e) => { - error_!("Couldn't parse JSON body: {:?}", e); - if e.is_data() { - Failure((Status::UnprocessableEntity, JsonError::Parse(string, e))) - } else { - Failure((Status::BadRequest, JsonError::Parse(string, e))) - } - } - } - }) + async fn from_data(req: &'r Request<'_>, data: Data) -> Outcome { + match Self::from_data(req, data).await { + Ok(value) => Outcome::Success(value), + Err(JsonError::Io(e)) if e.kind() == io::ErrorKind::UnexpectedEof => { + Outcome::Failure((Status::PayloadTooLarge, JsonError::Io(e))) + }, + Err(e) => Outcome::Failure((Status::BadRequest, e)), + } } } @@ -190,6 +217,26 @@ impl DerefMut for Json { } } +impl From> for form::Error<'_> { + fn from(e: JsonError<'_>) -> Self { + match e { + JsonError::Io(e) => e.into(), + JsonError::Parse(_, e) => form::Error::custom(e) + } + } +} + +#[rocket::async_trait] +impl<'v, T: DeserializeOwned + Send> form::FromFormField<'v> for Json { + fn from_value(field: form::ValueField<'v>) -> Result> { + Ok(Self::from_str(field.value)?) + } + + async fn from_data(f: form::DataField<'v, '_>) -> Result> { + Ok(Self::from_data(f.request, f.data).await?) + } +} + /// An arbitrary JSON value. /// /// This structure wraps `serde`'s [`Value`] type. Importantly, unlike `Value`, @@ -397,3 +444,5 @@ macro_rules! json { $crate::json::JsonValue($crate::json::json_internal!($($json)+)) }; } + +pub use json; diff --git a/contrib/lib/src/msgpack.rs b/contrib/lib/src/msgpack.rs index 0f476d4d4f..ff89ab205f 100644 --- a/contrib/lib/src/msgpack.rs +++ b/contrib/lib/src/msgpack.rs @@ -1,4 +1,5 @@ //! Automatic MessagePack (de)serialization support. + //! //! See the [`MsgPack`](crate::msgpack::MsgPack) type for further details. //! @@ -14,32 +15,32 @@ //! features = ["msgpack"] //! ``` +use std::io; use std::ops::{Deref, DerefMut}; -use tokio::io::AsyncReadExt; - -use rocket::request::Request; -use rocket::outcome::Outcome::*; -use rocket::data::{Data, ByteUnit, Transform::*, TransformFuture, Transformed}; -use rocket::data::{FromTransformedData, FromDataFuture}; -use rocket::response::{self, content, Responder}; +use rocket::request::{Request, local_cache}; +use rocket::data::{ByteUnit, Data, FromData, Outcome}; +use rocket::response::{self, Responder, content}; use rocket::http::Status; +use rocket::form::prelude as form; use serde::Serialize; -use serde::de::Deserialize; +use serde::de::{Deserialize, DeserializeOwned}; pub use rmp_serde::decode::Error; -/// The `MsgPack` type: implements [`FromTransformedData`] and [`Responder`], allowing you -/// to easily consume and respond with MessagePack data. +/// The `MsgPack` data guard and responder: easily consume and respond with +/// MessagePack. /// /// ## Receiving MessagePack /// -/// If you're receiving MessagePack data, simply add a `data` parameter to your -/// route arguments and ensure the type of the parameter is a `MsgPack`, -/// where `T` is some type you'd like to parse from MessagePack. `T` must -/// implement [`Deserialize`] from [`serde`]. The data is parsed from the HTTP -/// request body. +/// `MsgPack` is both a data guard and a form guard. +/// +/// ### Data Guard +/// +/// To parse request body data as MessagePack , add a `data` route argument with +/// a target type of `MsgPack`, where `T` is some type you'd like to parse +/// from JSON. `T` must implement [`serde::Deserialize`]. /// /// ```rust /// # #[macro_use] extern crate rocket; @@ -58,6 +59,30 @@ pub use rmp_serde::decode::Error; /// "application/msgpack" as its first `Content-Type:` header parameter will not /// be routed to this handler. /// +/// ### Form Guard +/// +/// `MsgPack`, as a form guard, accepts value and data fields and parses the +/// data as a `T`. Simple use `MsgPack`: +/// +/// ```rust +/// # #[macro_use] extern crate rocket; +/// # extern crate rocket_contrib; +/// # type Metadata = usize; +/// use rocket::form::{Form, FromForm}; +/// use rocket_contrib::msgpack::MsgPack; +/// +/// #[derive(FromForm)] +/// struct User<'r> { +/// name: &'r str, +/// metadata: MsgPack +/// } +/// +/// #[post("/users", data = "")] +/// fn new_user(form: Form>) { +/// /* ... */ +/// } +/// ``` +/// /// ## Sending MessagePack /// /// If you're responding with MessagePack data, return a `MsgPack` type, @@ -113,41 +138,37 @@ impl MsgPack { const DEFAULT_LIMIT: ByteUnit = ByteUnit::Mebibyte(1); -impl<'a, T: Deserialize<'a>> FromTransformedData<'a> for MsgPack { - type Error = Error; - type Owned = Vec; - type Borrowed = [u8]; - - fn transform<'r>(r: &'r Request<'_>, d: Data) -> TransformFuture<'r, Self::Owned, Self::Error> { - Box::pin(async move { - let size_limit = r.limits().get("msgpack").unwrap_or(DEFAULT_LIMIT); - let mut buf = Vec::new(); - let mut reader = d.open(size_limit); - match reader.read_to_end(&mut buf).await { - Ok(_) => Borrowed(Success(buf)), - Err(e) => Borrowed(Failure((Status::BadRequest, Error::InvalidDataRead(e)))), - } - }) +impl<'r, T: Deserialize<'r>> MsgPack { + fn from_bytes(buf: &'r [u8]) -> Result { + rmp_serde::from_slice(buf).map(MsgPack) } - fn from_data(_: &'a Request<'_>, o: Transformed<'a, Self>) -> FromDataFuture<'a, Self, Self::Error> { - use self::Error::*; - - Box::pin(async move { - let buf = try_outcome!(o.borrowed()); - match rmp_serde::from_slice(&buf) { - Ok(val) => Success(MsgPack(val)), - Err(e) => { - error_!("Couldn't parse MessagePack body: {:?}", e); - match e { - TypeMismatch(_) | OutOfRange | LengthMismatch(_) => { - Failure((Status::UnprocessableEntity, e)) - } - _ => Failure((Status::BadRequest, e)), - } - } - } - }) + async fn from_data(req: &'r Request<'_>, data: Data) -> Result { + let size_limit = req.limits().get("msgpack").unwrap_or(DEFAULT_LIMIT); + let bytes = match data.open(size_limit).into_bytes().await { + Ok(buf) if buf.is_complete() => buf.into_inner(), + Ok(_) => { + let eof = io::ErrorKind::UnexpectedEof; + return Err(Error::InvalidDataRead(io::Error::new(eof, "data limit exceeded"))); + }, + Err(e) => return Err(Error::InvalidDataRead(e)), + }; + + Self::from_bytes(local_cache!(req, bytes)) + } +} +#[rocket::async_trait] +impl<'r, T: Deserialize<'r>> FromData<'r> for MsgPack { + type Error = Error; + + async fn from_data(req: &'r Request<'_>, data: Data) -> Outcome { + match Self::from_data(req, data).await { + Ok(value) => Outcome::Success(value), + Err(Error::InvalidDataRead(e)) if e.kind() == io::ErrorKind::UnexpectedEof => { + Outcome::Failure((Status::PayloadTooLarge, Error::InvalidDataRead(e))) + }, + Err(e) => Outcome::Failure((Status::BadRequest, e)), + } } } @@ -166,6 +187,19 @@ impl<'r, T: Serialize> Responder<'r, 'static> for MsgPack { } } +#[rocket::async_trait] +impl<'v, T: DeserializeOwned + Send> form::FromFormField<'v> for MsgPack { + async fn from_data(f: form::DataField<'v, '_>) -> Result> { + Self::from_data(f.request, f.data).await.map_err(|e| { + match e { + Error::InvalidMarkerRead(e) | Error::InvalidDataRead(e) => e.into(), + Error::Utf8Error(e) => e.into(), + _ => form::Error::custom(e).into(), + } + }) + } +} + impl Deref for MsgPack { type Target = T; diff --git a/contrib/lib/src/serve.rs b/contrib/lib/src/serve.rs index d6f288b10f..b2bb137847 100644 --- a/contrib/lib/src/serve.rs +++ b/contrib/lib/src/serve.rs @@ -77,21 +77,22 @@ pub struct Options(u8); #[allow(non_upper_case_globals, non_snake_case)] impl Options { - /// `Options` representing the empty set. No dotfiles or index pages are - /// rendered. This is different than [`Options::default()`](#impl-Default), - /// which enables `Index`. + /// `Options` representing the empty set: no options are enabled. This is + /// different than [`Options::default()`](#impl-Default), which enables + /// `Index`. pub const None: Options = Options(0b0000); /// `Options` enabling responding to requests for a directory with the /// `index.html` file in that directory, if it exists. When this is enabled, /// the [`StaticFiles`] handler will respond to requests for a directory - /// `/foo` with the file `${root}/foo/index.html` if it exists. This is - /// enabled by default. + /// `/foo` of `/foo/` with the file `${root}/foo/index.html` if it exists. + /// This is enabled by default. pub const Index: Options = Options(0b0001); /// `Options` enabling returning dot files. When this is enabled, the /// [`StaticFiles`] handler will respond to requests for files or - /// directories beginning with `.`. This is _not_ enabled by default. + /// directories beginning with `.`. When disabled, any dotfiles will be + /// treated as missing. This is _not_ enabled by default. pub const DotFiles: Options = Options(0b0010); /// `Options` that normalizes directory requests by redirecting requests to @@ -100,8 +101,28 @@ impl Options { /// When enabled, the [`StaticFiles`] handler will respond to requests for a /// directory without a trailing `/` with a permanent redirect (308) to the /// same path with a trailing `/`. This ensures relative URLs within any - /// document served for that directory will be interpreted relative to that - /// directory, rather than its parent. This is _not_ enabled by default. + /// document served from that directory will be interpreted relative to that + /// directory rather than its parent. This is _not_ enabled by default. + /// + /// # Example + /// + /// Given the following directory structure... + /// + /// ```text + /// static/ + /// └── foo/ + /// ├── cat.jpeg + /// └── index.html + /// ``` + /// + /// ...with `StaticFiles::from("static")`, both requests to `/foo` and + /// `/foo/` will serve `static/foo/index.html`. If `index.html` references + /// `cat.jpeg` as a relative URL, the browser will request `/cat.jpeg` + /// (`static/cat.jpeg`) when the request for `/foo` was handled and + /// `/foo/cat.jpeg` (`static/foo/cat.jpeg`) if `/foo/` was handled. As a + /// result, the request in the former case will fail. To avoid this, + /// `NormalizeDirs` will redirect requests to `/foo` to `/foo/` if the file + /// that would be served is a directory. pub const NormalizeDirs: Options = Options(0b0100); /// Returns `true` if `self` is a superset of `other`. In other words, @@ -335,7 +356,7 @@ async fn handle_dir<'r, P>(opt: Options, r: &'r Request<'_>, d: Data, p: P) -> O where P: AsRef { if opt.contains(Options::NormalizeDirs) && !r.uri().path().ends_with('/') { - let new_path = r.uri().map_path(|p| p.to_owned() + "/") + let new_path = r.uri().map_path(|p| format!("{}/", p)) .expect("adding a trailing slash to a known good path results in a valid path") .into_owned(); @@ -364,9 +385,9 @@ impl Handler for StaticFiles { // Otherwise, we're handling segments. Get the segments as a `PathBuf`, // only allowing dotfiles if the user allowed it. let allow_dotfiles = self.options.contains(Options::DotFiles); - let path = req.get_segments::>(0) + let path = req.segments::>(0..) .and_then(|res| res.ok()) - .and_then(|segments| segments.into_path_buf(allow_dotfiles).ok()) + .and_then(|segments| segments.to_path_buf(allow_dotfiles).ok()) .map(|path| self.root.join(path)); match path { diff --git a/contrib/lib/src/uuid.rs b/contrib/lib/src/uuid.rs index 82848cd503..91c405aec0 100644 --- a/contrib/lib/src/uuid.rs +++ b/contrib/lib/src/uuid.rs @@ -20,8 +20,8 @@ use std::fmt; use std::str::FromStr; use std::ops::Deref; -use rocket::request::{FromParam, FromFormValue}; -use rocket::http::RawStr; +use rocket::request::FromParam; +use rocket::form::{self, FromFormField, ValueField}; type ParseError = ::Err; @@ -104,19 +104,14 @@ impl<'a> FromParam<'a> for Uuid { /// A value is successfully parsed if `param` is a properly formatted Uuid. /// Otherwise, a `ParseError` is returned. #[inline(always)] - fn from_param(param: &'a RawStr) -> Result { + fn from_param(param: &'a str) -> Result { param.parse() } } -impl<'v> FromFormValue<'v> for Uuid { - type Error = &'v RawStr; - - /// A value is successfully parsed if `form_value` is a properly formatted - /// Uuid. Otherwise, the raw form value is returned. - #[inline(always)] - fn from_form_value(form_value: &'v RawStr) -> Result { - form_value.parse().map_err(|_| form_value) +impl<'v> FromFormField<'v> for Uuid { + fn from_value(field: ValueField<'v>) -> form::Result<'v, Self> { + Ok(field.value.parse().map_err(form::error::Error::custom)?) } } diff --git a/contrib/lib/tests/static_files.rs b/contrib/lib/tests/static_files.rs index be2cbee70a..ff003582b6 100644 --- a/contrib/lib/tests/static_files.rs +++ b/contrib/lib/tests/static_files.rs @@ -119,14 +119,13 @@ mod static_tests { #[test] fn test_forwarding() { - use rocket::http::RawStr; use rocket::{get, routes}; #[get("/", rank = 20)] fn catch_one(value: String) -> String { value } #[get("//", rank = 20)] - fn catch_two(a: &RawStr, b: &RawStr) -> String { format!("{}/{}", a, b) } + fn catch_two(a: &str, b: &str) -> String { format!("{}/{}", a, b) } let rocket = rocket().mount("/default", routes![catch_one, catch_two]); let client = Client::tracked(rocket).expect("valid rocket"); diff --git a/contrib/lib/tests/templates.rs b/contrib/lib/tests/templates.rs index ba41f8f613..f42bc22d70 100644 --- a/contrib/lib/tests/templates.rs +++ b/contrib/lib/tests/templates.rs @@ -5,12 +5,12 @@ mod templates_tests { use std::path::{Path, PathBuf}; - use rocket::{Rocket, http::RawStr}; + use rocket::Rocket; use rocket::config::Config; use rocket_contrib::templates::{Template, Metadata}; #[get("//")] - fn template_check(md: Metadata<'_>, engine: &RawStr, name: &RawStr) -> Option<()> { + fn template_check(md: Metadata<'_>, engine: &str, name: &str) -> Option<()> { match md.contains_template(&format!("{}/{}", engine, name)) { true => Some(()), false => None diff --git a/core/codegen/Cargo.toml b/core/codegen/Cargo.toml index 29a1e5c0e8..8084d6275c 100644 --- a/core/codegen/Cargo.toml +++ b/core/codegen/Cargo.toml @@ -18,7 +18,7 @@ proc-macro = true indexmap = "1.0" quote = "1.0" rocket_http = { version = "0.5.0-dev", path = "../http/" } -devise = { git = "https://github.com/SergioBenitez/Devise.git", rev = "3648468" } +devise = { git = "https://github.com/SergioBenitez/Devise.git", rev = "bd221a4" } glob = "0.3" [dev-dependencies] diff --git a/core/codegen/src/attribute/catch.rs b/core/codegen/src/attribute/catch.rs index e01b29f1f5..d92ac9d40f 100644 --- a/core/codegen/src/attribute/catch.rs +++ b/core/codegen/src/attribute/catch.rs @@ -3,8 +3,7 @@ use devise::{syn, MetaItem, Spanned, Result, FromMeta, Diagnostic}; use crate::http_codegen::{self, Optional}; use crate::proc_macro2::{TokenStream, Span}; -use crate::syn_ext::{ReturnTypeExt, TokenStreamExt}; -use self::syn::{Attribute, parse::Parser}; +use crate::syn_ext::ReturnTypeExt; /// The raw, parsed `#[catch(code)]` attribute. #[derive(Debug, FromMeta)] @@ -18,19 +17,19 @@ struct CatchAttribute { struct CatcherCode(Option); impl FromMeta for CatcherCode { - fn from_meta(m: MetaItem<'_>) -> Result { - if usize::from_meta(m).is_ok() { - let status = http_codegen::Status::from_meta(m)?; + fn from_meta(meta: &MetaItem) -> Result { + if usize::from_meta(meta).is_ok() { + let status = http_codegen::Status::from_meta(meta)?; Ok(CatcherCode(Some(status))) - } else if let MetaItem::Path(path) = m { + } else if let MetaItem::Path(path) = meta { if path.is_ident("default") { Ok(CatcherCode(None)) } else { - Err(m.span().error(format!("expected `default`"))) + Err(meta.span().error("expected `default`")) } } else { - let msg = format!("expected integer or identifier, found {}", m.description()); - Err(m.span().error(msg)) + let msg = format!("expected integer or identifier, found {}", meta.description()); + Err(meta.span().error(msg)) } } } @@ -51,15 +50,9 @@ fn parse_params( .map_err(Diagnostic::from) .map_err(|diag| diag.help("`#[catch]` can only be used on functions"))?; - let full_attr = quote!(#[catch(#args)]); - let attrs = Attribute::parse_outer.parse2(full_attr)?; - let attribute = match CatchAttribute::from_attrs("catch", &attrs) { - Some(result) => result.map_err(|diag| { - diag.help("`#[catch]` expects a status code int or `default`: \ - `#[catch(404)]` or `#[catch(default)]`") - })?, - None => return Err(Span::call_site().error("internal error: bad attribute")) - }; + let attribute = CatchAttribute::from_meta(&syn::parse2(quote!(catch(#args)))?) + .map_err(|diag| diag.help("`#[catch]` expects a status code int or `default`: \ + `#[catch(404)]` or `#[catch(default)]`"))?; Ok(CatchParams { status: attribute.status.0, function }) } @@ -78,8 +71,8 @@ pub fn _catch( let status_code = Optional(catcher_status.as_ref().map(|s| s.0.code)); // Variables names we'll use and reuse. - define_vars_and_mods!(catch.function.span().into() => - req, status, _Box, Request, Response, StaticCatcherInfo, Catcher, + define_spanned_export!(catch.function.span().into() => + __req, __status, _Box, Request, Response, StaticCatcherInfo, Catcher, ErrorHandlerFuture, Status); // Determine the number of parameters that will be passed in. @@ -96,7 +89,7 @@ pub fn _catch( // Set the `req` and `status` spans to that of their respective function // arguments for a more correct `wrong type` error span. `rev` to be cute. - let codegen_args = &[&req, &status]; + let codegen_args = &[__req, __status]; let inputs = catch.function.sig.inputs.iter().rev() .zip(codegen_args.into_iter()) .map(|(fn_arg, codegen_arg)| match fn_arg { @@ -110,7 +103,7 @@ pub fn _catch( let catcher_response = quote_spanned!(return_type_span => { let ___responder = #user_catcher_fn_name(#(#inputs),*) #dot_await; - ::rocket::response::Responder::respond_to(___responder, #req)? + ::rocket::response::Responder::respond_to(___responder, #__req)? }); // Generate the catcher, keeping the user's input around. @@ -126,13 +119,13 @@ pub fn _catch( impl From<#user_catcher_fn_name> for #StaticCatcherInfo { fn from(_: #user_catcher_fn_name) -> #StaticCatcherInfo { fn monomorphized_function<'_b>( - #status: #Status, - #req: &'_b #Request<'_> + #__status: #Status, + #__req: &'_b #Request<'_> ) -> #ErrorHandlerFuture<'_b> { #_Box::pin(async move { let __response = #catcher_response; #Response::build() - .status(#status) + .status(#__status) .merge(__response) .ok() }) diff --git a/core/codegen/src/attribute/route.rs b/core/codegen/src/attribute/route.rs index e8c1fa04d8..386dd77dd3 100644 --- a/core/codegen/src/attribute/route.rs +++ b/core/codegen/src/attribute/route.rs @@ -11,7 +11,6 @@ use crate::syn_ext::{IdentExt, NameSource}; use crate::proc_macro2::{TokenStream, Span}; use crate::http_codegen::{Method, MediaType, RoutePath, DataSegment, Optional}; use crate::attribute::segments::{Source, Kind, Segment}; -use crate::syn::{Attribute, parse::Parser}; use crate::{URI_MACRO_PREFIX, ROCKET_PARAM_PREFIX}; @@ -51,6 +50,14 @@ struct Route { inputs: Vec<(NameSource, syn::Ident, syn::Type)>, } +impl Route { + fn find_input(&self, name: &T) -> Option<&(NameSource, syn::Ident, syn::Type)> + where T: PartialEq + { + self.inputs.iter().find(|(n, ..)| name == n) + } +} + fn parse_route(attr: RouteAttribute, function: syn::ItemFn) -> Result { // Gather diagnostics as we proceed. let mut diags = Diagnostics::new(); @@ -125,75 +132,60 @@ fn parse_route(attr: RouteAttribute, function: syn::ItemFn) -> Result { } fn param_expr(seg: &Segment, ident: &syn::Ident, ty: &syn::Type) -> TokenStream { - define_vars_and_mods!(req, data, error, log, request, _None, _Some, _Ok, _Err, Outcome); let i = seg.index.expect("dynamic parameters must be indexed"); let span = ident.span().join(ty.span()).unwrap_or_else(|| ty.span()); let name = ident.to_string(); + define_spanned_export!(span => + __req, __data, _log, _request, _None, _Some, _Ok, _Err, Outcome + ); + // All dynamic parameter should be found if this function is being called; // that's the point of statically checking the URI parameters. let internal_error = quote!({ - #log::error("Internal invariant error: expected dynamic parameter not found."); - #log::error("Please report this error to the Rocket issue tracker."); - #Outcome::Forward(#data) + #_log::error("Internal invariant error: expected dynamic parameter not found."); + #_log::error("Please report this error to the Rocket issue tracker."); + #Outcome::Forward(#__data) }); // Returned when a dynamic parameter fails to parse. let parse_error = quote!({ - #log::warn_(&format!("Failed to parse '{}': {:?}", #name, #error)); - #Outcome::Forward(#data) + #_log::warn_(&format!("Failed to parse '{}': {:?}", #name, __error)); + #Outcome::Forward(#__data) }); let expr = match seg.kind { Kind::Single => quote_spanned! { span => - match #req.raw_segment_str(#i) { - #_Some(__s) => match <#ty as #request::FromParam>::from_param(__s) { + match #__req.routed_segment(#i) { + #_Some(__s) => match <#ty as #_request::FromParam>::from_param(__s) { #_Ok(__v) => __v, - #_Err(#error) => return #parse_error, + #_Err(__error) => return #parse_error, }, #_None => return #internal_error } }, Kind::Multi => quote_spanned! { span => - match #req.raw_segments(#i) { - #_Some(__s) => match <#ty as #request::FromSegments>::from_segments(__s) { - #_Ok(__v) => __v, - #_Err(#error) => return #parse_error, - }, - #_None => return #internal_error + match <#ty as #_request::FromSegments>::from_segments(#__req.routed_segments(#i..)) { + #_Ok(__v) => __v, + #_Err(__error) => return #parse_error, } }, Kind::Static => return quote!() }; quote! { - #[allow(non_snake_case, unreachable_patterns, unreachable_code)] let #ident: #ty = #expr; } } fn data_expr(ident: &syn::Ident, ty: &syn::Type) -> TokenStream { - define_vars_and_mods!(req, data, FromTransformedData, Outcome, Transform); let span = ident.span().join(ty.span()).unwrap_or_else(|| ty.span()); + define_spanned_export!(span => __req, __data, FromData, Outcome); + quote_spanned! { span => - let __transform = <#ty as #FromTransformedData>::transform(#req, #data).await; - - #[allow(unreachable_patterns, unreachable_code)] - let __outcome = match __transform { - #Transform::Owned(#Outcome::Success(__v)) => { - #Transform::Owned(#Outcome::Success(__v)) - }, - #Transform::Borrowed(#Outcome::Success(ref __v)) => { - #Transform::Borrowed(#Outcome::Success(::std::borrow::Borrow::borrow(__v))) - }, - #Transform::Borrowed(__o) => #Transform::Borrowed(__o.map(|_| { - unreachable!("Borrowed(Success(..)) case handled in previous block") - })), - #Transform::Owned(__o) => #Transform::Owned(__o), - }; + let __outcome = <#ty as #FromData>::from_data(#__req, #__data).await; - #[allow(non_snake_case, unreachable_patterns, unreachable_code)] - let #ident: #ty = match <#ty as #FromTransformedData>::from_data(#req, __outcome).await { + let #ident: #ty = match __outcome { #Outcome::Success(__d) => __d, #Outcome::Forward(__d) => return #Outcome::Forward(__d), #Outcome::Failure((__c, _)) => return #Outcome::Failure(__c), @@ -202,118 +194,95 @@ fn data_expr(ident: &syn::Ident, ty: &syn::Type) -> TokenStream { } fn query_exprs(route: &Route) -> Option { - define_vars_and_mods!(_None, _Some, _Ok, _Err, _Option); - define_vars_and_mods!(data, trail, log, request, req, Outcome, SmallVec, Query); - let query_segments = route.attribute.path.query.as_ref()?; - let (mut decls, mut matchers, mut builders) = (vec![], vec![], vec![]); - for segment in query_segments { - let (ident, ty, span) = if segment.kind != Kind::Static { - let (ident, ty) = route.inputs.iter() - .find(|(name, _, _)| name == &segment.name) - .map(|(_, rocket_ident, ty)| (rocket_ident, ty)) - .unwrap(); - - let span = ident.span().join(ty.span()).unwrap_or_else(|| ty.span()); - (Some(ident), Some(ty), span.into()) - } else { - (None, None, segment.span.into()) - }; - - let decl = match segment.kind { - Kind::Single => quote_spanned! { span => - #[allow(non_snake_case)] - let mut #ident: #_Option<#ty> = #_None; - }, - Kind::Multi => quote_spanned! { span => - #[allow(non_snake_case)] - let mut #trail = #SmallVec::<[#request::FormItem; 8]>::new(); - }, - Kind::Static => quote!() - }; - - let name = segment.name.name(); - let matcher = match segment.kind { - Kind::Single => quote_spanned! { span => - (_, #name, __v) => { - #[allow(unreachable_patterns, unreachable_code)] - let __v = match <#ty as #request::FromFormValue>::from_form_value(__v) { - #_Ok(__v) => __v, - #_Err(__e) => { - #log::warn_(&format!("Failed to parse '{}': {:?}", #name, __e)); - return #Outcome::Forward(#data); - } - }; - - #ident = #_Some(__v); - } - }, - Kind::Static => quote! { - (#name, _, _) => continue, - }, - Kind::Multi => quote! { - _ => #trail.push(__i), - } - }; + use devise::ext::{Split2, Split6}; - let builder = match segment.kind { - Kind::Single => quote_spanned! { span => - #[allow(non_snake_case)] - let #ident = match #ident.or_else(<#ty as #request::FromFormValue>::default) { - #_Some(__v) => __v, - #_None => { - #log::warn_(&format!("Missing required query parameter '{}'.", #name)); - return #Outcome::Forward(#data); - } - }; - }, - Kind::Multi => quote_spanned! { span => - #[allow(non_snake_case)] - let #ident = match <#ty as #request::FromQuery>::from_query(#Query(&#trail)) { - #_Ok(__v) => __v, - #_Err(__e) => { - #log::warn_(&format!("Failed to parse '{}': {:?}", #name, __e)); - return #Outcome::Forward(#data); - } - }; - }, - Kind::Static => quote!() - }; + define_spanned_export!(Span::call_site() => + __req, __data, _log, _form, Outcome, _Ok, _Err, _Some, _None + ); - decls.push(decl); - matchers.push(matcher); - builders.push(builder); - } + let query_segments = route.attribute.path.query.as_ref()?; - matchers.push(quote!(_ => continue)); + // Record all of the static parameters for later filtering. + let (raw_name, raw_value) = query_segments.iter() + .filter(|s| !s.is_dynamic()) + .map(|s| { + let name = s.name.name(); + match name.find('=') { + Some(i) => (&name[..i], &name[i + 1..]), + None => (name, "") + } + }) + .split2(); + + // Now record all of the dynamic parameters. + let (name, matcher, ident, init_expr, push_expr, finalize_expr) = query_segments.iter() + .filter(|s| s.is_dynamic()) + .map(|s| (s, s.name.name(), route.find_input(&s.name).expect("dynamic has input"))) + .map(|(seg, name, (_, ident, ty))| { + let matcher = match seg.kind { + Kind::Multi => quote_spanned!(seg.span => _), + _ => quote_spanned!(seg.span => #name) + }; + + let span = ty.span(); + define_spanned_export!(span => FromForm, _form); + + let ty = quote_spanned!(span => <#ty as #FromForm>); + let i = ident.clone().with_span(span); + let init = quote_spanned!(span => #ty::init(#_form::Options::Lenient)); + let finalize = quote_spanned!(span => #ty::finalize(#i)); + let push = match seg.kind { + Kind::Multi => quote_spanned!(span => #ty::push_value(&mut #i, _f)), + _ => quote_spanned!(span => #ty::push_value(&mut #i, _f.shift())), + }; + + (name, matcher, ident, init, push, finalize) + }) + .split6(); + + #[allow(non_snake_case)] Some(quote! { - #(#decls)* - - if let #_Some(__items) = #req.raw_query_items() { - for __i in __items { - match (__i.raw.as_str(), __i.key.as_str(), __i.value) { - #( - #[allow(unreachable_patterns, unreachable_code)] - #matchers - )* - } + let mut _e = #_form::Errors::new(); + #(let mut #ident = #init_expr;)* + + for _f in #__req.query_fields() { + let _raw = (_f.name.source().as_str(), _f.value); + let _key = _f.name.key_lossy().as_str(); + match (_raw, _key) { + // Skip static parameters so doesn't see them. + #(((#raw_name, #raw_value), _) => { /* skip */ },)* + #((_, #matcher) => #push_expr,)* + _ => { /* in case we have no trailing, ignore all else */ }, } } #( - #[allow(unreachable_patterns, unreachable_code)] - #builders + let #ident = match #finalize_expr { + #_Ok(_v) => #_Some(_v), + #_Err(_err) => { + _e.extend(_err.with_name(#_form::NameView::new(#name))); + #_None + }, + }; )* + + if !_e.is_empty() { + #_log::warn_("query string failed to match declared route"); + for _err in _e { #_log::warn_(_err); } + return #Outcome::Forward(#__data); + } + + #(let #ident = #ident.unwrap();)* }) } fn request_guard_expr(ident: &syn::Ident, ty: &syn::Type) -> TokenStream { - define_vars_and_mods!(req, data, request, Outcome); let span = ident.span().join(ty.span()).unwrap_or_else(|| ty.span()); + define_spanned_export!(span => __req, __data, _request, Outcome); quote_spanned! { span => - #[allow(non_snake_case, unreachable_patterns, unreachable_code)] - let #ident: #ty = match <#ty as #request::FromRequest>::from_request(#req).await { + let #ident: #ty = match <#ty as #_request::FromRequest>::from_request(#__req).await { #Outcome::Success(__v) => __v, - #Outcome::Forward(_) => return #Outcome::Forward(#data), + #Outcome::Forward(_) => return #Outcome::Forward(#__data), #Outcome::Failure((__c, _)) => return #Outcome::Failure(__c), }; } @@ -327,7 +296,7 @@ fn generate_internal_uri_macro(route: &Route) -> TokenStream { .filter(|seg| seg.source == Source::Path || seg.source == Source::Query) .filter(|seg| seg.kind != Kind::Static) .map(|seg| &seg.name) - .map(|seg_name| route.inputs.iter().find(|(in_name, ..)| in_name == seg_name).unwrap()) + .map(|seg_name| route.find_input(seg_name).unwrap()) .map(|(name, _, ty)| (name.ident(), ty)) .map(|(ident, ty)| quote!(#ident: #ty)); @@ -364,20 +333,21 @@ fn generate_respond_expr(route: &Route) -> TokenStream { syn::ReturnType::Type(_, ref ty) => ty.span().into() }; - define_vars_and_mods!(req); - define_vars_and_mods!(ret_span => handler); + define_spanned_export!(ret_span => __req, _handler); let user_handler_fn_name = &route.function.sig.ident; let parameter_names = route.inputs.iter() .map(|(_, rocket_ident, _)| rocket_ident); - let _await = route.function.sig.asyncness.map(|a| quote_spanned!(a.span().into() => .await)); + let _await = route.function.sig.asyncness + .map(|a| quote_spanned!(a.span().into() => .await)); + let responder_stmt = quote_spanned! { ret_span => let ___responder = #user_handler_fn_name(#(#parameter_names),*) #_await; }; quote_spanned! { ret_span => #responder_stmt - #handler::Outcome::from(#req, ___responder) + #_handler::Outcome::from(#__req, ___responder) } } @@ -409,7 +379,10 @@ fn codegen_route(route: Route) -> Result { } // Gather everything we need. - define_vars_and_mods!(req, data, _Box, Request, Data, Route, StaticRouteInfo, HandlerFuture); + use crate::exports::{ + __req, __data, _Box, Request, Data, Route, StaticRouteInfo, HandlerFuture + }; + let (vis, user_handler_fn) = (&route.function.vis, &route.function); let user_handler_fn_name = &user_handler_fn.sig.ident; let generated_internal_uri_macro = generate_internal_uri_macro(&route); @@ -430,10 +403,11 @@ fn codegen_route(route: Route) -> Result { /// Rocket code generated proxy static conversion implementation. impl From<#user_handler_fn_name> for #StaticRouteInfo { + #[allow(non_snake_case, unreachable_patterns, unreachable_code)] fn from(_: #user_handler_fn_name) -> #StaticRouteInfo { fn monomorphized_function<'_b>( - #req: &'_b #Request<'_>, - #data: #Data + #__req: &'_b #Request<'_>, + #__data: #Data ) -> #HandlerFuture<'_b> { #_Box::pin(async move { #(#req_guard_definitions)* @@ -473,13 +447,8 @@ fn complete_route(args: TokenStream, input: TokenStream) -> Result .map_err(|e| Diagnostic::from(e)) .map_err(|diag| diag.help("`#[route]` can only be used on functions"))?; - let full_attr = quote!(#[route(#args)]); - let attrs = Attribute::parse_outer.parse2(full_attr)?; - let attribute = match RouteAttribute::from_attrs("route", &attrs) { - Some(result) => result?, - None => return Err(Span::call_site().error("internal error: bad attribute")) - }; - + let attr_tokens = quote!(route(#args)); + let attribute = RouteAttribute::from_meta(&syn::parse2(attr_tokens)?)?; codegen_route(parse_route(attribute, function)?) } @@ -499,12 +468,8 @@ fn incomplete_route( .map_err(|e| Diagnostic::from(e)) .map_err(|d| d.help(format!("#[{}] can only be used on functions", method_str)))?; - let full_attr = quote!(#[#method_ident(#args)]); - let attrs = Attribute::parse_outer.parse2(full_attr)?; - let method_attribute = match MethodRouteAttribute::from_attrs(&method_str, &attrs) { - Some(result) => result?, - None => return Err(Span::call_site().error("internal error: bad attribute")) - }; + let full_attr = quote!(#method_ident(#args)); + let method_attribute = MethodRouteAttribute::from_meta(&syn::parse2(full_attr)?)?; let attribute = RouteAttribute { method: SpanWrapped { diff --git a/core/codegen/src/attribute/segments.rs b/core/codegen/src/attribute/segments.rs index 74c7c869d7..fc501ee10b 100644 --- a/core/codegen/src/attribute/segments.rs +++ b/core/codegen/src/attribute/segments.rs @@ -3,6 +3,7 @@ use std::hash::{Hash, Hasher}; use devise::{syn, Diagnostic, ext::SpanDiagnosticExt}; use crate::proc_macro2::Span; +use crate::http::RawStr; use crate::http::uri::{self, UriPart}; use crate::http::route::RouteSegment; use crate::proc_macro_ext::{Diagnostics, StringLit, PResult, DResult}; @@ -145,7 +146,7 @@ pub fn parse_data_segment(segment: &str, span: Span) -> PResult { } pub fn parse_segments( - string: &str, + string: &RawStr, span: Span ) -> DResult> { let mut segments = vec![]; @@ -154,11 +155,11 @@ pub fn parse_segments( for result in >::parse_many(string) { match result { Ok(segment) => { - let seg_span = subspan(&segment.string, string, span); + let seg_span = subspan(&segment.string, string.as_str(), span); segments.push(Segment::from(segment, seg_span)); }, Err((segment_string, error)) => { - diags.push(into_diagnostic(segment_string, string, span, &error)); + diags.push(into_diagnostic(segment_string, string.as_str(), span, &error)); if let Error::Trailing(..) = error { break; } diff --git a/core/codegen/src/bang/mod.rs b/core/codegen/src/bang/mod.rs index 371bdd33a8..b084a283bb 100644 --- a/core/codegen/src/bang/mod.rs +++ b/core/codegen/src/bang/mod.rs @@ -12,7 +12,7 @@ fn struct_maker_vec( input: proc_macro::TokenStream, ty: TokenStream, ) -> Result { - define_vars_and_mods!(_Vec); + use crate::exports::_Vec; // Parse a comma-separated list of paths. let paths = >::parse_terminated.parse(input)?; diff --git a/core/codegen/src/bang/test_guide.rs b/core/codegen/src/bang/test_guide.rs index 194d41e744..e442ababc9 100644 --- a/core/codegen/src/bang/test_guide.rs +++ b/core/codegen/src/bang/test_guide.rs @@ -28,7 +28,12 @@ fn entry_to_tests(root_glob: &LitStr) -> Result, Box let ident = Ident::new(&name, root_glob.span()); let full_path = Path::new(&manifest_dir).join(&path).display().to_string(); - tests.push(quote_spanned!(root_glob.span() => doc_comment::doctest!(#full_path, #ident);)) + tests.push(quote_spanned!(root_glob.span() => + mod #ident { + macro_rules! doc_comment { ($x:expr) => (#[doc = $x] extern {}); } + doc_comment!(include_str!(#full_path)); + } + )); } Ok(tests) diff --git a/core/codegen/src/bang/uri.rs b/core/codegen/src/bang/uri.rs index b63b6e988d..10647c4e70 100644 --- a/core/codegen/src/bang/uri.rs +++ b/core/codegen/src/bang/uri.rs @@ -1,6 +1,7 @@ use std::fmt::Display; -use devise::{syn, Result, ext::SpanDiagnosticExt}; +use devise::{syn, Result}; +use devise::ext::{SpanDiagnosticExt, quote_respanned}; use crate::http::{uri::{Origin, Path, Query}, ext::IntoOwned}; use crate::http::route::{RouteSegment, Kind}; @@ -46,7 +47,7 @@ fn extract_exprs<'a>(internal: &'a InternalUriParams) -> Result<( let route_name = &internal.uri_params.route_path; match internal.validate() { Validation::Ok(exprs) => { - let path_param_count = internal.route_uri.path().matches('<').count(); + let path_param_count = internal.route_uri.path().as_str().matches('<').count(); for expr in exprs.iter().take(path_param_count) { if !expr.as_expr().is_some() { return Err(expr.span().error("path parameters cannot be ignored")); @@ -112,18 +113,19 @@ fn extract_exprs<'a>(internal: &'a InternalUriParams) -> Result<( } fn add_binding(to: &mut Vec, ident: &Ident, ty: &Type, expr: &Expr, source: Source) { - let uri_mod = quote!(rocket::http::uri); - let (span, ident_tmp) = (expr.span(), ident.prepend("__tmp_")); - let from_uri_param = if source == Source::Query { - quote_spanned!(span => #uri_mod::FromUriParam<#uri_mod::Query, _>) - } else { - quote_spanned!(span => #uri_mod::FromUriParam<#uri_mod::Path, _>) + let span = expr.span(); + define_spanned_export!(span => _uri); + let part = match source { + Source::Query => quote_spanned!(span => #_uri::Query), + _ => quote_spanned!(span => #_uri::Path), }; + let tmp_ident = ident.clone().with_span(expr.span()); + let let_stmt = quote_spanned!(span => let #tmp_ident = #expr); + to.push(quote_spanned!(span => - #[allow(non_snake_case)] - let #ident_tmp = #expr; - let #ident = <#ty as #from_uri_param>::from_uri_param(#ident_tmp); + #[allow(non_snake_case)] #let_stmt; + let #ident = <#ty as #_uri::FromUriParam<#part, _>>::from_uri_param(#tmp_ident); )); } @@ -132,7 +134,7 @@ fn explode_path<'a, I: Iterator>( bindings: &mut Vec, mut items: I ) -> TokenStream { - let (uri_mod, path) = (quote!(rocket::http::uri), uri.path()); + let (uri_mod, path) = (quote!(rocket::http::uri), uri.path().as_str()); if !path.contains('<') { return quote!(#uri_mod::UriArgumentsKind::Static(#path)); } @@ -161,7 +163,7 @@ fn explode_query<'a, I: Iterator>( bindings: &mut Vec, mut items: I ) -> Option { - let (uri_mod, query) = (quote!(rocket::http::uri), uri.query()?); + let (uri_mod, query) = (quote!(rocket::http::uri), uri.query()?.as_str()); if !query.contains('<') { return Some(quote!(#uri_mod::UriArgumentsKind::Static(#query))); } @@ -181,7 +183,7 @@ fn explode_query<'a, I: Iterator>( None => { // Force a typecheck for the `Ignoreable` trait. Note that write // out the path to `is_ignorable` to get the right span. - bindings.push(quote_spanned! { arg_expr.span() => + bindings.push(quote_respanned! { arg_expr.span() => rocket::http::uri::assert_ignorable::<#uri_mod::Query, #ty>(); }); @@ -210,11 +212,11 @@ fn explode_query<'a, I: Iterator>( // (``) with `param=`. fn build_origin(internal: &InternalUriParams) -> Origin<'static> { let mount_point = internal.uri_params.mount_point.as_ref() - .map(|origin| origin.path()) + .map(|origin| origin.path().as_str()) .unwrap_or(""); let path = format!("{}/{}", mount_point, internal.route_uri.path()); - let query = internal.route_uri.query(); + let query = internal.route_uri.query().map(|q| q.as_str()); Origin::new(path, query).into_normalized().into_owned() } diff --git a/core/codegen/src/derive/form_field.rs b/core/codegen/src/derive/form_field.rs new file mode 100644 index 0000000000..1919b47112 --- /dev/null +++ b/core/codegen/src/derive/form_field.rs @@ -0,0 +1,147 @@ +use devise::{*, ext::{TypeExt, SpanDiagnosticExt}}; + +use crate::exports::*; +use crate::proc_macro2::Span; +use crate::syn_ext::NameSource; + +pub struct FormField { + pub span: Span, + pub name: NameSource, +} + +#[derive(FromMeta)] +pub struct FieldAttr { + pub name: Option, + pub validate: Option, +} + +impl FieldAttr { + const NAME: &'static str = "field"; +} + +pub(crate) trait FieldExt { + fn ident(&self) -> &syn::Ident; + fn field_name(&self) -> Result; + fn stripped_ty(&self) -> syn::Type; + fn name_view(&self) -> Result; +} + +impl FromMeta for FormField { + fn from_meta(meta: &MetaItem) -> Result { + // These are used during parsing. + const CONTROL_CHARS: &[char] = &['&', '=', '?', '.', '[', ']']; + + fn is_valid_field_name(s: &str) -> bool { + // The HTML5 spec (4.10.18.1) says 'isindex' is not allowed. + if s == "isindex" || s.is_empty() { + return false + } + + // We allow all visible ASCII characters except `CONTROL_CHARS`. + s.chars().all(|c| c.is_ascii_graphic() && !CONTROL_CHARS.contains(&c)) + } + + let name = NameSource::from_meta(meta)?; + if !is_valid_field_name(name.name()) { + let chars = CONTROL_CHARS.iter() + .map(|c| format!("{:?}", c)) + .collect::>() + .join(", "); + + return Err(meta.value_span() + .error("invalid form field name") + .help(format!("field name cannot be `isindex` or contain {}", chars))); + } + + Ok(FormField { span: meta.value_span(), name }) + } +} + +impl FieldExt for Field<'_> { + fn ident(&self) -> &syn::Ident { + self.ident.as_ref().expect("named") + } + + fn field_name(&self) -> Result { + let mut fields = FieldAttr::from_attrs(FieldAttr::NAME, &self.attrs)? + .into_iter() + .filter_map(|attr| attr.name); + + let name = fields.next() + .map(|f| f.name) + .unwrap_or_else(|| NameSource::from(self.ident().clone())); + + if let Some(field) = fields.next() { + return Err(field.span + .error("duplicate form field renaming") + .help("a field can only be renamed once")); + } + + Ok(name.to_string()) + } + + fn stripped_ty(&self) -> syn::Type { + self.ty.with_stripped_lifetimes() + } + + fn name_view(&self) -> Result { + let field_name = self.field_name()?; + Ok(syn::parse_quote!(#_form::NameBuf::from((__c.__parent, #field_name)))) + } +} + +pub fn validators<'v>( + field: Field<'v>, + out: &'v syn::Ident, + local: bool, +) -> Result + 'v> { + use syn::visit_mut::VisitMut; + + struct ValidationMutator<'a> { + field: syn::Expr, + self_expr: syn::Expr, + form: &'a syn::Ident, + visited: bool, + rec: bool, + } + + impl<'a> ValidationMutator<'a> { + fn new(field: &'a syn::Ident, form: &'a syn::Ident) -> Self { + let self_expr = syn::parse_quote!(&#form.#field); + let field = syn::parse_quote!(&#field); + ValidationMutator { field, self_expr, form, visited: false, rec: false } + } + } + + impl VisitMut for ValidationMutator<'_> { + fn visit_expr_call_mut(&mut self, call: &mut syn::ExprCall) { + syn::visit_mut::visit_expr_call_mut(self, call); + + let ident = if self.rec { &self.self_expr } else { &self.field }; + if !self.visited { + call.args.insert(0, ident.clone()); + self.visited = true; + } + } + + fn visit_ident_mut(&mut self, i: &mut syn::Ident) { + if i == "self" { + *i = self.form.clone(); + self.rec = true; + } + + syn::visit_mut::visit_ident_mut(self, i); + } + } + + Ok(FieldAttr::from_attrs(FieldAttr::NAME, &field.attrs)? + .into_iter() + .filter_map(|a| a.validate) + .map(move |mut expr| { + let mut mutator = ValidationMutator::new(field.ident(), out); + mutator.visit_expr_mut(&mut expr); + (expr, mutator.rec) + }) + .filter(move |(_, global)| local != *global) + .map(|(e, _)| e)) +} diff --git a/core/codegen/src/derive/from_form.rs b/core/codegen/src/derive/from_form.rs index 5d33f60f15..c11a5b6b55 100644 --- a/core/codegen/src/derive/from_form.rs +++ b/core/codegen/src/derive/from_form.rs @@ -1,133 +1,268 @@ -use devise::{*, ext::{TypeExt, Split3, SpanDiagnosticExt}}; +use devise::{*, ext::{TypeExt, SpanDiagnosticExt, GenericsExt, Split2, quote_respanned}}; -use crate::proc_macro2::{Span, TokenStream}; -use crate::syn_ext::NameSource; +use crate::exports::*; +use crate::proc_macro2::TokenStream; +use crate::derive::form_field::*; -#[derive(FromMeta)] -pub struct Form { - pub field: FormField, -} - -pub struct FormField { - pub span: Span, - pub name: NameSource, -} +// F: fn(field_ty: Ty, field_context: Expr) +fn fields_map(fields: Fields<'_>, map_f: F) -> Result + where F: Fn(&syn::Type, &syn::Expr) -> TokenStream +{ + let matchers = fields.iter() + .map(|f| { + let (ident, field_name, ty) = (f.ident(), f.field_name()?, f.stripped_ty()); + let field_context = quote_spanned!(ty.span() => { + let _o = __c.__opts; + __c.#ident.get_or_insert_with(|| <#ty as #_form::FromForm<'__f>>::init(_o)) + }); -fn is_valid_field_name(s: &str) -> bool { - // The HTML5 spec (4.10.18.1) says 'isindex' is not allowed. - if s == "isindex" || s.is_empty() { - return false - } + let field_context = syn::parse2(field_context).expect("valid expr"); + let expr = map_f(&ty, &field_context); + Ok(quote!(#field_name => { #expr })) + }) + .collect::>>()?; - // We allow all visible ASCII characters except '&', '=', and '?' since we - // use those as control characters for parsing. - s.chars().all(|c| (c >= ' ' && c <= '~') && c != '&' && c != '=' && c != '?') -} + Ok(quote! { + __c.__parent = __f.name.parent(); -impl FromMeta for FormField { - fn from_meta(meta: MetaItem<'_>) -> Result { - let name = NameSource::from_meta(meta)?; - if !is_valid_field_name(name.name()) { - return Err(meta.value_span().error("invalid form field name")); + match __f.name.key_lossy().as_str() { + #(#matchers,)* + _k if _k == "_method" || !__c.__opts.strict => { /* ok */ }, + _ => __c.__errors.push(__f.unexpected()), } - - Ok(FormField { span: meta.value_span(), name }) - } + }) } -fn validate_struct(_: &DeriveGenerator, data: Struct<'_>) -> Result<()> { - if data.fields().is_empty() { - return Err(data.fields.span().error("at least one field is required")); - } - - let mut names = ::std::collections::HashMap::new(); - for field in data.fields().iter() { - let id = field.ident.as_ref().expect("named field"); - let field = match Form::from_attrs("form", &field.attrs) { - Some(result) => result?.field, - None => FormField { span: Spanned::span(&id), name: id.clone().into() } - }; - - if let Some(span) = names.get(&field.name) { - return Err(field.span.error("duplicate field name") - .span_note(*span, "previous definition here")); - } +fn context_type(input: Input<'_>) -> (TokenStream, Option) { + let mut gen = input.generics().clone(); - names.insert(field.name, field.span); + let lifetime = syn::parse_quote!('__f); + if !gen.replace_lifetime(0, &lifetime) { + gen.insert_lifetime(syn::LifetimeDef::new(lifetime.clone())); } - Ok(()) + let span = input.ident().span(); + gen.add_type_bound(&syn::parse_quote!(#_form::FromForm<#lifetime>)); + gen.add_type_bound(&syn::TypeParamBound::from(lifetime)); + let (_, ty_gen, where_clause) = gen.split_for_impl(); + (quote_spanned!(span => FromFormGeneratedContext #ty_gen), where_clause.cloned()) } + pub fn derive_from_form(input: proc_macro::TokenStream) -> TokenStream { - let form_error = quote!(::rocket::request::FormParseError); - DeriveGenerator::build_for(input, quote!(impl<'__f> ::rocket::request::FromForm<'__f>)) - .generic_support(GenericSupport::Lifetime | GenericSupport::Type) + DeriveGenerator::build_for(input, quote!(impl<'__f> #_form::FromForm<'__f>)) + .support(Support::NamedStruct | Support::Lifetime | Support::Type) .replace_generic(0, 0) - .data_support(DataSupport::NamedStruct) - .map_type_generic(|_, ident, _| quote! { - #ident : ::rocket::request::FromFormValue<'__f> - }) - .validate_generics(|_, generics| match generics.lifetimes().enumerate().last() { - Some((i, lt)) if i >= 1 => Err(lt.span().error("only one lifetime is supported")), - _ => Ok(()) - }) - .validate_struct(validate_struct) - .function(|_, inner| quote! { - type Error = ::rocket::request::FormParseError<'__f>; - - fn from_form( - __items: &mut ::rocket::request::FormItems<'__f>, - __strict: bool, - ) -> ::std::result::Result { - #inner - } - }) - .try_map_fields(move |_, fields| { - define_vars_and_mods!(_None, _Some, _Ok, _Err); - let (constructors, matchers, builders) = fields.iter().map(|field| { - let (ident, span) = (&field.ident, field.span()); - let default_name = NameSource::from(ident.clone().expect("named")); - let name = Form::from_attrs("form", &field.attrs) - .map(|result| result.map(|form| form.field.name)) - .unwrap_or_else(|| Ok(default_name))?; + .type_bound(quote!(#_form::FromForm<'__f> + '__f)) + .validator(ValidatorBuild::new() + .input_validate(|_, i| match i.generics().lifetimes().enumerate().last() { + Some((i, lt)) if i >= 1 => Err(lt.span().error("only one lifetime is supported")), + _ => Ok(()) + }) + .fields_validate(|_, fields| { + if fields.is_empty() { + return Err(fields.span().error("at least one field is required")); + } - let ty = field.ty.with_stripped_lifetimes(); - let ty = quote_spanned! { - span => <#ty as ::rocket::request::FromFormValue> - }; - - let constructor = quote_spanned!(span => let mut #ident = #_None;); - - let name = name.name(); - let matcher = quote_spanned! { span => - #name => { #ident = #_Some(#ty::from_form_value(__v) - .map_err(|_| #form_error::BadValue(__k, __v))?); }, - }; - - let builder = quote_spanned! { span => - #ident: #ident.or_else(#ty::default) - .ok_or_else(|| #form_error::Missing(#name.into()))?, - }; - - Ok((constructor, matcher, builder)) - }).collect::>>()?.into_iter().split3(); - - Ok(quote! { - #(#constructors)* - - for (__k, __v) in __items.map(|item| item.key_value()) { - match __k.as_str() { - #(#matchers)* - _ if __strict && __k != "_method" => { - return #_Err(#form_error::Unknown(__k, __v)); + let mut names = ::std::collections::HashMap::new(); + for field in fields.iter() { + let name = field.field_name()?; + if let Some(span) = names.get(&name) { + return Err(field.span().error("duplicate form field") + .span_note(*span, "previously defined here")); + } + + names.insert(name, field.span()); + } + + Ok(()) + }) + ) + .outer_mapper(MapperBuild::new() + .try_input_map(|mapper, input| { + let (ctxt_ty, where_clause) = context_type(input); + let output = mapper::input_default(mapper, input)?; + Ok(quote! { + /// Rocket generated FormForm context. + #[doc(hidden)] + pub struct #ctxt_ty #where_clause { + __opts: #_form::Options, + __errors: #_form::Errors<'__f>, + __parent: #_Option<&'__f #_form::Name>, + #output + } + }) + }) + .try_fields_map(|m, f| mapper::fields_null(m, f)) + .field_map(|_, field| { + let (ident, mut ty) = (field.ident(), field.stripped_ty()); + ty.replace_lifetimes(syn::parse_quote!('__f)); + let field_ty = quote_respanned!(ty.span() => + #_Option<<#ty as #_form::FromForm<'__f>>::Context> + ); + + quote_spanned!(ty.span() => #ident: #field_ty,) + }) + ) + .outer_mapper(quote!(#[rocket::async_trait])) + .inner_mapper(MapperBuild::new() + .try_input_map(|mapper, input| { + let (ctxt_ty, _) = context_type(input); + let output = mapper::input_default(mapper, input)?; + Ok(quote! { + type Context = #ctxt_ty; + + fn init(__opts: #_form::Options) -> Self::Context { + Self::Context { + __opts, + __errors: #_form::Errors::new(), + __parent: #_None, + #output } - _ => { /* lenient or "method"; let it pass */ } } + }) + }) + .try_fields_map(|m, f| mapper::fields_null(m, f)) + .field_map(|_, field| { + let ident = field.ident.as_ref().expect("named"); + let ty = field.ty.with_stripped_lifetimes(); + quote_spanned!(ty.span() => + #ident: #_None, + // #ident: <#ty as #_form::FromForm<'__f>>::init(__opts), + ) + }) + ) + .inner_mapper(MapperBuild::new() + .with_output(|_, output| quote! { + fn push_value(__c: &mut Self::Context, __f: #_form::ValueField<'__f>) { + #output } + }) + .try_fields_map(|_, f| fields_map(f, |ty, ctxt| quote_spanned!(ty.span() => { + <#ty as #_form::FromForm<'__f>>::push_value(#ctxt, __f.shift()); + }))) + ) + .inner_mapper(MapperBuild::new() + .try_input_map(|mapper, input| { + let (ctxt_ty, _) = context_type(input); + let output = mapper::input_default(mapper, input)?; + Ok(quote! { + async fn push_data( + __c: &mut #ctxt_ty, + __f: #_form::DataField<'__f, '_> + ) { + #output + } + }) + }) + // Without the `let _fut`, we get a wild lifetime error. It don't + // make no sense, Rust async/await, it don't make no sense. + .try_fields_map(|_, f| fields_map(f, |ty, ctxt| quote_spanned!(ty.span() => { + let _fut = <#ty as #_form::FromForm<'__f>>::push_data(#ctxt, __f.shift()); + _fut.await; + }))) + ) + .inner_mapper(MapperBuild::new() + .with_output(|_, output| quote! { + fn finalize(mut __c: Self::Context) -> #_Result> { + #[allow(unused_imports)] + use #_form::validate::*; - #_Ok(Self { #(#builders)* }) + #output + } }) - }) - .to_tokens2() + .try_fields_map(|mapper, fields| { + let finalize_field = fields.iter() + .map(|f| mapper.map_field(f)) + .collect::>>()?; + + let ident: Vec<_> = fields.iter() + .map(|f| f.ident().clone()) + .collect(); + + let o = syn::Ident::new("__o", fields.span()); + let (_ok, _some, _err, _none) = (_Ok, _Some, _Err, _None); + let (name_view, validate) = fields.iter() + .map(|f| (f.name_view().unwrap(), validators(f, &o, false).unwrap())) + .map(|(nv, vs)| vs.map(move |v| (nv.clone(), v))) + .flatten() + .split2(); + + Ok(quote_spanned! { fields.span() => + // if __c.__parent.is_none() { + // let _e = #_form::Error::from(#_form::ErrorKind::Missing) + // .with_entity(#_form::Entity::Form); + // + // return #_Err(_e.into()); + // } + + #(let #ident = match #finalize_field { + #_ok(#ident) => #_some(#ident), + #_err(_e) => { __c.__errors.extend(_e); #_none } + };)* + + if !__c.__errors.is_empty() { + return #_Err(__c.__errors); + } + + let #o = Self { #(#ident: #ident.unwrap()),* }; + + #( + if let #_err(_e) = #validate { + __c.__errors.extend(_e.with_name(#name_view)); + } + )* + + if !__c.__errors.is_empty() { + return #_Err(__c.__errors); + } + + Ok(#o) + }) + }) + .try_field_map(|_, f| { + let (ident, ty, name_view) = (f.ident(), f.stripped_ty(), f.name_view()?); + let validator = validators(f, &ident, true)?; + let _err = _Err; + Ok(quote_spanned! { ty.span() => { + let _name = #name_view; + __c.#ident + .map(<#ty as #_form::FromForm<'__f>>::finalize) + .unwrap_or_else(|| <#ty as #_form::FromForm<'__f>>::default() + .ok_or_else(|| #_form::ErrorKind::Missing.into()) + ) + // <#ty as #_form::FromForm<'__f>>::finalize(__c.#ident) + .and_then(|#ident| { + let mut _es = #_form::Errors::new(); + #(if let #_err(_e) = #validator { _es.extend(_e); })* + + match _es.is_empty() { + true => #_Ok(#ident), + false => #_Err(_es) + } + }) + .map_err(|_e| _e.with_name(_name)) + .map_err(|_e| match _e.is_empty() { + true => #_form::ErrorKind::Unknown.into(), + false => _e, + }) + }}) + }) + ) + // .inner_mapper(MapperBuild::new() + // .with_output(|_, output| quote! { + // fn default() -> #_Option { + // Some(Self { #output }) + // } + // }) + // .try_fields_map(|m, f| mapper::fields_null(m, f)) + // .field_map(|_, field| { + // let ident = field.ident.as_ref().expect("named"); + // let ty = field.ty.with_stripped_lifetimes(); + // quote_spanned!(ty.span() => + // #ident: <#ty as #_form::FromForm<'__f>>::default()?, + // ) + // }) + // ) + .to_tokens() } diff --git a/core/codegen/src/derive/from_form_field.rs b/core/codegen/src/derive/from_form_field.rs new file mode 100644 index 0000000000..eb8a6e435d --- /dev/null +++ b/core/codegen/src/derive/from_form_field.rs @@ -0,0 +1,75 @@ +use devise::{*, ext::SpanDiagnosticExt}; + +use crate::exports::*; +use crate::proc_macro2::TokenStream; +use crate::syn_ext::NameSource; + +#[derive(FromMeta)] +pub struct FieldAttr { + value: NameSource, +} + +pub fn derive_from_form_field(input: proc_macro::TokenStream) -> TokenStream { + DeriveGenerator::build_for(input, quote!(impl<'__v> #_form::FromFormField<'__v>)) + .support(Support::Enum) + .validator(ValidatorBuild::new() + // We only accepts C-like enums with at least one variant. + .fields_validate(|_, fields| { + if !fields.is_empty() { + return Err(fields.span().error("variants cannot have fields")); + } + + Ok(()) + }) + .enum_validate(|_, data| { + if data.variants.is_empty() { + return Err(data.span().error("enum must have at least one variant")); + } + + Ok(()) + }) + ) + // TODO: Devise should have a try_variant_map. + .inner_mapper(MapperBuild::new() + .try_enum_map(|_, data| { + let variant_name_sources = data.variants() + .map(|v| FieldAttr::one_from_attrs("field", &v.attrs).map(|o| { + o.map(|f| f.value).unwrap_or_else(|| v.ident.clone().into()) + })) + .collect::>>()?; + + let variant_name = variant_name_sources.iter() + .map(|n| n.name()) + .collect::>(); + + let builder = data.variants() + .map(|v| v.builder(|_| unreachable!("fieldless"))); + + let (_ok, _cow) = (std::iter::repeat(_Ok), std::iter::repeat(_Cow)); + Ok(quote! { + fn from_value( + __f: #_form::ValueField<'__v> + ) -> Result> { + #[allow(unused_imports)] + use #_http::uncased::AsUncased; + + #( + if __f.value.as_uncased() == #variant_name { + return #_ok(#builder); + } + )* + + const OPTS: &'static [#_Cow<'static, str>] = + &[#(#_cow::Borrowed(#variant_name)),*]; + + let _error = #_form::Error::from(OPTS) + .with_name(__f.name) + .with_value(__f.value); + + #_Err(_error)? + } + }) + }) + ) + .to_tokens() +} diff --git a/core/codegen/src/derive/from_form_value.rs b/core/codegen/src/derive/from_form_value.rs deleted file mode 100644 index b5ad334430..0000000000 --- a/core/codegen/src/derive/from_form_value.rs +++ /dev/null @@ -1,59 +0,0 @@ -use devise::{*, ext::SpanDiagnosticExt}; - -use crate::proc_macro2::TokenStream; -use crate::syn_ext::NameSource; - -#[derive(FromMeta)] -struct Form { - value: NameSource, -} - -pub fn derive_from_form_value(input: proc_macro::TokenStream) -> TokenStream { - define_vars_and_mods!(_Ok, _Err, _Result); - DeriveGenerator::build_for(input, quote!(impl<'__v> ::rocket::request::FromFormValue<'__v>)) - .generic_support(GenericSupport::None) - .data_support(DataSupport::Enum) - .validate_enum(|_, data| { - // This derive only works for variants that are nullary. - for variant in data.variants() { - if !variant.fields().is_empty() { - return Err(variant.fields().span().error("variants cannot have fields")); - } - } - - // Emit a warning if the enum is empty. - if data.variants.is_empty() { - return Err(data.brace_token.span.error("enum must have at least one field")); - } - - Ok(()) - }) - .function(move |_, inner| quote! { - type Error = &'__v ::rocket::http::RawStr; - - fn from_form_value( - value: &'__v ::rocket::http::RawStr - ) -> #_Result { - let uncased = value.as_uncased_str(); - #inner - #_Err(value) - } - }) - .try_map_enum(null_enum_mapper) - .try_map_variant(|_, variant| { - define_vars_and_mods!(_Ok); - let variant_name_source = Form::from_attrs("form", &variant.attrs) - .unwrap_or_else(|| Ok(Form { value: variant.ident.clone().into() }))? - .value; - - let variant_str = variant_name_source.name(); - - let builder = variant.builder(|_| unreachable!("no fields")); - Ok(quote! { - if uncased == #variant_str { - return #_Ok(#builder); - } - }) - }) - .to_tokens2() -} diff --git a/core/codegen/src/derive/mod.rs b/core/codegen/src/derive/mod.rs index 8fd1b7e4fb..134279e733 100644 --- a/core/codegen/src/derive/mod.rs +++ b/core/codegen/src/derive/mod.rs @@ -1,4 +1,5 @@ +mod form_field; pub mod from_form; -pub mod from_form_value; +pub mod from_form_field; pub mod responder; pub mod uri_display; diff --git a/core/codegen/src/derive/responder.rs b/core/codegen/src/derive/responder.rs index 9728efb2f2..88067da987 100644 --- a/core/codegen/src/derive/responder.rs +++ b/core/codegen/src/derive/responder.rs @@ -1,11 +1,11 @@ - use quote::ToTokens; use devise::{*, ext::{TypeExt, SpanDiagnosticExt}}; +use crate::exports::*; use crate::proc_macro2::TokenStream; use crate::http_codegen::{ContentType, Status}; -#[derive(Default, FromMeta)] +#[derive(Debug, Default, FromMeta)] struct ItemAttr { content_type: Option>, status: Option>, @@ -18,65 +18,64 @@ struct FieldAttr { pub fn derive_responder(input: proc_macro::TokenStream) -> TokenStream { DeriveGenerator::build_for(input, quote!(impl<'__r, '__o: '__r> ::rocket::response::Responder<'__r, '__o>)) - .generic_support(GenericSupport::Lifetime) - .data_support(DataSupport::Struct | DataSupport::Enum) + .support(Support::Struct | Support::Enum | Support::Lifetime) .replace_generic(1, 0) - .validate_generics(|_, generics| match generics.lifetimes().count() > 1 { - true => Err(generics.span().error("only one lifetime is supported")), - false => Ok(()) - }) - .validate_fields(|_, fields| match fields.is_empty() { - true => return Err(fields.span().error("need at least one field")), - false => Ok(()) - }) - .function(|_, inner| quote! { - fn respond_to( - self, - __req: &'__r ::rocket::request::Request - ) -> ::rocket::response::Result<'__o> { - #inner - } - }) - .try_map_fields(|_, fields| { - define_vars_and_mods!(_Ok); - fn set_header_tokens(item: T) -> TokenStream { - quote_spanned!(item.span().into() => __res.set_header(#item);) - } + .validator(ValidatorBuild::new() + .input_validate(|_, i| match i.generics().lifetimes().count() > 1 { + true => Err(i.generics().span().error("only one lifetime is supported")), + false => Ok(()) + }) + .fields_validate(|_, fields| match fields.is_empty() { + true => return Err(fields.span().error("need at least one field")), + false => Ok(()) + }) + ) + .inner_mapper(MapperBuild::new() + .with_output(|_, output| quote! { + fn respond_to(self, __req: &'__r #Request) -> #_response::Result<'__o> { + #output + } + }) + .try_fields_map(|_, fields| { + fn set_header_tokens(item: T) -> TokenStream { + quote_spanned!(item.span().into() => __res.set_header(#item);) + } - let attr = ItemAttr::from_attrs("response", fields.parent.attrs()) - .unwrap_or_else(|| Ok(Default::default()))?; + let attr = ItemAttr::one_from_attrs("response", fields.parent.attrs())? + .unwrap_or_default(); - let responder = fields.iter().next().map(|f| { - let (accessor, ty) = (f.accessor(), f.ty.with_stripped_lifetimes()); - quote_spanned! { f.span().into() => - let mut __res = <#ty as ::rocket::response::Responder>::respond_to( - #accessor, __req - )?; - } - }).expect("have at least one field"); + let responder = fields.iter().next().map(|f| { + let (accessor, ty) = (f.accessor(), f.ty.with_stripped_lifetimes()); + quote_spanned! { f.span().into() => + let mut __res = <#ty as ::rocket::response::Responder>::respond_to( + #accessor, __req + )?; + } + }).expect("have at least one field"); - let mut headers = vec![]; - for field in fields.iter().skip(1) { - let attr = FieldAttr::from_attrs("response", &field.attrs) - .unwrap_or_else(|| Ok(Default::default()))?; + let mut headers = vec![]; + for field in fields.iter().skip(1) { + let attr = FieldAttr::one_from_attrs("response", &field.attrs)? + .unwrap_or_default(); - if !attr.ignore { - headers.push(set_header_tokens(field.accessor())); + if !attr.ignore { + headers.push(set_header_tokens(field.accessor())); + } } - } - let content_type = attr.content_type.map(set_header_tokens); - let status = attr.status.map(|status| { - quote_spanned!(status.span().into() => __res.set_status(#status);) - }); + let content_type = attr.content_type.map(set_header_tokens); + let status = attr.status.map(|status| { + quote_spanned!(status.span().into() => __res.set_status(#status);) + }); - Ok(quote! { - #responder - #(#headers)* - #content_type - #status - #_Ok(__res) + Ok(quote! { + #responder + #(#headers)* + #content_type + #status + #_Ok(__res) + }) }) - }) - .to_tokens2() + ) + .to_tokens() } diff --git a/core/codegen/src/derive/uri_display.rs b/core/codegen/src/derive/uri_display.rs index a5ec80c1aa..0a5d76361e 100644 --- a/core/codegen/src/derive/uri_display.rs +++ b/core/codegen/src/derive/uri_display.rs @@ -1,7 +1,9 @@ use devise::{*, ext::SpanDiagnosticExt}; +use rocket_http::uri::UriPart; -use crate::derive::from_form::Form; +use crate::exports::*; +use crate::derive::form_field::FieldExt; use crate::proc_macro2::TokenStream; const NO_EMPTY_FIELDS: &str = "fieldless structs or variants are not supported"; @@ -10,113 +12,69 @@ const NO_EMPTY_ENUMS: &str = "empty enums are not supported"; const ONLY_ONE_UNNAMED: &str = "tuple structs or variants must have exactly one field"; const EXACTLY_ONE_FIELD: &str = "struct must have exactly one field"; -fn validate_fields(ident: &syn::Ident, fields: Fields<'_>) -> Result<()> { +fn validate_fields(fields: Fields<'_>) -> Result<()> { if fields.count() == 0 { - return Err(ident.span().error(NO_EMPTY_FIELDS)) + return Err(fields.parent.span().error(NO_EMPTY_FIELDS)) } else if fields.are_unnamed() && fields.count() > 1 { - return Err(fields.span.error(ONLY_ONE_UNNAMED)); + return Err(fields.span().error(ONLY_ONE_UNNAMED)); } else if fields.are_unit() { - return Err(fields.span.error(NO_NULLARY)); + return Err(fields.span().error(NO_NULLARY)); } Ok(()) } -fn validate_enum(_: &DeriveGenerator, data: Enum<'_>) -> Result<()> { +fn validate_enum(data: Enum<'_>) -> Result<()> { if data.variants().count() == 0 { return Err(data.brace_token.span.error(NO_EMPTY_ENUMS)); } - for variant in data.variants() { - validate_fields(&variant.ident, variant.fields())?; - } - Ok(()) } -#[allow(non_snake_case)] pub fn derive_uri_display_query(input: proc_macro::TokenStream) -> TokenStream { - let Query = quote!(::rocket::http::uri::Query); - let UriDisplay = quote!(::rocket::http::uri::UriDisplay<#Query>); - let Formatter = quote!(::rocket::http::uri::Formatter<#Query>); - let FromUriParam = quote!(::rocket::http::uri::FromUriParam); - - let uri_display = DeriveGenerator::build_for(input.clone(), quote!(impl #UriDisplay)) - .data_support(DataSupport::Struct | DataSupport::Enum) - .generic_support(GenericSupport::Type | GenericSupport::Lifetime) - .validate_enum(validate_enum) - .validate_struct(|gen, data| validate_fields(&gen.input.ident, data.fields())) - .map_type_generic(move |_, ident, _| quote!(#ident : #UriDisplay)) - .function(move |_, inner| quote! { - fn fmt(&self, f: &mut #Formatter) -> ::std::fmt::Result { - #inner - Ok(()) - } - }) - .try_map_field(|_, field| { - let span = field.span().into(); - let accessor = field.accessor(); - let tokens = if let Some(ref ident) = field.ident { - let name_source = Form::from_attrs("form", &field.attrs) - .map(|result| result.map(|form| form.field.name)) - .unwrap_or_else(|| Ok(ident.clone().into()))?; - - let name = name_source.name(); - quote_spanned!(span => f.write_named_value(#name, &#accessor)?;) - } else { - quote_spanned!(span => f.write_value(&#accessor)?;) - }; - - Ok(tokens) - }) - .try_to_tokens(); + use crate::http::uri::Query; + + const URI_DISPLAY: StaticTokens = quote_static!(#_uri::UriDisplay<#_uri::Query>); + const FORMATTER: StaticTokens = quote_static!(#_uri::Formatter<#_uri::Query>); + + let uri_display = DeriveGenerator::build_for(input.clone(), quote!(impl #URI_DISPLAY)) + .support(Support::Struct | Support::Enum | Support::Type | Support::Lifetime) + .validator(ValidatorBuild::new() + .enum_validate(|_, v| validate_enum(v)) + .fields_validate(|_, v| validate_fields(v)) + ) + .type_bound(URI_DISPLAY) + .inner_mapper(MapperBuild::new() + .with_output(|_, output| quote! { + fn fmt(&self, f: &mut #FORMATTER) -> ::std::fmt::Result { + #output + Ok(()) + } + }) + .try_field_map(|_, field| { + let span = field.span().into(); + let accessor = field.accessor(); + let tokens = if field.ident.is_some() { + let name = field.field_name()?; + quote_spanned!(span => f.write_named_value(#name, &#accessor)?;) + } else { + quote_spanned!(span => f.write_value(&#accessor)?;) + }; + + Ok(tokens) + }) + ) + .try_to_tokens::(); let uri_display = match uri_display { Ok(tokens) => tokens, Err(diag) => return diag.emit_as_item_tokens() }; - let i = input.clone(); - let gen_trait = quote!(impl #FromUriParam<#Query, Self>); - let UriDisplay = quote!(::rocket::http::uri::UriDisplay<#Query>); - let from_self = DeriveGenerator::build_for(i, gen_trait) - .data_support(DataSupport::Struct | DataSupport::Enum) - .generic_support(GenericSupport::Type | GenericSupport::Lifetime) - .map_type_generic(move |_, ident, _| quote!(#ident : #UriDisplay)) - .function(|_, _| quote! { - type Target = Self; - #[inline(always)] - fn from_uri_param(param: Self) -> Self { param } - }) - .to_tokens(); - - let i = input.clone(); - let gen_trait = quote!(impl<'__r> #FromUriParam<#Query, &'__r Self>); - let UriDisplay = quote!(::rocket::http::uri::UriDisplay<#Query>); - let from_ref = DeriveGenerator::build_for(i, gen_trait) - .data_support(DataSupport::Struct | DataSupport::Enum) - .generic_support(GenericSupport::Type | GenericSupport::Lifetime) - .map_type_generic(move |_, ident, _| quote!(#ident : #UriDisplay)) - .function(|_, _| quote! { - type Target = &'__r Self; - #[inline(always)] - fn from_uri_param(param: &'__r Self) -> &'__r Self { param } - }) - .to_tokens(); - - let i = input.clone(); - let gen_trait = quote!(impl<'__r> #FromUriParam<#Query, &'__r mut Self>); - let UriDisplay = quote!(::rocket::http::uri::UriDisplay<#Query>); - let from_mut = DeriveGenerator::build_for(i, gen_trait) - .data_support(DataSupport::Struct | DataSupport::Enum) - .generic_support(GenericSupport::Type | GenericSupport::Lifetime) - .map_type_generic(move |_, ident, _| quote!(#ident : #UriDisplay)) - .function(|_, _| quote! { - type Target = &'__r mut Self; - #[inline(always)] - fn from_uri_param(param: &'__r mut Self) -> &'__r mut Self { param } - }) - .to_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 mut ts = TokenStream::from(uri_display); ts.extend(TokenStream::from(from_self)); @@ -127,67 +85,72 @@ 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 { - let Path = quote!(::rocket::http::uri::Path); - let UriDisplay = quote!(::rocket::http::uri::UriDisplay<#Path>); - let Formatter = quote!(::rocket::http::uri::Formatter<#Path>); - let FromUriParam = quote!(::rocket::http::uri::FromUriParam); - - let uri_display = DeriveGenerator::build_for(input.clone(), quote!(impl #UriDisplay)) - .data_support(DataSupport::TupleStruct) - .generic_support(GenericSupport::Type | GenericSupport::Lifetime) - .map_type_generic(move |_, ident, _| quote!(#ident : #UriDisplay)) - .validate_fields(|_, fields| match fields.count() { - 1 => Ok(()), - _ => Err(fields.span.error(EXACTLY_ONE_FIELD)) - }) - .function(move |_, inner| quote! { - fn fmt(&self, f: &mut #Formatter) -> ::std::fmt::Result { - #inner - Ok(()) - } - }) - .map_field(|_, field| { - let span = field.span().into(); - let accessor = field.accessor(); - quote_spanned!(span => f.write_value(&#accessor)?;) - }) - .try_to_tokens(); + use crate::http::uri::Path; + + const URI_DISPLAY: StaticTokens = quote_static!(#_uri::UriDisplay<#_uri::Path>); + const FORMATTER: StaticTokens = quote_static!(#_uri::Formatter<#_uri::Path>); + + let uri_display = DeriveGenerator::build_for(input.clone(), quote!(impl #URI_DISPLAY)) + .support(Support::TupleStruct | Support::Type | Support::Lifetime) + .type_bound(URI_DISPLAY) + .validator(ValidatorBuild::new() + .fields_validate(|_, fields| match fields.count() { + 1 => Ok(()), + _ => Err(fields.span().error(EXACTLY_ONE_FIELD)) + }) + ) + .inner_mapper(MapperBuild::new() + .with_output(|_, output| quote! { + fn fmt(&self, f: &mut #FORMATTER) -> ::std::fmt::Result { + #output + Ok(()) + } + }) + .field_map(|_, field| { + let accessor = field.accessor(); + quote_spanned!(field.span() => f.write_value(&#accessor)?;) + }) + ) + .try_to_tokens::(); let uri_display = match uri_display { Ok(tokens) => tokens, Err(diag) => return diag.emit_as_item_tokens() }; - let i = input.clone(); - let gen_trait = quote!(impl #FromUriParam<#Path, Self>); - let UriDisplay = quote!(::rocket::http::uri::UriDisplay<#Path>); - let from_self = DeriveGenerator::build_for(i, gen_trait) - .data_support(DataSupport::All) - .generic_support(GenericSupport::Type | GenericSupport::Lifetime) - .map_type_generic(move |_, ident, _| quote!(#ident : #UriDisplay)) - .function(|_, _| quote! { - type Target = Self; - #[inline(always)] - fn from_uri_param(param: Self) -> Self { param } - }) - .to_tokens(); - - let i = input.clone(); - let gen_trait = quote!(impl<'__r> #FromUriParam<#Path, &'__r Self>); - let UriDisplay = quote!(::rocket::http::uri::UriDisplay<#Path>); - let from_ref = DeriveGenerator::build_for(i, gen_trait) - .data_support(DataSupport::All) - .generic_support(GenericSupport::Type | GenericSupport::Lifetime) - .map_type_generic(move |_, ident, _| quote!(#ident : #UriDisplay)) - .function(|_, _| quote! { - type Target = &'__r Self; - #[inline(always)] - fn from_uri_param(param: &'__r Self) -> &'__r Self { param } - }) - .to_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 mut ts = TokenStream::from(uri_display); ts.extend(TokenStream::from(from_self)); ts.extend(TokenStream::from(from_ref)); + ts.extend(TokenStream::from(from_mut)); ts.into() } + +fn from_uri_param(input: proc_macro::TokenStream, ty: TokenStream) -> TokenStream { + let part = match P::DELIMITER { + '/' => quote!(#_uri::Path), + '&' => quote!(#_uri::Query), + _ => unreachable!("sealed trait with path/query") + }; + + let ty: syn::Type = syn::parse2(ty).expect("valid type"); + let gen = match ty { + syn::Type::Reference(ref r) => r.lifetime.as_ref().map(|l| quote!(<#l>)), + _ => None + }; + + let param_trait = quote!(impl #gen #_uri::FromUriParam<#part, #ty>); + DeriveGenerator::build_for(input, param_trait) + .support(Support::All) + .type_bound(quote!(#_uri::UriDisplay<#part>)) + .inner_mapper(MapperBuild::new() + .with_output(move |_, _| quote! { + type Target = #ty; + #[inline(always)] fn from_uri_param(_p: #ty) -> #ty { _p } + }) + ) + .to_tokens() +} diff --git a/core/codegen/src/exports.rs b/core/codegen/src/exports.rs new file mode 100644 index 0000000000..aab38e3653 --- /dev/null +++ b/core/codegen/src/exports.rs @@ -0,0 +1,105 @@ +use crate::syn; +use crate::proc_macro2::{Span, TokenStream}; +use crate::quote::{ToTokens, TokenStreamExt}; + +#[derive(Debug, Copy, Clone)] +pub struct StaticPath(pub Option, pub &'static str); + +#[derive(Debug, Copy, Clone)] +pub struct StaticTokens(pub fn() -> TokenStream); + +macro_rules! quote_static { + ($($token:tt)*) => { + $crate::exports::StaticTokens(|| quote!($($token)*)) + } +} + +impl ToTokens for StaticTokens { + fn to_tokens(&self, tokens: &mut TokenStream) { + tokens.append_all((self.0)()); + } +} + +impl StaticPath { + pub fn respanned(mut self, span: Span) -> Self { + self.0 = Some(span); + self + } +} + +impl ToTokens for StaticPath { + fn to_tokens(&self, tokens: &mut TokenStream) { + let path: syn::Path = syn::parse_str(self.1).unwrap(); + if let Some(span) = self.0 { + let new_tokens = path.into_token_stream() + .into_iter() + .map(|mut t| { t.set_span(span); t }); + + tokens.append_all(new_tokens); + } else { + path.to_tokens(tokens) + } + } +} + +macro_rules! define_exported_paths { + ($($name:ident => $path:path),* $(,)?) => { + $( + #[allow(dead_code)] + #[allow(non_upper_case_globals)] + pub const $name: StaticPath = $crate::exports::StaticPath(None, stringify!($path)); + )* + + macro_rules! define { + // Note: the `i` is to capture the input's span. + $(($span:expr => $i:ident $name) => { + #[allow(non_snake_case)] + let $i = $crate::exports::StaticPath(Some($span), stringify!($path)); + };)* + } + }; +} + +define_exported_paths! { + __req => __req, + __status => __status, + __catcher => __catcher, + __data => __data, + __error => __error, + __trail => __trail, + _request => rocket::request, + _response => rocket::response, + _handler => rocket::handler, + _log => rocket::logger, + _form => rocket::form::prelude, + _http => rocket::http, + _uri => rocket::http::uri, + _Option => ::std::option::Option, + _Result => ::std::result::Result, + _Some => ::std::option::Option::Some, + _None => ::std::option::Option::None, + _Ok => ::std::result::Result::Ok, + _Err => ::std::result::Result::Err, + _Box => ::std::boxed::Box, + _Vec => ::std::vec::Vec, + _Cow => ::std::borrow::Cow, + BorrowMut => ::std::borrow::BorrowMut, + Outcome => rocket::outcome::Outcome, + FromForm => rocket::form::FromForm, + FromData => rocket::data::FromData, + Request => rocket::request::Request, + Response => rocket::response::Response, + Data => rocket::data::Data, + StaticRouteInfo => rocket::StaticRouteInfo, + StaticCatcherInfo => rocket::StaticCatcherInfo, + Route => rocket::Route, + Catcher => rocket::Catcher, + SmallVec => rocket::http::private::SmallVec, + Status => rocket::http::Status, + HandlerFuture => rocket::handler::HandlerFuture, + ErrorHandlerFuture => rocket::catcher::ErrorHandlerFuture, +} + +macro_rules! define_spanned_export { + ($span:expr => $($name:ident),*) => ($(define!($span => $name $name);)*) +} diff --git a/core/codegen/src/http_codegen.rs b/core/codegen/src/http_codegen.rs index 27df795228..674651a40a 100644 --- a/core/codegen/src/http_codegen.rs +++ b/core/codegen/src/http_codegen.rs @@ -30,7 +30,7 @@ pub struct DataSegment(pub Segment); pub struct Optional(pub Option); impl FromMeta for StringLit { - fn from_meta(meta: MetaItem<'_>) -> Result { + fn from_meta(meta: &MetaItem) -> Result { Ok(StringLit::new(String::from_meta(meta)?, meta.value_span())) } } @@ -43,7 +43,7 @@ pub struct RoutePath { } impl FromMeta for Status { - fn from_meta(meta: MetaItem<'_>) -> Result { + fn from_meta(meta: &MetaItem) -> Result { let num = usize::from_meta(meta)?; if num < 100 || num >= 600 { return Err(meta.value_span().error("status must be in range [100, 599]")); @@ -61,7 +61,7 @@ impl ToTokens for Status { } impl FromMeta for ContentType { - fn from_meta(meta: MetaItem<'_>) -> Result { + fn from_meta(meta: &MetaItem) -> Result { http::ContentType::parse_flexible(&String::from_meta(meta)?) .map(ContentType) .ok_or(meta.value_span().error("invalid or unknown content type")) @@ -77,7 +77,7 @@ impl ToTokens for ContentType { } impl FromMeta for MediaType { - fn from_meta(meta: MetaItem<'_>) -> Result { + fn from_meta(meta: &MetaItem) -> Result { let mt = http::MediaType::parse_flexible(&String::from_meta(meta)?) .ok_or(meta.value_span().error("invalid or unknown media type"))?; @@ -95,7 +95,7 @@ impl FromMeta for MediaType { impl ToTokens for MediaType { fn to_tokens(&self, tokens: &mut TokenStream) { let (top, sub) = (self.0.top().as_str(), self.0.sub().as_str()); - let (keys, values) = self.0.params().split2(); + let (keys, values) = self.0.params().map(|(k, v)| (k.as_str(), v)).split2(); let http = quote!(::rocket::http); tokens.extend(quote! { @@ -114,7 +114,7 @@ const VALID_METHODS: &[http::Method] = &[ ]; impl FromMeta for Method { - fn from_meta(meta: MetaItem<'_>) -> Result { + fn from_meta(meta: &MetaItem) -> Result { let span = meta.value_span(); let help_text = format!("method must be one of: {}", VALID_METHODS_STR); @@ -156,13 +156,13 @@ impl ToTokens for Method { } impl FromMeta for Origin { - fn from_meta(meta: MetaItem<'_>) -> Result { + fn from_meta(meta: &MetaItem) -> Result { let string = StringLit::from_meta(meta)?; let uri = http::uri::Origin::parse_route(&string) .map_err(|e| { - let span = string.subspan(e.index() + 1..); - span.error(format!("invalid path URI: {}", e)) + let span = string.subspan(e.index() + 1..(e.index() + 2)); + span.error(format!("invalid route URI: {}", e)) .help("expected path in origin form: \"/path/\"") })?; @@ -177,7 +177,7 @@ impl FromMeta for Origin { } impl FromMeta for DataSegment { - fn from_meta(meta: MetaItem<'_>) -> Result { + fn from_meta(meta: &MetaItem) -> Result { let string = StringLit::from_meta(meta)?; let span = string.subspan(1..(string.len() + 1)); @@ -192,7 +192,7 @@ impl FromMeta for DataSegment { } impl FromMeta for RoutePath { - fn from_meta(meta: MetaItem<'_>) -> Result { + fn from_meta(meta: &MetaItem) -> Result { let (origin, string) = (Origin::from_meta(meta)?, StringLit::from_meta(meta)?); let path_span = string.subspan(1..origin.0.path().len() + 1); let path = parse_segments::(origin.0.path(), path_span); @@ -220,9 +220,11 @@ impl FromMeta for RoutePath { impl ToTokens for Optional { fn to_tokens(&self, tokens: &mut TokenStream) { - define_vars_and_mods!(_Some, _None); + use crate::exports::{_Some, _None}; + use devise::Spanned; + let opt_tokens = match self.0 { - Some(ref val) => quote!(#_Some(#val)), + Some(ref val) => quote_spanned!(val.span() => #_Some(#val)), None => quote!(#_None) }; diff --git a/core/codegen/src/lib.rs b/core/codegen/src/lib.rs index 79316b6497..e50d55377f 100644 --- a/core/codegen/src/lib.rs +++ b/core/codegen/src/lib.rs @@ -58,63 +58,8 @@ use rocket_http as http; -macro_rules! vars_and_mods { - ($($name:ident => $path:path,)*) => { - macro_rules! define { - // Note: the `o` is to capture the input's span - $(($i:ident $name) => { - #[allow(non_snake_case)] let $i = quote!($path); - };)* - $(($span:expr => $i:ident $name) => { - #[allow(non_snake_case)] let $i = quote_spanned!($span => $path); - };)* - } - } -} - -vars_and_mods! { - req => __req, - status => __status, - catcher => __catcher, - data => __data, - error => __error, - trail => __trail, - request => rocket::request, - response => rocket::response, - handler => rocket::handler, - log => rocket::logger, - Outcome => rocket::outcome::Outcome, - FromTransformedData => rocket::data::FromTransformedData, - Transform => rocket::data::Transform, - Query => rocket::request::Query, - FromFormValue => rocket::request::FromFormValue, - Request => rocket::request::Request, - Response => rocket::response::Response, - Data => rocket::data::Data, - StaticRouteInfo => rocket::StaticRouteInfo, - StaticCatcherInfo => rocket::StaticCatcherInfo, - Route => rocket::Route, - Catcher => rocket::Catcher, - SmallVec => rocket::http::private::SmallVec, - Status => rocket::http::Status, - HandlerFuture => rocket::handler::HandlerFuture, - ErrorHandlerFuture => rocket::catcher::ErrorHandlerFuture, - _Option => ::std::option::Option, - _Result => ::std::result::Result, - _Some => ::std::option::Option::Some, - _None => ::std::option::Option::None, - _Ok => ::std::result::Result::Ok, - _Err => ::std::result::Result::Err, - _Box => ::std::boxed::Box, - _Vec => ::std::vec::Vec, -} - -macro_rules! define_vars_and_mods { - ($($name:ident),*) => ($(define!($name $name);)*); - ($span:expr => $($name:ident),*) => ($(define!($span => $name $name);)*) -} - #[macro_use] +mod exports; mod proc_macro_ext; mod derive; mod attribute; @@ -238,11 +183,11 @@ macro_rules! route_attribute { /// /// ```rust /// # #[macro_use] extern crate rocket; - /// # use rocket::request::Form; + /// # use rocket::form::Form; /// # use std::path::PathBuf; /// # #[derive(FromForm)] struct F { a: usize } /// #[get("//bar/?&closed&", data = "")] - /// # fn f(foo: usize, baz: PathBuf, msg: String, rest: Form, form: Form) { } + /// # fn f(foo: usize, baz: PathBuf, msg: String, rest: F, form: Form) { } /// ``` /// /// The type of each function argument corresponding to a dynamic @@ -256,9 +201,9 @@ macro_rules! route_attribute { /// |----------|-------------|-------------------| /// | path | `` | [`FromParam`] | /// | path | `` | [`FromSegments`] | - /// | query | `` | [`FromFormValue`] | - /// | query | `` | [`FromQuery`] | - /// | data | `` | [`FromTransformedData`] | + /// | query | `` | [`FromFormField`] | + /// | query | `` | [`FromFrom`] | + /// | data | `` | [`FromData`] | /// /// The type of each function argument that _does not_ have a /// corresponding dynamic parameter is required to implement the @@ -269,9 +214,9 @@ macro_rules! route_attribute { /// /// [`FromParam`]: ../rocket/request/trait.FromParam.html /// [`FromSegments`]: ../rocket/request/trait.FromSegments.html - /// [`FromFormValue`]: ../rocket/request/trait.FromFormValue.html - /// [`FromQuery`]: ../rocket/request/trait.FromQuery.html - /// [`FromTransformedData`]: ../rocket/data/trait.FromTransformedData.html + /// [`FromFormField`]: ../rocket/request/trait.FromFormField.html + /// [`FromForm`]: ../rocket/form/trait.FromForm.html + /// [`FromData`]: ../rocket/data/trait.FromData.html /// [`FromRequest`]: ../rocket/request/trait.FromRequest.html /// [`Route`]: ../rocket/struct.Route.html /// [`Responder`]: ../rocket/response/trait.Responder.html @@ -303,7 +248,7 @@ macro_rules! route_attribute { /// /// If a data guard fails, the request is forwarded if the /// [`Outcome`] is `Forward` or failed if the [`Outcome`] is - /// `Failure`. See [`FromTransformedData` Outcomes] for further detail. + /// `Failure`. See [`FromData` Outcomes] for further detail. /// /// If all validation succeeds, the decorated function is called. /// The returned value is used to generate a [`Response`] via the @@ -326,7 +271,7 @@ macro_rules! route_attribute { /// [`Outcome`]: ../rocket/outcome/enum.Outcome.html /// [`Response`]: ../rocket/struct.Response.html /// [`FromRequest` Outcomes]: ../rocket/request/trait.FromRequest.html#outcomes - /// [`FromTransformedData` Outcomes]: ../rocket/data/trait.FromTransformedData.html#outcomes + /// [`FromData` Outcomes]: ../rocket/data/trait.FromData.html#outcomes #[proc_macro_attribute] pub fn $name(args: TokenStream, input: TokenStream) -> TokenStream { emit!(attribute::route::route_attribute($method, args, input)) @@ -412,33 +357,30 @@ pub fn catch(args: TokenStream, input: TokenStream) -> TokenStream { emit!(attribute::catch::catch_attribute(args, input)) } -/// FIXME: Document. #[proc_macro_attribute] pub fn async_test(args: TokenStream, input: TokenStream) -> TokenStream { emit!(attribute::async_entry::async_test_attribute(args, input)) } -/// FIXME: Document. #[proc_macro_attribute] pub fn main(args: TokenStream, input: TokenStream) -> TokenStream { emit!(attribute::async_entry::main_attribute(args, input)) } -/// FIXME: Document. #[proc_macro_attribute] pub fn launch(args: TokenStream, input: TokenStream) -> TokenStream { emit!(attribute::async_entry::launch_attribute(args, input)) } -/// Derive for the [`FromFormValue`] trait. +/// Derive for the [`FromFormField`] trait. /// -/// The [`FromFormValue`] derive can be applied to enums with nullary +/// The [`FromFormField`] derive can be applied to enums with nullary /// (zero-length) fields: /// /// ```rust /// # #[macro_use] extern crate rocket; /// # -/// #[derive(FromFormValue)] +/// #[derive(FromFormField)] /// enum MyValue { /// First, /// Second, @@ -446,37 +388,36 @@ pub fn launch(args: TokenStream, input: TokenStream) -> TokenStream { /// } /// ``` /// -/// The derive generates an implementation of the [`FromFormValue`] trait for +/// The derive generates an implementation of the [`FromFormField`] trait for /// the decorated `enum`. The implementation returns successfully when the form /// value matches, case insensitively, the stringified version of a variant's /// name, returning an instance of said variant. If there is no match, an error -/// ([`FromFormValue::Error`]) of type [`&RawStr`] is returned, the value of -/// which is the raw form field value that failed to match. +/// recording all of the available options is returned. /// /// As an example, for the `enum` above, the form values `"first"`, `"FIRST"`, /// `"fiRSt"`, and so on would parse as `MyValue::First`, while `"second"` and /// `"third"` would parse as `MyValue::Second` and `MyValue::Third`, /// respectively. /// -/// The `form` field attribute can be used to change the string that is compared -/// against for a given variant: +/// The `field` field attribute can be used to change the string value that is +/// compared against for a given variant: /// /// ```rust /// # #[macro_use] extern crate rocket; /// # -/// #[derive(FromFormValue)] +/// #[derive(FromFormField)] /// enum MyValue { /// First, /// Second, -/// #[form(value = "fourth")] +/// #[field(value = "fourth")] /// Third, /// } /// ``` /// -/// The `#[form]` attribute's grammar is: +/// The `#[field]` attribute's grammar is: /// /// ```text -/// form := 'field' '=' STRING_LIT +/// field := 'value' '=' STRING_LIT /// /// STRING_LIT := any valid string literal, as defined by Rust /// ``` @@ -486,13 +427,12 @@ pub fn launch(args: TokenStream, input: TokenStream) -> TokenStream { /// variant. In the example above, the the strings `"fourth"`, `"FOUrth"` and so /// on would parse as `MyValue::Third`. /// -/// [`FromFormValue`]: ../rocket/request/trait.FromFormValue.html -/// [`FromFormValue::Error`]: ../rocket/request/trait.FromFormValue.html#associatedtype.Error -/// [`&RawStr`]: ../rocket/http/struct.RawStr.html +/// [`FromFormField`]: ../rocket/request/trait.FromFormField.html +/// [`FromFormField::Error`]: ../rocket/request/trait.FromFormField.html#associatedtype.Error // FIXME(rustdoc): We should be able to refer to items in `rocket`. -#[proc_macro_derive(FromFormValue, attributes(form))] -pub fn derive_from_form_value(input: TokenStream) -> TokenStream { - emit!(derive::from_form_value::derive_from_form_value(input)) +#[proc_macro_derive(FromFormField, attributes(field))] +pub fn derive_from_form_field(input: TokenStream) -> TokenStream { + emit!(derive::from_form_field::derive_from_form_field(input)) } /// Derive for the [`FromForm`] trait. @@ -509,22 +449,26 @@ pub fn derive_from_form_value(input: TokenStream) -> TokenStream { /// } /// ``` /// -/// Each field's type is required to implement [`FromFormValue`]. +/// Each field's type is required to implement [`FromFormField`]. /// /// The derive generates an implementation of the [`FromForm`] trait. The /// implementation parses a form whose field names match the field names of the /// structure on which the derive was applied. Each field's value is parsed with -/// the [`FromFormValue`] implementation of the field's type. The `FromForm` +/// the [`FromFormField`] implementation of the field's type. The `FromForm` /// implementation succeeds only when all of the field parses succeed. If /// parsing fails, an error ([`FromForm::Error`]) of type [`FormParseError`] is /// returned. /// -/// The derive accepts one field attribute: `form`, with the following syntax: +/// The derive accepts one field attribute: `field`, with the following syntax: /// /// ```text -/// form := 'field' '=' '"' IDENT '"' +/// field := name? validate* +/// +/// name := 'name' '=' '"' IDENT '"' +/// validate := 'validate' '=' EXPR /// /// IDENT := valid identifier, as defined by Rust +/// EXPR := valid expression, as defined by Rust /// ``` /// /// When applied, the attribute looks as follows: @@ -535,22 +479,22 @@ pub fn derive_from_form_value(input: TokenStream) -> TokenStream { /// #[derive(FromForm)] /// struct MyStruct { /// field: usize, -/// #[form(field = "renamed_field")] +/// #[field(name = "renamed_field")] /// other: String /// } /// ``` /// /// The field attribute directs that a different incoming field name is -/// expected, and the value of the `field` attribute is used instead of the -/// structure's actual field name when parsing a form. In the example above, the -/// value of the `MyStruct::other` struct field will be parsed from the incoming -/// form's `renamed_field` field. +/// expected, the value of `name`, which is used instead of the structure's +/// actual field name when parsing a form. In the example above, the value of +/// the `MyStruct::other` struct field will be parsed from the incoming form's +/// `renamed_field` field. /// /// [`FromForm`]: ../rocket/request/trait.FromForm.html -/// [`FromFormValue`]: ../rocket/request/trait.FromFormValue.html +/// [`FromFormField`]: ../rocket/request/trait.FromFormField.html /// [`FormParseError`]: ../rocket/request/enum.FormParseError.html /// [`FromForm::Error`]: ../rocket/request/trait.FromForm.html#associatedtype.Error -#[proc_macro_derive(FromForm, attributes(form))] +#[proc_macro_derive(FromForm, attributes(field))] pub fn derive_from_form(input: TokenStream) -> TokenStream { emit!(derive::from_form::derive_from_form(input)) } @@ -716,10 +660,10 @@ pub fn derive_responder(input: TokenStream) -> TokenStream { /// `name` parameter, and [`Formatter::write_value()`] for every unnamed field /// in the order the fields are declared. /// -/// The derive accepts one field attribute: `form`, with the following syntax: +/// The derive accepts one field attribute: `field`, with the following syntax: /// /// ```text -/// form := 'field' '=' '"' IDENT '"' +/// field := 'name' '=' '"' IDENT '"' /// /// IDENT := valid identifier, as defined by Rust /// ``` @@ -734,21 +678,21 @@ pub fn derive_responder(input: TokenStream) -> TokenStream { /// struct MyStruct { /// name: String, /// id: usize, -/// #[form(field = "type")] +/// #[field(name = "type")] /// kind: Kind, /// } /// ``` /// /// The field attribute directs that a different field name be used when calling /// [`Formatter::write_named_value()`] for the given field. The value of the -/// `field` attribute is used instead of the structure's actual field name. In +/// `name` attribute is used instead of the structure's actual field name. In /// the example above, the field `MyStruct::kind` is rendered with a name of /// `type`. /// /// [`UriDisplay`]: ../rocket/http/uri/trait.UriDisplay.html /// [`Formatter::write_named_value()`]: ../rocket/http/uri/struct.Formatter.html#method.write_named_value /// [`Formatter::write_value()`]: ../rocket/http/uri/struct.Formatter.html#method.write_value -#[proc_macro_derive(UriDisplayQuery, attributes(form))] +#[proc_macro_derive(UriDisplayQuery, attributes(field))] pub fn derive_uri_display_query(input: TokenStream) -> TokenStream { emit!(derive::uri_display::derive_uri_display_query(input)) } @@ -1030,6 +974,6 @@ pub fn rocket_internal_uri(input: TokenStream) -> TokenStream { #[doc(hidden)] #[proc_macro] -pub fn rocket_internal_guide_tests(input: TokenStream) -> TokenStream { +pub fn internal_guide_tests(input: TokenStream) -> TokenStream { emit!(bang::guide_tests_internal(input)) } diff --git a/core/codegen/src/syn_ext.rs b/core/codegen/src/syn_ext.rs index c9b0165481..6ebabef3dc 100644 --- a/core/codegen/src/syn_ext.rs +++ b/core/codegen/src/syn_ext.rs @@ -1,11 +1,14 @@ //! Extensions to `syn` types. use devise::ext::SpanDiagnosticExt; -use devise::syn::{self, Ident, ext::IdentExt as _}; + +use crate::syn::{self, Ident, ext::IdentExt as _}; +use crate::proc_macro2::Span; pub trait IdentExt { fn prepend(&self, string: &str) -> syn::Ident; fn append(&self, string: &str) -> syn::Ident; + fn with_span(self, span: Span) -> syn::Ident; } impl IdentExt for syn::Ident { @@ -16,6 +19,11 @@ impl IdentExt for syn::Ident { fn append(&self, string: &str) -> syn::Ident { syn::Ident::new(&format!("{}{}", self, string), self.span()) } + + fn with_span(mut self, span: Span) -> syn::Ident { + self.set_span(span); + self + } } pub trait ReturnTypeExt { @@ -83,7 +91,7 @@ impl NameSource { } impl devise::FromMeta for NameSource { - fn from_meta(meta: devise::MetaItem<'_>) -> devise::Result { + fn from_meta(meta: &devise::MetaItem) -> devise::Result { if let syn::Lit::Str(s) = meta.lit()? { return Ok(NameSource::new(s.value(), s.span())); } @@ -104,9 +112,9 @@ impl std::hash::Hash for NameSource { } } -impl PartialEq for NameSource { - fn eq(&self, other: &Self) -> bool { - self.name() == other.name() +impl AsRef for NameSource { + fn as_ref(&self) -> &str { + self.name() } } diff --git a/core/codegen/tests/from_form.rs b/core/codegen/tests/from_form.rs index 8446299284..58140833d6 100644 --- a/core/codegen/tests/from_form.rs +++ b/core/codegen/tests/from_form.rs @@ -1,30 +1,19 @@ -#[macro_use] extern crate rocket; +#[macro_use]extern crate rocket; -use rocket::request::{FromForm, FormItems, FormParseError}; -use rocket::http::RawStr; +use rocket::form::{Form, Strict, FromForm, Errors}; -fn parse<'f, T>(string: &'f str, strict: bool) -> Result> - where T: FromForm<'f, Error = FormParseError<'f>> -{ - let mut items = FormItems::from(string); - let result = T::from_form(items.by_ref(), strict); - if !items.exhaust() { - panic!("Invalid form input."); - } - - result +fn strict<'f, T: FromForm<'f>>(string: &'f str) -> Result> { + Form::>::parse(string).map(|s| s.into_inner()) } -fn strict<'f, T>(string: &'f str) -> Result> - where T: FromForm<'f, Error = FormParseError<'f>> -{ - parse(string, true) +fn lenient<'f, T: FromForm<'f>>(string: &'f str) -> Result> { + Form::::parse(string) } -fn lenient<'f, T>(string: &'f str) -> Result> - where T: FromForm<'f, Error = FormParseError<'f>> +fn strict_encoded(string: &'static str) -> Result> + where for<'a> T: FromForm<'a> { - parse(string, false) + Form::>::parse_encoded(string.into()).map(|s| s.into_inner()) } #[derive(Debug, PartialEq, FromForm)] @@ -46,6 +35,12 @@ fn simple() { let task: Option = strict("other=a&description=Hello&completed=on").ok(); assert!(task.is_none()); + let task: Option = lenient("other=a&description=Hello&completed=on").ok(); + assert_eq!(task, Some(TodoTask { + description: "Hello".to_string(), + completed: true + })); + // Ensure _method isn't required. let task: Option = strict("_method=patch&description=Hello&completed=off").ok(); assert_eq!(task, Some(TodoTask { @@ -54,7 +49,7 @@ fn simple() { })); } -#[derive(Debug, PartialEq, FromFormValue)] +#[derive(Debug, PartialEq, FromFormField)] enum FormOption { A, B, C } @@ -64,19 +59,19 @@ struct FormInput<'r> { checkbox: bool, number: usize, radio: FormOption, - password: &'r RawStr, + password: &'r str, textarea: String, select: FormOption, } #[derive(Debug, PartialEq, FromForm)] struct DefaultInput<'r> { - arg: Option<&'r RawStr>, + arg: Option<&'r str>, } #[derive(Debug, PartialEq, FromForm)] struct ManualMethod<'r> { - _method: Option<&'r RawStr>, + _method: Option<&'r str>, done: bool } @@ -88,23 +83,23 @@ struct UnpresentCheckbox { #[derive(Debug, PartialEq, FromForm)] struct UnpresentCheckboxTwo<'r> { checkbox: bool, - something: &'r RawStr + something: &'r str } #[derive(Debug, PartialEq, FromForm)] struct FieldNamedV<'r> { - v: &'r RawStr, + v: &'r str, } #[test] fn base_conditions() { let form_string = &[ - "password=testing", "checkbox=off", "checkbox=on", "number=10", - "checkbox=off", "textarea=", "select=a", "radio=c", + "password=testing", "checkbox=off", "number=10", "textarea=", + "select=a", "radio=c", ].join("&"); - let input: Option> = strict(&form_string).ok(); - assert_eq!(input, Some(FormInput { + let input: Result, _> = strict(&form_string); + assert_eq!(input, Ok(FormInput { checkbox: false, number: 10, radio: FormOption::C, @@ -193,28 +188,26 @@ fn lenient_parsing() { assert!(manual.is_none()); } -#[derive(Debug, PartialEq, FromForm)] -struct RenamedForm { - single: usize, - #[form(field = "camelCase")] - camel_case: String, - #[form(field = "TitleCase")] - title_case: String, - #[form(field = "type")] - field_type: isize, - #[form(field = "DOUBLE")] - double: String, - #[form(field = "a.b")] - dot: isize, - #[form(field = "some space")] - some_space: String, -} - #[test] fn field_renaming() { + #[derive(Debug, PartialEq, FromForm)] + struct RenamedForm { + single: usize, + #[field(name = "camelCase")] + camel_case: String, + #[field(name = "TitleCase")] + title_case: String, + #[field(name = "type")] + field_type: isize, + #[field(name = "DOUBLE")] + double: String, + #[field(name = "a:b")] + colon: isize, + } + let form_string = &[ "single=100", "camelCase=helloThere", "TitleCase=HiHi", "type=-2", - "DOUBLE=bing_bong", "a.b=123", "some space=okay" + "DOUBLE=bing_bong", "a:b=123" ].join("&"); let form: Option = strict(&form_string).ok(); @@ -224,35 +217,34 @@ fn field_renaming() { title_case: "HiHi".into(), field_type: -2, double: "bing_bong".into(), - dot: 123, - some_space: "okay".into(), + colon: 123, })); let form_string = &[ "single=100", "camel_case=helloThere", "TitleCase=HiHi", "type=-2", - "DOUBLE=bing_bong", "dot=123", "some_space=okay" + "DOUBLE=bing_bong", "colon=123" ].join("&"); let form: Option = strict(&form_string).ok(); assert!(form.is_none()); } -#[derive(FromForm, Debug, PartialEq)] -struct YetOneMore<'f, T> { - string: &'f RawStr, - other: T, -} - -#[derive(FromForm, Debug, PartialEq)] -struct Oops { - base: String, - a: A, - b: B, - c: C, -} - #[test] fn generics() { + #[derive(FromForm, Debug, PartialEq)] + struct Oops { + base: String, + a: A, + b: B, + c: C, + } + + #[derive(FromForm, Debug, PartialEq)] + struct YetOneMore<'f, T> { + string: &'f str, + other: T, + } + let form_string = &[ "string=hello", "other=00128" ].join("&"); @@ -272,59 +264,304 @@ fn generics() { let form: Option> = strict(&form_string).ok(); assert!(form.is_none()); - let form_string = &[ - "base=just%20a%20test", "a=hey%20there", "b=a", "c=811", - ].join("&"); - - let form: Option> = strict(&form_string).ok(); + let form_string = "base=just%20a%20test&a=hey%20there&b=a&c=811"; + let form: Option> = strict_encoded(&form_string).ok(); assert_eq!(form, Some(Oops { base: "just a test".into(), - a: "hey%20there".into(), + a: "hey there".into(), b: FormOption::A, c: 811, })); } -#[derive(Debug, PartialEq, FromForm)] -struct WhoopsForm { - complete: bool, - other: usize, -} - #[test] fn form_errors() { + use rocket::form::error::{ErrorKind, Entity}; + + #[derive(Debug, PartialEq, FromForm)] + struct WhoopsForm { + complete: bool, + other: usize, + } + let form: Result = strict("complete=true&other=781"); assert_eq!(form, Ok(WhoopsForm { complete: true, other: 781 })); - let form: Result = strict("complete=true&other=unknown"); - assert_eq!(form, Err(FormParseError::BadValue("other".into(), "unknown".into()))); - - let form: Result = strict("complete=unknown&other=unknown"); - assert_eq!(form, Err(FormParseError::BadValue("complete".into(), "unknown".into()))); + let errors = strict::("complete=true&other=unknown").unwrap_err(); + assert!(errors.iter().any(|e| { + "other" == e.name.as_ref().unwrap() + && Some("unknown") == e.value.as_deref() + && match e.kind { + ErrorKind::Int(..) => true, + _ => false + } + })); - let form: Result = strict("complete=true&other=1&extra=foo"); - assert_eq!(form, Err(FormParseError::Unknown("extra".into(), "foo".into()))); + let errors = strict::("complete=unknown&other=unknown").unwrap_err(); + assert!(errors.iter().any(|e| { + e.name.as_ref().unwrap() == "complete" + && Some("unknown") == e.value.as_deref() + && match e.kind { + ErrorKind::Bool(..) => true, + _ => false + } + })); - // Bad values take highest precedence. - let form: Result = strict("complete=unknown&unknown=foo"); - assert_eq!(form, Err(FormParseError::BadValue("complete".into(), "unknown".into()))); + let errors = strict::("complete=true&other=1&extra=foo").unwrap_err(); + dbg!(&errors); + assert!(errors.iter().any(|e| { + "extra" == e.name.as_ref().unwrap() + && Some("foo") == e.value.as_deref() + && match e.kind { + ErrorKind::Unexpected => true, + _ => false + } + })); - // Then unknown key/values for strict parses. - let form: Result = strict("complete=true&unknown=foo"); - assert_eq!(form, Err(FormParseError::Unknown("unknown".into(), "foo".into()))); + let errors = strict::("complete=unknown&unknown=!").unwrap_err(); + assert!(errors.iter().any(|e| { + "complete" == e.name.as_ref().unwrap() + && Some("unknown") == e.value.as_deref() + && match e.kind { + ErrorKind::Bool(..) => true, + _ => false + } + })); - // Finally, missing. - let form: Result = strict("complete=true"); - assert_eq!(form, Err(FormParseError::Missing("other".into()))); -} + assert!(errors.iter().any(|e| { + "unknown" == e.name.as_ref().unwrap() + && Some("!") == e.value.as_deref() + && match e.kind { + ErrorKind::Unexpected => true, + _ => false + } + })); -#[derive(Debug, PartialEq, FromForm)] -struct RawIdentForm { - r#type: String, + let errors = strict::("complete=true").unwrap_err(); + assert!(errors.iter().any(|e| { + "other" == e.name.as_ref().unwrap() + && e.value.is_none() + && e.entity == Entity::Field + && match e.kind { + ErrorKind::Missing => true, + _ => false + } + })); } #[test] fn raw_ident_form() { + #[derive(Debug, PartialEq, FromForm)] + struct RawIdentForm { + r#type: String, + } + let form: Result = strict("type=a"); assert_eq!(form, Ok(RawIdentForm { r#type: "a".into() })); } + +#[test] +fn test_multi() { + use std::collections::HashMap; + + #[derive(Debug, PartialEq, FromForm)] + struct Multi<'r> { + checks: Vec, + names: Vec<&'r str>, + news: Vec, + dogs: HashMap, + #[field(name = "more:dogs")] + more_dogs: HashMap<&'r str, Dog>, + } + + let multi: Multi = strict("checks=true&checks=false&checks=false\ + &names=Sam&names[]=Smith&names[]=Bob\ + &news[]=Here&news[]=also here\ + &dogs[fido].barks=true&dogs[George].barks=false\ + &dogs[fido].trained=on&dogs[George].trained=yes\ + &dogs[bob boo].trained=no&dogs[bob boo].barks=off\ + &more:dogs[k:0]=My Dog&more:dogs[v:0].barks=true&more:dogs[v:0].trained=yes\ + ").unwrap(); + assert_eq!(multi, Multi { + checks: vec![true, false, false], + names: vec!["Sam".into(), "Smith".into(), "Bob".into()], + news: vec!["Here".into(), "also here".into()], + dogs: { + let mut map = HashMap::new(); + map.insert("fido".into(), Dog { barks: true, trained: true }); + map.insert("George".into(), Dog { barks: false, trained: true }); + map.insert("bob boo".into(), Dog { barks: false, trained: false }); + map + }, + more_dogs: { + let mut map = HashMap::new(); + map.insert("My Dog".into(), Dog { barks: true, trained: true }); + map + } + }); + + #[derive(Debug, PartialEq, FromForm)] + struct MultiOwned { + names: Vec, + } + + let raw = "names=Sam&names%5B%5D=Smith&names%5B%5D=Bob%20Smith%3F"; + let multi: MultiOwned = strict_encoded(raw).unwrap(); + assert_eq!(multi, MultiOwned { + names: vec!["Sam".into(), "Smith".into(), "Bob Smith?".into()], + }); +} + +#[derive(Debug, FromForm, PartialEq)] +struct Dog { + barks: bool, + trained: bool, +} + +#[derive(Debug, FromForm, PartialEq)] +struct Cat<'r> { + nip: &'r str, + meows: bool +} + +#[derive(Debug, FromForm, PartialEq)] +struct Pet<'r, T> { + pet: T, + name: &'r str, + age: u8 +} + +#[derive(Debug, PartialEq, FromForm)] +struct Person<'r> { + dogs: Vec>, + cats: Vec>>, + sitting: Dog, +} + +#[test] +fn test_nested_multi() { + let person: Person = strict("sitting.barks=true&sitting.trained=true").unwrap(); + assert_eq!(person, Person { + sitting: Dog { barks: true, trained: true }, + cats: vec![], + dogs: vec![], + }); + + let person: Person = strict("sitting.barks=true&sitting.trained=true\ + &dogs[0].name=fido&dogs[0].pet.trained=yes&dogs[0].age=7&dogs[0].pet.barks=no\ + ").unwrap(); + assert_eq!(person, Person { + sitting: Dog { barks: true, trained: true }, + cats: vec![], + dogs: vec![Pet { + pet: Dog { barks: false, trained: true }, + name: "fido".into(), + age: 7 + }] + }); + + let person: Person = strict("sitting.trained=no&sitting.barks=true\ + &dogs[0].name=fido&dogs[0].pet.trained=yes&dogs[0].age=7&dogs[0].pet.barks=no\ + &dogs[1].pet.barks=true&dogs[1].name=Bob&dogs[1].pet.trained=no&dogs[1].age=1\ + ").unwrap(); + assert_eq!(person, Person { + sitting: Dog { barks: true, trained: false }, + cats: vec![], + dogs: vec![ + Pet { + pet: Dog { barks: false, trained: true }, + name: "fido".into(), + age: 7 + }, + Pet { + pet: Dog { barks: true, trained: false }, + name: "Bob".into(), + age: 1 + }, + ] + }); + + let person: Person = strict("sitting.barks=true&sitting.trained=no\ + &dogs[0].name=fido&dogs[0].pet.trained=yes&dogs[0].age=7&dogs[0].pet.barks=no\ + &dogs[1].pet.barks=true&dogs[1].name=Bob&dogs[1].pet.trained=no&dogs[1].age=1\ + &cats[george].pet.nip=paws&cats[george].name=George&cats[george].age=2\ + &cats[george].pet.meows=yes\ + ").unwrap(); + assert_eq!(person, Person { + sitting: Dog { barks: true, trained: false }, + cats: vec![ + Pet { + pet: Cat { nip: "paws".into(), meows: true }, + name: "George".into(), + age: 2 + } + ], + dogs: vec![ + Pet { + pet: Dog { barks: false, trained: true }, + name: "fido".into(), + age: 7 + }, + Pet { + pet: Dog { barks: true, trained: false }, + name: "Bob".into(), + age: 1 + }, + ] + }); +} + +// fn test_multipart() { +// use std::{io, path::Path}; +// +// use crate::*; +// use crate::http::ContentType; +// use crate::local::blocking::Client; +// use crate::{data::TempFile, form::Errors}; +// +// #[derive(FromForm)] +// struct MyForm { +// names: Vec, +// file: String, +// } +// +// #[post("/", data = "")] +// async fn form(mut form: Form) -> io::Result<&'static str> { +// let path = Path::new("/tmp").join(form.file_name().unwrap_or("upload")); +// form.persist(path).await?; +// println!("result: {:?}", form); +// Ok("hi") +// } +// +// let client = Client::untracked(crate::ignite().mount("/", routes![form])).unwrap(); +// let ct = "multipart/form-data; boundary=X-BOUNDARY" +// .parse::() +// .unwrap(); +// +// let body = &[ +// // "--X-BOUNDARY", +// // r#"Content-Disposition: form-data; name="names[]""#, +// // "", +// // "abcd", +// // "--X-BOUNDARY", +// // r#"Content-Disposition: form-data; name="names[]""#, +// // "", +// // "123", +// "--X-BOUNDARY", +// r#"Content-Disposition: form-data; name="file"; filename="foo.txt""#, +// "Content-Type: text/plain", +// "", +// "hi there", +// "--X-BOUNDARY--", +// "", +// ].join("\r\n"); +// +// let response = client.post("/") +// .header(ct) +// .body(body) +// .dispatch(); +// +// let string = response.into_string().unwrap(); +// println!("String: {}", string); +// panic!(string); +// } diff --git a/core/codegen/tests/from_form_value.rs b/core/codegen/tests/from_form_field.rs similarity index 59% rename from core/codegen/tests/from_form_value.rs rename to core/codegen/tests/from_form_field.rs index e6a77aa3fc..e5e14456e2 100644 --- a/core/codegen/tests/from_form_value.rs +++ b/core/codegen/tests/from_form_field.rs @@ -1,12 +1,18 @@ -use rocket::request::FromFormValue; +use rocket::form::{FromFormField, ValueField, FromForm, Options, Errors}; + +fn parse<'v, T: FromForm<'v>>(value: &'v str) -> Result> { + let mut context = T::init(Options::Lenient); + T::push_value(&mut context, ValueField::from_value(value)); + T::finalize(context) +} macro_rules! assert_parse { ($($string:expr),* => $item:ident :: $variant:ident) => ($( - match $item::from_form_value($string.into()) { + match parse::<$item>($string) { Ok($item::$variant) => { /* okay */ }, Ok(item) => panic!("Failed to parse {} as {:?}. Got {:?} instead.", $string, $item::$variant, item), - Err(e) => panic!("Failed to parse {} as {}: {:?}", + Err(e) => panic!("Failed to parse {} as {}: {}", $string, stringify!($item), e), } @@ -15,7 +21,7 @@ macro_rules! assert_parse { macro_rules! assert_no_parse { ($($string:expr),* => $item:ident) => ($( - match $item::from_form_value($string.into()) { + match parse::<$item>($string) { Err(_) => { /* okay */ }, Ok(item) => panic!("Unexpectedly parsed {} as {:?}", $string, item) } @@ -24,7 +30,7 @@ macro_rules! assert_no_parse { #[test] fn from_form_value_simple() { - #[derive(Debug, FromFormValue)] + #[derive(Debug, FromFormField)] enum Foo { A, B, C, } assert_parse!("a", "A" => Foo::A); @@ -35,7 +41,7 @@ fn from_form_value_simple() { #[test] fn from_form_value_weirder() { #[allow(non_camel_case_types)] - #[derive(Debug, FromFormValue)] + #[derive(Debug, FromFormField)] enum Foo { Ab_Cd, OtherA } assert_parse!("ab_cd", "ab_CD", "Ab_CD" => Foo::Ab_Cd); @@ -44,7 +50,7 @@ fn from_form_value_weirder() { #[test] fn from_form_value_no_parse() { - #[derive(Debug, FromFormValue)] + #[derive(Debug, FromFormField)] enum Foo { A, B, C, } assert_no_parse!("abc", "ab", "bc", "ca" => Foo); @@ -53,11 +59,11 @@ fn from_form_value_no_parse() { #[test] fn from_form_value_renames() { - #[derive(Debug, FromFormValue)] + #[derive(Debug, FromFormField)] enum Foo { - #[form(value = "foo")] + #[field(value = "foo")] Bar, - #[form(value = ":book")] + #[field(value = ":book")] Book } @@ -69,7 +75,7 @@ fn from_form_value_renames() { #[test] fn from_form_value_raw() { #[allow(non_camel_case_types)] - #[derive(Debug, FromFormValue)] + #[derive(Debug, FromFormField)] enum Keyword { r#type, this, @@ -79,3 +85,21 @@ fn from_form_value_raw() { assert_parse!("this" => Keyword::this); assert_no_parse!("r#type" => Keyword); } + +#[test] +fn form_value_errors() { + use rocket::form::error::{ErrorKind, Entity}; + + #[derive(Debug, FromFormField)] + enum Foo { Bar, Bob } + + let errors = parse::("blob").unwrap_err(); + assert!(errors.iter().any(|e| { + && "blob" == &e.value.as_ref().unwrap() + && e.entity == Entity::Value + && match &e.kind { + ErrorKind::InvalidChoice { choices } => &choices[..] == &["Bar", "Bob"], + _ => false + } + })); +} diff --git a/core/codegen/tests/route-data.rs b/core/codegen/tests/route-data.rs index afccfcef5c..0deee7a131 100644 --- a/core/codegen/tests/route-data.rs +++ b/core/codegen/tests/route-data.rs @@ -1,37 +1,34 @@ -#[macro_use] extern crate rocket; +#[macro_use]extern crate rocket; use rocket::{Request, Data}; use rocket::local::blocking::Client; -use rocket::request::Form; -use rocket::data::{self, FromData, ToByteUnit}; -use rocket::http::{RawStr, ContentType, Status}; +use rocket::data::{self, FromData}; +use rocket::http::ContentType; +use rocket::form::Form; // Test that the data parameters works as expected. #[derive(FromForm)] struct Inner<'r> { - field: &'r RawStr + field: &'r str } -struct Simple(String); +struct Simple<'r>(&'r str); #[async_trait] -impl FromData for Simple { - type Error = (); - - async fn from_data(_: &Request<'_>, data: Data) -> data::Outcome { - match data.open(64.bytes()).stream_to_string().await { - Ok(string) => data::Outcome::Success(Simple(string)), - Err(_) => data::Outcome::Failure((Status::InternalServerError, ())), - } +impl<'r> FromData<'r> for Simple<'r> { + type Error = std::io::Error; + + async fn from_data(req: &'r Request<'_>, data: Data) -> data::Outcome { + <&'r str>::from_data(req, data).await.map(Simple) } } #[post("/f", data = "")] -fn form(form: Form>) -> String { form.field.url_decode_lossy() } +fn form<'r>(form: Form>) -> &'r str { form.into_inner().field } #[post("/s", data = "")] -fn simple(simple: Simple) -> String { simple.0 } +fn simple<'r>(simple: Simple<'r>) -> &'r str { simple.0 } #[test] fn test_data() { diff --git a/core/codegen/tests/route-format.rs b/core/codegen/tests/route-format.rs index 1c991b84fc..c98e1e62f9 100644 --- a/core/codegen/tests/route-format.rs +++ b/core/codegen/tests/route-format.rs @@ -66,15 +66,19 @@ fn test_formats() { // Test custom formats. +// TODO: #[rocket(allow(unknown_format))] #[get("/", format = "application/foo")] fn get_foo() -> &'static str { "get_foo" } +// TODO: #[rocket(allow(unknown_format))] #[post("/", format = "application/foo")] fn post_foo() -> &'static str { "post_foo" } +// TODO: #[rocket(allow(unknown_format))] #[get("/", format = "bar/baz", rank = 2)] fn get_bar_baz() -> &'static str { "get_bar_baz" } +// TODO: #[rocket(allow(unknown_format))] #[put("/", format = "bar/baz")] fn put_bar_baz() -> &'static str { "put_bar_baz" } diff --git a/core/codegen/tests/route.rs b/core/codegen/tests/route.rs index 1eab6ed682..1bf50bf49a 100644 --- a/core/codegen/tests/route.rs +++ b/core/codegen/tests/route.rs @@ -7,61 +7,71 @@ use std::path::PathBuf; +use rocket::request::Request; use rocket::http::ext::Normalize; use rocket::local::blocking::Client; -use rocket::data::{self, Data, FromData, ToByteUnit}; -use rocket::request::{Request, Form}; +use rocket::data::{self, Data, FromData}; use rocket::http::{Status, RawStr, ContentType}; // Use all of the code generation available at once. #[derive(FromForm, UriDisplayQuery)] struct Inner<'r> { - field: &'r RawStr + field: &'r str } struct Simple(String); #[async_trait] -impl FromData for Simple { - type Error = (); +impl<'r> FromData<'r> for Simple { + type Error = std::io::Error; - async fn from_data(_: &Request<'_>, data: Data) -> data::Outcome { - let string = data.open(64.bytes()).stream_to_string().await.unwrap(); - data::Outcome::Success(Simple(string)) + async fn from_data(req: &'r Request<'_>, data: Data) -> data::Outcome { + String::from_data(req, data).await.map(Simple) } } -#[post("///name/?sky=blue&&", format = "json", data = "", rank = 138)] +#[post( + "///name/?sky=blue&&", + format = "json", + data = "", + rank = 138 +)] fn post1( sky: usize, - name: &RawStr, + name: &str, a: String, - query: Form>, + query: Inner<'_>, path: PathBuf, simple: Simple, ) -> String { let string = format!("{}, {}, {}, {}, {}, {}", sky, name, a, query.field, path.normalized_str(), simple.0); - let uri = uri!(post2: a, name.url_decode_lossy(), path, sky, query.into_inner()); + let uri = uri!(post1: a, name, path, sky, query); format!("({}) ({})", string, uri.to_string()) } -#[route(POST, path = "///name/?sky=blue&&", format = "json", data = "", rank = 138)] +#[route( + POST, + path = "///name/?sky=blue&&", + format = "json", + data = "", + rank = 138 +)] fn post2( sky: usize, - name: &RawStr, + name: &str, a: String, - query: Form>, + query: Inner<'_>, path: PathBuf, simple: Simple, ) -> String { let string = format!("{}, {}, {}, {}, {}, {}", sky, name, a, query.field, path.normalized_str(), simple.0); - let uri = uri!(post2: a, name.url_decode_lossy(), path, sky, query.into_inner()); + let uri = uri!(post2: a, name, path, sky, query); format!("({}) ({})", string, uri.to_string()) } @@ -79,8 +89,8 @@ fn test_full_route() { let client = Client::tracked(rocket).unwrap(); - let a = "A%20A"; - let name = "Bob%20McDonald"; + let a = RawStr::new("A%20A"); + let name = RawStr::new("Bob%20McDonald"); let path = "this/path/here"; let sky = 777; let query = "field=inside"; @@ -104,7 +114,7 @@ fn test_full_route() { .dispatch(); assert_eq!(response.into_string().unwrap(), format!("({}, {}, {}, {}, {}, {}) ({})", - sky, name, "A A", "inside", path, simple, expected_uri)); + sky, name.percent_decode().unwrap(), "A A", "inside", path, simple, expected_uri)); let response = client.post(format!("/2{}", uri)).body(simple).dispatch(); assert_eq!(response.status(), Status::NotFound); @@ -116,7 +126,7 @@ fn test_full_route() { .dispatch(); assert_eq!(response.into_string().unwrap(), format!("({}, {}, {}, {}, {}, {}) ({})", - sky, name, "A A", "inside", path, simple, expected_uri)); + sky, name.percent_decode().unwrap(), "A A", "inside", path, simple, expected_uri)); } mod scopes { @@ -138,3 +148,151 @@ mod scopes { rocket::ignite().mount("/", rocket::routes![hello, world]) } } + +use rocket::form::Contextual; + +#[derive(Default, Debug, PartialEq, FromForm)] +struct Filtered<'r> { + bird: Option<&'r str>, + color: Option<&'r str>, + cat: Option<&'r str>, + rest: Option<&'r str>, +} + +#[get("/?bird=1&color=blue&&&cat=bob&")] +fn filtered_raw_query(bird: usize, color: &str, rest: Contextual<'_, Filtered<'_>>) -> String { + assert_ne!(bird, 1); + assert_ne!(color, "blue"); + assert_eq!(rest.value.unwrap(), Filtered::default()); + + format!("{} - {}", bird, color) +} + +#[test] +fn test_filtered_raw_query() { + let rocket = rocket::ignite().mount("/", routes![filtered_raw_query]); + let client = Client::untracked(rocket).unwrap(); + + #[track_caller] + fn run(client: &Client, birds: &[&str], colors: &[&str], cats: &[&str]) -> (Status, String) { + let join = |slice: &[&str], name: &str| slice.iter() + .map(|v| format!("{}={}", name, v)) + .collect::>() + .join("&"); + + let q = format!("{}&{}&{}", + join(birds, "bird"), + join(colors, "color"), + join(cats, "cat")); + + let response = client.get(format!("/?{}", q)).dispatch(); + let status = response.status(); + let body = response.into_string().unwrap(); + + (status, body) + } + + let birds = &["2", "3"]; + let colors = &["red", "blue", "green"]; + let cats = &["bob", "bob"]; + assert_eq!(run(&client, birds, colors, cats).0, Status::NotFound); + + let birds = &["2", "1", "3"]; + let colors = &["red", "green"]; + let cats = &["bob", "bob"]; + assert_eq!(run(&client, birds, colors, cats).0, Status::NotFound); + + let birds = &["2", "1", "3"]; + let colors = &["red", "blue", "green"]; + let cats = &[]; + assert_eq!(run(&client, birds, colors, cats).0, Status::NotFound); + + let birds = &["2", "1", "3"]; + let colors = &["red", "blue", "green"]; + let cats = &["bob", "bob"]; + assert_eq!(run(&client, birds, colors, cats).1, "2 - red"); + + let birds = &["1", "2", "1", "3"]; + let colors = &["blue", "red", "blue", "green"]; + let cats = &["bob"]; + assert_eq!(run(&client, birds, colors, cats).1, "2 - red"); + + let birds = &["5", "1"]; + let colors = &["blue", "orange", "red", "blue", "green"]; + let cats = &["bob"]; + assert_eq!(run(&client, birds, colors, cats).1, "5 - orange"); +} + +#[derive(Debug, PartialEq, FromForm)] +struct Dog<'r> { + name: &'r str, + age: usize +} + +#[derive(Debug, PartialEq, FromForm)] +struct Q<'r> { + dog: Dog<'r> +} + +#[get("/?&color=red&")] +fn query_collection(color: Vec<&str>, q: Q<'_>) -> String { + format!("{} - {} - {}", color.join("&"), q.dog.name, q.dog.age) +} + +#[get("/?&color=red&")] +fn query_collection_2(color: Vec<&str>, dog: Dog<'_>) -> String { + format!("{} - {} - {}", color.join("&"), dog.name, dog.age) +} + +#[test] +fn test_query_collection() { + #[track_caller] + fn run(client: &Client, colors: &[&str], dog: &[&str]) -> (Status, String) { + let join = |slice: &[&str], prefix: &str| slice.iter() + .map(|v| format!("{}{}", prefix, v)) + .collect::>() + .join("&"); + + let q = format!("{}&{}", join(colors, "color="), join(dog, "dog.")); + let response = client.get(format!("/?{}", q)).dispatch(); + (response.status(), response.into_string().unwrap()) + } + + fn run_tests(rocket: rocket::Rocket) { + let client = Client::untracked(rocket).unwrap(); + + let colors = &["blue", "green"]; + let dog = &["name=Fido", "age=10"]; + assert_eq!(run(&client, colors, dog).0, Status::NotFound); + + let colors = &["red"]; + let dog = &["name=Fido"]; + assert_eq!(run(&client, colors, dog).0, Status::NotFound); + + let colors = &["red"]; + let dog = &["name=Fido", "age=2"]; + assert_eq!(run(&client, colors, dog).1, " - Fido - 2"); + + let colors = &["red", "blue", "green"]; + let dog = &["name=Fido", "age=10"]; + assert_eq!(run(&client, colors, dog).1, "blue&green - Fido - 10"); + + let colors = &["red", "blue", "green"]; + let dog = &["name=Fido", "age=10", "toy=yes"]; + assert_eq!(run(&client, colors, dog).1, "blue&green - Fido - 10"); + + let colors = &["blue", "red", "blue"]; + let dog = &["name=Fido", "age=10"]; + assert_eq!(run(&client, colors, dog).1, "blue&blue - Fido - 10"); + + let colors = &["blue", "green", "red", "blue"]; + let dog = &["name=Max+Fido", "age=10"]; + assert_eq!(run(&client, colors, dog).1, "blue&green&blue - Max Fido - 10"); + } + + let rocket = rocket::ignite().mount("/", routes![query_collection]); + run_tests(rocket); + + let rocket = rocket::ignite().mount("/", routes![query_collection_2]); + run_tests(rocket); +} diff --git a/core/codegen/tests/typed-uris.rs b/core/codegen/tests/typed-uris.rs index 8be6f9f233..584e6241d7 100644 --- a/core/codegen/tests/typed-uris.rs +++ b/core/codegen/tests/typed-uris.rs @@ -4,13 +4,13 @@ use std::path::PathBuf; -use rocket::http::{RawStr, CookieJar}; +use rocket::http::CookieJar; use rocket::http::uri::{FromUriParam, Query}; -use rocket::request::Form; +use rocket::form::{Form, error::{Errors, ErrorKind}}; #[derive(FromForm, UriDisplayQuery)] struct User<'a> { - name: &'a RawStr, + name: &'a str, nickname: String, } @@ -65,10 +65,10 @@ fn no_uri_display_okay(id: i32, form: Form) { } #[post("/name/?&bar=10&&", data = "", rank = 2)] fn complex<'r>( foo: usize, - name: &RawStr, - query: Form>, + name: &str, + query: User<'r>, user: Form>, - bar: &RawStr, + bar: &str, cookies: &CookieJar<'_> ) { } @@ -354,15 +354,15 @@ mod typed_uris { #[derive(FromForm, UriDisplayQuery)] struct Third<'r> { one: String, - two: &'r RawStr, + two: &'r str, } #[post("//?&")] fn optionals( foo: Option, - bar: Result, - q1: Result, - rest: Option>> + bar: Result, + q1: Result>, + rest: Option> ) { } #[test] @@ -408,7 +408,7 @@ fn test_optional_uri_parameters() { uri!(optionals: foo = 10, bar = &"hi there", - q1 = Err("foo".into()) as Result, + q1 = Err(ErrorKind::Missing.into()) as Result, rest = _ ) => "/10/hi%20there", diff --git a/core/codegen/tests/ui-fail-nightly/from_form.stderr b/core/codegen/tests/ui-fail-nightly/from_form.stderr index 2247f7ff58..474647b956 100644 --- a/core/codegen/tests/ui-fail-nightly/from_form.stderr +++ b/core/codegen/tests/ui-fail-nightly/from_form.stderr @@ -1,327 +1,338 @@ error: enums are not supported - --> $DIR/from_form.rs:6:1 + --> $DIR/from_form.rs:4:1 | -6 | enum Thing { } +4 | enum Thing { } | ^^^^^^^^^^^^^^ | note: error occurred while deriving `FromForm` - --> $DIR/from_form.rs:5:10 + --> $DIR/from_form.rs:3:10 | -5 | #[derive(FromForm)] +3 | #[derive(FromForm)] | ^^^^^^^^ = note: this error originates in a derive macro (in Nightly builds, run with -Z macro-backtrace for more info) error: tuple structs are not supported - --> $DIR/from_form.rs:9:1 + --> $DIR/from_form.rs:7:1 | -9 | struct Foo1; +7 | struct Foo1; | ^^^^^^^^^^^^ | note: error occurred while deriving `FromForm` - --> $DIR/from_form.rs:8:10 + --> $DIR/from_form.rs:6:10 | -8 | #[derive(FromForm)] +6 | #[derive(FromForm)] | ^^^^^^^^ = note: this error originates in a derive macro (in Nightly builds, run with -Z macro-backtrace for more info) error: at least one field is required - --> $DIR/from_form.rs:12:13 + --> $DIR/from_form.rs:10:13 | -12 | struct Foo2 { } +10 | struct Foo2 { } | ^^^^ | note: error occurred while deriving `FromForm` - --> $DIR/from_form.rs:11:10 + --> $DIR/from_form.rs:9:10 | -11 | #[derive(FromForm)] +9 | #[derive(FromForm)] | ^^^^^^^^ = note: this error originates in a derive macro (in Nightly builds, run with -Z macro-backtrace for more info) error: tuple structs are not supported - --> $DIR/from_form.rs:15:1 + --> $DIR/from_form.rs:13:1 | -15 | struct Foo3(usize); +13 | struct Foo3(usize); | ^^^^^^^^^^^^^^^^^^^ | note: error occurred while deriving `FromForm` - --> $DIR/from_form.rs:14:10 + --> $DIR/from_form.rs:12:10 | -14 | #[derive(FromForm)] +12 | #[derive(FromForm)] | ^^^^^^^^ = note: this error originates in a derive macro (in Nightly builds, run with -Z macro-backtrace for more info) error: only one lifetime is supported - --> $DIR/from_form.rs:18:25 + --> $DIR/from_form.rs:16:25 | -18 | struct NextTodoTask<'f, 'a> { +16 | struct NextTodoTask<'f, 'a> { | ^^ | note: error occurred while deriving `FromForm` - --> $DIR/from_form.rs:17:10 + --> $DIR/from_form.rs:15:10 | -17 | #[derive(FromForm)] +15 | #[derive(FromForm)] | ^^^^^^^^ = note: this error originates in a derive macro (in Nightly builds, run with -Z macro-backtrace for more info) error: invalid form field name - --> $DIR/from_form.rs:27:20 + --> $DIR/from_form.rs:25:20 | -27 | #[form(field = "isindex")] +25 | #[field(name = "isindex")] | ^^^^^^^^^ | + = help: field name cannot be `isindex` or contain '&', '=', '?', '.', '[', ']' note: error occurred while deriving `FromForm` - --> $DIR/from_form.rs:25:10 + --> $DIR/from_form.rs:23:10 | -25 | #[derive(FromForm)] +23 | #[derive(FromForm)] | ^^^^^^^^ = note: this error originates in a derive macro (in Nightly builds, run with -Z macro-backtrace for more info) -error: duplicate field name - --> $DIR/from_form.rs:35:5 +error: duplicate form field + --> $DIR/from_form.rs:33:5 | -35 | foo: usize, - | ^^^ +33 | foo: usize, + | ^^^^^^^^^^ | -note: previous definition here - --> $DIR/from_form.rs:33:20 +note: previously defined here + --> $DIR/from_form.rs:31:5 | -33 | #[form(field = "foo")] - | ^^^^^ +31 | / #[field(name = "foo")] +32 | | field: String, + | |_________________^ note: error occurred while deriving `FromForm` - --> $DIR/from_form.rs:31:10 + --> $DIR/from_form.rs:29:10 | -31 | #[derive(FromForm)] +29 | #[derive(FromForm)] | ^^^^^^^^ = note: this error originates in a derive macro (in Nightly builds, run with -Z macro-backtrace for more info) -error: duplicate field name - --> $DIR/from_form.rs:42:20 +error: duplicate form field + --> $DIR/from_form.rs:40:5 | -42 | #[form(field = "hello")] - | ^^^^^^^ +40 | / #[field(name = "hello")] +41 | | other: String, + | |_________________^ | -note: previous definition here - --> $DIR/from_form.rs:40:20 +note: previously defined here + --> $DIR/from_form.rs:38:5 | -40 | #[form(field = "hello")] - | ^^^^^^^ +38 | / #[field(name = "hello")] +39 | | first: String, + | |_________________^ note: error occurred while deriving `FromForm` - --> $DIR/from_form.rs:38:10 + --> $DIR/from_form.rs:36:10 | -38 | #[derive(FromForm)] +36 | #[derive(FromForm)] | ^^^^^^^^ = note: this error originates in a derive macro (in Nightly builds, run with -Z macro-backtrace for more info) -error: duplicate field name - --> $DIR/from_form.rs:49:20 +error: duplicate form field + --> $DIR/from_form.rs:47:5 | -49 | #[form(field = "first")] - | ^^^^^^^ +47 | / #[field(name = "first")] +48 | | other: String, + | |_________________^ | -note: previous definition here - --> $DIR/from_form.rs:48:5 +note: previously defined here + --> $DIR/from_form.rs:46:5 | -48 | first: String, - | ^^^^^ +46 | first: String, + | ^^^^^^^^^^^^^ note: error occurred while deriving `FromForm` - --> $DIR/from_form.rs:46:10 + --> $DIR/from_form.rs:44:10 | -46 | #[derive(FromForm)] +44 | #[derive(FromForm)] | ^^^^^^^^ = note: this error originates in a derive macro (in Nightly builds, run with -Z macro-backtrace for more info) -error: duplicate attribute parameter: field - --> $DIR/from_form.rs:55:28 +error: unexpected attribute parameter: `field` + --> $DIR/from_form.rs:53:28 | -55 | #[form(field = "blah", field = "bloo")] +53 | #[field(name = "blah", field = "bloo")] | ^^^^^^^^^^^^^^ | note: error occurred while deriving `FromForm` - --> $DIR/from_form.rs:53:10 + --> $DIR/from_form.rs:51:10 | -53 | #[derive(FromForm)] +51 | #[derive(FromForm)] | ^^^^^^^^ = note: this error originates in a derive macro (in Nightly builds, run with -Z macro-backtrace for more info) -error: malformed attribute: expected list - --> $DIR/from_form.rs:61:7 +error: expected list `#[field(..)]`, found bare path "field" + --> $DIR/from_form.rs:59:7 | -61 | #[form] - | ^^^^ +59 | #[field] + | ^^^^^ | - = help: expected syntax: #[form(key = value, ..)] note: error occurred while deriving `FromForm` - --> $DIR/from_form.rs:59:10 + --> $DIR/from_form.rs:57:10 | -59 | #[derive(FromForm)] +57 | #[derive(FromForm)] | ^^^^^^^^ = note: this error originates in a derive macro (in Nightly builds, run with -Z macro-backtrace for more info) -error: expected key/value pair - --> $DIR/from_form.rs:67:12 +error: expected key/value `key = value` + --> $DIR/from_form.rs:65:13 | -67 | #[form("blah")] - | ^^^^^^ +65 | #[field("blah")] + | ^^^^^^ | note: error occurred while deriving `FromForm` - --> $DIR/from_form.rs:65:10 + --> $DIR/from_form.rs:63:10 | -65 | #[derive(FromForm)] +63 | #[derive(FromForm)] | ^^^^^^^^ = note: this error originates in a derive macro (in Nightly builds, run with -Z macro-backtrace for more info) -error: expected key/value pair - --> $DIR/from_form.rs:73:12 +error: expected key/value `key = value` + --> $DIR/from_form.rs:71:13 | -73 | #[form(123)] - | ^^^ +71 | #[field(123)] + | ^^^ | note: error occurred while deriving `FromForm` - --> $DIR/from_form.rs:71:10 + --> $DIR/from_form.rs:69:10 | -71 | #[derive(FromForm)] +69 | #[derive(FromForm)] | ^^^^^^^^ = note: this error originates in a derive macro (in Nightly builds, run with -Z macro-backtrace for more info) error: unexpected attribute parameter: `beep` - --> $DIR/from_form.rs:79:12 + --> $DIR/from_form.rs:77:13 | -79 | #[form(beep = "bop")] - | ^^^^^^^^^^^^ +77 | #[field(beep = "bop")] + | ^^^^^^^^^^^^ | note: error occurred while deriving `FromForm` - --> $DIR/from_form.rs:77:10 + --> $DIR/from_form.rs:75:10 | -77 | #[derive(FromForm)] +75 | #[derive(FromForm)] | ^^^^^^^^ = note: this error originates in a derive macro (in Nightly builds, run with -Z macro-backtrace for more info) -error: duplicate invocation of `form` attribute - --> $DIR/from_form.rs:86:7 +error: duplicate form field renaming + --> $DIR/from_form.rs:84:20 | -86 | #[form(field = "bleh")] - | ^^^^ +84 | #[field(name = "blah")] + | ^^^^^^ | + = help: a field can only be renamed once note: error occurred while deriving `FromForm` - --> $DIR/from_form.rs:83:10 + --> $DIR/from_form.rs:81:10 | -83 | #[derive(FromForm)] +81 | #[derive(FromForm)] | ^^^^^^^^ = note: this error originates in a derive macro (in Nightly builds, run with -Z macro-backtrace for more info) error: invalid value: expected string literal - --> $DIR/from_form.rs:92:20 + --> $DIR/from_form.rs:90:20 | -92 | #[form(field = true)] +90 | #[field(name = true)] | ^^^^ | note: error occurred while deriving `FromForm` - --> $DIR/from_form.rs:90:10 + --> $DIR/from_form.rs:88:10 | -90 | #[derive(FromForm)] +88 | #[derive(FromForm)] | ^^^^^^^^ = note: this error originates in a derive macro (in Nightly builds, run with -Z macro-backtrace for more info) -error: expected literal or key/value pair - --> $DIR/from_form.rs:98:12 +error: expected literal, found bare path "name" + --> $DIR/from_form.rs:96:13 | -98 | #[form(field)] - | ^^^^^ +96 | #[field(name)] + | ^^^^ | note: error occurred while deriving `FromForm` - --> $DIR/from_form.rs:96:10 + --> $DIR/from_form.rs:94:10 | -96 | #[derive(FromForm)] +94 | #[derive(FromForm)] | ^^^^^^^^ = note: this error originates in a derive macro (in Nightly builds, run with -Z macro-backtrace for more info) error: invalid value: expected string literal - --> $DIR/from_form.rs:104:20 + --> $DIR/from_form.rs:102:20 | -104 | #[form(field = 123)] +102 | #[field(name = 123)] | ^^^ | note: error occurred while deriving `FromForm` - --> $DIR/from_form.rs:102:10 + --> $DIR/from_form.rs:100:10 | -102 | #[derive(FromForm)] +100 | #[derive(FromForm)] | ^^^^^^^^ = note: this error originates in a derive macro (in Nightly builds, run with -Z macro-backtrace for more info) error: invalid form field name - --> $DIR/from_form.rs:110:20 + --> $DIR/from_form.rs:108:20 | -110 | #[form(field = "hello&world")] +108 | #[field(name = "hello&world")] | ^^^^^^^^^^^^^ | + = help: field name cannot be `isindex` or contain '&', '=', '?', '.', '[', ']' note: error occurred while deriving `FromForm` - --> $DIR/from_form.rs:108:10 + --> $DIR/from_form.rs:106:10 | -108 | #[derive(FromForm)] +106 | #[derive(FromForm)] | ^^^^^^^^ = note: this error originates in a derive macro (in Nightly builds, run with -Z macro-backtrace for more info) error: invalid form field name - --> $DIR/from_form.rs:116:20 + --> $DIR/from_form.rs:114:20 | -116 | #[form(field = "!@#$%^&*()_")] +114 | #[field(name = "!@#$%^&*()_")] | ^^^^^^^^^^^^^ | + = help: field name cannot be `isindex` or contain '&', '=', '?', '.', '[', ']' note: error occurred while deriving `FromForm` - --> $DIR/from_form.rs:114:10 + --> $DIR/from_form.rs:112:10 | -114 | #[derive(FromForm)] +112 | #[derive(FromForm)] | ^^^^^^^^ = note: this error originates in a derive macro (in Nightly builds, run with -Z macro-backtrace for more info) error: invalid form field name - --> $DIR/from_form.rs:122:20 + --> $DIR/from_form.rs:120:20 | -122 | #[form(field = "?")] +120 | #[field(name = "?")] | ^^^ | + = help: field name cannot be `isindex` or contain '&', '=', '?', '.', '[', ']' note: error occurred while deriving `FromForm` - --> $DIR/from_form.rs:120:10 + --> $DIR/from_form.rs:118:10 | -120 | #[derive(FromForm)] +118 | #[derive(FromForm)] | ^^^^^^^^ = note: this error originates in a derive macro (in Nightly builds, run with -Z macro-backtrace for more info) error: invalid form field name - --> $DIR/from_form.rs:128:20 + --> $DIR/from_form.rs:126:20 | -128 | #[form(field = "")] +126 | #[field(name = "")] | ^^ | + = help: field name cannot be `isindex` or contain '&', '=', '?', '.', '[', ']' note: error occurred while deriving `FromForm` - --> $DIR/from_form.rs:126:10 + --> $DIR/from_form.rs:124:10 | -126 | #[derive(FromForm)] +124 | #[derive(FromForm)] | ^^^^^^^^ = note: this error originates in a derive macro (in Nightly builds, run with -Z macro-backtrace for more info) error: invalid form field name - --> $DIR/from_form.rs:134:20 + --> $DIR/from_form.rs:132:20 | -134 | #[form(field = "a&b")] +132 | #[field(name = "a&b")] | ^^^^^ | + = help: field name cannot be `isindex` or contain '&', '=', '?', '.', '[', ']' note: error occurred while deriving `FromForm` - --> $DIR/from_form.rs:132:10 + --> $DIR/from_form.rs:130:10 | -132 | #[derive(FromForm)] +130 | #[derive(FromForm)] | ^^^^^^^^ = note: this error originates in a derive macro (in Nightly builds, run with -Z macro-backtrace for more info) error: invalid form field name - --> $DIR/from_form.rs:140:20 + --> $DIR/from_form.rs:138:20 | -140 | #[form(field = "a=")] +138 | #[field(name = "a=")] | ^^^^ | + = help: field name cannot be `isindex` or contain '&', '=', '?', '.', '[', ']' note: error occurred while deriving `FromForm` - --> $DIR/from_form.rs:138:10 + --> $DIR/from_form.rs:136:10 | -138 | #[derive(FromForm)] +136 | #[derive(FromForm)] | ^^^^^^^^ = note: this error originates in a derive macro (in Nightly builds, run with -Z macro-backtrace for more info) diff --git a/core/codegen/tests/ui-fail-nightly/from_form_field.rs b/core/codegen/tests/ui-fail-nightly/from_form_field.rs new file mode 120000 index 0000000000..89dde8e7a9 --- /dev/null +++ b/core/codegen/tests/ui-fail-nightly/from_form_field.rs @@ -0,0 +1 @@ +../ui-fail/from_form_field.rs \ No newline at end of file diff --git a/core/codegen/tests/ui-fail-nightly/from_form_value.stderr b/core/codegen/tests/ui-fail-nightly/from_form_field.stderr similarity index 54% rename from core/codegen/tests/ui-fail-nightly/from_form_value.stderr rename to core/codegen/tests/ui-fail-nightly/from_form_field.stderr index 3d1939abb6..a912006e55 100644 --- a/core/codegen/tests/ui-fail-nightly/from_form_value.stderr +++ b/core/codegen/tests/ui-fail-nightly/from_form_field.stderr @@ -1,105 +1,105 @@ error: tuple structs are not supported - --> $DIR/from_form_value.rs:4:1 + --> $DIR/from_form_field.rs:4:1 | 4 | struct Foo1; | ^^^^^^^^^^^^ | -note: error occurred while deriving `FromFormValue` - --> $DIR/from_form_value.rs:3:10 +note: error occurred while deriving `FromFormField` + --> $DIR/from_form_field.rs:3:10 | -3 | #[derive(FromFormValue)] +3 | #[derive(FromFormField)] | ^^^^^^^^^^^^^ = note: this error originates in a derive macro (in Nightly builds, run with -Z macro-backtrace for more info) error: tuple structs are not supported - --> $DIR/from_form_value.rs:7:1 + --> $DIR/from_form_field.rs:7:1 | 7 | struct Foo2(usize); | ^^^^^^^^^^^^^^^^^^^ | -note: error occurred while deriving `FromFormValue` - --> $DIR/from_form_value.rs:6:10 +note: error occurred while deriving `FromFormField` + --> $DIR/from_form_field.rs:6:10 | -6 | #[derive(FromFormValue)] +6 | #[derive(FromFormField)] | ^^^^^^^^^^^^^ = note: this error originates in a derive macro (in Nightly builds, run with -Z macro-backtrace for more info) error: named structs are not supported - --> $DIR/from_form_value.rs:10:1 + --> $DIR/from_form_field.rs:10:1 | 10 | / struct Foo3 { 11 | | foo: usize, 12 | | } | |_^ | -note: error occurred while deriving `FromFormValue` - --> $DIR/from_form_value.rs:9:10 +note: error occurred while deriving `FromFormField` + --> $DIR/from_form_field.rs:9:10 | -9 | #[derive(FromFormValue)] +9 | #[derive(FromFormField)] | ^^^^^^^^^^^^^ = note: this error originates in a derive macro (in Nightly builds, run with -Z macro-backtrace for more info) error: variants cannot have fields - --> $DIR/from_form_value.rs:16:7 + --> $DIR/from_form_field.rs:16:6 | 16 | A(usize), - | ^^^^^ + | ^^^^^^^ | -note: error occurred while deriving `FromFormValue` - --> $DIR/from_form_value.rs:14:10 +note: error occurred while deriving `FromFormField` + --> $DIR/from_form_field.rs:14:10 | -14 | #[derive(FromFormValue)] +14 | #[derive(FromFormField)] | ^^^^^^^^^^^^^ = note: this error originates in a derive macro (in Nightly builds, run with -Z macro-backtrace for more info) -error: enum must have at least one field - --> $DIR/from_form_value.rs:20:11 +error: enum must have at least one variant + --> $DIR/from_form_field.rs:20:1 | 20 | enum Foo5 { } - | ^^^ + | ^^^^^^^^^^^^^ | -note: error occurred while deriving `FromFormValue` - --> $DIR/from_form_value.rs:19:10 +note: error occurred while deriving `FromFormField` + --> $DIR/from_form_field.rs:19:10 | -19 | #[derive(FromFormValue)] +19 | #[derive(FromFormField)] | ^^^^^^^^^^^^^ = note: this error originates in a derive macro (in Nightly builds, run with -Z macro-backtrace for more info) error: type generics are not supported - --> $DIR/from_form_value.rs:23:11 + --> $DIR/from_form_field.rs:23:11 | 23 | enum Foo6 { | ^ | -note: error occurred while deriving `FromFormValue` - --> $DIR/from_form_value.rs:22:10 +note: error occurred while deriving `FromFormField` + --> $DIR/from_form_field.rs:22:10 | -22 | #[derive(FromFormValue)] +22 | #[derive(FromFormField)] | ^^^^^^^^^^^^^ = note: this error originates in a derive macro (in Nightly builds, run with -Z macro-backtrace for more info) error: invalid value: expected string literal - --> $DIR/from_form_value.rs:29:20 + --> $DIR/from_form_field.rs:29:21 | -29 | #[form(value = 123)] - | ^^^ +29 | #[field(value = 123)] + | ^^^ | -note: error occurred while deriving `FromFormValue` - --> $DIR/from_form_value.rs:27:10 +note: error occurred while deriving `FromFormField` + --> $DIR/from_form_field.rs:27:10 | -27 | #[derive(FromFormValue)] +27 | #[derive(FromFormField)] | ^^^^^^^^^^^^^ = note: this error originates in a derive macro (in Nightly builds, run with -Z macro-backtrace for more info) -error: expected literal or key/value pair - --> $DIR/from_form_value.rs:35:12 +error: expected literal, found bare path "value" + --> $DIR/from_form_field.rs:35:13 | -35 | #[form(value)] - | ^^^^^ +35 | #[field(value)] + | ^^^^^ | -note: error occurred while deriving `FromFormValue` - --> $DIR/from_form_value.rs:33:10 +note: error occurred while deriving `FromFormField` + --> $DIR/from_form_field.rs:33:10 | -33 | #[derive(FromFormValue)] +33 | #[derive(FromFormField)] | ^^^^^^^^^^^^^ = note: this error originates in a derive macro (in Nightly builds, run with -Z macro-backtrace for more info) diff --git a/core/codegen/tests/ui-fail-nightly/from_form_type_errors.stderr b/core/codegen/tests/ui-fail-nightly/from_form_type_errors.stderr index 6af7c5f5cc..d1ce628fd0 100644 --- a/core/codegen/tests/ui-fail-nightly/from_form_type_errors.stderr +++ b/core/codegen/tests/ui-fail-nightly/from_form_type_errors.stderr @@ -1,15 +1,15 @@ -error[E0277]: the trait bound `Unknown: FromFormValue<'_>` is not satisfied - --> $DIR/from_form_type_errors.rs:7:5 +error[E0277]: the trait bound `Unknown: FromFormField<'_>` is not satisfied + --> $DIR/from_form_type_errors.rs:7:12 | 7 | field: Unknown, - | ^^^^^^^^^^^^^^ the trait `FromFormValue<'_>` is not implemented for `Unknown` + | ^^^^^^^ the trait `FromFormField<'_>` is not implemented for `Unknown` | - = note: required by `from_form_value` + = note: required because of the requirements on the impl of `FromForm<'__f>` for `Unknown` -error[E0277]: the trait bound `Foo: FromFormValue<'_>` is not satisfied - --> $DIR/from_form_type_errors.rs:14:5 +error[E0277]: the trait bound `Foo: FromFormField<'_>` is not satisfied + --> $DIR/from_form_type_errors.rs:14:12 | 14 | field: Foo, - | ^^^^^^^^^^^^^^^^^ the trait `FromFormValue<'_>` is not implemented for `Foo` + | ^^^^^^^^^^ the trait `FromFormField<'_>` is not implemented for `Foo` | - = note: required by `from_form_value` + = note: required because of the requirements on the impl of `FromForm<'__f>` for `Foo` diff --git a/core/codegen/tests/ui-fail-nightly/from_form_value.rs b/core/codegen/tests/ui-fail-nightly/from_form_value.rs deleted file mode 120000 index e48f57bb55..0000000000 --- a/core/codegen/tests/ui-fail-nightly/from_form_value.rs +++ /dev/null @@ -1 +0,0 @@ -../ui-fail/from_form_value.rs \ No newline at end of file diff --git a/core/codegen/tests/ui-fail-nightly/route-attribute-general-syntax.stderr b/core/codegen/tests/ui-fail-nightly/route-attribute-general-syntax.stderr index 62ff7ef232..0eb4ae5a30 100644 --- a/core/codegen/tests/ui-fail-nightly/route-attribute-general-syntax.stderr +++ b/core/codegen/tests/ui-fail-nightly/route-attribute-general-syntax.stderr @@ -38,13 +38,13 @@ error: expected `fn` | = help: #[get] can only be used on functions -error: expected key/value pair +error: expected key/value `key = value` --> $DIR/route-attribute-general-syntax.rs:21:12 | 21 | #[get("/", 123)] | ^^^ -error: expected key/value pair +error: expected key/value `key = value` --> $DIR/route-attribute-general-syntax.rs:24:12 | 24 | #[get("/", "/")] @@ -62,14 +62,11 @@ error: unexpected attribute parameter: `unknown` 30 | #[get("/", unknown = "foo")] | ^^^^^^^^^^^^^^^ -error: malformed attribute - --> $DIR/route-attribute-general-syntax.rs:33:1 +error: expected key/value `key = value` + --> $DIR/route-attribute-general-syntax.rs:33:12 | 33 | #[get("/", ...)] - | ^^^^^^^^^^^^^^^^ - | - = help: expected syntax: #[get(key = value, ..)] - = note: this error originates in an attribute macro (in Nightly builds, run with -Z macro-backtrace for more info) + | ^^^ error: handler arguments cannot be ignored --> $DIR/route-attribute-general-syntax.rs:39:7 diff --git a/core/codegen/tests/ui-fail-nightly/route-path-bad-syntax.stderr b/core/codegen/tests/ui-fail-nightly/route-path-bad-syntax.stderr index 7fca8c0741..29b9abe363 100644 --- a/core/codegen/tests/ui-fail-nightly/route-path-bad-syntax.stderr +++ b/core/codegen/tests/ui-fail-nightly/route-path-bad-syntax.stderr @@ -1,12 +1,12 @@ -error: invalid path URI: expected token '/' but found 'a' at index 0 +error: invalid route URI: expected token '/' but found 'a' at index 0 --> $DIR/route-path-bad-syntax.rs:5:8 | 5 | #[get("a")] - | ^^ + | ^ | = help: expected path in origin form: "/path/" -error: invalid path URI: unexpected EOF: expected token '/' at index 0 +error: invalid route URI: unexpected EOF: expected token '/' at index 0 --> $DIR/route-path-bad-syntax.rs:8:8 | 8 | #[get("")] @@ -14,11 +14,11 @@ error: invalid path URI: unexpected EOF: expected token '/' at index 0 | = help: expected path in origin form: "/path/" -error: invalid path URI: expected token '/' but found 'a' at index 0 +error: invalid route URI: expected token '/' but found 'a' at index 0 --> $DIR/route-path-bad-syntax.rs:11:8 | 11 | #[get("a/b/c")] - | ^^^^^^ + | ^ | = help: expected path in origin form: "/path/" @@ -50,41 +50,6 @@ error: paths cannot contain empty segments | = note: expected '/a/b', found '/a/b//' -error: invalid path URI: expected EOF but found '#' at index 3 - --> $DIR/route-path-bad-syntax.rs:28:11 - | -28 | #[get("/!@#$%^&*()")] - | ^^^^^^^^^ - | - = help: expected path in origin form: "/path/" - -error: segment contains invalid URI characters - --> $DIR/route-path-bad-syntax.rs:31:9 - | -31 | #[get("/a%20b")] - | ^^^^^ - | - = note: components cannot contain reserved characters - = help: reserved characters include: '%', '+', '&', etc. - -error: segment contains invalid URI characters - --> $DIR/route-path-bad-syntax.rs:34:11 - | -34 | #[get("/a?a%20b")] - | ^^^^^ - | - = note: components cannot contain reserved characters - = help: reserved characters include: '%', '+', '&', etc. - -error: segment contains invalid URI characters - --> $DIR/route-path-bad-syntax.rs:37:11 - | -37 | #[get("/a?a+b")] - | ^^^ - | - = note: components cannot contain reserved characters - = help: reserved characters include: '%', '+', '&', etc. - error: unused dynamic parameter --> $DIR/route-path-bad-syntax.rs:42:9 | diff --git a/core/codegen/tests/ui-fail-nightly/route-type-errors.stderr b/core/codegen/tests/ui-fail-nightly/route-type-errors.stderr index 3c2132af5b..6694a1bcd6 100644 --- a/core/codegen/tests/ui-fail-nightly/route-type-errors.stderr +++ b/core/codegen/tests/ui-fail-nightly/route-type-errors.stderr @@ -14,29 +14,32 @@ error[E0277]: the trait bound `Q: FromSegments<'_>` is not satisfied | = note: required by `from_segments` -error[E0277]: the trait bound `Q: FromFormValue<'_>` is not satisfied - --> $DIR/route-type-errors.rs:12:7 +error[E0277]: the trait bound `Q: FromFormField<'_>` is not satisfied + --> $DIR/route-type-errors.rs:12:12 | 12 | fn f2(foo: Q) {} - | ^^^^^^ the trait `FromFormValue<'_>` is not implemented for `Q` + | ^ the trait `FromFormField<'_>` is not implemented for `Q` | - = note: required by `from_form_value` + = note: required because of the requirements on the impl of `FromForm<'_>` for `Q` -error[E0277]: the trait bound `Q: FromQuery<'_>` is not satisfied - --> $DIR/route-type-errors.rs:15:7 +error[E0277]: the trait bound `Q: FromFormField<'_>` is not satisfied + --> $DIR/route-type-errors.rs:15:12 | 15 | fn f3(foo: Q) {} - | ^^^^^^ the trait `FromQuery<'_>` is not implemented for `Q` + | ^ the trait `FromFormField<'_>` is not implemented for `Q` | - = note: required by `from_query` + = note: required because of the requirements on the impl of `FromForm<'_>` for `Q` -error[E0277]: the trait bound `Q: FromData` is not satisfied - --> $DIR/route-type-errors.rs:18:7 - | -18 | fn f4(foo: Q) {} - | ^^^^^^ the trait `FromData` is not implemented for `Q` - | - = note: required because of the requirements on the impl of `FromTransformedData<'_>` for `Q` +error[E0277]: the trait bound `Q: FromData<'_>` is not satisfied + --> $DIR/route-type-errors.rs:18:7 + | +18 | fn f4(foo: Q) {} + | ^^^^^^ the trait `FromData<'_>` is not implemented for `Q` + | + ::: $WORKSPACE/core/lib/src/data/from_data.rs + | + | async fn from_data(req: &'r Request<'_>, data: Data) -> Outcome; + | -- required by this bound in `rocket::data::FromData::from_data` error[E0277]: the trait bound `Q: FromRequest<'_, '_>` is not satisfied --> $DIR/route-type-errors.rs:21:7 @@ -46,8 +49,8 @@ error[E0277]: the trait bound `Q: FromRequest<'_, '_>` is not satisfied | ::: $WORKSPACE/core/lib/src/request/from_request.rs | - | #[crate::async_trait] - | --------------------- required by this bound in `from_request` + | pub trait FromRequest<'a, 'r>: Sized { + | -- required by this bound in `from_request` error[E0277]: the trait bound `Q: FromParam<'_>` is not satisfied --> $DIR/route-type-errors.rs:21:13 @@ -65,8 +68,8 @@ error[E0277]: the trait bound `Q: FromRequest<'_, '_>` is not satisfied | ::: $WORKSPACE/core/lib/src/request/from_request.rs | - | #[crate::async_trait] - | --------------------- required by this bound in `from_request` + | pub trait FromRequest<'a, 'r>: Sized { + | -- required by this bound in `from_request` error[E0277]: the trait bound `Q: FromParam<'_>` is not satisfied --> $DIR/route-type-errors.rs:24:13 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 b30896237d..c4bd50fc96 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,7 +1,7 @@ error[E0277]: the trait bound `usize: FromUriParam` is not satisfied - --> $DIR/typed-uri-bad-type.rs:42:23 + --> $DIR/typed-uri-bad-type.rs:45:23 | -42 | uri!(simple: id = "hi"); +45 | uri!(simple: id = "hi"); | ^^^^ the trait `FromUriParam` is not implemented for `usize` | = help: the following implementations were found: @@ -11,9 +11,9 @@ error[E0277]: the trait bound `usize: FromUriParam` is not satisfied - --> $DIR/typed-uri-bad-type.rs:44:18 + --> $DIR/typed-uri-bad-type.rs:47:18 | -44 | uri!(simple: "hello"); +47 | uri!(simple: "hello"); | ^^^^^^^ the trait `FromUriParam` is not implemented for `usize` | = help: the following implementations were found: @@ -23,9 +23,9 @@ error[E0277]: the trait bound `usize: FromUriParam` is not satisfied - --> $DIR/typed-uri-bad-type.rs:46:23 + --> $DIR/typed-uri-bad-type.rs:49:23 | -46 | uri!(simple: id = 239239i64); +49 | uri!(simple: id = 239239i64); | ^^^^^^^^^ the trait `FromUriParam` is not implemented for `usize` | = help: the following implementations were found: @@ -35,17 +35,17 @@ 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:48:31 + --> $DIR/typed-uri-bad-type.rs:51:31 | -48 | uri!(not_uri_display: 10, 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:53:26 + --> $DIR/typed-uri-bad-type.rs:56:26 | -53 | uri!(optionals: id = Some(10), name = Ok("bob".into())); +56 | uri!(optionals: id = Some(10), name = Ok("bob".into())); | ^^^^^^^^ the trait `FromUriParam>` is not implemented for `i32` | = help: the following implementations were found: @@ -56,9 +56,9 @@ error[E0277]: the trait bound `i32: FromUriParam>` is not satisfied - --> $DIR/typed-uri-bad-type.rs:53:43 + --> $DIR/typed-uri-bad-type.rs:56:43 | -53 | uri!(optionals: id = Some(10), name = Ok("bob".into())); +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:55:20 +error[E0277]: the trait bound `isize: FromUriParam` is not satisfied + --> $DIR/typed-uri-bad-type.rs:58:20 | -55 | 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:57:25 +error[E0277]: the trait bound `isize: FromUriParam` is not satisfied + --> $DIR/typed-uri-bad-type.rs:60:25 | -57 | 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,82 +94,48 @@ 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:59:24 +error[E0277]: the trait bound `S: FromUriParam` is not satisfied + --> $DIR/typed-uri-bad-type.rs:62:24 | -59 | 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:61:26 +error[E0277]: the trait bound `S: FromUriParam` is not satisfied + --> $DIR/typed-uri-bad-type.rs:64:26 | -61 | 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:36:29 +error[E0277]: the trait bound `S: Ignorable` is not satisfied + --> $DIR/typed-uri-bad-type.rs:66:26 | -36 | fn other_q(id: usize, rest: S) { } - | ^ the trait `Ignorable` is not implemented for `S` -... -63 | uri!(other_q: rest = _, id = 100); - | ---------------------------------- in this macro invocation +66 | uri!(other_q: rest = _, id = 100); + | ^ the trait `Ignorable` is not implemented for `S` | ::: $WORKSPACE/core/http/src/uri/uri_display.rs | | pub fn assert_ignorable>() { } | ------------ required by this bound in `assert_ignorable` - | - = note: this error originates in a macro (in Nightly builds, run with -Z macro-backtrace for more info) -error[E0277]: the trait bound `usize: Ignorable` is not satisfied - --> $DIR/typed-uri-bad-type.rs:36:16 +error[E0277]: the trait bound `usize: Ignorable` is not satisfied + --> $DIR/typed-uri-bad-type.rs:68:34 | -36 | fn other_q(id: usize, rest: S) { } - | ^^^^^ the trait `Ignorable` is not implemented for `usize` -... -65 | uri!(other_q: rest = S, id = _); - | -------------------------------- in this macro invocation +68 | uri!(other_q: rest = S, id = _); + | ^ the trait `Ignorable` is not implemented for `usize` | ::: $WORKSPACE/core/http/src/uri/uri_display.rs | | pub fn assert_ignorable>() { } | ------------ required by this bound in `assert_ignorable` - | - = note: this error originates in a macro (in Nightly builds, run with -Z macro-backtrace for more info) - -error[E0277]: the trait bound `S: FromUriParam` is not satisfied - --> $DIR/typed-uri-bad-type.rs:65:26 - | -65 | 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 `std::option::Option: FromUriParam` is not satisfied - --> $DIR/typed-uri-bad-type.rs:69:28 +error[E0277]: the trait bound `S: FromUriParam` is not satisfied + --> $DIR/typed-uri-bad-type.rs:68:26 | -69 | uri!(optionals_q: id = 10, name = "Bob".to_string()); - | ^^ the trait `FromUriParam` is not implemented for `std::option::Option` +68 | uri!(other_q: rest = S, id = _); + | ^ the trait `FromUriParam` is not implemented for `S` | - = help: the following implementations were found: - as FromUriParam> - as FromUriParam>> - as FromUriParam>> - = note: required by `from_uri_param` - -error[E0277]: the trait bound `Result: FromUriParam` is not satisfied - --> $DIR/typed-uri-bad-type.rs:69:39 - | -69 | uri!(optionals_q: id = 10, name = "Bob".to_string()); - | ^^^^^^^^^^^^^^^^^ the trait `FromUriParam` is not implemented for `Result` - | - = help: the following implementations were found: - as FromUriParam> - as FromUriParam>> - as FromUriParam>> = note: required by `from_uri_param` diff --git a/core/codegen/tests/ui-fail-nightly/uri_display.stderr b/core/codegen/tests/ui-fail-nightly/uri_display.stderr index 9dcf7f6cae..921d47f079 100644 --- a/core/codegen/tests/ui-fail-nightly/uri_display.stderr +++ b/core/codegen/tests/ui-fail-nightly/uri_display.stderr @@ -1,8 +1,8 @@ error: fieldless structs or variants are not supported - --> $DIR/uri_display.rs:4:8 + --> $DIR/uri_display.rs:4:1 | 4 | struct Foo1; - | ^^^^ + | ^^^^^^^^^^^^ | note: error occurred while deriving `UriDisplay` --> $DIR/uri_display.rs:3:10 @@ -12,10 +12,10 @@ note: error occurred while deriving `UriDisplay` = note: this error originates in a derive macro (in Nightly builds, run with -Z macro-backtrace for more info) error: fieldless structs or variants are not supported - --> $DIR/uri_display.rs:7:8 + --> $DIR/uri_display.rs:7:1 | 7 | struct Foo2(); - | ^^^^ + | ^^^^^^^^^^^^^^ | note: error occurred while deriving `UriDisplay` --> $DIR/uri_display.rs:6:10 @@ -66,7 +66,7 @@ note: error occurred while deriving `UriDisplay` error: invalid value: expected string literal --> $DIR/uri_display.rs:22:20 | -22 | #[form(field = 123)] +22 | #[field(name = 123)] | ^^^ | note: error occurred while deriving `UriDisplay` @@ -90,10 +90,10 @@ note: error occurred while deriving `UriDisplay` = note: this error originates in a derive macro (in Nightly builds, run with -Z macro-backtrace for more info) error: struct must have exactly one field - --> $DIR/uri_display.rs:30:8 + --> $DIR/uri_display.rs:30:1 | 30 | struct Foo8; - | ^^^^ + | ^^^^^^^^^^^^ | note: error occurred while deriving `UriDisplay` --> $DIR/uri_display.rs:29:10 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 1114a84521..10085692b1 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,56 +1,56 @@ -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 --> $DIR/uri_display_type_errors.rs:40:12 diff --git a/core/codegen/tests/ui-fail-stable/from_form.stderr b/core/codegen/tests/ui-fail-stable/from_form.stderr index 65e7bda087..0ffec14fe4 100644 --- a/core/codegen/tests/ui-fail-stable/from_form.stderr +++ b/core/codegen/tests/ui-fail-stable/from_form.stderr @@ -1,354 +1,361 @@ error: enums are not supported - --> $DIR/from_form.rs:6:1 + --> $DIR/from_form.rs:4:1 | -6 | enum Thing { } +4 | enum Thing { } | ^^^^ error: [note] error occurred while deriving `FromForm` - --> $DIR/from_form.rs:5:10 + --> $DIR/from_form.rs:3:10 | -5 | #[derive(FromForm)] +3 | #[derive(FromForm)] | ^^^^^^^^ | = note: this error originates in a derive macro (in Nightly builds, run with -Z macro-backtrace for more info) error: tuple structs are not supported - --> $DIR/from_form.rs:9:1 + --> $DIR/from_form.rs:7:1 | -9 | struct Foo1; +7 | struct Foo1; | ^^^^^^ error: [note] error occurred while deriving `FromForm` - --> $DIR/from_form.rs:8:10 + --> $DIR/from_form.rs:6:10 | -8 | #[derive(FromForm)] +6 | #[derive(FromForm)] | ^^^^^^^^ | = note: this error originates in a derive macro (in Nightly builds, run with -Z macro-backtrace for more info) error: at least one field is required - --> $DIR/from_form.rs:12:13 + --> $DIR/from_form.rs:10:13 | -12 | struct Foo2 { } +10 | struct Foo2 { } | ^^^^ error: [note] error occurred while deriving `FromForm` - --> $DIR/from_form.rs:11:10 - | -11 | #[derive(FromForm)] - | ^^^^^^^^ - | - = note: this error originates in a derive macro (in Nightly builds, run with -Z macro-backtrace for more info) + --> $DIR/from_form.rs:9:10 + | +9 | #[derive(FromForm)] + | ^^^^^^^^ + | + = note: this error originates in a derive macro (in Nightly builds, run with -Z macro-backtrace for more info) error: tuple structs are not supported - --> $DIR/from_form.rs:15:1 + --> $DIR/from_form.rs:13:1 | -15 | struct Foo3(usize); +13 | struct Foo3(usize); | ^^^^^^ error: [note] error occurred while deriving `FromForm` - --> $DIR/from_form.rs:14:10 + --> $DIR/from_form.rs:12:10 | -14 | #[derive(FromForm)] +12 | #[derive(FromForm)] | ^^^^^^^^ | = note: this error originates in a derive macro (in Nightly builds, run with -Z macro-backtrace for more info) error: only one lifetime is supported - --> $DIR/from_form.rs:18:25 + --> $DIR/from_form.rs:16:25 | -18 | struct NextTodoTask<'f, 'a> { +16 | struct NextTodoTask<'f, 'a> { | ^^ error: [note] error occurred while deriving `FromForm` - --> $DIR/from_form.rs:17:10 + --> $DIR/from_form.rs:15:10 | -17 | #[derive(FromForm)] +15 | #[derive(FromForm)] | ^^^^^^^^ | = note: this error originates in a derive macro (in Nightly builds, run with -Z macro-backtrace for more info) error: invalid form field name - --> $DIR/from_form.rs:27:20 + --- help: field name cannot be `isindex` or contain '&', '=', '?', '.', '[', ']' + --> $DIR/from_form.rs:25:20 | -27 | #[form(field = "isindex")] +25 | #[field(name = "isindex")] | ^^^^^^^^^ error: [note] error occurred while deriving `FromForm` - --> $DIR/from_form.rs:25:10 + --> $DIR/from_form.rs:23:10 | -25 | #[derive(FromForm)] +23 | #[derive(FromForm)] | ^^^^^^^^ | = note: this error originates in a derive macro (in Nightly builds, run with -Z macro-backtrace for more info) -error: duplicate field name - --> $DIR/from_form.rs:35:5 +error: duplicate form field + --> $DIR/from_form.rs:33:5 | -35 | foo: usize, +33 | foo: usize, | ^^^ -error: [note] previous definition here - --> $DIR/from_form.rs:33:20 +error: [note] previously defined here + --> $DIR/from_form.rs:31:5 | -33 | #[form(field = "foo")] - | ^^^^^ +31 | #[field(name = "foo")] + | ^ error: [note] error occurred while deriving `FromForm` - --> $DIR/from_form.rs:31:10 + --> $DIR/from_form.rs:29:10 | -31 | #[derive(FromForm)] +29 | #[derive(FromForm)] | ^^^^^^^^ | = note: this error originates in a derive macro (in Nightly builds, run with -Z macro-backtrace for more info) -error: duplicate field name - --> $DIR/from_form.rs:42:20 +error: duplicate form field + --> $DIR/from_form.rs:40:5 | -42 | #[form(field = "hello")] - | ^^^^^^^ +40 | #[field(name = "hello")] + | ^ -error: [note] previous definition here - --> $DIR/from_form.rs:40:20 +error: [note] previously defined here + --> $DIR/from_form.rs:38:5 | -40 | #[form(field = "hello")] - | ^^^^^^^ +38 | #[field(name = "hello")] + | ^ error: [note] error occurred while deriving `FromForm` - --> $DIR/from_form.rs:38:10 + --> $DIR/from_form.rs:36:10 | -38 | #[derive(FromForm)] +36 | #[derive(FromForm)] | ^^^^^^^^ | = note: this error originates in a derive macro (in Nightly builds, run with -Z macro-backtrace for more info) -error: duplicate field name - --> $DIR/from_form.rs:49:20 +error: duplicate form field + --> $DIR/from_form.rs:47:5 | -49 | #[form(field = "first")] - | ^^^^^^^ +47 | #[field(name = "first")] + | ^ -error: [note] previous definition here - --> $DIR/from_form.rs:48:5 +error: [note] previously defined here + --> $DIR/from_form.rs:46:5 | -48 | first: String, +46 | first: String, | ^^^^^ error: [note] error occurred while deriving `FromForm` - --> $DIR/from_form.rs:46:10 + --> $DIR/from_form.rs:44:10 | -46 | #[derive(FromForm)] +44 | #[derive(FromForm)] | ^^^^^^^^ | = note: this error originates in a derive macro (in Nightly builds, run with -Z macro-backtrace for more info) -error: duplicate attribute parameter: field - --> $DIR/from_form.rs:55:28 +error: unexpected attribute parameter: `field` + --> $DIR/from_form.rs:53:28 | -55 | #[form(field = "blah", field = "bloo")] +53 | #[field(name = "blah", field = "bloo")] | ^^^^^ error: [note] error occurred while deriving `FromForm` - --> $DIR/from_form.rs:53:10 + --> $DIR/from_form.rs:51:10 | -53 | #[derive(FromForm)] +51 | #[derive(FromForm)] | ^^^^^^^^ | = note: this error originates in a derive macro (in Nightly builds, run with -Z macro-backtrace for more info) -error: malformed attribute: expected list - --- help: expected syntax: #[form(key = value, ..)] - --> $DIR/from_form.rs:61:7 +error: expected list `#[field(..)]`, found bare path "field" + --> $DIR/from_form.rs:59:7 | -61 | #[form] - | ^^^^ +59 | #[field] + | ^^^^^ error: [note] error occurred while deriving `FromForm` - --> $DIR/from_form.rs:59:10 + --> $DIR/from_form.rs:57:10 | -59 | #[derive(FromForm)] +57 | #[derive(FromForm)] | ^^^^^^^^ | = note: this error originates in a derive macro (in Nightly builds, run with -Z macro-backtrace for more info) -error: expected key/value pair - --> $DIR/from_form.rs:67:12 +error: expected key/value `key = value` + --> $DIR/from_form.rs:65:13 | -67 | #[form("blah")] - | ^^^^^^ +65 | #[field("blah")] + | ^^^^^^ error: [note] error occurred while deriving `FromForm` - --> $DIR/from_form.rs:65:10 + --> $DIR/from_form.rs:63:10 | -65 | #[derive(FromForm)] +63 | #[derive(FromForm)] | ^^^^^^^^ | = note: this error originates in a derive macro (in Nightly builds, run with -Z macro-backtrace for more info) -error: expected key/value pair - --> $DIR/from_form.rs:73:12 +error: expected key/value `key = value` + --> $DIR/from_form.rs:71:13 | -73 | #[form(123)] - | ^^^ +71 | #[field(123)] + | ^^^ error: [note] error occurred while deriving `FromForm` - --> $DIR/from_form.rs:71:10 + --> $DIR/from_form.rs:69:10 | -71 | #[derive(FromForm)] +69 | #[derive(FromForm)] | ^^^^^^^^ | = note: this error originates in a derive macro (in Nightly builds, run with -Z macro-backtrace for more info) error: unexpected attribute parameter: `beep` - --> $DIR/from_form.rs:79:12 + --> $DIR/from_form.rs:77:13 | -79 | #[form(beep = "bop")] - | ^^^^ +77 | #[field(beep = "bop")] + | ^^^^ error: [note] error occurred while deriving `FromForm` - --> $DIR/from_form.rs:77:10 + --> $DIR/from_form.rs:75:10 | -77 | #[derive(FromForm)] +75 | #[derive(FromForm)] | ^^^^^^^^ | = note: this error originates in a derive macro (in Nightly builds, run with -Z macro-backtrace for more info) -error: duplicate invocation of `form` attribute - --> $DIR/from_form.rs:86:7 +error: duplicate form field renaming + --- help: a field can only be renamed once + --> $DIR/from_form.rs:84:20 | -86 | #[form(field = "bleh")] - | ^^^^ +84 | #[field(name = "blah")] + | ^^^^^^ error: [note] error occurred while deriving `FromForm` - --> $DIR/from_form.rs:83:10 + --> $DIR/from_form.rs:81:10 | -83 | #[derive(FromForm)] +81 | #[derive(FromForm)] | ^^^^^^^^ | = note: this error originates in a derive macro (in Nightly builds, run with -Z macro-backtrace for more info) error: invalid value: expected string literal - --> $DIR/from_form.rs:92:20 + --> $DIR/from_form.rs:90:20 | -92 | #[form(field = true)] +90 | #[field(name = true)] | ^^^^ error: [note] error occurred while deriving `FromForm` - --> $DIR/from_form.rs:90:10 + --> $DIR/from_form.rs:88:10 | -90 | #[derive(FromForm)] +88 | #[derive(FromForm)] | ^^^^^^^^ | = note: this error originates in a derive macro (in Nightly builds, run with -Z macro-backtrace for more info) -error: expected literal or key/value pair - --> $DIR/from_form.rs:98:12 +error: expected literal, found bare path "name" + --> $DIR/from_form.rs:96:13 | -98 | #[form(field)] - | ^^^^^ +96 | #[field(name)] + | ^^^^ error: [note] error occurred while deriving `FromForm` - --> $DIR/from_form.rs:96:10 + --> $DIR/from_form.rs:94:10 | -96 | #[derive(FromForm)] +94 | #[derive(FromForm)] | ^^^^^^^^ | = note: this error originates in a derive macro (in Nightly builds, run with -Z macro-backtrace for more info) error: invalid value: expected string literal - --> $DIR/from_form.rs:104:20 + --> $DIR/from_form.rs:102:20 | -104 | #[form(field = 123)] +102 | #[field(name = 123)] | ^^^ error: [note] error occurred while deriving `FromForm` - --> $DIR/from_form.rs:102:10 + --> $DIR/from_form.rs:100:10 | -102 | #[derive(FromForm)] +100 | #[derive(FromForm)] | ^^^^^^^^ | = note: this error originates in a derive macro (in Nightly builds, run with -Z macro-backtrace for more info) error: invalid form field name - --> $DIR/from_form.rs:110:20 + --- help: field name cannot be `isindex` or contain '&', '=', '?', '.', '[', ']' + --> $DIR/from_form.rs:108:20 | -110 | #[form(field = "hello&world")] +108 | #[field(name = "hello&world")] | ^^^^^^^^^^^^^ error: [note] error occurred while deriving `FromForm` - --> $DIR/from_form.rs:108:10 + --> $DIR/from_form.rs:106:10 | -108 | #[derive(FromForm)] +106 | #[derive(FromForm)] | ^^^^^^^^ | = note: this error originates in a derive macro (in Nightly builds, run with -Z macro-backtrace for more info) error: invalid form field name - --> $DIR/from_form.rs:116:20 + --- help: field name cannot be `isindex` or contain '&', '=', '?', '.', '[', ']' + --> $DIR/from_form.rs:114:20 | -116 | #[form(field = "!@#$%^&*()_")] +114 | #[field(name = "!@#$%^&*()_")] | ^^^^^^^^^^^^^ error: [note] error occurred while deriving `FromForm` - --> $DIR/from_form.rs:114:10 + --> $DIR/from_form.rs:112:10 | -114 | #[derive(FromForm)] +112 | #[derive(FromForm)] | ^^^^^^^^ | = note: this error originates in a derive macro (in Nightly builds, run with -Z macro-backtrace for more info) error: invalid form field name - --> $DIR/from_form.rs:122:20 + --- help: field name cannot be `isindex` or contain '&', '=', '?', '.', '[', ']' + --> $DIR/from_form.rs:120:20 | -122 | #[form(field = "?")] +120 | #[field(name = "?")] | ^^^ error: [note] error occurred while deriving `FromForm` - --> $DIR/from_form.rs:120:10 + --> $DIR/from_form.rs:118:10 | -120 | #[derive(FromForm)] +118 | #[derive(FromForm)] | ^^^^^^^^ | = note: this error originates in a derive macro (in Nightly builds, run with -Z macro-backtrace for more info) error: invalid form field name - --> $DIR/from_form.rs:128:20 + --- help: field name cannot be `isindex` or contain '&', '=', '?', '.', '[', ']' + --> $DIR/from_form.rs:126:20 | -128 | #[form(field = "")] +126 | #[field(name = "")] | ^^ error: [note] error occurred while deriving `FromForm` - --> $DIR/from_form.rs:126:10 + --> $DIR/from_form.rs:124:10 | -126 | #[derive(FromForm)] +124 | #[derive(FromForm)] | ^^^^^^^^ | = note: this error originates in a derive macro (in Nightly builds, run with -Z macro-backtrace for more info) error: invalid form field name - --> $DIR/from_form.rs:134:20 + --- help: field name cannot be `isindex` or contain '&', '=', '?', '.', '[', ']' + --> $DIR/from_form.rs:132:20 | -134 | #[form(field = "a&b")] +132 | #[field(name = "a&b")] | ^^^^^ error: [note] error occurred while deriving `FromForm` - --> $DIR/from_form.rs:132:10 + --> $DIR/from_form.rs:130:10 | -132 | #[derive(FromForm)] +130 | #[derive(FromForm)] | ^^^^^^^^ | = note: this error originates in a derive macro (in Nightly builds, run with -Z macro-backtrace for more info) error: invalid form field name - --> $DIR/from_form.rs:140:20 + --- help: field name cannot be `isindex` or contain '&', '=', '?', '.', '[', ']' + --> $DIR/from_form.rs:138:20 | -140 | #[form(field = "a=")] +138 | #[field(name = "a=")] | ^^^^ error: [note] error occurred while deriving `FromForm` - --> $DIR/from_form.rs:138:10 + --> $DIR/from_form.rs:136:10 | -138 | #[derive(FromForm)] +136 | #[derive(FromForm)] | ^^^^^^^^ | = note: this error originates in a derive macro (in Nightly builds, run with -Z macro-backtrace for more info) diff --git a/core/codegen/tests/ui-fail-stable/from_form_field.rs b/core/codegen/tests/ui-fail-stable/from_form_field.rs new file mode 120000 index 0000000000..89dde8e7a9 --- /dev/null +++ b/core/codegen/tests/ui-fail-stable/from_form_field.rs @@ -0,0 +1 @@ +../ui-fail/from_form_field.rs \ No newline at end of file diff --git a/core/codegen/tests/ui-fail-stable/from_form_value.stderr b/core/codegen/tests/ui-fail-stable/from_form_field.stderr similarity index 52% rename from core/codegen/tests/ui-fail-stable/from_form_value.stderr rename to core/codegen/tests/ui-fail-stable/from_form_field.stderr index bb5a9a593d..757410bf85 100644 --- a/core/codegen/tests/ui-fail-stable/from_form_value.stderr +++ b/core/codegen/tests/ui-fail-stable/from_form_field.stderr @@ -1,111 +1,111 @@ error: tuple structs are not supported - --> $DIR/from_form_value.rs:4:1 + --> $DIR/from_form_field.rs:4:1 | 4 | struct Foo1; | ^^^^^^ -error: [note] error occurred while deriving `FromFormValue` - --> $DIR/from_form_value.rs:3:10 +error: [note] error occurred while deriving `FromFormField` + --> $DIR/from_form_field.rs:3:10 | -3 | #[derive(FromFormValue)] +3 | #[derive(FromFormField)] | ^^^^^^^^^^^^^ | = note: this error originates in a derive macro (in Nightly builds, run with -Z macro-backtrace for more info) error: tuple structs are not supported - --> $DIR/from_form_value.rs:7:1 + --> $DIR/from_form_field.rs:7:1 | 7 | struct Foo2(usize); | ^^^^^^ -error: [note] error occurred while deriving `FromFormValue` - --> $DIR/from_form_value.rs:6:10 +error: [note] error occurred while deriving `FromFormField` + --> $DIR/from_form_field.rs:6:10 | -6 | #[derive(FromFormValue)] +6 | #[derive(FromFormField)] | ^^^^^^^^^^^^^ | = note: this error originates in a derive macro (in Nightly builds, run with -Z macro-backtrace for more info) error: named structs are not supported - --> $DIR/from_form_value.rs:10:1 + --> $DIR/from_form_field.rs:10:1 | 10 | struct Foo3 { | ^^^^^^ -error: [note] error occurred while deriving `FromFormValue` - --> $DIR/from_form_value.rs:9:10 +error: [note] error occurred while deriving `FromFormField` + --> $DIR/from_form_field.rs:9:10 | -9 | #[derive(FromFormValue)] +9 | #[derive(FromFormField)] | ^^^^^^^^^^^^^ | = note: this error originates in a derive macro (in Nightly builds, run with -Z macro-backtrace for more info) error: variants cannot have fields - --> $DIR/from_form_value.rs:16:7 + --> $DIR/from_form_field.rs:16:6 | 16 | A(usize), - | ^^^^^ + | ^^^^^^^ -error: [note] error occurred while deriving `FromFormValue` - --> $DIR/from_form_value.rs:14:10 +error: [note] error occurred while deriving `FromFormField` + --> $DIR/from_form_field.rs:14:10 | -14 | #[derive(FromFormValue)] +14 | #[derive(FromFormField)] | ^^^^^^^^^^^^^ | = note: this error originates in a derive macro (in Nightly builds, run with -Z macro-backtrace for more info) -error: enum must have at least one field - --> $DIR/from_form_value.rs:20:11 +error: enum must have at least one variant + --> $DIR/from_form_field.rs:20:1 | 20 | enum Foo5 { } - | ^^^ + | ^^^^ -error: [note] error occurred while deriving `FromFormValue` - --> $DIR/from_form_value.rs:19:10 +error: [note] error occurred while deriving `FromFormField` + --> $DIR/from_form_field.rs:19:10 | -19 | #[derive(FromFormValue)] +19 | #[derive(FromFormField)] | ^^^^^^^^^^^^^ | = note: this error originates in a derive macro (in Nightly builds, run with -Z macro-backtrace for more info) error: type generics are not supported - --> $DIR/from_form_value.rs:23:11 + --> $DIR/from_form_field.rs:23:11 | 23 | enum Foo6 { | ^ -error: [note] error occurred while deriving `FromFormValue` - --> $DIR/from_form_value.rs:22:10 +error: [note] error occurred while deriving `FromFormField` + --> $DIR/from_form_field.rs:22:10 | -22 | #[derive(FromFormValue)] +22 | #[derive(FromFormField)] | ^^^^^^^^^^^^^ | = note: this error originates in a derive macro (in Nightly builds, run with -Z macro-backtrace for more info) error: invalid value: expected string literal - --> $DIR/from_form_value.rs:29:20 + --> $DIR/from_form_field.rs:29:21 | -29 | #[form(value = 123)] - | ^^^ +29 | #[field(value = 123)] + | ^^^ -error: [note] error occurred while deriving `FromFormValue` - --> $DIR/from_form_value.rs:27:10 +error: [note] error occurred while deriving `FromFormField` + --> $DIR/from_form_field.rs:27:10 | -27 | #[derive(FromFormValue)] +27 | #[derive(FromFormField)] | ^^^^^^^^^^^^^ | = note: this error originates in a derive macro (in Nightly builds, run with -Z macro-backtrace for more info) -error: expected literal or key/value pair - --> $DIR/from_form_value.rs:35:12 +error: expected literal, found bare path "value" + --> $DIR/from_form_field.rs:35:13 | -35 | #[form(value)] - | ^^^^^ +35 | #[field(value)] + | ^^^^^ -error: [note] error occurred while deriving `FromFormValue` - --> $DIR/from_form_value.rs:33:10 +error: [note] error occurred while deriving `FromFormField` + --> $DIR/from_form_field.rs:33:10 | -33 | #[derive(FromFormValue)] +33 | #[derive(FromFormField)] | ^^^^^^^^^^^^^ | = note: this error originates in a derive macro (in Nightly builds, run with -Z macro-backtrace for more info) diff --git a/core/codegen/tests/ui-fail-stable/from_form_type_errors.stderr b/core/codegen/tests/ui-fail-stable/from_form_type_errors.stderr index db8525ba0f..dd5bd6a848 100644 --- a/core/codegen/tests/ui-fail-stable/from_form_type_errors.stderr +++ b/core/codegen/tests/ui-fail-stable/from_form_type_errors.stderr @@ -1,15 +1,15 @@ -error[E0277]: the trait bound `Unknown: FromFormValue<'_>` is not satisfied - --> $DIR/from_form_type_errors.rs:7:5 +error[E0277]: the trait bound `Unknown: FromFormField<'_>` is not satisfied + --> $DIR/from_form_type_errors.rs:7:12 | 7 | field: Unknown, - | ^^^^^ the trait `FromFormValue<'_>` is not implemented for `Unknown` + | ^^^^^^^ the trait `FromFormField<'_>` is not implemented for `Unknown` | - = note: required by `from_form_value` + = note: required because of the requirements on the impl of `FromForm<'__f>` for `Unknown` -error[E0277]: the trait bound `Foo: FromFormValue<'_>` is not satisfied - --> $DIR/from_form_type_errors.rs:14:5 +error[E0277]: the trait bound `Foo: FromFormField<'_>` is not satisfied + --> $DIR/from_form_type_errors.rs:14:12 | 14 | field: Foo, - | ^^^^^ the trait `FromFormValue<'_>` is not implemented for `Foo` + | ^^^ the trait `FromFormField<'_>` is not implemented for `Foo` | - = note: required by `from_form_value` + = note: required because of the requirements on the impl of `FromForm<'__f>` for `Foo` diff --git a/core/codegen/tests/ui-fail-stable/from_form_value.rs b/core/codegen/tests/ui-fail-stable/from_form_value.rs deleted file mode 120000 index e48f57bb55..0000000000 --- a/core/codegen/tests/ui-fail-stable/from_form_value.rs +++ /dev/null @@ -1 +0,0 @@ -../ui-fail/from_form_value.rs \ No newline at end of file diff --git a/core/codegen/tests/ui-fail-stable/route-attribute-general-syntax.stderr b/core/codegen/tests/ui-fail-stable/route-attribute-general-syntax.stderr index 77b383bbe9..bd742b5d63 100644 --- a/core/codegen/tests/ui-fail-stable/route-attribute-general-syntax.stderr +++ b/core/codegen/tests/ui-fail-stable/route-attribute-general-syntax.stderr @@ -34,13 +34,13 @@ error: expected `fn` 18 | impl S { } | ^^^^ -error: expected key/value pair +error: expected key/value `key = value` --> $DIR/route-attribute-general-syntax.rs:21:12 | 21 | #[get("/", 123)] | ^^^ -error: expected key/value pair +error: expected key/value `key = value` --> $DIR/route-attribute-general-syntax.rs:24:12 | 24 | #[get("/", "/")] @@ -58,14 +58,11 @@ error: unexpected attribute parameter: `unknown` 30 | #[get("/", unknown = "foo")] | ^^^^^^^ -error: malformed attribute - --- help: expected syntax: #[get(key = value, ..)] - --> $DIR/route-attribute-general-syntax.rs:33:1 +error: expected key/value `key = value` + --> $DIR/route-attribute-general-syntax.rs:33:12 | 33 | #[get("/", ...)] - | ^^^^^^^^^^^^^^^^ - | - = note: this error originates in an attribute macro (in Nightly builds, run with -Z macro-backtrace for more info) + | ^^^ error: handler arguments cannot be ignored --- help: all handler arguments must be of the form: `ident: Type` diff --git a/core/codegen/tests/ui-fail-stable/route-path-bad-syntax.stderr b/core/codegen/tests/ui-fail-stable/route-path-bad-syntax.stderr index d274e60a6e..dc254fa199 100644 --- a/core/codegen/tests/ui-fail-stable/route-path-bad-syntax.stderr +++ b/core/codegen/tests/ui-fail-stable/route-path-bad-syntax.stderr @@ -1,18 +1,18 @@ -error: invalid path URI: expected token '/' but found 'a' at index 0 +error: invalid route URI: expected token '/' but found 'a' at index 0 --- help: expected path in origin form: "/path/" --> $DIR/route-path-bad-syntax.rs:5:7 | 5 | #[get("a")] | ^^^ -error: invalid path URI: unexpected EOF: expected token '/' at index 0 +error: invalid route URI: unexpected EOF: expected token '/' at index 0 --- help: expected path in origin form: "/path/" --> $DIR/route-path-bad-syntax.rs:8:7 | 8 | #[get("")] | ^^ -error: invalid path URI: expected token '/' but found 'a' at index 0 +error: invalid route URI: expected token '/' but found 'a' at index 0 --- help: expected path in origin form: "/path/" --> $DIR/route-path-bad-syntax.rs:11:7 | @@ -45,37 +45,6 @@ error: paths cannot contain empty segments 23 | #[get("/a/b//")] | ^^^^^^^^ -error: invalid path URI: expected EOF but found '#' at index 3 - --- help: expected path in origin form: "/path/" - --> $DIR/route-path-bad-syntax.rs:28:7 - | -28 | #[get("/!@#$%^&*()")] - | ^^^^^^^^^^^^^ - -error: segment contains invalid URI characters - --- note: components cannot contain reserved characters - --- help: reserved characters include: '%', '+', '&', etc. - --> $DIR/route-path-bad-syntax.rs:31:7 - | -31 | #[get("/a%20b")] - | ^^^^^^^^ - -error: segment contains invalid URI characters - --- note: components cannot contain reserved characters - --- help: reserved characters include: '%', '+', '&', etc. - --> $DIR/route-path-bad-syntax.rs:34:7 - | -34 | #[get("/a?a%20b")] - | ^^^^^^^^^^ - -error: segment contains invalid URI characters - --- note: components cannot contain reserved characters - --- help: reserved characters include: '%', '+', '&', etc. - --> $DIR/route-path-bad-syntax.rs:37:7 - | -37 | #[get("/a?a+b")] - | ^^^^^^^^ - error: unused dynamic parameter --> $DIR/route-path-bad-syntax.rs:42:7 | diff --git a/core/codegen/tests/ui-fail-stable/route-type-errors.stderr b/core/codegen/tests/ui-fail-stable/route-type-errors.stderr index 26a525ff7e..9cfd1fe75b 100644 --- a/core/codegen/tests/ui-fail-stable/route-type-errors.stderr +++ b/core/codegen/tests/ui-fail-stable/route-type-errors.stderr @@ -14,40 +14,32 @@ error[E0277]: the trait bound `Q: FromSegments<'_>` is not satisfied | = note: required by `from_segments` -error[E0277]: the trait bound `Q: FromFormValue<'_>` is not satisfied +error[E0277]: the trait bound `Q: FromFormField<'_>` is not satisfied --> $DIR/route-type-errors.rs:12:12 | 12 | fn f2(foo: Q) {} - | ^ the trait `FromFormValue<'_>` is not implemented for `Q` + | ^ the trait `FromFormField<'_>` is not implemented for `Q` | - = note: required by `from_form_value` + = note: required because of the requirements on the impl of `FromForm<'_>` for `Q` -error[E0277]: the trait bound `Q: FromFormValue<'_>` is not satisfied - --> $DIR/route-type-errors.rs:12:7 - | -12 | fn f2(foo: Q) {} - | ^^^^^^ the trait `FromFormValue<'_>` is not implemented for `Q` - | - ::: $WORKSPACE/core/lib/src/request/form/from_form_value.rs - | - | pub trait FromFormValue<'v>: Sized { - | ---------------------------------- required by this bound in `FromFormValue` - -error[E0277]: the trait bound `Q: FromQuery<'_>` is not satisfied +error[E0277]: the trait bound `Q: FromFormField<'_>` is not satisfied --> $DIR/route-type-errors.rs:15:12 | 15 | fn f3(foo: Q) {} - | ^ the trait `FromQuery<'_>` is not implemented for `Q` + | ^ the trait `FromFormField<'_>` is not implemented for `Q` | - = note: required by `from_query` + = note: required because of the requirements on the impl of `FromForm<'_>` for `Q` -error[E0277]: the trait bound `Q: FromData` is not satisfied - --> $DIR/route-type-errors.rs:18:12 - | -18 | fn f4(foo: Q) {} - | ^ the trait `FromData` is not implemented for `Q` - | - = note: required because of the requirements on the impl of `FromTransformedData<'_>` for `Q` +error[E0277]: the trait bound `Q: FromData<'_>` is not satisfied + --> $DIR/route-type-errors.rs:18:12 + | +18 | fn f4(foo: Q) {} + | ^ the trait `FromData<'_>` is not implemented for `Q` + | + ::: $WORKSPACE/core/lib/src/data/from_data.rs + | + | async fn from_data(req: &'r Request<'_>, data: Data) -> Outcome; + | -- required by this bound in `rocket::data::FromData::from_data` error[E0277]: the trait bound `Q: FromRequest<'_, '_>` is not satisfied --> $DIR/route-type-errors.rs:21:10 @@ -57,8 +49,8 @@ error[E0277]: the trait bound `Q: FromRequest<'_, '_>` is not satisfied | ::: $WORKSPACE/core/lib/src/request/from_request.rs | - | #[crate::async_trait] - | --------------------- required by this bound in `from_request` + | pub trait FromRequest<'a, 'r>: Sized { + | -- required by this bound in `from_request` error[E0277]: the trait bound `Q: FromParam<'_>` is not satisfied --> $DIR/route-type-errors.rs:21:18 @@ -76,8 +68,8 @@ error[E0277]: the trait bound `Q: FromRequest<'_, '_>` is not satisfied | ::: $WORKSPACE/core/lib/src/request/from_request.rs | - | #[crate::async_trait] - | --------------------- required by this bound in `from_request` + | pub trait FromRequest<'a, 'r>: Sized { + | -- required by this bound in `from_request` error[E0277]: the trait bound `Q: FromParam<'_>` is not satisfied --> $DIR/route-type-errors.rs:24:18 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 b5b6164115..686693d1f7 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,7 +1,7 @@ error[E0277]: the trait bound `usize: FromUriParam` is not satisfied - --> $DIR/typed-uri-bad-type.rs:42:23 + --> $DIR/typed-uri-bad-type.rs:45:23 | -42 | uri!(simple: id = "hi"); +45 | uri!(simple: id = "hi"); | ^^^^ the trait `FromUriParam` is not implemented for `usize` | = help: the following implementations were found: @@ -11,9 +11,9 @@ error[E0277]: the trait bound `usize: FromUriParam` is not satisfied - --> $DIR/typed-uri-bad-type.rs:44:18 + --> $DIR/typed-uri-bad-type.rs:47:18 | -44 | uri!(simple: "hello"); +47 | uri!(simple: "hello"); | ^^^^^^^ the trait `FromUriParam` is not implemented for `usize` | = help: the following implementations were found: @@ -23,9 +23,9 @@ error[E0277]: the trait bound `usize: FromUriParam` is not satisfied - --> $DIR/typed-uri-bad-type.rs:46:23 + --> $DIR/typed-uri-bad-type.rs:49:23 | -46 | uri!(simple: id = 239239i64); +49 | uri!(simple: id = 239239i64); | ^^^^^^^^^ the trait `FromUriParam` is not implemented for `usize` | = help: the following implementations were found: @@ -35,17 +35,17 @@ 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:48:31 + --> $DIR/typed-uri-bad-type.rs:51:31 | -48 | uri!(not_uri_display: 10, 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:53:26 + --> $DIR/typed-uri-bad-type.rs:56:26 | -53 | uri!(optionals: id = Some(10), name = Ok("bob".into())); +56 | uri!(optionals: id = Some(10), name = Ok("bob".into())); | ^^^^ the trait `FromUriParam>` is not implemented for `i32` | = help: the following implementations were found: @@ -56,9 +56,9 @@ error[E0277]: the trait bound `i32: FromUriParam>` is not satisfied - --> $DIR/typed-uri-bad-type.rs:53:43 + --> $DIR/typed-uri-bad-type.rs:56:43 | -53 | uri!(optionals: id = Some(10), name = Ok("bob".into())); +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 `std::result::Result` + = note: required because of the requirements on the impl of `FromUriParam>` for `std::result::Result` = note: required by `from_uri_param` -error[E0277]: the trait bound `isize: FromUriParam` is not satisfied - --> $DIR/typed-uri-bad-type.rs:55:20 +error[E0277]: the trait bound `isize: FromUriParam` is not satisfied + --> $DIR/typed-uri-bad-type.rs:58:20 | -55 | 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:57:25 +error[E0277]: the trait bound `isize: FromUriParam` is not satisfied + --> $DIR/typed-uri-bad-type.rs:60:25 | -57 | 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,82 +94,48 @@ 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:59:24 +error[E0277]: the trait bound `S: FromUriParam` is not satisfied + --> $DIR/typed-uri-bad-type.rs:62:24 | -59 | 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:61:26 +error[E0277]: the trait bound `S: FromUriParam` is not satisfied + --> $DIR/typed-uri-bad-type.rs:64:26 | -61 | 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:36:29 +error[E0277]: the trait bound `S: Ignorable` is not satisfied + --> $DIR/typed-uri-bad-type.rs:66:26 | -36 | fn other_q(id: usize, rest: S) { } - | ^ the trait `Ignorable` is not implemented for `S` -... -63 | uri!(other_q: rest = _, id = 100); - | ---------------------------------- in this macro invocation +66 | uri!(other_q: rest = _, id = 100); + | ^ the trait `Ignorable` is not implemented for `S` | ::: $WORKSPACE/core/http/src/uri/uri_display.rs | | pub fn assert_ignorable>() { } | ------------ required by this bound in `assert_ignorable` - | - = note: this error originates in a macro (in Nightly builds, run with -Z macro-backtrace for more info) -error[E0277]: the trait bound `usize: Ignorable` is not satisfied - --> $DIR/typed-uri-bad-type.rs:36:16 +error[E0277]: the trait bound `usize: Ignorable` is not satisfied + --> $DIR/typed-uri-bad-type.rs:68:34 | -36 | fn other_q(id: usize, rest: S) { } - | ^^^^^ the trait `Ignorable` is not implemented for `usize` -... -65 | uri!(other_q: rest = S, id = _); - | -------------------------------- in this macro invocation +68 | uri!(other_q: rest = S, id = _); + | ^ the trait `Ignorable` is not implemented for `usize` | ::: $WORKSPACE/core/http/src/uri/uri_display.rs | | pub fn assert_ignorable>() { } | ------------ required by this bound in `assert_ignorable` - | - = note: this error originates in a macro (in Nightly builds, run with -Z macro-backtrace for more info) - -error[E0277]: the trait bound `S: FromUriParam` is not satisfied - --> $DIR/typed-uri-bad-type.rs:65:26 - | -65 | 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 `std::option::Option: FromUriParam` is not satisfied - --> $DIR/typed-uri-bad-type.rs:69:28 +error[E0277]: the trait bound `S: FromUriParam` is not satisfied + --> $DIR/typed-uri-bad-type.rs:68:26 | -69 | uri!(optionals_q: id = 10, name = "Bob".to_string()); - | ^^ the trait `FromUriParam` is not implemented for `std::option::Option` +68 | uri!(other_q: rest = S, id = _); + | ^ the trait `FromUriParam` is not implemented for `S` | - = help: the following implementations were found: - as FromUriParam> - as FromUriParam>> - as FromUriParam>> - = note: required by `from_uri_param` - -error[E0277]: the trait bound `std::result::Result: FromUriParam` is not satisfied - --> $DIR/typed-uri-bad-type.rs:69:39 - | -69 | uri!(optionals_q: id = 10, name = "Bob".to_string()); - | ^^^^^ the trait `FromUriParam` is not implemented for `std::result::Result` - | - = help: the following implementations were found: - as FromUriParam> - as FromUriParam>> - as FromUriParam>> = note: required by `from_uri_param` diff --git a/core/codegen/tests/ui-fail-stable/uri_display.stderr b/core/codegen/tests/ui-fail-stable/uri_display.stderr index b0730b4de9..4e3b090a78 100644 --- a/core/codegen/tests/ui-fail-stable/uri_display.stderr +++ b/core/codegen/tests/ui-fail-stable/uri_display.stderr @@ -1,8 +1,8 @@ error: fieldless structs or variants are not supported - --> $DIR/uri_display.rs:4:8 + --> $DIR/uri_display.rs:4:1 | 4 | struct Foo1; - | ^^^^ + | ^^^^^^ error: [note] error occurred while deriving `UriDisplay` --> $DIR/uri_display.rs:3:10 @@ -13,10 +13,10 @@ error: [note] error occurred while deriving `UriDisplay` = note: this error originates in a derive macro (in Nightly builds, run with -Z macro-backtrace for more info) error: fieldless structs or variants are not supported - --> $DIR/uri_display.rs:7:8 + --> $DIR/uri_display.rs:7:1 | 7 | struct Foo2(); - | ^^^^ + | ^^^^^^ error: [note] error occurred while deriving `UriDisplay` --> $DIR/uri_display.rs:6:10 @@ -71,7 +71,7 @@ error: [note] error occurred while deriving `UriDisplay` error: invalid value: expected string literal --> $DIR/uri_display.rs:22:20 | -22 | #[form(field = 123)] +22 | #[field(name = 123)] | ^^^ error: [note] error occurred while deriving `UriDisplay` @@ -97,10 +97,10 @@ error: [note] error occurred while deriving `UriDisplay` = note: this error originates in a derive macro (in Nightly builds, run with -Z macro-backtrace for more info) error: struct must have exactly one field - --> $DIR/uri_display.rs:30:8 + --> $DIR/uri_display.rs:30:1 | 30 | struct Foo8; - | ^^^^ + | ^^^^^^ error: [note] error occurred while deriving `UriDisplay` --> $DIR/uri_display.rs:29:10 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 45f0b8f923..1a473fa0e2 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,56 +1,56 @@ -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 --> $DIR/uri_display_type_errors.rs:40:12 diff --git a/core/codegen/tests/ui-fail/from_form.rs b/core/codegen/tests/ui-fail/from_form.rs index 007be45c2b..3133c96912 100644 --- a/core/codegen/tests/ui-fail/from_form.rs +++ b/core/codegen/tests/ui-fail/from_form.rs @@ -1,7 +1,5 @@ #[macro_use] extern crate rocket; -use rocket::http::RawStr; - #[derive(FromForm)] enum Thing { } @@ -17,127 +15,127 @@ struct Foo3(usize); #[derive(FromForm)] struct NextTodoTask<'f, 'a> { description: String, - raw_description: &'f RawStr, - other: &'a RawStr, + raw_description: &'f str, + other: &'a str, completed: bool, } #[derive(FromForm)] struct BadName1 { - #[form(field = "isindex")] + #[field(name = "isindex")] field: String, } #[derive(FromForm)] struct Demo2 { - #[form(field = "foo")] + #[field(name = "foo")] field: String, foo: usize, } #[derive(FromForm)] struct MyForm9 { - #[form(field = "hello")] + #[field(name = "hello")] first: String, - #[form(field = "hello")] + #[field(name = "hello")] other: String, } #[derive(FromForm)] struct MyForm10 { first: String, - #[form(field = "first")] + #[field(name = "first")] other: String, } #[derive(FromForm)] struct MyForm { - #[form(field = "blah", field = "bloo")] + #[field(name = "blah", field = "bloo")] my_field: String, } #[derive(FromForm)] struct MyForm1 { - #[form] + #[field] my_field: String, } #[derive(FromForm)] struct MyForm2 { - #[form("blah")] + #[field("blah")] my_field: String, } #[derive(FromForm)] struct MyForm3 { - #[form(123)] + #[field(123)] my_field: String, } #[derive(FromForm)] struct MyForm4 { - #[form(beep = "bop")] + #[field(beep = "bop")] my_field: String, } #[derive(FromForm)] struct MyForm5 { - #[form(field = "blah")] - #[form(field = "bleh")] + #[field(name = "blah")] + #[field(name = "blah")] my_field: String, } #[derive(FromForm)] struct MyForm6 { - #[form(field = true)] + #[field(name = true)] my_field: String, } #[derive(FromForm)] struct MyForm7 { - #[form(field)] + #[field(name)] my_field: String, } #[derive(FromForm)] struct MyForm8 { - #[form(field = 123)] + #[field(name = 123)] my_field: String, } #[derive(FromForm)] struct MyForm11 { - #[form(field = "hello&world")] + #[field(name = "hello&world")] first: String, } #[derive(FromForm)] struct MyForm12 { - #[form(field = "!@#$%^&*()_")] + #[field(name = "!@#$%^&*()_")] first: String, } #[derive(FromForm)] struct MyForm13 { - #[form(field = "?")] + #[field(name = "?")] first: String, } #[derive(FromForm)] struct MyForm14 { - #[form(field = "")] + #[field(name = "")] first: String, } #[derive(FromForm)] struct BadName2 { - #[form(field = "a&b")] + #[field(name = "a&b")] field: String, } #[derive(FromForm)] struct BadName3 { - #[form(field = "a=")] + #[field(name = "a=")] field: String, } diff --git a/core/codegen/tests/ui-fail/from_form_field.rs b/core/codegen/tests/ui-fail/from_form_field.rs new file mode 100644 index 0000000000..5a53664c57 --- /dev/null +++ b/core/codegen/tests/ui-fail/from_form_field.rs @@ -0,0 +1,39 @@ +#[macro_use] extern crate rocket; + +#[derive(FromFormField)] +struct Foo1; + +#[derive(FromFormField)] +struct Foo2(usize); + +#[derive(FromFormField)] +struct Foo3 { + foo: usize, +} + +#[derive(FromFormField)] +enum Foo4 { + A(usize), +} + +#[derive(FromFormField)] +enum Foo5 { } + +#[derive(FromFormField)] +enum Foo6 { + A(T), +} + +#[derive(FromFormField)] +enum Bar1 { + #[field(value = 123)] + A, +} + +#[derive(FromFormField)] +enum Bar2 { + #[field(value)] + A, +} + +fn main() { } diff --git a/core/codegen/tests/ui-fail/from_form_value.rs b/core/codegen/tests/ui-fail/from_form_value.rs deleted file mode 100644 index a24f1f7add..0000000000 --- a/core/codegen/tests/ui-fail/from_form_value.rs +++ /dev/null @@ -1,39 +0,0 @@ -#[macro_use] extern crate rocket; - -#[derive(FromFormValue)] -struct Foo1; - -#[derive(FromFormValue)] -struct Foo2(usize); - -#[derive(FromFormValue)] -struct Foo3 { - foo: usize, -} - -#[derive(FromFormValue)] -enum Foo4 { - A(usize), -} - -#[derive(FromFormValue)] -enum Foo5 { } - -#[derive(FromFormValue)] -enum Foo6 { - A(T), -} - -#[derive(FromFormValue)] -enum Bar1 { - #[form(value = 123)] - A, -} - -#[derive(FromFormValue)] -enum Bar2 { - #[form(value)] - A, -} - -fn main() { } 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 ce348284ca..4379561b14 100644 --- a/core/codegen/tests/ui-fail/typed-uri-bad-type.rs +++ b/core/codegen/tests/ui-fail/typed-uri-bad-type.rs @@ -1,13 +1,12 @@ #[macro_use] extern crate rocket; -use rocket::http::RawStr; use rocket::request::FromParam; struct S; impl<'a> FromParam<'a> for S { type Error = (); - fn from_param(param: &'a RawStr) -> Result { Ok(S) } + fn from_param(param: &'a str) -> Result { Ok(S) } } #[post("/")] @@ -20,13 +19,17 @@ fn not_uri_display(id: i32, name: S) { } fn not_uri_display_but_unused(id: i32, name: S) { } #[post("//")] -fn optionals(id: Option, name: Result) { } +fn optionals(id: Option, name: Result) { } -use rocket::request::{Query, FromQuery}; +use rocket::form::{FromFormField, Errors, ValueField, DataField}; -impl<'q> FromQuery<'q> for S { - type Error = (); - fn from_query(query: Query<'q>) -> Result { Ok(S) } +#[rocket::async_trait] +impl<'v> FromFormField<'v> for S { + fn default() -> Option { None } + + fn from_value(_: ValueField<'v>) -> Result> { Ok(S) } + + async fn from_data(_: DataField<'v, '_>) -> Result> { Ok(S) } } #[post("/?")] @@ -36,7 +39,7 @@ fn simple_q(id: isize) { } fn other_q(id: usize, rest: S) { } #[post("/?&")] -fn optionals_q(id: Option, name: Result) { } +fn optionals_q(id: Option, name: Result>) { } fn main() { uri!(simple: id = "hi"); @@ -66,7 +69,7 @@ fn main() { // These are all okay. uri!(optionals_q: _, _); - uri!(optionals_q: id = 10, name = "Bob".to_string()); - uri!(optionals_q: _, "Bob".into()); + uri!(optionals_q: id = Some(10), name = Some("Bob".to_string())); + uri!(optionals_q: _, Some("Bob".into())); uri!(optionals_q: id = _, name = _); } 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 dc7cbe191c..9f18a27e1b 100644 --- a/core/codegen/tests/ui-fail/typed-uris-bad-params.rs +++ b/core/codegen/tests/ui-fail/typed-uris-bad-params.rs @@ -1,6 +1,6 @@ #[macro_use] extern crate rocket; -use rocket::http::{CookieJar, RawStr}; +use rocket::http::CookieJar; #[post("/")] fn has_one(id: i32) { } @@ -12,7 +12,7 @@ fn has_one_guarded(cookies: &CookieJar<'_>, id: i32) { } fn has_two(cookies: &CookieJar<'_>, id: i32, name: String) { } #[post("//")] -fn optionals(id: Option, name: Result) { } +fn optionals(id: Option, name: Result) { } fn main() { uri!(has_one); diff --git a/core/codegen/tests/ui-fail/uri_display.rs b/core/codegen/tests/ui-fail/uri_display.rs index 42cc6db300..4d469f92c2 100644 --- a/core/codegen/tests/ui-fail/uri_display.rs +++ b/core/codegen/tests/ui-fail/uri_display.rs @@ -19,7 +19,7 @@ struct Foo5(String, String); #[derive(UriDisplayQuery)] struct Foo6 { - #[form(field = 123)] + #[field(name = 123)] field: String, } diff --git a/core/codegen/tests/uri_display.rs b/core/codegen/tests/uri_display.rs index c1f41b8f9f..5cd5aa477a 100644 --- a/core/codegen/tests/uri_display.rs +++ b/core/codegen/tests/uri_display.rs @@ -1,6 +1,5 @@ #[macro_use] extern crate rocket; -use rocket::http::RawStr; use rocket::http::uri::{UriDisplay, Query, Path}; macro_rules! assert_uri_display_query { @@ -12,13 +11,13 @@ macro_rules! assert_uri_display_query { #[derive(UriDisplayQuery, Clone)] enum Foo<'r> { - First(&'r RawStr), + First(&'r str), Second { - inner: &'r RawStr, + inner: &'r str, other: usize, }, Third { - #[form(field = "type")] + #[field(name = "type")] kind: String, }, } @@ -95,7 +94,7 @@ fn uri_display_baz() { struct Bam<'a> { foo: &'a str, bar: Option, - baz: Result<&'a RawStr, usize>, + baz: Result<&'a str, usize>, } #[test] diff --git a/core/http/Cargo.toml b/core/http/Cargo.toml index fad87ea4c5..011a3150f9 100644 --- a/core/http/Cargo.toml +++ b/core/http/Cargo.toml @@ -18,6 +18,7 @@ edition = "2018" default = [] tls = ["tokio-rustls"] private-cookies = ["cookie/private", "cookie/key-expansion"] +serde = ["uncased/with-serde-alloc", "_serde"] [dependencies] smallvec = "1.0" @@ -27,22 +28,31 @@ http = "0.2" mime = "0.3.13" time = "0.2.11" indexmap = { version = "1.5.2", features = ["std"] } -state = "0.4" tokio-rustls = { version = "0.22.0", optional = true } tokio = { version = "1.0", features = ["net", "sync", "time"] } unicode-xid = "0.2" log = "0.4" ref-cast = "1.0" -uncased = "0.9" +uncased = "0.9.4" parking_lot = "0.11" either = "1" -pear = "0.2" +pear = "0.2.1" pin-project-lite = "0.2" +memchr = "2" +stable-pattern = "0.1" +cookie = { version = "0.15", features = ["percent-encode"] } + +[dependencies.state] +git = "https://github.com/SergioBenitez/state.git" +rev = "7576652" + +[dependencies._serde] +package = "serde" +version = "1.0" +optional = true +default-features = false +features = ["std"] -[dependencies.cookie] -git = "https://github.com/SergioBenitez/cookie-rs.git" -rev = "1c3ca83" -features = ["percent-encode"] [dev-dependencies] rocket = { version = "0.5.0-dev", path = "../lib" } diff --git a/core/http/src/cookies.rs b/core/http/src/cookies.rs index cb4e5ca8cb..6c27c9f861 100644 --- a/core/http/src/cookies.rs +++ b/core/http/src/cookies.rs @@ -15,6 +15,7 @@ mod key { /// Types and methods to manage a `Key` when private cookies are disabled. #[cfg(not(feature = "private-cookies"))] +#[allow(missing_docs)] mod key { #[derive(Copy, Clone)] pub struct Key; diff --git a/core/http/src/ext.rs b/core/http/src/ext.rs index ee9ea2a713..7741d31cf2 100644 --- a/core/http/src/ext.rs +++ b/core/http/src/ext.rs @@ -1,6 +1,7 @@ //! Extension traits implemented by several HTTP types. use smallvec::{Array, SmallVec}; +use state::Storage; // TODO: It would be nice if we could somehow have one trait that could give us // either SmallVec or Vec. @@ -96,6 +97,38 @@ impl IntoOwned for Option { } } +impl IntoOwned for Vec { + type Owned = Vec; + + #[inline(always)] + fn into_owned(self) -> Self::Owned { + self.into_iter() + .map(|inner| inner.into_owned()) + .collect() + } +} + +impl IntoOwned for Storage + where T::Owned: Send + Sync +{ + type Owned = Storage; + + #[inline(always)] + fn into_owned(self) -> Self::Owned { + self.map(|inner| inner.into_owned()) + } +} + +impl IntoOwned for (A, B) { + type Owned = (A::Owned, B::Owned); + + #[inline(always)] + fn into_owned(self) -> Self::Owned { + (self.0.into_owned(), self.1.into_owned()) + } +} + + impl IntoOwned for Cow<'_, B> { type Owned = Cow<'static, B>; diff --git a/core/http/src/accept.rs b/core/http/src/header/accept.rs similarity index 100% rename from core/http/src/accept.rs rename to core/http/src/header/accept.rs diff --git a/core/http/src/content_type.rs b/core/http/src/header/content_type.rs similarity index 86% rename from core/http/src/content_type.rs rename to core/http/src/header/content_type.rs index 9856fb2b94..36bcbfdf9d 100644 --- a/core/http/src/content_type.rs +++ b/core/http/src/header/content_type.rs @@ -3,8 +3,8 @@ use std::ops::Deref; use std::str::FromStr; use std::fmt; -use crate::header::Header; -use crate::media_type::{MediaType, Source}; +use crate::header::{Header, MediaType}; +use crate::uncased::UncasedStr; use crate::ext::IntoCollection; /// Representation of HTTP Content-Types. @@ -102,6 +102,47 @@ macro_rules! from_extension { );) } +macro_rules! extension { + ($($ext:expr => $name:ident,)*) => ( + docify!([ + Returns the most common file extension associated with the + @[Content-Type] @code{self} if it is known. Otherwise, returns + @code{None}. The currently recognized extensions are identical to those + in @{"[`ContentType::from_extension()`]"} with the @{"most common"} + extension being the first extension appearing in the list for a given + @[Content-Type]. + ]; + /// # Example + /// + /// Known extension: + /// + /// ```rust + /// # extern crate rocket; + /// use rocket::http::ContentType; + /// + /// assert_eq!(ContentType::JSON.extension().unwrap(), "json"); + /// assert_eq!(ContentType::JPEG.extension().unwrap(), "jpeg"); + /// assert_eq!(ContentType::JPEG.extension().unwrap(), "JPEG"); + /// assert_eq!(ContentType::PDF.extension().unwrap(), "pdf"); + /// ``` + /// + /// An unknown extension: + /// + /// ```rust + /// # extern crate rocket; + /// use rocket::http::ContentType; + /// + /// let foo = ContentType::new("foo", "bar"); + /// assert!(foo.extension().is_none()); + /// ``` + #[inline] + pub fn extension(&self) -> Option<&UncasedStr> { + $(if self == &ContentType::$name { return Some($ext.into()) })* + None + } + );) +} + macro_rules! parse_flexible { ($($short:expr => $name:ident,)*) => ( docify!([ @@ -245,6 +286,8 @@ impl ContentType { &self.0 } + known_extensions!(extension); + known_media_types!(content_types); } @@ -336,14 +379,7 @@ impl fmt::Display for ContentType { impl Into> for ContentType { #[inline(always)] fn into(self) -> Header<'static> { - // FIXME: For known media types, don't do `to_string`. Store the whole - // string as a `source` and have a way to know that the source is - // everything. That removes the allocation here. Then, in - // `MediaType::fmt`, write the source string out directly as well. - // - // We could also use an `enum` for MediaType. But that kinda sucks. But - // maybe it's what we want. - if let Source::Known(src) = self.0.source { + if let Some(src) = self.known_source() { Header::new("Content-Type", src) } else { Header::new("Content-Type", self.to_string()) diff --git a/core/http/src/header.rs b/core/http/src/header/header.rs similarity index 100% rename from core/http/src/header.rs rename to core/http/src/header/header.rs diff --git a/core/http/src/known_media_types.rs b/core/http/src/header/known_media_types.rs similarity index 99% rename from core/http/src/known_media_types.rs rename to core/http/src/header/known_media_types.rs index f4d4e03451..66796ea560 100644 --- a/core/http/src/known_media_types.rs +++ b/core/http/src/header/known_media_types.rs @@ -104,5 +104,6 @@ macro_rules! known_shorthands { "css" => CSS, "multipart" => FormData, "xml" => XML, + "pdf" => PDF, }) } diff --git a/core/http/src/media_type.rs b/core/http/src/header/media_type.rs similarity index 85% rename from core/http/src/media_type.rs rename to core/http/src/header/media_type.rs index 27b4c30691..69fc514be0 100644 --- a/core/http/src/media_type.rs +++ b/core/http/src/header/media_type.rs @@ -7,7 +7,7 @@ use either::Either; use crate::ext::IntoCollection; use crate::uncased::UncasedStr; -use crate::parse::{Indexed, IndexedString, parse_media_type}; +use crate::parse::{Indexed, IndexedStr, parse_media_type}; use smallvec::SmallVec; @@ -54,24 +54,18 @@ pub struct MediaType { /// Storage for the entire media type string. pub(crate) source: Source, /// The top-level type. - pub(crate) top: IndexedString, + pub(crate) top: IndexedStr<'static>, /// The subtype. - pub(crate) sub: IndexedString, + pub(crate) sub: IndexedStr<'static>, /// The parameters, if any. pub(crate) params: MediaParams } -#[derive(Debug, Clone)] -struct MediaParam { - key: IndexedString, - value: IndexedString, -} - -// FIXME: `Static` is needed for `const` items. Need `const SmallVec::new`. +// FIXME: `Static` variant is needed for `const`. Need `const SmallVec::new`. #[derive(Debug, Clone)] pub(crate) enum MediaParams { Static(&'static [(&'static str, &'static str)]), - Dynamic(SmallVec<[(IndexedString, IndexedString); 2]>) + Dynamic(SmallVec<[(IndexedStr<'static>, IndexedStr<'static>); 2]>) } #[derive(Debug, Clone, PartialEq, Eq)] @@ -81,6 +75,12 @@ pub(crate) enum Source { None } +impl From> for Source { + fn from(custom: Cow<'static, str>) -> Source { + Source::Custom(custom) + } +} + macro_rules! media_types { ($($name:ident ($check:ident): $str:expr, $t:expr, $s:expr $(; $k:expr => $v:expr)*,)+) => { @@ -169,6 +169,47 @@ macro_rules! from_extension { );) } +macro_rules! extension { + ($($ext:expr => $name:ident,)*) => ( + docify!([ + Returns the most common file extension associated with the @[Media-Type] + @code{self} if it is known. Otherwise, returns @code{None}. The + currently recognized extensions are identical to those in + @{"[`MediaType::from_extension()`]"} with the @{"most common"} extension + being the first extension appearing in the list for a given + @[Media-Type]. + ]; + /// # Example + /// + /// Known extension: + /// + /// ```rust + /// # extern crate rocket; + /// use rocket::http::MediaType; + /// + /// assert_eq!(MediaType::JSON.extension().unwrap(), "json"); + /// assert_eq!(MediaType::JPEG.extension().unwrap(), "jpeg"); + /// assert_eq!(MediaType::JPEG.extension().unwrap(), "JPEG"); + /// assert_eq!(MediaType::PDF.extension().unwrap(), "pdf"); + /// ``` + /// + /// An unknown extension: + /// + /// ```rust + /// # extern crate rocket; + /// use rocket::http::MediaType; + /// + /// let foo = MediaType::new("foo", "bar"); + /// assert!(foo.extension().is_none()); + /// ``` + #[inline] + pub fn extension(&self) -> Option<&UncasedStr> { + $(if self == &MediaType::$name { return Some($ext.into()) })* + None + } + );) +} + macro_rules! parse_flexible { ($($short:expr => $name:ident,)*) => ( docify!([ @@ -353,6 +394,14 @@ impl MediaType { } } + pub(crate) fn known_source(&self) -> Option<&'static str> { + match self.source { + Source::Known(string) => Some(string), + Source::Custom(Cow::Borrowed(string)) => Some(string), + _ => None + } + } + known_shorthands!(parse_flexible); known_extensions!(from_extension); @@ -482,8 +531,9 @@ impl MediaType { /// use rocket::http::MediaType; /// /// let plain = MediaType::Plain; - /// let plain_params: Vec<_> = plain.params().collect(); - /// assert_eq!(plain_params, vec![("charset", "utf-8")]); + /// let (key, val) = plain.params().next().unwrap(); + /// assert_eq!(key, "charset"); + /// assert_eq!(val, "utf-8"); /// ``` /// /// The `MediaType::PNG` type has no parameters: @@ -496,8 +546,8 @@ impl MediaType { /// assert_eq!(png.params().count(), 0); /// ``` #[inline] - pub fn params<'a>(&'a self) -> impl Iterator + 'a { - match self.params { + pub fn params<'a>(&'a self) -> impl Iterator + 'a { + let raw = match self.params { MediaParams::Static(ref slice) => Either::Left(slice.iter().cloned()), MediaParams::Dynamic(ref vec) => { Either::Right(vec.iter().map(move |&(ref key, ref val)| { @@ -505,9 +555,22 @@ impl MediaType { (key.from_source(source_str), val.from_source(source_str)) })) } - } + }; + + raw.map(|(k, v)| (k.into(), v)) } + /// Returns the first parameter with name `name`, if there is any. + #[inline] + pub fn param<'a>(&'a self, name: &str) -> Option<&'a str> { + self.params() + .filter(|(k, _)| *k == name) + .map(|(_, v)| v) + .next() + } + + known_extensions!(extension); + known_media_types!(media_types); } @@ -544,7 +607,7 @@ impl Hash for MediaType { impl fmt::Display for MediaType { #[inline] fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - if let Source::Known(src) = self.source { + if let Some(src) = self.known_source() { src.fmt(f) } else { write!(f, "{}/{}", self.top(), self.sub())?; @@ -563,8 +626,10 @@ impl Default for MediaParams { } } -impl Extend<(IndexedString, IndexedString)> for MediaParams { - fn extend>(&mut self, iter: T) { +impl Extend<(IndexedStr<'static>, IndexedStr<'static>)> for MediaParams { + fn extend(&mut self, iter: T) + where T: IntoIterator, IndexedStr<'static>)> + { match self { MediaParams::Static(..) => panic!("can't add to static collection!"), MediaParams::Dynamic(ref mut v) => v.extend(iter) diff --git a/core/http/src/header/mod.rs b/core/http/src/header/mod.rs new file mode 100644 index 0000000000..b91521ff38 --- /dev/null +++ b/core/http/src/header/mod.rs @@ -0,0 +1,13 @@ +#[macro_use] +mod known_media_types; +mod media_type; +mod content_type; +mod accept; +mod header; + +pub use self::content_type::ContentType; +pub use self::accept::{Accept, QMediaType}; +pub use self::media_type::MediaType; +pub use self::header::{Header, HeaderMap}; + +pub(crate) use self::media_type::Source; diff --git a/core/http/src/lib.rs b/core/http/src/lib.rs index 56e8760a6f..888fdce14d 100644 --- a/core/http/src/lib.rs +++ b/core/http/src/lib.rs @@ -3,6 +3,7 @@ #![cfg_attr(nightly, feature(doc_cfg))] #![warn(rust_2018_idioms)] +#![warn(missing_docs)] //! Types that map to concepts in HTTP. //! @@ -12,12 +13,16 @@ //! //! [#17]: https://github.com/SergioBenitez/Rocket/issues/17 -#[macro_use] extern crate pear; +#[macro_use] +extern crate pear; pub mod hyper; pub mod uri; pub mod ext; +#[macro_use] +mod docify; + #[doc(hidden)] #[cfg(feature = "tls")] pub mod tls; @@ -26,16 +31,10 @@ pub mod tls; pub mod route; #[macro_use] -mod docify; -#[macro_use] -mod known_media_types; +mod header; mod cookies; mod method; -mod media_type; -mod content_type; mod status; -mod header; -mod accept; mod raw_str; mod parse; mod listener; @@ -64,10 +63,7 @@ pub mod private { } pub use crate::method::Method; -pub use crate::content_type::ContentType; -pub use crate::accept::{Accept, QMediaType}; pub use crate::status::{Status, StatusClass}; -pub use crate::header::{Header, HeaderMap}; pub use crate::raw_str::RawStr; -pub use crate::media_type::MediaType; pub use crate::cookies::{Cookie, CookieJar, SameSite}; +pub use crate::header::*; diff --git a/core/http/src/listener.rs b/core/http/src/listener.rs index f3265d0a05..9c0e35ce12 100644 --- a/core/http/src/listener.rs +++ b/core/http/src/listener.rs @@ -18,6 +18,7 @@ use tokio::net::{TcpListener, TcpStream}; // that they could be introduced in upstream libraries. /// A 'Listener' yields incoming connections pub trait Listener { + /// The connection type returned by this listener. type Connection: Connection; /// Return the actual address this listener bound to. @@ -29,6 +30,7 @@ pub trait Listener { /// A 'Connection' represents an open connection to a client pub trait Connection: AsyncRead + AsyncWrite { + /// The remote address, i.e. the client's socket address. fn remote_addr(&self) -> Option; } @@ -150,6 +152,7 @@ impl fmt::Debug for Incoming { } } +/// Binds a TCP listener to `address` and returns it. pub async fn bind_tcp(address: SocketAddr) -> io::Result { Ok(TcpListener::bind(address).await?) } diff --git a/core/http/src/method.rs b/core/http/src/method.rs index 962d519c5d..ecb06123cf 100644 --- a/core/http/src/method.rs +++ b/core/http/src/method.rs @@ -8,14 +8,23 @@ use self::Method::*; /// Representation of HTTP methods. #[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)] pub enum Method { + /// The `GET` variant. Get, + /// The `PUT` variant. Put, + /// The `POST` variant. Post, + /// The `DELETE` variant. Delete, + /// The `OPTIONS` variant. Options, + /// The `HEAD` variant. Head, + /// The `TRACE` variant. Trace, + /// The `CONNECT` variant. Connect, + /// The `PATCH` variant. Patch } diff --git a/core/http/src/parse/accept.rs b/core/http/src/parse/accept.rs index 7016d32d22..6ea87e1344 100644 --- a/core/http/src/parse/accept.rs +++ b/core/http/src/parse/accept.rs @@ -11,8 +11,13 @@ type Result<'a, T> = pear::input::Result>; #[parser] fn weighted_media_type<'a>(input: &mut Input<'a>) -> Result<'a, QMediaType> { let media_type = media_type()?; - let weight = match media_type.params().next() { - Some(("q", value)) if value.len() <= 5 => match value.parse::().ok() { + let q = match media_type.params().next() { + Some((name, value)) if name == "q" => Some(value), + _ => None + }; + + let weight = match q { + Some(value) if value.len() <= 5 => match value.parse::().ok() { Some(q) if q > 1. => parse_error!("q value must be <= 1")?, Some(q) if q < 0. => parse_error!("q value must be > 0")?, Some(q) => Some(q), diff --git a/core/http/src/parse/indexed.rs b/core/http/src/parse/indexed.rs index 699ab99021..1195aafed0 100644 --- a/core/http/src/parse/indexed.rs +++ b/core/http/src/parse/indexed.rs @@ -10,7 +10,6 @@ use crate::ext::IntoOwned; pub use pear::input::Extent; -pub type IndexedString = Indexed<'static, str>; pub type IndexedStr<'a> = Indexed<'a, str>; pub type IndexedBytes<'a> = Indexed<'a, [u8]>; @@ -32,9 +31,12 @@ impl AsPtr for [u8] { } } +/// Either a concrete string or indices to the start and end of a string. #[derive(PartialEq)] pub enum Indexed<'a, T: ?Sized + ToOwned> { + /// The start and end index of a string. Indexed(usize, usize), + /// A conrete string. Concrete(Cow<'a, T>) } @@ -111,16 +113,18 @@ impl<'a, T: ?Sized + ToOwned + 'a> Add for Indexed<'a, T> { impl<'a, T: ?Sized + ToOwned + 'a> Indexed<'a, T> where T: Length + AsPtr + Index, Output = T> { - // Returns `None` if `needle` is not a substring of `haystack`. + /// Returns `None` if `needle` is not a substring of `haystack`. Otherwise + /// returns an `Indexed` with the indices of `needle` in `haystack`. pub fn checked_from(needle: &T, haystack: &T) -> Option> { - let haystack_start = haystack.as_ptr() as usize; let needle_start = needle.as_ptr() as usize; - + let haystack_start = haystack.as_ptr() as usize; if needle_start < haystack_start { return None; } - if (needle_start + needle.len()) > (haystack_start + haystack.len()) { + let needle_end = needle_start + needle.len(); + let haystack_end = haystack_start + haystack.len(); + if needle_end > haystack_end { return None; } @@ -129,7 +133,13 @@ impl<'a, T: ?Sized + ToOwned + 'a> Indexed<'a, T> Some(Indexed::Indexed(start, end)) } - // Caller must ensure that `needle` is a substring of `haystack`. + /// Like `checked_from` but without checking if `needle` is indeed a + /// substring of `haystack`. + /// + /// # Safety + /// + /// The caller must ensure that `needle` is indeed a substring of + /// `haystack`. pub unsafe fn unchecked_from(needle: &T, haystack: &T) -> Indexed<'a, T> { let haystack_start = haystack.as_ptr() as usize; let needle_start = needle.as_ptr() as usize; @@ -148,7 +158,7 @@ impl<'a, T: ?Sized + ToOwned + 'a> Indexed<'a, T> } } - /// Whether this string is derived from indexes or not. + /// Whether this string is empty. #[inline] pub fn is_empty(&self) -> bool { self.len() == 0 diff --git a/core/http/src/parse/media_type.rs b/core/http/src/parse/media_type.rs index 80c889fc19..76f9a2aa33 100644 --- a/core/http/src/parse/media_type.rs +++ b/core/http/src/parse/media_type.rs @@ -5,7 +5,7 @@ use pear::combinators::{prefixed_series, surrounded}; use pear::macros::{parser, switch, parse}; use pear::parsers::*; -use crate::media_type::{MediaType, Source}; +use crate::header::{MediaType, Source}; use crate::parse::checkers::{is_whitespace, is_valid_token}; type Input<'a> = pear::input::Pear>; diff --git a/core/http/src/parse/uri/error.rs b/core/http/src/parse/uri/error.rs index e2576df05f..4e215ed260 100644 --- a/core/http/src/parse/uri/error.rs +++ b/core/http/src/parse/uri/error.rs @@ -14,8 +14,8 @@ use crate::ext::IntoOwned; /// `Display` implementation. In other words, by printing a value of this type. #[derive(Debug)] pub struct Error<'a> { - expected: Expected>, - index: usize, + pub(crate) expected: Expected>, + pub(crate) index: usize, } impl<'a> From>> for Error<'a> { diff --git a/core/http/src/parse/uri/mod.rs b/core/http/src/parse/uri/mod.rs index a28e72033d..6e6e6fd321 100644 --- a/core/http/src/parse/uri/mod.rs +++ b/core/http/src/parse/uri/mod.rs @@ -6,7 +6,7 @@ pub(crate) mod tables; use crate::uri::{Uri, Origin, Absolute, Authority}; -use self::parser::{uri, origin, authority_only, absolute_only, rocket_route_origin}; +use self::parser::{uri, origin, authority_only, absolute_only}; pub use self::error::Error; @@ -22,11 +22,6 @@ pub fn origin_from_str(s: &str) -> Result, Error<'_>> { Ok(parse!(origin: RawInput::new(s.as_bytes()))?) } -#[inline] -pub fn route_origin_from_str(s: &str) -> Result, Error<'_>> { - Ok(parse!(rocket_route_origin: RawInput::new(s.as_bytes()))?) -} - #[inline] pub fn authority_from_str(s: &str) -> Result, Error<'_>> { Ok(parse!(authority_only: RawInput::new(s.as_bytes()))?) diff --git a/core/http/src/parse/uri/parser.rs b/core/http/src/parse/uri/parser.rs index 6c50621691..540abb782d 100644 --- a/core/http/src/parse/uri/parser.rs +++ b/core/http/src/parse/uri/parser.rs @@ -3,7 +3,7 @@ use pear::input::{Extent, Rewind}; use pear::macros::{parser, switch, parse_current_marker, 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, is_rchar}; +use crate::parse::uri::tables::{is_reg_name_char, is_pchar, is_qchar}; use crate::parse::uri::RawInput; type Result<'a, T> = pear::input::Result>; @@ -35,13 +35,6 @@ pub fn origin<'a>(input: &mut RawInput<'a>) -> Result<'a, Origin<'a>> { (peek(b'/')?, path_and_query(is_pchar, is_qchar)?).1 } -#[parser] -pub fn rocket_route_origin<'a>(input: &mut RawInput<'a>) -> Result<'a, Origin<'a>> { - fn is_pchar_or_rchar(c: &u8) -> bool { is_pchar(c) || is_rchar(c) } - fn is_qchar_or_rchar(c: &u8) -> bool { is_qchar(c) || is_rchar(c) } - (peek(b'/')?, path_and_query(is_pchar_or_rchar, is_qchar_or_rchar)?).1 -} - #[parser] fn path_and_query<'a, F, Q>( input: &mut RawInput<'a>, @@ -57,7 +50,7 @@ fn path_and_query<'a, F, Q>( 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())) }) + Ok(unsafe { Origin::raw(input.start.into(), path.into(), query.map(|q| q.into())) }) } } diff --git a/core/http/src/parse/uri/tables.rs b/core/http/src/parse/uri/tables.rs index 3e070a36bd..5d9db6c051 100644 --- a/core/http/src/parse/uri/tables.rs +++ b/core/http/src/parse/uri/tables.rs @@ -1,3 +1,5 @@ +/// Takes a set of sets of byte characters, return a 2^8 array with non-zero +/// values at the indices corresponding to the character byte values. const fn char_table(sets: &[&[u8]]) -> [u8; 256] { let mut table = [0u8; 256]; @@ -40,10 +42,6 @@ pub const PATH_CHARS: [u8; 256] = char_table(&[ UNRESERVED, PCT_ENCODED, SUB_DELIMS, &[b':', b'@', b'/'] ]); -const ROUTE_CHARS: [u8; 256] = char_table(&[&[ - b'<', b'>' -]]); - const QUERY_CHARS: [u8; 256] = char_table(&[ &PATH_CHARS, &[b'/', b'?'], @@ -59,9 +57,6 @@ const REG_NAME_CHARS: [u8; 256] = char_table(&[ #[inline(always)] pub const fn is_pchar(&c: &u8) -> bool { PATH_CHARS[c as usize] != 0 } -#[inline(always)] -pub const fn is_rchar(&c: &u8) -> bool { ROUTE_CHARS[c as usize] != 0 } - #[inline(always)] pub const fn is_qchar(&c: &u8) -> bool { QUERY_CHARS[c as usize] != 0 } @@ -82,7 +77,6 @@ mod tests { fn check_tables() { test_char_table(&super::PATH_CHARS[..]); test_char_table(&super::QUERY_CHARS[..]); - test_char_table(&super::ROUTE_CHARS[..]); test_char_table(&super::REG_NAME_CHARS[..]); } } diff --git a/core/http/src/raw_str.rs b/core/http/src/raw_str.rs index 0106cd602b..75799b2a6a 100644 --- a/core/http/src/raw_str.rs +++ b/core/http/src/raw_str.rs @@ -1,4 +1,3 @@ -use std::ops::{Deref, DerefMut}; use std::borrow::Cow; use std::convert::AsRef; use std::cmp::Ordering; @@ -6,6 +5,7 @@ use std::str::Utf8Error; use std::fmt; use ref_cast::RefCast; +use stable_pattern::{Pattern, ReverseSearcher, Split, SplitInternal}; use crate::uncased::UncasedStr; @@ -52,11 +52,11 @@ use crate::uncased::UncasedStr; /// [`FromParam`]: rocket::request::FromParam /// [`FromFormValue`]: rocket::request::FromFormValue #[repr(transparent)] -#[derive(RefCast, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] +#[derive(RefCast, PartialEq, Eq, PartialOrd, Ord, Hash)] pub struct RawStr(str); impl RawStr { - /// Constructs an `&RawStr` from an `&str` at no cost. + /// Constructs an `&RawStr` from a string-like type at no cost. /// /// # Example /// @@ -64,14 +64,18 @@ impl RawStr { /// # extern crate rocket; /// use rocket::http::RawStr; /// - /// let raw_str = RawStr::from_str("Hello, world!"); + /// let raw_str = RawStr::new("Hello, world!"); /// /// // `into` can also be used; note that the type must be specified /// let raw_str: &RawStr = "Hello, world!".into(); /// ``` - #[inline(always)] - pub fn from_str(string: &str) -> &RawStr { - string.into() + pub fn new + ?Sized>(string: &S) -> &RawStr { + RawStr::ref_cast(string.as_ref()) + } + + /// Performs percent decoding. + fn _percent_decode(&self) -> percent_encoding::PercentDecode<'_> { + percent_encoding::percent_decode(self.as_bytes()) } /// Returns a percent-decoded version of the string. @@ -88,7 +92,7 @@ impl RawStr { /// # extern crate rocket; /// use rocket::http::RawStr; /// - /// let raw_str = RawStr::from_str("Hello%21"); + /// let raw_str = RawStr::new("Hello%21"); /// let decoded = raw_str.percent_decode(); /// assert_eq!(decoded, Ok("Hello!".into())); /// ``` @@ -101,12 +105,12 @@ impl RawStr { /// /// // Note: Rocket should never hand you a bad `&RawStr`. /// let bad_str = unsafe { std::str::from_utf8_unchecked(b"a=\xff") }; - /// let bad_raw_str = RawStr::from_str(bad_str); + /// let bad_raw_str = RawStr::new(bad_str); /// assert!(bad_raw_str.percent_decode().is_err()); /// ``` #[inline(always)] pub fn percent_decode(&self) -> Result, Utf8Error> { - percent_encoding::percent_decode(self.as_bytes()).decode_utf8() + self._percent_decode().decode_utf8() } /// Returns a percent-decoded version of the string. Any invalid UTF-8 @@ -121,7 +125,7 @@ impl RawStr { /// # extern crate rocket; /// use rocket::http::RawStr; /// - /// let raw_str = RawStr::from_str("Hello%21"); + /// let raw_str = RawStr::new("Hello%21"); /// let decoded = raw_str.percent_decode_lossy(); /// assert_eq!(decoded, "Hello!"); /// ``` @@ -134,12 +138,30 @@ impl RawStr { /// /// // Note: Rocket should never hand you a bad `&RawStr`. /// let bad_str = unsafe { std::str::from_utf8_unchecked(b"a=\xff") }; - /// let bad_raw_str = RawStr::from_str(bad_str); + /// let bad_raw_str = RawStr::new(bad_str); /// assert_eq!(bad_raw_str.percent_decode_lossy(), "a=�"); /// ``` #[inline(always)] pub fn percent_decode_lossy(&self) -> Cow<'_, str> { - percent_encoding::percent_decode(self.as_bytes()).decode_utf8_lossy() + self._percent_decode().decode_utf8_lossy() + } + + /// Replaces '+' with ' ' in `self`, allocating only when necessary. + fn _replace_plus(&self) -> Cow<'_, str> { + let string = self.as_str(); + let mut allocated = String::new(); // this is allocation free + for i in memchr::memchr_iter(b'+', string.as_bytes()) { + if allocated.is_empty() { + allocated = string.into(); + } + + unsafe { allocated.as_bytes_mut()[i] = b' '; } + } + + match allocated.is_empty() { + true => Cow::Borrowed(string), + false => Cow::Owned(allocated) + } } /// Returns a URL-decoded version of the string. This is identical to @@ -156,16 +178,16 @@ impl RawStr { /// # extern crate rocket; /// use rocket::http::RawStr; /// - /// let raw_str: &RawStr = "Hello%2C+world%21".into(); + /// let raw_str = RawStr::new("Hello%2C+world%21"); /// let decoded = raw_str.url_decode(); - /// assert_eq!(decoded, Ok("Hello, world!".to_string())); + /// assert_eq!(decoded.unwrap(), "Hello, world!"); /// ``` - pub fn url_decode(&self) -> Result { - // TODO: Make this more efficient! - let replaced = self.replace("+", " "); - RawStr::from_str(replaced.as_str()) - .percent_decode() - .map(|cow| cow.into_owned()) + pub fn url_decode(&self) -> Result, Utf8Error> { + let string = self._replace_plus(); + match percent_encoding::percent_decode(string.as_bytes()).decode_utf8()? { + Cow::Owned(s) => Ok(Cow::Owned(s)), + Cow::Borrowed(_) => Ok(string) + } } /// Returns a URL-decoded version of the string. @@ -196,14 +218,15 @@ impl RawStr { /// /// // Note: Rocket should never hand you a bad `&RawStr`. /// let bad_str = unsafe { std::str::from_utf8_unchecked(b"a+b=\xff") }; - /// let bad_raw_str = RawStr::from_str(bad_str); + /// let bad_raw_str = RawStr::new(bad_str); /// assert_eq!(bad_raw_str.url_decode_lossy(), "a b=�"); /// ``` - pub fn url_decode_lossy(&self) -> String { - let replaced = self.replace("+", " "); - RawStr::from_str(replaced.as_str()) - .percent_decode_lossy() - .into_owned() + pub fn url_decode_lossy(&self) -> Cow<'_, str> { + let string = self._replace_plus(); + match percent_encoding::percent_decode(string.as_bytes()).decode_utf8_lossy() { + Cow::Owned(s) => Cow::Owned(s), + Cow::Borrowed(_) => string + } } /// Returns an HTML escaped version of `self`. Allocates only when @@ -247,6 +270,9 @@ impl RawStr { /// let escaped = raw_str.html_escape(); /// assert_eq!(escaped, "大阪"); /// ``` + // NOTE: This is the ~fastest (a table-based implementation is slightly + // faster) implementation benchmarked for dense-ish escaping. For sparser + // texts, a regex-based-find solution is much faster. pub fn html_escape(&self) -> Cow<'_, str> { let mut escaped = false; let mut allocated = Vec::new(); // this is allocation free @@ -312,6 +338,44 @@ impl RawStr { } } + /// Returns the length of `self`. + /// + /// This length is in bytes, not [`char`]s or graphemes. In other words, + /// it may not be what a human considers the length of the string. + /// + /// # Example + /// + /// ```rust + /// # extern crate rocket; + /// use rocket::http::RawStr; + /// + /// let raw_str = RawStr::new("Hello, world!"); + /// assert_eq!(raw_str.len(), 13); + /// ``` + #[inline] + pub const fn len(&self) -> usize { + self.0.len() + } + + /// Returns `true` if `self` has a length of zero bytes. + /// + /// # Example + /// + /// ```rust + /// # extern crate rocket; + /// use rocket::http::RawStr; + /// + /// let raw_str = RawStr::new("Hello, world!"); + /// assert!(!raw_str.is_empty()); + /// + /// let raw_str = RawStr::new(""); + /// assert!(raw_str.is_empty()); + /// ``` + #[inline] + pub const fn is_empty(&self) -> bool { + self.len() == 0 + } + /// Converts `self` into an `&str`. /// /// This method should be used sparingly. **Only use this method when you @@ -323,12 +387,54 @@ impl RawStr { /// # extern crate rocket; /// use rocket::http::RawStr; /// - /// let raw_str = RawStr::from_str("Hello, world!"); + /// let raw_str = RawStr::new("Hello, world!"); /// assert_eq!(raw_str.as_str(), "Hello, world!"); /// ``` #[inline(always)] - pub fn as_str(&self) -> &str { - self + pub const fn as_str(&self) -> &str { + &self.0 + } + + /// Converts `self` into an `&[u8]`. + /// + /// # Example + /// + /// ```rust + /// # extern crate rocket; + /// use rocket::http::RawStr; + /// + /// let raw_str = RawStr::new("hi"); + /// assert_eq!(raw_str.as_bytes(), &[0x68, 0x69]); + /// ``` + #[inline(always)] + pub const fn as_bytes(&self) -> &[u8] { + self.0.as_bytes() + } + + /// Converts a string slice to a raw pointer. + /// + /// As string slices are a slice of bytes, the raw pointer points to a + /// [`u8`]. This pointer will be pointing to the first byte of the string + /// slice. + /// + /// The caller must ensure that the returned pointer is never written to. + /// If you need to mutate the contents of the string slice, use [`as_mut_ptr`]. + /// + /// [`as_mut_ptr`]: str::as_mut_ptr + /// + /// # Examples + /// + /// Basic usage: + /// + /// ``` + /// # extern crate rocket; + /// use rocket::http::RawStr; + /// + /// let raw_str = RawStr::new("hi"); + /// let ptr = raw_str.as_ptr(); + /// ``` + pub const fn as_ptr(&self) -> *const u8 { + self.as_str().as_ptr() } /// Converts `self` into an `&UncasedStr`. @@ -342,54 +448,317 @@ impl RawStr { /// # extern crate rocket; /// use rocket::http::RawStr; /// - /// let raw_str = RawStr::from_str("Content-Type"); + /// let raw_str = RawStr::new("Content-Type"); /// assert!(raw_str.as_uncased_str() == "content-TYPE"); /// ``` #[inline(always)] pub fn as_uncased_str(&self) -> &UncasedStr { self.as_str().into() } -} -impl<'a> From<&'a str> for &'a RawStr { - #[inline(always)] - fn from(string: &'a str) -> &'a RawStr { - RawStr::ref_cast(string) + /// Returns `true` if the given pattern matches a sub-slice of + /// this string slice. + /// + /// Returns `false` if it does not. + /// + /// The pattern can be a `&str`, [`char`], a slice of [`char`]s, or a + /// function or closure that determines if a character matches. + /// + /// [`char`]: prim@char + /// + /// # Examples + /// + /// Basic usage: + /// + /// ``` + /// # extern crate rocket; + /// use rocket::http::RawStr; + /// + /// let bananas = RawStr::new("bananas"); + /// + /// assert!(bananas.contains("nana")); + /// assert!(!bananas.contains("apples")); + /// ``` + #[inline] + pub fn contains<'a, P: Pattern<'a>>(&'a self, pat: P) -> bool { + pat.is_contained_in(self.as_str()) } -} -impl PartialEq for RawStr { - #[inline(always)] - fn eq(&self, other: &str) -> bool { - self.as_str() == other + /// Returns `true` if the given pattern matches a prefix of this + /// string slice. + /// + /// Returns `false` if it does not. + /// + /// The pattern can be a `&str`, [`char`], a slice of [`char`]s, or a + /// function or closure that determines if a character matches. + /// + /// [`char`]: prim@char + /// + /// # Examples + /// + /// Basic usage: + /// + /// ``` + /// # extern crate rocket; + /// use rocket::http::RawStr; + /// + /// let bananas = RawStr::new("bananas"); + /// + /// assert!(bananas.starts_with("bana")); + /// assert!(!bananas.starts_with("nana")); + /// ``` + pub fn starts_with<'a, P: Pattern<'a>>(&'a self, pat: P) -> bool { + pat.is_prefix_of(self.as_str()) + } + + /// Returns `true` if the given pattern matches a suffix of this + /// string slice. + /// + /// Returns `false` if it does not. + /// + /// The pattern can be a `&str`, [`char`], a slice of [`char`]s, or a + /// function or closure that determines if a character matches. + /// + /// [`char`]: prim@char + /// + /// # Examples + /// + /// Basic usage: + /// + /// ``` + /// # extern crate rocket; + /// use rocket::http::RawStr; + /// + /// let bananas = RawStr::new("bananas"); + /// + /// assert!(bananas.ends_with("anas")); + /// assert!(!bananas.ends_with("nana")); + /// ``` + pub fn ends_with<'a, P>(&'a self, pat: P) -> bool + where P: Pattern<'a>,

>::Searcher: ReverseSearcher<'a> + { + pat.is_suffix_of(self.as_str()) + } + + /// An iterator over substrings of this string slice, separated by + /// characters matched by a pattern. + /// + /// The pattern can be a `&str`, [`char`], a slice of [`char`]s, or a + /// function or closure that determines if a character matches. + /// + /// [`char`]: prim@char + /// + /// # Examples + /// + /// Simple patterns: + /// + /// ``` + /// # extern crate rocket; + /// use rocket::http::RawStr; + /// + /// let v: Vec<_> = RawStr::new("Mary had a little lamb") + /// .split(' ') + /// .map(|r| r.as_str()) + /// .collect(); + /// + /// assert_eq!(v, ["Mary", "had", "a", "little", "lamb"]); + /// ``` + #[inline] + pub fn split<'a, P>(&'a self, pat: P) -> impl Iterator + where P: Pattern<'a> + { + let split: Split<'_, P> = Split(SplitInternal { + start: 0, + end: self.len(), + matcher: pat.into_searcher(self.as_str()), + allow_trailing_empty: true, + finished: false, + }); + + split.map(|s| s.into()) + } + + /// Splits `self` into two pieces: the piece _before_ the first byte `b` and + /// the piece _after_ (not including `b`). Returns the tuple (`before`, + /// `after`). If `b` is not in `self`, or `b` is not an ASCII characters, + /// returns the entire string `self` as `before` and the empty string as + /// `after`. + /// + /// # Example + /// + /// ```rust + /// # extern crate rocket; + /// use rocket::http::RawStr; + /// + /// let haystack = RawStr::new("a good boy!"); + /// + /// let (before, after) = haystack.split_at_byte(b'a'); + /// assert_eq!(before, ""); + /// assert_eq!(after, " good boy!"); + /// + /// let (before, after) = haystack.split_at_byte(b' '); + /// assert_eq!(before, "a"); + /// assert_eq!(after, "good boy!"); + /// + /// let (before, after) = haystack.split_at_byte(b'o'); + /// assert_eq!(before, "a g"); + /// assert_eq!(after, "od boy!"); + /// + /// let (before, after) = haystack.split_at_byte(b'!'); + /// assert_eq!(before, "a good boy"); + /// assert_eq!(after, ""); + /// + /// let (before, after) = haystack.split_at_byte(b'?'); + /// assert_eq!(before, "a good boy!"); + /// assert_eq!(after, ""); + /// + /// let haystack = RawStr::new(""); + /// let (before, after) = haystack.split_at_byte(b' '); + /// assert_eq!(before, ""); + /// assert_eq!(after, ""); + /// ``` + #[inline] + pub fn split_at_byte(&self, b: u8) -> (&RawStr, &RawStr) { + if !b.is_ascii() { + return (self, &self[0..0]); + } + + match memchr::memchr(b, self.as_bytes()) { + // SAFETY: `b` is a character boundary since it's ASCII, `i` is in + // bounds in `self` (or else None), and i is at most len - 1, so i + + // 1 is at most len. + Some(i) => unsafe { + let s = self.as_str(); + let start = s.get_unchecked(0..i); + let end = s.get_unchecked((i + 1)..self.len()); + (start.into(), end.into()) + }, + None => (self, &self[0..0]) + } + } + + /// Parses this string slice into another type. + /// + /// Because `parse` is so general, it can cause problems with type + /// inference. As such, `parse` is one of the few times you'll see + /// the syntax affectionately known as the 'turbofish': `::<>`. This + /// helps the inference algorithm understand specifically which type + /// you're trying to parse into. + /// + /// `parse` can parse any type that implements the [`FromStr`] trait. + /// + /// # Errors + /// + /// Will return [`Err`] if it's not possible to parse this string slice into + /// the desired type. + /// + /// [`Err`]: FromStr::Err + /// + /// # Examples + /// + /// Basic usage + /// + /// ``` + /// # extern crate rocket; + /// use rocket::http::RawStr; + /// + /// let four: u32 = RawStr::new("4").parse().unwrap(); + /// + /// assert_eq!(4, four); + /// ``` + #[inline] + pub fn parse(&self) -> Result { + std::str::FromStr::from_str(self.as_str()) } } -impl PartialEq for RawStr { - #[inline(always)] - fn eq(&self, other: &String) -> bool { - self.as_str() == other.as_str() +#[cfg(feature = "serde")] +mod serde { + use _serde::{ser, de, Serialize, Deserialize}; + + use super::*; + + impl Serialize for RawStr { + fn serialize(&self, ser: S) -> Result + where S: ser::Serializer + { + self.as_str().serialize(ser) + } + } + + impl<'de: 'a, 'a> Deserialize<'de> for &'a RawStr { + fn deserialize(de: D) -> Result + where D: de::Deserializer<'de> + { + <&'a str as Deserialize<'de>>::deserialize(de).map(RawStr::new) + } } + } -impl PartialEq for &'_ RawStr { - #[inline(always)] - fn eq(&self, other: &String) -> bool { - self.as_str() == other.as_str() +impl fmt::Debug for RawStr { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + self.0.fmt(f) } } -impl PartialOrd for RawStr { +impl<'a> From<&'a str> for &'a RawStr { #[inline(always)] - fn partial_cmp(&self, other: &str) -> Option { - (self as &str).partial_cmp(other) + fn from(string: &'a str) -> &'a RawStr { + RawStr::new(string) } } +macro_rules! impl_partial { + ($A:ty : $B:ty) => ( + impl PartialEq<$A> for $B { + #[inline(always)] + fn eq(&self, other: &$A) -> bool { + let left: &str = self.as_ref(); + let right: &str = other.as_ref(); + left == right + } + } + + impl PartialOrd<$A> for $B { + #[inline(always)] + fn partial_cmp(&self, other: &$A) -> Option { + let left: &str = self.as_ref(); + let right: &str = other.as_ref(); + left.partial_cmp(right) + } + } + ) +} + +impl_partial!(RawStr : &RawStr); +impl_partial!(&RawStr : RawStr); + +impl_partial!(str : RawStr); +impl_partial!(str : &RawStr); +impl_partial!(&str : RawStr); +impl_partial!(&&str : RawStr); + +impl_partial!(Cow<'_, str> : RawStr); +impl_partial!(Cow<'_, str> : &RawStr); +impl_partial!(RawStr : Cow<'_, str>); +impl_partial!(&RawStr : Cow<'_, str>); + +impl_partial!(String : RawStr); +impl_partial!(String : &RawStr); + +impl_partial!(RawStr : String); +impl_partial!(&RawStr : String); + +impl_partial!(RawStr : str); +impl_partial!(RawStr : &str); +impl_partial!(RawStr : &&str); +impl_partial!(&RawStr : str); + impl AsRef for RawStr { #[inline(always)] fn as_ref(&self) -> &str { - self + self.as_str() } } @@ -400,19 +769,26 @@ impl AsRef<[u8]> for RawStr { } } -impl Deref for RawStr { - type Target = str; +impl> core::ops::Index for RawStr { + type Output = RawStr; + #[inline] + fn index(&self, index: I) -> &Self::Output { + self.as_str()[index].into() + } +} + +impl std::borrow::Borrow for RawStr { #[inline(always)] - fn deref(&self) -> &str { - &self.0 + fn borrow(&self) -> &str { + self.as_str() } } -impl DerefMut for RawStr { +impl std::borrow::Borrow for &str { #[inline(always)] - fn deref_mut(&mut self) -> &mut str { - &mut self.0 + fn borrow(&self) -> &RawStr { + (*self).into() } } @@ -429,10 +805,10 @@ mod tests { #[test] fn can_compare() { - let raw_str = RawStr::from_str("abc"); + let raw_str = RawStr::new("abc"); assert_eq!(raw_str, "abc"); assert_eq!("abc", raw_str.as_str()); - assert_eq!(raw_str, RawStr::from_str("abc")); + assert_eq!(raw_str, RawStr::new("abc")); assert_eq!(raw_str, "abc".to_string()); assert_eq!("abc".to_string(), raw_str.as_str()); } diff --git a/core/http/src/route.rs b/core/http/src/route.rs index d4c357f236..30176a8eac 100644 --- a/core/http/src/route.rs +++ b/core/http/src/route.rs @@ -5,7 +5,6 @@ use unicode_xid::UnicodeXID; use crate::ext::IntoOwned; use crate::uri::{Origin, UriPart, Path, Query}; -use crate::uri::encoding::unsafe_percent_encode; use self::Error::*; @@ -120,8 +119,6 @@ impl<'a, P: UriPart> RouteSegment<'a, P> { return Err(MissingClose); } else if segment.contains('>') || segment.contains('<') { return Err(Malformed); - } else if unsafe_percent_encode::

(segment) != segment { - return Err(Uri); } Ok(RouteSegment { @@ -132,12 +129,13 @@ impl<'a, P: UriPart> RouteSegment<'a, P> { }) } - pub fn parse_many( - string: &'a str, + pub fn parse_many + ?Sized> ( + string: &'a S, ) -> impl Iterator> { let mut last_multi_seg: Option<&str> = None; // We check for empty segments when we parse an `Origin` in `FromMeta`. - string.split(P::DELIMITER) + string.as_ref() + .split(P::DELIMITER) .filter(|s| !s.is_empty()) .enumerate() .map(move |(i, seg)| { @@ -158,12 +156,12 @@ impl<'a, P: UriPart> RouteSegment<'a, P> { impl<'a> RouteSegment<'a, Path> { pub fn parse(uri: &'a Origin<'_>) -> impl Iterator> { - Self::parse_many(uri.path()) + Self::parse_many(uri.path().as_str()) } } impl<'a> RouteSegment<'a, Query> { pub fn parse(uri: &'a Origin<'_>) -> Option>> { - uri.query().map(|q| Self::parse_many(q)) + uri.query().map(|q| Self::parse_many(q.as_str())) } } diff --git a/core/http/src/status.rs b/core/http/src/status.rs index 00e673b6ae..5da1e2ab55 100644 --- a/core/http/src/status.rs +++ b/core/http/src/status.rs @@ -50,6 +50,26 @@ impl StatusClass { /// constant should be used; one is declared for every status defined /// in the HTTP standard. /// +/// # Responding +/// +/// To set a custom `Status` on a response, use a [`response::status`] +/// responder. Alternatively, respond with `(Status, T)` where `T: Responder`, but +/// note that the response may require additional headers to be valid as +/// enforced by the types in [`response::status`]. +/// +/// ```rust +/// # extern crate rocket; +/// # use rocket::get; +/// use rocket::http::Status; +/// +/// #[get("/")] +/// fn index() -> (Status, &'static str) { +/// (Status::NotFound, "Hey, there's no index!") +/// } +/// ``` +/// +/// [`response::status`]: ../response/status/index.html +/// /// ## Example /// /// A status of `200 OK` can be instantiated via the `Ok` constant: @@ -84,7 +104,7 @@ impl StatusClass { /// assert_eq!(not_found.reason, "Not Found"); /// assert_eq!(not_found.to_string(), "404 Not Found".to_string()); /// ``` -#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq)] +#[derive(Debug, Clone, Copy)] pub struct Status { /// The HTTP status code associated with this status. pub code: u16, @@ -92,6 +112,12 @@ pub struct Status { pub reason: &'static str } +impl Default for Status { + fn default() -> Self { + Status::Ok + } +} + macro_rules! ctrs { ($($code:expr, $code_str:expr, $name:ident => $reason:expr),+) => { $( @@ -277,3 +303,29 @@ impl fmt::Display for Status { write!(f, "{} {}", self.code, self.reason) } } + +impl std::hash::Hash for Status { + fn hash(&self, state: &mut H) { + self.code.hash(state) + } +} + +impl PartialEq for Status { + fn eq(&self, other: &Self) -> bool { + self.code.eq(&other.code) + } +} + +impl Eq for Status { } + +impl PartialOrd for Status { + fn partial_cmp(&self, other: &Self) -> Option { + self.code.partial_cmp(&other.code) + } +} + +impl Ord for Status { + fn cmp(&self, other: &Self) -> std::cmp::Ordering { + self.code.cmp(&other.code) + } +} diff --git a/core/http/src/uri/absolute.rs b/core/http/src/uri/absolute.rs index 4fae0a2d77..8d3db5c01c 100644 --- a/core/http/src/uri/absolute.rs +++ b/core/http/src/uri/absolute.rs @@ -139,7 +139,7 @@ impl<'a> Absolute<'a> { /// assert_eq!(uri.scheme(), "file"); /// let origin = uri.origin().unwrap(); /// assert_eq!(origin.path(), "/web/home.html"); - /// assert_eq!(origin.query(), Some("new")); + /// assert_eq!(origin.query().unwrap(), "new"); /// /// let uri = Absolute::parse("https://rocket.rs").expect("valid URI"); /// assert_eq!(uri.origin(), None); @@ -148,9 +148,107 @@ impl<'a> Absolute<'a> { pub fn origin(&self) -> Option<&Origin<'a>> { self.origin.as_ref() } + + /// Sets the authority in `self` to `authority` and returns `self`. + /// + /// # Example + /// + /// ```rust + /// # extern crate rocket; + /// use rocket::http::uri::{Absolute, Authority}; + /// + /// 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); + /// ``` + #[inline(always)] + pub fn with_authority(mut self, authority: Authority<'a>) -> Self { + self.set_authority(authority); + self + } + + /// Sets the authority in `self` to `authority`. + /// + /// # Example + /// + /// ```rust + /// # extern crate rocket; + /// use rocket::http::uri::{Absolute, Authority}; + /// + /// let mut 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:443").unwrap(); + /// uri.set_authority(new_authority); + /// let authority = uri.authority().unwrap(); + /// assert_eq!(authority.host(), "google.com"); + /// assert_eq!(authority.port(), Some(443)); + /// ``` + #[inline(always)] + pub fn set_authority(&mut self, authority: Authority<'a>) { + self.authority = Some(authority); + } + + /// Sets the origin in `self` to `origin` and returns `self`. + /// + /// # 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").unwrap(); + /// let uri = uri.with_origin(new_origin); + /// let origin = uri.origin().unwrap(); + /// assert_eq!(origin.path(), "/launch"); + /// assert_eq!(origin.query(), None); + /// ``` + #[inline(always)] + pub fn with_origin(mut self, origin: Origin<'a>) -> Self { + self.set_origin(origin); + self + } + + /// Sets the origin in `self` to `origin`. + /// + /// # 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); + } } -impl<'b> PartialEq> for Absolute<'_> { +impl<'a, 'b> PartialEq> for Absolute<'a> { fn eq(&self, other: &Absolute<'b>) -> bool { self.scheme() == other.scheme() && self.authority() == other.authority() @@ -173,4 +271,3 @@ impl Display for Absolute<'_> { Ok(()) } } - diff --git a/core/http/src/uri/encoding.rs b/core/http/src/uri/encoding.rs index 1c4d9f22a4..a7bbdedebf 100644 --- a/core/http/src/uri/encoding.rs +++ b/core/http/src/uri/encoding.rs @@ -3,6 +3,7 @@ use std::borrow::Cow; use percent_encoding::{AsciiSet, utf8_percent_encode}; +use crate::RawStr; use crate::uri::{UriPart, Path, Query}; use crate::parse::uri::tables::PATH_CHARS; @@ -79,14 +80,6 @@ impl EncodeSet for DEFAULT_ENCODE_SET { .add(b'='); } -pub fn unsafe_percent_encode(string: &str) -> Cow<'_, str> { - match P::DELIMITER { - '/' => percent_encode::>(string), - '&' => percent_encode::>(string), - _ => percent_encode::(string) - } -} - -pub fn percent_encode(string: &str) -> Cow<'_, str> { - utf8_percent_encode(string, &S::SET).into() +pub fn percent_encode(string: &RawStr) -> Cow<'_, str> { + utf8_percent_encode(string.as_str(), &S::SET).into() } diff --git a/core/http/src/uri/from_uri_param.rs b/core/http/src/uri/from_uri_param.rs index fa473bf07c..65a62e19ed 100644 --- a/core/http/src/uri/from_uri_param.rs +++ b/core/http/src/uri/from_uri_param.rs @@ -1,6 +1,5 @@ use std::path::{Path, PathBuf}; -use crate::RawStr; use crate::uri::{self, UriPart, UriDisplay}; /// Conversion trait for parameters used in [`uri!`] invocations. @@ -52,19 +51,7 @@ use crate::uri::{self, UriPart, UriDisplay}; /// /// Because the [`FromUriParam::Target`] type is the same as the input type, the /// conversion is a no-op and free of cost, allowing an `&str` to be used in -/// place of a `String` without penalty. A similar no-op conversion exists for -/// [`&RawStr`](RawStr): -/// -/// ```rust -/// # extern crate rocket; -/// # use rocket::http::uri::{FromUriParam, UriPart}; -/// # struct S; -/// # type RawStr = S; -/// impl<'a, 'b, P: UriPart> FromUriParam for &'b RawStr { -/// type Target = &'a str; -/// # fn from_uri_param(s: &'a str) -> Self::Target { "hi" } -/// } -/// ``` +/// place of a `String` without penalty. /// /// # Provided Implementations /// @@ -72,7 +59,7 @@ use crate::uri::{self, UriPart, UriDisplay}; /// /// * `String`, `i8`, `i16`, `i32`, `i64`, `i128`, `isize`, `u8`, `u16`, /// `u32`, `u64`, `u128`, `usize`, `f32`, `f64`, `bool`, `IpAddr`, -/// `Ipv4Addr`, `Ipv6Addr`, `&str`, `&RawStr`, `Cow` +/// `Ipv4Addr`, `Ipv6Addr`, `&str`, `Cow` /// /// The following types have _identity_ implementations _only in [`Path`]_: /// @@ -87,9 +74,7 @@ use crate::uri::{self, UriPart, UriDisplay}; /// is expected by a route: /// /// * `&str` to `String` -/// * `&str` to `RawStr` /// * `String` to `&str` -/// * `String` to `RawStr` /// * `T` to `Form` /// /// The following conversions are implemented _only in [`Path`]_: @@ -136,12 +121,11 @@ use crate::uri::{self, UriPart, UriDisplay}; /// # #[macro_use] extern crate rocket; /// use std::fmt; /// -/// use rocket::http::RawStr; /// use rocket::http::uri::{Formatter, UriDisplay, FromUriParam, Query}; /// /// #[derive(FromForm)] /// struct User<'a> { -/// name: &'a RawStr, +/// name: &'a str, /// nickname: String, /// } /// @@ -166,12 +150,10 @@ use crate::uri::{self, UriPart, UriDisplay}; /// ```rust /// # #[macro_use] extern crate rocket; /// # use std::fmt; -/// use rocket::http::RawStr; -/// use rocket::request::Form; /// # use rocket::http::uri::{Formatter, UriDisplay, FromUriParam, Query}; /// # /// # #[derive(FromForm)] -/// # struct User<'a> { name: &'a RawStr, nickname: String, } +/// # struct User<'a> { name: &'a str, nickname: String, } /// # /// # impl UriDisplay for User<'_> { /// # fn fmt(&self, f: &mut Formatter) -> fmt::Result { @@ -186,13 +168,13 @@ use crate::uri::{self, UriPart, UriDisplay}; /// # User { name: name.into(), nickname: nickname.to_string() } /// # } /// # } -/// +/// # /// #[post("/?")] -/// fn some_route(name: &RawStr, user: Form) { /* .. */ } +/// fn some_route(name: &str, user: User<'_>) { /* .. */ } /// /// let uri = uri!(some_route: name = "hey", user = ("Robert Mike", "Bob")); /// assert_eq!(uri.path(), "/hey"); -/// assert_eq!(uri.query(), Some("name=Robert%20Mike&nickname=Bob")); +/// assert_eq!(uri.query().unwrap(), "name=Robert%20Mike&nickname=Bob"); /// ``` /// /// [`uri!`]: crate::uri @@ -306,16 +288,13 @@ impl_from_uri_param_identity! { impl_from_uri_param_identity! { ('a) &'a str, - ('a) &'a RawStr, ('a) Cow<'a, str> } impl_conversion_ref! { ('a) &'a str => String, - ('a, 'b) &'a str => &'b RawStr, - ('a) String => &'a str, - ('a) String => &'a RawStr + ('a) String => &'a str } impl_from_uri_param_identity!([uri::Path] ('a) &'a Path); diff --git a/core/http/src/uri/mod.rs b/core/http/src/uri/mod.rs index f13ff2e50b..94b14bfa4a 100644 --- a/core/http/src/uri/mod.rs +++ b/core/http/src/uri/mod.rs @@ -53,6 +53,9 @@ mod private { /// [`UriDisplay`]: crate::uri::UriDisplay /// [`Formatter`]: crate::uri::Formatter pub trait UriPart: private::Sealed { + /// The delimiter used to separate components of this URI part. + /// Specifically, `/` for `Path` and `&` for `Query`. + #[doc(hidden)] const DELIMITER: char; } diff --git a/core/http/src/uri/origin.rs b/core/http/src/uri/origin.rs index e28a1d0063..6fbc6343e6 100644 --- a/core/http/src/uri/origin.rs +++ b/core/http/src/uri/origin.rs @@ -3,7 +3,8 @@ use std::borrow::Cow; use crate::ext::IntoOwned; use crate::parse::{Indexed, Extent, IndexedStr}; -use crate::uri::{as_utf8_unchecked, Error, Segments}; +use crate::uri::{as_utf8_unchecked, Error, UriPart, Query, Path, Segments, QuerySegments}; +use crate::RawStr; use state::Storage; @@ -88,10 +89,12 @@ pub struct Origin<'a> { pub(crate) source: Option>, pub(crate) path: IndexedStr<'a>, pub(crate) query: Option>, - pub(crate) segment_count: Storage, + + pub(crate) decoded_path_segs: Storage>>, + pub(crate) decoded_query_segs: Storage, IndexedStr<'static>)>>, } -impl<'b> PartialEq> for Origin<'_> { +impl<'a, 'b> PartialEq> for Origin<'a> { fn eq(&self, other: &Origin<'b>) -> bool { self.path() == other.path() && self.query() == other.query() } @@ -105,12 +108,34 @@ impl IntoOwned for Origin<'_> { source: self.source.into_owned(), path: self.path.into_owned(), query: self.query.into_owned(), - segment_count: self.segment_count + 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::DELIMITER { + Query::DELIMITER => value.url_decode_lossy(), + Path::DELIMITER => value.percent_decode_lossy(), + _ => unreachable!("sealed trait admits only path and query") + }; + + 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> { + /// SAFETY: `source` must be UTF-8. #[inline] pub(crate) unsafe fn raw( source: Cow<'a, [u8]>, @@ -121,7 +146,9 @@ impl<'a> Origin<'a> { source: Some(as_utf8_unchecked(source)), path: path.into(), query: query.map(|q| q.into()), - segment_count: Storage::new() + + decoded_path_segs: Storage::new(), + decoded_query_segs: Storage::new(), } } @@ -136,7 +163,8 @@ impl<'a> Origin<'a> { source: None, path: Indexed::from(path.into()), query: query.map(|q| Indexed::from(q.into())), - segment_count: Storage::new() + decoded_path_segs: Storage::new(), + decoded_query_segs: Storage::new(), } } @@ -160,7 +188,7 @@ impl<'a> Origin<'a> { /// // Parse a valid origin URI. /// let uri = Origin::parse("/a/b/c?query").expect("valid URI"); /// assert_eq!(uri.path(), "/a/b/c"); - /// assert_eq!(uri.query(), Some("query")); + /// assert_eq!(uri.query().unwrap(), "query"); /// /// // Invalid URIs fail to parse. /// Origin::parse("foo bar").expect_err("invalid URI"); @@ -169,12 +197,26 @@ impl<'a> Origin<'a> { crate::parse::uri::origin_from_str(string) } - // Parses an `Origin` that may contain `<` or `>` characters which are - // invalid according to the RFC but used by Rocket's routing URIs. - // Don't use this outside of Rocket! + // Parses an `Origin` which is allowed to contain _any_ `UTF-8` character. + // The path must still be absolute `/..`. Don't use this outside of Rocket! #[doc(hidden)] pub fn parse_route(string: &'a str) -> Result, Error<'a>> { - crate::parse::uri::route_origin_from_str(string) + use pear::error::Expected; + + if !string.starts_with('/') { + return Err(Error { + expected: Expected::token(Some(&b'/'), string.as_bytes().get(0).cloned()), + index: 0, + }); + } + + let (path, query) = RawStr::new(string).split_at_byte(b'?'); + let query = match query.is_empty() { + false => Some(query.as_str()), + true => None, + }; + + Ok(Origin::new(path.as_str(), query)) } /// Parses the string `string` into an `Origin`. Parsing will never @@ -206,25 +248,24 @@ 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)?; - - let uri = match origin { - Origin { source: Some(_), path, query, segment_count } => Origin { - segment_count, - path: path.into_owned(), - query: query.into_owned(), - // At this point, it's impossible for anything to be borrowing - // `string` except for `source`, even though Rust doesn't know - // it. Because we're replacing `source` here, there can't - // possibly be a borrow remaining, it's safe to "move out of the - // borrow". - source: Some(Cow::Owned(string)), - }, - _ => unreachable!("parser always parses with a source") + debug_assert!(origin.source.is_some(), "Origin source parsed w/o source"); + + 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 + // borrow remaining, it's safe to "move out of the borrow". + source: Some(Cow::Owned(string)), }; - Ok(uri) + Ok(origin) } /// Returns `true` if `self` is normalized. Otherwise, returns `false`. @@ -248,9 +289,10 @@ impl<'a> Origin<'a> { /// assert!(!abnormal.is_normalized()); /// ``` pub fn is_normalized(&self) -> bool { - self.path().starts_with('/') && - !self.path().contains("//") && - !(self.path().len() > 1 && self.path().ends_with('/')) + let path_str = self.path().as_str(); + path_str.starts_with('/') && + !path_str.contains("//") && + !(path_str.len() > 1 && path_str.ends_with('/')) } /// Normalizes `self`. @@ -276,7 +318,7 @@ impl<'a> Origin<'a> { self } else { let mut new_path = String::with_capacity(self.path().len()); - for segment in self.segments() { + for segment in self.raw_path_segments() { use std::fmt::Write; let _ = write!(new_path, "/{}", segment); } @@ -315,8 +357,8 @@ impl<'a> Origin<'a> { /// assert_eq!(uri.path(), "/a/b/c"); /// ``` #[inline] - pub fn path(&self) -> &str { - self.path.from_cow_source(&self.source) + pub fn path(&self) -> &RawStr { + self.path.from_cow_source(&self.source).into() } /// Applies the function `f` to the internal `path` and returns a new @@ -334,14 +376,17 @@ impl<'a> Origin<'a> { /// /// 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| p.to_owned() + "/"), Some(expected_uri)); + /// assert_eq!(old_uri.map_path(|p| format!("{}/", 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| p.to_owned() + "/"), Some(expected_uri)); + /// assert_eq!(old_uri.map_path(|p| format!("{}/", p)), Some(expected_uri)); + /// + /// let old_uri = Origin::parse("/a/b/c/").unwrap(); + /// assert_eq!(old_uri.map_path(|p| format!("hi/{}", p)), None); /// ``` #[inline] - pub fn map_path String>(&self, f: F) -> Option { + pub fn map_path String>(&self, f: F) -> Option { let path = f(self.path()); if !path.starts_with('/') || !path.bytes().all(|b| crate::parse::uri::tables::is_pchar(&b)) @@ -353,7 +398,8 @@ impl<'a> Origin<'a> { source: self.source.clone(), path: Cow::from(path).into(), query: self.query.clone(), - segment_count: Storage::new(), + decoded_path_segs: Storage::new(), + decoded_query_segs: Storage::new(), }) } @@ -369,7 +415,7 @@ impl<'a> Origin<'a> { /// use rocket::http::uri::Origin; /// /// let uri = Origin::parse("/a/b/c?alphabet=true").unwrap(); - /// assert_eq!(uri.query(), Some("alphabet=true")); + /// assert_eq!(uri.query().unwrap(), "alphabet=true"); /// ``` /// /// A URI without the query part: @@ -382,8 +428,8 @@ impl<'a> Origin<'a> { /// assert_eq!(uri.query(), None); /// ``` #[inline] - pub fn query(&self) -> Option<&str> { - self.query.as_ref().map(|q| q.from_cow_source(&self.source)) + 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. @@ -395,7 +441,7 @@ impl<'a> Origin<'a> { /// use rocket::http::uri::Origin; /// /// let mut uri = Origin::parse("/a/b/c?query=some").unwrap(); - /// assert_eq!(uri.query(), Some("query=some")); + /// assert_eq!(uri.query().unwrap(), "query=some"); /// /// uri.clear_query(); /// assert_eq!(uri.query(), None); @@ -404,8 +450,69 @@ impl<'a> Origin<'a> { self.query = None; } - /// Returns an iterator over the segments of the path in this URI. Skips - /// empty segments. + /// Returns a (smart) iterator over the non-empty, percent-decoded segments + /// of the path of this URI. + /// + /// # Example + /// + /// ```rust + /// # 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 path_segments(&self) -> Segments<'_> { + let cached = self.decoded_path_segs.get_or_set(|| { + let (indexed, path) = (&self.path, self.path()); + self.raw_path_segments() + .map(|s| decode_to_indexed_str::(s, (indexed, path))) + .collect() + }); + + Segments { source: self.path(), segments: cached, pos: 0 } + } + + /// 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. + /// + /// # Example + /// + /// ```rust + /// # 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", "")]); + /// ``` + 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() + .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. /// /// ### Examples /// @@ -416,7 +523,10 @@ impl<'a> Origin<'a> { /// use rocket::http::uri::Origin; /// /// let uri = Origin::parse("/a/b/c?a=true").unwrap(); - /// for (i, segment) in uri.segments().enumerate() { + /// # let segments: Vec<_> = uri.raw_path_segments().collect(); + /// # assert_eq!(segments, &["a", "b", "c"]); + /// + /// for (i, segment) in uri.raw_path_segments().enumerate() { /// match i { /// 0 => assert_eq!(segment, "a"), /// 1 => assert_eq!(segment, "b"), @@ -433,7 +543,10 @@ impl<'a> Origin<'a> { /// use rocket::http::uri::Origin; /// /// let uri = Origin::parse("///a//b///c////d?query¶m").unwrap(); - /// for (i, segment) in uri.segments().enumerate() { + /// # let segments: Vec<_> = uri.raw_path_segments().collect(); + /// # assert_eq!(segments, &["a", "b", "c", "d"]); + /// + /// for (i, segment) in uri.raw_path_segments().enumerate() { /// match i { /// 0 => assert_eq!(segment, "a"), /// 1 => assert_eq!(segment, "b"), @@ -444,40 +557,38 @@ impl<'a> Origin<'a> { /// } /// ``` #[inline(always)] - pub fn segments(&self) -> Segments<'_> { - Segments(self.path()) + pub fn raw_path_segments(&self) -> impl Iterator { + self.path().split(Path::DELIMITER).filter(|s| !s.is_empty()) } - /// Returns the number of segments in the URI. Empty segments, which are - /// invalid according to RFC#3986, are not counted. - /// - /// The segment count is cached after the first invocation. As a result, - /// this function is O(1) after the first invocation, and O(n) before. - /// - /// ### Examples + /// Returns a (smart) 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. /// - /// A valid URI with only non-empty segments: + /// # Example /// /// ```rust /// # extern crate rocket; /// use rocket::http::uri::Origin; /// - /// let uri = Origin::parse("/a/b/c").unwrap(); - /// assert_eq!(uri.segment_count(), 3); - /// ``` + /// let uri = Origin::parse("/").unwrap(); + /// let query_segs: Vec<_> = uri.raw_query_segments().collect(); + /// assert!(query_segs.is_empty()); /// - /// A URI with empty segments: + /// let uri = Origin::parse("/foo/bar?a+b%2F=some+one%40gmail.com&&%26%3D2").unwrap(); + /// let query_segs: Vec<_> = uri.raw_query_segments() + /// .map(|(name, val)| (name.as_str(), val.as_str())) + /// .collect(); /// - /// ```rust - /// # extern crate rocket; - /// use rocket::http::uri::Origin; - /// - /// let uri = Origin::parse("/a/b//c/d///e").unwrap(); - /// assert_eq!(uri.segment_count(), 5); + /// assert_eq!(query_segs, &[("a+b%2F", "some+one%40gmail.com"), ("%26%3D2", "")]); /// ``` - #[inline] - pub fn segment_count(&self) -> usize { - *self.segment_count.get_or_set(|| self.segments().count()) + #[inline(always)] + pub fn raw_query_segments(&self) -> impl Iterator { + self.query().into_iter().flat_map(|q| { + q.split(Query::DELIMITER) + .filter(|s| !s.is_empty()) + .map(|q| q.split_at_byte(b'=')) + }) } } @@ -497,12 +608,14 @@ mod tests { use super::Origin; fn seg_count(path: &str, expected: usize) -> bool { - let actual = Origin::parse(path).unwrap().segment_count(); + let origin = Origin::parse(path).unwrap(); + let segments = origin.path_segments(); + let actual = segments.len(); if actual != expected { eprintln!("Count mismatch: expected {}, got {}.", expected, actual); eprintln!("{}", if actual != expected { "lifetime" } else { "buf" }); eprintln!("Segments (for {}):", path); - for (i, segment) in Origin::parse(path).unwrap().segments().enumerate() { + for (i, segment) in segments.enumerate() { eprintln!("{}: {}", i, segment); } } @@ -516,7 +629,7 @@ mod tests { Err(e) => panic!("failed to parse {}: {}", path, e) }; - let actual: Vec<&str> = uri.segments().collect(); + let actual: Vec<&str> = uri.path_segments().collect(); actual == expected } @@ -601,7 +714,7 @@ mod tests { fn test_query(uri: &str, query: Option<&str>) { let uri = Origin::parse(uri).unwrap(); - assert_eq!(uri.query(), query); + assert_eq!(uri.query().map(|s| s.as_str()), query); } #[test] diff --git a/core/http/src/uri/segments.rs b/core/http/src/uri/segments.rs index 5d91f7abe1..86368262a4 100644 --- a/core/http/src/uri/segments.rs +++ b/core/http/src/uri/segments.rs @@ -1,9 +1,11 @@ use std::path::PathBuf; -use std::str::Utf8Error; -use crate::uri::Uri; +use crate::RawStr; +use crate::parse::IndexedStr; -/// Iterator over the segments of an absolute URI path. Skips empty segments. +/// Iterator over the non-empty, percent-decoded segments of a URI path. +/// +/// Returned by [`Origin::path_segments()`]. /// /// ### Examples /// @@ -11,27 +13,32 @@ use crate::uri::Uri; /// # extern crate rocket; /// use rocket::http::uri::Origin; /// -/// let uri = Origin::parse("/a/////b/c////////d").unwrap(); -/// let segments = uri.segments(); +/// let uri = Origin::parse("/a%20z/////b/c////////d").unwrap(); +/// let segments = uri.path_segments(); /// for (i, segment) in segments.enumerate() { /// match i { -/// 0 => assert_eq!(segment, "a"), +/// 0 => assert_eq!(segment, "a z"), /// 1 => assert_eq!(segment, "b"), /// 2 => assert_eq!(segment, "c"), /// 3 => assert_eq!(segment, "d"), /// _ => 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")); /// ``` -#[derive(Clone, Debug)] -pub struct Segments<'a>(pub &'a str); +#[derive(Debug, Clone, Copy)] +pub struct Segments<'o> { + pub(super) source: &'o RawStr, + pub(super) segments: &'o [IndexedStr<'static>], + pub(super) pos: usize, +} -/// Errors which can occur when attempting to interpret a segment string as a -/// valid path segment. +/// An error interpreting a segment as a [`PathBuf`] component in +/// [`Segments::to_path_buf()`]. #[derive(Debug, PartialEq, Eq, Clone)] -pub enum SegmentError { - /// The segment contained invalid UTF8 characters when percent decoded. - Utf8(Utf8Error), +pub enum PathError { /// The segment started with the wrapped invalid character. BadStart(char), /// The segment contained the wrapped invalid character. @@ -40,10 +47,37 @@ pub enum SegmentError { BadEnd(char), } -impl Segments<'_> { - /// Creates a `PathBuf` from a `Segments` iterator. The returned `PathBuf` - /// is percent-decoded. If a segment is equal to "..", the previous segment - /// (if any) is skipped. +impl<'o> Segments<'o> { + /// Returns the number of path segments left. + #[inline] + pub fn len(&self) -> usize { + let max_pos = std::cmp::min(self.pos, self.segments.len()); + self.segments.len() - max_pos + } + + /// Returns `true` if there are no segments left. + #[inline] + pub fn is_empty(&self) -> bool { + self.len() == 0 + } + + /// Skips `n` segments. + #[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. + #[inline] + pub fn get(&self, n: usize) -> Option<&'o str> { + self.segments.get(self.pos + n) + .map(|i| i.from_source(Some(self.source.as_str()))) + } + + /// Creates a `PathBuf` from `self`. The returned `PathBuf` is + /// 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: @@ -62,30 +96,27 @@ impl Segments<'_> { /// 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. - pub fn into_path_buf(self, allow_dotfiles: bool) -> Result { + pub fn to_path_buf(&self, allow_dotfiles: bool) -> Result { let mut buf = PathBuf::new(); - for segment in self { - let decoded = Uri::percent_decode(segment.as_bytes()) - .map_err(SegmentError::Utf8)?; - - if decoded == ".." { + for segment in self.clone() { + if segment == ".." { buf.pop(); - } else if !allow_dotfiles && decoded.starts_with('.') { - return Err(SegmentError::BadStart('.')) - } else if decoded.starts_with('*') { - return Err(SegmentError::BadStart('*')) - } else if decoded.ends_with(':') { - return Err(SegmentError::BadEnd(':')) - } else if decoded.ends_with('>') { - return Err(SegmentError::BadEnd('>')) - } else if decoded.ends_with('<') { - return Err(SegmentError::BadEnd('<')) - } else if decoded.contains('/') { - return Err(SegmentError::BadChar('/')) - } else if cfg!(windows) && decoded.contains('\\') { - return Err(SegmentError::BadChar('\\')) + } else if !allow_dotfiles && segment.starts_with('.') { + return Err(PathError::BadStart('.')) + } else if segment.starts_with('*') { + return Err(PathError::BadStart('*')) + } else if segment.ends_with(':') { + return Err(PathError::BadEnd(':')) + } else if segment.ends_with('>') { + return Err(PathError::BadEnd('>')) + } else if segment.ends_with('<') { + return Err(PathError::BadEnd('<')) + } else if segment.contains('/') { + return Err(PathError::BadChar('/')) + } else if cfg!(windows) && segment.contains('\\') { + return Err(PathError::BadChar('\\')) } else { - buf.push(&*decoded) + buf.push(&*segment) } } @@ -93,30 +124,70 @@ impl Segments<'_> { } } -impl<'a> Iterator for Segments<'a> { - type Item = &'a str; +impl<'o> Iterator for Segments<'o> { + type Item = &'o str; - #[inline] fn next(&mut self) -> Option { - // Find the start of the next segment (first that's not '/'). - let i = self.0.find(|c| c != '/')?; + 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. + #[inline] + pub fn get(&self, n: usize) -> Option<(&'o str, &'o str)> { + let (name, val) = self.segments.get(self.pos + n)?; + let source = self.source.map(|s| s.as_str()); + let name = name.from_source(source); + let val = val.from_source(source); + Some((name, val)) + } +} - // Get the index of the first character that _is_ a '/' after start. - // j = index of first character after i (hence the i +) that's not a '/' - let j = self.0[i..].find('/').map_or(self.0.len(), |j| i + j); +impl<'o> Iterator for QuerySegments<'o> { + type Item = (&'o str, &'o str); - // Save the result, update the iterator, and return! - let result = Some(&self.0[i..j]); - self.0 = &self.0[j..]; - result + 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())) } - // TODO: Potentially take a second parameter with Option and - // return it here if it's Some. The downside is that a decision has to be - // made about -when- to compute and cache that count. A place to do it is in - // the segments() method. But this means that the count will always be - // computed regardless of whether it's needed. Maybe this is ok. We'll see. - // fn count(self) -> usize where Self: Sized { - // self.1.unwrap_or_else(self.fold(0, |cnt, _| cnt + 1)) - // } + fn count(self) -> usize { + self.len() + } } diff --git a/core/http/src/uri/uri.rs b/core/http/src/uri/uri.rs index 84d1a76a8b..a4bc2b9da7 100644 --- a/core/http/src/uri/uri.rs +++ b/core/http/src/uri/uri.rs @@ -4,6 +4,7 @@ use std::borrow::Cow; use std::str::Utf8Error; use std::convert::TryFrom; +use crate::RawStr; use crate::ext::IntoOwned; use crate::parse::Extent; use crate::uri::{Origin, Authority, Absolute, Error}; @@ -96,7 +97,7 @@ impl<'a> Uri<'a> { /// let uri = Uri::parse("/a/b/c?query").expect("valid URI"); /// let origin = uri.origin().expect("origin URI"); /// assert_eq!(origin.path(), "/a/b/c"); - /// assert_eq!(origin.query(), Some("query")); + /// assert_eq!(origin.query().unwrap(), "query"); /// /// // Invalid URIs fail to parse. /// Uri::parse("foo bar").expect_err("invalid URI"); @@ -105,20 +106,6 @@ impl<'a> Uri<'a> { crate::parse::uri::from_str(string) } -// pub fn from_hyp(uri: &'a hyper::Uri) -> Uri<'a> { -// match uri.is_absolute() { -// true => Uri::Absolute(Absolute::new( -// uri.scheme().unwrap(), -// match uri.host() { -// Some(host) => Some(Authority::new(None, Host::Raw(host), uri.port())), -// None => None -// }, -// None -// )), -// false => Uri::Asterisk -// } -// } - /// Returns the internal instance of `Origin` if `self` is a `Uri::Origin`. /// Otherwise, returns `None`. /// @@ -197,8 +184,10 @@ impl<'a> Uri<'a> { /// let encoded = Uri::percent_encode("hello?a=hi"); /// assert_eq!(encoded, "hello%3Fa%3D%3Cb%3Ehi%3C%2Fb%3E"); /// ``` - pub fn percent_encode(string: &str) -> Cow<'_, str> { - percent_encode::(string) + 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 @@ -213,8 +202,10 @@ impl<'a> Uri<'a> { /// let decoded = Uri::percent_decode("/Hello%2C%20world%21".as_bytes()); /// assert_eq!(decoded.unwrap(), "/Hello, world!"); /// ``` - pub fn percent_decode(string: &[u8]) -> Result, Utf8Error> { - let decoder = percent_encoding::percent_decode(string); + 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() } @@ -231,8 +222,10 @@ impl<'a> Uri<'a> { /// let decoded = Uri::percent_decode_lossy("/Hello%2C%20world%21".as_bytes()); /// assert_eq!(decoded, "/Hello, world!"); /// ``` - pub fn percent_decode_lossy(string: &[u8]) -> Cow<'_, str> { - let decoder = percent_encoding::percent_decode(string); + 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() } } diff --git a/core/http/src/uri/uri_display.rs b/core/http/src/uri/uri_display.rs index a3c6cca36e..b20281490e 100644 --- a/core/http/src/uri/uri_display.rs +++ b/core/http/src/uri/uri_display.rs @@ -1,7 +1,6 @@ use std::{fmt, path}; use std::borrow::Cow; -use crate::RawStr; use crate::uri::{Uri, UriPart, Path, Query, Formatter}; /// Trait implemented by types that can be displayed as part of a URI in @@ -119,7 +118,7 @@ use crate::uri::{Uri, UriPart, Path, Query, Formatter}; /// The implementation of `UriDisplay` for these types is identical to the /// `Display` implementation. /// -/// * **[`&RawStr`](RawStr), `String`, `&str`, `Cow`** +/// * **`String`, `&str`, `Cow`** /// /// The string is percent encoded. /// @@ -237,25 +236,23 @@ use crate::uri::{Uri, UriPart, Path, Query, Formatter}; /// /// ```rust /// # #[macro_use] extern crate rocket; -/// use rocket::http::RawStr; /// use rocket::request::FromParam; /// -/// struct Name(String); +/// struct Name<'r>(&'r str); /// /// const PREFIX: &str = "name:"; /// -/// impl<'r> FromParam<'r> for Name { -/// type Error = &'r RawStr; +/// impl<'r> FromParam<'r> for Name<'r> { +/// type Error = &'r str; /// /// /// Validates parameters that start with 'name:', extracting the text /// /// after 'name:' as long as there is at least one character. -/// fn from_param(param: &'r RawStr) -> Result { -/// let decoded = param.percent_decode().map_err(|_| param)?; -/// if !decoded.starts_with(PREFIX) || decoded.len() < (PREFIX.len() + 1) { +/// fn from_param(param: &'r str) -> Result { +/// if !param.starts_with(PREFIX) || param.len() < (PREFIX.len() + 1) { /// return Err(param); /// } /// -/// let real_name = decoded[PREFIX.len()..].to_string(); +/// let real_name = ¶m[PREFIX.len()..]; /// Ok(Name(real_name)) /// } /// } @@ -265,25 +262,25 @@ use crate::uri::{Uri, UriPart, Path, Query, Formatter}; /// use rocket::http::uri::{Formatter, FromUriParam, UriDisplay, Path}; /// use rocket::response::Redirect; /// -/// impl UriDisplay for Name { -/// // Delegates to the `UriDisplay` implementation for `String` via the -/// // call to `write_value` to ensure that the written string is -/// // URI-safe. In this case, the string will be percent encoded. -/// // Prefixes the inner name with `name:`. +/// impl UriDisplay for Name<'_> { +/// // Delegates to the `UriDisplay` implementation for `str` via the call +/// // to `write_value` to ensure that the written string is URI-safe. In +/// // this case, the string will be percent encoded. Prefixes the inner +/// // name with `name:`. /// fn fmt(&self, f: &mut Formatter) -> fmt::Result { /// f.write_value(&format!("name:{}", self.0)) /// } /// } /// -/// impl_from_uri_param_identity!([Path] Name); +/// impl_from_uri_param_identity!([Path] ('a) Name<'a>); /// /// #[get("/name/")] -/// fn redirector(name: Name) -> Redirect { +/// fn redirector(name: Name<'_>) -> Redirect { /// Redirect::to(uri!(real: name)) /// } /// /// #[get("/")] -/// fn real(name: Name) -> String { +/// fn real(name: Name<'_>) -> String { /// format!("Hello, {}!", name.0) /// } /// @@ -353,14 +350,6 @@ impl_with_display! { // These are second level implementations: they all defer to an existing // implementation. -/// Percent-encodes the raw string. Defers to `str`. -impl UriDisplay

for RawStr { - #[inline(always)] - fn fmt(&self, f: &mut Formatter<'_, P>) -> fmt::Result { - self.as_str().fmt(f) - } -} - /// Percent-encodes the raw string. Defers to `str`. impl UriDisplay

for String { #[inline(always)] diff --git a/core/lib/Cargo.toml b/core/lib/Cargo.toml index 0a545bb243..cc2a39c9ca 100644 --- a/core/lib/Cargo.toml +++ b/core/lib/Cargo.toml @@ -24,18 +24,14 @@ tls = ["rocket_http/tls"] secrets = ["rocket_http/private-cookies"] [dependencies] -rocket_codegen = { version = "0.5.0-dev", path = "../codegen" } -rocket_http = { version = "0.5.0-dev", path = "../http" } futures = "0.3.0" yansi = "0.5" log = { version = "0.4", features = ["std"] } num_cpus = "1.0" -state = "0.4.1" time = "0.2.11" memchr = "2" # TODO: Use pear instead. binascii = "0.1" atty = "0.2" -async-trait = "0.1" ref-cast = "1.0" atomic = "0.5" parking_lot = "0.11" @@ -44,6 +40,31 @@ serde = { version = "1.0", features = ["derive"] } figment = { version = "0.10.2", features = ["toml", "env"] } rand = "0.8" either = "1" +pin-project-lite = "0.1" +indexmap = { version = "1.0", features = ["serde"] } +tempfile = "3" + +[dependencies.state] +git = "https://github.com/SergioBenitez/state.git" +rev = "7576652" + +[dependencies.async-trait] +git = "https://github.com/SergioBenitez/async-trait.git" +rev = "0cda89bd" + +[dependencies.rocket_codegen] +version = "0.5.0-dev" +path = "../codegen" + +[dependencies.rocket_http] +version = "0.5.0-dev" +path = "../http" +features = ["serde"] + +[dependencies.multer] +git = "https://github.com/rousan/multer-rs.git" +rev = "0620e54" +features = ["tokio-io"] [dependencies.tokio] version = "1.0" diff --git a/core/lib/benches/simple-routing.rs b/core/lib/benches/simple-routing.rs index be220389cf..58d054a1c6 100644 --- a/core/lib/benches/simple-routing.rs +++ b/core/lib/benches/simple-routing.rs @@ -1,8 +1,6 @@ #[macro_use] extern crate rocket; #[macro_use] extern crate bencher; -use rocket::http::RawStr; - #[get("/")] fn hello_world() -> &'static str { "Hello, world!" } @@ -22,7 +20,7 @@ fn index_b() -> &'static str { "index" } fn index_c() -> &'static str { "index" } #[get("/<_a>")] -fn index_dyn_a(_a: &RawStr) -> &'static str { "index" } +fn index_dyn_a(_a: &str) -> &'static str { "index" } fn hello_world_rocket() -> rocket::Rocket { let config = rocket::Config::figment().merge(("log_level", "off")); @@ -93,7 +91,7 @@ fn bench_simple_routing(b: &mut Bencher) { // Hold all of the requests we're going to make during the benchmark. let mut requests = vec![]; for route in client.rocket().routes() { - let request = client.req(route.method, route.uri.path()); + let request = client.req(route.method, route.uri.path().as_str()); requests.push(request); } diff --git a/core/lib/src/catcher.rs b/core/lib/src/catcher.rs index 8ac6cd374c..e2f502796c 100644 --- a/core/lib/src/catcher.rs +++ b/core/lib/src/catcher.rs @@ -285,25 +285,24 @@ impl fmt::Debug for Catcher { macro_rules! html_error_template { ($code:expr, $reason:expr, $description:expr) => ( - concat!(r#" - - - - - "#, $code, " ", $reason, r#" - - -

-

"#, $code, ": ", $reason, r#"

-

"#, $description, r#"

-
-
-
- Rocket -
- - - "# + concat!( +r#" + + + + "#, $code, " ", $reason, r#" + + +
+

"#, $code, ": ", $reason, r#"

+

"#, $description, r#"

+
+
+
+ Rocket +
+ +"# ) ) } diff --git a/core/lib/src/config/config.rs b/core/lib/src/config/config.rs index 7d1fa61bc0..830ce502b6 100644 --- a/core/lib/src/config/config.rs +++ b/core/lib/src/config/config.rs @@ -1,3 +1,4 @@ +use std::path::PathBuf; use std::net::{IpAddr, Ipv4Addr}; use figment::{Figment, Profile, Provider, Metadata, error::Result}; @@ -7,6 +8,7 @@ use serde::{Deserialize, Serialize}; use yansi::Paint; use crate::config::{SecretKey, TlsConfig, LogLevel}; +use crate::request::{self, Request, FromRequest}; use crate::data::Limits; /// Rocket server configuration. @@ -57,21 +59,24 @@ pub struct Config { pub address: IpAddr, /// Port to serve on. **(default: `8000`)** pub port: u16, - /// Number of future-executing threads. **(default: `num cores`)** + /// Number of threads to use for executing futures. **(default: `num_cores`)** pub workers: usize, /// Keep-alive timeout in seconds; disabled when `0`. **(default: `5`)** pub keep_alive: u32, + /// Streaming read size limits. **(default: [`Limits::default()`])** + pub limits: Limits, + /// The TLS configuration, if any. **(default: `None`)** + pub tls: Option, + /// The secret key for signing and encrypting. **(default: `0`)** + pub secret_key: SecretKey, + /// The directory to store temporary files in. **(default: + /// [`std::env::temp_dir`]). + pub temp_dir: PathBuf, /// Max level to log. **(default: _debug_ `normal` / _release_ `critical`)** pub log_level: LogLevel, /// Whether to use colors and emoji when logging. **(default: `true`)** #[serde(deserialize_with = "figment::util::bool_from_str_or_int")] pub cli_colors: bool, - /// The secret key for signing and encrypting. **(default: `0`)** - pub secret_key: SecretKey, - /// The TLS configuration, if any. **(default: `None`)** - pub tls: Option, - /// Streaming read size limits. **(default: [`Limits::default()`])** - pub limits: Limits, /// Whether `ctrl-c` initiates a server shutdown. **(default: `true`)** #[serde(deserialize_with = "figment::util::bool_from_str_or_int")] pub ctrlc: bool, @@ -141,6 +146,7 @@ impl Config { secret_key: SecretKey::zero(), tls: None, limits: Limits::default(), + temp_dir: std::env::temp_dir(), ctrlc: true, } } @@ -270,10 +276,6 @@ impl Config { launch_info_!("address: {}", Paint::default(&self.address).bold()); launch_info_!("port: {}", Paint::default(&self.port).bold()); launch_info_!("workers: {}", Paint::default(self.workers).bold()); - launch_info_!("log level: {}", Paint::default(self.log_level).bold()); - launch_info_!("secret key: {:?}", Paint::default(&self.secret_key).bold()); - launch_info_!("limits: {}", Paint::default(&self.limits).bold()); - launch_info_!("cli colors: {}", Paint::default(&self.cli_colors).bold()); let ka = self.keep_alive; if ka > 0 { @@ -282,11 +284,14 @@ impl Config { launch_info_!("keep-alive: {}", Paint::default("disabled").bold()); } + launch_info_!("limits: {}", Paint::default(&self.limits).bold()); match self.tls_enabled() { true => launch_info_!("tls: {}", Paint::default("enabled").bold()), false => launch_info_!("tls: {}", Paint::default("disabled").bold()), } + launch_info_!("secret key: {:?}", Paint::default(&self.secret_key).bold()); + #[cfg(all(feature = "secrets", not(test), not(rocket_unsafe_secret_key)))] if !self.secret_key.is_provided() { warn!("secrets enabled without a configured `secret_key`"); @@ -294,6 +299,10 @@ impl Config { info_!("this becomes a {} in non-debug profiles", Paint::red("hard error").bold()); } + launch_info_!("temp dir: {}", Paint::default(&self.temp_dir.display()).bold()); + launch_info_!("log level: {}", Paint::default(self.log_level).bold()); + launch_info_!("cli colors: {}", Paint::default(&self.cli_colors).bold()); + // Check for now depreacted config values. for (key, replacement) in Self::DEPRECATED_KEYS { if let Some(md) = figment.find_metadata(key) { @@ -346,6 +355,15 @@ impl Provider for Config { } } +#[crate::async_trait] +impl<'a, 'r> FromRequest<'a, 'r> for &'r Config { + type Error = std::convert::Infallible; + + async fn from_request(req: &'a Request<'r>) -> request::Outcome { + request::Outcome::Success(req.config()) + } +} + #[doc(hidden)] pub fn pretty_print_error(error: figment::Error) { use figment::error::{Kind, OneOf}; diff --git a/core/lib/src/data/capped.rs b/core/lib/src/data/capped.rs new file mode 100644 index 0000000000..3627abe12f --- /dev/null +++ b/core/lib/src/data/capped.rs @@ -0,0 +1,270 @@ +/// Number of bytes read/written and whether that consisted of the entire +/// stream. +#[derive(Debug, Copy, Clone)] +pub struct N { + /// The number of bytes written out. + pub written: u64, + /// Whether the entire stream was read and written out. + pub complete: bool, +} + +impl std::fmt::Display for N { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + self.written.fmt(f) + } +} + +impl std::ops::Deref for N { + type Target = u64; + + fn deref(&self) -> &Self::Target { + &self.written + } +} + +/// Encapsulates a value capped to a data limit. +/// +/// A `Capped` type represents a `T` that has been limited (capped) to some +/// number of bytes. The internal [`N`] specifies whether the value is complete +/// (also [`Capped::is_complete()`]) or whether it was capped prematurely. A +/// [`Capped`] is returned by various methods of [`DataStream`]. Some +/// `Capped` types, like `Capped` and `Capped`, implement +/// traits like [`FromData`] and [`FromForm`]. +/// +/// # Example +/// +/// Since `Capped` implements `FromData`, it can be used as a data +/// guard. The following Rocket route accepts a raw upload and stores the upload +/// in a different directory depending on whether the file exceeded the data +/// limit or not. See [`TempFile`] for details on temporary file storage +/// locations and limits. +/// +/// ```rust +/// # #[macro_use] extern crate rocket; +/// use rocket::data::{Capped, TempFile}; +/// +/// #[post("/upload", data = "")] +/// async fn upload(mut file: Capped>) -> std::io::Result<()> { +/// if file.is_complete() { +/// file.persist_to("/tmp/complete/file.txt").await?; +/// } else { +/// file.persist_to("/tmp/incomplete/file.txt").await?; +/// } +/// +/// Ok(()) +/// } +/// ``` +/// +/// [`DataStream`]: crate::data::DataStream +/// [`FromData`]: crate::data::FromData +/// [`FromForm`]: crate::form::FromForm +/// [`TempFile`]: crate::data::TempFile +// TODO: `Capped` not particularly usable outside Rocket due to coherence. +#[derive(Debug, Copy, Clone)] +pub struct Capped { + /// The capped value itself. + pub value: T, + /// The number of bytes written and whether `value` is complete. + pub n: N +} + +impl Capped { + /// Creates a new `Capped` from a `value` and an `n`. Prefer to use + /// [`Capped::from()`] when possible. + /// + /// # Example + /// + /// ```rust + /// use rocket::data::{Capped, N}; + /// + /// let n = N { written: 2, complete: true }; + /// let capped = Capped::new("hi".to_string(), n); + /// ``` + #[inline(always)] + pub fn new(value: T, n: N) -> Self { + Capped { value, n, } + } + + /// Creates a new `Capped` from a `value` and the length of `value` `n`, + /// marking `value` as complete. Prefer to use [`Capped::from()`] when + /// possible. + /// + /// # Example + /// + /// ```rust + /// use rocket::data::{Capped, N}; + /// + /// let string = "hi"; + /// let capped = Capped::complete("hi", string.len()); + /// ``` + #[inline(always)] + pub fn complete(value: T, len: usize) -> Self { + Capped { value, n: N { written: len as u64, complete: true } } + } + + /// Converts a `Capped` to `Capped` by applying `f` to the contained + /// value. + /// + /// # Example + /// + /// ```rust + /// use rocket::data::{Capped, N}; + /// + /// let n = N { written: 2, complete: true }; + /// let capped: Capped = Capped::new(10usize, n); + /// let mapped: Capped = capped.map(|n| n.to_string()); + /// ``` + #[inline(always)] + pub fn map U>(self, f: F) -> Capped { + Capped { value: f(self.value), n: self.n } + } + + /// Returns `true` if `self.n.written` is `0`, that is, no bytes were + /// written to `value`. + /// + /// # Example + /// + /// ```rust + /// use rocket::data::{Capped, N}; + /// + /// let n = N { written: 2, complete: true }; + /// let capped = Capped::new("hi".to_string(), n); + /// assert!(!capped.is_empty()); + /// + /// let n = N { written: 0, complete: true }; + /// let capped = Capped::new("".to_string(), n); + /// assert!(capped.is_empty()); + /// ``` + #[inline(always)] + pub fn is_empty(&self) -> bool { + self.n.written == 0 + } + + /// Returns `true` if `self.n.complete`, that is, `value` represents the + /// entire data stream. + /// + /// # Example + /// + /// ```rust + /// use rocket::data::{Capped, N}; + /// + /// let n = N { written: 2, complete: true }; + /// let capped = Capped::new("hi".to_string(), n); + /// assert!(capped.is_complete()); + /// + /// let n = N { written: 4, complete: false }; + /// let capped = Capped::new("hell".to_string(), n); + /// assert!(!capped.is_complete()); + /// ``` + #[inline(always)] + pub fn is_complete(&self) -> bool { + self.n.complete + } + + /// Returns the internal value. + /// + /// # Example + /// + /// ```rust + /// use rocket::data::{Capped, N}; + /// + /// let n = N { written: 2, complete: true }; + /// let capped = Capped::new("hi".to_string(), n); + /// assert_eq!(capped.into_inner(), "hi"); + /// ``` + #[inline(always)] + pub fn into_inner(self) -> T { + self.value + } +} + +impl std::ops::Deref for Capped { + type Target = T; + + fn deref(&self) -> &Self::Target { + &self.value + } +} + +impl std::ops::DerefMut for Capped { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.value + } +} + +impl> From for Capped { + /// Creates a `Capped` from a `value`, setting `complete` to `true`. + fn from(value: T) -> Self { + let len = value.as_ref().len(); + Capped::complete(value, len) + } +} + +use crate::response::{self, Responder}; +use crate::request::Request; + +impl<'r, 'o: 'r, T: Responder<'r, 'o>> Responder<'r, 'o> for Capped { + fn respond_to(self, request: &'r Request<'_>) -> response::Result<'o> { + self.value.respond_to(request) + } +} + +macro_rules! impl_strict_from_form_field_from_capped { + ($T:ty) => (const _: () = { + use $crate::form::{FromFormField, ValueField, DataField, Result}; + use $crate::data::Capped; + + #[crate::async_trait] + impl<'v> FromFormField<'v> for $T { + fn default() -> Option { + as FromFormField<'v>>::default().map(|c| c.value) + } + + fn from_value(f: ValueField<'v>) -> Result<'v, Self> { + let capped = as FromFormField<'v>>::from_value(f)?; + if capped.is_complete() { + Ok(capped.value) + } else { + Err((None, Some(capped.n.written)))? + } + } + + async fn from_data(field: DataField<'v, '_>) -> Result<'v, Self> { + let capped = as FromFormField<'v>>::from_data(field); + let capped = capped.await?; + if capped.is_complete() { + Ok(capped.value) + } else { + Err((None, Some(capped.n.written)))? + } + } + } + };) +} + +macro_rules! impl_strict_from_data_from_capped { + ($T:ty) => ( + #[crate::async_trait] + impl<'r> $crate::data::FromData<'r> for $T { + type Error = <$crate::data::Capped as $crate::data::FromData<'r>>::Error; + + async fn from_data( + r: &'r $crate::Request<'_>, + d: $crate::Data + ) -> $crate::data::Outcome { + use $crate::outcome::Outcome::*; + use std::io::{Error, ErrorKind::UnexpectedEof}; + + match <$crate::data::Capped<$T> as FromData>::from_data(r, d).await { + Success(p) if p.is_complete() => Success(p.into_inner()), + Success(_) => { + let e = Error::new(UnexpectedEof, "data limit exceeded"); + Failure((Status::BadRequest, e.into())) + }, + Forward(d) => Forward(d), + Failure((s, e)) => Failure((s, e)), + } + } + } + ) +} diff --git a/core/lib/src/data/data.rs b/core/lib/src/data/data.rs index de83b2da46..bf09bebb6b 100644 --- a/core/lib/src/data/data.rs +++ b/core/lib/src/data/data.rs @@ -1,20 +1,17 @@ -use std::io::Cursor; - -use crate::http::hyper; -use crate::ext::AsyncReadBody; use crate::tokio::io::AsyncReadExt; use crate::data::data_stream::DataStream; -use crate::data::ByteUnit; +use crate::data::{ByteUnit, StreamReader}; /// The number of bytes to read into the "peek" buffer. pub const PEEK_BYTES: usize = 512; -/// Type representing the data in the body of an incoming request. +/// Type representing the body data of a request. /// /// This type is the only means by which the body of a request can be retrieved. -/// This type is not usually used directly. Instead, types that implement -/// [`FromTransformedData`](crate::data::FromTransformedData) are used via code -/// generation by specifying the `data = ""` route parameter as follows: +/// This type is not usually used directly. Instead, data guards (types that +/// implement [`FromData`](crate::data::FromData)) are created indirectly via +/// code generation by specifying the `data = ""` route parameter as +/// follows: /// /// ```rust /// # #[macro_use] extern crate rocket; @@ -24,9 +21,8 @@ pub const PEEK_BYTES: usize = 512; /// # fn main() { } /// ``` /// -/// Above, `DataGuard` can be any type that implements `FromTransformedData` (or -/// equivalently, `FromData`). Note that `Data` itself implements -/// `FromTransformedData`. +/// Above, `DataGuard` can be any type that implements `FromData`. Note that +/// `Data` itself implements `FromData`. /// /// # Reading Data /// @@ -44,16 +40,17 @@ pub const PEEK_BYTES: usize = 512; pub struct Data { buffer: Vec, is_complete: bool, - stream: AsyncReadBody, + stream: StreamReader, } impl Data { - pub(crate) async fn from_hyp(body: hyper::Body) -> Data { + /// Create a `Data` from a recognized `stream`. + pub(crate) fn from>(stream: S) -> Data { // TODO.async: This used to also set the read timeout to 5 seconds. // Such a short read timeout is likely no longer necessary, but some // kind of idle timeout should be implemented. - let stream = AsyncReadBody::from(body); + let stream = stream.into(); let buffer = Vec::with_capacity(PEEK_BYTES / 8); Data { buffer, stream, is_complete: false } } @@ -63,7 +60,7 @@ impl Data { pub(crate) fn local(data: Vec) -> Data { Data { buffer: data, - stream: AsyncReadBody::empty(), + stream: StreamReader::empty(), is_complete: true, } } @@ -86,11 +83,7 @@ impl Data { /// } /// ``` pub fn open(self, limit: ByteUnit) -> DataStream { - let buffer_limit = std::cmp::min(self.buffer.len().into(), limit); - let stream_limit = limit - buffer_limit; - let buffer = Cursor::new(self.buffer).take(buffer_limit.into()); - let stream = self.stream.take(stream_limit.into()); - DataStream { buffer, stream } + DataStream::new(self.buffer, self.stream, limit.into()) } /// Retrieve at most `num` bytes from the `peek` buffer without consuming @@ -113,10 +106,13 @@ impl Data { /// # type MyError = String; /// /// #[rocket::async_trait] - /// impl FromData for MyType { + /// impl<'r> FromData<'r> for MyType { /// type Error = MyError; /// - /// async fn from_data(req: &Request<'_>, mut data: Data) -> data::Outcome { + /// async fn from_data( + /// req: &'r Request<'_>, + /// mut data: Data + /// ) -> data::Outcome { /// if data.peek(2).await != b"hi" { /// return data::Outcome::Forward(data) /// } diff --git a/core/lib/src/data/data_stream.rs b/core/lib/src/data/data_stream.rs index a968ec4215..9a6d17e316 100644 --- a/core/lib/src/data/data_stream.rs +++ b/core/lib/src/data/data_stream.rs @@ -3,26 +3,107 @@ use std::task::{Context, Poll}; use std::path::Path; use std::io::{self, Cursor}; +use tokio::fs::File; use tokio::io::{AsyncRead, AsyncWrite, AsyncReadExt, ReadBuf, Take}; +use futures::stream::Stream; +use futures::ready; -use crate::ext::AsyncReadBody; +use crate::http::hyper; +use crate::ext::{PollExt, Chain}; +use crate::data::{Capped, N}; /// Raw data stream of a request body. /// /// This stream can only be obtained by calling -/// [`Data::open()`](crate::data::Data::open()). The stream contains all of the -/// data in the body of the request. It exposes no methods directly. Instead, it -/// must be used as an opaque [`AsyncRead`] structure. +/// [`Data::open()`](crate::data::Data::open()) with a data limit. The stream +/// contains all of the data in the body of the request. +/// +/// Reading from a `DataStream` is accomplished via the various methods on the +/// structure. In general, methods exists in two variants: those that _check_ +/// whether the entire stream was read and those that don't. The former either +/// directly or indirectly (via [`Capped`]) return an [`N`] which allows +/// checking if the stream was read to completion while the latter do not. +/// +/// | Read Into | Method | Notes | +/// |-----------|--------------------------------------|----------------------------------| +/// | `String` | [`DataStream::into_string()`] | Completeness checked. Preferred. | +/// | `String` | [`AsyncReadExt::read_to_string()`] | Unchecked w/existing `String`. | +/// | `Vec` | [`DataStream::into_bytes()`] | Checked. Preferred. | +/// | `Vec` | [`DataStream::stream_to(&mut vec)`] | Checked w/existing `Vec`. | +/// | `Vec` | [`DataStream::stream_precise_to()`] | Unchecked w/existing `Vec`. | +/// | `File` | [`DataStream::into_file()`] | Checked. Preferred. | +/// | `File` | [`DataStream::stream_to(&mut file)`] | Checked w/ existing `File`. | +/// | `File` | [`DataStream::stream_precise_to()`] | Unchecked w/ existing `File`. | +/// | `T` | [`DataStream::stream_to()`] | Checked. Any `T: AsyncWrite`. | +/// | `T` | [`DataStream::stream_precise_to()`] | Unchecked. Any `T: AsyncWrite`. | +/// +/// [`DataStream::stream_to(&mut vec)`]: DataStream::stream_to() +/// [`DataStream::stream_to(&mut file)`]: DataStream::stream_to() pub struct DataStream { - pub(crate) buffer: Take>>, - pub(crate) stream: Take + pub(crate) chain: Take>, StreamReader>>, +} + +/// An adapter: turns a `T: Stream` (in `StreamKind`) into a `tokio::AsyncRead`. +pub struct StreamReader { + state: State, + inner: StreamKind, +} + +/// The current state of `StreamReader` `AsyncRead` adapter. +enum State { + Pending, + Partial(Cursor), + Done, +} + +/// The kinds of streams we accept as `Data`. +enum StreamKind { + Body(hyper::Body), + Multipart(multer::Field) } impl DataStream { + pub(crate) fn new(buf: Vec, stream: StreamReader, limit: u64) -> Self { + let chain = Chain::new(Cursor::new(buf), stream).take(limit); + Self { chain } + } + + /// Whether a previous read exhausted the set limit _and then some_. + async fn limit_exceeded(&mut self) -> io::Result { + #[cold] + async fn _limit_exceeded(stream: &mut DataStream) -> io::Result { + stream.chain.set_limit(1); + let mut buf = [0u8; 1]; + Ok(stream.read(&mut buf).await? != 0) + } + + Ok(self.chain.limit() == 0 && _limit_exceeded(self).await?) + } + + /// Number of bytes a full read from `self` will _definitely_ read. + /// + /// # Example + /// + /// ```rust + /// use rocket::data::{Data, ToByteUnit}; + /// + /// async fn f(data: Data) { + /// let definitely_have_n_bytes = data.open(1.kibibytes()).hint(); + /// } + /// ``` + pub fn hint(&self) -> usize { + let buf_len = self.chain.get_ref().get_ref().0.get_ref().len(); + std::cmp::min(buf_len, self.chain.limit() as usize) + } + /// A helper method to write the body of the request to any `AsyncWrite` - /// type. + /// type. Returns an [`N`] which indicates how many bytes were written and + /// whether the entire stream was read. An additional read from `self` may + /// be required to check if all of the sream has been read. If that + /// information is not needed, use [`DataStream::stream_precise_to()`]. /// - /// This method is identical to `tokio::io::copy(&mut self, &mut writer)`. + /// This method is identical to `tokio::io::copy(&mut self, &mut writer)` + /// except in that it returns an `N` to check for completeness. /// /// # Example /// @@ -30,24 +111,24 @@ impl DataStream { /// use std::io; /// use rocket::data::{Data, ToByteUnit}; /// - /// async fn handler(mut data: Data) -> io::Result { + /// async fn data_guard(mut data: Data) -> io::Result { /// // write all of the data to stdout - /// let written = data.open(512.kibibytes()).stream_to(tokio::io::stdout()).await?; + /// let written = data.open(512.kibibytes()) + /// .stream_to(tokio::io::stdout()).await?; + /// /// Ok(format!("Wrote {} bytes.", written)) /// } /// ``` #[inline(always)] - pub async fn stream_to(mut self, mut writer: W) -> io::Result + pub async fn stream_to(mut self, mut writer: W) -> io::Result where W: AsyncWrite + Unpin { - tokio::io::copy(&mut self, &mut writer).await + let written = tokio::io::copy(&mut self, &mut writer).await?; + Ok(N { written, complete: !self.limit_exceeded().await? }) } - /// A helper method to write the body of the request to a file at the path - /// determined by `path`. - /// - /// This method is identical to `self.stream_to(&mut - /// File::create(path).await?)`. + /// Like [`DataStream::stream_to()`] except that no end-of-stream check is + /// conducted and thus read/write completeness is unknown. /// /// # Example /// @@ -55,15 +136,42 @@ impl DataStream { /// use std::io; /// use rocket::data::{Data, ToByteUnit}; /// - /// async fn handler(mut data: Data) -> io::Result { - /// let written = data.open(1.megabytes()).stream_to_file("/static/file").await?; - /// Ok(format!("Wrote {} bytes to /static/file", written)) + /// async fn data_guard(mut data: Data) -> io::Result { + /// // write all of the data to stdout + /// let written = data.open(512.kibibytes()) + /// .stream_precise_to(tokio::io::stdout()).await?; + /// + /// Ok(format!("Wrote {} bytes.", written)) /// } /// ``` #[inline(always)] - pub async fn stream_to_file>(self, path: P) -> io::Result { - let mut file = tokio::fs::File::create(path).await?; - self.stream_to(&mut file).await + pub async fn stream_precise_to(mut self, mut writer: W) -> io::Result + where W: AsyncWrite + Unpin + { + tokio::io::copy(&mut self, &mut writer).await + } + + /// A helper method to write the body of the request to a `Vec`. + /// + /// # Example + /// + /// ```rust + /// use std::io; + /// use rocket::data::{Data, ToByteUnit}; + /// + /// async fn data_guard(data: Data) -> io::Result> { + /// let bytes = data.open(4.kibibytes()).into_bytes().await?; + /// if !bytes.is_complete() { + /// println!("there are bytes remaining in the stream"); + /// } + /// + /// Ok(bytes.into_inner()) + /// } + /// ``` + pub async fn into_bytes(self) -> io::Result>> { + let mut vec = Vec::with_capacity(self.hint()); + let n = self.stream_to(&mut vec).await?; + Ok(Capped { value: vec, n }) } /// A helper method to write the body of the request to a `String`. @@ -74,20 +182,25 @@ impl DataStream { /// use std::io; /// use rocket::data::{Data, ToByteUnit}; /// - /// async fn handler(data: Data) -> io::Result { - /// data.open(10.bytes()).stream_to_string().await + /// async fn data_guard(data: Data) -> io::Result { + /// let string = data.open(10.bytes()).into_string().await?; + /// if !string.is_complete() { + /// println!("there are bytes remaining in the stream"); + /// } + /// + /// Ok(string.into_inner()) /// } /// ``` - pub async fn stream_to_string(mut self) -> io::Result { - let buf_len = self.buffer.get_ref().get_ref().len(); - let max_from_buf = std::cmp::min(buf_len, self.buffer.limit() as usize); - let capacity = std::cmp::min(max_from_buf, 1024); - let mut string = String::with_capacity(capacity); - self.read_to_string(&mut string).await?; - Ok(string) + pub async fn into_string(mut self) -> io::Result> { + let mut string = String::with_capacity(self.hint()); + let written = self.read_to_string(&mut string).await?; + let n = N { written: written as u64, complete: !self.limit_exceeded().await? }; + Ok(Capped { value: string, n }) } - /// A helper method to write the body of the request to a `Vec`. + /// A helper method to write the body of the request to a file at the path + /// determined by `path`. If a file at the path already exists, it is + /// overwritten. The opened file is returned. /// /// # Example /// @@ -95,22 +208,42 @@ impl DataStream { /// use std::io; /// use rocket::data::{Data, ToByteUnit}; /// - /// async fn handler(data: Data) -> io::Result> { - /// data.open(4.kibibytes()).stream_to_vec().await + /// async fn data_guard(mut data: Data) -> io::Result { + /// let file = data.open(1.megabytes()).into_file("/static/file").await?; + /// if !file.is_complete() { + /// println!("there are bytes remaining in the stream"); + /// } + /// + /// Ok(format!("Wrote {} bytes to /static/file", file.n)) /// } /// ``` - pub async fn stream_to_vec(mut self) -> io::Result> { - let buf_len = self.buffer.get_ref().get_ref().len(); - let max_from_buf = std::cmp::min(buf_len, self.buffer.limit() as usize); - let capacity = std::cmp::min(max_from_buf, 1024); - let mut vec = Vec::with_capacity(capacity); - self.read_to_end(&mut vec).await?; - Ok(vec) + pub async fn into_file>(self, path: P) -> io::Result> { + let mut file = File::create(path).await?; + let n = self.stream_to(&mut tokio::io::BufWriter::new(&mut file)).await?; + Ok(Capped { value: file, n }) } } // TODO.async: Consider implementing `AsyncBufRead`. +impl StreamReader { + pub fn empty() -> Self { + Self { inner: StreamKind::Body(hyper::Body::empty()), state: State::Done } + } +} + +impl From for StreamReader { + fn from(body: hyper::Body) -> Self { + Self { inner: StreamKind::Body(body), state: State::Pending } + } +} + +impl From for StreamReader { + fn from(field: multer::Field) -> Self { + Self { inner: StreamKind::Multipart(field), state: State::Pending } + } +} + impl AsyncRead for DataStream { #[inline(always)] fn poll_read( @@ -118,15 +251,57 @@ impl AsyncRead for DataStream { cx: &mut Context<'_>, buf: &mut ReadBuf<'_>, ) -> Poll> { - if self.buffer.limit() > 0 { - trace_!("DataStream::buffer_read()"); - match Pin::new(&mut self.buffer).poll_read(cx, buf) { - Poll::Ready(Ok(())) if buf.filled().is_empty() => { /* fall through */ }, - poll => return poll, - } + Pin::new(&mut self.chain).poll_read(cx, buf) + } +} + +impl Stream for StreamKind { + type Item = io::Result; + + fn poll_next( + self: Pin<&mut Self>, + cx: &mut Context<'_>, + ) -> Poll> { + match self.get_mut() { + StreamKind::Body(body) => Pin::new(body).poll_next(cx) + .map_err_ext(|e| io::Error::new(io::ErrorKind::Other, e)), + StreamKind::Multipart(mp) => Pin::new(mp).poll_next(cx) + .map_err_ext(|e| io::Error::new(io::ErrorKind::Other, e)), } + } - trace_!("DataStream::stream_read()"); - Pin::new(&mut self.stream).poll_read(cx, buf) + fn size_hint(&self) -> (usize, Option) { + match self { + StreamKind::Body(body) => body.size_hint(), + StreamKind::Multipart(mp) => mp.size_hint(), + } + } +} + +impl AsyncRead for StreamReader { + fn poll_read( + mut self: Pin<&mut Self>, + cx: &mut Context<'_>, + buf: &mut ReadBuf<'_>, + ) -> Poll> { + loop { + self.state = match self.state { + State::Pending => { + match ready!(Pin::new(&mut self.inner).poll_next(cx)) { + Some(Err(e)) => return Poll::Ready(Err(e)), + Some(Ok(bytes)) => State::Partial(Cursor::new(bytes)), + None => State::Done, + } + }, + State::Partial(ref mut cursor) => { + let rem = buf.remaining(); + match ready!(Pin::new(cursor).poll_read(cx, buf)) { + Ok(()) if rem == buf.remaining() => State::Pending, + result => return Poll::Ready(result), + } + } + State::Done => return Poll::Ready(Ok(())), + } + } } } diff --git a/core/lib/src/data/from_data.rs b/core/lib/src/data/from_data.rs index ac289e5499..d4b927ca10 100644 --- a/core/lib/src/data/from_data.rs +++ b/core/lib/src/data/from_data.rs @@ -1,15 +1,11 @@ -use std::borrow::Borrow; +use crate::http::{RawStr, Status}; +use crate::request::{Request, local_cache}; +use crate::data::{Data, Limits}; +use crate::outcome::{self, IntoOutcome, Outcome::*}; -use futures::future::BoxFuture; -use futures::future::{ready, FutureExt}; - -use crate::outcome::{self, IntoOutcome}; -use crate::outcome::Outcome::*; -use crate::http::Status; -use crate::request::Request; -use crate::data::Data; - -/// Type alias for the `Outcome` of a `FromTransformedData` conversion. +/// Type alias for the `Outcome` of [`FromData`]. +/// +/// [`FromData`]: crate::data::FromData pub type Outcome = outcome::Outcome; impl IntoOutcome for Result { @@ -33,397 +29,27 @@ impl IntoOutcome for Result { } } -/// Indicates how incoming data should be transformed before being parsed and -/// validated by a data guard. -/// -/// See the documentation for [`FromTransformedData`] for usage details. -pub enum Transform { - /// Indicates that data should be or has been transformed into the - /// [`FromTransformedData::Owned`] variant. - Owned(T), - - /// Indicates that data should be or has been transformed into the - /// [`FromTransformedData::Borrowed`] variant. - Borrowed(B) -} - -impl Transform { - /// Returns the `Owned` value if `self` is `Owned`. - /// - /// # Panics - /// - /// Panics if `self` is `Borrowed`. - /// - /// - /// # Example - /// - /// ```rust - /// use rocket::data::Transform; - /// - /// let owned: Transform = Transform::Owned(10); - /// assert_eq!(owned.owned(), 10); - /// ``` - #[inline] - pub fn owned(self) -> T { - match self { - Transform::Owned(val) => val, - Transform::Borrowed(_) => panic!("Transform::owned() called on Borrowed"), - } - } - - /// Returns the `Borrowed` value if `self` is `Borrowed`. - /// - /// # Panics - /// - /// Panics if `self` is `Owned`. - /// - /// ```rust - /// use rocket::data::Transform; - /// - /// let borrowed: Transform = Transform::Borrowed(&[10]); - /// assert_eq!(borrowed.borrowed(), &[10]); - /// ``` - #[inline] - pub fn borrowed(self) -> B { - match self { - Transform::Borrowed(val) => val, - Transform::Owned(_) => panic!("Transform::borrowed() called on Owned"), - } - } -} - -/// Type alias to the `outcome` input type of [`FromTransformedData::from_data`]. -/// -/// This is a hairy type, but the gist is that this is a [`Transform`] where, -/// for a given `T: FromTransformedData`: -/// -/// * The `Owned` variant is an `Outcome` whose `Success` value is of type -/// [`FromTransformedData::Owned`]. -/// -/// * The `Borrowed` variant is an `Outcome` whose `Success` value is a borrow -/// of type [`FromTransformedData::Borrowed`]. -/// -/// * In either case, the `Outcome`'s `Failure` variant is a value of type -/// [`FromTransformedData::Error`]. -pub type Transformed<'a, T> = - Transform< - Outcome<>::Owned, >::Error>, - Outcome<&'a >::Borrowed, >::Error> - >; - -/// Type alias to the `Future` returned by [`FromTransformedData::transform`]. -pub type TransformFuture<'fut, T, E> = BoxFuture<'fut, Transform>>; - -/// Type alias to the `Future` returned by [`FromTransformedData::from_data`]. -pub type FromDataFuture<'fut, T, E> = BoxFuture<'fut, Outcome>; - /// Trait implemented by data guards to derive a value from request body data. /// /// # Data Guards /// -/// A data guard is a [request guard] that operates on a request's body data. -/// Data guards validate, parse, and optionally convert request body data. -/// Validation and parsing/conversion is implemented through -/// `FromTransformedData`. In other words, every type that implements -/// `FromTransformedData` is a data guard. -/// -/// Data guards are used as the target of the `data` route attribute parameter. -/// A handler can have at most one data guard. -/// -/// For many data guards, implementing [`FromData`] will be simpler and -/// sufficient. All types that implement `FromData` automatically implement -/// `FromTransformedData`. Thus, when possible, prefer to implement [`FromData`] -/// instead of `FromTransformedData`. -/// -/// [request guard]: crate::request::FromRequest -/// -/// ## Example +/// A data guard is a guard that operates on a request's body data. Data guards +/// validate and parse request body data via implementations of `FromData`. In +/// other words, a type is a data guard _iff_ it implements `FromData`. /// -/// In the example below, `var` is used as the argument name for the data guard -/// type `DataGuard`. When the `submit` route matches, Rocket will call the -/// `FromTransformedData` implementation for the type `T`. The handler will only be called -/// if the guard returns successfully. +/// Data guards are the target of the `data` route attribute parameter: /// /// ```rust /// # #[macro_use] extern crate rocket; /// # type DataGuard = rocket::data::Data; /// #[post("/submit", data = "")] /// fn submit(var: DataGuard) { /* ... */ } -/// # fn main() { } -/// ``` -/// -/// # Transforming -/// -/// Data guards can optionally _transform_ incoming data before processing it -/// via an implementation of the [`FromTransformedData::transform()`] method. -/// This is useful when a data guard requires or could benefit from a reference -/// to body data as opposed to an owned version. If a data guard has no need to -/// operate on a reference to body data, [`FromData`] should be implemented -/// instead; it is simpler to implement and less error prone. All types that -/// implement `FromData` automatically implement `FromTransformedData`. -/// -/// When exercising a data guard, Rocket first calls the guard's -/// [`FromTransformedData::transform()`] method and awaits on the returned -/// future, then calls the guard's [`FromTransformedData::from_data()`] method -/// and awaits on that returned future. Rocket stores data returned by -/// [`FromTransformedData::transform()`] on the stack. If `transform` returns a -/// [`Transform::Owned`], Rocket moves the data back to the data guard in the -/// subsequent `from_data` call as a `Transform::Owned`. If instead `transform` -/// returns a [`Transform::Borrowed`] variant, Rocket calls `borrow()` on the -/// owned value, producing a borrow of the associated -/// [`FromTransformedData::Borrowed`] type and passing it as a -/// `Transform::Borrowed`. -/// -/// ## Example -/// -/// Consider a data guard type that wishes to hold a slice to two different -/// parts of the incoming data: -/// -/// ```rust -/// struct Name<'a> { -/// first: &'a str, -/// last: &'a str -/// } /// ``` /// -/// Without the ability to transform into a borrow, implementing such a data -/// guard would be impossible. With transformation, however, we can instruct -/// Rocket to produce a borrow to a `Data` that has been transformed into a -/// `String` (an `&str`). -/// -/// ```rust -/// # #[macro_use] extern crate rocket; -/// # #[derive(Debug)] -/// # struct Name<'a> { first: &'a str, last: &'a str, } -/// use std::io::{self, Read}; -/// -/// use rocket::Request; -/// use rocket::data::{Data, Outcome, FromDataFuture, ByteUnit}; -/// use rocket::data::{FromTransformedData, Transform, Transformed, TransformFuture}; -/// use rocket::http::Status; -/// -/// const NAME_LIMIT: ByteUnit = ByteUnit::Byte(256); -/// -/// enum NameError { -/// Io(io::Error), -/// Parse -/// } -/// -/// impl<'r> FromTransformedData<'r> for Name<'r> { -/// type Error = NameError; -/// type Owned = String; -/// type Borrowed = str; -/// -/// fn transform(_: &'r Request, data: Data) -> TransformFuture<'r, Self::Owned, Self::Error> { -/// Box::pin(async move { -/// let outcome = match data.open(NAME_LIMIT).stream_to_string().await { -/// Ok(string) => Outcome::Success(string), -/// Err(e) => Outcome::Failure((Status::InternalServerError, NameError::Io(e))) -/// }; -/// -/// // Returning `Borrowed` here means we get `Borrowed` in `from_data`. -/// Transform::Borrowed(outcome) -/// }) -/// } -/// -/// fn from_data(_: &'r Request, outcome: Transformed<'r, Self>) -> FromDataFuture<'r, Self, Self::Error> { -/// Box::pin(async move { -/// // Retrieve a borrow to the now transformed `String` (an &str). -/// // This is only correct because we know we _always_ return a -/// // `Borrowed` from `transform` above. -/// let string = try_outcome!(outcome.borrowed()); -/// -/// // Perform a crude, inefficient parse. -/// let splits: Vec<&str> = string.split(" ").collect(); -/// if splits.len() != 2 || splits.iter().any(|s| s.is_empty()) { -/// return Outcome::Failure((Status::UnprocessableEntity, NameError::Parse)); -/// } -/// -/// // Return successfully. -/// Outcome::Success(Name { first: splits[0], last: splits[1] }) -/// }) -/// } -/// } -/// # #[post("/person", data = "")] -/// # fn person(person: Name) { } -/// # #[post("/person", data = "")] -/// # fn person2(person: Result) { } -/// # fn main() { } -/// ``` -/// -/// # Outcomes -/// -/// The returned [`Outcome`] of a `from_data` call determines how the incoming -/// request will be processed. -/// -/// * **Success**(S) -/// -/// If the `Outcome` is [`Success`], then the `Success` value will be used as -/// the value for the data parameter. As long as all other parsed types -/// succeed, the request will be handled by the requesting handler. -/// -/// * **Failure**(Status, E) -/// -/// If the `Outcome` is [`Failure`], the request will fail with the given -/// status code and error. The designated error [`Catcher`](crate::Catcher) will be -/// used to respond to the request. Note that users can request types of -/// `Result` and `Option` to catch `Failure`s and retrieve the error -/// value. -/// -/// * **Forward**(Data) -/// -/// If the `Outcome` is [`Forward`], the request will be forwarded to the next -/// matching request. This requires that no data has been read from the `Data` -/// parameter. Note that users can request an `Option` to catch `Forward`s. -/// -/// # Provided Implementations -/// -/// Rocket implements `FromTransformedData` for several built-in types. Their behavior is -/// documented here. -/// -/// * **Data** -/// -/// The identity implementation; simply returns [`Data`] directly. -/// -/// _This implementation always returns successfully._ -/// -/// * **Option<T>** _where_ **T: FromTransformedData** -/// -/// The type `T` is derived from the incoming data using `T`'s `FromTransformedData` -/// implementation. If the derivation is a `Success`, the derived value is -/// returned in `Some`. Otherwise, a `None` is returned. -/// -/// _This implementation always returns successfully._ -/// -/// * **Result<T, T::Error>** _where_ **T: FromTransformedData** -/// -/// The type `T` is derived from the incoming data using `T`'s `FromTransformedData` -/// implementation. If derivation is a `Success`, the value is returned in -/// `Ok`. If the derivation is a `Failure`, the error value is returned in -/// `Err`. If the derivation is a `Forward`, the request is forwarded. -/// -/// * **String** -/// -/// **Note:** _An implementation of `FromTransformedData` for `String` is only available -/// when compiling in debug mode!_ -/// -/// Reads the entire request body into a `String`. If reading fails, returns -/// a `Failure` with the corresponding `io::Error`. -/// -/// **WARNING:** Do **not** use this implementation for anything _but_ -/// debugging. This is because the implementation reads the entire body into -/// memory; since the user controls the size of the body, this is an obvious -/// vector for a denial of service attack. -/// -/// * **Vec<u8>** -/// -/// **Note:** _An implementation of `FromTransformedData` for `Vec` is only -/// available when compiling in debug mode!_ -/// -/// Reads the entire request body into a `Vec`. If reading fails, -/// returns a `Failure` with the corresponding `io::Error`. -/// -/// **WARNING:** Do **not** use this implementation for anything _but_ -/// debugging. This is because the implementation reads the entire body into -/// memory; since the user controls the size of the body, this is an obvious -/// vector for a denial of service attack. -/// -/// # Simplified `FromTransformedData` -/// -/// For an example of a type that wouldn't require transformation, see the -/// [`FromData`] documentation. -pub trait FromTransformedData<'r>: Sized { - /// The associated error to be returned when the guard fails. - type Error: Send; - - /// The owned type returned from [`FromTransformedData::transform()`]. - /// - /// The trait bounds ensures that it is is possible to borrow an - /// `&Self::Borrowed` from a value of this type. - type Owned: Borrow; - - /// The _borrowed_ type consumed by [`FromTransformedData::from_data()`] when - /// [`FromTransformedData::transform()`] returns a [`Transform::Borrowed`]. - /// - /// If [`FromTransformedData::from_data()`] returns a [`Transform::Owned`], this - /// associated type should be set to `Self::Owned`. - type Borrowed: ?Sized; - - /// Asynchronously transforms `data` into a value of type `Self::Owned`. - /// - /// If the returned future resolves to `Transform::Owned(Self::Owned)`, then - /// `from_data` should subsequently be called with a `data` value of - /// `Transform::Owned(Self::Owned)`. If the future resolves to - /// `Transform::Borrowed(Self::Owned)`, `from_data` should subsequently be - /// called with a `data` value of `Transform::Borrowed(&Self::Borrowed)`. In - /// other words, the variant of `Transform` returned from this method is - /// used to determine which variant of `Transform` should be passed to the - /// `from_data` method. Rocket _always_ makes the subsequent call correctly. - /// - /// It is very unlikely that a correct implementation of this method is - /// capable of returning either of an `Owned` or `Borrowed` variant. - /// Instead, this method should return exactly _one_ of these variants. - /// - /// If transformation succeeds, an outcome of `Success` is returned. - /// If the data is not appropriate given the type of `Self`, `Forward` is - /// returned. On failure, `Failure` is returned. - fn transform(request: &'r Request<'_>, data: Data) -> TransformFuture<'r, Self::Owned, Self::Error>; - - /// Asynchronously validates, parses, and converts the incoming request body - /// data into an instance of `Self`. - /// - /// If validation and parsing succeeds, an outcome of `Success` is returned. - /// If the data is not appropriate given the type of `Self`, `Forward` is - /// returned. If parsing or validation fails, `Failure` is returned. - /// - /// # Example - /// - /// When implementing this method, you rarely need to destruct the `outcome` - /// parameter. Instead, the first line of the method should be one of the - /// following: - /// - /// ```rust - /// # #[macro_use] extern crate rocket; - /// # use rocket::data::{Data, FromTransformedData, Transformed, Outcome}; - /// # fn f<'a>(outcome: Transformed<'a, Data>) -> Outcome>::Error> { - /// // If `Owned` was returned from `transform`: - /// let data = try_outcome!(outcome.owned()); - /// # unimplemented!() - /// # } - /// - /// # fn g<'a>(outcome: Transformed<'a, Data>) -> Outcome>::Error> { - /// // If `Borrowed` was returned from `transform`: - /// let data = try_outcome!(outcome.borrowed()); - /// # unimplemented!() - /// # } - /// ``` - fn from_data(request: &'r Request<'_>, outcome: Transformed<'r, Self>) -> FromDataFuture<'r, Self, Self::Error>; -} - -/// The identity implementation of `FromTransformedData`. Always returns `Success`. -impl<'r> FromTransformedData<'r> for Data { - type Error = std::convert::Infallible; - type Owned = Data; - type Borrowed = Data; - - #[inline(always)] - fn transform(_: &'r Request<'_>, data: Data) -> TransformFuture<'r, Self::Owned, Self::Error> { - Box::pin(ready(Transform::Owned(Success(data)))) - } - - #[inline(always)] - fn from_data(_: &'r Request<'_>, outcome: Transformed<'r, Self>) -> FromDataFuture<'r, Self, Self::Error> { - Box::pin(ready(outcome.owned())) - } -} - -/// A variant of [`FromTransformedData`] for data guards that don't require -/// transformations. -/// -/// When transformation of incoming data isn't required, data guards should -/// implement this trait instead of [`FromTransformedData`]. Any type that -/// implements `FromData` automatically implements `FromTransformedData`. For a -/// description of data guards, see the [`FromTransformedData`] documentation. +/// A route can have at most one data guard. Above, `var` is used as the +/// argument name for the data guard type `DataGuard`. When the `submit` route +/// matches, Rocket will call the `FromData` implementation for the type `T`. +/// The handler will only be called if the guard returns successfully. /// /// ## Async Trait /// @@ -437,10 +63,10 @@ impl<'r> FromTransformedData<'r> for Data { /// # type MyError = String; /// /// #[rocket::async_trait] -/// impl FromData for MyType { +/// impl<'r> FromData<'r> for MyType { /// type Error = MyError; /// -/// async fn from_data(req: &Request<'_>, data: Data) -> data::Outcome { +/// async fn from_data(req: &'r Request<'_>, data: Data) -> data::Outcome { /// /* .. */ /// # unimplemented!() /// } @@ -452,88 +78,110 @@ impl<'r> FromTransformedData<'r> for Data { /// Say that you have a custom type, `Person`: /// /// ```rust -/// struct Person { -/// name: String, +/// struct Person<'r> { +/// name: &'r str, /// age: u16 /// } /// ``` /// /// `Person` has a custom serialization format, so the built-in `Json` type /// doesn't suffice. The format is `:` with `Content-Type: -/// application/x-person`. You'd like to use `Person` as a `FromTransformedData` type so -/// that you can retrieve it directly from a client's request body: +/// application/x-person`. You'd like to use `Person` as a data guard, so that +/// you can retrieve it directly from a client's request body: /// /// ```rust -/// # #[macro_use] extern crate rocket; -/// # type Person = rocket::data::Data; +/// # use rocket::post; +/// # type Person<'r> = &'r rocket::http::RawStr; /// #[post("/person", data = "")] -/// fn person(person: Person) -> &'static str { +/// fn person(person: Person<'_>) -> &'static str { /// "Saved the new person to the database!" /// } /// ``` /// -/// A `FromData` implementation allowing this looks like: +/// A `FromData` implementation for such a type might look like: /// /// ```rust /// # #[macro_use] extern crate rocket; /// # /// # #[derive(Debug)] -/// # struct Person { name: String, age: u16 } +/// # struct Person<'r> { name: &'r str, age: u16 } /// # -/// use std::io::Read; -/// -/// use rocket::{Request, Data}; -/// use rocket::data::{self, Outcome, FromData, FromDataFuture, ByteUnit}; +/// use rocket::request::{self, Request}; +/// use rocket::data::{self, Data, FromData, ToByteUnit}; /// use rocket::http::{Status, ContentType}; -/// use rocket::tokio::io::AsyncReadExt; /// -/// // Always use a limit to prevent DoS attacks. -/// const LIMIT: ByteUnit = ByteUnit::Byte(256); +/// enum Error { +/// TooLarge, +/// NoColon, +/// InvalidAge, +/// Io(std::io::Error), +/// } /// /// #[rocket::async_trait] -/// impl FromData for Person { -/// type Error = String; +/// impl<'r> FromData<'r> for Person<'r> { +/// type Error = Error; +/// +/// async fn from_data(req: &'r Request<'_>, data: Data) -> data::Outcome { +/// use Error::*; +/// use rocket::outcome::Outcome::*; /// -/// async fn from_data(req: &Request<'_>, data: Data) -> Outcome { /// // Ensure the content type is correct before opening the data. /// let person_ct = ContentType::new("application", "x-person"); /// if req.content_type() != Some(&person_ct) { -/// return Outcome::Forward(data); +/// return Forward(data); /// } /// -/// // Read the data into a String. -/// let limit = req.limits().get("person").unwrap_or(LIMIT); -/// let string = match data.open(limit).stream_to_string().await { -/// Ok(string) => string, -/// Err(e) => return Outcome::Failure((Status::InternalServerError, format!("{}", e))) +/// // Use a configured limit with name 'person' or fallback to default. +/// let limit = req.limits().get("person").unwrap_or(256.bytes()); +/// +/// // Read the data into a string. +/// let string = match data.open(limit).into_string().await { +/// Ok(string) if string.is_complete() => string.into_inner(), +/// Ok(_) => return Failure((Status::PayloadTooLarge, TooLarge)), +/// Err(e) => return Failure((Status::InternalServerError, Io(e))), /// }; /// +/// // We store `string` in request-local cache for long-lived borrows. +/// let string = request::local_cache!(req, string); +/// /// // Split the string into two pieces at ':'. /// let (name, age) = match string.find(':') { -/// Some(i) => (string[..i].to_string(), &string[(i + 1)..]), -/// None => return Outcome::Failure((Status::UnprocessableEntity, "':'".into())) +/// Some(i) => (&string[..i], &string[(i + 1)..]), +/// None => return Failure((Status::UnprocessableEntity, NoColon)), /// }; /// /// // Parse the age. /// let age: u16 = match age.parse() { /// Ok(age) => age, -/// Err(_) => return Outcome::Failure((Status::UnprocessableEntity, "Age".into())) +/// Err(_) => return Failure((Status::UnprocessableEntity, InvalidAge)), /// }; /// -/// // Return successfully. -/// Outcome::Success(Person { name, age }) +/// Success(Person { name, age }) /// } /// } -/// # #[post("/person", data = "")] -/// # fn person(person: Person) { } -/// # #[post("/person", data = "")] -/// # fn person2(person: Result) { } -/// # fn main() { } +/// +/// // The following routes now typecheck... +/// +/// #[post("/person", data = "")] +/// fn person(person: Person<'_>) { /* .. */ } +/// +/// #[post("/person", data = "")] +/// fn person2(person: Result, Error>) { /* .. */ } +/// +/// #[post("/person", data = "")] +/// fn person3(person: Option>) { /* .. */ } +/// +/// #[post("/person", data = "")] +/// fn person4(person: Person<'_>) -> &str { +/// // Note that this is only possible because the data in `person` live +/// // as long as the request through request-local cache. +/// person.name +/// } /// ``` #[crate::async_trait] -pub trait FromData: Sized { +pub trait FromData<'r>: Sized { /// The associated error to be returned when the guard fails. - type Error: Send + 'static; + type Error: Send; /// Asynchronously validates, parses, and converts an instance of `Self` /// from the incoming request body data. @@ -541,98 +189,119 @@ pub trait FromData: Sized { /// If validation and parsing succeeds, an outcome of `Success` is returned. /// If the data is not appropriate given the type of `Self`, `Forward` is /// returned. If parsing fails, `Failure` is returned. - async fn from_data(request: &Request<'_>, data: Data) -> Outcome; + async fn from_data(req: &'r Request<'_>, data: Data) -> Outcome; } -impl<'r, T: FromData + 'r> FromTransformedData<'r> for T { - type Error = T::Error; - type Owned = Data; - type Borrowed = Data; +use crate::data::Capped; - #[inline(always)] - fn transform(_: &'r Request<'_>, d: Data) -> TransformFuture<'r, Self::Owned, Self::Error> { - Box::pin(ready(Transform::Owned(Success(d)))) - } +#[crate::async_trait] +impl<'r> FromData<'r> for Capped { + type Error = std::io::Error; - #[inline(always)] - fn from_data(req: &'r Request<'_>, o: Transformed<'r, Self>) -> FromDataFuture<'r, Self, Self::Error> { - match o.owned() { - Success(data) => T::from_data(req, data), - _ => unreachable!(), - } + async fn from_data(req: &'r Request<'_>, data: Data) -> Outcome { + let limit = req.limits().get("string").unwrap_or(Limits::STRING); + data.open(limit).into_string().await.into_outcome(Status::BadRequest) } } -impl<'r, T: FromTransformedData<'r> + 'r> FromTransformedData<'r> for Result { - type Error = T::Error; - type Owned = T::Owned; - type Borrowed = T::Borrowed; +impl_strict_from_data_from_capped!(String); - #[inline(always)] - fn transform(r: &'r Request<'_>, d: Data) -> TransformFuture<'r, Self::Owned, Self::Error> { - T::transform(r, d) - } +#[crate::async_trait] +impl<'r> FromData<'r> for Capped<&'r str> { + type Error = std::io::Error; - #[inline(always)] - fn from_data(r: &'r Request<'_>, o: Transformed<'r, Self>) -> FromDataFuture<'r, Self, Self::Error> { - Box::pin(T::from_data(r, o).map(|x| match x { - Success(val) => Success(Ok(val)), - Forward(data) => Forward(data), - Failure((_, e)) => Success(Err(e)), - })) + async fn from_data(req: &'r Request<'_>, data: Data) -> Outcome { + let capped = try_outcome!(>::from_data(req, data).await); + let string = capped.map(|s| local_cache!(req, s).as_str()); + Success(string) } } -impl<'r, T: FromTransformedData<'r> + 'r> FromTransformedData<'r> for Option { - type Error = T::Error; - type Owned = T::Owned; - type Borrowed = T::Borrowed; +impl_strict_from_data_from_capped!(&'r str); - #[inline(always)] - fn transform(r: &'r Request<'_>, d: Data) -> TransformFuture<'r, Self::Owned, Self::Error> { - T::transform(r, d) +#[crate::async_trait] +impl<'r> FromData<'r> for Capped<&'r RawStr> { + type Error = std::io::Error; + + async fn from_data(req: &'r Request<'_>, data: Data) -> Outcome { + let capped = try_outcome!(>::from_data(req, data).await); + let raw = capped.map(|s| RawStr::new(local_cache!(req, s))); + Success(raw) } +} - #[inline(always)] - fn from_data(r: &'r Request<'_>, o: Transformed<'r, Self>) -> FromDataFuture<'r, Self, Self::Error> { - Box::pin(T::from_data(r, o).map(|x| match x { - Success(val) => Success(Some(val)), - Failure(_) | Forward(_) => Success(None), - })) +impl_strict_from_data_from_capped!(&'r RawStr); + +#[crate::async_trait] +impl<'r> FromData<'r> for Capped> { + type Error = std::io::Error; + + async fn from_data(req: &'r Request<'_>, data: Data) -> Outcome { + let capped = try_outcome!(>::from_data(req, data).await); + Success(capped.map(|s| s.into())) } } -#[cfg(debug_assertions)] -use crate::data::ByteUnit; +impl_strict_from_data_from_capped!(std::borrow::Cow<'_, str>); #[crate::async_trait] -#[cfg(debug_assertions)] -impl FromData for String { +impl<'r> FromData<'r> for Capped<&'r [u8]> { type Error = std::io::Error; - #[inline(always)] - async fn from_data(_: &Request<'_>, data: Data) -> Outcome { - match data.open(ByteUnit::max_value()).stream_to_string().await { - Ok(string) => Success(string), - Err(e) => Failure((Status::BadRequest, e)), - } + async fn from_data(req: &'r Request<'_>, data: Data) -> Outcome { + let capped = try_outcome!(>>::from_data(req, data).await); + let raw = capped.map(|b| local_cache!(req, b).as_slice()); + Success(raw) } } +impl_strict_from_data_from_capped!(&'r [u8]); + #[crate::async_trait] -#[cfg(debug_assertions)] -impl FromData for Vec { +impl<'r> FromData<'r> for Capped> { type Error = std::io::Error; - #[inline(always)] - async fn from_data(_: &Request<'_>, data: Data) -> Outcome { - use tokio::io::AsyncReadExt; + async fn from_data(req: &'r Request<'_>, data: Data) -> Outcome { + let limit = req.limits().get("bytes").unwrap_or(Limits::BYTES); + data.open(limit).into_bytes().await.into_outcome(Status::BadRequest) + } +} + +impl_strict_from_data_from_capped!(Vec); + +#[crate::async_trait] +impl<'r> FromData<'r> for Data { + type Error = std::convert::Infallible; + + async fn from_data(_: &'r Request<'_>, data: Data) -> Outcome { + Success(data) + } +} + +#[crate::async_trait] +impl<'r, T: FromData<'r> + 'r> FromData<'r> for Result { + type Error = std::convert::Infallible; + + async fn from_data( + req: &'r Request<'_>, + data: Data + ) -> Outcome>::Error>, Self::Error> { + match T::from_data(req, data).await { + Success(v) => Success(Ok(v)), + Failure((_, e)) => Success(Err(e)), + Forward(d) => Forward(d), + } + } +} + +#[crate::async_trait] +impl<'r, T: FromData<'r>> FromData<'r> for Option { + type Error = std::convert::Infallible; - let mut stream = data.open(ByteUnit::max_value()); - let mut buf = Vec::new(); - match stream.read_to_end(&mut buf).await { - Ok(_) => Success(buf), - Err(e) => Failure((Status::BadRequest, e)), + async fn from_data(req: &'r Request<'_>, data: Data) -> Outcome { + match T::from_data(req, data).await { + Success(v) => Success(Some(v)), + Failure(..) | Forward(..) => Success(None), } } } diff --git a/core/lib/src/data/limits.rs b/core/lib/src/data/limits.rs index db2a3379bd..e1dbe1132f 100644 --- a/core/lib/src/data/limits.rs +++ b/core/lib/src/data/limits.rs @@ -3,21 +3,69 @@ use std::fmt; use serde::{Serialize, Deserialize}; use crate::request::{Request, FromRequest, Outcome}; -use crate::data::{ByteUnit, ToByteUnit}; +use crate::data::ByteUnit; +use crate::http::uncased::Uncased; -/// Mapping from data types to read limits. +/// Mapping from (hierarchical) data types to size limits. /// -/// A `Limits` structure contains a mapping from a given data type ("forms", -/// "json", and so on) to the maximum size in bytes that should be accepted by a -/// Rocket application for that data type. For instance, if the limit for -/// "forms" is set to `256`, only 256 bytes from an incoming form request will -/// be read. +/// A `Limits` structure contains a mapping from a given hierarchical data type +/// ("form", "data-form", "ext/pdf", and so on) to the maximum size in bytes +/// that should be accepted by Rocket for said data type. For instance, if the +/// limit for "form" is set to `256`, only 256 bytes from an incoming non-data +/// form (that is, url-encoded) will be accepted. /// -/// # Defaults +/// To help in preventing DoS attacks, all incoming data reads must capped by a +/// limit. As such, all data guards impose a limit. The _name_ of the limit is +/// dictated by the data guard or type itself. For instance, [`Form`] imposes +/// the `form` limit for value-based forms and `data-form` limit for data-based +/// forms. /// -/// The default limits are: +/// If a limit is exceeded, a guard will typically fail. The [`Capped`] type +/// allows retrieving some data types even when the limit is exceeded. /// -/// * **forms**: 32KiB +/// [`Capped`]: crate::data::Capped +/// [`Form`]: crate::form::Form +/// +/// # Hierarchy +/// +/// Data limits are hierarchical. The `/` (forward slash) character delimits the +/// levels, or layers, of a given limit. To obtain a limit value for a given +/// name, layers are peeled from right to left until a match is found, if any. +/// For example, fetching the limit named `pet/dog/bingo` will return the first +/// of `pet/dog/bingo`, `pet/dog` or `pet`: +/// +/// ```rust +/// use rocket::data::{Limits, ToByteUnit}; +/// +/// let limits = Limits::default() +/// .limit("pet", 64.kibibytes()) +/// .limit("pet/dog", 128.kibibytes()) +/// .limit("pet/dog/bingo", 96.kibibytes()); +/// +/// assert_eq!(limits.get("pet/dog/bingo"), Some(96.kibibytes())); +/// assert_eq!(limits.get("pet/dog/ralph"), Some(128.kibibytes())); +/// assert_eq!(limits.get("pet/cat/bingo"), Some(64.kibibytes())); +/// +/// assert_eq!(limits.get("pet/dog/bingo/hat"), Some(96.kibibytes())); +/// ``` +/// +/// # Built-in Limits +/// +/// The following table details recognized built-in limits used by Rocket. Note +/// that this table _does not_ include limits for types outside of Rocket's +/// core. In particular, this does not include limits applicable to contrib +/// types like `json` and `msgpack`. +/// +/// | Limit Name | Default | Type | Description | +/// |-------------------|---------|--------------|---------------------------------------| +/// | `form` | 32KiB | [`Form`] | entire non-data-based form | +/// | `data-form` | 2MiB | [`Form`] | entire data-based form | +/// | `file` | 1MiB | [`TempFile`] | [`TempFile`] data guard or form field | +/// | `file/$ext` | _N/A_ | [`TempFile`] | file form field with extension `$ext` | +/// | `string` | 8KiB | [`String`] | data guard or data form field | +/// | `bytes` | 8KiB | [`Vec`] | data guard | +/// +/// [`TempFile`]: crate::data::TempFile /// /// # Usage /// @@ -26,13 +74,16 @@ use crate::data::{ByteUnit, ToByteUnit}; /// ```rust /// use rocket::data::{Limits, ToByteUnit}; /// -/// // Set a limit of 64KiB for forms and 3MiB for JSON. +/// // Set a limit of 64KiB for forms, 3MiB for PDFs, and 1MiB for JSON. /// let limits = Limits::default() -/// .limit("forms", 64.kibibytes()) -/// .limit("json", 3.mebibytes()); +/// .limit("form", 64.kibibytes()) +/// .limit("file/pdf", 3.mebibytes()) +/// .limit("json", 1.mebibytes()); /// ``` /// -/// The configured limits can be retrieved via the `&Limits` request guard: +/// The [`Limits::default()`](#impl-Default) method populates the `Limits` +/// structure with default limits in the [table above](#built-in-limits). A +/// configured limit can be retrieved via the `&Limits` request guard: /// /// ```rust /// # #[macro_use] extern crate rocket; @@ -44,7 +95,7 @@ use crate::data::{ByteUnit, ToByteUnit}; /// #[post("/echo", data = "")] /// async fn echo(data: Data, limits: &Limits) -> Result> { /// let limit = limits.get("data").unwrap_or(1.mebibytes()); -/// Ok(data.open(limit).stream_to_string().await?) +/// Ok(data.open(limit).into_string().await?.value) /// } /// ``` /// @@ -58,10 +109,10 @@ use crate::data::{ByteUnit, ToByteUnit}; /// # struct MyType; /// # type MyError = (); /// #[rocket::async_trait] -/// impl FromData for MyType { +/// impl<'r> FromData<'r> for MyType { /// type Error = MyError; /// -/// async fn from_data(req: &Request<'_>, data: Data) -> data::Outcome { +/// async fn from_data(req: &'r Request<'_>, data: Data) -> data::Outcome { /// let limit = req.limits().get("my-data-type"); /// /* .. */ /// # unimplemented!() @@ -71,22 +122,37 @@ use crate::data::{ByteUnit, ToByteUnit}; #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] #[serde(transparent)] pub struct Limits { - // We cache this internally but don't share that fact in the API. #[serde(with = "figment::util::vec_tuple_map")] - limits: Vec<(String, ByteUnit)> + limits: Vec<(Uncased<'static>, ByteUnit)> } -/// The default limits are: -/// -/// * **forms**: 32KiB impl Default for Limits { fn default() -> Limits { - // Default limit for forms is 32KiB. - Limits { limits: vec![("forms".into(), 32.kibibytes())] } + Limits::new() + .limit("form", Limits::FORM) + .limit("data-form", Limits::DATA_FORM) + .limit("file", Limits::FILE) + .limit("string", Limits::STRING) + .limit("bytes", Limits::BYTES) } } impl Limits { + /// Default limit for value-based forms. + pub const FORM: ByteUnit = ByteUnit::Kibibyte(32); + + /// Default limit for data-based forms. + pub const DATA_FORM: ByteUnit = ByteUnit::Mebibyte(2); + + /// Default limit for temporary files. + pub const FILE: ByteUnit = ByteUnit::Mebibyte(1); + + /// Default limit for strings. + pub const STRING: ByteUnit = ByteUnit::Kibibyte(8); + + /// Default limit for bytes. + pub const BYTES: ByteUnit = ByteUnit::Kibibyte(8); + /// Construct a new `Limits` structure with no limits set. /// /// # Example @@ -95,10 +161,10 @@ impl Limits { /// use rocket::data::{Limits, ToByteUnit}; /// /// let limits = Limits::default(); - /// assert_eq!(limits.get("forms"), Some(32.kibibytes())); + /// assert_eq!(limits.get("form"), Some(32.kibibytes())); /// /// let limits = Limits::new(); - /// assert_eq!(limits.get("forms"), None); + /// assert_eq!(limits.get("form"), None); /// ``` #[inline] pub fn new() -> Self { @@ -113,42 +179,112 @@ impl Limits { /// ```rust /// use rocket::data::{Limits, ToByteUnit}; /// - /// let limits = Limits::default().limit("json", 1.mebibytes()); + /// let limits = Limits::default(); + /// assert_eq!(limits.get("form"), Some(32.kibibytes())); + /// assert_eq!(limits.get("json"), None); /// - /// assert_eq!(limits.get("forms"), Some(32.kibibytes())); + /// let limits = limits.limit("json", 1.mebibytes()); + /// assert_eq!(limits.get("form"), Some(32.kibibytes())); /// assert_eq!(limits.get("json"), Some(1.mebibytes())); /// - /// let new_limits = limits.limit("json", 64.mebibytes()); - /// assert_eq!(new_limits.get("json"), Some(64.mebibytes())); + /// let limits = limits.limit("json", 64.mebibytes()); + /// assert_eq!(limits.get("json"), Some(64.mebibytes())); /// ``` - pub fn limit>(mut self, name: S, limit: ByteUnit) -> Self { + pub fn limit>>(mut self, name: S, limit: ByteUnit) -> Self { let name = name.into(); - match self.limits.iter_mut().find(|(k, _)| *k == name) { - Some((_, v)) => *v = limit, - None => self.limits.push((name, limit)), + match self.limits.binary_search_by(|(k, _)| k.cmp(&name)) { + Ok(i) => self.limits[i].1 = limit, + Err(i) => self.limits.insert(i, (name.into(), limit)) } - self.limits.sort_by(|a, b| a.0.cmp(&b.0)); self } - /// Retrieve the set limit, if any, for the data type with name `name`. + /// Returns the limit named `name`, proceeding hierarchically from right + /// to left until one is found, or returning `None` if none is found. /// /// # Example /// /// ```rust /// use rocket::data::{Limits, ToByteUnit}; /// - /// let limits = Limits::default().limit("json", 64.mebibytes()); + /// let limits = Limits::default() + /// .limit("json", 1.mebibytes()) + /// .limit("file/jpeg", 4.mebibytes()); + /// + /// assert_eq!(limits.get("form"), Some(32.kibibytes())); + /// assert_eq!(limits.get("json"), Some(1.mebibytes())); + /// assert_eq!(limits.get("data-form"), Some(Limits::DATA_FORM)); + /// + /// assert_eq!(limits.get("file"), Some(1.mebibytes())); + /// assert_eq!(limits.get("file/png"), Some(1.mebibytes())); + /// assert_eq!(limits.get("file/jpeg"), Some(4.mebibytes())); + /// assert_eq!(limits.get("file/jpeg/inner"), Some(4.mebibytes())); /// - /// assert_eq!(limits.get("forms"), Some(32.kibibytes())); - /// assert_eq!(limits.get("json"), Some(64.mebibytes())); /// assert!(limits.get("msgpack").is_none()); /// ``` - pub fn get(&self, name: &str) -> Option { - self.limits.iter() - .find(|(k, _)| *k == name) - .map(|(_, v)| *v) + pub fn get>(&self, name: S) -> Option { + let mut name = name.as_ref(); + let mut indices = name.rmatch_indices('/'); + loop { + let exact_limit = self.limits + .binary_search_by(|(k, _)| k.as_uncased_str().cmp(name.into())) + .map(|i| self.limits[i].1); + + if let Ok(exact) = exact_limit { + return Some(exact); + } + + let (i, _) = indices.next()?; + name = &name[..i]; + } + } + + /// Returns the limit for the name created by joining the strings in + /// `layers` with `/` as a separator, then proceeding like + /// [`Limits::get()`], hierarchically from right to left until one is found, + /// or returning `None` if none is found. + /// + /// This methods exists to allow finding hierarchical limits without + /// constructing a string to call `get()` with but otherwise returns the + /// same results. + /// + /// # Example + /// + /// ```rust + /// use rocket::data::{Limits, ToByteUnit}; + /// + /// let limits = Limits::default() + /// .limit("json", 2.mebibytes()) + /// .limit("file/jpeg", 4.mebibytes()); + /// + /// assert_eq!(limits.find(["json"]), Some(2.mebibytes())); + /// assert_eq!(limits.find(["json", "person"]), Some(2.mebibytes())); + /// + /// assert_eq!(limits.find(["file"]), Some(1.mebibytes())); + /// assert_eq!(limits.find(["file", "png"]), Some(1.mebibytes())); + /// assert_eq!(limits.find(["file", "jpeg"]), Some(4.mebibytes())); + /// assert_eq!(limits.find(["file", "jpeg", "inner"]), Some(4.mebibytes())); + /// + /// # let s: &[&str] = &[]; assert_eq!(limits.find(s), None); + /// ``` + pub fn find, L: AsRef<[S]>>(&self, layers: L) -> Option { + let layers = layers.as_ref(); + for j in (1..=layers.len()).rev() { + let layers = &layers[..j]; + let opt = self.limits + .binary_search_by(|(k, _)| { + let k_layers = k.as_str().split('/'); + k_layers.cmp(layers.iter().map(|s| s.as_ref())) + }) + .map(|i| self.limits[i].1); + + if let Ok(byte_unit) = opt { + return Some(byte_unit); + } + } + + None } } diff --git a/core/lib/src/data/mod.rs b/core/lib/src/data/mod.rs index e93d43375e..fd90316e5c 100644 --- a/core/lib/src/data/mod.rs +++ b/core/lib/src/data/mod.rs @@ -1,13 +1,19 @@ //! Types and traits for handling incoming body data. +#[macro_use] +mod capped; mod data; mod data_stream; mod from_data; mod limits; +mod temp_file; pub use self::data::Data; pub use self::data_stream::DataStream; -pub use self::from_data::{FromData, Outcome, FromTransformedData, FromDataFuture}; -pub use self::from_data::{Transform, Transformed, TransformFuture}; +pub use self::from_data::{FromData, Outcome}; pub use self::limits::Limits; +pub use self::capped::{N, Capped}; pub use ubyte::{ByteUnit, ToByteUnit}; +pub use temp_file::TempFile; + +pub(crate) use self::data_stream::StreamReader; diff --git a/core/lib/src/data/temp_file.rs b/core/lib/src/data/temp_file.rs new file mode 100644 index 0000000000..d360a39444 --- /dev/null +++ b/core/lib/src/data/temp_file.rs @@ -0,0 +1,354 @@ +use std::io; +use std::path::{PathBuf, Path}; + +use crate::http::{ContentType, Status}; +use crate::data::{FromData, Data, Capped, N, Limits}; +use crate::form::{FromFormField, ValueField, DataField, error::Errors}; +use crate::outcome::IntoOutcome; +use crate::request::Request; + +use tokio::fs::{self, File}; +use tempfile::{NamedTempFile, TempPath}; +use either::Either; + +/// A file in temporary storage, deleted when dropped unless persisted. +/// +/// `TempFile` is a data and form field (both value and data fields) guard that +/// streams incoming data into file in a temporary location. The file is deleted +/// when the `TempFile` handle is dropped. The file can be persisted with +/// [`TempFile::persist_to()`]. +/// +/// # Hazards +/// +/// Temporary files are cleaned by system file cleaners periodically. While an +/// attempt is made not to delete temporary files in use, _detection_ of when a +/// temporary file is being used is unreliable. As a result, a time-of-check to +/// time-of-use race condition from the creation of a `TempFile` to the +/// persistance of the `TempFile` may occur. Specifically, the following +/// sequence may occur: +/// +/// 1. A `TempFile` is created at random path `foo`. +/// 2. The system cleaner removes the file at path `foo`. +/// 3. Another application creates a file at path `foo`. +/// 4. The `TempFile`, ostesnsibly at path, `foo`, is persisted unexpectedly +/// with contents different from those in step 1. +/// +/// To safe-guard against this issue, you should ensure that your temporary file +/// cleaner, if any, does not delete files too eagerly. +/// +/// # Configuration +/// +/// * **temporary file directory** +/// +/// Configured via the [`temp_dir`](crate::Config::temp_dir) configuration +/// parameter, defaulting to the system's default temporary +/// ([`std::env::temp_dir()`]). Specifies where the files are stored. +/// +/// * **data limit** +/// +/// Controlled via [limits](crate::data::Limits) named `file` and `file/$ext`. +/// When used as a form guard, the extension `ext` is identified by the form +/// field's `Content-Type` ([`ContentType::extension()`]). When used as a data +/// guard, the extension is identified by the Content-Type of the request, if +/// any. If there is no Content-Type, the limit `file` is used. +/// +/// # Cappable +/// +/// A data stream can be partially read into a `TempFile` even if the incoming +/// stream exceeds the data limit via the [`Capped`] data and form +/// guard. +/// +/// # Examples +/// +/// **Data Guard** +/// +/// ```rust +/// # use rocket::post; +/// use rocket::data::TempFile; +/// +/// #[post("/upload", data = "")] +/// async fn upload(mut file: TempFile<'_>) -> std::io::Result<()> { +/// file.persist_to("/tmp/complete/file.txt").await?; +/// Ok(()) +/// } +/// ``` +/// +/// **Form Field** +/// +/// ```rust +/// # #[macro_use] extern crate rocket; +/// use rocket::data::TempFile; +/// use rocket::form::Form; +/// +/// #[derive(FromForm)] +/// struct Upload<'f> { +/// upload: TempFile<'f> +/// } +/// +/// #[post("/form", data = "")] +/// async fn upload(mut form: Form>) -> std::io::Result<()> { +/// form.upload.persist_to("/tmp/complete/file.txt").await?; +/// Ok(()) +/// } +/// ``` +/// +/// See also the [`Capped`] documentation for an example of `Capped` +/// as a data guard. +#[derive(Debug)] +pub enum TempFile<'v> { + #[doc(hidden)] + File { + file_name: Option<&'v str>, + content_type: Option, + path: Either, + len: u64, + }, + #[doc(hidden)] + Buffered { + content: &'v str, + } +} + +impl<'v> TempFile<'v> { + /// Persists the temporary file, moving it to `path`. + /// + /// This method _does not_ create a copy of `self`, nor a new link to the + /// contents of `self`: it renames the temporary file to `path` and marks it + /// as non-temporary. As a result, this method _cannot_ be used to create + /// multiple copies of `self`. To create multiple links, use + /// [`std::fs::hard_link()`] with `path` as the `src` _after_ calling this + /// method. + /// + /// # Example + /// + /// ```rust + /// # #[macro_use] extern crate rocket; + /// use rocket::data::TempFile; + /// + /// #[post("/", data = "")] + /// async fn handle(mut file: TempFile<'_>) -> std::io::Result<()> { + /// # assert!(file.path().is_none()); + /// # let some_path = std::env::temp_dir().join("some-file.txt"); + /// file.persist_to(&some_path).await?; + /// assert_eq!(file.path(), Some(&*some_path)); + /// + /// Ok(()) + /// } + /// # let file = TempFile::Buffered { content: "hi".into() }; + /// # rocket::async_test(handle(file)).unwrap(); + /// ``` + pub async fn persist_to

(&mut self, path: P) -> io::Result<()> + where P: AsRef + { + use std::mem::replace; + use tokio::io::AsyncWriteExt; + + let new_path = path.as_ref(); + match self { + TempFile::File { path: either, .. } => { + let path = replace(either, Either::Right(new_path.to_path_buf())); + match path { + Either::Left(temp_path) => { + let new_path = new_path.to_path_buf(); + let result = tokio::task::spawn_blocking(move || { + temp_path.persist(new_path) + }).await.map_err(|_| { + io::Error::new(io::ErrorKind::BrokenPipe, "spawn_block") + })?; + + if let Err(e) = result { + *either = Either::Left(e.path); + return Err(e.error); + } + }, + Either::Right(prev) => { + if let Err(e) = fs::rename(&prev, new_path).await { + *either = Either::Right(prev); + return Err(e); + } + } + } + } + TempFile::Buffered { content } => { + let mut file = File::create(new_path).await?; + file.write_all(content.as_bytes()).await?; + *self = TempFile::File { + file_name: None, + content_type: None, + path: Either::Right(new_path.to_path_buf()), + len: content.len() as u64 + }; + } + } + + Ok(()) + } + + /// Returns the size, in bytes, of the file. + /// + /// This method does not perform any system calls. + /// + /// ```rust + /// # #[macro_use] extern crate rocket; + /// use rocket::data::TempFile; + /// + /// #[post("/", data = "")] + /// fn handler(file: TempFile<'_>) { + /// let file_len = file.len(); + /// } + /// ``` + pub fn len(&self) -> u64 { + match self { + TempFile::File { len, .. } => *len, + TempFile::Buffered { content } => content.len() as u64, + } + } + + /// Returns the path to the file if it is known. + /// + /// Once a file is persisted with [`TempFile::persist_to()`], this method is + /// guaranteed to return `Some`. Prior to this point, however, this method + /// may return `Some` or `None`, depending on whether the file is on disk or + /// partially buffered in memory. + /// + /// ```rust + /// # #[macro_use] extern crate rocket; + /// use rocket::data::TempFile; + /// + /// #[post("/", data = "")] + /// async fn handle(mut file: TempFile<'_>) -> std::io::Result<()> { + /// # assert!(file.path().is_none()); + /// # let some_path = std::env::temp_dir().join("some-file.txt"); + /// file.persist_to(&some_path).await?; + /// assert_eq!(file.path(), Some(&*some_path)); + /// + /// Ok(()) + /// } + /// # let file = TempFile::Buffered { content: "hi".into() }; + /// # rocket::async_test(handle(file)).unwrap(); + /// ``` + pub fn path(&self) -> Option<&Path> { + match self { + TempFile::File { path: Either::Left(p), .. } => Some(p.as_ref()), + TempFile::File { path: Either::Right(p), .. } => Some(p.as_path()), + TempFile::Buffered { .. } => None, + } + } + + /// Returns the name of the file as specified in the form field. + /// + /// A multipart data form field can optionally specify the name of a file. + /// A browser will typically send the actual name of a user's selected file + /// in this field. This method returns that value, if it was specified, + /// without a file extension. + /// + /// The name is guaranteed to be a _true_ filename minus the extension. It + /// has been sanitized so as to not to contain path components, start with + /// `.` or `*`, or end with `:`, `>`, or `<`, making it safe for direct use + /// as the name of a file. + /// + /// ```rust + /// # #[macro_use] extern crate rocket; + /// use rocket::data::TempFile; + /// + /// #[post("/", data = "")] + /// async fn handle(mut file: TempFile<'_>) -> std::io::Result<()> { + /// # let some_dir = std::env::temp_dir(); + /// if let Some(name) = file.file_name() { + /// // Due to Rocket's sanitization, this is safe. + /// file.persist_to(&some_dir.join(name)).await?; + /// } + /// + /// Ok(()) + /// } + /// ``` + pub fn file_name(&self) -> Option<&str> { + match *self { + TempFile::File { file_name, .. } => file_name, + TempFile::Buffered { .. } => None + } + } + + /// Returns the Content-Type of the file as specified in the form field. + /// + /// A multipart data form field can optionally specify the content-type of a + /// file. A browser will typically sniff the file's extension to set the + /// content-type. This method returns that value, if it was specified. + /// + /// ```rust + /// # #[macro_use] extern crate rocket; + /// use rocket::data::TempFile; + /// + /// #[post("/", data = "")] + /// fn handle(file: TempFile<'_>) { + /// let content_type = file.content_type(); + /// } + /// ``` + pub fn content_type(&self) -> Option<&ContentType> { + match self { + TempFile::File { content_type, .. } => content_type.as_ref(), + TempFile::Buffered { .. } => None + } + } + + async fn from<'a>( + req: &Request<'_>, + data: Data, + file_name: Option<&'a str>, + content_type: Option, + ) -> io::Result>> { + let limit = content_type.as_ref() + .and_then(|ct| ct.extension()) + .and_then(|ext| req.limits().find(&["file", ext.as_str()])) + .or_else(|| req.limits().get("file")) + .unwrap_or(Limits::FILE); + + let temp_dir = req.config().temp_dir.clone(); + let file = tokio::task::spawn_blocking(move || { + NamedTempFile::new_in(temp_dir) + }).await.map_err(|_| { + io::Error::new(io::ErrorKind::BrokenPipe, "spawn_block panic") + })??; + + let (file, temp_path) = file.into_parts(); + let mut file = File::from_std(file); + let n = data.open(limit).stream_to(tokio::io::BufWriter::new(&mut file)).await?; + let temp_file = TempFile::File { + content_type, file_name, + path: Either::Left(temp_path), + len: n.written, + }; + + Ok(Capped::new(temp_file, n)) + } +} + +#[crate::async_trait] +impl<'v> FromFormField<'v> for Capped> { + fn from_value(field: ValueField<'v>) -> Result> { + let n = N { written: field.value.len() as u64, complete: true }; + Ok(Capped::new(TempFile::Buffered { content: field.value }, n)) + } + + async fn from_data( + f: DataField<'v, '_> + ) -> Result> { + Ok(TempFile::from(f.request, f.data, f.file_name, Some(f.content_type)).await?) + } +} + +#[crate::async_trait] +impl<'r> FromData<'r> for Capped> { + type Error = io::Error; + + async fn from_data( + req: &'r crate::Request<'_>, + data: crate::Data + ) -> crate::data::Outcome { + TempFile::from(req, data, None, req.content_type().cloned()).await + .into_outcome(Status::BadRequest) + } +} + +impl_strict_from_form_field_from_capped!(TempFile<'v>); +impl_strict_from_data_from_capped!(TempFile<'_>); diff --git a/core/lib/src/error.rs b/core/lib/src/error.rs index 66e60df1c8..c5a71673c8 100644 --- a/core/lib/src/error.rs +++ b/core/lib/src/error.rs @@ -170,7 +170,7 @@ impl Drop for Error { ErrorKind::Bind(ref e) => { error!("Rocket failed to bind network socket to given address/port."); info_!("{}", e); - panic!("aborting due to binding o error"); + panic!("aborting due to socket bind error"); } ErrorKind::Io(ref e) => { error!("Rocket failed to launch due to an I/O error."); diff --git a/core/lib/src/ext.rs b/core/lib/src/ext.rs index d851a507f9..111deb951e 100644 --- a/core/lib/src/ext.rs +++ b/core/lib/src/ext.rs @@ -1,11 +1,12 @@ -use std::io::{self, Cursor}; +use std::io; use std::pin::Pin; use std::task::{Poll, Context}; use futures::{ready, stream::Stream}; use tokio::io::{AsyncRead, ReadBuf}; +use pin_project_lite::pin_project; -use crate::http::hyper::{self, Bytes, HttpBody}; +use crate::http::hyper::Bytes; pub struct IntoBytesStream { inner: R, @@ -47,57 +48,67 @@ pub trait AsyncReadExt: AsyncRead + Sized { impl AsyncReadExt for T { } -pub struct AsyncReadBody { - inner: hyper::Body, - state: State, +pub trait PollExt { + fn map_err_ext(self, f: F) -> Poll>> + where F: FnOnce(E) -> U; } -enum State { - Pending, - Partial(Cursor), - Done, +impl PollExt for Poll>> { + /// Changes the error value of this `Poll` with the closure provided. + fn map_err_ext(self, f: F) -> Poll>> + where F: FnOnce(E) -> U + { + match self { + Poll::Ready(Some(Ok(t))) => Poll::Ready(Some(Ok(t))), + Poll::Ready(Some(Err(e))) => Poll::Ready(Some(Err(f(e)))), + Poll::Ready(None) => Poll::Ready(None), + Poll::Pending => Poll::Pending, + } + } } -impl AsyncReadBody { - pub fn empty() -> Self { - Self { inner: hyper::Body::empty(), state: State::Done } +pin_project! { + /// Stream for the [`chain`](super::AsyncReadExt::chain) method. + #[must_use = "streams do nothing unless polled"] + pub struct Chain { + #[pin] + first: T, + #[pin] + second: U, + done_first: bool, } } -impl From for AsyncReadBody { - fn from(body: hyper::Body) -> Self { - Self { inner: body, state: State::Pending } +impl Chain { + pub(crate) fn new(first: T, second: U) -> Self { + Self { first, second, done_first: false } } } -impl AsyncRead for AsyncReadBody { +impl Chain { + /// Gets references to the underlying readers in this `Chain`. + pub fn get_ref(&self) -> (&T, &U) { + (&self.first, &self.second) + } +} + +impl AsyncRead for Chain { fn poll_read( - mut self: Pin<&mut Self>, + self: Pin<&mut Self>, cx: &mut Context<'_>, buf: &mut ReadBuf<'_>, ) -> Poll> { - loop { - match self.state { - State::Pending => { - match ready!(Pin::new(&mut self.inner).poll_data(cx)) { - Some(Ok(bytes)) => { - self.state = State::Partial(Cursor::new(bytes)); - } - Some(Err(e)) => { - let error = io::Error::new(io::ErrorKind::Other, e); - return Poll::Ready(Err(error)); - } - None => self.state = State::Done, - } - }, - State::Partial(ref mut cursor) => { - match ready!(Pin::new(cursor).poll_read(cx, buf)) { - Ok(()) if buf.filled().is_empty() => self.state = State::Pending, - result => return Poll::Ready(result), - } - } - State::Done => return Poll::Ready(Ok(())), + let me = self.project(); + + if !*me.done_first { + let init_rem = buf.remaining(); + ready!(me.first.poll_read(cx, buf))?; + if buf.remaining() == init_rem { + *me.done_first = true; + } else { + return Poll::Ready(Ok(())); } } + me.second.poll_read(cx, buf) } } diff --git a/core/lib/src/fairing/mod.rs b/core/lib/src/fairing/mod.rs index 76482d5ab5..0f4c29e587 100644 --- a/core/lib/src/fairing/mod.rs +++ b/core/lib/src/fairing/mod.rs @@ -90,7 +90,7 @@ pub use self::info_kind::{Info, Kind}; /// /// [request guard]: crate::request::FromRequest /// [request guards]: crate::request::FromRequest -/// [data guards]: crate::data::FromTransformedData +/// [data guards]: crate::data::FromData /// /// ## Fairing Callbacks /// diff --git a/core/lib/src/form/context.rs b/core/lib/src/form/context.rs new file mode 100644 index 0000000000..425b755711 --- /dev/null +++ b/core/lib/src/form/context.rs @@ -0,0 +1,164 @@ +use serde::Serialize; +use indexmap::{IndexMap, IndexSet}; + +use crate::form::prelude::*; +use crate::http::Status; + +/// An infallible form guard that records form fields while parsing any form +/// type. +#[derive(Debug)] +pub struct Contextual<'v, T> { + pub value: Option, + pub context: Context<'v> +} + +/// A form context containing received fields, values, and encountered errors. +/// +/// # Serialization +/// +/// When a value of this type is serialized, a `struct` or map with the +/// following fields is emitted: +/// +/// | field | type | description | +/// |---------------|-------------------|------------------------------------------------| +/// | `errors` | &str => &[Error] | map from a field name to errors it encountered | +/// | `values` | &str => &[&str] | map from a field name to its submitted values | +/// | `data_values` | &[&str] | field names of all data fields received | +/// | `form_errors` | &[Error] | errors not corresponding to specific fields | +/// +/// See [`Error`] for details on how an `Error` is serialized. +#[derive(Debug, Default, Serialize)] +pub struct Context<'v> { + errors: IndexMap, Errors<'v>>, + values: IndexMap<&'v Name, Vec<&'v str>>, + data_values: IndexSet<&'v Name>, + form_errors: Errors<'v>, + #[serde(skip)] + status: Status, +} + +impl<'v> Context<'v> { + pub fn value>(&self, name: N) -> Option<&'v str> { + self.values.get(name.as_ref())?.get(0).cloned() + } + + pub fn values<'a, N>(&'a self, name: N) -> impl Iterator + 'a + where N: AsRef + { + self.values + .get(name.as_ref()) + .map(|e| e.iter().cloned()) + .into_iter() + .flatten() + } + + pub fn has_error>(&self, name: &N) -> bool { + self.errors(name).next().is_some() + } + + pub fn errors<'a, N>(&'a self, name: &'a N) -> impl Iterator> + 'a + where N: AsRef + ?Sized + { + let name = name.as_ref(); + name.prefixes() + .filter_map(move |name| self.errors.get(name)) + .map(|e| e.iter()) + .flatten() + } + + pub fn all_errors(&self) -> impl Iterator> { + self.errors.values() + .map(|e| e.iter()) + .flatten() + .chain(self.form_errors.iter()) + } + + pub fn status(&self) -> Status { + self.status + } + + pub(crate) fn push_error(&mut self, e: Error<'v>) { + self.status = std::cmp::max(self.status, e.status()); + match e.name { + Some(ref name) => match self.errors.get_mut(name) { + Some(errors) => errors.push(e), + None => { self.errors.insert(name.clone(), e.into()); }, + } + None => self.form_errors.push(e) + } + } + + pub(crate) fn push_errors(&mut self, errors: Errors<'v>) { + errors.into_iter().for_each(|e| self.push_error(e)) + } +} + +impl<'f> From> for Context<'f> { + fn from(errors: Errors<'f>) -> Self { + let mut context = Context::default(); + context.push_errors(errors); + context + } +} + +// impl<'v, T> From> for Contextual<'v, T> { +// fn from(e: Errors<'v>) -> Self { +// Contextual { value: None, context: Context::from(e) } +// } +// } + +// #[crate::async_trait] +// impl<'r, T: FromForm<'r>> FromData<'r> for Contextual<'r, T> { +// type Error = std::convert::Infallible; +// +// async fn from_data(req: &'r Request<'_>, data: Data) -> Outcome { +// match Form::>::from_data(req, data).await { +// Outcome::Success(form) => Outcome::Success(form.into_inner()), +// Outcome::Failure((_, e)) => Outcome::Success(Contextual::from(e)), +// Outcome::Forward(d) => Outcome::Forward(d) +// } +// } +// } + +#[crate::async_trait] +impl<'v, T: FromForm<'v>> FromForm<'v> for Contextual<'v, T> { + type Context = (>::Context, Context<'v>); + + fn init(opts: Options) -> Self::Context { + (T::init(opts), Context::default()) + } + + fn push_value((ref mut val_ctxt, ctxt): &mut Self::Context, field: ValueField<'v>) { + ctxt.values.entry(field.name.source()).or_default().push(field.value); + T::push_value(val_ctxt, field); + } + + async fn push_data( + (ref mut val_ctxt, ctxt): &mut Self::Context, + field: DataField<'v, '_> + ) { + ctxt.data_values.insert(field.name.source()); + T::push_data(val_ctxt, field).await; + } + + fn push_error((_, ref mut ctxt): &mut Self::Context, e: Error<'v>) { + ctxt.push_error(e); + } + + fn finalize((val_ctxt, mut context): Self::Context) -> Result<'v, Self> { + let value = match T::finalize(val_ctxt) { + Ok(value) => Some(value), + Err(errors) => { + context.push_errors(errors); + None + } + }; + + Ok(Contextual { value, context }) + } + + + fn default() -> Option { + Self::finalize(Self::init(Options::Lenient)).ok() + } +} diff --git a/core/lib/src/form/error.rs b/core/lib/src/form/error.rs new file mode 100644 index 0000000000..3a18e514d6 --- /dev/null +++ b/core/lib/src/form/error.rs @@ -0,0 +1,554 @@ +use std::{fmt, io}; +use std::num::{ParseIntError, ParseFloatError}; +use std::str::{Utf8Error, ParseBoolError}; +use std::net::AddrParseError; +use std::borrow::Cow; + +use serde::{Serialize, ser::{Serializer, SerializeStruct}}; + +use crate::http::Status; +use crate::form::name::{NameBuf, Name}; +use crate::data::ByteUnit; + +/// A collection of [`Error`]s. +#[derive(Default, Debug, PartialEq, Serialize)] +#[serde(transparent)] +pub struct Errors<'v>(Vec>); + +impl crate::http::ext::IntoOwned for Errors<'_> { + type Owned = Errors<'static>; + + fn into_owned(self) -> Self::Owned { + Errors(self.0.into_owned()) + } +} + +/// A form error, potentially tied to a specific form field. +/// +/// # Serialization +/// +/// When a value of this type is serialized, a `struct` or map with the +/// following fields is emitted: +/// +/// | field | type | description | +/// |----------|----------------|--------------------------------------------------| +/// | `name` | `Option<&str>` | the erroring field's name, if known | +/// | `value` | `Option<&str>` | the erroring field's value, if known | +/// | `entity` | `&str` | string representation of the erroring [`Entity`] | +/// | `msg` | `&str` | concise message of the error | +#[derive(Debug, PartialEq)] +pub struct Error<'v> { + /// The name of the field, if it is known. + pub name: Option>, + /// The field's value, if it is known. + pub value: Option>, + /// The kind of error that occured. + pub kind: ErrorKind<'v>, + /// The entitiy that caused the error. + pub entity: Entity, +} + +impl<'v> Serialize for Error<'v> { + fn serialize(&self, ser: S) -> Result { + let mut err = ser.serialize_struct("Error", 3)?; + err.serialize_field("name", &self.name)?; + err.serialize_field("value", &self.value)?; + err.serialize_field("entity", &self.entity.to_string())?; + err.serialize_field("msg", &self.to_string())?; + err.end() + } +} + +impl crate::http::ext::IntoOwned for Error<'_> { + type Owned = Error<'static>; + + fn into_owned(self) -> Self::Owned { + Error { + name: self.name.into_owned(), + value: self.value.into_owned(), + kind: self.kind.into_owned(), + entity: self.entity, + } + } +} + +#[derive(Debug)] +pub enum ErrorKind<'v> { + InvalidLength { + min: Option, + max: Option, + }, + InvalidChoice { + choices: Cow<'v, [Cow<'v, str>]>, + }, + OutOfRange { + start: Option, + end: Option, + }, + Validation(Cow<'v, str>), + Duplicate, + Missing, + Unexpected, + Unknown, + Custom(Box), + Multipart(multer::Error), + Utf8(Utf8Error), + Int(ParseIntError), + Bool(ParseBoolError), + Float(ParseFloatError), + Addr(AddrParseError), + Io(io::Error), +} + +impl crate::http::ext::IntoOwned for ErrorKind<'_> { + type Owned = ErrorKind<'static>; + + fn into_owned(self) -> Self::Owned { + use ErrorKind::*; + + match self { + InvalidLength { min, max } => InvalidLength { min, max }, + OutOfRange { start, end } => OutOfRange { start, end }, + Validation(s) => Validation(s.into_owned().into()), + Duplicate => Duplicate, + Missing => Missing, + Unexpected => Unexpected, + Unknown => Unknown, + Custom(e) => Custom(e), + Multipart(e) => Multipart(e), + Utf8(e) => Utf8(e), + Int(e) => Int(e), + Bool(e) => Bool(e), + Float(e) => Float(e), + Addr(e) => Addr(e), + Io(e) => Io(e), + InvalidChoice { choices } => InvalidChoice { + choices: choices.iter() + .map(|s| Cow::Owned(s.to_string())) + .collect::>() + .into() + } + } + } +} + +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +pub enum Entity { + Form, + Field, + ValueField, + DataField, + Name, + Value, + Key, + Indices, + Index(usize), +} + +impl<'v> Errors<'v> { + pub fn new() -> Self { + Errors(vec![]) + } + + pub fn with_name>>(mut self, name: N) -> Self { + self.set_name(name); + self + } + + pub fn set_name>>(&mut self, name: N) { + let name = name.into(); + for error in self.iter_mut() { + if error.name.is_none() { + error.set_name(name.clone()); + } + } + } + + pub fn with_value(mut self, value: &'v str) -> Self { + self.set_value(value); + self + } + + pub fn set_value(&mut self, value: &'v str) { + self.iter_mut().for_each(|e| e.set_value(value)); + } + + pub fn status(&self) -> Status { + match &*self.0 { + &[] => Status::InternalServerError, + &[ref error] => error.status(), + &[ref e1, ref errors@..] => errors.iter() + .map(|e| e.status()) + .max() + .unwrap_or_else(|| e1.status()), + } + } +} + +impl<'v> Error<'v> { + pub fn custom(error: E) -> Self + where E: std::error::Error + Send + 'static + { + (Box::new(error) as Box).into() + } + + pub fn validation>>(msg: S) -> Self { + ErrorKind::Validation(msg.into()).into() + } + + pub fn with_entity(mut self, entity: Entity) -> Self { + self.set_entity(entity); + self + } + + pub fn set_entity(&mut self, entity: Entity) { + self.entity = entity; + } + + pub fn with_name>>(mut self, name: N) -> Self { + self.set_name(name); + self + } + + pub fn set_name>>(&mut self, name: N) { + if self.name.is_none() { + self.name = Some(name.into()); + } + } + + pub fn with_value(mut self, value: &'v str) -> Self { + self.set_value(value); + self + } + + pub fn set_value(&mut self, value: &'v str) { + if self.value.is_none() { + self.value = Some(value.into()); + } + } + + pub fn is_for_exactly>(&self, name: N) -> bool { + self.name.as_ref() + .map(|n| name.as_ref() == n) + .unwrap_or(false) + } + + pub fn is_for>(&self, name: N) -> bool { + self.name.as_ref().map(|e_name| { + if e_name.is_empty() != name.as_ref().is_empty() { + return false; + } + + let mut e_keys = e_name.keys(); + let mut n_keys = name.as_ref().keys(); + loop { + match (e_keys.next(), n_keys.next()) { + (Some(e), Some(n)) if e == n => continue, + (Some(_), Some(_)) => return false, + (Some(_), None) => return false, + (None, _) => break, + } + } + + true + }) + .unwrap_or(false) + } + + pub fn status(&self) -> Status { + use ErrorKind::*; + use multer::Error::*; + + match self.kind { + InvalidLength { min: None, .. } + | Multipart(FieldSizeExceeded { .. }) + | Multipart(StreamSizeExceeded { .. }) + => Status::PayloadTooLarge, + Unknown => Status::InternalServerError, + Io(_) | _ if self.entity == Entity::Form => Status::BadRequest, + _ => Status::UnprocessableEntity + } + } +} + +impl<'v> ErrorKind<'v> { + pub fn default_entity(&self) -> Entity { + match self { + | ErrorKind::InvalidLength { .. } + | ErrorKind::InvalidChoice { .. } + | ErrorKind::OutOfRange {.. } + | ErrorKind::Validation {.. } + | ErrorKind::Utf8(_) + | ErrorKind::Int(_) + | ErrorKind::Float(_) + | ErrorKind::Bool(_) + | ErrorKind::Custom(_) + | ErrorKind::Addr(_) => Entity::Value, + + | ErrorKind::Duplicate + | ErrorKind::Missing + | ErrorKind::Unknown + | ErrorKind::Unexpected => Entity::Field, + + | ErrorKind::Multipart(_) + | ErrorKind::Io(_) => Entity::Form, + } + } +} + +impl fmt::Display for ErrorKind<'_> { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + ErrorKind::InvalidLength { min, max } => { + match (min, max) { + (None, None) => write!(f, "unexpected or incomplete")?, + (None, Some(k)) => write!(f, "length cannot exceed {}", k)?, + (Some(1), None) => write!(f, "value cannot be empty")?, + (Some(k), None) => write!(f, "length must be at least {}", k)?, + (Some(i), Some(j)) => write!(f, "length must be between {} and {}", i, j)?, + } + } + ErrorKind::InvalidChoice { choices } => { + match choices.as_ref() { + &[] => write!(f, "invalid choice")?, + &[ref choice] => write!(f, "expected {}", choice)?, + _ => { + write!(f, "expected one of ")?; + for (i, choice) in choices.iter().enumerate() { + if i != 0 { write!(f, ", ")?; } + write!(f, "`{}`", choice)?; + } + } + } + } + ErrorKind::OutOfRange { start, end } => { + match (start, end) { + (None, None) => write!(f, "out of range")?, + (None, Some(k)) => write!(f, "value cannot exceed {}", k)?, + (Some(k), None) => write!(f, "value must be at least {}", k)?, + (Some(i), Some(j)) => write!(f, "value must be between {} and {}", i, j)?, + } + } + ErrorKind::Validation(msg) => msg.fmt(f)?, + ErrorKind::Duplicate => "duplicate".fmt(f)?, + ErrorKind::Missing => "missing".fmt(f)?, + ErrorKind::Unexpected => "unexpected".fmt(f)?, + ErrorKind::Unknown => "unknown internal error".fmt(f)?, + ErrorKind::Custom(e) => e.fmt(f)?, + ErrorKind::Multipart(e) => write!(f, "invalid multipart: {}", e)?, + ErrorKind::Utf8(e) => write!(f, "invalid UTF-8: {}", e)?, + ErrorKind::Int(e) => write!(f, "invalid integer: {}", e)?, + ErrorKind::Bool(e) => write!(f, "invalid boolean: {}", e)?, + ErrorKind::Float(e) => write!(f, "invalid float: {}", e)?, + ErrorKind::Addr(e) => write!(f, "invalid address: {}", e)?, + ErrorKind::Io(e) => write!(f, "i/o error: {}", e)?, + } + + Ok(()) + } +} + +impl fmt::Display for Error<'_> { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + self.kind.fmt(f) + } +} + +impl fmt::Display for Entity { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let string = match self { + Entity::Form => "form", + Entity::Field => "field", + Entity::ValueField => "value field", + Entity::DataField => "data field", + Entity::Name => "name", + Entity::Value => "value", + Entity::Key => "key", + Entity::Indices => "indices", + Entity::Index(k) => return write!(f, "index {}", k), + }; + + string.fmt(f) + } +} + +impl fmt::Display for Errors<'_> { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{} errors:", self.len())?; + for error in self.iter() { + write!(f, "\n{}", error)?; + } + + Ok(()) + } +} + +impl<'a, 'b> PartialEq> for ErrorKind<'a> { + fn eq(&self, other: &ErrorKind<'b>) -> bool { + use ErrorKind::*; + match (self, other) { + (InvalidLength { min: a, max: b }, InvalidLength { min, max }) => min == a && max == b, + (InvalidChoice { choices: a }, InvalidChoice { choices }) => choices == a, + (OutOfRange { start: a, end: b }, OutOfRange { start, end }) => start == a && end == b, + (Validation(a), Validation(b)) => a == b, + (Duplicate, Duplicate) => true, + (Missing, Missing) => true, + (Unexpected, Unexpected) => true, + (Custom(_), Custom(_)) => true, + (Multipart(a), Multipart(b)) => a == b, + (Utf8(a), Utf8(b)) => a == b, + (Int(a), Int(b)) => a == b, + (Bool(a), Bool(b)) => a == b, + (Float(a), Float(b)) => a == b, + (Addr(a), Addr(b)) => a == b, + (Io(a), Io(b)) => a.kind() == b.kind(), + _ => false, + } + } +} + +impl<'v> std::ops::Deref for Errors<'v> { + type Target = Vec>; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl<'v> std::ops::DerefMut for Errors<'v> { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.0 + } +} + +impl<'v, T: Into>> From for Errors<'v> { + #[inline(always)] + fn from(e: T) -> Self { + Errors(vec![e.into()]) + } +} + +impl<'v> From>> for Errors<'v> { + #[inline(always)] + fn from(v: Vec>) -> Self { + Errors(v) + } +} + +impl<'v, T: Into>> From for Error<'v> { + #[inline(always)] + fn from(k: T) -> Self { + let kind = k.into(); + let entity = kind.default_entity(); + Error { name: None, value: None, kind, entity } + } +} + +impl<'v> IntoIterator for Errors<'v> { + type Item = Error<'v>; + + type IntoIter = > as IntoIterator>::IntoIter; + + fn into_iter(self) -> Self::IntoIter { + self.0.into_iter() + } +} + +impl<'v> std::ops::Deref for Error<'v> { + type Target = ErrorKind<'v>; + + fn deref(&self) -> &Self::Target { + &self.kind + } +} + +impl From<(Option, Option)> for ErrorKind<'_> { + fn from((min, max): (Option, Option)) -> Self { + ErrorKind::InvalidLength { min, max } + } +} + +impl<'a, 'v: 'a> From<&'static [Cow<'v, str>]> for ErrorKind<'a> { + fn from(choices: &'static [Cow<'v, str>]) -> Self { + ErrorKind::InvalidChoice { choices: choices.into() } + } +} + +impl<'a, 'v: 'a> From>> for ErrorKind<'a> { + fn from(choices: Vec>) -> Self { + ErrorKind::InvalidChoice { choices: choices.into() } + } +} + +impl From<(Option, Option)> for ErrorKind<'_> { + fn from((start, end): (Option, Option)) -> Self { + ErrorKind::OutOfRange { start, end } + } +} + +impl From<(Option, Option)> for ErrorKind<'_> { + fn from((start, end): (Option, Option)) -> Self { + use std::convert::TryFrom; + + let as_isize = |b: ByteUnit| isize::try_from(b.as_u64()).ok(); + ErrorKind::from((start.and_then(as_isize), end.and_then(as_isize))) + } +} + +macro_rules! impl_from_choices { + ($($size:literal),*) => ($( + impl<'a, 'v: 'a> From<&'static [Cow<'v, str>; $size]> for ErrorKind<'a> { + fn from(choices: &'static [Cow<'v, str>; $size]) -> Self { + let choices = &choices[..]; + ErrorKind::InvalidChoice { choices: choices.into() } + } + } + )*) +} + +impl_from_choices!(1, 2, 3, 4, 5, 6, 7, 8); + +macro_rules! impl_from_for { + (<$l:lifetime> $T:ty => $V:ty as $variant:ident) => ( + impl<$l> From<$T> for $V { + fn from(value: $T) -> Self { + <$V>::$variant(value) + } + } + ) +} + +impl<'a> From for Error<'a> { + fn from(error: multer::Error) -> Self { + use multer::Error::*; + use self::ErrorKind::*; + + let incomplete = Error::from(InvalidLength { min: None, max: None }); + match error { + UnknownField { field_name: Some(name) } => Error::from(Unexpected).with_name(name), + UnknownField { field_name: None } => Error::from(Unexpected), + FieldSizeExceeded { limit, field_name } => { + let e = Error::from((None, Some(limit))); + match field_name { + Some(name) => e.with_name(name), + None => e + } + }, + StreamSizeExceeded { limit } => { + Error::from((None, Some(limit))).with_entity(Entity::Form) + } + IncompleteFieldData { field_name: Some(name) } => incomplete.with_name(name), + IncompleteFieldData { field_name: None } => incomplete, + IncompleteStream | IncompleteHeaders => incomplete.with_entity(Entity::Form), + e => Error::from(ErrorKind::Multipart(e)) + } + } +} + +impl_from_for!(<'a> Utf8Error => ErrorKind<'a> as Utf8); +impl_from_for!(<'a> ParseIntError => ErrorKind<'a> as Int); +impl_from_for!(<'a> ParseFloatError => ErrorKind<'a> as Float); +impl_from_for!(<'a> ParseBoolError => ErrorKind<'a> as Bool); +impl_from_for!(<'a> AddrParseError => ErrorKind<'a> as Addr); +impl_from_for!(<'a> io::Error => ErrorKind<'a> as Io); +impl_from_for!(<'a> Box => ErrorKind<'a> as Custom); diff --git a/core/lib/src/form/field.rs b/core/lib/src/form/field.rs new file mode 100644 index 0000000000..d08ea53563 --- /dev/null +++ b/core/lib/src/form/field.rs @@ -0,0 +1,75 @@ +use crate::form::name::NameView; +use crate::form::error::{Error, ErrorKind, Entity}; +use crate::http::{ContentType, RawStr}; +use crate::{Request, Data}; + +#[derive(Debug, Clone)] +pub struct ValueField<'r> { + pub name: NameView<'r>, + pub value: &'r str, +} + +pub struct DataField<'r, 'i> { + pub name: NameView<'r>, + pub file_name: Option<&'r str>, + pub content_type: ContentType, + pub request: &'r Request<'i>, + pub data: Data, +} + +impl<'v> ValueField<'v> { + /// `raw` must already be URL-decoded. This is weird. + pub fn parse(field: &'v str) -> Self { + // WHATWG URL Living Standard 5.1 steps 3.2, 3.3. + let (name, val) = RawStr::new(field).split_at_byte(b'='); + ValueField::from((name.as_str(), val.as_str())) + } + + pub fn from_value(value: &'v str) -> Self { + ValueField::from(("", value)) + } + + pub fn shift(mut self) -> Self { + self.name.shift(); + self + } + + pub fn unexpected(&self) -> Error<'v> { + Error::from(ErrorKind::Unexpected) + .with_name(NameView::new(self.name.source())) + .with_value(self.value) + .with_entity(Entity::ValueField) + } + + pub fn missing(&self) -> Error<'v> { + Error::from(ErrorKind::Missing) + .with_name(NameView::new(self.name.source())) + .with_value(self.value) + .with_entity(Entity::ValueField) + } +} + +impl<'a> From<(&'a str, &'a str)> for ValueField<'a> { + fn from((name, value): (&'a str, &'a str)) -> Self { + ValueField { name: NameView::new(name), value } + } +} + +impl<'a, 'b> PartialEq> for ValueField<'a> { + fn eq(&self, other: &ValueField<'b>) -> bool { + self.name == other.name && self.value == other.value + } +} + +impl<'v> DataField<'v, '_> { + pub fn shift(mut self) -> Self { + self.name.shift(); + self + } + + pub fn unexpected(&self) -> Error<'v> { + Error::from(ErrorKind::Unexpected) + .with_name(self.name) + .with_entity(Entity::DataField) + } +} diff --git a/core/lib/src/form/form.rs b/core/lib/src/form/form.rs new file mode 100644 index 0000000000..6d80ba8ade --- /dev/null +++ b/core/lib/src/form/form.rs @@ -0,0 +1,184 @@ +use std::ops::{Deref, DerefMut}; + +use crate::request::Request; +use crate::data::{Data, FromData, Outcome}; +use crate::http::{RawStr, ext::IntoOwned}; +use crate::form::parser::{Parser, RawStrParser, Buffer}; +use crate::form::prelude::*; + +/// A data guard for [`FromForm`] types. +/// +/// This type implements the [`FromData`] trait. It provides a generic means to +/// parse arbitrary structures from incoming form data of any kind. +/// +/// See the [forms guide](https://rocket.rs/master/guide/requests#forms) for +/// general form support documentation. +/// +/// # Leniency +/// +/// A `Form` will parse successfully from an incoming form if the form +/// contains a superset of the fields in `T`. Said another way, a `Form` +/// automatically discards extra fields without error. For instance, if an +/// incoming form contains the fields "a", "b", and "c" while `T` only contains +/// "a" and "c", the form _will_ parse as `Form`. To parse strictly, use the +/// [`Strict`](crate::form::Strict) form guard. +/// +/// # Usage +/// +/// This type can be used with any type that implements the `FromForm` trait. +/// The trait can be automatically derived; see the [`FromForm`] documentation +/// for more information on deriving or implementing the trait. +/// +/// Because `Form` implements `FromData`, it can be used directly as a target of +/// the `data = ""` route parameter as long as its generic type +/// implements the `FromForm` trait: +/// +/// ```rust +/// # #[macro_use] extern crate rocket; +/// use rocket::form::Form; +/// use rocket::http::RawStr; +/// +/// #[derive(FromForm)] +/// struct UserInput<'r> { +/// value: &'r str +/// } +/// +/// #[post("/submit", data = "")] +/// fn submit_task(user_input: Form>) -> String { +/// format!("Your value: {}", user_input.value) +/// } +/// ``` +/// +/// A type of `Form` automatically dereferences into an `&T` or `&mut T`, +/// though you can also transform a `Form` into a `T` by calling +/// [`into_inner()`](Form::into_inner()). Thanks to automatic dereferencing, you +/// can access fields of `T` transparently through a `Form`, as seen above +/// with `user_input.value`. +/// +/// ## Data Limits +/// +/// The default size limit for incoming form data is 32KiB. Setting a limit +/// protects your application from denial of service (DOS) attacks and from +/// resource exhaustion through high memory consumption. The limit can be +/// modified by setting the `limits.form` configuration parameter. For instance, +/// to increase the forms limit to 512KiB for all environments, you may add the +/// following to your `Rocket.toml`: +/// +/// ```toml +/// [global.limits] +/// form = 524288 +/// ``` +/// +/// See the [`Limits`](crate::data::Limits) docs for more. +#[derive(Debug)] +pub struct Form(T); + +impl Form { + /// Consumes `self` and returns the inner value. + /// + /// Note that since `Form` implements [`Deref`] and [`DerefMut`] with + /// target `T`, reading and writing an inner value can be accomplished + /// transparently. + /// + /// # Example + /// + /// ```rust + /// # #[macro_use] extern crate rocket; + /// use rocket::form::Form; + /// + /// #[derive(FromForm)] + /// struct MyForm { + /// field: String, + /// } + /// + /// #[post("/submit", data = "")] + /// fn submit(form: Form) -> String { + /// // We can read or mutate a value transparently: + /// let field: &str = &form.field; + /// + /// // To gain ownership, however, use `into_inner()`: + /// form.into_inner().field + /// } + /// ``` + pub fn into_inner(self) -> T { + self.0 + } +} + +impl From for Form { + #[inline] + fn from(val: T) -> Form { + Form(val) + } +} + +impl Form<()> { + /// `string` must represent a decoded string. + pub fn values(string: &str) -> impl Iterator> { + // WHATWG URL Living Standard 5.1 steps 1, 2, 3.1 - 3.3. + string.split('&') + .filter(|s| !s.is_empty()) + .map(ValueField::parse) + } +} + +impl<'r, T: FromForm<'r>> Form { + /// `string` must represent a decoded string. + pub fn parse(string: &'r str) -> Result<'r, T> { + // WHATWG URL Living Standard 5.1 steps 1, 2, 3.1 - 3.3. + let mut ctxt = T::init(Options::Lenient); + Form::values(string).for_each(|f| T::push_value(&mut ctxt, f)); + T::finalize(ctxt) + } +} + +impl FromForm<'a> + 'static> Form { + /// `string` must represent an undecoded string. + pub fn parse_encoded(string: &RawStr) -> Result<'static, T> { + let buffer = Buffer::new(); + let mut ctxt = T::init(Options::Lenient); + for field in RawStrParser::new(&buffer, string) { + T::push_value(&mut ctxt, field) + } + + T::finalize(ctxt).map_err(|e| e.into_owned()) + } +} + +impl Deref for Form { + type Target = T; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl DerefMut for Form { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.0 + } +} + +#[crate::async_trait] +impl<'r, T: FromForm<'r>> FromData<'r> for Form { + type Error = Errors<'r>; + + async fn from_data(req: &'r Request<'_>, data: Data) -> Outcome { + use either::Either; + + let mut parser = try_outcome!(Parser::new(req, data).await); + let mut context = T::init(Options::Lenient); + while let Some(field) = parser.next().await { + match field { + Ok(Either::Left(value)) => T::push_value(&mut context, value), + Ok(Either::Right(data)) => T::push_data(&mut context, data).await, + Err(e) => T::push_error(&mut context, e), + } + } + + match T::finalize(context) { + Ok(value) => Outcome::Success(Form(value)), + Err(e) => Outcome::Failure((e.status(), e)), + } + } +} diff --git a/core/lib/src/form/from_form.rs b/core/lib/src/form/from_form.rs new file mode 100644 index 0000000000..d68c797f71 --- /dev/null +++ b/core/lib/src/form/from_form.rs @@ -0,0 +1,743 @@ +use std::borrow::Cow; +use std::collections::{HashMap, BTreeMap}; +use std::hash::Hash; + +use either::Either; +use indexmap::IndexMap; + +use crate::form::prelude::*; +use crate::http::uncased::AsUncased; + +/// Trait for implementing form guards: types parseable from HTTP form fields. +/// +/// Only form guards that are _collections_, that is, collect more than one form +/// field while parsing, should implement `FromForm`. All other types should +/// implement [`FromFormField`] instead, which offers a simplified interface to +/// parsing a single form field. +/// +/// For a gentle introduction to forms in Rocket, see the [forms guide]. +/// +/// # Form Guards +/// +/// A form guard is a guard that operates on form fields, typically those with a +/// particular name prefix. Form guards validate and parse form field data via +/// implementations of `FromForm`. In other words, a type is a form guard _iff_ +/// it implements `FromFrom`. +/// +/// Form guards are used as the inner type of the [`Form`] data guard: +/// +/// ```rust +/// # use rocket::post; +/// use rocket::form::Form; +/// +/// # type FormGuard = String; +/// #[post("/submit", data = "")] +/// fn submit(var: Form) { /* ... */ } +/// ``` +/// +/// # Deriving +/// +/// This trait can, and largely _should_, be automatically derived. When +/// deriving `FromForm`, every field in the structure must implement +/// [`FromForm`]. Form fields with the struct field's name are [shifted] and +/// then pushed to the struct field's `FromForm` parser. +/// +/// ```rust +/// use rocket::form::FromForm; +/// +/// #[derive(FromForm)] +/// struct TodoTask<'r> { +/// #[field(validate = len(1..))] +/// description: &'r str, +/// #[field(name = "done")] +/// completed: bool +/// } +/// ``` +/// +/// For full details on deriving `FromForm`, see the [`FromForm` derive]. +/// +/// [`Form`]: crate::form::Form +/// [`FromForm`]: crate::form::FromForm +/// [`FromForm` derive]: ../derive.FromForm.html +/// [FromFormField]: crate::form::FromFormField +/// [`shift()`ed]: NameView::shift() +/// [`key()`]: NameView::key() +/// [forms guide]: https://rocket.rs/master/guide/requests/#forms +/// +/// # Provided Implementations +/// +/// Rocket implements `FromForm` for several types. Their behavior is documented +/// here. +/// +/// * **`T` where `T: FromFormField`** +/// +/// This includes types like `&str`, `usize`, and [`Date`](time::Date). See +/// [`FromFormField`] for details. +/// +/// * **`Vec` where `T: FromForm`** +/// +/// Parses a sequence of `T`'s. A new `T` is created whenever the field +/// name's key changes or is empty; the previous `T` is finalized and errors +/// are stored. While the key remains the same and non-empty, form values +/// are pushed to the current `T` after being shifted. All collected errors +/// are returned at finalization, if any, or the successfully created vector +/// is returned. +/// +/// * **`HashMap` where `K: FromForm + Eq + Hash`, `V: FromForm`** +/// +/// **`BTreeMap` where `K: FromForm + Ord`, `V: FromForm`** +/// +/// Parses a sequence of `(K, V)`'s. A new pair is created for every unique +/// first index of the key. +/// +/// If the key has only one index (`map[index]=value`), the index itself is +/// pushed to `K`'s parser and the remaining shifted field is pushed to +/// `V`'s parser. +/// +/// If the key has two indices (`map[index:k]=value` or +/// `map[index:v]=value`), the second index must start with `k` or `v`. If +/// the second index starts with `k`, the shifted field is pushed to `K`'s +/// parser. If the second index starts with `v`, the shifted field is pushed +/// to `V`'s parser. If the second index is anything else, an error is +/// created for the offending form field. +/// +/// Errors are collected as they occur. Finalization finalizes all pairs and +/// returns errors, if any, or the map. +/// +/// * **`Option` where `T: FromForm`** +/// +/// _This form guard always succeeds._ +/// +/// Forwards all pushes to `T` without shifting. Finalizes successfully as +/// `Some(T)` if `T` finalizes without error or `None` otherwise. +/// +/// * **`Result>` where `T: FromForm`** +/// +/// _This form guard always succeeds._ +/// +/// Forwards all pushes to `T` without shifting. Finalizes successfully as +/// `Some(T)` if `T` finalizes without error or `Err(Errors)` with the +/// errors from `T` otherwise. +/// +/// # Push Parsing +/// +/// `FromForm` describes a 3-stage push-based interface to form parsing. After +/// preprocessing (see [the top-level docs](crate::form#parsing)), the three +/// stages are: +/// +/// 1. **Initialization.** The type sets up a context for later `push`es. +/// +/// ```rust +/// # use rocket::form::prelude::*; +/// # struct Foo; +/// use rocket::form::Options; +/// +/// # #[rocket::async_trait] +/// # impl<'r> FromForm<'r> for Foo { +/// # type Context = std::convert::Infallible; +/// fn init(opts: Options) -> Self::Context { +/// todo!("return a context for storing parse state") +/// } +/// # fn push_value(ctxt: &mut Self::Context, field: ValueField<'r>) { todo!() } +/// # async fn push_data(ctxt: &mut Self::Context, field: DataField<'r, '_>) { todo!() } +/// # fn finalize(ctxt: Self::Context) -> Result<'r, Self> { todo!() } +/// # } +/// ``` +/// +/// 2. **Push.** The structure is repeatedly pushed form fields; the latest +/// context is provided with each `push`. If the structure contains +/// children, it uses the first [`key()`] to identify a child to which it +/// then `push`es the remaining `field` to, likely with a [`shift()`ed] +/// name. Otherwise, the structure parses the `value` itself. The context +/// is updated as needed. +/// +/// ```rust +/// # use rocket::form::prelude::*; +/// # struct Foo; +/// use rocket::form::{ValueField, DataField}; +/// +/// # #[rocket::async_trait] +/// # impl<'r> FromForm<'r> for Foo { +/// # type Context = std::convert::Infallible; +/// # fn init(opts: Options) -> Self::Context { todo!() } +/// fn push_value(ctxt: &mut Self::Context, field: ValueField<'r>) { +/// todo!("modify context as necessary for `field`") +/// } +/// +/// async fn push_data(ctxt: &mut Self::Context, field: DataField<'r, '_>) { +/// todo!("modify context as necessary for `field`") +/// } +/// # fn finalize(ctxt: Self::Context) -> Result<'r, Self> { todo!() } +/// # } +/// ``` +/// +/// 3. **Finalization.** The structure is informed that there are no further +/// fields. It systemizes the effects of previous `push`es via its context +/// to return a parsed structure or generate [`Errors`]. +/// +/// ```rust +/// # use rocket::form::prelude::*; +/// # struct Foo; +/// use rocket::form::Result; +/// +/// # #[rocket::async_trait] +/// # impl<'r> FromForm<'r> for Foo { +/// # type Context = std::convert::Infallible; +/// # fn init(opts: Options) -> Self::Context { todo!() } +/// # fn push_value(ctxt: &mut Self::Context, field: ValueField<'r>) { todo!() } +/// # async fn push_data(ctxt: &mut Self::Context, field: DataField<'r, '_>) { todo!() } +/// fn finalize(ctxt: Self::Context) -> Result<'r, Self> { +/// todo!("inspect context to generate `Self` or `Errors`") +/// } +/// # } +/// ``` +/// +/// These three stages make up the entirety of the `FromForm` trait. +/// +/// ## Nesting and [`NameView`] +/// +/// Each field name key typically identifies a unique child of a structure. As +/// such, when processed left-to-right, the keys of a field jointly identify a +/// unique leaf of a structure. The value of the field typically represents the +/// desired value of the leaf. +/// +/// A [`NameView`] captures and simplifies this "left-to-right" processing of a +/// field's name by exposing a sliding-prefix view into a name. A [`shift()`] +/// shifts the view one key to the right. Thus, a `Name` of `a.b.c` when viewed +/// through a new [`NameView`] is `a`. Shifted once, the view is `a.b`. +/// [`key()`] returns the last (or "current") key in the view. A nested +/// structure can thus handle a field with a `NameView`, operate on the +/// [`key()`], [`shift()`] the `NameView`, and pass the field with the shifted +/// `NameView` to the next processor which handles `b` and so on. +/// +/// [`shift()`]: NameView::shift() +/// [`key()`]: NameView::key() +/// +/// # Implementing +/// +/// Implementing `FromForm` should be a rare occurrence. Prefer instead to use +/// Rocket's built-in derivation or, for custom types, implementing +/// [`FromFormField`]. +/// +/// An implementation of `FromForm` consists of implementing the three stages +/// outlined above. `FromForm` is an async trait, so implementations must be +/// decorated with an attribute of `#[rocket::async_trait]`: +/// +/// ```rust +/// # #[macro_use] extern crate rocket; +/// # struct MyType; +/// # struct MyContext; +/// use rocket::form::{self, FromForm, DataField, ValueField}; +/// +/// #[rocket::async_trait] +/// impl<'r> FromForm<'r> for MyType { +/// type Context = MyContext; +/// +/// fn init(opts: form::Options) -> Self::Context { +/// todo!() +/// } +/// +/// fn push_value(ctxt: &mut Self::Context, field: ValueField<'r>) { +/// todo!() +/// } +/// +/// async fn push_data(ctxt: &mut Self::Context, field: DataField<'r, '_>) { +/// todo!() +/// } +/// +/// fn finalize(this: Self::Context) -> form::Result<'r, Self> { +/// todo!() +/// } +/// } +/// ``` +/// +/// ## Lifetime +/// +/// The lifetime `'r` correponds to the lifetime of the request. +/// +/// ## Example +/// +/// We illustrate implementation of `FromForm` through an example. The example +/// implements `FromForm` for a `Pair(A, B)` type where `A: FromForm` and `B: +/// FromForm`, parseable from forms with at least two fields, one with a key of +/// `0` and the other with a key of `1`. The field with key `0` is parsed as an +/// `A` while the field with key `1` is parsed as a `B`. Specifically, to parse +/// a `Pair(A, B)` from a field with prefix `pair`, a form with the following +/// fields must be submitted: +/// +/// * `pair[0]` - type A +/// * `pair[1]` - type B +/// +/// Examples include: +/// +/// * `pair[0]=id&pair[1]=100` as `Pair(&str, usize)` +/// * `pair[0]=id&pair[1]=100` as `Pair(&str, &str)` +/// * `pair[0]=2012-10-12&pair[1]=100` as `Pair(time::Date, &str)` +/// * `pair.0=2012-10-12&pair.1=100` as `Pair(time::Date, usize)` +/// +/// ```rust +/// use rocket::form::{self, FromForm, ValueField, DataField, Error, Errors}; +/// use either::Either; +/// +/// /// A form guard parseable from fields `.0` and `.1`. +/// struct Pair(A, B); +/// +/// // The parsing context. We'll be pushing fields with key `.0` to `left` +/// // and fields with `.1` to `right`. We'll collect errors along the way. +/// struct PairContext<'v, A: FromForm<'v>, B: FromForm<'v>> { +/// left: A::Context, +/// right: B::Context, +/// errors: Errors<'v>, +/// } +/// +/// #[rocket::async_trait] +/// impl<'v, A: FromForm<'v>, B: FromForm<'v>> FromForm<'v> for Pair { +/// type Context = PairContext<'v, A, B>; +/// +/// // We initialize the `PairContext` as expected. +/// fn init(opts: form::Options) -> Self::Context { +/// PairContext { +/// left: A::init(opts), +/// right: B::init(opts), +/// errors: Errors::new() +/// } +/// } +/// +/// // For each value, we determine if the key is `.0` (left) or `.1` +/// // (right) and push to the appropriate parser. If it was neither, we +/// // store the error for emission on finalization. The parsers for `A` and +/// // `B` will handle duplicate values and so on. +/// fn push_value(ctxt: &mut Self::Context, field: ValueField<'v>) { +/// match ctxt.context(field.name) { +/// Ok(Either::Left(ctxt)) => A::push_value(ctxt, field.shift()), +/// Ok(Either::Right(ctxt)) => B::push_value(ctxt, field.shift()), +/// Err(e) => ctxt.errors.push(e), +/// } +/// } +/// +/// // This is identical to `push_value` but for data fields. +/// async fn push_data(ctxt: &mut Self::Context, field: DataField<'v, '_>) { +/// match ctxt.context(field.name) { +/// Ok(Either::Left(ctxt)) => A::push_data(ctxt, field.shift()).await, +/// Ok(Either::Right(ctxt)) => B::push_data(ctxt, field.shift()).await, +/// Err(e) => ctxt.errors.push(e), +/// } +/// } +/// +/// // Finally, we finalize `A` and `B`. If both returned `Ok` and we +/// // encountered no errors during the push phase, we return our pair. If +/// // there were errors, we return them. If `A` and/or `B` failed, we +/// // return the commulative errors. +/// fn finalize(mut ctxt: Self::Context) -> form::Result<'v, Self> { +/// match (A::finalize(ctxt.left), B::finalize(ctxt.right)) { +/// (Ok(l), Ok(r)) if ctxt.errors.is_empty() => Ok(Pair(l, r)), +/// (Ok(_), Ok(_)) => Err(ctxt.errors), +/// (left, right) => { +/// if let Err(e) = left { ctxt.errors.extend(e); } +/// if let Err(e) = right { ctxt.errors.extend(e); } +/// Err(ctxt.errors) +/// } +/// } +/// } +/// } +/// +/// impl<'v, A: FromForm<'v>, B: FromForm<'v>> PairContext<'v, A, B> { +/// // Helper method used by `push_{value, data}`. Determines which context +/// // we should push to based on the field name's key. If the key is +/// // neither `0` nor `1`, we return an error. +/// fn context( +/// &mut self, +/// name: form::name::NameView<'v> +/// ) -> Result, Error<'v>> { +/// use std::borrow::Cow; +/// +/// match name.key().map(|k| k.as_str()) { +/// Some("0") => Ok(Either::Left(&mut self.left)), +/// Some("1") => Ok(Either::Right(&mut self.right)), +/// _ => Err(Error::from(&[Cow::Borrowed("0"), Cow::Borrowed("1")]) +/// .with_entity(form::error::Entity::Index(0)) +/// .with_name(name)), +/// } +/// } +/// } +/// ``` +#[crate::async_trait] +pub trait FromForm<'r>: Send + Sized { + /// The form guard's parsing context. + type Context: Send; + + /// Initializes and returns the parsing context for `Self`. + fn init(opts: Options) -> Self::Context; + + /// Processes the value field `field`. + fn push_value(ctxt: &mut Self::Context, field: ValueField<'r>); + + /// Processes the data field `field`. + async fn push_data(ctxt: &mut Self::Context, field: DataField<'r, '_>); + + /// Processes the extern form or field error `_error`. + /// + /// The default implementation does nothing, which is always correct. + fn push_error(_ctxt: &mut Self::Context, _error: Error<'r>) { } + + /// Finalizes parsing. Returns the parsed value when successful or + /// collection of [`Errors`] otherwise. + fn finalize(ctxt: Self::Context) -> Result<'r, Self>; + + /// Returns a default value, if any, to use when a value is desired and + /// parsing fails. + /// + /// The default implementation initializes `Self` with lenient options and + /// finalizes immediately, returning the value if finalization succeeds. + fn default() -> Option { + Self::finalize(Self::init(Options::Lenient)).ok() + } +} + +#[doc(hidden)] +pub struct VecContext<'v, T: FromForm<'v>> { + opts: Options, + last_key: Option<&'v Key>, + current: Option, + errors: Errors<'v>, + items: Vec +} + +impl<'v, T: FromForm<'v>> VecContext<'v, T> { + fn shift(&mut self) { + if let Some(current) = self.current.take() { + match T::finalize(current) { + Ok(v) => self.items.push(v), + Err(e) => self.errors.extend(e) + } + } + } + + fn context(&mut self, name: &NameView<'v>) -> &mut T::Context { + let this_key = name.key(); + let keys_match = match (self.last_key, this_key) { + (Some(k1), Some(k2)) if k1 == k2 => true, + _ => false + }; + + if !keys_match { + self.shift(); + self.current = Some(T::init(self.opts)); + } + + self.last_key = name.key(); + self.current.as_mut().expect("must have current if last == index") + } +} + +#[crate::async_trait] +impl<'v, T: FromForm<'v> + 'v> FromForm<'v> for Vec { + type Context = VecContext<'v, T>; + + fn init(opts: Options) -> Self::Context { + VecContext { + opts, + last_key: None, + current: None, + items: vec![], + errors: Errors::new(), + } + } + + fn push_value(this: &mut Self::Context, field: ValueField<'v>) { + T::push_value(this.context(&field.name), field.shift()); + } + + async fn push_data(ctxt: &mut Self::Context, field: DataField<'v, '_>) { + T::push_data(ctxt.context(&field.name), field.shift()).await + } + + fn finalize(mut this: Self::Context) -> Result<'v, Self> { + this.shift(); + match this.errors.is_empty() { + true => Ok(this.items), + false => Err(this.errors)?, + } + } +} + +#[doc(hidden)] +pub struct MapContext<'v, K, V> where K: FromForm<'v>, V: FromForm<'v> { + opts: Options, + /// Maps from the string key to the index in `map`. + key_map: IndexMap<&'v str, (usize, NameView<'v>)>, + keys: Vec, + values: Vec, + errors: Errors<'v>, +} + +impl<'v, K, V> MapContext<'v, K, V> + where K: FromForm<'v>, V: FromForm<'v> +{ + fn new(opts: Options) -> Self { + MapContext { + opts, + key_map: IndexMap::new(), + keys: vec![], + values: vec![], + errors: Errors::new(), + } + } + + fn ctxt(&mut self, key: &'v str, name: NameView<'v>) -> (&mut K::Context, &mut V::Context) { + match self.key_map.get(key) { + Some(&(i, _)) => (&mut self.keys[i], &mut self.values[i]), + None => { + debug_assert_eq!(self.keys.len(), self.values.len()); + let map_index = self.keys.len(); + self.keys.push(K::init(self.opts)); + self.values.push(V::init(self.opts)); + self.key_map.insert(key, (map_index, name)); + (self.keys.last_mut().unwrap(), self.values.last_mut().unwrap()) + } + } + } + + fn push( + &mut self, + name: NameView<'v> + ) -> Option> { + let index_pair = name.key() + .map(|k| k.indices()) + .map(|mut i| (i.next(), i.next())) + .unwrap_or_default(); + + match index_pair { + (Some(key), None) => { + let is_new_key = !self.key_map.contains_key(key); + let (key_ctxt, val_ctxt) = self.ctxt(key, name); + if is_new_key { + K::push_value(key_ctxt, ValueField::from_value(key)); + } + + return Some(Either::Right(val_ctxt)); + }, + (Some(kind), Some(key)) => { + if kind.as_uncased().starts_with("k") { + return Some(Either::Left(self.ctxt(key, name).0)); + } else if kind.as_uncased().starts_with("v") { + return Some(Either::Right(self.ctxt(key, name).1)); + } else { + let error = Error::from(&[Cow::Borrowed("k"), Cow::Borrowed("v")]) + .with_entity(Entity::Index(0)) + .with_name(name); + + self.errors.push(error); + } + } + _ => { + let error = Error::from(ErrorKind::Missing) + .with_entity(Entity::Indices) + .with_name(name); + + self.errors.push(error); + } + }; + + None + } + + fn push_value(&mut self, field: ValueField<'v>) { + match self.push(field.name) { + Some(Either::Left(ctxt)) => K::push_value(ctxt, field.shift()), + Some(Either::Right(ctxt)) => V::push_value(ctxt, field.shift()), + _ => {} + } + } + + async fn push_data(&mut self, field: DataField<'v, '_>) { + match self.push(field.name) { + Some(Either::Left(ctxt)) => K::push_data(ctxt, field.shift()).await, + Some(Either::Right(ctxt)) => V::push_data(ctxt, field.shift()).await, + _ => {} + } + } + + fn finalize>(self) -> Result<'v, T> { + let (keys, values, key_map) = (self.keys, self.values, self.key_map); + let errors = std::cell::RefCell::new(self.errors); + + let keys = keys.into_iter() + .zip(key_map.values().map(|(_, name)| name)) + .filter_map(|(ctxt, name)| match K::finalize(ctxt) { + Ok(value) => Some(value), + Err(e) => { errors.borrow_mut().extend(e.with_name(*name)); None } + }); + + let values = values.into_iter() + .zip(key_map.values().map(|(_, name)| name)) + .filter_map(|(ctxt, name)| match V::finalize(ctxt) { + Ok(value) => Some(value), + Err(e) => { errors.borrow_mut().extend(e.with_name(*name)); None } + }); + + let map: T = keys.zip(values).collect(); + let no_errors = errors.borrow().is_empty(); + match no_errors { + true => Ok(map), + false => Err(errors.into_inner()) + } + } +} + +#[crate::async_trait] +impl<'v, K, V> FromForm<'v> for HashMap + where K: FromForm<'v> + Eq + Hash, V: FromForm<'v> +{ + type Context = MapContext<'v, K, V>; + + fn init(opts: Options) -> Self::Context { + MapContext::new(opts) + } + + fn push_value(ctxt: &mut Self::Context, field: ValueField<'v>) { + ctxt.push_value(field); + } + + async fn push_data(ctxt: &mut Self::Context, field: DataField<'v, '_>) { + ctxt.push_data(field).await; + } + + fn finalize(this: Self::Context) -> Result<'v, Self> { + this.finalize() + } +} + +#[crate::async_trait] +impl<'v, K, V> FromForm<'v> for BTreeMap + where K: FromForm<'v> + Ord, V: FromForm<'v> +{ + type Context = MapContext<'v, K, V>; + + fn init(opts: Options) -> Self::Context { + MapContext::new(opts) + } + + fn push_value(ctxt: &mut Self::Context, field: ValueField<'v>) { + ctxt.push_value(field); + } + + async fn push_data(ctxt: &mut Self::Context, field: DataField<'v, '_>) { + ctxt.push_data(field).await; + } + + fn finalize(this: Self::Context) -> Result<'v, Self> { + this.finalize() + } +} + +#[crate::async_trait] +impl<'v, T: FromForm<'v>> FromForm<'v> for Option { + type Context = >::Context; + + fn init(opts: Options) -> Self::Context { + T::init(opts) + } + + fn push_value(ctxt: &mut Self::Context, field: ValueField<'v>) { + T::push_value(ctxt, field) + } + + async fn push_data(ctxt: &mut Self::Context, field: DataField<'v, '_>) { + T::push_data(ctxt, field).await + } + + fn finalize(this: Self::Context) -> Result<'v, Self> { + match T::finalize(this) { + Ok(v) => Ok(Some(v)), + Err(_) => Ok(None) + } + } +} + +#[crate::async_trait] +impl<'v, T: FromForm<'v>> FromForm<'v> for Result<'v, T> { + type Context = >::Context; + + fn init(opts: Options) -> Self::Context { + T::init(opts) + } + + fn push_value(ctxt: &mut Self::Context, field: ValueField<'v>) { + T::push_value(ctxt, field) + } + + async fn push_data(ctxt: &mut Self::Context, field: DataField<'v, '_>) { + T::push_data(ctxt, field).await + } + + fn finalize(this: Self::Context) -> Result<'v, Self> { + match T::finalize(this) { + Ok(v) => Ok(Ok(v)), + Err(e) => Ok(Err(e)) + } + } +} + +#[doc(hidden)] +pub struct PairContext<'v, A: FromForm<'v>, B: FromForm<'v>> { + left: A::Context, + right: B::Context, + errors: Errors<'v>, +} + +impl<'v, A: FromForm<'v>, B: FromForm<'v>> PairContext<'v, A, B> { + fn context( + &mut self, + name: NameView<'v> + ) -> std::result::Result, Error<'v>> { + match name.key().map(|k| k.as_str()) { + Some("0") => Ok(Either::Left(&mut self.left)), + Some("1") => Ok(Either::Right(&mut self.right)), + _ => Err(Error::from(&[Cow::Borrowed("0"), Cow::Borrowed("1")]) + .with_entity(Entity::Index(0)) + .with_name(name)), + } + } +} + +#[crate::async_trait] +impl<'v, A: FromForm<'v>, B: FromForm<'v>> FromForm<'v> for (A, B) { + type Context = PairContext<'v, A, B>; + + fn init(opts: Options) -> Self::Context { + PairContext { + left: A::init(opts), + right: B::init(opts), + errors: Errors::new() + } + } + + fn push_value(ctxt: &mut Self::Context, field: ValueField<'v>) { + match ctxt.context(field.name) { + Ok(Either::Left(ctxt)) => A::push_value(ctxt, field.shift()), + Ok(Either::Right(ctxt)) => B::push_value(ctxt, field.shift()), + Err(e) => ctxt.errors.push(e), + } + } + + async fn push_data(ctxt: &mut Self::Context, field: DataField<'v, '_>) { + match ctxt.context(field.name) { + Ok(Either::Left(ctxt)) => A::push_data(ctxt, field.shift()).await, + Ok(Either::Right(ctxt)) => B::push_data(ctxt, field.shift()).await, + Err(e) => ctxt.errors.push(e), + } + } + + fn finalize(mut ctxt: Self::Context) -> Result<'v, Self> { + match (A::finalize(ctxt.left), B::finalize(ctxt.right)) { + (Ok(key), Ok(val)) if ctxt.errors.is_empty() => Ok((key, val)), + (Ok(_), Ok(_)) => Err(ctxt.errors)?, + (left, right) => { + if let Err(e) = left { ctxt.errors.extend(e); } + if let Err(e) = right { ctxt.errors.extend(e); } + Err(ctxt.errors)? + } + } + } +} diff --git a/core/lib/src/form/from_form_field.rs b/core/lib/src/form/from_form_field.rs new file mode 100644 index 0000000000..4df742d05a --- /dev/null +++ b/core/lib/src/form/from_form_field.rs @@ -0,0 +1,413 @@ +use std::net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddrV4, SocketAddrV6, SocketAddr}; +use std::num::{ + NonZeroIsize, NonZeroI8, NonZeroI16, NonZeroI32, NonZeroI64, NonZeroI128, + NonZeroUsize, NonZeroU8, NonZeroU16, NonZeroU32, NonZeroU64, NonZeroU128, +}; + +use time::{Date, Time, PrimitiveDateTime}; + +use crate::data::Capped; +use crate::http::uncased::AsUncased; +use crate::form::prelude::*; + +/// Implied form guard ([`FromForm`]) for parsing a single form field. +/// +/// Types that implement `FromFormField` automatically implement [`FromForm`] +/// via a blanket implementation. As such, all `FromFormField` types are form +/// guards and can appear as the type of values in derived `FromForm` struct +/// fields: +/// +/// ```rust +/// # use rocket::form::FromForm; +/// #[derive(FromForm)] +/// struct Person<'r> { +/// name: &'r str, +/// age: u16 +/// } +/// ``` +/// +/// # Deriving +/// +/// `FromFormField` can be derived for C-like enums, where the generated +/// implementation case-insensitively parses fields with values equal to the +/// name of the variant or the value in `field(value = "...")`. +/// +/// ```rust +/// # use rocket::form::FromFormField; +/// /// Fields with value `"simple"` parse as `Kind::Simple`. Fields with value +/// /// `"fancy"` parse as `Kind::SoFancy`. +/// #[derive(FromFormField)] +/// enum Kind { +/// Simple, +/// #[field(value = "fancy")] +/// SoFancy, +/// } +/// ``` +/// +/// # Provided Implementations +/// +/// Rocket implements `FromFormField` for many types. Their behavior is +/// documented here. +/// +/// * +/// * Numeric types: **`f32`, `f64`, `isize`, `i8`, `i16`, `i32`, `i64`, +/// `i128`, `usize`, `u8`, `u16`, `u32`, `u64`, `u128`** +/// * Address types: **`IpAddr`, `Ipv4Addr`, `Ipv6Addr`, `SocketAddrV4`, +/// `SocketAddrV6`, `SocketAddr`** +/// * Non-zero types: **`NonZeroI8`, `NonZeroI16`, `NonZeroI32`, +/// `NonZeroI64`, `NonZeroI128`, `NonZeroIsize`, `NonZeroU8`, +/// `NonZeroU16`, `NonZeroU32`, `NonZeroU64`, `NonZeroU128`, +/// `NonZeroUsize`** +/// +/// A value is validated successfully if the `from_str` method for the given +/// type returns successfully. Only accepts form _values_, not binary data. +/// +/// * **`bool`** +/// +/// A value is validated successfully as `true` if the the form value is one +/// of `"on"`, `"yes"`, or `"true"` and `false` if the value is one of +/// `"off"`, `"no"`, or `"false"`. Defaults to `false` otherwise. Only +/// accepts form _values_, not binary data. +/// +/// * **`&str`, `String`** +/// +/// The decoded form value or data is returned directly without +/// modification. +/// +/// * **[`TempFile`]** +/// +/// Streams the form field value or data to a temporary file. See +/// [`TempFile`] for details. +/// +/// * **[`Capped`], [`Capped`]** +/// +/// Streams the form value or data to the inner value, succeeding even if +/// the data exceeds the respective type limit by truncating the data. See +/// [`Capped`] for details. +/// +/// * **[`time::Date`]** +/// +/// Parses a date in the `%F` format, that is, `%Y-$m-%d` or `YYYY-MM-DD`. +/// This is the `"date"` HTML input type. Only accepts form _values_, not +/// binary data. +/// +/// * **[`time::PrimitiveDateTime`]** +/// +/// Parses a date in `%FT%R` or `%FT%T` format, that is, `YYYY-MM-DDTHH:MM` +/// or `YYYY-MM-DDTHH:MM:SS`. This is the `"datetime-local"` HTML input type +/// without support for the millisecond variant. Only accepts form _values_, +/// not binary data. +/// +/// * **[`time::Time`]** +/// +/// Parses a time in `%R` or `%T` format, that is, `HH:MM` or `HH:MM:SS`. +/// This is the `"time"` HTML input type without support for the millisecond +/// variant. Only accepts form _values_, not binary data. +/// +/// [`TempFile`]: crate::data::TempFile +/// +/// # Implementing +/// +/// Implementing `FromFormField` requires implementing one or both of +/// `from_value` or `from_data`, depending on whether the type can be parsed +/// from a value field (text) and/or streaming binary data. Typically, a value +/// can be parsed from either, either directly or by using request-local cache +/// as an intermediary, and parsing from both should be preferred when sensible. +/// +/// `FromFormField` is an async trait, so implementations must be decorated with +/// an attribute of `#[rocket::async_trait]`: +/// +/// ```rust +/// # #[macro_use] extern crate rocket; +/// # struct MyType; +/// use rocket::form::{self, FromFormField, DataField, ValueField}; +/// +/// #[rocket::async_trait] +/// impl<'r> FromFormField<'r> for MyType { +/// fn from_value(field: ValueField<'r>) -> form::Result<'r, Self> { +/// todo!("parse from a value or use default impl") +/// } +/// +/// async fn from_data(field: DataField<'r, '_>) -> form::Result<'r, Self> { +/// todo!("parse from a value or use default impl") +/// } +/// } +/// ``` +/// +/// ## Example +/// +/// The following example parses a custom `Person` type with the format +/// `$name:$data`, where `$name` is expected to be string and `data` is expected +/// to be any slice of bytes. +/// +/// ```rust +/// # use rocket::post; +/// use rocket::data::ToByteUnit; +/// use rocket::form::{self, FromFormField, DataField, ValueField}; +/// +/// use memchr::memchr; +/// +/// struct Person<'r> { +/// name: &'r str, +/// data: &'r [u8] +/// } +/// +/// #[rocket::async_trait] +/// impl<'r> FromFormField<'r> for Person<'r> { +/// fn from_value(field: ValueField<'r>) -> form::Result<'r, Self> { +/// match field.value.find(':') { +/// Some(i) => Ok(Person { +/// name: &field.value[..i], +/// data: field.value[(i + 1)..].as_bytes() +/// }), +/// None => Err(form::Error::validation("does not contain ':'"))? +/// } +/// } +/// +/// async fn from_data(field: DataField<'r, '_>) -> form::Result<'r, Self> { +/// // Retrieve the configured data limit or use `256KiB` as default. +/// let limit = field.request.limits() +/// .get("person") +/// .unwrap_or(256.kibibytes()); +/// +/// // Read the capped data stream, returning a limit error as needed. +/// let bytes = field.data.open(limit).into_bytes().await?; +/// if !bytes.is_complete() { +/// Err((None, Some(limit)))?; +/// } +/// +/// // Store the bytes in request-local cache and split at ':'. +/// let bytes = bytes.into_inner(); +/// let bytes = rocket::request::local_cache!(field.request, bytes); +/// let (raw_name, data) = match memchr(b':', bytes) { +/// Some(i) => (&bytes[..i], &bytes[(i + 1)..]), +/// None => Err(form::Error::validation("does not contain ':'"))? +/// }; +/// +/// // Try to parse the name as UTF-8 or return an error if it fails. +/// let name = std::str::from_utf8(raw_name)?; +/// Ok(Person { name, data }) +/// } +/// } +/// +/// use rocket::form::{Form, FromForm}; +/// +/// // The type can be used directly, if only one field is expected... +/// #[post("/person", data = "")] +/// fn person(person: Form>) { /* ... */ } +/// +/// // ...or as a named field in another form guard... +/// #[derive(FromForm)] +/// struct NewPerson<'r> { +/// person: Person<'r> +/// } +/// +/// #[post("/person", data = "")] +/// fn new_person(person: Form>) { /* ... */ } +/// ``` +// NOTE: Ideally, we would have two traits instead one with two fallible +// methods: `FromFormValue` and `FromFormData`. This would be especially nice +// for use with query values, where `FromFormData` would make no sense. +// +// However, blanket implementations of `FromForm` for these traits would result +// in duplicate implementations of `FromForm`; we need specialization to resolve +// this concern. Thus, for now, we keep this as one trait. +#[crate::async_trait] +pub trait FromFormField<'v>: Send + Sized { + fn from_value(field: ValueField<'v>) -> Result<'v, Self> { + Err(field.unexpected())? + } + + async fn from_data(field: DataField<'v, '_>) -> Result<'v, Self> { + Err(field.unexpected())? + } + + /// Returns a default value to be used when the form field does not exist or + /// parsing otherwise fails. + /// + /// If this returns `None`, the field is required. Otherwise, this should + /// return `Some(default_value)`. The default implementation returns `None`. + fn default() -> Option { None } +} + +#[doc(hidden)] +pub struct FromFieldContext<'v, T: FromFormField<'v>> { + field_name: Option>, + field_value: Option<&'v str>, + opts: Options, + value: Option>, + pushes: usize +} + +impl<'v, T: FromFormField<'v>> FromFieldContext<'v, T> { + fn can_push(&mut self) -> bool { + self.pushes += 1; + self.value.is_none() + } + + fn push(&mut self, name: NameView<'v>, result: Result<'v, T>) { + let is_unexpected = |e: &Errors<'_>| e.last().map_or(false, |e| { + if let ErrorKind::Unexpected = e.kind { true } else { false } + }); + + self.field_name = Some(name); + match result { + Err(e) if !self.opts.strict && is_unexpected(&e) => { /* ok */ }, + result => self.value = Some(result), + } + } +} + +#[crate::async_trait] +impl<'v, T: FromFormField<'v>> FromForm<'v> for T { + type Context = FromFieldContext<'v, T>; + + fn init(opts: Options) -> Self::Context { + FromFieldContext { + opts, + field_name: None, + field_value: None, + value: None, + pushes: 0, + } + } + + fn push_value(ctxt: &mut Self::Context, field: ValueField<'v>) { + if ctxt.can_push() { + ctxt.field_value = Some(field.value); + ctxt.push(field.name, Self::from_value(field)) + } + } + + async fn push_data(ctxt: &mut FromFieldContext<'v, T>, field: DataField<'v, '_>) { + if ctxt.can_push() { + ctxt.push(field.name, Self::from_data(field).await); + } + } + + fn finalize(ctxt: Self::Context) -> Result<'v, Self> { + let mut errors = match ctxt.value { + Some(Ok(val)) if !ctxt.opts.strict || ctxt.pushes <= 1 => return Ok(val), + Some(Err(e)) => e, + Some(Ok(_)) => Errors::from(ErrorKind::Duplicate), + None => match ::default() { + Some(default) => return Ok(default), + None => Errors::from(ErrorKind::Missing) + } + }; + + if let Some(name) = ctxt.field_name { + errors.set_name(name); + } + + if let Some(value) = ctxt.field_value { + errors.set_value(value); + } + + Err(errors) + } +} + +#[crate::async_trait] +impl<'v> FromFormField<'v> for Capped<&'v str> { + fn from_value(field: ValueField<'v>) -> Result<'v, Self> { + Ok(Capped::from(field.value)) + } + + async fn from_data(f: DataField<'v, '_>) -> Result<'v, Self> { + use crate::data::{Capped, Outcome, FromData}; + + match as FromData>::from_data(f.request, f.data).await { + Outcome::Success(p) => Ok(p), + Outcome::Failure((_, e)) => Err(e)?, + Outcome::Forward(..) => { + Err(Error::from(ErrorKind::Unexpected).with_entity(Entity::DataField))? + } + } + } +} + +impl_strict_from_form_field_from_capped!(&'v str); + +#[crate::async_trait] +impl<'v> FromFormField<'v> for Capped { + fn from_value(field: ValueField<'v>) -> Result<'v, Self> { + Ok(Capped::from(field.value.to_string())) + } + + async fn from_data(f: DataField<'v, '_>) -> Result<'v, Self> { + use crate::data::{Capped, Outcome, FromData}; + + match as FromData>::from_data(f.request, f.data).await { + Outcome::Success(p) => Ok(p), + Outcome::Failure((_, e)) => Err(e)?, + Outcome::Forward(..) => { + Err(Error::from(ErrorKind::Unexpected).with_entity(Entity::DataField))? + } + } + } +} + +impl_strict_from_form_field_from_capped!(String); + +impl<'v> FromFormField<'v> for bool { + fn default() -> Option { Some(false) } + + fn from_value(field: ValueField<'v>) -> Result<'v, Self> { + match field.value.as_uncased() { + v if v == "on" || v == "yes" || v == "true" => Ok(true), + v if v == "off" || v == "no" || v == "false" => Ok(false), + // force a `ParseBoolError` + _ => Ok("".parse()?), + } + } +} + +macro_rules! impl_with_parse { + ($($T:ident),+ $(,)?) => ($( + impl<'v> FromFormField<'v> for $T { + #[inline(always)] + fn from_value(field: ValueField<'v>) -> Result<'v, Self> { + Ok(field.value.parse()?) + } + } + )+) +} + +impl_with_parse!( + f32, f64, + isize, i8, i16, i32, i64, i128, + usize, u8, u16, u32, u64, u128, + NonZeroIsize, NonZeroI8, NonZeroI16, NonZeroI32, NonZeroI64, NonZeroI128, + NonZeroUsize, NonZeroU8, NonZeroU16, NonZeroU32, NonZeroU64, NonZeroU128, + Ipv4Addr, IpAddr, Ipv6Addr, SocketAddrV4, SocketAddrV6, SocketAddr +); + +impl<'v> FromFormField<'v> for Date { + fn from_value(field: ValueField<'v>) -> Result<'v, Self> { + let date = Self::parse(field.value, "%F") + .map_err(|e| Box::new(e) as Box)?; + + Ok(date) + } +} + +impl<'v> FromFormField<'v> for Time { + fn from_value(field: ValueField<'v>) -> Result<'v, Self> { + let time = Self::parse(field.value, "%T") + .or_else(|_| Self::parse(field.value, "%R")) + .map_err(|e| Box::new(e) as Box)?; + + Ok(time) + } +} + +impl<'v> FromFormField<'v> for PrimitiveDateTime { + fn from_value(field: ValueField<'v>) -> Result<'v, Self> { + let dt = Self::parse(field.value, "%FT%T") + .or_else(|_| Self::parse(field.value, "%FT%R")) + .map_err(|e| Box::new(e) as Box)?; + + Ok(dt) + } +} diff --git a/core/lib/src/form/mod.rs b/core/lib/src/form/mod.rs new file mode 100644 index 0000000000..3a33090b42 --- /dev/null +++ b/core/lib/src/form/mod.rs @@ -0,0 +1,474 @@ +//! Parsing and validation of HTTP forms and fields. +//! +//! # Field Wire Format +//! +//! Rocket's field wire format is a flexible, non-self-descriptive, text-based +//! encoding of arbitrarily nested structure keys and their corresponding +//! values. The general grammar is: +//! +//! ```ebnf +//! field := name ('=' value)? +//! +//! name := key* +//! +//! key := indices +//! | '[' indices ']' +//! | '.' indices +//! +//! indices := index (':' index)* +//! +//! index := STRING except ':' +//! +//! value := STRING +//! ``` +//! +//! Each field name consists of any number of `key`s and at most one `value`. +//! Keys are delimited by `[]` or `.`. A `key` consists of indices delimited by +//! `:`. +//! +//! The meaning of a key or index is type-dependent, hence the format is +//! non-self-descriptive. _Any_ structure can be described by this format. The +//! delimiters `.`, `[`, `:`, and `]` have no semantic meaning. +//! +//! Some examples of valid fields are: +//! +//! * `=` +//! * `key=value` +//! * `key[]=value` +//! * `.0=value` +//! * `[0]=value` +//! * `people[].name=Bob` +//! * `bob.cousin.names[]=Bob` +//! * `map[k:1]=Bob` +//! * `people[bob]nickname=Stan` +//! +//! # Parsing +//! +//! The [`FromForm`] trait describes a push-based parser for this wire format. +//! Fields are preprocessed into either [`ValueField`]s or [`DataField`]s which +//! are then pushed to the parser in [`FromForm::push_value()`] or +//! [`FromForm::push_data()`], respectively. Both url-encoded forms and +//! multipart forms are supported. All url-encoded form fields are preprocessed +//! as [`ValueField`]s. Multipart form fields with Content-Types are processed +//! as [`DataField`]s while those without a set Content-Type are processed as +//! [`ValueField`]s. +//! +//! # Data Limits +//! +//! The total amount of data accepted by the [`Form`] data guard is limited by +//! the following limits: +//! +//! | Limit Name | Default | Description | +//! |-------------|---------|------------------------------------| +//! | `form` | 32KiB | total limit for url-encoded forms | +//! | `data-form` | 2MiB | total limit for multipart forms | +//! | `*` | N/A | each field type has its own limits | +//! +//! Additionally, as noted above, each form field type (a form guard) typically +//! imposes its own limits. For example, the `&str` form guard imposes a data +//! limit of `string` when multipart data is streamed. +//! +//! See the [`Limits`](crate::data::Limits) docs for more. +//! +/// # Examples +/// +/// The following examples use `f1=v1&f2=v2` to illustrate field/value pairs +/// `(f1, v2)` and `(f2, v2)`. This is the same encoding used to send HTML forms +/// over HTTP but Rocket's push-parsers are unaware of any specific encoding, +/// dealing only with logical `field`s, `index`es, and `value`s. +/// +/// ## A Single Value (`T: FormFormValue`) +/// +/// The simplest example parses a single value of type `T` from a string with an +/// optional default value: this is `impl FromForm for T`: +/// +/// 1. **Initialization.** The context stores parsing options and an `Option` +/// of `Result` for storing the `result` of parsing `T`, which is +/// initially set to `None`. +/// +/// ```rust,ignore +/// struct Context { +/// opts: FormOptions, +/// result: Option>, +/// } +/// ``` +/// +/// 2. **Push.** The field is treated as a string. If `context.result` is `None`, +/// `T` is parsed from `field`, and the result is stored in `context.result`. +/// +/// ```rust,ignore +/// fn push(this: &mut Self::Context, field: FormField<'v>) { +/// if this.result.is_none() { +/// this.result = Some(Self::from_value(field)); +/// } +/// } +/// ``` +/// +/// 3. **Finalization.** If `context.result` is `None` and `T` has a default, +/// the default is returned; otherwise a `Missing` error is returned. If +/// `context.result` is `Some(v)`, the result `v` is returned. +/// +/// ```rust,ignore +/// fn finalize(this: Self::Context) -> Result { +/// match this.result { +/// Some(value) => Ok(value), +/// None => match ::default() { +/// Some(default) => Ok(default), +/// None => Err(Error::Missing) +/// } +/// } +/// } +/// ``` +/// +/// This implementation is complete, barring checking for duplicate pushes when +/// paring is requested as `strict`. +/// +/// ## Maps w/named Fields (`struct`) +/// +/// A `struct` with named fields parses values of multiple types, indexed by the +/// name of its fields: +/// +/// ```rust,ignore +/// struct Dog { name: String, barks: bool, friends: Vec, } +/// struct Cat { name: String, meows: bool } +/// ``` +/// +/// Candidates for parsing into a `Dog` include: +/// +/// * `name=Fido&barks=0` +/// +/// `Dog { "Fido", false }` +/// +/// * `name=Fido&barks=1&friends[0]name=Sally&friends[0]meows=0` +/// `name=Fido&barks=1&friends[0].name=Sally&friends[0].meows=0` +/// `name=Fido&barks=1&friends.0.name=Sally&friends.0.meows=0` +/// +/// `Dog { "Fido", true, vec![Cat { "Sally", false }] }` +/// +/// Parsers for structs are code-generated to proceed as follows: +/// +/// 1. **Initialization.** The context stores parsing options, a `T::Context` +/// for each field of type `T`, and a vector called `extra`. +/// +/// ```rust,ignore +/// struct Context<'v> { +/// opts: FormOptions, +/// field_a: A::Context, +/// field_b: B::Context, +/// /* ... */ +/// extra: Vec> +/// } +/// ``` +/// +/// 2. **Push.** The index of the first key is compared to known field names. +/// If none matches, the index is added to `extra`. Otherwise the key is +/// stripped from the field, and the remaining field is pushed to `T`. +/// +/// ```rust,ignore +/// fn push(this: &mut Self::Context, field: FormField<'v>) { +/// match field.key() { +/// "field_a" => A::push(&mut this.field_a, field.next()), +/// "field_b" => B::push(&mut this.field_b, field.next()), +/// /* ... */ +/// _ => this.extra.push(field) +/// } +/// } +/// ``` +/// +/// 3. **Finalization.** Every context is finalized; errors and `Ok` values +/// are collected. If parsing is strict and extras is non-empty, an error +/// added to the collection of errors. If there are no errors, all `Ok` +/// values are used to create the `struct`, and the created struct is +/// returned. Otherwise, `Err(errors)` is returned. +/// +/// ```rust,ignore +/// fn finalize(mut this: Self::Context) -> Result { +/// let mut errors = vec![]; +/// +/// let field_a = A::finalize(&mut this.field_a) +/// .map_err(|e| errors.push(e)) +/// .map(Some).unwrap_or(None); +/// +/// let field_b = B::finblize(&mut this.field_b) +/// .map_err(|e| errors.push(e)) +/// .map(Some).unwrap_or(None); +/// +/// /* .. */ +/// +/// if !errors.is_empty() { +/// return Err(Values(errors)); +/// } else if this.opts.is_strict() && !this.extra.is_empty() { +/// return Err(Extra(this.extra)); +/// } else { +/// // NOTE: All unwraps will succeed since `errors.is_empty()`. +/// Struct { +/// field_a: field_a.unwrap(), +/// field_b: field_b.unwrap(), +/// /* .. */ +/// } +/// } +/// } +/// ``` +/// +/// ## Sequences: (`Vec`) +/// +/// A `Vec` invokes `T`'s push-parser on every push, adding instances +/// of `T` to an internal vector. The instance of `T` whose parser is invoked +/// depends on the index of the first key: +/// +/// * if it is the first push, the index differs from the previous, or there is no +/// index, a new `T::Context` is `init`ialized and added to the internal vector +/// * if the index matches the previously seen index, the last initialized +/// `T::Context` is `push`ed to. +/// +/// For instance, the sequentially pushed values `=1`, `=2`, and `=3` for a +/// `Vec` (or any other integer) is expected to parse as `vec![1, 2, 3]`. The +/// same is true for `[]=1&[]=2&[]=3`. In the first example (`=1&..`), the fields +/// passed to `Vec`'s push-parser (`=1`, ..) have no key and thus no index. In the +/// second example (`[]=1&..`), the key is `[]` (`[]=1`) without an index. In both +/// cases, there is no index. The `Vec` parser takes this to mean that a _new_ `T` +/// should be parsed using the field's value. +/// +/// If, instead, the index was non-empty and equal to the index of the field in the +/// _previous_ push, `Vec` pushes the value to the parser of the previously parsed +/// `T`: `[]=1&[0]=2&[0]=3` results in `vec![1, 2]` and `[0]=1&[0]=2&[]=3` results +/// in `vec![1, 3]` (see [`FromFormValue`]). +/// +/// This generalizes. Consider a `Vec>` named `x`, so `x` and an +/// optional `=` are stripped before being passed to `Vec`'s push-parser: +/// +/// * `x=1&x=2&x=3` parses as `vec![vec![1], vec![2], vec![3]]` +/// +/// Every push (`1`, `2`, `3`) has no key, thus no index: a new `T` (here, +/// `Vec`) is thus initialized for every `push()` and passed the +/// value (here, `1`, `2`, and `3`). Each of these `push`es proceeds +/// recursively: every push again has no key, thus no index, so a new `T` is +/// initialized for every push (now a `usize`), which finally parse as +/// integers `1`, `2`, and `3`. +/// +/// Note: `x=1&x=2&x=3` _also_ can also parse as `vec![1, 2, 3]` when viewed +/// as a `Vec`; this is the non-self-descriptive part of the format. +/// +/// * `x[]=1&x[]=2&x[]=3` parses as `vec![vec![1], vec![2], vec![3]]` +/// +/// This proceeds nearly identically to the previous example, with the exception +/// that the top-level `Vec` sees the values `[]=1`, `[]=2`, and `[]=3`. +/// +/// * `x[0]=1&x[0]=2&x[]=3` parses as `vec![vec![1, 2], vec![3]]` +/// +/// The top-level `Vec` sees the values `[0]=1`, `[0]=2`, and `[]=3`. The first +/// value results in a new `Vec` being initialized, as before, which is +/// pushed a `1`. The second value has the same index as the first, `0`, and so +/// `2` is pushed to the previous `T`, the `Vec` which contains the `1`. +/// Finally, the third value has no index, so a new `Vec` is initialized +/// and pushed a `3`. +/// +/// * `x[0]=1&x[0]=2&x[]=3&x[]=4` parses as `vec![vec![1, 2], vec![3], vec![4]]` +/// * `x[0]=1&x[0]=2&x[1]=3&x[1]=4` parses as `vec![vec![1, 2], vec![3, 4]]` +/// +/// The indexing kind `[]` is purely by convention: the first two examples are +/// equivalent to `x.=1&x.=2`, while the third to `x.0=1&x.0=&x.=3`. +/// +/// The parser proceeds as follows: +/// +/// 1. **Initialization.** The context stores parsing options, the +/// `last_index` encountered in a `push`, an `Option` of a `T::Context` for +/// the `current` value being parsed, a `Vec` of `errors`, and +/// finally a `Vec` of already parsed `items`. +/// +/// ```rust,ignore +/// struct VecContext<'v, T: FromForm<'v>> { +/// opts: FormOptions, +/// last_index: Index<'v>, +/// current: Option, +/// errors: Vec, +/// items: Vec +/// } +/// ``` +/// +/// 2. **Push.** The index of the first key is compared against `last_index`. +/// If it differs, a new context for `T` is created and the previous is +/// finalized. The `Ok` result from finalization is stored in `items` and +/// the `Err` in `errors`. Otherwise the `index` is the same, the `current` +/// context is retrieved, and the field stripped of the current key is +/// pushed to `T`. `last_index` is updated. +/// +/// ```rust,ignore +/// fn push(this: &mut Self::Context, field: FormField<'v>) { +/// if this.last_index != field.index() { +/// this.shift(); // finalize `current`, add to `items`, `errors` +/// let mut context = T::init(this.opts); +/// T::push(&mut context, field.next()); +/// this.current = Some(context); +/// } else { +/// let context = this.current.as_mut(); +/// T::push(context, field.next()) +/// } +/// +/// this.last_index = field.index(); +/// } +/// ``` +/// +/// 3. **Finalization.** Any `current` context is finalized, storing the `Ok` +/// or `Err` as before. `Ok(items)` is returned if `errors` is empty, +/// otherwise `Err(errors)` is returned. +/// +/// ```rust,ignore +/// fn finalize(mut this: Self::Context) -> Result { +/// this.shift(); // finalizes `current`, as before. +/// match this.errors.is_empty() { +/// true => Ok(this.items), +/// false => Err(this.errors) +/// } +/// } +/// ``` +/// +/// ## Arbitrary Maps (`HashMap`) +/// +/// A `HashMap` can be parsed from keys with one index or, for composite +/// key values, such as structures or sequences, multiple indices. We begin with +/// a discussion of the simpler case: non-composite keys. +/// +/// ### Non-Composite Keys +/// +/// A non-composite value can be described by a single field with no indices. +/// Strings and integers are examples of non-composite values. The push-parser +/// for `HashMap` for a non-composite `K` uses the index of the first key +/// as the value of `K`; the remainder of the field is pushed to `V`'s parser: +/// +/// 1. **Initialization.** The context stores a column-based representation of +/// `keys` and `values`, a `key_map` from a string key to the column index, +/// an `errors` vector for storing errors as they arise, and the parsing +/// options. +/// +/// ```rust,ignore +/// struct MapContext<'v, K: FromForm<'v>, V: FromForm<'v>> { +/// opts: FormOptions, +/// key_map: HashMap<&'v str, usize>, +/// keys: Vec, +/// values: Vec, +/// errors: Vec>, +/// } +/// ``` +/// +/// 2. **Push.** The `key_map` index for the key associated with the index of +/// the first key in the field is retrieved. If such a key has not yet been +/// seen, a new key and value context are created, the key is pushed to +/// `K`'s parser, and the field minus the first key is pushed to `V`'s +/// parser. +/// +/// ```rust,ignore +/// fn push(this: &mut Self::Context, field: FormField<'v>) { +/// let key = field.index(); +/// let value_context = match this.key_map.get(Key) { +/// Some(i) => &mut this.values[i], +/// None => { +/// let i = this.keys.len(); +/// this.key_map.insert(key, i); +/// this.keys.push(K::init(this.opts)); +/// this.values.push(V::init(this.opts)); +/// K::push(&mut this.keys[i], key.into()); +/// &mut this.values[i] +/// } +/// }; +/// +/// V::push(value_context, field.next()); +/// } +/// ``` +/// +/// 3. **Finalization.** All key and value contexts are finalized; any errors +/// are collected in `errors`. If there are no errors, `keys` and `values` +/// are collected into a `HashMap` and returned. Otherwise, the errors are +/// returned. +/// +/// ```rust,ignore +/// fn finalize(mut this: Self::Context) -> Result { +/// this.finalize_keys(); +/// this.finalize_values(); +/// if this.errors.is_empty() { +/// Ok(this.keys.into_iter().zip(this.values.into_iter()).collect()) +/// } else { +/// Err(this.errors) +/// } +/// } +/// ``` +/// +/// Examples of forms parseable via this parser are: +/// +/// * `x[0].name=Bob&x[0].meows=true`as a `HashMap` parses with +/// `0` mapping to `Cat { name: "Bob", meows: true }` +/// * `x[0]name=Bob&x[0]meows=true`as a `HashMap` parses just as +/// above. +/// * `x[0]=Bob&x[0]=Sally&x[1]=Craig`as a `HashMap>` +/// just as `{ 0 => vec!["Bob", "Sally"], 1 => vec!["Craig"] }`. +/// +/// A `HashMap` can be thought of as a vector of key-value pairs: `Vec<(K, +/// V)` (row-based) or equivalently, as two vectors of keys and values: `Vec` +/// and `Vec` (column-based). The implication is that indexing into a +/// specific key or value requires _two_ indexes: the first to determine whether +/// a key or value is being indexed to, and the second to determine _which_ key +/// or value. The push-parser for maps thus optionally accepts two indexes for a +/// single key to allow piece-by-piece build-up of arbitrary keys and values. +/// +/// The parser proceeds as follows: +/// +/// 1. **Initialization.** The context stores parsing options, a vector of +/// `key_contexts: Vec`, a vector of `value_contexts: +/// Vec`, a `mapping` from a string index to an integer index +/// into the `contexts`, and a vector of `errors`. +/// 2. **Push.** An index is required; an error is emitted and `push` returns +/// if they field's first key does not contain an index. If the first key +/// contains _one_ index, a new `K::Context` and `V::Context` are created. +/// The key is pushed as the value to `K` and the remaining field as the +/// value to `V`. The key and value are finalized; if both succeed, the key +/// and value are stored in `keys` and `values`; otherwise the error(s) is +/// stored in `errors`. +/// +/// If the first keys contains _two_ indices, the first must starts with +/// `k` or `v`, while the `second` is arbitrary. `mapping` is indexed by +/// `second`; the integer is retrieved. If none exists, new contexts are +/// created an added to `{key,value}_contexts`, and their index is mapped +/// to `second` in `mapping`. If the first index is `k`, the field, +/// stripped of the first key, is pushed to the key's context; the same is +/// done for the value's context is the first index is `v`. +/// 3. **Finalization.** Every context is finalized; errors and `Ok` values +/// are collected. TODO: FINISH. Split this into two: one for single-index, +/// another for two-indices. + +mod field; +mod options; +mod from_form; +mod from_form_field; +mod form; +mod context; +mod strict; +mod parser; +pub mod validate; +pub mod name; +pub mod error; + +#[cfg(test)] +mod tests; + +pub type Result<'v, T> = std::result::Result>; + +#[doc(hidden)] +pub use rocket_codegen::{FromForm, FromFormField}; + +#[doc(inline)] +pub use self::error::{Errors, Error}; + +pub use field::*; +pub use options::*; +pub use from_form_field::*; +pub use from_form::*; +pub use form::*; +pub use context::*; +pub use strict::*; + +#[doc(hidden)] +pub mod prelude { + pub use super::*; + pub use super::name::*; + pub use super::error::*; +} diff --git a/core/lib/src/form/name.rs b/core/lib/src/form/name.rs new file mode 100644 index 0000000000..83f0c9627f --- /dev/null +++ b/core/lib/src/form/name.rs @@ -0,0 +1,907 @@ +//! Types for handling field names, name keys, and key indices. + +use std::ops::Deref; +use std::borrow::Cow; + +use ref_cast::RefCast; + +use crate::http::RawStr; + +/// A field name composed of keys. +/// +/// A form field name is composed of _keys_, delimited by `.` or `[]`. Keys, in +/// turn, are composed of _indices_, delimited by `:`. The graphic below +/// illustrates this composition for a single field in `$name=$value` format: +/// +/// ```text +/// food.bart[bar:foo].blam[0_0][1000]=some-value +/// name |--------------------------------| +/// key |--| |--| |-----| |--| |-| |--| +/// index |--| |--| |-| |-| |--| |-| |--| +/// ``` +/// +/// A `Name` is a wrapper around the field name string with methods to easily +/// access its sub-components. +/// +/// # Serialization +/// +/// A value of this type is serialized exactly as an `&str` consisting of the +/// entire field name. +#[repr(transparent)] +#[derive(RefCast)] +pub struct Name(str); + +impl Name { + /// Wraps a string as a `Name`. This is cost-free. + /// + /// # Example + /// + /// ```rust + /// use rocket::form::name::Name; + /// + /// let name = Name::new("a.b.c"); + /// assert_eq!(name.as_str(), "a.b.c"); + /// ``` + pub fn new + ?Sized>(string: &S) -> &Name { + Name::ref_cast(string.as_ref()) + } + + /// Returns an iterator over the keys of `self`, including empty keys. + /// + /// See the [top-level docs](Self) for a description of "keys". + /// + /// # Example + /// + /// ```rust + /// use rocket::form::name::Name; + /// + /// let name = Name::new("apple.b[foo:bar]zoo.[barb].bat"); + /// let keys: Vec<_> = name.keys().map(|k| k.as_str()).collect(); + /// assert_eq!(keys, &["apple", "b", "foo:bar", "zoo", "", "barb", "bat"]); + /// ``` + pub fn keys(&self) -> impl Iterator { + struct Keys<'v>(NameView<'v>); + + impl<'v> Iterator for Keys<'v> { + type Item = &'v Key; + + fn next(&mut self) -> Option { + if self.0.is_terminal() { + return None; + } + + let key = self.0.key_lossy(); + self.0.shift(); + Some(key) + } + } + + Keys(NameView::new(self)) + } + + /// Returns an iterator over overlapping name prefixes of `self`, each + /// succeeding prefix containing one more key than the previous. + /// + /// # Example + /// + /// ```rust + /// use rocket::form::name::Name; + /// + /// let name = Name::new("apple.b[foo:bar]"); + /// let prefixes: Vec<_> = name.prefixes().map(|p| p.as_str()).collect(); + /// assert_eq!(prefixes, &["apple", "apple.b", "apple.b[foo:bar]"]); + /// + /// let name = Name::new("a.b.[foo]"); + /// let prefixes: Vec<_> = name.prefixes().map(|p| p.as_str()).collect(); + /// assert_eq!(prefixes, &["a", "a.b", "a.b.", "a.b.[foo]"]); + /// ``` + pub fn prefixes(&self) -> impl Iterator { + struct Prefixes<'v>(NameView<'v>); + + impl<'v> Iterator for Prefixes<'v> { + type Item = &'v Name; + + fn next(&mut self) -> Option { + if self.0.is_terminal() { + return None; + } + + let name = self.0.as_name(); + self.0.shift(); + Some(name) + } + } + + Prefixes(NameView::new(self)) + } + + /// Borrows the underlying string. + /// + /// # Example + /// + /// ```rust + /// use rocket::form::name::Name; + /// + /// let name = Name::new("a.b.c"); + /// assert_eq!(name.as_str(), "a.b.c"); + /// ``` + pub fn as_str(&self) -> &str { + &self.0 + } +} + +impl serde::Serialize for Name { + fn serialize(&self, ser: S) -> Result + where S: serde::Serializer + { + self.0.serialize(ser) + } +} + +impl<'de: 'a, 'a> serde::Deserialize<'de> for &'a Name { + fn deserialize(de: D) -> Result + where D: serde::Deserializer<'de> + { + <&'a str as serde::Deserialize<'de>>::deserialize(de).map(Name::new) + } +} + +impl<'a, S: AsRef + ?Sized> From<&'a S> for &'a Name { + #[inline] + fn from(string: &'a S) -> Self { + Name::new(string) + } +} + +impl Deref for Name { + type Target = str; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl> core::ops::Index for Name { + type Output = Name; + + #[inline] + fn index(&self, index: I) -> &Self::Output { + self.0[index].into() + } +} + +impl PartialEq for Name { + fn eq(&self, other: &Self) -> bool { + self.keys().eq(other.keys()) + } +} + +impl PartialEq for Name { + fn eq(&self, other: &str) -> bool { + self == Name::new(other) + } +} + +impl PartialEq for str { + fn eq(&self, other: &Name) -> bool { + Name::new(self) == other + } +} + +impl PartialEq<&str> for Name { + fn eq(&self, other: &&str) -> bool { + self == Name::new(other) + } +} + +impl PartialEq for &str { + fn eq(&self, other: &Name) -> bool { + Name::new(self) == other + } +} + +impl AsRef for str { + fn as_ref(&self) -> &Name { + Name::new(self) + } +} + +impl AsRef for RawStr { + fn as_ref(&self) -> &Name { + Name::new(self) + } +} + +impl Eq for Name { } + +impl std::hash::Hash for Name { + fn hash(&self, state: &mut H) { + self.keys().for_each(|k| k.0.hash(state)) + } +} + +impl std::fmt::Display for Name { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + self.0.fmt(f) + } +} + +impl std::fmt::Debug for Name { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + self.0.fmt(f) + } +} + +/// A field name key composed of indices. +/// +/// A form field name key is composed of _indices_, delimited by `:`. The +/// graphic below illustrates this composition for a single field in +/// `$name=$value` format: +/// +/// ```text +/// food.bart[bar:foo:baz]=some-value +/// name |--------------------| +/// key |--| |--| |---------| +/// index |--| |--| |-| |-| |-| +/// ``` +/// +/// A `Key` is a wrapper around a given key string with methods to easily access +/// its indices. +/// +/// # Serialization +/// +/// A value of this type is serialized exactly as an `&str` consisting of the +/// entire key. +#[repr(transparent)] +#[derive(RefCast, Debug, PartialEq, Eq, Hash)] +pub struct Key(str); + +impl Key { + /// Wraps a string as a `Key`. This is cost-free. + /// + /// # Example + /// + /// ```rust + /// use rocket::form::name::Key; + /// + /// let key = Key::new("a:b:c"); + /// assert_eq!(key.as_str(), "a:b:c"); + /// ``` + pub fn new + ?Sized>(string: &S) -> &Key { + Key::ref_cast(string.as_ref()) + } + + /// Returns an iterator over the indices of `self`, including empty indices. + /// + /// See the [top-level docs](Self) for a description of "indices". + /// + /// # Example + /// + /// ```rust + /// use rocket::form::name::Key; + /// + /// let key = Key::new("foo:bar::baz:a.b.c"); + /// let indices: Vec<_> = key.indices().collect(); + /// assert_eq!(indices, &["foo", "bar", "", "baz", "a.b.c"]); + /// ``` + pub fn indices(&self) -> impl Iterator { + self.split(':') + } + + /// Borrows the underlying string. + /// + /// # Example + /// + /// ```rust + /// use rocket::form::name::Key; + /// + /// let key = Key::new("a:b:c"); + /// assert_eq!(key.as_str(), "a:b:c"); + /// ``` + pub fn as_str(&self) -> &str { + &*self + } +} + +impl Deref for Key { + type Target = str; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl serde::Serialize for Key { + fn serialize(&self, ser: S) -> Result + where S: serde::Serializer + { + self.0.serialize(ser) + } +} + +impl<'de: 'a, 'a> serde::Deserialize<'de> for &'a Key { + fn deserialize(de: D) -> Result + where D: serde::Deserializer<'de> + { + <&'a str as serde::Deserialize<'de>>::deserialize(de).map(Key::new) + } +} + +impl> core::ops::Index for Key { + type Output = Key; + + #[inline] + fn index(&self, index: I) -> &Self::Output { + self.0[index].into() + } +} + +impl PartialEq for Key { + fn eq(&self, other: &str) -> bool { + self == Key::new(other) + } +} + +impl PartialEq for str { + fn eq(&self, other: &Key) -> bool { + Key::new(self) == other + } +} + +impl<'a, S: AsRef + ?Sized> From<&'a S> for &'a Key { + #[inline] + fn from(string: &'a S) -> Self { + Key::new(string) + } +} + +impl AsRef for str { + fn as_ref(&self) -> &Key { + Key::new(self) + } +} + +impl AsRef for RawStr { + fn as_ref(&self) -> &Key { + Key::new(self) + } +} + +impl std::fmt::Display for Key { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + self.0.fmt(f) + } +} + +/// A sliding-prefix view into a [`Name`]. +/// +/// A [`NameView`] maintains a sliding key view into a [`Name`]. The current key +/// ([`key()`]) can be [`shift()`ed](NameView::shift()) one key to the right. +/// The `Name` prefix including the current key can be extracted via +/// [`as_name()`] and the prefix _not_ including the current key via +/// [`parent()`]. +/// +/// [`key()`]: NameView::key() +/// [`as_name()`]: NameView::as_name() +/// [`parent()`]: NameView::parent() +/// +/// This is best illustrated via an example: +/// +/// ```rust +/// use rocket::form::name::NameView; +/// +/// // The view begins at the first key. Illustrated: `(a).b[c:d]` where +/// // parenthesis enclose the current key. +/// let mut view = NameView::new("a.b[c:d]"); +/// assert_eq!(view.key().unwrap(), "a"); +/// assert_eq!(view.as_name(), "a"); +/// assert_eq!(view.parent(), None); +/// +/// // Shifted once to the right views the second key: `a.(b)[c:d]`. +/// view.shift(); +/// assert_eq!(view.key().unwrap(), "b"); +/// assert_eq!(view.as_name(), "a.b"); +/// assert_eq!(view.parent().unwrap(), "a"); +/// +/// // Shifting again now has predictable results: `a.b[(c:d)]`. +/// view.shift(); +/// assert_eq!(view.key().unwrap(), "c:d"); +/// assert_eq!(view.as_name(), "a.b[c:d]"); +/// assert_eq!(view.parent().unwrap(), "a.b"); +/// +/// // Shifting past the end means we have no further keys. +/// view.shift(); +/// assert_eq!(view.key(), None); +/// assert_eq!(view.key_lossy(), ""); +/// assert_eq!(view.as_name(), "a.b[c:d]"); +/// assert_eq!(view.parent().unwrap(), "a.b[c:d]"); +/// +/// view.shift(); +/// assert_eq!(view.key(), None); +/// assert_eq!(view.as_name(), "a.b[c:d]"); +/// assert_eq!(view.parent().unwrap(), "a.b[c:d]"); +/// ``` +/// +/// # Equality +/// +/// `PartialEq`, `Eq`, and `Hash` all operate on the name prefix including the +/// current key. Only key values are compared; delimiters are insignificant. +/// Again, illustrated via examples: +/// +/// ```rust +/// use rocket::form::name::NameView; +/// +/// let mut view = NameView::new("a.b[c:d]"); +/// assert_eq!(view, "a"); +/// +/// // Shifted once to the right views the second key: `a.(b)[c:d]`. +/// view.shift(); +/// assert_eq!(view.key().unwrap(), "b"); +/// assert_eq!(view.as_name(), "a.b"); +/// assert_eq!(view, "a.b"); +/// assert_eq!(view, "a[b]"); +/// +/// // Shifting again now has predictable results: `a.b[(c:d)]`. +/// view.shift(); +/// assert_eq!(view, "a.b[c:d]"); +/// assert_eq!(view, "a.b.c:d"); +/// assert_eq!(view, "a[b].c:d"); +/// assert_eq!(view, "a[b]c:d"); +/// ``` +#[derive(Copy, Clone)] +pub struct NameView<'v> { + name: &'v Name, + start: usize, + end: usize, +} + +impl<'v> NameView<'v> { + /// Initializes a new `NameView` at the first key of `name`. + /// + /// # Example + /// + /// ```rust + /// use rocket::form::name::NameView; + /// + /// let mut view = NameView::new("a.b[c:d]"); + /// assert_eq!(view.key().unwrap(), "a"); + /// assert_eq!(view.as_name(), "a"); + /// assert_eq!(view.parent(), None); + /// ``` + pub fn new>(name: N) -> Self { + let mut view = NameView { name: name.into(), start: 0, end: 0 }; + view.shift(); + view + } + + /// Shifts the current key once to the right. + /// + /// # Example + /// + /// ```rust + /// use rocket::form::name::NameView; + /// + /// let mut view = NameView::new("a.b[c:d]"); + /// assert_eq!(view.key().unwrap(), "a"); + /// + /// view.shift(); + /// assert_eq!(view.key().unwrap(), "b"); + /// ``` + pub fn shift(&mut self) { + const START_DELIMS: &'static [char] = &['.', '[']; + + let string = &self.name[self.end..]; + let bytes = string.as_bytes(); + let shift = match bytes.get(0) { + None | Some(b'=') => 0, + Some(b'[') => match string[1..].find(&[']', '.'][..]) { + Some(j) => match string[1..].as_bytes()[j] { + b']' => j + 2, + _ => j + 1, + } + None => bytes.len(), + } + Some(b'.') => match string[1..].find(START_DELIMS) { + Some(j) => j + 1, + None => bytes.len(), + }, + _ => match string.find(START_DELIMS) { + Some(j) => j, + None => bytes.len() + } + }; + + debug_assert!(self.end + shift <= self.name.len()); + *self = NameView { + name: self.name, + start: self.end, + end: self.end + shift, + }; + } + + /// Returns the key currently viewed by `self` if it is non-empty. + /// + /// # Example + /// + /// ```rust + /// use rocket::form::name::NameView; + /// + /// let mut view = NameView::new("a[b]"); + /// assert_eq!(view.key().unwrap(), "a"); + /// + /// view.shift(); + /// assert_eq!(view.key().unwrap(), "b"); + /// + /// view.shift(); + /// assert_eq!(view.key(), None); + /// # view.shift(); assert_eq!(view.key(), None); + /// # view.shift(); assert_eq!(view.key(), None); + /// ``` + pub fn key(&self) -> Option<&'v Key> { + let lossy_key = self.key_lossy(); + if lossy_key.is_empty() { + return None; + } + + Some(lossy_key) + } + + /// Returns the key currently viewed by `self`, even if it is non-empty. + /// + /// # Example + /// + /// ```rust + /// use rocket::form::name::NameView; + /// + /// let mut view = NameView::new("a[b]"); + /// assert_eq!(view.key_lossy(), "a"); + /// + /// view.shift(); + /// assert_eq!(view.key_lossy(), "b"); + /// + /// view.shift(); + /// assert_eq!(view.key_lossy(), ""); + /// # view.shift(); assert_eq!(view.key_lossy(), ""); + /// # view.shift(); assert_eq!(view.key_lossy(), ""); + /// ``` + pub fn key_lossy(&self) -> &'v Key { + let view = &self.name[self.start..self.end]; + let key = match view.as_bytes().get(0) { + Some(b'.') => &view[1..], + Some(b'[') if view.ends_with(']') => &view[1..view.len() - 1], + _ => view + }; + + key.0.into() + } + + /// Returns the `Name` _up to and including_ the current key. + /// + /// # Example + /// + /// ```rust + /// use rocket::form::name::NameView; + /// + /// let mut view = NameView::new("a[b]"); + /// assert_eq!(view.as_name(), "a"); + /// + /// view.shift(); + /// assert_eq!(view.as_name(), "a[b]"); + /// # view.shift(); assert_eq!(view.as_name(), "a[b]"); + /// # view.shift(); assert_eq!(view.as_name(), "a[b]"); + /// ``` + pub fn as_name(&self) -> &'v Name { + &self.name[..self.end] + } + + /// Returns the `Name` _prior to_ the current key. + /// + /// # Example + /// + /// ```rust + /// use rocket::form::name::NameView; + /// + /// let mut view = NameView::new("a[b]"); + /// assert_eq!(view.parent(), None); + /// + /// view.shift(); + /// assert_eq!(view.parent().unwrap(), "a"); + /// + /// view.shift(); + /// assert_eq!(view.parent().unwrap(), "a[b]"); + /// # view.shift(); assert_eq!(view.parent().unwrap(), "a[b]"); + /// # view.shift(); assert_eq!(view.parent().unwrap(), "a[b]"); + /// ``` + pub fn parent(&self) -> Option<&'v Name> { + if self.start > 0 { + Some(&self.name[..self.start]) + } else { + None + } + } + + /// Returns the underlying `Name`. + /// + /// # Example + /// + /// ```rust + /// use rocket::form::name::NameView; + /// + /// let mut view = NameView::new("a[b]"); + /// assert_eq!(view.source(), "a[b]"); + /// + /// view.shift(); + /// assert_eq!(view.source(), "a[b]"); + /// + /// view.shift(); + /// assert_eq!(view.source(), "a[b]"); + /// + /// # view.shift(); assert_eq!(view.source(), "a[b]"); + /// # view.shift(); assert_eq!(view.source(), "a[b]"); + /// ``` + pub fn source(&self) -> &'v Name { + self.name + } + + fn is_terminal(&self) -> bool { + self.start == self.name.len() + } +} + +impl std::fmt::Debug for NameView<'_> { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + self.as_name().fmt(f) + } +} + +impl std::fmt::Display for NameView<'_> { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + self.as_name().fmt(f) + } +} + +impl<'a, 'b> PartialEq> for NameView<'a> { + fn eq(&self, other: &NameView<'b>) -> bool { + self.as_name() == other.as_name() + } +} + +impl> PartialEq for NameView<'_> { + fn eq(&self, other: &B) -> bool { + other == self.as_name() + } +} + +impl Eq for NameView<'_> { } + +impl std::hash::Hash for NameView<'_> { + fn hash(&self, state: &mut H) { + self.as_name().hash(state) + } +} + +impl std::borrow::Borrow for NameView<'_> { + fn borrow(&self) -> &Name { + self.as_name() + } +} + +/// A potentially owned [`Name`]. +/// +/// Constructible from a [`NameView`], [`Name`], `&str`, or `String`, a +/// `NameBuf` acts much like a [`Name`] but can be converted into an owned +/// version via [`IntoOwned`](crate::http::ext::IntoOwned). +/// +/// ```rust +/// use rocket::form::name::NameBuf; +/// use rocket::http::ext::IntoOwned; +/// +/// let alloc = String::from("a.b.c"); +/// let name = NameBuf::from(alloc.as_str()); +/// let owned: NameBuf<'static> = name.into_owned(); +/// ``` +#[derive(Clone)] +pub struct NameBuf<'v> { + left: &'v Name, + right: Cow<'v, str>, +} + +impl<'v> NameBuf<'v> { + #[inline] + fn split(&self) -> (&Name, &Name) { + (self.left, Name::new(&self.right)) + } + + /// Returns an iterator over the keys of `self`, including empty keys. + /// + /// See [`Name`] for a description of "keys". + /// + /// # Example + /// + /// ```rust + /// use rocket::form::name::NameBuf; + /// + /// let name = NameBuf::from("apple.b[foo:bar]zoo.[barb].bat"); + /// let keys: Vec<_> = name.keys().map(|k| k.as_str()).collect(); + /// assert_eq!(keys, &["apple", "b", "foo:bar", "zoo", "", "barb", "bat"]); + /// ``` + #[inline] + pub fn keys(&self) -> impl Iterator { + let (left, right) = self.split(); + left.keys().chain(right.keys()) + } + + /// Returns `true` if `self` is empty. + /// + /// # Example + /// + /// ```rust + /// use rocket::form::name::NameBuf; + /// + /// let name = NameBuf::from("apple.b[foo:bar]zoo.[barb].bat"); + /// assert!(!name.is_empty()); + /// + /// let name = NameBuf::from(""); + /// assert!(name.is_empty()); + /// ``` + #[inline] + pub fn is_empty(&self) -> bool { + let (left, right) = self.split(); + left.is_empty() && right.is_empty() + } +} + +impl crate::http::ext::IntoOwned for NameBuf<'_> { + type Owned = NameBuf<'static>; + + fn into_owned(self) -> Self::Owned { + let right = match (self.left, self.right) { + (l, Cow::Owned(r)) if l.is_empty() => Cow::Owned(r), + (l, r) if l.is_empty() => r.to_string().into(), + (l, r) if r.is_empty() => l.to_string().into(), + (l, r) => format!("{}.{}", l, r).into(), + }; + + NameBuf { left: "".into(), right } + } +} + +impl serde::Serialize for NameBuf<'_> { + fn serialize(&self, serializer: S) -> Result + where S: serde::Serializer + { + serializer.serialize_str(&self.to_string()) + } +} + +impl<'v> From> for NameBuf<'v> { + fn from(nv: NameView<'v>) -> Self { + NameBuf { left: nv.as_name(), right: Cow::Borrowed("") } + } +} + +impl<'v> From<&'v Name> for NameBuf<'v> { + fn from(name: &'v Name) -> Self { + NameBuf { left: name, right: Cow::Borrowed("") } + } +} + +impl<'v> From<&'v str> for NameBuf<'v> { + fn from(name: &'v str) -> Self { + NameBuf::from((None, Cow::Borrowed(name))) + } +} + +impl<'v> From for NameBuf<'v> { + fn from(name: String) -> Self { + NameBuf::from((None, Cow::Owned(name))) + } +} + +#[doc(hidden)] +impl<'v> From<(Option<&'v Name>, Cow<'v, str>)> for NameBuf<'v> { + fn from((prefix, right): (Option<&'v Name>, Cow<'v, str>)) -> Self { + match prefix { + Some(left) => NameBuf { left, right }, + None => NameBuf { left: "".into(), right } + } + } +} + +#[doc(hidden)] +impl<'v> From<(Option<&'v Name>, &'v str)> for NameBuf<'v> { + fn from((prefix, suffix): (Option<&'v Name>, &'v str)) -> Self { + NameBuf::from((prefix, Cow::Borrowed(suffix))) + } +} + +#[doc(hidden)] +impl<'v> From<(&'v Name, &'v str)> for NameBuf<'v> { + fn from((prefix, suffix): (&'v Name, &'v str)) -> Self { + NameBuf::from((Some(prefix), Cow::Borrowed(suffix))) + } +} + +impl std::fmt::Debug for NameBuf<'_> { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "\"")?; + + let (left, right) = self.split(); + if !left.is_empty() { write!(f, "{}", left.escape_debug())? } + if !right.is_empty() { + if !left.is_empty() { f.write_str(".")?; } + write!(f, "{}", right.escape_debug())?; + } + + write!(f, "\"") + } +} + +impl std::fmt::Display for NameBuf<'_> { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let (left, right) = self.split(); + if !left.is_empty() { left.fmt(f)?; } + if !right.is_empty() { + if !left.is_empty() { f.write_str(".")?; } + right.fmt(f)?; + } + + Ok(()) + } +} + +impl PartialEq for NameBuf<'_> { + fn eq(&self, other: &Self) -> bool { + self.keys().eq(other.keys()) + } +} + +impl + ?Sized> PartialEq for NameBuf<'_> { + fn eq(&self, other: &N) -> bool { + self.keys().eq(other.as_ref().keys()) + } +} + +impl PartialEq for NameBuf<'_> { + fn eq(&self, other: &Name) -> bool { + self.keys().eq(other.keys()) + } +} + +impl PartialEq> for Name { + fn eq(&self, other: &NameBuf<'_>) -> bool { + self.keys().eq(other.keys()) + } +} + +impl PartialEq> for str { + fn eq(&self, other: &NameBuf<'_>) -> bool { + Name::new(self) == other + } +} + +impl PartialEq> for &str { + fn eq(&self, other: &NameBuf<'_>) -> bool { + Name::new(self) == other + } +} + +impl Eq for NameBuf<'_> { } + +impl std::hash::Hash for NameBuf<'_> { + fn hash(&self, state: &mut H) { + self.keys().for_each(|k| k.0.hash(state)) + } +} + +impl indexmap::Equivalent for NameBuf<'_> { + fn equivalent(&self, key: &Name) -> bool { + self.keys().eq(key.keys()) + } +} + +impl indexmap::Equivalent> for Name { + fn equivalent(&self, key: &NameBuf<'_>) -> bool { + self.keys().eq(key.keys()) + } +} diff --git a/core/lib/src/form/options.rs b/core/lib/src/form/options.rs new file mode 100644 index 0000000000..9df4d33550 --- /dev/null +++ b/core/lib/src/form/options.rs @@ -0,0 +1,11 @@ +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +pub struct Options { + pub strict: bool, +} + +#[allow(non_upper_case_globals, dead_code)] +impl Options { + pub const Lenient: Self = Options { strict: false }; + + pub const Strict: Self = Options { strict: true }; +} diff --git a/core/lib/src/form/parser.rs b/core/lib/src/form/parser.rs new file mode 100644 index 0000000000..895e7757d9 --- /dev/null +++ b/core/lib/src/form/parser.rs @@ -0,0 +1,277 @@ +use std::cell::UnsafeCell; + +use multer::Multipart; +use parking_lot::{RawMutex, lock_api::RawMutex as _}; +use either::Either; + +use crate::request::{Request, local_cache}; +use crate::data::{Data, Limits, Outcome}; +use crate::form::prelude::*; +use crate::http::RawStr; + +type Result<'r, T> = std::result::Result>; + +type Field<'r, 'i> = Either, DataField<'r, 'i>>; + +pub struct Buffer { + strings: UnsafeCell>, + mutex: RawMutex, +} + +pub struct MultipartParser<'r, 'i> { + request: &'r Request<'i>, + buffer: &'r Buffer, + source: Multipart, + done: bool, +} + +pub struct RawStrParser<'r> { + buffer: &'r Buffer, + source: &'r RawStr, +} + +pub enum Parser<'r, 'i> { + Multipart(MultipartParser<'r, 'i>), + RawStr(RawStrParser<'r>), +} + +impl<'r, 'i> Parser<'r, 'i> { + pub async fn new(req: &'r Request<'i>, data: Data) -> Outcome, Errors<'r>> { + let parser = match req.content_type() { + Some(c) if c.is_form() => Self::from_form(req, data).await, + Some(c) if c.is_form_data() => Self::from_multipart(req, data).await, + _ => return Outcome::Forward(data), + }; + + match parser { + Ok(storage) => Outcome::Success(storage), + Err(e) => Outcome::Failure((e.status(), e.into())) + } + } + + async fn from_form(req: &'r Request<'i>, data: Data) -> Result<'r, Parser<'r, 'i>> { + let limit = req.limits().get("form").unwrap_or(Limits::FORM); + let string = data.open(limit).into_string().await?; + if !string.is_complete() { + Err((None, Some(limit.as_u64())))? + } + + Ok(Parser::RawStr(RawStrParser { + buffer: local_cache!(req, Buffer::new()), + source: RawStr::new(local_cache!(req, string.into_inner())), + })) + } + + async fn from_multipart(req: &'r Request<'i>, data: Data) -> Result<'r, Parser<'r, 'i>> { + let boundary = req.content_type() + .ok_or(multer::Error::NoMultipart)? + .param("boundary") + .ok_or(multer::Error::NoBoundary)?; + + let form_limit = req.limits() + .get("data-form") + .unwrap_or(Limits::DATA_FORM); + + Ok(Parser::Multipart(MultipartParser { + request: req, + buffer: local_cache!(req, Buffer::new()), + source: Multipart::with_reader(data.open(form_limit), boundary), + done: false, + })) + } + + pub async fn next(&mut self) -> Option>> { + match self { + Parser::Multipart(ref mut p) => p.next().await, + Parser::RawStr(ref mut p) => p.next().map(|f| Ok(Either::Left(f))) + } + } +} + +impl<'r> RawStrParser<'r> { + pub fn new(buffer: &'r Buffer, source: &'r RawStr) -> Self { + RawStrParser { buffer, source } + } +} + +impl<'r> Iterator for RawStrParser<'r> { + type Item = ValueField<'r>; + + fn next(&mut self) -> Option { + use std::borrow::Cow::*; + + let (name, value) = loop { + if self.source.is_empty() { + return None; + } + + let (field_str, rest) = self.source.split_at_byte(b'&'); + self.source = rest; + + if !field_str.is_empty() { + break field_str.split_at_byte(b'='); + } + }; + + let name_val = match (name.url_decode_lossy(), value.url_decode_lossy()) { + (Borrowed(name), Borrowed(val)) => (name, val), + (Borrowed(name), Owned(v)) => (name, self.buffer.push_one(v)), + (Owned(name), Borrowed(val)) => (self.buffer.push_one(name), val), + (Owned(mut name), Owned(val)) => { + let len = name.len(); + name.push_str(&val); + self.buffer.push_split(name, len) + } + }; + + Some(ValueField::from(name_val)) + } +} + +#[cfg(test)] +mod raw_str_parse_tests { + use crate::form::ValueField as Field; + + #[test] + fn test_skips_empty() { + let buffer = super::Buffer::new(); + let fields: Vec<_> = super::RawStrParser::new(&buffer, "a&b=c&&&c".into()).collect(); + assert_eq!(fields, &[Field::parse("a"), Field::parse("b=c"), Field::parse("c")]); + } + + #[test] + fn test_decodes() { + let buffer = super::Buffer::new(); + let fields: Vec<_> = super::RawStrParser::new(&buffer, "a+b=c%20d&%26".into()).collect(); + assert_eq!(fields, &[Field::parse("a b=c d"), Field::parse("&")]); + } +} + +impl<'r, 'i> MultipartParser<'r, 'i> { + async fn next(&mut self) -> Option>> { + if self.done { + return None; + } + + let field = match self.source.next_field().await { + Ok(Some(field)) => field, + Ok(None) => return None, + Err(e) => { + self.done = true; + return Some(Err(e.into())); + } + }; + + // A field with a content-type is data; one without is "value". + trace_!("multipart field: {:?}", field.name()); + let content_type = field.content_type().and_then(|m| m.as_ref().parse().ok()); + let field = if let Some(content_type) = content_type { + let (name, file_name) = match (field.name(), field.file_name()) { + (None, None) => ("", None), + (None, Some(file_name)) => ("", Some(self.buffer.push_one(file_name))), + (Some(name), None) => (self.buffer.push_one(name), None), + (Some(a), Some(b)) => { + let (field_name, file_name) = self.buffer.push_two(a, b); + (field_name, Some(file_name)) + } + }; + + Either::Right(DataField { + content_type, + request: self.request, + name: NameView::new(name), + file_name: file_name.and_then(sanitize), + data: Data::from(field), + }) + } else { + let (mut buf, len) = match field.name() { + Some(s) => (s.to_string(), s.len()), + None => (String::new(), 0) + }; + + match field.text().await { + Ok(text) => buf.push_str(&text), + Err(e) => return Some(Err(e.into())), + }; + + let name_val = self.buffer.push_split(buf, len); + Either::Left(ValueField::from(name_val)) + }; + + Some(Ok(field)) + } +} + +fn sanitize(file_name: &str) -> Option<&str> { + let file_name = std::path::Path::new(file_name) + .file_name() + .and_then(|n| n.to_str()) + .map(|n| n.find('.').map(|i| n.split_at(i).0).unwrap_or(n))?; + + if file_name.is_empty() + || file_name.starts_with(|c| c == '.' || c == '*') + || file_name.ends_with(|c| c == ':' || c == '>' || c == '<') + || file_name.contains(|c| c == '/' || c == '\\') + { + return None + } + + Some(file_name) +} + +impl Buffer { + pub fn new() -> Self { + Buffer { + strings: UnsafeCell::new(vec![]), + mutex: RawMutex::INIT, + } + } + + pub fn push_one<'a, S: Into>(&'a self, string: S) -> &'a str { + // SAFETY: + // * Aliasing: We retrieve a mutable reference to the last slot (via + // `push()`) and then return said reference as immutable; these + // occur in serial, so they don't alias. This method accesses a + // unique slot each call: the last slot, subsequently replaced by + // `push()` each next call. No other method accesses the internal + // buffer directly. Thus, the outstanding reference to the last slot + // is never accessed again mutably, preserving aliasing guarantees. + // * Liveness: The returned reference is to a `String`; we must ensure + // that the `String` is never dropped while `self` lives. This is + // guaranteed by returning a reference with the same lifetime as + // `self`, so `self` can't be dropped while the string is live, and + // by never removing elements from the internal `Vec` thus not + // dropping `String` itself: `push()` is the only mutating operation + // called on `Vec`, which preserves all previous elements; the + // stability of `String` itself means that the returned address + // remains valid even after internal realloc of `Vec`. + // * Thread-Safety: Parallel calls to `push_one` without exclusion + // would result in a race to `vec.push()`; `RawMutex` ensures that + // this doesn't occur. + unsafe { + self.mutex.lock(); + let vec: &mut Vec = &mut *self.strings.get(); + vec.push(string.into()); + let last = vec.last().expect("push() => non-empty"); + self.mutex.unlock(); + last + } + } + + pub fn push_split(&self, string: String, len: usize) -> (&str, &str) { + let buffered = self.push_one(string); + let a = &buffered[..len]; + let b = &buffered[len..]; + (a, b) + } + + pub fn push_two<'a>(&'a self, a: &str, b: &str) -> (&'a str, &'a str) { + let mut buffer = String::new(); + buffer.push_str(a); + buffer.push_str(b); + + self.push_split(buffer, a.len()) + } +} + +unsafe impl Sync for Buffer {} diff --git a/core/lib/src/form/strict.rs b/core/lib/src/form/strict.rs new file mode 100644 index 0000000000..39cced53a5 --- /dev/null +++ b/core/lib/src/form/strict.rs @@ -0,0 +1,129 @@ +use std::ops::{Deref, DerefMut}; + +use crate::form::prelude::*; +use crate::http::uri::{Query, FromUriParam}; + +/// A form guard for parsing form types strictly. +/// +/// This type implements the [`FromForm`] trait and thus can be used as a +/// generic parameter to the [`Form`] data guard: `Form>`, where `T` +/// implements `FromForm`. Unlike using `Form` directly, this type uses a +/// _strict_ parsing strategy: forms that contains a superset of the expected +/// fields (i.e, extra fields) will fail to parse. +/// +/// # Strictness +/// +/// A `Strict` will parse successfully from an incoming form only if +/// the form contains the exact set of fields in `T`. Said another way, a +/// `Strict` will error on missing and/or extra fields. For instance, if an +/// incoming form contains the fields "a", "b", and "c" while `T` only contains +/// "a" and "c", the form _will not_ parse as `Strict`. +/// +/// # Usage +/// +/// `Strict` implements [`FromForm`] as long as `T` implements `FromForm`. As +/// such, `Form>` is a data guard: +/// +/// ```rust +/// # #[macro_use] extern crate rocket; +/// use rocket::form::{Form, Strict}; +/// +/// #[derive(FromForm)] +/// struct UserInput { +/// value: String +/// } +/// +/// #[post("/submit", data = "")] +/// fn submit_task(user_input: Form>) -> String { +/// format!("Your value: {}", user_input.value) +/// } +/// ``` +#[derive(Debug)] +pub struct Strict(T); + +impl Strict { + /// Consumes `self` and returns the inner value. + /// + /// Note that since `Strict` implements [`Deref`] and [`DerefMut`] with + /// target `T`, reading and writing an inner value can be accomplished + /// transparently. + /// + /// # Example + /// + /// ```rust + /// # #[macro_use] extern crate rocket; + /// use rocket::form::{Form, Strict}; + /// + /// #[derive(FromForm)] + /// struct MyForm { + /// field: String, + /// } + /// + /// #[post("/submit", data = "")] + /// fn submit(form: Form>) -> String { + /// // We can read or mutate a value transparently: + /// let field: &str = &form.field; + /// + /// // To gain ownership, however, use `into_inner()`: + /// form.into_inner().into_inner().field + /// } + /// ``` + pub fn into_inner(self) -> T { + self.0 + } +} + +#[crate::async_trait] +impl<'v, T: FromForm<'v>> FromForm<'v> for Strict { + type Context = T::Context; + + #[inline(always)] + fn init(opts: Options) -> Self::Context { + T::init(Options { strict: true, ..opts }) + } + + #[inline(always)] + fn push_value(ctxt: &mut Self::Context, field: ValueField<'v>) { + T::push_value(ctxt, field) + } + + #[inline(always)] + async fn push_data(ctxt: &mut Self::Context, field: DataField<'v, '_>) { + T::push_data(ctxt, field).await + } + + #[inline(always)] + fn finalize(this: Self::Context) -> Result<'v, Self> { + T::finalize(this).map(Self) + } +} + +impl Deref for Strict { + type Target = T; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl DerefMut for Strict { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.0 + } +} + +impl From for Strict { + #[inline] + fn from(val: T) -> Strict { + Strict(val) + } +} + +impl<'f, A, T: FromUriParam + FromForm<'f>> FromUriParam for Strict { + type Target = T::Target; + + #[inline(always)] + fn from_uri_param(param: A) -> Self::Target { + T::from_uri_param(param) + } +} diff --git a/core/lib/src/form/tests.rs b/core/lib/src/form/tests.rs new file mode 100644 index 0000000000..e1fd03b315 --- /dev/null +++ b/core/lib/src/form/tests.rs @@ -0,0 +1,121 @@ +use std::collections::HashMap; + +use crate::form::*; + +fn parse<'v, T: FromForm<'v>>(values: &[&'v str]) -> Result<'v, T> { + let mut context = T::init(Options::Lenient); + values.iter().for_each(|v| T::push_value(&mut context, ValueField::parse(*v))); + T::finalize(context) +} + +macro_rules! map { + ($($key:expr => $value:expr),* $(,)?) => ({ + let mut map = std::collections::HashMap::new(); + $(map.insert($key.into(), $value.into());)* + map + }); +} + +macro_rules! vec { + ($($value:expr),* $(,)?) => ({ + let mut vec = Vec::new(); + $(vec.push($value.into());)* + vec + }); +} + +macro_rules! assert_values_parse_eq { + ($($v:expr => $T:ty = $expected:expr),* $(,)?) => ( + $( + assert_value_parse_eq!($v => $T = $expected); + )* + ) +} + +macro_rules! assert_value_parse_eq { + ($v:expr => $T:ty = $expected:expr) => ( + let expected: $T = $expected; + match parse::<$T>($v) { + Ok(actual) if actual == expected => { /* ok */ }, + Ok(actual) => { + panic!("unexpected parse of {:?}\n {:?} instead of {:?}", + $v, actual, expected) + } + Err(e) => panic!("parse of {:?} failed: {:?}", $v, e) + } + ) +} + +#[test] +fn time() { + use time::{date, time, Date, Time, PrimitiveDateTime as DateTime}; + + assert_values_parse_eq! { + &["=2010-10-20"] => Date = date!(2010-10-20), + &["=2012-01-20"] => Date = date!(2012-01-20), + &["=2020-01-20T02:30"] => DateTime = DateTime::new(date!(2020-01-20), time!(2:30)), + &["=2020-01-01T02:30:12"] => DateTime = DateTime::new(date!(2020-01-01), time!(2:30:12)), + &["=20:20:52"] => Time = time!(20:20:52), + &["=06:08"] => Time = time!(06:08), + } +} + +#[test] +fn bool() { + assert_values_parse_eq! { + &["=true", "=yes", "=on"] => Vec = vec![true, true, true], + &["=false", "=no", "=off"] => Vec = vec![false, false, false], + } +} + +#[test] +fn potpourri() { + assert_values_parse_eq! { + &["a.b=10"] => usize = 10, + &["a=10"] => u8 = 10, + &["=10"] => u8 = 10, + &["=5", "=3", "=4"] => Vec<&str> = vec!["5", "3", "4"], + &["=5", "=3", "=4"] => Vec<&str> = vec!["5", "3", "4"], + &["a=3", "b=4", "c=5"] => Vec = vec![3, 4, 5], + &["=3", "=4", "=5"] => Vec = vec![3, 4, 5], + &["=3", "=4", "=5"] => Vec> = vec![vec![3], vec![4], vec![5]], + &["[]=3", "[]=4", "[]=5"] => Vec> = vec![vec![3], vec![4], vec![5]], + &["[][]=3", "[][]=4", "[][]=5"] => Vec> = vec![vec![3], vec![4], vec![5]], + &["[]=5", "[]=3", "[]=4"] => Vec<&str> = vec!["5", "3", "4"], + &["[0]=5", "[0]=3", "=4", "=6"] => Vec> + = vec![vec![5, 3], vec![4], vec![6]], + &[".0=5", ".1=3"] => (u8, usize) = (5, 3), + &["0=5", "1=3"] => (u8, usize) = (5, 3), + &["[bob]=Robert", ".j=Jack", "s=Stan", "[s]=Steve"] => HashMap<&str, &str> + = map!["bob" => "Robert", "j" => "Jack", "s" => "Stan"], + &["[bob]=Robert", ".j=Jack", "s=Stan", "[s]=Steve"] + => HashMap<&str, Vec<&str>> + = map![ + "bob" => vec!["Robert"], + "j" => vec!["Jack"], + "s" => vec!["Stan", "Steve"] + ], + &["[k:0]=5", "[k:0]=3", "[v:0]=20", "[56]=2"] => HashMap, usize> + = map![vec!["5", "3"] => 20u8, vec!["56"] => 2u8], + &["[k:0]=5", "[k:0]=3", "[0]=20", "[56]=2"] => HashMap, usize> + = map![vec!["5", "3"] => 20u8, vec!["56"] => 2u8], + &[ + "[k:a]0=5", "[a]=hi", "[v:b][0]=10", "[k:b].0=1", + "[k:b].1=hi", "[a]=hey", "[k:a]1=3" + ] => HashMap<(usize, &str), Vec<&str>> + = map![ + (5, "3".into()) => vec!["hi", "hey"], + (1, "hi".into()) => vec!["10"] + ], + &[ + "[0][hi]=10", "[0][hey]=12", "[1][bob]=0", "[1].blam=58", "[].0=1", + "[].whoops=999", + ] => Vec> + = vec![ + map!["hi" => 10u8, "hey" => 12u8], + map!["bob" => 0u8, "blam" => 58u8], + map!["0" => 1u8], + map!["whoops" => 999usize] + ], + } +} diff --git a/core/lib/src/form/validate.rs b/core/lib/src/form/validate.rs new file mode 100644 index 0000000000..ffd53f7dee --- /dev/null +++ b/core/lib/src/form/validate.rs @@ -0,0 +1,246 @@ +use std::{borrow::Cow, convert::TryInto, fmt::Display, ops::{RangeBounds, Bound}}; + +use rocket_http::ContentType; + +use crate::{data::TempFile, form::error::{Error, Errors}}; + +pub fn eq<'v, A, B>(a: &A, b: B) -> Result<(), Errors<'v>> + where A: PartialEq +{ + if a != &b { + Err(Error::validation("value does not match"))? + } + + Ok(()) +} + +pub trait Len { + fn len(&self) -> usize; + + fn len_u64(&self) -> u64 { + self.len() as u64 + } +} + +impl Len for str { + fn len(&self) -> usize { self.len() } +} + +impl Len for String { + fn len(&self) -> usize { self.len() } +} + +impl Len for Vec { + fn len(&self) -> usize { >::len(self) } +} + +impl Len for TempFile<'_> { + fn len(&self) -> usize { TempFile::len(self) as usize } + + fn len_u64(&self) -> u64 { TempFile::len(self) } +} + +impl Len for std::collections::HashMap { + fn len(&self) -> usize { >::len(self) } +} + +impl Len for &T { + fn len(&self) -> usize { + ::len(self) + } +} + +pub fn len<'v, V, R>(value: V, range: R) -> Result<(), Errors<'v>> + where V: Len, R: RangeBounds +{ + if !range.contains(&value.len_u64()) { + let start = match range.start_bound() { + Bound::Included(v) => Some(*v), + Bound::Excluded(v) => Some(v.saturating_add(1)), + Bound::Unbounded => None + }; + + let end = match range.end_bound() { + Bound::Included(v) => Some(*v), + Bound::Excluded(v) => Some(v.saturating_sub(1)), + Bound::Unbounded => None, + }; + + Err((start, end))? + } + + Ok(()) +} + +pub trait Contains { + fn contains(&self, item: I) -> bool; +} + +impl> Contains for &T { + fn contains(&self, item: I) -> bool { + >::contains(self, item) + } +} + +impl Contains<&str> for str { + fn contains(&self, string: &str) -> bool { + ::contains(self, string) + } +} + +impl Contains<&&str> for str { + fn contains(&self, string: &&str) -> bool { + ::contains(self, string) + } +} + +impl Contains for str { + fn contains(&self, c: char) -> bool { + ::contains(self, c) + } +} + +impl Contains<&char> for str { + fn contains(&self, c: &char) -> bool { + ::contains(self, *c) + } +} + +impl Contains<&str> for &str { + fn contains(&self, string: &str) -> bool { + ::contains(self, string) + } +} + +impl Contains<&&str> for &str { + fn contains(&self, string: &&str) -> bool { + ::contains(self, string) + } +} + +impl Contains for &str { + fn contains(&self, c: char) -> bool { + ::contains(self, c) + } +} + +impl Contains<&char> for &str { + fn contains(&self, c: &char) -> bool { + ::contains(self, *c) + } +} + +impl Contains<&str> for String { + fn contains(&self, string: &str) -> bool { + ::contains(self, string) + } +} + +impl Contains<&&str> for String { + fn contains(&self, string: &&str) -> bool { + ::contains(self, string) + } +} + +impl Contains for String { + fn contains(&self, c: char) -> bool { + ::contains(self, c) + } +} + +impl Contains<&char> for String { + fn contains(&self, c: &char) -> bool { + ::contains(self, *c) + } +} + +impl Contains for Vec { + fn contains(&self, item: T) -> bool { + <[T]>::contains(self, &item) + } +} + +impl Contains<&T> for Vec { + fn contains(&self, item: &T) -> bool { + <[T]>::contains(self, item) + } +} + +pub fn contains<'v, V, I>(value: V, item: I) -> Result<(), Errors<'v>> + where V: for<'a> Contains<&'a I>, I: std::fmt::Debug +{ + if !value.contains(&item) { + Err(Error::validation(format!("must contain {:?}", item)))? + } + + Ok(()) +} + +pub fn omits<'v, V, I>(value: V, item: I) -> Result<(), Errors<'v>> + where V: for<'a> Contains<&'a I>, I: std::fmt::Debug +{ + if value.contains(&item) { + Err(Error::validation(format!("cannot contain {:?}", item)))? + } + + Ok(()) +} + +pub fn range<'v, V, R>(value: &V, range: R) -> Result<(), Errors<'v>> + where V: TryInto + Copy, R: RangeBounds +{ + if let Ok(v) = (*value).try_into() { + if range.contains(&v) { + return Ok(()); + } + } + + let start = match range.start_bound() { + Bound::Included(v) => Some(*v), + Bound::Excluded(v) => Some(v.saturating_add(1)), + Bound::Unbounded => None + }; + + let end = match range.end_bound() { + Bound::Included(v) => Some(*v), + Bound::Excluded(v) => Some(v.saturating_sub(1)), + Bound::Unbounded => None, + }; + + + Err((start, end))? +} + +pub fn one_of<'v, V, I>(value: V, items: &[I]) -> Result<(), Errors<'v>> + where V: for<'a> Contains<&'a I>, I: Display +{ + for item in items { + if value.contains(item) { + return Ok(()); + } + } + + let choices = items.iter() + .map(|item| item.to_string().into()) + .collect::>>(); + + Err(choices)? +} + +pub fn ext<'v>(file: &TempFile<'_>, ext: &str) -> Result<(), Errors<'v>> { + if let Some(file_ct) = file.content_type() { + if let Some(ext_ct) = ContentType::from_extension(ext) { + if file_ct == &ext_ct { + return Ok(()); + } + + let m = file_ct.extension() + .map(|fext| format!("file type was .{} but must be .{}", fext, ext)) + .unwrap_or_else(|| format!("file type must be .{}", ext)); + + Err(Error::validation(m))? + } + } + + Err(Error::validation(format!("invalid extension: expected {}", ext)))? +} diff --git a/core/lib/src/lib.rs b/core/lib/src/lib.rs index e9cd26a915..a453e552c4 100644 --- a/core/lib/src/lib.rs +++ b/core/lib/src/lib.rs @@ -79,10 +79,11 @@ //! //! ## Configuration //! -//! Rocket and Rocket libraries are configured via the `Rocket.toml` file and/or -//! `ROCKET_{PARAM}` environment variables. For more information on how to -//! configure Rocket, see the [configuration section] of the guide as well as -//! the [`config`] module documentation. +//! By default, Rocket applications are configured via a `Rocket.toml` file +//! and/or `ROCKET_{PARAM}` environment variables. For more information on how +//! to configure Rocket, including how to completely customize configuration +//! sources, see the [configuration section] of the guide as well as the +//! [`config`] module documentation. //! //! [configuration section]: https://rocket.rs/master/guide/configuration/ //! @@ -109,13 +110,15 @@ pub use futures; pub use tokio; pub use figment; -#[doc(hidden)] #[macro_use] pub mod logger; +#[doc(hidden)] +#[macro_use] pub mod logger; #[macro_use] pub mod outcome; +#[macro_use] pub mod data; pub mod local; pub mod request; pub mod response; pub mod config; -pub mod data; +pub mod form; pub mod handler; pub mod fairing; pub mod error; @@ -138,6 +141,7 @@ mod rocket; mod server; mod codegen; mod ext; +mod state; #[doc(hidden)] pub use log::{info, warn, error, debug}; #[doc(inline)] pub use crate::response::Response; @@ -146,17 +150,18 @@ mod ext; #[doc(inline)] pub use crate::config::Config; #[doc(inline)] pub use crate::catcher::Catcher; pub use crate::router::Route; -pub use crate::request::{Request, State}; +pub use crate::request::Request; pub use crate::rocket::Rocket; pub use crate::shutdown::Shutdown; +pub use crate::state::State; -/// Alias to [`Rocket::ignite()`] Creates a new instance of `Rocket`. +/// Creates a new instance of `Rocket`: aliases [`Rocket::ignite()`]. pub fn ignite() -> Rocket { Rocket::ignite() } -/// Alias to [`Rocket::custom()`]. Creates a new instance of `Rocket` with a -/// custom configuration provider. +/// Creates a new instance of `Rocket` with a custom configuration provider: +/// aliases [`Rocket::custom()`]. pub fn custom(provider: T) -> Rocket { Rocket::custom(provider) } diff --git a/core/lib/src/local/asynchronous/request.rs b/core/lib/src/local/asynchronous/request.rs index b16dd1fc91..2fbd0f7538 100644 --- a/core/lib/src/local/asynchronous/request.rs +++ b/core/lib/src/local/asynchronous/request.rs @@ -97,7 +97,7 @@ impl<'c> LocalRequest<'c> { self.client._with_raw_cookies_mut(|jar| { let current_time = time::OffsetDateTime::now_utc(); for cookie in response.cookies().iter() { - if let Some(expires) = cookie.expires() { + if let Some(expires) = cookie.expires_datetime() { if expires <= current_time { jar.force_remove(cookie); continue; diff --git a/core/lib/src/logger.rs b/core/lib/src/logger.rs index 3e825d576d..70f1cffbd9 100644 --- a/core/lib/src/logger.rs +++ b/core/lib/src/logger.rs @@ -208,7 +208,7 @@ pub fn init(level: LogLevel) -> bool { macro_rules! external_log_function { ($fn_name:ident: $macro_name:ident) => ( #[doc(hidden)] #[inline(always)] - pub fn $fn_name(msg: &str) { $macro_name!("{}", msg); } + pub fn $fn_name(msg: T) { $macro_name!("{}", msg); } ) } diff --git a/core/lib/src/outcome.rs b/core/lib/src/outcome.rs index 3e3810c1c8..b0e23e6ba9 100644 --- a/core/lib/src/outcome.rs +++ b/core/lib/src/outcome.rs @@ -9,11 +9,11 @@ //! processing next. //! //! The `Outcome` type is the return type of many of the core Rocket traits, -//! including [`FromRequest`](crate::request::FromRequest), -//! [`FromTransformedData`] [`Responder`]. It is also the return type of request -//! handlers via the [`Response`](crate::response::Response) type. +//! including [`FromRequest`](crate::request::FromRequest), [`FromData`] +//! [`Responder`]. It is also the return type of request handlers via the +//! [`Response`](crate::response::Response) type. //! -//! [`FromTransformedData`]: crate::data::FromTransformedData +//! [`FromData`]: crate::data::FromData //! [`Responder`]: crate::response::Responder //! //! # Success @@ -21,7 +21,7 @@ //! A successful `Outcome`, `Success(S)`, is returned from functions //! that complete successfully. The meaning of a `Success` outcome depends on //! the context. For instance, the `Outcome` of the `from_data` method of the -//! [`FromTransformedData`] trait will be matched against the type expected by +//! [`FromData`] trait will be matched against the type expected by //! the user. For example, consider the following handler: //! //! ```rust @@ -31,10 +31,9 @@ //! fn hello(my_val: S) { /* ... */ } //! ``` //! -//! The [`FromTransformedData`] implementation for the type `S` returns an -//! `Outcome` with a `Success(S)`. If `from_data` returns a `Success`, the -//! `Success` value will be unwrapped and the value will be used as the value of -//! `my_val`. +//! The [`FromData`] implementation for the type `S` returns an `Outcome` with a +//! `Success(S)`. If `from_data` returns a `Success`, the `Success` value will +//! be unwrapped and the value will be used as the value of `my_val`. //! //! # Failure //! @@ -56,11 +55,11 @@ //! fn hello(my_val: Result) { /* ... */ } //! ``` //! -//! The [`FromTransformedData`] implementation for the type `S` returns an -//! `Outcome` with a `Success(S)` and `Failure(E)`. If `from_data` returns a -//! `Failure`, the `Failure` value will be unwrapped and the value will be used -//! as the `Err` value of `my_val` while a `Success` will be unwrapped and used -//! the `Ok` value. +//! The [`FromData`] implementation for the type `S` returns an `Outcome` with a +//! `Success(S)` and `Failure(E)`. If `from_data` returns a `Failure`, the +//! `Failure` value will be unwrapped and the value will be used as the `Err` +//! value of `my_val` while a `Success` will be unwrapped and used the `Ok` +//! value. //! //! # Forward //! @@ -79,14 +78,14 @@ //! fn hello(my_val: S) { /* ... */ } //! ``` //! -//! The [`FromTransformedData`] implementation for the type `S` returns an -//! `Outcome` with a `Success(S)`, `Failure(E)`, and `Forward(F)`. If the -//! `Outcome` is a `Forward`, the `hello` handler isn't called. Instead, the -//! incoming request is forwarded, or passed on to, the next matching route, if -//! any. Ultimately, if there are no non-forwarding routes, forwarded requests -//! are handled by the 404 catcher. Similar to `Failure`s, users can catch -//! `Forward`s by requesting a type of `Option`. If an `Outcome` is a -//! `Forward`, the `Option` will be `None`. +//! The [`FromData`] implementation for the type `S` returns an `Outcome` with a +//! `Success(S)`, `Failure(E)`, and `Forward(F)`. If the `Outcome` is a +//! `Forward`, the `hello` handler isn't called. Instead, the incoming request +//! is forwarded, or passed on to, the next matching route, if any. Ultimately, +//! if there are no non-forwarding routes, forwarded requests are handled by the +//! 404 catcher. Similar to `Failure`s, users can catch `Forward`s by requesting +//! a type of `Option`. If an `Outcome` is a `Forward`, the `Option` will be +//! `None`. use std::fmt; @@ -215,10 +214,7 @@ impl Outcome { /// ``` #[inline] pub fn is_success(&self) -> bool { - match *self { - Success(_) => true, - _ => false - } + matches!(self, Success(_)) } /// Return true if this `Outcome` is a `Failure`. @@ -240,10 +236,7 @@ impl Outcome { /// ``` #[inline] pub fn is_failure(&self) -> bool { - match *self { - Failure(_) => true, - _ => false - } + matches!(self, Failure(_)) } /// Return true if this `Outcome` is a `Forward`. @@ -265,10 +258,7 @@ impl Outcome { /// ``` #[inline] pub fn is_forward(&self) -> bool { - match *self { - Forward(_) => true, - _ => false - } + matches!(self, Forward(_)) } /// Converts from `Outcome` to `Option`. @@ -349,7 +339,8 @@ impl Outcome { } } - /// Converts from `Outcome` to `Result` for a given `T`. + /// Returns a `Success` value as `Ok()` or `value` in `Err`. Converts from + /// `Outcome` to `Result` for a given `T`. /// /// Returns `Ok` with the `Success` value if this is a `Success`, otherwise /// returns an `Err` with the provided value. `self` is consumed, and all @@ -376,8 +367,9 @@ impl Outcome { } } - /// Converts from `Outcome` to `Result` for a given `T` - /// produced from a supplied function or closure. + /// Returns a `Success` value as `Ok()` or `f()` in `Err`. Converts from + /// `Outcome` to `Result` for a given `T` produced from a + /// supplied function or closure. /// /// Returns `Ok` with the `Success` value if this is a `Success`, otherwise /// returns an `Err` with the result of calling `f`. `self` is consumed, and @@ -425,9 +417,9 @@ impl Outcome { } } - /// Maps an `Outcome` to an `Outcome` by applying the - /// function `f` to the value of type `S` in `self` if `self` is an - /// `Outcome::Success`. + /// Maps the `Success` value using `f`. Maps an `Outcome` to an + /// `Outcome` by applying the function `f` to the value of type `S` + /// in `self` if `self` is an `Outcome::Success`. /// /// ```rust /// # use rocket::outcome::Outcome; @@ -447,9 +439,9 @@ impl Outcome { } } - /// Maps an `Outcome` to an `Outcome` by applying the - /// function `f` to the value of type `E` in `self` if `self` is an - /// `Outcome::Failure`. + /// Maps the `Failure` value using `f`. Maps an `Outcome` to an + /// `Outcome` by applying the function `f` to the value of type `E` + /// in `self` if `self` is an `Outcome::Failure`. /// /// ```rust /// # use rocket::outcome::Outcome; @@ -469,9 +461,9 @@ impl Outcome { } } - /// Maps an `Outcome` to an `Outcome` by applying the - /// function `f` to the value of type `F` in `self` if `self` is an - /// `Outcome::Forward`. + /// Maps the `Forward` value using `f`. Maps an `Outcome` to an + /// `Outcome` by applying the function `f` to the value of type `F` + /// in `self` if `self` is an `Outcome::Forward`. /// /// ```rust /// # use rocket::outcome::Outcome; @@ -491,9 +483,10 @@ impl Outcome { } } - /// Maps an `Outcome` to an `Outcome` by applying the - /// function `f` to the value of type `S` in `self` if `self` is an - /// `Outcome::Success`. + /// Maps the `Success` value using `f()`, returning the `Outcome` from `f()` + /// or the original `self` if `self` is not `Success`. Maps an `Outcome` to an `Outcome` by applying the function `f` to the + /// value of type `S` in `self` if `self` is an `Outcome::Success`. /// /// # Examples /// @@ -520,6 +513,8 @@ impl Outcome { } } + /// Maps the `Failure` value using `f()`, returning the `Outcome` from `f()` + /// or the original `self` if `self` is not `Failure`. Maps an `Outcome` to an `Outcome` by applying the /// function `f` to the value of type `E` in `self` if `self` is an /// `Outcome::Failure`. @@ -549,9 +544,10 @@ impl Outcome { } } - /// Maps an `Outcome` to an `Outcome` by applying the - /// function `f` to the value of type `F` in `self` if `self` is an - /// `Outcome::Forward`. + /// Maps the `Forward` value using `f()`, returning the `Outcome` from `f()` + /// or the original `self` if `self` is not `Forward`. Maps an `Outcome` to an `Outcome` by applying the function `f` to the + /// value of type `F` in `self` if `self` is an `Outcome::Forward`. /// /// # Examples /// @@ -616,10 +612,11 @@ impl<'a, S: Send + 'a, E: Send + 'a, F: Send + 'a> Outcome { } } -/// Unwraps an [`Outcome`] to its success value, otherwise propagating the -/// forward or failure. +/// Unwraps a [`Success`](Outcome::Success) or propagates a `Forward` or +/// `Failure`. /// -/// In the case of a `Forward` or `Failure` variant, the inner type is passed to +/// This is just like `?` (or previously, `try!`), but for `Outcome`. In the +/// case of a `Forward` or `Failure` variant, the inner type is passed to /// [`From`](std::convert::From), allowing for the conversion between specific /// and more general types. The resulting forward/error is immediately returned. /// @@ -632,8 +629,10 @@ impl<'a, S: Send + 'a, E: Send + 'a, F: Send + 'a> Outcome { /// /// ```rust,no_run /// # #[macro_use] extern crate rocket; -/// # use std::sync::atomic::{AtomicUsize, Ordering}; -/// use rocket::request::{self, Request, FromRequest, State}; +/// use std::sync::atomic::{AtomicUsize, Ordering}; +/// +/// use rocket::State; +/// use rocket::request::{self, Request, FromRequest}; /// use rocket::outcome::Outcome::*; /// /// #[derive(Default)] diff --git a/core/lib/src/request/form/error.rs b/core/lib/src/request/form/error.rs deleted file mode 100644 index a707b73533..0000000000 --- a/core/lib/src/request/form/error.rs +++ /dev/null @@ -1,73 +0,0 @@ -use std::io; -use crate::http::RawStr; - -/// Error returned by the [`FromForm`](crate::request::FromForm) derive on form -/// parsing errors. -/// -/// If multiple errors occur while parsing a form, the first error in the -/// following precedence, from highest to lowest, is returned: -/// -/// * `BadValue` or `Unknown` in incoming form string field order -/// * `Missing` in lexical field order -#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)] -pub enum FormParseError<'f> { - /// The field named `.0` with value `.1` failed to parse or validate. - BadValue(&'f RawStr, &'f RawStr), - /// The parse was strict and the field named `.0` with value `.1` appeared - /// in the incoming form string but was unexpected. - /// - /// This error cannot occur when parsing is lenient. - Unknown(&'f RawStr, &'f RawStr), - /// The field named `.0` was expected but is missing in the incoming form. - Missing(&'f RawStr), -} - -/// Error returned by the [`FromTransformedData`](crate::data::FromTransformedData) implementations of -/// [`Form`](crate::request::Form) and [`LenientForm`](crate::request::LenientForm). -#[derive(Debug)] -pub enum FormDataError<'f, E> { - /// An I/O error occurred while reading reading the data stream. This can - /// also mean that the form contained invalid UTF-8. - Io(io::Error), - /// The form string (in `.0`) is malformed and was unable to be parsed as - /// HTTP `application/x-www-form-urlencoded` data. - Malformed(&'f str), - /// The form string (in `.1`) failed to parse as the intended structure. The - /// error type in `.0` contains further details. - Parse(E, &'f str) -} - -/// Alias to the type of form errors returned by the [`FromTransformedData`] -/// implementations of [`Form`] where the [`FromForm`] implementation for `T` -/// was derived. -/// -/// This alias is particularly useful when "catching" form errors in routes. -/// -/// [`FromTransformedData`]: crate::data::FromTransformedData -/// [`Form`]: crate::request::Form -/// [`FromForm`]: crate::request::FromForm -/// -/// # Example -/// -/// ```rust -/// # #[macro_use] extern crate rocket; -/// use rocket::request::{Form, FormError, FormDataError}; -/// -/// #[derive(FromForm)] -/// struct Input { -/// value: String, -/// } -/// -/// #[post("/", data = "")] -/// fn submit(sink: Result, FormError>) -> String { -/// match sink { -/// Ok(form) => form.into_inner().value, -/// Err(FormDataError::Io(_)) => "I/O error".into(), -/// Err(FormDataError::Malformed(f)) | Err(FormDataError::Parse(_, f)) => { -/// format!("invalid form input: {}", f) -/// } -/// } -/// } -/// # fn main() {} -/// ``` -pub type FormError<'f> = FormDataError<'f, FormParseError<'f>>; diff --git a/core/lib/src/request/form/form.rs b/core/lib/src/request/form/form.rs deleted file mode 100644 index 5b91e6489c..0000000000 --- a/core/lib/src/request/form/form.rs +++ /dev/null @@ -1,235 +0,0 @@ -use std::ops::{Deref, DerefMut}; - -use crate::outcome::Outcome::*; -use crate::request::{Request, form::{FromForm, FormItems, FormDataError}}; -use crate::data::{Data, Outcome, Transform, Transformed, ToByteUnit}; -use crate::data::{TransformFuture, FromTransformedData, FromDataFuture}; -use crate::http::{Status, uri::{Query, FromUriParam}}; - -/// A data guard for parsing [`FromForm`] types strictly. -/// -/// This type implements the [`FromTransformedData`] trait. It provides a -/// generic means to parse arbitrary structures from incoming form data. -/// -/// # Strictness -/// -/// A `Form` will parse successfully from an incoming form only if the form -/// contains the exact set of fields in `T`. Said another way, a `Form` will -/// error on missing and/or extra fields. For instance, if an incoming form -/// contains the fields "a", "b", and "c" while `T` only contains "a" and "c", -/// the form _will not_ parse as `Form`. If you would like to admit extra -/// fields without error, see [`LenientForm`](crate::request::LenientForm). -/// -/// # Usage -/// -/// This type can be used with any type that implements the `FromForm` trait. -/// The trait can be automatically derived; see the [`FromForm`] documentation -/// for more information on deriving or implementing the trait. -/// -/// Because `Form` implements `FromTransformedData`, it can be used directly as a target of -/// the `data = ""` route parameter as long as its generic type -/// implements the `FromForm` trait: -/// -/// ```rust -/// # #[macro_use] extern crate rocket; -/// use rocket::request::Form; -/// use rocket::http::RawStr; -/// -/// #[derive(FromForm)] -/// struct UserInput<'f> { -/// // The raw, undecoded value. You _probably_ want `String` instead. -/// value: &'f RawStr -/// } -/// -/// #[post("/submit", data = "")] -/// fn submit_task(user_input: Form) -> String { -/// format!("Your value: {}", user_input.value) -/// } -/// # fn main() { } -/// ``` -/// -/// A type of `Form` automatically dereferences into an `&T` or `&mut T`, -/// though you can also transform a `Form` into a `T` by calling -/// [`into_inner()`](Form::into_inner()). Thanks to automatic dereferencing, you -/// can access fields of `T` transparently through a `Form`, as seen above -/// with `user_input.value`. -/// -/// For posterity, the owned analog of the `UserInput` type above is: -/// -/// ```rust -/// struct OwnedUserInput { -/// // The decoded value. You _probably_ want this. -/// value: String -/// } -/// ``` -/// -/// A handler that handles a form of this type can similarly by written: -/// -/// ```rust -/// # #![allow(deprecated, unused_attributes)] -/// # #[macro_use] extern crate rocket; -/// # use rocket::request::Form; -/// # #[derive(FromForm)] -/// # struct OwnedUserInput { -/// # value: String -/// # } -/// #[post("/submit", data = "")] -/// fn submit_task(user_input: Form) -> String { -/// format!("Your value: {}", user_input.value) -/// } -/// # fn main() { } -/// ``` -/// -/// Note that no lifetime annotations are required in either case. -/// -/// ## `&RawStr` vs. `String` -/// -/// Whether you should use a `&RawStr` or `String` in your `FromForm` type -/// depends on your use case. The primary question to answer is: _Can the input -/// contain characters that must be URL encoded?_ Note that this includes common -/// characters such as spaces. If so, then you must use `String`, whose -/// [`FromFormValue`](crate::request::FromFormValue) implementation automatically URL -/// decodes the value. Because the `&RawStr` references will refer directly to -/// the underlying form data, they will be raw and URL encoded. -/// -/// If it is known that string values will not contain URL encoded characters, -/// or you wish to handle decoding and validation yourself, using `&RawStr` will -/// result in fewer allocation and is thus preferred. -/// -/// ## Incoming Data Limits -/// -/// The default size limit for incoming form data is 32KiB. Setting a limit -/// protects your application from denial of service (DOS) attacks and from -/// resource exhaustion through high memory consumption. The limit can be -/// increased by setting the `limits.forms` configuration parameter. For -/// instance, to increase the forms limit to 512KiB for all environments, you -/// may add the following to your `Rocket.toml`: -/// -/// ```toml -/// [global.limits] -/// forms = 524288 -/// ``` -#[derive(Debug)] -pub struct Form(pub T); - -impl Form { - /// Consumes `self` and returns the parsed value. - /// - /// # Example - /// - /// ```rust - /// # #[macro_use] extern crate rocket; - /// use rocket::request::Form; - /// - /// #[derive(FromForm)] - /// struct MyForm { - /// field: String, - /// } - /// - /// #[post("/submit", data = "")] - /// fn submit(form: Form) -> String { - /// form.into_inner().field - /// } - /// # fn main() { } - /// ``` - #[inline(always)] - pub fn into_inner(self) -> T { - self.0 - } -} - -impl Deref for Form { - type Target = T; - - fn deref(&self) -> &T { - &self.0 - } -} - -impl DerefMut for Form { - fn deref_mut(&mut self) -> &mut T { - &mut self.0 - } -} - -impl<'f, T: FromForm<'f>> Form { - pub(crate) fn from_data( - form_str: &'f str, - strict: bool - ) -> Outcome> { - use self::FormDataError::*; - - let mut items = FormItems::from(form_str); - let result = T::from_form(&mut items, strict); - if !items.exhaust() { - error_!("The request's form string was malformed."); - return Failure((Status::BadRequest, Malformed(form_str))); - } - - match result { - Ok(v) => Success(v), - Err(e) => { - error_!("The incoming form failed to parse."); - Failure((Status::UnprocessableEntity, Parse(e, form_str))) - } - } - } -} - -/// Parses a `Form` from incoming form data. -/// -/// If the content type of the request data is not -/// `application/x-www-form-urlencoded`, `Forward`s the request. If the form -/// data cannot be parsed into a `T`, a `Failure` with status code -/// `UnprocessableEntity` is returned. If the form string is malformed, a -/// `Failure` with status code `BadRequest` is returned. Finally, if reading the -/// incoming stream fails, returns a `Failure` with status code -/// `InternalServerError`. In all failure cases, the raw form string is returned -/// if it was able to be retrieved from the incoming stream. -/// -/// All relevant warnings and errors are written to the console in Rocket -/// logging format. -impl<'r, T: FromForm<'r> + Send + 'r> FromTransformedData<'r> for Form { - type Error = FormDataError<'r, T::Error>; - type Owned = String; - type Borrowed = str; - - fn transform( - request: &'r Request<'_>, - data: Data - ) -> TransformFuture<'r, Self::Owned, Self::Error> { - Box::pin(async move { - if !request.content_type().map_or(false, |ct| ct.is_form()) { - warn_!("Form data does not have form content type."); - return Transform::Borrowed(Forward(data)); - } - - let limit = request.limits().get("forms").unwrap_or(32.kibibytes()); - match data.open(limit).stream_to_string().await { - Ok(form_string) => Transform::Borrowed(Success(form_string)), - Err(e) => { - let err = (Status::InternalServerError, FormDataError::Io(e)); - Transform::Borrowed(Failure(err)) - } - } - }) - } - - fn from_data( - _: &'r Request<'_>, - o: Transformed<'r, Self> - ) -> FromDataFuture<'r, Self, Self::Error> { - Box::pin(async move { - o.borrowed().and_then(|data| >::from_data(data, true).map(Form)) - }) - } -} - -impl<'r, A, T: FromUriParam + FromForm<'r>> FromUriParam for Form { - type Target = T::Target; - - #[inline(always)] - fn from_uri_param(param: A) -> Self::Target { - T::from_uri_param(param) - } -} diff --git a/core/lib/src/request/form/form_items.rs b/core/lib/src/request/form/form_items.rs deleted file mode 100644 index f1d5c6357b..0000000000 --- a/core/lib/src/request/form/form_items.rs +++ /dev/null @@ -1,484 +0,0 @@ -use memchr::memchr2; - -use crate::http::RawStr; - -/// Iterator over the key/value pairs of a given HTTP form string. -/// -/// ```rust -/// use rocket::request::{FormItems, FromFormValue}; -/// -/// // Using the `key_value_decoded` method of `FormItem`. -/// let form_string = "greeting=Hello%2C+Mark%21&username=jake%2Fother"; -/// for (key, value) in FormItems::from(form_string).map(|i| i.key_value_decoded()) { -/// match &*key { -/// "greeting" => assert_eq!(value, "Hello, Mark!".to_string()), -/// "username" => assert_eq!(value, "jake/other".to_string()), -/// _ => unreachable!() -/// } -/// } -/// -/// // Accessing the fields of `FormItem` directly, including `raw`. -/// for item in FormItems::from(form_string) { -/// match item.key.as_str() { -/// "greeting" => { -/// assert_eq!(item.raw, "greeting=Hello%2C+Mark%21"); -/// assert_eq!(item.value, "Hello%2C+Mark%21"); -/// assert_eq!(item.value.url_decode(), Ok("Hello, Mark!".into())); -/// } -/// "username" => { -/// assert_eq!(item.raw, "username=jake%2Fother"); -/// assert_eq!(item.value, "jake%2Fother"); -/// assert_eq!(item.value.url_decode(), Ok("jake/other".into())); -/// } -/// _ => unreachable!() -/// } -/// } -/// ``` -/// -/// # Form Items via. `FormItem` -/// -/// This iterator returns values of the type [`FormItem`]. To access the -/// associated key/value pairs of the form item, either directly access them via -/// the [`key`](FormItem::key) and [`value`](FormItem::value) fields, use the -/// [`FormItem::key_value()`] method to get a tuple of the _raw_ `(key, value)`, -/// or use the [`key_value_decoded()`](FormItem::key_value_decoded()) method to -/// get a tuple of the decoded (`key`, `value`). -/// -/// # Completion -/// -/// The iterator keeps track of whether the form string was parsed to completion -/// to determine if the form string was malformed. The iterator can be queried -/// for completion via the [`completed()`](#method.completed) method, which -/// returns `true` if the iterator parsed the entire string that was passed to -/// it. The iterator can also attempt to parse any remaining contents via -/// [`exhaust()`](#method.exhaust); this method returns `true` if exhaustion -/// succeeded. -/// -/// This iterator guarantees that all valid form strings are parsed to -/// completion. The iterator attempts to be lenient. In particular, it allows -/// the following oddball behavior: -/// -/// * Trailing and consecutive `&` characters are allowed. -/// * Empty keys and/or values are allowed. -/// -/// Additionally, the iterator skips items with both an empty key _and_ an empty -/// value: at least one of the two must be non-empty to be returned from this -/// iterator. -/// -/// # Examples -/// -/// `FormItems` can be used directly as an iterator: -/// -/// ```rust -/// use rocket::request::FormItems; -/// -/// // prints "greeting = hello", "username = jake", and "done = " -/// let form_string = "greeting=hello&username=jake&done"; -/// for (key, value) in FormItems::from(form_string).map(|item| item.key_value()) { -/// println!("{} = {}", key, value); -/// } -/// ``` -/// -/// This is the same example as above, but the iterator is used explicitly. -/// -/// ```rust -/// use rocket::request::FormItems; -/// -/// let form_string = "greeting=hello&username=jake&done"; -/// let mut items = FormItems::from(form_string); -/// -/// let next = items.next().unwrap(); -/// assert_eq!(next.key, "greeting"); -/// assert_eq!(next.value, "hello"); -/// -/// let next = items.next().unwrap(); -/// assert_eq!(next.key, "username"); -/// assert_eq!(next.value, "jake"); -/// -/// let next = items.next().unwrap(); -/// assert_eq!(next.key, "done"); -/// assert_eq!(next.value, ""); -/// -/// assert_eq!(items.next(), None); -/// assert!(items.completed()); -/// ``` -#[derive(Debug)] -pub enum FormItems<'f> { - #[doc(hidden)] - Raw { - string: &'f RawStr, - next_index: usize - }, - #[doc(hidden)] - Cooked { - items: &'f [FormItem<'f>], - next_index: usize - } -} - -/// A form items returned by the [`FormItems`] iterator. -#[derive(Debug, Copy, Clone, PartialEq, Eq)] -pub struct FormItem<'f> { - /// The full, nonempty string for the item, not including `&` delimiters. - pub raw: &'f RawStr, - /// The key for the item, which may be empty if `value` is nonempty. - /// - /// **Note:** The key is _not_ URL decoded. To URL decode the raw strings, - /// use the [`RawStr::url_decode()`] method or access key-value pairs with - /// [`key_value_decoded()`](FormItem::key_value_decoded()). - pub key: &'f RawStr, - /// The value for the item, which may be empty if `key` is nonempty. - /// - /// **Note:** The value is _not_ URL decoded. To URL decode the raw strings, - /// use the [`RawStr::url_decode()`] method or access key-value pairs with - /// [`key_value_decoded()`](FormItem::key_value_decoded()). - pub value: &'f RawStr -} - -impl<'f> FormItem<'f> { - /// Extracts the raw `key` and `value` as a tuple. - /// - /// This is equivalent to `(item.key, item.value)`. - /// - /// # Example - /// - /// ```rust - /// use rocket::request::FormItem; - /// - /// let item = FormItem { - /// raw: "hello=%2C+world%21".into(), - /// key: "hello".into(), - /// value: "%2C+world%21".into(), - /// }; - /// - /// let (key, value) = item.key_value(); - /// assert_eq!(key, "hello"); - /// assert_eq!(value, "%2C+world%21"); - /// ``` - #[inline(always)] - pub fn key_value(&self) -> (&'f RawStr, &'f RawStr) { - (self.key, self.value) - } - - /// Extracts and lossy URL decodes the `key` and `value` as a tuple. - /// - /// This is equivalent to `(item.key.url_decode_lossy(), - /// item.value.url_decode_lossy)`. - /// - /// # Example - /// - /// ```rust - /// use rocket::request::FormItem; - /// - /// let item = FormItem { - /// raw: "hello=%2C+world%21".into(), - /// key: "hello".into(), - /// value: "%2C+world%21".into(), - /// }; - /// - /// let (key, value) = item.key_value_decoded(); - /// assert_eq!(key, "hello"); - /// assert_eq!(value, ", world!"); - /// ``` - #[inline(always)] - pub fn key_value_decoded(&self) -> (String, String) { - (self.key.url_decode_lossy(), self.value.url_decode_lossy()) - } - - /// Extracts `raw` and the raw `key` and `value` as a triple. - /// - /// This is equivalent to `(item.raw, item.key, item.value)`. - /// - /// # Example - /// - /// ```rust - /// use rocket::request::FormItem; - /// - /// let item = FormItem { - /// raw: "hello=%2C+world%21".into(), - /// key: "hello".into(), - /// value: "%2C+world%21".into(), - /// }; - /// - /// let (raw, key, value) = item.explode(); - /// assert_eq!(raw, "hello=%2C+world%21"); - /// assert_eq!(key, "hello"); - /// assert_eq!(value, "%2C+world%21"); - /// ``` - #[inline(always)] - pub fn explode(&self) -> (&'f RawStr, &'f RawStr, &'f RawStr) { - (self.raw, self.key, self.value) - } -} - -impl FormItems<'_> { - /// Returns `true` if the form string was parsed to completion. Returns - /// `false` otherwise. All valid form strings will parse to completion, - /// while invalid form strings will not. - /// - /// # Example - /// - /// A valid form string parses to completion: - /// - /// ```rust - /// use rocket::request::FormItems; - /// - /// let mut items = FormItems::from("a=b&c=d"); - /// let key_values: Vec<_> = items.by_ref().collect(); - /// - /// assert_eq!(key_values.len(), 2); - /// assert_eq!(items.completed(), true); - /// ``` - /// - /// In invalid form string does not parse to completion: - /// - /// ```rust - /// use rocket::request::FormItems; - /// - /// let mut items = FormItems::from("a=b&==d"); - /// let key_values: Vec<_> = items.by_ref().collect(); - /// - /// assert_eq!(key_values.len(), 1); - /// assert_eq!(items.completed(), false); - /// ``` - #[inline] - pub fn completed(&self) -> bool { - match self { - FormItems::Raw { string, next_index } => *next_index >= string.len(), - FormItems::Cooked { items, next_index } => *next_index >= items.len(), - } - } - - /// Parses all remaining key/value pairs and returns `true` if parsing ran - /// to completion. All valid form strings will parse to completion, while - /// invalid form strings will not. - /// - /// # Example - /// - /// A valid form string can be exhausted: - /// - /// ```rust - /// use rocket::request::FormItems; - /// - /// let mut items = FormItems::from("a=b&c=d"); - /// - /// assert!(items.next().is_some()); - /// assert_eq!(items.completed(), false); - /// assert_eq!(items.exhaust(), true); - /// assert_eq!(items.completed(), true); - /// ``` - /// - /// An invalid form string cannot be exhausted: - /// - /// ```rust - /// use rocket::request::FormItems; - /// - /// let mut items = FormItems::from("a=b&=d="); - /// - /// assert!(items.next().is_some()); - /// assert_eq!(items.completed(), false); - /// assert_eq!(items.exhaust(), false); - /// assert_eq!(items.completed(), false); - /// assert!(items.next().is_none()); - /// ``` - #[inline] - pub fn exhaust(&mut self) -> bool { - while let Some(_) = self.next() { } - self.completed() - } - - #[inline] - #[doc(hidden)] - pub fn mark_complete(&mut self) { - match self { - FormItems::Raw { string, ref mut next_index } => *next_index = string.len(), - FormItems::Cooked { items, ref mut next_index } => *next_index = items.len(), - } - } -} - -impl<'f> From<&'f RawStr> for FormItems<'f> { - #[inline(always)] - fn from(string: &'f RawStr) -> FormItems<'f> { - FormItems::Raw { string, next_index: 0 } - } -} - -impl<'f> From<&'f str> for FormItems<'f> { - #[inline(always)] - fn from(string: &'f str) -> FormItems<'f> { - FormItems::from(RawStr::from_str(string)) - } -} - -impl<'f> From<&'f [FormItem<'f>]> for FormItems<'f> { - #[inline(always)] - fn from(items: &'f [FormItem<'f>]) -> FormItems<'f> { - FormItems::Cooked { items, next_index: 0 } - } -} - -fn raw<'f>(string: &mut &'f RawStr, index: &mut usize) -> Option> { - loop { - let start = *index; - let s = &string[start..]; - if s.is_empty() { - return None; - } - - let (key, rest, key_consumed) = match memchr2(b'=', b'&', s.as_bytes()) { - Some(i) if s.as_bytes()[i] == b'=' => (&s[..i], &s[(i + 1)..], i + 1), - Some(i) => (&s[..i], &s[i..], i), - None => (s, &s[s.len()..], s.len()) - }; - - let (value, val_consumed) = match memchr2(b'=', b'&', rest.as_bytes()) { - Some(i) if rest.as_bytes()[i] == b'=' => return None, - Some(i) => (&rest[..i], i + 1), - None => (rest, rest.len()) - }; - - *index += key_consumed + val_consumed; - let raw = &string[start..(start + key_consumed + value.len())]; - match (key.is_empty(), value.is_empty()) { - (true, true) => continue, - _ => return Some(FormItem { - raw: raw.into(), - key: key.into(), - value: value.into() - }) - } - } -} - -impl<'f> Iterator for FormItems<'f> { - type Item = FormItem<'f>; - - fn next(&mut self) -> Option { - match self { - FormItems::Raw { ref mut string, ref mut next_index } => { - raw(string, next_index) - } - FormItems::Cooked { items, ref mut next_index } => { - if *next_index < items.len() { - let item = items[*next_index]; - *next_index += 1; - Some(item) - } else { - None - } - } - } - } -} - -// #[cfg(test)] -// mod test { -// use super::FormItems; - -// impl<'f> From<&'f [(&'f str, &'f str, &'f str)]> for FormItems<'f> { -// #[inline(always)] -// fn from(triples: &'f [(&'f str, &'f str, &'f str)]) -> FormItems<'f> { -// // Safe because RawStr(str) is repr(transparent). -// let triples = unsafe { std::mem::transmute(triples) }; -// FormItems::Cooked { triples, next_index: 0 } -// } -// } - -// macro_rules! check_form { -// (@bad $string:expr) => (check_form($string, None)); -// ($string:expr, $expected:expr) => (check_form(&$string[..], Some($expected))); -// } - -// fn check_form<'a, T>(items: T, expected: Option<&[(&str, &str, &str)]>) -// where T: Into> + std::fmt::Debug -// { -// let string = format!("{:?}", items); -// let mut items = items.into(); -// let results: Vec<_> = items.by_ref().map(|item| item.explode()).collect(); -// if let Some(expected) = expected { -// assert_eq!(expected.len(), results.len(), -// "expected {:?}, got {:?} for {:?}", expected, results, string); - -// for i in 0..results.len() { -// let (expected_raw, expected_key, expected_val) = expected[i]; -// let (actual_raw, actual_key, actual_val) = results[i]; - -// assert!(actual_raw == expected_raw, -// "raw [{}] mismatch for {}: expected {}, got {}", -// i, string, expected_raw, actual_raw); - -// assert!(actual_key == expected_key, -// "key [{}] mismatch for {}: expected {}, got {}", -// i, string, expected_key, actual_key); - -// assert!(actual_val == expected_val, -// "val [{}] mismatch for {}: expected {}, got {}", -// i, string, expected_val, actual_val); -// } -// } else { -// assert!(!items.exhaust(), "{} unexpectedly parsed successfully", string); -// } -// } - -// #[test] -// fn test_cooked_items() { -// check_form!( -// &[("username=user", "username", "user"), ("password=pass", "password", "pass")], -// &[("username=user", "username", "user"), ("password=pass", "password", "pass")] -// ); - -// let empty: &[(&str, &str, &str)] = &[]; -// check_form!(empty, &[]); - -// check_form!(&[("a=b", "a", "b")], &[("a=b", "a", "b")]); - -// check_form!( -// &[("user=x", "user", "x"), ("pass=word", "pass", "word"), -// ("x=z", "x", "z"), ("d=", "d", ""), ("e=", "e", "")], - -// &[("user=x", "user", "x"), ("pass=word", "pass", "word"), -// ("x=z", "x", "z"), ("d=", "d", ""), ("e=", "e", "")] -// ); -// } - -// // #[test] -// // fn test_form_string() { -// // check_form!("username=user&password=pass", -// // &[("username", "user"), ("password", "pass")]); - -// // check_form!("user=user&user=pass", &[("user", "user"), ("user", "pass")]); -// // check_form!("user=&password=pass", &[("user", ""), ("password", "pass")]); -// // check_form!("user&password=pass", &[("user", ""), ("password", "pass")]); -// // check_form!("foo&bar", &[("foo", ""), ("bar", "")]); - -// // check_form!("a=b", &[("a", "b")]); -// // check_form!("value=Hello+World", &[("value", "Hello+World")]); - -// // check_form!("user=", &[("user", "")]); -// // check_form!("user=&", &[("user", "")]); -// // check_form!("a=b&a=", &[("a", "b"), ("a", "")]); -// // check_form!("user=&password", &[("user", ""), ("password", "")]); -// // check_form!("a=b&a", &[("a", "b"), ("a", "")]); - -// // check_form!("user=x&&", &[("user", "x")]); -// // check_form!("user=x&&&&pass=word", &[("user", "x"), ("pass", "word")]); -// // check_form!("user=x&&&&pass=word&&&x=z&d&&&e", -// // &[("user", "x"), ("pass", "word"), ("x", "z"), ("d", ""), ("e", "")]); - -// // check_form!("=&a=b&&=", &[("a", "b")]); -// // check_form!("=b", &[("", "b")]); -// // check_form!("=b&=c", &[("", "b"), ("", "c")]); - -// // check_form!("=", &[]); -// // check_form!("&=&", &[]); -// // check_form!("&", &[]); -// // check_form!("=&=", &[]); - -// // check_form!(@bad "=b&=="); -// // check_form!(@bad "=="); -// // check_form!(@bad "=k="); -// // check_form!(@bad "=abc="); -// // check_form!(@bad "=abc=cd"); -// // } -// } diff --git a/core/lib/src/request/form/from_form.rs b/core/lib/src/request/form/from_form.rs deleted file mode 100644 index 5ecd0fb0fc..0000000000 --- a/core/lib/src/request/form/from_form.rs +++ /dev/null @@ -1,127 +0,0 @@ -use crate::request::FormItems; - -/// Trait to create an instance of some type from an HTTP form. -/// [`Form`](crate::request::Form) requires its generic type to implement this trait. -/// -/// # Deriving -/// -/// This trait can be automatically derived. When deriving `FromForm`, every -/// field in the structure must implement -/// [`FromFormValue`](crate::request::FromFormValue). Rocket validates each field in -/// the structure by calling its `FromFormValue` implementation. You may wish to -/// implement `FromFormValue` for your own types for custom, automatic -/// validation. -/// -/// ```rust -/// # #![allow(deprecated, dead_code, unused_attributes)] -/// # #[macro_use] extern crate rocket; -/// #[derive(FromForm)] -/// struct TodoTask { -/// description: String, -/// completed: bool -/// } -/// # fn main() { } -/// ``` -/// -/// # Data Guard -/// -/// Types that implement `FromForm` can be parsed directly from incoming form -/// data via the `data` parameter and `Form` type. -/// -/// ```rust -/// # #![allow(deprecated, dead_code, unused_attributes)] -/// # #[macro_use] extern crate rocket; -/// # use rocket::request::Form; -/// # #[derive(FromForm)] -/// # struct TodoTask { description: String, completed: bool } -/// #[post("/submit", data = "")] -/// fn submit_task(task: Form) -> String { -/// format!("New task: {}", task.description) -/// } -/// # fn main() { } -/// ``` -/// -/// # Implementing -/// -/// Implementing `FromForm` should be a rare occurrence. Prefer instead to use -/// Rocket's built-in derivation. -/// -/// When implementing `FromForm`, use the [`FormItems`] iterator to iterate -/// through the raw form key/value pairs. Be aware that form fields that are -/// typically hidden from your application, such as `_method`, will be present -/// while iterating. Ensure that you adhere to the properties of the `strict` -/// parameter, as detailed in the documentation below. -/// -/// ## Example -/// -/// Consider the following scenario: we have a struct `Item` with field name -/// `field`. We'd like to parse any form that has a field named either `balloon` -/// _or_ `space`, and we'd like that field's value to be the value for our -/// structure's `field`. The following snippet shows how this would be -/// implemented: -/// -/// ```rust -/// use rocket::request::{FromForm, FormItems}; -/// -/// struct Item { -/// field: String -/// } -/// -/// impl<'f> FromForm<'f> for Item { -/// // In practice, we'd use a more descriptive error type. -/// type Error = (); -/// -/// fn from_form(items: &mut FormItems<'f>, strict: bool) -> Result { -/// let mut field = None; -/// -/// for item in items { -/// match item.key.as_str() { -/// "balloon" | "space" if field.is_none() => { -/// let decoded = item.value.url_decode().map_err(|_| ())?; -/// field = Some(decoded); -/// } -/// _ if strict => return Err(()), -/// _ => { /* allow extra value when not strict */ } -/// } -/// } -/// -/// field.map(|field| Item { field }).ok_or(()) -/// } -/// } -/// ``` -pub trait FromForm<'f>: Sized { - /// The associated error to be returned when parsing fails. - type Error: Send; - - /// Parses an instance of `Self` from the iterator of form items `it`. - /// - /// Extra form field are allowed when `strict` is `false` and disallowed - /// when `strict` is `true`. - /// - /// # Errors - /// - /// If `Self` cannot be parsed from the given form items, an instance of - /// `Self::Error` will be returned. - /// - /// When `strict` is `true` and unexpected, extra fields are present in - /// `it`, an instance of `Self::Error` will be returned. - fn from_form(it: &mut FormItems<'f>, strict: bool) -> Result; -} - -impl<'f, T: FromForm<'f>> FromForm<'f> for Option { - type Error = std::convert::Infallible; - - #[inline] - fn from_form(items: &mut FormItems<'f>, strict: bool) -> Result, Self::Error> { - Ok(T::from_form(items, strict).ok()) - } -} - -impl<'f, T: FromForm<'f>> FromForm<'f> for Result { - type Error = std::convert::Infallible; - - #[inline] - fn from_form(items: &mut FormItems<'f>, strict: bool) -> Result { - Ok(T::from_form(items, strict)) - } -} diff --git a/core/lib/src/request/form/from_form_value.rs b/core/lib/src/request/form/from_form_value.rs deleted file mode 100644 index 60b47337b8..0000000000 --- a/core/lib/src/request/form/from_form_value.rs +++ /dev/null @@ -1,294 +0,0 @@ -use std::str::FromStr; -use std::net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddrV4, SocketAddrV6, SocketAddr}; -use std::num::{ - NonZeroI8, NonZeroI16, NonZeroI32, NonZeroI64, NonZeroI128, NonZeroIsize, - NonZeroU8, NonZeroU16, NonZeroU32, NonZeroU64, NonZeroU128, NonZeroUsize, -}; - -use crate::http::RawStr; - -/// Trait to parse a typed value from a form value. -/// -/// This trait is used by Rocket's code generation in two places: -/// -/// 1. Fields in structs deriving [`FromForm`](crate::request::FromForm) are -/// required to implement this trait. -/// 2. Types of dynamic query parameters (`?`) are required to -/// implement this trait. -/// -/// # `FromForm` Fields -/// -/// When deriving the `FromForm` trait, Rocket uses the `FromFormValue` -/// implementation of each field's type to validate the form input. To -/// illustrate, consider the following structure: -/// -/// ```rust -/// # #[macro_use] extern crate rocket; -/// #[derive(FromForm)] -/// struct Person { -/// name: String, -/// age: u16 -/// } -/// ``` -/// -/// The `FromForm` implementation generated by Rocket will call -/// `String::from_form_value` for the `name` field, and `u16::from_form_value` -/// for the `age` field. The `Person` structure can only be created from a form -/// if both calls return successfully. -/// -/// # Dynamic Query Parameters -/// -/// Types of dynamic query parameters are required to implement this trait. The -/// `FromFormValue` implementation is used to parse and validate each parameter -/// according to its target type: -/// -/// ```rust -/// # #[macro_use] extern crate rocket; -/// # type Size = String; -/// #[get("/item?&")] -/// fn item(id: usize, size: Size) { /* ... */ } -/// # fn main() { } -/// ``` -/// -/// To generate values for `id` and `size`, Rocket calls -/// `usize::from_form_value()` and `Size::from_form_value()`, respectively. -/// -/// # Validation Errors -/// -/// It is sometimes desired to prevent a validation error from forwarding a -/// request to another route. The `FromFormValue` implementation for `Option` -/// and `Result` make this possible. Their implementations always -/// return successfully, effectively "catching" the error. -/// -/// For instance, if we wanted to know if a user entered an invalid `age` in the -/// form corresponding to the `Person` structure in the first example, we could -/// use the following structure: -/// -/// ```rust -/// # use rocket::http::RawStr; -/// struct Person<'r> { -/// name: String, -/// age: Result -/// } -/// ``` -/// -/// The `Err` value in this case is `&RawStr` since `u16::from_form_value` -/// returns a `Result`. -/// -/// # Provided Implementations -/// -/// Rocket implements `FromFormValue` for many standard library types. Their -/// behavior is documented here. -/// -/// * -/// * Primitive types: **f32, f64, isize, i8, i16, i32, i64, i128, -/// usize, u8, u16, u32, u64, u128** -/// * `IpAddr` and `SocketAddr` types: **IpAddr, Ipv4Addr, Ipv6Addr, -/// SocketAddrV4, SocketAddrV6, SocketAddr** -/// * `NonZero*` types: **NonZeroI8, NonZeroI16, NonZeroI32, NonZeroI64, -/// NonZeroI128, NonZeroIsize, NonZeroU8, NonZeroU16, NonZeroU32, -/// NonZeroU64, NonZeroU128, NonZeroUsize** -/// -/// A value is validated successfully if the `from_str` method for the given -/// type returns successfully. Otherwise, the raw form value is returned as -/// the `Err` value. -/// -/// * **bool** -/// -/// A value is validated successfully as `true` if the the form value is -/// `"true"` or `"on"`, and as a `false` value if the form value is -/// `"false"`, `"off"`, or not present. In any other case, the raw form -/// value is returned in the `Err` value. -/// -/// * **[`&RawStr`](RawStr)** -/// -/// _This implementation always returns successfully._ -/// -/// The raw, undecoded string is returned directly without modification. -/// -/// * **String** -/// -/// URL decodes the form value. If the decode is successful, the decoded -/// string is returned. Otherwise, an `Err` with the original form value is -/// returned. -/// -/// * **Option<T>** _where_ **T: FromFormValue** -/// -/// _This implementation always returns successfully._ -/// -/// The form value is validated by `T`'s `FromFormValue` implementation. If -/// the validation succeeds, a `Some(validated_value)` is returned. -/// Otherwise, a `None` is returned. -/// -/// * **Result<T, T::Error>** _where_ **T: FromFormValue** -/// -/// _This implementation always returns successfully._ -/// -/// The from value is validated by `T`'s `FromFormvalue` implementation. The -/// returned `Result` value is returned. -/// -/// # Example -/// -/// This trait is generally implemented to parse and validate form values. While -/// Rocket provides parsing and validation for many of the standard library -/// types such as `u16` and `String`, you can implement `FromFormValue` for a -/// custom type to get custom validation. -/// -/// Imagine you'd like to verify that some user is over some age in a form. You -/// might define a new type and implement `FromFormValue` as follows: -/// -/// ```rust -/// use rocket::request::FromFormValue; -/// use rocket::http::RawStr; -/// -/// struct AdultAge(usize); -/// -/// impl<'v> FromFormValue<'v> for AdultAge { -/// type Error = &'v RawStr; -/// -/// fn from_form_value(form_value: &'v RawStr) -> Result { -/// match form_value.parse::() { -/// Ok(age) if age >= 21 => Ok(AdultAge(age)), -/// _ => Err(form_value), -/// } -/// } -/// } -/// ``` -/// -/// The type can then be used in a `FromForm` struct as follows: -/// -/// ```rust -/// # #[macro_use] extern crate rocket; -/// # type AdultAge = usize; -/// #[derive(FromForm)] -/// struct Person { -/// name: String, -/// age: AdultAge -/// } -/// ``` -/// -/// A form using the `Person` structure as its target will only parse and -/// validate if the `age` field contains a `usize` greater than `21`. -pub trait FromFormValue<'v>: Sized { - /// The associated error which can be returned from parsing. It is a good - /// idea to have the return type be or contain an `&'v str` so that the - /// unparseable string can be examined after a bad parse. - type Error; - - /// Parses an instance of `Self` from an HTTP form field value or returns an - /// `Error` if one cannot be parsed. - fn from_form_value(form_value: &'v RawStr) -> Result; - - /// Returns a default value to be used when the form field does not exist. - /// If this returns `None`, then the field is required. Otherwise, this - /// should return `Some(default_value)`. The default implementation simply - /// returns `None`. - #[inline(always)] - fn default() -> Option { - None - } -} - -impl<'v> FromFormValue<'v> for &'v RawStr { - type Error = std::convert::Infallible; - - // This just gives the raw string. - #[inline(always)] - fn from_form_value(v: &'v RawStr) -> Result { - Ok(v) - } -} - -impl<'v> FromFormValue<'v> for String { - type Error = &'v RawStr; - - // This actually parses the value according to the standard. - #[inline(always)] - fn from_form_value(v: &'v RawStr) -> Result { - v.url_decode().map_err(|_| v) - } -} - -impl<'v> FromFormValue<'v> for bool { - type Error = &'v RawStr; - - fn from_form_value(v: &'v RawStr) -> Result { - match v.as_str() { - "on" | "true" => Ok(true), - "off" | "false" => Ok(false), - _ => Err(v), - } - } - - #[inline(always)] - fn default() -> Option { - Some(false) - } -} - -macro_rules! impl_with_fromstr { - ($($T:ident),+) => ($( - impl<'v> FromFormValue<'v> for $T { - type Error = &'v RawStr; - - #[inline(always)] - fn from_form_value(v: &'v RawStr) -> Result { - $T::from_str(v.as_str()).map_err(|_| v) - } - } - )+) -} - -impl_with_fromstr!( - f32, f64, isize, i8, i16, i32, i64, i128, usize, u8, u16, u32, u64, u128, - NonZeroI8, NonZeroI16, NonZeroI32, NonZeroI64, NonZeroI128, NonZeroIsize, - NonZeroU8, NonZeroU16, NonZeroU32, NonZeroU64, NonZeroU128, NonZeroUsize, - Ipv4Addr -); - -macro_rules! impl_with_fromstr_encoded { - ($($T:ident),+) => ($( - impl<'v> FromFormValue<'v> for $T { - type Error = &'v RawStr; - - #[inline(always)] - fn from_form_value(v: &'v RawStr) -> Result { - $T::from_str(&v.url_decode().map_err(|_| v)?).map_err(|_| v) - } - } - )+) -} - -impl_with_fromstr_encoded!( - IpAddr, Ipv6Addr, SocketAddrV4, SocketAddrV6, SocketAddr -); - -impl<'v, T: FromFormValue<'v>> FromFormValue<'v> for Option { - type Error = std::convert::Infallible; - - #[inline(always)] - fn from_form_value(v: &'v RawStr) -> Result { - match T::from_form_value(v) { - Ok(v) => Ok(Some(v)), - Err(_) => Ok(None), - } - } - - #[inline(always)] - fn default() -> Option> { - Some(None) - } -} - -// // TODO: Add more useful implementations (range, regex, etc.). -impl<'v, T: FromFormValue<'v>> FromFormValue<'v> for Result { - type Error = std::convert::Infallible; - - #[inline(always)] - fn from_form_value(v: &'v RawStr) -> Result { - match T::from_form_value(v) { - ok@Ok(_) => Ok(ok), - e@Err(_) => Ok(e), - } - } -} diff --git a/core/lib/src/request/form/lenient.rs b/core/lib/src/request/form/lenient.rs deleted file mode 100644 index 04e49c1046..0000000000 --- a/core/lib/src/request/form/lenient.rs +++ /dev/null @@ -1,125 +0,0 @@ -use std::ops::{Deref, DerefMut}; - -use crate::request::{Request, form::{Form, FormDataError, FromForm}}; -use crate::data::{Data, Transformed, FromTransformedData, TransformFuture, FromDataFuture}; -use crate::http::uri::{Query, FromUriParam}; - -/// A data guard for parsing [`FromForm`] types leniently. -/// -/// This type implements the [`FromTransformedData`] trait, and like [`Form`], provides a -/// generic means to parse arbitrary structures from incoming form data. Unlike -/// `Form`, this type uses a _lenient_ parsing strategy: forms that contains a -/// superset of the expected fields (i.e, extra fields) will parse successfully. -/// -/// # Leniency -/// -/// A `LenientForm` will parse successfully from an incoming form if the form -/// contains a superset of the fields in `T`. Said another way, a -/// `LenientForm` automatically discards extra fields without error. For -/// instance, if an incoming form contains the fields "a", "b", and "c" while -/// `T` only contains "a" and "c", the form _will_ parse as `LenientForm`. -/// -/// # Usage -/// -/// The usage of a `LenientForm` type is equivalent to that of [`Form`], so we -/// defer details to its documentation. -/// -/// `LenientForm` implements `FromTransformedData`, so it can be used directly as a target -/// of the `data = ""` route parameter. For instance, if some structure -/// of type `T` implements the `FromForm` trait, an incoming form can be -/// automatically parsed into the `T` structure with the following route and -/// handler: -/// -/// ```rust -/// # #[macro_use] extern crate rocket; -/// use rocket::request::LenientForm; -/// -/// #[derive(FromForm)] -/// struct UserInput { -/// value: String -/// } -/// -/// #[post("/submit", data = "")] -/// fn submit_task(user_input: LenientForm) -> String { -/// format!("Your value: {}", user_input.value) -/// } -/// # fn main() { } -/// ``` -/// -/// ## Incoming Data Limits -/// -/// A `LenientForm` obeys the same data limits as a `Form` and defaults to -/// 32KiB. The limit can be increased by setting the `limits.forms` -/// configuration parameter. For instance, to increase the forms limit to 512KiB -/// for all environments, you may add the following to your `Rocket.toml`: -/// -/// ```toml -/// [global.limits] -/// forms = 524288 -/// ``` -#[derive(Debug)] -pub struct LenientForm(pub T); - -impl LenientForm { - /// Consumes `self` and returns the parsed value. - /// - /// # Example - /// - /// ```rust - /// # #[macro_use] extern crate rocket; - /// use rocket::request::LenientForm; - /// - /// #[derive(FromForm)] - /// struct MyForm { - /// field: String, - /// } - /// - /// #[post("/submit", data = "")] - /// fn submit(form: LenientForm) -> String { - /// form.into_inner().field - /// } - /// # fn main() { } - #[inline(always)] - pub fn into_inner(self) -> T { - self.0 - } -} - -impl Deref for LenientForm { - type Target = T; - - fn deref(&self) -> &T { - &self.0 - } -} - -impl DerefMut for LenientForm { - fn deref_mut(&mut self) -> &mut T { - &mut self.0 - } -} - -impl<'r, T: FromForm<'r> + Send + 'r> FromTransformedData<'r> for LenientForm { - type Error = FormDataError<'r, T::Error>; - type Owned = String; - type Borrowed = str; - - fn transform(r: &'r Request<'_>, d: Data) -> TransformFuture<'r, Self::Owned, Self::Error> { - >::transform(r, d) - } - - fn from_data(_: &'r Request<'_>, o: Transformed<'r, Self>) -> FromDataFuture<'r, Self, Self::Error> { - Box::pin(futures::future::ready(o.borrowed().and_then(|form| { - >::from_data(form, false).map(LenientForm) - }))) - } -} - -impl<'r, A, T: FromUriParam + FromForm<'r>> FromUriParam for LenientForm { - type Target = T::Target; - - #[inline(always)] - fn from_uri_param(param: A) -> Self::Target { - T::from_uri_param(param) - } -} diff --git a/core/lib/src/request/form/mod.rs b/core/lib/src/request/form/mod.rs deleted file mode 100644 index cd8958714c..0000000000 --- a/core/lib/src/request/form/mod.rs +++ /dev/null @@ -1,15 +0,0 @@ -//! Types and traits for form processing. - -mod form_items; -mod from_form; -mod from_form_value; -mod lenient; -mod error; -mod form; - -pub use self::form_items::{FormItems, FormItem}; -pub use self::from_form::FromForm; -pub use self::from_form_value::FromFormValue; -pub use self::form::Form; -pub use self::lenient::LenientForm; -pub use self::error::{FormError, FormParseError, FormDataError}; diff --git a/core/lib/src/request/param.rs b/core/lib/src/request/from_param.rs similarity index 76% rename from core/lib/src/request/param.rs rename to core/lib/src/request/from_param.rs index 457726ff07..00f9a168aa 100644 --- a/core/lib/src/request/param.rs +++ b/core/lib/src/request/from_param.rs @@ -1,9 +1,8 @@ use std::str::FromStr; use std::path::PathBuf; use std::fmt::Debug; -use std::borrow::Cow; -use crate::http::{RawStr, uri::{Segments, SegmentError}}; +use crate::http::uri::{Segments, PathError}; /// Trait to convert a dynamic path segment string to a concrete value. /// @@ -49,14 +48,13 @@ use crate::http::{RawStr, uri::{Segments, SegmentError}}; /// /// For instance, imagine you've asked for an `` as a `usize`. To determine /// when the `` was not a valid `usize` and retrieve the string that failed -/// to parse, you can use a `Result` type for the `` -/// parameter as follows: +/// to parse, you can use a `Result` type for the `` parameter +/// as follows: /// /// ```rust /// # #[macro_use] extern crate rocket; -/// # use rocket::http::RawStr; /// #[get("/")] -/// fn hello(id: Result) -> String { +/// fn hello(id: Result) -> String { /// match id { /// Ok(id_num) => format!("usize: {}", id_num), /// Err(string) => format!("Not a usize: {}", string) @@ -83,23 +81,12 @@ use crate::http::{RawStr, uri::{Segments, SegmentError}}; /// type returns successfully. Otherwise, the raw path segment is returned /// in the `Err` value. /// -/// * **[`&RawStr`](RawStr)** +/// * **&str, String** /// /// _This implementation always returns successfully._ /// -/// The path segment is passed directly with no modification. -/// -/// * **String** -/// -/// Percent decodes the path segment. If the decode is successful, the -/// decoded string is returned. Otherwise, an `Err` with the original path -/// segment is returned. -/// -/// * **Cow** -/// -/// Percent decodes the path segment, allocating only when necessary. If the -/// decode is successful, the decoded string is returned. Otherwise, an -/// `Err` with the original path segment is returned. +/// Returns the percent-decoded path segment with invalid UTF-8 byte +/// sequences replaced by � U+FFFD. /// /// * **Option<T>** _where_ **T: FromParam** /// @@ -128,7 +115,6 @@ use crate::http::{RawStr, uri::{Segments, SegmentError}}; /// `key` and the number after the colon is stored in `value`: /// /// ```rust -/// # #[allow(dead_code)] /// struct MyParam<'r> { /// key: &'r str, /// value: usize @@ -139,14 +125,15 @@ use crate::http::{RawStr, uri::{Segments, SegmentError}}; /// /// ```rust /// use rocket::request::FromParam; -/// use rocket::http::RawStr; /// # #[allow(dead_code)] /// # struct MyParam<'r> { key: &'r str, value: usize } /// /// impl<'r> FromParam<'r> for MyParam<'r> { -/// type Error = &'r RawStr; +/// type Error = &'r str; /// -/// fn from_param(param: &'r RawStr) -> Result { +/// fn from_param(param: &'r str) -> Result { +/// // We can convert `param` into a `str` since we'll check every +/// // character for safety later. /// let (key, val_str) = match param.find(':') { /// Some(i) if i > 0 => (¶m[..i], ¶m[(i + 1)..]), /// _ => return Err(param) @@ -156,12 +143,9 @@ use crate::http::{RawStr, uri::{Segments, SegmentError}}; /// return Err(param); /// } /// -/// val_str.parse().map(|value| { -/// MyParam { -/// key: key, -/// value: value -/// } -/// }).map_err(|_| param) +/// val_str.parse() +/// .map(|value| MyParam { key, value }) +/// .map_err(|_| param) /// } /// } /// ``` @@ -172,12 +156,11 @@ use crate::http::{RawStr, uri::{Segments, SegmentError}}; /// ```rust /// # #[macro_use] extern crate rocket; /// # use rocket::request::FromParam; -/// # use rocket::http::RawStr; /// # #[allow(dead_code)] /// # struct MyParam<'r> { key: &'r str, value: usize } /// # impl<'r> FromParam<'r> for MyParam<'r> { -/// # type Error = &'r RawStr; -/// # fn from_param(param: &'r RawStr) -> Result { +/// # type Error = &'r str; +/// # fn from_param(param: &'r str) -> Result { /// # Err(param) /// # } /// # } @@ -198,44 +181,36 @@ pub trait FromParam<'a>: Sized { /// Parses and validates an instance of `Self` from a path parameter string /// or returns an `Error` if parsing or validation fails. - fn from_param(param: &'a RawStr) -> Result; + fn from_param(param: &'a str) -> Result; } -impl<'a> FromParam<'a> for &'a RawStr { +impl<'a> FromParam<'a> for &'a str { type Error = std::convert::Infallible; #[inline(always)] - fn from_param(param: &'a RawStr) -> Result<&'a RawStr, Self::Error> { + fn from_param(param: &'a str) -> Result<&'a str, Self::Error> { Ok(param) } } impl<'a> FromParam<'a> for String { - type Error = &'a RawStr; - - #[inline(always)] - fn from_param(param: &'a RawStr) -> Result { - param.percent_decode().map(|cow| cow.into_owned()).map_err(|_| param) - } -} - -impl<'a> FromParam<'a> for Cow<'a, str> { - type Error = &'a RawStr; + type Error = &'a str; #[inline(always)] - fn from_param(param: &'a RawStr) -> Result, Self::Error> { - param.percent_decode().map_err(|_| param) + fn from_param(param: &'a str) -> Result { + // TODO: Tell the user they're being inefficient? + Ok(param.to_string()) } } macro_rules! impl_with_fromstr { ($($T:ty),+) => ($( impl<'a> FromParam<'a> for $T { - type Error = &'a RawStr; + type Error = &'a str; #[inline(always)] - fn from_param(param: &'a RawStr) -> Result { - <$T as FromStr>::from_str(param.as_str()).map_err(|_| param) + fn from_param(param: &'a str) -> Result { + <$T as FromStr>::from_str(param).map_err(|_| param) } } )+) @@ -258,7 +233,7 @@ impl<'a, T: FromParam<'a>> FromParam<'a> for Result { type Error = std::convert::Infallible; #[inline] - fn from_param(param: &'a RawStr) -> Result { + fn from_param(param: &'a str) -> Result { match T::from_param(param) { Ok(val) => Ok(Ok(val)), Err(e) => Ok(Err(e)), @@ -270,7 +245,7 @@ impl<'a, T: FromParam<'a>> FromParam<'a> for Option { type Error = std::convert::Infallible; #[inline] - fn from_param(param: &'a RawStr) -> Result { + fn from_param(param: &'a str) -> Result { match T::from_param(param) { Ok(val) => Ok(Some(val)), Err(_) => Ok(None) @@ -296,20 +271,20 @@ impl<'a, T: FromParam<'a>> FromParam<'a> for Option { /// any other segments that begin with "*" or "." are ignored. If a /// percent-decoded segment results in invalid UTF8, an `Err` is returned with /// the `Utf8Error`. -pub trait FromSegments<'a>: Sized { +pub trait FromSegments<'r>: Sized { /// The associated error to be returned when parsing fails. type Error: Debug; /// 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<'a>) -> Result; + fn from_segments(segments: Segments<'r>) -> Result; } -impl<'a> FromSegments<'a> for Segments<'a> { +impl<'r> FromSegments<'r> for Segments<'r> { type Error = std::convert::Infallible; #[inline(always)] - fn from_segments(segments: Segments<'a>) -> Result, Self::Error> { + fn from_segments(segments: Segments<'r>) -> Result, Self::Error> { Ok(segments) } } @@ -331,18 +306,18 @@ impl<'a> FromSegments<'a> for Segments<'a> { /// safe to interpolate within, or use as a suffix of, a path without additional /// checks. impl FromSegments<'_> for PathBuf { - type Error = SegmentError; + type Error = PathError; - fn from_segments(segments: Segments<'_>) -> Result { - segments.into_path_buf(false) + fn from_segments(segments: Segments<'_>) -> Result { + segments.to_path_buf(false) } } -impl<'a, T: FromSegments<'a>> FromSegments<'a> for Result { +impl<'r, T: FromSegments<'r>> FromSegments<'r> for Result { type Error = std::convert::Infallible; #[inline] - fn from_segments(segments: Segments<'a>) -> Result, Self::Error> { + fn from_segments(segments: Segments<'r>) -> Result, Self::Error> { match T::from_segments(segments) { Ok(val) => Ok(Ok(val)), Err(e) => Ok(Err(e)), @@ -350,11 +325,11 @@ impl<'a, T: FromSegments<'a>> FromSegments<'a> for Result { } } -impl<'a, T: FromSegments<'a>> FromSegments<'a> for Option { +impl<'r, T: FromSegments<'r>> FromSegments<'r> for Option { type Error = std::convert::Infallible; #[inline] - fn from_segments(segments: Segments<'a>) -> Result, Self::Error> { + fn from_segments(segments: Segments<'r>) -> Result, Self::Error> { match T::from_segments(segments) { Ok(val) => Ok(Some(val)), Err(_) => Ok(None) diff --git a/core/lib/src/request/from_request.rs b/core/lib/src/request/from_request.rs index 69f7316de8..562419e660 100644 --- a/core/lib/src/request/from_request.rs +++ b/core/lib/src/request/from_request.rs @@ -148,6 +148,12 @@ impl IntoOutcome for Result { /// /// _This implementation always returns successfully._ /// +/// * **&[`Config`](crate::config::Config)** +/// +/// Extracts the application [`Config`]. +/// +/// _This implementation always returns successfully._ +/// /// * **ContentType** /// /// Extracts the [`ContentType`] from the incoming request. If the request diff --git a/core/lib/src/request/mod.rs b/core/lib/src/request/mod.rs index 94a6d43a42..7c807a6626 100644 --- a/core/lib/src/request/mod.rs +++ b/core/lib/src/request/mod.rs @@ -1,25 +1,49 @@ //! Types and traits for request parsing and handling. mod request; -mod param; -mod form; +mod from_param; mod from_request; -mod state; -mod query; #[cfg(test)] mod tests; -#[doc(hidden)] pub use rocket_codegen::{FromForm, FromFormValue}; - pub use self::request::Request; pub use self::from_request::{FromRequest, Outcome}; -pub use self::param::{FromParam, FromSegments}; -pub use self::form::{FromForm, FromFormValue}; -pub use self::form::{Form, LenientForm, FormItems, FormItem}; -pub use self::form::{FormError, FormParseError, FormDataError}; -pub use self::state::State; -pub use self::query::{Query, FromQuery}; +pub use self::from_param::{FromParam, FromSegments}; #[doc(inline)] pub use crate::response::flash::FlashMessage; + +/// Store and immediately retrieve a value `$v` in `$request`'s local cache +/// using a locally generated anonymous type to avoid type conflicts. +/// +/// # Example +/// +/// ```rust +/// use rocket::request; +/// +/// # rocket::Request::example(rocket::http::Method::Get, "/uri", |request| { +/// // The first store into local cache for a given type wins. +/// let value = request.local_cache(|| "hello"); +/// assert_eq!(*request.local_cache(|| "hello"), "hello"); +/// +/// // The following return the cached, previously stored value for the type. +/// assert_eq!(*request.local_cache(|| "goodbye"), "hello"); +/// +/// // We cannot cache different values of the same type; we _must_ use a proxy +/// // type. To avoid the need to write these manually, use `local_cache!`, +/// // which generates one of the fly. +/// assert_eq!(*request::local_cache!(request, "hello"), "hello"); +/// assert_eq!(*request::local_cache!(request, "goodbye"), "goodbye"); +/// # }); +/// ``` +#[macro_export] +macro_rules! local_cache { + ($request:expr, $v:expr) => ({ + struct Local(T); + &$request.local_cache(move || Local($v)).0 + }) +} + +#[doc(inline)] +pub use local_cache; diff --git a/core/lib/src/request/query.rs b/core/lib/src/request/query.rs deleted file mode 100644 index 611bf5c7d4..0000000000 --- a/core/lib/src/request/query.rs +++ /dev/null @@ -1,235 +0,0 @@ -use crate::request::{FormItems, FormItem, Form, LenientForm, FromForm}; - -/// Iterator over form items in a query string. -/// -/// The `Query` type exists to separate, at the type level, _form_ form items -/// ([`FormItems`]) from _query_ form items (`Query`). A value of type `Query` -/// is passed in to implementations of the [`FromQuery`] trait by Rocket's code -/// generation for every trailing query parameter, `` below: -/// -/// ```rust -/// # #[macro_use] extern crate rocket; -/// # -/// # use rocket::request::Form; -/// # #[derive(FromForm)] struct Q { foo: usize } -/// # type T = Form; -/// # -/// #[get("/user?")] -/// fn user(params: T) { /* ... */ } -/// # fn main() { } -/// ``` -/// -/// # Usage -/// -/// A value of type `Query` can only be used as an iterator over values of type -/// [`FormItem`]. As such, its usage is equivalent to that of [`FormItems`], and -/// we refer you to its documentation for further details. -/// -/// ## Example -/// -/// ```rust -/// use rocket::request::Query; -/// -/// # use rocket::request::FromQuery; -/// # -/// # struct MyType; -/// # type Result = std::result::Result; -/// # -/// # impl FromQuery<'_> for MyType { -/// # type Error = (); -/// # -/// fn from_query(query: Query) -> Result { -/// for item in query { -/// println!("query key/value: ({}, {})", item.key, item.value); -/// } -/// -/// // ... -/// # Ok(MyType) -/// } -/// # } -/// ``` -#[derive(Debug, Clone)] -pub struct Query<'q>(#[doc(hidden)] pub &'q [FormItem<'q>]); - -impl<'q> Iterator for Query<'q> { - type Item = FormItem<'q>; - - #[inline(always)] - fn next(&mut self) -> Option { - if self.0.is_empty() { - return None; - } - - let next = self.0[0]; - self.0 = &self.0[1..]; - Some(next) - } -} - -/// Trait implemented by query guards to derive a value from a query string. -/// -/// # Query Guards -/// -/// A query guard operates on multiple items of a request's query string. It -/// validates and optionally converts a query string into another value. -/// Validation and parsing/conversion is implemented through `FromQuery`. In -/// other words, every type that implements `FromQuery` is a query guard. -/// -/// Query guards are used as the target of trailing query parameters, which -/// syntactically take the form `` after a `?` in a route's path. For -/// example, the parameter `user` is a trailing query parameter in the following -/// route: -/// -/// ```rust -/// # #[macro_use] extern crate rocket; -/// use rocket::request::Form; -/// -/// #[derive(FromForm)] -/// struct User { -/// name: String, -/// account: usize, -/// } -/// -/// #[get("/item?&")] -/// fn item(id: usize, user: Form) { /* ... */ } -/// # fn main() { } -/// ``` -/// -/// The `FromQuery` implementation of `Form` will be passed in a [`Query`] -/// that iterates over all of the query items that don't have the key `id` -/// (because of the `` dynamic query parameter). For posterity, note that -/// the `value` of an `id=value` item in a query string will be parsed as a -/// `usize` and passed in to `item` as `id`. -/// -/// # Forwarding -/// -/// If the conversion fails, signaled by returning an `Err` from a `FromQuery` -/// implementation, the incoming request will be forwarded to the next matching -/// route, if any. For instance, in the `item` route above, if a query string is -/// missing either a `name` or `account` key/value pair, or there is a query -/// item with a key that is not `id`, `name`, or `account`, the request will be -/// forwarded. Note that this strictness is imposed by the [`Form`] type. As an -/// example, using the [`LenientForm`] type instead would allow extra form items -/// to be ignored without forwarding. Alternatively, _not_ having a trailing -/// parameter at all would result in the same. -/// -/// # Provided Implementations -/// -/// Rocket implements `FromQuery` for several standard types. Their behavior is -/// documented here. -/// -/// * **Form<T>** _where_ **T: FromForm** -/// -/// Parses the query as a strict form, where each key is mapped to a field -/// in `T`. See [`Form`] for more information. -/// -/// * **LenientForm<T>** _where_ **T: FromForm** -/// -/// Parses the query as a lenient form, where each key is mapped to a field -/// in `T`. See [`LenientForm`] for more information. -/// -/// * **Option<T>** _where_ **T: FromQuery** -/// -/// _This implementation always returns successfully._ -/// -/// The query is parsed by `T`'s `FromQuery` implementation. If the parse -/// succeeds, a `Some(parsed_value)` is returned. Otherwise, a `None` is -/// returned. -/// -/// * **Result<T, T::Error>** _where_ **T: FromQuery** -/// -/// _This implementation always returns successfully._ -/// -/// The path segment is parsed by `T`'s `FromQuery` implementation. The -/// returned `Result` value is returned. -/// -/// # Example -/// -/// Explicitly implementing `FromQuery` should be rare. For most use-cases, a -/// query guard of `Form` or `LenientForm`, coupled with deriving -/// `FromForm` (as in the previous example) will suffice. For special cases -/// however, an implementation of `FromQuery` may be warranted. -/// -/// Consider a contrived scheme where we expect to receive one query key, `key`, -/// three times and wish to take the middle value. For instance, consider the -/// query: -/// -/// ```text -/// key=first_value&key=second_value&key=third_value -/// ``` -/// -/// We wish to extract `second_value` from this query into a `Contrived` struct. -/// Because `Form` and `LenientForm` will take the _last_ value (`third_value` -/// here) and don't check that there are exactly three keys named `key`, we -/// cannot make use of them and must implement `FromQuery` manually. Such an -/// implementation might look like: -/// -/// ```rust -/// use rocket::http::RawStr; -/// use rocket::request::{Query, FromQuery}; -/// -/// /// Our custom query guard. -/// struct Contrived<'q>(&'q RawStr); -/// -/// impl<'q> FromQuery<'q> for Contrived<'q> { -/// /// The number of `key`s we actually saw. -/// type Error = usize; -/// -/// fn from_query(query: Query<'q>) -> Result { -/// let mut key_items = query.filter(|i| i.key == "key"); -/// -/// // This is cloning an iterator, which is cheap. -/// let count = key_items.clone().count(); -/// if count != 3 { -/// return Err(count); -/// } -/// -/// // The `ok_or` gets us a `Result`. We will never see `Err(0)`. -/// key_items.map(|i| Contrived(i.value)).nth(1).ok_or(0) -/// } -/// } -/// ``` -pub trait FromQuery<'q>: Sized { - /// The associated error to be returned if parsing/validation fails. - type Error; - - /// Parses and validates an instance of `Self` from a query or returns an - /// `Error` if parsing or validation fails. - fn from_query(query: Query<'q>) -> Result; -} - -impl<'q, T: FromForm<'q>> FromQuery<'q> for Form { - type Error = T::Error; - - #[inline] - fn from_query(q: Query<'q>) -> Result { - T::from_form(&mut FormItems::from(q.0), true).map(Form) - } -} - -impl<'q, T: FromForm<'q>> FromQuery<'q> for LenientForm { - type Error = >::Error; - - #[inline] - fn from_query(q: Query<'q>) -> Result { - T::from_form(&mut FormItems::from(q.0), false).map(LenientForm) - } -} - -impl<'q, T: FromQuery<'q>> FromQuery<'q> for Option { - type Error = std::convert::Infallible; - - #[inline] - fn from_query(q: Query<'q>) -> Result { - Ok(T::from_query(q).ok()) - } -} - -impl<'q, T: FromQuery<'q>> FromQuery<'q> for Result { - type Error = std::convert::Infallible; - - #[inline] - fn from_query(q: Query<'q>) -> Result { - Ok(T::from_query(q)) - } -} diff --git a/core/lib/src/request/request.rs b/core/lib/src/request/request.rs index 1327620b2e..e9069864fa 100644 --- a/core/lib/src/request/request.rs +++ b/core/lib/src/request/request.rs @@ -1,4 +1,4 @@ -use std::sync::Arc; +use std::{ops::RangeFrom, sync::Arc}; use std::net::{IpAddr, SocketAddr}; use std::future::Future; use std::fmt; @@ -9,14 +9,14 @@ use state::{Container, Storage}; use futures::future::BoxFuture; use atomic::{Atomic, Ordering}; +// use crate::request::{FromParam, FromSegments, FromRequest, Outcome}; use crate::request::{FromParam, FromSegments, FromRequest, Outcome}; -use crate::request::{FromFormValue, FormItems, FormItem}; +use crate::form::{self, ValueField, FromForm}; use crate::{Rocket, Config, Shutdown, Route}; -use crate::http::{hyper, uri::{Origin, Segments}}; -use crate::http::{Method, Header, HeaderMap, uncased::UncasedStr}; -use crate::http::{RawStr, ContentType, Accept, MediaType, CookieJar, Cookie}; -use crate::http::private::{Indexed, SmallVec}; +use crate::http::{hyper, uri::{Origin, Segments}, uncased::UncasedStr}; +use crate::http::{Method, Header, HeaderMap}; +use crate::http::{ContentType, Accept, MediaType, CookieJar, Cookie}; use crate::data::Limits; /// The type of an incoming web request. @@ -35,15 +35,13 @@ pub struct Request<'r> { pub(crate) struct RequestState<'r> { pub config: &'r Config, - pub managed: &'r Container, + pub managed: &'r Container![Send + Sync], pub shutdown: &'r Shutdown, - pub path_segments: SmallVec<[Indices; 12]>, - pub query_items: Option>, pub route: Atomic>, pub cookies: CookieJar<'r>, pub accept: Storage>, pub content_type: Storage>, - pub cache: Arc, + pub cache: Arc, } impl Request<'_> { @@ -64,8 +62,6 @@ impl RequestState<'_> { config: self.config, managed: self.managed, shutdown: self.shutdown, - path_segments: self.path_segments.clone(), - query_items: self.query_items.clone(), route: Atomic::new(self.route.load(Ordering::Acquire)), cookies: self.cookies.clone(), accept: self.accept.clone(), @@ -83,14 +79,12 @@ impl<'r> Request<'r> { method: Method, uri: Origin<'s> ) -> Request<'r> { - let mut request = Request { + Request { uri, method: Atomic::new(method), headers: HeaderMap::new(), remote: None, state: RequestState { - path_segments: SmallVec::new(), - query_items: None, config: &rocket.config, managed: &rocket.managed_state, shutdown: &rocket.shutdown_handle, @@ -98,12 +92,9 @@ impl<'r> Request<'r> { cookies: CookieJar::new(&rocket.config.secret_key), accept: Storage::new(), content_type: Storage::new(), - cache: Arc::new(Container::new()), + cache: Arc::new(::new()), } - }; - - request.update_cached_uri_info(); - request + } } /// Retrieve the method from `self`. @@ -173,12 +164,11 @@ impl<'r> Request<'r> { /// let uri = Origin::parse("/hello/Sergio?type=greeting").unwrap(); /// request.set_uri(uri); /// assert_eq!(request.uri().path(), "/hello/Sergio"); - /// assert_eq!(request.uri().query(), Some("type=greeting")); + /// assert_eq!(request.uri().query().unwrap(), "type=greeting"); /// # }); /// ``` pub fn set_uri<'u: 'r>(&mut self, uri: Origin<'u>) { self.uri = uri; - self.update_cached_uri_info(); } /// Returns the address of the remote connection that initiated this @@ -470,6 +460,11 @@ impl<'r> Request<'r> { } } + /// Returns the Rocket server configuration. + pub fn config(&self) -> &'r Config { + &self.state.config + } + /// Returns the configured application data limits. /// /// # Example @@ -518,8 +513,15 @@ impl<'r> Request<'r> { /// let outcome = request.guard::(); /// # }); /// ``` + pub fn guard<'z, 'a, T>(&'a self) -> BoxFuture<'z, Outcome> + where T: FromRequest<'a, 'r> + 'z, 'a: 'z, 'r: 'z + { + T::from_request(self) + } + + /// Retrieve managed state. /// - /// Retrieve managed state inside of a guard implementation: + /// # Example /// /// ```rust /// # use rocket::Request; @@ -528,15 +530,9 @@ impl<'r> Request<'r> { /// /// # type Pool = usize; /// # Request::example(Method::Get, "/uri", |request| { - /// let pool = request.guard::>(); + /// let pool = request.managed_state::(); /// # }); /// ``` - pub fn guard<'z, 'a, T>(&'a self) -> BoxFuture<'z, Outcome> - where T: FromRequest<'a, 'r> + 'z, 'a: 'z, 'r: 'z - { - T::from_request(self) - } - #[inline(always)] pub fn managed_state(&self) -> Option<&'r T> where T: Send + Sync + 'static @@ -549,18 +545,22 @@ impl<'r> Request<'r> { /// request, `f` is called to produce the value which is subsequently /// returned. /// + /// Different values of the same type _cannot_ be cached without using a + /// proxy, wrapper type. To avoid the need to write these manually, or for + /// libraries wishing to store values of public types, use the + /// [`local_cache!`] macro to generate a locally anonymous wrapper type, + /// store, and retrieve the wrapped value from request-local cache. + /// /// # Example /// /// ```rust - /// # use rocket::http::Method; - /// # use rocket::Request; - /// # type User = (); - /// fn current_user(request: &Request) -> User { - /// // Validate request for a given user, load from database, etc. - /// } + /// # rocket::Request::example(rocket::http::Method::Get, "/uri", |request| { + /// // The first store into local cache for a given type wins. + /// let value = request.local_cache(|| "hello"); + /// assert_eq!(*request.local_cache(|| "hello"), "hello"); /// - /// # Request::example(Method::Get, "/uri", |request| { - /// let user = request.local_cache(|| current_user(request)); + /// // The following return the cached, previously stored value for the type. + /// assert_eq!(*request.local_cache(|| "goodbye"), "hello"); /// # }); /// ``` pub fn local_cache(&self, f: F) -> &T @@ -619,30 +619,30 @@ impl<'r> Request<'r> { /// /// ```rust /// # use rocket::{Request, http::Method}; - /// use rocket::http::{RawStr, uri::Origin}; + /// use rocket::http::uri::Origin; /// /// # Request::example(Method::Get, "/", |req| { - /// fn string<'s>(req: &'s mut Request, uri: &'static str, n: usize) -> &'s RawStr { + /// fn string<'s>(req: &'s mut Request, uri: &'static str, n: usize) -> &'s str { /// req.set_uri(Origin::parse(uri).unwrap()); /// - /// req.get_param(n) + /// req.param(n) /// .and_then(|r| r.ok()) /// .unwrap_or("unnamed".into()) /// } /// - /// assert_eq!(string(req, "/", 0).as_str(), "unnamed"); - /// assert_eq!(string(req, "/a/b/this_one", 0).as_str(), "a"); - /// assert_eq!(string(req, "/a/b/this_one", 1).as_str(), "b"); - /// assert_eq!(string(req, "/a/b/this_one", 2).as_str(), "this_one"); - /// assert_eq!(string(req, "/a/b/this_one", 3).as_str(), "unnamed"); - /// assert_eq!(string(req, "/a/b/c/d/e/f/g/h", 7).as_str(), "h"); + /// assert_eq!(string(req, "/", 0), "unnamed"); + /// assert_eq!(string(req, "/a/b/this_one", 0), "a"); + /// assert_eq!(string(req, "/a/b/this_one", 1), "b"); + /// assert_eq!(string(req, "/a/b/this_one", 2), "this_one"); + /// assert_eq!(string(req, "/a/b/this_one", 3), "unnamed"); + /// assert_eq!(string(req, "/a/b/c/d/e/f/g/h", 7), "h"); /// # }); /// ``` #[inline] - pub fn get_param<'a, T>(&'a self, n: usize) -> Option> + pub fn param<'a, T>(&'a self, n: usize) -> Option> where T: FromParam<'a> { - Some(T::from_param(self.raw_segment_str(n)?)) + self.routed_segment(n).map(T::from_param) } /// Retrieves and parses into `T` all of the path segments in the request @@ -669,7 +669,7 @@ impl<'r> Request<'r> { /// fn path<'s>(req: &'s mut Request, uri: &'static str, n: usize) -> PathBuf { /// req.set_uri(Origin::parse(uri).unwrap()); /// - /// req.get_segments(n) + /// req.segments(n..) /// .and_then(|r| r.ok()) /// .unwrap_or_else(|| "whoops".into()) /// } @@ -683,57 +683,78 @@ impl<'r> Request<'r> { /// # }); /// ``` #[inline] - pub fn get_segments<'a, T>(&'a self, n: usize) -> Option> + pub fn segments<'a, T>(&'a self, n: RangeFrom) -> Option> where T: FromSegments<'a> { - Some(T::from_segments(self.raw_segments(n)?)) + // FIXME: https://github.com/SergioBenitez/Rocket/issues/985. + let segments = self.routed_segments(n); + if segments.is_empty() { + None + } else { + Some(T::from_segments(segments)) + } } - /// Retrieves and parses into `T` the query value with key `key`. `T` must - /// implement [`FromFormValue`], which is used to parse the query's value. - /// Key matching is performed case-sensitively. If there are multiple pairs - /// with key `key`, the _last_ one is returned. + /// Retrieves and parses into `T` the query value with field name `name`. + /// `T` must implement [`FromFormValue`], which is used to parse the query's + /// value. Key matching is performed case-sensitively. If there are multiple + /// pairs with key `key`, the _first_ one is returned. /// - /// This method exists only to be used by manual routing. To retrieve - /// query values from a request, use Rocket's code generation facilities. + /// # Warning + /// + /// This method exists _only_ to be used by manual routing and should + /// _never_ be used in a regular Rocket application. It is much more + /// expensive to use this method than to retrieve query parameters via + /// Rocket's codegen. To retrieve query values from a request, _always_ + /// prefer to use Rocket's code generation facilities. /// /// # Error /// - /// If a query segment with key `key` isn't present, returns `None`. If + /// If a query segment with name `name` isn't present, returns `None`. If /// parsing the value fails, returns `Some(Err(T:Error))`. /// /// # Example /// /// ```rust - /// # use rocket::{Request, http::Method}; - /// use std::path::PathBuf; - /// use rocket::http::{RawStr, uri::Origin}; - /// - /// # Request::example(Method::Get, "/", |req| { - /// fn value<'s>(req: &'s mut Request, uri: &'static str, key: &str) -> &'s RawStr { - /// req.set_uri(Origin::parse(uri).unwrap()); - /// - /// req.get_query_value(key) - /// .and_then(|r| r.ok()) - /// .unwrap_or("n/a".into()) + /// # use rocket::{Request, http::Method, form::FromForm}; + /// # fn with_request)>(uri: &str, f: F) { + /// # Request::example(Method::Get, uri, f); + /// # } + /// with_request("/?a=apple&z=zebra&a=aardvark", |req| { + /// assert_eq!(req.query_value::<&str>("a").unwrap(), Ok("apple")); + /// assert_eq!(req.query_value::<&str>("z").unwrap(), Ok("zebra")); + /// assert_eq!(req.query_value::<&str>("b"), None); + /// + /// let a_seq = req.query_value::>("a").unwrap(); + /// assert_eq!(a_seq.unwrap(), ["apple", "aardvark"]); + /// }); + /// + /// #[derive(Debug, PartialEq, FromForm)] + /// struct Dog<'r> { + /// name: &'r str, + /// age: usize /// } /// - /// assert_eq!(value(req, "/?a=apple&z=zebra", "a").as_str(), "apple"); - /// assert_eq!(value(req, "/?a=apple&z=zebra", "z").as_str(), "zebra"); - /// assert_eq!(value(req, "/?a=apple&z=zebra", "A").as_str(), "n/a"); - /// assert_eq!(value(req, "/?a=apple&z=zebra&a=argon", "a").as_str(), "argon"); - /// assert_eq!(value(req, "/?a=1&a=2&a=3&b=4", "a").as_str(), "3"); - /// assert_eq!(value(req, "/?a=apple&z=zebra", "apple").as_str(), "n/a"); - /// # }); + /// with_request("/?dog.name=Max+Fido&dog.age=3", |req| { + /// let dog = req.query_value::("dog").unwrap().unwrap(); + /// assert_eq!(dog, Dog { name: "Max Fido", age: 3 }); + /// }); /// ``` #[inline] - pub fn get_query_value<'a, T>(&'a self, key: &str) -> Option> - where T: FromFormValue<'a> + pub fn query_value<'a, T>(&'a self, name: &str) -> Option> + where T: FromForm<'a> { - self.raw_query_items()? - .rev() - .find(|item| item.key.as_str() == key) - .map(|item| T::from_form_value(item.value)) + if self.query_fields().find(|f| f.name == name).is_none() { + return None; + } + + let mut ctxt = T::init(form::Options::Lenient); + + self.query_fields() + .filter(|f| f.name == name) + .for_each(|f| T::push_value(&mut ctxt, f.shift())); + + Some(T::finalize(ctxt)) } } @@ -762,67 +783,28 @@ impl<'r> Request<'r> { f(&mut request); } - // Updates the cached `path_segments` and `query_items` in `self.state`. - // MUST be called whenever a new URI is set or updated. - #[inline] - fn update_cached_uri_info(&mut self) { - let path_segments = Segments(self.uri.path()) - .map(|s| indices(s, self.uri.path())) - .collect(); - - let query_items = self.uri.query() - .map(|query_str| FormItems::from(query_str) - .map(|item| IndexedFormItem::from(query_str, item)) - .collect() - ); - - self.state.path_segments = path_segments; - self.state.query_items = query_items; - } - /// Get the `n`th path segment, 0-indexed, after the mount point for the /// currently matched route, as a string, if it exists. Used by codegen. #[inline] - pub fn raw_segment_str(&self, n: usize) -> Option<&RawStr> { - self.routed_path_segment(n) - .map(|(i, j)| self.uri.path()[i..j].into()) + pub fn routed_segment(&self, n: usize) -> Option<&str> { + self.routed_segments(0..).get(n) } /// 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 raw_segments(&self, n: usize) -> Option> { - self.routed_path_segment(n) - .map(|(i, _)| Segments(&self.uri.path()[i..]) ) - } - - // Returns an iterator over the raw segments of the path URI. Does not take - // into account the current route. This is used during routing. - #[inline] - pub(crate) fn raw_path_segments(&self) -> impl Iterator { - let path = self.uri.path(); - self.state.path_segments.iter().cloned() - .map(move |(i, j)| path[i..j].into()) - } - - #[inline] - fn routed_path_segment(&self, n: usize) -> Option<(usize, usize)> { + pub fn routed_segments(&self, n: RangeFrom) -> Segments<'_> { let mount_segments = self.route() - .map(|r| r.base.segment_count()) + .map(|r| r.base.path_segments().len()) .unwrap_or(0); - self.state.path_segments.get(mount_segments + n).map(|(i, j)| (*i, *j)) + self.uri().path_segments().skip(mount_segments + n.start) } // Retrieves the pre-parsed query items. Used by matching and codegen. #[inline] - pub fn raw_query_items( - &self - ) -> Option> + DoubleEndedIterator + Clone> { - let query = self.uri.query()?; - self.state.query_items.as_ref().map(move |items| { - items.iter().map(move |item| item.convert(query)) - }) + pub fn query_fields(&self) -> impl Iterator> { + self.uri().query_segments().map(ValueField::from) } /// Set `self`'s parameters given that the route used to reach this request @@ -850,21 +832,21 @@ impl<'r> Request<'r> { h_headers: hyper::HeaderMap, h_uri: &'r hyper::Uri, h_addr: SocketAddr, - ) -> Result, String> { + ) -> 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(paq)) => paq.as_str(), - _ => return Err(format!("Bad URI: {}", h_uri)), + _ => return Err(Error::InvalidUri(h_uri)), }; // Ensure that the method is known. TODO: Allow made-up methods? let method = match Method::from_hyp(&h_method) { Some(method) => method, - None => return Err(format!("Unknown or invalid method: {}", h_method)) + 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).map_err(|e| e.to_string())?; + let uri = Origin::parse(uri)?; // Construct the request object. let mut request = Request::new(rocket, method, uri); @@ -885,8 +867,9 @@ impl<'r> Request<'r> { } // Set the rest of the headers. + // This is rather unfortunate and slow. for (name, value) in h_headers.iter() { - // This is not totally correct since values needn't be UTF8. + // FIXME: This is not totally correct since values needn't be UTF8. let value_str = String::from_utf8_lossy(value.as_bytes()).into_owned(); let header = Header::new(name.to_string(), value_str); request.add_header(header); @@ -896,6 +879,31 @@ impl<'r> Request<'r> { } } +#[derive(Debug)] +pub(crate) enum Error<'r> { + InvalidUri(&'r hyper::Uri), + UriParse(crate::http::uri::Error<'r>), + BadMethod(hyper::Method), +} + +impl fmt::Display for Error<'_> { + /// Pretty prints a Request. This is primarily used by Rocket's logging + /// infrastructure. + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Error::InvalidUri(u) => write!(f, "invalid origin URI: {}", u), + Error::UriParse(u) => write!(f, "URI `{}` failed to parse as origin", u), + Error::BadMethod(m) => write!(f, "invalid or unrecognized method: {}", m), + } + } +} + +impl<'r> From> for Error<'r> { + fn from(uri_parse: crate::http::uri::Error<'r>) -> Self { + Error::UriParse(uri_parse) + } +} + impl fmt::Debug for Request<'_> { fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> fmt::Result { fmt.debug_struct("Request") @@ -924,35 +932,3 @@ impl fmt::Display for Request<'_> { Ok(()) } } - -type Indices = (usize, usize); - -#[derive(Clone)] -pub(crate) struct IndexedFormItem { - raw: Indices, - key: Indices, - value: Indices -} - -impl IndexedFormItem { - #[inline(always)] - fn from(s: &str, i: FormItem<'_>) -> Self { - let (r, k, v) = (indices(i.raw, s), indices(i.key, s), indices(i.value, s)); - IndexedFormItem { raw: r, key: k, value: v } - } - - #[inline(always)] - fn convert<'s>(&self, source: &'s str) -> FormItem<'s> { - FormItem { - raw: source[self.raw.0..self.raw.1].into(), - key: source[self.key.0..self.key.1].into(), - value: source[self.value.0..self.value.1].into(), - } - } -} - -fn indices(needle: &str, haystack: &str) -> (usize, usize) { - Indexed::checked_from(needle, haystack) - .expect("segments inside of path/query") - .indices() -} diff --git a/core/lib/src/response/debug.rs b/core/lib/src/response/debug.rs index 8ff46e3671..6225217a37 100644 --- a/core/lib/src/response/debug.rs +++ b/core/lib/src/response/debug.rs @@ -1,15 +1,31 @@ use crate::request::Request; -use crate::response::{self, Response, Responder}; +use crate::response::{self, Responder}; use crate::http::Status; use yansi::Paint; -/// Debug prints the internal value before responding with a 500 error. +/// Debug prints the internal value before forwarding to the 500 error catcher. /// /// This value exists primarily to allow handler return types that would not /// otherwise implement [`Responder`]. It is typically used in conjunction with /// `Result` where `E` implements `Debug` but not `Responder`. /// +/// Note that because of it's common use as an error value, `std::io::Error` +/// _does_ implement `Responder`. As a result, a `std::io::Result` can be +/// returned directly without the need for `Debug`: +/// +/// ```rust +/// use std::io; +/// +/// # use rocket::get; +/// use rocket::response::NamedFile; +/// +/// #[get("/")] +/// async fn index() -> io::Result { +/// NamedFile::open("index.html").await +/// } +/// ``` +/// /// # Example /// /// Because of the generic `From` implementation for `Debug`, conversions @@ -17,16 +33,18 @@ use yansi::Paint; /// automatically: /// /// ```rust -/// use std::io; +/// use std::string::FromUtf8Error; /// -/// # use rocket::post; -/// use rocket::data::{Data, ToByteUnit}; +/// # use rocket::get; /// use rocket::response::Debug; /// -/// #[post("/", format = "plain", data = "")] -/// async fn post(data: Data) -> Result> { -/// let name = data.open(32.bytes()).stream_to_string().await?; -/// Ok(name) +/// #[get("/")] +/// fn rand_str() -> Result> { +/// # /* +/// let bytes: Vec = random_bytes(); +/// # */ +/// # let bytes: Vec = vec![]; +/// Ok(String::from_utf8(bytes)?) /// } /// ``` /// @@ -62,6 +80,14 @@ impl<'r, E: std::fmt::Debug> Responder<'r, 'static> for Debug { fn respond_to(self, _: &'r Request<'_>) -> response::Result<'static> { warn_!("Debug: {:?}", Paint::default(self.0)); warn_!("Debug always responds with {}.", Status::InternalServerError); - Response::build().status(Status::InternalServerError).ok() + Err(Status::InternalServerError) + } +} + +/// Prints a warning with the error and forwards to the `500` error catcher. +impl<'r> Responder<'r, 'static> for std::io::Error { + fn respond_to(self, _: &'r Request<'_>) -> response::Result<'static> { + warn_!("I/O Error: {:?}", yansi::Paint::default(self)); + Err(Status::InternalServerError) } } diff --git a/core/lib/src/response/flash.rs b/core/lib/src/response/flash.rs index 198b599424..fccbad3550 100644 --- a/core/lib/src/response/flash.rs +++ b/core/lib/src/response/flash.rs @@ -6,7 +6,7 @@ use serde::ser::{Serialize, Serializer, SerializeStruct}; use crate::outcome::IntoOutcome; use crate::response::{self, Responder}; use crate::request::{self, Request, FromRequest}; -use crate::http::{Status, Cookie}; +use crate::http::{Status, Cookie, CookieJar}; use std::sync::atomic::{AtomicBool, Ordering}; // The name of the actual flash cookie. @@ -52,10 +52,9 @@ const FLASH_COOKIE_DELIM: char = ':'; /// # #[macro_use] extern crate rocket; /// use rocket::response::{Flash, Redirect}; /// use rocket::request::FlashMessage; -/// use rocket::http::RawStr; /// /// #[post("/login/")] -/// fn login(name: &RawStr) -> Result<&'static str, Flash> { +/// fn login(name: &str) -> Result<&'static str, Flash> { /// if name == "special_user" { /// Ok("Hello, special user!") /// } else { @@ -97,7 +96,7 @@ pub struct Flash { /// /// [`name()`]: Flash::name() /// [`msg()`]: Flash::msg() -pub type FlashMessage<'a, 'r> = crate::response::Flash<&'a Request<'r>>; +pub type FlashMessage<'a> = crate::response::Flash<&'a CookieJar<'a>>; impl Flash { /// Constructs a new `Flash` message with the given `name`, `msg`, and @@ -199,15 +198,15 @@ impl<'r, 'o: 'r, R: Responder<'r, 'o>> Responder<'r, 'o> for Flash { } } -impl<'a, 'r> Flash<&'a Request<'r>> { +impl<'a> FlashMessage<'a> { /// Constructs a new message with the given name and message for the given /// request. - fn named(name: &str, msg: &str, req: &'a Request<'r>) -> Flash<&'a Request<'r>> { + fn named<'r: 'a>(name: &str, msg: &str, req: &'a Request<'r>) -> FlashMessage<'a> { Flash { name: name.to_string(), message: msg.to_string(), consumed: AtomicBool::new(false), - inner: req, + inner: req.cookies(), } } @@ -215,7 +214,7 @@ impl<'a, 'r> Flash<&'a Request<'r>> { fn clear_cookie_if_needed(&self) { // Remove the cookie if it hasn't already been removed. if !self.consumed.swap(true, Ordering::Relaxed) { - self.inner.cookies().remove(Cookie::named(FLASH_COOKIE_NAME)); + self.inner.remove(Cookie::named(FLASH_COOKIE_NAME)); } } @@ -238,7 +237,7 @@ impl<'a, 'r> Flash<&'a Request<'r>> { /// The suggested use is through an `Option` and the `FlashMessage` type alias /// in `request`: `Option`. #[crate::async_trait] -impl<'a, 'r> FromRequest<'a, 'r> for Flash<&'a Request<'r>> { +impl<'a, 'r> FromRequest<'a, 'r> for FlashMessage<'a> { type Error = (); async fn from_request(req: &'a Request<'r>) -> request::Outcome { diff --git a/core/lib/src/response/named_file.rs b/core/lib/src/response/named_file.rs index f6a7b2b1ba..e810ceeb8d 100644 --- a/core/lib/src/response/named_file.rs +++ b/core/lib/src/response/named_file.rs @@ -22,15 +22,16 @@ impl NamedFile { /// errors may also be returned according to /// [`OpenOptions::open()`](std::fs::OpenOptions::open()). /// - /// # Examples + /// # Example /// /// ```rust + /// # use rocket::get; /// use rocket::response::NamedFile; /// - /// #[allow(unused_variables)] - /// # rocket::async_test(async { - /// let file = NamedFile::open("foo.txt").await; - /// }); + /// #[get("/")] + /// async fn index() -> Option { + /// NamedFile::open("index.html").await.ok() + /// } /// ``` pub async fn open>(path: P) -> io::Result { // FIXME: Grab the file size here and prohibit `seek`ing later (or else @@ -42,18 +43,54 @@ impl NamedFile { } /// Retrieve the underlying `File`. + /// + /// # Example + /// + /// ```rust + /// use rocket::response::NamedFile; + /// + /// # async fn f() -> std::io::Result<()> { + /// let named_file = NamedFile::open("index.html").await?; + /// let file = named_file.file(); + /// # Ok(()) + /// # } + /// ``` #[inline(always)] pub fn file(&self) -> &File { &self.1 } /// Retrieve a mutable borrow to the underlying `File`. + /// + /// # Example + /// + /// ```rust + /// use rocket::response::NamedFile; + /// + /// # async fn f() -> std::io::Result<()> { + /// let mut named_file = NamedFile::open("index.html").await?; + /// let file = named_file.file_mut(); + /// # Ok(()) + /// # } + /// ``` #[inline(always)] pub fn file_mut(&mut self) -> &mut File { &mut self.1 } /// Take the underlying `File`. + /// + /// # Example + /// + /// ```rust + /// use rocket::response::NamedFile; + /// + /// # async fn f() -> std::io::Result<()> { + /// let named_file = NamedFile::open("index.html").await?; + /// let file = named_file.take_file(); + /// # Ok(()) + /// # } + /// ``` #[inline(always)] pub fn take_file(self) -> File { self.1 @@ -64,11 +101,9 @@ impl NamedFile { /// # Examples /// /// ```rust - /// # use std::io; /// use rocket::response::NamedFile; /// - /// # #[allow(dead_code)] - /// # async fn demo_path() -> io::Result<()> { + /// # async fn demo_path() -> std::io::Result<()> { /// let file = NamedFile::open("foo.txt").await?; /// assert_eq!(file.path().as_os_str(), "foo.txt"); /// # Ok(()) diff --git a/core/lib/src/response/status.rs b/core/lib/src/response/status.rs index 342c559057..fa20d64db5 100644 --- a/core/lib/src/response/status.rs +++ b/core/lib/src/response/status.rs @@ -1,11 +1,31 @@ //! Contains types that set the status code and corresponding headers of a //! response. //! -//! These types are designed to make it easier to respond correctly with a given -//! status code. Each type takes in the minimum number of parameters required to -//! construct a proper response with that status code. Some types take in +//! # Responding +//! +//! Types in this module designed to make it easier to construct correct +//! responses with a given status code. Each type takes in the minimum number of +//! parameters required to construct a correct response. Some types take in //! responders; when they do, the responder finalizes the response by writing //! out additional headers and, importantly, the body of the response. +//! +//! +//! +//! The [`Custom`] type allows responding with _any_ `Status` but _does not_ +//! ensure that all of the required headers are present. As a convenience, +//! `(Status, R)` where `R: Responder` is _also_ a `Responder`, identical to +//! `Custom`. +//! +//! ```rust +//! # extern crate rocket; +//! # use rocket::get; +//! use rocket::http::Status; +//! +//! #[get("/")] +//! fn index() -> (Status, &'static str) { +//! (Status::NotFound, "Hey, there's no index!") +//! } +//! ``` use std::hash::{Hash, Hasher}; use std::collections::hash_map::DefaultHasher; @@ -418,11 +438,15 @@ impl<'r, 'o: 'r, R: Responder<'r, 'o>> Responder<'r, 'o> for Conflict { /// # Example /// /// ```rust +/// # use rocket::get; /// use rocket::response::status; /// use rocket::http::Status; /// /// # #[allow(unused_variables)] -/// let response = status::Custom(Status::ImATeapot, "Hi!"); +/// #[get("/")] +/// fn handler() -> status::Custom<&'static str> { +/// status::Custom(Status::ImATeapot, "Hi!") +/// } /// ``` #[derive(Debug, Clone, PartialEq)] pub struct Custom(pub Status, pub R); @@ -437,5 +461,12 @@ impl<'r, 'o: 'r, R: Responder<'r, 'o>> Responder<'r, 'o> for Custom { } } +impl<'r, 'o: 'r, R: Responder<'r, 'o>> Responder<'r, 'o> for (Status, R) { + #[inline(always)] + fn respond_to(self, request: &'r Request<'_>) -> response::Result<'o> { + Custom(self.0, self.1).respond_to(request) + } +} + // The following are unimplemented. // 206 Partial Content (variant), 203 Non-Authoritative Information (headers). diff --git a/core/lib/src/rocket.rs b/core/lib/src/rocket.rs index 5b2655d873..23d4682b93 100644 --- a/core/lib/src/rocket.rs +++ b/core/lib/src/rocket.rs @@ -21,7 +21,7 @@ use crate::error::{Error, ErrorKind}; pub struct Rocket { pub(crate) config: Config, pub(crate) figment: Figment, - pub(crate) managed_state: Container, + pub(crate) managed_state: Container![Send + Sync], pub(crate) router: Router, pub(crate) default_catcher: Option, pub(crate) catchers: HashMap, @@ -85,7 +85,7 @@ impl Rocket { logger::try_init(config.log_level, config.cli_colors, false); config.pretty_print(&figment); - let managed_state = Container::new(); + let managed_state = ::new(); let (shutdown_sender, shutdown_receiver) = mpsc::channel(1); Rocket { config, figment, diff --git a/core/lib/src/router/collider.rs b/core/lib/src/router/collider.rs index df343a85b6..aec4b3fe46 100644 --- a/core/lib/src/router/collider.rs +++ b/core/lib/src/router/collider.rs @@ -2,6 +2,7 @@ use super::Route; use crate::http::MediaType; use crate::http::route::Kind; +use crate::form::ValueField; use crate::request::Request; impl Route { @@ -64,45 +65,41 @@ fn paths_collide(route: &Route, other: &Route) -> bool { a_segments.len() == b_segments.len() } -fn paths_match(route: &Route, request: &Request<'_>) -> bool { +fn paths_match(route: &Route, req: &Request<'_>) -> bool { let route_segments = &route.metadata.path_segments; - if route_segments.len() > request.state.path_segments.len() { + let req_segments = req.routed_segments(0..); + if route_segments.len() > req_segments.len() { return false; } - let request_segments = request.raw_path_segments(); - for (route_seg, req_seg) in route_segments.iter().zip(request_segments) { + for (route_seg, req_seg) in route_segments.iter().zip(req_segments) { match route_seg.kind { Kind::Multi => return true, - Kind::Static if &*route_seg.string != req_seg.as_str() => return false, + Kind::Static if route_seg.string != req_seg => return false, _ => continue, } } - route_segments.len() == request.state.path_segments.len() + route_segments.len() == req_segments.len() } -fn queries_match(route: &Route, request: &Request<'_>) -> bool { +fn queries_match(route: &Route, req: &Request<'_>) -> bool { if route.metadata.fully_dynamic_query { return true; } - let route_query_segments = match route.metadata.query_segments { - Some(ref segments) => segments, + let route_segments = match route.metadata.query_segments { + Some(ref segments) => segments.iter(), None => return true }; - let req_query_segments = match request.raw_query_items() { - Some(iter) => iter.map(|item| item.raw.as_str()), - None => return route.metadata.fully_dynamic_query - }; - - for seg in route_query_segments.iter() { - if seg.kind == Kind::Static { - // it's okay; this clones the iterator - if !req_query_segments.clone().any(|r| r == seg.string) { - return false; + for seg in route_segments.filter(|s| s.kind == Kind::Static) { + if !req.query_fields().any(|f| f == ValueField::parse(&seg.string)) { + trace_!("route {} missing static query {}", route, seg.string); + for f in req.query_fields() { + trace_!("field: {:?}", f); } + return false; } } diff --git a/core/lib/src/router/route.rs b/core/lib/src/router/route.rs index da0bf51d58..a25125ef7d 100644 --- a/core/lib/src/router/route.rs +++ b/core/lib/src/router/route.rs @@ -213,7 +213,8 @@ impl Route { /// ``` #[inline] pub fn base(&self) -> &str { - self.base.path() + // This is ~ok as the route path is assumed to be percent decoded. + self.base.path().as_str() } /// Retrieves this route's path. @@ -228,10 +229,10 @@ impl Route { /// let index = Route::new(Method::Get, "/foo/bar?a=1", handler); /// let index = index.map_base(|base| format!("{}{}", "/boo", base)).unwrap(); /// assert_eq!(index.uri.path(), "/boo/foo/bar"); - /// assert_eq!(index.uri.query(), Some("a=1")); + /// assert_eq!(index.uri.query().unwrap(), "a=1"); /// assert_eq!(index.base(), "/boo"); /// assert_eq!(index.path().path(), "/foo/bar"); - /// assert_eq!(index.path().query(), Some("a=1")); + /// assert_eq!(index.path().query().unwrap(), "a=1"); /// ``` #[inline] pub fn path(&self) -> &Origin<'_> { @@ -278,6 +279,10 @@ impl Route { impl fmt::Display for Route { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + if let Some(n) = self.name { + write!(f, "{}{}{} ", Paint::cyan("("), Paint::white(n), Paint::cyan(")"))?; + } + write!(f, "{} ", Paint::green(&self.method))?; if self.base.path() != "/" { write!(f, "{}", Paint::blue(&self.base).underline())?; @@ -293,11 +298,6 @@ impl fmt::Display for Route { write!(f, " {}", Paint::yellow(format))?; } - if let Some(name) = self.name { - write!(f, " {}{}{}", - Paint::cyan("("), Paint::magenta(name), Paint::cyan(")"))?; - } - Ok(()) } } diff --git a/core/lib/src/server.rs b/core/lib/src/server.rs index 8f2d270d56..ca7e8420c1 100644 --- a/core/lib/src/server.rs +++ b/core/lib/src/server.rs @@ -6,11 +6,9 @@ use futures::future::{Future, BoxFuture}; use tokio::sync::oneshot; use yansi::Paint; -use crate::Rocket; -use crate::handler; -use crate::request::{Request, FormItems}; -use crate::data::Data; -use crate::response::{Body, Response}; +use crate::{Rocket, Request, Data, handler}; +use crate::form::Form; +use crate::response::{Response, Body}; use crate::outcome::Outcome; use crate::error::{Error, ErrorKind}; use crate::logger::PaintExt; @@ -61,7 +59,7 @@ async fn hyper_service_fn( }; // Retrieve the data from the hyper body. - let mut data = Data::from_hyp(h_body).await; + let mut data = Data::from(h_body); // Dispatch the request to get a response, then write that response out. let token = rocket.preprocess_request(&mut req, &mut data).await; @@ -160,15 +158,13 @@ impl Rocket { let is_form = req.content_type().map_or(false, |ct| ct.is_form()); if is_form && req.method() == Method::Post && peek_buffer.len() >= min_len { - if let Ok(form) = std::str::from_utf8(peek_buffer) { - let method: Option> = FormItems::from(form) - .filter(|item| item.key.as_str() == "_method") - .map(|item| item.value.parse()) - .next(); - - if let Some(Ok(method)) = method { - req._set_method(method); - } + let method = std::str::from_utf8(peek_buffer).ok() + .and_then(|raw_form| Form::values(raw_form).next()) + .filter(|field| field.name == "_method") + .and_then(|field| field.value.parse().ok()); + + if let Some(method) = method { + req._set_method(method); } } diff --git a/core/lib/src/request/state.rs b/core/lib/src/state.rs similarity index 97% rename from core/lib/src/request/state.rs rename to core/lib/src/state.rs index 811b46edbf..25a9947c36 100644 --- a/core/lib/src/request/state.rs +++ b/core/lib/src/state.rs @@ -32,7 +32,7 @@ use crate::http::Status; /// } /// /// #[get("/")] -/// fn index(state: State) -> String { +/// fn index(state: State<'_, MyConfig>) -> String { /// format!("The config value is: {}", state.user_val) /// } /// @@ -88,7 +88,7 @@ use crate::http::Status; /// struct MyManagedState(usize); /// /// #[get("/")] -/// fn handler(state: State) -> String { +/// fn handler(state: State<'_, MyManagedState>) -> String { /// state.0.to_string() /// } /// @@ -122,7 +122,7 @@ impl<'r, T: Send + Sync + 'static> State<'r, T> { /// } /// /// // Use the `Deref` implementation which coerces implicitly - /// fn handler2(config: State) -> String { + /// fn handler2(config: State<'_, MyConfig>) -> String { /// config.user_val.clone() /// } /// ``` diff --git a/core/lib/tests/derive-reexports.rs b/core/lib/tests/derive-reexports.rs index ee7f2a1542..f3f46336aa 100644 --- a/core/lib/tests/derive-reexports.rs +++ b/core/lib/tests/derive-reexports.rs @@ -1,10 +1,10 @@ use rocket; use rocket::{get, routes}; -use rocket::request::{Form, FromForm, FromFormValue}; +use rocket::form::{FromForm, FromFormField}; use rocket::response::Responder; -#[derive(FromFormValue)] +#[derive(FromFormField)] enum Thing { A, B, @@ -37,7 +37,7 @@ fn index() -> DerivedResponder { } #[get("/?")] -fn number(params: Form) -> DerivedResponder { +fn number(params: ThingForm) -> DerivedResponder { DerivedResponder { data: params.thing.to_string() } } diff --git a/core/lib/tests/encoded-uris.rs b/core/lib/tests/encoded-uris.rs new file mode 100644 index 0000000000..2be27b40e2 --- /dev/null +++ b/core/lib/tests/encoded-uris.rs @@ -0,0 +1,21 @@ +#[macro_use] extern crate rocket; + +#[get("/hello süper $?a&?&")] +fn index(value: &str) -> &str { + value +} + +mod encoded_uris { + use rocket::local::blocking::Client; + + #[test] + fn can_route_to_encoded_uri() { + let rocket = rocket::ignite().mount("/", routes![super::index]); + let client = Client::untracked(rocket).unwrap(); + let response = client.get("/hello%20s%C3%BCper%20%24?a&%3F&value=a+b") + .dispatch() + .into_string(); + + assert_eq!(response.unwrap(), "a b"); + } +} diff --git a/core/lib/tests/flash-lazy-removes-issue-466.rs b/core/lib/tests/flash-lazy-removes-issue-466.rs index 4eca7a2f58..44f3e9f55c 100644 --- a/core/lib/tests/flash-lazy-removes-issue-466.rs +++ b/core/lib/tests/flash-lazy-removes-issue-466.rs @@ -11,12 +11,12 @@ fn set() -> Flash<&'static str> { } #[get("/unused")] -fn unused(flash: Option>) -> Option<()> { +fn unused(flash: Option>) -> Option<()> { flash.map(|_| ()) } #[get("/use")] -fn used(flash: Option>) -> Option { +fn used(flash: Option>) -> Option { flash.map(|flash| flash.msg().into()) } diff --git a/core/lib/tests/form-validation-names.rs b/core/lib/tests/form-validation-names.rs new file mode 100644 index 0000000000..9438c43844 --- /dev/null +++ b/core/lib/tests/form-validation-names.rs @@ -0,0 +1,138 @@ +use std::fmt::Debug; + +use rocket::form::{Form, FromForm}; +use rocket::form::error::{Error, Errors, ErrorKind}; + +#[derive(Debug, FromForm)] +struct Cat<'v> { + #[field(validate = len(5..))] + name: &'v str, + #[field(validate = starts_with("kitty"))] + nick: &'v str, +} + +#[derive(Debug, FromForm)] +struct Dog<'v> { + #[field(validate = len(5..))] + name: &'v str, +} + +#[derive(Debug, FromForm)] +struct Person<'v> { + kitty: Cat<'v>, + #[field(validate = len(1..))] + cats: Vec>, + dog: Dog<'v>, +} + +fn starts_with<'v, S: AsRef>(string: S, prefix: &str) -> Result<(), Errors<'v>> { + if !string.as_ref().starts_with(prefix) { + Err(Error::validation(format!("must start with {:?}", prefix)))? + } + + Ok(()) +} + +#[track_caller] +fn errors<'v, T: FromForm<'v> + Debug + 'v>(string: &'v str) -> Errors<'v> { + Form::::parse(string).expect_err("expected an error") +} + +#[test] +fn test_form_validation_context() { + use ErrorKind::*; + + fn count<'a, K>(c: &Errors<'_>, n: &str, kind: K, fuzz: bool) -> usize + where K: Into>> + { + let kind = kind.into(); + c.iter().filter(|e| { + let matches = (fuzz && e.is_for(n)) || (!fuzz && e.is_for_exactly(n)); + let kinded = kind.as_ref().map(|k| k == &e.kind).unwrap_or(true); + matches && kinded + }).count() + } + + fn fuzzy<'a, K>(c: &Errors<'_>, n: &str, kind: K) -> usize + where K: Into>> + { + count(c, n, kind, true) + } + + fn exact<'a, K>(c: &Errors<'_>, n: &str, kind: K) -> usize + where K: Into>> + { + count(c, n, kind, false) + } + + let c = errors::("name=littlebobby"); + assert_eq!(exact(&c, "nick", Missing), 1); + assert_eq!(fuzzy(&c, "nick", Missing), 1); + assert_eq!(fuzzy(&c, "nick", None), 1); + + let c = errors::("cats[0].name=Bob"); + assert_eq!(exact(&c, "kitty", None), 1); + assert_eq!(exact(&c, "kitty", Missing), 1); + assert_eq!(exact(&c, "cats[0].nick", None), 1); + assert_eq!(exact(&c, "cats[0].nick", Missing), 1); + assert_eq!(exact(&c, "dog", None), 1); + assert_eq!(exact(&c, "dog", Missing), 1); + assert_eq!(exact(&c, "dog.name", None), 0); + assert_eq!(exact(&c, "kitty.name", None), 0); + assert_eq!(exact(&c, "kitty.nick", None), 0); + + assert_eq!(fuzzy(&c, "kitty", None), 1); + assert_eq!(fuzzy(&c, "kitty.name", Missing), 1); + assert_eq!(fuzzy(&c, "kitty.nick", Missing), 1); + assert_eq!(fuzzy(&c, "cats[0].nick", Missing), 1); + assert_eq!(fuzzy(&c, "dog.name", Missing), 1); + assert_eq!(fuzzy(&c, "dog", None), 1); + + let c = errors::("cats[0].name=Bob&cats[0].nick=kit&kitty.name=Hi"); + assert_eq!(exact(&c, "kitty.nick", Missing), 1); + assert_eq!(exact(&c, "kitty", None), 0); + assert_eq!(exact(&c, "dog", Missing), 1); + assert_eq!(exact(&c, "dog", None), 1); + assert_eq!(exact(&c, "cats[0].name", None), 1); + assert_eq!(exact(&c, "cats[0].name", InvalidLength { min: Some(5), max: None }), 1); + assert_eq!(exact(&c, "cats[0].nick", None), 1); + assert_eq!(exact(&c, "cats[0].nick", Validation("must start with \"kitty\"".into())), 1); + + assert_eq!(fuzzy(&c, "kitty.nick", Missing), 1); + assert_eq!(fuzzy(&c, "kitty.nick", None), 1); + assert_eq!(fuzzy(&c, "kitty", None), 0); + assert_eq!(fuzzy(&c, "dog.name", Missing), 1); + assert_eq!(fuzzy(&c, "dog", Missing), 1); + assert_eq!(fuzzy(&c, "cats[0].nick", None), 1); + assert_eq!(exact(&c, "cats[0].name", None), 1); + + let c = errors::("kitty.name=Michael"); + assert_eq!(exact(&c, "kitty.nick", Missing), 1); + assert_eq!(exact(&c, "dog", Missing), 1); + assert_eq!(exact(&c, "cats[0].name", None), 0); + assert_eq!(exact(&c, "cats[0].nick", None), 0); + + assert_eq!(exact(&c, "cats", None), 1); + assert_eq!(exact(&c, "cats", InvalidLength { min: Some(1), max: None }), 1); + + assert_eq!(fuzzy(&c, "kitty.nick", Missing), 1); + assert_eq!(fuzzy(&c, "kitty.nick", None), 1); + assert_eq!(fuzzy(&c, "dog", None), 1); + assert_eq!(fuzzy(&c, "dog.name", Missing), 1); + assert_eq!(exact(&c, "cats[0].name", None), 0); + assert_eq!(exact(&c, "cats[0].nick", None), 0); + + let c = errors::("kitty.name=Michael&kitty.nick=kittykat&dog.name=woofy"); + assert_eq!(c.iter().count(), 1); + assert_eq!(exact(&c, "cats", None), 1); + assert_eq!(exact(&c, "cats", InvalidLength { min: Some(1), max: None }), 1); + assert_eq!(fuzzy(&c, "cats[0].name", None), 1); +} + +// #[derive(Debug, FromForm)] +// struct Person<'v> { +// kitty: Cat<'v>, +// #[field(validate = len(1..))] +// cats: Vec>, +// dog: Dog<'v>, +// } diff --git a/core/lib/tests/form_method-issue-45.rs b/core/lib/tests/form_method-issue-45.rs index c60b5224e1..8c876e23e6 100644 --- a/core/lib/tests/form_method-issue-45.rs +++ b/core/lib/tests/form_method-issue-45.rs @@ -1,6 +1,6 @@ #[macro_use] extern crate rocket; -use rocket::request::Form; +use rocket::form::Form; #[derive(FromForm)] struct FormData { @@ -9,7 +9,7 @@ struct FormData { #[patch("/", data = "")] fn bug(form_data: Form) -> &'static str { - assert_eq!("Form data", form_data.form_data); + assert_eq!("Form data", form_data.into_inner().form_data); "OK" } diff --git a/core/lib/tests/form_value_decoding-issue-82.rs b/core/lib/tests/form_value_decoding-issue-82.rs index 8a2c796090..7adb16e038 100644 --- a/core/lib/tests/form_value_decoding-issue-82.rs +++ b/core/lib/tests/form_value_decoding-issue-82.rs @@ -1,15 +1,10 @@ #[macro_use] extern crate rocket; -use rocket::request::Form; - -#[derive(FromForm)] -struct FormData { - form_data: String, -} +use rocket::form::Form; #[post("/", data = "")] -fn bug(form_data: Form) -> String { - form_data.into_inner().form_data +fn bug(form_data: Form) -> String { + form_data.into_inner() } mod tests { diff --git a/core/lib/tests/form_value_from_encoded_str-issue-1425.rs b/core/lib/tests/form_value_from_encoded_str-issue-1425.rs index bb6e29a56e..fbcbd65ce8 100644 --- a/core/lib/tests/form_value_from_encoded_str-issue-1425.rs +++ b/core/lib/tests/form_value_from_encoded_str-issue-1425.rs @@ -1,27 +1,29 @@ use std::net::{Ipv4Addr, Ipv6Addr, SocketAddrV4, SocketAddrV6}; -use rocket::request::FromFormValue; +use rocket::http::RawStr; +use rocket::form::Form; -macro_rules! assert_from_form_value_eq { +macro_rules! assert_from_form_field_eq { ($string:literal as $T:ty, $expected:expr) => ( - let value: $T = FromFormValue::from_form_value($string.into()).unwrap(); + let value_str = RawStr::new(concat!("=", $string)); + let value = Form::<$T>::parse_encoded(value_str).unwrap(); assert_eq!(value, $expected); ) } #[test] fn test_from_form_value_encoded() { - assert_from_form_value_eq!( + assert_from_form_field_eq!( "127.0.0.1%3A80" as SocketAddrV4, SocketAddrV4::new(Ipv4Addr::new(127, 0, 0, 1), 80) ); - assert_from_form_value_eq!( + assert_from_form_field_eq!( "2001%3A0db8%3A85a3%3A0000%3A0000%3A8a2e%3A0370%3A7334" as Ipv6Addr, Ipv6Addr::new(0x2001, 0x0db8, 0x85a3, 0, 0, 0x8a2e, 0x0370, 0x7334) ); - assert_from_form_value_eq!( + assert_from_form_field_eq!( "%5B2001%3Adb8%3A%3A1%5D%3A8080" as SocketAddrV6, SocketAddrV6::new(Ipv6Addr::new(0x2001, 0xdb8, 0, 0, 0, 0, 0, 1), 8080, 0, 0) ); diff --git a/core/lib/tests/limits.rs b/core/lib/tests/limits.rs index 253e8545d1..6b4ed29432 100644 --- a/core/lib/tests/limits.rs +++ b/core/lib/tests/limits.rs @@ -1,15 +1,10 @@ #[macro_use] extern crate rocket; -use rocket::request::Form; - -#[derive(FromForm)] -struct Simple { - value: String -} +use rocket::form::Form; #[post("/", data = "")] -fn index(form: Form) -> String { - form.into_inner().value +fn index(form: Form) -> String { + form.into_inner() } mod limits_tests { @@ -19,7 +14,7 @@ mod limits_tests { use rocket::data::Limits; fn rocket_with_forms_limit(limit: u64) -> rocket::Rocket { - let limits = Limits::default().limit("forms", limit.into()); + let limits = Limits::default().limit("form", limit.into()); let config = rocket::Config::figment().merge(("limits", limits)); rocket::custom(config).mount("/", routes![super::index]) } @@ -54,7 +49,7 @@ mod limits_tests { .header(ContentType::Form) .dispatch(); - assert_eq!(response.status(), Status::UnprocessableEntity); + assert_eq!(response.status(), Status::PayloadTooLarge); } #[test] @@ -65,6 +60,6 @@ mod limits_tests { .header(ContentType::Form) .dispatch(); - assert_eq!(response.into_string(), Some("Hell".into())); + assert_eq!(response.status(), Status::PayloadTooLarge); } } diff --git a/core/lib/tests/local-request-content-type-issue-505.rs b/core/lib/tests/local-request-content-type-issue-505.rs index b56a98e6fe..eaebe0a629 100644 --- a/core/lib/tests/local-request-content-type-issue-505.rs +++ b/core/lib/tests/local-request-content-type-issue-505.rs @@ -18,10 +18,10 @@ impl<'a, 'r> FromRequest<'a, 'r> for HasContentType { use rocket::data::{self, FromData}; #[rocket::async_trait] -impl FromData for HasContentType { +impl<'r> FromData<'r> for HasContentType { type Error = (); - async fn from_data(req: &Request<'_>, data: Data) -> data::Outcome { + async fn from_data(req: &'r Request<'_>, data: Data) -> data::Outcome { req.content_type().map(|_| HasContentType).or_forward(data) } } diff --git a/core/lib/tests/session-cookies-issue-1506.rs b/core/lib/tests/session-cookies-issue-1506.rs new file mode 100644 index 0000000000..1d8d9ba986 --- /dev/null +++ b/core/lib/tests/session-cookies-issue-1506.rs @@ -0,0 +1,25 @@ +#[cfg(feature = "secrets")] +mod with_secrets { + use rocket::http::{CookieJar, Cookie}; + + #[rocket::get("/")] + fn index(jar: &CookieJar<'_>) { + let session_cookie = Cookie::build("key", "value").expires(None); + jar.add_private(session_cookie.finish()); + } + + mod test_session_cookies { + use super::*; + use rocket::local::blocking::Client; + + #[test] + fn session_cookie_is_session() { + let rocket = rocket::ignite().mount("/", rocket::routes![index]); + let client = Client::tracked(rocket).unwrap(); + + let response = client.get("/").dispatch(); + let cookie = response.cookies().get_private("key").unwrap(); + assert_eq!(cookie.expires_datetime(), None); + } + } +} diff --git a/core/lib/tests/strict_and_lenient_forms.rs b/core/lib/tests/strict_and_lenient_forms.rs index 04fe1b722c..37f8bbcf36 100644 --- a/core/lib/tests/strict_and_lenient_forms.rs +++ b/core/lib/tests/strict_and_lenient_forms.rs @@ -1,21 +1,20 @@ #[macro_use] extern crate rocket; -use rocket::request::{Form, LenientForm}; -use rocket::http::RawStr; +use rocket::form::{Form, Strict}; #[derive(FromForm)] struct MyForm<'r> { - field: &'r RawStr, + field: &'r str, } #[post("/strict", data = "")] -fn strict<'r>(form: Form>) -> String { - form.field.as_str().into() +fn strict<'r>(form: Form>>) -> &'r str { + form.field } #[post("/lenient", data = "")] -fn lenient<'r>(form: LenientForm>) -> String { - form.field.as_str().into() +fn lenient<'r>(form: Form>) -> &'r str { + form.field } mod strict_and_lenient_forms_tests { diff --git a/examples/content_types/src/main.rs b/examples/content_types/src/main.rs index ba972174e7..ea694f3ca3 100644 --- a/examples/content_types/src/main.rs +++ b/examples/content_types/src/main.rs @@ -6,7 +6,7 @@ use std::io; use rocket::request::Request; use rocket::data::{Data, ToByteUnit}; -use rocket::response::{Debug, content::{Json, Html}}; +use rocket::response::content::{Json, Html}; use serde::{Serialize, Deserialize}; @@ -25,10 +25,10 @@ struct Person { // the route attribute. Note: if this was a real application, we'd use // `rocket_contrib`'s built-in JSON support and return a `JsonValue` instead. #[get("//", format = "json")] -fn get_hello(name: String, age: u8) -> Json { +fn get_hello(name: String, age: u8) -> io::Result> { // NOTE: In a real application, we'd use `rocket_contrib::json::Json`. let person = Person { name, age }; - Json(serde_json::to_string(&person).unwrap()) + Ok(Json(serde_json::to_string(&person)?)) } // In a `POST` request and all other payload supporting request types, the @@ -38,11 +38,11 @@ fn get_hello(name: String, age: u8) -> Json { // In a real application, we wouldn't use `serde_json` directly; instead, we'd // use `contrib::Json` to automatically serialize a type into JSON. #[post("/", format = "plain", data = "")] -async fn post_hello(age: u8, name_data: Data) -> Result, Debug> { - let name = name_data.open(64.bytes()).stream_to_string().await?; - let person = Person { name, age }; +async fn post_hello(age: u8, name_data: Data) -> io::Result> { + let name = name_data.open(64.bytes()).into_string().await?; + let person = Person { name: name.into_inner(), age }; // NOTE: In a real application, we'd use `rocket_contrib::json::Json`. - Ok(Json(serde_json::to_string(&person).expect("valid JSON"))) + Ok(Json(serde_json::to_string(&person)?)) } #[catch(404)] diff --git a/examples/cookies/src/main.rs b/examples/cookies/src/main.rs index d2f148013e..10f0c10f62 100644 --- a/examples/cookies/src/main.rs +++ b/examples/cookies/src/main.rs @@ -5,19 +5,14 @@ mod tests; use std::collections::HashMap; -use rocket::request::Form; +use rocket::form::Form; use rocket::response::Redirect; use rocket::http::{Cookie, CookieJar}; use rocket_contrib::templates::Template; -#[derive(FromForm)] -struct Message { - message: String, -} - #[post("/submit", data = "")] -fn submit(cookies: &CookieJar<'_>, message: Form) -> Redirect { - cookies.add(Cookie::new("message", message.into_inner().message)); +fn submit(cookies: &CookieJar<'_>, message: Form) -> Redirect { + cookies.add(Cookie::new("message", message.into_inner())); Redirect::to("/") } diff --git a/examples/form_kitchen_sink/src/main.rs b/examples/form_kitchen_sink/src/main.rs deleted file mode 100644 index 2a5481b31d..0000000000 --- a/examples/form_kitchen_sink/src/main.rs +++ /dev/null @@ -1,43 +0,0 @@ -#[macro_use] extern crate rocket; - -use rocket::request::{Form, FormError, FormDataError}; -use rocket::http::RawStr; - -use rocket_contrib::serve::{StaticFiles, crate_relative}; - -#[cfg(test)] mod tests; - -#[derive(Debug, FromFormValue)] -enum FormOption { - A, B, C -} - -#[derive(Debug, FromForm)] -struct FormInput<'r> { - checkbox: bool, - number: usize, - #[form(field = "type")] - radio: FormOption, - password: &'r RawStr, - #[form(field = "textarea")] - text_area: String, - select: FormOption, -} - -#[post("/", data = "")] -fn sink(sink: Result>, FormError<'_>>) -> String { - match sink { - Ok(form) => format!("{:?}", &*form), - Err(FormDataError::Io(_)) => format!("Form input was invalid UTF-8."), - Err(FormDataError::Malformed(f)) | Err(FormDataError::Parse(_, f)) => { - format!("Invalid form input: {}", f) - } - } -} - -#[launch] -fn rocket() -> rocket::Rocket { - rocket::ignite() - .mount("/", routes![sink]) - .mount("/", StaticFiles::from(crate_relative!("/static"))) -} diff --git a/examples/form_kitchen_sink/static/index.html b/examples/form_kitchen_sink/static/index.html deleted file mode 100644 index 7fc87cf9c1..0000000000 --- a/examples/form_kitchen_sink/static/index.html +++ /dev/null @@ -1,48 +0,0 @@ -

Rocket Form Kitchen Sink

- - -

- -

- - -

- - -

- - -

- - -

- - -

- diff --git a/examples/form_validation/Cargo.toml b/examples/form_validation/Cargo.toml deleted file mode 100644 index e1be14f49f..0000000000 --- a/examples/form_validation/Cargo.toml +++ /dev/null @@ -1,10 +0,0 @@ -[package] -name = "form_validation" -version = "0.0.0" -workspace = "../../" -edition = "2018" -publish = false - -[dependencies] -rocket = { path = "../../core/lib" } -rocket_contrib = { path = "../../contrib/lib" } diff --git a/examples/form_validation/src/main.rs b/examples/form_validation/src/main.rs deleted file mode 100644 index b253ffefcd..0000000000 --- a/examples/form_validation/src/main.rs +++ /dev/null @@ -1,83 +0,0 @@ -#[macro_use] extern crate rocket; - -#[cfg(test)] mod tests; - -use rocket::response::Redirect; -use rocket::request::{Form, FromFormValue}; -use rocket::http::RawStr; - -use rocket_contrib::serve::{StaticFiles, crate_relative}; - -#[derive(Debug)] -struct StrongPassword<'r>(&'r str); - -#[derive(Debug)] -struct AdultAge(isize); - -#[derive(FromForm)] -struct UserLogin<'r> { - username: &'r RawStr, - password: Result, &'static str>, - age: Result, -} - -impl<'v> FromFormValue<'v> for StrongPassword<'v> { - type Error = &'static str; - - fn from_form_value(v: &'v RawStr) -> Result { - if v.len() < 8 { - Err("too short!") - } else { - Ok(StrongPassword(v.as_str())) - } - } -} - -impl<'v> FromFormValue<'v> for AdultAge { - type Error = &'static str; - - fn from_form_value(v: &'v RawStr) -> Result { - let age = match isize::from_form_value(v) { - Ok(v) => v, - Err(_) => return Err("value is not a number."), - }; - - match age > 20 { - true => Ok(AdultAge(age)), - false => Err("must be at least 21."), - } - } -} - -#[post("/login", data = "")] -fn login(user: Form>) -> Result { - if let Err(e) = user.age { - return Err(format!("Age is invalid: {}", e)); - } - - if let Err(e) = user.password { - return Err(format!("Password is invalid: {}", e)); - } - - if user.username == "Sergio" { - if let Ok(StrongPassword("password")) = user.password { - Ok(Redirect::to("/user/Sergio")) - } else { - Err("Wrong password!".to_string()) - } - } else { - Err(format!("Unrecognized user, '{}'.", user.username)) - } -} - -#[get("/user/")] -fn user_page(username: &RawStr) -> String { - format!("This is {}'s page.", username) -} - -#[launch] -fn rocket() -> rocket::Rocket { - rocket::ignite() - .mount("/", routes![user_page, login]) - .mount("/", StaticFiles::from(crate_relative!("/static"))) -} diff --git a/examples/form_validation/src/tests.rs b/examples/form_validation/src/tests.rs deleted file mode 100644 index 105ca04162..0000000000 --- a/examples/form_validation/src/tests.rs +++ /dev/null @@ -1,84 +0,0 @@ -use super::rocket; -use rocket::local::blocking::Client; -use rocket::http::{ContentType, Status}; - -fn test_login(user: &str, pass: &str, age: &str, status: Status, body: T) - where T: Into> + Send -{ - let client = Client::tracked(rocket()).unwrap(); - let query = format!("username={}&password={}&age={}", user, pass, age); - let response = client.post("/login") - .header(ContentType::Form) - .body(&query) - .dispatch(); - - assert_eq!(response.status(), status); - if let Some(expected_str) = body.into() { - let body_str = response.into_string(); - assert!(body_str.map_or(false, |s| s.contains(expected_str))); - } -} - -#[test] -fn test_good_login() { - test_login("Sergio", "password", "30", Status::SeeOther, None); -} - -#[test] -fn test_invalid_user() { - test_login("-1", "password", "30", Status::Ok, "Unrecognized user"); - test_login("Mike", "password", "30", Status::Ok, "Unrecognized user"); -} - -#[test] -fn test_invalid_password() { - test_login("Sergio", "password1", "30", Status::Ok, "Wrong password!"); - test_login("Sergio", "ok", "30", Status::Ok, "Password is invalid: too short!"); -} - -#[test] -fn test_invalid_age() { - test_login("Sergio", "password", "20", Status::Ok, "must be at least 21."); - test_login("Sergio", "password", "-100", Status::Ok, "must be at least 21."); - test_login("Sergio", "password", "hi", Status::Ok, "value is not a number"); -} - -fn check_bad_form(form_str: &str, status: Status) { - let client = Client::tracked(rocket()).unwrap(); - let response = client.post("/login") - .header(ContentType::Form) - .body(form_str) - .dispatch(); - - assert_eq!(response.status(), status); -} - -#[test] -fn test_bad_form_abnromal_inputs() { - check_bad_form("&&&===&", Status::BadRequest); - check_bad_form("&&&=hi==&", Status::BadRequest); -} - -#[test] -fn test_bad_form_missing_fields() { - let bad_inputs: [&str; 8] = [ - "&", - "=", - "username=Sergio", - "password=pass", - "age=30", - "username=Sergio&password=pass", - "username=Sergio&age=30", - "password=pass&age=30" - ]; - - for bad_input in bad_inputs.iter() { - check_bad_form(bad_input, Status::UnprocessableEntity); - } -} - -#[test] -fn test_bad_form_additional_fields() { - check_bad_form("username=Sergio&password=pass&age=30&addition=1", - Status::UnprocessableEntity); -} diff --git a/examples/form_validation/static/index.html b/examples/form_validation/static/index.html deleted file mode 100644 index 66bf233913..0000000000 --- a/examples/form_validation/static/index.html +++ /dev/null @@ -1,8 +0,0 @@ -

Login

- -
- Username: - Password: - Age: - -
diff --git a/examples/form_kitchen_sink/Cargo.toml b/examples/forms/Cargo.toml similarity index 55% rename from examples/form_kitchen_sink/Cargo.toml rename to examples/forms/Cargo.toml index 41ee2fdccd..2c9ebe0407 100644 --- a/examples/form_kitchen_sink/Cargo.toml +++ b/examples/forms/Cargo.toml @@ -1,5 +1,5 @@ [package] -name = "form_kitchen_sink" +name = "forms" version = "0.0.0" workspace = "../../" edition = "2018" @@ -7,4 +7,5 @@ publish = false [dependencies] rocket = { path = "../../core/lib" } -rocket_contrib = { path = "../../contrib/lib" } +rocket_contrib = { path = "../../contrib/lib", features = ["tera_templates"] } +time = "0.2" diff --git a/examples/forms/Rocket.toml b/examples/forms/Rocket.toml new file mode 100644 index 0000000000..2bb2fabc7b --- /dev/null +++ b/examples/forms/Rocket.toml @@ -0,0 +1,2 @@ +[global.limits] +data-form = "2MiB" diff --git a/examples/forms/src/main.rs b/examples/forms/src/main.rs new file mode 100644 index 0000000000..0ed253f911 --- /dev/null +++ b/examples/forms/src/main.rs @@ -0,0 +1,90 @@ +#[macro_use]extern crate rocket; + +use rocket::http::Status; +use rocket::form::{Form, Contextual, FromForm, FromFormField, Context}; +use rocket::data::TempFile; + +use rocket_contrib::serve::{StaticFiles, crate_relative}; +use rocket_contrib::templates::Template; + +#[derive(Debug, FromForm)] +struct Password<'v> { + #[field(validate = len(6..))] + #[field(validate = eq(self.second))] + first: &'v str, + #[field(validate = eq(self.first))] + second: &'v str, +} + +#[derive(Debug, FromFormField)] +enum Rights { + Public, + Reserved, + Exclusive, +} + +#[derive(Debug, FromFormField)] +enum Category { + Biology, + Chemistry, + Physics, + #[field(value = "CS")] + ComputerScience, +} + +#[derive(Debug, FromForm)] +struct Submission<'v> { + #[field(validate = len(1..))] + title: &'v str, + date: time::Date, + #[field(validate = len(1..=250))] + r#abstract: &'v str, + #[field(validate = ext("pdf"))] + file: TempFile<'v>, + #[field(validate = len(1..))] + category: Vec, + rights: Rights, + ready: bool, +} + +#[derive(Debug, FromForm)] +struct Account<'v> { + #[field(validate = len(1..))] + name: &'v str, + password: Password<'v>, + #[field(validate = contains('@'))] + #[field(validate = omits(self.password.first))] + email: &'v str, +} + +#[derive(Debug, FromForm)] +struct Submit<'v> { + account: Account<'v>, + submission: Submission<'v>, +} + +#[get("/")] +fn index<'r>() -> Template { + Template::render("index", &Context::default()) +} + +#[post("/", data = "
")] +fn submit<'r>(form: Form>>) -> (Status, Template) { + let template = match form.value { + Some(ref submission) => { + println!("submission: {:#?}", submission); + Template::render("success", &form.context) + } + None => Template::render("index", &form.context), + }; + + (form.context.status(), template) +} + +#[launch] +fn rocket() -> rocket::Rocket { + rocket::ignite() + .mount("/", routes![index, submit]) + .attach(Template::fairing()) + .mount("/", StaticFiles::from(crate_relative!("/static"))) +} diff --git a/examples/form_kitchen_sink/src/tests.rs b/examples/forms/src/tests.rs similarity index 100% rename from examples/form_kitchen_sink/src/tests.rs rename to examples/forms/src/tests.rs diff --git a/examples/forms/static/chota.min.css b/examples/forms/static/chota.min.css new file mode 100644 index 0000000000..7acf70a954 --- /dev/null +++ b/examples/forms/static/chota.min.css @@ -0,0 +1 @@ +/*! chota.css v0.8.0 | MIT License | github.com/jenil/chota */:root{--bg-color:#fff;--bg-secondary-color:#f3f3f6;--color-primary:#14854f;--color-lightGrey:#d2d6dd;--color-grey:#747681;--color-darkGrey:#3f4144;--color-error:#d43939;--color-success:#28bd14;--grid-maxWidth:120rem;--grid-gutter:2rem;--font-size:1.6rem;--font-color:#333;--font-family-sans:-apple-system,BlinkMacSystemFont,Avenir,"Avenir Next","Segoe UI","Roboto","Oxygen","Ubuntu","Cantarell","Fira Sans","Droid Sans","Helvetica Neue",sans-serif;--font-family-mono:monaco,"Consolas","Lucida Console",monospace}html{-webkit-box-sizing:border-box;box-sizing:border-box;font-size:62.5%;line-height:1.15;-ms-text-size-adjust:100%;-webkit-text-size-adjust:100%}*,:after,:before{-webkit-box-sizing:inherit;box-sizing:inherit}*{scrollbar-width:thin;scrollbar-color:var(--color-lightGrey) var(--bg-primary)}::-webkit-scrollbar{width:8px}::-webkit-scrollbar-track{background:var(--bg-primary)}::-webkit-scrollbar-thumb{background:var(--color-lightGrey)}body{background-color:var(--bg-color);line-height:1.6;font-size:var(--font-size);color:var(--font-color);font-family:Segoe UI,Helvetica Neue,sans-serif;font-family:var(--font-family-sans);margin:0;padding:0}h1,h2,h3,h4,h5,h6{font-weight:500;margin:.35em 0 .7em}h1{font-size:2em}h2{font-size:1.75em}h3{font-size:1.5em}h4{font-size:1.25em}h5{font-size:1em}h6{font-size:.85em}a{color:var(--color-primary);text-decoration:none}a:hover:not(.button){opacity:.75}button{font-family:inherit}p{margin-top:0}blockquote{background-color:var(--bg-secondary-color);padding:1.5rem 2rem;border-left:3px solid var(--color-lightGrey)}dl dt{font-weight:700}hr{background-color:var(--color-lightGrey);height:1px;margin:1rem 0}hr,table{border:none}table{width:100%;border-collapse:collapse;border-spacing:0;text-align:left}table.striped tr:nth-of-type(2n){background-color:var(--bg-secondary-color)}td,th{vertical-align:middle;padding:1.2rem .4rem}thead{border-bottom:2px solid var(--color-lightGrey)}tfoot{border-top:2px solid var(--color-lightGrey)}code,kbd,pre,samp,tt{font-family:var(--font-family-mono)}code,kbd{font-size:90%;white-space:pre-wrap;border-radius:4px;padding:.2em .4em;color:var(--color-error)}code,kbd,pre{background-color:var(--bg-secondary-color)}pre{font-size:1em;padding:1rem;overflow-x:auto}pre code{background:none;padding:0}abbr[title]{border-bottom:none;text-decoration:underline;-webkit-text-decoration:underline dotted;text-decoration:underline dotted}img{max-width:100%}fieldset{border:1px solid var(--color-lightGrey)}iframe{border:0}.container{max-width:var(--grid-maxWidth);margin:0 auto;width:96%;padding:0 calc(var(--grid-gutter)/2)}.row{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-direction:normal;-ms-flex-flow:row wrap;flex-flow:row wrap;-webkit-box-pack:start;-ms-flex-pack:start;justify-content:flex-start;margin-left:calc(var(--grid-gutter)/-2);margin-right:calc(var(--grid-gutter)/-2)}.row,.row.reverse{-webkit-box-orient:horizontal}.row.reverse{-webkit-box-direction:reverse;-ms-flex-direction:row-reverse;flex-direction:row-reverse}.col{-webkit-box-flex:1;-ms-flex:1;flex:1}.col,[class*=" col-"],[class^=col-]{margin:0 calc(var(--grid-gutter)/2) calc(var(--grid-gutter)/2)}.col-1{-ms-flex:0 0 calc(8.33333% - var(--grid-gutter));flex:0 0 calc(8.33333% - var(--grid-gutter));max-width:calc(8.33333% - var(--grid-gutter))}.col-1,.col-2{-webkit-box-flex:0}.col-2{-ms-flex:0 0 calc(16.66667% - var(--grid-gutter));flex:0 0 calc(16.66667% - var(--grid-gutter));max-width:calc(16.66667% - var(--grid-gutter))}.col-3{-ms-flex:0 0 calc(25% - var(--grid-gutter));flex:0 0 calc(25% - var(--grid-gutter));max-width:calc(25% - var(--grid-gutter))}.col-3,.col-4{-webkit-box-flex:0}.col-4{-ms-flex:0 0 calc(33.33333% - var(--grid-gutter));flex:0 0 calc(33.33333% - var(--grid-gutter));max-width:calc(33.33333% - var(--grid-gutter))}.col-5{-ms-flex:0 0 calc(41.66667% - var(--grid-gutter));flex:0 0 calc(41.66667% - var(--grid-gutter));max-width:calc(41.66667% - var(--grid-gutter))}.col-5,.col-6{-webkit-box-flex:0}.col-6{-ms-flex:0 0 calc(50% - var(--grid-gutter));flex:0 0 calc(50% - var(--grid-gutter));max-width:calc(50% - var(--grid-gutter))}.col-7{-ms-flex:0 0 calc(58.33333% - var(--grid-gutter));flex:0 0 calc(58.33333% - var(--grid-gutter));max-width:calc(58.33333% - var(--grid-gutter))}.col-7,.col-8{-webkit-box-flex:0}.col-8{-ms-flex:0 0 calc(66.66667% - var(--grid-gutter));flex:0 0 calc(66.66667% - var(--grid-gutter));max-width:calc(66.66667% - var(--grid-gutter))}.col-9{-ms-flex:0 0 calc(75% - var(--grid-gutter));flex:0 0 calc(75% - var(--grid-gutter));max-width:calc(75% - var(--grid-gutter))}.col-9,.col-10{-webkit-box-flex:0}.col-10{-ms-flex:0 0 calc(83.33333% - var(--grid-gutter));flex:0 0 calc(83.33333% - var(--grid-gutter));max-width:calc(83.33333% - var(--grid-gutter))}.col-11{-ms-flex:0 0 calc(91.66667% - var(--grid-gutter));flex:0 0 calc(91.66667% - var(--grid-gutter));max-width:calc(91.66667% - var(--grid-gutter))}.col-11,.col-12{-webkit-box-flex:0}.col-12{-ms-flex:0 0 calc(100% - var(--grid-gutter));flex:0 0 calc(100% - var(--grid-gutter));max-width:calc(100% - var(--grid-gutter))}@media screen and (max-width:599px){.container{width:100%}.col,[class*=col-],[class^=col-]{-webkit-box-flex:0;-ms-flex:0 1 100%;flex:0 1 100%;max-width:100%}}@media screen and (min-width:900px){.col-1-md{-webkit-box-flex:0;-ms-flex:0 0 calc(8.33333% - var(--grid-gutter));flex:0 0 calc(8.33333% - var(--grid-gutter));max-width:calc(8.33333% - var(--grid-gutter))}.col-2-md{-webkit-box-flex:0;-ms-flex:0 0 calc(16.66667% - var(--grid-gutter));flex:0 0 calc(16.66667% - var(--grid-gutter));max-width:calc(16.66667% - var(--grid-gutter))}.col-3-md{-webkit-box-flex:0;-ms-flex:0 0 calc(25% - var(--grid-gutter));flex:0 0 calc(25% - var(--grid-gutter));max-width:calc(25% - var(--grid-gutter))}.col-4-md{-webkit-box-flex:0;-ms-flex:0 0 calc(33.33333% - var(--grid-gutter));flex:0 0 calc(33.33333% - var(--grid-gutter));max-width:calc(33.33333% - var(--grid-gutter))}.col-5-md{-webkit-box-flex:0;-ms-flex:0 0 calc(41.66667% - var(--grid-gutter));flex:0 0 calc(41.66667% - var(--grid-gutter));max-width:calc(41.66667% - var(--grid-gutter))}.col-6-md{-webkit-box-flex:0;-ms-flex:0 0 calc(50% - var(--grid-gutter));flex:0 0 calc(50% - var(--grid-gutter));max-width:calc(50% - var(--grid-gutter))}.col-7-md{-webkit-box-flex:0;-ms-flex:0 0 calc(58.33333% - var(--grid-gutter));flex:0 0 calc(58.33333% - var(--grid-gutter));max-width:calc(58.33333% - var(--grid-gutter))}.col-8-md{-webkit-box-flex:0;-ms-flex:0 0 calc(66.66667% - var(--grid-gutter));flex:0 0 calc(66.66667% - var(--grid-gutter));max-width:calc(66.66667% - var(--grid-gutter))}.col-9-md{-webkit-box-flex:0;-ms-flex:0 0 calc(75% - var(--grid-gutter));flex:0 0 calc(75% - var(--grid-gutter));max-width:calc(75% - var(--grid-gutter))}.col-10-md{-webkit-box-flex:0;-ms-flex:0 0 calc(83.33333% - var(--grid-gutter));flex:0 0 calc(83.33333% - var(--grid-gutter));max-width:calc(83.33333% - var(--grid-gutter))}.col-11-md{-webkit-box-flex:0;-ms-flex:0 0 calc(91.66667% - var(--grid-gutter));flex:0 0 calc(91.66667% - var(--grid-gutter));max-width:calc(91.66667% - var(--grid-gutter))}.col-12-md{-webkit-box-flex:0;-ms-flex:0 0 calc(100% - var(--grid-gutter));flex:0 0 calc(100% - var(--grid-gutter));max-width:calc(100% - var(--grid-gutter))}}@media screen and (min-width:1200px){.col-1-lg{-webkit-box-flex:0;-ms-flex:0 0 calc(8.33333% - var(--grid-gutter));flex:0 0 calc(8.33333% - var(--grid-gutter));max-width:calc(8.33333% - var(--grid-gutter))}.col-2-lg{-webkit-box-flex:0;-ms-flex:0 0 calc(16.66667% - var(--grid-gutter));flex:0 0 calc(16.66667% - var(--grid-gutter));max-width:calc(16.66667% - var(--grid-gutter))}.col-3-lg{-webkit-box-flex:0;-ms-flex:0 0 calc(25% - var(--grid-gutter));flex:0 0 calc(25% - var(--grid-gutter));max-width:calc(25% - var(--grid-gutter))}.col-4-lg{-webkit-box-flex:0;-ms-flex:0 0 calc(33.33333% - var(--grid-gutter));flex:0 0 calc(33.33333% - var(--grid-gutter));max-width:calc(33.33333% - var(--grid-gutter))}.col-5-lg{-webkit-box-flex:0;-ms-flex:0 0 calc(41.66667% - var(--grid-gutter));flex:0 0 calc(41.66667% - var(--grid-gutter));max-width:calc(41.66667% - var(--grid-gutter))}.col-6-lg{-webkit-box-flex:0;-ms-flex:0 0 calc(50% - var(--grid-gutter));flex:0 0 calc(50% - var(--grid-gutter));max-width:calc(50% - var(--grid-gutter))}.col-7-lg{-webkit-box-flex:0;-ms-flex:0 0 calc(58.33333% - var(--grid-gutter));flex:0 0 calc(58.33333% - var(--grid-gutter));max-width:calc(58.33333% - var(--grid-gutter))}.col-8-lg{-webkit-box-flex:0;-ms-flex:0 0 calc(66.66667% - var(--grid-gutter));flex:0 0 calc(66.66667% - var(--grid-gutter));max-width:calc(66.66667% - var(--grid-gutter))}.col-9-lg{-webkit-box-flex:0;-ms-flex:0 0 calc(75% - var(--grid-gutter));flex:0 0 calc(75% - var(--grid-gutter));max-width:calc(75% - var(--grid-gutter))}.col-10-lg{-webkit-box-flex:0;-ms-flex:0 0 calc(83.33333% - var(--grid-gutter));flex:0 0 calc(83.33333% - var(--grid-gutter));max-width:calc(83.33333% - var(--grid-gutter))}.col-11-lg{-webkit-box-flex:0;-ms-flex:0 0 calc(91.66667% - var(--grid-gutter));flex:0 0 calc(91.66667% - var(--grid-gutter));max-width:calc(91.66667% - var(--grid-gutter))}.col-12-lg{-webkit-box-flex:0;-ms-flex:0 0 calc(100% - var(--grid-gutter));flex:0 0 calc(100% - var(--grid-gutter));max-width:calc(100% - var(--grid-gutter))}}fieldset{padding:.5rem 2rem}legend{text-transform:uppercase;font-size:.8em;letter-spacing:.1rem}input:not([type=checkbox]):not([type=radio]):not([type=submit]):not([type=color]):not([type=button]):not([type=reset]),select,textarea,textarea[type=text]{font-family:inherit;padding:.8rem 1rem;border-radius:4px;border:1px solid var(--color-lightGrey);font-size:1em;-webkit-transition:all .2s ease;transition:all .2s ease;display:block;width:100%}input:not([type=checkbox]):not([type=radio]):not([type=submit]):not([type=color]):not([type=button]):not([type=reset]):not(:disabled):hover,select:hover,textarea:hover,textarea[type=text]:hover{border-color:var(--color-grey)}input:not([type=checkbox]):not([type=radio]):not([type=submit]):not([type=color]):not([type=button]):not([type=reset]):focus,select:focus,textarea:focus,textarea[type=text]:focus{outline:none;border-color:var(--color-primary);-webkit-box-shadow:0 0 1px var(--color-primary);box-shadow:0 0 1px var(--color-primary)}input.error:not([type=checkbox]):not([type=radio]):not([type=submit]):not([type=color]):not([type=button]):not([type=reset]),textarea.error{border-color:var(--color-error)}input.success:not([type=checkbox]):not([type=radio]):not([type=submit]):not([type=color]):not([type=button]):not([type=reset]),textarea.success{border-color:var(--color-success)}select{-webkit-appearance:none;background:#f3f3f6 no-repeat 100%;background-size:1ex;background-origin:content-box;background-image:url("data:image/svg+xml;utf8,")}[type=checkbox],[type=radio]{width:1.6rem;height:1.6rem}.button,[type=button],[type=reset],[type=submit],button{padding:1rem 2.5rem;color:var(--color-darkGrey);background:var(--color-lightGrey);border-radius:4px;border:1px solid transparent;font-size:var(--font-size);line-height:1;text-align:center;-webkit-transition:opacity .2s ease;transition:opacity .2s ease;text-decoration:none;-webkit-transform:scale(1);transform:scale(1);display:inline-block;cursor:pointer}.grouped{display:-webkit-box;display:-ms-flexbox;display:flex}.grouped>:not(:last-child){margin-right:16px}.grouped.gapless>*{margin:0 0 0 -1px!important;border-radius:0!important}.grouped.gapless>:first-child{margin:0!important;border-radius:4px 0 0 4px!important}.grouped.gapless>:last-child{border-radius:0 4px 4px 0!important}.button+.button{margin-left:1rem}.button:hover,[type=button]:hover,[type=reset]:hover,[type=submit]:hover,button:hover{opacity:.8}.button:active,[type=button]:active,[type=reset]:active,[type=submit]:active,button:active{-webkit-transform:scale(.98);transform:scale(.98)}button:disabled,button:disabled:hover,input:disabled,input:disabled:hover{opacity:.4;cursor:not-allowed}.button.dark,.button.error,.button.primary,.button.secondary,.button.success,[type=submit]{color:#fff;z-index:1;background-color:#000;background-color:var(--color-primary)}.button.secondary{background-color:var(--color-grey)}.button.dark{background-color:var(--color-darkGrey)}.button.error{background-color:var(--color-error)}.button.success{background-color:var(--color-success)}.button.outline{background-color:transparent;border-color:var(--color-lightGrey)}.button.outline.primary{border-color:var(--color-primary);color:var(--color-primary)}.button.outline.secondary{border-color:var(--color-grey);color:var(--color-grey)}.button.outline.dark{border-color:var(--color-darkGrey);color:var(--color-darkGrey)}.button.clear{background-color:transparent;border-color:transparent;color:var(--color-primary)}.button.icon{display:-webkit-inline-box;display:-ms-inline-flexbox;display:inline-flex;-webkit-box-align:center;-ms-flex-align:center;align-items:center}.button.icon>img{margin-left:2px}.button.icon-only{padding:1rem}::-webkit-input-placeholder{color:#bdbfc4}::-moz-placeholder{color:#bdbfc4}:-ms-input-placeholder{color:#bdbfc4}::-ms-input-placeholder{color:#bdbfc4}::placeholder{color:#bdbfc4}.nav{display:-webkit-box;display:-ms-flexbox;display:flex;min-height:5rem;-webkit-box-align:stretch;-ms-flex-align:stretch;align-items:stretch}.nav img{max-height:3rem}.nav-center,.nav-left,.nav-right,.nav>.container{display:-webkit-box;display:-ms-flexbox;display:flex}.nav-center,.nav-left,.nav-right{-webkit-box-flex:1;-ms-flex:1;flex:1}.nav-left{-webkit-box-pack:start;-ms-flex-pack:start;justify-content:flex-start}.nav-right{-webkit-box-pack:end;-ms-flex-pack:end;justify-content:flex-end}.nav-center{-webkit-box-pack:center;-ms-flex-pack:center;justify-content:center}@media screen and (max-width:480px){.nav,.nav>.container{-webkit-box-orient:vertical;-webkit-box-direction:normal;-ms-flex-direction:column;flex-direction:column}.nav-center,.nav-left,.nav-right{-ms-flex-wrap:wrap;flex-wrap:wrap;-webkit-box-pack:center;-ms-flex-pack:center;justify-content:center}}.nav .brand,.nav a{text-decoration:none;display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-align:center;-ms-flex-align:center;align-items:center;padding:1rem 2rem;color:var(--color-darkGrey)}.nav .active:not(.button),.nav [aria-current=page]:not(.button){color:#000;color:var(--color-primary)}.nav .brand{font-size:1.75em;padding-top:0;padding-bottom:0}.nav .brand img{padding-right:1rem}.nav .button{margin:auto 1rem}.card{padding:1rem 2rem;border-radius:4px;background:var(--bg-color);-webkit-box-shadow:0 1px 3px var(--color-grey);box-shadow:0 1px 3px var(--color-grey)}.card p:last-child{margin:0}.card header>*{margin-top:0;margin-bottom:1rem}.tabs{display:-webkit-box;display:-ms-flexbox;display:flex}.tabs a{text-decoration:none}.tabs>.dropdown>summary,.tabs>a{padding:1rem 2rem;-webkit-box-flex:0;-ms-flex:0 1 auto;flex:0 1 auto;color:var(--color-darkGrey);border-bottom:2px solid var(--color-lightGrey);text-align:center}.tabs>a.active,.tabs>a:hover,.tabs>a[aria-current=page]{opacity:1;border-bottom:2px solid var(--color-darkGrey)}.tabs>a.active,.tabs>a[aria-current=page]{border-color:var(--color-primary)}.tabs.is-full a{-webkit-box-flex:1;-ms-flex:1 1 auto;flex:1 1 auto}.tag{display:inline-block;border:1px solid var(--color-lightGrey);text-transform:uppercase;color:var(--color-grey);padding:.5rem;line-height:1;letter-spacing:.5px}.tag.is-small{padding:.4rem;font-size:.75em}.tag.is-large{padding:.7rem;font-size:1.125em}.tag+.tag{margin-left:1rem}details.dropdown{position:relative;display:inline-block}details.dropdown>:last-child{position:absolute;left:0;white-space:nowrap}.bg-primary{background-color:var(--color-primary)!important}.bg-light{background-color:var(--color-lightGrey)!important}.bg-dark{background-color:var(--color-darkGrey)!important}.bg-grey{background-color:var(--color-grey)!important}.bg-error{background-color:var(--color-error)!important}.bg-success{background-color:var(--color-success)!important}.bd-primary{border:1px solid var(--color-primary)!important}.bd-light{border:1px solid var(--color-lightGrey)!important}.bd-dark{border:1px solid var(--color-darkGrey)!important}.bd-grey{border:1px solid var(--color-grey)!important}.bd-error{border:1px solid var(--color-error)!important}.bd-success{border:1px solid var(--color-success)!important}.text-primary{color:var(--color-primary)!important}.text-light{color:var(--color-lightGrey)!important}.text-dark{color:var(--color-darkGrey)!important}.text-grey{color:var(--color-grey)!important}.text-error{color:var(--color-error)!important}.text-success{color:var(--color-success)!important}.text-white{color:#fff!important}.pull-right{float:right!important}.pull-left{float:left!important}.text-center{text-align:center}.text-left{text-align:left}.text-right{text-align:right}.text-justify{text-align:justify}.text-uppercase{text-transform:uppercase}.text-lowercase{text-transform:lowercase}.text-capitalize{text-transform:capitalize}.is-full-screen{width:100%;min-height:100vh}.is-full-width{width:100%!important}.is-vertical-align{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-align:center;-ms-flex-align:center;align-items:center}.is-center,.is-horizontal-align{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-pack:center;-ms-flex-pack:center;justify-content:center}.is-center{-webkit-box-align:center;-ms-flex-align:center;align-items:center}.is-right{-webkit-box-pack:end;-ms-flex-pack:end;justify-content:flex-end}.is-left,.is-right{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-align:center;-ms-flex-align:center;align-items:center}.is-left{-webkit-box-pack:start;-ms-flex-pack:start;justify-content:flex-start}.is-fixed{position:fixed;width:100%}.is-paddingless{padding:0!important}.is-marginless{margin:0!important}.is-pointer{cursor:pointer!important}.is-rounded{border-radius:100%}.clearfix{content:"";display:table;clear:both}.is-hidden{display:none!important}@media screen and (max-width:599px){.hide-xs{display:none!important}}@media screen and (min-width:600px) and (max-width:899px){.hide-sm{display:none!important}}@media screen and (min-width:900px) and (max-width:1199px){.hide-md{display:none!important}}@media screen and (min-width:1200px){.hide-lg{display:none!important}}@media print{.hide-pr{display:none!important}} \ No newline at end of file diff --git a/examples/forms/templates/index.html.tera b/examples/forms/templates/index.html.tera new file mode 100644 index 0000000000..4e43a8a886 --- /dev/null +++ b/examples/forms/templates/index.html.tera @@ -0,0 +1,140 @@ +{% import "macros" as m %} + + + + + + + Rocket Form Example + + + + +
+

Form Example

+ + {% if errors | length > 1 %} + + {{ errors | length }} field(s) have errors + + {% endif %} + + +
+ About You +
+
+ {{ m::input(label="Name", type="text", name="account.name") }} + +
+
+ {{ m::input(label="Email Address", type="text", name="account.email") }} + +
+
+ +
+
+ {{ m::input(label="Password", type="password", name="account.password.first") }} + +
+ +
+ + {{ + m::input(label="Confirm Password", + type="password", + name="account.password.second") + }} + + +
+
+
+ +
+ Metadata + +
+
+ {{ m::input(label="Title", type="text", name="submission.title") }} + +
+
+ +
+
+ {{ m::input(label="Publish Date", type="date", name="submission.date") }} + +
+ +
+ {{ + m::select( + label="Rights Assignment", + name="submission.rights", + options=["Public", "Reserved", "Exclusive"] + ) + }} +
+
+ +
+
+ +
+ {{ m::checkbox(name="submission.category", value="Biology") }} +
+ {{ m::checkbox(name="submission.category", value="Chemistry") }} +
+ {{ m::checkbox(name="submission.category", value="Physics") }} +
+ {{ m::checkbox(name="submission.category", value="CS") }} +
+
+ +
+ +
+ Contents + + {{ + m::textarea( + label="Abstract", + name="submission.abstract", + placeholder="Your abstract, max 250 characters...", + max=250 + ) + }} + + {{ + m::input( + label="File to Upload (PDF, max 1MiB)", + type="file", + name="submission.file" + ) + }} + + + +
+
+ {{ m::checkbox(name="submission.ready", value="Submission is + ready for review.") }} +
+
+ +
+ +
+ + +
+ + diff --git a/examples/forms/templates/macros.html.tera b/examples/forms/templates/macros.html.tera new file mode 100644 index 0000000000..d7f8a05f79 --- /dev/null +++ b/examples/forms/templates/macros.html.tera @@ -0,0 +1,63 @@ +{% macro value_for(name) %} + {%- if name in values -%} + {{- values | get(key=name) | first -}} + {%- endif -%} +{% endmacro %} + +{% macro errors_for(name) %} + {%- if name in errors -%} + {% set field_errors = errors | get(key=name) %} + {% for error in field_errors %} +

{{ error.msg }}

+ {% endfor %} + {%- endif -%} +{% endmacro %} + +{% macro input(type, label, name, value="") %} + + + + {{ self::errors_for(name=name) }} +{% endmacro input %} + +{% macro checkbox(name, value) %} + +{% endmacro input %} + +{% macro textarea(label, name, placeholder="", max=250) %} + + + + {{ self::errors_for(name=name) }} +{% endmacro input %} + +{% macro select(label, name, options) %} + + +{% endmacro input %} diff --git a/examples/forms/templates/success.html.tera b/examples/forms/templates/success.html.tera new file mode 100644 index 0000000000..cd3addb88c --- /dev/null +++ b/examples/forms/templates/success.html.tera @@ -0,0 +1,30 @@ + + + + + + Rocket Form Example + + + + +
+

Success!

+ +

Submission Data

+ +
    + {% for key, value in values %} +
  • {{ key }} - {{ value }}
  • + {% endfor %} +
+ +
< Submit Another + + diff --git a/examples/hello_person/src/main.rs b/examples/hello_person/src/main.rs index cdcc07b781..72307c0881 100644 --- a/examples/hello_person/src/main.rs +++ b/examples/hello_person/src/main.rs @@ -8,7 +8,7 @@ fn hello(name: String, age: u8) -> String { } #[get("/hello/")] -fn hi(name: String) -> String { +fn hi(name: &str) -> &str { name } diff --git a/examples/hello_world/src/main.rs b/examples/hello_world/src/main.rs index 0d210d8497..203605d077 100644 --- a/examples/hello_world/src/main.rs +++ b/examples/hello_world/src/main.rs @@ -2,12 +2,28 @@ #[cfg(test)] mod tests; -#[get("/")] -fn hello() -> &'static str { +#[get("/?")] +fn hello(lang: Option<&str>) -> &'static str { + match lang { + Some("en") | None => world(), + Some("русский") => mir(), + _ => "Hello, voyager!" + } +} + +#[get("/world")] +fn world() -> &'static str { "Hello, world!" } +#[get("/мир")] +fn mir() -> &'static str { + "Привет, мир!" +} + #[launch] fn rocket() -> rocket::Rocket { - rocket::ignite().mount("/", routes![hello]) + rocket::ignite() + .mount("/", routes![hello]) + .mount("/hello", routes![world, mir]) } diff --git a/examples/json/src/main.rs b/examples/json/src/main.rs index 286c910b14..f58c833176 100644 --- a/examples/json/src/main.rs +++ b/examples/json/src/main.rs @@ -1,48 +1,47 @@ #[macro_use] extern crate rocket; -#[macro_use] extern crate rocket_contrib; #[cfg(test)] mod tests; -use std::sync::Mutex; use std::collections::HashMap; +use std::borrow::Cow; use rocket::State; -use rocket_contrib::json::{Json, JsonValue}; +use rocket::tokio::sync::Mutex; +use rocket_contrib::json::{Json, JsonValue, json}; use serde::{Serialize, Deserialize}; // The type to represent the ID of a message. -type ID = usize; +type Id = usize; // We're going to store all of the messages here. No need for a DB. -type MessageMap = Mutex>; +type MessageMap<'r> = State<'r, Mutex>>; #[derive(Serialize, Deserialize)] -struct Message { - id: Option, - contents: String +struct Message<'r> { + id: Option, + contents: Cow<'r, str> } -// TODO: This example can be improved by using `route` with multiple HTTP verbs. #[post("/", format = "json", data = "")] -fn new(id: ID, message: Json, map: State<'_, MessageMap>) -> JsonValue { - let mut hashmap = map.lock().expect("map lock."); +async fn new(id: Id, message: Json>, map: MessageMap<'_>) -> JsonValue { + let mut hashmap = map.lock().await; if hashmap.contains_key(&id) { json!({ "status": "error", "reason": "ID exists. Try put." }) } else { - hashmap.insert(id, message.0.contents); + hashmap.insert(id, message.contents.to_string()); json!({ "status": "ok" }) } } #[put("/", format = "json", data = "")] -fn update(id: ID, message: Json, map: State<'_, MessageMap>) -> Option { - let mut hashmap = map.lock().unwrap(); +async fn update(id: Id, message: Json>, map: MessageMap<'_>) -> Option { + let mut hashmap = map.lock().await; if hashmap.contains_key(&id) { - hashmap.insert(id, message.0.contents); + hashmap.insert(id, message.contents.to_string()); Some(json!({ "status": "ok" })) } else { None @@ -50,14 +49,18 @@ fn update(id: ID, message: Json, map: State<'_, MessageMap>) -> Option< } #[get("/", format = "json")] -fn get(id: ID, map: State<'_, MessageMap>) -> Option> { - let hashmap = map.lock().unwrap(); - hashmap.get(&id).map(|contents| { - Json(Message { - id: Some(id), - contents: contents.clone() - }) - }) +async fn get<'r>(id: Id, map: MessageMap<'r>) -> Option>> { + let hashmap = map.lock().await; + let contents = hashmap.get(&id)?.clone(); + Some(Json(Message { + id: Some(id), + contents: contents.into() + })) +} + +#[get("/echo", data = "")] +fn echo<'r>(msg: Json>) -> Cow<'r, str> { + msg.into_inner().contents } #[catch(404)] @@ -69,9 +72,9 @@ fn not_found() -> JsonValue { } #[launch] -fn rocket() -> rocket::Rocket { +fn rocket() -> _ { rocket::ignite() - .mount("/message", routes![new, update, get]) + .mount("/message", routes![new, update, get, echo]) .register(catchers![not_found]) - .manage(Mutex::new(HashMap::::new())) + .manage(Mutex::new(HashMap::::new())) } diff --git a/examples/manual_routes/src/main.rs b/examples/manual_routes/src/main.rs index aa16a3bb89..2a67bf6aab 100644 --- a/examples/manual_routes/src/main.rs +++ b/examples/manual_routes/src/main.rs @@ -5,7 +5,7 @@ use std::env; use rocket::{Request, Route}; use rocket::data::{Data, ToByteUnit}; -use rocket::http::{Status, RawStr, Method::*}; +use rocket::http::{Status, Method::*}; use rocket::response::{Responder, status::Custom}; use rocket::handler::{Handler, Outcome, HandlerFuture}; use rocket::catcher::{Catcher, ErrorHandlerFuture}; @@ -21,21 +21,20 @@ fn hi<'r>(req: &'r Request, _: Data) -> HandlerFuture<'r> { } fn name<'a>(req: &'a Request, _: Data) -> HandlerFuture<'a> { - let param = req.get_param::<&'a RawStr>(0) + let param = req.param::<&'a str>(0) .and_then(|res| res.ok()) .unwrap_or("unnamed".into()); - Outcome::from(req, param.as_str()).pin() + Outcome::from(req, param).pin() } fn echo_url<'r>(req: &'r Request, _: Data) -> HandlerFuture<'r> { - let param_outcome = req.get_param::<&RawStr>(1) + let param_outcome = req.param::<&str>(1) .and_then(|res| res.ok()) .into_outcome(Status::BadRequest); Box::pin(async move { - let param = try_outcome!(param_outcome); - Outcome::try_from(req, RawStr::from_str(param).url_decode()) + Outcome::from(req, try_outcome!(param_outcome)) }) } @@ -85,7 +84,7 @@ impl CustomHandler { impl Handler for CustomHandler { async fn handle<'r, 's: 'r>(&'s self, req: &'r Request<'_>, data: Data) -> Outcome<'r> { let self_data = self.data; - let id = req.get_param::<&RawStr>(0) + let id = req.param::<&str>(0) .and_then(|res| res.ok()) .or_forward(data); diff --git a/examples/optional_redirect/src/main.rs b/examples/optional_redirect/src/main.rs index d975a699a6..7e33f4985b 100644 --- a/examples/optional_redirect/src/main.rs +++ b/examples/optional_redirect/src/main.rs @@ -4,7 +4,6 @@ mod tests; use rocket::response::Redirect; -use rocket::http::RawStr; #[get("/")] fn root() -> Redirect { @@ -12,8 +11,8 @@ fn root() -> Redirect { } #[get("/users/")] -fn user(name: &RawStr) -> Result<&'static str, Redirect> { - match name.as_str() { +fn user(name: &str) -> Result<&'static str, Redirect> { + match name { "Sergio" => Ok("Hello, Sergio!"), _ => Err(Redirect::to("/users/login")), } diff --git a/examples/pastebin/src/main.rs b/examples/pastebin/src/main.rs index f3b13c1a80..5835139cc8 100644 --- a/examples/pastebin/src/main.rs +++ b/examples/pastebin/src/main.rs @@ -5,29 +5,30 @@ mod paste_id; use std::io; +use rocket::State; use rocket::data::{Data, ToByteUnit}; -use rocket::response::{content::Plain, Debug}; +use rocket::http::uri::Absolute; +use rocket::response::content::Plain; use rocket::tokio::fs::File; -use crate::paste_id::PasteID; +use crate::paste_id::PasteId; const HOST: &str = "http://localhost:8000"; const ID_LENGTH: usize = 3; #[post("/", data = "")] -async fn upload(paste: Data) -> Result> { - let id = PasteID::new(ID_LENGTH); - let filename = format!("upload/{id}", id = id); - let url = format!("{host}/{id}\n", host = HOST, id = id); +async fn upload(paste: Data, host: State<'_, Absolute<'_>>) -> io::Result { + let id = PasteId::new(ID_LENGTH); + paste.open(128.kibibytes()).into_file(id.file_path()).await?; - paste.open(128.kibibytes()).stream_to_file(filename).await?; - Ok(url) + // TODO: Ok(uri!(HOST, retrieve: id)) + let host = host.inner().clone(); + Ok(host.with_origin(uri!(retrieve: id)).to_string()) } #[get("/")] -async fn retrieve(id: PasteID<'_>) -> Option> { - let filename = format!("upload/{id}", id = id); - File::open(&filename).await.map(Plain).ok() +async fn retrieve(id: PasteId<'_>) -> Option> { + File::open(id.file_path()).await.map(Plain).ok() } #[get("/")] @@ -50,5 +51,7 @@ fn index() -> &'static str { #[launch] fn rocket() -> rocket::Rocket { - rocket::ignite().mount("/", routes![index, upload, retrieve]) + rocket::ignite() + .manage(Absolute::parse(HOST).expect("valid host")) + .mount("/", routes![index, upload, retrieve]) } diff --git a/examples/pastebin/src/paste_id.rs b/examples/pastebin/src/paste_id.rs index c3ec20327b..25c9fc020b 100644 --- a/examples/pastebin/src/paste_id.rs +++ b/examples/pastebin/src/paste_id.rs @@ -1,53 +1,46 @@ -use std::fmt; use std::borrow::Cow; +use std::path::{Path, PathBuf}; use rocket::request::FromParam; -use rocket::http::RawStr; use rand::{self, Rng}; /// Table to retrieve base62 values from. const BASE62: &[u8] = b"0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"; /// A _probably_ unique paste ID. -pub struct PasteID<'a>(Cow<'a, str>); +#[derive(UriDisplayPath)] +pub struct PasteId<'a>(Cow<'a, str>); -impl<'a> PasteID<'a> { +impl PasteId<'_> { /// Generate a _probably_ unique ID with `size` characters. For readability, /// the characters used are from the sets [0-9], [A-Z], [a-z]. The /// probability of a collision depends on the value of `size` and the number /// of IDs generated thus far. - pub fn new(size: usize) -> PasteID<'static> { + pub fn new(size: usize) -> PasteId<'static> { let mut id = String::with_capacity(size); let mut rng = rand::thread_rng(); for _ in 0..size { id.push(BASE62[rng.gen::() % 62] as char); } - PasteID(Cow::Owned(id)) + PasteId(Cow::Owned(id)) } -} -impl<'a> fmt::Display for PasteID<'a> { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "{}", self.0) + pub fn file_path(&self) -> PathBuf { + let root = concat!(env!("CARGO_MANIFEST_DIR"), "/", "upload"); + Path::new(root).join(self.0.as_ref()) } } -/// Returns `true` if `id` is a valid paste ID and `false` otherwise. -fn valid_id(id: &str) -> bool { - id.chars().all(|c| c.is_ascii_alphanumeric()) -} - -/// Returns an instance of `PasteID` if the path segment is a valid ID. +/// Returns an instance of `PasteId` if the path segment is a valid ID. /// Otherwise returns the invalid ID as the `Err` value. -impl<'a> FromParam<'a> for PasteID<'a> { - type Error = &'a RawStr; +impl<'a> FromParam<'a> for PasteId<'a> { + type Error = &'a str; - fn from_param(param: &'a RawStr) -> Result, &'a RawStr> { - match valid_id(param) { - true => Ok(PasteID(Cow::Borrowed(param))), + fn from_param(param: &'a str) -> Result { + match param.chars().all(|c| c.is_ascii_alphanumeric()) { + true => Ok(PasteId(param.into())), false => Err(param) } } } - diff --git a/examples/query_params/src/main.rs b/examples/query_params/src/main.rs index 154e414711..ec1a282c50 100644 --- a/examples/query_params/src/main.rs +++ b/examples/query_params/src/main.rs @@ -2,18 +2,18 @@ #[cfg(test)] mod tests; -use rocket::request::{Form, LenientForm}; +use rocket::form::Strict; #[derive(FromForm)] struct Person { /// Use the `form` attribute to expect an invalid Rust identifier in the HTTP form. - #[form(field = "first-name")] + #[field(name = "first-name")] name: String, age: Option } #[get("/hello?")] -fn hello(person: Option>) -> String { +fn hello(person: Option>) -> String { if let Some(person) = person { if let Some(age) = person.age { format!("Hello, {} year old named {}!", age, person.name) @@ -26,7 +26,7 @@ fn hello(person: Option>) -> String { } #[get("/hello?age=20&")] -fn hello_20(person: LenientForm) -> String { +fn hello_20(person: Person) -> String { format!("20 years old? Hi, {}!", person.name) } diff --git a/examples/ranking/src/main.rs b/examples/ranking/src/main.rs index 25d26a5337..82f1aa5331 100644 --- a/examples/ranking/src/main.rs +++ b/examples/ranking/src/main.rs @@ -1,7 +1,5 @@ #[macro_use] extern crate rocket; -use rocket::http::RawStr; - #[cfg(test)] mod tests; #[get("/hello//")] @@ -10,7 +8,7 @@ fn hello(name: String, age: i8) -> String { } #[get("/hello//", rank = 2)] -fn hi(name: String, age: &RawStr) -> String { +fn hi(name: String, age: &str) -> String { format!("Hi {}! Your age ({}) is kind of funky.", name, age) } diff --git a/examples/raw_upload/src/main.rs b/examples/raw_upload/src/main.rs index 22f971a659..49ffd6f477 100644 --- a/examples/raw_upload/src/main.rs +++ b/examples/raw_upload/src/main.rs @@ -3,18 +3,18 @@ #[cfg(test)] mod tests; use std::{io, env}; -use rocket::data::{Data, ToByteUnit}; -use rocket::response::Debug; +use rocket::data::{Capped, TempFile}; -#[post("/upload", format = "plain", data = "")] -async fn upload(data: Data) -> Result> { - let path = env::temp_dir().join("upload.txt"); - Ok(data.open(128.kibibytes()).stream_to_file(path).await?.to_string()) +#[post("/upload", data = "")] +async fn upload(mut file: Capped>) -> io::Result { + file.persist_to(env::temp_dir().join("upload.txt")).await?; + Ok(format!("{} bytes at {}", file.n.written, file.path().unwrap().display())) } #[get("/")] fn index() -> &'static str { - "Upload your text files by POSTing them to /upload." + "Upload your text files by POSTing them to /upload.\n\ + Try `curl --data-binary @file.txt http://127.0.0.1:8000/upload`." } #[launch] diff --git a/examples/raw_upload/src/tests.rs b/examples/raw_upload/src/tests.rs index 13ec8e3e40..b375760972 100644 --- a/examples/raw_upload/src/tests.rs +++ b/examples/raw_upload/src/tests.rs @@ -28,7 +28,7 @@ fn test_raw_upload() { .dispatch(); assert_eq!(res.status(), Status::Ok); - assert_eq!(res.into_string(), Some(UPLOAD_CONTENTS.len().to_string())); + assert!(res.into_string().unwrap().contains(&UPLOAD_CONTENTS.len().to_string())); // Ensure we find the body in the /tmp/upload.txt file. let mut file_contents = String::new(); diff --git a/examples/request_local_state/src/main.rs b/examples/request_local_state/src/main.rs index 50a461eba8..996534a5f7 100644 --- a/examples/request_local_state/src/main.rs +++ b/examples/request_local_state/src/main.rs @@ -2,8 +2,9 @@ use std::sync::atomic::{AtomicUsize, Ordering}; -use rocket::outcome::Outcome::*; -use rocket::request::{self, FromRequest, Request, State}; +use rocket::State; +use rocket::outcome::Outcome; +use rocket::request::{self, FromRequest, Request}; #[cfg(test)] mod tests; @@ -27,7 +28,7 @@ impl<'a, 'r> FromRequest<'a, 'r> for Guard1 { atomics.uncached.fetch_add(1, Ordering::Relaxed); req.local_cache(|| atomics.cached.fetch_add(1, Ordering::Relaxed)); - Success(Guard1) + Outcome::Success(Guard1) } } @@ -37,7 +38,7 @@ impl<'a, 'r> FromRequest<'a, 'r> for Guard2 { async fn from_request(req: &'a Request<'r>) -> request::Outcome { try_outcome!(req.guard::().await); - Success(Guard2) + Outcome::Success(Guard2) } } @@ -52,7 +53,7 @@ impl<'a, 'r> FromRequest<'a, 'r> for Guard3 { atomics.cached.fetch_add(1, Ordering::Relaxed) }).await; - Success(Guard3) + Outcome::Success(Guard3) } } @@ -62,7 +63,7 @@ impl<'a, 'r> FromRequest<'a, 'r> for Guard4 { async fn from_request(req: &'a Request<'r>) -> request::Outcome { try_outcome!(Guard3::from_request(req).await); - Success(Guard4) + Outcome::Success(Guard4) } } diff --git a/examples/session/src/main.rs b/examples/session/src/main.rs index cc18988d64..5b7d4bdd17 100644 --- a/examples/session/src/main.rs +++ b/examples/session/src/main.rs @@ -5,9 +5,11 @@ use std::collections::HashMap; use rocket::outcome::IntoOutcome; -use rocket::request::{self, Form, FlashMessage, FromRequest, Request}; +use rocket::request::{self, FlashMessage, FromRequest, Request}; use rocket::response::{Redirect, Flash}; use rocket::http::{Cookie, CookieJar}; +use rocket::form::Form; + use rocket_contrib::templates::Template; #[derive(FromForm)] @@ -54,7 +56,7 @@ fn login_user(_user: User) -> Redirect { } #[get("/login", rank = 2)] -fn login_page(flash: Option>) -> Template { +fn login_page(flash: Option>) -> Template { let mut context = HashMap::new(); if let Some(ref msg) = flash { context.insert("flash", msg.msg()); diff --git a/examples/todo/src/main.rs b/examples/todo/src/main.rs index b5be64874c..31417195f3 100644 --- a/examples/todo/src/main.rs +++ b/examples/todo/src/main.rs @@ -11,7 +11,8 @@ use std::fmt::Display; use rocket::Rocket; use rocket::fairing::AdHoc; -use rocket::request::{Form, FlashMessage}; +use rocket::request::FlashMessage; +use rocket::form::Form; use rocket::response::{Flash, Redirect}; use rocket_contrib::{templates::Template, serve::{StaticFiles, crate_relative}}; use diesel::SqliteConnection; @@ -90,7 +91,7 @@ async fn delete(id: i32, conn: DbConn) -> Result, Template> { } #[get("/")] -async fn index(msg: Option>, conn: DbConn) -> Template { +async fn index(msg: Option>, conn: DbConn) -> Template { let msg = msg.map(|m| (m.name().to_string(), m.msg().to_string())); Template::render("index", Context::raw(&conn, msg).await) } diff --git a/site/guide/10-pastebin.md b/site/guide/10-pastebin.md index e278bd7683..05109c9351 100644 --- a/site/guide/10-pastebin.md +++ b/site/guide/10-pastebin.md @@ -160,12 +160,6 @@ impl<'a> PasteId<'a> { PasteId(Cow::Owned(id)) } } - -impl<'a> fmt::Display for PasteId<'a> { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - write!(f, "{}", self.0) - } -} ``` Then, in `src/main.rs`, add the following after `extern crate rocket`: @@ -207,12 +201,10 @@ an `upload` directory next to the `src` directory: mkdir upload ``` -For the `upload` route, we'll need to `use` a few items: +For the `upload` route, we'll need to import `Data`: ```rust use rocket::Data; -use rocket::http::RawStr; -use rocket::response::Debug; ``` The [Data](@api/rocket/data/struct.Data.html) structure is key @@ -227,13 +219,12 @@ and handler signature look like this: ```rust # #[macro_use] extern crate rocket; -# fn main() {} use rocket::Data; use rocket::response::Debug; #[post("/", data = "")] -fn upload(paste: Data) -> Result> { +fn upload(paste: Data) -> std::io::Result { # unimplemented!() /* .. */ } @@ -270,7 +261,7 @@ async fn upload(paste: Data) -> Result> { let url = format!("{host}/{id}\n", host = "http://localhost:8000", id = id); // Write the paste out, limited to 128KiB, and return the URL. - paste.open(128.kibibytes()).stream_to_file(filename).await?; + paste.open(128.kibibytes()).into_file(filename).await?; Ok(url) } ``` @@ -330,11 +321,10 @@ paste doesn't exist. ```rust # #[macro_use] extern crate rocket; -use rocket::http::RawStr; use rocket::tokio::fs::File; #[get("/")] -async fn retrieve(id: &RawStr) -> Option { +async fn retrieve(id: &str) -> Option { let filename = format!("upload/{id}", id = id); File::open(&filename).await.ok() } @@ -356,7 +346,7 @@ fn rocket() -> rocket::Rocket { ``` Unfortunately, there's a problem with this code. Can you spot the issue? The -[`RawStr`](@api/rocket/http/struct.RawStr.html) type should tip you off! +`&str` type should tip you off! The issue is that the _user_ controls the value of `id`, and as a result, can coerce the service into opening files inside `upload/` that aren't meant to be @@ -378,29 +368,19 @@ using it. We do this by implementing `FromParam` for `PasteId` in ```rust use std::borrow::Cow; -use rocket::http::RawStr; use rocket::request::FromParam; /// A _probably_ unique paste ID. pub struct PasteId<'a>(Cow<'a, str>); -/// Returns `true` if `id` is a valid paste ID and `false` otherwise. -fn valid_id(id: &str) -> bool { - id.chars().all(|c| { - (c >= 'a' && c <= 'z') - || (c >= 'A' && c <= 'Z') - || (c >= '0' && c <= '9') - }) -} - /// Returns an instance of `PasteId` if the path segment is a valid ID. /// Otherwise returns the invalid ID as the `Err` value. impl<'a> FromParam<'a> for PasteId<'a> { - type Error = &'a RawStr; + type Error = &'a str; - fn from_param(param: &'a RawStr) -> Result, &'a RawStr> { - match valid_id(param) { - true => Ok(PasteId(Cow::Borrowed(param))), + fn from_param(param: &'a str) -> Result { + match param.chars().all(|c| c.is_ascii_alphanumeric()) { + true => Ok(PasteId(param.into())), false => Err(param) } } @@ -417,7 +397,7 @@ the `retrieve` route, preventing attacks on the `retrieve` route: # use std::borrow::Cow; # use rocket::tokio::fs::File; -# type PasteId<'a> = Cow<'a, str>; +# type PasteId<'a> = &'a str; #[get("/")] async fn retrieve(id: PasteId<'_>) -> Option { @@ -426,7 +406,7 @@ async fn retrieve(id: PasteId<'_>) -> Option { } ``` -Note that our `valid_id` function is simplistic and could be improved by, for +Note that our `from_param` function is simplistic and could be improved by, for example, checking that the length of the `id` is within some known bound or potentially blacklisting sensitive files as needed. diff --git a/site/guide/3-overview.md b/site/guide/3-overview.md index b1ac1fcb1a..59d792063f 100644 --- a/site/guide/3-overview.md +++ b/site/guide/3-overview.md @@ -265,6 +265,24 @@ You can find async-ready libraries on [crates.io](https://crates.io) with the use `#[launch]` or `#[rocket::main]`, but you can still `launch()` a Rocket instance on a custom-built runtime by not using _either_ attribute. +### Async Routes + +Rocket makes it easy to use `async/await` in routes. + +```rust +# #[macro_use] extern crate rocket; +use rocket::tokio::time::{sleep, Duration}; +#[get("/delay/")] +async fn delay(seconds: u64) -> String { + sleep(Duration::from_secs(seconds)).await; + format!("Waited for {} seconds", seconds) +} +``` + +First, notice that the route function is an `async fn`. This enables the use of +`await` inside the handler. `sleep` is an asynchronous function, so we must +`await` it. + ### Multitasking Rust's `Future`s are a form of *cooperative multitasking*. In general, `Future`s diff --git a/site/guide/4-requests.md b/site/guide/4-requests.md index 5cf96e1702..a6d198a129 100644 --- a/site/guide/4-requests.md +++ b/site/guide/4-requests.md @@ -77,10 +77,8 @@ not just the world, we can declare a route like so: # #[macro_use] extern crate rocket; # fn main() {} -use rocket::http::RawStr; - #[get("/hello/")] -fn hello(name: &RawStr) -> String { +fn hello(name: &str) -> String { format!("Hello, {}!", name) } ``` @@ -114,25 +112,6 @@ fn hello(name: String, age: u8, cool: bool) -> String { [`FromParam`]: @api/rocket/request/trait.FromParam.html [`FromParam` API docs]: @api/rocket/request/trait.FromParam.html -! note: Rocket types _raw_ strings separately from decoded strings. - - You may have noticed an unfamiliar [`RawStr`] type in the code example above. - This is a special type, provided by Rocket, that represents an unsanitized, - unvalidated, and undecoded raw string from an HTTP message. It exists to - separate validated string inputs, represented by types such as `String`, - `&str`, and `Cow`, from unvalidated inputs, represented by `&RawStr`. It - also provides helpful methods to convert the unvalidated string into a - validated one. - - Because `&RawStr` implements [`FromParam`], it can be used as the type of a - dynamic segment, as in the example above, where the value refers to a - potentially undecoded string. By contrast, a `String` is guaranteed to be - decoded. Which you should use depends on whether you want direct but - potentially unsafe access to the string (`&RawStr`), or safe access to the - string at the cost of an allocation (`String`). - - [`RawStr`]: @api/rocket/http/struct.RawStr.html - ### Multiple Segments You can also match against multiple segments by using `` in a route @@ -210,8 +189,6 @@ routes: ```rust # #[macro_use] extern crate rocket; -# use rocket::http::RawStr; - #[get("/user/")] fn user(id: usize) { /* ... */ } @@ -219,7 +196,7 @@ fn user(id: usize) { /* ... */ } fn user_int(id: isize) { /* ... */ } #[get("/user/", rank = 3)] -fn user_str(id: &RawStr) { /* ... */ } +fn user_str(id: &str) { /* ... */ } #[launch] fn rocket() -> rocket::Rocket { @@ -248,7 +225,7 @@ will be routed as follows: `GET /user/ [3] (user_str)`. Forwards can be _caught_ by using a `Result` or `Option` type. For example, if -the type of `id` in the `user` function was `Result`, then `user` +the type of `id` in the `user` function was `Result`, then `user` would never forward. An `Ok` variant would indicate that `` was a valid `usize`, while an `Err` would indicate that `` was not a `usize`. The `Err`'s value would contain the string that failed to parse as a `usize`. @@ -280,126 +257,6 @@ of a route given its properties. | no | fully dynamic | -2 | `/?` | | no | none | -1 | `/` | -## Query Strings - -Query segments can be declared static or dynamic in much the same way as path -segments: - -```rust -# #[macro_use] extern crate rocket; -# fn main() {} - -# use rocket::http::RawStr; - -#[get("/hello?wave&")] -fn hello(name: &RawStr) -> String { - format!("Hello, {}!", name.as_str()) -} -``` - -The `hello` route above matches any `GET` request to `/hello` that has at least -one query key of `name` and a query segment of `wave` in any order, ignoring any -extra query segments. The value of the `name` query parameter is used as the -value of the `name` function argument. For instance, a request to -`/hello?wave&name=John` would return `Hello, John!`. Other requests that would -result in the same response include: - - * `/hello?name=John&wave` (reordered) - * `/hello?name=John&wave&id=123` (extra segments) - * `/hello?id=123&name=John&wave` (reordered, extra segments) - * `/hello?name=Bob&name=John&wave` (last value taken) - -Any number of dynamic query segments are allowed. A query segment can be of any -type, including your own, as long as the type implements the [`FromFormValue`] -trait. - -[`FromFormValue`]: @api/rocket/request/trait.FromFormValue.html - -### Optional Parameters - -Query parameters are allowed to be _missing_. As long as a request's query -string contains all of the static components of a route's query string, the -request will be routed to that route. This allows for optional parameters, -validating even when a parameter is missing. - -To achieve this, use `Option` as the parameter type. Whenever the query -parameter is missing in a request, `None` will be provided as the value. A -route using `Option` looks as follows: - -```rust -# #[macro_use] extern crate rocket; -# fn main() {} - -#[get("/hello?wave&")] -fn hello(name: Option) -> String { - name.map(|name| format!("Hi, {}!", name)) - .unwrap_or_else(|| "Hello!".into()) -} -``` - -Any `GET` request with a path of `/hello` and a `wave` query segment will be -routed to this route. If a `name=value` query segment is present, the route -returns the string `"Hi, value!"`. If no `name` query segment is present, the -route returns `"Hello!"`. - -Just like a parameter of type `Option` will have the value `None` if the -parameter is missing from a query, a parameter of type `bool` will have the -value `false` if it is missing. The default value for a missing parameter can be -customized for your own types that implement `FromFormValue` by implementing -[`FromFormValue::default()`]. - -[`FromFormValue::default()`]: @api/rocket/request/trait.FromFormValue.html#method.default - -### Multiple Segments - -As with paths, you can also match against multiple segments in a query by using -``. The type of such parameters, known as _query guards_, must -implement the [`FromQuery`] trait. Query guards must be the final component of a -query: any text after a query parameter will result in a compile-time error. - -A query guard validates all otherwise unmatched (by static or dynamic query -parameters) query segments. While you can implement [`FromQuery`] yourself, most -use cases will be handled by using the [`Form`] or [`LenientForm`] query guard. -The [Forms](#forms) section explains using these types in detail. In short, -these types allow you to use a structure with named fields to automatically -validate query/form parameters: - -```rust -# #[macro_use] extern crate rocket; -# fn main() {} - -use rocket::request::Form; - -#[derive(FromForm)] -struct User { - name: String, - account: usize, -} - -#[get("/item?&")] -fn item(id: usize, user: Form) { /* ... */ } -``` - -For a request to `/item?id=100&name=sandal&account=400`, the `item` route above -sets `id` to `100` and `user` to `User { name: "sandal", account: 400 }`. To -catch forms that fail to validate, use a type of `Option` or `Result`: - -```rust -# #[macro_use] extern crate rocket; -# fn main() {} - -# use rocket::request::Form; -# #[derive(FromForm)] struct User { name: String, account: usize, } - -#[get("/item?&")] -fn item(id: usize, user: Option>) { /* ... */ } -``` - -For more query handling examples, see [the `query_params` -example](@example/query_params). - -[`FromQuery`]: @api/rocket/request/trait.FromQuery.html - ## Request Guards Request guards are one of Rocket's most powerful instruments. As the name might @@ -737,24 +594,107 @@ Any type that implements [`FromData`] is also known as _a data guard_. [`FromData`]: @api/rocket/data/trait.FromData.html -### Forms +### JSON + +The [`Json`](@api/rocket_contrib/json/struct.Json.html) type from +[`rocket_contrib`] is a data guard that parses the deserialzies body data as +JSON. The only condition is that the generic type `T` implements the +`Deserialize` trait from [Serde](https://github.com/serde-rs/json). + +```rust +# #[macro_use] extern crate rocket; +# extern crate rocket_contrib; +# fn main() {} + +use serde::Deserialize; +use rocket_contrib::json::Json; + +#[derive(Deserialize)] +struct Task { + description: String, + complete: bool +} + +#[post("/todo", data = "")] +fn new(task: Json) { /* .. */ } +``` + +See the [JSON example] on GitHub for a complete example. + +[JSON example]: @example/json + +### Temporary Files + +The [`TempFile`] data guard streams data directly to a temporary file which can +the be persisted. It makes accepting file uploads trivial: + +```rust +# #[macro_use] extern crate rocket; + +use rocket::data::TempFile; + +#[post("/upload", format = "plain", data = "")] +async fn upload(mut file: TempFile<'_>) -> std::io::Result<()> { + # let permanent_location = "/tmp/perm.txt"; + file.persist_to(permanent_location).await +} +``` + +[`TempFile`]: @api/rocket/data/struct.TempFile.html + +### Streaming + +Sometimes you just want to handle incoming data directly. For example, you might +want to stream the incoming data to some sink. Rocket makes this as simple as +possible via the [`Data`](@api/rocket/data/struct.Data.html) type: + +```rust +# #[macro_use] extern crate rocket; + +use rocket::tokio; + +use rocket::data::{Data, ToByteUnit}; + +#[post("/debug", data = "")] +async fn debug(data: Data) -> std::io::Result<()> { + // Stream at most 512KiB all of the body data to stdout. + data.open(512.kibibytes()) + .stream_to(tokio::io::stdout()) + .await?; + + Ok(()) +} +``` + +The route above accepts any `POST` request to the `/debug` path. At most 512KiB +of the incoming is streamed out to `stdout`. If the upload fails, an error +response is returned. The handler above is complete. It really is that simple! + +! note: Rocket requires setting limits when reading incoming data. + + To aid in preventing DoS attacks, Rocket requires you to specify, as a + [`ByteUnit`](@api/rocket/data/struct.ByteUnit.html), the amount of data you're + willing to accept from the client when `open`ing a data stream. The + [`ToByteUnit`](@api/rocket/data/trait.ToByteUnit.html) trait makes specifying + such a value as idiomatic as `128.kibibytes()`. + +## Forms Forms are one of the most common types of data handled in web applications, and Rocket makes handling them easy. Say your application is processing a form submission for a new todo `Task`. The form contains two fields: `complete`, a -checkbox, and `description`, a text field. You can easily handle the form -request in Rocket as follows: +checkbox, and `type`, a text field. You can easily handle the form request in +Rocket as follows: ```rust # #[macro_use] extern crate rocket; -# fn main() {} -use rocket::request::Form; +use rocket::form::Form; #[derive(FromForm)] struct Task { complete: bool, - description: String, + r#type: String, } #[post("/todo", data = "")] @@ -764,54 +704,45 @@ fn new(task: Form) { /* .. */ } The [`Form`] type implements the `FromData` trait as long as its generic parameter implements the [`FromForm`] trait. In the example, we've derived the `FromForm` trait automatically for the `Task` structure. `FromForm` can be -derived for any structure whose fields implement [`FromFormValue`]. If a `POST -/todo` request arrives, the form data will automatically be parsed into the -`Task` structure. If the data that arrives isn't of the correct Content-Type, -the request is forwarded. If the data doesn't parse or is simply invalid, a -customizable `400 - Bad Request` or `422 - Unprocessable Entity` error is -returned. As before, a forward or failure can be caught by using the `Option` -and `Result` types: +derived for any structure whose fields implement [`FromForm`], or equivalently, +[`FromFormField`]. If a `POST /todo` request arrives, the form data will +automatically be parsed into the `Task` structure. If the data that arrives +isn't of the correct Content-Type, the request is forwarded. If the data doesn't +parse or is simply invalid, a customizable error is returned. As before, a +forward or failure can be caught by using the `Option` and `Result` types: ```rust # #[macro_use] extern crate rocket; # fn main() {} -# use rocket::request::Form; -# #[derive(FromForm)] struct Task { complete: bool, description: String, } +# use rocket::form::Form; +# #[derive(FromForm)] struct Task { complete: bool } #[post("/todo", data = "")] fn new(task: Option>) { /* .. */ } ``` -[`Form`]: @api/rocket/request/struct.Form.html -[`FromForm`]: @api/rocket/request/trait.FromForm.html -[`FromFormValue`]: @api/rocket/request/trait.FromFormValue.html - -#### Lenient Parsing +[`Form`]: @api/rocket/form/struct.Form.html +[`FromForm`]: @api/rocket/form/trait.FromForm.html -Rocket's `FromForm` parsing is _strict_ by default. In other words, a `Form` -will parse successfully from an incoming form only if the form contains the -exact set of fields in `T`. Said another way, a `Form` will error on missing -and/or extra fields. For instance, if an incoming form contains the fields "a", -"b", and "c" while `T` only contains "a" and "c", the form _will not_ parse as -`Form`. +### Strict Parsing -Rocket allows you to opt-out of this behavior via the [`LenientForm`] data type. -A `LenientForm` will parse successfully from an incoming form as long as the -form contains a superset of the fields in `T`. Said another way, a -`LenientForm` automatically discards extra fields without error. For -instance, if an incoming form contains the fields "a", "b", and "c" while `T` -only contains "a" and "c", the form _will_ parse as `LenientForm`. +Rocket's `FromForm` parsing is _lenient_ by default: a `Form` will parse +successfully from an incoming form even if it contains extra or duplicate +fields. The extras or duplicates are ignored -- no validation or parsing of the +fields occurs. To change this behavior and make form parsing _strict_, use the +[`Form>`] data type, which errors if there are any extra, undeclared +fields. -You can use a `LenientForm` anywhere you'd use a `Form`. Its generic parameter -is also required to implement `FromForm`. For instance, we can simply replace -`Form` with `LenientForm` above to get lenient parsing: +You can use a `Form>` anywhere you'd use a `Form`. Its generic +parameter is also required to implement `FromForm`. For instance, we can simply +replace `Form` with `Form>` above to get strict parsing: ```rust # #[macro_use] extern crate rocket; # fn main() {} -use rocket::request::LenientForm; +use rocket::form::{Form, Strict}; #[derive(FromForm)] struct Task { @@ -821,23 +752,23 @@ struct Task { } #[post("/todo", data = "")] -fn new(task: LenientForm) { /* .. */ } +fn new(task: Form>) { /* .. */ } ``` -[`LenientForm`]: @api/rocket/request/struct.LenientForm.html +[`Form>`]: @api/rocket/form/struct.Strict.html -#### Field Renaming +### Field Renaming By default, Rocket matches the name of an incoming form field to the name of a structure field. While this behavior is typical, it may also be desired to use different names for form fields and struct fields while still parsing as expected. You can ask Rocket to look for a different form field for a given -structure field by using the `#[form(field = "name")]` field annotation. +structure field by using the `#[field(name = "name")]` field annotation. As an example, say that you're writing an application that receives data from an -external service. The external service `POST`s a form with a field named `type`. -Since `type` is a reserved keyword in Rust, it cannot be used as the name of a -field. To get around this, you can use field renaming as follows: +external service. The external service `POST`s a form with a field named +`first-Name` which you'd like to write as `first_name` in Rust. Field renaming +helps: ```rust # #[macro_use] extern crate rocket; @@ -845,171 +776,856 @@ field. To get around this, you can use field renaming as follows: #[derive(FromForm)] struct External { - #[form(field = "type")] - api_type: String + #[field(name = "first-Name")] + first_name: String } ``` -Rocket will then match the form field named `type` to the structure field named -`api_type` automatically. +Rocket will then match the form field named `first-Name` to the structure field +named `first_name`. -#### Field Validation +### Ad-Hoc Validation -Fields of forms can be easily validated via implementations of the -[`FromFormValue`] trait. For example, if you'd like to verify that some user is -over some age in a form, then you might define a new `AdultAge` type, use it as -a field in a form structure, and implement `FromFormValue` so that it only -validates integers over that age: +Fields of forms can be easily ad-hoc validated via the `#[field(validate)]` +attribute. As an example, consider a form field `age: u16` which we'd like to +ensure is greater than `21`. The following structure accomplishes this: ```rust # #[macro_use] extern crate rocket; -# fn main() {} -use rocket::http::RawStr; -use rocket::request::FromFormValue; +#[derive(FromForm)] +struct Person { + #[field(validate = range(21..))] + age: u16 +} +``` -struct AdultAge(usize); +The expression `range(21..)` is a call to [`form::validate::range`]. Rocket +passes a borrow of the attributed field, here `self.age`, as the first parameter +to the function call. The rest of the fields are pass as written in the +expression. -impl<'v> FromFormValue<'v> for AdultAge { - type Error = &'v RawStr; +Any function in the [`form::validate`] module can be called, and other fields of +the form can be passed in by using `self.$field` `$field` is the name of the +field in the structure. For example, the following form validates that the value +of the field `confirm` is equal to the value of the field `value`: - fn from_form_value(form_value: &'v RawStr) -> Result { - match form_value.parse::() { - Ok(age) if age >= 21 => Ok(AdultAge(age)), - _ => Err(form_value), - } - } +```rust +# #[macro_use] extern crate rocket; + +#[derive(FromForm)] +struct Password { + #[field(name = "password")] + value: String, + #[field(validate = eq(&*self.value))] + confirm: String, +} +``` + +[`form::validate`]: @api/rocket/form/validate/index.html +[`form::validate::range`]: @api/rocket/form/validate/fn.range.html +[`Errors<'_>`]: @api/rocket/form/error/struct.Errors.html + +In reality, the expression after `validate =` can be _any_ expression as long as +it evaluates to a value of type `Result<(), Errors<'_>>`, where an `Ok` value +means that validation was successful while an `Err` of [`Errors<'_>`] indicates +the error(s) that occured. For instance, if you wanted to implement an ad-hoc +Luhn validator for credit-card-like numbers, you might write: + + +```rust +# #[macro_use] extern crate rocket; +extern crate time; + +#[derive(FromForm)] +struct CreditCard<'v> { + #[field(validate = luhn())] + number: &'v str, + # #[field(validate = luhn())] + # other: String, + #[field(validate = range(..9999))] + cvv: u16, + expiration: time::Date, +} + +fn luhn<'v, S: AsRef>(field: S) -> rocket::form::Result<'v, ()> { + let num = field.as_ref().parse::()?; + + /* implementation of Luhn validator... */ + # Ok(()) +} +``` + +### Defaults + +The [`FromForm`] trait allows types to specify a default value if one isn't +provided in a submitted form. This includes types such as `bool`, useful for +checkboxes, and `Option`. Additionally, `FromForm` is implemented for +`Result>` where the error value is [`Errors<'_>`]. All of these +types can be used just like any other form field: + + +```rust +# use rocket::form::FromForm; +use rocket::form::Errors; + +#[derive(FromForm)] +struct MyForm<'v> { + maybe_string: Option, + ok_or_error: Result, Errors<'v>>, + here: bool, +} + +# rocket_guide_tests::assert_form_parses_ok!(MyForm, ""); +``` + +[`Errors<'_>`]: @api/rocket/forms/struct.Errors.html + +### Collections + +Rocket's form support allows your application to express _any_ structure with +_any_ level of nesting and collection, eclipsing the expressivity offered by any +other web framework. To parse into these structures, Rocket separates a field's +name into "keys" by the delimiters `.` and `[]`, each of which in turn is +separated into "indices" by `:`. In other words, a name has keys and a key has +indices, each a strict subset of its parent. This is depicted in the example +below with two form fields: + +```html +food.bart[bar:foo].blam[0_0][1000]=some-value&another_field=another_val +|-------------------------------| name +|--| |--| |-----| |--| |-| |--| keys +|--| |--| |-| |-| |--| |-| |--| indices +``` + +Rocket _pushes_ form fields to `FromForm` types as they arrive. The type then +operates on _one_ key (and all of its indices) at a time and _shifts_ to the +next `key`, from left-to-right, before invoking any other `FromForm` types with +the rest of the field. A _shift_ encodes a nested structure while indices allows +for structures that need more than one value to allow indexing. + +! note: A `.` after a `[]` is optional. + + The form field name `a[b]c` is exactly equivalent to `a[b].c`. Likewise, the + form field name `.a` is equivalent to `a`. + +### Nesting + +Form structs can be nested: + +```rust +use rocket::form::FromForm; + +#[derive(FromForm)] +struct MyForm { + owner: Person, + pet: Pet, } #[derive(FromForm)] struct Person { - age: AdultAge + name: String +} + +#[derive(FromForm)] +struct Pet { + name: String, + #[field(validate = eq(true))] + good_pet: bool, } ``` -If a form is submitted with a bad age, Rocket won't call a handler requiring a -valid form for that structure. You can use `Option` or `Result` types for fields -to catch parse failures: +To parse into a `MyForm`, a form with the following fields must be submitted: + + * `owner.name` - string + * `pet.name` - string + * `pet.good_pet` - boolean + +Such a form, URL-encoded, may look like: ```rust -# #[macro_use] extern crate rocket; -# fn main() {} +# use rocket::form::FromForm; +# use rocket_guide_tests::{assert_form_parses, assert_not_form_parses}; +# #[derive(FromForm, Debug, PartialEq)] struct MyForm { owner: Person, pet: Pet, } +# #[derive(FromForm, Debug, PartialEq)] struct Person { name: String } +# #[derive(FromForm, Debug, PartialEq)] struct Pet { name: String, good_pet: bool, } + +# assert_form_parses! { MyForm, +"owner.name=Bob&pet.name=Sally&pet.good_pet=on", +# "owner.name=Bob&pet.name=Sally&pet.good_pet=yes", +# "owner.name=Bob&pet.name=Sally&pet.good_pet=on", +# "pet.name=Sally&owner.name=Bob&pet.good_pet=on", +# "pet.name=Sally&pet.good_pet=on&owner.name=Bob", +# => + +// ...which parses as this struct. +MyForm { + owner: Person { + name: "Bob".into() + }, + pet: Pet { + name: "Sally".into(), + good_pet: true, + } +} +# }; +``` + +Note that `.` is used to separate each field. Identically, `[]` can be used in +place of or in addition to `.`: + +```rust +# use rocket::form::FromForm; +# use rocket_guide_tests::{assert_form_parses, assert_not_form_parses}; +# #[derive(FromForm, Debug, PartialEq)] struct MyForm { owner: Person, pet: Pet, } +# #[derive(FromForm, Debug, PartialEq)] struct Person { name: String } +# #[derive(FromForm, Debug, PartialEq)] struct Pet { name: String, good_pet: bool, } + +// All of these are identical to the previous... +# assert_form_parses! { MyForm, +"owner[name]=Bob&pet[name]=Sally&pet[good_pet]=on", +"owner[name]=Bob&pet[name]=Sally&pet.good_pet=on", +"owner.name=Bob&pet[name]=Sally&pet.good_pet=on", +"pet[name]=Sally&owner.name=Bob&pet.good_pet=on", +# => + +// ...and thus parse as this struct. +MyForm { + owner: Person { + name: "Bob".into() + }, + pet: Pet { + name: "Sally".into(), + good_pet: true, + } +} +# }; +``` + +Any level of nesting is allowed. + +### Vectors + +A form can also contain sequences: + +```rust +# use rocket::form::FromForm; + +#[derive(FromForm)] +struct MyForm { + numbers: Vec, +} +``` + +To parse into a `MyForm`, a form with the following fields must be submitted: + + * `numbers[$k]` - usize (or equivalently, `numbers.$k`) + +...where `$k` is the "key" used to determine whether to push the rest of the +field to the last element in the vector or create a new one. If the key is the +same as the previous key seen by the vector, then the field's value is pushed to +the last element. Otherwise, a new element is created. The actual value of `$k` +is irrelevant: it is only used for comparison, has no semantic meaning, and is +not remembered by `Vec`. The special blank key is never equal to any other key. + +Consider the following examples. + +```rust +# use rocket::form::FromForm; +# use rocket_guide_tests::{assert_form_parses, assert_not_form_parses}; +# #[derive(FromForm, PartialEq, Debug)] struct MyForm { numbers: Vec, } +// These form strings... +# assert_form_parses! { MyForm, +"numbers[]=1&numbers[]=2&numbers[]=3", +"numbers[a]=1&numbers[b]=2&numbers[c]=3", +"numbers[a]=1&numbers[b]=2&numbers[a]=3", +"numbers[]=1&numbers[b]=2&numbers[c]=3", +"numbers.0=1&numbers.1=2&numbers[c]=3", +"numbers=1&numbers=2&numbers=3", +# => + +// ...parse as this struct: +MyForm { + numbers: vec![1 ,2, 3] +} +# }; + +// These, on the other hand... +# assert_form_parses! { MyForm, +"numbers[0]=1&numbers[0]=2&numbers[]=3", +"numbers[]=1&numbers[b]=3&numbers[b]=2", +# => + +// ...parse as this struct: +MyForm { + numbers: vec![1, 3] +} +# }; +``` + +You might be surprised to see the last example, +`"numbers=1&numbers=2&numbers=3"`, in the first list. This is equivalent to the +previous examples as the "key" seen by the `Vec` (everything after `numbers`) is +empty. Thus, `Vec` pushes to a new `usize` for every field. `usize`, like all +types that implement `FromFormField`, discard duplicate and extra fields when +parsed leniently, keeping only the _first_ field. + +### Nesting in Vectors -# type AdultAge = usize; +Any `FromForm` type can appear in a sequence: + +```rust +# use rocket::form::FromForm; + +#[derive(FromForm)] +struct MyForm { + name: String, + pets: Vec, +} + +#[derive(FromForm)] +struct Pet { + name: String, + #[field(validate = eq(true))] + good_pet: bool, +} +``` + +To parse into a `MyForm`, a form with the following fields must be submitted: + + * `name` - string + * `pets[$k].name` - string + * `pets[$k].good_pet` - boolean + +Examples include: + +```rust +# use rocket::form::FromForm; +# use rocket_guide_tests::{assert_form_parses, assert_not_form_parses}; +# #[derive(FromForm, Debug, PartialEq)] struct MyForm { name: String, pets: Vec, } +# #[derive(FromForm, Debug, PartialEq)] struct Pet { name: String, good_pet: bool, } +// These form strings... +assert_form_parses! { MyForm, +"name=Bob&pets[0].name=Sally&pets[0].good_pet=on", +"name=Bob&pets[sally].name=Sally&pets[sally].good_pet=yes", +# => + +// ...parse as this struct: +MyForm { + name: "Bob".into(), + pets: vec![Pet { name: "Sally".into(), good_pet: true }], +} +# }; + +// These, on the other hand, fail to parse: +# assert_not_form_parses! { MyForm, +"name=Bob&pets[0].name=Sally&pets[1].good_pet=on", +"name=Bob&pets[].name=Sally&pets[].good_pet=on", +# }; +``` + +### Nested Vectors + +Since vectors are `FromForm` themselves, they can appear inside of vectors: + +```rust +# use rocket::form::FromForm; + +#[derive(FromForm)] +struct MyForm { + v: Vec>, +} +``` + +The rules are exactly the same. + +```rust +# use rocket::form::FromForm; +# use rocket_guide_tests::assert_form_parses; +# #[derive(FromForm, Debug, PartialEq)] struct MyForm { v: Vec>, } +# assert_form_parses! { MyForm, +"v=1&v=2&v=3" => MyForm { v: vec![vec![1], vec![2], vec![3]] }, +"v[][]=1&v[][]=2&v[][]=3" => MyForm { v: vec![vec![1], vec![2], vec![3]] }, +"v[0][]=1&v[0][]=2&v[][]=3" => MyForm { v: vec![vec![1, 2], vec![3]] }, +"v[][]=1&v[0][]=2&v[0][]=3" => MyForm { v: vec![vec![1], vec![2, 3]] }, +"v[0][]=1&v[0][]=2&v[0][]=3" => MyForm { v: vec![vec![1, 2, 3]] }, +"v[0][0]=1&v[0][0]=2&v[0][]=3" => MyForm { v: vec![vec![1, 3]] }, +"v[0][0]=1&v[0][0]=2&v[0][0]=3" => MyForm { v: vec![vec![1]] }, +# }; +``` + +### Maps + +A form can also contain maps: + +```rust +# use rocket::form::FromForm; +use std::collections::HashMap; + +#[derive(FromForm)] +struct MyForm { + ids: HashMap, +} +``` + +To parse into a `MyForm`, a form with the following fields must be submitted: + + * `ids[$string]` - usize (or equivalently, `ids.$string`) + +...where `$string` is the "key" used to determine which value in the map to push +the rest of the field to. Unlike with vectors, the key _does_ have a semantic +meaning and _is_ remembered, so ordering of fields is inconsequential: a given +string `$string` always maps to the same element. + +As an example, the following are equivalent and all parse to `{ "a" => 1, "b" => +2 }`: + +```rust +# use std::collections::HashMap; +# +# use rocket::form::FromForm; +# use rocket_guide_tests::{map, assert_form_parses}; +# +# #[derive(Debug, PartialEq, FromForm)] +# struct MyForm { +# ids: HashMap, +# } +// These form strings... +# assert_form_parses! { MyForm, +"ids[a]=1&ids[b]=2", +"ids[b]=2&ids[a]=1", +"ids[a]=1&ids[a]=2&ids[b]=2", +"ids.a=1&ids.b=2", +# => + +// ...parse as this struct: +MyForm { + ids: map! { + "a" => 1usize, + "b" => 2usize, + } +} +# }; +``` + +Both the key and value of a `HashMap` can be any type that implements +`FromForm`. Consider a value representing another structure: + +```rust +# use std::collections::HashMap; + +# use rocket::form::FromForm; + +#[derive(FromForm)] +struct MyForm { + ids: HashMap, +} #[derive(FromForm)] struct Person { - age: Option + name: String, + age: usize } ``` -The `FromFormValue` trait can also be derived for enums with nullary fields: +To parse into a `MyForm`, a form with the following fields must be submitted: + + * `ids[$usize].name` - string + * `ids[$usize].age` - usize + +Examples include: ```rust -# #[macro_use] extern crate rocket; -# fn main() {} +# use std::collections::HashMap; +# +# use rocket::form::FromForm; +# use rocket_guide_tests::{map, assert_form_parses}; +# + +# #[derive(FromForm, Debug, PartialEq)] struct MyForm { ids: HashMap, } +# #[derive(FromForm, Debug, PartialEq)] struct Person { name: String, age: usize } + +// These form strings... +# assert_form_parses! { MyForm, +"ids[0]name=Bob&ids[0]age=3&ids[1]name=Sally&ids[1]age=10", +"ids[0]name=Bob&ids[1]age=10&ids[1]name=Sally&ids[0]age=3", +"ids[0]name=Bob&ids[1]name=Sally&ids[0]age=3&ids[1]age=10", +# => + +// ...which parse as this struct: +MyForm { + ids: map! { + 0usize => Person { name: "Bob".into(), age: 3 }, + 1usize => Person { name: "Sally".into(), age: 10 }, + } +} +# }; +``` + +Now consider the following structure where both the key and value represent +structures: -#[derive(FromFormValue)] -enum MyValue { - First, - Second, - Third, +```rust +# use std::collections::HashMap; + +# use rocket::form::FromForm; + +#[derive(FromForm)] +struct MyForm { + m: HashMap, +} + +#[derive(FromForm, PartialEq, Eq, Hash)] +struct Person { + name: String, + age: usize +} + +#[derive(FromForm)] +struct Pet { + wags: bool } ``` -The derive generates an implementation of the `FromFormValue` trait for the -decorated enum. The implementation returns successfully when the form value -matches, case insensitively, the stringified version of a variant's name, -returning an instance of said variant. +! warning: The `HashMap` key type, here `Person`, must implement `Eq + Hash`. -The [form validation](@example/form_validation) and [form kitchen -sink](@example/form_kitchen_sink) examples provide further illustrations. +Since the key is a collection, here `Person`, it must be built up from multiple +fields. This requires being able to specify via the form field name that the +field's value corresponds to a key in the map. The is done with the syntax +`k:$key` which indicates that the field corresponds to the `k`ey named `$key`. +Thus, to parse into a `MyForm`, a form with the following fields must be +submitted: -### JSON + * `m[k:$key].name` - string + * `m[k:$key].age` - usize + * `m[$key].wags` or `m[v:$key].wags` - boolean + +! note: The syntax `v:$key` also exists. + + The shorthand `m[$key]` is equivalent to `m[v:$key]`. -Handling JSON data is no harder: simply use the -[`Json`](@api/rocket_contrib/json/struct.Json.html) type from -[`rocket_contrib`]: +Note that `$key` can be _anything_: it is simply a symbolic identifier for a +key/value pair in the map and has no bearing on the actual values that will be +parsed into the map. + +Examples include: ```rust -# #[macro_use] extern crate rocket; -# extern crate rocket_contrib; -# fn main() {} +# use std::collections::HashMap; +# +# use rocket::form::FromForm; +# use rocket_guide_tests::{map, assert_form_parses}; +# + +# #[derive(FromForm, Debug, PartialEq)] struct MyForm { m: HashMap, } +# #[derive(FromForm, Debug, PartialEq, Eq, Hash)] struct Person { name: String, age: usize } +# #[derive(FromForm, Debug, PartialEq)] struct Pet { wags: bool } + +// These form strings... +# assert_form_parses! { MyForm, +"m[k:alice]name=Alice&m[k:alice]age=30&m[v:alice].wags=no", +"m[k:alice]name=Alice&m[k:alice]age=30&m[alice].wags=no", +"m[k:123]name=Alice&m[k:123]age=30&m[123].wags=no", +# => + +// ...which parse as this struct: +MyForm { + m: map! { + Person { name: "Alice".into(), age: 30 } => Pet { wags: false } + } +} +# }; + +// While this longer form string... +# assert_form_parses! { MyForm, +"m[k:a]name=Alice&m[k:a]age=40&m[a].wags=no&\ +m[k:b]name=Bob&m[k:b]age=72&m[b]wags=yes&\ +m[k:cat]name=Katie&m[k:cat]age=12&m[cat]wags=yes", +# => + +// ...parses as this struct: +MyForm { + m: map! { + Person { name: "Alice".into(), age: 40 } => Pet { wags: false }, + Person { name: "Bob".into(), age: 72 } => Pet { wags: true }, + Person { name: "Katie".into(), age: 12 } => Pet { wags: true }, + } +} +# }; +``` -use serde::Deserialize; -use rocket_contrib::json::Json; +### Arbitrary Collections -#[derive(Deserialize)] -struct Task { - description: String, - complete: bool +_Any_ collection can be expressed with any level of arbitrary nesting, maps, and +sequences. Consider the extravagently contrived type: + +```rust +use std::collections::{BTreeMap, HashMap}; +# use rocket::form::FromForm; + +#[derive(FromForm, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)] +struct Person { + name: String, + age: usize } -#[post("/todo", data = "")] -fn new(task: Json) { /* .. */ } +# type Foo = +HashMap>, HashMap> +# ; +# /* +|-[k:$k1]-----------|------|------| |-[$k1]-----------------| + |---[$i]-------|------|------| |-[k:$j]*| + |-[k:$k2]|------| ~~[$j]~~|name*| + |-name*| ~~[$j]~~|age-*| + |-age*-| + |~~~~~~~~~~~~~~~|v:$k2*| +# */ ``` -The only condition is that the generic type in `Json` implements the -`Deserialize` trait from [Serde](https://github.com/serde-rs/json). See the -[JSON example] on GitHub for a complete example. +! warning: The `BTreeMap` key type, here `Person`, must implement `Ord`. -[JSON example]: @example/json +As illustrated above with `*` marking terminals, we need the following form +fields for this structure: -### Streaming + * `[k:$k1][$i][k:$k2]name` - string + * `[k:$k1][$i][k:$k2]age` - usize + * `[k:$k1][$i][$k2]` - usize + * `[$k1][k:$j]` - usize + * `[$k1][$j]name` - string + * `[$k1][$j]age` - string -Sometimes you just want to handle incoming data directly. For example, you might -want to stream the incoming data out to a file. Rocket makes this as simple as -possible via the [`Data`](@api/rocket/data/struct.Data.html) type: +Where we have the following symbolic keys: + + * `$k1`: symbolic name of the top-level key + * `$i`: symbolic name of the vector index + * `$k2`: symbolic name of the sub-level (`BTreeMap`) key + * `$j`: symbolic name and/or value top-level value's key ```rust -# #[macro_use] extern crate rocket; -# fn main() {} +# use std::collections::BTreeMap; +# use std::collections::HashMap; +# +# use rocket::form::FromForm; +# use rocket_guide_tests::{map, bmap, assert_form_parses}; +# #[derive(FromForm, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)] +# struct Person { name: String, age: usize } + +type Foo = HashMap>, HashMap>; + +// This (long, contrived) form string... +# assert_form_parses! { Foo, +"[k:top_key][i][k:sub_key]name=Bobert&\ +[k:top_key][i][k:sub_key]age=22&\ +[k:top_key][i][sub_key]=1337&\ +[top_key][7]name=Builder&\ +[top_key][7]age=99", + +// We could also set the top-level value's key explicitly: +// [top_key][k:7]=7 +# "[k:top_key][i][k:sub_key]name=Bobert&\ +# [k:top_key][i][k:sub_key]age=22&\ +# [top_key][k:7]=7&\ +# [k:top_key][i][sub_key]=1337&\ +# [top_key][7]name=Builder&\ +# [top_key][7]age=99", +# => + +// ...parses as this (long, contrived) map: +map! { + vec![bmap! { + Person { name: "Bobert".into(), age: 22 } => 1337usize, + }] + => + map! { + 7usize => Person { name: "Builder".into(), age: 99 } + } +} +# }; +``` -use rocket::data::{Data, ToByteUnit}; -use rocket::response::Debug; +### Context -#[post("/upload", format = "plain", data = "")] -async fn upload(data: Data) -> Result> { - let bytes_written = data.open(128.kibibytes()) - .stream_to_file("/tmp/upload.txt") - .await?; +The [`Contextual`] type acts as a proxy for any form type, recording all of the +submitted form values and produced errors and associating them with their +corresponding field name. `Contextual` is particularly useful to render a form +with previously submitted values and render errors associated with a form input. + +To retrieve the context for a form, use `Form>` as a data +guard, where `T` implements `FromForm`. The `context` field contains the form's +[`Context`]: + +```rust +# use rocket::post; +# type T = String; - Ok(bytes_written.to_string()) +use rocket::form::{Form, Contextual}; + +#[post("/submit", data = "
")] +fn submit(form: Form>) { + if let Some(ref value) = form.value { + // The form parsed successfully. `value` is the `T`. + } + + // In all cases, `form.context` contains the `Context`. + // We can retrieve raw field values and errors. + let raw_id_value = form.context.value("id"); + let id_errors = form.context.errors("id"); } ``` -The route above accepts any `POST` request to the `/upload` path with -`Content-Type: text/plain` At most 128KiB (`128 << 10` bytes) of the incoming -data are streamed out to `tmp/upload.txt`, and the number of bytes written is -returned as a plain text response if the upload succeeds. If the upload fails, -an error response is returned. The handler above is complete. It really is that -simple! See the [GitHub example code](@example/raw_upload) for the full crate. +`Context` serializes as a map, so it can be rendered in templates that require +`Serialize` types. See +[`Context`](@api/rocket/form/struct.Context.html#Serialization) for details +about its serialization format. The [forms example], too, makes use of form +contexts, as well as every other forms feature. -! note: Rocket requires setting limits when reading incoming data. +[`Contextual`]: @api/rocket/form/struct.Contextual.html +[`Context`]: @api/rocket/form/struct.Context.html +[forms example]: @example/forms - To aid in preventing DoS attacks, Rocket requires you to specify, as a - [`ByteUnit`](@api/rocket/data/struct.ByteUnit.html), the amount of data you're - willing to accept from the client when `open`ing a data stream. The - [`ToByteUnit`](@api/rocket/data/trait.ToByteUnit.html) trait makes specifying - such a value as idiomatic as `128.kibibytes()`. +## Query Strings + +Query strings are URL-encoded forms that appear in the URL of a request. Query +parameters are declared like path parameters but otherwise handled like regular +URL-encoded form fields. The table below summarizes the analogy: + +| Path Synax | Query Syntax | Path Type Bound | Query Type Bound | +|-------------|--------------|------------------|------------------| +| `` | `` | [`FromParam`] | [`FromForm`] | +| `` | `` | [`FromSegments`] | [`FromForm`] | +| `static` | `static` | N/A | N/A | + +Because dynamic parameters are form types, they can be single values, +collections, nested collections, or anything in between, just like any other +form field. + +### Static Parameters + +A request matches a route _iff_ its query string contains all of the static +parameters in the route's query string. A route with a static parameter `param` +(any UTF-8 text string) in a query will only match requests with that exact path +segment in its query string. + +! note: This is truly an _iff_! + + Only the static parameters in query route string affect routing. Dynamic + parameters are allowed to be missing by default. + + +For example, the route below will match requests with path `/` and _at least_ +the query segments `hello` and `cat=♥`: + +```rust,ignore +# FIXME: https://github.com/rust-lang/rust/issues/82583 +# #[macro_use] extern crate rocket; + +#[get("/?hello&cat=♥")] +fn cats() -> &'static str { + "Hello, kittens!" +} -## Async Routes +// The following GET requests match `cats`. +# let status = rocket_guide_tests::client(routes![cats]).get( +"/?cat%3D%E2%99%A5%26hello" +# ).dispatch().status(); +# assert_eq!(status, rocket::http::Status::Ok); +# let status = rocket_guide_tests::client(routes![cats]).get( +"/?hello&cat%3D%E2%99%A5%26" +# ).dispatch().status(); +# assert_eq!(status, rocket::http::Status::Ok); +# let status = rocket_guide_tests::client(routes![cats]).get( +"/?dogs=amazing&hello&there&cat%3D%E2%99%A5%26" +# ).dispatch().status(); +# assert_eq!(status, rocket::http::Status::Ok); +``` + +### Dynamic Parameters -Rocket makes it easy to use `async/await` in routes. +A single dynamic parameter of `` acts identically to a form field +declared as `param`. In particular, Rocket will expect the query form to contain +a field with key `param` and push the shifted field to the `param` type. As with +forms, default values are used when parsing fails. The example below illustrates +this with a single value `name`, a collection `color`, a nested form `person`, +and an `other` value that will default to `None`: ```rust # #[macro_use] extern crate rocket; -use rocket::tokio::time::{sleep, Duration}; -#[get("/delay/")] -async fn delay(seconds: u64) -> String { - sleep(Duration::from_secs(seconds)).await; - format!("Waited for {} seconds", seconds) + +#[derive(Debug, PartialEq, FromFormField)] +enum Color { + Red, + Blue, + Green +} + +#[derive(Debug, PartialEq, FromForm)] +struct Pet<'r> { + name: &'r str, + age: usize, +} + +#[derive(Debug, PartialEq, FromForm)] +struct Person<'r> { + pet: Pet<'r>, +} + +#[get("/?&&&")] +fn hello(name: &str, color: Vec, person: Person<'_>, other: Option) { + assert_eq!(name, "George"); + assert_eq!(color, [Color::Red, Color::Green, Color::Green, Color::Blue]); + assert_eq!(other, None); + assert_eq!(person, Person { + pet: Pet { name: "Fi Fo Alex", age: 1 } + }); } + +// A request with these query segments matches as above. +# rocket_guide_tests::client(routes![hello]).get("/?\ +color=reg&\ +color=green&\ +person.pet.name=Fi+Fo+Alex&\ +color=green&\ +person.pet.age=1\ +color=blue&\ +extra=yes\ +# ").dispatch(); ``` -First, notice that the route function is an `async fn`. This enables -the use of `await` inside the handler. `sleep` is an asynchronous -function, so we must `await` it. +Note that, like forms, parsing is field-ordering insensitive and lenient by +default. + +### Trailing Parameter + +A trailing dynamic parameter of `` collects all of the query segments +that don't otherwise match a declared static or dynamic parameter. In other +words, the otherwise unmatched segments are pushed, unshifted, to the +`` type: + +```rust +# #[macro_use] extern crate rocket; + +use rocket::form::Form; + +#[derive(FromForm)] +struct User<'r> { + name: &'r str, + active: bool, +} + +#[get("/?hello&&")] +fn user(id: usize, user: User<'_>) { + assert_eq!(id, 1337); + assert_eq!(user.name, "Bob Smith"); + assert_eq!(user.active, true); +} + +// A request with these query segments matches as above. +# rocket_guide_tests::client(routes![user]).get("/?\ +name=Bob+Smith&\ +id=1337\ +active=yes\ +# ").dispatch(); +``` ## Error Catchers diff --git a/site/guide/5-responses.md b/site/guide/5-responses.md index 4b97289069..2baddcf9b1 100644 --- a/site/guide/5-responses.md +++ b/site/guide/5-responses.md @@ -531,17 +531,16 @@ As an example, consider the following form structure and route: # #[macro_use] extern crate rocket; # fn main() {} -use rocket::http::RawStr; -use rocket::request::Form; +use rocket::form::Form; #[derive(FromForm, UriDisplayQuery)] struct UserDetails<'r> { age: Option, - nickname: &'r RawStr, + nickname: &'r str, } #[post("/user/?")] -fn add_user(id: usize, details: Form) { /* .. */ } +fn add_user(id: usize, details: UserDetails) { /* .. */ } ``` By deriving using `UriDisplayQuery`, an implementation of `UriDisplay` is @@ -551,17 +550,16 @@ automatically generated, allowing for URIs to `add_user` to be generated using ```rust # #[macro_use] extern crate rocket; -# use rocket::http::RawStr; -# use rocket::request::Form; +# use rocket::form::Form; # #[derive(FromForm, UriDisplayQuery)] # struct UserDetails<'r> { # age: Option, -# nickname: &'r RawStr, +# nickname: &'r str, # } # #[post("/user/?")] -# fn add_user(id: usize, details: Form) { /* .. */ } +# fn add_user(id: usize, details: UserDetails) { /* .. */ } let link = uri!(add_user: 120, UserDetails { age: Some(20), nickname: "Bob".into() }); assert_eq!(link.to_string(), "/user/120?age=20&nickname=Bob"); @@ -609,7 +607,7 @@ assert_eq!(mike.to_string(), "/101/Mike?age=28"); ### Conversions [`FromUriParam`] is used to perform a conversion for each value passed to `uri!` -before it is displayed with `UriDisplay`. If a `FromUriParam` +before it is displayed with `UriDisplay`. If a `T: 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 @@ -630,9 +628,7 @@ Other conversions to be aware of are: * `&T` to `T` * `&mut T` to `T` - * `&str` to `RawStr` * `String` to `&str` - * `String` to `RawStr` * `&str` to `&Path` * `&str` to `PathBuf` * `T` to `Form` @@ -642,23 +638,24 @@ The following conversions only apply to path parts: * `T` to `Option` * `T` to `Result` -Conversions _nest_. For instance, a value of type `&T` can be supplied when a -value of type `Option>` is expected: +The following conversions are implemented only in query parts: + + * `Option` to `Result` (for any `E`) + * `Result` to `Option` (for any `E`) + +Conversions are transitive. That is, a conversion from `A -> B` and a conversion +`B -> C` implies a conversion from `A -> C`. For instance, a value of type +`&str` can be supplied when a value of type `Option` is expected: ```rust # #[macro_use] extern crate rocket; -# use rocket::http::RawStr; -# use rocket::request::Form; - -# #[derive(FromForm, UriDisplayQuery)] -# struct UserDetails<'r> { age: Option, nickname: &'r RawStr, } +use std::path::PathBuf; -#[get("/person/?")] -fn person(id: usize, details: Option>) { /* .. */ } +#[get("/person//")] +fn person(id: usize, details: Option) { /* .. */ } -let details = UserDetails { age: Some(20), nickname: "Bob".into() }; -uri!(person: id = 100, details = Some(&details) ); +uri!(person: id = 100, details = "a/b/c"); ``` See the [`FromUriParam`] documentation for further details. diff --git a/site/guide/9-configuration.md b/site/guide/9-configuration.md index 5aeae89883..a159790f50 100644 --- a/site/guide/9-configuration.md +++ b/site/guide/9-configuration.md @@ -21,7 +21,7 @@ values: |----------------|-----------------|-------------------------------------------------|-----------------------| | `address` | `IpAddr` | IP address to serve on | `127.0.0.1` | | `port` | `u16` | Port to serve on. | `8000` | -| `workers` | `usize` | Number of threads to use for executing futures. | cpu core count | +| `workers` | `usize` | Number of threads to use for executing futures. | cpu core count | | `keep_alive` | `u32` | Keep-alive timeout seconds; disabled when `0`. | `5` | | `log_level` | `LogLevel` | Max level to log. (off/normal/debug/critical) | `normal`/`critical` | | `cli_colors` | `bool` | Whether to use colors and emoji when logging. | `true` | diff --git a/site/tests/Cargo.toml b/site/tests/Cargo.toml index b6a0acd278..bf59fd61dc 100644 --- a/site/tests/Cargo.toml +++ b/site/tests/Cargo.toml @@ -5,10 +5,13 @@ workspace = "../../" edition = "2018" publish = false +[dependencies] +rocket = { path = "../../core/lib", features = ["secrets"] } + [dev-dependencies] rocket = { path = "../../core/lib", features = ["secrets"] } -doc-comment = "0.3" rocket_contrib = { path = "../../contrib/lib", features = ["json", "tera_templates", "diesel_sqlite_pool"] } serde = { version = "1.0", features = ["derive"] } rand = "0.8" figment = { version = "0.10", features = ["toml", "env"] } +time = "0.2" diff --git a/site/tests/src/lib.rs b/site/tests/src/lib.rs index 17c9d60d0f..a9a7350c3a 100644 --- a/site/tests/src/lib.rs +++ b/site/tests/src/lib.rs @@ -1,9 +1,55 @@ -#[cfg(any(test, doctest))] -mod site_guide { - rocket::rocket_internal_guide_tests!("../guide/*.md"); +#[cfg(any(test, doctest))] rocket::internal_guide_tests!("../guide/*.md"); +#[cfg(any(test, doctest))] rocket::internal_guide_tests!("../../../README.md"); + +#[macro_export] +macro_rules! map { + ($($key:expr => $value:expr),* $(,)?) => ({ + let mut map = std::collections::HashMap::new(); + $(map.insert($key.into(), $value.into());)* + map + }); +} + +#[macro_export] +macro_rules! bmap { + ($($key:expr => $value:expr),* $(,)?) => ({ + let mut map = std::collections::BTreeMap::new(); + $(map.insert($key.into(), $value.into());)* + map + }); +} + +#[macro_export] +macro_rules! assert_form_parses { + ($T:ty, $form:expr => $value:expr) => ( + let v = rocket::form::Form::<$T>::parse($form).unwrap(); + assert_eq!(v, $value, "{}", $form); + ); + + ($T:ty, $($form:expr => $value:expr),+ $(,)?) => ( + $(assert_form_parses!($T, $form => $value);)+ + ); + + ($T:ty, $($form:expr),+ $(,)? => $value:expr) => ( + $(assert_form_parses!($T, $form => $value);)+ + ); +} + +#[macro_export] +macro_rules! assert_not_form_parses { + ($T:ty, $($form:expr),* $(,)?) => ($( + rocket::form::Form::<$T>::parse($form).unwrap_err(); + )*); +} + +#[macro_export] +macro_rules! assert_form_parses_ok { + ($T:ty, $($form:expr),* $(,)?) => ($( + rocket::form::Form::<$T>::parse($form).expect("form to parse"); + )*); } -#[cfg(any(test, doctest))] -mod readme { - doc_comment::doctest!("../../../README.md", readme); +pub fn client(routes: Vec) -> rocket::local::blocking::Client { + let rocket = rocket::custom(rocket::Config::debug_default()).mount("/", routes); + rocket::local::blocking::Client::tracked(rocket).unwrap() }