From 63a14525d86595a8033715e3bdcd2bf2581eecb1 Mon Sep 17 00:00:00 2001 From: Sergio Benitez Date: Thu, 29 Oct 2020 20:50:06 -0700 Subject: [PATCH] UTF-8 routes. Forms revamp. Temp files. Capped. So. Many. Changes. This is an insane commit: simultaneously one of the best (because of all the wonderful improvements!) and one of the worst (because it is just massive) in the project's history. Routing: * All UTF-8 characters are accepted everywhere in route paths. (#998) * `path` is now `uri` in `route` attribute: `#[route(GET, path = "..")]` becomes `#[route(GET, uri = "..")]`. Forms Revamp * All form related types now reside in a new `form` module. * Multipart forms are supported. (resolves #106) * Collections are supported in forms and queries. (resolves #205) * Nested structures in forms and queries are supported. (resolves #313) * Form fields can be ad-hoc validated with `#[field(validate = expr)]`. * `FromFormValue` is now `FromFormField`, blanket implements `FromForm`. * Form field values are always percent-decoded apriori. Temporary Files * A new `TempFile` data and form guard allows streaming data directly to a file which can then be persisted. * A new `temp_dir` config parameter specifies where to store `TempFile`. * The limits `file` and `file/$ext`, where `$ext` is the file extension, determines the data limit for a `TempFile`. Capped * A new `Capped` type is used to indicate when data has been truncated due to incoming data limits. It allows checking whether data is complete or truncated. * `DataStream` methods return `Capped` types. * `DataStream` API has been revamped to account for `Capped` types. * Several `Capped` types implement `FromData`, `FromForm`. * HTTP 413 (Payload Too Large) errors are now returned when data limits are exceeded. (resolves #972) Hierarchical Limits * Data limits are now hierarchical, delimited with `/`. A limit of `a/b/c` falls back to `a/b` then `a`. Core * `&RawStr` no longer implements `FromParam`. * `&str` implements `FromParam`, `FromData`, `FromForm`. * `FromTransformedData` was removed. * `FromData` gained a lifetime for use with request-local data. * The default error HTML is more compact. * `&Config` is a request guard. * The `DataStream` interface was entirely revamped. * `State` is only exported via `rocket::State`. * A `request::local_cache!()` macro was added for storing values in request-local cache without consideration for type uniqueness by using a locally generated anonymous type. * `Request::get_param()` is now `Request::param()`. * `Request::get_segments()` is now `Request::segments()`, takes a range. * `Request::get_query_value()` is now `Request::query_value()`, can parse any `FromForm` including sequences. * `std::io::Error` implements `Responder` like `Debug`. * `(Status, R)` where `R: Responder` implements `Responder` by overriding the `Status` of `R`. * The name of a route is printed first during route matching. * `FlashMessage` now only has one lifetime generic. HTTP * `RawStr` implements `serde::{Serialize, Deserialize}`. * `RawStr` implements _many_ more methods, in particular, those related to the `Pattern` API. * `RawStr::from_str()` is now `RawStr::new()`. * `RawStr::url_decode()` and `RawStr::url_decode_lossy()` only allocate as necessary, return `Cow`. * `Status` implements `Default` with `Status::Ok`. * `Status` implements `PartialEq`, `Eq`, `Hash`, `PartialOrd`, `Ord`. * Authority and origin part of `Absolute` can be modified with new `Absolute::{with,set}_authority()`, `Absolute::{with,set}_origin()` methods. * `Origin::segments()` was removed in favor of methods split into query and path parts and into raw and decoded versions. * The `Segments` iterator is smarter, returns decoded `&str` items. * `Segments::into_path_buf()` is now `Segments::to_path_buf()`. * A new `QuerySegments` is the analogous query segment iterator. * Once set, `expires` on private cookies is not overwritten. (resolves #1506) * `Origin::path()` and `Origin::query()` return `&RawStr`, not `&str`. Codegen * Preserve more spans in `uri!` macro. * Preserve spans `FromForm` field types. * All dynamic parameters in a query string must typecheck as `FromForm`. * `FromFormValue` derive removed; `FromFormField` added. * The `form` `FromForm` and `FromFormField` field attribute is now named `field`. `#[form(field = ..)]` is now `#[field(name = ..)]`. Contrib * `Json` implements `FromForm`. * `MsgPack` implements `FromForm`. * The `json!` macro is exported as `rocket_contrib::json::json!`. * Added clarifying docs to `StaticFiles`. Examples * `form_validation` and `form_kitchen_sink` removed in favor of `forms`. * The `hello_world` example uses unicode in paths. * The `json` example only allocates as necessary. Internal * Codegen uses new `exports` module with the following conventions: - Locals starts with `__` and are lowercased. - Rocket modules start with `_` and are lowercased. - `std` types start with `_` and are titlecased. - Rocket types are titlecased. * A `header` module was added to `http`, contains header types. * `SAFETY` is used as doc-string keyword for `unsafe` related comments. * The `Uri` parser no longer recognizes Rocket route URIs. --- Cargo.toml | 6 +- contrib/codegen/Cargo.toml | 2 +- contrib/lib/src/json.rs | 163 +- contrib/lib/src/msgpack.rs | 128 +- contrib/lib/src/serve.rs | 43 +- contrib/lib/src/uuid.rs | 17 +- contrib/lib/tests/static_files.rs | 3 +- contrib/lib/tests/templates.rs | 4 +- core/codegen/Cargo.toml | 2 +- core/codegen/src/attribute/catch.rs | 43 +- core/codegen/src/attribute/route.rs | 261 ++-- core/codegen/src/attribute/segments.rs | 7 +- core/codegen/src/bang/mod.rs | 2 +- core/codegen/src/bang/test_guide.rs | 7 +- core/codegen/src/bang/uri.rs | 34 +- core/codegen/src/derive/form_field.rs | 147 ++ core/codegen/src/derive/from_form.rs | 365 +++-- core/codegen/src/derive/from_form_field.rs | 75 + core/codegen/src/derive/from_form_value.rs | 59 - core/codegen/src/derive/mod.rs | 3 +- core/codegen/src/derive/responder.rs | 121 +- core/codegen/src/derive/uri_display.rs | 231 ++- core/codegen/src/exports.rs | 105 ++ core/codegen/src/http_codegen.rs | 28 +- core/codegen/src/lib.rs | 152 +- core/codegen/src/syn_ext.rs | 18 +- core/codegen/tests/from_form.rs | 431 ++++-- ...{from_form_value.rs => from_form_field.rs} | 46 +- core/codegen/tests/route-data.rs | 27 +- core/codegen/tests/route-format.rs | 4 + core/codegen/tests/route.rs | 198 ++- core/codegen/tests/typed-uris.rs | 22 +- .../tests/ui-fail-nightly/from_form.stderr | 265 ++-- .../tests/ui-fail-nightly/from_form_field.rs | 1 + ...rm_value.stderr => from_form_field.stderr} | 80 +- .../from_form_type_errors.stderr | 16 +- .../tests/ui-fail-nightly/from_form_value.rs | 1 - .../route-attribute-general-syntax.stderr | 13 +- .../route-path-bad-syntax.stderr | 45 +- .../ui-fail-nightly/route-type-errors.stderr | 41 +- .../ui-fail-nightly/typed-uri-bad-type.stderr | 116 +- .../tests/ui-fail-nightly/uri_display.stderr | 14 +- .../uri_display_type_errors.stderr | 42 +- .../tests/ui-fail-stable/from_form.stderr | 265 ++-- .../tests/ui-fail-stable/from_form_field.rs | 1 + ...rm_value.stderr => from_form_field.stderr} | 80 +- .../from_form_type_errors.stderr | 16 +- .../tests/ui-fail-stable/from_form_value.rs | 1 - .../route-attribute-general-syntax.stderr | 13 +- .../route-path-bad-syntax.stderr | 37 +- .../ui-fail-stable/route-type-errors.stderr | 48 +- .../ui-fail-stable/typed-uri-bad-type.stderr | 116 +- .../tests/ui-fail-stable/uri_display.stderr | 14 +- .../uri_display_type_errors.stderr | 42 +- core/codegen/tests/ui-fail/from_form.rs | 48 +- core/codegen/tests/ui-fail/from_form_field.rs | 39 + core/codegen/tests/ui-fail/from_form_value.rs | 39 - .../tests/ui-fail/typed-uri-bad-type.rs | 23 +- .../tests/ui-fail/typed-uris-bad-params.rs | 4 +- core/codegen/tests/ui-fail/uri_display.rs | 2 +- core/codegen/tests/uri_display.rs | 9 +- core/http/Cargo.toml | 24 +- core/http/src/cookies.rs | 1 + core/http/src/ext.rs | 33 + core/http/src/{ => header}/accept.rs | 0 core/http/src/{ => header}/content_type.rs | 56 +- core/http/src/{ => header}/header.rs | 0 .../src/{ => header}/known_media_types.rs | 1 + core/http/src/{ => header}/media_type.rs | 103 +- core/http/src/header/mod.rs | 13 + core/http/src/lib.rs | 20 +- core/http/src/listener.rs | 3 + core/http/src/method.rs | 9 + core/http/src/parse/accept.rs | 9 +- core/http/src/parse/indexed.rs | 24 +- core/http/src/parse/media_type.rs | 2 +- core/http/src/parse/uri/error.rs | 4 +- core/http/src/parse/uri/mod.rs | 7 +- core/http/src/parse/uri/parser.rs | 11 +- core/http/src/parse/uri/tables.rs | 10 +- core/http/src/raw_str.rs | 508 ++++++- core/http/src/route.rs | 14 +- core/http/src/status.rs | 54 +- core/http/src/uri/absolute.rs | 103 +- core/http/src/uri/encoding.rs | 13 +- core/http/src/uri/from_uri_param.rs | 37 +- core/http/src/uri/mod.rs | 3 + core/http/src/uri/origin.rs | 257 +++- core/http/src/uri/segments.rs | 191 ++- core/http/src/uri/uri.rs | 35 +- core/http/src/uri/uri_display.rs | 41 +- core/lib/Cargo.toml | 29 +- core/lib/benches/simple-routing.rs | 6 +- core/lib/src/catcher.rs | 37 +- core/lib/src/config/config.rs | 40 +- core/lib/src/data/capped.rs | 270 ++++ core/lib/src/data/data.rs | 42 +- core/lib/src/data/data_stream.rs | 303 +++- core/lib/src/data/from_data.rs | 675 +++------ core/lib/src/data/limits.rs | 222 ++- core/lib/src/data/mod.rs | 10 +- core/lib/src/data/temp_file.rs | 354 +++++ core/lib/src/error.rs | 2 +- core/lib/src/ext.rs | 113 +- core/lib/src/fairing/mod.rs | 2 +- core/lib/src/form/context.rs | 164 ++ core/lib/src/form/error.rs | 554 +++++++ core/lib/src/form/field.rs | 75 + core/lib/src/form/form.rs | 184 +++ core/lib/src/form/from_form.rs | 743 +++++++++ core/lib/src/form/from_form_field.rs | 413 +++++ core/lib/src/form/mod.rs | 474 ++++++ core/lib/src/form/name.rs | 907 +++++++++++ core/lib/src/form/options.rs | 11 + core/lib/src/form/parser.rs | 277 ++++ core/lib/src/form/strict.rs | 129 ++ core/lib/src/form/tests.rs | 121 ++ core/lib/src/form/validate.rs | 246 +++ core/lib/src/lib.rs | 25 +- core/lib/src/local/asynchronous/request.rs | 2 +- core/lib/src/logger.rs | 2 +- core/lib/src/outcome.rs | 113 +- core/lib/src/request/form/error.rs | 73 - core/lib/src/request/form/form.rs | 235 --- core/lib/src/request/form/form_items.rs | 484 ------ core/lib/src/request/form/from_form.rs | 127 -- core/lib/src/request/form/from_form_value.rs | 294 ---- core/lib/src/request/form/lenient.rs | 125 -- core/lib/src/request/form/mod.rs | 15 - .../src/request/{param.rs => from_param.rs} | 103 +- core/lib/src/request/from_request.rs | 6 + core/lib/src/request/mod.rs | 48 +- core/lib/src/request/query.rs | 235 --- core/lib/src/request/request.rs | 304 ++-- core/lib/src/response/debug.rs | 46 +- core/lib/src/response/flash.rs | 17 +- core/lib/src/response/named_file.rs | 51 +- core/lib/src/response/status.rs | 39 +- core/lib/src/rocket.rs | 4 +- core/lib/src/router/collider.rs | 35 +- core/lib/src/router/route.rs | 16 +- core/lib/src/server.rs | 24 +- core/lib/src/{request => }/state.rs | 6 +- core/lib/tests/derive-reexports.rs | 6 +- core/lib/tests/encoded-uris.rs | 21 + .../lib/tests/flash-lazy-removes-issue-466.rs | 4 +- core/lib/tests/form-validation-names.rs | 138 ++ core/lib/tests/form_method-issue-45.rs | 4 +- .../lib/tests/form_value_decoding-issue-82.rs | 11 +- .../form_value_from_encoded_str-issue-1425.rs | 14 +- core/lib/tests/limits.rs | 17 +- .../local-request-content-type-issue-505.rs | 4 +- core/lib/tests/session-cookies-issue-1506.rs | 25 + core/lib/tests/strict_and_lenient_forms.rs | 13 +- examples/content_types/src/main.rs | 14 +- examples/cookies/src/main.rs | 11 +- examples/form_kitchen_sink/src/main.rs | 43 - examples/form_kitchen_sink/static/index.html | 48 - examples/form_validation/Cargo.toml | 10 - examples/form_validation/src/main.rs | 83 - examples/form_validation/src/tests.rs | 84 -- examples/form_validation/static/index.html | 8 - .../{form_kitchen_sink => forms}/Cargo.toml | 5 +- examples/forms/Rocket.toml | 2 + examples/forms/src/main.rs | 90 ++ .../{form_kitchen_sink => forms}/src/tests.rs | 0 examples/forms/static/chota.min.css | 1 + examples/forms/templates/index.html.tera | 140 ++ examples/forms/templates/macros.html.tera | 63 + examples/forms/templates/success.html.tera | 30 + examples/hello_person/src/main.rs | 2 +- examples/hello_world/src/main.rs | 22 +- examples/json/src/main.rs | 55 +- examples/manual_routes/src/main.rs | 13 +- examples/optional_redirect/src/main.rs | 5 +- examples/pastebin/src/main.rs | 27 +- examples/pastebin/src/paste_id.rs | 41 +- examples/query_params/src/main.rs | 8 +- examples/ranking/src/main.rs | 4 +- examples/raw_upload/src/main.rs | 14 +- examples/raw_upload/src/tests.rs | 2 +- examples/request_local_state/src/main.rs | 13 +- examples/session/src/main.rs | 6 +- examples/todo/src/main.rs | 5 +- site/guide/10-pastebin.md | 42 +- site/guide/3-overview.md | 18 + site/guide/4-requests.md | 1344 ++++++++++++----- site/guide/5-responses.md | 41 +- site/guide/9-configuration.md | 2 +- site/tests/Cargo.toml | 5 +- site/tests/src/lib.rs | 58 +- 191 files changed, 11524 insertions(+), 5695 deletions(-) create mode 100644 core/codegen/src/derive/form_field.rs create mode 100644 core/codegen/src/derive/from_form_field.rs delete mode 100644 core/codegen/src/derive/from_form_value.rs create mode 100644 core/codegen/src/exports.rs rename core/codegen/tests/{from_form_value.rs => from_form_field.rs} (59%) create mode 120000 core/codegen/tests/ui-fail-nightly/from_form_field.rs rename core/codegen/tests/ui-fail-nightly/{from_form_value.stderr => from_form_field.stderr} (54%) delete mode 120000 core/codegen/tests/ui-fail-nightly/from_form_value.rs create mode 120000 core/codegen/tests/ui-fail-stable/from_form_field.rs rename core/codegen/tests/ui-fail-stable/{from_form_value.stderr => from_form_field.stderr} (52%) delete mode 120000 core/codegen/tests/ui-fail-stable/from_form_value.rs create mode 100644 core/codegen/tests/ui-fail/from_form_field.rs delete mode 100644 core/codegen/tests/ui-fail/from_form_value.rs rename core/http/src/{ => header}/accept.rs (100%) rename core/http/src/{ => header}/content_type.rs (86%) rename core/http/src/{ => header}/header.rs (100%) rename core/http/src/{ => header}/known_media_types.rs (99%) rename core/http/src/{ => header}/media_type.rs (85%) create mode 100644 core/http/src/header/mod.rs create mode 100644 core/lib/src/data/capped.rs create mode 100644 core/lib/src/data/temp_file.rs create mode 100644 core/lib/src/form/context.rs create mode 100644 core/lib/src/form/error.rs create mode 100644 core/lib/src/form/field.rs create mode 100644 core/lib/src/form/form.rs create mode 100644 core/lib/src/form/from_form.rs create mode 100644 core/lib/src/form/from_form_field.rs create mode 100644 core/lib/src/form/mod.rs create mode 100644 core/lib/src/form/name.rs create mode 100644 core/lib/src/form/options.rs create mode 100644 core/lib/src/form/parser.rs create mode 100644 core/lib/src/form/strict.rs create mode 100644 core/lib/src/form/tests.rs create mode 100644 core/lib/src/form/validate.rs delete mode 100644 core/lib/src/request/form/error.rs delete mode 100644 core/lib/src/request/form/form.rs delete mode 100644 core/lib/src/request/form/form_items.rs delete mode 100644 core/lib/src/request/form/from_form.rs delete mode 100644 core/lib/src/request/form/from_form_value.rs delete mode 100644 core/lib/src/request/form/lenient.rs delete mode 100644 core/lib/src/request/form/mod.rs rename core/lib/src/request/{param.rs => from_param.rs} (76%) delete mode 100644 core/lib/src/request/query.rs rename core/lib/src/{request => }/state.rs (97%) create mode 100644 core/lib/tests/encoded-uris.rs create mode 100644 core/lib/tests/form-validation-names.rs create mode 100644 core/lib/tests/session-cookies-issue-1506.rs delete mode 100644 examples/form_kitchen_sink/src/main.rs delete mode 100644 examples/form_kitchen_sink/static/index.html delete mode 100644 examples/form_validation/Cargo.toml delete mode 100644 examples/form_validation/src/main.rs delete mode 100644 examples/form_validation/src/tests.rs delete mode 100644 examples/form_validation/static/index.html rename examples/{form_kitchen_sink => forms}/Cargo.toml (55%) create mode 100644 examples/forms/Rocket.toml create mode 100644 examples/forms/src/main.rs rename examples/{form_kitchen_sink => forms}/src/tests.rs (100%) create mode 100644 examples/forms/static/chota.min.css create mode 100644 examples/forms/templates/index.html.tera create mode 100644 examples/forms/templates/macros.html.tera create mode 100644 examples/forms/templates/success.html.tera diff --git a/Cargo.toml b/Cargo.toml index 3af5843f..25cf1274 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 83f5e1f3..3f70e15f 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 2ac5867c..7c37a976 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,22 +119,6 @@ pub use serde_json::{json_internal, json_internal_vec}; #[derive(Debug)] pub struct Json(pub T); -impl Json { - /// Consumes the JSON wrapper and returns the wrapped item. - /// - /// # Example - /// ```rust - /// # use rocket_contrib::json::Json; - /// let string = "Hello".to_string(); - /// let my_json = Json(string); - /// assert_eq!(my_json.into_inner(), "Hello".to_string()); - /// ``` - #[inline(always)] - pub fn into_inner(self) -> T { - self.0 - } -} - /// An error returned by the [`Json`] data guard when incoming data fails to /// serialize as JSON. #[derive(Debug)] @@ -126,36 +135,54 @@ pub enum JsonError<'a> { const DEFAULT_LIMIT: ByteUnit = ByteUnit::Mebibyte(1); -impl<'a, T: Deserialize<'a>> FromTransformedData<'a> for Json { - type Error = JsonError<'a>; - type Owned = String; - type Borrowed = str; +impl Json { + /// Consumes the JSON wrapper and returns the wrapped item. + /// + /// # Example + /// ```rust + /// # use rocket_contrib::json::Json; + /// let string = "Hello".to_string(); + /// let my_json = Json(string); + /// assert_eq!(my_json.into_inner(), "Hello".to_string()); + /// ``` + #[inline(always)] + pub fn into_inner(self) -> T { + self.0 + } +} - 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)))) - } - }) +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)) } - 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) -> 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)), + }; + + Self::from_str(local_cache!(req, string)) + } +} + +#[rocket::async_trait] +impl<'r, T: Deserialize<'r>> FromData<'r> for Json { + type Error = JsonError<'r>; + + 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 0f476d4d..ff89ab20 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::*; + 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)), + }; - 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)), - } - } - } - }) + 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 d6f288b1..b2bb1378 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 82848cd5..91c405ae 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 be2cbee7..ff003582 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 ba41f8f6..f42bc22d 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 29a1e5c0..8084d627 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 e01b29f1..d92ac9d4 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 e8c1fa04..386dd77d 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; + let __outcome = <#ty as #FromData>::from_data(#__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), - }; - - #[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); + use devise::ext::{Split2, Split6}; + + define_spanned_export!(Span::call_site() => + __req, __data, _log, _form, Outcome, _Ok, _Err, _Some, _None + ); + 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), + // 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(); - 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!() - }; + // 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) + }; - decls.push(decl); - matchers.push(matcher); - builders.push(builder); - } + let span = ty.span(); + define_spanned_export!(span => FromForm, _form); - matchers.push(quote!(_ => continue)); + 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)* + let mut _e = #_form::Errors::new(); + #(let mut #ident = #init_expr;)* - 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 - )* - } + 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 74c7c869..fc501ee1 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 371bdd33..b084a283 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 194d41e7..e442abab 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 b63b6e98..10647c4e 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 00000000..1919b471 --- /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 5d33f60f..c11a5b6b 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, -} +// 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)) + }); -pub struct FormField { - pub span: Span, - pub name: NameSource, -} + let field_context = syn::parse2(field_context).expect("valid expr"); + let expr = map_f(&ty, &field_context); + Ok(quote!(#field_name => { #expr })) + }) + .collect::>>()?; -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 - } + Ok(quote! { + __c.__parent = __f.name.parent(); - // 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 != '?') -} - -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")); +fn context_type(input: Input<'_>) -> (TokenStream, Option) { + let mut gen = input.generics().clone(); + + let lifetime = syn::parse_quote!('__f); + if !gen.replace_lifetime(0, &lifetime) { + gen.insert_lifetime(syn::LifetimeDef::new(lifetime.clone())); } - 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")); - } - - names.insert(field.name, field.span); - } - - 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))?; - - 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)); - } - _ => { /* lenient or "method"; let it pass */ } - } + .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")); } - #_Ok(Self { #(#builders)* }) + 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(()) }) - }) - .to_tokens2() + ) + .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 + } + } + }) + }) + .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::*; + + #output + } + }) + .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 00000000..eb8a6e43 --- /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 b5ad3344..00000000 --- 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 8fd1b7e4..134279e7 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 9728efb2..88067da9 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);) - } - - let attr = ItemAttr::from_attrs("response", fields.parent.attrs()) - .unwrap_or_else(|| Ok(Default::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 mut headers = vec![]; - for field in fields.iter().skip(1) { - let attr = FieldAttr::from_attrs("response", &field.attrs) - .unwrap_or_else(|| Ok(Default::default()))?; - - 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);) - }); - - Ok(quote! { - #responder - #(#headers)* - #content_type - #status - #_Ok(__res) + .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(()) }) - }) - .to_tokens2() + .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::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 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())); + } + } + + 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) + }) + }) + ) + .to_tokens() } diff --git a/core/codegen/src/derive/uri_display.rs b/core/codegen/src/derive/uri_display.rs index a5ec80c1..0a5d7636 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); + use crate::http::uri::Query; - 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()))?; + const URI_DISPLAY: StaticTokens = quote_static!(#_uri::UriDisplay<#_uri::Query>); + const FORMATTER: StaticTokens = quote_static!(#_uri::Formatter<#_uri::Query>); - let name = name_source.name(); - quote_spanned!(span => f.write_named_value(#name, &#accessor)?;) - } else { - quote_spanned!(span => f.write_value(&#accessor)?;) - }; + 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(); + 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); + use crate::http::uri::Path; - 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(); + 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 00000000..aab38e36 --- /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 27df7952..674651a4 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 79316b64..e50d5537 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 c9b01654..6ebabef3 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 84462992..58140833 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 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=unknown&other=unknown"); - assert_eq!(form, Err(FormParseError::BadValue("complete".into(), "unknown".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 + } + })); - let form: Result = strict("complete=true&other=1&extra=foo"); - assert_eq!(form, Err(FormParseError::Unknown("extra".into(), "foo".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 + } + })); - // 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=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 + } + })); - // 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()))); + assert!(errors.iter().any(|e| { + "unknown" == e.name.as_ref().unwrap() + && Some("!") == e.value.as_deref() + && match e.kind { + ErrorKind::Unexpected => true, + _ => false + } + })); - // Finally, missing. - let form: Result = strict("complete=true"); - assert_eq!(form, Err(FormParseError::Missing("other".into()))); -} - -#[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 e6a77aa3..e5e14456 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 afccfcef..0deee7a1 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 = (); +impl<'r> FromData<'r> for Simple<'r> { + type Error = std::io::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, ())), - } + 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 1c991b84..c98e1e62 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 1eab6ed6..1bf50bf4 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 8be6f9f2..584e6241 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 2247f7ff..474647b9 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 00000000..89dde8e7 --- /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 3d1939ab..a912006e 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 6af7c5f5..d1ce628f 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 e48f57bb..00000000 --- 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 62ff7ef2..0eb4ae5a 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 7fca8c07..29b9abe3 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 3c2132af..6694a1bc 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 b3089623..c4bd50fc 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 +error[E0277]: the trait bound `S: FromUriParam` is not satisfied + --> $DIR/typed-uri-bad-type.rs:68:26 | -65 | uri!(other_q: rest = S, id = _); - | ^ the trait `FromUriParam` is not implemented for `S` +68 | uri!(other_q: rest = S, id = _); + | ^ the trait `FromUriParam` is not implemented for `S` | = note: required by `from_uri_param` - -error[E0277]: the trait bound `std::option::Option: FromUriParam` is not satisfied - --> $DIR/typed-uri-bad-type.rs:69:28 - | -69 | uri!(optionals_q: id = 10, name = "Bob".to_string()); - | ^^ the trait `FromUriParam` is not implemented for `std::option::Option` - | - = 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 9dcf7f6c..921d47f0 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 1114a845..10085692 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 65e7bda0..0ffec14f 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 00000000..89dde8e7 --- /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 bb5a9a59..757410bf 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 db8525ba..dd5bd6a8 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 e48f57bb..00000000 --- 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 77b383bb..bd742b5d 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 d274e60a..dc254fa1 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 26a525ff..9cfd1fe7 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 b5b61641..686693d1 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 +error[E0277]: the trait bound `S: FromUriParam` is not satisfied + --> $DIR/typed-uri-bad-type.rs:68:26 | -65 | uri!(other_q: rest = S, id = _); - | ^ the trait `FromUriParam` is not implemented for `S` +68 | uri!(other_q: rest = S, id = _); + | ^ the trait `FromUriParam` is not implemented for `S` | = note: required by `from_uri_param` - -error[E0277]: the trait bound `std::option::Option: FromUriParam` is not satisfied - --> $DIR/typed-uri-bad-type.rs:69:28 - | -69 | uri!(optionals_q: id = 10, name = "Bob".to_string()); - | ^^ the trait `FromUriParam` is not implemented for `std::option::Option` - | - = 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 b0730b4d..4e3b090a 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 45f0b8f9..1a473fa0 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 007be45c..3133c969 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 00000000..5a53664c --- /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 a24f1f7a..00000000 --- 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 ce348284..4379561b 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 dc7cbe19..9f18a27e 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 42cc6db3..4d469f92 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 c1f41b8f..5cd5aa47 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 fad87ea4..011a3150 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 cb4e5ca8..6c27c9f8 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 ee9ea2a7..7741d31c 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 9856fb2b..36bcbfdf 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 f4d4e034..66796ea5 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 27b4c306..69fc514b 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 00000000..b91521ff --- /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 56e8760a..888fdce1 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 f3265d0a..9c0e35ce 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 962d519c..ecb06123 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 7016d32d..6ea87e13 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 699ab990..1195aafe 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 80c889fc..76f9a2aa 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 e2576df0..4e215ed2 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 a28e7203..6e6e6fd3 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 6c506216..540abb78 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 3e070a36..5d9db6c0 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 0106cd60..75799b2a 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() } + + /// 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()) + } + + /// 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()) + } +} + +#[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 fmt::Debug for RawStr { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + self.0.fmt(f) + } } impl<'a> From<&'a str> for &'a RawStr { #[inline(always)] fn from(string: &'a str) -> &'a RawStr { - RawStr::ref_cast(string) + RawStr::new(string) } } -impl PartialEq for RawStr { - #[inline(always)] - fn eq(&self, other: &str) -> bool { - self.as_str() == other - } +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 PartialEq for RawStr { - #[inline(always)] - fn eq(&self, other: &String) -> bool { - self.as_str() == other.as_str() - } -} +impl_partial!(RawStr : &RawStr); +impl_partial!(&RawStr : RawStr); -impl PartialEq for &'_ RawStr { - #[inline(always)] - fn eq(&self, other: &String) -> bool { - self.as_str() == other.as_str() - } -} +impl_partial!(str : RawStr); +impl_partial!(str : &RawStr); +impl_partial!(&str : RawStr); +impl_partial!(&&str : RawStr); -impl PartialOrd for RawStr { - #[inline(always)] - fn partial_cmp(&self, other: &str) -> Option { - (self as &str).partial_cmp(other) - } -} +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(always)] - fn deref(&self) -> &str { - &self.0 + #[inline] + fn index(&self, index: I) -> &Self::Output { + self.as_str()[index].into() } } -impl DerefMut for RawStr { +impl std::borrow::Borrow for RawStr { #[inline(always)] - fn deref_mut(&mut self) -> &mut str { - &mut self.0 + fn borrow(&self) -> &str { + self.as_str() + } +} + +impl std::borrow::Borrow for &str { + #[inline(always)] + 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 d4c357f2..30176a8e 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 00e673b6..5da1e2ab 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 4fae0a2d..8d3db5c0 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 1c4d9f22..a7bbdede 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 fa473bf0..65a62e19 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 f13ff2e5..94b14bfa 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 e28a1d00..6fbc6343 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)?; + debug_assert!(origin.source.is_some(), "Origin source parsed w/o source"); - 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") + 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. + /// 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. /// - /// The segment count is cached after the first invocation. As a result, - /// this function is O(1) after the first invocation, and O(n) before. - /// - /// ### Examples - /// - /// A valid URI with only non-empty segments: + /// # 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()); + /// + /// 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(); + /// + /// assert_eq!(query_segs, &[("a+b%2F", "some+one%40gmail.com"), ("%26%3D2", "")]); /// ``` - /// - /// A URI with empty segments: - /// - /// ```rust - /// # extern crate rocket; - /// use rocket::http::uri::Origin; - /// - /// let uri = Origin::parse("/a/b//c/d///e").unwrap(); - /// assert_eq!(uri.segment_count(), 5); - /// ``` - #[inline] - pub fn segment_count(&self) -> usize { - *self.segment_count.get_or_set(|| self.segments().count()) + #[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 5d91f7ab..86368262 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 != '/')?; - - // Get the index of the first character that _is_ a '/' after start. - // j = index of first character after i (hence the i +) that's not a '/' - let j = self.0[i..].find('/').map_or(self.0.len(), |j| i + j); - - // Save the result, update the iterator, and return! - let result = Some(&self.0[i..j]); - self.0 = &self.0[j..]; - result + let item = self.get(0)?; + self.pos += 1; + Some(item) } - // 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 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)) + } +} + +impl<'o> Iterator for QuerySegments<'o> { + type Item = (&'o str, &'o str); + + fn next(&mut self) -> Option { + let item = self.get(0)?; + self.pos += 1; + Some(item) + } + + fn size_hint(&self) -> (usize, Option) { + (self.len(), Some(self.len())) + } + + fn count(self) -> usize { + self.len() + } } diff --git a/core/http/src/uri/uri.rs b/core/http/src/uri/uri.rs index 84d1a76a..a4bc2b9d 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 a3c6cca3..b2028149 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 0a545bb2..cc2a39c9 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 be220389..58d054a1 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 8ac6cd37..e2f50279 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 7d1fa61b..830ce502 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 00000000..3627abe1 --- /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 de83b2da..bf09bebb 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 a968ec42..9a6d17e3 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 { - /// A helper method to write the body of the request to any `AsyncWrite` - /// type. + 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. /// - /// This method is identical to `tokio::io::copy(&mut self, &mut writer)`. + /// # 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. 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)` + /// 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,36 +136,19 @@ 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 - } - - /// A helper method to write the body of the request to a `String`. - /// - /// # Example - /// - /// ```rust - /// use std::io; - /// use rocket::data::{Data, ToByteUnit}; - /// - /// async fn handler(data: Data) -> io::Result { - /// data.open(10.bytes()).stream_to_string().await - /// } - /// ``` - 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 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`. @@ -95,22 +159,91 @@ 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(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 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_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`. + /// + /// # Example + /// + /// ```rust + /// use std::io; + /// use rocket::data::{Data, ToByteUnit}; + /// + /// 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 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 file at the path + /// determined by `path`. If a file at the path already exists, it is + /// overwritten. The opened file is returned. + /// + /// # Example + /// + /// ```rust + /// use std::io; + /// use rocket::data::{Data, ToByteUnit}; + /// + /// 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 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, - } - } - - trace_!("DataStream::stream_read()"); - Pin::new(&mut self.stream).poll_read(cx, buf) + 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)), + } + } + + 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 ac289e54..d4b927ca 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. +/// 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`. /// -/// 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 -/// -/// 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; + + 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) } +} - #[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), +impl_strict_from_data_from_capped!(&'r str); + +#[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) + } +} + +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())) + } +} + +impl_strict_from_data_from_capped!(std::borrow::Cow<'_, str>); + +#[crate::async_trait] +impl<'r> FromData<'r> for Capped<&'r [u8]> { + 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(|b| local_cache!(req, b).as_slice()); + Success(raw) + } +} + +impl_strict_from_data_from_capped!(&'r [u8]); + +#[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 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)), - })) - } -} - -impl<'r, T: FromTransformedData<'r> + 'r> FromTransformedData<'r> for Option { - type Error = T::Error; - type Owned = T::Owned; - type Borrowed = T::Borrowed; - - #[inline(always)] - fn transform(r: &'r Request<'_>, d: Data) -> TransformFuture<'r, Self::Owned, Self::Error> { - T::transform(r, d) - } - - #[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), - })) - } -} - -#[cfg(debug_assertions)] -use crate::data::ByteUnit; - -#[crate::async_trait] -#[cfg(debug_assertions)] -impl FromData for String { - 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)), + Forward(d) => Forward(d), } } } #[crate::async_trait] -#[cfg(debug_assertions)] -impl FromData for Vec { - type Error = std::io::Error; +impl<'r, T: FromData<'r>> FromData<'r> for Option { + type Error = std::convert::Infallible; - #[inline(always)] - async fn from_data(_: &Request<'_>, data: Data) -> Outcome { - use tokio::io::AsyncReadExt; - - 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 db2a3379..e1dbe113 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 e93d4337..fd90316e 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 00000000..d360a394 --- /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 66e60df1..c5a71673 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 d851a507..111deb95 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 AsyncReadBody { - pub fn empty() -> Self { - Self { inner: hyper::Body::empty(), state: State::Done } - } -} - -impl From for AsyncReadBody { - fn from(body: hyper::Body) -> Self { - Self { inner: body, state: State::Pending } - } -} - -impl AsyncRead for AsyncReadBody { - fn poll_read( - mut 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(())), - } +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, } } } + +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 Chain { + pub(crate) fn new(first: T, second: U) -> Self { + Self { first, second, done_first: false } + } +} + +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( + self: Pin<&mut Self>, + cx: &mut Context<'_>, + buf: &mut ReadBuf<'_>, + ) -> Poll> { + 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 76482d5a..0f4c29e5 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 00000000..425b7557 --- /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 00000000..3a18e514 --- /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 00000000..d08ea535 --- /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 00000000..6d80ba8a --- /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 00000000..d68c797f --- /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 00000000..4df742d0 --- /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 00000000..3a33090b --- /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 00000000..83f0c962 --- /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 00000000..9df4d335 --- /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 00000000..895e7757 --- /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 00000000..39cced53 --- /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 00000000..e1fd03b3 --- /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 00000000..ffd53f7d --- /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 e9cd26a9..a453e552 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 b16dd1fc..2fbd0f75 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 3e825d57..70f1cffb 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 3e3810c1..b0e23e6b 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 a707b735..00000000 --- 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 5b91e648..00000000 --- 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 f1d5c635..00000000 --- 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 5ecd0fb0..00000000 --- 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 60b47337..00000000 --- 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 04e49c10..00000000 --- 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 cd895871..00000000 --- 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 457726ff..00f9a168 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; + type Error = &'a str; #[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; - - #[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 69f7316d..562419e6 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 94a6d43a..7c807a66 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 611bf5c7..00000000 --- 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 1327620b..e9069864 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}; + /// # 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); /// - /// # 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()); + /// let a_seq = req.query_value::>("a").unwrap(); + /// assert_eq!(a_seq.unwrap(), ["apple", "aardvark"]); + /// }); /// - /// req.get_query_value(key) - /// .and_then(|r| r.ok()) - /// .unwrap_or("n/a".into()) + /// #[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 8ff46e36..6225217a 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 198b5994..fccbad35 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 f6a7b2b1..e810ceeb 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 342c5590..fa20d64d 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 5b2655d8..23d4682b 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 df343a85..aec4b3fe 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 da0bf51d..a25125ef 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 8f2d270d..ca7e8420 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(); + 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(Ok(method)) = method { - req._set_method(method); - } + 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 811b46ed..25a9947c 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 ee7f2a15..f3f46336 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 00000000..2be27b40 --- /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 4eca7a2f..44f3e9f5 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 00000000..9438c438 --- /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 c60b5224..8c876e23 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 8a2c7960..7adb16e0 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 bb6e29a5..fbcbd65c 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 253e8545..6b4ed294 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 b56a98e6..eaebe0a6 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 00000000..1d8d9ba9 --- /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 04fe1b72..37f8bbcf 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 ba972174..ea694f3c 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 d2f14801..10f0c10f 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 2a5481b3..00000000 --- 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 7fc87cf9..00000000 --- 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 e1be14f4..00000000 --- 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 b253ffef..00000000 --- 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 105ca041..00000000 --- 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 66bf2339..00000000 --- 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 41ee2fdc..2c9ebe04 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 00000000..2bb2fabc --- /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 00000000..0ed253f9 --- /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 00000000..7acf70a9 --- /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 00000000..4e43a8a8 --- /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 00000000..d7f8a05f --- /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 00000000..cd3addb8 --- /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 cdcc07b7..72307c08 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 0d210d84..203605d0 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 286c910b..f58c8331 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 aa16a3bb..2a67bf6a 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 d975a699..7e33f498 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 f3b13c1a..5835139c 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 c3ec2032..25c9fc02 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)) + } + + pub fn file_path(&self) -> PathBuf { + let root = concat!(env!("CARGO_MANIFEST_DIR"), "/", "upload"); + Path::new(root).join(self.0.as_ref()) } } -impl<'a> fmt::Display for PasteID<'a> { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "{}", self.0) - } -} - -/// 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 154e4147..ec1a282c 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 25d26a53..82f1aa53 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 22f971a6..49ffd6f4 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 13ec8e3e..b3757609 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 50a461eb..996534a5 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 cc18988d..5b7d4bdd 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 b5be6487..31417195 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 e278bd76..05109c93 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 b1ac1fcb..59d79206 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 5cf96e17..a6d198a1 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,199 +594,12 @@ Any type that implements [`FromData`] is also known as _a data guard_. [`FromData`]: @api/rocket/data/trait.FromData.html -### 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: - -```rust -# #[macro_use] extern crate rocket; -# fn main() {} - -use rocket::request::Form; - -#[derive(FromForm)] -struct Task { - complete: bool, - description: String, -} - -#[post("/todo", data = "")] -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: - -```rust -# #[macro_use] extern crate rocket; -# fn main() {} - -# use rocket::request::Form; -# #[derive(FromForm)] struct Task { complete: bool, description: String, } - -#[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 - -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`. - -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`. - -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: - -```rust -# #[macro_use] extern crate rocket; -# fn main() {} - -use rocket::request::LenientForm; - -#[derive(FromForm)] -struct Task { - /* .. */ - # complete: bool, - # description: String, -} - -#[post("/todo", data = "")] -fn new(task: LenientForm) { /* .. */ } -``` - -[`LenientForm`]: @api/rocket/request/struct.LenientForm.html - -#### 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. - -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: - -```rust -# #[macro_use] extern crate rocket; -# fn main() {} - -#[derive(FromForm)] -struct External { - #[form(field = "type")] - api_type: String -} -``` - -Rocket will then match the form field named `type` to the structure field named -`api_type` automatically. - -#### Field 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: - -```rust -# #[macro_use] extern crate rocket; -# fn main() {} - -use rocket::http::RawStr; -use rocket::request::FromFormValue; - -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), - } - } -} - -#[derive(FromForm)] -struct Person { - age: AdultAge -} -``` - -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: - -```rust -# #[macro_use] extern crate rocket; -# fn main() {} - -# type AdultAge = usize; - -#[derive(FromForm)] -struct Person { - age: Option -} -``` - -The `FromFormValue` trait can also be derived for enums with nullary fields: - -```rust -# #[macro_use] extern crate rocket; -# fn main() {} - -#[derive(FromFormValue)] -enum MyValue { - First, - Second, - Third, -} -``` - -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. - -The [form validation](@example/form_validation) and [form kitchen -sink](@example/form_kitchen_sink) examples provide further illustrations. - ### JSON -Handling JSON data is no harder: simply use the -[`Json`](@api/rocket_contrib/json/struct.Json.html) type from -[`rocket_contrib`]: +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; @@ -949,41 +619,56 @@ struct Task { fn new(task: Json) { /* .. */ } ``` -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. +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 out to a file. Rocket makes this as simple as +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; -# fn main() {} + +use rocket::tokio; use rocket::data::{Data, ToByteUnit}; -use rocket::response::Debug; -#[post("/upload", format = "plain", data = "")] -async fn upload(data: Data) -> Result> { - let bytes_written = data.open(128.kibibytes()) - .stream_to_file("/tmp/upload.txt") +#[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(bytes_written.to_string()) + Ok(()) } ``` -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. +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. @@ -993,23 +678,954 @@ simple! See the [GitHub example code](@example/raw_upload) for the full crate. [`ToByteUnit`](@api/rocket/data/trait.ToByteUnit.html) trait makes specifying such a value as idiomatic as `128.kibibytes()`. -## Async Routes +## Forms -Rocket makes it easy to use `async/await` in routes. +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 `type`, a text field. You can easily handle the form request in +Rocket as follows: ```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) + +use rocket::form::Form; + +#[derive(FromForm)] +struct Task { + complete: bool, + r#type: String, +} + +#[post("/todo", data = "")] +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 [`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::form::Form; +# #[derive(FromForm)] struct Task { complete: bool } + +#[post("/todo", data = "")] +fn new(task: Option>) { /* .. */ } +``` + +[`Form`]: @api/rocket/form/struct.Form.html +[`FromForm`]: @api/rocket/form/trait.FromForm.html + +### Strict Parsing + +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 `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::form::{Form, Strict}; + +#[derive(FromForm)] +struct Task { + /* .. */ + # complete: bool, + # description: String, +} + +#[post("/todo", data = "")] +fn new(task: Form>) { /* .. */ } +``` + +[`Form>`]: @api/rocket/form/struct.Strict.html + +### 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 `#[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 +`first-Name` which you'd like to write as `first_name` in Rust. Field renaming +helps: + +```rust +# #[macro_use] extern crate rocket; +# fn main() {} + +#[derive(FromForm)] +struct External { + #[field(name = "first-Name")] + first_name: String } ``` -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. +Rocket will then match the form field named `first-Name` to the structure field +named `first_name`. + +### Ad-Hoc Validation + +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; + +#[derive(FromForm)] +struct Person { + #[field(validate = range(21..))] + age: u16 +} +``` + +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. + +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`: + +```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 { + name: String +} + +#[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: + + * `owner.name` - string + * `pet.name` - string + * `pet.good_pet` - boolean + +Such a form, URL-encoded, may look like: + +```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, } + +# 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 + +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 { + name: String, + age: usize +} +``` + +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 +# 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: + +```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 +} +``` + +! warning: The `HashMap` key type, here `Person`, must implement `Eq + Hash`. + +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: + + * `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]`. + +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 +# 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 }, + } +} +# }; +``` + +### Arbitrary Collections + +_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 +} + +# type Foo = +HashMap>, HashMap> +# ; +# /* +|-[k:$k1]-----------|------|------| |-[$k1]-----------------| + |---[$i]-------|------|------| |-[k:$j]*| + |-[k:$k2]|------| ~~[$j]~~|name*| + |-name*| ~~[$j]~~|age-*| + |-age*-| + |~~~~~~~~~~~~~~~|v:$k2*| +# */ +``` + +! warning: The `BTreeMap` key type, here `Person`, must implement `Ord`. + +As illustrated above with `*` marking terminals, we need the following form +fields for this structure: + + * `[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 + +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 +# 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 } + } +} +# }; +``` + +### Context + +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; + +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"); +} +``` + +`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. + +[`Contextual`]: @api/rocket/form/struct.Contextual.html +[`Context`]: @api/rocket/form/struct.Context.html +[forms example]: @example/forms + +## 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!" +} + +// 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 + +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; + +#[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(); +``` + +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 4b972890..2baddcf9 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; +use std::path::PathBuf; -# #[derive(FromForm, UriDisplayQuery)] -# struct UserDetails<'r> { age: Option, nickname: &'r RawStr, } +#[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 5aeae898..a159790f 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 b6a0acd2..bf59fd61 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 17c9d60d..a9a7350c 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 + }); } -#[cfg(any(test, doctest))] -mod readme { - doc_comment::doctest!("../../../README.md", readme); +#[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"); + )*); +} + +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() }