From c17cc9d23f76181543d74bf48b49ac5b2d524103 Mon Sep 17 00:00:00 2001 From: Dimitri Sabadie Date: Mon, 9 May 2022 09:42:59 +0200 Subject: [PATCH 001/166] Fix typo in 'Rocket' docs: iterior -> interior. --- core/lib/src/rocket.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/lib/src/rocket.rs b/core/lib/src/rocket.rs index 338c8a7bf4..a48d2989b5 100644 --- a/core/lib/src/rocket.rs +++ b/core/lib/src/rocket.rs @@ -42,7 +42,7 @@ use crate::log::PaintExt; /// * **Ignite**: _verification and finalization of configuration_ /// /// An instance in the [`Ignite`] phase is in its final configuration, -/// available via [`Rocket::config()`]. Barring user-supplied iterior +/// available via [`Rocket::config()`]. Barring user-supplied interior /// mutation, application state is guaranteed to remain unchanged beyond this /// point. An instance in the ignite phase can be launched into orbit to serve /// requests via [`Rocket::launch()`]. From 8237ca22e10bd522d11113167d1f84926d457ba6 Mon Sep 17 00:00:00 2001 From: Sergio Benitez Date: Mon, 9 May 2022 16:08:07 -0500 Subject: [PATCH 002/166] Fix minor typos in rc.2 release docs. --- CHANGELOG.md | 4 ++-- site/news/2022-05-09-version-0.5-rc.2.md | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 75b71a77a3..f94f23b340 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,11 +1,11 @@ -# Version 0.5.0-rc.2 (May 9, 2022) +# Version 0.5.0-rc.2 (May 09, 2022) ## Major Features and Improvements * Introduced [`rocket_db_pools`] for asynchronous database pooling. * Introduced support for [mutual TLS] and client [`Certificate`]s. * Added a [`local_cache_once!`] macro for request-local storage. - * Added a [v0.4 to v0.5 migration guide] and [FAQ] the Rocket's website. + * Added a [v0.4 to v0.5 migration guide] and [FAQ] to Rocket's website. * Introduced [shutdown fairings]. ## Breaking Changes diff --git a/site/news/2022-05-09-version-0.5-rc.2.md b/site/news/2022-05-09-version-0.5-rc.2.md index dd7596c862..1186d2a0bd 100644 --- a/site/news/2022-05-09-version-0.5-rc.2.md +++ b/site/news/2022-05-09-version-0.5-rc.2.md @@ -1,4 +1,4 @@ -# Rocket 2nd v0.5 Release Candidate +# Rocket's 2nd v0.5 Release Candidate

', which was overly conservative. This change, in effect, marks only 'Rocket' 'must_use', which is a much more precise implementation of the intended safety guard. --- core/lib/src/rocket.rs | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/core/lib/src/rocket.rs b/core/lib/src/rocket.rs index a48d2989b5..e91881250b 100644 --- a/core/lib/src/rocket.rs +++ b/core/lib/src/rocket.rs @@ -133,7 +133,6 @@ use crate::log::PaintExt; /// rocket::build() /// } /// ``` -#[must_use] pub struct Rocket(pub(crate) P::State); impl Rocket { @@ -152,6 +151,7 @@ impl Rocket { /// rocket::build() /// } /// ``` + #[must_use] #[inline(always)] pub fn build() -> Self { Rocket::custom(Config::figment()) @@ -178,6 +178,7 @@ impl Rocket { /// rocket::custom(figment) /// } /// ``` + #[must_use] pub fn custom(provider: T) -> Self { // We initialize the logger here so that logging from fairings and so on // are visible; we use the final config to set a max log-level in ignite @@ -235,6 +236,7 @@ impl Rocket { /// # Ok(()) /// # }); /// ``` + #[must_use] pub fn configure(mut self, provider: T) -> Self { self.figment = Figment::from(provider); self @@ -334,6 +336,7 @@ impl Rocket { /// rocket::build().mount("/hello", vec![hi_route]) /// } /// ``` + #[must_use] #[track_caller] pub fn mount<'a, B, R>(self, base: B, routes: R) -> Self where B: TryInto> + Clone + fmt::Display, @@ -373,6 +376,7 @@ impl Rocket { /// rocket::build().register("/", catchers![internal_error, not_found]) /// } /// ``` + #[must_use] pub fn register<'a, B, C>(self, base: B, catchers: C) -> Self where B: TryInto> + Clone + fmt::Display, B::Error: fmt::Display, @@ -424,6 +428,7 @@ impl Rocket { /// .mount("/", routes![int, string]) /// } /// ``` + #[must_use] pub fn manage(self, state: T) -> Self where T: Send + Sync + 'static { @@ -457,6 +462,7 @@ impl Rocket { /// }))) /// } /// ``` + #[must_use] pub fn attach(mut self, fairing: F) -> Self { self.fairings.add(Box::new(fairing)); self From d92b7249cb039a1e398fb29bba42345a39e19ab4 Mon Sep 17 00:00:00 2001 From: Sergio Benitez Date: Thu, 19 May 2022 14:15:59 -0700 Subject: [PATCH 009/166] Update UI tests for latest stable rustc. --- core/codegen/tests/ui-fail-stable/typed-uri-bad-type.stderr | 6 ++++++ .../tests/ui-fail-stable/typed-uris-bad-params.stderr | 6 ++++++ 2 files changed, 12 insertions(+) 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 f56bbf6908..1a1715ca6b 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 @@ -252,3 +252,9 @@ error[E0271]: type mismatch resolving `>::E | 22 | fn optionals(id: Option, name: Result) { } | ^^^^^^ expected enum `Infallible`, found `&str` + +error[E0271]: type mismatch resolving `>::Error == &str` + --> tests/ui-fail-stable/typed-uri-bad-type.rs:22:37 + | +22 | fn optionals(id: Option, name: Result) { } + | ^^^^^^ expected `&str`, found enum `Infallible` diff --git a/core/codegen/tests/ui-fail-stable/typed-uris-bad-params.stderr b/core/codegen/tests/ui-fail-stable/typed-uris-bad-params.stderr index d5757f6177..a0fe694267 100644 --- a/core/codegen/tests/ui-fail-stable/typed-uris-bad-params.stderr +++ b/core/codegen/tests/ui-fail-stable/typed-uris-bad-params.stderr @@ -277,3 +277,9 @@ error[E0271]: type mismatch resolving `>::E | 15 | fn optionals(id: Option, name: Result) { } | ^^^^^^ expected enum `Infallible`, found `&str` + +error[E0271]: type mismatch resolving `>::Error == &str` + --> tests/ui-fail-stable/typed-uris-bad-params.rs:15:37 + | +15 | fn optionals(id: Option, name: Result) { } + | ^^^^^^ expected `&str`, found enum `Infallible` From f21da79f44c773e2761fe7c9aae3c008c999e62d Mon Sep 17 00:00:00 2001 From: Matthew Pomes Date: Wed, 18 May 2022 22:39:51 -0500 Subject: [PATCH 010/166] Make 'form::ErrorKind' 'From' impl const generic. Converts an older style array impl to one that uses const generics, allowing any array length, not just a few sizes. --- core/lib/src/form/error.rs | 15 +++++---------- 1 file changed, 5 insertions(+), 10 deletions(-) diff --git a/core/lib/src/form/error.rs b/core/lib/src/form/error.rs index 156ad1283e..965eb2fc04 100644 --- a/core/lib/src/form/error.rs +++ b/core/lib/src/form/error.rs @@ -947,18 +947,13 @@ impl From<(Option, Option)> for ErrorKind<'_> { } } -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<'a, 'v: 'a, const N: usize> From<&'static [Cow<'v, str>; N]> for ErrorKind<'a> { + fn from(choices: &'static [Cow<'v, str>; N]) -> 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) => ( From 4827948401d86241ac6c5c3136e0e77c3b6d0474 Mon Sep 17 00:00:00 2001 From: Sergio Benitez Date: Thu, 19 May 2022 18:23:55 -0700 Subject: [PATCH 011/166] Use '-q' (quiet) when running tests in CI. --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 6f183ce45d..e2fdaa7976 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -79,5 +79,5 @@ jobs: key: ${{ matrix.test.name }} - name: Run Tests - run: ./scripts/test.sh ${{ matrix.test.flag }} + run: ./scripts/test.sh ${{ matrix.test.flag }} -q shell: bash From 9b3c83eb70a4b1856625d0ebdce4cc0ce64651e2 Mon Sep 17 00:00:00 2001 From: Sergio Benitez Date: Fri, 20 May 2022 16:04:23 -0700 Subject: [PATCH 012/166] Add 'Metadata::render()': direct template render. Resolves #1177. --- contrib/dyn_templates/src/lib.rs | 32 +++++++++------- contrib/dyn_templates/src/metadata.rs | 48 +++++++++++++++++++++++- contrib/dyn_templates/tests/templates.rs | 45 +++++++++++++++------- 3 files changed, 95 insertions(+), 30 deletions(-) diff --git a/contrib/dyn_templates/src/lib.rs b/contrib/dyn_templates/src/lib.rs index 5410beb3d9..2b6519b462 100644 --- a/contrib/dyn_templates/src/lib.rs +++ b/contrib/dyn_templates/src/lib.rs @@ -115,10 +115,14 @@ //! //! ## Rendering //! -//! Templates are rendered with the `render` method. The method takes in the -//! name of a template and a context to render the template with. The context -//! can be any type that implements [`Serialize`] and would serialize to an -//! `Object` value. The [`context!`] macro can also be used to create inline +//! Templates are typically rendered indirectly via [`Template::render()`] which +//! returns a `Template` responder which renders the template at response time. +//! To render a template directly into a `String`, use [`Metadata::render()`] +//! instead. +//! +//! Both methods take in a template name and context to use while rendering. The +//! context can be any [`Serialize`] type that serializes to an `Object` (a +//! dictionary) value. The [`context!`] macro may be used to create inline //! `Serialize`-able context objects. //! //! ## Automatic Reloading @@ -308,6 +312,8 @@ impl Template { /// be of any type that implements `Serialize`, such as `HashMap` or a /// custom `struct`. /// + /// To render a template directly into a string, use [`Metadata::render()`]. + /// /// # Examples /// /// Using the `context` macro: @@ -383,14 +389,14 @@ impl Template { None })?; - Template::render(name, context).finalize(&ctxt).ok().map(|v| v.0) + Template::render(name, context).finalize(&ctxt).ok().map(|v| v.1) } /// Actually render this template given a template context. This method is /// called by the `Template` `Responder` implementation as well as /// `Template::show()`. #[inline(always)] - fn finalize(self, ctxt: &Context) -> Result<(String, ContentType), Status> { + fn finalize(self, ctxt: &Context) -> Result<(ContentType, String), Status> { let name = &*self.name; let info = ctxt.templates.get(name).ok_or_else(|| { let ts: Vec<_> = ctxt.templates.keys().map(|s| s.as_str()).collect(); @@ -410,7 +416,7 @@ impl Template { Status::InternalServerError })?; - Ok((string, info.data_type.clone())) + Ok((info.data_type.clone(), string)) } } @@ -419,18 +425,16 @@ impl Template { /// rendering fails, an `Err` of `Status::InternalServerError` is returned. impl<'r> Responder<'r, 'static> for Template { fn respond_to(self, req: &'r Request<'_>) -> response::Result<'static> { - let (render, content_type) = { - let ctxt = req.rocket().state::().ok_or_else(|| { + let ctxt = req.rocket() + .state::() + .ok_or_else(|| { error_!("Uninitialized template context: missing fairing."); info_!("To use templates, you must attach `Template::fairing()`."); info_!("See the `Template` documentation for more information."); Status::InternalServerError - })?.context(); - - self.finalize(&ctxt)? - }; + })?; - (content_type, render).respond_to(req) + self.finalize(&ctxt.context())?.respond_to(req) } } diff --git a/contrib/dyn_templates/src/metadata.rs b/contrib/dyn_templates/src/metadata.rs index 019a7c25f0..feceab36d3 100644 --- a/contrib/dyn_templates/src/metadata.rs +++ b/contrib/dyn_templates/src/metadata.rs @@ -1,8 +1,11 @@ +use std::borrow::Cow; + use rocket::{Request, Rocket, Ignite, Sentinel}; -use rocket::http::Status; +use rocket::http::{Status, ContentType}; use rocket::request::{self, FromRequest}; +use rocket::serde::Serialize; -use crate::context::ContextManager; +use crate::{Template, context::ContextManager}; /// Request guard for dynamically querying template metadata. /// @@ -77,6 +80,47 @@ impl Metadata<'_> { pub fn reloading(&self) -> bool { self.0.is_reloading() } + + /// Directly render the template named `name` with the context `context` + /// into a `String`. Also returns the template's detected `ContentType`. See + /// [`Template::render()`] for more details on rendering. + /// + /// # Examples + /// + /// ```rust + /// # #[macro_use] extern crate rocket; + /// use rocket::http::ContentType; + /// use rocket_dyn_templates::{Metadata, Template, context}; + /// + /// #[get("/")] + /// fn send_email(metadata: Metadata) -> Option<()> { + /// let (mime, string) = metadata.render("email", context! { + /// field: "Hello, world!" + /// })?; + /// + /// # /* + /// send_email(mime, string).await?; + /// # */ + /// Some(()) + /// } + /// + /// #[get("/")] + /// fn raw_render(metadata: Metadata) -> Option<(ContentType, String)> { + /// metadata.render("index", context! { field: "Hello, world!" }) + /// } + /// + /// // Prefer the following, however, which is nearly identical but pithier: + /// + /// #[get("/")] + /// fn render() -> Template { + /// Template::render("index", context! { field: "Hello, world!" }) + /// } + /// ``` + pub fn render(&self, name: S, context: C) -> Option<(ContentType, String)> + where S: Into>, C: Serialize + { + Template::render(name.into(), context).finalize(&self.0.context()).ok() + } } impl Sentinel for Metadata<'_> { diff --git a/contrib/dyn_templates/tests/templates.rs b/contrib/dyn_templates/tests/templates.rs index 5ba42ea261..c73927f94a 100644 --- a/contrib/dyn_templates/tests/templates.rs +++ b/contrib/dyn_templates/tests/templates.rs @@ -10,10 +10,7 @@ use rocket_dyn_templates::{Template, Metadata, context}; #[get("//")] fn template_check(md: Metadata<'_>, engine: &str, name: &str) -> Option<()> { - match md.contains_template(&format!("{}/{}", engine, name)) { - true => Some(()), - false => None - } + md.contains_template(&format!("{}/{}", engine, name)).then(|| ()) } #[get("/is_reloading")] @@ -207,28 +204,37 @@ fn test_context_macro() { mod tera_tests { use super::*; use std::collections::HashMap; - use rocket::http::Status; - use rocket::local::blocking::Client; + use rocket::http::{ContentType, Status}; + use rocket::request::FromRequest; const UNESCAPED_EXPECTED: &'static str = "\nh_start\ntitle: _test_\nh_end\n\n\n diff --git a/examples/upgrade/src/main.rs b/examples/upgrade/src/main.rs new file mode 100644 index 0000000000..fd3589459a --- /dev/null +++ b/examples/upgrade/src/main.rs @@ -0,0 +1,28 @@ +#[macro_use] extern crate rocket; + +use rocket::futures::{SinkExt, StreamExt}; +use rocket::response::content::RawHtml; + +mod ws; + +#[get("/")] +fn index() -> RawHtml<&'static str> { + RawHtml(include_str!("../index.html")) +} + +#[get("/echo")] +fn echo(ws: ws::WebSocket) -> ws::Channel { + ws.channel(|mut stream| Box::pin(async move { + while let Some(message) = stream.next().await { + let _ = stream.send(message?).await; + } + + Ok(()) + })) +} + +#[launch] +fn rocket() -> _ { + rocket::build() + .mount("/", routes![index, echo]) +} diff --git a/examples/upgrade/src/ws.rs b/examples/upgrade/src/ws.rs new file mode 100644 index 0000000000..c5342089d8 --- /dev/null +++ b/examples/upgrade/src/ws.rs @@ -0,0 +1,67 @@ +use std::io; + +use rocket::{Request, response}; +use rocket::data::{IoHandler, IoStream}; +use rocket::request::{FromRequest, Outcome}; +use rocket::response::{Responder, Response}; +use rocket::futures::future::BoxFuture; + +use tokio_tungstenite::WebSocketStream; +use tokio_tungstenite::tungstenite::handshake::derive_accept_key; +use tokio_tungstenite::tungstenite::protocol::Role; +use tokio_tungstenite::tungstenite::error::{Result, Error}; + +pub struct WebSocket(String); + +#[rocket::async_trait] +impl<'r> FromRequest<'r> for WebSocket { + type Error = std::convert::Infallible; + + async fn from_request(req: &'r Request<'_>) -> Outcome { + use rocket::http::uncased::eq; + + let headers = req.headers(); + let is_upgrade = headers.get_one("Connection").map_or(false, |c| eq(c, "upgrade")); + let is_ws = headers.get("Upgrade").any(|p| eq(p, "websocket")); + let is_ws_13 = headers.get_one("Sec-WebSocket-Version").map_or(false, |v| v == "13"); + let key = headers.get_one("Sec-WebSocket-Key").map(|k| derive_accept_key(k.as_bytes())); + match key { + Some(key) if is_upgrade && is_ws && is_ws_13 => Outcome::Success(WebSocket(key)), + Some(_) | None => Outcome::Forward(()) + } + } +} + +pub struct Channel { + ws: WebSocket, + handler: Box) -> BoxFuture<'static, Result<()>> + Send>, +} + +impl WebSocket { + pub fn channel(self, handler: F) -> Channel + where F: FnMut(WebSocketStream) -> BoxFuture<'static, Result<()>> + { + Channel { ws: self, handler: Box::new(handler), } + } +} + +impl<'r> Responder<'r, 'static> for Channel { + fn respond_to(self, _: &'r Request<'_>) -> response::Result<'static> { + Response::build() + .raw_header("Sec-Websocket-Version", "13") + .raw_header("Sec-WebSocket-Accept", self.ws.0.clone()) + .upgrade("websocket", self) + .ok() + } +} + +#[rocket::async_trait] +impl IoHandler for Channel { + async fn io(&mut self, io: IoStream) -> io::Result<()> { + let stream = WebSocketStream::from_raw_socket(io, Role::Server, None).await; + (self.handler)(stream).await.map_err(|e| match e { + Error::Io(e) => e, + other => io::Error::new(io::ErrorKind::Other, other) + }) + } +} From 2a63b1a41f5c3d7490829a481474ebd7a9e4812a Mon Sep 17 00:00:00 2001 From: Sergio Benitez Date: Thu, 30 Mar 2023 12:46:34 -0700 Subject: [PATCH 102/166] Downgrade I/O stream closing to warning. Since active I/O streams will be closed by graceful shutdown, an error, as was previously emitted, was necessarily alarmist. This reduces the severity of the log message to a warning. --- core/lib/src/server.rs | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/core/lib/src/server.rs b/core/lib/src/server.rs index 9a473a2c56..213a32147d 100644 --- a/core/lib/src/server.rs +++ b/core/lib/src/server.rs @@ -173,22 +173,26 @@ impl Rocket { async fn handle_upgrade<'r>( &self, mut response: Response<'r>, - protocol: uncased::Uncased<'r>, + proto: uncased::Uncased<'r>, mut io_handler: Box, pending_upgrade: hyper::upgrade::OnUpgrade, tx: oneshot::Sender>, ) { - info_!("Upgrading connection to {}.", Paint::white(&protocol)); + info_!("Upgrading connection to {}.", Paint::white(&proto).bold()); response.set_status(Status::SwitchingProtocols); response.set_raw_header("Connection", "Upgrade"); - response.set_raw_header("Upgrade", protocol.into_cow()); + response.set_raw_header("Upgrade", proto.clone().into_cow()); self.send_response(response, tx).await; match pending_upgrade.await { Ok(io_stream) => { info_!("Upgrade successful."); if let Err(e) = io_handler.io(io_stream.into()).await { - error!("Upgraded I/O handler failed: {}", e); + if e.kind() == io::ErrorKind::BrokenPipe { + warn!("Upgraded {} I/O handler was closed.", proto); + } else { + error!("Upgraded {} I/O handler failed: {}", proto, e); + } } }, Err(e) => { From 2abddd923e36b49b01821f09f4dc82ee19d69dcc Mon Sep 17 00:00:00 2001 From: Sergio Benitez Date: Thu, 30 Mar 2023 12:48:20 -0700 Subject: [PATCH 103/166] Implement stream websocket API in upgrade example. --- examples/upgrade/index.html | 69 ---------------------- examples/upgrade/src/main.rs | 25 ++++---- examples/upgrade/src/ws.rs | 92 +++++++++++++++++++++++++++--- examples/upgrade/static/index.html | 74 ++++++++++++++++++++++++ 4 files changed, 172 insertions(+), 88 deletions(-) delete mode 100644 examples/upgrade/index.html create mode 100644 examples/upgrade/static/index.html diff --git a/examples/upgrade/index.html b/examples/upgrade/index.html deleted file mode 100644 index 0293461c1c..0000000000 --- a/examples/upgrade/index.html +++ /dev/null @@ -1,69 +0,0 @@ - - - - WebSocket client test - - - -

WebSocket Client Test

-
- - - diff --git a/examples/upgrade/src/main.rs b/examples/upgrade/src/main.rs index fd3589459a..b23c2ca849 100644 --- a/examples/upgrade/src/main.rs +++ b/examples/upgrade/src/main.rs @@ -1,18 +1,13 @@ #[macro_use] extern crate rocket; +use rocket::fs::{self, FileServer}; use rocket::futures::{SinkExt, StreamExt}; -use rocket::response::content::RawHtml; mod ws; -#[get("/")] -fn index() -> RawHtml<&'static str> { - RawHtml(include_str!("../index.html")) -} - -#[get("/echo")] -fn echo(ws: ws::WebSocket) -> ws::Channel { - ws.channel(|mut stream| Box::pin(async move { +#[get("/echo/manual")] +fn echo_manual<'r>(ws: ws::WebSocket) -> ws::Channel<'r> { + ws.channel(move |mut stream| Box::pin(async move { while let Some(message) = stream.next().await { let _ = stream.send(message?).await; } @@ -21,8 +16,18 @@ fn echo(ws: ws::WebSocket) -> ws::Channel { })) } +#[get("/echo")] +fn echo_stream<'r>(ws: ws::WebSocket) -> ws::Stream!['r] { + ws::stream! { ws => + for await message in ws { + yield message?; + } + } +} + #[launch] fn rocket() -> _ { rocket::build() - .mount("/", routes![index, echo]) + .mount("/", routes![echo_manual, echo_stream]) + .mount("/", FileServer::from(fs::relative!("static"))) } diff --git a/examples/upgrade/src/ws.rs b/examples/upgrade/src/ws.rs index c5342089d8..7122a80121 100644 --- a/examples/upgrade/src/ws.rs +++ b/examples/upgrade/src/ws.rs @@ -1,15 +1,19 @@ use std::io; +use rocket::futures::{StreamExt, SinkExt}; +use rocket::futures::stream::SplitStream; use rocket::{Request, response}; use rocket::data::{IoHandler, IoStream}; use rocket::request::{FromRequest, Outcome}; use rocket::response::{Responder, Response}; -use rocket::futures::future::BoxFuture; +use rocket::futures::{self, future::BoxFuture}; use tokio_tungstenite::WebSocketStream; use tokio_tungstenite::tungstenite::handshake::derive_accept_key; use tokio_tungstenite::tungstenite::protocol::Role; -use tokio_tungstenite::tungstenite::error::{Result, Error}; + +pub use tokio_tungstenite::tungstenite::error::{Result, Error}; +pub use tokio_tungstenite::tungstenite::Message; pub struct WebSocket(String); @@ -32,21 +36,45 @@ impl<'r> FromRequest<'r> for WebSocket { } } -pub struct Channel { +pub struct Channel<'r> { + ws: WebSocket, + handler: Box) -> BoxFuture<'r, Result<()>> + Send + 'r>, +} + +pub struct MessageStream<'r, S> { ws: WebSocket, - handler: Box) -> BoxFuture<'static, Result<()>> + Send>, + handler: Box>) -> S + Send + 'r> } impl WebSocket { - pub fn channel(self, handler: F) -> Channel - where F: FnMut(WebSocketStream) -> BoxFuture<'static, Result<()>> + pub fn channel<'r, F: Send + 'r>(self, handler: F) -> Channel<'r> + where F: FnMut(WebSocketStream) -> BoxFuture<'r, Result<()>> + 'r { Channel { ws: self, handler: Box::new(handler), } } + + pub fn stream<'r, F, S>(self, stream: F) -> MessageStream<'r, S> + where F: FnMut(SplitStream>) -> S + Send + 'r, + S: futures::Stream> + Send + 'r + { + MessageStream { ws: self, handler: Box::new(stream), } + } +} + +impl<'r, 'o: 'r> Responder<'r, 'o> for Channel<'o> { + fn respond_to(self, _: &'r Request<'_>) -> response::Result<'o> { + Response::build() + .raw_header("Sec-Websocket-Version", "13") + .raw_header("Sec-WebSocket-Accept", self.ws.0.clone()) + .upgrade("websocket", self) + .ok() + } } -impl<'r> Responder<'r, 'static> for Channel { - fn respond_to(self, _: &'r Request<'_>) -> response::Result<'static> { +impl<'r, 'o: 'r, S> Responder<'r, 'o> for MessageStream<'o, S> + where S: futures::Stream> + Send + 'o +{ + fn respond_to(self, _: &'r Request<'_>) -> response::Result<'o> { Response::build() .raw_header("Sec-Websocket-Version", "13") .raw_header("Sec-WebSocket-Accept", self.ws.0.clone()) @@ -56,7 +84,7 @@ impl<'r> Responder<'r, 'static> for Channel { } #[rocket::async_trait] -impl IoHandler for Channel { +impl IoHandler for Channel<'_> { async fn io(&mut self, io: IoStream) -> io::Result<()> { let stream = WebSocketStream::from_raw_socket(io, Role::Server, None).await; (self.handler)(stream).await.map_err(|e| match e { @@ -65,3 +93,49 @@ impl IoHandler for Channel { }) } } + +#[rocket::async_trait] +impl<'r, S> IoHandler for MessageStream<'r, S> + where S: futures::Stream> + Send + 'r +{ + async fn io(&mut self, io: IoStream) -> io::Result<()> { + let stream = WebSocketStream::from_raw_socket(io, Role::Server, None).await; + let (mut sink, stream) = stream.split(); + let mut stream = std::pin::pin!((self.handler)(stream)); + while let Some(msg) = stream.next().await { + let result = match msg { + Ok(msg) => sink.send(msg).await, + Err(e) => Err(e) + }; + + result.map_err(|e| match e { + Error::Io(e) => e, + other => io::Error::new(io::ErrorKind::Other, other) + })?; + } + + Ok(()) + } +} + +#[macro_export] +macro_rules! Stream { + ($l:lifetime) => ( + $crate::ws::MessageStream<$l, impl rocket::futures::Stream< + Item = $crate::ws::Result<$crate::ws::Message> + > + $l> + ) +} + +#[macro_export] +macro_rules! stream { + ($channel:ident => $($token:tt)*) => ( + let ws: $crate::ws::WebSocket = $channel; + ws.stream(move |$channel| rocket::async_stream::try_stream! { + $($token)* + }) + ) +} + +pub use Stream as Stream; +pub use stream as stream; diff --git a/examples/upgrade/static/index.html b/examples/upgrade/static/index.html new file mode 100644 index 0000000000..f4bf469445 --- /dev/null +++ b/examples/upgrade/static/index.html @@ -0,0 +1,74 @@ + + + + WebSocket Client Test + + + + +

WebSocket Client Test

+
+ + +
+
+ + + From 64a7bfb37c6e3271dd899f5188ce8e4f013ca408 Mon Sep 17 00:00:00 2001 From: Sergio Benitez Date: Thu, 30 Mar 2023 15:09:25 -0700 Subject: [PATCH 104/166] Fix header lookups for connection upgrades. --- core/lib/src/response/response.rs | 24 +++++++++++++++++++++--- core/lib/src/server.rs | 7 ++++++- examples/upgrade/src/ws.rs | 8 ++++++-- 3 files changed, 33 insertions(+), 6 deletions(-) diff --git a/core/lib/src/response/response.rs b/core/lib/src/response/response.rs index d01bc0044a..2e1fdfa8ea 100644 --- a/core/lib/src/response/response.rs +++ b/core/lib/src/response/response.rs @@ -794,11 +794,29 @@ impl<'r> Response<'r> { &self.body } + /// Returns `Ok(Some(_))` if `self` contains a suitable handler for any of + /// the comma-separated protocols any of the strings in `I`. Returns + /// `Ok(None)` if `self` doesn't support any kind of upgrade. Returns + /// `Err(_)` if `protocols` is non-empty but no match was found in `self`. pub(crate) fn take_upgrade>( &mut self, - mut protocols: I - ) -> Option<(Uncased<'r>, Box)> { - protocols.find_map(|p| self.upgrade.remove_entry(p.as_uncased())) + protocols: I + ) -> Result, Box)>, ()> { + if self.upgrade.is_empty() { + return Ok(None); + } + + let mut protocols = protocols.peekable(); + let have_protocols = protocols.peek().is_some(); + let found = protocols + .flat_map(|v| v.split(',').map(str::trim)) + .find_map(|p| self.upgrade.remove_entry(p.as_uncased())); + + match found { + Some(handler) => Ok(Some(handler)), + None if have_protocols => Err(()), + None => Ok(None) + } } /// Returns the [`IoHandler`] for the protocol `proto`. diff --git a/core/lib/src/server.rs b/core/lib/src/server.rs index 213a32147d..6b315ed07e 100644 --- a/core/lib/src/server.rs +++ b/core/lib/src/server.rs @@ -86,9 +86,14 @@ async fn hyper_service_fn( let token = rocket.preprocess_request(&mut req, &mut data).await; let mut response = rocket.dispatch(token, &req, data).await; let upgrade = response.take_upgrade(req.headers().get("upgrade")); - if let Some((proto, handler)) = upgrade { + if let Ok(Some((proto, handler))) = upgrade { rocket.handle_upgrade(response, proto, handler, pending_upgrade, tx).await; } else { + if upgrade.is_err() { + warn_!("Request wants upgrade but no I/O handler matched."); + info_!("Request is not being upgraded."); + } + rocket.send_response(response, tx).await; } }, diff --git a/examples/upgrade/src/ws.rs b/examples/upgrade/src/ws.rs index 7122a80121..ace8c5faf1 100644 --- a/examples/upgrade/src/ws.rs +++ b/examples/upgrade/src/ws.rs @@ -25,8 +25,12 @@ impl<'r> FromRequest<'r> for WebSocket { use rocket::http::uncased::eq; let headers = req.headers(); - let is_upgrade = headers.get_one("Connection").map_or(false, |c| eq(c, "upgrade")); - let is_ws = headers.get("Upgrade").any(|p| eq(p, "websocket")); + let is_upgrade = headers.get("Connection") + .any(|h| h.split(',').any(|v| eq(v.trim(), "upgrade"))); + + let is_ws = headers.get("Upgrade") + .any(|h| h.split(',').any(|v| eq(v.trim(), "websocket"))); + let is_ws_13 = headers.get_one("Sec-WebSocket-Version").map_or(false, |v| v == "13"); let key = headers.get_one("Sec-WebSocket-Key").map(|k| derive_accept_key(k.as_bytes())); match key { From 1d2cc257dc8598f63a1a9303e2cc37222fe91d24 Mon Sep 17 00:00:00 2001 From: Sergio Benitez Date: Thu, 30 Mar 2023 15:24:18 -0700 Subject: [PATCH 105/166] Preprocess tungstenite 'ConnectionClosed' errors. Tungstenite abuses `Err(ConnectionClosed)` to indicate the non-error condition of a websocket closing. This commit changes error processing such that the error is caught and converted into a successful termination of websocket handling. --- examples/upgrade/src/ws.rs | 24 ++++++++++++++++-------- 1 file changed, 16 insertions(+), 8 deletions(-) diff --git a/examples/upgrade/src/ws.rs b/examples/upgrade/src/ws.rs index ace8c5faf1..483531867f 100644 --- a/examples/upgrade/src/ws.rs +++ b/examples/upgrade/src/ws.rs @@ -87,14 +87,23 @@ impl<'r, 'o: 'r, S> Responder<'r, 'o> for MessageStream<'o, S> } } +/// Returns `Ok(true)` if processing should continue, `Ok(false)` if processing +/// has terminated without error, and `Err(e)` if an error has occurred. +fn handle_result(result: Result<()>) -> io::Result { + match result { + Ok(_) => Ok(true), + Err(Error::ConnectionClosed) => Ok(false), + Err(Error::Io(e)) => Err(e), + Err(e) => Err(io::Error::new(io::ErrorKind::Other, e)) + } +} + #[rocket::async_trait] impl IoHandler for Channel<'_> { async fn io(&mut self, io: IoStream) -> io::Result<()> { let stream = WebSocketStream::from_raw_socket(io, Role::Server, None).await; - (self.handler)(stream).await.map_err(|e| match e { - Error::Io(e) => e, - other => io::Error::new(io::ErrorKind::Other, other) - }) + let result = (self.handler)(stream).await; + handle_result(result).map(|_| ()) } } @@ -112,10 +121,9 @@ impl<'r, S> IoHandler for MessageStream<'r, S> Err(e) => Err(e) }; - result.map_err(|e| match e { - Error::Io(e) => e, - other => io::Error::new(io::ErrorKind::Other, other) - })?; + if !handle_result(result)? { + return Ok(()); + } } Ok(()) From 847e87d5c98fad0c23746a4fd0b34a1238ef5e62 Mon Sep 17 00:00:00 2001 From: Sergio Benitez Date: Fri, 31 Mar 2023 08:34:45 -0700 Subject: [PATCH 106/166] Make 'ws::Stream![]' mean 'ws::Stream!['static]'. This is in line with the stream macros in Rocket core. --- examples/upgrade/src/main.rs | 2 +- examples/upgrade/src/ws.rs | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/examples/upgrade/src/main.rs b/examples/upgrade/src/main.rs index b23c2ca849..bb6b661726 100644 --- a/examples/upgrade/src/main.rs +++ b/examples/upgrade/src/main.rs @@ -17,7 +17,7 @@ fn echo_manual<'r>(ws: ws::WebSocket) -> ws::Channel<'r> { } #[get("/echo")] -fn echo_stream<'r>(ws: ws::WebSocket) -> ws::Stream!['r] { +fn echo_stream(ws: ws::WebSocket) -> ws::Stream!['static] { ws::stream! { ws => for await message in ws { yield message?; diff --git a/examples/upgrade/src/ws.rs b/examples/upgrade/src/ws.rs index 483531867f..a2758977a3 100644 --- a/examples/upgrade/src/ws.rs +++ b/examples/upgrade/src/ws.rs @@ -132,11 +132,12 @@ impl<'r, S> IoHandler for MessageStream<'r, S> #[macro_export] macro_rules! Stream { + () => (Stream!['static]); ($l:lifetime) => ( $crate::ws::MessageStream<$l, impl rocket::futures::Stream< Item = $crate::ws::Result<$crate::ws::Message> > + $l> - ) + ); } #[macro_export] From aa6ad7030ad3b374295ae4a11465a4c14e0dbf0f Mon Sep 17 00:00:00 2001 From: Sergio Benitez Date: Fri, 31 Mar 2023 11:13:40 -0700 Subject: [PATCH 107/166] Allow setting mTLS certificates on local 'Client'. This allows testing with client certificates. Co-authored-by: Brett Buford --- core/http/src/listener.rs | 6 +++++ core/http/src/tls/mod.rs | 2 +- core/http/src/tls/mtls.rs | 2 +- core/lib/src/local/request.rs | 41 ++++++++++++++++++++++++++++++++++- examples/tls/src/tests.rs | 23 +++++++++++++++++++- 5 files changed, 70 insertions(+), 4 deletions(-) diff --git a/core/http/src/listener.rs b/core/http/src/listener.rs index 7258721e3f..e958702e08 100644 --- a/core/http/src/listener.rs +++ b/core/http/src/listener.rs @@ -31,6 +31,12 @@ pub struct CertificateData(pub Vec); #[derive(Clone, Default)] pub struct Certificates(Arc>>); +impl From> for Certificates { + fn from(value: Vec) -> Self { + Certificates(Arc::new(value.into())) + } +} + impl Certificates { /// Set the the raw certificate chain data. Only the first call actually /// sets the data; the remaining do nothing. diff --git a/core/http/src/tls/mod.rs b/core/http/src/tls/mod.rs index b529ee4082..04959ba23e 100644 --- a/core/http/src/tls/mod.rs +++ b/core/http/src/tls/mod.rs @@ -1,8 +1,8 @@ mod listener; -mod util; #[cfg(feature = "mtls")] pub mod mtls; pub use rustls; pub use listener::{TlsListener, Config}; +pub mod util; diff --git a/core/http/src/tls/mtls.rs b/core/http/src/tls/mtls.rs index 2269a3978c..65246d4d89 100644 --- a/core/http/src/tls/mtls.rs +++ b/core/http/src/tls/mtls.rs @@ -62,7 +62,7 @@ pub type Result = std::result::Result; /// * The certificates are active and not yet expired. /// * The client's certificate chain was signed by the CA identified by the /// configured `ca_certs` and with respect to SNI, if any. See [module level -/// docs](self) for configuration details. +/// docs](crate::mtls) for configuration details. /// /// If the client does not present certificates, the guard _forwards_. /// diff --git a/core/lib/src/local/request.rs b/core/lib/src/local/request.rs index 76ce4af359..4ba38634c8 100644 --- a/core/lib/src/local/request.rs +++ b/core/lib/src/local/request.rs @@ -191,8 +191,47 @@ macro_rules! pub_request_impl { self } - /// Sets the body data of the request. + /// Set mTLS client certificates to send along with the request. + /// + /// If the request already contained certificates, they are replaced with + /// thsoe in `reader.` + /// + /// `reader` is expected to be PEM-formatted and contain X509 certificates. + /// If it contains more than one certificate, the entire chain is set on the + /// request. If it contains items other than certificates, the certificate + /// chain up to the first non-certificate item is set on the request. If + /// `reader` is syntactically invalid PEM, certificates are cleared on the + /// request. /// + /// The type `C` can be anything that implements [`std::io::Read`]. This + /// includes: `&[u8]`, `File`, `&File`, `Stdin`, and so on. To read a file + /// in at compile-time, use [`include_bytes!()`]. + /// + /// ```rust + /// use std::fs::File; + /// + #[doc = $import] + /// use rocket::fs::relative; + /// + /// # Client::_test(|_, request, _| { + /// let request: LocalRequest = request; + /// let path = relative!("../../examples/tls/private/ed25519_cert.pem"); + /// let req = request.identity(File::open(path).unwrap()); + /// # }); + /// ``` + #[cfg(feature = "mtls")] + #[cfg_attr(nightly, doc(cfg(feature = "mtls")))] + pub fn identity(mut self, reader: C) -> Self { + use crate::http::{tls::util::load_certs, private::Certificates}; + + let mut reader = std::io::BufReader::new(reader); + let certs = load_certs(&mut reader).map(Certificates::from); + self._request_mut().connection.client_certificates = certs.ok(); + self + } + + /// Sets the body data of the request. + ///core/lib/src/local/request.rs /// # Examples /// /// ```rust diff --git a/examples/tls/src/tests.rs b/examples/tls/src/tests.rs index ae0f12d79b..171b5b5def 100644 --- a/examples/tls/src/tests.rs +++ b/examples/tls/src/tests.rs @@ -1,4 +1,25 @@ +use std::fs::{self, File}; + use rocket::local::blocking::Client; +use rocket::fs::relative; + +#[test] +fn hello_mutual() { + let client = Client::tracked(super::rocket()).unwrap(); + let cert_paths = fs::read_dir(relative!("private")).unwrap() + .map(|entry| entry.unwrap().path().to_string_lossy().into_owned()) + .filter(|path| path.ends_with("_cert.pem") && !path.ends_with("ca_cert.pem")); + + for path in cert_paths { + let response = client.get("/") + .identity(File::open(&path).unwrap()) + .dispatch(); + + let response = response.into_string().unwrap(); + let subject = response.split(']').nth(1).unwrap().trim(); + assert_eq!(subject, "C=US, ST=CA, O=Rocket, CN=localhost"); + } +} #[test] fn hello_world() { @@ -16,6 +37,6 @@ fn hello_world() { let config = rocket::Config::figment().select(profile); let client = Client::tracked(super::rocket().configure(config)).unwrap(); let response = client.get("/").dispatch(); - assert_eq!(response.into_string(), Some("Hello, world!".into())); + assert_eq!(response.into_string().unwrap(), "Hello, world!"); } } From 1a66e50be03366ef36fb4273c2f1ea7f077f52e3 Mon Sep 17 00:00:00 2001 From: Sergio Benitez Date: Fri, 31 Mar 2023 11:40:31 -0700 Subject: [PATCH 108/166] Add Revolt to list of known projects in FAQ. --- site/guide/12-faq.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/site/guide/12-faq.md b/site/guide/12-faq.md index f91aa7a878..1356ef31cf 100644 --- a/site/guide/12-faq.md +++ b/site/guide/12-faq.md @@ -208,6 +208,7 @@ Here are some notable projects and websites in Rocket we're aware of: * [Plume] - Federated Blogging Engine * [Hagrid] - OpenPGP KeyServer ([keys.openpgp.org](https://keys.openpgp.org/)) * [SourceGraph Syntax Highlighter] - Syntax Highlighting API + * [Revolt] - Open source user-first chat platform [Let us know] if you have a notable, public facing application written in Rocket you'd like to see here! @@ -219,6 +220,7 @@ you'd like to see here! [Hagrid]: https://gitlab.com/hagrid-keyserver/hagrid/ [SourceGraph Syntax Highlighter]: https://github.com/sourcegraph/sourcegraph/tree/main/docker-images/syntax-highlighter [Let us know]: https://github.com/SergioBenitez/Rocket/discussions/categories/show-and-tell +[Revolt]: https://github.com/revoltchat/backend From fbb0ace52981fde8825cad08ec42710c4e61c216 Mon Sep 17 00:00:00 2001 From: Sergio Benitez Date: Fri, 31 Mar 2023 12:08:45 -0700 Subject: [PATCH 109/166] Update 'rustls' to 0.21, 'tokio-rustls' to 0.24. --- core/http/Cargo.toml | 4 ++-- core/http/src/tls/listener.rs | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/core/http/Cargo.toml b/core/http/Cargo.toml index 5ad3bfd48f..3e95424587 100644 --- a/core/http/Cargo.toml +++ b/core/http/Cargo.toml @@ -30,8 +30,8 @@ percent-encoding = "2" http = "0.2" time = { version = "0.3", features = ["formatting", "macros"] } indexmap = { version = "1.5.2", features = ["std"] } -rustls = { version = "0.20", optional = true } -tokio-rustls = { version = "0.23.4", optional = true } +rustls = { version = "0.21", optional = true } +tokio-rustls = { version = "0.24", optional = true } rustls-pemfile = { version = "1.0.2", optional = true } tokio = { version = "1.6.1", features = ["net", "sync", "time"] } log = "0.4" diff --git a/core/http/src/tls/listener.rs b/core/http/src/tls/listener.rs index 1dbd676974..f8263c6fbf 100644 --- a/core/http/src/tls/listener.rs +++ b/core/http/src/tls/listener.rs @@ -86,11 +86,11 @@ impl TlsListener { let client_auth = match c.ca_certs { Some(ref mut ca_certs) => match load_ca_certs(ca_certs) { - Ok(ca_roots) if c.mandatory_mtls => AllowAnyAuthenticatedClient::new(ca_roots), - Ok(ca_roots) => AllowAnyAnonymousOrAuthenticatedClient::new(ca_roots), + Ok(ca) if c.mandatory_mtls => AllowAnyAuthenticatedClient::new(ca).boxed(), + Ok(ca) => AllowAnyAnonymousOrAuthenticatedClient::new(ca).boxed(), Err(e) => return Err(io::Error::new(e.kind(), format!("bad CA cert(s): {}", e))), }, - None => NoClientAuth::new(), + None => NoClientAuth::boxed(), }; let mut tls_config = ServerConfig::builder() From 7d895eb9f674ac493942cc2a56dea36556aa87ac Mon Sep 17 00:00:00 2001 From: Sergio Benitez Date: Sat, 1 Apr 2023 15:02:24 -0700 Subject: [PATCH 110/166] Add initial implementation of 'rocket_ws'. This provides WebSocket support in Rocket's official 'contrib'. --- Cargo.toml | 1 + contrib/ws/Cargo.toml | 28 +++ contrib/ws/README.md | 35 ++++ contrib/ws/src/duplex.rs | 91 +++++++++ contrib/ws/src/lib.rs | 182 ++++++++++++++++++ .../src/ws.rs => contrib/ws/src/websocket.rs | 145 +++++++------- examples/upgrade/Cargo.toml | 2 +- examples/upgrade/src/main.rs | 5 +- scripts/config.sh | 1 + scripts/mk-docs.sh | 2 +- scripts/test.sh | 9 + 11 files changed, 420 insertions(+), 81 deletions(-) create mode 100644 contrib/ws/Cargo.toml create mode 100644 contrib/ws/README.md create mode 100644 contrib/ws/src/duplex.rs create mode 100644 contrib/ws/src/lib.rs rename examples/upgrade/src/ws.rs => contrib/ws/src/websocket.rs (60%) diff --git a/Cargo.toml b/Cargo.toml index 8ec081ef00..7260ca1c3a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -8,5 +8,6 @@ members = [ "contrib/sync_db_pools/codegen/", "contrib/sync_db_pools/lib/", "contrib/dyn_templates/", + "contrib/ws/", "site/tests", ] diff --git a/contrib/ws/Cargo.toml b/contrib/ws/Cargo.toml new file mode 100644 index 0000000000..b4a9481ffa --- /dev/null +++ b/contrib/ws/Cargo.toml @@ -0,0 +1,28 @@ +[package] +name = "rocket_ws" +version = "0.1.0-rc.3" +authors = ["Sergio Benitez "] +description = "WebSocket support for Rocket." +documentation = "https://api.rocket.rs/v0.5-rc/rocket_ws/" +homepage = "https://rocket.rs" +repository = "https://github.com/SergioBenitez/Rocket/tree/master/contrib/ws" +readme = "README.md" +keywords = ["rocket", "web", "framework", "websocket"] +license = "MIT OR Apache-2.0" +edition = "2021" +rust-version = "1.56" + +[features] +default = ["tungstenite"] +tungstenite = ["tokio-tungstenite"] + +[dependencies] +tokio-tungstenite = { version = "0.18", optional = true } + +[dependencies.rocket] +version = "=0.5.0-rc.3" +path = "../../core/lib" +default-features = false + +[package.metadata.docs.rs] +all-features = true diff --git a/contrib/ws/README.md b/contrib/ws/README.md new file mode 100644 index 0000000000..377597078a --- /dev/null +++ b/contrib/ws/README.md @@ -0,0 +1,35 @@ +# `ws` [![ci.svg]][ci] [![crates.io]][crate] [![docs.svg]][crate docs] + +[crates.io]: https://img.shields.io/crates/v/rocket_ws.svg +[crate]: https://crates.io/crates/rocket_ws +[docs.svg]: https://img.shields.io/badge/web-master-red.svg?style=flat&label=docs&colorB=d33847 +[crate docs]: https://api.rocket.rs/v0.5-rc/rocket_ws +[ci.svg]: https://github.com/SergioBenitez/Rocket/workflows/CI/badge.svg +[ci]: https://github.com/SergioBenitez/Rocket/actions + +This crate provides WebSocket support for Rocket via integration with Rocket's +[connection upgrades] API. + +# Usage + + 1. Depend on `rocket_ws`, renamed here to `ws`: + + ```toml + [dependencies] + ws = { package = "rocket_ws", version ="=0.1.0-rc.3" } + ``` + + 2. Use it! + + ```rust + #[get("/echo")] + fn echo_stream(ws: ws::WebSocket) -> ws::Stream!['static] { + ws::stream! { ws => + for await message in ws { + yield message?; + } + } + } + ``` + +See the [crate docs] for full details. diff --git a/contrib/ws/src/duplex.rs b/contrib/ws/src/duplex.rs new file mode 100644 index 0000000000..2a57ae90e8 --- /dev/null +++ b/contrib/ws/src/duplex.rs @@ -0,0 +1,91 @@ +use std::pin::Pin; +use std::task::{Context, Poll}; + +use rocket::data::IoStream; +use rocket::futures::{StreamExt, SinkExt, Sink}; +use rocket::futures::stream::{Stream, FusedStream}; + +use crate::frame::{Message, CloseFrame}; +use crate::result::{Result, Error}; + +/// A readable and writeable WebSocket [`Message`] `async` stream. +/// +/// This struct implements [`Stream`] and [`Sink`], allowing for `async` reading +/// and writing of [`Message`]s. The [`StreamExt`] and [`SinkExt`] traits can be +/// imported to provide additional functionality for streams and sinks: +/// +/// ```rust +/// # use rocket::get; +/// use rocket_ws as ws; +/// +/// use rocket::futures::{SinkExt, StreamExt}; +/// +/// #[get("/echo/manual")] +/// fn echo_manual<'r>(ws: ws::WebSocket) -> ws::Channel<'r> { +/// ws.channel(move |mut stream| Box::pin(async move { +/// while let Some(message) = stream.next().await { +/// let _ = stream.send(message?).await; +/// } +/// +/// Ok(()) +/// })) +/// } +/// ```` +/// +/// [`StreamExt`]: rocket::futures::StreamExt +/// [`SinkExt`]: rocket::futures::SinkExt + +pub struct DuplexStream(tokio_tungstenite::WebSocketStream); + +impl DuplexStream { + pub(crate) async fn new(stream: IoStream, config: crate::Config) -> Self { + use tokio_tungstenite::WebSocketStream; + use crate::tungstenite::protocol::Role; + + let inner = WebSocketStream::from_raw_socket(stream, Role::Server, Some(config)); + DuplexStream(inner.await) + } + + /// Close the stream now. This does not typically need to be called. + pub async fn close(&mut self, msg: Option>) -> Result<()> { + self.0.close(msg).await + } +} + +impl Stream for DuplexStream { + type Item = Result; + + fn poll_next(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { + self.get_mut().0.poll_next_unpin(cx) + } + + fn size_hint(&self) -> (usize, Option) { + self.0.size_hint() + } +} + +impl FusedStream for DuplexStream { + fn is_terminated(&self) -> bool { + self.0.is_terminated() + } +} + +impl Sink for DuplexStream { + type Error = Error; + + fn poll_ready(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { + self.get_mut().0.poll_ready_unpin(cx) + } + + fn start_send(self: Pin<&mut Self>, item: Message) -> Result<(), Self::Error> { + self.get_mut().0.start_send_unpin(item) + } + + fn poll_flush(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { + self.get_mut().0.poll_flush_unpin(cx) + } + + fn poll_close(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { + self.get_mut().0.poll_close_unpin(cx) + } +} diff --git a/contrib/ws/src/lib.rs b/contrib/ws/src/lib.rs new file mode 100644 index 0000000000..7c180fb68b --- /dev/null +++ b/contrib/ws/src/lib.rs @@ -0,0 +1,182 @@ +//! WebSocket support for Rocket. + +#![doc(html_root_url = "https://api.rocket.rs/v0.5-rc/rocket_ws")] +#![doc(html_favicon_url = "https://rocket.rs/images/favicon.ico")] +#![doc(html_logo_url = "https://rocket.rs/images/logo-boxed.png")] + +mod tungstenite { + #[doc(inline)] pub use tokio_tungstenite::tungstenite::*; +} + +mod duplex; +mod websocket; + +pub use self::tungstenite::Message; +pub use self::tungstenite::protocol::WebSocketConfig as Config; +pub use self::websocket::{WebSocket, Channel}; + +/// Structures for constructing raw WebSocket frames. +pub mod frame { + #[doc(hidden)] pub use crate::Message; + pub use crate::tungstenite::protocol::frame::{CloseFrame, Frame}; + pub use crate::tungstenite::protocol::frame::coding::CloseCode; +} + +/// Types representing incoming and/or outgoing `async` [`Message`] streams. +pub mod stream { + pub use crate::duplex::DuplexStream; + pub use crate::websocket::MessageStream; +} + +/// Library [`Error`](crate::result::Error) and +/// [`Result`](crate::result::Result) types. +pub mod result { + pub use crate::tungstenite::error::{Result, Error}; +} + +/// Type and expression macro for `async` WebSocket [`Message`] streams. +/// +/// This macro can be used both where types are expected or +/// where expressions are expected. +/// +/// # Type Position +/// +/// When used in a type position, the macro invoked as `Stream['r]` expands to: +/// +/// - [`MessageStream`]`<'r, impl `[`Stream`]`>> + 'r>` +/// +/// The lifetime need not be specified as `'r`. For instance, `Stream['request]` +/// is valid and expands as expected: +/// +/// - [`MessageStream`]`<'request, impl `[`Stream`]`>> + 'request>` +/// +/// As a convenience, when the macro is invoked as `Stream![]`, the lifetime +/// defaults to `'static`. That is, `Stream![]` is equivalent to +/// `Stream!['static]`. +/// +/// [`MessageStream`]: crate::stream::MessageStream +/// [`Stream`]: rocket::futures::stream::Stream +/// [`Result`]: crate::result::Result +/// [`Message`]: crate::Message +/// +/// # Expression Position +/// +/// When invoked as an expression, the macro behaves similarly to Rocket's +/// [`stream!`](rocket::response::stream::stream) macro. Specifically, it +/// supports `yield` and `for await` syntax. It is invoked as follows: +/// +/// ```rust +/// # use rocket::get; +/// use rocket_ws as ws; +/// +/// #[get("/")] +/// fn echo(ws: ws::WebSocket) -> ws::Stream![] { +/// ws::Stream! { ws => +/// for await message in ws { +/// yield message?; +/// yield "foo".into(); +/// yield vec![1, 2, 3, 4].into(); +/// } +/// } +/// } +/// ``` +/// +/// It enjoins the following type requirements: +/// +/// * The type of `ws` _must_ be [`WebSocket`]. `ws` can be any ident. +/// * The type of yielded expressions (`expr` in `yield expr`) _must_ be [`Message`]. +/// * The `Err` type of expressions short-circuited with `?` _must_ be [`Error`]. +/// +/// [`Error`]: crate::result::Error +/// +/// The macro takes any series of statements and expands them into an expression +/// of type `impl Stream>`, a stream that `yield`s elements of +/// type [`Result`]``. It automatically converts yielded items of type `T` into +/// `Ok(T)`. It supports any Rust statement syntax with the following +/// extensions: +/// +/// * `?` short-circuits stream termination on `Err` +/// +/// The type of the error value must be [`Error`]. +///

+/// +/// * `yield expr` +/// +/// Yields the result of evaluating `expr` to the caller (the stream +/// consumer) wrapped in `Ok`. +/// +/// `expr` must be of type `T`. +///

+/// +/// * `for await x in stream { .. }` +/// +/// `await`s the next element in `stream`, binds it to `x`, and executes the +/// block with the binding. +/// +/// `stream` must implement `Stream`; the type of `x` is `T`. +/// +/// ### Examples +/// +/// Borrow from the request. Send a single message and close: +/// +/// ```rust +/// # use rocket::get; +/// use rocket_ws as ws; +/// +/// #[get("/hello/")] +/// fn ws_hello(ws: ws::WebSocket, user: &str) -> ws::Stream!['_] { +/// ws::Stream! { ws => +/// yield user.into(); +/// } +/// } +/// ``` +/// +/// Borrow from the request with explicit lifetime: +/// +/// ```rust +/// # use rocket::get; +/// use rocket_ws as ws; +/// +/// #[get("/hello/")] +/// fn ws_hello<'r>(ws: ws::WebSocket, user: &'r str) -> ws::Stream!['r] { +/// ws::Stream! { ws => +/// yield user.into(); +/// } +/// } +/// ``` +/// +/// Emit several messages and short-circuit if the client sends a bad message: +/// +/// ```rust +/// # use rocket::get; +/// use rocket_ws as ws; +/// +/// #[get("/")] +/// fn echo(ws: ws::WebSocket) -> ws::Stream![] { +/// ws::Stream! { ws => +/// for await message in ws { +/// for i in 0..5u8 { +/// yield i.to_string().into(); +/// } +/// +/// yield message?; +/// } +/// } +/// } +/// ``` +/// +#[macro_export] +macro_rules! Stream { + () => ($crate::Stream!['static]); + ($l:lifetime) => ( + $crate::stream::MessageStream<$l, impl rocket::futures::Stream< + Item = $crate::result::Result<$crate::Message> + > + $l> + ); + ($channel:ident => $($token:tt)*) => ( + let ws: $crate::WebSocket = $channel; + ws.stream(move |$channel| rocket::async_stream::try_stream! { + $($token)* + }) + ); +} diff --git a/examples/upgrade/src/ws.rs b/contrib/ws/src/websocket.rs similarity index 60% rename from examples/upgrade/src/ws.rs rename to contrib/ws/src/websocket.rs index a2758977a3..ce03cae17e 100644 --- a/examples/upgrade/src/ws.rs +++ b/contrib/ws/src/websocket.rs @@ -1,27 +1,70 @@ use std::io; -use rocket::futures::{StreamExt, SinkExt}; -use rocket::futures::stream::SplitStream; -use rocket::{Request, response}; use rocket::data::{IoHandler, IoStream}; +use rocket::futures::{self, StreamExt, SinkExt, future::BoxFuture, stream::SplitStream}; +use rocket::response::{self, Responder, Response}; use rocket::request::{FromRequest, Outcome}; -use rocket::response::{Responder, Response}; -use rocket::futures::{self, future::BoxFuture}; +use rocket::request::Request; -use tokio_tungstenite::WebSocketStream; -use tokio_tungstenite::tungstenite::handshake::derive_accept_key; -use tokio_tungstenite::tungstenite::protocol::Role; +use crate::{Config, Message}; +use crate::stream::DuplexStream; +use crate::result::{Result, Error}; -pub use tokio_tungstenite::tungstenite::error::{Result, Error}; -pub use tokio_tungstenite::tungstenite::Message; +/// A request guard that identifies WebSocket requests. Converts into a +/// [`Channel`] or [`MessageStream`]. +pub struct WebSocket { + config: Config, + key: String, +} + +impl WebSocket { + fn new(key: String) -> WebSocket { + WebSocket { config: Config::default(), key } + } + + pub fn config(mut self, config: Config) -> Self { + self.config = config; + self + } + + pub fn channel<'r, F: Send + 'r>(self, handler: F) -> Channel<'r> + where F: FnMut(DuplexStream) -> BoxFuture<'r, Result<()>> + 'r + { + Channel { ws: self, handler: Box::new(handler), } + } -pub struct WebSocket(String); + pub fn stream<'r, F, S>(self, stream: F) -> MessageStream<'r, S> + where F: FnMut(SplitStream) -> S + Send + 'r, + S: futures::Stream> + Send + 'r + { + MessageStream { ws: self, handler: Box::new(stream), } + } +} + +/// A streaming channel, returned by [`WebSocket::channel()`]. +pub struct Channel<'r> { + ws: WebSocket, + handler: Box BoxFuture<'r, Result<()>> + Send + 'r>, +} + +/// A [`Stream`](futures::Stream) of [`Message`]s, returned by +/// [`WebSocket::stream()`], used via [`Stream!`]. +/// +/// This type is not typically used directly. Instead, it is used via the +/// [`Stream!`] macro, which expands to both the type itself and an expression +/// which evaluates to this type. +// TODO: Get rid of this or `Channel` via a single `enum`. +pub struct MessageStream<'r, S> { + ws: WebSocket, + handler: Box) -> S + Send + 'r> +} #[rocket::async_trait] impl<'r> FromRequest<'r> for WebSocket { type Error = std::convert::Infallible; async fn from_request(req: &'r Request<'_>) -> Outcome { + use crate::tungstenite::handshake::derive_accept_key; use rocket::http::uncased::eq; let headers = req.headers(); @@ -31,45 +74,20 @@ impl<'r> FromRequest<'r> for WebSocket { let is_ws = headers.get("Upgrade") .any(|h| h.split(',').any(|v| eq(v.trim(), "websocket"))); - let is_ws_13 = headers.get_one("Sec-WebSocket-Version").map_or(false, |v| v == "13"); + let is_13 = headers.get_one("Sec-WebSocket-Version").map_or(false, |v| v == "13"); let key = headers.get_one("Sec-WebSocket-Key").map(|k| derive_accept_key(k.as_bytes())); match key { - Some(key) if is_upgrade && is_ws && is_ws_13 => Outcome::Success(WebSocket(key)), + Some(key) if is_upgrade && is_ws && is_13 => Outcome::Success(WebSocket::new(key)), Some(_) | None => Outcome::Forward(()) } } } -pub struct Channel<'r> { - ws: WebSocket, - handler: Box) -> BoxFuture<'r, Result<()>> + Send + 'r>, -} - -pub struct MessageStream<'r, S> { - ws: WebSocket, - handler: Box>) -> S + Send + 'r> -} - -impl WebSocket { - pub fn channel<'r, F: Send + 'r>(self, handler: F) -> Channel<'r> - where F: FnMut(WebSocketStream) -> BoxFuture<'r, Result<()>> + 'r - { - Channel { ws: self, handler: Box::new(handler), } - } - - pub fn stream<'r, F, S>(self, stream: F) -> MessageStream<'r, S> - where F: FnMut(SplitStream>) -> S + Send + 'r, - S: futures::Stream> + Send + 'r - { - MessageStream { ws: self, handler: Box::new(stream), } - } -} - impl<'r, 'o: 'r> Responder<'r, 'o> for Channel<'o> { fn respond_to(self, _: &'r Request<'_>) -> response::Result<'o> { Response::build() .raw_header("Sec-Websocket-Version", "13") - .raw_header("Sec-WebSocket-Accept", self.ws.0.clone()) + .raw_header("Sec-WebSocket-Accept", self.ws.key.clone()) .upgrade("websocket", self) .ok() } @@ -81,28 +99,16 @@ impl<'r, 'o: 'r, S> Responder<'r, 'o> for MessageStream<'o, S> fn respond_to(self, _: &'r Request<'_>) -> response::Result<'o> { Response::build() .raw_header("Sec-Websocket-Version", "13") - .raw_header("Sec-WebSocket-Accept", self.ws.0.clone()) + .raw_header("Sec-WebSocket-Accept", self.ws.key.clone()) .upgrade("websocket", self) .ok() } } -/// Returns `Ok(true)` if processing should continue, `Ok(false)` if processing -/// has terminated without error, and `Err(e)` if an error has occurred. -fn handle_result(result: Result<()>) -> io::Result { - match result { - Ok(_) => Ok(true), - Err(Error::ConnectionClosed) => Ok(false), - Err(Error::Io(e)) => Err(e), - Err(e) => Err(io::Error::new(io::ErrorKind::Other, e)) - } -} - #[rocket::async_trait] impl IoHandler for Channel<'_> { async fn io(&mut self, io: IoStream) -> io::Result<()> { - let stream = WebSocketStream::from_raw_socket(io, Role::Server, None).await; - let result = (self.handler)(stream).await; + let result = (self.handler)(DuplexStream::new(io, self.ws.config).await).await; handle_result(result).map(|_| ()) } } @@ -112,8 +118,7 @@ impl<'r, S> IoHandler for MessageStream<'r, S> where S: futures::Stream> + Send + 'r { async fn io(&mut self, io: IoStream) -> io::Result<()> { - let stream = WebSocketStream::from_raw_socket(io, Role::Server, None).await; - let (mut sink, stream) = stream.split(); + let (mut sink, stream) = DuplexStream::new(io, self.ws.config).await.split(); let mut stream = std::pin::pin!((self.handler)(stream)); while let Some(msg) = stream.next().await { let result = match msg { @@ -130,25 +135,13 @@ impl<'r, S> IoHandler for MessageStream<'r, S> } } -#[macro_export] -macro_rules! Stream { - () => (Stream!['static]); - ($l:lifetime) => ( - $crate::ws::MessageStream<$l, impl rocket::futures::Stream< - Item = $crate::ws::Result<$crate::ws::Message> - > + $l> - ); -} - -#[macro_export] -macro_rules! stream { - ($channel:ident => $($token:tt)*) => ( - let ws: $crate::ws::WebSocket = $channel; - ws.stream(move |$channel| rocket::async_stream::try_stream! { - $($token)* - }) - ) +/// Returns `Ok(true)` if processing should continue, `Ok(false)` if processing +/// has terminated without error, and `Err(e)` if an error has occurred. +fn handle_result(result: Result<()>) -> io::Result { + match result { + Ok(_) => Ok(true), + Err(Error::ConnectionClosed) => Ok(false), + Err(Error::Io(e)) => Err(e), + Err(e) => Err(io::Error::new(io::ErrorKind::Other, e)) + } } - -pub use Stream as Stream; -pub use stream as stream; diff --git a/examples/upgrade/Cargo.toml b/examples/upgrade/Cargo.toml index 0b70adf0c0..e34a504ff8 100644 --- a/examples/upgrade/Cargo.toml +++ b/examples/upgrade/Cargo.toml @@ -7,4 +7,4 @@ publish = false [dependencies] rocket = { path = "../../core/lib" } -tokio-tungstenite = "0.18" +ws = { package = "rocket_ws", path = "../../contrib/ws" } diff --git a/examples/upgrade/src/main.rs b/examples/upgrade/src/main.rs index bb6b661726..a572e0221c 100644 --- a/examples/upgrade/src/main.rs +++ b/examples/upgrade/src/main.rs @@ -3,8 +3,6 @@ use rocket::fs::{self, FileServer}; use rocket::futures::{SinkExt, StreamExt}; -mod ws; - #[get("/echo/manual")] fn echo_manual<'r>(ws: ws::WebSocket) -> ws::Channel<'r> { ws.channel(move |mut stream| Box::pin(async move { @@ -18,7 +16,8 @@ fn echo_manual<'r>(ws: ws::WebSocket) -> ws::Channel<'r> { #[get("/echo")] fn echo_stream(ws: ws::WebSocket) -> ws::Stream!['static] { - ws::stream! { ws => + let ws = ws.config(ws::Config { max_send_queue: Some(5), ..Default::default() }); + ws::Stream! { ws => for await message in ws { yield message?; } diff --git a/scripts/config.sh b/scripts/config.sh index 07c3ab65d5..bfae241342 100755 --- a/scripts/config.sh +++ b/scripts/config.sh @@ -98,6 +98,7 @@ ALL_CRATE_ROOTS=( "${CONTRIB_ROOT}/db_pools/codegen" "${CONTRIB_ROOT}/db_pools/lib" "${CONTRIB_ROOT}/dyn_templates" + "${CONTRIB_ROOT}/ws" ) function print_environment() { diff --git a/scripts/mk-docs.sh b/scripts/mk-docs.sh index bb0c7375a6..93659d091d 100755 --- a/scripts/mk-docs.sh +++ b/scripts/mk-docs.sh @@ -22,7 +22,7 @@ pushd "${PROJECT_ROOT}" > /dev/null 2>&1 # Set the crate version and fill in missing doc URLs with docs.rs links. RUSTDOCFLAGS="-Zunstable-options --crate-version ${DOC_VERSION}" \ cargo doc -p rocket \ - -p rocket_sync_db_pools -p rocket_dyn_templates -p rocket_db_pools \ + -p rocket_sync_db_pools -p rocket_dyn_templates -p rocket_db_pools -p rocket_ws \ -Zrustdoc-map --no-deps --all-features popd > /dev/null 2>&1 diff --git a/scripts/test.sh b/scripts/test.sh index bb21d1846e..b691dab980 100755 --- a/scripts/test.sh +++ b/scripts/test.sh @@ -98,6 +98,10 @@ function test_contrib() { handlebars ) + WS_FEATURES=( + tungstenite + ) + for feature in "${DB_POOLS_FEATURES[@]}"; do echo ":: Building and testing db_pools [$feature]..." $CARGO test -p rocket_db_pools --no-default-features --features $feature $@ @@ -112,6 +116,11 @@ function test_contrib() { echo ":: Building and testing dyn_templates [$feature]..." $CARGO test -p rocket_dyn_templates --no-default-features --features $feature $@ done + + for feature in "${WS_FEATURES[@]}"; do + echo ":: Building and testing ws [$feature]..." + $CARGO test -p rocket_ws --no-default-features --features $feature $@ + done } function test_core() { From 9c4ac797481df34b9d9aa78a13b27aae248cb3c7 Mon Sep 17 00:00:00 2001 From: Sergio Benitez Date: Mon, 3 Apr 2023 12:18:09 -0700 Subject: [PATCH 111/166] Use D: disk on Windows for more space. This fixes the Windows CI: it was previously running out of disk space. --- .github/workflows/ci.yml | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 8612056c77..a7a0f5cd61 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -34,12 +34,14 @@ jobs: - platform: { name: Linux, distro: ubuntu-latest, toolchain: nightly } test: { name: UI, flag: "--ui" } fallible: true + - platform: { name: Windows } + working-directory: "C:\\a\\${{ github.event.repository.name }}\\${{ github.event.repository.name }}" steps: - name: Checkout Sources - uses: actions/checkout@v2 + uses: actions/checkout@v3 - - name: Free Disk Space + - name: Free Disk Space (Linux) if: matrix.platform.name == 'Linux' run: | echo "Freeing up disk space on Linux CI" @@ -100,7 +102,16 @@ jobs: with: key: ${{ matrix.test.name }}-${{ steps.toolchain.outputs.cachekey }} + # Don't run out of disk space on Windows. C: has much much space than D:. + - name: Switch Disk (Windows) + if: matrix.platform.name == 'Windows' + run: | + Get-PSDrive + cp D:\a C:\ -Recurse + Get-PSDrive + - name: Run Tests continue-on-error: ${{ matrix.fallible }} + working-directory: ${{ matrix.working-directory || github.workspace }} run: ./scripts/test.sh ${{ matrix.test.flag }} -q shell: bash From 887558be60c2a2a7ac5ecf2390bc793baab279be Mon Sep 17 00:00:00 2001 From: Sergio Benitez Date: Mon, 3 Apr 2023 12:06:10 -0700 Subject: [PATCH 112/166] Emit warnings when data limits are reached. Aid in debugging incorrectly configured data limits. --- core/lib/src/data/data_stream.rs | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/core/lib/src/data/data_stream.rs b/core/lib/src/data/data_stream.rs index a94600796f..b335547662 100644 --- a/core/lib/src/data/data_stream.rs +++ b/core/lib/src/data/data_stream.rs @@ -65,7 +65,7 @@ enum StreamKind<'r> { impl<'r> DataStream<'r> { pub(crate) fn new(buf: Vec, stream: StreamReader<'r>, limit: u64) -> Self { - let chain = Chain::new(Cursor::new(buf), stream).take(limit); + let chain = Chain::new(Cursor::new(buf), stream).take(limit).into(); Self { chain } } @@ -73,6 +73,7 @@ impl<'r> DataStream<'r> { async fn limit_exceeded(&mut self) -> io::Result { #[cold] async fn _limit_exceeded(stream: &mut DataStream<'_>) -> io::Result { + // Read one more byte after reaching limit to see if we cut early. stream.chain.set_limit(1); let mut buf = [0u8; 1]; Ok(stream.read(&mut buf).await? != 0) @@ -252,6 +253,18 @@ impl AsyncRead for DataStream<'_> { cx: &mut Context<'_>, buf: &mut ReadBuf<'_>, ) -> Poll> { + if self.chain.limit() == 0 { + let stream: &StreamReader<'_> = &self.chain.get_ref().get_ref().1; + let kind = match stream.inner { + StreamKind::Empty => "an empty stream (vacuous)", + StreamKind::Body(_) => "the request body", + StreamKind::Multipart(_) => "a multipart form field", + }; + + let msg = yansi::Paint::default(kind).bold(); + warn_!("Data limit reached while reading {}.", msg); + } + Pin::new(&mut self.chain).poll_read(cx, buf) } } From d9f86d86471494ea8a1abc6d9e4588145c6a2ac1 Mon Sep 17 00:00:00 2001 From: Sergio Benitez Date: Mon, 3 Apr 2023 16:09:45 -0700 Subject: [PATCH 113/166] Fully document the 'ws' contrib crate. --- contrib/ws/README.md | 20 ++--- contrib/ws/src/duplex.rs | 5 +- contrib/ws/src/lib.rs | 146 ++++++++++++++++++++++++++++++++++- contrib/ws/src/websocket.rs | 129 ++++++++++++++++++++++++++++++- examples/upgrade/src/main.rs | 32 +++++--- 5 files changed, 303 insertions(+), 29 deletions(-) diff --git a/contrib/ws/README.md b/contrib/ws/README.md index 377597078a..b77e98223e 100644 --- a/contrib/ws/README.md +++ b/contrib/ws/README.md @@ -21,15 +21,15 @@ This crate provides WebSocket support for Rocket via integration with Rocket's 2. Use it! - ```rust - #[get("/echo")] - fn echo_stream(ws: ws::WebSocket) -> ws::Stream!['static] { - ws::stream! { ws => - for await message in ws { - yield message?; - } - } - } - ``` + ```rust + #[get("/echo")] + fn echo_stream(ws: ws::WebSocket) -> ws::Stream!['static] { + ws::stream! { ws => + for await message in ws { + yield message?; + } + } + } + ``` See the [crate docs] for full details. diff --git a/contrib/ws/src/duplex.rs b/contrib/ws/src/duplex.rs index 2a57ae90e8..76b5eac289 100644 --- a/contrib/ws/src/duplex.rs +++ b/contrib/ws/src/duplex.rs @@ -16,8 +16,7 @@ use crate::result::{Result, Error}; /// /// ```rust /// # use rocket::get; -/// use rocket_ws as ws; -/// +/// # use rocket_ws as ws; /// use rocket::futures::{SinkExt, StreamExt}; /// /// #[get("/echo/manual")] @@ -30,7 +29,7 @@ use crate::result::{Result, Error}; /// Ok(()) /// })) /// } -/// ```` +/// ``` /// /// [`StreamExt`]: rocket::futures::StreamExt /// [`SinkExt`]: rocket::futures::SinkExt diff --git a/contrib/ws/src/lib.rs b/contrib/ws/src/lib.rs index 7c180fb68b..4b424a7697 100644 --- a/contrib/ws/src/lib.rs +++ b/contrib/ws/src/lib.rs @@ -1,4 +1,74 @@ //! WebSocket support for Rocket. +//! +//! This crate implements support for WebSockets via Rocket's [connection +//! upgrade API](rocket::Response#upgrading) and +//! [tungstenite](tokio_tungstenite). +//! +//! # Usage +//! +//! Depend on the crate. Here, we rename the dependency to `ws` for convenience: +//! +//! ```toml +//! [dependencies] +//! ws = { package = "rocket_ws", version ="=0.1.0-rc.3" } +//! ``` +//! +//! Then, use [`WebSocket`] as a request guard in any route and either call +//! [`WebSocket::channel()`] or return a stream via [`Stream!`] or +//! [`WebSocket::stream()`] in the handler. The examples below are equivalent: +//! +//! ```rust +//! # use rocket::get; +//! # use rocket_ws as ws; +//! # +//! #[get("/echo?channel")] +//! fn echo_channel(ws: ws::WebSocket) -> ws::Channel<'static> { +//! use rocket::futures::{SinkExt, StreamExt}; +//! +//! ws.channel(move |mut stream| Box::pin(async move { +//! while let Some(message) = stream.next().await { +//! let _ = stream.send(message?).await; +//! } +//! +//! Ok(()) +//! })) +//! } +//! +//! #[get("/echo?stream")] +//! fn echo_stream(ws: ws::WebSocket) -> ws::Stream!['static] { +//! ws::Stream! { ws => +//! for await message in ws { +//! yield message?; +//! } +//! } +//! } +//! +//! #[get("/echo?compose")] +//! fn echo_compose(ws: ws::WebSocket) -> ws::Stream!['static] { +//! ws.stream(|io| io) +//! } +//! ``` +//! +//! WebSocket connections are configurable via [`WebSocket::config()`]: +//! +//! ```rust +//! # use rocket::get; +//! # use rocket_ws as ws; +//! # +//! #[get("/echo")] +//! fn echo_stream(ws: ws::WebSocket) -> ws::Stream!['static] { +//! let ws = ws.config(ws::Config { +//! max_send_queue: Some(5), +//! ..Default::default() +//! }); +//! +//! ws::Stream! { ws => +//! for await message in ws { +//! yield message?; +//! } +//! } +//! } +//! ``` #![doc(html_root_url = "https://api.rocket.rs/v0.5-rc/rocket_ws")] #![doc(html_favicon_url = "https://rocket.rs/images/favicon.ico")] @@ -11,9 +81,83 @@ mod tungstenite { mod duplex; mod websocket; +pub use self::websocket::{WebSocket, Channel}; + +/// A WebSocket message. +/// +/// A value of this type is typically constructed by calling `.into()` on a +/// supported message type. This includes strings via `&str` and `String` and +/// bytes via `&[u8]` and `Vec`: +/// +/// ```rust +/// # use rocket::get; +/// # use rocket_ws as ws; +/// # +/// #[get("/echo")] +/// fn echo_stream(ws: ws::WebSocket) -> ws::Stream!['static] { +/// ws::Stream! { ws => +/// yield "Hello".into(); +/// yield String::from("Hello").into(); +/// yield (&[1u8, 2, 3][..]).into(); +/// yield vec![1u8, 2, 3].into(); +/// } +/// } +/// ``` +/// +/// Other kinds of messages can be constructed directly: +/// +/// ```rust +/// # use rocket::get; +/// # use rocket_ws as ws; +/// # +/// #[get("/echo")] +/// fn echo_stream(ws: ws::WebSocket) -> ws::Stream!['static] { +/// ws::Stream! { ws => +/// yield ws::Message::Ping(vec![b'h', b'i']) +/// } +/// } +/// ``` pub use self::tungstenite::Message; + +/// WebSocket connection configuration. +/// +/// The default configuration for a [`WebSocket`] can be changed by calling +/// [`WebSocket::config()`] with a value of this type. The defaults are obtained +/// via [`Default::default()`]. You don't generally need to reconfigure a +/// `WebSocket` unless you're certain you need different values. In other words, +/// this structure should rarely be used. +/// +/// # Example +/// +/// ```rust +/// # use rocket::get; +/// # use rocket_ws as ws; +/// use rocket::data::ToByteUnit; +/// +/// #[get("/echo")] +/// fn echo_stream(ws: ws::WebSocket) -> ws::Stream!['static] { +/// let ws = ws.config(ws::Config { +/// // Enable backpressure with a max send queue size of `5`. +/// max_send_queue: Some(5), +/// // Decrease the maximum (complete) message size to 4MiB. +/// max_message_size: Some(4.mebibytes().as_u64() as usize), +/// // Decrease the maximum size of _one_ frame (not messag) to 1MiB. +/// max_frame_size: Some(1.mebibytes().as_u64() as usize), +/// // Use the default values for the rest. +/// ..Default::default() +/// }); +/// +/// ws::Stream! { ws => +/// for await message in ws { +/// yield message?; +/// } +/// } +/// } +/// ``` +/// +/// **Original `tungstenite` Documentation Follows** +/// pub use self::tungstenite::protocol::WebSocketConfig as Config; -pub use self::websocket::{WebSocket, Channel}; /// Structures for constructing raw WebSocket frames. pub mod frame { diff --git a/contrib/ws/src/websocket.rs b/contrib/ws/src/websocket.rs index ce03cae17e..fbf4ad110e 100644 --- a/contrib/ws/src/websocket.rs +++ b/contrib/ws/src/websocket.rs @@ -10,8 +10,25 @@ use crate::{Config, Message}; use crate::stream::DuplexStream; use crate::result::{Result, Error}; -/// A request guard that identifies WebSocket requests. Converts into a -/// [`Channel`] or [`MessageStream`]. +/// A request guard identifying WebSocket requests. Converts into a [`Channel`] +/// or [`MessageStream`]. +/// +/// For example usage, see the [crate docs](crate#usage). +/// +/// ## Details +/// +/// This is the entrypoint to the library. Every WebSocket response _must_ +/// initiate via the `WebSocket` request guard. The guard identifies valid +/// WebSocket connection requests and, if the request is valid, succeeds to be +/// converted into a streaming WebSocket response via [`Stream!`], +/// [`WebSocket::channel()`], or [`WebSocket::stream()`]. The connection can be +/// configured via [`WebSocket::config()`]; see [`Config`] for details on +/// configuring a connection. +/// +/// ### Forwarding +/// +/// If the incoming request is not a valid WebSocket request, the guard +/// forwards. The guard never fails. pub struct WebSocket { config: Config, key: String, @@ -22,17 +39,119 @@ impl WebSocket { WebSocket { config: Config::default(), key } } + /// Change the default connection configuration to `config`. + /// + /// # Example + /// + /// ```rust + /// # use rocket::get; + /// # use rocket_ws as ws; + /// # + /// #[get("/echo")] + /// fn echo_stream(ws: ws::WebSocket) -> ws::Stream!['static] { + /// let ws = ws.config(ws::Config { + /// max_send_queue: Some(5), + /// ..Default::default() + /// }); + /// + /// ws::Stream! { ws => + /// for await message in ws { + /// yield message?; + /// } + /// } + /// } + /// ``` pub fn config(mut self, config: Config) -> Self { self.config = config; self } + /// Create a read/write channel to the client and call `handler` with it. + /// + /// This method takes a `FnMut`, `handler`, that consumes a read/write + /// WebSocket channel, [`DuplexStream`] to the client. See [`DuplexStream`] + /// for details on how to make use of the channel. + /// + /// The `handler` must return a `Box`ed and `Pin`ned future: calling + /// [`Box::pin()`] with a future does just this as is the preferred + /// mechanism to create a `Box>`. The future must return a + /// [`Result<()>`](crate::result::Result). The WebSocket connection is + /// closed successfully if the future returns `Ok` and with an error if + /// the future returns `Err`. + /// + /// # Lifetimes + /// + /// The `Channel` may borrow from the request. If it does, the lifetime + /// should be specified as something other than `'static`. Otherwise, the + /// `'static` lifetime should be used. + /// + /// # Example + /// + /// ```rust + /// # use rocket::get; + /// # use rocket_ws as ws; + /// use rocket::futures::{SinkExt, StreamExt}; + /// + /// #[get("/hello/")] + /// fn hello(ws: ws::WebSocket, name: &str) -> ws::Channel<'_> { + /// ws.channel(move |mut stream| Box::pin(async move { + /// let message = format!("Hello, {}!", name); + /// let _ = stream.send(message.into()).await; + /// Ok(()) + /// })) + /// } + /// + /// #[get("/echo")] + /// fn echo(ws: ws::WebSocket) -> ws::Channel<'static> { + /// ws.channel(move |mut stream| Box::pin(async move { + /// while let Some(message) = stream.next().await { + /// let _ = stream.send(message?).await; + /// } + /// + /// Ok(()) + /// })) + /// } + /// ``` pub fn channel<'r, F: Send + 'r>(self, handler: F) -> Channel<'r> where F: FnMut(DuplexStream) -> BoxFuture<'r, Result<()>> + 'r { Channel { ws: self, handler: Box::new(handler), } } + /// Create a stream that consumes client [`Message`]s and emits its own. + /// + /// This method takes a `FnMut` `stream` that consumes a read-only stream + /// and returns a stream of [`Message`]s. While the returned stream can be + /// constructed in any manner, the [`Stream!`] macro is the preferred + /// method. In any case, the stream must be `Send`. + /// + /// The returned stream must emit items of type `Result`. Items + /// that are `Ok(Message)` are sent to the client while items of type + /// `Err(Error)` result in the connection being closed and the remainder of + /// the stream discarded. + /// + /// # Example + /// + /// ```rust + /// # use rocket::get; + /// # use rocket_ws as ws; + /// + /// // Use `Stream!`, which internally calls `WebSocket::stream()`. + /// #[get("/echo?stream")] + /// fn echo_stream(ws: ws::WebSocket) -> ws::Stream!['static] { + /// ws::Stream! { ws => + /// for await message in ws { + /// yield message?; + /// } + /// } + /// } + /// + /// // Use a raw stream. + /// #[get("/echo?compose")] + /// fn echo_compose(ws: ws::WebSocket) -> ws::Stream!['static] { + /// ws.stream(|io| io) + /// } + /// ``` pub fn stream<'r, F, S>(self, stream: F) -> MessageStream<'r, S> where F: FnMut(SplitStream) -> S + Send + 'r, S: futures::Stream> + Send + 'r @@ -42,6 +161,8 @@ impl WebSocket { } /// A streaming channel, returned by [`WebSocket::channel()`]. +/// +/// `Channel` has no methods or functionality beyond its trait implementations. pub struct Channel<'r> { ws: WebSocket, handler: Box BoxFuture<'r, Result<()>> + Send + 'r>, @@ -50,9 +171,9 @@ pub struct Channel<'r> { /// A [`Stream`](futures::Stream) of [`Message`]s, returned by /// [`WebSocket::stream()`], used via [`Stream!`]. /// -/// This type is not typically used directly. Instead, it is used via the +/// This type should not be used directly. Instead, it is used via the /// [`Stream!`] macro, which expands to both the type itself and an expression -/// which evaluates to this type. +/// which evaluates to this type. See [`Stream!`] for details. // TODO: Get rid of this or `Channel` via a single `enum`. pub struct MessageStream<'r, S> { ws: WebSocket, diff --git a/examples/upgrade/src/main.rs b/examples/upgrade/src/main.rs index a572e0221c..943ff07098 100644 --- a/examples/upgrade/src/main.rs +++ b/examples/upgrade/src/main.rs @@ -3,8 +3,23 @@ use rocket::fs::{self, FileServer}; use rocket::futures::{SinkExt, StreamExt}; -#[get("/echo/manual")] -fn echo_manual<'r>(ws: ws::WebSocket) -> ws::Channel<'r> { +#[get("/echo?stream", rank = 1)] +fn echo_stream(ws: ws::WebSocket) -> ws::Stream!['static] { + ws::Stream! { ws => + for await message in ws { + yield message?; + } + } +} + +#[get("/echo?channel", rank = 2)] +fn echo_channel(ws: ws::WebSocket) -> ws::Channel<'static> { + // This is entirely optional. Change default configuration. + let ws = ws.config(ws::Config { + max_send_queue: Some(5), + ..Default::default() + }); + ws.channel(move |mut stream| Box::pin(async move { while let Some(message) = stream.next().await { let _ = stream.send(message?).await; @@ -14,19 +29,14 @@ fn echo_manual<'r>(ws: ws::WebSocket) -> ws::Channel<'r> { })) } -#[get("/echo")] -fn echo_stream(ws: ws::WebSocket) -> ws::Stream!['static] { - let ws = ws.config(ws::Config { max_send_queue: Some(5), ..Default::default() }); - ws::Stream! { ws => - for await message in ws { - yield message?; - } - } +#[get("/echo?raw", rank = 3)] +fn echo_raw(ws: ws::WebSocket) -> ws::Stream!['static] { + ws.stream(|stream| stream) } #[launch] fn rocket() -> _ { rocket::build() - .mount("/", routes![echo_manual, echo_stream]) + .mount("/", routes![echo_channel, echo_stream, echo_raw]) .mount("/", FileServer::from(fs::relative!("static"))) } From 5e7a75e1a5252f063ba1068920a0dd11a5477721 Mon Sep 17 00:00:00 2001 From: Sergio Benitez Date: Tue, 4 Apr 2023 15:11:09 -0700 Subject: [PATCH 114/166] Use 'parking_lot' 'Mutex' in fairing's 'Once'. --- core/lib/src/fairing/ad_hoc.rs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/core/lib/src/fairing/ad_hoc.rs b/core/lib/src/fairing/ad_hoc.rs index 2add55a7a3..f4ea6e1b89 100644 --- a/core/lib/src/fairing/ad_hoc.rs +++ b/core/lib/src/fairing/ad_hoc.rs @@ -1,6 +1,5 @@ -use std::sync::Mutex; - use futures::future::{Future, BoxFuture, FutureExt}; +use parking_lot::Mutex; use crate::{Rocket, Request, Response, Data, Build, Orbit}; use crate::fairing::{Fairing, Kind, Info, Result}; @@ -47,7 +46,7 @@ impl Once { #[track_caller] fn take(&self) -> Box { - self.0.lock().expect("Once::lock()").take().expect("Once::take() called once") + self.0.lock().take().expect("Once::take() called once") } } From c3520fb4a1f00d8705123a03e9188ec892b9153d Mon Sep 17 00:00:00 2001 From: Sergio Benitez Date: Tue, 4 Apr 2023 15:11:30 -0700 Subject: [PATCH 115/166] Pin I/O handlers. Allow 'FnOnce' in 'ws' handlers. This modifies the 'IoHandler::io()' method so that it takes a 'Pin>', allowing handlers to move internally and assume that the data is pinned. The change is then used in the 'ws' contrib crate to allow 'FnOnce' handlers instead of 'FnMut'. The net effect is that streams, such as those crated by 'Stream!', are now allowed to move internally. --- contrib/ws/src/websocket.rs | 25 ++++++++++++++----------- core/lib/src/data/io_stream.rs | 6 ++++-- core/lib/src/response/response.rs | 23 +++++++++++++++-------- core/lib/src/server.rs | 3 ++- 4 files changed, 35 insertions(+), 22 deletions(-) diff --git a/contrib/ws/src/websocket.rs b/contrib/ws/src/websocket.rs index fbf4ad110e..c819f49fb6 100644 --- a/contrib/ws/src/websocket.rs +++ b/contrib/ws/src/websocket.rs @@ -1,4 +1,5 @@ use std::io; +use std::pin::Pin; use rocket::data::{IoHandler, IoStream}; use rocket::futures::{self, StreamExt, SinkExt, future::BoxFuture, stream::SplitStream}; @@ -68,7 +69,7 @@ impl WebSocket { /// Create a read/write channel to the client and call `handler` with it. /// - /// This method takes a `FnMut`, `handler`, that consumes a read/write + /// This method takes a `FnOnce`, `handler`, that consumes a read/write /// WebSocket channel, [`DuplexStream`] to the client. See [`DuplexStream`] /// for details on how to make use of the channel. /// @@ -113,14 +114,14 @@ impl WebSocket { /// } /// ``` pub fn channel<'r, F: Send + 'r>(self, handler: F) -> Channel<'r> - where F: FnMut(DuplexStream) -> BoxFuture<'r, Result<()>> + 'r + where F: FnOnce(DuplexStream) -> BoxFuture<'r, Result<()>> + 'r { Channel { ws: self, handler: Box::new(handler), } } /// Create a stream that consumes client [`Message`]s and emits its own. /// - /// This method takes a `FnMut` `stream` that consumes a read-only stream + /// This method takes a `FnOnce` `stream` that consumes a read-only stream /// and returns a stream of [`Message`]s. While the returned stream can be /// constructed in any manner, the [`Stream!`] macro is the preferred /// method. In any case, the stream must be `Send`. @@ -153,7 +154,7 @@ impl WebSocket { /// } /// ``` pub fn stream<'r, F, S>(self, stream: F) -> MessageStream<'r, S> - where F: FnMut(SplitStream) -> S + Send + 'r, + where F: FnOnce(SplitStream) -> S + Send + 'r, S: futures::Stream> + Send + 'r { MessageStream { ws: self, handler: Box::new(stream), } @@ -165,7 +166,7 @@ impl WebSocket { /// `Channel` has no methods or functionality beyond its trait implementations. pub struct Channel<'r> { ws: WebSocket, - handler: Box BoxFuture<'r, Result<()>> + Send + 'r>, + handler: Box BoxFuture<'r, Result<()>> + Send + 'r>, } /// A [`Stream`](futures::Stream) of [`Message`]s, returned by @@ -177,7 +178,7 @@ pub struct Channel<'r> { // TODO: Get rid of this or `Channel` via a single `enum`. pub struct MessageStream<'r, S> { ws: WebSocket, - handler: Box) -> S + Send + 'r> + handler: Box) -> S + Send + 'r> } #[rocket::async_trait] @@ -228,8 +229,9 @@ impl<'r, 'o: 'r, S> Responder<'r, 'o> for MessageStream<'o, S> #[rocket::async_trait] impl IoHandler for Channel<'_> { - async fn io(&mut self, io: IoStream) -> io::Result<()> { - let result = (self.handler)(DuplexStream::new(io, self.ws.config).await).await; + async fn io(self: Pin>, io: IoStream) -> io::Result<()> { + let channel = Pin::into_inner(self); + let result = (channel.handler)(DuplexStream::new(io, channel.ws.config).await).await; handle_result(result).map(|_| ()) } } @@ -238,9 +240,10 @@ impl IoHandler for Channel<'_> { impl<'r, S> IoHandler for MessageStream<'r, S> where S: futures::Stream> + Send + 'r { - async fn io(&mut self, io: IoStream) -> io::Result<()> { - let (mut sink, stream) = DuplexStream::new(io, self.ws.config).await.split(); - let mut stream = std::pin::pin!((self.handler)(stream)); + async fn io(self: Pin>, io: IoStream) -> io::Result<()> { + let (mut sink, source) = DuplexStream::new(io, self.ws.config).await.split(); + let handler = Pin::into_inner(self).handler; + let mut stream = std::pin::pin!((handler)(source)); while let Some(msg) = stream.next().await { let result = match msg { Ok(msg) => sink.send(msg).await, diff --git a/core/lib/src/data/io_stream.rs b/core/lib/src/data/io_stream.rs index d965b957ae..0945c5c0f1 100644 --- a/core/lib/src/data/io_stream.rs +++ b/core/lib/src/data/io_stream.rs @@ -42,6 +42,8 @@ enum IoStreamKind { /// to the client. /// /// ```rust +/// use std::pin::Pin; +/// /// use rocket::tokio::io; /// use rocket::data::{IoHandler, IoStream}; /// @@ -49,7 +51,7 @@ enum IoStreamKind { /// /// #[rocket::async_trait] /// impl IoHandler for EchoHandler { -/// async fn io(&mut self, io: IoStream) -> io::Result<()> { +/// async fn io(self: Pin>, io: IoStream) -> io::Result<()> { /// let (mut reader, mut writer) = io::split(io); /// io::copy(&mut reader, &mut writer).await?; /// Ok(()) @@ -66,7 +68,7 @@ enum IoStreamKind { #[crate::async_trait] pub trait IoHandler: Send { /// Performs the raw I/O. - async fn io(&mut self, io: IoStream) -> io::Result<()>; + async fn io(self: Pin>, io: IoStream) -> io::Result<()>; } #[doc(hidden)] diff --git a/core/lib/src/response/response.rs b/core/lib/src/response/response.rs index 2e1fdfa8ea..588497e1c2 100644 --- a/core/lib/src/response/response.rs +++ b/core/lib/src/response/response.rs @@ -1,6 +1,7 @@ use std::{fmt, str}; use std::borrow::Cow; use std::collections::HashMap; +use std::pin::Pin; use tokio::io::{AsyncRead, AsyncSeek}; @@ -276,6 +277,8 @@ impl<'r> Builder<'r> { /// # Example /// /// ```rust + /// use std::pin::Pin; + /// /// use rocket::Response; /// use rocket::data::{IoHandler, IoStream}; /// use rocket::tokio::io; @@ -284,7 +287,7 @@ impl<'r> Builder<'r> { /// /// #[rocket::async_trait] /// impl IoHandler for EchoHandler { - /// async fn io(&mut self, io: IoStream) -> io::Result<()> { + /// async fn io(self: Pin>, io: IoStream) -> io::Result<()> { /// let (mut reader, mut writer) = io::split(io); /// io::copy(&mut reader, &mut writer).await?; /// Ok(()) @@ -485,7 +488,7 @@ pub struct Response<'r> { status: Option, headers: HeaderMap<'r>, body: Body<'r>, - upgrade: HashMap, Box>, + upgrade: HashMap, Pin>>, } impl<'r> Response<'r> { @@ -801,7 +804,7 @@ impl<'r> Response<'r> { pub(crate) fn take_upgrade>( &mut self, protocols: I - ) -> Result, Box)>, ()> { + ) -> Result, Pin>)>, ()> { if self.upgrade.is_empty() { return Ok(None); } @@ -826,6 +829,8 @@ impl<'r> Response<'r> { /// [`upgrade()`](Builder::upgrade()). Otherwise returns `None`. /// /// ```rust + /// use std::pin::Pin; + /// /// use rocket::Response; /// use rocket::data::{IoHandler, IoStream}; /// use rocket::tokio::io; @@ -834,7 +839,7 @@ impl<'r> Response<'r> { /// /// #[rocket::async_trait] /// impl IoHandler for EchoHandler { - /// async fn io(&mut self, io: IoStream) -> io::Result<()> { + /// async fn io(self: Pin>, io: IoStream) -> io::Result<()> { /// let (mut reader, mut writer) = io::split(io); /// io::copy(&mut reader, &mut writer).await?; /// Ok(()) @@ -849,8 +854,8 @@ impl<'r> Response<'r> { /// assert!(response.upgrade("raw-echo").is_some()); /// # }) /// ``` - pub fn upgrade(&mut self, proto: &str) -> Option<&mut (dyn IoHandler + 'r)> { - self.upgrade.get_mut(proto.as_uncased()).map(|h| &mut **h) + pub fn upgrade(&mut self, proto: &str) -> Option> { + self.upgrade.get_mut(proto.as_uncased()).map(|h| h.as_mut()) } /// Returns a mutable borrow of the body of `self`, if there is one. A @@ -957,6 +962,8 @@ impl<'r> Response<'r> { /// # Example /// /// ```rust + /// use std::pin::Pin; + /// /// use rocket::Response; /// use rocket::data::{IoHandler, IoStream}; /// use rocket::tokio::io; @@ -965,7 +972,7 @@ impl<'r> Response<'r> { /// /// #[rocket::async_trait] /// impl IoHandler for EchoHandler { - /// async fn io(&mut self, io: IoStream) -> io::Result<()> { + /// async fn io(self: Pin>, io: IoStream) -> io::Result<()> { /// let (mut reader, mut writer) = io::split(io); /// io::copy(&mut reader, &mut writer).await?; /// Ok(()) @@ -983,7 +990,7 @@ impl<'r> Response<'r> { pub fn add_upgrade(&mut self, protocol: N, handler: H) where N: Into>, H: IoHandler + 'r { - self.upgrade.insert(protocol.into(), Box::new(handler)); + self.upgrade.insert(protocol.into(), Box::pin(handler)); } /// Sets the body's maximum chunk size to `size` bytes. diff --git a/core/lib/src/server.rs b/core/lib/src/server.rs index 6b315ed07e..faae6ddcd1 100644 --- a/core/lib/src/server.rs +++ b/core/lib/src/server.rs @@ -1,6 +1,7 @@ use std::io; use std::sync::Arc; use std::time::Duration; +use std::pin::Pin; use yansi::Paint; use tokio::sync::oneshot; @@ -179,7 +180,7 @@ impl Rocket { &self, mut response: Response<'r>, proto: uncased::Uncased<'r>, - mut io_handler: Box, + io_handler: Pin>, pending_upgrade: hyper::upgrade::OnUpgrade, tx: oneshot::Sender>, ) { From db96f670b7f2cd9f9eeff07e3ea885b9732fb701 Mon Sep 17 00:00:00 2001 From: Sergio Benitez Date: Tue, 4 Apr 2023 15:37:22 -0700 Subject: [PATCH 116/166] Fix contrib 'ws' README formatting. --- contrib/ws/README.md | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/contrib/ws/README.md b/contrib/ws/README.md index b77e98223e..bd889e8950 100644 --- a/contrib/ws/README.md +++ b/contrib/ws/README.md @@ -21,15 +21,15 @@ This crate provides WebSocket support for Rocket via integration with Rocket's 2. Use it! - ```rust - #[get("/echo")] - fn echo_stream(ws: ws::WebSocket) -> ws::Stream!['static] { - ws::stream! { ws => - for await message in ws { - yield message?; - } - } - } - ``` + ```rust + #[get("/echo")] + fn echo_stream(ws: ws::WebSocket) -> ws::Stream!['static] { + ws::stream! { ws => + for await message in ws { + yield message?; + } + } + } + ``` See the [crate docs] for full details. From 03433c10ea77ffc3e1a89821063f8643efdf9a1f Mon Sep 17 00:00:00 2001 From: Sergio Benitez Date: Wed, 5 Apr 2023 09:56:49 -0700 Subject: [PATCH 117/166] Allow specifying 'Status' in custom form errors. Resolves #1694. --- core/http/src/header/media_type.rs | 2 +- core/lib/src/form/error.rs | 49 ++++++++++++++++++++---------- core/lib/src/form/form.rs | 20 ++++++++++++ core/lib/src/form/mod.rs | 3 +- 4 files changed, 56 insertions(+), 18 deletions(-) diff --git a/core/http/src/header/media_type.rs b/core/http/src/header/media_type.rs index e51fea810e..b217dbd5fa 100644 --- a/core/http/src/header/media_type.rs +++ b/core/http/src/header/media_type.rs @@ -112,7 +112,7 @@ macro_rules! media_types { docify!([ Returns @code{true} if the @[top-level] and sublevel types of @code{self} are the same as those of @{"`MediaType::"}! $name - @{"`"}!. + @{"`"}!, i.e @{"`"} @{$t}! @[/]! @{$s}! $(; @{$k}! @[=]! @{$v}!)* @{"`"}!. ]; #[inline(always)] pub fn $check(&self) -> bool { diff --git a/core/lib/src/form/error.rs b/core/lib/src/form/error.rs index c550c14986..7167eb5e7b 100644 --- a/core/lib/src/form/error.rs +++ b/core/lib/src/form/error.rs @@ -160,6 +160,7 @@ pub struct Error<'v> { /// * [`AddrParseError`] => [`ErrorKind::Addr`] /// * [`io::Error`] => [`ErrorKind::Io`] /// * `Box [`ErrorKind::Custom`] +/// * `(Status, Box [`ErrorKind::Custom`] #[derive(Debug)] #[non_exhaustive] pub enum ErrorKind<'v> { @@ -192,8 +193,9 @@ pub enum ErrorKind<'v> { Unexpected, /// An unknown entity was received. Unknown, - /// A custom error occurred. - Custom(Box), + /// A custom error occurred. Status defaults to + /// [`Status::UnprocessableEntity`] if one is not directly specified. + Custom(Status, Box), /// An error while parsing a multipart form occurred. Multipart(multer::Error), /// A string was invalid UTF-8. @@ -350,6 +352,9 @@ impl<'v> Errors<'v> { /// status that is set by the [`Form`](crate::form::Form) data guard on /// failure. /// + /// See [`Error::status()`] for the corresponding status code of each + /// [`Error`] variant. + /// /// # Example /// /// ```rust @@ -677,17 +682,19 @@ impl<'v> Error<'v> { .unwrap_or(false) } - /// Returns the most reasonable `Status` associated with this error. These - /// are: + /// Returns the most reasonable [`Status`] associated with this error. + /// + /// For an [`ErrorKind::Custom`], this is the variant's `Status`, which + /// defaults to [`Status::UnprocessableEntity`]. For all others, it is: /// - /// * **`PayloadTooLarge`** if the error kind is: + /// * **`PayloadTooLarge`** if the [error kind](ErrorKind) is: /// - `InvalidLength` with min of `None` - /// - `Multpart(FieldSizeExceeded | StreamSizeExceeded)` - /// * **`InternalServerError`** if the error kind is: + /// - `Multipart(FieldSizeExceeded)` or `Multipart(StreamSizeExceeded)` + /// * **`InternalServerError`** if the [error kind](ErrorKind) is: /// - `Unknown` - /// * **`BadRequest`** if the error kind is: + /// * **`BadRequest`** if the [error kind](ErrorKind) is: /// - `Io` with an `entity` of `Form` - /// * **`UnprocessableEntity`** otherwise + /// * **`UnprocessableEntity`** for all other variants /// /// # Example /// @@ -716,11 +723,12 @@ impl<'v> Error<'v> { use multer::Error::*; match self.kind { - InvalidLength { min: None, .. } + | InvalidLength { min: None, .. } | Multipart(FieldSizeExceeded { .. }) | Multipart(StreamSizeExceeded { .. }) => Status::PayloadTooLarge, Unknown => Status::InternalServerError, Io(_) | _ if self.entity == Entity::Form => Status::BadRequest, + Custom(status, _) => status, _ => Status::UnprocessableEntity } } @@ -846,7 +854,7 @@ impl fmt::Display for ErrorKind<'_> { ErrorKind::Missing => "missing".fmt(f)?, ErrorKind::Unexpected => "unexpected".fmt(f)?, ErrorKind::Unknown => "unknown internal error".fmt(f)?, - ErrorKind::Custom(e) => e.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)?, @@ -874,7 +882,7 @@ impl crate::http::ext::IntoOwned for ErrorKind<'_> { Missing => Missing, Unexpected => Unexpected, Unknown => Unknown, - Custom(e) => Custom(e), + Custom(s, e) => Custom(s, e), Multipart(e) => Multipart(e), Utf8(e) => Utf8(e), Int(e) => Int(e), @@ -892,7 +900,6 @@ impl crate::http::ext::IntoOwned for ErrorKind<'_> { } } - impl<'a, 'b> PartialEq> for ErrorKind<'a> { fn eq(&self, other: &ErrorKind<'b>) -> bool { use ErrorKind::*; @@ -904,7 +911,7 @@ impl<'a, 'b> PartialEq> for ErrorKind<'a> { (Duplicate, Duplicate) => true, (Missing, Missing) => true, (Unexpected, Unexpected) => true, - (Custom(_), Custom(_)) => true, + (Custom(a, _), Custom(b, _)) => a == b, (Multipart(a), Multipart(b)) => a == b, (Utf8(a), Utf8(b)) => a == b, (Int(a), Int(b)) => a == b, @@ -954,6 +961,17 @@ impl<'a, 'v: 'a, const N: usize> From<&'static [Cow<'v, str>; N]> for ErrorKind< } } +impl<'a> From> for ErrorKind<'a> { + fn from(e: Box) -> Self { + ErrorKind::Custom(Status::UnprocessableEntity, e) + } +} + +impl<'a> From<(Status, Box)> for ErrorKind<'a> { + fn from((status, e): (Status, Box)) -> Self { + ErrorKind::Custom(status, e) + } +} macro_rules! impl_from_for { (<$l:lifetime> $T:ty => $V:ty as $variant:ident) => ( @@ -971,7 +989,6 @@ 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); impl fmt::Display for Entity { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { @@ -1010,7 +1027,7 @@ impl Entity { | ErrorKind::Int(_) | ErrorKind::Float(_) | ErrorKind::Bool(_) - | ErrorKind::Custom(_) + | ErrorKind::Custom(..) | ErrorKind::Addr(_) => Entity::Value, | ErrorKind::Duplicate diff --git a/core/lib/src/form/form.rs b/core/lib/src/form/form.rs index 4d96a5cdc3..c747adc2a1 100644 --- a/core/lib/src/form/form.rs +++ b/core/lib/src/form/form.rs @@ -56,6 +56,26 @@ use crate::form::prelude::*; /// can access fields of `T` transparently through a `Form`, as seen above /// with `user_input.value`. /// +/// ## Errors +/// +/// A `Form` data guard may fail, forward, or succeed. +/// +/// If a request's content-type is neither [`ContentType::Form`] nor +/// [`ContentType::FormData`], the guard **forwards**. +/// +/// If the request `ContentType` _does_ identify as a form but the form data +/// does not parse as `T`, according to `T`'s [`FromForm`] implementation, the +/// guard **fails**. The `Failure` variant contains of the [`Errors`] emitted by +/// `T`'s `FromForm` parser. If the error is not caught by a +/// [`form::Result`](Result) or `Option>` data guard, the status code +/// is set to [`Errors::status()`], and the corresponding error catcher is +/// called. +/// +/// Otherwise the guard **succeeds**. +/// +/// [`ContentType::Form`]: crate::http::ContentType::Form +/// [`ContentType::FormData`]: crate::http::ContentType::FormData +/// /// ## Data Limits /// /// The total amount of data accepted by the `Form` data guard is limited by the diff --git a/core/lib/src/form/mod.rs b/core/lib/src/form/mod.rs index 739c518574..442c455a14 100644 --- a/core/lib/src/form/mod.rs +++ b/core/lib/src/form/mod.rs @@ -45,7 +45,8 @@ //! * `map[k:1]=Bob` //! * `people[bob]nickname=Stan` //! -//! See [`FromForm`] for full details on push-parsing and complete examples. +//! See [`FromForm`] for full details on push-parsing and complete examples, and +//! [`Form`] for how to accept forms in a request handler. // ## Maps w/named Fields (`struct`) // From 07ea3df0c2e00826b81528c8995956308738a912 Mon Sep 17 00:00:00 2001 From: Sergio Benitez Date: Wed, 5 Apr 2023 09:58:21 -0700 Subject: [PATCH 118/166] Fix links to 'Stream!' in 'ws' rustdocs. --- contrib/ws/src/websocket.rs | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/contrib/ws/src/websocket.rs b/contrib/ws/src/websocket.rs index c819f49fb6..7e2f2c4933 100644 --- a/contrib/ws/src/websocket.rs +++ b/contrib/ws/src/websocket.rs @@ -21,10 +21,11 @@ use crate::result::{Result, Error}; /// This is the entrypoint to the library. Every WebSocket response _must_ /// initiate via the `WebSocket` request guard. The guard identifies valid /// WebSocket connection requests and, if the request is valid, succeeds to be -/// converted into a streaming WebSocket response via [`Stream!`], -/// [`WebSocket::channel()`], or [`WebSocket::stream()`]. The connection can be -/// configured via [`WebSocket::config()`]; see [`Config`] for details on -/// configuring a connection. +/// converted into a streaming WebSocket response via +/// [`Stream!`](crate::Stream!), [`WebSocket::channel()`], or +/// [`WebSocket::stream()`]. The connection can be configured via +/// [`WebSocket::config()`]; see [`Config`] for details on configuring a +/// connection. /// /// ### Forwarding /// @@ -123,8 +124,8 @@ impl WebSocket { /// /// This method takes a `FnOnce` `stream` that consumes a read-only stream /// and returns a stream of [`Message`]s. While the returned stream can be - /// constructed in any manner, the [`Stream!`] macro is the preferred - /// method. In any case, the stream must be `Send`. + /// constructed in any manner, the [`Stream!`](crate::Stream!) macro is the + /// preferred method. In any case, the stream must be `Send`. /// /// The returned stream must emit items of type `Result`. Items /// that are `Ok(Message)` are sent to the client while items of type @@ -175,6 +176,8 @@ pub struct Channel<'r> { /// This type should not be used directly. Instead, it is used via the /// [`Stream!`] macro, which expands to both the type itself and an expression /// which evaluates to this type. See [`Stream!`] for details. +/// +/// [`Stream!`]: crate::Stream! // TODO: Get rid of this or `Channel` via a single `enum`. pub struct MessageStream<'r, S> { ws: WebSocket, From c48ce64a775ead8624a4aeacca04bd4def4915f6 Mon Sep 17 00:00:00 2001 From: Sergio Benitez Date: Wed, 5 Apr 2023 10:51:05 -0700 Subject: [PATCH 119/166] Standardize 'response::status' responders. This commit modifies all of the non-empty responders in the `response::status` module so that they look like `Status(pub R)`. Prior to this commit, some responders looked like this, while others contained an `Option`. Resolves #2351. --- core/lib/src/response/status.rs | 300 ++++++++------------------------ site/guide/5-responses.md | 2 +- 2 files changed, 71 insertions(+), 231 deletions(-) diff --git a/core/lib/src/response/status.rs b/core/lib/src/response/status.rs index e995dbf76e..f3abc50c3b 100644 --- a/core/lib/src/response/status.rs +++ b/core/lib/src/response/status.rs @@ -33,7 +33,7 @@ use crate::request::Request; use crate::response::{self, Responder, Response}; use crate::http::Status; -/// Sets the status of the response to 201 (Created). +/// Sets the status of the response to 201 Created. /// /// Sets the `Location` header and optionally the `ETag` header in the response. /// The body of the response, which identifies the created resource, can be set @@ -179,47 +179,7 @@ impl<'r, 'o: 'r, R: Responder<'r, 'o>> Responder<'r, 'o> for Created { } } -/// Sets the status of the response to 202 (Accepted). -/// -/// If a responder is supplied, the remainder of the response is delegated to -/// it. If there is no responder, the body of the response will be empty. -/// -/// # Examples -/// -/// A 202 Accepted response without a body: -/// -/// ```rust -/// use rocket::response::status; -/// -/// # #[allow(unused_variables)] -/// let response = status::Accepted::<()>(None); -/// ``` -/// -/// A 202 Accepted response _with_ a body: -/// -/// ```rust -/// use rocket::response::status; -/// -/// # #[allow(unused_variables)] -/// let response = status::Accepted(Some("processing")); -/// ``` -#[derive(Debug, Clone, PartialEq)] -pub struct Accepted(pub Option); - -/// Sets the status code of the response to 202 Accepted. If the responder is -/// `Some`, it is used to finalize the response. -impl<'r, 'o: 'r, R: Responder<'r, 'o>> Responder<'r, 'o> for Accepted { - fn respond_to(self, req: &'r Request<'_>) -> response::Result<'o> { - let mut build = Response::build(); - if let Some(responder) = self.0 { - build.merge(responder.respond_to(req)?); - } - - build.status(Status::Accepted).ok() - } -} - -/// Sets the status of the response to 204 (No Content). +/// Sets the status of the response to 204 No Content. /// /// The response body will be empty. /// @@ -228,10 +188,13 @@ impl<'r, 'o: 'r, R: Responder<'r, 'o>> Responder<'r, 'o> for Accepted { /// A 204 No Content response: /// /// ```rust +/// # use rocket::get; /// use rocket::response::status; /// -/// # #[allow(unused_variables)] -/// let response = status::NoContent; +/// #[get("/")] +/// fn foo() -> status::NoContent { +/// status::NoContent +/// } /// ``` #[derive(Debug, Clone, PartialEq)] pub struct NoContent; @@ -243,192 +206,9 @@ impl<'r> Responder<'r, 'static> for NoContent { } } -/// Sets the status of the response to 400 (Bad Request). -/// -/// If a responder is supplied, the remainder of the response is delegated to -/// it. If there is no responder, the body of the response will be empty. -/// -/// # Examples -/// -/// A 400 Bad Request response without a body: -/// -/// ```rust -/// use rocket::response::status; -/// -/// # #[allow(unused_variables)] -/// let response = status::BadRequest::<()>(None); -/// ``` -/// -/// A 400 Bad Request response _with_ a body: -/// -/// ```rust -/// use rocket::response::status; -/// -/// # #[allow(unused_variables)] -/// let response = status::BadRequest(Some("error message")); -/// ``` -#[derive(Debug, Clone, PartialEq)] -pub struct BadRequest(pub Option); - -/// Sets the status code of the response to 400 Bad Request. If the responder is -/// `Some`, it is used to finalize the response. -impl<'r, 'o: 'r, R: Responder<'r, 'o>> Responder<'r, 'o> for BadRequest { - fn respond_to(self, req: &'r Request<'_>) -> response::Result<'o> { - let mut build = Response::build(); - if let Some(responder) = self.0 { - build.merge(responder.respond_to(req)?); - } - - build.status(Status::BadRequest).ok() - } -} - -/// Sets the status of the response to 401 (Unauthorized). -/// -/// If a responder is supplied, the remainder of the response is delegated to -/// it. If there is no responder, the body of the response will be empty. -/// -/// # Examples -/// -/// A 401 Unauthorized response without a body: -/// -/// ```rust -/// use rocket::response::status; -/// -/// # #[allow(unused_variables)] -/// let response = status::Unauthorized::<()>(None); -/// ``` -/// -/// A 401 Unauthorized response _with_ a body: -/// -/// ```rust -/// use rocket::response::status; -/// -/// # #[allow(unused_variables)] -/// let response = status::Unauthorized(Some("error message")); -/// ``` -#[derive(Debug, Clone, PartialEq)] -pub struct Unauthorized(pub Option); - -/// Sets the status code of the response to 401 Unauthorized. If the responder is -/// `Some`, it is used to finalize the response. -impl<'r, 'o: 'r, R: Responder<'r, 'o>> Responder<'r, 'o> for Unauthorized { - fn respond_to(self, req: &'r Request<'_>) -> response::Result<'o> { - let mut build = Response::build(); - if let Some(responder) = self.0 { - build.merge(responder.respond_to(req)?); - } - - build.status(Status::Unauthorized).ok() - } -} - -/// Sets the status of the response to 403 (Forbidden). -/// -/// If a responder is supplied, the remainder of the response is delegated to -/// it. If there is no responder, the body of the response will be empty. -/// -/// # Examples -/// -/// A 403 Forbidden response without a body: -/// -/// ```rust -/// use rocket::response::status; -/// -/// # #[allow(unused_variables)] -/// let response = status::Forbidden::<()>(None); -/// ``` -/// -/// A 403 Forbidden response _with_ a body: -/// -/// ```rust -/// use rocket::response::status; -/// -/// # #[allow(unused_variables)] -/// let response = status::Forbidden(Some("error message")); -/// ``` -#[derive(Debug, Clone, PartialEq)] -pub struct Forbidden(pub Option); - -/// Sets the status code of the response to 403 Forbidden. If the responder is -/// `Some`, it is used to finalize the response. -impl<'r, 'o: 'r, R: Responder<'r, 'o>> Responder<'r, 'o> for Forbidden { - fn respond_to(self, req: &'r Request<'_>) -> response::Result<'o> { - let mut build = Response::build(); - if let Some(responder) = self.0 { - build.merge(responder.respond_to(req)?); - } - - build.status(Status::Forbidden).ok() - } -} - -/// Sets the status of the response to 404 (Not Found). -/// -/// The remainder of the response is delegated to the wrapped `Responder`. -/// -/// # Example -/// -/// ```rust -/// use rocket::response::status; -/// -/// # #[allow(unused_variables)] -/// let response = status::NotFound("Sorry, I couldn't find it!"); -/// ``` -#[derive(Debug, Clone, PartialEq)] -pub struct NotFound(pub R); - -/// Sets the status code of the response to 404 Not Found. -impl<'r, 'o: 'r, R: Responder<'r, 'o>> Responder<'r, 'o> for NotFound { - fn respond_to(self, req: &'r Request<'_>) -> response::Result<'o> { - Response::build_from(self.0.respond_to(req)?) - .status(Status::NotFound) - .ok() - } -} - - -/// Sets the status of the response to 409 (Conflict). -/// -/// If a responder is supplied, the remainder of the response is delegated to -/// it. If there is no responder, the body of the response will be empty. -/// -/// # Examples -/// -/// A 409 Conflict response without a body: -/// -/// ```rust -/// use rocket::response::status; -/// -/// # #[allow(unused_variables)] -/// let response = status::Conflict::<()>(None); -/// ``` -/// -/// A 409 Conflict response _with_ a body: -/// -/// ```rust -/// use rocket::response::status; +/// Creates a response with a status code and underlying responder. /// -/// # #[allow(unused_variables)] -/// let response = status::Conflict(Some("error message")); -/// ``` -#[derive(Debug, Clone, PartialEq)] -pub struct Conflict(pub Option); - -/// Sets the status code of the response to 409 Conflict. If the responder is -/// `Some`, it is used to finalize the response. -impl<'r, 'o: 'r, R: Responder<'r, 'o>> Responder<'r, 'o> for Conflict { - fn respond_to(self, req: &'r Request<'_>) -> response::Result<'o> { - let mut build = Response::build(); - if let Some(responder) = self.0 { - build.merge(responder.respond_to(req)?); - } - - build.status(Status::Conflict).ok() - } -} - -/// Creates a response with the given status code and underlying responder. +/// Note that this is equivalent to `(Status, R)`. /// /// # Example /// @@ -437,11 +217,16 @@ impl<'r, 'o: 'r, R: Responder<'r, 'o>> Responder<'r, 'o> for Conflict { /// use rocket::response::status; /// use rocket::http::Status; /// -/// # #[allow(unused_variables)] /// #[get("/")] /// fn handler() -> status::Custom<&'static str> { /// status::Custom(Status::ImATeapot, "Hi!") /// } +/// +/// // This is equivalent to the above. +/// #[get("/")] +/// fn handler2() -> (Status, &'static str) { +/// (Status::ImATeapot, "Hi!") +/// } /// ``` #[derive(Debug, Clone, PartialEq)] pub struct Custom(pub Status, pub R); @@ -449,6 +234,7 @@ pub struct Custom(pub Status, pub R); /// Sets the status code of the response and then delegates the remainder of the /// response to the wrapped responder. impl<'r, 'o: 'r, R: Responder<'r, 'o>> Responder<'r, 'o> for Custom { + #[inline] fn respond_to(self, req: &'r Request<'_>) -> response::Result<'o> { Response::build_from(self.1.respond_to(req)?) .status(self.0) @@ -463,5 +249,59 @@ impl<'r, 'o: 'r, R: Responder<'r, 'o>> Responder<'r, 'o> for (Status, R) { } } +macro_rules! status_response { + ($T:ident $kind:expr) => { + /// Sets the status of the response to + #[doc = concat!($kind, concat!(" ([`Status::", stringify!($T), "`])."))] + /// + /// The remainder of the response is delegated to `self.0`. + /// # Examples + /// + /// A + #[doc = $kind] + /// response without a body: + /// + /// ```rust + /// # use rocket::get; + /// use rocket::response::status; + /// + /// #[get("/")] + #[doc = concat!("fn handler() -> status::", stringify!($T), "<()> {")] + #[doc = concat!(" status::", stringify!($T), "(())")] + /// } + /// ``` + /// + /// A + #[doc = $kind] + /// response _with_ a body: + /// + /// ```rust + /// # use rocket::get; + /// use rocket::response::status; + /// + /// #[get("/")] + #[doc = concat!("fn handler() -> status::", stringify!($T), "<&'static str> {")] + #[doc = concat!(" status::", stringify!($T), "(\"body\")")] + /// } + /// ``` + #[derive(Debug, Clone, PartialEq)] + pub struct $T(pub R); + + impl<'r, 'o: 'r, R: Responder<'r, 'o>> Responder<'r, 'o> for $T { + #[inline(always)] + fn respond_to(self, req: &'r Request<'_>) -> response::Result<'o> { + Custom(Status::$T, self.0).respond_to(req) + } + } + } +} + +status_response!(Accepted "202 Accepted"); +status_response!(BadRequest "400 Bad Request"); +status_response!(Unauthorized "401 Unauthorized"); +status_response!(Forbidden "403 Forbidden"); +status_response!(NotFound "404 NotFound"); +status_response!(Conflict "409 Conflict"); + // The following are unimplemented. // 206 Partial Content (variant), 203 Non-Authoritative Information (headers). diff --git a/site/guide/5-responses.md b/site/guide/5-responses.md index 8fbe9e8dea..c5bf662bc4 100644 --- a/site/guide/5-responses.md +++ b/site/guide/5-responses.md @@ -43,7 +43,7 @@ use rocket::response::status; #[post("/")] fn new(id: usize) -> status::Accepted { - status::Accepted(Some(format!("id: '{}'", id))) + status::Accepted(format!("id: '{}'", id)) } ``` From 80b77553178bd634bfba0a8d117f941bba941e5d Mon Sep 17 00:00:00 2001 From: Sergio Benitez Date: Wed, 5 Apr 2023 11:15:49 -0700 Subject: [PATCH 120/166] Properly forward 'deprecated' items in codegen. Resolves #2262. --- core/codegen/src/attribute/route/mod.rs | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/core/codegen/src/attribute/route/mod.rs b/core/codegen/src/attribute/route/mod.rs index f7579f5bea..40797e5d41 100644 --- a/core/codegen/src/attribute/route/mod.rs +++ b/core/codegen/src/attribute/route/mod.rs @@ -324,6 +324,7 @@ fn codegen_route(route: Route) -> Result { // Gather info about the function. let (vis, handler_fn) = (&route.handler.vis, &route.handler); + let deprecated = handler_fn.attrs.iter().find(|a| a.path().is_ident("deprecated")); let handler_fn_name = &handler_fn.sig.ident; let internal_uri_macro = internal_uri_macro_decl(&route); let responder_outcome = responder_outcome_expr(&route); @@ -337,13 +338,13 @@ fn codegen_route(route: Route) -> Result { #handler_fn #[doc(hidden)] - #[allow(non_camel_case_types)] + #[allow(nonstandard_style)] /// Rocket code generated proxy structure. - #vis struct #handler_fn_name { } + #deprecated #vis struct #handler_fn_name { } /// Rocket code generated proxy static conversion implementations. + #[allow(nonstandard_style, deprecated)] impl #handler_fn_name { - #[allow(non_snake_case, unreachable_patterns, unreachable_code)] fn into_info(self) -> #_route::StaticInfo { fn monomorphized_function<'__r>( #__req: &'__r #Request<'_>, From 89534129de8739689e113c35fae503ec5ba086ee Mon Sep 17 00:00:00 2001 From: Sergio Benitez Date: Wed, 5 Apr 2023 12:45:48 -0700 Subject: [PATCH 121/166] Add 'TempFile::open()' to stream its data. Resolves #2296. --- core/lib/src/fs/temp_file.rs | 61 ++++++++++++++++++++++++++++++------ 1 file changed, 52 insertions(+), 9 deletions(-) diff --git a/core/lib/src/fs/temp_file.rs b/core/lib/src/fs/temp_file.rs index 437e4d20b9..6924b40c0f 100644 --- a/core/lib/src/fs/temp_file.rs +++ b/core/lib/src/fs/temp_file.rs @@ -10,7 +10,7 @@ use crate::fs::FileName; use tokio::task; use tokio::fs::{self, File}; -use tokio::io::AsyncWriteExt; +use tokio::io::{AsyncWriteExt, AsyncBufRead, BufReader}; use tempfile::{NamedTempFile, TempPath}; use either::Either; @@ -110,7 +110,7 @@ pub enum TempFile<'v> { }, #[doc(hidden)] Buffered { - content: &'v str, + content: &'v [u8], } } @@ -160,7 +160,7 @@ impl<'v> TempFile<'v> { /// /// Ok(()) /// } - /// # let file = TempFile::Buffered { content: "hi".into() }; + /// # let file = TempFile::Buffered { content: "hi".as_bytes() }; /// # rocket::async_test(handle(file)).unwrap(); /// ``` pub async fn persist_to

(&mut self, path: P) -> io::Result<()> @@ -190,7 +190,7 @@ impl<'v> TempFile<'v> { } TempFile::Buffered { content } => { let mut file = File::create(&new_path).await?; - file.write_all(content.as_bytes()).await?; + file.write_all(content).await?; *self = TempFile::File { file_name: None, content_type: None, @@ -231,7 +231,7 @@ impl<'v> TempFile<'v> { /// /// Ok(()) /// } - /// # let file = TempFile::Buffered { content: "hi".into() }; + /// # let file = TempFile::Buffered { content: "hi".as_bytes() }; /// # rocket::async_test(handle(file)).unwrap(); /// ``` pub async fn copy_to

(&mut self, path: P) -> io::Result<()> @@ -258,7 +258,7 @@ impl<'v> TempFile<'v> { TempFile::Buffered { content } => { let path = path.as_ref(); let mut file = File::create(path).await?; - file.write_all(content.as_bytes()).await?; + file.write_all(content).await?; *self = TempFile::File { file_name: None, content_type: None, @@ -296,7 +296,7 @@ impl<'v> TempFile<'v> { /// /// Ok(()) /// } - /// # let file = TempFile::Buffered { content: "hi".into() }; + /// # let file = TempFile::Buffered { content: "hi".as_bytes() }; /// # rocket::async_test(handle(file)).unwrap(); /// ``` pub async fn move_copy_to

(&mut self, path: P) -> io::Result<()> @@ -313,6 +313,49 @@ impl<'v> TempFile<'v> { Ok(()) } + /// Open the file for reading, returning an `async` stream of the file. + /// + /// This method should be used sparingly. `TempFile` is intended to be used + /// when the incoming data is destined to be stored on disk. If the incoming + /// data is intended to be streamed elsewhere, prefer to implement a custom + /// form guard via [`FromFormField`] that directly streams the incoming data + /// to the ultimate destination. + /// + /// # Example + /// + /// ```rust + /// # #[macro_use] extern crate rocket; + /// use rocket::fs::TempFile; + /// use rocket::tokio::io; + /// + /// #[post("/", data = "")] + /// async fn handle(file: TempFile<'_>) -> std::io::Result<()> { + /// let mut stream = file.open().await?; + /// io::copy(&mut stream, &mut io::stdout()).await?; + /// Ok(()) + /// } + /// # let file = TempFile::Buffered { content: "hi".as_bytes() }; + /// # rocket::async_test(handle(file)).unwrap(); + /// ``` + pub async fn open(&self) -> io::Result { + use tokio_util::either::Either; + + match self { + TempFile::File { path, .. } => { + let path = match path { + either::Either::Left(p) => p.as_ref(), + either::Either::Right(p) => p.as_path(), + }; + + let reader = BufReader::new(File::open(path).await?); + Ok(Either::Left(reader)) + }, + TempFile::Buffered { content } => { + Ok(Either::Right(*content)) + }, + } + } + /// Returns the size, in bytes, of the file. /// /// This method does not perform any system calls. @@ -353,7 +396,7 @@ impl<'v> TempFile<'v> { /// /// Ok(()) /// } - /// # let file = TempFile::Buffered { content: "hi".into() }; + /// # let file = TempFile::Buffered { content: "hi".as_bytes() }; /// # rocket::async_test(handle(file)).unwrap(); /// ``` pub fn path(&self) -> Option<&Path> { @@ -474,7 +517,7 @@ impl<'v> TempFile<'v> { 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)) + Ok(Capped::new(TempFile::Buffered { content: field.value.as_bytes() }, n)) } async fn from_data( From a82508b403420bd941c32ddec3ee3e4875f2b8a5 Mon Sep 17 00:00:00 2001 From: Sergio Benitez Date: Wed, 5 Apr 2023 13:04:39 -0700 Subject: [PATCH 122/166] Set 'Secure' cookie flag by default under TLS. If TLS is enabled and active, Rocket will now set the `Secure` cookie attribute by default. Resolves #2425. --- core/lib/src/cookies.rs | 25 +++++++++++++++++++------ examples/tls/Cargo.toml | 2 +- examples/tls/src/tests.rs | 29 +++++++++++++++++++++++++++++ 3 files changed, 49 insertions(+), 7 deletions(-) diff --git a/core/lib/src/cookies.rs b/core/lib/src/cookies.rs index 9e375d2edd..032d4eae08 100644 --- a/core/lib/src/cookies.rs +++ b/core/lib/src/cookies.rs @@ -279,6 +279,8 @@ impl<'a> CookieJar<'a> { /// * `path`: `"/"` /// * `SameSite`: `Strict` /// + /// Furthermore, if TLS is enabled, the `Secure` cookie flag is set. + /// /// # Example /// /// ```rust @@ -298,7 +300,7 @@ impl<'a> CookieJar<'a> { /// } /// ``` pub fn add(&self, mut cookie: Cookie<'static>) { - Self::set_defaults(&mut cookie); + Self::set_defaults(self.config, &mut cookie); self.ops.lock().push(Op::Add(cookie, false)); } @@ -316,8 +318,9 @@ impl<'a> CookieJar<'a> { /// * `HttpOnly`: `true` /// * `Expires`: 1 week from now /// - /// These defaults ensure maximum usability and security. For additional - /// security, you may wish to set the `secure` flag. + /// Furthermore, if TLS is enabled, the `Secure` cookie flag is set. These + /// defaults ensure maximum usability and security. For additional security, + /// you may wish to set the `secure` flag. /// /// # Example /// @@ -333,7 +336,7 @@ impl<'a> CookieJar<'a> { #[cfg(feature = "secrets")] #[cfg_attr(nightly, doc(cfg(feature = "secrets")))] pub fn add_private(&self, mut cookie: Cookie<'static>) { - Self::set_private_defaults(&mut cookie); + Self::set_private_defaults(self.config, &mut cookie); self.ops.lock().push(Op::Add(cookie, true)); } @@ -473,7 +476,8 @@ impl<'a> CookieJar<'a> { /// * `path`: `"/"` /// * `SameSite`: `Strict` /// - fn set_defaults(cookie: &mut Cookie<'static>) { + /// Furthermore, if TLS is enabled, the `Secure` cookie flag is set. + fn set_defaults(config: &Config, cookie: &mut Cookie<'static>) { if cookie.path().is_none() { cookie.set_path("/"); } @@ -481,6 +485,10 @@ impl<'a> CookieJar<'a> { if cookie.same_site().is_none() { cookie.set_same_site(SameSite::Strict); } + + if cookie.secure().is_none() && config.tls_enabled() { + cookie.set_secure(true); + } } /// For each property mentioned below, this method checks if there is a @@ -492,9 +500,10 @@ impl<'a> CookieJar<'a> { /// * `HttpOnly`: `true` /// * `Expires`: 1 week from now /// + /// Furthermore, if TLS is enabled, the `Secure` cookie flag is set. #[cfg(feature = "secrets")] #[cfg_attr(nightly, doc(cfg(feature = "secrets")))] - fn set_private_defaults(cookie: &mut Cookie<'static>) { + fn set_private_defaults(config: &Config, cookie: &mut Cookie<'static>) { if cookie.path().is_none() { cookie.set_path("/"); } @@ -510,6 +519,10 @@ impl<'a> CookieJar<'a> { if cookie.expires().is_none() { cookie.set_expires(time::OffsetDateTime::now_utc() + time::Duration::weeks(1)); } + + if cookie.secure().is_none() && config.tls_enabled() { + cookie.set_secure(true); + } } } diff --git a/examples/tls/Cargo.toml b/examples/tls/Cargo.toml index 50eb6511e5..11bcc1ff2f 100644 --- a/examples/tls/Cargo.toml +++ b/examples/tls/Cargo.toml @@ -6,4 +6,4 @@ edition = "2021" publish = false [dependencies] -rocket = { path = "../../core/lib", features = ["tls", "mtls"] } +rocket = { path = "../../core/lib", features = ["tls", "mtls", "secrets"] } diff --git a/examples/tls/src/tests.rs b/examples/tls/src/tests.rs index 171b5b5def..2f48af0274 100644 --- a/examples/tls/src/tests.rs +++ b/examples/tls/src/tests.rs @@ -21,6 +21,35 @@ fn hello_mutual() { } } +#[test] +fn secure_cookies() { + use rocket::http::{CookieJar, Cookie}; + + #[get("/cookie")] + fn cookie(jar: &CookieJar<'_>) { + jar.add(Cookie::new("k1", "v1")); + jar.add_private(Cookie::new("k2", "v2")); + + jar.add(Cookie::build("k1u", "v1u").secure(false).finish()); + jar.add_private(Cookie::build("k2u", "v2u").secure(false).finish()); + } + + let client = Client::tracked(super::rocket().mount("/", routes![cookie])).unwrap(); + let response = client.get("/cookie").dispatch(); + + let c1 = response.cookies().get("k1").unwrap(); + assert_eq!(c1.secure(), Some(true)); + + let c2 = response.cookies().get_private("k2").unwrap(); + assert_eq!(c2.secure(), Some(true)); + + let c1 = response.cookies().get("k1u").unwrap(); + assert_ne!(c1.secure(), Some(true)); + + let c2 = response.cookies().get_private("k2u").unwrap(); + assert_ne!(c2.secure(), Some(true)); +} + #[test] fn hello_world() { let profiles = [ From 0a5631260789032c13514794a795c6e2f5494e6d Mon Sep 17 00:00:00 2001 From: Sergio Benitez Date: Thu, 6 Apr 2023 18:57:49 -0700 Subject: [PATCH 123/166] Implement more conservative URI normalization. * Trailing slashes are now allowed in all normalized URI paths, except for route attribute URIs: `/foo/` is considered normalized. * Query parts of URIs may now be empty: `/foo?` and `/foo/?` are now considered normalized. * The `base` field of `Catcher` is now only accessible via a new getter method: `Catcher::base()`. * `RawStr::split()` returns a `DoubleEndedIterator`. * Introduced a second normalization for `Origin`, "nontrailing", and associated methods: `Origin::normalize_nontrailing()`, and `Origin::is_normalized_nontrailing()`. * Added `Origin::has_trailing_slash()`. * The `Segments` iterator will now return an empty string if there is a trailing slash in the referenced path. * `Segments::len()` is now `Segments::num()`. * Added `RawStr::trim()`. Resolves #2512. --- benchmarks/src/routing.rs | 4 +- core/codegen/src/attribute/route/mod.rs | 2 +- core/codegen/src/attribute/route/parse.rs | 6 +- core/codegen/src/bang/uri_parsing.rs | 2 +- core/codegen/tests/route.rs | 6 +- core/codegen/tests/typed-uris.rs | 27 +- .../tests/ui-fail-nightly/async-entry.stderr | 5 +- .../route-path-bad-syntax.stderr | 24 + .../route-path-bad-syntax.stderr | 21 + .../tests/ui-fail/route-path-bad-syntax.rs | 13 +- core/http/src/ext.rs | 2 +- core/http/src/raw_str.rs | 38 +- core/http/src/uri/absolute.rs | 43 +- core/http/src/uri/origin.rs | 219 ++++-- core/http/src/uri/path_query.rs | 165 +++-- core/http/src/uri/reference.rs | 41 +- core/http/src/uri/segments.rs | 41 +- core/http/src/uri/uri.rs | 32 + core/lib/fuzz/Cargo.toml | 7 +- core/lib/fuzz/targets/uri-normalization.rs | 23 + core/lib/src/catcher/catcher.rs | 60 +- core/lib/src/request/request.rs | 10 +- core/lib/src/rocket.rs | 10 +- core/lib/src/route/segment.rs | 15 +- core/lib/src/route/uri.rs | 72 +- core/lib/src/router/collider.rs | 624 ++++++------------ core/lib/src/router/matcher.rs | 257 ++++++++ core/lib/src/router/mod.rs | 1 + core/lib/src/router/router.rs | 42 +- core/lib/src/sentinel.rs | 2 +- examples/hello/src/main.rs | 4 + 31 files changed, 1123 insertions(+), 695 deletions(-) create mode 100644 core/lib/fuzz/targets/uri-normalization.rs create mode 100644 core/lib/src/router/matcher.rs diff --git a/benchmarks/src/routing.rs b/benchmarks/src/routing.rs index c4fb8238a9..1a6dafcd8b 100644 --- a/benchmarks/src/routing.rs +++ b/benchmarks/src/routing.rs @@ -45,13 +45,13 @@ fn generate_matching_requests<'c>(client: &'c Client, routes: &[Route]) -> Vec(client: &'c Client, route: &Route) -> LocalRequest<'c> { - let path = route.uri.origin.path() + let path = route.uri.uri.path() .raw_segments() .map(staticify_segment) .collect::>() .join("/"); - let query = route.uri.origin.query() + let query = route.uri.uri.query() .map(|q| q.raw_segments()) .into_iter() .flatten() diff --git a/core/codegen/src/attribute/route/mod.rs b/core/codegen/src/attribute/route/mod.rs index 40797e5d41..1b66e9ce07 100644 --- a/core/codegen/src/attribute/route/mod.rs +++ b/core/codegen/src/attribute/route/mod.rs @@ -158,7 +158,7 @@ fn param_guard_decl(guard: &Guard) -> TokenStream { #_Err(__error) => return #parse_error, }, #_None => { - #_log::error_!("Internal invariant broken: dyn param not found."); + #_log::error_!("Internal invariant broken: dyn param {} not found.", #i); #_log::error_!("Please report this to the Rocket issue tracker."); #_log::error_!("https://github.com/SergioBenitez/Rocket/issues"); return #Outcome::Forward(#__data); diff --git a/core/codegen/src/attribute/route/parse.rs b/core/codegen/src/attribute/route/parse.rs index 59cbd4e34d..888951615c 100644 --- a/core/codegen/src/attribute/route/parse.rs +++ b/core/codegen/src/attribute/route/parse.rs @@ -77,9 +77,11 @@ impl FromMeta for RouteUri { .help("expected URI in origin form: \"/path/\"") })?; - if !origin.is_normalized() { - let normalized = origin.clone().into_normalized(); + if !origin.is_normalized_nontrailing() { + let normalized = origin.clone().into_normalized_nontrailing(); let span = origin.path().find("//") + .or_else(|| origin.has_trailing_slash() + .then_some(origin.path().len() - 1)) .or_else(|| origin.query() .and_then(|q| q.find("&&")) .map(|i| origin.path().len() + 1 + i)) diff --git a/core/codegen/src/bang/uri_parsing.rs b/core/codegen/src/bang/uri_parsing.rs index d7c772dc59..e6ae1e23ad 100644 --- a/core/codegen/src/bang/uri_parsing.rs +++ b/core/codegen/src/bang/uri_parsing.rs @@ -309,7 +309,7 @@ impl Parse for InternalUriParams { // Validation should always succeed since this macro can only be called // if the route attribute succeeded, implying a valid route URI. let route_uri = Origin::parse_route(&route_uri_str) - .map(|o| o.into_normalized().into_owned()) + .map(|o| o.into_normalized_nontrailing().into_owned()) .map_err(|_| input.error("internal error: invalid route URI"))?; let content; diff --git a/core/codegen/tests/route.rs b/core/codegen/tests/route.rs index db74a45b3b..6fe269aa00 100644 --- a/core/codegen/tests/route.rs +++ b/core/codegen/tests/route.rs @@ -334,8 +334,9 @@ fn test_inclusive_segments() { assert_eq!(get("/"), "empty+"); assert_eq!(get("//"), "empty+"); - assert_eq!(get("//a/"), "empty+a"); - assert_eq!(get("//a//"), "empty+a"); + assert_eq!(get("//a"), "empty+a"); + assert_eq!(get("//a/"), "empty+a/"); + assert_eq!(get("//a//"), "empty+a/"); assert_eq!(get("//a//c/d"), "empty+a/c/d"); assert_eq!(get("//a/b"), "nonempty+"); @@ -343,4 +344,5 @@ fn test_inclusive_segments() { assert_eq!(get("//a/b//c"), "nonempty+c"); assert_eq!(get("//a//b////c"), "nonempty+c"); assert_eq!(get("//a//b////c/d/e"), "nonempty+c/d/e"); + assert_eq!(get("//a//b////c/d/e/"), "nonempty+c/d/e/"); } diff --git a/core/codegen/tests/typed-uris.rs b/core/codegen/tests/typed-uris.rs index da9eb6709c..bef5eedfa5 100644 --- a/core/codegen/tests/typed-uris.rs +++ b/core/codegen/tests/typed-uris.rs @@ -14,7 +14,7 @@ macro_rules! assert_uri_eq { let actual = $uri; let expected = rocket::http::uri::Uri::parse_any($expected).expect("valid URI"); if actual != expected { - panic!("URI mismatch: got {}, expected {}\nGot) {:?}\nExpected) {:?}", + panic!("\nURI mismatch: got {}, expected {}\nGot) {:?}\nExpected) {:?}\n", actual, expected, actual, expected); } )+ @@ -186,6 +186,7 @@ fn check_simple_named() { fn check_route_prefix_suffix() { assert_uri_eq! { uri!(index) => "/", + uri!("/") => "/", uri!("/", index) => "/", uri!("/hi", index) => "/hi", uri!("/", simple3(10)) => "/?id=10", @@ -194,21 +195,33 @@ fn check_route_prefix_suffix() { uri!("/mount", simple(id = 23)) => "/mount/23", uri!("/another", simple(100)) => "/another/100", uri!("/another", simple(id = 23)) => "/another/23", + uri!("/foo") => "/foo", + uri!("/foo/") => "/foo/", + uri!("/foo///") => "/foo/", + uri!("/foo/bar/") => "/foo/bar/", + uri!("/foo/", index) => "/foo/", + uri!("/foo", index) => "/foo", } assert_uri_eq! { uri!("http://rocket.rs", index) => "http://rocket.rs", - uri!("http://rocket.rs/", index) => "http://rocket.rs", - uri!("http://rocket.rs", index) => "http://rocket.rs", + uri!("http://rocket.rs/", index) => "http://rocket.rs/", + uri!("http://rocket.rs/foo", index) => "http://rocket.rs/foo", + uri!("http://rocket.rs/foo/", index) => "http://rocket.rs/foo/", uri!("http://", index) => "http://", uri!("ftp:", index) => "ftp:/", } assert_uri_eq! { uri!("http://rocket.rs", index, "?foo") => "http://rocket.rs?foo", - uri!("http://rocket.rs/", index, "#bar") => "http://rocket.rs#bar", + uri!("http://rocket.rs", index, "?") => "http://rocket.rs?", + uri!("http://rocket.rs", index, "#") => "http://rocket.rs#", + uri!("http://rocket.rs/", index, "?") => "http://rocket.rs/?", + uri!("http://rocket.rs/", index, "#") => "http://rocket.rs/#", + uri!("http://rocket.rs", index, "#bar") => "http://rocket.rs#bar", + uri!("http://rocket.rs/", index, "#bar") => "http://rocket.rs/#bar", uri!("http://rocket.rs", index, "?bar#baz") => "http://rocket.rs?bar#baz", - uri!("http://rocket.rs/", index, "?bar#baz") => "http://rocket.rs?bar#baz", + uri!("http://rocket.rs/", index, "?bar#baz") => "http://rocket.rs/?bar#baz", uri!("http://", index, "?foo") => "http://?foo", uri!("http://rocket.rs", simple3(id = 100), "?foo") => "http://rocket.rs?id=100", uri!("http://rocket.rs", simple3(id = 100), "?foo#bar") => "http://rocket.rs?id=100#bar", @@ -239,8 +252,8 @@ fn check_route_prefix_suffix() { let dyn_abs = uri!("http://rocket.rs?foo"); assert_uri_eq! { uri!(_, index, dyn_abs.clone()) => "/?foo", - uri!("http://rocket.rs/", index, dyn_abs.clone()) => "http://rocket.rs?foo", uri!("http://rocket.rs", index, dyn_abs.clone()) => "http://rocket.rs?foo", + uri!("http://rocket.rs/", index, dyn_abs.clone()) => "http://rocket.rs/?foo", uri!("http://", index, dyn_abs.clone()) => "http://?foo", uri!(_, simple3(id = 123), dyn_abs) => "/?id=123", } @@ -248,8 +261,8 @@ fn check_route_prefix_suffix() { let dyn_ref = uri!("?foo#bar"); assert_uri_eq! { uri!(_, index, dyn_ref.clone()) => "/?foo#bar", - uri!("http://rocket.rs/", index, dyn_ref.clone()) => "http://rocket.rs?foo#bar", uri!("http://rocket.rs", index, dyn_ref.clone()) => "http://rocket.rs?foo#bar", + uri!("http://rocket.rs/", index, dyn_ref.clone()) => "http://rocket.rs/?foo#bar", uri!("http://", index, dyn_ref.clone()) => "http://?foo#bar", uri!(_, simple3(id = 123), dyn_ref) => "/?id=123#bar", } diff --git a/core/codegen/tests/ui-fail-nightly/async-entry.stderr b/core/codegen/tests/ui-fail-nightly/async-entry.stderr index dc7cf6124c..3ae1393b85 100644 --- a/core/codegen/tests/ui-fail-nightly/async-entry.stderr +++ b/core/codegen/tests/ui-fail-nightly/async-entry.stderr @@ -141,8 +141,9 @@ error[E0308]: mismatched types --> tests/ui-fail-nightly/async-entry.rs:24:21 | 24 | async fn main() { - | ^ expected `()` because of default return type - | _____________________| + | ^ + | | + | _____________________expected `()` because of default return type | | 25 | | rocket::build() 26 | | } 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 c59c4ebc5b..f322538b5d 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 @@ -240,3 +240,27 @@ warning: `segment` starts with `<` but does not end with `>` | ^^^^^^^^ | = help: perhaps you meant the dynamic parameter ``? + +error: route URIs cannot contain empty segments + --> tests/ui-fail-nightly/route-path-bad-syntax.rs:107:10 + | +107 | #[get("/a/")] + | ^^ + | + = note: expected "/a", found "/a/" + +error: route URIs cannot contain empty segments + --> tests/ui-fail-nightly/route-path-bad-syntax.rs:110:12 + | +110 | #[get("/a/b/")] + | ^^ + | + = note: expected "/a/b", found "/a/b/" + +error: route URIs cannot contain empty segments + --> tests/ui-fail-nightly/route-path-bad-syntax.rs:113:14 + | +113 | #[get("/a/b/c/")] + | ^^ + | + = note: expected "/a/b/c", found "/a/b/c/" 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 c48ccadc01..bc4b8ccc94 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 @@ -180,3 +180,24 @@ error: parameters cannot be empty | 93 | #[get("/<>")] | ^^^^^ + +error: route URIs cannot contain empty segments + --- note: expected "/a", found "/a/" + --> tests/ui-fail-stable/route-path-bad-syntax.rs:107:7 + | +107 | #[get("/a/")] + | ^^^^^ + +error: route URIs cannot contain empty segments + --- note: expected "/a/b", found "/a/b/" + --> tests/ui-fail-stable/route-path-bad-syntax.rs:110:7 + | +110 | #[get("/a/b/")] + | ^^^^^^^ + +error: route URIs cannot contain empty segments + --- note: expected "/a/b/c", found "/a/b/c/" + --> tests/ui-fail-stable/route-path-bad-syntax.rs:113:7 + | +113 | #[get("/a/b/c/")] + | ^^^^^^^^^ diff --git a/core/codegen/tests/ui-fail/route-path-bad-syntax.rs b/core/codegen/tests/ui-fail/route-path-bad-syntax.rs index eb7b02e5c9..9eea869be7 100644 --- a/core/codegen/tests/ui-fail/route-path-bad-syntax.rs +++ b/core/codegen/tests/ui-fail/route-path-bad-syntax.rs @@ -54,7 +54,7 @@ fn h3() {} #[get("/<_r>/")] fn h4() {} - +// // Check dynamic parameters are valid idents #[get("/")] @@ -102,4 +102,15 @@ fn m2() {} #[get("/<>name><")] fn m3() {} +// New additions for trailing paths, which we artificially disallow. + +#[get("/a/")] +fn n1() {} + +#[get("/a/b/")] +fn n2() {} + +#[get("/a/b/c/")] +fn n3() {} + fn main() { } diff --git a/core/http/src/ext.rs b/core/http/src/ext.rs index 1e263d4e6d..b1b40eaa40 100644 --- a/core/http/src/ext.rs +++ b/core/http/src/ext.rs @@ -126,7 +126,6 @@ impl IntoOwned for (A, B) { } } - impl IntoOwned for Cow<'_, B> { type Owned = Cow<'static, B>; @@ -149,6 +148,7 @@ macro_rules! impl_into_owned_self { )*) } +impl_into_owned_self!(bool); impl_into_owned_self!(u8, u16, u32, u64, usize); impl_into_owned_self!(i8, i16, i32, i64, isize); diff --git a/core/http/src/raw_str.rs b/core/http/src/raw_str.rs index b22d6c880e..1aa37b2bfa 100644 --- a/core/http/src/raw_str.rs +++ b/core/http/src/raw_str.rs @@ -180,6 +180,11 @@ impl RawStr { /// ``` #[inline(always)] pub fn percent_decode(&self) -> Result, Utf8Error> { + // don't let `percent-encoding` return a random empty string + if self.is_empty() { + return Ok(self.as_str().into()); + } + self._percent_decode().decode_utf8() } @@ -213,6 +218,11 @@ impl RawStr { /// ``` #[inline(always)] pub fn percent_decode_lossy(&self) -> Cow<'_, str> { + // don't let `percent-encoding` return a random empty string + if self.is_empty() { + return self.as_str().into(); + } + self._percent_decode().decode_utf8_lossy() } @@ -658,7 +668,6 @@ impl RawStr { pat.is_suffix_of(self.as_str()) } - /// Returns the byte index of the first character of this string slice that /// matches the pattern. /// @@ -710,8 +719,9 @@ impl RawStr { /// assert_eq!(v, ["Mary", "had", "a", "little", "lamb"]); /// ``` #[inline] - pub fn split<'a, P>(&'a self, pat: P) -> impl Iterator - where P: Pattern<'a> + pub fn split<'a, P>(&'a self, pat: P) -> impl DoubleEndedIterator + where P: Pattern<'a>, +

>::Searcher: stable_pattern::DoubleEndedSearcher<'a> { let split: Split<'_, P> = Split(SplitInternal { start: 0, @@ -837,6 +847,28 @@ impl RawStr { suffix.strip_suffix_of(self.as_str()).map(RawStr::new) } + /// Returns a string slice with leading and trailing whitespace removed. + /// + /// 'Whitespace' is defined according to the terms of the Unicode Derived + /// Core Property `White_Space`, which includes newlines. + /// + /// # Examples + /// + /// Basic usage: + /// + /// ``` + /// # extern crate rocket; + /// use rocket::http::RawStr; + /// + /// let s = RawStr::new("\n Hello\tworld\t\n"); + /// + /// assert_eq!("Hello\tworld", s.trim()); + /// ``` + #[inline] + pub fn trim(&self) -> &RawStr { + RawStr::new(self.as_str().trim_matches(|c: char| c.is_whitespace())) + } + /// Parses this string slice into another type. /// /// Because `parse` is so general, it can cause problems with type diff --git a/core/http/src/uri/absolute.rs b/core/http/src/uri/absolute.rs index 8d5dbaf530..e084e63cec 100644 --- a/core/http/src/uri/absolute.rs +++ b/core/http/src/uri/absolute.rs @@ -24,9 +24,9 @@ use crate::uri::{Authority, Path, Query, Data, Error, as_utf8_unchecked, fmt}; /// Rocket prefers _normalized_ absolute URIs, an absolute URI with the /// following properties: /// -/// * The path and query, if any, are normalized with no empty segments. -/// * If there is an authority, the path is empty or absolute with more than -/// one character. +/// * If there is an authority, the path is empty or absolute. +/// * The path and query, if any, are normalized with no empty segments except +/// optionally for one trailing slash. /// /// The [`Absolute::is_normalized()`] method checks for normalization while /// [`Absolute::into_normalized()`] normalizes any absolute URI. @@ -38,8 +38,13 @@ use crate::uri::{Authority, Path, Query, Data, Error, as_utf8_unchecked, fmt}; /// # use rocket::http::uri::Absolute; /// # let valid_uris = [ /// "http://rocket.rs", +/// "http://rocket.rs/", +/// "ftp:/a/b/", +/// "ftp:/a/b/?", /// "scheme:/foo/bar", -/// "scheme:/foo/bar?abc", +/// "scheme:/foo/bar/", +/// "scheme:/foo/bar/?", +/// "scheme:/foo/bar/?abc", /// # ]; /// # for uri in &valid_uris { /// # let uri = Absolute::parse(uri).unwrap(); @@ -53,11 +58,9 @@ use crate::uri::{Authority, Path, Query, Data, Error, as_utf8_unchecked, fmt}; /// # extern crate rocket; /// # use rocket::http::uri::Absolute; /// # let invalid = [ -/// "http://rocket.rs/", // trailing '/' -/// "ftp:/a/b/", // trailing empty segment /// "ftp:/a//c//d", // two empty segments -/// "ftp:/a/b/?", // empty path segment /// "ftp:/?foo&", // trailing empty query segment +/// "ftp:/?fooa&&b", // empty query segment /// # ]; /// # for uri in &invalid { /// # assert!(!Absolute::parse(uri).unwrap().is_normalized()); @@ -263,17 +266,15 @@ impl<'a> Absolute<'a> { /// assert!(Absolute::parse("http://").unwrap().is_normalized()); /// assert!(Absolute::parse("http://foo.rs/foo/bar").unwrap().is_normalized()); /// assert!(Absolute::parse("foo:bar").unwrap().is_normalized()); + /// assert!(Absolute::parse("git://rocket.rs/").unwrap().is_normalized()); /// - /// assert!(!Absolute::parse("git://rocket.rs/").unwrap().is_normalized()); /// assert!(!Absolute::parse("http:/foo//bar").unwrap().is_normalized()); /// assert!(!Absolute::parse("foo:bar?baz&&bop").unwrap().is_normalized()); /// ``` pub fn is_normalized(&self) -> bool { let normalized_query = self.query().map_or(true, |q| q.is_normalized()); if self.authority().is_some() && !self.path().is_empty() { - self.path().is_normalized(true) - && self.path() != "/" - && normalized_query + self.path().is_normalized(true) && normalized_query } else { self.path().is_normalized(false) && normalized_query } @@ -287,9 +288,10 @@ impl<'a> Absolute<'a> { /// ```rust /// use rocket::http::uri::Absolute; /// + /// let mut uri = Absolute::parse("git://rocket.rs").unwrap(); + /// assert!(uri.is_normalized()); + /// /// let mut uri = Absolute::parse("git://rocket.rs/").unwrap(); - /// assert!(!uri.is_normalized()); - /// uri.normalize(); /// assert!(uri.is_normalized()); /// /// let mut uri = Absolute::parse("http:/foo//bar").unwrap(); @@ -304,18 +306,18 @@ impl<'a> Absolute<'a> { /// ``` pub fn normalize(&mut self) { if self.authority().is_some() && !self.path().is_empty() { - if self.path() == "/" { - self.set_path(""); - } else if !self.path().is_normalized(true) { - self.path = self.path().to_normalized(true); + if !self.path().is_normalized(true) { + self.path = self.path().to_normalized(true, true); } } else { - self.path = self.path().to_normalized(false); + if !self.path().is_normalized(false) { + self.path = self.path().to_normalized(false, true); + } } if let Some(query) = self.query() { if !query.is_normalized() { - self.query = query.to_normalized(); + self.query = Some(query.to_normalized()); } } } @@ -328,8 +330,7 @@ impl<'a> Absolute<'a> { /// use rocket::http::uri::Absolute; /// /// let mut uri = Absolute::parse("git://rocket.rs/").unwrap(); - /// assert!(!uri.is_normalized()); - /// assert!(uri.into_normalized().is_normalized()); + /// assert!(uri.is_normalized()); /// /// let mut uri = Absolute::parse("http:/foo//bar").unwrap(); /// assert!(!uri.is_normalized()); diff --git a/core/http/src/uri/origin.rs b/core/http/src/uri/origin.rs index fa2c214e4d..716dd1635d 100644 --- a/core/http/src/uri/origin.rs +++ b/core/http/src/uri/origin.rs @@ -27,8 +27,8 @@ use crate::{RawStr, RawStrBuf}; /// # Normalization /// /// Rocket prefers, and will sometimes require, origin URIs to be _normalized_. -/// A normalized origin URI is a valid origin URI that contains zero empty -/// segments except when there are no segments. +/// A normalized origin URI is a valid origin URI that contains no empty +/// segments except optionally a trailing slash. /// /// As an example, the following URIs are all valid, normalized URIs: /// @@ -37,9 +37,14 @@ use crate::{RawStr, RawStrBuf}; /// # use rocket::http::uri::Origin; /// # let valid_uris = [ /// "/", +/// "/?", +/// "/a/b/", /// "/a/b/c", +/// "/a/b/c/", +/// "/a/b/c?", /// "/a/b/c?q", /// "/hello?lang=en", +/// "/hello/?lang=en", /// "/some%20thing?q=foo&lang=fr", /// # ]; /// # for uri in &valid_uris { @@ -53,8 +58,7 @@ use crate::{RawStr, RawStrBuf}; /// # extern crate rocket; /// # use rocket::http::uri::Origin; /// # let invalid = [ -/// "//", // one empty segment -/// "/a/b/", // trailing empty segment +/// "//", // an empty segment /// "/a/ab//c//d", // two empty segments /// "/?a&&b", // empty query segment /// "/?foo&", // trailing empty query segment @@ -72,10 +76,10 @@ use crate::{RawStr, RawStrBuf}; /// # use rocket::http::uri::Origin; /// # let invalid = [ /// // non-normal versions -/// "//", "/a/b/", "/a/ab//c//d", "/a?a&&b&", +/// "//", "/a/b//c", "/a/ab//c//d/", "/a?a&&b&", /// /// // normalized versions -/// "/", "/a/b", "/a/ab/c/d", "/a?a&b", +/// "/", "/a/b/c", "/a/ab/c/d/", "/a?a&b", /// # ]; /// # for i in 0..(invalid.len() / 2) { /// # let abnormal = Origin::parse(invalid[i]).unwrap(); @@ -219,9 +223,11 @@ impl<'a> Origin<'a> { }); } - let (path, query) = RawStr::new(string).split_at_byte(b'?'); - let query = (!query.is_empty()).then(|| query.as_str()); - Ok(Origin::new(path.as_str(), query)) + let (path, query) = string.split_once('?') + .map(|(path, query)| (path, Some(query))) + .unwrap_or((string, None)); + + Ok(Origin::new(path, query)) } /// Parses the string `string` into an `Origin`. Never allocates on success. @@ -376,6 +382,18 @@ impl<'a> Origin<'a> { self.path().is_normalized(true) && self.query().map_or(true, |q| q.is_normalized()) } + fn _normalize(&mut self, allow_trail: bool) { + if !self.path().is_normalized(true) { + self.path = self.path().to_normalized(true, allow_trail); + } + + if let Some(query) = self.query() { + if !query.is_normalized() { + self.query = Some(query.to_normalized()); + } + } + } + /// Normalizes `self`. This is a no-op if `self` is already normalized. /// /// See [Normalization](#normalization) for more information on what it @@ -393,15 +411,7 @@ impl<'a> Origin<'a> { /// assert!(abnormal.is_normalized()); /// ``` pub fn normalize(&mut self) { - if !self.path().is_normalized(true) { - self.path = self.path().to_normalized(true); - } - - if let Some(query) = self.query() { - if !query.is_normalized() { - self.query = query.to_normalized(); - } - } + self._normalize(true); } /// Consumes `self` and returns a normalized version. @@ -424,6 +434,116 @@ impl<'a> Origin<'a> { self.normalize(); self } + + /// Returns `true` if `self` has a _trailing_ slash. + /// + /// This is defined as `path.len() > 1` && `path.ends_with('/')`. This + /// implies that the URI `/` is _not_ considered to have a trailing slash. + /// + /// # Example + /// + /// ```rust + /// # #[macro_use] extern crate rocket; + /// + /// assert!(!uri!("/").has_trailing_slash()); + /// assert!(!uri!("/a").has_trailing_slash()); + /// assert!(!uri!("/foo/bar/baz").has_trailing_slash()); + /// + /// assert!(uri!("/a/").has_trailing_slash()); + /// assert!(uri!("/foo/").has_trailing_slash()); + /// assert!(uri!("/foo/bar/baz/").has_trailing_slash()); + /// ``` + pub fn has_trailing_slash(&self) -> bool { + self.path().len() > 1 && self.path().ends_with('/') + } + + /// Returns `true` if `self` is normalized ([`Origin::is_normalized()`]) and + /// **does not** have a trailing slash ([Origin::has_trailing_slash()]). + /// Otherwise returns `false`. + /// + /// # Example + /// + /// ```rust + /// # #[macro_use] extern crate rocket; + /// use rocket::http::uri::Origin; + /// + /// let origin = Origin::parse("/").unwrap(); + /// assert!(origin.is_normalized_nontrailing()); + /// + /// let origin = Origin::parse("/foo/bar").unwrap(); + /// assert!(origin.is_normalized_nontrailing()); + /// + /// let origin = Origin::parse("//").unwrap(); + /// assert!(!origin.is_normalized_nontrailing()); + /// + /// let origin = Origin::parse("/foo/bar//baz/").unwrap(); + /// assert!(!origin.is_normalized_nontrailing()); + /// + /// let origin = Origin::parse("/foo/bar/").unwrap(); + /// assert!(!origin.is_normalized_nontrailing()); + /// ``` + pub fn is_normalized_nontrailing(&self) -> bool { + self.is_normalized() && !self.has_trailing_slash() + } + + /// Converts `self` into a normalized origin path without a trailing slash. + /// Does nothing is `self` is already [`normalized_nontrailing`]. + /// + /// [`normalized_nontrailing`]: Origin::is_normalized_nontrailing() + /// + /// # Example + /// + /// ```rust + /// # #[macro_use] extern crate rocket; + /// use rocket::http::uri::Origin; + /// + /// let origin = Origin::parse("/").unwrap(); + /// assert!(origin.is_normalized_nontrailing()); + /// + /// let normalized = origin.into_normalized_nontrailing(); + /// assert_eq!(normalized, uri!("/")); + /// + /// let origin = Origin::parse("//").unwrap(); + /// assert!(!origin.is_normalized_nontrailing()); + /// + /// let normalized = origin.into_normalized_nontrailing(); + /// assert_eq!(normalized, uri!("/")); + /// + /// let origin = Origin::parse_owned("/foo/bar//baz/".into()).unwrap(); + /// assert!(!origin.is_normalized_nontrailing()); + /// + /// let normalized = origin.into_normalized_nontrailing(); + /// assert_eq!(normalized, uri!("/foo/bar/baz")); + /// + /// let origin = Origin::parse("/foo/bar/").unwrap(); + /// assert!(!origin.is_normalized_nontrailing()); + /// + /// let normalized = origin.into_normalized_nontrailing(); + /// assert_eq!(normalized, uri!("/foo/bar")); + /// ``` + pub fn into_normalized_nontrailing(mut self) -> Self { + if !self.is_normalized_nontrailing() { + if self.is_normalized() && self.has_trailing_slash() { + let indexed = match self.path.value { + IndexedStr::Indexed(i, j) => IndexedStr::Indexed(i, j - 1), + IndexedStr::Concrete(cow) => IndexedStr::Concrete(match cow { + Cow::Borrowed(s) => Cow::Borrowed(&s[..s.len() - 1]), + Cow::Owned(mut s) => Cow::Owned({ s.pop(); s }), + }) + }; + + self.path = Data { + value: indexed, + decoded_segments: state::Storage::new(), + }; + } else { + self._normalize(false); + } + } + + self + + } } impl_serde!(Origin<'a>, "an origin-form URI"); @@ -448,7 +568,7 @@ mod tests { fn seg_count(path: &str, expected: usize) -> bool { let origin = Origin::parse(path).unwrap(); let segments = origin.path().segments(); - let actual = segments.len(); + let actual = segments.num(); if actual != expected { eprintln!("Count mismatch: expected {}, got {}.", expected, actual); eprintln!("{}", if actual != expected { "lifetime" } else { "buf" }); @@ -479,26 +599,24 @@ mod tests { #[test] fn simple_segment_count() { - assert!(seg_count("/", 0)); + assert!(seg_count("/", 1)); assert!(seg_count("/a", 1)); - assert!(seg_count("/a/", 1)); - assert!(seg_count("/a/", 1)); + assert!(seg_count("/a/", 2)); assert!(seg_count("/a/b", 2)); - assert!(seg_count("/a/b/", 2)); - assert!(seg_count("/a/b/", 2)); - assert!(seg_count("/ab/", 1)); + assert!(seg_count("/a/b/", 3)); + assert!(seg_count("/ab/", 2)); } #[test] fn segment_count() { - assert!(seg_count("////", 0)); - assert!(seg_count("//a//", 1)); - assert!(seg_count("//abc//", 1)); - assert!(seg_count("//abc/def/", 2)); - assert!(seg_count("//////abc///def//////////", 2)); + assert!(seg_count("////", 1)); + assert!(seg_count("//a//", 2)); + assert!(seg_count("//abc//", 2)); + assert!(seg_count("//abc/def/", 3)); + assert!(seg_count("//////abc///def//////////", 3)); assert!(seg_count("/a/b/c/d/e/f/g", 7)); assert!(seg_count("/a/b/c/d/e/f/g", 7)); - assert!(seg_count("/a/b/c/d/e/f/g/", 7)); + assert!(seg_count("/a/b/c/d/e/f/g/", 8)); assert!(seg_count("/a/b/cdjflk/d/e/f/g", 7)); assert!(seg_count("//aaflja/b/cdjflk/d/e/f/g", 7)); assert!(seg_count("/a/b", 2)); @@ -506,18 +624,18 @@ mod tests { #[test] fn single_segments_match() { - assert!(eq_segments("/", &[])); + assert!(eq_segments("/", &[""])); assert!(eq_segments("/a", &["a"])); - assert!(eq_segments("/a/", &["a"])); - assert!(eq_segments("///a/", &["a"])); - assert!(eq_segments("///a///////", &["a"])); - assert!(eq_segments("/a///////", &["a"])); + assert!(eq_segments("/a/", &["a", ""])); + assert!(eq_segments("///a/", &["a", ""])); + assert!(eq_segments("///a///////", &["a", ""])); + assert!(eq_segments("/a///////", &["a", ""])); assert!(eq_segments("//a", &["a"])); assert!(eq_segments("/abc", &["abc"])); - assert!(eq_segments("/abc/", &["abc"])); - assert!(eq_segments("///abc/", &["abc"])); - assert!(eq_segments("///abc///////", &["abc"])); - assert!(eq_segments("/abc///////", &["abc"])); + assert!(eq_segments("/abc/", &["abc", ""])); + assert!(eq_segments("///abc/", &["abc", ""])); + assert!(eq_segments("///abc///////", &["abc", ""])); + assert!(eq_segments("/abc///////", &["abc", ""])); assert!(eq_segments("//abc", &["abc"])); } @@ -529,10 +647,11 @@ mod tests { assert!(eq_segments("/a/b/c/d", &["a", "b", "c", "d"])); assert!(eq_segments("///a///////d////c", &["a", "d", "c"])); assert!(eq_segments("/abc/abc", &["abc", "abc"])); - assert!(eq_segments("/abc/abc/", &["abc", "abc"])); + assert!(eq_segments("/abc/abc/", &["abc", "abc", ""])); assert!(eq_segments("///abc///////a", &["abc", "a"])); assert!(eq_segments("/////abc/b", &["abc", "b"])); assert!(eq_segments("//abc//c////////d", &["abc", "c", "d"])); + assert!(eq_segments("//abc//c////////d/", &["abc", "c", "d", ""])); } #[test] @@ -548,6 +667,8 @@ mod tests { assert!(!eq_segments("/a/b", &["b", "a"])); assert!(!eq_segments("/a/a/b", &["a", "b"])); assert!(!eq_segments("///a/", &[])); + assert!(!eq_segments("///a/", &["a"])); + assert!(!eq_segments("///a/", &["a", "a"])); } fn test_query(uri: &str, query: Option<&str>) { @@ -575,20 +696,4 @@ mod tests { test_query("/?", Some("")); test_query("/?hi", Some("hi")); } - - #[test] - fn normalized() { - let uri_to_string = |s| Origin::parse(s) - .unwrap() - .into_normalized() - .to_string(); - - assert_eq!(uri_to_string("/"), "/".to_string()); - assert_eq!(uri_to_string("//"), "/".to_string()); - assert_eq!(uri_to_string("//////a/"), "/a".to_string()); - assert_eq!(uri_to_string("//ab"), "/ab".to_string()); - assert_eq!(uri_to_string("//a"), "/a".to_string()); - assert_eq!(uri_to_string("/a/b///c"), "/a/b/c".to_string()); - assert_eq!(uri_to_string("/a///b/c/d///"), "/a/b/c/d".to_string()); - } } diff --git a/core/http/src/uri/path_query.rs b/core/http/src/uri/path_query.rs index 54725709b7..2b9ea23dfc 100644 --- a/core/http/src/uri/path_query.rs +++ b/core/http/src/uri/path_query.rs @@ -1,6 +1,5 @@ use std::hash::Hash; use std::borrow::Cow; -use std::fmt::Write; use state::Storage; @@ -57,9 +56,9 @@ fn decode_to_indexed_str( 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_else(|| IndexedStr::from(Cow::Borrowed(""))) + let checked = IndexedStr::checked_from(b, source.as_str()); + debug_assert!(checked.is_some(), "\nunindexed {:?} in {:?} {:?}", b, indexed, source); + checked.unwrap_or_else(|| IndexedStr::from(Cow::Borrowed(""))) } cow => IndexedStr::from(Cow::Owned(cow.into_owned())), } @@ -94,24 +93,37 @@ impl<'a> Path<'a> { self.raw().as_str() } - /// Whether `self` is normalized, i.e, it has no empty segments. + /// Whether `self` is normalized, i.e, it has no empty segments except the + /// last one. /// /// If `absolute`, then a starting `/` is required. pub(crate) fn is_normalized(&self, absolute: bool) -> bool { - (!absolute || self.raw().starts_with('/')) - && self.raw_segments().all(|s| !s.is_empty()) + if absolute && !self.raw().starts_with('/') { + return false; + } + + self.raw_segments() + .rev() + .skip(1) + .all(|s| !s.is_empty()) } - /// Normalizes `self`. If `absolute`, a starting `/` is required. - pub(crate) fn to_normalized(self, absolute: bool) -> Data<'static, fmt::Path> { - let mut path = String::with_capacity(self.raw().len()); - let absolute = absolute || self.raw().starts_with('/'); - for (i, seg) in self.raw_segments().filter(|s| !s.is_empty()).enumerate() { - if absolute || i != 0 { path.push('/'); } - let _ = write!(path, "{}", seg); + /// Normalizes `self`. If `absolute`, a starting `/` is required. If + /// `trail`, a trailing slash is allowed. Otherwise it is not. + pub(crate) fn to_normalized(self, absolute: bool, trail: bool) -> Data<'static, fmt::Path> { + let raw = self.raw().trim(); + let mut path = String::with_capacity(raw.len()); + + if absolute || raw.starts_with('/') { + path.push('/'); + } + + for (i, segment) in self.raw_segments().filter(|s| !s.is_empty()).enumerate() { + if i != 0 { path.push('/'); } + path.push_str(segment.as_str()); } - if path.is_empty() && absolute { + if trail && raw.len() > 1 && raw.ends_with('/') && !path.ends_with('/') { path.push('/'); } @@ -121,8 +133,8 @@ impl<'a> Path<'a> { } } - /// Returns an iterator over the raw, undecoded segments. Segments may be - /// empty. + /// Returns an iterator over the raw, undecoded segments, potentially empty + /// segments. /// /// ### Example /// @@ -131,38 +143,41 @@ impl<'a> Path<'a> { /// use rocket::http::uri::Origin; /// /// let uri = Origin::parse("/").unwrap(); - /// assert_eq!(uri.path().raw_segments().count(), 0); + /// let segments: Vec<_> = uri.path().raw_segments().collect(); + /// assert_eq!(segments, &[""]); /// /// let uri = Origin::parse("//").unwrap(); /// let segments: Vec<_> = uri.path().raw_segments().collect(); /// assert_eq!(segments, &["", ""]); /// + /// let uri = Origin::parse("/foo").unwrap(); + /// let segments: Vec<_> = uri.path().raw_segments().collect(); + /// assert_eq!(segments, &["foo"]); + /// + /// let uri = Origin::parse("/a/").unwrap(); + /// let segments: Vec<_> = uri.path().raw_segments().collect(); + /// assert_eq!(segments, &["a", ""]); + /// /// // Recall that `uri!()` normalizes static inputs. /// let uri = uri!("//"); - /// assert_eq!(uri.path().raw_segments().count(), 0); - /// - /// let uri = Origin::parse("/a").unwrap(); /// let segments: Vec<_> = uri.path().raw_segments().collect(); - /// assert_eq!(segments, &["a"]); + /// assert_eq!(segments, &[""]); /// /// let uri = Origin::parse("/a//b///c/d?query¶m").unwrap(); /// let segments: Vec<_> = uri.path().raw_segments().collect(); /// assert_eq!(segments, &["a", "", "b", "", "", "c", "d"]); /// ``` - #[inline(always)] - pub fn raw_segments(&self) -> impl Iterator { - let path = match self.raw() { - p if p.is_empty() || p == "/" => None, - p if p.starts_with(fmt::Path::DELIMITER) => Some(&p[1..]), - p => Some(p) - }; - - path.map(|p| p.split(fmt::Path::DELIMITER)) - .into_iter() - .flatten() + #[inline] + pub fn raw_segments(&self) -> impl DoubleEndedIterator { + let raw = self.raw().trim(); + raw.strip_prefix(fmt::Path::DELIMITER) + .unwrap_or(raw) + .split(fmt::Path::DELIMITER) } - /// Returns a (smart) iterator over the non-empty, percent-decoded segments. + /// Returns a (smart) iterator over the percent-decoded segments. Empty + /// segments between non-empty segments are skipped. A trailing slash will + /// result in an empty segment emitted as the final item. /// /// # Example /// @@ -170,20 +185,52 @@ impl<'a> Path<'a> { /// # #[macro_use] extern crate rocket; /// use rocket::http::uri::Origin; /// + /// let uri = Origin::parse("/").unwrap(); + /// let path_segs: Vec<&str> = uri.path().segments().collect(); + /// assert_eq!(path_segs, &[""]); + /// + /// let uri = Origin::parse("/a").unwrap(); + /// let path_segs: Vec<&str> = uri.path().segments().collect(); + /// assert_eq!(path_segs, &["a"]); + /// + /// let uri = Origin::parse("/a/").unwrap(); + /// let path_segs: Vec<&str> = uri.path().segments().collect(); + /// assert_eq!(path_segs, &["a", ""]); + /// + /// let uri = Origin::parse("/foo/bar").unwrap(); + /// let path_segs: Vec<&str> = uri.path().segments().collect(); + /// assert_eq!(path_segs, &["foo", "bar"]); + /// + /// let uri = Origin::parse("/foo///bar").unwrap(); + /// let path_segs: Vec<&str> = uri.path().segments().collect(); + /// assert_eq!(path_segs, &["foo", "bar"]); + /// + /// let uri = Origin::parse("/foo///bar//").unwrap(); + /// let path_segs: Vec<&str> = uri.path().segments().collect(); + /// assert_eq!(path_segs, &["foo", "bar", ""]); + /// /// let uri = Origin::parse("/a%20b/b%2Fc/d//e?query=some").unwrap(); /// let path_segs: Vec<&str> = uri.path().segments().collect(); /// assert_eq!(path_segs, &["a b", "b/c", "d", "e"]); /// ``` pub fn segments(&self) -> Segments<'a, fmt::Path> { + let raw = self.raw(); let cached = self.data.decoded_segments.get_or_set(|| { - let (indexed, path) = (&self.data.value, self.raw()); - self.raw_segments() - .filter(|r| !r.is_empty()) - .map(|s| decode_to_indexed_str::(s, (indexed, path))) - .collect() + let mut segments = vec![]; + let mut raw_segments = self.raw_segments().peekable(); + while let Some(s) = raw_segments.next() { + // Only allow an empty segment if it's the last one. + if s.is_empty() && raw_segments.peek().is_some() { + continue; + } + + segments.push(decode_to_indexed_str::(s, (&self.data.value, raw))); + } + + segments }); - Segments::new(self.raw(), cached) + Segments::new(raw, cached) } } @@ -218,30 +265,26 @@ impl<'a> Query<'a> { /// Whether `self` is normalized, i.e, it has no empty segments. pub(crate) fn is_normalized(&self) -> bool { - !self.is_empty() && self.raw_segments().all(|s| !s.is_empty()) + self.raw_segments().all(|s| !s.is_empty()) } /// Normalizes `self`. - pub(crate) fn to_normalized(self) -> Option> { - let mut query = String::with_capacity(self.raw().len()); + pub(crate) fn to_normalized(self) -> Data<'static, fmt::Query> { + let mut query = String::with_capacity(self.raw().trim().len()); for (i, seg) in self.raw_segments().filter(|s| !s.is_empty()).enumerate() { if i != 0 { query.push('&'); } - let _ = write!(query, "{}", seg); - } - - if query.is_empty() { - return None; + query.push_str(seg.as_str()); } - Some(Data { + Data { value: IndexedStr::from(Cow::Owned(query)), decoded_segments: Storage::new(), - }) + } } - /// Returns an iterator over the non-empty, undecoded `(name, value)` pairs - /// of this query. If there is no query, the iterator is empty. Segments may - /// be empty. + /// Returns an iterator over the undecoded, potentially empty `(name, + /// value)` pairs of this query. If there is no query, the iterator is + /// empty. /// /// # Example /// @@ -252,18 +295,26 @@ impl<'a> Query<'a> { /// let uri = Origin::parse("/").unwrap(); /// assert!(uri.query().is_none()); /// + /// let uri = Origin::parse("/?").unwrap(); + /// let query_segs: Vec<_> = uri.query().unwrap().raw_segments().collect(); + /// assert!(query_segs.is_empty()); + /// + /// let uri = Origin::parse("/?foo").unwrap(); + /// let query_segs: Vec<_> = uri.query().unwrap().raw_segments().collect(); + /// assert_eq!(query_segs, &["foo"]); + /// /// let uri = Origin::parse("/?a=b&dog").unwrap(); /// let query_segs: Vec<_> = uri.query().unwrap().raw_segments().collect(); /// assert_eq!(query_segs, &["a=b", "dog"]); /// - /// // This is not normalized, so the query is `""`, the empty string. /// let uri = Origin::parse("/?&").unwrap(); /// let query_segs: Vec<_> = uri.query().unwrap().raw_segments().collect(); /// assert_eq!(query_segs, &["", ""]); /// - /// // Recall that `uri!()` normalizes. + /// // Recall that `uri!()` normalizes, so this is equivalent to `/?`. /// let uri = uri!("/?&"); - /// assert!(uri.query().is_none()); + /// let query_segs: Vec<_> = uri.query().unwrap().raw_segments().collect(); + /// assert!(query_segs.is_empty()); /// /// // These are raw and undecoded. Use `segments()` for decoded variant. /// let uri = Origin::parse("/foo/bar?a+b%2F=some+one%40gmail.com&&%26%3D2").unwrap(); @@ -272,7 +323,7 @@ impl<'a> Query<'a> { /// ``` #[inline] pub fn raw_segments(&self) -> impl Iterator { - let query = match self.raw() { + let query = match self.raw().trim() { q if q.is_empty() => None, q => Some(q) }; diff --git a/core/http/src/uri/reference.rs b/core/http/src/uri/reference.rs index 58eb06fd88..c3b2fa2c94 100644 --- a/core/http/src/uri/reference.rs +++ b/core/http/src/uri/reference.rs @@ -264,15 +264,17 @@ impl<'a> Reference<'a> { /// /// ```rust /// # #[macro_use] extern crate rocket; + /// let uri = uri!("http://rocket.rs/guide"); + /// assert!(uri.query().is_none()); + /// + /// let uri = uri!("http://rocket.rs/guide?"); + /// assert_eq!(uri.query().unwrap(), ""); + /// /// let uri = uri!("http://rocket.rs/guide?foo#bar"); /// assert_eq!(uri.query().unwrap(), "foo"); /// /// let uri = uri!("http://rocket.rs/guide?q=bar"); /// assert_eq!(uri.query().unwrap(), "q=bar"); - /// - /// // Empty query parts are normalized away by `uri!()`. - /// let uri = uri!("http://rocket.rs/guide?#bar"); - /// assert!(uri.query().is_none()); /// ``` #[inline(always)] pub fn query(&self) -> Option> { @@ -316,23 +318,23 @@ impl<'a> Reference<'a> { /// assert!(Reference::parse("http://foo.rs/foo/bar").unwrap().is_normalized()); /// assert!(Reference::parse("foo:bar#baz").unwrap().is_normalized()); /// assert!(Reference::parse("http://rocket.rs#foo").unwrap().is_normalized()); + /// assert!(Reference::parse("http://?").unwrap().is_normalized()); + /// assert!(Reference::parse("git://rocket.rs/").unwrap().is_normalized()); + /// assert!(Reference::parse("http://rocket.rs?#foo").unwrap().is_normalized()); + /// assert!(Reference::parse("http://rocket.rs#foo").unwrap().is_normalized()); /// - /// assert!(!Reference::parse("http://?").unwrap().is_normalized()); - /// assert!(!Reference::parse("git://rocket.rs/").unwrap().is_normalized()); /// assert!(!Reference::parse("http:/foo//bar").unwrap().is_normalized()); /// assert!(!Reference::parse("foo:bar?baz&&bop#c").unwrap().is_normalized()); - /// assert!(!Reference::parse("http://rocket.rs?#foo").unwrap().is_normalized()); /// /// // Recall that `uri!()` normalizes static input. - /// assert!(uri!("http://rocket.rs#foo").is_normalized()); + /// assert!(uri!("http:/foo//bar").is_normalized()); + /// assert!(uri!("foo:bar?baz&&bop#c").is_normalized()); /// assert!(uri!("http://rocket.rs///foo////bar#cat").is_normalized()); /// ``` pub fn is_normalized(&self) -> bool { let normalized_query = self.query().map_or(true, |q| q.is_normalized()); if self.authority().is_some() && !self.path().is_empty() { - self.path().is_normalized(true) - && self.path() != "/" - && normalized_query + self.path().is_normalized(true) && normalized_query } else { self.path().is_normalized(false) && normalized_query } @@ -347,8 +349,6 @@ impl<'a> Reference<'a> { /// use rocket::http::uri::Reference; /// /// let mut uri = Reference::parse("git://rocket.rs/").unwrap(); - /// assert!(!uri.is_normalized()); - /// uri.normalize(); /// assert!(uri.is_normalized()); /// /// let mut uri = Reference::parse("http:/foo//bar?baz&&#cat").unwrap(); @@ -363,18 +363,18 @@ impl<'a> Reference<'a> { /// ``` pub fn normalize(&mut self) { if self.authority().is_some() && !self.path().is_empty() { - if self.path() == "/" { - self.set_path(""); - } else if !self.path().is_normalized(true) { - self.path = self.path().to_normalized(true); + if !self.path().is_normalized(true) { + self.path = self.path().to_normalized(true, true); } } else { - self.path = self.path().to_normalized(false); + if !self.path().is_normalized(false) { + self.path = self.path().to_normalized(false, true); + } } if let Some(query) = self.query() { if !query.is_normalized() { - self.query = query.to_normalized(); + self.query = Some(query.to_normalized()); } } } @@ -387,7 +387,7 @@ impl<'a> Reference<'a> { /// use rocket::http::uri::Reference; /// /// let mut uri = Reference::parse("git://rocket.rs/").unwrap(); - /// assert!(!uri.is_normalized()); + /// assert!(uri.is_normalized()); /// assert!(uri.into_normalized().is_normalized()); /// /// let mut uri = Reference::parse("http:/foo//bar?baz&&#cat").unwrap(); @@ -403,6 +403,7 @@ impl<'a> Reference<'a> { self } + #[allow(unused)] pub(crate) fn set_path

(&mut self, path: P) where P: Into> { diff --git a/core/http/src/uri/segments.rs b/core/http/src/uri/segments.rs index f42596ebc3..9d62edc51d 100644 --- a/core/http/src/uri/segments.rs +++ b/core/http/src/uri/segments.rs @@ -28,7 +28,7 @@ use crate::uri::error::PathError; /// _ => panic!("only four segments") /// } /// } -/// # assert_eq!(uri.path().segments().len(), 4); +/// # assert_eq!(uri.path().segments().num(), 4); /// # assert_eq!(uri.path().segments().count(), 4); /// # assert_eq!(uri.path().segments().next(), Some("a z")); /// ``` @@ -55,19 +55,19 @@ impl Segments<'_, P> { /// let uri = uri!("/foo/bar?baz&cat&car"); /// /// let mut segments = uri.path().segments(); - /// assert_eq!(segments.len(), 2); + /// assert_eq!(segments.num(), 2); /// /// segments.next(); - /// assert_eq!(segments.len(), 1); + /// assert_eq!(segments.num(), 1); /// /// segments.next(); - /// assert_eq!(segments.len(), 0); + /// assert_eq!(segments.num(), 0); /// /// segments.next(); - /// assert_eq!(segments.len(), 0); + /// assert_eq!(segments.num(), 0); /// ``` #[inline] - pub fn len(&self) -> usize { + pub fn num(&self) -> usize { let max_pos = std::cmp::min(self.pos, self.segments.len()); self.segments.len() - max_pos } @@ -89,7 +89,7 @@ impl Segments<'_, P> { /// ``` #[inline] pub fn is_empty(&self) -> bool { - self.len() == 0 + self.num() == 0 } /// Returns a new `Segments` with `n` segments skipped. @@ -101,11 +101,11 @@ impl Segments<'_, P> { /// let uri = uri!("/foo/bar/baz/cat"); /// /// let mut segments = uri.path().segments(); - /// assert_eq!(segments.len(), 4); + /// assert_eq!(segments.num(), 4); /// assert_eq!(segments.next(), Some("foo")); /// /// let mut segments = segments.skip(2); - /// assert_eq!(segments.len(), 1); + /// assert_eq!(segments.num(), 1); /// assert_eq!(segments.next(), Some("cat")); /// ``` #[inline] @@ -143,6 +143,21 @@ impl<'a> Segments<'a, Path> { /// /// ```rust /// # #[macro_use] extern crate rocket; + /// let a = uri!("/"); + /// let b = uri!("/"); + /// assert!(a.path().segments().prefix_of(b.path().segments())); + /// assert!(b.path().segments().prefix_of(a.path().segments())); + /// + /// let a = uri!("/"); + /// let b = uri!("/foo"); + /// assert!(a.path().segments().prefix_of(b.path().segments())); + /// assert!(!b.path().segments().prefix_of(a.path().segments())); + /// + /// let a = uri!("/foo"); + /// let b = uri!("/foo/"); + /// assert!(a.path().segments().prefix_of(b.path().segments())); + /// assert!(!b.path().segments().prefix_of(a.path().segments())); + /// /// let a = uri!("/foo/bar/baaaz/cat"); /// let b = uri!("/foo/bar"); /// @@ -155,11 +170,11 @@ impl<'a> Segments<'a, Path> { /// ``` #[inline] pub fn prefix_of(self, other: Segments<'_, Path>) -> bool { - if self.len() > other.len() { + if self.num() > other.num() { return false; } - self.zip(other).all(|(a, b)| a == b) + self.zip(other).all(|(a, b)| a.is_empty() || a == b) } /// Creates a `PathBuf` from `self`. The returned `PathBuf` is @@ -271,11 +286,11 @@ macro_rules! impl_iterator { } fn size_hint(&self) -> (usize, Option) { - (self.len(), Some(self.len())) + (self.num(), Some(self.num())) } fn count(self) -> usize { - self.len() + self.num() } } ) diff --git a/core/http/src/uri/uri.rs b/core/http/src/uri/uri.rs index 913bf08afc..57e992ea00 100644 --- a/core/http/src/uri/uri.rs +++ b/core/http/src/uri/uri.rs @@ -467,3 +467,35 @@ macro_rules! impl_base_traits { } } } + +mod tests { + #[test] + fn normalization() { + fn normalize(uri: &str) -> String { + use crate::uri::Uri; + + match Uri::parse_any(uri).unwrap() { + Uri::Origin(uri) => uri.into_normalized().to_string(), + Uri::Absolute(uri) => uri.into_normalized().to_string(), + Uri::Reference(uri) => uri.into_normalized().to_string(), + uri => uri.to_string(), + } + } + + assert_eq!(normalize("/#"), "/#"); + + assert_eq!(normalize("/"), "/"); + assert_eq!(normalize("//"), "/"); + assert_eq!(normalize("//////a/"), "/a/"); + assert_eq!(normalize("//ab"), "/ab"); + assert_eq!(normalize("//a"), "/a"); + assert_eq!(normalize("/a/b///c"), "/a/b/c"); + assert_eq!(normalize("/a/b///c/"), "/a/b/c/"); + assert_eq!(normalize("/a///b/c/d///"), "/a/b/c/d/"); + + assert_eq!(normalize("/?"), "/?"); + assert_eq!(normalize("/?foo"), "/?foo"); + assert_eq!(normalize("/a/?"), "/a/?"); + assert_eq!(normalize("/a/?foo"), "/a/?foo"); + } +} diff --git a/core/lib/fuzz/Cargo.toml b/core/lib/fuzz/Cargo.toml index 5e12bea43b..14a00c61dd 100644 --- a/core/lib/fuzz/Cargo.toml +++ b/core/lib/fuzz/Cargo.toml @@ -1,4 +1,3 @@ - [package] name = "rocket-fuzz" version = "0.0.0" @@ -30,3 +29,9 @@ name = "uri-roundtrip" path = "targets/uri-roundtrip.rs" test = false doc = false + +[[bin]] +name = "uri-normalization" +path = "targets/uri-normalization.rs" +test = false +doc = false diff --git a/core/lib/fuzz/targets/uri-normalization.rs b/core/lib/fuzz/targets/uri-normalization.rs new file mode 100644 index 0000000000..f228a2a0d0 --- /dev/null +++ b/core/lib/fuzz/targets/uri-normalization.rs @@ -0,0 +1,23 @@ +#![no_main] + +use rocket::http::uri::*; +use libfuzzer_sys::fuzz_target; + +fn fuzz(data: &str) { + if let Ok(uri) = Uri::parse_any(data) { + match uri { + Uri::Origin(uri) if uri.is_normalized() => { + assert_eq!(uri.clone(), uri.into_normalized()); + } + Uri::Absolute(uri) if uri.is_normalized() => { + assert_eq!(uri.clone(), uri.into_normalized()); + } + Uri::Reference(uri) if uri.is_normalized() => { + assert_eq!(uri.clone(), uri.into_normalized()); + } + _ => { /* not normalizable */ }, + } + } +} + +fuzz_target!(|data: &str| { fuzz(data) }); diff --git a/core/lib/src/catcher/catcher.rs b/core/lib/src/catcher/catcher.rs index 95c86b635a..a477fac401 100644 --- a/core/lib/src/catcher/catcher.rs +++ b/core/lib/src/catcher/catcher.rs @@ -107,14 +107,25 @@ pub struct Catcher { /// The name of this catcher, if one was given. pub name: Option>, - /// The mount point. - pub base: uri::Origin<'static>, - /// The HTTP status to match against if this route is not `default`. pub code: Option, /// The catcher's associated error handler. pub handler: Box, + + /// The mount point. + pub(crate) base: uri::Origin<'static>, + + /// The catcher's calculated rank. + /// + /// This is [base.segments().len() | base.chars().len()]. + pub(crate) rank: u64, +} + +fn compute_rank(base: &uri::Origin<'_>) -> u64 { + let major = u32::MAX - base.path().segments().num() as u32; + let minor = u32::MAX - base.path().as_str().chars().count() as u32; + ((major as u64) << 32) | (minor as u64) } impl Catcher { @@ -166,10 +177,36 @@ impl Catcher { name: None, base: uri::Origin::ROOT, handler: Box::new(handler), - code, + rank: compute_rank(&uri::Origin::ROOT), + code } } + /// Returns the mount point (base) of the catcher, which defaults to `/`. + /// + /// # Example + /// + /// ```rust + /// use rocket::request::Request; + /// use rocket::catcher::{Catcher, BoxFuture}; + /// use rocket::response::Responder; + /// use rocket::http::Status; + /// + /// fn handle_404<'r>(status: Status, req: &'r Request<'_>) -> BoxFuture<'r> { + /// let res = (status, format!("404: {}", req.uri())); + /// Box::pin(async move { res.respond_to(req) }) + /// } + /// + /// let catcher = Catcher::new(404, handle_404); + /// assert_eq!(catcher.base().path(), "/"); + /// + /// let catcher = catcher.map_base(|base| format!("/foo/bar/{}", base)).unwrap(); + /// assert_eq!(catcher.base().path(), "/foo/bar"); + /// ``` + pub fn base(&self) -> &uri::Origin<'_> { + &self.base + } + /// Maps the `base` of this catcher using `mapper`, returning a new /// `Catcher` with the returned base. /// @@ -192,13 +229,13 @@ impl Catcher { /// } /// /// let catcher = Catcher::new(404, handle_404); - /// assert_eq!(catcher.base.path(), "/"); + /// assert_eq!(catcher.base().path(), "/"); /// /// let catcher = catcher.map_base(|_| format!("/bar")).unwrap(); - /// assert_eq!(catcher.base.path(), "/bar"); + /// assert_eq!(catcher.base().path(), "/bar"); /// /// let catcher = catcher.map_base(|base| format!("/foo{}", base)).unwrap(); - /// assert_eq!(catcher.base.path(), "/foo/bar"); + /// assert_eq!(catcher.base().path(), "/foo/bar"); /// /// let catcher = catcher.map_base(|base| format!("/foo ? {}", base)); /// assert!(catcher.is_err()); @@ -209,8 +246,10 @@ impl Catcher { ) -> std::result::Result> where F: FnOnce(uri::Origin<'a>) -> String { - self.base = uri::Origin::parse_owned(mapper(self.base))?.into_normalized(); + let new_base = uri::Origin::parse_owned(mapper(self.base))?; + self.base = new_base.into_normalized_nontrailing(); self.base.clear_query(); + self.rank = compute_rank(&self.base); Ok(self) } } @@ -254,9 +293,7 @@ impl fmt::Display for Catcher { write!(f, "{}{}{} ", Paint::cyan("("), Paint::white(n), Paint::cyan(")"))?; } - if self.base.path() != "/" { - write!(f, "{} ", Paint::green(self.base.path()))?; - } + write!(f, "{} ", Paint::green(self.base.path()))?; match self.code { Some(code) => write!(f, "{}", Paint::blue(code)), @@ -271,6 +308,7 @@ impl fmt::Debug for Catcher { .field("name", &self.name) .field("base", &self.base) .field("code", &self.code) + .field("rank", &self.rank) .finish() } } diff --git a/core/lib/src/request/request.rs b/core/lib/src/request/request.rs index bf9121feb3..7365ec9e01 100644 --- a/core/lib/src/request/request.rs +++ b/core/lib/src/request/request.rs @@ -935,11 +935,12 @@ impl<'r> Request<'r> { } } - /// 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. + /// Get the `n`th non-empty 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 routed_segment(&self, n: usize) -> Option<&str> { - self.routed_segments(0..).get(n) + self.routed_segments(0..).get(n).filter(|p| !p.is_empty()) } /// Get the segments beginning at the `n`th, 0-indexed, after the mount @@ -947,9 +948,10 @@ impl<'r> Request<'r> { #[inline] pub fn routed_segments(&self, n: RangeFrom) -> Segments<'_, Path> { let mount_segments = self.route() - .map(|r| r.uri.metadata.base_segs.len()) + .map(|r| r.uri.metadata.base_len) .unwrap_or(0); + trace!("requesting {}.. ({}..) from {}", n.start, mount_segments, self); self.uri().path().segments().skip(mount_segments + n.start) } diff --git a/core/lib/src/rocket.rs b/core/lib/src/rocket.rs index edc1e49ff8..99bc66423d 100644 --- a/core/lib/src/rocket.rs +++ b/core/lib/src/rocket.rs @@ -579,9 +579,9 @@ fn log_items(e: &str, t: &str, items: I, base: B, origin: O) } items.sort_by_key(|i| origin(i).path().as_str().chars().count()); - items.sort_by_key(|i| origin(i).path().segments().len()); + items.sort_by_key(|i| origin(i).path().segments().count()); items.sort_by_key(|i| base(i).path().as_str().chars().count()); - items.sort_by_key(|i| base(i).path().segments().len()); + items.sort_by_key(|i| base(i).path().segments().count()); items.iter().for_each(|i| launch_meta_!("{}", i)); } @@ -794,9 +794,9 @@ impl Rocket

{ /// .register("/", catchers![just_500, some_default]); /// /// assert_eq!(rocket.catchers().count(), 3); - /// assert!(rocket.catchers().any(|c| c.code == Some(404) && c.base == "/foo")); - /// assert!(rocket.catchers().any(|c| c.code == Some(500) && c.base == "/")); - /// assert!(rocket.catchers().any(|c| c.code == None && c.base == "/")); + /// assert!(rocket.catchers().any(|c| c.code == Some(404) && c.base() == "/foo")); + /// assert!(rocket.catchers().any(|c| c.code == Some(500) && c.base() == "/")); + /// assert!(rocket.catchers().any(|c| c.code == None && c.base() == "/")); /// ``` pub fn catchers(&self) -> impl Iterator { match self.0.as_state_ref() { diff --git a/core/lib/src/route/segment.rs b/core/lib/src/route/segment.rs index 3e39441031..81573e9f1c 100644 --- a/core/lib/src/route/segment.rs +++ b/core/lib/src/route/segment.rs @@ -1,28 +1,29 @@ -use crate::http::RawStr; - #[derive(Debug, Clone)] pub struct Segment { + /// The name of the parameter or just the static string. pub value: String, + /// This is a ``. pub dynamic: bool, - pub trailing: bool, + /// This is a ``. + pub dynamic_trail: bool, } impl Segment { - pub fn from(segment: &RawStr) -> Self { + pub fn from(segment: &crate::http::RawStr) -> Self { let mut value = segment; let mut dynamic = false; - let mut trailing = false; + let mut dynamic_trail = false; if segment.starts_with('<') && segment.ends_with('>') { dynamic = true; value = &segment[1..(segment.len() - 1)]; if value.ends_with("..") { - trailing = true; + dynamic_trail = true; value = &value[..(value.len() - 2)]; } } - Segment { value: value.to_string(), dynamic, trailing } + Segment { value: value.to_string(), dynamic, dynamic_trail } } } diff --git a/core/lib/src/route/uri.rs b/core/lib/src/route/uri.rs index d770c50a03..a0c885b79c 100644 --- a/core/lib/src/route/uri.rs +++ b/core/lib/src/route/uri.rs @@ -62,7 +62,7 @@ pub struct RouteUri<'a> { /// The URI _without_ the `base` mount point. pub unmounted_origin: Origin<'a>, /// The URI _with_ the base mount point. This is the canonical route URI. - pub origin: Origin<'a>, + pub uri: Origin<'a>, /// Cached metadata about this URI. pub(crate) metadata: Metadata, } @@ -79,10 +79,10 @@ pub(crate) enum Color { #[derive(Debug, Clone)] pub(crate) struct Metadata { - /// Segments in the base. - pub base_segs: Vec, - /// Segments in the path, including base. - pub path_segs: Vec, + /// Segments in the route URI, including base. + pub uri_segments: Vec, + /// Numbers of segments in `uri_segments` that belong to the base. + pub base_len: usize, /// `(name, value)` of the query segments that are static. pub static_query_fields: Vec<(String, String)>, /// The "color" of the route path. @@ -90,7 +90,7 @@ pub(crate) struct Metadata { /// The "color" of the route query, if there is query. pub query_color: Option, /// Whether the path has a `` parameter. - pub trailing_path: bool, + pub dynamic_trail: bool, } type Result> = std::result::Result; @@ -103,25 +103,36 @@ impl<'a> RouteUri<'a> { pub(crate) fn try_new(base: &str, uri: &str) -> Result> { let mut base = Origin::parse(base) .map_err(|e| e.into_owned())? - .into_normalized() + .into_normalized_nontrailing() .into_owned(); base.clear_query(); - let unmounted_origin = Origin::parse_route(uri) + let origin = Origin::parse_route(uri) .map_err(|e| e.into_owned())? .into_normalized() .into_owned(); - let origin = Origin::parse_route(&format!("{}/{}", base, unmounted_origin)) + let compiled_uri = match base.path().as_str() { + "/" => origin.to_string(), + base => match (origin.path().as_str(), origin.query()) { + ("/", None) => base.to_string(), + ("/", Some(q)) => format!("{}?{}", base, q), + _ => format!("{}{}", base, origin), + } + }; + + let uri = Origin::parse_route(&compiled_uri) .map_err(|e| e.into_owned())? .into_normalized() .into_owned(); - let source = origin.to_string().into(); - let metadata = Metadata::from(&base, &origin); + dbg!(&base, &origin, &compiled_uri, &uri); + + let source = uri.to_string().into(); + let metadata = Metadata::from(&base, &uri); - Ok(RouteUri { source, base, unmounted_origin, origin, metadata }) + Ok(RouteUri { source, base, unmounted_origin: origin, uri, metadata }) } /// Create a new `RouteUri`. @@ -167,7 +178,7 @@ impl<'a> RouteUri<'a> { /// ``` #[inline(always)] pub fn path(&self) -> &str { - self.origin.path().as_str() + self.uri.path().as_str() } /// The query part of this route URI, if there is one. @@ -184,7 +195,7 @@ impl<'a> RouteUri<'a> { /// /// // Normalization clears the empty '?'. /// let index = Route::new(Method::Get, "/foo/bar?", handler); - /// assert!(index.uri.query().is_none()); + /// assert_eq!(index.uri.query().unwrap(), ""); /// /// let index = Route::new(Method::Get, "/foo/bar?a=1", handler); /// assert_eq!(index.uri.query().unwrap(), "a=1"); @@ -194,7 +205,7 @@ impl<'a> RouteUri<'a> { /// ``` #[inline(always)] pub fn query(&self) -> Option<&str> { - self.origin.query().map(|q| q.as_str()) + self.uri.query().map(|q| q.as_str()) } /// The full URI as an `&str`. @@ -247,16 +258,13 @@ impl<'a> RouteUri<'a> { } impl Metadata { - fn from(base: &Origin<'_>, origin: &Origin<'_>) -> Self { - let base_segs = base.path().raw_segments() - .map(Segment::from) - .collect::>(); - - let path_segs = origin.path().raw_segments() + fn from(base: &Origin<'_>, uri: &Origin<'_>) -> Self { + let uri_segments = uri.path() + .raw_segments() .map(Segment::from) .collect::>(); - let query_segs = origin.query() + let query_segs = uri.query() .map(|q| q.raw_segments().map(Segment::from).collect::>()) .unwrap_or_default(); @@ -265,8 +273,8 @@ impl Metadata { .map(|f| (f.name.source().to_string(), f.value.to_string())) .collect(); - let static_path = path_segs.iter().all(|s| !s.dynamic); - let wild_path = !path_segs.is_empty() && path_segs.iter().all(|s| s.dynamic); + let static_path = uri_segments.iter().all(|s| !s.dynamic); + let wild_path = !uri_segments.is_empty() && uri_segments.iter().all(|s| s.dynamic); let path_color = match (static_path, wild_path) { (true, _) => Color::Static, (_, true) => Color::Wild, @@ -283,11 +291,13 @@ impl Metadata { } }); - let trailing_path = path_segs.last().map_or(false, |p| p.trailing); + let dynamic_trail = uri_segments.last().map_or(false, |p| p.dynamic_trail); + let segments = base.path().segments(); + let num_empty = segments.clone().filter(|s| s.is_empty()).count(); + let base_len = segments.num() - num_empty; Metadata { - base_segs, path_segs, static_query_fields, path_color, query_color, - trailing_path, + uri_segments, base_len, static_query_fields, path_color, query_color, dynamic_trail } } } @@ -296,13 +306,13 @@ impl<'a> std::ops::Deref for RouteUri<'a> { type Target = Origin<'a>; fn deref(&self) -> &Self::Target { - &self.origin + &self.uri } } impl fmt::Display for RouteUri<'_> { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - self.origin.fmt(f) + self.uri.fmt(f) } } @@ -311,14 +321,14 @@ impl fmt::Debug for RouteUri<'_> { f.debug_struct("RouteUri") .field("base", &self.base) .field("unmounted_origin", &self.unmounted_origin) - .field("origin", &self.origin) + .field("origin", &self.uri) .field("metadata", &self.metadata) .finish() } } impl<'a, 'b> PartialEq> for RouteUri<'a> { - fn eq(&self, other: &Origin<'b>) -> bool { &self.origin == other } + fn eq(&self, other: &Origin<'b>) -> bool { &self.uri == other } } impl PartialEq for RouteUri<'_> { diff --git a/core/lib/src/router/collider.rs b/core/lib/src/router/collider.rs index ade9e1e65c..757a624d26 100644 --- a/core/lib/src/router/collider.rs +++ b/core/lib/src/router/collider.rs @@ -1,66 +1,12 @@ use crate::catcher::Catcher; -use crate::route::{Route, Color}; +use crate::route::{Route, Segment, RouteUri}; -use crate::http::{MediaType, Status}; -use crate::request::Request; +use crate::http::MediaType; pub trait Collide { fn collides_with(&self, other: &T) -> bool; } -impl<'a, 'b, T: Collide> Collide<&T> for &T { - fn collides_with(&self, other: &&T) -> bool { - T::collides_with(*self, *other) - } -} - -impl Collide for MediaType { - fn collides_with(&self, other: &Self) -> bool { - let collide = |a, b| a == "*" || b == "*" || a == b; - collide(self.top(), other.top()) && collide(self.sub(), other.sub()) - } -} - -fn paths_collide(route: &Route, other: &Route) -> bool { - let a_segments = &route.uri.metadata.path_segs; - let b_segments = &other.uri.metadata.path_segs; - for (seg_a, seg_b) in a_segments.iter().zip(b_segments.iter()) { - if seg_a.trailing || seg_b.trailing { - return true; - } - - if seg_a.dynamic || seg_b.dynamic { - continue; - } - - if seg_a.value != seg_b.value { - return false; - } - } - - a_segments.get(b_segments.len()).map_or(false, |s| s.trailing) - || b_segments.get(a_segments.len()).map_or(false, |s| s.trailing) - || a_segments.len() == b_segments.len() -} - -fn formats_collide(route: &Route, other: &Route) -> bool { - // When matching against the `Accept` header, the client can always provide - // a media type that will cause a collision through non-specificity, i.e, - // `*/*` matches everything. - if !route.method.supports_payload() { - return true; - } - - // When matching against the `Content-Type` header, we'll only consider - // requests as having a `Content-Type` if they're fully specified. If a - // route doesn't have a `format`, it accepts all `Content-Type`s. If a - // request doesn't have a format, it only matches routes without a format. - match (route.format.as_ref(), other.format.as_ref()) { - (Some(a), Some(b)) => a.collides_with(b), - _ => true - } -} - impl Collide for Route { /// Determines if two routes can match against some request. That is, if two /// routes `collide`, there exists a request that can match against both @@ -77,128 +23,75 @@ impl Collide for Route { fn collides_with(&self, other: &Route) -> bool { self.method == other.method && self.rank == other.rank - && paths_collide(self, other) + && self.uri.collides_with(&other.uri) && formats_collide(self, other) } } -impl Route { - /// Determines if this route matches against the given request. - /// - /// This means that: +impl Collide for Catcher { + /// Determines if two catchers are in conflict: there exists a request for + /// which there exist no rule to determine _which_ of the two catchers to + /// use. This means that the catchers: /// - /// * The route's method matches that of the incoming request. - /// * The route's format (if any) matches that of the incoming request. - /// - If route specifies format, it only gets requests for that format. - /// - If route doesn't specify format, it gets requests for any format. - /// * All static components in the route's path match the corresponding - /// components in the same position in the incoming request. - /// * All static components in the route's query string are also in the - /// request query string, though in any position. If there is no query - /// in the route, requests with/without queries match. - pub(crate) fn matches(&self, req: &Request<'_>) -> bool { - self.method == req.method() - && paths_match(self, req) - && queries_match(self, req) - && formats_match(self, req) - } -} - -fn paths_match(route: &Route, req: &Request<'_>) -> bool { - let route_segments = &route.uri.metadata.path_segs; - let req_segments = req.uri().path().segments(); - - if route.uri.metadata.trailing_path { - // The last route segment can be trailing, which is allowed to be empty. - // So we can have one more segment in `route` than in `req` and match. - // ok if: req_segments.len() >= route_segments.len() - 1 - if req_segments.len() + 1 < route_segments.len() { - return false; - } - } else if route_segments.len() != req_segments.len() { - return false; - } - - if route.uri.metadata.path_color == Color::Wild { - return true; - } - - for (route_seg, req_seg) in route_segments.iter().zip(req_segments) { - if route_seg.trailing { - return true; - } - - if !(route_seg.dynamic || route_seg.value == req_seg) { - return false; - } + /// * Have the same base. + /// * Have the same status code or are both defaults. + fn collides_with(&self, other: &Self) -> bool { + self.code == other.code + && self.base.path().segments().eq(other.base.path().segments()) } - - true } -fn queries_match(route: &Route, req: &Request<'_>) -> bool { - if matches!(route.uri.metadata.query_color, None | Some(Color::Wild)) { - return true; - } - - let route_query_fields = route.uri.metadata.static_query_fields.iter() - .map(|(k, v)| (k.as_str(), v.as_str())); +impl Collide for RouteUri<'_> { + fn collides_with(&self, other: &Self) -> bool { + let a_segments = &self.metadata.uri_segments; + let b_segments = &other.metadata.uri_segments; + for (seg_a, seg_b) in a_segments.iter().zip(b_segments.iter()) { + if seg_a.dynamic_trail || seg_b.dynamic_trail { + return true; + } - for route_seg in route_query_fields { - if let Some(query) = req.uri().query() { - if !query.segments().any(|req_seg| req_seg == route_seg) { - trace_!("request {} missing static query {:?}", req, route_seg); + if !seg_a.collides_with(seg_b) { return false; } - } else { - trace_!("query-less request {} missing static query {:?}", req, route_seg); - return false; } - } - true + // Check for `/a/` vs. `/a`, which should collide. + a_segments.get(b_segments.len()).map_or(false, |s| s.dynamic_trail) + || b_segments.get(a_segments.len()).map_or(false, |s| s.dynamic_trail) + || a_segments.len() == b_segments.len() + } } -fn formats_match(route: &Route, request: &Request<'_>) -> bool { - if !route.method.supports_payload() { - route.format.as_ref() - .and_then(|a| request.format().map(|b| (a, b))) - .map(|(a, b)| a.collides_with(b)) - .unwrap_or(true) - } else { - match route.format.as_ref() { - Some(a) => match request.format() { - Some(b) if b.specificity() == 2 => a.collides_with(b), - _ => false - } - None => true - } +impl Collide for Segment { + fn collides_with(&self, other: &Self) -> bool { + self.dynamic && !other.value.is_empty() + || other.dynamic && !self.value.is_empty() + || self.value == other.value } } - -impl Collide for Catcher { - /// Determines if two catchers are in conflict: there exists a request for - /// which there exist no rule to determine _which_ of the two catchers to - /// use. This means that the catchers: - /// - /// * Have the same base. - /// * Have the same status code or are both defaults. +impl Collide for MediaType { fn collides_with(&self, other: &Self) -> bool { - self.code == other.code - && self.base.path().segments().eq(other.base.path().segments()) + let collide = |a, b| a == "*" || b == "*" || a == b; + collide(self.top(), other.top()) && collide(self.sub(), other.sub()) } } -impl Catcher { - /// Determines if this catcher is responsible for handling the error with - /// `status` that occurred during request `req`. A catcher matches if: - /// - /// * It is a default catcher _or_ has a code of `status`. - /// * Its base is a prefix of the normalized/decoded `req.path()`. - pub(crate) fn matches(&self, status: Status, req: &Request<'_>) -> bool { - self.code.map_or(true, |code| code == status.code) - && self.base.path().segments().prefix_of(req.uri().path().segments()) +fn formats_collide(route: &Route, other: &Route) -> bool { + // When matching against the `Accept` header, the client can always provide + // a media type that will cause a collision through non-specificity, i.e, + // `*/*` matches everything. + if !route.method.supports_payload() { + return true; + } + + // When matching against the `Content-Type` header, we'll only consider + // requests as having a `Content-Type` if they're fully specified. If a + // route doesn't have a `format`, it accepts all `Content-Type`s. If a + // request doesn't have a format, it only matches routes without a format. + match (route.format.as_ref(), other.format.as_ref()) { + (Some(a), Some(b)) => a.collides_with(b), + _ => true } } @@ -208,172 +101,166 @@ mod tests { use super::*; use crate::route::{Route, dummy_handler}; - use crate::local::blocking::Client; - use crate::http::{Method, Method::*, MediaType, ContentType, Accept}; - use crate::http::uri::Origin; - - type SimpleRoute = (Method, &'static str); + use crate::http::{Method, Method::*, MediaType}; - fn m_collide(a: SimpleRoute, b: SimpleRoute) -> bool { - let route_a = Route::new(a.0, a.1, dummy_handler); - route_a.collides_with(&Route::new(b.0, b.1, dummy_handler)) - } - - fn unranked_collide(a: &'static str, b: &'static str) -> bool { - let route_a = Route::ranked(0, Get, a, dummy_handler); - let route_b = Route::ranked(0, Get, b, dummy_handler); - eprintln!("Checking {} against {}.", route_a, route_b); - route_a.collides_with(&route_b) + fn dummy_route(ranked: bool, method: impl Into>, uri: &'static str) -> Route { + let method = method.into().unwrap_or(Get); + Route::ranked((!ranked).then(|| 0), method, uri, dummy_handler) } - fn s_s_collide(a: &'static str, b: &'static str) -> bool { - let a = Route::new(Get, a, dummy_handler); - let b = Route::new(Get, b, dummy_handler); - paths_collide(&a, &b) - } - - #[test] - fn simple_collisions() { - assert!(unranked_collide("/a", "/a")); - assert!(unranked_collide("/hello", "/hello")); - assert!(unranked_collide("/hello", "/hello/")); - assert!(unranked_collide("/hello/there/how/ar", "/hello/there/how/ar")); - assert!(unranked_collide("/hello/there", "/hello/there/")); - } - - #[test] - fn simple_param_collisions() { - assert!(unranked_collide("/", "/")); - assert!(unranked_collide("/", "/b")); - assert!(unranked_collide("/hello/", "/hello/")); - assert!(unranked_collide("/hello//hi", "/hello//hi")); - assert!(unranked_collide("/hello//hi/there", "/hello//hi/there")); - assert!(unranked_collide("//hi/there", "//hi/there")); - assert!(unranked_collide("//hi/there", "/dude//there")); - assert!(unranked_collide("///", "///")); - assert!(unranked_collide("////", "////")); - assert!(unranked_collide("/", "/hi")); - assert!(unranked_collide("/", "/hi/hey")); - assert!(unranked_collide("/", "/hi/hey/hayo")); - assert!(unranked_collide("/a/", "/a/hi/hey/hayo")); - assert!(unranked_collide("/a//", "/a/hi/hey/hayo")); - assert!(unranked_collide("/a///", "/a/hi/hey/hayo")); - assert!(unranked_collide("///", "/a/hi/hey/hayo")); - assert!(unranked_collide("///hey/hayo", "/a/hi/hey/hayo")); - assert!(unranked_collide("/", "/foo")); - } - - #[test] - fn medium_param_collisions() { - assert!(unranked_collide("/", "/b")); - assert!(unranked_collide("/hello/", "/hello/bob")); - assert!(unranked_collide("/", "//bob")); - } - - #[test] - fn hard_param_collisions() { - assert!(unranked_collide("/", "///a///")); - assert!(unranked_collide("/", "//a/bcjdklfj//")); - assert!(unranked_collide("/a/", "//a/bcjdklfj//")); - assert!(unranked_collide("/a//", "//a/bcjdklfj//")); - assert!(unranked_collide("/", "/")); - assert!(unranked_collide("/", "/<_..>")); - assert!(unranked_collide("/a/b/", "/a/")); - assert!(unranked_collide("/a/b/", "/a//")); - assert!(unranked_collide("/hi/", "/hi")); - assert!(unranked_collide("/hi/", "/hi/")); - assert!(unranked_collide("/", "//////")); - } - - #[test] - fn query_collisions() { - assert!(unranked_collide("/?", "/?")); - assert!(unranked_collide("/a/?", "/a/?")); - assert!(unranked_collide("/a?", "/a?")); - assert!(unranked_collide("/?", "/?")); - assert!(unranked_collide("/a/b/c?", "/a/b/c?")); - assert!(unranked_collide("//b/c?", "/a/b/?")); - assert!(unranked_collide("/?", "/")); - assert!(unranked_collide("/a?", "/a")); - assert!(unranked_collide("/a?", "/a")); - assert!(unranked_collide("/a/b?", "/a/b")); - assert!(unranked_collide("/a/b", "/a/b?")); + macro_rules! assert_collision { + ($ranked:expr, $p1:expr, $p2:expr) => (assert_collision!($ranked, None $p1, None $p2)); + ($ranked:expr, $m1:ident $p1:expr, $m2:ident $p2:expr) => { + let (a, b) = (dummy_route($ranked, $m1, $p1), dummy_route($ranked, $m2, $p2)); + assert! { + a.collides_with(&b), + "\nroutes failed to collide:\n{} does not collide with {}\n", a, b + } + }; + (ranked $($t:tt)+) => (assert_collision!(true, $($t)+)); + ($($t:tt)+) => (assert_collision!(false, $($t)+)); + } + + macro_rules! assert_no_collision { + ($ranked:expr, $p1:expr, $p2:expr) => (assert_no_collision!($ranked, None $p1, None $p2)); + ($ranked:expr, $m1:ident $p1:expr, $m2:ident $p2:expr) => { + let (a, b) = (dummy_route($ranked, $m1, $p1), dummy_route($ranked, $m2, $p2)); + assert! { + !a.collides_with(&b), + "\nunexpected collision:\n{} collides with {}\n", a, b + } + }; + (ranked $($t:tt)+) => (assert_no_collision!(true, $($t)+)); + ($($t:tt)+) => (assert_no_collision!(false, $($t)+)); } #[test] fn non_collisions() { - assert!(!unranked_collide("/", "/")); - assert!(!unranked_collide("/a", "/b")); - assert!(!unranked_collide("/a/b", "/a")); - assert!(!unranked_collide("/a/b", "/a/c")); - assert!(!unranked_collide("/a/hello", "/a/c")); - assert!(!unranked_collide("/hello", "/a/c")); - assert!(!unranked_collide("/hello/there", "/hello/there/guy")); - assert!(!unranked_collide("/a/", "/b/")); - assert!(!unranked_collide("/t", "/test")); - assert!(!unranked_collide("/a", "/aa")); - assert!(!unranked_collide("/a", "/aaa")); - assert!(!unranked_collide("/", "/a")); + assert_no_collision!("/", "/"); + assert_no_collision!("/a", "/b"); + assert_no_collision!("/a/b", "/a"); + assert_no_collision!("/a/b", "/a/c"); + assert_no_collision!("/a/hello", "/a/c"); + assert_no_collision!("/hello", "/a/c"); + assert_no_collision!("/hello/there", "/hello/there/guy"); + assert_no_collision!("/a/", "/b/"); + assert_no_collision!("//b", "//a"); + assert_no_collision!("/t", "/test"); + assert_no_collision!("/a", "/aa"); + assert_no_collision!("/a", "/aaa"); + assert_no_collision!("/", "/a"); + + assert_no_collision!("/hello", "/hello/"); + assert_no_collision!("/hello/there", "/hello/there/"); + assert_no_collision!("/hello/", "/hello/"); + + assert_no_collision!("/a?", "/b"); + assert_no_collision!("/a/b", "/a?"); + assert_no_collision!("/a/b/c?", "/a/b/c/d"); + assert_no_collision!("/a/hello", "/a/?"); + assert_no_collision!("/?", "/hi"); + + assert_no_collision!(Get "/", Post "/"); + assert_no_collision!(Post "/", Put "/"); + assert_no_collision!(Put "/a", Put "/"); + assert_no_collision!(Post "/a", Put "/"); + assert_no_collision!(Get "/a", Put "/"); + assert_no_collision!(Get "/hello", Put "/hello"); + assert_no_collision!(Get "/", Post "/"); + + assert_no_collision!("/a", "/b"); + assert_no_collision!("/a/b", "/a"); + assert_no_collision!("/a/b", "/a/c"); + assert_no_collision!("/a/hello", "/a/c"); + assert_no_collision!("/hello", "/a/c"); + assert_no_collision!("/hello/there", "/hello/there/guy"); + assert_no_collision!("/a/", "/b/"); + assert_no_collision!("/a", "/b"); + assert_no_collision!("/a/b", "/a"); + assert_no_collision!("/a/b", "/a/c"); + assert_no_collision!("/a/hello", "/a/c"); + assert_no_collision!("/hello", "/a/c"); + assert_no_collision!("/hello/there", "/hello/there/guy"); + assert_no_collision!("/a/", "/b/"); + assert_no_collision!("/a", "/b"); + assert_no_collision!("/a/b", "/a"); + assert_no_collision!("/a/b", "/a/c"); + assert_no_collision!("/a/hello", "/a/c"); + assert_no_collision!("/hello", "/a/c"); + assert_no_collision!("/hello/there", "/hello/there/guy"); + assert_no_collision!("/a/", "/b/"); + assert_no_collision!("/t", "/test"); + assert_no_collision!("/a", "/aa"); + assert_no_collision!("/a", "/aaa"); + assert_no_collision!("/", "/a"); + + assert_no_collision!(ranked "/", "/?a"); + assert_no_collision!(ranked "/", "/?"); + assert_no_collision!(ranked "/a/", "/a/?d"); } #[test] - fn query_non_collisions() { - assert!(!unranked_collide("/a?", "/b")); - assert!(!unranked_collide("/a/b", "/a?")); - assert!(!unranked_collide("/a/b/c?", "/a/b/c/d")); - assert!(!unranked_collide("/a/hello", "/a/?")); - assert!(!unranked_collide("/?", "/hi")); - } - - #[test] - fn method_dependent_non_collisions() { - assert!(!m_collide((Get, "/"), (Post, "/"))); - assert!(!m_collide((Post, "/"), (Put, "/"))); - assert!(!m_collide((Put, "/a"), (Put, "/"))); - assert!(!m_collide((Post, "/a"), (Put, "/"))); - assert!(!m_collide((Get, "/a"), (Put, "/"))); - assert!(!m_collide((Get, "/hello"), (Put, "/hello"))); - assert!(!m_collide((Get, "/"), (Post, "/"))); - } - - #[test] - fn query_dependent_non_collisions() { - assert!(!m_collide((Get, "/"), (Get, "/?a"))); - assert!(!m_collide((Get, "/"), (Get, "/?"))); - assert!(!m_collide((Get, "/a/"), (Get, "/a/?d"))); - } - - #[test] - fn test_str_non_collisions() { - assert!(!s_s_collide("/a", "/b")); - assert!(!s_s_collide("/a/b", "/a")); - assert!(!s_s_collide("/a/b", "/a/c")); - assert!(!s_s_collide("/a/hello", "/a/c")); - assert!(!s_s_collide("/hello", "/a/c")); - assert!(!s_s_collide("/hello/there", "/hello/there/guy")); - assert!(!s_s_collide("/a/", "/b/")); - assert!(!s_s_collide("/a", "/b")); - assert!(!s_s_collide("/a/b", "/a")); - assert!(!s_s_collide("/a/b", "/a/c")); - assert!(!s_s_collide("/a/hello", "/a/c")); - assert!(!s_s_collide("/hello", "/a/c")); - assert!(!s_s_collide("/hello/there", "/hello/there/guy")); - assert!(!s_s_collide("/a/", "/b/")); - assert!(!s_s_collide("/a", "/b")); - assert!(!s_s_collide("/a/b", "/a")); - assert!(!s_s_collide("/a/b", "/a/c")); - assert!(!s_s_collide("/a/hello", "/a/c")); - assert!(!s_s_collide("/hello", "/a/c")); - assert!(!s_s_collide("/hello/there", "/hello/there/guy")); - assert!(!s_s_collide("/a/", "/b/")); - assert!(!s_s_collide("/t", "/test")); - assert!(!s_s_collide("/a", "/aa")); - assert!(!s_s_collide("/a", "/aaa")); - assert!(!s_s_collide("/", "/a")); - - assert!(s_s_collide("/a/hi/", "/a/hi/")); - assert!(s_s_collide("/hi/", "/hi/")); - assert!(s_s_collide("/", "/")); + fn collisions() { + assert_collision!("/a", "/a"); + assert_collision!("/hello", "/hello"); + assert_collision!("/hello/there/how/ar", "/hello/there/how/ar"); + + assert_collision!("/", "/"); + assert_collision!("/", "/b"); + assert_collision!("/hello/", "/hello/"); + assert_collision!("/hello//hi", "/hello//hi"); + assert_collision!("/hello//hi/there", "/hello//hi/there"); + assert_collision!("//hi/there", "//hi/there"); + assert_collision!("//hi/there", "/dude//there"); + assert_collision!("///", "///"); + assert_collision!("////", "////"); + assert_collision!("/", "/hi"); + assert_collision!("/", "/hi/hey"); + assert_collision!("/", "/hi/hey/hayo"); + assert_collision!("/a/", "/a/hi/hey/hayo"); + assert_collision!("/a//", "/a/hi/hey/hayo"); + assert_collision!("/a///", "/a/hi/hey/hayo"); + assert_collision!("///", "/a/hi/hey/hayo"); + assert_collision!("///hey/hayo", "/a/hi/hey/hayo"); + assert_collision!("/", "/foo"); + + assert_collision!("/", "/"); + assert_collision!("/a", "/a/"); + assert_collision!("/a/", "/a/"); + assert_collision!("//", "/a/"); + assert_collision!("/", "/a/"); + + assert_collision!("/", "/b"); + assert_collision!("/hello/", "/hello/bob"); + assert_collision!("/", "//bob"); + + assert_collision!("/", "///a///"); + assert_collision!("/", "//a/bcjdklfj//"); + assert_collision!("/a/", "//a/bcjdklfj//"); + assert_collision!("/a//", "//a/bcjdklfj//"); + assert_collision!("/", "/"); + assert_collision!("/", "/<_..>"); + assert_collision!("/a/b/", "/a/"); + assert_collision!("/a/b/", "/a//"); + assert_collision!("/hi/", "/hi"); + assert_collision!("/hi/", "/hi/"); + assert_collision!("/", "//////"); + + assert_collision!("/?", "/?"); + assert_collision!("/a/?", "/a/?"); + assert_collision!("/a?", "/a?"); + assert_collision!("/?", "/?"); + assert_collision!("/a/b/c?", "/a/b/c?"); + assert_collision!("//b/c?", "/a/b/?"); + assert_collision!("/?", "/"); + assert_collision!("/a?", "/a"); + assert_collision!("/a?", "/a"); + assert_collision!("/a/b?", "/a/b"); + assert_collision!("/a/b", "/a/b?"); + + assert_collision!("/a/hi/", "/a/hi/"); + assert_collision!("/hi/", "/hi/"); + assert_collision!("/", "/"); } fn mt_mt_collide(mt1: &str, mt2: &str) -> bool { @@ -458,119 +345,6 @@ mod tests { assert!(!r_mt_mt_collide(Post, "other/html", "text/html")); } - fn req_route_mt_collide(m: Method, mt1: S1, mt2: S2) -> bool - where S1: Into>, S2: Into> - { - let client = Client::debug_with(vec![]).expect("client"); - let mut req = client.req(m, "/"); - if let Some(mt_str) = mt1.into() { - if m.supports_payload() { - req.replace_header(mt_str.parse::().unwrap()); - } else { - req.replace_header(mt_str.parse::().unwrap()); - } - } - - let mut route = Route::new(m, "/", dummy_handler); - if let Some(mt_str) = mt2.into() { - route.format = Some(mt_str.parse::().unwrap()); - } - - route.matches(&req) - } - - #[test] - fn test_req_route_mt_collisions() { - assert!(req_route_mt_collide(Post, "application/json", "application/json")); - assert!(req_route_mt_collide(Post, "application/json", "application/*")); - assert!(req_route_mt_collide(Post, "application/json", "*/json")); - assert!(req_route_mt_collide(Post, "text/html", "*/*")); - - assert!(req_route_mt_collide(Get, "application/json", "application/json")); - assert!(req_route_mt_collide(Get, "text/html", "text/html")); - assert!(req_route_mt_collide(Get, "text/html", "*/*")); - assert!(req_route_mt_collide(Get, None, "*/*")); - assert!(req_route_mt_collide(Get, None, "text/*")); - assert!(req_route_mt_collide(Get, None, "text/html")); - assert!(req_route_mt_collide(Get, None, "application/json")); - - assert!(req_route_mt_collide(Post, "text/html", None)); - assert!(req_route_mt_collide(Post, "application/json", None)); - assert!(req_route_mt_collide(Post, "x-custom/anything", None)); - assert!(req_route_mt_collide(Post, None, None)); - - assert!(req_route_mt_collide(Get, "text/html", None)); - assert!(req_route_mt_collide(Get, "application/json", None)); - assert!(req_route_mt_collide(Get, "x-custom/anything", None)); - assert!(req_route_mt_collide(Get, None, None)); - assert!(req_route_mt_collide(Get, None, "text/html")); - assert!(req_route_mt_collide(Get, None, "application/json")); - - assert!(req_route_mt_collide(Get, "text/html, text/plain", "text/html")); - assert!(req_route_mt_collide(Get, "text/html; q=0.5, text/xml", "text/xml")); - - assert!(!req_route_mt_collide(Post, None, "text/html")); - assert!(!req_route_mt_collide(Post, None, "text/*")); - assert!(!req_route_mt_collide(Post, None, "*/text")); - assert!(!req_route_mt_collide(Post, None, "*/*")); - assert!(!req_route_mt_collide(Post, None, "text/html")); - assert!(!req_route_mt_collide(Post, None, "application/json")); - - assert!(!req_route_mt_collide(Post, "application/json", "text/html")); - assert!(!req_route_mt_collide(Post, "application/json", "text/*")); - assert!(!req_route_mt_collide(Post, "application/json", "*/xml")); - assert!(!req_route_mt_collide(Get, "application/json", "text/html")); - assert!(!req_route_mt_collide(Get, "application/json", "text/*")); - assert!(!req_route_mt_collide(Get, "application/json", "*/xml")); - - assert!(!req_route_mt_collide(Post, None, "text/html")); - assert!(!req_route_mt_collide(Post, None, "application/json")); - } - - fn req_route_path_match(a: &'static str, b: &'static str) -> bool { - let client = Client::debug_with(vec![]).expect("client"); - let req = client.get(Origin::parse(a).expect("valid URI")); - let route = Route::ranked(0, Get, b, dummy_handler); - route.matches(&req) - } - - #[test] - fn test_req_route_query_collisions() { - assert!(req_route_path_match("/a/b?a=b", "/a/b?")); - assert!(req_route_path_match("/a/b?a=b", "//b?")); - assert!(req_route_path_match("/a/b?a=b", "//?")); - assert!(req_route_path_match("/a/b?a=b", "/a/?")); - assert!(req_route_path_match("/?b=c", "/?")); - - assert!(req_route_path_match("/a/b?a=b", "/a/b")); - assert!(req_route_path_match("/a/b", "/a/b")); - assert!(req_route_path_match("/a/b/c/d?", "/a/b/c/d")); - assert!(req_route_path_match("/a/b/c/d?v=1&v=2", "/a/b/c/d")); - - assert!(req_route_path_match("/a/b", "/a/b?")); - assert!(req_route_path_match("/a/b", "/a/b?")); - assert!(req_route_path_match("/a/b?c", "/a/b?c")); - assert!(req_route_path_match("/a/b?c", "/a/b?")); - assert!(req_route_path_match("/a/b?c=foo&d=z", "/a/b?")); - assert!(req_route_path_match("/a/b?c=foo&d=z", "/a/b?")); - - assert!(req_route_path_match("/a/b?c=foo&d=z", "/a/b?c=foo&")); - assert!(req_route_path_match("/a/b?c=foo&d=z", "/a/b?d=z&")); - - assert!(!req_route_path_match("/a/b/c", "/a/b?")); - assert!(!req_route_path_match("/a?b=c", "/a/b?")); - assert!(!req_route_path_match("/?b=c", "/a/b?")); - assert!(!req_route_path_match("/?b=c", "/a?")); - - assert!(!req_route_path_match("/a/b?c=foo&d=z", "/a/b?a=b&")); - assert!(!req_route_path_match("/a/b?c=foo&d=z", "/a/b?d=b&")); - assert!(!req_route_path_match("/a/b", "/a/b?c")); - assert!(!req_route_path_match("/a/b", "/a/b?foo")); - assert!(!req_route_path_match("/a/b", "/a/b?foo&")); - assert!(!req_route_path_match("/a/b", "/a/b?&b&")); - } - - fn catchers_collide(a: A, ap: &str, b: B, bp: &str) -> bool where A: Into>, B: Into> { diff --git a/core/lib/src/router/matcher.rs b/core/lib/src/router/matcher.rs new file mode 100644 index 0000000000..496efac042 --- /dev/null +++ b/core/lib/src/router/matcher.rs @@ -0,0 +1,257 @@ +use crate::{Route, Request, Catcher}; +use crate::router::Collide; +use crate::http::Status; +use crate::route::Color; + +impl Route { + /// Determines if this route matches against the given request. + /// + /// This means that: + /// + /// * The route's method matches that of the incoming request. + /// * The route's format (if any) matches that of the incoming request. + /// - If route specifies format, it only gets requests for that format. + /// - If route doesn't specify format, it gets requests for any format. + /// * All static components in the route's path match the corresponding + /// components in the same position in the incoming request. + /// * All static components in the route's query string are also in the + /// request query string, though in any position. If there is no query + /// in the route, requests with/without queries match. + pub(crate) fn matches(&self, req: &Request<'_>) -> bool { + self.method == req.method() + && paths_match(self, req) + && queries_match(self, req) + && formats_match(self, req) + } +} + +impl Catcher { + /// Determines if this catcher is responsible for handling the error with + /// `status` that occurred during request `req`. A catcher matches if: + /// + /// * It is a default catcher _or_ has a code of `status`. + /// * Its base is a prefix of the normalized/decoded `req.path()`. + pub(crate) fn matches(&self, status: Status, req: &Request<'_>) -> bool { + dbg!(self.base.path().segments()); + dbg!(req.uri().path().segments()); + + self.code.map_or(true, |code| code == status.code) + && self.base.path().segments().prefix_of(req.uri().path().segments()) + } +} + +fn paths_match(route: &Route, req: &Request<'_>) -> bool { + trace!("checking path match: route {} vs. request {}", route, req); + let route_segments = &route.uri.metadata.uri_segments; + let req_segments = req.uri().path().segments(); + + // requests with longer paths only match if we have dynamic trail (). + if req_segments.num() > route_segments.len() { + if !route.uri.metadata.dynamic_trail { + return false; + } + } + + // The last route segment can be trailing (`/<..>`), which is allowed to be + // empty in the request. That is, we want to match `GET /a` to `/a/`. + if route_segments.len() > req_segments.num() { + if route_segments.len() != req_segments.num() + 1 { + return false; + } + + if !route.uri.metadata.dynamic_trail { + return false; + } + } + + // We've checked everything beyond the zip of their lengths already. + for (route_seg, req_seg) in route_segments.iter().zip(req_segments.clone()) { + if route_seg.dynamic_trail { + return true; + } + + if route_seg.dynamic && req_seg.is_empty() { + return false; + } + + if !route_seg.dynamic && route_seg.value != req_seg { + return false; + } + } + + true +} + +fn queries_match(route: &Route, req: &Request<'_>) -> bool { + trace!("checking query match: route {} vs. request {}", route, req); + if matches!(route.uri.metadata.query_color, None | Some(Color::Wild)) { + return true; + } + + let route_query_fields = route.uri.metadata.static_query_fields.iter() + .map(|(k, v)| (k.as_str(), v.as_str())); + + for route_seg in route_query_fields { + if let Some(query) = req.uri().query() { + if !query.segments().any(|req_seg| req_seg == route_seg) { + trace_!("request {} missing static query {:?}", req, route_seg); + return false; + } + } else { + trace_!("query-less request {} missing static query {:?}", req, route_seg); + return false; + } + } + + true +} + +fn formats_match(route: &Route, req: &Request<'_>) -> bool { + trace!("checking format match: route {} vs. request {}", route, req); + let route_format = match route.format { + Some(ref format) => format, + None => return true, + }; + + if route.method.supports_payload() { + match req.format() { + Some(f) if f.specificity() == 2 => route_format.collides_with(f), + _ => false + } + } else { + match req.format() { + Some(f) => route_format.collides_with(f), + None => true + } + } +} + +#[cfg(test)] +mod tests { + use crate::local::blocking::Client; + use crate::route::{Route, dummy_handler}; + use crate::http::{Method, Method::*, MediaType, ContentType, Accept}; + + fn req_matches_route(a: &'static str, b: &'static str) -> bool { + let client = Client::debug_with(vec![]).expect("client"); + let route = Route::ranked(0, Get, b, dummy_handler); + route.matches(&client.get(a)) + } + + #[test] + fn request_route_matching() { + assert!(req_matches_route("/a/b?a=b", "/a/b?")); + assert!(req_matches_route("/a/b?a=b", "//b?")); + assert!(req_matches_route("/a/b?a=b", "//?")); + assert!(req_matches_route("/a/b?a=b", "/a/?")); + assert!(req_matches_route("/?b=c", "/?")); + + assert!(req_matches_route("/a/b?a=b", "/a/b")); + assert!(req_matches_route("/a/b", "/a/b")); + assert!(req_matches_route("/a/b/c/d?", "/a/b/c/d")); + assert!(req_matches_route("/a/b/c/d?v=1&v=2", "/a/b/c/d")); + + assert!(req_matches_route("/a/b", "/a/b?")); + assert!(req_matches_route("/a/b", "/a/b?")); + assert!(req_matches_route("/a/b?c", "/a/b?c")); + assert!(req_matches_route("/a/b?c", "/a/b?")); + assert!(req_matches_route("/a/b?c=foo&d=z", "/a/b?")); + assert!(req_matches_route("/a/b?c=foo&d=z", "/a/b?")); + + assert!(req_matches_route("/a/b?c=foo&d=z", "/a/b?c=foo&")); + assert!(req_matches_route("/a/b?c=foo&d=z", "/a/b?d=z&")); + + assert!(req_matches_route("/a", "/a")); + assert!(req_matches_route("/a/", "/a/")); + + assert!(req_matches_route("//", "/")); + assert!(req_matches_route("/a///", "/a/")); + assert!(req_matches_route("/a/b", "/a/b")); + + assert!(!req_matches_route("/a///", "/a")); + assert!(!req_matches_route("/a", "/a/")); + assert!(!req_matches_route("/a/", "/a")); + assert!(!req_matches_route("/a/b", "/a/b/")); + + assert!(!req_matches_route("/a/b/c", "/a/b?")); + assert!(!req_matches_route("/a?b=c", "/a/b?")); + assert!(!req_matches_route("/?b=c", "/a/b?")); + assert!(!req_matches_route("/?b=c", "/a?")); + + assert!(!req_matches_route("/a/b?c=foo&d=z", "/a/b?a=b&")); + assert!(!req_matches_route("/a/b?c=foo&d=z", "/a/b?d=b&")); + assert!(!req_matches_route("/a/b", "/a/b?c")); + assert!(!req_matches_route("/a/b", "/a/b?foo")); + assert!(!req_matches_route("/a/b", "/a/b?foo&")); + assert!(!req_matches_route("/a/b", "/a/b?&b&")); + } + + fn req_matches_format(m: Method, mt1: S1, mt2: S2) -> bool + where S1: Into>, S2: Into> + { + let client = Client::debug_with(vec![]).expect("client"); + let mut req = client.req(m, "/"); + if let Some(mt_str) = mt1.into() { + if m.supports_payload() { + req.replace_header(mt_str.parse::().unwrap()); + } else { + req.replace_header(mt_str.parse::().unwrap()); + } + } + + let mut route = Route::new(m, "/", dummy_handler); + if let Some(mt_str) = mt2.into() { + route.format = Some(mt_str.parse::().unwrap()); + } + + route.matches(&req) + } + + #[test] + fn test_req_route_mt_collisions() { + assert!(req_matches_format(Post, "application/json", "application/json")); + assert!(req_matches_format(Post, "application/json", "application/*")); + assert!(req_matches_format(Post, "application/json", "*/json")); + assert!(req_matches_format(Post, "text/html", "*/*")); + + assert!(req_matches_format(Get, "application/json", "application/json")); + assert!(req_matches_format(Get, "text/html", "text/html")); + assert!(req_matches_format(Get, "text/html", "*/*")); + assert!(req_matches_format(Get, None, "*/*")); + assert!(req_matches_format(Get, None, "text/*")); + assert!(req_matches_format(Get, None, "text/html")); + assert!(req_matches_format(Get, None, "application/json")); + + assert!(req_matches_format(Post, "text/html", None)); + assert!(req_matches_format(Post, "application/json", None)); + assert!(req_matches_format(Post, "x-custom/anything", None)); + assert!(req_matches_format(Post, None, None)); + + assert!(req_matches_format(Get, "text/html", None)); + assert!(req_matches_format(Get, "application/json", None)); + assert!(req_matches_format(Get, "x-custom/anything", None)); + assert!(req_matches_format(Get, None, None)); + assert!(req_matches_format(Get, None, "text/html")); + assert!(req_matches_format(Get, None, "application/json")); + + assert!(req_matches_format(Get, "text/html, text/plain", "text/html")); + assert!(req_matches_format(Get, "text/html; q=0.5, text/xml", "text/xml")); + + assert!(!req_matches_format(Post, None, "text/html")); + assert!(!req_matches_format(Post, None, "text/*")); + assert!(!req_matches_format(Post, None, "*/text")); + assert!(!req_matches_format(Post, None, "*/*")); + assert!(!req_matches_format(Post, None, "text/html")); + assert!(!req_matches_format(Post, None, "application/json")); + + assert!(!req_matches_format(Post, "application/json", "text/html")); + assert!(!req_matches_format(Post, "application/json", "text/*")); + assert!(!req_matches_format(Post, "application/json", "*/xml")); + assert!(!req_matches_format(Get, "application/json", "text/html")); + assert!(!req_matches_format(Get, "application/json", "text/*")); + assert!(!req_matches_format(Get, "application/json", "*/xml")); + + assert!(!req_matches_format(Post, None, "text/html")); + assert!(!req_matches_format(Post, None, "application/json")); + } +} diff --git a/core/lib/src/router/mod.rs b/core/lib/src/router/mod.rs index dc1a662187..c0bbccfb6b 100644 --- a/core/lib/src/router/mod.rs +++ b/core/lib/src/router/mod.rs @@ -2,6 +2,7 @@ mod router; mod collider; +mod matcher; pub(crate) use router::*; pub(crate) use collider::*; diff --git a/core/lib/src/router/router.rs b/core/lib/src/router/router.rs index 08408a4fca..d74e6d3b2b 100644 --- a/core/lib/src/router/router.rs +++ b/core/lib/src/router/router.rs @@ -32,7 +32,7 @@ impl Router { pub fn add_catcher(&mut self, catcher: Catcher) { let catchers = self.catchers.entry(catcher.code).or_default(); catchers.push(catcher); - catchers.sort_by(|a, b| b.base.path().segments().len().cmp(&a.base.path().segments().len())) + catchers.sort_by_key(|c| c.rank); } #[inline] @@ -67,13 +67,8 @@ impl Router { match (explicit, default) { (None, None) => None, (None, c@Some(_)) | (c@Some(_), None) => c, - (Some(a), Some(b)) => { - if b.base.path().segments().len() > a.base.path().segments().len() { - Some(b) - } else { - Some(a) - } - } + (Some(a), Some(b)) if a.rank <= b.rank => Some(a), + (Some(_), Some(b)) => Some(b), } } @@ -194,15 +189,11 @@ mod test { #[test] fn test_collisions_normalize() { - assert!(rankless_route_collisions(&["/hello/", "/hello"])); - assert!(rankless_route_collisions(&["//hello/", "/hello"])); assert!(rankless_route_collisions(&["//hello/", "/hello//"])); - assert!(rankless_route_collisions(&["/", "/hello//"])); - assert!(rankless_route_collisions(&["/", "/hello///"])); assert!(rankless_route_collisions(&["/hello///bob", "/hello/"])); - assert!(rankless_route_collisions(&["///", "/a//"])); assert!(rankless_route_collisions(&["/a///", "/a/"])); assert!(rankless_route_collisions(&["/a///", "/a/b//c//d/"])); + assert!(rankless_route_collisions(&["///", "/a//"])); assert!(rankless_route_collisions(&["/a//", "/a/bd/e/"])); assert!(rankless_route_collisions(&["//", "/a/bd/e/"])); assert!(rankless_route_collisions(&["//", "/"])); @@ -233,6 +224,11 @@ mod test { #[test] fn test_no_collisions() { + assert!(!rankless_route_collisions(&["/a", "/a/"])); + assert!(!rankless_route_collisions(&["/", "/hello//"])); + assert!(!rankless_route_collisions(&["/", "/hello///"])); + assert!(!rankless_route_collisions(&["/hello/", "/hello"])); + assert!(!rankless_route_collisions(&["//hello/", "/hello"])); assert!(!rankless_route_collisions(&["/a/b", "/a/b/c"])); assert!(!rankless_route_collisions(&["/a/b/c/d", "/a/b/c//e"])); assert!(!rankless_route_collisions(&["/a/d/", "/a/b/c"])); @@ -309,7 +305,6 @@ mod test { let router = router_with_routes(&["//"]); assert!(route(&router, Get, "/hello/hi").is_some()); - assert!(route(&router, Get, "/a/b/").is_some()); assert!(route(&router, Get, "/i/a").is_some()); assert!(route(&router, Get, "/jdlk/asdij").is_some()); @@ -352,29 +347,33 @@ mod test { assert!(route(&router, Put, "/hello").is_none()); assert!(route(&router, Post, "/hello").is_none()); assert!(route(&router, Options, "/hello").is_none()); - assert!(route(&router, Get, "/hello/there").is_none()); - assert!(route(&router, Get, "/hello/i").is_none()); + assert!(route(&router, Get, "/").is_none()); + assert!(route(&router, Get, "/hello/").is_none()); + assert!(route(&router, Get, "/hello/there/").is_none()); + assert!(route(&router, Get, "/hello/there/").is_none()); let router = router_with_routes(&["//"]); assert!(route(&router, Get, "/a/b/c").is_none()); assert!(route(&router, Get, "/a").is_none()); assert!(route(&router, Get, "/a/").is_none()); assert!(route(&router, Get, "/a/b/c/d").is_none()); + assert!(route(&router, Get, "/a/b/").is_none()); assert!(route(&router, Put, "/hello/hi").is_none()); assert!(route(&router, Put, "/a/b").is_none()); - assert!(route(&router, Put, "/a/b").is_none()); let router = router_with_routes(&["/prefix/"]); assert!(route(&router, Get, "/").is_none()); assert!(route(&router, Get, "/prefi/").is_none()); } + /// Asserts that `$to` routes to `$want` given `$routes` are present. macro_rules! assert_ranked_match { ($routes:expr, $to:expr => $want:expr) => ({ let router = router_with_routes($routes); assert!(!router.has_collisions()); let route_path = route(&router, Get, $to).unwrap().uri.to_string(); - assert_eq!(route_path, $want.to_string()); + assert_eq!(route_path, $want.to_string(), + "\nmatched {} with {}, wanted {} in {:#?}", $to, route_path, $want, router); }) } @@ -578,8 +577,11 @@ mod test { for (req, expected) in requests.iter().zip(expected.iter()) { let req_status = Status::from_code(req.0).expect("valid status"); let catcher = catcher(&router, req_status, req.1).expect("some catcher"); - assert_eq!(catcher.code, expected.0, "<- got, expected ->"); - assert_eq!(catcher.base.path(), expected.1, "<- got, expected ->"); + assert_eq!(catcher.code, expected.0, + "\nmatched {}, expected {:?} for req {:?}", catcher, expected, req); + + assert_eq!(catcher.base.path(), expected.1, + "\nmatched {}, expected {:?} for req {:?}", catcher, expected, req); } }) } diff --git a/core/lib/src/sentinel.rs b/core/lib/src/sentinel.rs index 39df58131f..df503c31c4 100644 --- a/core/lib/src/sentinel.rs +++ b/core/lib/src/sentinel.rs @@ -263,7 +263,7 @@ use crate::{Rocket, Ignite}; /// return true; /// } /// -/// if !rocket.catchers().any(|c| c.code == Some(400) && c.base == "/") { +/// if !rocket.catchers().any(|c| c.code == Some(400) && c.base() == "/") { /// return true; /// } /// diff --git a/examples/hello/src/main.rs b/examples/hello/src/main.rs index 0f8c55cb1a..e9c1081a10 100644 --- a/examples/hello/src/main.rs +++ b/examples/hello/src/main.rs @@ -74,6 +74,10 @@ fn hello(lang: Option, opt: Options<'_>) -> String { #[launch] fn rocket() -> _ { + // FIXME: Check docs corresponding to normalization/matching/colliding. + // FUZZ: If rand_req1.matches(foo) && rand_req2.matches(bar) => + // rand_req1.collides_with(rand_req2) + rocket::build() .mount("/", routes![hello]) .mount("/hello", routes![world, mir]) From ac0a77bae228a6adf7437dcc9144d81264116645 Mon Sep 17 00:00:00 2001 From: Sergio Benitez Date: Fri, 7 Apr 2023 10:46:53 -0700 Subject: [PATCH 124/166] Allow dynamic parameters to match empty segments. The net effect of this commit is three-fold: * A request to `/` now matches `/`. `/foo/` matches `//`. * A segment matched to a dynamic parameter may be empty. * A request to `/foo/` no longer matches `/foo` or `/`. Instead, such a request would match `/foo/` or `/foo/`. The `&str` and `String` parameter guards were updated to reflect this change: they now error, with a newly introduced error type `Empty` in the `rocket::error` module, when the parameter is empty. As this was the only built-in parameter guard that would be effected by this change (all other guards already required nonempty parameters to succeed), the majority of applications will see no effect as a result. For applications wanting the previous functionality, a new `AdHoc::uri_normalizer()` fairing was introduced. --- .../ui-fail-nightly/typed-uri-bad-type.stderr | 4 ++-- .../typed-uris-bad-params.stderr | 4 ++-- .../ui-fail-stable/typed-uri-bad-type.stderr | 4 ++-- .../typed-uris-bad-params.stderr | 4 ++-- core/lib/src/error.rs | 18 ++++++++++++++++++ core/lib/src/request/from_param.rs | 14 +++++++++++--- core/lib/src/request/request.rs | 6 ++++-- core/lib/src/route/uri.rs | 2 -- core/lib/src/router/collider.rs | 14 ++++++++------ core/lib/src/router/matcher.rs | 9 ++------- core/lib/src/router/router.rs | 7 ++++--- examples/hello/src/main.rs | 4 ---- 12 files changed, 55 insertions(+), 35 deletions(-) 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 9d41bd6bba..af0c2e5088 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 @@ -2,13 +2,13 @@ error[E0271]: type mismatch resolving `>::Error == &str` --> tests/ui-fail-nightly/typed-uri-bad-type.rs:22:37 | 22 | fn optionals(id: Option, name: Result) { } - | ^^^^^^^^^^^^^^^^^^^^ expected `Infallible`, found `&str` + | ^^^^^^^^^^^^^^^^^^^^ expected `Empty`, found `&str` error[E0271]: type mismatch resolving `>::Error == &str` --> tests/ui-fail-nightly/typed-uri-bad-type.rs:22:37 | 22 | fn optionals(id: Option, name: Result) { } - | ^^^^^^^^^^^^^^^^^^^^ expected `&str`, found `Infallible` + | ^^^^^^^^^^^^^^^^^^^^ expected `&str`, found `Empty` error[E0277]: the trait bound `usize: FromUriParam` is not satisfied --> tests/ui-fail-nightly/typed-uri-bad-type.rs:45:22 diff --git a/core/codegen/tests/ui-fail-nightly/typed-uris-bad-params.stderr b/core/codegen/tests/ui-fail-nightly/typed-uris-bad-params.stderr index 4afd2f509e..536db778c7 100644 --- a/core/codegen/tests/ui-fail-nightly/typed-uris-bad-params.stderr +++ b/core/codegen/tests/ui-fail-nightly/typed-uris-bad-params.stderr @@ -285,10 +285,10 @@ error[E0271]: type mismatch resolving `>::Error == &str` --> tests/ui-fail-nightly/typed-uris-bad-params.rs:15:37 | 15 | fn optionals(id: Option, name: Result) { } - | ^^^^^^^^^^^^^^^^^^^^ expected `Infallible`, found `&str` + | ^^^^^^^^^^^^^^^^^^^^ expected `Empty`, found `&str` error[E0271]: type mismatch resolving `>::Error == &str` --> tests/ui-fail-nightly/typed-uris-bad-params.rs:15:37 | 15 | fn optionals(id: Option, name: Result) { } - | ^^^^^^^^^^^^^^^^^^^^ expected `&str`, found `Infallible` + | ^^^^^^^^^^^^^^^^^^^^ expected `&str`, found `Empty` 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 70fba713c0..218e523aeb 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 @@ -2,13 +2,13 @@ error[E0271]: type mismatch resolving `>::Error == &str` --> tests/ui-fail-stable/typed-uri-bad-type.rs:22:37 | 22 | fn optionals(id: Option, name: Result) { } - | ^^^^^^ expected enum `Infallible`, found `&str` + | ^^^^^^ expected struct `Empty`, found `&str` error[E0271]: type mismatch resolving `>::Error == &str` --> tests/ui-fail-stable/typed-uri-bad-type.rs:22:37 | 22 | fn optionals(id: Option, name: Result) { } - | ^^^^^^ expected `&str`, found enum `Infallible` + | ^^^^^^ expected `&str`, found struct `Empty` error[E0277]: the trait bound `usize: FromUriParam` is not satisfied --> tests/ui-fail-stable/typed-uri-bad-type.rs:45:22 diff --git a/core/codegen/tests/ui-fail-stable/typed-uris-bad-params.stderr b/core/codegen/tests/ui-fail-stable/typed-uris-bad-params.stderr index 87e3ed4d14..227923a0dc 100644 --- a/core/codegen/tests/ui-fail-stable/typed-uris-bad-params.stderr +++ b/core/codegen/tests/ui-fail-stable/typed-uris-bad-params.stderr @@ -276,10 +276,10 @@ error[E0271]: type mismatch resolving `>::Error == &str` --> tests/ui-fail-stable/typed-uris-bad-params.rs:15:37 | 15 | fn optionals(id: Option, name: Result) { } - | ^^^^^^ expected enum `Infallible`, found `&str` + | ^^^^^^ expected struct `Empty`, found `&str` error[E0271]: type mismatch resolving `>::Error == &str` --> tests/ui-fail-stable/typed-uris-bad-params.rs:15:37 | 15 | fn optionals(id: Option, name: Result) { } - | ^^^^^^ expected `&str`, found enum `Infallible` + | ^^^^^^ expected `&str`, found struct `Empty` diff --git a/core/lib/src/error.rs b/core/lib/src/error.rs index 0fcb91c4cc..314cf2c8b2 100644 --- a/core/lib/src/error.rs +++ b/core/lib/src/error.rs @@ -98,6 +98,10 @@ pub enum ErrorKind { ), } +/// An error that occurs when a value was unexpectedly empty. +#[derive(Clone, Copy, Default, PartialEq, Eq, Hash, PartialOrd, Ord)] +pub struct Empty; + impl From for Error { fn from(kind: ErrorKind) -> Self { Error::new(kind) @@ -259,3 +263,17 @@ impl Drop for Error { } } } + +impl fmt::Debug for Empty { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.write_str("empty parameter") + } +} + +impl fmt::Display for Empty { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.write_str("empty parameter") + } +} + +impl StdError for Empty { } diff --git a/core/lib/src/request/from_param.rs b/core/lib/src/request/from_param.rs index 3eca00846b..ffd73e34b8 100644 --- a/core/lib/src/request/from_param.rs +++ b/core/lib/src/request/from_param.rs @@ -1,6 +1,7 @@ use std::str::FromStr; use std::path::PathBuf; +use crate::error::Empty; use crate::http::uri::{Segments, error::PathError, fmt::Path}; /// Trait to convert a dynamic path segment string to a concrete value. @@ -184,20 +185,27 @@ pub trait FromParam<'a>: Sized { } impl<'a> FromParam<'a> for &'a str { - type Error = std::convert::Infallible; + type Error = Empty; #[inline(always)] fn from_param(param: &'a str) -> Result<&'a str, Self::Error> { + if param.is_empty() { + return Err(Empty); + } + Ok(param) } } impl<'a> FromParam<'a> for String { - type Error = std::convert::Infallible; + type Error = Empty; #[inline(always)] fn from_param(param: &'a str) -> Result { - // TODO: Tell the user they're being inefficient? + if param.is_empty() { + return Err(Empty); + } + Ok(param.to_string()) } } diff --git a/core/lib/src/request/request.rs b/core/lib/src/request/request.rs index 7365ec9e01..625c471568 100644 --- a/core/lib/src/request/request.rs +++ b/core/lib/src/request/request.rs @@ -802,6 +802,8 @@ impl<'r> Request<'r> { /// ```rust /// # let c = rocket::local::blocking::Client::debug_with(vec![]).unwrap(); /// # let get = |uri| c.get(uri); + /// use rocket::error::Empty; + /// /// assert_eq!(get("/a/b/c").param(0), Some(Ok("a"))); /// assert_eq!(get("/a/b/c").param(1), Some(Ok("b"))); /// assert_eq!(get("/a/b/c").param(2), Some(Ok("c"))); @@ -811,7 +813,7 @@ impl<'r> Request<'r> { /// assert!(get("/1/b/3").param::(1).unwrap().is_err()); /// assert_eq!(get("/1/b/3").param(2), Some(Ok(3))); /// - /// assert_eq!(get("/").param::<&str>(0), None); + /// assert_eq!(get("/").param::<&str>(0), Some(Err(Empty))); /// ``` #[inline] pub fn param<'a, T>(&'a self, n: usize) -> Option> @@ -940,7 +942,7 @@ impl<'r> Request<'r> { /// codegen. #[inline] pub fn routed_segment(&self, n: usize) -> Option<&str> { - self.routed_segments(0..).get(n).filter(|p| !p.is_empty()) + self.routed_segments(0..).get(n) } /// Get the segments beginning at the `n`th, 0-indexed, after the mount diff --git a/core/lib/src/route/uri.rs b/core/lib/src/route/uri.rs index a0c885b79c..7d0f0f0187 100644 --- a/core/lib/src/route/uri.rs +++ b/core/lib/src/route/uri.rs @@ -127,8 +127,6 @@ impl<'a> RouteUri<'a> { .into_normalized() .into_owned(); - dbg!(&base, &origin, &compiled_uri, &uri); - let source = uri.to_string().into(); let metadata = Metadata::from(&base, &uri); diff --git a/core/lib/src/router/collider.rs b/core/lib/src/router/collider.rs index 757a624d26..1384077492 100644 --- a/core/lib/src/router/collider.rs +++ b/core/lib/src/router/collider.rs @@ -19,7 +19,9 @@ impl Collide for Route { /// * If route doesn't specify a format, it gets requests for any format. /// /// Because query parsing is lenient, and dynamic query parameters can be - /// missing, queries do not impact whether two routes collide. + /// missing, the particularities of a query string do not impact whether two + /// routes collide. The query effects the route's color, however, which + /// effects its rank. fn collides_with(&self, other: &Route) -> bool { self.method == other.method && self.rank == other.rank @@ -64,9 +66,7 @@ impl Collide for RouteUri<'_> { impl Collide for Segment { fn collides_with(&self, other: &Self) -> bool { - self.dynamic && !other.value.is_empty() - || other.dynamic && !self.value.is_empty() - || self.value == other.value + self.dynamic || other.dynamic || self.value == other.value } } @@ -136,7 +136,6 @@ mod tests { #[test] fn non_collisions() { - assert_no_collision!("/", "/"); assert_no_collision!("/a", "/b"); assert_no_collision!("/a/b", "/a"); assert_no_collision!("/a/b", "/a/c"); @@ -152,7 +151,6 @@ mod tests { assert_no_collision!("/hello", "/hello/"); assert_no_collision!("/hello/there", "/hello/there/"); - assert_no_collision!("/hello/", "/hello/"); assert_no_collision!("/a?", "/b"); assert_no_collision!("/a/b", "/a?"); @@ -194,6 +192,8 @@ mod tests { assert_no_collision!("/a", "/aaa"); assert_no_collision!("/", "/a"); + assert_no_collision!(ranked "/", "/"); + assert_no_collision!(ranked "/hello/", "/hello/"); assert_no_collision!(ranked "/", "/?a"); assert_no_collision!(ranked "/", "/?"); assert_no_collision!(ranked "/a/", "/a/?d"); @@ -201,9 +201,11 @@ mod tests { #[test] fn collisions() { + assert_collision!("/", "/"); assert_collision!("/a", "/a"); assert_collision!("/hello", "/hello"); assert_collision!("/hello/there/how/ar", "/hello/there/how/ar"); + assert_collision!("/hello/", "/hello/"); assert_collision!("/", "/"); assert_collision!("/", "/b"); diff --git a/core/lib/src/router/matcher.rs b/core/lib/src/router/matcher.rs index 496efac042..521d96c9b4 100644 --- a/core/lib/src/router/matcher.rs +++ b/core/lib/src/router/matcher.rs @@ -32,9 +32,6 @@ impl Catcher { /// * It is a default catcher _or_ has a code of `status`. /// * Its base is a prefix of the normalized/decoded `req.path()`. pub(crate) fn matches(&self, status: Status, req: &Request<'_>) -> bool { - dbg!(self.base.path().segments()); - dbg!(req.uri().path().segments()); - self.code.map_or(true, |code| code == status.code) && self.base.path().segments().prefix_of(req.uri().path().segments()) } @@ -70,10 +67,6 @@ fn paths_match(route: &Route, req: &Request<'_>) -> bool { return true; } - if route_seg.dynamic && req_seg.is_empty() { - return false; - } - if !route_seg.dynamic && route_seg.value != req_seg { return false; } @@ -161,6 +154,8 @@ mod tests { assert!(req_matches_route("/a/b?c=foo&d=z", "/a/b?c=foo&")); assert!(req_matches_route("/a/b?c=foo&d=z", "/a/b?d=z&")); + assert!(req_matches_route("/", "/")); + assert!(req_matches_route("/a", "/")); assert!(req_matches_route("/a", "/a")); assert!(req_matches_route("/a/", "/a/")); diff --git a/core/lib/src/router/router.rs b/core/lib/src/router/router.rs index d74e6d3b2b..88d3a29353 100644 --- a/core/lib/src/router/router.rs +++ b/core/lib/src/router/router.rs @@ -185,6 +185,7 @@ mod test { assert!(rankless_route_collisions(&["/<_..>", "/<_>"])); assert!(rankless_route_collisions(&["/<_>/b", "/a/b"])); assert!(rankless_route_collisions(&["/", "/"])); + assert!(rankless_route_collisions(&["/<_>", "/"])); } #[test] @@ -232,13 +233,13 @@ mod test { assert!(!rankless_route_collisions(&["/a/b", "/a/b/c"])); assert!(!rankless_route_collisions(&["/a/b/c/d", "/a/b/c//e"])); assert!(!rankless_route_collisions(&["/a/d/", "/a/b/c"])); - assert!(!rankless_route_collisions(&["/<_>", "/"])); assert!(!rankless_route_collisions(&["/a/<_>", "/a"])); assert!(!rankless_route_collisions(&["/a/<_>", "/<_>"])); } #[test] fn test_no_collision_when_ranked() { + assert!(!default_rank_route_collisions(&["/<_>", "/"])); assert!(!default_rank_route_collisions(&["/", "/hello"])); assert!(!default_rank_route_collisions(&["/hello/bob", "/hello/"])); assert!(!default_rank_route_collisions(&["/a/b/c/d", "///c/d"])); @@ -298,6 +299,7 @@ mod test { assert!(route(&router, Get, "/hello").is_some()); let router = router_with_routes(&["/"]); + assert!(route(&router, Get, "/").is_some()); assert!(route(&router, Get, "/hello").is_some()); assert!(route(&router, Get, "/hi").is_some()); assert!(route(&router, Get, "/bobbbbbbbbbby").is_some()); @@ -307,6 +309,7 @@ mod test { assert!(route(&router, Get, "/hello/hi").is_some()); assert!(route(&router, Get, "/i/a").is_some()); assert!(route(&router, Get, "/jdlk/asdij").is_some()); + assert!(route(&router, Get, "/a/").is_some()); let mut router = Router::new(); router.add_route(Route::new(Put, "/hello", dummy_handler)); @@ -347,7 +350,6 @@ mod test { assert!(route(&router, Put, "/hello").is_none()); assert!(route(&router, Post, "/hello").is_none()); assert!(route(&router, Options, "/hello").is_none()); - assert!(route(&router, Get, "/").is_none()); assert!(route(&router, Get, "/hello/").is_none()); assert!(route(&router, Get, "/hello/there/").is_none()); assert!(route(&router, Get, "/hello/there/").is_none()); @@ -355,7 +357,6 @@ mod test { let router = router_with_routes(&["//"]); assert!(route(&router, Get, "/a/b/c").is_none()); assert!(route(&router, Get, "/a").is_none()); - assert!(route(&router, Get, "/a/").is_none()); assert!(route(&router, Get, "/a/b/c/d").is_none()); assert!(route(&router, Get, "/a/b/").is_none()); assert!(route(&router, Put, "/hello/hi").is_none()); diff --git a/examples/hello/src/main.rs b/examples/hello/src/main.rs index e9c1081a10..0f8c55cb1a 100644 --- a/examples/hello/src/main.rs +++ b/examples/hello/src/main.rs @@ -74,10 +74,6 @@ fn hello(lang: Option, opt: Options<'_>) -> String { #[launch] fn rocket() -> _ { - // FIXME: Check docs corresponding to normalization/matching/colliding. - // FUZZ: If rand_req1.matches(foo) && rand_req2.matches(bar) => - // rand_req1.collides_with(rand_req2) - rocket::build() .mount("/", routes![hello]) .mount("/hello", routes![world, mir]) From 908a918e8b2392eccbb809f05805a67034de966b Mon Sep 17 00:00:00 2001 From: Sergio Benitez Date: Fri, 7 Apr 2023 16:07:50 -0700 Subject: [PATCH 125/166] Fuzz to validate routing collision safety. The fuzzing target introduced in this commit attemps to assert "collision safety". Formally, this is the property that: matches(request, route) := request is matched to route collides(route1, route2) := there is a a collision between routes forall requests req. !exist routes r1, r2 s.t. matches(req, r1) AND matches(req, r2) AND not collides(r1, r2) Alternatively: forall requests req, routes r1, r2. matches(req, r1) AND matches(req, r2) => collides(r1, r2) The target was run for 20 CPU hours without failure. --- core/lib/fuzz/Cargo.toml | 13 ++ .../corpus/collision-matching/another.seed | 1 + .../fuzz/corpus/collision-matching/base.seed | 1 + .../corpus/collision-matching/complex.seed | 1 + .../fuzz/corpus/collision-matching/large.seed | 1 + core/lib/fuzz/targets/collision-matching.rs | 217 ++++++++++++++++++ core/lib/src/route/route.rs | 6 + core/lib/src/route/uri.rs | 4 +- core/lib/src/router/matcher.rs | 3 +- 9 files changed, 245 insertions(+), 2 deletions(-) create mode 100644 core/lib/fuzz/corpus/collision-matching/another.seed create mode 100644 core/lib/fuzz/corpus/collision-matching/base.seed create mode 100644 core/lib/fuzz/corpus/collision-matching/complex.seed create mode 100644 core/lib/fuzz/corpus/collision-matching/large.seed create mode 100644 core/lib/fuzz/targets/collision-matching.rs diff --git a/core/lib/fuzz/Cargo.toml b/core/lib/fuzz/Cargo.toml index 14a00c61dd..eeae9d2d54 100644 --- a/core/lib/fuzz/Cargo.toml +++ b/core/lib/fuzz/Cargo.toml @@ -10,6 +10,13 @@ cargo-fuzz = true [dependencies] libfuzzer-sys = "0.4" +arbitrary = { version = "1.3", features = ["derive"] } + +[target.'cfg(afl)'.dependencies] +afl = "*" + +[target.'cfg(honggfuzz)'.dependencies] +honggfuzz = "*" [dependencies.rocket] path = ".." @@ -35,3 +42,9 @@ name = "uri-normalization" path = "targets/uri-normalization.rs" test = false doc = false + +[[bin]] +name = "collision-matching" +path = "targets/collision-matching.rs" +test = false +doc = false diff --git a/core/lib/fuzz/corpus/collision-matching/another.seed b/core/lib/fuzz/corpus/collision-matching/another.seed new file mode 100644 index 0000000000..34add6a287 --- /dev/null +++ b/core/lib/fuzz/corpus/collision-matching/another.seed @@ -0,0 +1 @@ +01//foo/bar/b01/foo/a/b1/text/html diff --git a/core/lib/fuzz/corpus/collision-matching/base.seed b/core/lib/fuzz/corpus/collision-matching/base.seed new file mode 100644 index 0000000000..0a8a0a3890 --- /dev/null +++ b/core/lib/fuzz/corpus/collision-matching/base.seed @@ -0,0 +1 @@ +01//a/b01//a/b0/a/b diff --git a/core/lib/fuzz/corpus/collision-matching/complex.seed b/core/lib/fuzz/corpus/collision-matching/complex.seed new file mode 100644 index 0000000000..2a69fa9826 --- /dev/null +++ b/core/lib/fuzz/corpus/collision-matching/complex.seed @@ -0,0 +1 @@ +44/foo/bar/applicatiom/json1bazb01/foo/a/btext/plain1/fooktext/html diff --git a/core/lib/fuzz/corpus/collision-matching/large.seed b/core/lib/fuzz/corpus/collision-matching/large.seed new file mode 100644 index 0000000000..374cb09e73 --- /dev/null +++ b/core/lib/fuzz/corpus/collision-matching/large.seed @@ -0,0 +1 @@ +------------------------------------------------------ diff --git a/core/lib/fuzz/targets/collision-matching.rs b/core/lib/fuzz/targets/collision-matching.rs new file mode 100644 index 0000000000..b38a287575 --- /dev/null +++ b/core/lib/fuzz/targets/collision-matching.rs @@ -0,0 +1,217 @@ +#![cfg_attr(all(not(honggfuzz), not(afl)), no_main)] + +use arbitrary::{Arbitrary, Unstructured, Result, Error}; + +use rocket::http::QMediaType; +use rocket::local::blocking::{LocalRequest, Client}; +use rocket::http::{Method, Accept, ContentType, MediaType, uri::Origin}; +use rocket::route::{Route, RouteUri, dummy_handler}; + +#[derive(Arbitrary)] +struct ArbitraryRequestData<'a> { + method: ArbitraryMethod, + origin: ArbitraryOrigin<'a>, + format: Result, +} + +#[derive(Arbitrary)] +struct ArbitraryRouteData<'a> { + method: ArbitraryMethod, + uri: ArbitraryRouteUri<'a>, + format: Option, +} + +impl std::fmt::Debug for ArbitraryRouteData<'_> { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("ArbitraryRouteData") + .field("method", &self.method.0) + .field("base", &self.uri.0.base()) + .field("path", &self.uri.0.unmounted_origin.to_string()) + .field("uri", &self.uri.0.uri.to_string()) + .field("format", &self.format.as_ref().map(|v| v.0.to_string())) + .finish() + } +} + +impl std::fmt::Debug for ArbitraryRequestData<'_> { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("ArbitraryRequestData") + .field("method", &self.method.0) + .field("origin", &self.origin.0.to_string()) + .field("format", &self.format.as_ref() + .map_err(|v| v.0.to_string()) + .map(|v| v.0.to_string())) + .finish() + } +} + +impl<'c, 'a: 'c> ArbitraryRequestData<'a> { + fn into_local_request(self, client: &'c Client) -> LocalRequest<'c> { + let mut req = client.req(self.method.0, self.origin.0); + match self.format { + Ok(accept) => req.add_header(accept.0), + Err(content_type) => req.add_header(content_type.0), + } + + req + } +} + +impl<'a> ArbitraryRouteData<'a> { + fn into_route(self) -> Route { + let mut r = Route::ranked(0, self.method.0, self.uri.0.as_str(), dummy_handler); + r.format = self.format.map(|f| f.0); + r + } +} + +struct ArbitraryMethod(Method); + +struct ArbitraryOrigin<'a>(Origin<'a>); + +struct ArbitraryAccept(Accept); + +struct ArbitraryContentType(ContentType); + +struct ArbitraryMediaType(MediaType); + +struct ArbitraryRouteUri<'a>(RouteUri<'a>); + +impl<'a> Arbitrary<'a> for ArbitraryMethod { + fn arbitrary(u: &mut Unstructured<'a>) -> Result { + let all_methods = &[ + Method::Get, Method::Put, Method::Post, Method::Delete, Method::Options, + Method::Head, Method::Trace, Method::Connect, Method::Patch + ]; + + Ok(ArbitraryMethod(*u.choose(all_methods)?)) + } + + fn size_hint(_: usize) -> (usize, Option) { + (1, None) + } +} + +impl<'a> Arbitrary<'a> for ArbitraryOrigin<'a> { + fn arbitrary(u: &mut Unstructured<'a>) -> Result { + let string = u.arbitrary::<&str>()?; + if string.is_empty() { + return Err(Error::NotEnoughData); + } + + Origin::parse(string) + .map(ArbitraryOrigin) + .map_err(|_| Error::IncorrectFormat) + } + + fn size_hint(_: usize) -> (usize, Option) { + (1, None) + } +} + +impl<'a> Arbitrary<'a> for ArbitraryAccept { + fn arbitrary(u: &mut Unstructured<'a>) -> Result { + let media_type: ArbitraryMediaType = u.arbitrary()?; + Ok(Self(Accept::new(QMediaType(media_type.0, None)))) + } + + fn size_hint(depth: usize) -> (usize, Option) { + ArbitraryMediaType::size_hint(depth) + } +} + +impl<'a> Arbitrary<'a> for ArbitraryContentType { + fn arbitrary(u: &mut Unstructured<'a>) -> Result { + let media_type: ArbitraryMediaType = u.arbitrary()?; + Ok(ArbitraryContentType(ContentType(media_type.0))) + } + + fn size_hint(depth: usize) -> (usize, Option) { + ArbitraryMediaType::size_hint(depth) + } +} + +impl<'a> Arbitrary<'a> for ArbitraryMediaType { + fn arbitrary(u: &mut Unstructured<'a>) -> Result { + let known = [ + "txt", "html", "htm", "xml", "opf", "xhtml", "csv", "js", "css", "json", + "png", "gif", "bmp", "jpeg", "jpg", "webp", "avif", "svg", "ico", "flac", "wav", + "webm", "weba", "ogg", "ogv", "pdf", "ttf", "otf", "woff", "woff2", "mp3", "mp4", + "mpeg4", "wasm", "aac", "ics", "bin", "mpg", "mpeg", "tar", "gz", "tif", "tiff", "mov", + "zip", "cbz", "cbr", "rar", "epub", "md", "markdown" + ]; + + let choice = u.choose(&known[..])?; + let known = MediaType::from_extension(choice).unwrap(); + + let top = u.ratio(1, 100)?.then_some("*".into()).unwrap_or(known.top().to_string()); + let sub = u.ratio(1, 100)?.then_some("*".into()).unwrap_or(known.sub().to_string()); + let params = u.ratio(1, 10)? + .then_some(vec![]) + .unwrap_or(known.params().map(|(k, v)| (k.to_string(), v.to_owned())).collect()); + + let media_type = MediaType::new(top, sub).with_params(params); + Ok(ArbitraryMediaType(media_type)) + } + + fn size_hint(_: usize) -> (usize, Option) { + (3, None) + } +} + +impl<'a> Arbitrary<'a> for ArbitraryRouteUri<'a> { + fn arbitrary(u: &mut Unstructured<'a>) -> Result { + let (base, path) = (u.arbitrary::<&str>()?, u.arbitrary::<&str>()?); + if base.is_empty() || path.is_empty() { + return Err(Error::NotEnoughData); + } + + RouteUri::try_new(base, path) + .map(ArbitraryRouteUri) + .map_err(|_| Error::IncorrectFormat) + } + + fn size_hint(_: usize) -> (usize, Option) { + (2, None) + } +} + +type TestData<'a> = ( + ArbitraryRouteData<'a>, + ArbitraryRouteData<'a>, + ArbitraryRequestData<'a> +); + +fn fuzz((route_a, route_b, req): TestData<'_>) { + let rocket = rocket::custom(rocket::Config { + workers: 2, + log_level: rocket::log::LogLevel::Off, + cli_colors: false, + ..rocket::Config::debug_default() + }); + + let client = Client::untracked(rocket).expect("debug rocket is okay"); + let (route_a, route_b) = (route_a.into_route(), route_b.into_route()); + let local_request = req.into_local_request(&client); + let request = local_request.inner(); + + if route_a.matches(request) && route_b.matches(request) { + assert!(route_a.collides_with(&route_b)); + assert!(route_b.collides_with(&route_a)); + } +} + +#[cfg(all(not(honggfuzz), not(afl)))] +libfuzzer_sys::fuzz_target!(|data: TestData| { fuzz(data) }); + +#[cfg(honggbuzz)] +fn main() { + loop { + honggfuzz::fuzz!(|data: TestData| { fuzz(data) }); + } +} + +#[cfg(afl)] +fn main() { + afl::fuzz!(|data: TestData| { fuzz(data) }); +} diff --git a/core/lib/src/route/route.rs b/core/lib/src/route/route.rs index a069e8069a..994cb05897 100644 --- a/core/lib/src/route/route.rs +++ b/core/lib/src/route/route.rs @@ -290,6 +290,12 @@ impl Route { self.uri = RouteUri::try_new(&base, &self.uri.unmounted_origin.to_string())?; Ok(self) } + + /// Returns `true` if `self` collides with `other`. + #[doc(hidden)] + pub fn collides_with(&self, other: &Route) -> bool { + crate::router::Collide::collides_with(self, other) + } } impl fmt::Display for Route { diff --git a/core/lib/src/route/uri.rs b/core/lib/src/route/uri.rs index 7d0f0f0187..b3b8b19ac3 100644 --- a/core/lib/src/route/uri.rs +++ b/core/lib/src/route/uri.rs @@ -100,7 +100,9 @@ impl<'a> RouteUri<'a> { /// /// This is a fallible variant of [`RouteUri::new`] which returns an `Err` /// if `base` or `uri` cannot be parsed as [`Origin`]s. - pub(crate) fn try_new(base: &str, uri: &str) -> Result> { + /// INTERNAL! + #[doc(hidden)] + pub fn try_new(base: &str, uri: &str) -> Result> { let mut base = Origin::parse(base) .map_err(|e| e.into_owned())? .into_normalized_nontrailing() diff --git a/core/lib/src/router/matcher.rs b/core/lib/src/router/matcher.rs index 521d96c9b4..b6f3497826 100644 --- a/core/lib/src/router/matcher.rs +++ b/core/lib/src/router/matcher.rs @@ -17,7 +17,8 @@ impl Route { /// * All static components in the route's query string are also in the /// request query string, though in any position. If there is no query /// in the route, requests with/without queries match. - pub(crate) fn matches(&self, req: &Request<'_>) -> bool { + #[doc(hidden)] + pub fn matches(&self, req: &Request<'_>) -> bool { self.method == req.method() && paths_match(self, req) && queries_match(self, req) From 51ed332127df2737a7d1c1a22a1e13379ea5f6d5 Mon Sep 17 00:00:00 2001 From: Sergio Benitez Date: Mon, 10 Apr 2023 10:48:30 -0700 Subject: [PATCH 126/166] Make trailing slashes significant during routing. This commit modifies request routing in a backwards incompatible manner. The change is summarized as: trailing slashes are now significant and never transparently disregarded. This has the following implications, all representing behavior that differs from that before this change: * Route URIs with trailing slashes (`/foo/`, `//`) are legal. * A request `/foo/` is routed to route `/foo/` but not `/foo`. * Similarly, a request `/bar/` is routed to `//` but not `/`. * A request `/bar/foo` is not routed to `///`. A new `AdHoc::uri_normalizer()` fairing was added that recovers the previous behavior. In addition to the above, the `Options::NormalizeDirs` `FileServer` option is now enabled by default to remain consistent with the above changes and reduce breaking changes at the `FileServer` level. --- core/codegen/src/attribute/route/parse.rs | 6 +- core/codegen/src/bang/uri_parsing.rs | 2 +- core/codegen/tests/route.rs | 3 +- .../route-path-bad-syntax.stderr | 26 +------ .../route-path-bad-syntax.stderr | 23 +----- core/lib/src/fairing/ad_hoc.rs | 78 +++++++++++++++++++ core/lib/src/fs/server.rs | 52 +++++++------ core/lib/src/router/collider.rs | 25 ++++-- core/lib/src/router/matcher.rs | 27 +++---- core/lib/src/router/router.rs | 21 ++--- core/lib/tests/file_server.rs | 16 ++-- examples/hello/src/main.rs | 11 +++ examples/static-files/src/tests.rs | 2 +- 13 files changed, 179 insertions(+), 113 deletions(-) diff --git a/core/codegen/src/attribute/route/parse.rs b/core/codegen/src/attribute/route/parse.rs index 888951615c..59cbd4e34d 100644 --- a/core/codegen/src/attribute/route/parse.rs +++ b/core/codegen/src/attribute/route/parse.rs @@ -77,11 +77,9 @@ impl FromMeta for RouteUri { .help("expected URI in origin form: \"/path/\"") })?; - if !origin.is_normalized_nontrailing() { - let normalized = origin.clone().into_normalized_nontrailing(); + if !origin.is_normalized() { + let normalized = origin.clone().into_normalized(); let span = origin.path().find("//") - .or_else(|| origin.has_trailing_slash() - .then_some(origin.path().len() - 1)) .or_else(|| origin.query() .and_then(|q| q.find("&&")) .map(|i| origin.path().len() + 1 + i)) diff --git a/core/codegen/src/bang/uri_parsing.rs b/core/codegen/src/bang/uri_parsing.rs index e6ae1e23ad..d7c772dc59 100644 --- a/core/codegen/src/bang/uri_parsing.rs +++ b/core/codegen/src/bang/uri_parsing.rs @@ -309,7 +309,7 @@ impl Parse for InternalUriParams { // Validation should always succeed since this macro can only be called // if the route attribute succeeded, implying a valid route URI. let route_uri = Origin::parse_route(&route_uri_str) - .map(|o| o.into_normalized_nontrailing().into_owned()) + .map(|o| o.into_normalized().into_owned()) .map_err(|_| input.error("internal error: invalid route URI"))?; let content; diff --git a/core/codegen/tests/route.rs b/core/codegen/tests/route.rs index 6fe269aa00..d8e4509418 100644 --- a/core/codegen/tests/route.rs +++ b/core/codegen/tests/route.rs @@ -338,8 +338,9 @@ fn test_inclusive_segments() { assert_eq!(get("//a/"), "empty+a/"); assert_eq!(get("//a//"), "empty+a/"); assert_eq!(get("//a//c/d"), "empty+a/c/d"); + assert_eq!(get("//a/b"), "empty+a/b"); - assert_eq!(get("//a/b"), "nonempty+"); + assert_eq!(get("//a/b/"), "nonempty+"); assert_eq!(get("//a/b/c"), "nonempty+c"); assert_eq!(get("//a/b//c"), "nonempty+c"); assert_eq!(get("//a//b////c"), "nonempty+c"); 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 f322538b5d..a592cc8609 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 @@ -52,7 +52,7 @@ error: route URIs cannot contain empty segments 23 | #[get("/a/b//")] | ^^ | - = note: expected "/a/b", found "/a/b//" + = note: expected "/a/b/", found "/a/b//" error: unused parameter --> tests/ui-fail-nightly/route-path-bad-syntax.rs:42:10 @@ -240,27 +240,3 @@ warning: `segment` starts with `<` but does not end with `>` | ^^^^^^^^ | = help: perhaps you meant the dynamic parameter ``? - -error: route URIs cannot contain empty segments - --> tests/ui-fail-nightly/route-path-bad-syntax.rs:107:10 - | -107 | #[get("/a/")] - | ^^ - | - = note: expected "/a", found "/a/" - -error: route URIs cannot contain empty segments - --> tests/ui-fail-nightly/route-path-bad-syntax.rs:110:12 - | -110 | #[get("/a/b/")] - | ^^ - | - = note: expected "/a/b", found "/a/b/" - -error: route URIs cannot contain empty segments - --> tests/ui-fail-nightly/route-path-bad-syntax.rs:113:14 - | -113 | #[get("/a/b/c/")] - | ^^ - | - = note: expected "/a/b/c", found "/a/b/c/" 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 bc4b8ccc94..7263aa202e 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 @@ -41,7 +41,7 @@ error: route URIs cannot contain empty segments | ^^^^^^^^^ error: route URIs cannot contain empty segments - --- note: expected "/a/b", found "/a/b//" + --- note: expected "/a/b/", found "/a/b//" --> tests/ui-fail-stable/route-path-bad-syntax.rs:23:7 | 23 | #[get("/a/b//")] @@ -180,24 +180,3 @@ error: parameters cannot be empty | 93 | #[get("/<>")] | ^^^^^ - -error: route URIs cannot contain empty segments - --- note: expected "/a", found "/a/" - --> tests/ui-fail-stable/route-path-bad-syntax.rs:107:7 - | -107 | #[get("/a/")] - | ^^^^^ - -error: route URIs cannot contain empty segments - --- note: expected "/a/b", found "/a/b/" - --> tests/ui-fail-stable/route-path-bad-syntax.rs:110:7 - | -110 | #[get("/a/b/")] - | ^^^^^^^ - -error: route URIs cannot contain empty segments - --- note: expected "/a/b/c", found "/a/b/c/" - --> tests/ui-fail-stable/route-path-bad-syntax.rs:113:7 - | -113 | #[get("/a/b/c/")] - | ^^^^^^^^^ diff --git a/core/lib/src/fairing/ad_hoc.rs b/core/lib/src/fairing/ad_hoc.rs index f4ea6e1b89..4a4d75eef7 100644 --- a/core/lib/src/fairing/ad_hoc.rs +++ b/core/lib/src/fairing/ad_hoc.rs @@ -242,6 +242,84 @@ impl AdHoc { Ok(rocket.manage(app_config)) }) } + + /// Constructs an `AdHoc` request fairing that strips trailing slashes from + /// all URIs in all incoming requests. + /// + /// The fairing returned by this method is intended largely for applications + /// that migrated from Rocket v0.4 to Rocket v0.5. In Rocket v0.4, requests + /// with a trailing slash in the URI were treated as if the trailing slash + /// were not present. For example, the request URI `/foo/` would match the + /// route `/` with `a = foo`. If the application depended on this + /// behavior, say by using URIs with previously innocuous trailing slashes + /// in an external application, requests will not be routed as expected. + /// + /// This fairing resolves this issue by stripping a trailing slash, if any, + /// in all incoming URIs. When it does so, it logs a warning. It is + /// recommended to use this fairing as a stop-gap measure instead of a + /// permanent resolution, if possible. + // + /// # Example + /// + /// With the fairing attached, request URIs have a trailing slash stripped: + /// + /// ```rust + /// # #[macro_use] extern crate rocket; + /// use rocket::local::blocking::Client; + /// use rocket::fairing::AdHoc; + /// + /// #[get("/")] + /// fn foo(param: &str) -> &str { + /// param + /// } + /// + /// #[launch] + /// fn rocket() -> _ { + /// rocket::build() + /// .mount("/", routes![foo]) + /// .attach(AdHoc::uri_normalizer()) + /// } + /// + /// # let client = Client::debug(rocket()).unwrap(); + /// let response = client.get("/bar/").dispatch(); + /// assert_eq!(response.into_string().unwrap(), "bar"); + /// ``` + /// + /// Without it, request URIs are unchanged and routed normally: + /// + /// ```rust + /// # #[macro_use] extern crate rocket; + /// use rocket::local::blocking::Client; + /// use rocket::fairing::AdHoc; + /// + /// #[get("/")] + /// fn foo(param: &str) -> &str { + /// param + /// } + /// + /// #[launch] + /// fn rocket() -> _ { + /// rocket::build().mount("/", routes![foo]) + /// } + /// + /// # let client = Client::debug(rocket()).unwrap(); + /// let response = client.get("/bar/").dispatch(); + /// assert!(response.status().class().is_client_error()); + /// + /// let response = client.get("/bar").dispatch(); + /// assert_eq!(response.into_string().unwrap(), "bar"); + /// ``` + #[deprecated(since = "0.6", note = "routing from Rocket v0.5 is now standard")] + pub fn uri_normalizer() -> AdHoc { + AdHoc::on_request("URI Normalizer", |req, _| Box::pin(async move { + if !req.uri().is_normalized_nontrailing() { + let normal = req.uri().clone().into_normalized_nontrailing(); + warn!("Incoming request URI was normalized for compatibility."); + info_!("{} -> {}", req.uri(), normal); + req.set_uri(normal); + } + })) + } } #[crate::async_trait] diff --git a/core/lib/src/fs/server.rs b/core/lib/src/fs/server.rs index e806a932a7..267c419163 100644 --- a/core/lib/src/fs/server.rs +++ b/core/lib/src/fs/server.rs @@ -24,9 +24,8 @@ use crate::fs::NamedFile; /// /// # Example /// -/// To serve files from the `/static` directory on the local file system at the -/// `/public` path, allowing `index.html` files to be used to respond to -/// requests for a directory (the default), you might write the following: +/// Serve files from the `/static` directory on the local file system at the +/// `/public` path with the [default options](#impl-Default): /// /// ```rust,no_run /// # #[macro_use] extern crate rocket; @@ -38,18 +37,18 @@ use crate::fs::NamedFile; /// } /// ``` /// -/// With this, requests for files at `/public/` will be handled by -/// returning the contents of `/static/`. Requests for _directories_ at +/// Requests for files at `/public/` will be handled by returning the +/// contents of `/static/`. Requests for _directories_ at /// `/public/` will be handled by returning the contents of /// `/static//index.html`. /// /// ## Relative Paths /// /// In the example above, `/static` is an absolute path. If your static files -/// are stored relative to your crate and your project is managed by Rocket, use -/// the [`relative!`] macro to obtain a path that is relative to your -/// crate's root. For example, to serve files in the `static` subdirectory of -/// your crate at `/`, you might write: +/// are stored relative to your crate and your project is managed by Cargo, use +/// the [`relative!`] macro to obtain a path that is relative to your crate's +/// root. For example, to serve files in the `static` subdirectory of your crate +/// at `/`, you might write: /// /// ```rust,no_run /// # #[macro_use] extern crate rocket; @@ -263,8 +262,8 @@ pub struct Options(u8); impl Options { /// All options disabled. /// - /// This is different than [`Options::default()`](#impl-Default), which - /// enables `Options::Index`. + /// Note that this is different than [`Options::default()`](#impl-Default), + /// which enables options. pub const None: Options = Options(0); /// Respond to requests for a directory with the `index.html` file in that @@ -289,14 +288,14 @@ impl Options { /// Normalizes directory requests by redirecting requests to directory paths /// without a trailing slash to ones with a trailing slash. /// + /// **Enabled by default.** + /// /// When enabled, the [`FileServer`] 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 from that directory will be interpreted relative to that /// directory rather than its parent. /// - /// **Disabled by default.** - /// /// # Example /// /// Given the following directory structure... @@ -308,14 +307,21 @@ impl Options { /// └── index.html /// ``` /// - /// ...with `FileServer::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. + /// And the following server: + /// + /// ```text + /// rocket.mount("/", FileServer::from("static")) + /// ``` + /// + /// ...requests to `example.com/foo` will be redirected to + /// `example.com/foo/`. If `index.html` references `cat.jpeg` as a relative + /// URL, the browser will resolve the URL to `example.com/foo/cat.jpeg`, + /// which in-turn Rocket will match to `/static/foo/cat.jpg`. + /// + /// Without this option, requests to `example.com/foo` would not be + /// redirected. `index.html` would be rendered, and the relative link to + /// `cat.jpeg` would be resolved by the browser as `example.com/cat.jpeg`. + /// Rocket would thus try to find `/static/cat.jpeg`, which does not exist. pub const NormalizeDirs: Options = Options(1 << 2); /// Allow serving a file instead of a directory. @@ -380,10 +386,10 @@ impl Options { } } -/// The default set of options: `Options::Index`. +/// The default set of options: `Options::Index | Options:NormalizeDirs`. impl Default for Options { fn default() -> Self { - Options::Index + Options::Index | Options::NormalizeDirs } } diff --git a/core/lib/src/router/collider.rs b/core/lib/src/router/collider.rs index 1384077492..7968bd0811 100644 --- a/core/lib/src/router/collider.rs +++ b/core/lib/src/router/collider.rs @@ -57,10 +57,7 @@ impl Collide for RouteUri<'_> { } } - // Check for `/a/` vs. `/a`, which should collide. - a_segments.get(b_segments.len()).map_or(false, |s| s.dynamic_trail) - || b_segments.get(a_segments.len()).map_or(false, |s| s.dynamic_trail) - || a_segments.len() == b_segments.len() + a_segments.len() == b_segments.len() } } @@ -192,7 +189,23 @@ mod tests { assert_no_collision!("/a", "/aaa"); assert_no_collision!("/", "/a"); + assert_no_collision!("/foo", "/foo/"); + assert_no_collision!("/foo/bar", "/foo/"); + assert_no_collision!("/foo/bar", "/foo/bar/"); + assert_no_collision!("/foo/", "/foo//"); + assert_no_collision!("/foo/", "///"); + assert_no_collision!("//", "///"); + assert_no_collision!("/a/", "///"); + + assert_no_collision!("/a", "/a/"); + assert_no_collision!("/", "/a/"); + assert_no_collision!("/a/b", "///"); + assert_no_collision!("/a/", "///"); + assert_no_collision!("//b", "///"); + assert_no_collision!("/hi/", "/hi"); + assert_no_collision!(ranked "/", "/"); + assert_no_collision!(ranked "/a/", "//"); assert_no_collision!(ranked "/hello/", "/hello/"); assert_no_collision!(ranked "/", "/?a"); assert_no_collision!(ranked "/", "/?"); @@ -227,10 +240,9 @@ mod tests { assert_collision!("/", "/foo"); assert_collision!("/", "/"); - assert_collision!("/a", "/a/"); assert_collision!("/a/", "/a/"); assert_collision!("//", "/a/"); - assert_collision!("/", "/a/"); + assert_collision!("//bar/", "/a/"); assert_collision!("/", "/b"); assert_collision!("/hello/", "/hello/bob"); @@ -244,7 +256,6 @@ mod tests { assert_collision!("/", "/<_..>"); assert_collision!("/a/b/", "/a/"); assert_collision!("/a/b/", "/a//"); - assert_collision!("/hi/", "/hi"); assert_collision!("/hi/", "/hi/"); assert_collision!("/", "//////"); diff --git a/core/lib/src/router/matcher.rs b/core/lib/src/router/matcher.rs index b6f3497826..2f2b89a3c8 100644 --- a/core/lib/src/router/matcher.rs +++ b/core/lib/src/router/matcher.rs @@ -43,20 +43,14 @@ fn paths_match(route: &Route, req: &Request<'_>) -> bool { let route_segments = &route.uri.metadata.uri_segments; let req_segments = req.uri().path().segments(); - // requests with longer paths only match if we have dynamic trail (). - if req_segments.num() > route_segments.len() { - if !route.uri.metadata.dynamic_trail { - return false; - } - } - - // The last route segment can be trailing (`/<..>`), which is allowed to be - // empty in the request. That is, we want to match `GET /a` to `/a/`. + // A route can never have more segments than a request. Recall that a + // trailing slash is considering a segment, albeit empty. if route_segments.len() > req_segments.num() { - if route_segments.len() != req_segments.num() + 1 { - return false; - } + return false; + } + // requests with longer paths only match if we have dynamic trail (). + if req_segments.num() > route_segments.len() { if !route.uri.metadata.dynamic_trail { return false; } @@ -151,7 +145,6 @@ mod tests { assert!(req_matches_route("/a/b?c", "/a/b?")); assert!(req_matches_route("/a/b?c=foo&d=z", "/a/b?")); assert!(req_matches_route("/a/b?c=foo&d=z", "/a/b?")); - assert!(req_matches_route("/a/b?c=foo&d=z", "/a/b?c=foo&")); assert!(req_matches_route("/a/b?c=foo&d=z", "/a/b?d=z&")); @@ -169,11 +162,19 @@ mod tests { assert!(!req_matches_route("/a/", "/a")); assert!(!req_matches_route("/a/b", "/a/b/")); + assert!(!req_matches_route("/a", "//")); + assert!(!req_matches_route("/a/", "/")); + assert!(!req_matches_route("/a/b", "//b/")); + assert!(!req_matches_route("/a/b", "///")); + assert!(!req_matches_route("/a/b/c", "/a/b?")); assert!(!req_matches_route("/a?b=c", "/a/b?")); assert!(!req_matches_route("/?b=c", "/a/b?")); assert!(!req_matches_route("/?b=c", "/a?")); + assert!(!req_matches_route("/a/", "///")); + assert!(!req_matches_route("/a/b", "///")); + assert!(!req_matches_route("/a/b?c=foo&d=z", "/a/b?a=b&")); assert!(!req_matches_route("/a/b?c=foo&d=z", "/a/b?d=b&")); assert!(!req_matches_route("/a/b", "/a/b?c")); diff --git a/core/lib/src/router/router.rs b/core/lib/src/router/router.rs index 88d3a29353..5617f4fbcd 100644 --- a/core/lib/src/router/router.rs +++ b/core/lib/src/router/router.rs @@ -170,13 +170,7 @@ mod test { assert!(rankless_route_collisions(&["/", "/"])); assert!(rankless_route_collisions(&["/a/<_>", "/a/"])); assert!(rankless_route_collisions(&["/a/<_>", "/a/<_..>"])); - assert!(rankless_route_collisions(&["/<_>", "/a/<_..>"])); - assert!(rankless_route_collisions(&["/foo", "/foo/<_..>"])); assert!(rankless_route_collisions(&["/foo/bar/baz", "/foo/<_..>"])); - assert!(rankless_route_collisions(&["/a/d/", "/a/d"])); - assert!(rankless_route_collisions(&["/a/<_..>", "/<_>"])); - assert!(rankless_route_collisions(&["/a/<_..>", "/a"])); - assert!(rankless_route_collisions(&["/", "/a/"])); assert!(rankless_route_collisions(&["/<_>", "/<_>"])); assert!(rankless_route_collisions(&["/a/<_>", "/a/b"])); @@ -235,6 +229,13 @@ mod test { assert!(!rankless_route_collisions(&["/a/d/", "/a/b/c"])); assert!(!rankless_route_collisions(&["/a/<_>", "/a"])); assert!(!rankless_route_collisions(&["/a/<_>", "/<_>"])); + assert!(!rankless_route_collisions(&["/a//", "/a/"])); + assert!(!rankless_route_collisions(&["/<_>", "/a/<_..>"])); + assert!(!rankless_route_collisions(&["/foo", "/foo/<_..>"])); + assert!(!rankless_route_collisions(&["/a/<_..>", "/<_>"])); + assert!(!rankless_route_collisions(&["/a/<_..>", "/a"])); + assert!(!rankless_route_collisions(&["/", "/a/"])); + assert!(!rankless_route_collisions(&["/a/d/", "/a/d"])); } #[test] @@ -259,11 +260,11 @@ mod test { assert!(!default_rank_route_collisions(&["/", "/hello"])); assert!(!default_rank_route_collisions(&["/", "/a/"])); assert!(!default_rank_route_collisions(&["/a//c", "//"])); + assert!(!default_rank_route_collisions(&["/a//", "/a/"])); } #[test] fn test_collision_when_ranked() { - assert!(default_rank_route_collisions(&["/a//", "/a/"])); assert!(default_rank_route_collisions(&["//b", "/a/"])); } @@ -329,7 +330,7 @@ mod test { assert!(route(&router, Get, "/a/b/c/d/e/f").is_some()); let router = router_with_routes(&["/foo/"]); - assert!(route(&router, Get, "/foo").is_some()); + assert!(route(&router, Get, "/foo").is_none()); assert!(route(&router, Get, "/foo/").is_some()); assert!(route(&router, Get, "/foo///bar").is_some()); } @@ -497,9 +498,9 @@ mod test { ); assert_ranked_routing!( - to: "/hi", + to: "/hi/", with: [(1, "/hi/"), (0, "/hi/")], - expect: (1, "/hi/") + expect: (0, "/hi/"), (1, "/hi/") ); } diff --git a/core/lib/tests/file_server.rs b/core/lib/tests/file_server.rs index 1271d6effd..69416edae2 100644 --- a/core/lib/tests/file_server.rs +++ b/core/lib/tests/file_server.rs @@ -172,17 +172,21 @@ fn test_redirection() { assert_eq!(response.status(), Status::Ok); // Root of route is also redirected. - let response = client.get("/no_index").dispatch(); + let response = client.get("/no_index/").dispatch(); assert_eq!(response.status(), Status::NotFound); - let response = client.get("/index").dispatch(); + let response = client.get("/index/").dispatch(); assert_eq!(response.status(), Status::Ok); - let response = client.get("/redir").dispatch(); + let response = client.get("/redir/inner").dispatch(); + assert_eq!(response.status(), Status::PermanentRedirect); + assert_eq!(response.headers().get("Location").next(), Some("/redir/inner/")); + + let response = client.get("/redir/other").dispatch(); assert_eq!(response.status(), Status::PermanentRedirect); - assert_eq!(response.headers().get("Location").next(), Some("/redir/")); + assert_eq!(response.headers().get("Location").next(), Some("/redir/other/")); - let response = client.get("/redir_index").dispatch(); + let response = client.get("/redir_index/other").dispatch(); assert_eq!(response.status(), Status::PermanentRedirect); - assert_eq!(response.headers().get("Location").next(), Some("/redir_index/")); + assert_eq!(response.headers().get("Location").next(), Some("/redir_index/other/")); } diff --git a/examples/hello/src/main.rs b/examples/hello/src/main.rs index 0f8c55cb1a..79b4588aca 100644 --- a/examples/hello/src/main.rs +++ b/examples/hello/src/main.rs @@ -74,8 +74,19 @@ fn hello(lang: Option, opt: Options<'_>) -> String { #[launch] fn rocket() -> _ { + use rocket::fairing::AdHoc; + rocket::build() .mount("/", routes![hello]) .mount("/hello", routes![world, mir]) .mount("/wave", routes![wave]) + .attach(AdHoc::on_request("Compatibility Normalizer", |req, _| Box::pin(async move { + if !req.uri().is_normalized_nontrailing() { + let normal = req.uri().clone().into_normalized_nontrailing(); + warn!("Incoming request URI was normalized for compatibility."); + info_!("{} -> {}", req.uri(), normal); + req.set_uri(normal); + } + }))) + } diff --git a/examples/static-files/src/tests.rs b/examples/static-files/src/tests.rs index b437f77df3..df096dc604 100644 --- a/examples/static-files/src/tests.rs +++ b/examples/static-files/src/tests.rs @@ -40,7 +40,6 @@ fn test_index_html() { #[test] fn test_hidden_index_html() { - test_query_file("/hidden", "static/hidden/index.html", Status::Ok); test_query_file("/hidden/", "static/hidden/index.html", Status::Ok); test_query_file("//hidden//", "static/hidden/index.html", Status::Ok); test_query_file("/second/hidden", "static/hidden/index.html", Status::Ok); @@ -65,6 +64,7 @@ fn test_icon_file() { #[test] fn test_invalid_path() { + test_query_file("/hidden", None, Status::PermanentRedirect); test_query_file("/thou_shalt_not_exist", None, Status::NotFound); test_query_file("/thou/shalt/not/exist", None, Status::NotFound); test_query_file("/thou/shalt/not/exist?a=b&c=d", None, Status::NotFound); From 3a44b1b28e6c22b1d316409269999dc8e8395444 Mon Sep 17 00:00:00 2001 From: Sergio Benitez Date: Mon, 10 Apr 2023 12:31:17 -0700 Subject: [PATCH 127/166] Hide 'RouteUri' fields to ensure URI coherence. Prior to this commit, several `RouteUri` fields were public, allowing those values to be changed at will. These changes were at times not reflected by the rest of the library, meaning that the values in the route URI structure for a route became incoherent with the reflected values. This commit makes all fields private, forcing all changes to go through methods that can ensure coherence. All values remain accessible via getter methods. --- benchmarks/src/routing.rs | 4 +- core/http/src/raw_str.rs | 7 ++ core/http/src/uri/origin.rs | 2 +- core/http/src/uri/path_query.rs | 6 + core/http/src/uri/uri.rs | 10 +- core/lib/fuzz/targets/collision-matching.rs | 6 +- core/lib/src/route/route.rs | 18 ++- core/lib/src/route/uri.rs | 124 +++++++------------- core/lib/tests/route_guard.rs | 2 +- 9 files changed, 87 insertions(+), 92 deletions(-) diff --git a/benchmarks/src/routing.rs b/benchmarks/src/routing.rs index 1a6dafcd8b..8f842896fe 100644 --- a/benchmarks/src/routing.rs +++ b/benchmarks/src/routing.rs @@ -45,13 +45,13 @@ fn generate_matching_requests<'c>(client: &'c Client, routes: &[Route]) -> Vec(client: &'c Client, route: &Route) -> LocalRequest<'c> { - let path = route.uri.uri.path() + let path = route.uri.path() .raw_segments() .map(staticify_segment) .collect::>() .join("/"); - let query = route.uri.uri.query() + let query = route.uri.query() .map(|q| q.raw_segments()) .into_iter() .flatten() diff --git a/core/http/src/raw_str.rs b/core/http/src/raw_str.rs index 1aa37b2bfa..372453e831 100644 --- a/core/http/src/raw_str.rs +++ b/core/http/src/raw_str.rs @@ -1009,6 +1009,13 @@ impl AsRef for RawStr { } } +impl AsRef for RawStr { + #[inline(always)] + fn as_ref(&self) -> &std::ffi::OsStr { + self.as_str().as_ref() + } +} + impl AsRef for str { #[inline(always)] fn as_ref(&self) -> &RawStr { diff --git a/core/http/src/uri/origin.rs b/core/http/src/uri/origin.rs index 716dd1635d..ebe5e444d6 100644 --- a/core/http/src/uri/origin.rs +++ b/core/http/src/uri/origin.rs @@ -548,7 +548,7 @@ impl<'a> Origin<'a> { impl_serde!(Origin<'a>, "an origin-form URI"); -impl_traits!(Origin, path, query); +impl_traits!(Origin [parse_route], path, query); impl std::fmt::Display for Origin<'_> { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { diff --git a/core/http/src/uri/path_query.rs b/core/http/src/uri/path_query.rs index 2b9ea23dfc..16733bc7af 100644 --- a/core/http/src/uri/path_query.rs +++ b/core/http/src/uri/path_query.rs @@ -416,6 +416,12 @@ macro_rules! impl_traits { } } + impl AsRef for $T<'_> { + fn as_ref(&self) -> &std::ffi::OsStr { + self.raw().as_ref() + } + } + impl std::fmt::Display for $T<'_> { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!(f, "{}", self.raw()) diff --git a/core/http/src/uri/uri.rs b/core/http/src/uri/uri.rs index 57e992ea00..14247670e1 100644 --- a/core/http/src/uri/uri.rs +++ b/core/http/src/uri/uri.rs @@ -391,7 +391,10 @@ macro_rules! impl_serde { /// Implements traits from `impl_base_traits` and IntoOwned for a URI. macro_rules! impl_traits { ($T:ident, $($field:ident),* $(,)?) => { - impl_base_traits!($T, $($field),*); + impl_traits!($T [parse], $($field),*); + }; + ($T:ident [$partial_eq_parse:ident], $($field:ident),* $(,)?) => { + impl_base_traits!($T [$partial_eq_parse], $($field),*); impl crate::ext::IntoOwned for $T<'_> { type Owned = $T<'static>; @@ -409,6 +412,9 @@ macro_rules! impl_traits { /// Implements PartialEq, Eq, Hash, and TryFrom. macro_rules! impl_base_traits { ($T:ident, $($field:ident),* $(,)?) => { + impl_base_traits!($T [parse], $($field),*); + }; + ($T:ident [$partial_eq_parse:ident], $($field:ident),* $(,)?) => { impl std::convert::TryFrom for $T<'static> { type Error = Error<'static>; @@ -442,7 +448,7 @@ macro_rules! impl_base_traits { impl PartialEq for $T<'_> { fn eq(&self, string: &str) -> bool { - $T::parse(string).map_or(false, |v| &v == self) + $T::$partial_eq_parse(string).map_or(false, |v| &v == self) } } diff --git a/core/lib/fuzz/targets/collision-matching.rs b/core/lib/fuzz/targets/collision-matching.rs index b38a287575..ad350eac35 100644 --- a/core/lib/fuzz/targets/collision-matching.rs +++ b/core/lib/fuzz/targets/collision-matching.rs @@ -26,8 +26,8 @@ impl std::fmt::Debug for ArbitraryRouteData<'_> { f.debug_struct("ArbitraryRouteData") .field("method", &self.method.0) .field("base", &self.uri.0.base()) - .field("path", &self.uri.0.unmounted_origin.to_string()) - .field("uri", &self.uri.0.uri.to_string()) + .field("unmounted", &self.uri.0.unmounted().to_string()) + .field("uri", &self.uri.0.to_string()) .field("format", &self.format.as_ref().map(|v| v.0.to_string())) .finish() } @@ -59,7 +59,7 @@ impl<'c, 'a: 'c> ArbitraryRequestData<'a> { impl<'a> ArbitraryRouteData<'a> { fn into_route(self) -> Route { - let mut r = Route::ranked(0, self.method.0, self.uri.0.as_str(), dummy_handler); + let mut r = Route::ranked(0, self.method.0, &self.uri.0.to_string(), dummy_handler); r.format = self.format.map(|f| f.0); r } diff --git a/core/lib/src/route/route.rs b/core/lib/src/route/route.rs index 994cb05897..944b56e439 100644 --- a/core/lib/src/route/route.rs +++ b/core/lib/src/route/route.rs @@ -200,6 +200,11 @@ impl Route { /// /// Panics if `path` is not a valid Rocket route URI. /// + /// A valid route URI is any valid [`Origin`](uri::Origin) URI that is + /// normalized, that is, does not contain any empty segments except for an + /// optional trailing slash. Unlike a strict `Origin`, route URIs are also + /// allowed to contain any UTF-8 characters. + /// /// # Example /// /// ```rust @@ -207,7 +212,7 @@ impl Route { /// use rocket::http::Method; /// # use rocket::route::dummy_handler as handler; /// - /// // this is a rank 1 route matching requests to `GET /` + /// // this is a route matching requests to `GET /` /// let index = Route::new(Method::Get, "/", handler); /// assert_eq!(index.rank, -9); /// assert_eq!(index.method, Method::Get); @@ -226,6 +231,11 @@ impl Route { /// /// Panics if `path` is not a valid Rocket route URI. /// + /// A valid route URI is any valid [`Origin`](uri::Origin) URI that is + /// normalized, that is, does not contain any empty segments except for an + /// optional trailing slash. Unlike a strict `Origin`, route URIs are also + /// allowed to contain any UTF-8 characters. + /// /// # Example /// /// ```rust @@ -275,12 +285,12 @@ impl Route { /// /// let index = Route::new(Method::Get, "/foo/bar", handler); /// assert_eq!(index.uri.base(), "/"); - /// assert_eq!(index.uri.unmounted_origin.path(), "/foo/bar"); + /// assert_eq!(index.uri.unmounted().path(), "/foo/bar"); /// assert_eq!(index.uri.path(), "/foo/bar"); /// /// let index = index.map_base(|base| format!("{}{}", "/boo", base)).unwrap(); /// assert_eq!(index.uri.base(), "/boo"); - /// assert_eq!(index.uri.unmounted_origin.path(), "/foo/bar"); + /// assert_eq!(index.uri.unmounted().path(), "/foo/bar"); /// assert_eq!(index.uri.path(), "/boo/foo/bar"); /// ``` pub fn map_base<'a, F>(mut self, mapper: F) -> Result> @@ -309,7 +319,7 @@ impl fmt::Display for Route { write!(f, "{}", Paint::blue(self.uri.base()).underline())?; } - write!(f, "{}", Paint::blue(&self.uri.unmounted_origin))?; + write!(f, "{}", Paint::blue(&self.uri.unmounted()))?; if self.rank > 1 { write!(f, " [{}]", Paint::default(&self.rank).bold())?; diff --git a/core/lib/src/route/uri.rs b/core/lib/src/route/uri.rs index b3b8b19ac3..9fc073b0b8 100644 --- a/core/lib/src/route/uri.rs +++ b/core/lib/src/route/uri.rs @@ -1,7 +1,6 @@ use std::fmt; -use std::borrow::Cow; -use crate::http::uri::{self, Origin}; +use crate::http::uri::{self, Origin, Path}; use crate::http::ext::IntoOwned; use crate::form::ValueField; use crate::route::Segment; @@ -53,16 +52,14 @@ use crate::route::Segment; /// /// [`Rocket::mount()`]: crate::Rocket::mount() /// [`Route::new()`]: crate::Route::new() -#[derive(Clone)] +#[derive(Debug, Clone)] pub struct RouteUri<'a> { - /// The source string for this URI. - source: Cow<'a, str>, /// The mount point. - pub base: Origin<'a>, + pub(crate) base: Origin<'a>, /// The URI _without_ the `base` mount point. - pub unmounted_origin: Origin<'a>, + pub(crate) unmounted_origin: Origin<'a>, /// The URI _with_ the base mount point. This is the canonical route URI. - pub uri: Origin<'a>, + pub(crate) uri: Origin<'a>, /// Cached metadata about this URI. pub(crate) metadata: Metadata, } @@ -98,6 +95,14 @@ type Result> = std::result::Result; impl<'a> RouteUri<'a> { /// Create a new `RouteUri`. /// + /// Panics if `base` or `uri` cannot be parsed as `Origin`s. + #[track_caller] + pub(crate) fn new(base: &str, uri: &str) -> RouteUri<'static> { + Self::try_new(base, uri).expect("Expected valid URIs") + } + + /// Creates a new `RouteUri` from a `base` mount point and a route `uri`. + /// /// This is a fallible variant of [`RouteUri::new`] which returns an `Err` /// if `base` or `uri` cannot be parsed as [`Origin`]s. /// INTERNAL! @@ -129,21 +134,15 @@ impl<'a> RouteUri<'a> { .into_normalized() .into_owned(); - let source = uri.to_string().into(); let metadata = Metadata::from(&base, &uri); - Ok(RouteUri { source, base, unmounted_origin: origin, uri, metadata }) + Ok(RouteUri { base, unmounted_origin: origin, uri, metadata }) } - /// Create a new `RouteUri`. + /// Returns the complete route URI. /// - /// Panics if `base` or `uri` cannot be parsed as `Origin`s. - #[track_caller] - pub(crate) fn new(base: &str, uri: &str) -> RouteUri<'static> { - Self::try_new(base, uri).expect("Expected valid URIs") - } - - /// The path of the base mount point of this route URI as an `&str`. + /// **Note:** `RouteURI` derefs to the `Origin` returned by this method, so + /// this method should rarely be called directly. /// /// # Example /// @@ -152,36 +151,19 @@ impl<'a> RouteUri<'a> { /// use rocket::http::Method; /// # use rocket::route::dummy_handler as handler; /// - /// let index = Route::new(Method::Get, "/foo/bar?a=1", handler); - /// assert_eq!(index.uri.base(), "/"); - /// let index = index.map_base(|base| format!("{}{}", "/boo", base)).unwrap(); - /// assert_eq!(index.uri.base(), "/boo"); - /// ``` - #[inline(always)] - pub fn base(&self) -> &str { - self.base.path().as_str() - } - - /// The path part of this route URI as an `&str`. + /// let route = Route::new(Method::Get, "/foo/bar?a=1", handler); /// - /// # Example + /// // Use `inner()` directly: + /// assert_eq!(route.uri.inner().query().unwrap(), "a=1"); /// - /// ```rust - /// use rocket::Route; - /// use rocket::http::Method; - /// # use rocket::route::dummy_handler as handler; - /// - /// let index = Route::new(Method::Get, "/foo/bar?a=1", handler); - /// assert_eq!(index.uri.path(), "/foo/bar"); - /// let index = index.map_base(|base| format!("{}{}", "/boo", base)).unwrap(); - /// assert_eq!(index.uri.path(), "/boo/foo/bar"); + /// // Use the deref implementation. This is preferred: + /// assert_eq!(route.uri.query().unwrap(), "a=1"); /// ``` - #[inline(always)] - pub fn path(&self) -> &str { - self.uri.path().as_str() + pub fn inner(&self) -> &Origin<'a> { + &self.uri } - /// The query part of this route URI, if there is one. + /// The base mount point of this route URI. /// /// # Example /// @@ -190,25 +172,18 @@ impl<'a> RouteUri<'a> { /// use rocket::http::Method; /// # use rocket::route::dummy_handler as handler; /// - /// let index = Route::new(Method::Get, "/foo/bar", handler); - /// assert!(index.uri.query().is_none()); + /// let route = Route::new(Method::Get, "/foo/bar?a=1", handler); + /// assert_eq!(route.uri.base(), "/"); /// - /// // Normalization clears the empty '?'. - /// let index = Route::new(Method::Get, "/foo/bar?", handler); - /// assert_eq!(index.uri.query().unwrap(), ""); - /// - /// let index = Route::new(Method::Get, "/foo/bar?a=1", handler); - /// assert_eq!(index.uri.query().unwrap(), "a=1"); - /// - /// let index = index.map_base(|base| format!("{}{}", "/boo", base)).unwrap(); - /// assert_eq!(index.uri.query().unwrap(), "a=1"); + /// let route = route.map_base(|base| format!("{}{}", "/boo", base)).unwrap(); + /// assert_eq!(route.uri.base(), "/boo"); /// ``` #[inline(always)] - pub fn query(&self) -> Option<&str> { - self.uri.query().map(|q| q.as_str()) + pub fn base(&self) -> Path<'_> { + self.base.path() } - /// The full URI as an `&str`. + /// The route URI _without_ the base mount point. /// /// # Example /// @@ -217,14 +192,16 @@ impl<'a> RouteUri<'a> { /// use rocket::http::Method; /// # use rocket::route::dummy_handler as handler; /// - /// let index = Route::new(Method::Get, "/foo/bar?a=1", handler); - /// assert_eq!(index.uri.as_str(), "/foo/bar?a=1"); - /// let index = index.map_base(|base| format!("{}{}", "/boo", base)).unwrap(); - /// assert_eq!(index.uri.as_str(), "/boo/foo/bar?a=1"); + /// let route = Route::new(Method::Get, "/foo/bar?a=1", handler); + /// let route = route.map_base(|base| format!("{}{}", "/boo", base)).unwrap(); + /// + /// assert_eq!(route.uri, "/boo/foo/bar?a=1"); + /// assert_eq!(route.uri.base(), "/boo"); + /// assert_eq!(route.uri.unmounted(), "/foo/bar?a=1"); /// ``` #[inline(always)] - pub fn as_str(&self) -> &str { - &self.source + pub fn unmounted(&self) -> &Origin<'a> { + &self.unmounted_origin } /// Get the default rank of a route with this URI. @@ -306,35 +283,24 @@ impl<'a> std::ops::Deref for RouteUri<'a> { type Target = Origin<'a>; fn deref(&self) -> &Self::Target { - &self.uri + self.inner() } } impl fmt::Display for RouteUri<'_> { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - self.uri.fmt(f) - } -} - -impl fmt::Debug for RouteUri<'_> { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - f.debug_struct("RouteUri") - .field("base", &self.base) - .field("unmounted_origin", &self.unmounted_origin) - .field("origin", &self.uri) - .field("metadata", &self.metadata) - .finish() + self.inner().fmt(f) } } impl<'a, 'b> PartialEq> for RouteUri<'a> { - fn eq(&self, other: &Origin<'b>) -> bool { &self.uri == other } + fn eq(&self, other: &Origin<'b>) -> bool { self.inner() == other } } impl PartialEq for RouteUri<'_> { - fn eq(&self, other: &str) -> bool { self.as_str() == other } + fn eq(&self, other: &str) -> bool { self.inner() == other } } impl PartialEq<&str> for RouteUri<'_> { - fn eq(&self, other: &&str) -> bool { self.as_str() == *other } + fn eq(&self, other: &&str) -> bool { self.inner() == *other } } diff --git a/core/lib/tests/route_guard.rs b/core/lib/tests/route_guard.rs index 2a88b24f7b..117efddff6 100644 --- a/core/lib/tests/route_guard.rs +++ b/core/lib/tests/route_guard.rs @@ -6,7 +6,7 @@ use rocket::Route; #[get("/")] fn files(route: &Route, path: PathBuf) -> String { - Path::new(route.uri.base()).join(path).normalized_str().to_string() + Path::new(&route.uri.base()).join(path).normalized_str().to_string() } mod route_guard_tests { From c13a6c6a79d807b52937221eb34737b9b8d9c0ba Mon Sep 17 00:00:00 2001 From: Sergio Benitez Date: Mon, 10 Apr 2023 12:47:09 -0700 Subject: [PATCH 128/166] Emit warning when 'String' is used as a parameter. The warning is only emitted when Rocket is compiled in debug. --- core/lib/src/request/from_param.rs | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/core/lib/src/request/from_param.rs b/core/lib/src/request/from_param.rs index ffd73e34b8..3097695cf8 100644 --- a/core/lib/src/request/from_param.rs +++ b/core/lib/src/request/from_param.rs @@ -200,8 +200,15 @@ impl<'a> FromParam<'a> for &'a str { impl<'a> FromParam<'a> for String { type Error = Empty; + #[track_caller] #[inline(always)] fn from_param(param: &'a str) -> Result { + #[cfg(debug_assertions)] { + let loc = std::panic::Location::caller(); + warn_!("Note: Using `String` as a parameter type is inefficient. Use `&str` instead."); + info_!("`String` is used a parameter guard in {}:{}.", loc.file(), loc.line()); + } + if param.is_empty() { return Err(Empty); } From 0c80f7d9e0fa5c88c41a3c21502a35f97d31e969 Mon Sep 17 00:00:00 2001 From: Sergio Benitez Date: Mon, 10 Apr 2023 13:42:20 -0700 Subject: [PATCH 129/166] Return 'Path' from 'Catcher::base()'. --- core/lib/src/catcher/catcher.rs | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/core/lib/src/catcher/catcher.rs b/core/lib/src/catcher/catcher.rs index a477fac401..8ec7b1b7e7 100644 --- a/core/lib/src/catcher/catcher.rs +++ b/core/lib/src/catcher/catcher.rs @@ -1,6 +1,7 @@ use std::fmt; use std::io::Cursor; +use crate::http::uri::Path; use crate::response::Response; use crate::request::Request; use crate::http::{Status, ContentType, uri}; @@ -198,13 +199,13 @@ impl Catcher { /// } /// /// let catcher = Catcher::new(404, handle_404); - /// assert_eq!(catcher.base().path(), "/"); + /// assert_eq!(catcher.base(), "/"); /// /// let catcher = catcher.map_base(|base| format!("/foo/bar/{}", base)).unwrap(); - /// assert_eq!(catcher.base().path(), "/foo/bar"); + /// assert_eq!(catcher.base(), "/foo/bar"); /// ``` - pub fn base(&self) -> &uri::Origin<'_> { - &self.base + pub fn base(&self) -> Path<'_> { + self.base.path() } /// Maps the `base` of this catcher using `mapper`, returning a new @@ -229,13 +230,13 @@ impl Catcher { /// } /// /// let catcher = Catcher::new(404, handle_404); - /// assert_eq!(catcher.base().path(), "/"); + /// assert_eq!(catcher.base(), "/"); /// /// let catcher = catcher.map_base(|_| format!("/bar")).unwrap(); - /// assert_eq!(catcher.base().path(), "/bar"); + /// assert_eq!(catcher.base(), "/bar"); /// /// let catcher = catcher.map_base(|base| format!("/foo{}", base)).unwrap(); - /// assert_eq!(catcher.base().path(), "/foo/bar"); + /// assert_eq!(catcher.base(), "/foo/bar"); /// /// let catcher = catcher.map_base(|base| format!("/foo ? {}", base)); /// assert!(catcher.is_err()); From b61ac6eb188f9eaec9ccc84680f9ff310ceb9487 Mon Sep 17 00:00:00 2001 From: Sergio Benitez Date: Mon, 10 Apr 2023 15:16:39 -0700 Subject: [PATCH 130/166] Expose 'Route', 'Catcher' collision and matching. This commit exposes four new methods: * `Route::collides_with(&Route)` * `Route::matches(&Request)` * `Catcher::collides_with(&Catcher)` * `Catcher::matches(Status, &Request)` Each method checks the corresponding condition: whether two routes collide, whether a route matches a request, whether two catchers collide, and whether a catcher matches an error arising from a request. This functionality is used internally by Rocket to make routing decisions. By exposing these methods, external libraries can use guaranteed consistent logic to check the same routing conditions. Resolves #1561. --- core/lib/src/catcher/catcher.rs | 35 ++++--- core/lib/src/catcher/handler.rs | 3 +- core/lib/src/route/route.rs | 49 +++------ core/lib/src/router/collider.rs | 174 ++++++++++++++++++++++++++------ core/lib/src/router/matcher.rs | 144 ++++++++++++++++++++++---- 5 files changed, 302 insertions(+), 103 deletions(-) diff --git a/core/lib/src/catcher/catcher.rs b/core/lib/src/catcher/catcher.rs index 8ec7b1b7e7..e8d0687d36 100644 --- a/core/lib/src/catcher/catcher.rs +++ b/core/lib/src/catcher/catcher.rs @@ -32,16 +32,21 @@ use yansi::Paint; /// /// # Routing /// -/// An error arising from a particular request _matches_ a catcher _iff_: +/// If a route fails by returning a failure [`Outcome`], Rocket routes the +/// erroring request to the highest precedence catcher among all the catchers +/// that [match](Catcher::matches()). See [`Catcher::matches()`] for details on +/// matching. Precedence is determined by the catcher's _base_, which is +/// provided as the first argument to [`Rocket::register()`]. Catchers with more +/// non-empty segments have a higher precedence. /// -/// * It is a default catcher _or_ has a status code matching the error code. -/// * Its base is a prefix of the normalized/decoded request URI path. +/// Rocket provides [built-in defaults](#built-in-default), but _default_ +/// catchers can also be registered. A _default_ catcher is a catcher with no +/// explicit status code: `None`. /// -/// A _default_ catcher is a catcher with no explicit status code: `None`. The -/// catcher's _base_ is provided as the first argument to -/// [`Rocket::register()`](crate::Rocket::register()). +/// [`Outcome`]: crate::request::Outcome +/// [`Rocket::register()`]: crate::Rocket::register() /// -/// # Collisions +/// ## Collisions /// /// Two catchers are said to _collide_ if there exists an error that matches /// both catchers. Colliding catchers present a routing ambiguity and are thus @@ -50,7 +55,7 @@ use yansi::Paint; /// after it becomes statically impossible to register any more catchers on an /// instance of `Rocket`. /// -/// ### Built-In Default +/// ## Built-In Default /// /// Rocket's provides a built-in default catcher that can handle all errors. It /// produces HTML or JSON, depending on the value of the `Accept` header. As @@ -119,14 +124,8 @@ pub struct Catcher { /// The catcher's calculated rank. /// - /// This is [base.segments().len() | base.chars().len()]. - pub(crate) rank: u64, -} - -fn compute_rank(base: &uri::Origin<'_>) -> u64 { - let major = u32::MAX - base.path().segments().num() as u32; - let minor = u32::MAX - base.path().as_str().chars().count() as u32; - ((major as u64) << 32) | (minor as u64) + /// This is -(number of nonempty segments in base). + pub(crate) rank: isize, } impl Catcher { @@ -178,7 +177,7 @@ impl Catcher { name: None, base: uri::Origin::ROOT, handler: Box::new(handler), - rank: compute_rank(&uri::Origin::ROOT), + rank: 0, code } } @@ -250,7 +249,7 @@ impl Catcher { let new_base = uri::Origin::parse_owned(mapper(self.base))?; self.base = new_base.into_normalized_nontrailing(); self.base.clear_query(); - self.rank = compute_rank(&self.base); + self.rank = -1 * (self.base().segments().filter(|s| !s.is_empty()).count() as isize); Ok(self) } } diff --git a/core/lib/src/catcher/handler.rs b/core/lib/src/catcher/handler.rs index 9650140142..f33ceba0e3 100644 --- a/core/lib/src/catcher/handler.rs +++ b/core/lib/src/catcher/handler.rs @@ -118,7 +118,8 @@ impl Handler for F } } -#[cfg(test)] +// Used in tests! Do not use, please. +#[doc(hidden)] pub fn dummy_handler<'r>(_: Status, _: &'r Request<'_>) -> BoxFuture<'r> { Box::pin(async move { Ok(Response::new()) }) } diff --git a/core/lib/src/route/route.rs b/core/lib/src/route/route.rs index 944b56e439..25f8436fa3 100644 --- a/core/lib/src/route/route.rs +++ b/core/lib/src/route/route.rs @@ -38,33 +38,22 @@ use crate::sentinel::Sentry; /// /// # Routing /// -/// A request _matches_ a route _iff_: -/// -/// * The route's method matches that of the incoming request. -/// * The route's format (if any) matches that of the incoming request. -/// - If route specifies a format, it only matches requests for that format. -/// - If route doesn't specify a format, it matches requests for any format. -/// - A route's `format` matches against the `Accept` header in the request -/// when the route's method [`supports_payload()`] and `Content-Type` -/// header otherwise. -/// - Non-specific `Accept` header components (`*`) match anything. -/// * All static components in the route's path match the corresponding -/// components in the same position in the incoming request. -/// * All static components in the route's query string are also in the -/// request query string, though in any position. If there is no query -/// in the route, requests with and without queries match. -/// -/// Rocket routes requests to matching routes. -/// -/// [`supports_payload()`]: Method::supports_payload() -/// -/// # Collisions -/// -/// Two routes are said to _collide_ if there exists a request that matches both -/// routes. Colliding routes present a routing ambiguity and are thus disallowed -/// by Rocket. Because routes can be constructed dynamically, collision checking -/// is done at [`ignite`](crate::Rocket::ignite()) time, after it becomes -/// statically impossible to add any more routes to an instance of `Rocket`. +/// A request is _routed_ to a route if it has the highest precedence (lowest +/// rank) among all routes that [match](Route::matches()) the request. See +/// [`Route::matches()`] for details on what it means for a request to match. +/// +/// Note that a single request _may_ be routed to multiple routes if a route +/// forwards. If a route fails, the request is instead routed to the highest +/// precedence [`Catcher`](crate::Catcher). +/// +/// ## Collisions +/// +/// Two routes are said to [collide](Route::collides_with()) if there exists a +/// request that matches both routes. Colliding routes present a routing +/// ambiguity and are thus disallowed by Rocket. Because routes can be +/// constructed dynamically, collision checking is done at +/// [`ignite`](crate::Rocket::ignite()) time, after it becomes statically +/// impossible to add any more routes to an instance of `Rocket`. /// /// Note that because query parsing is always lenient -- extra and missing query /// parameters are allowed -- queries do not directly impact whether two routes @@ -300,12 +289,6 @@ impl Route { self.uri = RouteUri::try_new(&base, &self.uri.unmounted_origin.to_string())?; Ok(self) } - - /// Returns `true` if `self` collides with `other`. - #[doc(hidden)] - pub fn collides_with(&self, other: &Route) -> bool { - crate::router::Collide::collides_with(self, other) - } } impl fmt::Display for Route { diff --git a/core/lib/src/router/collider.rs b/core/lib/src/router/collider.rs index 7968bd0811..500af5c99f 100644 --- a/core/lib/src/router/collider.rs +++ b/core/lib/src/router/collider.rs @@ -7,22 +7,86 @@ pub trait Collide { fn collides_with(&self, other: &T) -> bool; } -impl Collide for Route { - /// Determines if two routes can match against some request. That is, if two - /// routes `collide`, there exists a request that can match against both - /// routes. +impl Route { + /// Returns `true` if `self` collides with `other`. /// - /// This implementation is used at initialization to check if two user - /// routes collide before launching. Format collisions works like this: + /// A [_collision_](Route#collisions) between two routes occurs when there + /// exists a request that could [match](Route::matches()) either route. That + /// is, a routing ambiguity would ensue if both routes were made available + /// to the router. /// - /// * If route specifies a format, it only gets requests for that format. - /// * If route doesn't specify a format, it gets requests for any format. + /// Specifically, a collision occurs when two routes `a` and `b`: /// - /// Because query parsing is lenient, and dynamic query parameters can be - /// missing, the particularities of a query string do not impact whether two - /// routes collide. The query effects the route's color, however, which - /// effects its rank. - fn collides_with(&self, other: &Route) -> bool { + /// * Have the same [method](Route::method). + /// * Have the same [rank](Route#default-ranking). + /// * The routes' methods don't support a payload _or_ the routes' + /// methods support a payload and the formats overlap. Formats overlap + /// when: + /// - The top-level type of either is `*` or the top-level types are + /// equivalent. + /// - The sub-level type of either is `*` or the sub-level types are + /// equivalent. + /// * Have overlapping route URIs. This means that either: + /// - The URIs have the same number of segments `n`, and for `i` in + /// `0..n`, either `a.uri[i]` is dynamic _or_ `b.uri[i]` is dynamic + /// _or_ they're both static with the same value. + /// - One URI has fewer segments _and_ ends with a trailing dynamic + /// parameter _and_ the preceeding segments in both routes match the + /// conditions above. + /// + /// Collisions are symmetric: for any routes `a` and `b`, + /// `a.collides_with(b) => b.collides_with(a)`. + /// + /// # Example + /// + /// ```rust + /// use rocket::Route; + /// use rocket::http::{Method, MediaType}; + /// # use rocket::route::dummy_handler as handler; + /// + /// // Two routes with the same method, rank, URI, and formats collide. + /// let a = Route::new(Method::Get, "/", handler); + /// let b = Route::new(Method::Get, "/", handler); + /// assert!(a.collides_with(&b)); + /// + /// // Two routes with the same method, rank, URI, and overlapping formats. + /// let mut a = Route::new(Method::Post, "/", handler); + /// a.format = Some(MediaType::new("*", "custom")); + /// let mut b = Route::new(Method::Post, "/", handler); + /// b.format = Some(MediaType::new("text", "*")); + /// assert!(a.collides_with(&b)); + /// + /// // Two routes with different ranks don't collide. + /// let a = Route::ranked(1, Method::Get, "/", handler); + /// let b = Route::ranked(2, Method::Get, "/", handler); + /// assert!(!a.collides_with(&b)); + /// + /// // Two routes with different methods don't collide. + /// let a = Route::new(Method::Put, "/", handler); + /// let b = Route::new(Method::Post, "/", handler); + /// assert!(!a.collides_with(&b)); + /// + /// // Two routes with non-overlapping URIs do not collide. + /// let a = Route::new(Method::Get, "/foo", handler); + /// let b = Route::new(Method::Get, "/bar/", handler); + /// assert!(!a.collides_with(&b)); + /// + /// // Two payload-supporting routes with non-overlapping formats. + /// let mut a = Route::new(Method::Post, "/", handler); + /// a.format = Some(MediaType::HTML); + /// let mut b = Route::new(Method::Post, "/", handler); + /// b.format = Some(MediaType::JSON); + /// assert!(!a.collides_with(&b)); + /// + /// // Two non payload-supporting routes with non-overlapping formats + /// // collide. A request with `Accept: */*` matches both. + /// let mut a = Route::new(Method::Get, "/", handler); + /// a.format = Some(MediaType::HTML); + /// let mut b = Route::new(Method::Get, "/", handler); + /// b.format = Some(MediaType::JSON); + /// assert!(a.collides_with(&b)); + /// ``` + pub fn collides_with(&self, other: &Route) -> bool { self.method == other.method && self.rank == other.rank && self.uri.collides_with(&other.uri) @@ -30,16 +94,68 @@ impl Collide for Route { } } -impl Collide for Catcher { - /// Determines if two catchers are in conflict: there exists a request for - /// which there exist no rule to determine _which_ of the two catchers to - /// use. This means that the catchers: +impl Catcher { + /// Returns `true` if `self` collides with `other`. + /// + /// A [_collision_](Catcher#collisions) between two catchers occurs when + /// there exists a request and ensuing error that could + /// [match](Catcher::matches()) both catchers. That is, a routing ambiguity + /// would ensue if both catchers were made available to the router. + /// + /// Specifically, a collision occurs when two catchers: + /// + /// * Have the same [base](Catcher::base()). + /// * Have the same status [code](Catcher::code) or are both `default`. /// - /// * Have the same base. - /// * Have the same status code or are both defaults. + /// Collisions are symmetric: for any catchers `a` and `b`, + /// `a.collides_with(b) => b.collides_with(a)`. + /// + /// # Example + /// + /// ```rust + /// use rocket::Catcher; + /// # use rocket::catcher::dummy_handler as handler; + /// + /// // Two catchers with the same status code and base collide. + /// let a = Catcher::new(404, handler).map_base(|_| format!("/foo")).unwrap(); + /// let b = Catcher::new(404, handler).map_base(|_| format!("/foo")).unwrap(); + /// assert!(a.collides_with(&b)); + /// + /// // Two catchers with a different base _do not_ collide. + /// let a = Catcher::new(404, handler); + /// let b = a.clone().map_base(|_| format!("/bar")).unwrap(); + /// assert_eq!(a.base(), "/"); + /// assert_eq!(b.base(), "/bar"); + /// assert!(!a.collides_with(&b)); + /// + /// // Two catchers with a different codes _do not_ collide. + /// let a = Catcher::new(404, handler); + /// let b = Catcher::new(500, handler); + /// assert_eq!(a.base(), "/"); + /// assert_eq!(b.base(), "/"); + /// assert!(!a.collides_with(&b)); + /// + /// // A catcher _with_ a status code and one _without_ do not collide. + /// let a = Catcher::new(404, handler); + /// let b = Catcher::new(None, handler); + /// assert!(!a.collides_with(&b)); + /// ``` + pub fn collides_with(&self, other: &Self) -> bool { + self.code == other.code && self.base().segments().eq(other.base().segments()) + } +} + +impl Collide for Route { + #[inline(always)] + fn collides_with(&self, other: &Route) -> bool { + Route::collides_with(&self, other) + } +} + +impl Collide for Catcher { + #[inline(always)] fn collides_with(&self, other: &Self) -> bool { - self.code == other.code - && self.base.path().segments().eq(other.base.path().segments()) + Catcher::collides_with(&self, other) } } @@ -75,17 +191,17 @@ impl Collide for MediaType { } fn formats_collide(route: &Route, other: &Route) -> bool { - // When matching against the `Accept` header, the client can always provide - // a media type that will cause a collision through non-specificity, i.e, - // `*/*` matches everything. - if !route.method.supports_payload() { + // If the routes' method doesn't support a payload, then format matching + // considers the `Accept` header. The client can always provide a media type + // that will cause a collision through non-specificity, i.e, `*/*`. + if !route.method.supports_payload() && !other.method.supports_payload() { return true; } - // When matching against the `Content-Type` header, we'll only consider - // requests as having a `Content-Type` if they're fully specified. If a - // route doesn't have a `format`, it accepts all `Content-Type`s. If a - // request doesn't have a format, it only matches routes without a format. + // Payload supporting methods match against `Content-Type`. We only + // consider requests as having a `Content-Type` if they're fully + // specified. A route without a `format` accepts all `Content-Type`s. A + // request without a format only matches routes without a format. match (route.format.as_ref(), other.format.as_ref()) { (Some(a), Some(b)) => a.collides_with(b), _ => true diff --git a/core/lib/src/router/matcher.rs b/core/lib/src/router/matcher.rs index 2f2b89a3c8..83d30c876b 100644 --- a/core/lib/src/router/matcher.rs +++ b/core/lib/src/router/matcher.rs @@ -4,37 +4,137 @@ use crate::http::Status; use crate::route::Color; impl Route { - /// Determines if this route matches against the given request. + /// Returns `true` if `self` matches `request`. /// - /// This means that: + /// A [_match_](Route#routing) occurs when: /// /// * The route's method matches that of the incoming request. - /// * The route's format (if any) matches that of the incoming request. - /// - If route specifies format, it only gets requests for that format. - /// - If route doesn't specify format, it gets requests for any format. - /// * All static components in the route's path match the corresponding - /// components in the same position in the incoming request. - /// * All static components in the route's query string are also in the - /// request query string, though in any position. If there is no query - /// in the route, requests with/without queries match. - #[doc(hidden)] - pub fn matches(&self, req: &Request<'_>) -> bool { - self.method == req.method() - && paths_match(self, req) - && queries_match(self, req) - && formats_match(self, req) + /// * Either the route has no format _or_: + /// - If the route's method supports a payload, the request's + /// `Content-Type` is [fully specified] and [collides with] the + /// route's format. + /// - If the route's method does not support a payload, the request + /// either has no `Accept` header or it [collides with] with the + /// route's format. + /// * All static segments in the route's URI match the corresponding + /// components in the same position in the incoming request URI. + /// * The route URI has no query part _or_ all static segments in the + /// route's query string are in the request query string, though in any + /// position. + /// + /// [fully specified]: crate::http::MediaType::specificity() + /// [collides with]: Route::collides_with() + /// + /// For a request to be routed to a particular route, that route must both + /// `match` _and_ have the highest precedence among all matching routes for + /// that request. In other words, a `match` is a necessary but insufficient + /// condition to determine if a route will handle a particular request. + /// + /// The precedence of a route is determined by its rank. Routes with lower + /// ranks have higher precedence. [By default](Route#default-ranking), more + /// specific routes are assigned a lower ranking. + /// + /// # Example + /// + /// ```rust + /// use rocket::Route; + /// use rocket::http::Method; + /// # use rocket::local::blocking::Client; + /// # use rocket::route::dummy_handler as handler; + /// + /// // This route handles GET requests to `/`. + /// let a = Route::new(Method::Get, "/", handler); + /// + /// // This route handles GET requests to `/здрасти`. + /// let b = Route::new(Method::Get, "/здрасти", handler); + /// + /// # let client = Client::debug(rocket::build()).unwrap(); + /// // Let's say `request` is `GET /hello`. The request matches only `a`: + /// let request = client.get("/hello"); + /// # let request = request.inner(); + /// assert!(a.matches(&request)); + /// assert!(!b.matches(&request)); + /// + /// // Now `request` is `GET /здрасти`. It matches both `a` and `b`: + /// let request = client.get("/здрасти"); + /// # let request = request.inner(); + /// assert!(a.matches(&request)); + /// assert!(b.matches(&request)); + /// + /// // But `b` is more specific, so it has lower rank (higher precedence) + /// // by default, so Rocket would route the request to `b`, not `a`. + /// assert!(b.rank < a.rank); + /// ``` + pub fn matches(&self, request: &Request<'_>) -> bool { + self.method == request.method() + && paths_match(self, request) + && queries_match(self, request) + && formats_match(self, request) } } impl Catcher { - /// Determines if this catcher is responsible for handling the error with - /// `status` that occurred during request `req`. A catcher matches if: + /// Returns `true` if `self` matches errors with `status` that occured + /// during `request`. + /// + /// A [_match_](Catcher#routing) between a `Catcher` and a (`Status`, + /// `&Request`) pair occurs when: + /// + /// * The catcher has the same [code](Catcher::code) as + /// [`status`](Status::code) _or_ is `default`. + /// * The catcher's [base](Catcher::base()) is a prefix of the `request`'s + /// [normalized](crate::http::uri::Origin#normalization) URI. + /// + /// For an error arising from a request to be routed to a particular + /// catcher, that catcher must both `match` _and_ have higher precedence + /// than any other catcher that matches. In other words, a `match` is a + /// necessary but insufficient condition to determine if a catcher will + /// handle a particular error. + /// + /// The precedence of a catcher is determined by: + /// + /// 1. The number of _complete_ segments in the catcher's `base`. + /// 2. Whether the catcher is `default` or not. + /// + /// Non-default routes, and routes with more complete segments in their + /// base, have higher precedence. + /// + /// # Example + /// + /// ```rust + /// use rocket::Catcher; + /// use rocket::http::Status; + /// # use rocket::local::blocking::Client; + /// # use rocket::catcher::dummy_handler as handler; + /// + /// // This catcher handles 404 errors with a base of `/`. + /// let a = Catcher::new(404, handler); + /// + /// // This catcher handles 404 errors with a base of `/bar`. + /// let b = a.clone().map_base(|_| format!("/bar")).unwrap(); + /// + /// # let client = Client::debug(rocket::build()).unwrap(); + /// // Let's say `request` is `GET /` that 404s. The error matches only `a`: + /// let request = client.get("/"); + /// # let request = request.inner(); + /// assert!(a.matches(Status::NotFound, &request)); + /// assert!(!b.matches(Status::NotFound, &request)); + /// + /// // Now `request` is a 404 `GET /bar`. The error matches `a` and `b`: + /// let request = client.get("/bar"); + /// # let request = request.inner(); + /// assert!(a.matches(Status::NotFound, &request)); + /// assert!(b.matches(Status::NotFound, &request)); /// - /// * It is a default catcher _or_ has a code of `status`. - /// * Its base is a prefix of the normalized/decoded `req.path()`. - pub(crate) fn matches(&self, status: Status, req: &Request<'_>) -> bool { + /// // Note that because `b`'s base' has more complete segments that `a's, + /// // Rocket would route the error to `b`, not `a`, even though both match. + /// let a_count = a.base().segments().filter(|s| !s.is_empty()).count(); + /// let b_count = b.base().segments().filter(|s| !s.is_empty()).count(); + /// assert!(b_count > a_count); + /// ``` + pub fn matches(&self, status: Status, request: &Request<'_>) -> bool { self.code.map_or(true, |code| code == status.code) - && self.base.path().segments().prefix_of(req.uri().path().segments()) + && self.base().segments().prefix_of(request.uri().path().segments()) } } From 055ad107df9753765c2cfe02bccb99942750d57d Mon Sep 17 00:00:00 2001 From: Benedikt Weber Date: Tue, 11 Apr 2023 11:54:05 -0700 Subject: [PATCH 131/166] Allow status customization in 'Forward' outcomes. Prior to this commit, all forward outcomes resulted in a 404. This commit changes request and data guards so that they are able to provide a `Status` on `Forward` outcomes. The router uses this status, if the final outcome is to forward, to identify the catcher to invoke. The net effect is that guards can now customize the status code of a forward and thus the error catcher invoked if the final outcome of a request is to forward. Resolves #1560. --- contrib/ws/src/websocket.rs | 6 +- core/codegen/src/attribute/route/mod.rs | 18 ++--- core/lib/src/cookies.rs | 4 +- core/lib/src/data/data.rs | 3 +- core/lib/src/data/from_data.rs | 12 +-- core/lib/src/form/parser.rs | 4 +- core/lib/src/mtls.rs | 4 +- core/lib/src/request/from_request.rs | 58 ++++++++------ core/lib/src/route/handler.rs | 8 +- core/lib/src/server.rs | 14 ++-- core/lib/src/state.rs | 3 +- core/lib/tests/forward-includes-error-1560.rs | 79 +++++++++++++++++++ .../local-request-content-type-issue-505.rs | 5 +- examples/cookies/src/session.rs | 4 +- examples/manual-routing/src/main.rs | 2 +- site/guide/6-state.md | 3 +- 16 files changed, 159 insertions(+), 68 deletions(-) create mode 100644 core/lib/tests/forward-includes-error-1560.rs diff --git a/contrib/ws/src/websocket.rs b/contrib/ws/src/websocket.rs index 7e2f2c4933..6ad295ac85 100644 --- a/contrib/ws/src/websocket.rs +++ b/contrib/ws/src/websocket.rs @@ -4,8 +4,8 @@ use std::pin::Pin; use rocket::data::{IoHandler, IoStream}; use rocket::futures::{self, StreamExt, SinkExt, future::BoxFuture, stream::SplitStream}; use rocket::response::{self, Responder, Response}; -use rocket::request::{FromRequest, Outcome}; -use rocket::request::Request; +use rocket::request::{FromRequest, Request, Outcome}; +use rocket::http::Status; use crate::{Config, Message}; use crate::stream::DuplexStream; @@ -203,7 +203,7 @@ impl<'r> FromRequest<'r> for WebSocket { let key = headers.get_one("Sec-WebSocket-Key").map(|k| derive_accept_key(k.as_bytes())); match key { Some(key) if is_upgrade && is_ws && is_13 => Outcome::Success(WebSocket::new(key)), - Some(_) | None => Outcome::Forward(()) + Some(_) | None => Outcome::Forward(Status::NotFound) } } } diff --git a/core/codegen/src/attribute/route/mod.rs b/core/codegen/src/attribute/route/mod.rs index 1b66e9ce07..5de3130643 100644 --- a/core/codegen/src/attribute/route/mod.rs +++ b/core/codegen/src/attribute/route/mod.rs @@ -38,7 +38,7 @@ fn query_decls(route: &Route) -> Option { } define_spanned_export!(Span::call_site() => - __req, __data, _log, _form, Outcome, _Ok, _Err, _Some, _None + __req, __data, _log, _form, Outcome, _Ok, _Err, _Some, _None, Status ); // Record all of the static parameters for later filtering. @@ -104,7 +104,7 @@ fn query_decls(route: &Route) -> Option { if !__e.is_empty() { #_log::warn_!("Query string failed to match route declaration."); for _err in __e { #_log::warn_!("{}", _err); } - return #Outcome::Forward(#__data); + return #Outcome::Forward((#__data, #Status::NotFound)); } (#(#ident.unwrap()),*) @@ -121,9 +121,9 @@ fn request_guard_decl(guard: &Guard) -> TokenStream { quote_spanned! { ty.span() => let #ident: #ty = match <#ty as #FromRequest>::from_request(#__req).await { #Outcome::Success(__v) => __v, - #Outcome::Forward(_) => { + #Outcome::Forward(__e) => { #_log::warn_!("Request guard `{}` is forwarding.", stringify!(#ty)); - return #Outcome::Forward(#__data); + return #Outcome::Forward((#__data, __e)); }, #Outcome::Failure((__c, __e)) => { #_log::warn_!("Request guard `{}` failed: {:?}.", stringify!(#ty), __e); @@ -137,7 +137,7 @@ fn param_guard_decl(guard: &Guard) -> TokenStream { let (i, name, ty) = (guard.index, &guard.name, &guard.ty); define_spanned_export!(ty.span() => __req, __data, _log, _None, _Some, _Ok, _Err, - Outcome, FromSegments, FromParam + Outcome, FromSegments, FromParam, Status ); // Returned when a dynamic parameter fails to parse. @@ -145,7 +145,7 @@ fn param_guard_decl(guard: &Guard) -> TokenStream { #_log::warn_!("Parameter guard `{}: {}` is forwarding: {:?}.", #name, stringify!(#ty), __error); - #Outcome::Forward(#__data) + #Outcome::Forward((#__data, #Status::NotFound)) }); // All dynamic parameters should be found if this function is being called; @@ -161,7 +161,7 @@ fn param_guard_decl(guard: &Guard) -> TokenStream { #_log::error_!("Internal invariant broken: dyn param {} not found.", #i); #_log::error_!("Please report this to the Rocket issue tracker."); #_log::error_!("https://github.com/SergioBenitez/Rocket/issues"); - return #Outcome::Forward(#__data); + return #Outcome::Forward((#__data, #Status::InternalServerError)); } } }, @@ -184,9 +184,9 @@ fn data_guard_decl(guard: &Guard) -> TokenStream { quote_spanned! { ty.span() => let #ident: #ty = match <#ty as #FromData>::from_data(#__req, #__data).await { #Outcome::Success(__d) => __d, - #Outcome::Forward(__d) => { + #Outcome::Forward((__d, __e)) => { #_log::warn_!("Data guard `{}` is forwarding.", stringify!(#ty)); - return #Outcome::Forward(__d); + return #Outcome::Forward((__d, __e)); } #Outcome::Failure((__c, __e)) => { #_log::warn_!("Data guard `{}` failed: {:?}.", stringify!(#ty), __e); diff --git a/core/lib/src/cookies.rs b/core/lib/src/cookies.rs index 032d4eae08..d52a31f575 100644 --- a/core/lib/src/cookies.rs +++ b/core/lib/src/cookies.rs @@ -101,8 +101,8 @@ pub use self::cookie::{Cookie, SameSite, Iter}; /// # #[macro_use] extern crate rocket; /// # #[cfg(feature = "secrets")] { /// use rocket::http::Status; -/// use rocket::outcome::IntoOutcome; /// use rocket::request::{self, Request, FromRequest}; +/// use rocket::outcome::IntoOutcome; /// /// // In practice, we'd probably fetch the user from the database. /// struct User(usize); @@ -116,7 +116,7 @@ pub use self::cookie::{Cookie, SameSite, Iter}; /// .get_private("user_id") /// .and_then(|c| c.value().parse().ok()) /// .map(|id| User(id)) -/// .or_forward(()) +/// .or_forward(Status::Unauthorized) /// } /// } /// # } diff --git a/core/lib/src/data/data.rs b/core/lib/src/data/data.rs index cb3d5c5db7..3f393c5598 100644 --- a/core/lib/src/data/data.rs +++ b/core/lib/src/data/data.rs @@ -102,6 +102,7 @@ impl<'r> Data<'r> { /// ```rust /// use rocket::request::{self, Request, FromRequest}; /// use rocket::data::{Data, FromData, Outcome}; + /// use rocket::http::Status; /// # struct MyType; /// # type MyError = String; /// @@ -111,7 +112,7 @@ impl<'r> Data<'r> { /// /// async fn from_data(r: &'r Request<'_>, mut data: Data<'r>) -> Outcome<'r, Self> { /// if data.peek(2).await != b"hi" { - /// return Outcome::Forward(data) + /// return Outcome::Forward((data, Status::NotFound)) /// } /// /// /* .. */ diff --git a/core/lib/src/data/from_data.rs b/core/lib/src/data/from_data.rs index 8f5fe90f1a..ac7246093f 100644 --- a/core/lib/src/data/from_data.rs +++ b/core/lib/src/data/from_data.rs @@ -7,11 +7,11 @@ use crate::outcome::{self, IntoOutcome, try_outcome, Outcome::*}; /// /// [`FromData`]: crate::data::FromData pub type Outcome<'r, T, E = >::Error> - = outcome::Outcome>; + = outcome::Outcome, Status)>; -impl<'r, S, E> IntoOutcome> for Result { +impl<'r, S, E> IntoOutcome, Status)> for Result { type Failure = Status; - type Forward = Data<'r>; + type Forward = (Data<'r>, Status); #[inline] fn into_outcome(self, status: Status) -> Outcome<'r, S, E> { @@ -22,10 +22,10 @@ impl<'r, S, E> IntoOutcome> for Result { } #[inline] - fn or_forward(self, data: Data<'r>) -> Outcome<'r, S, E> { + fn or_forward(self, (data, error): (Data<'r>, Status)) -> Outcome<'r, S, E> { match self { Ok(val) => Success(val), - Err(_) => Forward(data) + Err(_) => Forward((data, error)) } } } @@ -130,7 +130,7 @@ impl<'r, S, E> IntoOutcome> for Result { /// // 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 Forward(data); +/// return Forward((data, Status::NotFound)); /// } /// /// // Use a configured limit with name 'person' or fallback to default. diff --git a/core/lib/src/form/parser.rs b/core/lib/src/form/parser.rs index 8c670dad48..89c42eb888 100644 --- a/core/lib/src/form/parser.rs +++ b/core/lib/src/form/parser.rs @@ -4,7 +4,7 @@ use either::Either; use crate::request::{Request, local_cache_once}; use crate::data::{Data, Limits, Outcome}; use crate::form::{SharedStack, prelude::*}; -use crate::http::RawStr; +use crate::http::{RawStr, Status}; type Result<'r, T> = std::result::Result>; @@ -35,7 +35,7 @@ impl<'r, 'i> Parser<'r, 'i> { 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), + _ => return Outcome::Forward((data, Status::NotFound)), }; match parser { diff --git a/core/lib/src/mtls.rs b/core/lib/src/mtls.rs index aabdab4a48..07367bcf30 100644 --- a/core/lib/src/mtls.rs +++ b/core/lib/src/mtls.rs @@ -18,8 +18,8 @@ impl<'r> FromRequest<'r> for Certificate<'r> { type Error = Error; async fn from_request(req: &'r Request<'_>) -> Outcome { - let certs = try_outcome!(req.connection.client_certificates.as_ref().or_forward(())); - let data = try_outcome!(certs.chain_data().or_forward(())); + let certs = req.connection.client_certificates.as_ref().or_forward(Status::Unauthorized); + let data = try_outcome!(try_outcome!(certs).chain_data().or_forward(Status::Unauthorized)); Certificate::parse(data).into_outcome(Status::Unauthorized) } } diff --git a/core/lib/src/request/from_request.rs b/core/lib/src/request/from_request.rs index c164303ccc..e50a47429f 100644 --- a/core/lib/src/request/from_request.rs +++ b/core/lib/src/request/from_request.rs @@ -9,11 +9,11 @@ use crate::http::{Status, ContentType, Accept, Method, CookieJar}; use crate::http::uri::{Host, Origin}; /// Type alias for the `Outcome` of a `FromRequest` conversion. -pub type Outcome = outcome::Outcome; +pub type Outcome = outcome::Outcome; -impl IntoOutcome for Result { +impl IntoOutcome for Result { type Failure = Status; - type Forward = (); + type Forward = Status; #[inline] fn into_outcome(self, status: Status) -> Outcome { @@ -24,10 +24,10 @@ impl IntoOutcome for Result { } #[inline] - fn or_forward(self, _: ()) -> Outcome { + fn or_forward(self, status: Status) -> Outcome { match self { Ok(val) => Success(val), - Err(_) => Forward(()) + Err(_) => Forward(status) } } } @@ -102,16 +102,18 @@ impl IntoOutcome for Result { /// * **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. +/// 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** +/// * **Forward**(Status) /// /// If the `Outcome` is [`Forward`], the request will be forwarded to the next -/// matching route. Note that users can request an `Option` to catch -/// `Forward`s. +/// matching route until either one succeds or there are no further matching +/// routes to attempt. In the latter case, the request will be sent to the +/// [`Catcher`](crate::Catcher) for the designated `Status`. Note that users +/// can request an `Option` to catch `Forward`s. /// /// # Provided Implementations /// @@ -137,10 +139,12 @@ impl IntoOutcome for Result { /// /// * **&Route** /// -/// Extracts the [`Route`] from the request if one is available. If a route -/// is not available, the request is forwarded. +/// Extracts the [`Route`] from the request if one is available. When used +/// as a request guard in a route handler, this will always succeed. Outside +/// of a route handler, a route may not be available, and the request is +/// forwarded with a 500 status. /// -/// For information on when an `&Route` is available, see +/// For more information on when an `&Route` is available, see /// [`Request::route()`]. /// /// * **&CookieJar** @@ -256,6 +260,7 @@ impl IntoOutcome for Result { /// # #[cfg(feature = "secrets")] mod wrapper { /// # use rocket::outcome::{IntoOutcome, try_outcome}; /// # use rocket::request::{self, Outcome, FromRequest, Request}; +/// # use rocket::http::Status; /// # struct User { id: String, is_admin: bool } /// # struct Database; /// # impl Database { @@ -283,7 +288,7 @@ impl IntoOutcome for Result { /// .get_private("user_id") /// .and_then(|cookie| cookie.value().parse().ok()) /// .and_then(|id| db.get_user(id).ok()) -/// .or_forward(()) +/// .or_forward(Status::Unauthorized) /// } /// } /// @@ -297,7 +302,7 @@ impl IntoOutcome for Result { /// if user.is_admin { /// Outcome::Success(Admin { user }) /// } else { -/// Outcome::Forward(()) +/// Outcome::Forward(Status::Unauthorized) /// } /// } /// } @@ -320,6 +325,7 @@ impl IntoOutcome for Result { /// # #[cfg(feature = "secrets")] mod wrapper { /// # use rocket::outcome::{IntoOutcome, try_outcome}; /// # use rocket::request::{self, Outcome, FromRequest, Request}; +/// # use rocket::http::Status; /// # struct User { id: String, is_admin: bool } /// # struct Database; /// # impl Database { @@ -352,7 +358,7 @@ impl IntoOutcome for Result { /// .and_then(|id| db.get_user(id).ok()) /// }).await; /// -/// user_result.as_ref().or_forward(()) +/// user_result.as_ref().or_forward(Status::Unauthorized) /// } /// } /// @@ -365,7 +371,7 @@ impl IntoOutcome for Result { /// if user.is_admin { /// Outcome::Success(Admin { user }) /// } else { -/// Outcome::Forward(()) +/// Outcome::Forward(Status::Unauthorized) /// } /// } /// } @@ -415,7 +421,7 @@ impl<'r> FromRequest<'r> for &'r Host<'r> { async fn from_request(request: &'r Request<'_>) -> Outcome { match request.host() { Some(host) => Success(host), - None => Forward(()) + None => Forward(Status::NotFound) } } } @@ -427,7 +433,7 @@ impl<'r> FromRequest<'r> for &'r Route { async fn from_request(request: &'r Request<'_>) -> Outcome { match request.route() { Some(route) => Success(route), - None => Forward(()) + None => Forward(Status::InternalServerError) } } } @@ -448,7 +454,7 @@ impl<'r> FromRequest<'r> for &'r Accept { async fn from_request(request: &'r Request<'_>) -> Outcome { match request.accept() { Some(accept) => Success(accept), - None => Forward(()) + None => Forward(Status::NotFound) } } } @@ -460,7 +466,7 @@ impl<'r> FromRequest<'r> for &'r ContentType { async fn from_request(request: &'r Request<'_>) -> Outcome { match request.content_type() { Some(content_type) => Success(content_type), - None => Forward(()) + None => Forward(Status::NotFound) } } } @@ -472,7 +478,7 @@ impl<'r> FromRequest<'r> for IpAddr { async fn from_request(request: &'r Request<'_>) -> Outcome { match request.client_ip() { Some(addr) => Success(addr), - None => Forward(()) + None => Forward(Status::NotFound) } } } @@ -484,7 +490,7 @@ impl<'r> FromRequest<'r> for SocketAddr { async fn from_request(request: &'r Request<'_>) -> Outcome { match request.remote() { Some(addr) => Success(addr), - None => Forward(()) + None => Forward(Status::NotFound) } } } @@ -497,7 +503,7 @@ impl<'r, T: FromRequest<'r>> FromRequest<'r> for Result { match T::from_request(request).await { Success(val) => Success(Ok(val)), Failure((_, e)) => Success(Err(e)), - Forward(_) => Forward(()), + Forward(_) => Forward(Status::NotFound), } } } diff --git a/core/lib/src/route/handler.rs b/core/lib/src/route/handler.rs index 5e4e62ff9e..739639d065 100644 --- a/core/lib/src/route/handler.rs +++ b/core/lib/src/route/handler.rs @@ -4,7 +4,7 @@ use crate::http::Status; /// Type alias for the return type of a [`Route`](crate::Route)'s /// [`Handler::handle()`]. -pub type Outcome<'r> = crate::outcome::Outcome, Status, Data<'r>>; +pub type Outcome<'r> = crate::outcome::Outcome, Status, (Data<'r>, Status)>; /// Type alias for the return type of a _raw_ [`Route`](crate::Route)'s /// [`Handler`]. @@ -239,7 +239,7 @@ impl<'r, 'o: 'r> Outcome<'o> { { match responder.respond_to(req) { Ok(response) => Outcome::Success(response), - Err(_) => Outcome::Forward(data) + Err(_) => Outcome::Forward((data, Status::NotFound)) } } @@ -264,7 +264,7 @@ impl<'r, 'o: 'r> Outcome<'o> { } /// Return an `Outcome` of `Forward` with the data `data`. This is - /// equivalent to `Outcome::Forward(data)`. + /// equivalent to `Outcome::Forward((data, Status::NotFound))`. /// /// This method exists to be used during manual routing. /// @@ -279,7 +279,7 @@ impl<'r, 'o: 'r> Outcome<'o> { /// ``` #[inline(always)] pub fn forward(data: Data<'r>) -> Outcome<'r> { - Outcome::Forward(data) + Outcome::Forward((data, Status::NotFound)) } } diff --git a/core/lib/src/server.rs b/core/lib/src/server.rs index faae6ddcd1..b00111357b 100644 --- a/core/lib/src/server.rs +++ b/core/lib/src/server.rs @@ -283,7 +283,7 @@ impl Rocket { ) -> Response<'r> { let mut response = match self.route(request, data).await { Outcome::Success(response) => response, - Outcome::Forward(data) if request.method() == Method::Head => { + Outcome::Forward((data, _)) if request.method() == Method::Head => { info_!("Autohandling {} request.", Paint::default("HEAD").bold()); // Dispatch the request again with Method `GET`. @@ -291,10 +291,10 @@ impl Rocket { match self.route(request, data).await { Outcome::Success(response) => response, Outcome::Failure(status) => self.handle_error(status, request).await, - Outcome::Forward(_) => self.handle_error(Status::NotFound, request).await, + Outcome::Forward((_, status)) => self.handle_error(status, request).await, } } - Outcome::Forward(_) => self.handle_error(Status::NotFound, request).await, + Outcome::Forward((_, status)) => self.handle_error(status, request).await, Outcome::Failure(status) => self.handle_error(status, request).await, }; @@ -319,7 +319,9 @@ impl Rocket { request: &'r Request<'s>, mut data: Data<'r>, ) -> route::Outcome<'r> { - // Go through the list of matching routes until we fail or succeed. + // Go through all matching routes until we fail or succeed or run out of + // routes to try, in which case we forward with the last status. + let mut status = Status::NotFound; for route in self.router.route(request) { // Retrieve and set the requests parameters. info_!("Matched: {}", route); @@ -335,12 +337,12 @@ impl Rocket { info_!("{} {}", Paint::default("Outcome:").bold(), outcome); match outcome { o@Outcome::Success(_) | o@Outcome::Failure(_) => return o, - Outcome::Forward(unused_data) => data = unused_data, + Outcome::Forward(forwarded) => (data, status) = forwarded, } } error_!("No matching routes for {}.", request); - Outcome::Forward(data) + Outcome::Forward((data, status)) } /// Invokes the handler with `req` for catcher with status `status`. diff --git a/core/lib/src/state.rs b/core/lib/src/state.rs index 17f4b238c2..b323f77d4b 100644 --- a/core/lib/src/state.rs +++ b/core/lib/src/state.rs @@ -63,6 +63,7 @@ use crate::http::Status; /// use rocket::State; /// use rocket::request::{self, Request, FromRequest}; /// use rocket::outcome::IntoOutcome; +/// use rocket::http::Status; /// /// # struct MyConfig { user_val: String }; /// struct Item<'r>(&'r str); @@ -79,7 +80,7 @@ use crate::http::Status; /// // Or alternatively, using `Rocket::state()`: /// let outcome = request.rocket().state::() /// .map(|my_config| Item(&my_config.user_val)) -/// .or_forward(()); +/// .or_forward(Status::NotFound); /// /// outcome /// } diff --git a/core/lib/tests/forward-includes-error-1560.rs b/core/lib/tests/forward-includes-error-1560.rs new file mode 100644 index 0000000000..73cb145b4c --- /dev/null +++ b/core/lib/tests/forward-includes-error-1560.rs @@ -0,0 +1,79 @@ +#[macro_use] extern crate rocket; + +use rocket::http::Status; +use rocket::request::{self, Request, FromRequest}; + +pub struct Authenticated; + +#[rocket::async_trait] +impl<'r> FromRequest<'r> for Authenticated { + type Error = std::convert::Infallible; + + async fn from_request(request: &'r Request<'_>) -> request::Outcome { + if request.headers().contains("Authenticated") { + request::Outcome::Success(Authenticated) + } else { + request::Outcome::Forward(Status::Unauthorized) + } + } +} + +#[get("/one")] +pub async fn get_protected_one(_user: Authenticated) -> &'static str { + "Protected" +} + +#[get("/one", rank = 2)] +pub async fn get_public_one() -> &'static str { + "Public" +} + +#[get("/two")] +pub async fn get_protected_two(_user: Authenticated) -> &'static str { + "Protected" +} + +mod tests { + use super::*; + use rocket::routes; + use rocket::local::blocking::Client; + use rocket::http::{Header, Status}; + + #[test] + fn one_protected_returned_for_authenticated() { + let rocket = rocket::build().mount("/", + routes![get_protected_one, get_public_one, get_protected_two]); + + let client = Client::debug(rocket).unwrap(); + let req = client.get("/one").header(Header::new("Authenticated", "true")); + let response = req.dispatch(); + + assert_eq!(response.status(), Status::Ok); + assert_eq!(response.into_string(), Some("Protected".into())); + } + + #[test] + fn one_public_returned_for_unauthenticated() { + let rocket = rocket::build().mount("/", + routes![get_protected_one, get_public_one, get_protected_two]); + + let client = Client::debug(rocket).unwrap(); + let req = client.get("/one"); + let response = req.dispatch(); + + assert_eq!(response.status(), Status::Ok); + assert_eq!(response.into_string(), Some("Public".into())); + } + + #[test] + fn two_unauthorized_returned_for_unauthenticated() { + let rocket = rocket::build().mount("/", + routes![get_protected_one, get_public_one, get_protected_two]); + + let client = Client::debug(rocket).unwrap(); + let req = client.get("/two"); + let response = req.dispatch(); + + assert_eq!(response.status(), Status::Unauthorized); + } +} 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 b95f898535..d5042803b0 100644 --- a/core/lib/tests/local-request-content-type-issue-505.rs +++ b/core/lib/tests/local-request-content-type-issue-505.rs @@ -3,6 +3,7 @@ use rocket::{Request, Data}; use rocket::request::{self, FromRequest}; use rocket::outcome::IntoOutcome; +use rocket::http::Status; struct HasContentType; @@ -11,7 +12,7 @@ impl<'r> FromRequest<'r> for HasContentType { type Error = (); async fn from_request(req: &'r Request<'_>) -> request::Outcome { - req.content_type().map(|_| HasContentType).or_forward(()) + req.content_type().map(|_| HasContentType).or_forward(Status::NotFound) } } @@ -22,7 +23,7 @@ impl<'r> FromData<'r> for HasContentType { type Error = (); async fn from_data(req: &'r Request<'_>, data: Data<'r>) -> data::Outcome<'r, Self> { - req.content_type().map(|_| HasContentType).or_forward(data) + req.content_type().map(|_| HasContentType).or_forward((data, Status::NotFound)) } } diff --git a/examples/cookies/src/session.rs b/examples/cookies/src/session.rs index 04ebeffe58..de74831b97 100644 --- a/examples/cookies/src/session.rs +++ b/examples/cookies/src/session.rs @@ -1,7 +1,7 @@ use rocket::outcome::IntoOutcome; use rocket::request::{self, FlashMessage, FromRequest, Request}; use rocket::response::{Redirect, Flash}; -use rocket::http::{Cookie, CookieJar}; +use rocket::http::{Cookie, CookieJar, Status}; use rocket::form::Form; use rocket_dyn_templates::{Template, context}; @@ -24,7 +24,7 @@ impl<'r> FromRequest<'r> for User { .get_private("user_id") .and_then(|cookie| cookie.value().parse().ok()) .map(User) - .or_forward(()) + .or_forward(Status::NotFound) } } diff --git a/examples/manual-routing/src/main.rs b/examples/manual-routing/src/main.rs index 69e03a1b87..704b69c705 100644 --- a/examples/manual-routing/src/main.rs +++ b/examples/manual-routing/src/main.rs @@ -84,7 +84,7 @@ impl route::Handler for CustomHandler { let self_data = self.data; let id = req.param::<&str>(0) .and_then(Result::ok) - .or_forward(data); + .or_forward((data, Status::NotFound)); route::Outcome::from(req, format!("{} - {}", self_data, try_outcome!(id))) } diff --git a/site/guide/6-state.md b/site/guide/6-state.md index e8d568c282..e884788ab8 100644 --- a/site/guide/6-state.md +++ b/site/guide/6-state.md @@ -124,6 +124,7 @@ retrieves `MyConfig` from managed state using both methods: use rocket::State; use rocket::request::{self, Request, FromRequest}; use rocket::outcome::IntoOutcome; +use rocket::http::Status; # struct MyConfig { user_val: String }; struct Item<'r>(&'r str); @@ -140,7 +141,7 @@ impl<'r> FromRequest<'r> for Item<'r> { // Or alternatively, using `Rocket::state()`: let outcome = request.rocket().state::() .map(|my_config| Item(&my_config.user_val)) - .or_forward(()); + .or_forward(Status::NotFound); outcome } From 9b0564ed27f90686b333337d9f6ed76484a84b27 Mon Sep 17 00:00:00 2001 From: Sergio Benitez Date: Tue, 11 Apr 2023 12:39:02 -0700 Subject: [PATCH 132/166] Tidy custom forward status changes, update docs. --- .../ui-fail-nightly/responder-types.stderr | 4 +- .../ui-fail-stable/responder-types.stderr | 4 +- core/http/src/tls/mtls.rs | 6 +- core/lib/src/data/from_data.rs | 4 +- core/lib/src/request/from_request.rs | 21 +++- core/lib/src/route/handler.rs | 4 +- core/lib/src/state.rs | 2 +- core/lib/tests/forward-includes-error-1560.rs | 79 ------------- .../lib/tests/forward-includes-status-1560.rs | 108 ++++++++++++++++++ examples/cookies/src/session.rs | 2 +- site/guide/4-requests.md | 12 +- site/guide/6-state.md | 3 +- 12 files changed, 145 insertions(+), 104 deletions(-) delete mode 100644 core/lib/tests/forward-includes-error-1560.rs create mode 100644 core/lib/tests/forward-includes-status-1560.rs diff --git a/core/codegen/tests/ui-fail-nightly/responder-types.stderr b/core/codegen/tests/ui-fail-nightly/responder-types.stderr index 6885bc43fc..84995fe04c 100644 --- a/core/codegen/tests/ui-fail-nightly/responder-types.stderr +++ b/core/codegen/tests/ui-fail-nightly/responder-types.stderr @@ -117,8 +117,8 @@ error[E0277]: the trait bound `usize: Responder<'_, '_>` is not satisfied as Responder<'r, 'static>> as Responder<'r, 'static>> and $N others -note: required by a bound in `route::handler::, Status, rocket::Data<'o>>>::from` +note: required by a bound in `route::handler::, Status, (rocket::Data<'o>, Status)>>::from` --> $WORKSPACE/core/lib/src/route/handler.rs | | pub fn from>(req: &'r Request<'_>, responder: R) -> Outcome<'r> { - | ^^^^^^^^^^^^^^^^^ required by this bound in `route::handler::, Status, Data<'o>>>::from` + | ^^^^^^^^^^^^^^^^^ required by this bound in `route::handler::, Status, (Data<'o>, Status)>>::from` diff --git a/core/codegen/tests/ui-fail-stable/responder-types.stderr b/core/codegen/tests/ui-fail-stable/responder-types.stderr index 4f14b511c9..4dafb9f599 100644 --- a/core/codegen/tests/ui-fail-stable/responder-types.stderr +++ b/core/codegen/tests/ui-fail-stable/responder-types.stderr @@ -117,8 +117,8 @@ error[E0277]: the trait bound `usize: Responder<'_, '_>` is not satisfied as Responder<'r, 'static>> as Responder<'r, 'static>> and $N others -note: required by a bound in `route::handler::, Status, rocket::Data<'o>>>::from` +note: required by a bound in `route::handler::, Status, (rocket::Data<'o>, Status)>>::from` --> $WORKSPACE/core/lib/src/route/handler.rs | | pub fn from>(req: &'r Request<'_>, responder: R) -> Outcome<'r> { - | ^^^^^^^^^^^^^^^^^ required by this bound in `route::handler::, Status, Data<'o>>>::from` + | ^^^^^^^^^^^^^^^^^ required by this bound in `route::handler::, Status, (Data<'o>, Status)>>::from` diff --git a/core/http/src/tls/mtls.rs b/core/http/src/tls/mtls.rs index 65246d4d89..f8b67dac09 100644 --- a/core/http/src/tls/mtls.rs +++ b/core/http/src/tls/mtls.rs @@ -64,7 +64,8 @@ pub type Result = std::result::Result; /// configured `ca_certs` and with respect to SNI, if any. See [module level /// docs](crate::mtls) for configuration details. /// -/// If the client does not present certificates, the guard _forwards_. +/// If the client does not present certificates, the guard _forwards_ with a +/// status of 401 Unauthorized. /// /// If the certificate chain fails to validate or verify, the guard _fails_ with /// the respective [`Error`]. @@ -81,6 +82,7 @@ pub type Result = std::result::Result; /// use rocket::mtls::{self, bigint::BigUint, Certificate}; /// use rocket::request::{Request, FromRequest, Outcome}; /// use rocket::outcome::try_outcome; +/// use rocket::http::Status; /// /// // The serial number for the certificate issued to the admin. /// const ADMIN_SERIAL: &str = "65828378108300243895479600452308786010218223563"; @@ -97,7 +99,7 @@ pub type Result = std::result::Result; /// if let Some(true) = cert.has_serial(ADMIN_SERIAL) { /// Outcome::Success(CertifiedAdmin(cert)) /// } else { -/// Outcome::Forward(()) +/// Outcome::Forward(Status::Unauthorized) /// } /// } /// } diff --git a/core/lib/src/data/from_data.rs b/core/lib/src/data/from_data.rs index ac7246093f..2bc1aa47a8 100644 --- a/core/lib/src/data/from_data.rs +++ b/core/lib/src/data/from_data.rs @@ -22,10 +22,10 @@ impl<'r, S, E> IntoOutcome, Status)> for Result } #[inline] - fn or_forward(self, (data, error): (Data<'r>, Status)) -> Outcome<'r, S, E> { + fn or_forward(self, (data, status): (Data<'r>, Status)) -> Outcome<'r, S, E> { match self { Ok(val) => Success(val), - Err(_) => Forward((data, error)) + Err(_) => Forward((data, status)) } } } diff --git a/core/lib/src/request/from_request.rs b/core/lib/src/request/from_request.rs index e50a47429f..33aa5ef86e 100644 --- a/core/lib/src/request/from_request.rs +++ b/core/lib/src/request/from_request.rs @@ -142,7 +142,7 @@ impl IntoOutcome for Result { /// Extracts the [`Route`] from the request if one is available. When used /// as a request guard in a route handler, this will always succeed. Outside /// of a route handler, a route may not be available, and the request is -/// forwarded with a 500 status. +/// forwarded with a 500 Internal Server Error status. /// /// For more information on when an `&Route` is available, see /// [`Request::route()`]. @@ -165,19 +165,19 @@ impl IntoOutcome for Result { /// /// Extracts the [`ContentType`] from the incoming request via /// [`Request::content_type()`]. If the request didn't specify a -/// Content-Type, the request is forwarded. +/// Content-Type, the request is forwarded with a 404 Not Found status. /// /// * **IpAddr** /// /// Extracts the client ip address of the incoming request as an [`IpAddr`] /// via [`Request::client_ip()`]. If the client's IP address is not known, -/// the request is forwarded. +/// the request is forwarded with a 404 Not Found status. /// /// * **SocketAddr** /// /// Extracts the remote address of the incoming request as a [`SocketAddr`] /// via [`Request::remote()`]. If the remote address is not known, the -/// request is forwarded. +/// request is forwarded with a 404 Not Found status. /// /// * **Option<T>** _where_ **T: FromRequest** /// @@ -193,7 +193,7 @@ impl IntoOutcome for Result { /// `FromRequest` 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. +/// forwarded with the same status code as the original forward. /// /// [`Config`]: crate::config::Config /// @@ -503,7 +503,7 @@ impl<'r, T: FromRequest<'r>> FromRequest<'r> for Result { match T::from_request(request).await { Success(val) => Success(Ok(val)), Failure((_, e)) => Success(Err(e)), - Forward(_) => Forward(Status::NotFound), + Forward(status) => Forward(status), } } } @@ -519,3 +519,12 @@ impl<'r, T: FromRequest<'r>> FromRequest<'r> for Option { } } } + +#[crate::async_trait] +impl<'r, T: FromRequest<'r>> FromRequest<'r> for Outcome { + type Error = std::convert::Infallible; + + async fn from_request(request: &'r Request<'_>) -> Outcome { + Success(T::from_request(request).await) + } +} diff --git a/core/lib/src/route/handler.rs b/core/lib/src/route/handler.rs index 739639d065..9fee0d67a8 100644 --- a/core/lib/src/route/handler.rs +++ b/core/lib/src/route/handler.rs @@ -221,8 +221,8 @@ impl<'r, 'o: 'r> Outcome<'o> { /// Return the `Outcome` of response to `req` from `responder`. /// /// If the responder returns `Ok`, an outcome of `Success` is returned with - /// the response. If the responder returns `Err`, an outcome of `Forward` is - /// returned. + /// the response. If the responder returns `Err`, an outcome of `Forward` + /// with a status of `404 Not Found` is returned. /// /// # Example /// diff --git a/core/lib/src/state.rs b/core/lib/src/state.rs index b323f77d4b..577e873b4c 100644 --- a/core/lib/src/state.rs +++ b/core/lib/src/state.rs @@ -80,7 +80,7 @@ use crate::http::Status; /// // Or alternatively, using `Rocket::state()`: /// let outcome = request.rocket().state::() /// .map(|my_config| Item(&my_config.user_val)) -/// .or_forward(Status::NotFound); +/// .or_forward(Status::InternalServerError); /// /// outcome /// } diff --git a/core/lib/tests/forward-includes-error-1560.rs b/core/lib/tests/forward-includes-error-1560.rs deleted file mode 100644 index 73cb145b4c..0000000000 --- a/core/lib/tests/forward-includes-error-1560.rs +++ /dev/null @@ -1,79 +0,0 @@ -#[macro_use] extern crate rocket; - -use rocket::http::Status; -use rocket::request::{self, Request, FromRequest}; - -pub struct Authenticated; - -#[rocket::async_trait] -impl<'r> FromRequest<'r> for Authenticated { - type Error = std::convert::Infallible; - - async fn from_request(request: &'r Request<'_>) -> request::Outcome { - if request.headers().contains("Authenticated") { - request::Outcome::Success(Authenticated) - } else { - request::Outcome::Forward(Status::Unauthorized) - } - } -} - -#[get("/one")] -pub async fn get_protected_one(_user: Authenticated) -> &'static str { - "Protected" -} - -#[get("/one", rank = 2)] -pub async fn get_public_one() -> &'static str { - "Public" -} - -#[get("/two")] -pub async fn get_protected_two(_user: Authenticated) -> &'static str { - "Protected" -} - -mod tests { - use super::*; - use rocket::routes; - use rocket::local::blocking::Client; - use rocket::http::{Header, Status}; - - #[test] - fn one_protected_returned_for_authenticated() { - let rocket = rocket::build().mount("/", - routes![get_protected_one, get_public_one, get_protected_two]); - - let client = Client::debug(rocket).unwrap(); - let req = client.get("/one").header(Header::new("Authenticated", "true")); - let response = req.dispatch(); - - assert_eq!(response.status(), Status::Ok); - assert_eq!(response.into_string(), Some("Protected".into())); - } - - #[test] - fn one_public_returned_for_unauthenticated() { - let rocket = rocket::build().mount("/", - routes![get_protected_one, get_public_one, get_protected_two]); - - let client = Client::debug(rocket).unwrap(); - let req = client.get("/one"); - let response = req.dispatch(); - - assert_eq!(response.status(), Status::Ok); - assert_eq!(response.into_string(), Some("Public".into())); - } - - #[test] - fn two_unauthorized_returned_for_unauthenticated() { - let rocket = rocket::build().mount("/", - routes![get_protected_one, get_public_one, get_protected_two]); - - let client = Client::debug(rocket).unwrap(); - let req = client.get("/two"); - let response = req.dispatch(); - - assert_eq!(response.status(), Status::Unauthorized); - } -} diff --git a/core/lib/tests/forward-includes-status-1560.rs b/core/lib/tests/forward-includes-status-1560.rs new file mode 100644 index 0000000000..b325672a41 --- /dev/null +++ b/core/lib/tests/forward-includes-status-1560.rs @@ -0,0 +1,108 @@ +#[macro_use] extern crate rocket; + +use rocket::http::Status; +use rocket::request::{self, Request, FromRequest}; + +struct Authenticated; + +#[rocket::async_trait] +impl<'r> FromRequest<'r> for Authenticated { + type Error = std::convert::Infallible; + + async fn from_request(request: &'r Request<'_>) -> request::Outcome { + if request.headers().contains("Authenticated") { + request::Outcome::Success(Authenticated) + } else { + request::Outcome::Forward(Status::Unauthorized) + } + } +} + +struct TeapotForward; + +#[rocket::async_trait] +impl<'r> FromRequest<'r> for TeapotForward { + type Error = std::convert::Infallible; + + async fn from_request(_: &'r Request<'_>) -> request::Outcome { + request::Outcome::Forward(Status::ImATeapot) + } +} + +#[get("/auth")] +fn auth(_name: Authenticated) -> &'static str { + "Protected" +} + +#[get("/auth", rank = 2)] +fn public() -> &'static str { + "Public" +} + +#[get("/auth", rank = 3)] +fn teapot(_teapot: TeapotForward) -> &'static str { + "Protected" +} + +#[get("/need-auth")] +fn auth_needed(_auth: Authenticated) -> &'static str { + "Have Auth" +} + +#[catch(401)] +fn catcher() -> &'static str { + "Custom Catcher" +} + +mod tests { + use super::*; + use rocket::routes; + use rocket::local::blocking::Client; + use rocket::http::{Header, Status}; + + #[test] + fn authorized_forwards() { + let client = Client::debug_with(routes![auth, public, auth_needed]).unwrap(); + + let response = client.get("/auth") + .header(Header::new("Authenticated", "true")) + .dispatch(); + + assert_eq!(response.status(), Status::Ok); + assert_eq!(response.into_string().unwrap(), "Protected"); + + let response = client.get("/auth").dispatch(); + assert_eq!(response.status(), Status::Ok); + assert_eq!(response.into_string().unwrap(), "Public"); + + let response = client.get("/need-auth") + .header(Header::new("Authenticated", "true")) + .dispatch(); + + assert_eq!(response.status(), Status::Ok); + assert_eq!(response.into_string().unwrap(), "Have Auth"); + + let response = client.get("/need-auth").dispatch(); + assert_eq!(response.status(), Status::Unauthorized); + assert!(response.into_string().unwrap().contains("Rocket")); + } + + #[test] + fn unauthorized_custom_catcher() { + let rocket = rocket::build() + .mount("/", routes![auth_needed]) + .register("/", catchers![catcher]); + + let client = Client::debug(rocket).unwrap(); + let response = client.get("/need-auth").dispatch(); + assert_eq!(response.status(), Status::Unauthorized); + assert_eq!(response.into_string().unwrap(), "Custom Catcher"); + } + + #[test] + fn use_last_forward() { + let client = Client::debug_with(routes![auth, teapot]).unwrap(); + let response = client.get("/auth").dispatch(); + assert_eq!(response.status(), Status::ImATeapot); + } +} diff --git a/examples/cookies/src/session.rs b/examples/cookies/src/session.rs index de74831b97..c4309a4283 100644 --- a/examples/cookies/src/session.rs +++ b/examples/cookies/src/session.rs @@ -24,7 +24,7 @@ impl<'r> FromRequest<'r> for User { .get_private("user_id") .and_then(|cookie| cookie.value().parse().ok()) .map(User) - .or_forward(Status::NotFound) + .or_forward(Status::Unauthorized) } } diff --git a/site/guide/4-requests.md b/site/guide/4-requests.md index 45670455ba..4fe8fee5b7 100644 --- a/site/guide/4-requests.md +++ b/site/guide/4-requests.md @@ -206,9 +206,10 @@ fn hello(name: &str, age: u8, cool: bool) { /* ... */ } What if `cool` isn't a `bool`? Or, what if `age` isn't a `u8`? When a parameter type mismatch occurs, Rocket _forwards_ the request to the next matching route, -if there is any. This continues until a route doesn't forward the request or -there are no remaining routes to try. When there are no remaining routes, a -customizable **404 error** is returned. +if there is any. This continues until a route succeeds or fails, or there are no +other matching routes to try. When there are no remaining routes, the [error +catcher](#error-catchers) associated with the status set by the last forwarding +guard is called. Routes are attempted in increasing _rank_ order. Rocket chooses a default ranking from -12 to -1, detailed in the next section, but a route's rank can also @@ -436,13 +437,14 @@ We start with two request guards: The `FromRequest` implementation for `User` checks that a cookie identifies a user and returns a `User` value if so. If no user can be authenticated, - the guard forwards. + the guard forwards with a 401 Unauthorized status. * `AdminUser`: A user authenticated as an administrator. The `FromRequest` implementation for `AdminUser` checks that a cookie identifies an _administrative_ user and returns an `AdminUser` value if so. - If no user can be authenticated, the guard forwards. + If no user can be authenticated, the guard forwards with a 401 Unauthorized + status. We now use these two guards in combination with forwarding to implement the following three routes, each leading to an administrative control panel at diff --git a/site/guide/6-state.md b/site/guide/6-state.md index e884788ab8..0eb6d65a1d 100644 --- a/site/guide/6-state.md +++ b/site/guide/6-state.md @@ -141,14 +141,13 @@ impl<'r> FromRequest<'r> for Item<'r> { // Or alternatively, using `Rocket::state()`: let outcome = request.rocket().state::() .map(|my_config| Item(&my_config.user_val)) - .or_forward(Status::NotFound); + .or_forward(Status::InternalServerError); outcome } } ``` - [`Request::guard()`]: @api/rocket/struct.Request.html#method.guard [`Rocket::state()`]: @api/rocket/struct.Rocket.html#method.state From c86da1327051a2ab68acce0fb5f89a35e4654e78 Mon Sep 17 00:00:00 2001 From: Sergio Benitez Date: Mon, 1 May 2023 17:25:22 -0700 Subject: [PATCH 133/166] Mark '.exe', '.iso', '.dmg' as known extensions. 'EXE' is IANA registered, and the registered media type is used here for the '.exe' extension. The '.iso' and '.dmg' extensions do not appear to correspond to any IANA registered media type, but they have a de facto media type of "application/octet-stream", and that media type is used by this commit. Closes #2530. --- core/http/src/header/known_media_types.rs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/core/http/src/header/known_media_types.rs b/core/http/src/header/known_media_types.rs index 2f56ea716c..1fe8ce73cf 100644 --- a/core/http/src/header/known_media_types.rs +++ b/core/http/src/header/known_media_types.rs @@ -52,6 +52,7 @@ macro_rules! known_media_types { EPUB (is_epub): "EPUB", "application", "epub+zip", EventStream (is_event_stream): "SSE stream", "text", "event-stream", Markdown (is_markdown): "markdown text", "text", "markdown" ; "charset" => "utf-8", + EXE (is_exe): "executable", "application", "vnd.microsoft.portable-executable", }) } @@ -95,6 +96,8 @@ macro_rules! known_extensions { "aac" => AAC, "ics" => Calendar, "bin" => Binary, + "iso" => Binary, + "dmg" => Binary, "mpg" => MPEG, "mpeg" => MPEG, "tar" => TAR, @@ -109,6 +112,7 @@ macro_rules! known_extensions { "epub" => EPUB, "md" => Markdown, "markdown" => Markdown, + "exe" => EXE, }) } From 6ab85b6643289016be5e7905ad5c1fa44e9ce2d6 Mon Sep 17 00:00:00 2001 From: Sergio Benitez Date: Mon, 1 May 2023 17:46:03 -0700 Subject: [PATCH 134/166] Remove unnecessary 'mut' in 'uri!' impl. --- core/codegen/src/bang/uri.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/codegen/src/bang/uri.rs b/core/codegen/src/bang/uri.rs index 5ff50d5031..175b73da7e 100644 --- a/core/codegen/src/bang/uri.rs +++ b/core/codegen/src/bang/uri.rs @@ -23,7 +23,7 @@ macro_rules! p { } pub fn prefix_last_segment(path: &mut syn::Path, prefix: &str) { - let mut last_seg = path.segments.last_mut().expect("syn::Path has segments"); + let last_seg = path.segments.last_mut().expect("syn::Path has segments"); last_seg.ident = last_seg.ident.prepend(prefix); } From dbc43c41a3b03151b7890e49a2b98013f514f30e Mon Sep 17 00:00:00 2001 From: Sergio Benitez Date: Wed, 3 May 2023 17:36:47 -0700 Subject: [PATCH 135/166] Fix missing port parsing in 'Authority'. If a port part was missing, the 'Authority' parser previously set the port to `0`. This is incorrect. As in RFC#3986 3.2.3: > URI producers and normalizers should omit the port component and its ":" delimiter if port is empty [..] This commit fixes the parser's behavior to align with the RFC. --- core/http/src/parse/uri/parser.rs | 4 +++- core/http/src/parse/uri/tests.rs | 12 ++++++------ 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/core/http/src/parse/uri/parser.rs b/core/http/src/parse/uri/parser.rs index e9438da8e2..d7bac58b93 100644 --- a/core/http/src/parse/uri/parser.rs +++ b/core/http/src/parse/uri/parser.rs @@ -167,7 +167,9 @@ fn port<'a>( // current - bytes.len(). Or something like that. #[parser] fn maybe_port<'a>(input: &mut RawInput<'a>, bytes: &[u8]) -> Result<'a, Option> { - if bytes.len() > 5 { + if bytes.is_empty() { + return Ok(None); + } else if bytes.len() > 5 { parse_error!("port len is out of range")?; } else if !bytes.iter().all(|b| b.is_ascii_digit()) { parse_error!("invalid port bytes")?; diff --git a/core/http/src/parse/uri/tests.rs b/core/http/src/parse/uri/tests.rs index 0f8a262ebf..7106e4b2f0 100644 --- a/core/http/src/parse/uri/tests.rs +++ b/core/http/src/parse/uri/tests.rs @@ -139,7 +139,7 @@ fn single_byte() { "%" => Authority::new(None, "%", None), "?" => Reference::new(None, None, "", "", None), "#" => Reference::new(None, None, "", None, ""), - ":" => Authority::new(None, "", 0), + ":" => Authority::new(None, "", None), "@" => Authority::new("", "", None), ); @@ -169,13 +169,13 @@ fn origin() { #[test] fn authority() { assert_parse_eq!( - "@:" => Authority::new("", "", 0), + "@:" => Authority::new("", "", None), "abc" => Authority::new(None, "abc", None), "@abc" => Authority::new("", "abc", None), "a@b" => Authority::new("a", "b", None), "a@" => Authority::new("a", "", None), ":@" => Authority::new(":", "", None), - ":@:" => Authority::new(":", "", 0), + ":@:" => Authority::new(":", "", None), "sergio:benitez@spark" => Authority::new("sergio:benitez", "spark", None), "a:b:c@1.2.3:12121" => Authority::new("a:b:c", "1.2.3", 12121), "sergio@spark" => Authority::new("sergio", "spark", None), @@ -183,7 +183,7 @@ fn authority() { "sergio@[1::]:230" => Authority::new("sergio", "[1::]", 230), "rocket.rs:8000" => Authority::new(None, "rocket.rs", 8000), "[1::2::3]:80" => Authority::new(None, "[1::2::3]", 80), - "bar:" => Authority::new(None, "bar", 0), // could be absolute too + "bar:" => Authority::new(None, "bar", None), // could be absolute too ); } @@ -227,7 +227,7 @@ fn absolute() { "git://:@rocket.rs:443/abc?q" => Absolute::new("git", Authority::new(":", "rocket.rs", 443), "/abc", "q"), "a://b?test" => Absolute::new("a", Authority::new(None, "b", None), "", "test"), - "a://b:?test" => Absolute::new("a", Authority::new(None, "b", 0), "", "test"), + "a://b:?test" => Absolute::new("a", Authority::new(None, "b", None), "", "test"), "a://b:1?test" => Absolute::new("a", Authority::new(None, "b", 1), "", "test"), }; } @@ -256,7 +256,7 @@ fn reference() { "a:/?a#b" => Reference::new("a", None, "/", "a", "b"), "a:?a#b" => Reference::new("a", None, "", "a", "b"), "a://?a#b" => Reference::new("a", Authority::new(None, "", None), "", "a", "b"), - "a://:?a#b" => Reference::new("a", Authority::new(None, "", 0), "", "a", "b"), + "a://:?a#b" => Reference::new("a", Authority::new(None, "", None), "", "a", "b"), "a://:2000?a#b" => Reference::new("a", Authority::new(None, "", 2000), "", "a", "b"), "a://a:2000?a#b" => Reference::new("a", Authority::new(None, "a", 2000), "", "a", "b"), "a://a:@2000?a#b" => Reference::new("a", Authority::new("a:", "2000", None), "", "a", "b"), From 56cf905c6edcea672ef9896dd3e1da7593da793c Mon Sep 17 00:00:00 2001 From: Sergio Benitez Date: Wed, 3 May 2023 19:56:49 -0700 Subject: [PATCH 136/166] Introduce more flexible mounting. Prior to this commit, a route with a URI of `/` could not be mounted in such a way that the resulting effective URI contained a trailing slash. This commit changes the semantics of mounting so that mounting such a route to a mount point with a trailing slash yields an effective URI with a trailing slash. When mounted to points without a trailing slash, the effective URI does not have a trailing slash. This commit also introduces the `Route::rebase()` and `Catcher::rebase()` methods for easier rebasing of existing routes and catchers. Finally, this commit improves logging such that mount points of `/` are underlined in the logs. Tests and docs were added and modified as necessary. Resolves #2533. --- core/codegen/src/bang/uri_parsing.rs | 10 +--- core/codegen/tests/typed-uris.rs | 64 ++++++++++++++++++-- core/http/src/uri/fmt/formatter.rs | 31 +++++----- core/lib/src/catcher/catcher.rs | 55 +++++++++++++++-- core/lib/src/rocket.rs | 77 ++++++++++++++++-------- core/lib/src/route/route.rs | 66 ++++++++++++++++---- core/lib/src/route/uri.rs | 90 ++++++++++++++++++++++++---- 7 files changed, 313 insertions(+), 80 deletions(-) diff --git a/core/codegen/src/bang/uri_parsing.rs b/core/codegen/src/bang/uri_parsing.rs index d7c772dc59..ea3665a824 100644 --- a/core/codegen/src/bang/uri_parsing.rs +++ b/core/codegen/src/bang/uri_parsing.rs @@ -318,7 +318,7 @@ impl Parse for InternalUriParams { let fn_args = fn_args.into_iter().collect(); input.parse::()?; - let uri_params = input.parse::()?; + let uri_mac = input.parse::()?; let span = route_uri_str.subspan(1..route_uri.path().len() + 1); let path_params = Parameter::parse_many::(route_uri.path().as_str(), span) @@ -334,13 +334,7 @@ impl Parse for InternalUriParams { .collect::>() }).unwrap_or_default(); - Ok(InternalUriParams { - route_uri, - path_params, - query_params, - fn_args, - uri_mac: uri_params - }) + Ok(InternalUriParams { route_uri, path_params, query_params, fn_args, uri_mac }) } } diff --git a/core/codegen/tests/typed-uris.rs b/core/codegen/tests/typed-uris.rs index bef5eedfa5..e413eccb3e 100644 --- a/core/codegen/tests/typed-uris.rs +++ b/core/codegen/tests/typed-uris.rs @@ -189,47 +189,58 @@ fn check_route_prefix_suffix() { uri!("/") => "/", uri!("/", index) => "/", uri!("/hi", index) => "/hi", + uri!("/foo", index) => "/foo", + uri!("/hi/", index) => "/hi/", + uri!("/foo/", index) => "/foo/", uri!("/", simple3(10)) => "/?id=10", uri!("/hi", simple3(11)) => "/hi?id=11", + uri!("/hi/", simple3(11)) => "/hi/?id=11", uri!("/mount", simple(100)) => "/mount/100", uri!("/mount", simple(id = 23)) => "/mount/23", + uri!("/mount/", simple(100)) => "/mount/100", + uri!("/mount/", simple(id = 23)) => "/mount/23", uri!("/another", simple(100)) => "/another/100", uri!("/another", simple(id = 23)) => "/another/23", uri!("/foo") => "/foo", uri!("/foo/") => "/foo/", uri!("/foo///") => "/foo/", uri!("/foo/bar/") => "/foo/bar/", - uri!("/foo/", index) => "/foo/", - uri!("/foo", index) => "/foo", } assert_uri_eq! { uri!("http://rocket.rs", index) => "http://rocket.rs", uri!("http://rocket.rs/", index) => "http://rocket.rs/", + uri!("http://rocket.rs///", index) => "http://rocket.rs/", uri!("http://rocket.rs/foo", index) => "http://rocket.rs/foo", uri!("http://rocket.rs/foo/", index) => "http://rocket.rs/foo/", uri!("http://", index) => "http://", - uri!("ftp:", index) => "ftp:/", + uri!("http:///", index) => "http:///", + uri!("http:////", index) => "http:///", + uri!("ftp:/", index) => "ftp:/", } assert_uri_eq! { uri!("http://rocket.rs", index, "?foo") => "http://rocket.rs?foo", uri!("http://rocket.rs", index, "?") => "http://rocket.rs?", uri!("http://rocket.rs", index, "#") => "http://rocket.rs#", + uri!("http://rocket.rs", index, "#bar") => "http://rocket.rs#bar", + uri!("http://rocket.rs", index, "?bar#baz") => "http://rocket.rs?bar#baz", + uri!("http://rocket.rs/", index, "?foo") => "http://rocket.rs/?foo", uri!("http://rocket.rs/", index, "?") => "http://rocket.rs/?", uri!("http://rocket.rs/", index, "#") => "http://rocket.rs/#", - uri!("http://rocket.rs", index, "#bar") => "http://rocket.rs#bar", uri!("http://rocket.rs/", index, "#bar") => "http://rocket.rs/#bar", - uri!("http://rocket.rs", index, "?bar#baz") => "http://rocket.rs?bar#baz", uri!("http://rocket.rs/", index, "?bar#baz") => "http://rocket.rs/?bar#baz", uri!("http://", index, "?foo") => "http://?foo", uri!("http://rocket.rs", simple3(id = 100), "?foo") => "http://rocket.rs?id=100", uri!("http://rocket.rs", simple3(id = 100), "?foo#bar") => "http://rocket.rs?id=100#bar", + uri!("http://rocket.rs/", simple3(id = 100), "?foo") => "http://rocket.rs/?id=100", + uri!("http://rocket.rs/", simple3(id = 100), "?foo#bar") => "http://rocket.rs/?id=100#bar", uri!(_, simple3(id = 100), "?foo#bar") => "/?id=100#bar", } let dyn_origin = uri!("/a/b/c"); let dyn_origin2 = uri!("/a/b/c?foo-bar"); + let dyn_origin_slash = uri!("/a/b/c/"); assert_uri_eq! { uri!(dyn_origin.clone(), index) => "/a/b/c", uri!(dyn_origin2.clone(), index) => "/a/b/c", @@ -241,12 +252,25 @@ fn check_route_prefix_suffix() { uri!(dyn_origin2.clone(), simple2(100, "hey")) => "/a/b/c/100/hey", uri!(dyn_origin.clone(), simple2(id = 23, name = "hey")) => "/a/b/c/23/hey", uri!(dyn_origin2.clone(), simple2(id = 23, name = "hey")) => "/a/b/c/23/hey", + + uri!(dyn_origin_slash.clone(), index) => "/a/b/c/", + uri!(dyn_origin_slash.clone(), simple3(10)) => "/a/b/c/?id=10", + uri!(dyn_origin_slash.clone(), simple(100)) => "/a/b/c/100", } let dyn_absolute = uri!("http://rocket.rs"); + let dyn_absolute_slash = uri!("http://rocket.rs/"); assert_uri_eq! { uri!(dyn_absolute.clone(), index) => "http://rocket.rs", + uri!(dyn_absolute.clone(), simple(100)) => "http://rocket.rs/100", + uri!(dyn_absolute.clone(), simple3(123)) => "http://rocket.rs?id=123", + uri!(dyn_absolute_slash.clone(), index) => "http://rocket.rs/", + uri!(dyn_absolute_slash.clone(), simple(100)) => "http://rocket.rs/100", + uri!(dyn_absolute_slash.clone(), simple3(123)) => "http://rocket.rs/?id=123", + uri!(uri!("http://rocket.rs/a/b"), index) => "http://rocket.rs/a/b", + uri!("http://rocket.rs/a/b") => "http://rocket.rs/a/b", uri!(uri!("http://rocket.rs/a/b"), index) => "http://rocket.rs/a/b", + uri!("http://rocket.rs/a/b") => "http://rocket.rs/a/b", } let dyn_abs = uri!("http://rocket.rs?foo"); @@ -258,12 +282,23 @@ fn check_route_prefix_suffix() { uri!(_, simple3(id = 123), dyn_abs) => "/?id=123", } + let dyn_abs = uri!("http://rocket.rs/?foo"); + assert_uri_eq! { + uri!(_, index, dyn_abs.clone()) => "/?foo", + uri!("http://rocket.rs", index, dyn_abs.clone()) => "http://rocket.rs?foo", + uri!("http://rocket.rs/", index, dyn_abs.clone()) => "http://rocket.rs/?foo", + uri!("http://", index, dyn_abs.clone()) => "http://?foo", + uri!("http:///", index, dyn_abs.clone()) => "http:///?foo", + uri!(_, simple3(id = 123), dyn_abs) => "/?id=123", + } + let dyn_ref = uri!("?foo#bar"); assert_uri_eq! { uri!(_, index, dyn_ref.clone()) => "/?foo#bar", uri!("http://rocket.rs", index, dyn_ref.clone()) => "http://rocket.rs?foo#bar", uri!("http://rocket.rs/", index, dyn_ref.clone()) => "http://rocket.rs/?foo#bar", uri!("http://", index, dyn_ref.clone()) => "http://?foo#bar", + uri!("http:///", index, dyn_ref.clone()) => "http:///?foo#bar", uri!(_, simple3(id = 123), dyn_ref) => "/?id=123#bar", } } @@ -619,3 +654,22 @@ fn test_json() { uri!(bar(&mut Json(inner))) => "/?json=%7B%22foo%22:%7B%22foo%22:%22hi%22%7D%7D", } } + +#[test] +fn test_route_uri_normalization_with_prefix() { + #[get("/world")] fn world() {} + + assert_uri_eq! { + uri!("/", index()) => "/", + uri!("/foo", index()) => "/foo", + uri!("/bar/", index()) => "/bar/", + uri!("/foo/bar", index()) => "/foo/bar", + uri!("/foo/bar/", index()) => "/foo/bar/", + + uri!("/", world()) => "/world", + uri!("/foo", world()) => "/foo/world", + uri!("/bar/", world()) => "/bar/world", + uri!("/foo/bar", world()) => "/foo/bar/world", + uri!("/foo/bar/", world()) => "/foo/bar/world", + } +} diff --git a/core/http/src/uri/fmt/formatter.rs b/core/http/src/uri/fmt/formatter.rs index 9fe18c4656..c2e81ac839 100644 --- a/core/http/src/uri/fmt/formatter.rs +++ b/core/http/src/uri/fmt/formatter.rs @@ -437,15 +437,23 @@ impl<'a> ValidRoutePrefix for Origin<'a> { let mut prefix = self.into_normalized(); prefix.clear_query(); + // Avoid a double `//` to start. if prefix.path() == "/" { - // Avoid a double `//` to start. return Origin::new(path, query); - } else if path == "/" { - // Appending path to `/` is a no-op, but append any query. + } + + // Avoid allocating if the `path` would result in just the prefix. + if path == "/" { prefix.set_query(query); return prefix; } + // Avoid a `//` resulting from joining. + if prefix.has_trailing_slash() && path.starts_with('/') { + return Origin::new(format!("{}{}", prefix.path(), &path[1..]), query); + } + + // Join normally. Origin::new(format!("{}{}", prefix.path(), path), query) } } @@ -458,12 +466,11 @@ impl<'a> ValidRoutePrefix for Absolute<'a> { let mut prefix = self.into_normalized(); prefix.clear_query(); - if prefix.authority().is_some() { - // The prefix is normalized. Appending a `/` is a no-op. - if path == "/" { - prefix.set_query(query); - return prefix; - } + // Distinguish for routes `/` with bases of `/foo/` and `/foo`. The + // latter base, without a trailing slash, should combine as `/foo`. + if path == "/" { + prefix.set_query(query); + return prefix; } // In these cases, appending `path` would be a no-op or worse. @@ -473,11 +480,7 @@ impl<'a> ValidRoutePrefix for Absolute<'a> { return prefix; } - if path == "/" { - prefix.set_query(query); - return prefix; - } - + // Create the combined URI. prefix.set_path(format!("{}{}", prefix.path(), path)); prefix.set_query(query); prefix diff --git a/core/lib/src/catcher/catcher.rs b/core/lib/src/catcher/catcher.rs index e8d0687d36..9cb4d6081f 100644 --- a/core/lib/src/catcher/catcher.rs +++ b/core/lib/src/catcher/catcher.rs @@ -2,6 +2,7 @@ use std::fmt; use std::io::Cursor; use crate::http::uri::Path; +use crate::http::ext::IntoOwned; use crate::response::Response; use crate::request::Request; use crate::http::{Status, ContentType, uri}; @@ -207,9 +208,58 @@ impl Catcher { self.base.path() } + /// Prefix `base` to the current `base` in `self.` + /// + /// If the the current base is `/`, then the base is replaced by `base`. + /// Otherwise, `base` is prefixed to the existing `base`. + /// + /// ```rust + /// use rocket::request::Request; + /// use rocket::catcher::{Catcher, BoxFuture}; + /// use rocket::response::Responder; + /// use rocket::http::Status; + /// # use rocket::uri; + /// + /// fn handle_404<'r>(status: Status, req: &'r Request<'_>) -> BoxFuture<'r> { + /// let res = (status, format!("404: {}", req.uri())); + /// Box::pin(async move { res.respond_to(req) }) + /// } + /// + /// let catcher = Catcher::new(404, handle_404); + /// assert_eq!(catcher.base(), "/"); + /// + /// // Since the base is `/`, rebasing replaces the base. + /// let rebased = catcher.rebase(uri!("/boo")); + /// assert_eq!(rebased.base(), "/boo"); + /// + /// // Now every rebase prefixes. + /// let rebased = rebased.rebase(uri!("/base")); + /// assert_eq!(rebased.base(), "/base/boo"); + /// + /// // Note that trailing slashes have no effect and are thus removed: + /// let catcher = Catcher::new(404, handle_404); + /// let rebased = catcher.rebase(uri!("/boo/")); + /// assert_eq!(rebased.base(), "/boo"); + /// ``` + pub fn rebase(mut self, mut base: uri::Origin<'_>) -> Self { + self.base = if self.base.path() == "/" { + base.clear_query(); + base.into_normalized_nontrailing().into_owned() + } else { + uri::Origin::parse_owned(format!("{}{}", base.path(), self.base)) + .expect("catcher rebase: {new}{old} is valid origin URI") + .into_normalized_nontrailing() + }; + + self.rank = -1 * (self.base().segments().filter(|s| !s.is_empty()).count() as isize); + self + } + /// Maps the `base` of this catcher using `mapper`, returning a new /// `Catcher` with the returned base. /// + /// **Note:** Prefer to use [`Catcher::rebase()`] whenever possible! + /// /// `mapper` is called with the current base. The returned `String` is used /// as the new base if it is a valid URI. If the returned base URI contains /// a query, it is ignored. Returns an error if the base produced by @@ -240,10 +290,7 @@ impl Catcher { /// let catcher = catcher.map_base(|base| format!("/foo ? {}", base)); /// assert!(catcher.is_err()); /// ``` - pub fn map_base<'a, F>( - mut self, - mapper: F - ) -> std::result::Result> + pub fn map_base<'a, F>(mut self, mapper: F) -> Result> where F: FnOnce(uri::Origin<'a>) -> String { let new_base = uri::Origin::parse_owned(mapper(self.base))?; diff --git a/core/lib/src/rocket.rs b/core/lib/src/rocket.rs index 99bc66423d..e3ceb17bb8 100644 --- a/core/lib/src/rocket.rs +++ b/core/lib/src/rocket.rs @@ -12,7 +12,7 @@ use crate::trip_wire::TripWire; use crate::fairing::{Fairing, Fairings}; use crate::phase::{Phase, Build, Building, Ignite, Igniting, Orbit, Orbiting}; use crate::phase::{Stateful, StateRef, State}; -use crate::http::uri::{self, Origin}; +use crate::http::uri::Origin; use crate::http::ext::IntoOwned; use crate::error::{Error, ErrorKind}; use crate::log::PaintExt; @@ -246,7 +246,7 @@ impl Rocket { fn load<'a, B, T, F, M>(mut self, kind: &str, base: B, items: Vec, m: M, f: F) -> Self where B: TryInto> + Clone + fmt::Display, B::Error: fmt::Display, - M: Fn(&Origin<'a>, T) -> Result>, + M: Fn(&Origin<'a>, T) -> T, F: Fn(&mut Self, T), T: Clone + fmt::Display, { @@ -266,42 +266,65 @@ impl Rocket { } for unmounted_item in items { - let item = match m(&base, unmounted_item.clone()) { - Ok(item) => item, - Err(e) => { - error!("malformed URI in {} {}", kind, unmounted_item); - error_!("{}", e); - info_!("{} {}", Paint::white("in"), std::panic::Location::caller()); - panic!("aborting due to invalid {} URI", kind); - } - }; - - f(&mut self, item) + f(&mut self, m(&base, unmounted_item.clone())) } self } - /// Mounts all of the routes in the supplied vector at the given `base` - /// path. Mounting a route with path `path` at path `base` makes the route - /// available at `base/path`. + /// Mounts all of the `routes` at the given `base` mount point. + /// + /// A route _mounted_ at `base` has an effective URI of `base/route`, where + /// `route` is the route URI. In other words, `base` is added as a prefix to + /// the route's URI. The URI resulting from joining the `base` URI and the + /// route URI is called the route's _effective URI_, as this is the URI used + /// for request matching during routing. + /// + /// A `base` URI is not allowed to have a query part. If a `base` _does_ + /// have a query part, it is ignored when producing the effective URI. + /// + /// A `base` may have an optional trailing slash. A route with a URI path of + /// `/` (and any optional query) mounted at a `base` has an effective URI + /// equal to the `base` (plus any optional query). That is, if the base has + /// a trailing slash, the effective URI path has a trailing slash, and + /// otherwise it does not. Routes with URI paths other than `/` are not + /// effected by trailing slashes in their corresponding mount point. + /// + /// As concrete examples, consider the following table: + /// + /// | mount point | route URI | effective URI | + /// |-------------|-----------|---------------| + /// | `/` | `/foo` | `/foo` | + /// | `/` | `/foo/` | `/foo/` | + /// | `/foo` | `/` | `/foo` | + /// | `/foo` | `/?bar` | `/foo?bar` | + /// | `/foo` | `/bar` | `/foo/bar` | + /// | `/foo` | `/bar/` | `/foo/bar/` | + /// | `/foo/` | `/` | `/foo/` | + /// | `/foo/` | `/bar` | `/foo/bar` | + /// | `/foo/` | `/?bar` | `/foo/?bar` | + /// | `/foo/bar` | `/` | `/foo/bar` | + /// | `/foo/bar/` | `/` | `/foo/bar/` | + /// | `/foo/?bar` | `/` | `/foo/` | + /// | `/foo/?bar` | `/baz` | `/foo/baz` | + /// | `/foo/?bar` | `/baz/` | `/foo/baz/` | /// /// # Panics /// /// Panics if either: - /// * the `base` mount point is not a valid static path: a valid origin - /// URI without dynamic parameters. /// - /// * any route's URI is not a valid origin URI. + /// * the `base` mount point is not a valid origin URI without dynamic + /// parameters /// - /// **Note:** _This kind of panic is guaranteed not to occur if the routes - /// were generated using Rocket's code generation._ + /// * any route URI is not a valid origin URI. (**Note:** _This kind of + /// panic is guaranteed not to occur if the routes were generated using + /// Rocket's code generation._) /// /// # Examples /// /// Use the `routes!` macro to mount routes created using the code - /// generation facilities. Requests to the `/hello/world` URI will be - /// dispatched to the `hi` route. + /// generation facilities. Requests to both `/world` and `/hello/world` URI + /// will be dispatched to the `hi` route. /// /// ```rust,no_run /// # #[macro_use] extern crate rocket; @@ -313,7 +336,9 @@ impl Rocket { /// /// #[launch] /// fn rocket() -> _ { - /// rocket::build().mount("/hello", routes![hi]) + /// rocket::build() + /// .mount("/", routes![hi]) + /// .mount("/hello", routes![hi]) /// } /// ``` /// @@ -344,7 +369,7 @@ impl Rocket { R: Into> { self.load("route", base, routes.into(), - |base, route| route.map_base(|old| format!("{}{}", base, old)), + |base, route| route.rebase(base.clone()), |r, route| r.0.routes.push(route)) } @@ -383,7 +408,7 @@ impl Rocket { C: Into> { self.load("catcher", base, catchers.into(), - |base, catcher| catcher.map_base(|old| format!("{}{}", base, old)), + |base, catcher| catcher.rebase(base.clone()), |r, catcher| r.0.catchers.push(catcher)) } diff --git a/core/lib/src/route/route.rs b/core/lib/src/route/route.rs index 25f8436fa3..131d2a302a 100644 --- a/core/lib/src/route/route.rs +++ b/core/lib/src/route/route.rs @@ -1,5 +1,4 @@ use std::fmt; -use std::convert::From; use std::borrow::Cow; use yansi::Paint; @@ -257,9 +256,48 @@ impl Route { } } + /// Prefix `base` to any existing mount point base in `self`. + /// + /// If the the current mount point base is `/`, then the base is replaced by + /// `base`. Otherwise, `base` is prefixed to the existing `base`. + /// + /// ```rust + /// use rocket::Route; + /// use rocket::http::Method; + /// # use rocket::route::dummy_handler as handler; + /// # use rocket::uri; + /// + /// // The default base is `/`. + /// let index = Route::new(Method::Get, "/foo/bar", handler); + /// + /// // Since the base is `/`, rebasing replaces the base. + /// let rebased = index.rebase(uri!("/boo")); + /// assert_eq!(rebased.uri.base(), "/boo"); + /// + /// // Now every rebase prefixes. + /// let rebased = rebased.rebase(uri!("/base")); + /// assert_eq!(rebased.uri.base(), "/base/boo"); + /// + /// // Note that trailing slashes are preserved: + /// let index = Route::new(Method::Get, "/foo", handler); + /// let rebased = index.rebase(uri!("/boo/")); + /// assert_eq!(rebased.uri.base(), "/boo/"); + /// ``` + pub fn rebase(mut self, base: uri::Origin<'_>) -> Self { + let new_base = match self.uri.base().as_str() { + "/" => base.path().to_string(), + _ => format!("{}{}", base.path(), self.uri.base()), + }; + + self.uri = RouteUri::new(&new_base, &self.uri.unmounted_origin.to_string()); + self + } + /// Maps the `base` of this route using `mapper`, returning a new `Route` /// with the returned base. /// + /// **Note:** Prefer to use [`Route::rebase()`] whenever possible! + /// /// `mapper` is called with the current base. The returned `String` is used /// as the new base if it is a valid URI. If the returned base URI contains /// a query, it is ignored. Returns an error if the base produced by @@ -269,18 +307,28 @@ impl Route { /// /// ```rust /// use rocket::Route; - /// use rocket::http::{Method, uri::Origin}; + /// use rocket::http::Method; /// # use rocket::route::dummy_handler as handler; + /// # use rocket::uri; /// /// let index = Route::new(Method::Get, "/foo/bar", handler); /// assert_eq!(index.uri.base(), "/"); /// assert_eq!(index.uri.unmounted().path(), "/foo/bar"); /// assert_eq!(index.uri.path(), "/foo/bar"); /// - /// let index = index.map_base(|base| format!("{}{}", "/boo", base)).unwrap(); - /// assert_eq!(index.uri.base(), "/boo"); - /// assert_eq!(index.uri.unmounted().path(), "/foo/bar"); - /// assert_eq!(index.uri.path(), "/boo/foo/bar"); + /// # let old_index = index; + /// # let index = old_index.clone(); + /// let mapped = index.map_base(|base| format!("{}{}", "/boo", base)).unwrap(); + /// assert_eq!(mapped.uri.base(), "/boo/"); + /// assert_eq!(mapped.uri.unmounted().path(), "/foo/bar"); + /// assert_eq!(mapped.uri.path(), "/boo/foo/bar"); + /// + /// // Note that this produces different `base` results than `rebase`! + /// # let index = old_index.clone(); + /// let rebased = index.rebase(uri!("/boo")); + /// assert_eq!(rebased.uri.base(), "/boo"); + /// assert_eq!(rebased.uri.unmounted().path(), "/foo/bar"); + /// assert_eq!(rebased.uri.path(), "/boo/foo/bar"); /// ``` pub fn map_base<'a, F>(mut self, mapper: F) -> Result> where F: FnOnce(uri::Origin<'a>) -> String @@ -298,11 +346,7 @@ impl fmt::Display for Route { } write!(f, "{} ", Paint::green(&self.method))?; - if self.uri.base() != "/" { - write!(f, "{}", Paint::blue(self.uri.base()).underline())?; - } - - write!(f, "{}", Paint::blue(&self.uri.unmounted()))?; + self.uri.color_fmt(f)?; if self.rank > 1 { write!(f, " [{}]", Paint::default(&self.rank).bold())?; diff --git a/core/lib/src/route/uri.rs b/core/lib/src/route/uri.rs index 9fc073b0b8..9ef156bafa 100644 --- a/core/lib/src/route/uri.rs +++ b/core/lib/src/route/uri.rs @@ -98,7 +98,7 @@ impl<'a> RouteUri<'a> { /// Panics if `base` or `uri` cannot be parsed as `Origin`s. #[track_caller] pub(crate) fn new(base: &str, uri: &str) -> RouteUri<'static> { - Self::try_new(base, uri).expect("Expected valid URIs") + Self::try_new(base, uri).expect("expected valid route URIs") } /// Creates a new `RouteUri` from a `base` mount point and a route `uri`. @@ -110,7 +110,7 @@ impl<'a> RouteUri<'a> { pub fn try_new(base: &str, uri: &str) -> Result> { let mut base = Origin::parse(base) .map_err(|e| e.into_owned())? - .into_normalized_nontrailing() + .into_normalized() .into_owned(); base.clear_query(); @@ -120,16 +120,17 @@ impl<'a> RouteUri<'a> { .into_normalized() .into_owned(); - let compiled_uri = match base.path().as_str() { - "/" => origin.to_string(), - base => match (origin.path().as_str(), origin.query()) { - ("/", None) => base.to_string(), - ("/", Some(q)) => format!("{}?{}", base, q), - _ => format!("{}{}", base, origin), + // Distinguish for routes `/` with bases of `/foo/` and `/foo`. The + // latter base, without a trailing slash, should combine as `/foo`. + let route_uri = match origin.path().as_str() { + "/" if !base.has_trailing_slash() => match origin.query() { + Some(query) => format!("{}?{}", base, query), + None => base.to_string(), } + _ => format!("{}{}", base, origin), }; - let uri = Origin::parse_route(&compiled_uri) + let uri = Origin::parse_route(&route_uri) .map_err(|e| e.into_owned())? .into_normalized() .into_owned(); @@ -171,12 +172,16 @@ impl<'a> RouteUri<'a> { /// use rocket::Route; /// use rocket::http::Method; /// # use rocket::route::dummy_handler as handler; + /// # use rocket::uri; /// /// let route = Route::new(Method::Get, "/foo/bar?a=1", handler); /// assert_eq!(route.uri.base(), "/"); /// - /// let route = route.map_base(|base| format!("{}{}", "/boo", base)).unwrap(); + /// let route = route.rebase(uri!("/boo")); /// assert_eq!(route.uri.base(), "/boo"); + /// + /// let route = route.rebase(uri!("/foo")); + /// assert_eq!(route.uri.base(), "/foo/boo"); /// ``` #[inline(always)] pub fn base(&self) -> Path<'_> { @@ -191,9 +196,10 @@ impl<'a> RouteUri<'a> { /// use rocket::Route; /// use rocket::http::Method; /// # use rocket::route::dummy_handler as handler; + /// # use rocket::uri; /// /// let route = Route::new(Method::Get, "/foo/bar?a=1", handler); - /// let route = route.map_base(|base| format!("{}{}", "/boo", base)).unwrap(); + /// let route = route.rebase(uri!("/boo")); /// /// assert_eq!(route.uri, "/boo/foo/bar?a=1"); /// assert_eq!(route.uri.base(), "/boo"); @@ -232,6 +238,23 @@ impl<'a> RouteUri<'a> { // We subtract `3` because `raw_path` is never `0`: 0b0100 = 4 - 3 = 1. -((raw_weight as isize) - 3) } + + pub(crate) fn color_fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + use yansi::Paint; + + let (path, base, unmounted) = (self.uri.path(), self.base(), self.unmounted().path()); + let unmounted_part = path.strip_prefix(base.as_str()) + .map(|raw| raw.as_str()) + .unwrap_or(unmounted.as_str()); + + write!(f, "{}", Paint::blue(self.base()).underline())?; + write!(f, "{}", Paint::blue(unmounted_part))?; + if let Some(q) = self.unmounted().query() { + write!(f, "?{}", Paint::green(q))?; + } + + Ok(()) + } } impl Metadata { @@ -289,7 +312,7 @@ impl<'a> std::ops::Deref for RouteUri<'a> { impl fmt::Display for RouteUri<'_> { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - self.inner().fmt(f) + self.uri.fmt(f) } } @@ -304,3 +327,46 @@ impl PartialEq for RouteUri<'_> { impl PartialEq<&str> for RouteUri<'_> { fn eq(&self, other: &&str) -> bool { self.inner() == *other } } + +#[cfg(test)] +mod tests { + macro_rules! assert_uri_equality { + ($base:expr, $path:expr => $ebase:expr, $epath:expr, $efull:expr) => { + let uri = super::RouteUri::new($base, $path); + assert_eq!(uri, $efull, "complete URI mismatch. expected {}, got {}", $efull, uri); + assert_eq!(uri.base(), $ebase, "expected base {}, got {}", $ebase, uri.base()); + assert_eq!(uri.unmounted(), $epath, "expected unmounted {}, got {}", $epath, + uri.unmounted()); + }; + } + + #[test] + fn test_route_uri_composition() { + assert_uri_equality!("/", "/" => "/", "/", "/"); + assert_uri_equality!("/", "/foo" => "/", "/foo", "/foo"); + assert_uri_equality!("/", "/foo/bar" => "/", "/foo/bar", "/foo/bar"); + assert_uri_equality!("/", "/foo/" => "/", "/foo/", "/foo/"); + assert_uri_equality!("/", "/foo/bar/" => "/", "/foo/bar/", "/foo/bar/"); + + assert_uri_equality!("/foo", "/" => "/foo", "/", "/foo"); + assert_uri_equality!("/foo", "/bar" => "/foo", "/bar", "/foo/bar"); + assert_uri_equality!("/foo", "/bar/" => "/foo", "/bar/", "/foo/bar/"); + assert_uri_equality!("/foo", "/?baz" => "/foo", "/?baz", "/foo?baz"); + assert_uri_equality!("/foo", "/bar?baz" => "/foo", "/bar?baz", "/foo/bar?baz"); + assert_uri_equality!("/foo", "/bar/?baz" => "/foo", "/bar/?baz", "/foo/bar/?baz"); + + assert_uri_equality!("/foo/", "/" => "/foo/", "/", "/foo/"); + assert_uri_equality!("/foo/", "/bar" => "/foo/", "/bar", "/foo/bar"); + assert_uri_equality!("/foo/", "/bar/" => "/foo/", "/bar/", "/foo/bar/"); + assert_uri_equality!("/foo/", "/?baz" => "/foo/", "/?baz", "/foo/?baz"); + assert_uri_equality!("/foo/", "/bar?baz" => "/foo/", "/bar?baz", "/foo/bar?baz"); + assert_uri_equality!("/foo/", "/bar/?baz" => "/foo/", "/bar/?baz", "/foo/bar/?baz"); + + assert_uri_equality!("/foo?baz", "/" => "/foo", "/", "/foo"); + assert_uri_equality!("/foo?baz", "/bar" => "/foo", "/bar", "/foo/bar"); + assert_uri_equality!("/foo?baz", "/bar/" => "/foo", "/bar/", "/foo/bar/"); + assert_uri_equality!("/foo/?baz", "/" => "/foo/", "/", "/foo/"); + assert_uri_equality!("/foo/?baz", "/bar" => "/foo/", "/bar", "/foo/bar"); + assert_uri_equality!("/foo/?baz", "/bar/" => "/foo/", "/bar/", "/foo/bar/"); + } +} From c1ead84ec503b2be118676f6eb8b421bd5b37e59 Mon Sep 17 00:00:00 2001 From: Sergio Benitez Date: Thu, 4 May 2023 14:44:38 -0700 Subject: [PATCH 137/166] Allow 'clippy::style' warnings in attr codegen. Furthermore, properly forward 'deprecated' items in catcher codegen. --- core/codegen/src/attribute/catch/mod.rs | 6 ++++-- core/codegen/src/attribute/route/mod.rs | 2 +- core/codegen/src/derive/uri_display.rs | 1 - 3 files changed, 5 insertions(+), 4 deletions(-) diff --git a/core/codegen/src/attribute/catch/mod.rs b/core/codegen/src/attribute/catch/mod.rs index bc22f15a20..09528c71e2 100644 --- a/core/codegen/src/attribute/catch/mod.rs +++ b/core/codegen/src/attribute/catch/mod.rs @@ -20,6 +20,7 @@ pub fn _catch( let user_catcher_fn_name = &catch.function.sig.ident; let vis = &catch.function.vis; let status_code = Optional(catch.status.map(|s| s.code)); + let deprecated = catch.function.attrs.iter().find(|a| a.path().is_ident("deprecated")); // Determine the number of parameters that will be passed in. if catch.function.sig.inputs.len() > 2 { @@ -57,11 +58,12 @@ pub fn _catch( #user_catcher_fn #[doc(hidden)] - #[allow(non_camel_case_types)] + #[allow(nonstandard_style)] /// Rocket code generated proxy structure. - #vis struct #user_catcher_fn_name { } + #deprecated #vis struct #user_catcher_fn_name { } /// Rocket code generated proxy static conversion implementations. + #[allow(nonstandard_style, deprecated, clippy::style)] impl #user_catcher_fn_name { fn into_info(self) -> #_catcher::StaticInfo { fn monomorphized_function<'__r>( diff --git a/core/codegen/src/attribute/route/mod.rs b/core/codegen/src/attribute/route/mod.rs index 5de3130643..8003dda430 100644 --- a/core/codegen/src/attribute/route/mod.rs +++ b/core/codegen/src/attribute/route/mod.rs @@ -343,7 +343,7 @@ fn codegen_route(route: Route) -> Result { #deprecated #vis struct #handler_fn_name { } /// Rocket code generated proxy static conversion implementations. - #[allow(nonstandard_style, deprecated)] + #[allow(nonstandard_style, deprecated, clippy::style)] impl #handler_fn_name { fn into_info(self) -> #_route::StaticInfo { fn monomorphized_function<'__r>( diff --git a/core/codegen/src/derive/uri_display.rs b/core/codegen/src/derive/uri_display.rs index 3afbfd110c..1b5f2bf134 100644 --- a/core/codegen/src/derive/uri_display.rs +++ b/core/codegen/src/derive/uri_display.rs @@ -110,7 +110,6 @@ pub fn derive_uri_display_query(input: proc_macro::TokenStream) -> TokenStream { ts } -#[allow(non_snake_case)] pub fn derive_uri_display_path(input: proc_macro::TokenStream) -> TokenStream { let uri_display = DeriveGenerator::build_for(input.clone(), quote!(impl #P_URI_DISPLAY)) .support(Support::TupleStruct | Support::Type | Support::Lifetime) From 541952bc58c8eecdca6c5113c249efa265f10c21 Mon Sep 17 00:00:00 2001 From: Sergio Benitez Date: Thu, 4 May 2023 17:30:23 -0700 Subject: [PATCH 138/166] Paint route URI query parts yellow. --- core/lib/src/route/uri.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/lib/src/route/uri.rs b/core/lib/src/route/uri.rs index 9ef156bafa..e440270faa 100644 --- a/core/lib/src/route/uri.rs +++ b/core/lib/src/route/uri.rs @@ -250,7 +250,7 @@ impl<'a> RouteUri<'a> { write!(f, "{}", Paint::blue(self.base()).underline())?; write!(f, "{}", Paint::blue(unmounted_part))?; if let Some(q) = self.unmounted().query() { - write!(f, "?{}", Paint::green(q))?; + write!(f, "{}{}", Paint::yellow("?"), Paint::yellow(q))?; } Ok(()) From d24b5d4d6de1460003feda5f39b339942d60e67d Mon Sep 17 00:00:00 2001 From: Sergio Benitez Date: Thu, 4 May 2023 17:30:37 -0700 Subject: [PATCH 139/166] Handle more cases in 'AdHoc::normalizer()'. The compatibility normalizer previously missed or was overly egregious in several cases. This commit resolves those issue. In particular: * Only request URIs that would not match any route are normalized. * Synthetic routes are added to the igniting `Rocket` so that requests with URIs of the form `/foo` match routes with URIs of the form `/foo/`, as they did prior to the trailing slash overhaul. Tests are added for all of these cases. --- core/lib/src/fairing/ad_hoc.rs | 94 +++++++++++++++++++++++--- core/lib/src/route/route.rs | 4 ++ core/lib/tests/adhoc-uri-normalizer.rs | 79 ++++++++++++++++++++++ 3 files changed, 168 insertions(+), 9 deletions(-) create mode 100644 core/lib/tests/adhoc-uri-normalizer.rs diff --git a/core/lib/src/fairing/ad_hoc.rs b/core/lib/src/fairing/ad_hoc.rs index 4a4d75eef7..4da164b3ae 100644 --- a/core/lib/src/fairing/ad_hoc.rs +++ b/core/lib/src/fairing/ad_hoc.rs @@ -1,6 +1,7 @@ use futures::future::{Future, BoxFuture, FutureExt}; use parking_lot::Mutex; +use crate::route::RouteUri; use crate::{Rocket, Request, Response, Data, Build, Orbit}; use crate::fairing::{Fairing, Kind, Info, Result}; @@ -309,16 +310,91 @@ impl AdHoc { /// let response = client.get("/bar").dispatch(); /// assert_eq!(response.into_string().unwrap(), "bar"); /// ``` - #[deprecated(since = "0.6", note = "routing from Rocket v0.5 is now standard")] - pub fn uri_normalizer() -> AdHoc { - AdHoc::on_request("URI Normalizer", |req, _| Box::pin(async move { - if !req.uri().is_normalized_nontrailing() { - let normal = req.uri().clone().into_normalized_nontrailing(); - warn!("Incoming request URI was normalized for compatibility."); - info_!("{} -> {}", req.uri(), normal); - req.set_uri(normal); + // #[deprecated(since = "0.6", note = "routing from Rocket v0.5 is now standard")] + pub fn uri_normalizer() -> impl Fairing { + #[derive(Default)] + struct Normalizer { + routes: state::Storage>, + } + + impl Normalizer { + fn routes(&self, rocket: &Rocket) -> &[crate::Route] { + self.routes.get_or_set(|| { + rocket.routes() + .filter(|r| r.uri.has_trailing_slash() || r.uri.metadata.dynamic_trail) + .cloned() + .collect() + }) + } + } + + #[crate::async_trait] + impl Fairing for Normalizer { + fn info(&self) -> Info { + Info { name: "URI Normalizer", kind: Kind::Ignite | Kind::Liftoff | Kind::Request } + } + + async fn on_ignite(&self, rocket: Rocket) -> Result { + // We want a route like `/foo/` to match a request for + // `/foo` as it would have before. While we could check if a + // route is mounted that would cause this match and then rewrite + // the request URI as `/foo/`, doing so is expensive and + // potentially incorrect due to request guards and ranking. + // + // Instead, we generate a new route with URI `/foo` with the + // same rank and handler as the `/foo/` route and mount + // it to this instance of `rocket`. This preserves the previous + // matching while still checking request guards. + let normalized_trailing = rocket.routes() + .filter(|r| r.uri.metadata.dynamic_trail) + .filter(|r| r.uri.path().segments().num() > 1) + .filter_map(|route| { + let path = route.uri.unmounted().path(); + let new_path = path.as_str() + .rsplit_once('/') + .map(|(prefix, _)| prefix) + .unwrap_or(path.as_str()); + + let base = route.uri.base().as_str(); + let uri = match route.uri.unmounted().query() { + Some(q) => format!("{}?{}", new_path, q), + None => new_path.to_string() + }; + + let mut route = route.clone(); + route.uri = RouteUri::try_new(base, &uri).expect("valid => valid"); + route.name = route.name.map(|r| format!("{} [normalized]", r).into()); + Some(route) + }) + .collect::>(); + + Ok(rocket.mount("/", normalized_trailing)) } - })) + + async fn on_liftoff(&self, rocket: &Rocket) { + let _ = self.routes(rocket); + } + + async fn on_request(&self, req: &mut Request<'_>, _: &mut Data<'_>) { + // If the URI has no trailing slash, it routes as before. + if req.uri().is_normalized_nontrailing() { + return + } + + // Otherwise, check if there's a route that matches the request + // with a trailing slash. If there is, leave the request alone. + // This allows incremental compatibility updates. Otherwise, + // rewrite the request URI to remove the `/`. + if !self.routes(req.rocket()).iter().any(|r| r.matches(req)) { + let normal = req.uri().clone().into_normalized_nontrailing(); + warn!("Incoming request URI was normalized for compatibility."); + info_!("{} -> {}", req.uri(), normal); + req.set_uri(normal); + } + } + } + + Normalizer::default() } } diff --git a/core/lib/src/route/route.rs b/core/lib/src/route/route.rs index 131d2a302a..44ef2790c0 100644 --- a/core/lib/src/route/route.rs +++ b/core/lib/src/route/route.rs @@ -278,6 +278,10 @@ impl Route { /// let rebased = rebased.rebase(uri!("/base")); /// assert_eq!(rebased.uri.base(), "/base/boo"); /// + /// // Rebasing to `/` does nothing. + /// let rebased = rebased.rebase(uri!("/")); + /// assert_eq!(rebased.uri.base(), "/base/boo"); + /// /// // Note that trailing slashes are preserved: /// let index = Route::new(Method::Get, "/foo", handler); /// let rebased = index.rebase(uri!("/boo/")); diff --git a/core/lib/tests/adhoc-uri-normalizer.rs b/core/lib/tests/adhoc-uri-normalizer.rs new file mode 100644 index 0000000000..a3774f046a --- /dev/null +++ b/core/lib/tests/adhoc-uri-normalizer.rs @@ -0,0 +1,79 @@ +#[macro_use] extern crate rocket; + +use std::path::PathBuf; + +use rocket::local::blocking::Client; +use rocket::fairing::AdHoc; + +#[get("/foo")] +fn foo() -> &'static str { "foo" } + +#[get("/bar")] +fn not_bar() -> &'static str { "not_bar" } + +#[get("/bar/")] +fn bar() -> &'static str { "bar" } + +#[get("/foo/<_>/<_baz..>")] +fn baz(_baz: PathBuf) -> &'static str { "baz" } + +#[get("/doggy/<_>/<_baz..>?doggy")] +fn doggy(_baz: PathBuf) -> &'static str { "doggy" } + +#[test] +fn test_adhoc_normalizer_works_as_expected () { + let rocket = rocket::build() + .mount("/", routes![foo, bar, not_bar, baz, doggy]) + .mount("/base", routes![foo, bar, not_bar, baz, doggy]) + .attach(AdHoc::uri_normalizer()); + + let client = Client::debug(rocket).unwrap(); + + let response = client.get("/foo/").dispatch(); + assert_eq!(response.into_string().unwrap(), "foo"); + + let response = client.get("/foo").dispatch(); + assert_eq!(response.into_string().unwrap(), "foo"); + + let response = client.get("/bar/").dispatch(); + assert_eq!(response.into_string().unwrap(), "bar"); + + let response = client.get("/bar").dispatch(); + assert_eq!(response.into_string().unwrap(), "not_bar"); + + let response = client.get("/foo/bar").dispatch(); + assert_eq!(response.into_string().unwrap(), "baz"); + + let response = client.get("/doggy/bar?doggy").dispatch(); + assert_eq!(response.into_string().unwrap(), "doggy"); + + let response = client.get("/foo/bar/").dispatch(); + assert_eq!(response.into_string().unwrap(), "baz"); + + let response = client.get("/foo/bar/baz").dispatch(); + assert_eq!(response.into_string().unwrap(), "baz"); + + let response = client.get("/base/foo/").dispatch(); + assert_eq!(response.into_string().unwrap(), "foo"); + + let response = client.get("/base/foo").dispatch(); + assert_eq!(response.into_string().unwrap(), "foo"); + + let response = client.get("/base/bar/").dispatch(); + assert_eq!(response.into_string().unwrap(), "bar"); + + let response = client.get("/base/bar").dispatch(); + assert_eq!(response.into_string().unwrap(), "not_bar"); + + let response = client.get("/base/foo/bar").dispatch(); + assert_eq!(response.into_string().unwrap(), "baz"); + + let response = client.get("/doggy/foo/bar?doggy").dispatch(); + assert_eq!(response.into_string().unwrap(), "doggy"); + + let response = client.get("/base/foo/bar/").dispatch(); + assert_eq!(response.into_string().unwrap(), "baz"); + + let response = client.get("/base/foo/bar/baz").dispatch(); + assert_eq!(response.into_string().unwrap(), "baz"); +} From db535812b0b993d1529b571fe5253ab8c2e98904 Mon Sep 17 00:00:00 2001 From: Sergio Benitez Date: Thu, 4 May 2023 17:44:59 -0700 Subject: [PATCH 140/166] Factor out 'Catcher' rank computation. --- core/lib/src/catcher/catcher.rs | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/core/lib/src/catcher/catcher.rs b/core/lib/src/catcher/catcher.rs index 9cb4d6081f..9068ea19cf 100644 --- a/core/lib/src/catcher/catcher.rs +++ b/core/lib/src/catcher/catcher.rs @@ -129,6 +129,12 @@ pub struct Catcher { pub(crate) rank: isize, } +// The rank is computed as -(number of nonempty segments in base) => catchers +// with more nonempty segments have lower ranks => higher precedence. +fn rank(base: Path<'_>) -> isize { + -1 * (base.segments().filter(|s| !s.is_empty()).count() as isize) +} + impl Catcher { /// Creates a catcher for the given `status`, or a default catcher if /// `status` is `None`, using the given error handler. This should only be @@ -178,7 +184,7 @@ impl Catcher { name: None, base: uri::Origin::ROOT, handler: Box::new(handler), - rank: 0, + rank: rank(uri::Origin::ROOT.path()), code } } @@ -251,7 +257,7 @@ impl Catcher { .into_normalized_nontrailing() }; - self.rank = -1 * (self.base().segments().filter(|s| !s.is_empty()).count() as isize); + self.rank = rank(self.base()); self } @@ -296,7 +302,7 @@ impl Catcher { let new_base = uri::Origin::parse_owned(mapper(self.base))?; self.base = new_base.into_normalized_nontrailing(); self.base.clear_query(); - self.rank = -1 * (self.base().segments().filter(|s| !s.is_empty()).count() as isize); + self.rank = rank(self.base()); Ok(self) } } From 615e70fdad61354e9148977f6650f599a017944c Mon Sep 17 00:00:00 2001 From: Sergio Benitez Date: Thu, 4 May 2023 18:27:44 -0700 Subject: [PATCH 141/166] Log config provenance in debug. This helps identify configuration issues by printing the source of every configuration value used by Rocket. --- core/lib/src/config/config.rs | 62 ++++++++++++++++++++++++++++------- 1 file changed, 50 insertions(+), 12 deletions(-) diff --git a/core/lib/src/config/config.rs b/core/lib/src/config/config.rs index 3b721c1e32..6fbd6e7e81 100644 --- a/core/lib/src/config/config.rs +++ b/core/lib/src/config/config.rs @@ -6,6 +6,7 @@ use figment::value::{Map, Dict, magic::RelativePathBuf}; use serde::{Deserialize, Serialize}; use yansi::Paint; +use crate::log::PaintExt; use crate::config::{LogLevel, Shutdown, Ident}; use crate::request::{self, Request, FromRequest}; use crate::http::uncased::Uncased; @@ -376,13 +377,31 @@ impl Config { }) } - pub(crate) fn pretty_print(&self, figment: &Figment) { - use crate::log::PaintExt; + #[inline] + pub(crate) fn trace_print(&self, figment: &Figment) { + if self.log_level != LogLevel::Debug { + return; + } + trace!("-- configuration trace information --"); + for param in Self::PARAMETERS { + if let Some(meta) = figment.find_metadata(param) { + let (param, name) = (Paint::blue(param), Paint::white(&meta.name)); + if let Some(ref source) = meta.source { + trace_!("{:?} parameter source: {} ({})", param, name, source); + } else { + trace_!("{:?} parameter source: {}", param, name); + } + } + } + } + + pub(crate) fn pretty_print(&self, figment: &Figment) { fn bold(val: T) -> Paint { Paint::default(val).bold() } + self.trace_print(figment); launch_meta!("{}Configured for {}.", Paint::emoji("🔧 "), self.profile); launch_meta_!("address: {}", bold(&self.address)); launch_meta_!("port: {}", bold(&self.port)); @@ -410,15 +429,6 @@ impl Config { (false, _) => launch_meta_!("tls: {}", bold("disabled")), } - #[cfg(feature = "secrets")] { - launch_meta_!("secret key: {}", bold(&self.secret_key)); - if !self.secret_key.is_provided() { - warn!("secrets enabled without a stable `secret_key`"); - launch_meta_!("disable `secrets` feature or configure a `secret_key`"); - launch_meta_!("this becomes an {} in non-debug profiles", Paint::red("error")); - } - } - launch_meta_!("shutdown: {}", bold(&self.shutdown)); launch_meta_!("log level: {}", bold(self.log_level)); launch_meta_!("cli colors: {}", bold(&self.cli_colors)); @@ -451,6 +461,15 @@ impl Config { } } } + + #[cfg(feature = "secrets")] { + launch_meta_!("secret key: {}", bold(&self.secret_key)); + if !self.secret_key.is_provided() { + warn!("secrets enabled without a stable `secret_key`"); + launch_meta_!("disable `secrets` feature or configure a `secret_key`"); + launch_meta_!("this becomes an {} in non-debug profiles", Paint::red("error")); + } + } } } @@ -493,6 +512,12 @@ impl Config { /// The stringy parameter name for setting/extracting [`Config::keep_alive`]. pub const KEEP_ALIVE: &'static str = "keep_alive"; + /// The stringy parameter name for setting/extracting [`Config::ident`]. + pub const IDENT: &'static str = "ident"; + + /// The stringy parameter name for setting/extracting [`Config::ip_header`]. + pub const IP_HEADER: &'static str = "ip_header"; + /// The stringy parameter name for setting/extracting [`Config::limits`]. pub const LIMITS: &'static str = "limits"; @@ -513,11 +538,24 @@ impl Config { /// The stringy parameter name for setting/extracting [`Config::cli_colors`]. pub const CLI_COLORS: &'static str = "cli_colors"; + + /// An array of all of the stringy parameter names. + pub const PARAMETERS: &'static [&'static str] = &[ + Self::ADDRESS, Self::PORT, Self::WORKERS, Self::MAX_BLOCKING, + Self::KEEP_ALIVE, Self::IDENT, Self::IP_HEADER, Self::LIMITS, Self::TLS, + Self::SECRET_KEY, Self::TEMP_DIR, Self::LOG_LEVEL, Self::SHUTDOWN, + Self::CLI_COLORS, + ]; } impl Provider for Config { + #[track_caller] fn metadata(&self) -> Metadata { - Metadata::named("Rocket Config") + if self == &Config::default() { + Metadata::named("rocket::Config::default()") + } else { + Metadata::named("rocket::Config") + } } #[track_caller] From 311a82e3d7bae2a0ddfcb71eb1bebb5c5e3051da Mon Sep 17 00:00:00 2001 From: Sergio Benitez Date: Fri, 5 May 2023 11:41:10 -0700 Subject: [PATCH 142/166] Add 'Error::pretty_print()'. --- core/lib/src/error.rs | 120 ++++++++++++++++++++++++------------------ 1 file changed, 70 insertions(+), 50 deletions(-) diff --git a/core/lib/src/error.rs b/core/lib/src/error.rs index 314cf2c8b2..2dc361cccc 100644 --- a/core/lib/src/error.rs +++ b/core/lib/src/error.rs @@ -153,60 +153,34 @@ impl Error { self.mark_handled(); &self.kind } -} - -impl std::error::Error for Error { } - -impl fmt::Display for ErrorKind { - #[inline] - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match self { - ErrorKind::Bind(e) => write!(f, "binding failed: {}", e), - ErrorKind::Io(e) => write!(f, "I/O error: {}", e), - ErrorKind::Collisions(_) => "collisions detected".fmt(f), - ErrorKind::FailedFairings(_) => "launch fairing(s) failed".fmt(f), - ErrorKind::InsecureSecretKey(_) => "insecure secret key config".fmt(f), - ErrorKind::Config(_) => "failed to extract configuration".fmt(f), - ErrorKind::SentinelAborts(_) => "sentinel(s) aborted".fmt(f), - ErrorKind::Shutdown(_, Some(e)) => write!(f, "shutdown failed: {}", e), - ErrorKind::Shutdown(_, None) => "shutdown failed".fmt(f), - } - } -} - -impl fmt::Debug for Error { - #[inline] - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - self.mark_handled(); - self.kind().fmt(f) - } -} -impl fmt::Display for Error { - #[inline] - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + /// Prints the error with color (if enabled) and detail. Returns a string + /// that indicates the abort condition such as "aborting due to i/o error". + /// + /// This function is called on `Drop` to display the error message. By + /// contrast, the `Display` implementation prints a succinct version of the + /// error, without detail. + /// + /// ```rust + /// # let _ = async { + /// if let Err(error) = rocket::build().launch().await { + /// let abort = error.pretty_print(); + /// panic!("{}", abort); + /// } + /// # }; + /// ``` + pub fn pretty_print(&self) -> &'static str { self.mark_handled(); - write!(f, "{}", self.kind()) - } -} - -impl Drop for Error { - fn drop(&mut self) { - // Don't panic if the message has been seen. Don't double-panic. - if self.was_handled() || std::thread::panicking() { - return - } - match self.kind() { ErrorKind::Bind(ref e) => { error!("Rocket failed to bind network socket to given address/port."); info_!("{}", e); - panic!("aborting due to socket bind error"); + "aborting due to socket bind error" } ErrorKind::Io(ref e) => { error!("Rocket failed to launch due to an I/O error."); info_!("{}", e); - panic!("aborting due to i/o error"); + "aborting due to i/o error" } ErrorKind::Collisions(ref collisions) => { fn log_collisions(kind: &str, collisions: &[(T, T)]) { @@ -222,7 +196,7 @@ impl Drop for Error { log_collisions("catcher", &collisions.catchers); info_!("Note: Route collisions can usually be resolved by ranking routes."); - panic!("routing collisions detected"); + "aborting due to detected routing collisions" } ErrorKind::FailedFairings(ref failures) => { error!("Rocket failed to launch due to failing fairings:"); @@ -230,17 +204,17 @@ impl Drop for Error { info_!("{}", fairing.name); } - panic!("aborting due to fairing failure(s)"); + "aborting due to fairing failure(s)" } ErrorKind::InsecureSecretKey(profile) => { error!("secrets enabled in non-debug without `secret_key`"); info_!("selected profile: {}", Paint::default(profile).bold()); info_!("disable `secrets` feature or configure a `secret_key`"); - panic!("aborting due to insecure configuration") + "aborting due to insecure configuration" } ErrorKind::Config(error) => { crate::config::pretty_print_error(error.clone()); - panic!("aborting due to invalid configuration") + "aborting due to invalid configuration" } ErrorKind::SentinelAborts(ref failures) => { error!("Rocket failed to launch due to aborting sentinels:"); @@ -250,7 +224,7 @@ impl Drop for Error { info_!("{} ({}:{}:{})", name, file, line, col); } - panic!("aborting due to sentinel-triggered abort(s)"); + "aborting due to sentinel-triggered abort(s)" } ErrorKind::Shutdown(_, error) => { error!("Rocket failed to shutdown gracefully."); @@ -258,12 +232,58 @@ impl Drop for Error { info_!("{}", e); } - panic!("aborting due to failed shutdown"); + "aborting due to failed shutdown" } } } } +impl std::error::Error for Error { } + +impl fmt::Display for ErrorKind { + #[inline] + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + ErrorKind::Bind(e) => write!(f, "binding failed: {}", e), + ErrorKind::Io(e) => write!(f, "I/O error: {}", e), + ErrorKind::Collisions(_) => "collisions detected".fmt(f), + ErrorKind::FailedFairings(_) => "launch fairing(s) failed".fmt(f), + ErrorKind::InsecureSecretKey(_) => "insecure secret key config".fmt(f), + ErrorKind::Config(_) => "failed to extract configuration".fmt(f), + ErrorKind::SentinelAborts(_) => "sentinel(s) aborted".fmt(f), + ErrorKind::Shutdown(_, Some(e)) => write!(f, "shutdown failed: {}", e), + ErrorKind::Shutdown(_, None) => "shutdown failed".fmt(f), + } + } +} + +impl fmt::Debug for Error { + #[inline] + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + self.mark_handled(); + self.kind().fmt(f) + } +} + +impl fmt::Display for Error { + #[inline] + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + self.mark_handled(); + write!(f, "{}", self.kind()) + } +} + +impl Drop for Error { + fn drop(&mut self) { + // Don't panic if the message has been seen. Don't double-panic. + if self.was_handled() || std::thread::panicking() { + return + } + + panic!("{}", self.pretty_print()); + } +} + impl fmt::Debug for Empty { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.write_str("empty parameter") From f1f533c1e5b0df5b44877d7cca39fb0f596a21b6 Mon Sep 17 00:00:00 2001 From: Sergio Benitez Date: Fri, 5 May 2023 11:41:44 -0700 Subject: [PATCH 143/166] Improve and fix panic in 'AdHoc::normalizer()'. The normalizer now handles more cases. --- core/lib/src/fairing/ad_hoc.rs | 7 ++- core/lib/tests/adhoc-uri-normalizer.rs | 87 +++++++++++--------------- 2 files changed, 41 insertions(+), 53 deletions(-) diff --git a/core/lib/src/fairing/ad_hoc.rs b/core/lib/src/fairing/ad_hoc.rs index 4da164b3ae..232e2f2a13 100644 --- a/core/lib/src/fairing/ad_hoc.rs +++ b/core/lib/src/fairing/ad_hoc.rs @@ -321,7 +321,7 @@ impl AdHoc { fn routes(&self, rocket: &Rocket) -> &[crate::Route] { self.routes.get_or_set(|| { rocket.routes() - .filter(|r| r.uri.has_trailing_slash() || r.uri.metadata.dynamic_trail) + .filter(|r| r.uri.has_trailing_slash()) .cloned() .collect() }) @@ -353,7 +353,8 @@ impl AdHoc { let new_path = path.as_str() .rsplit_once('/') .map(|(prefix, _)| prefix) - .unwrap_or(path.as_str()); + .filter(|path| !path.is_empty()) + .unwrap_or("/"); let base = route.uri.base().as_str(); let uri = match route.uri.unmounted().query() { @@ -362,7 +363,7 @@ impl AdHoc { }; let mut route = route.clone(); - route.uri = RouteUri::try_new(base, &uri).expect("valid => valid"); + route.uri = RouteUri::try_new(base, &uri).ok()?; route.name = route.name.map(|r| format!("{} [normalized]", r).into()); Some(route) }) diff --git a/core/lib/tests/adhoc-uri-normalizer.rs b/core/lib/tests/adhoc-uri-normalizer.rs index a3774f046a..ecd4579357 100644 --- a/core/lib/tests/adhoc-uri-normalizer.rs +++ b/core/lib/tests/adhoc-uri-normalizer.rs @@ -20,60 +20,47 @@ fn baz(_baz: PathBuf) -> &'static str { "baz" } #[get("/doggy/<_>/<_baz..>?doggy")] fn doggy(_baz: PathBuf) -> &'static str { "doggy" } +#[get("/<_..>")] +fn rest() -> &'static str { "rest" } + +macro_rules! assert_response { + ($client:ident : $path:expr => $response:expr) => { + let response = $client.get($path).dispatch().into_string().unwrap(); + assert_eq!(response, $response, "\nGET {}: got {} but expected {}", + $path, response, $response); + }; +} + #[test] fn test_adhoc_normalizer_works_as_expected () { let rocket = rocket::build() .mount("/", routes![foo, bar, not_bar, baz, doggy]) - .mount("/base", routes![foo, bar, not_bar, baz, doggy]) + .mount("/base", routes![foo, bar, not_bar, baz, doggy, rest]) .attach(AdHoc::uri_normalizer()); - let client = Client::debug(rocket).unwrap(); - - let response = client.get("/foo/").dispatch(); - assert_eq!(response.into_string().unwrap(), "foo"); - - let response = client.get("/foo").dispatch(); - assert_eq!(response.into_string().unwrap(), "foo"); - - let response = client.get("/bar/").dispatch(); - assert_eq!(response.into_string().unwrap(), "bar"); - - let response = client.get("/bar").dispatch(); - assert_eq!(response.into_string().unwrap(), "not_bar"); - - let response = client.get("/foo/bar").dispatch(); - assert_eq!(response.into_string().unwrap(), "baz"); - - let response = client.get("/doggy/bar?doggy").dispatch(); - assert_eq!(response.into_string().unwrap(), "doggy"); - - let response = client.get("/foo/bar/").dispatch(); - assert_eq!(response.into_string().unwrap(), "baz"); - - let response = client.get("/foo/bar/baz").dispatch(); - assert_eq!(response.into_string().unwrap(), "baz"); - - let response = client.get("/base/foo/").dispatch(); - assert_eq!(response.into_string().unwrap(), "foo"); - - let response = client.get("/base/foo").dispatch(); - assert_eq!(response.into_string().unwrap(), "foo"); - - let response = client.get("/base/bar/").dispatch(); - assert_eq!(response.into_string().unwrap(), "bar"); - - let response = client.get("/base/bar").dispatch(); - assert_eq!(response.into_string().unwrap(), "not_bar"); - - let response = client.get("/base/foo/bar").dispatch(); - assert_eq!(response.into_string().unwrap(), "baz"); - - let response = client.get("/doggy/foo/bar?doggy").dispatch(); - assert_eq!(response.into_string().unwrap(), "doggy"); - - let response = client.get("/base/foo/bar/").dispatch(); - assert_eq!(response.into_string().unwrap(), "baz"); - - let response = client.get("/base/foo/bar/baz").dispatch(); - assert_eq!(response.into_string().unwrap(), "baz"); + let client = match Client::debug(rocket) { + Ok(client) => client, + Err(e) => { e.pretty_print(); panic!("failed to build client"); } + }; + + assert_response!(client: "/foo" => "foo"); + assert_response!(client: "/foo/" => "foo"); + assert_response!(client: "/bar/" => "bar"); + assert_response!(client: "/bar" => "not_bar"); + assert_response!(client: "/foo/bar" => "baz"); + assert_response!(client: "/doggy/bar?doggy" => "doggy"); + assert_response!(client: "/foo/bar/" => "baz"); + assert_response!(client: "/foo/bar/baz" => "baz"); + assert_response!(client: "/base/foo/" => "foo"); + assert_response!(client: "/base/foo" => "foo"); + assert_response!(client: "/base/bar/" => "bar"); + assert_response!(client: "/base/bar" => "not_bar"); + assert_response!(client: "/base/foo/bar" => "baz"); + assert_response!(client: "/doggy/foo/bar?doggy" => "doggy"); + assert_response!(client: "/base/foo/bar/" => "baz"); + assert_response!(client: "/base/foo/bar/baz" => "baz"); + + assert_response!(client: "/base/cat" => "rest"); + assert_response!(client: "/base/cat/" => "rest"); + assert_response!(client: "/base/cat/dog" => "rest"); } From 9f65977b99de084abaae6d6475367be00ce27435 Mon Sep 17 00:00:00 2001 From: Sergio Benitez Date: Mon, 15 May 2023 16:53:26 -0700 Subject: [PATCH 144/166] Remove closure borrow in 'FromForm' derive. The codegen for field validations previously included a closure that could potentially partially borrow a 'Copy' field of the context structure. To prevent this, 'let'-assign the field before the closure is created, and use the assignment inside of the closure. --- core/codegen/src/derive/form_field.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/core/codegen/src/derive/form_field.rs b/core/codegen/src/derive/form_field.rs index e1723f2301..737673d674 100644 --- a/core/codegen/src/derive/form_field.rs +++ b/core/codegen/src/derive/form_field.rs @@ -368,7 +368,8 @@ pub fn validators<'v>(field: Field<'v>) -> Result Ok(()), }; - __result.map_err(|__e| match #name_opt { + let __e_name = #name_opt; + __result.map_err(|__e| match __e_name { Some(__name) => __e.with_name(__name), None => __e }) From 29cd271f8ad3035f2aa3e396d59e0864f37cf149 Mon Sep 17 00:00:00 2001 From: Sergio Benitez Date: Thu, 18 May 2023 17:33:57 -0700 Subject: [PATCH 145/166] Only extract needed values in 'async_main'. Previously, `async_main` would extract a full `Config`. This mean that values like `address` were read and parsed even when they were unused. Should they exist and be malformed, a configuration error would needlessly arise. This commit fixes this by only extract values that are subsequently used. --- core/lib/src/config/config.rs | 13 ++++++++----- core/lib/src/config/mod.rs | 3 ++- core/lib/src/lib.rs | 21 ++++++++++++++++----- 3 files changed, 26 insertions(+), 11 deletions(-) diff --git a/core/lib/src/config/config.rs b/core/lib/src/config/config.rs index 6fbd6e7e81..993d7c00bf 100644 --- a/core/lib/src/config/config.rs +++ b/core/lib/src/config/config.rs @@ -306,10 +306,7 @@ impl Config { /// let config = Config::from(figment); /// ``` pub fn from(provider: T) -> Self { - Self::try_from(provider).unwrap_or_else(|e| { - pretty_print_error(e); - panic!("aborting due to configuration error(s)") - }) + Self::try_from(provider).unwrap_or_else(bail_with_config_error) } /// Returns `true` if TLS is enabled. @@ -588,12 +585,18 @@ impl<'r> FromRequest<'r> for &'r Config { } } +#[doc(hidden)] +pub fn bail_with_config_error(error: figment::Error) -> T { + pretty_print_error(error); + panic!("aborting due to configuration error(s)") +} + #[doc(hidden)] pub fn pretty_print_error(error: figment::Error) { use figment::error::{Kind, OneOf}; crate::log::init_default(); - error!("Rocket configuration extraction from provider failed."); + error!("Failed to extract valid configuration."); for e in error { fn w(v: T) -> Paint { Paint::white(v) } diff --git a/core/lib/src/config/mod.rs b/core/lib/src/config/mod.rs index 00a2613ca6..a811cf999a 100644 --- a/core/lib/src/config/mod.rs +++ b/core/lib/src/config/mod.rs @@ -123,7 +123,8 @@ mod tls; mod secret_key; #[doc(hidden)] -pub use config::pretty_print_error; +pub use config::{pretty_print_error, bail_with_config_error}; + pub use config::Config; pub use crate::log::LogLevel; pub use shutdown::Shutdown; diff --git a/core/lib/src/lib.rs b/core/lib/src/lib.rs index e1b8a26085..f3a11ddf33 100644 --- a/core/lib/src/lib.rs +++ b/core/lib/src/lib.rs @@ -261,11 +261,22 @@ pub fn async_test(fut: impl std::future::Future) -> R { /// WARNING: This is unstable! Do not use this method outside of Rocket! #[doc(hidden)] pub fn async_main(fut: impl std::future::Future + Send) -> R { - // FIXME: These config values won't reflect swaps of `Rocket` in attach - // fairings with different config values, or values from non-Rocket configs. - // See tokio-rs/tokio#3329 for a necessary solution in `tokio`. - let c = Config::from(Config::figment()); - async_run(fut, c.workers, c.max_blocking, c.shutdown.force, "rocket-worker-thread") + // FIXME: We need to run `fut` to get the user's `Figment` to properly set + // up the async env, but we need the async env to run `fut`. So we're stuck. + // Tokio doesn't let us take the state from one async env and migrate it to + // another, so we need to use one, making this impossible. + // + // So as a result, we only use values from Rocket's figment. These + // values won't reflect swaps of `Rocket` in attach fairings with different + // config values, or values from non-Rocket configs. See tokio-rs/tokio#3329 + // for a necessary resolution in `tokio`. + use config::bail_with_config_error as bail; + + let fig = Config::figment(); + let workers = fig.extract_inner(Config::WORKERS).unwrap_or_else(bail); + let max_blocking = fig.extract_inner(Config::MAX_BLOCKING).unwrap_or_else(bail); + let force = fig.focus(Config::SHUTDOWN).extract_inner("force").unwrap_or_else(bail); + async_run(fut, workers, max_blocking, force, "rocket-worker-thread") } /// Executes a `future` to completion on a new tokio-based Rocket async runtime. From b6b060f75c5e5b5a1c2b859d458ffaa3f844b29e Mon Sep 17 00:00:00 2001 From: Sergio Benitez Date: Thu, 18 May 2023 18:22:40 -0700 Subject: [PATCH 146/166] Document built-in data guards. --- core/lib/src/data/from_data.rs | 141 +++++++++++++++++++++++++++++++++ 1 file changed, 141 insertions(+) diff --git a/core/lib/src/data/from_data.rs b/core/lib/src/data/from_data.rs index 2bc1aa47a8..2b7465c351 100644 --- a/core/lib/src/data/from_data.rs +++ b/core/lib/src/data/from_data.rs @@ -52,6 +52,147 @@ impl<'r, S, E> IntoOutcome, Status)> for Result /// matches, Rocket will call the `FromData` implementation for the type `T`. /// The handler will only be called if the guard returns successfully. /// +/// ## Build-In Guards +/// +/// Rocket provides implementations for `FromData` for many types. Their +/// behavior is documented here: +/// +/// * `Data`: Returns the untouched `Data`. +/// +/// - **Fails:** Never. +/// +/// - **Succeeds:** Always. +/// +/// - **Forwards:** Never. +/// +/// * Strings: `Cow`, `&str`, `&RawStr`, `String` +/// +/// _Limited by the `string` [data limit]._ +/// +/// Reads the body data into a string via [`DataStream::into_string()`]. +/// +/// - **Fails:** If the body data is not valid UTF-8 or on I/O errors while +/// reading. The error type is [`io::Error`]. +/// +/// - **Succeeds:** If the body data _is_ valid UTF-8. If the limit is +/// exceeded, the string is truncated to the limit. +/// +/// - **Forwards:** Never. +/// +/// * Bytes: `&[u8]`, `Vec` +/// +/// _Limited by the `bytes` [data limit]._ +/// +/// Reads the body data into a byte vector via [`DataStream::into_bytes()`]. +/// +/// - **Fails:** On I/O errors while reading. The error type is +/// [`io::Error`]. +/// +/// - **Succeeds:** As long as no I/O error occurs. If the limit is +/// exceeded, the slice is truncated to the limit. +/// +/// - **Forwards:** Never. +/// +/// * [`TempFile`](crate::fs::TempFile) +/// +/// _Limited by the `file` and/or `file/$ext` [data limit]._ +/// +/// Streams the body data directly into a temporary file. The data is never +/// buffered in memory. +/// +/// - **Fails:** On I/O errors while reading data or creating the temporary +/// file. The error type is [`io::Error`]. +/// +/// - **Succeeds:** As long as no I/O error occurs and the temporary file +/// could be created. If the limit is exceeded, only data up to the limit is +/// read and subsequently written. +/// +/// - **Forwards:** Never. +/// +/// * Deserializers: [`Json`], [`MsgPack`] +/// +/// _Limited by the `json`, `msgpack` [data limit], respectively._ +/// +/// Reads up to the configured limit and deserializes the read data into `T` +/// using the respective format's parser. +/// +/// - **Fails:** On I/O errors while reading the data, or if the data fails +/// to parse as a `T` according to the deserializer. The error type for +/// `Json` is [`json::Error`](crate::serde::json::Error) and the error type +/// for `MsgPack` is [`msgpack::Error`](crate::serde::msgpack::Error). +/// +/// - **Succeeds:** As long as no I/O error occurs and the (limited) body +/// data was successfully deserialized as a `T`. +/// +/// - **Forwards:** Never. +/// +/// * Forms: [`Form`] +/// +/// _Limited by the `form` or `data-form` [data limit]._ +/// +/// Parses the incoming data stream into fields according to Rocket's [field +/// wire format], pushes each field to `T`'s [`FromForm`] [push parser], and +/// finalizes the form. Parsing is done on the stream without reading the +/// data into memory. If the request has as a [`ContentType::Form`], the +/// `form` limit is applied, otherwise if the request has a +/// [`ContentType::FormData`], the `data-form` limit is applied. +/// +/// - **Fails:** On I/O errors while reading the data, or if the data fails +/// to parse as a `T` according to its `FromForm` implementation. The errors +/// are collected into an [`Errors`](crate::form::Errors), the error type. +/// +/// - **Succeeds:** As long as no I/O error occurs and the (limited) body +/// data was successfully parsed as a `T`. +/// +/// - **Forwards:** If the request's `Content-Type` is neither +/// [`ContentType::Form`] nor [`ContentType::FormData`]. +/// +/// * `Option` +/// +/// Forwards to `T`'s `FromData` implementation, capturing the outcome. +/// +/// - **Fails:** Never. +/// +/// - **Succeeds:** Always. If `T`'s `FromData` implementation succeeds, the +/// parsed value is returned in `Some`. If its implementation forwards or +/// fails, `None` is returned. +/// +/// - **Forwards:** Never. +/// +/// * `Result` +/// +/// Forwards to `T`'s `FromData` implementation, capturing the outcome. +/// +/// - **Fails:** Never. +/// +/// - **Succeeds:** If `T`'s `FromData` implementation succeeds or fails. If +/// it succeeds, the value is returned in `Ok`. If it fails, the error value +/// is returned in `Err`. +/// +/// - **Forwards:** If `T`'s implementation forwards. +/// +/// * [`Capped`] +/// +/// Forwards to `T`'s `FromData` implementation, recording whether the data +/// was truncated (a.k.a. capped) due to `T`'s limit being exceeded. +/// +/// - **Fails:** If `T`'s implementation fails. +/// - **Succeeds:** If `T`'s implementation succeeds. +/// - **Forwards:** If `T`'s implementation forwards. +/// +/// [data limit]: crate::data::Limits#built-in-limits +/// [`DataStream::into_string()`]: crate::data::DataStream::into_string() +/// [`DataStream::into_bytes()`]: crate::data::DataStream::into_bytes() +/// [`io::Error`]: std::io::Error +/// [`Json`]: crate::serde::json::Json +/// [`MsgPack`]: crate::serde::msgpack::MsgPack +/// [`Form`]: crate::form::Form +/// [field wire format]: crate::form#field-wire-format +/// [`FromForm`]: crate::form::FromForm +/// [push parser]: crate::form::FromForm#push-parsing +/// [`ContentType::Form`]: crate::http::ContentType::Form +/// [`ContentType::FormData`]: crate::http::ContentType::FormData +/// /// ## Async Trait /// /// [`FromData`] is an _async_ trait. Implementations of `FromData` must be From f1a95ce1d9fc470542d6a0f08e59923630c3cf48 Mon Sep 17 00:00:00 2001 From: BlackDex Date: Fri, 19 May 2023 10:15:08 +0200 Subject: [PATCH 147/166] Update 'tungstenite' to 0.19 in 'rocket-ws'. --- contrib/ws/Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contrib/ws/Cargo.toml b/contrib/ws/Cargo.toml index b4a9481ffa..81a8af7b65 100644 --- a/contrib/ws/Cargo.toml +++ b/contrib/ws/Cargo.toml @@ -17,7 +17,7 @@ default = ["tungstenite"] tungstenite = ["tokio-tungstenite"] [dependencies] -tokio-tungstenite = { version = "0.18", optional = true } +tokio-tungstenite = { version = "0.19", optional = true } [dependencies.rocket] version = "=0.5.0-rc.3" From 1211525c98df46e56fd349745bd2b69c2e0d64e0 Mon Sep 17 00:00:00 2001 From: Sergio Benitez Date: Wed, 24 May 2023 09:55:28 -0700 Subject: [PATCH 148/166] Note required 'json' crate feature in guide. Co-authored-by: Manuel <2084639+tennox@users.noreply.github.com> --- site/guide/5-responses.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/site/guide/5-responses.md b/site/guide/5-responses.md index c5bf662bc4..eda5c6b10d 100644 --- a/site/guide/5-responses.md +++ b/site/guide/5-responses.md @@ -446,6 +446,8 @@ fn todo() -> Json { } ``` +! note: You must enable Rocket's `json` crate feature to use the [`Json`] type. + The `Json` type serializes the structure into JSON, sets the Content-Type to JSON, and emits the serialized data in a fixed-sized body. If serialization fails, a **500 - Internal Server Error** is returned. From b3abd7b4841961cc5dc4565be0bb652e2dfbb593 Mon Sep 17 00:00:00 2001 From: Sergio Benitez Date: Wed, 24 May 2023 09:57:04 -0700 Subject: [PATCH 149/166] Fix link to 'context!' in guide. --- site/guide/5-responses.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/site/guide/5-responses.md b/site/guide/5-responses.md index eda5c6b10d..2a3889500c 100644 --- a/site/guide/5-responses.md +++ b/site/guide/5-responses.md @@ -535,7 +535,7 @@ used. the name `"index"` in templates, i.e, `{% extends "index" %}` or `{% extends "base" %}` for `base.html.tera`. -[`context`]: @api/rocket_dyn_templates/macro.context.html +[`context!`]: @api/rocket_dyn_templates/macro.context.html ### Live Reloading From afb537415780d97bede0aa1361987599dd895eda Mon Sep 17 00:00:00 2001 From: Sergio Benitez Date: Wed, 24 May 2023 11:33:56 -0700 Subject: [PATCH 150/166] Warn if a task is spawned in a sync '#[launch]'. The warning is fairly conservative. Heuristics are used to determine if a call to `tokio::spawn()` occurs in the `#[launch]` function. Addresses #2547. --- core/codegen/src/attribute/entry/launch.rs | 53 ++++++++++++++++++++++ 1 file changed, 53 insertions(+) diff --git a/core/codegen/src/attribute/entry/launch.rs b/core/codegen/src/attribute/entry/launch.rs index f3081e3adb..90f5d6129d 100644 --- a/core/codegen/src/attribute/entry/launch.rs +++ b/core/codegen/src/attribute/entry/launch.rs @@ -9,6 +9,48 @@ use proc_macro2::{TokenStream, Span}; /// returned instance inside of an `rocket::async_main`. pub struct Launch; +/// Determines if `f` likely spawns an async task, returning the spawn call. +fn likely_spawns(f: &syn::ItemFn) -> Option<&syn::ExprCall> { + use syn::visit::{self, Visit}; + + struct SpawnFinder<'a>(Option<&'a syn::ExprCall>); + + impl<'ast> Visit<'ast> for SpawnFinder<'ast> { + fn visit_expr_call(&mut self, i: &'ast syn::ExprCall) { + if self.0.is_some() { + return; + } + + if let syn::Expr::Path(ref e) = *i.func { + let mut segments = e.path.segments.clone(); + if let Some(last) = segments.pop() { + if last.value().ident != "spawn" { + return visit::visit_expr_call(self, i); + } + + if let Some(prefix) = segments.pop() { + if prefix.value().ident == "tokio" { + self.0 = Some(i); + return; + } + } + + if let Some(syn::Expr::Async(_)) = i.args.first() { + self.0 = Some(i); + return; + } + } + }; + + visit::visit_expr_call(self, i); + } + } + + let mut v = SpawnFinder(None); + v.visit_item_fn(f); + v.0 +} + impl EntryAttr for Launch { const REQUIRES_ASYNC: bool = false; @@ -47,6 +89,17 @@ impl EntryAttr for Launch { None => quote_spanned!(ty.span() => #rocket.launch()), }; + if f.sig.asyncness.is_none() { + if let Some(call) = likely_spawns(f) { + call.span() + .warning("task is being spawned outside an async context") + .span_help(f.sig.span(), "declare this function as `async fn` \ + to require async execution") + .span_note(Span::call_site(), "`#[launch]` call is here") + .emit_as_expr_tokens(); + } + } + let (vis, mut sig) = (&f.vis, f.sig.clone()); sig.ident = syn::Ident::new("main", sig.ident.span()); sig.output = syn::ReturnType::Default; From e3f1b53efa518b33126575d41d48a8d2188b0a73 Mon Sep 17 00:00:00 2001 From: Sergio Benitez Date: Thu, 25 May 2023 11:54:27 -0700 Subject: [PATCH 151/166] Update 'state' to 0.6. --- core/http/Cargo.toml | 2 +- core/http/src/ext.rs | 6 +++--- core/http/src/header/media_type.rs | 2 +- core/http/src/listener.rs | 4 ++-- core/http/src/tls/listener.rs | 2 +- core/http/src/uri/absolute.rs | 4 ++-- core/http/src/uri/origin.rs | 6 +++--- core/http/src/uri/path_query.rs | 16 ++++++++-------- core/http/src/uri/reference.rs | 4 ++-- core/lib/Cargo.toml | 2 +- core/lib/src/fairing/ad_hoc.rs | 4 ++-- core/lib/src/phase.rs | 8 ++++---- core/lib/src/request/request.rs | 22 +++++++++++----------- core/lib/src/shield/shield.rs | 12 ++++++------ 14 files changed, 47 insertions(+), 47 deletions(-) diff --git a/core/http/Cargo.toml b/core/http/Cargo.toml index 3e95424587..2aa700f01c 100644 --- a/core/http/Cargo.toml +++ b/core/http/Cargo.toml @@ -43,7 +43,7 @@ pin-project-lite = "0.2" memchr = "2" stable-pattern = "0.1" cookie = { version = "0.17.0", features = ["percent-encode"] } -state = "0.5.3" +state = "0.6" futures = { version = "0.3", default-features = false } [dependencies.x509-parser] diff --git a/core/http/src/ext.rs b/core/http/src/ext.rs index b1b40eaa40..e78012e5d2 100644 --- a/core/http/src/ext.rs +++ b/core/http/src/ext.rs @@ -1,7 +1,7 @@ //! Extension traits implemented by several HTTP types. use smallvec::{Array, SmallVec}; -use state::Storage; +use state::InitCell; // TODO: It would be nice if we could somehow have one trait that could give us // either SmallVec or Vec. @@ -106,10 +106,10 @@ impl IntoOwned for Vec { } } -impl IntoOwned for Storage +impl IntoOwned for InitCell where T::Owned: Send + Sync { - type Owned = Storage; + type Owned = InitCell; #[inline(always)] fn into_owned(self) -> Self::Owned { diff --git a/core/http/src/header/media_type.rs b/core/http/src/header/media_type.rs index b217dbd5fa..b764b7da8b 100644 --- a/core/http/src/header/media_type.rs +++ b/core/http/src/header/media_type.rs @@ -51,7 +51,7 @@ use smallvec::SmallVec; /// [`exact_eq()`](MediaType::exact_eq()) method can be used. #[derive(Debug, Clone)] pub struct MediaType { - /// Storage for the entire media type string. + /// InitCell for the entire media type string. pub(crate) source: Source, /// The top-level type. pub(crate) top: IndexedStr<'static>, diff --git a/core/http/src/listener.rs b/core/http/src/listener.rs index e958702e08..f898a10883 100644 --- a/core/http/src/listener.rs +++ b/core/http/src/listener.rs @@ -12,7 +12,7 @@ use tokio::time::Sleep; use tokio::io::{AsyncRead, AsyncWrite}; use tokio::net::TcpStream; use hyper::server::accept::Accept; -use state::Storage; +use state::InitCell; pub use tokio::net::TcpListener; @@ -29,7 +29,7 @@ pub struct CertificateData(pub Vec); /// A collection of raw certificate data. #[derive(Clone, Default)] -pub struct Certificates(Arc>>); +pub struct Certificates(Arc>>); impl From> for Certificates { fn from(value: Vec) -> Self { diff --git a/core/http/src/tls/listener.rs b/core/http/src/tls/listener.rs index f8263c6fbf..8c675d6110 100644 --- a/core/http/src/tls/listener.rs +++ b/core/http/src/tls/listener.rs @@ -40,7 +40,7 @@ pub struct TlsListener { /// /// To work around this, we "lie" when `peer_certificates()` are requested and /// always return `Some(Certificates)`. Internally, `Certificates` is an -/// `Arc>>`, effectively a shared, thread-safe, +/// `Arc>>`, effectively a shared, thread-safe, /// `OnceCell`. The cell is initially empty and is filled as soon as the /// handshake is complete. If the certificate data were to be requested prior to /// this point, it would be empty. However, in Rocket, we only request diff --git a/core/http/src/uri/absolute.rs b/core/http/src/uri/absolute.rs index e084e63cec..385f754a3f 100644 --- a/core/http/src/uri/absolute.rs +++ b/core/http/src/uri/absolute.rs @@ -441,12 +441,12 @@ impl<'a> Absolute<'a> { authority, path: Data { value: IndexedStr::Concrete(Cow::Borrowed(path)), - decoded_segments: state::Storage::new(), + decoded_segments: state::InitCell::new(), }, query: match query { Some(query) => Some(Data { value: IndexedStr::Concrete(Cow::Borrowed(query)), - decoded_segments: state::Storage::new(), + decoded_segments: state::InitCell::new(), }), None => None, }, diff --git a/core/http/src/uri/origin.rs b/core/http/src/uri/origin.rs index ebe5e444d6..42aa917d71 100644 --- a/core/http/src/uri/origin.rs +++ b/core/http/src/uri/origin.rs @@ -168,12 +168,12 @@ impl<'a> Origin<'a> { source: None, path: Data { value: IndexedStr::Concrete(Cow::Borrowed(path)), - decoded_segments: state::Storage::new(), + decoded_segments: state::InitCell::new(), }, query: match query { Some(query) => Some(Data { value: IndexedStr::Concrete(Cow::Borrowed(query)), - decoded_segments: state::Storage::new(), + decoded_segments: state::InitCell::new(), }), None => None, }, @@ -534,7 +534,7 @@ impl<'a> Origin<'a> { self.path = Data { value: indexed, - decoded_segments: state::Storage::new(), + decoded_segments: state::InitCell::new(), }; } else { self._normalize(false); diff --git a/core/http/src/uri/path_query.rs b/core/http/src/uri/path_query.rs index 16733bc7af..5cc65a65cb 100644 --- a/core/http/src/uri/path_query.rs +++ b/core/http/src/uri/path_query.rs @@ -1,7 +1,7 @@ use std::hash::Hash; use std::borrow::Cow; -use state::Storage; +use state::InitCell; use crate::{RawStr, ext::IntoOwned}; use crate::uri::Segments; @@ -13,12 +13,12 @@ use crate::parse::{IndexedStr, Extent}; #[derive(Debug, Clone)] pub struct Data<'a, P: Part> { pub(crate) value: IndexedStr<'a>, - pub(crate) decoded_segments: Storage>, + pub(crate) decoded_segments: InitCell>, } impl<'a, P: Part> Data<'a, P> { pub(crate) fn raw(value: Extent<&'a [u8]>) -> Self { - Data { value: value.into(), decoded_segments: Storage::new() } + Data { value: value.into(), decoded_segments: InitCell::new() } } // INTERNAL METHOD. @@ -26,7 +26,7 @@ impl<'a, P: Part> Data<'a, P> { pub fn new>>(value: S) -> Self { Data { value: IndexedStr::from(value.into()), - decoded_segments: Storage::new(), + decoded_segments: InitCell::new(), } } } @@ -129,7 +129,7 @@ impl<'a> Path<'a> { Data { value: IndexedStr::from(Cow::Owned(path)), - decoded_segments: Storage::new(), + decoded_segments: InitCell::new(), } } @@ -215,7 +215,7 @@ impl<'a> Path<'a> { /// ``` pub fn segments(&self) -> Segments<'a, fmt::Path> { let raw = self.raw(); - let cached = self.data.decoded_segments.get_or_set(|| { + let cached = self.data.decoded_segments.get_or_init(|| { let mut segments = vec![]; let mut raw_segments = self.raw_segments().peekable(); while let Some(s) = raw_segments.next() { @@ -278,7 +278,7 @@ impl<'a> Query<'a> { Data { value: IndexedStr::from(Cow::Owned(query)), - decoded_segments: Storage::new(), + decoded_segments: InitCell::new(), } } @@ -351,7 +351,7 @@ impl<'a> Query<'a> { /// assert_eq!(query_segs, &[("a b/", "some one@gmail.com"), ("&=2", "")]); /// ``` pub fn segments(&self) -> Segments<'a, fmt::Query> { - let cached = self.data.decoded_segments.get_or_set(|| { + let cached = self.data.decoded_segments.get_or_init(|| { let (indexed, query) = (&self.data.value, self.raw()); self.raw_segments() .filter(|s| !s.is_empty()) diff --git a/core/http/src/uri/reference.rs b/core/http/src/uri/reference.rs index c3b2fa2c94..a626950670 100644 --- a/core/http/src/uri/reference.rs +++ b/core/http/src/uri/reference.rs @@ -129,12 +129,12 @@ impl<'a> Reference<'a> { authority, path: Data { value: IndexedStr::Concrete(Cow::Borrowed(path)), - decoded_segments: state::Storage::new(), + decoded_segments: state::InitCell::new(), }, query: match query { Some(query) => Some(Data { value: IndexedStr::Concrete(Cow::Borrowed(query)), - decoded_segments: state::Storage::new(), + decoded_segments: state::InitCell::new(), }), None => None, }, diff --git a/core/lib/Cargo.toml b/core/lib/Cargo.toml index e007f102e7..44de9448e3 100644 --- a/core/lib/Cargo.toml +++ b/core/lib/Cargo.toml @@ -59,7 +59,7 @@ async-trait = "0.1.43" async-stream = "0.3.2" multer = { version = "2", features = ["tokio-io"] } tokio-stream = { version = "0.1.6", features = ["signal", "time"] } -state = "0.5.1" +state = "0.6" [dependencies.rocket_codegen] version = "=0.5.0-rc.3" diff --git a/core/lib/src/fairing/ad_hoc.rs b/core/lib/src/fairing/ad_hoc.rs index 232e2f2a13..b428d3070f 100644 --- a/core/lib/src/fairing/ad_hoc.rs +++ b/core/lib/src/fairing/ad_hoc.rs @@ -314,12 +314,12 @@ impl AdHoc { pub fn uri_normalizer() -> impl Fairing { #[derive(Default)] struct Normalizer { - routes: state::Storage>, + routes: state::InitCell>, } impl Normalizer { fn routes(&self, rocket: &Rocket) -> &[crate::Route] { - self.routes.get_or_set(|| { + self.routes.get_or_init(|| { rocket.routes() .filter(|r| r.uri.has_trailing_slash()) .cloned() diff --git a/core/lib/src/phase.rs b/core/lib/src/phase.rs index 213f1215ea..3b6ca870c3 100644 --- a/core/lib/src/phase.rs +++ b/core/lib/src/phase.rs @@ -1,4 +1,4 @@ -use state::Container; +use state::TypeMap; use figment::Figment; use crate::{Catcher, Config, Rocket, Route, Shutdown}; @@ -83,7 +83,7 @@ phases! { pub(crate) catchers: Vec, pub(crate) fairings: Fairings, pub(crate) figment: Figment, - pub(crate) state: Container![Send + Sync], + pub(crate) state: TypeMap![Send + Sync], } /// The second launch [`Phase`]: post-build but pre-orbit. See @@ -97,7 +97,7 @@ phases! { pub(crate) fairings: Fairings, pub(crate) figment: Figment, pub(crate) config: Config, - pub(crate) state: Container![Send + Sync], + pub(crate) state: TypeMap![Send + Sync], pub(crate) shutdown: Shutdown, } @@ -111,7 +111,7 @@ phases! { pub(crate) fairings: Fairings, pub(crate) figment: Figment, pub(crate) config: Config, - pub(crate) state: Container![Send + Sync], + pub(crate) state: TypeMap![Send + Sync], pub(crate) shutdown: Shutdown, } } diff --git a/core/lib/src/request/request.rs b/core/lib/src/request/request.rs index 625c471568..0b44d13199 100644 --- a/core/lib/src/request/request.rs +++ b/core/lib/src/request/request.rs @@ -4,7 +4,7 @@ use std::{future::Future, borrow::Cow, sync::Arc}; use std::net::{IpAddr, SocketAddr}; use yansi::Paint; -use state::{Container, Storage}; +use state::{TypeMap, InitCell}; use futures::future::BoxFuture; use atomic::{Atomic, Ordering}; @@ -46,9 +46,9 @@ pub(crate) struct RequestState<'r> { pub rocket: &'r Rocket, pub route: Atomic>, pub cookies: CookieJar<'r>, - pub accept: Storage>, - pub content_type: Storage>, - pub cache: Arc, + pub accept: InitCell>, + pub content_type: InitCell>, + pub cache: Arc, pub host: Option>, } @@ -98,9 +98,9 @@ impl<'r> Request<'r> { rocket, route: Atomic::new(None), cookies: CookieJar::new(rocket.config()), - accept: Storage::new(), - content_type: Storage::new(), - cache: Arc::new(::new()), + accept: InitCell::new(), + content_type: InitCell::new(), + cache: Arc::new(::new()), host: None, } } @@ -547,7 +547,7 @@ impl<'r> Request<'r> { /// ``` #[inline] pub fn content_type(&self) -> Option<&ContentType> { - self.state.content_type.get_or_set(|| { + self.state.content_type.get_or_init(|| { self.headers().get_one("Content-Type").and_then(|v| v.parse().ok()) }).as_ref() } @@ -567,7 +567,7 @@ impl<'r> Request<'r> { /// ``` #[inline] pub fn accept(&self) -> Option<&Accept> { - self.state.accept.get_or_set(|| { + self.state.accept.get_or_init(|| { self.headers().get_one("Accept").and_then(|v| v.parse().ok()) }).as_ref() } @@ -928,11 +928,11 @@ impl<'r> Request<'r> { fn bust_header_cache(&mut self, name: &UncasedStr, replace: bool) { if name == "Content-Type" { if self.content_type().is_none() || replace { - self.state.content_type = Storage::new(); + self.state.content_type = InitCell::new(); } } else if name == "Accept" { if self.accept().is_none() || replace { - self.state.accept = Storage::new(); + self.state.accept = InitCell::new(); } } } diff --git a/core/lib/src/shield/shield.rs b/core/lib/src/shield/shield.rs index a614fa541d..1e58175feb 100644 --- a/core/lib/src/shield/shield.rs +++ b/core/lib/src/shield/shield.rs @@ -1,7 +1,7 @@ use std::collections::HashMap; use std::sync::atomic::{AtomicBool, Ordering}; -use state::Storage; +use state::InitCell; use yansi::Paint; use crate::{Rocket, Request, Response, Orbit, Config}; @@ -72,7 +72,7 @@ pub struct Shield { /// Whether to enforce HSTS even though the user didn't enable it. force_hsts: AtomicBool, /// Headers pre-rendered at liftoff from the configured policies. - rendered: Storage>>, + rendered: InitCell>>, } impl Default for Shield { @@ -111,7 +111,7 @@ impl Shield { Shield { policies: HashMap::new(), force_hsts: AtomicBool::new(false), - rendered: Storage::new(), + rendered: InitCell::new(), } } @@ -129,7 +129,7 @@ impl Shield { /// let shield = Shield::new().enable(NoSniff::default()); /// ``` pub fn enable(mut self, policy: P) -> Self { - self.rendered = Storage::new(); + self.rendered = InitCell::new(); self.policies.insert(P::NAME.into(), Box::new(policy)); self } @@ -145,7 +145,7 @@ impl Shield { /// let shield = Shield::default().disable::(); /// ``` pub fn disable(mut self) -> Self { - self.rendered = Storage::new(); + self.rendered = InitCell::new(); self.policies.remove(UncasedStr::new(P::NAME)); self } @@ -174,7 +174,7 @@ impl Shield { } fn headers(&self) -> &[Header<'static>] { - self.rendered.get_or_set(|| { + self.rendered.get_or_init(|| { let mut headers: Vec<_> = self.policies.values() .map(|p| p.header()) .collect(); From be92fe648be37d76d57f2ae33825ee7868f57696 Mon Sep 17 00:00:00 2001 From: Sergio Benitez Date: Thu, 25 May 2023 11:59:46 -0700 Subject: [PATCH 152/166] Update UI tests for latest rustc. --- .../tests/ui-fail-nightly/async-entry.stderr | 4 +-- .../ui-fail-nightly/typed-uri-bad-type.stderr | 4 +++ .../tests/ui-fail-stable/async-entry.stderr | 30 +++++++++++++++++-- .../codegen/tests/ui-fail-stable/catch.stderr | 2 +- .../tests/ui-fail-stable/from_form.stderr | 12 ++++---- .../ui-fail-stable/typed-uri-bad-type.stderr | 4 +-- .../typed-uris-bad-params.stderr | 4 +-- 7 files changed, 44 insertions(+), 16 deletions(-) diff --git a/core/codegen/tests/ui-fail-nightly/async-entry.stderr b/core/codegen/tests/ui-fail-nightly/async-entry.stderr index 3ae1393b85..0c76b4b436 100644 --- a/core/codegen/tests/ui-fail-nightly/async-entry.stderr +++ b/core/codegen/tests/ui-fail-nightly/async-entry.stderr @@ -106,12 +106,12 @@ note: this function cannot be `main` = note: this error originates in the attribute macro `rocket::launch` (in Nightly builds, run with -Z macro-backtrace for more info) error[E0728]: `await` is only allowed inside `async` functions and blocks - --> tests/ui-fail-nightly/async-entry.rs:73:41 + --> tests/ui-fail-nightly/async-entry.rs:73:42 | 72 | fn rocket() -> _ { | ------ this is not `async` 73 | let _ = rocket::build().launch().await; - | ^^^^^^ only allowed inside `async` functions and blocks + | ^^^^^ only allowed inside `async` functions and blocks error[E0308]: mismatched types --> tests/ui-fail-nightly/async-entry.rs:35:9 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 af0c2e5088..900f294759 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 @@ -276,6 +276,8 @@ error[E0277]: the trait bound `rocket::http::uri::Asterisk: ValidRouteSuffix $WORKSPACE/core/http/src/uri/fmt/formatter.rs | + | pub fn with_suffix(self, suffix: S) -> SuffixedRouteUri + | ----------- required by a bound in this associated function | where S: ValidRouteSuffix> | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ required by this bound in `RouteUriBuilder::with_suffix` @@ -307,5 +309,7 @@ error[E0277]: the trait bound `rocket::http::uri::Origin<'_>: ValidRouteSuffix $WORKSPACE/core/http/src/uri/fmt/formatter.rs | + | pub fn with_suffix(self, suffix: S) -> SuffixedRouteUri + | ----------- required by a bound in this associated function | where S: ValidRouteSuffix> | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ required by this bound in `RouteUriBuilder::with_suffix` diff --git a/core/codegen/tests/ui-fail-stable/async-entry.stderr b/core/codegen/tests/ui-fail-stable/async-entry.stderr index 1725775cc9..d7467aca76 100644 --- a/core/codegen/tests/ui-fail-stable/async-entry.stderr +++ b/core/codegen/tests/ui-fail-stable/async-entry.stderr @@ -110,8 +110,11 @@ error[E0728]: `await` is only allowed inside `async` functions and blocks error[E0308]: mismatched types --> tests/ui-fail-stable/async-entry.rs:35:9 | +33 | async fn rocket() -> String { + | ------ expected `std::string::String` because of return type +34 | let _ = rocket::build().launch().await; 35 | rocket::build() - | ^^^^^^^^^^^^^^^ expected struct `String`, found struct `Rocket` + | ^^^^^^^^^^^^^^^ expected `String`, found `Rocket` | = note: expected struct `std::string::String` found struct `Rocket` @@ -119,8 +122,11 @@ error[E0308]: mismatched types error[E0308]: mismatched types --> tests/ui-fail-stable/async-entry.rs:44:9 | +42 | async fn rocket() -> _ { + | - expected `Rocket` because of return type +43 | let _ = rocket::build().launch().await; 44 | "hi".to_string() - | ^^^^^^^^^^^^^^^^ expected struct `Rocket`, found struct `String` + | ^^^^^^^^^^^^^^^^ expected `Rocket`, found `String` | = note: expected struct `Rocket` found struct `std::string::String` @@ -136,11 +142,29 @@ error[E0308]: mismatched types 26 | | } | | ^- help: consider using a semicolon here: `;` | |_____| - | expected `()`, found struct `Rocket` + | expected `()`, found `Rocket` | = note: expected unit type `()` found struct `Rocket` +error[E0308]: mismatched types + --> tests/ui-fail-stable/async-entry.rs:35:9 + | +35 | rocket::build() + | ^^^^^^^^^^^^^^^ expected `String`, found `Rocket` + | + = note: expected struct `std::string::String` + found struct `Rocket` + +error[E0308]: mismatched types + --> tests/ui-fail-stable/async-entry.rs:44:9 + | +44 | "hi".to_string() + | ^^^^^^^^^^^^^^^^ expected `Rocket`, found `String` + | + = note: expected struct `Rocket` + found struct `std::string::String` + error[E0277]: `main` has invalid return type `Rocket` --> tests/ui-fail-stable/async-entry.rs:94:20 | diff --git a/core/codegen/tests/ui-fail-stable/catch.stderr b/core/codegen/tests/ui-fail-stable/catch.stderr index 71552ba53c..ac419d3126 100644 --- a/core/codegen/tests/ui-fail-stable/catch.stderr +++ b/core/codegen/tests/ui-fail-stable/catch.stderr @@ -60,7 +60,7 @@ error[E0308]: arguments to this function are incorrect 30 | fn f3(_request: &Request, other: bool) { } | ^^ - ---- an argument of type `bool` is missing | | - | argument of type `Status` unexpected + | unexpected argument of type `Status` | note: function defined here --> tests/ui-fail-stable/catch.rs:30:4 diff --git a/core/codegen/tests/ui-fail-stable/from_form.stderr b/core/codegen/tests/ui-fail-stable/from_form.stderr index a6acfa2440..94daef79d3 100644 --- a/core/codegen/tests/ui-fail-stable/from_form.stderr +++ b/core/codegen/tests/ui-fail-stable/from_form.stderr @@ -464,7 +464,7 @@ error[E0308]: mismatched types --> tests/ui-fail-stable/from_form.rs:147:24 | 147 | #[field(validate = 123)] - | ^^^ expected enum `Result`, found integer + | ^^^ expected `Result<(), Errors<'_>>`, found integer | = note: expected enum `Result<(), Errors<'_>>` found type `{integer}` @@ -481,7 +481,7 @@ error[E0308]: mismatched types 159 | #[field(validate = ext(rocket::http::ContentType::HTML))] | --- arguments to this function are incorrect 160 | first: String, - | ^^^^^^ expected enum `TempFile`, found struct `String` + | ^^^^^^ expected `&TempFile<'_>`, found `&String` | = note: expected reference `&TempFile<'_>` found reference `&std::string::String` @@ -495,9 +495,9 @@ error[E0308]: arguments to this function are incorrect --> tests/ui-fail-stable/from_form.rs:165:24 | 165 | #[field(validate = ext("hello"))] - | ^^^ ------- expected struct `ContentType`, found `&str` + | ^^^ ------- expected `ContentType`, found `&str` 166 | first: String, - | ------ expected enum `TempFile`, found struct `String` + | ------ expected `&TempFile<'_>`, found `&String` | = note: expected reference `&TempFile<'_>` found reference `&std::string::String` @@ -513,7 +513,7 @@ error[E0308]: mismatched types 171 | #[field(default = 123)] | ^^^- help: try using a conversion method: `.to_string()` | | - | expected struct `String`, found integer + | expected `String`, found integer | arguments to this enum variant are incorrect | help: the type constructed contains `{integer}` due to the type of the argument passed @@ -530,7 +530,7 @@ error[E0308]: mismatched types 203 | #[field(default_with = Some("hi"))] | ---- ^^^^- help: try using a conversion method: `.to_string()` | | | - | | expected struct `String`, found `&str` + | | expected `String`, found `&str` | arguments to this enum variant are incorrect | help: the type constructed contains `&'static str` due to the type of the argument passed 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 218e523aeb..cd8c538700 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 @@ -2,13 +2,13 @@ error[E0271]: type mismatch resolving `>::Error == &str` --> tests/ui-fail-stable/typed-uri-bad-type.rs:22:37 | 22 | fn optionals(id: Option, name: Result) { } - | ^^^^^^ expected struct `Empty`, found `&str` + | ^^^^^^ expected `Empty`, found `&str` error[E0271]: type mismatch resolving `>::Error == &str` --> tests/ui-fail-stable/typed-uri-bad-type.rs:22:37 | 22 | fn optionals(id: Option, name: Result) { } - | ^^^^^^ expected `&str`, found struct `Empty` + | ^^^^^^ expected `&str`, found `Empty` error[E0277]: the trait bound `usize: FromUriParam` is not satisfied --> tests/ui-fail-stable/typed-uri-bad-type.rs:45:22 diff --git a/core/codegen/tests/ui-fail-stable/typed-uris-bad-params.stderr b/core/codegen/tests/ui-fail-stable/typed-uris-bad-params.stderr index 227923a0dc..22b2d36346 100644 --- a/core/codegen/tests/ui-fail-stable/typed-uris-bad-params.stderr +++ b/core/codegen/tests/ui-fail-stable/typed-uris-bad-params.stderr @@ -276,10 +276,10 @@ error[E0271]: type mismatch resolving `>::Error == &str` --> tests/ui-fail-stable/typed-uris-bad-params.rs:15:37 | 15 | fn optionals(id: Option, name: Result) { } - | ^^^^^^ expected struct `Empty`, found `&str` + | ^^^^^^ expected `Empty`, found `&str` error[E0271]: type mismatch resolving `>::Error == &str` --> tests/ui-fail-stable/typed-uris-bad-params.rs:15:37 | 15 | fn optionals(id: Option, name: Result) { } - | ^^^^^^ expected `&str`, found struct `Empty` + | ^^^^^^ expected `&str`, found `Empty` From 23bf83d50d88c3893372d4e42c6f4a73d6488c88 Mon Sep 17 00:00:00 2001 From: Sergio Benitez Date: Wed, 7 Jun 2023 17:59:59 -0700 Subject: [PATCH 153/166] Add 'mtls::Certificate::as_bytes()' method. --- core/http/src/tls/mtls.rs | 40 ++++++++++++++++++++++++++++++++------- 1 file changed, 33 insertions(+), 7 deletions(-) diff --git a/core/http/src/tls/mtls.rs b/core/http/src/tls/mtls.rs index f8b67dac09..9d7554fb7f 100644 --- a/core/http/src/tls/mtls.rs +++ b/core/http/src/tls/mtls.rs @@ -142,9 +142,11 @@ pub type Result = std::result::Result; /// // _does_ run if a valid (Ok) or invalid (Err) one was presented. /// } /// ``` -#[repr(transparent)] #[derive(Debug, PartialEq)] -pub struct Certificate<'a>(X509Certificate<'a>); +pub struct Certificate<'a> { + x509: X509Certificate<'a>, + data: &'a CertificateData, +} /// An X.509 Distinguished Name (DN) found in a [`Certificate`]. /// @@ -218,16 +220,15 @@ impl<'a> Certificate<'a> { #[inline(always)] fn inner(&self) -> &TbsCertificate<'a> { - &self.0.tbs_certificate + &self.x509.tbs_certificate } /// PRIVATE: For internal Rocket use only! #[doc(hidden)] pub fn parse(chain: &[CertificateData]) -> Result> { - match chain.first() { - Some(cert) => Certificate::parse_one(&cert.0).map(Certificate), - None => Err(Error::Empty) - } + let data = chain.first().ok_or_else(|| Error::Empty)?; + let x509 = Certificate::parse_one(&data.0)?; + Ok(Certificate { x509, data }) } /// Returns the serial number of the X.509 certificate. @@ -364,6 +365,31 @@ impl<'a> Certificate<'a> { let uint: bigint::BigUint = number.parse().ok()?; Some(&uint == self.serial()) } + + /// Returns the raw, unmodified, DER-encoded X.509 certificate data bytes. + /// + /// # Example + /// + /// ```rust + /// # extern crate rocket; + /// # use rocket::get; + /// use rocket::mtls::Certificate; + /// + /// const SHA256_FINGERPRINT: &str = + /// "CE C2 4E 01 00 FF F7 78 CB A4 AA CB D2 49 DD 09 \ + /// 02 EF 0E 9B DA 89 2A E4 0D F4 09 83 97 C1 97 0D"; + /// + /// #[get("/auth")] + /// fn auth(cert: Certificate<'_>) { + /// # fn sha256_fingerprint(bytes: &[u8]) -> String { todo!() } + /// if sha256_fingerprint(cert.as_bytes()) == SHA256_FINGERPRINT { + /// println!("certificate fingerprint matched"); + /// } + /// } + /// ``` + pub fn as_bytes(&self) -> &'a [u8] { + &self.data.0 + } } impl<'a> Deref for Certificate<'a> { From a9549cd4e83379384b90b8cb09963f8787200eb0 Mon Sep 17 00:00:00 2001 From: Sergio Benitez Date: Wed, 7 Jun 2023 20:43:54 -0700 Subject: [PATCH 154/166] Remove use of 'unsafe' in 'RawStr' doctests. --- core/http/src/raw_str.rs | 25 +++++++++++++------------ 1 file changed, 13 insertions(+), 12 deletions(-) diff --git a/core/http/src/raw_str.rs b/core/http/src/raw_str.rs index 372453e831..02a54e3bed 100644 --- a/core/http/src/raw_str.rs +++ b/core/http/src/raw_str.rs @@ -173,9 +173,7 @@ impl RawStr { /// # extern crate rocket; /// use rocket::http::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::new(bad_str); + /// let bad_raw_str = RawStr::new("%FF"); /// assert!(bad_raw_str.percent_decode().is_err()); /// ``` #[inline(always)] @@ -211,9 +209,7 @@ impl RawStr { /// # extern crate rocket; /// use rocket::http::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::new(bad_str); + /// let bad_raw_str = RawStr::new("a=%FF"); /// assert_eq!(bad_raw_str.percent_decode_lossy(), "a=�"); /// ``` #[inline(always)] @@ -235,6 +231,15 @@ impl RawStr { allocated = string.into(); } + // SAFETY: + // + // 1. The caller must ensure that the content of the slice is valid + // UTF-8 before the borrow ends and the underlying `str` is used. + // + // `allocated[i]` is `+` since that is what we searched for. The + // `+` char is ASCII => the character is one byte wide. ' ' is + // also one byte and ASCII => UTF-8. The replacement of `+` with + // ` ` thus yields a valid UTF-8 string. unsafe { allocated.as_bytes_mut()[i] = b' '; } } @@ -265,9 +270,7 @@ impl RawStr { /// # extern crate rocket; /// use rocket::http::RawStr; /// - /// // NOTE: Rocket will never hand you a bad `&RawStr`. - /// let bad_str = unsafe { std::str::from_utf8_unchecked(b"a=\xff") }; - /// let bad_raw_str = RawStr::new(bad_str); + /// let bad_raw_str = RawStr::new("%FF"); /// assert!(bad_raw_str.percent_decode().is_err()); /// ``` #[inline(always)] @@ -344,9 +347,7 @@ impl RawStr { /// # extern crate rocket; /// use rocket::http::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::new(bad_str); + /// let bad_raw_str = RawStr::new("a+b=%FF"); /// assert_eq!(bad_raw_str.url_decode_lossy(), "a b=�"); /// ``` pub fn url_decode_lossy(&self) -> Cow<'_, str> { From 6db63d6bb373ffbf3136a9e318c6dc54ed3cb818 Mon Sep 17 00:00:00 2001 From: Sergio Benitez Date: Thu, 8 Jun 2023 15:37:53 -0700 Subject: [PATCH 155/166] Use 'resolver = 2' across workspaces. --- Cargo.toml | 1 + examples/Cargo.toml | 1 + 2 files changed, 2 insertions(+) diff --git a/Cargo.toml b/Cargo.toml index 7260ca1c3a..82586944dc 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,4 +1,5 @@ [workspace] +resolver = "2" members = [ "core/lib/", "core/codegen/", diff --git a/examples/Cargo.toml b/examples/Cargo.toml index c4c7d4ce22..c2ecd5fc59 100644 --- a/examples/Cargo.toml +++ b/examples/Cargo.toml @@ -1,4 +1,5 @@ [workspace] +resolver = "2" members = [ "config", "cookies", From 792bab251e579fb7c6197afd6950e892bd9558f2 Mon Sep 17 00:00:00 2001 From: Sergio Benitez Date: Thu, 8 Jun 2023 15:38:22 -0700 Subject: [PATCH 156/166] Update 'deadpool-redis' to '0.12'. --- contrib/db_pools/lib/Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contrib/db_pools/lib/Cargo.toml b/contrib/db_pools/lib/Cargo.toml index bfa1230c03..19ad8b23f1 100644 --- a/contrib/db_pools/lib/Cargo.toml +++ b/contrib/db_pools/lib/Cargo.toml @@ -47,7 +47,7 @@ features = ["rt_tokio_1"] optional = true [dependencies.deadpool-redis] -version = "0.11" +version = "0.12" default-features = false features = ["rt_tokio_1"] optional = true From 9a9cd76c0121f46765ff0df9ef81e36563a2a31f Mon Sep 17 00:00:00 2001 From: Sergio Benitez Date: Fri, 9 Jun 2023 16:46:24 -0700 Subject: [PATCH 157/166] Add support for 'diesel-async' to 'db_pools'. --- contrib/db_pools/lib/Cargo.toml | 13 ++ contrib/db_pools/lib/src/diesel.rs | 149 ++++++++++++++++++ contrib/db_pools/lib/src/lib.rs | 34 ++-- contrib/db_pools/lib/src/pool.rs | 17 ++ contrib/sync_db_pools/codegen/Cargo.toml | 3 +- .../ui-fail-nightly/database-syntax.stderr | 50 +++--- .../ui-fail-nightly/database-types.stderr | 4 - .../ui-fail-stable/database-syntax.stderr | 50 +++--- .../ui-fail-stable/database-types.stderr | 4 - .../codegen/tests/ui-fail/database-syntax.rs | 35 +++- contrib/sync_db_pools/lib/src/poolable.rs | 1 + .../tests/ui-fail-stable/async-entry.stderr | 5 +- examples/databases/Cargo.toml | 6 +- examples/databases/Rocket.toml | 3 + .../down.sql | 1 + .../20210329150332_create_posts_table/up.sql | 6 + examples/databases/src/diesel_mysql.rs | 97 ++++++++++++ examples/databases/src/diesel_sqlite.rs | 4 +- examples/databases/src/main.rs | 10 ++ scripts/test.sh | 2 + 20 files changed, 410 insertions(+), 84 deletions(-) create mode 100644 contrib/db_pools/lib/src/diesel.rs create mode 100644 examples/databases/db/diesel/mysql-migrations/20210329150332_create_posts_table/down.sql create mode 100644 examples/databases/db/diesel/mysql-migrations/20210329150332_create_posts_table/up.sql create mode 100644 examples/databases/src/diesel_mysql.rs diff --git a/contrib/db_pools/lib/Cargo.toml b/contrib/db_pools/lib/Cargo.toml index 19ad8b23f1..af9efca2fb 100644 --- a/contrib/db_pools/lib/Cargo.toml +++ b/contrib/db_pools/lib/Cargo.toml @@ -23,6 +23,9 @@ sqlx_postgres = ["sqlx", "sqlx/postgres"] sqlx_sqlite = ["sqlx", "sqlx/sqlite"] sqlx_mssql = ["sqlx", "sqlx/mssql"] sqlx_macros = ["sqlx/macros"] +# diesel features +diesel_postgres = ["diesel-async/postgres", "diesel-async/deadpool", "diesel", "deadpool"] +diesel_mysql = ["diesel-async/mysql", "diesel-async/deadpool", "diesel", "deadpool"] # implicit features: mongodb [dependencies.rocket] @@ -58,6 +61,16 @@ default-features = false features = ["tokio-runtime"] optional = true +[dependencies.diesel-async] +version = "0.3.1" +default-features = false +optional = true + +[dependencies.diesel] +version = "2.1" +default-features = false +optional = true + [dependencies.sqlx] version = "0.6" default-features = false diff --git a/contrib/db_pools/lib/src/diesel.rs b/contrib/db_pools/lib/src/diesel.rs new file mode 100644 index 0000000000..89c606be97 --- /dev/null +++ b/contrib/db_pools/lib/src/diesel.rs @@ -0,0 +1,149 @@ +//! Re-export of [`diesel`] with prelude types overridden with `async` variants +//! from [`diesel_async`]. +//! +//! # Usage +//! +//! To use `async` `diesel` support provided here, enable the following +//! dependencies in your `Cargo.toml`: +//! +//! ```toml +//! [dependencies] +//! rocket = "=0.5.0-rc.3" +//! diesel = "2" +//! +//! [dependencies.rocket_db_pools] +//! version = "=0.1.0-rc.3" +//! features = ["diesel_mysql"] +//! ``` +//! +//! Then, import `rocket_db_pools::diesel::prelude::*` as well as the +//! appropriate pool type and, optionally, [`QueryResult`]. To use macros or +//! `diesel` functions, use `diesel::` directly. That is, _do not_ import +//! `rocket_db_pools::diesel`. Doing so will, by design, cause import errors. +//! +//! # Example +//! +//! ```rust +//! # #[macro_use] extern crate rocket; +//! # #[cfg(feature = "diesel_mysql")] { +//! use rocket_db_pools::{Database, Connection}; +//! use rocket_db_pools::diesel::{QueryResult, MysqlPool, prelude::*}; +//! +//! #[derive(Database)] +//! #[database("diesel_mysql")] +//! struct Db(MysqlPool); +//! +//! #[derive(Queryable, Insertable)] +//! #[diesel(table_name = posts)] +//! struct Post { +//! id: i64, +//! title: String, +//! published: bool, +//! } +//! +//! diesel::table! { +//! posts (id) { +//! id -> BigInt, +//! title -> Text, +//! published -> Bool, +//! } +//! } +//! +//! #[get("/")] +//! async fn list(mut db: Connection) -> QueryResult { +//! let post_ids: Vec = posts::table +//! .select(posts::id) +//! .load(&mut db) +//! .await?; +//! +//! Ok(format!("{post_ids:?}")) +//! } +//! # } +//! ``` + +/// The [`diesel`] prelude with `sync`-only traits replaced with their +/// [`diesel_async`] variants. +pub mod prelude { + #[doc(inline)] + pub use diesel::prelude::*; + + #[doc(inline)] + pub use diesel_async::{AsyncConnection, RunQueryDsl, SaveChangesDsl}; +} + +#[doc(hidden)] +pub use diesel::*; + +#[doc(hidden)] +pub use diesel_async::{RunQueryDsl, SaveChangesDsl, *}; + +#[doc(hidden)] +#[cfg(feature = "diesel_postgres")] +pub use diesel_async::pg; + +#[doc(inline)] +pub use diesel_async::pooled_connection::deadpool::Pool; + +#[doc(inline)] +#[cfg(feature = "diesel_mysql")] +pub use diesel_async::AsyncMysqlConnection; + +#[doc(inline)] +#[cfg(feature = "diesel_postgres")] +pub use diesel_async::AsyncPgConnection; + +/// Alias of a `Result` with an error type of [`Debug`] for a `diesel::Error`. +/// +/// `QueryResult` is a [`Responder`](rocket::response::Responder) when `T` (the +/// `Ok` value) is a `Responder`. By using this alias as a route handler's +/// return type, the `?` operator can be applied to fallible `diesel` functions +/// in the route handler while still providing a valid `Responder` return type. +/// +/// See the [module level docs](self#example) for a usage example. +/// +/// [`Debug`]: rocket::response::Debug +pub type QueryResult> = Result; + +/// Type alias for an `async` pool of MySQL connections for `async` [diesel]. +/// +/// ```rust +/// # extern crate rocket; +/// # #[cfg(feature = "diesel_mysql")] { +/// # use rocket::get; +/// use rocket_db_pools::{Database, Connection}; +/// use rocket_db_pools::diesel::{MysqlPool, prelude::*}; +/// +/// #[derive(Database)] +/// #[database("my_mysql_db_name")] +/// struct Db(MysqlPool); +/// +/// #[get("/")] +/// async fn use_db(mut db: Connection) { +/// /* .. */ +/// } +/// # } +/// ``` +#[cfg(feature = "diesel_mysql")] +pub type MysqlPool = Pool; + +/// Type alias for an `async` pool of Postgres connections for `async` [diesel]. +/// +/// ```rust +/// # extern crate rocket; +/// # #[cfg(feature = "diesel_postgres")] { +/// # use rocket::get; +/// use rocket_db_pools::{Database, Connection}; +/// use rocket_db_pools::diesel::{PgPool, prelude::*}; +/// +/// #[derive(Database)] +/// #[database("my_pg_db_name")] +/// struct Db(PgPool); +/// +/// #[get("/")] +/// async fn use_db(mut db: Connection) { +/// /* .. */ +/// } +/// # } +/// ``` +#[cfg(feature = "diesel_postgres")] +pub type PgPool = Pool; diff --git a/contrib/db_pools/lib/src/lib.rs b/contrib/db_pools/lib/src/lib.rs index 5130097d45..d79fa4ea05 100644 --- a/contrib/db_pools/lib/src/lib.rs +++ b/contrib/db_pools/lib/src/lib.rs @@ -98,10 +98,10 @@ //! //! # Supported Drivers //! -//! At present, this crate supports _three_ drivers: [`deadpool`], [`sqlx`], and -//! [`mongodb`]. Each driver may support multiple databases. Drivers have a -//! varying degree of support for graceful shutdown, affected by the -//! `Type::init()` fairing on Rocket shutdown. +//! At present, this crate supports _four_ drivers: [`deadpool`], [`sqlx`], +//! [`mongodb`], and [`diesel`]. Each driver may support multiple databases. +//! Drivers have a varying degree of support for graceful shutdown, affected by +//! the `Type::init()` fairing on Rocket shutdown. //! //! ## `deadpool` (v0.9) //! @@ -126,10 +126,10 @@ //! [`sqlx::MySqlPool`]: https://docs.rs/sqlx/0.6/sqlx/type.MySqlPool.html //! [`sqlx::SqlitePool`]: https://docs.rs/sqlx/0.6/sqlx/type.SqlitePool.html //! [`sqlx::MssqlPool`]: https://docs.rs/sqlx/0.6/sqlx/type.MssqlPool.html -//! [`sqlx::PoolConnection`]: https://docs.rs/sqlx/0.6/sqlx/pool/struct.PoolConnection.html -//! [`sqlx::PoolConnection`]: https://docs.rs/sqlx/0.6/sqlx/pool/struct.PoolConnection.html -//! [`sqlx::PoolConnection`]: https://docs.rs/sqlx/0.6/sqlx/pool/struct.PoolConnection.html -//! [`sqlx::PoolConnection`]: https://docs.rs/sqlx/0.6/sqlx/pool/struct.PoolConnection.html +//! [`sqlx::pool::PoolConnection`]: https://docs.rs/sqlx/0.6/sqlx/pool/struct.PoolConnection.html +//! [`sqlx::pool::PoolConnection`]: https://docs.rs/sqlx/0.6/sqlx/pool/struct.PoolConnection.html +//! [`sqlx::pool::PoolConnection`]: https://docs.rs/sqlx/0.6/sqlx/pool/struct.PoolConnection.html +//! [`sqlx::pool::PoolConnection`]: https://docs.rs/sqlx/0.6/sqlx/pool/struct.PoolConnection.html //! //! On shutdown, new connections are denied. Shutdown waits for connections to //! be returned. @@ -142,6 +142,18 @@ //! //! Graceful shutdown is not supported. //! +//! ## `diesel` (v2) +//! +//! | Database | Feature | [`Pool`] Type | [`Connection`] Deref | +//! |----------|-------------------|-----------------------|----------------------------------| +//! | Postgres | `diesel_postgres` | [`diesel::PgPool`] | [`diesel::AsyncPgConnection`] | +//! | MySQL | `diesel_mysql` | [`diesel::MysqlPool`] | [`diesel::AsyncMysqlConnection`] | //! +//! +//! See [`diesel`] for usage details. +//! +//! On shutdown, new connections are denied. Shutdown _does not_ wait for +//! connections to be returned. +//! //! ## Enabling Additional Driver Features //! //! Only the minimal features for each driver crate are enabled by @@ -186,7 +198,7 @@ //! //! See [`Config`] for details on configuration parameters. //! -//! **Note:** `deadpool` drivers do not support and thus ignore the +//! **Note:** `deadpool` and `diesel` drivers do not support and thus ignore the //! `min_connections` value. //! //! ## Driver Defaults @@ -225,11 +237,13 @@ #![deny(missing_docs)] +pub use rocket; + /// Re-export of the `figment` crate. #[doc(inline)] pub use rocket::figment; -pub use rocket; +#[cfg(any(feature = "diesel_postgres", feature = "diesel_mysql"))] pub mod diesel; #[cfg(feature = "deadpool_postgres")] pub use deadpool_postgres; #[cfg(feature = "deadpool_redis")] pub use deadpool_redis; #[cfg(feature = "mongodb")] pub use mongodb; diff --git a/contrib/db_pools/lib/src/pool.rs b/contrib/db_pools/lib/src/pool.rs index d07a0f13ff..49d298cba1 100644 --- a/contrib/db_pools/lib/src/pool.rs +++ b/contrib/db_pools/lib/src/pool.rs @@ -157,6 +157,9 @@ mod deadpool_postgres { use deadpool::{managed::{Manager, Pool, PoolError, Object, BuildError}, Runtime}; use super::{Duration, Error, Config, Figment}; + #[cfg(any(feature = "diesel_postgres", feature = "diesel_mysql"))] + use diesel_async::pooled_connection::AsyncDieselConnectionManager; + pub trait DeadManager: Manager + Sized + Send + Sync + 'static { fn new(config: &Config) -> Result; } @@ -175,6 +178,20 @@ mod deadpool_postgres { } } + #[cfg(feature = "diesel_postgres")] + impl DeadManager for AsyncDieselConnectionManager { + fn new(config: &Config) -> Result { + Ok(Self::new(config.url.as_str())) + } + } + + #[cfg(feature = "diesel_mysql")] + impl DeadManager for AsyncDieselConnectionManager { + fn new(config: &Config) -> Result { + Ok(Self::new(config.url.as_str())) + } + } + #[rocket::async_trait] impl>> crate::Pool for Pool where M::Type: Send, C: Send + Sync + 'static, M::Error: std::error::Error diff --git a/contrib/sync_db_pools/codegen/Cargo.toml b/contrib/sync_db_pools/codegen/Cargo.toml index ad6a9818d9..cee6a484ef 100644 --- a/contrib/sync_db_pools/codegen/Cargo.toml +++ b/contrib/sync_db_pools/codegen/Cargo.toml @@ -20,4 +20,5 @@ devise = "0.4" [dev-dependencies] version_check = "0.9" trybuild = "1.0" -rocket_sync_db_pools = { path = "../lib", features = ["diesel_sqlite_pool"] } +rocket_sync_db_pools = { path = "../lib" } +rocket = { path = "../../../core/lib" } diff --git a/contrib/sync_db_pools/codegen/tests/ui-fail-nightly/database-syntax.stderr b/contrib/sync_db_pools/codegen/tests/ui-fail-nightly/database-syntax.stderr index e89220bded..1528e2a77f 100644 --- a/contrib/sync_db_pools/codegen/tests/ui-fail-nightly/database-syntax.stderr +++ b/contrib/sync_db_pools/codegen/tests/ui-fail-nightly/database-syntax.stderr @@ -1,57 +1,57 @@ error: unexpected end of input, expected string literal - --> tests/ui-fail-nightly/database-syntax.rs:6:1 - | -6 | #[database] - | ^^^^^^^^^^^ - | - = note: this error originates in the attribute macro `database` (in Nightly builds, run with -Z macro-backtrace for more info) + --> tests/ui-fail-nightly/database-syntax.rs:27:1 + | +27 | #[database] + | ^^^^^^^^^^^ + | + = note: this error originates in the attribute macro `database` (in Nightly builds, run with -Z macro-backtrace for more info) error: expected string literal - --> tests/ui-fail-nightly/database-syntax.rs:9:12 - | -9 | #[database(1)] - | ^ + --> tests/ui-fail-nightly/database-syntax.rs:30:12 + | +30 | #[database(1)] + | ^ error: expected string literal - --> tests/ui-fail-nightly/database-syntax.rs:12:12 + --> tests/ui-fail-nightly/database-syntax.rs:33:12 | -12 | #[database(123)] +33 | #[database(123)] | ^^^ error: unexpected token - --> tests/ui-fail-nightly/database-syntax.rs:15:20 + --> tests/ui-fail-nightly/database-syntax.rs:36:20 | -15 | #[database("hello" "hi")] +36 | #[database("hello" "hi")] | ^^^^ error: `database` attribute can only be used on structs - --> tests/ui-fail-nightly/database-syntax.rs:19:1 + --> tests/ui-fail-nightly/database-syntax.rs:40:1 | -19 | enum Foo { } +40 | enum Foo { } | ^^^^^^^^^^^^^ error: `database` attribute can only be applied to structs with exactly one unnamed field - --> tests/ui-fail-nightly/database-syntax.rs:22:11 + --> tests/ui-fail-nightly/database-syntax.rs:43:11 | -22 | struct Bar(diesel::SqliteConnection, diesel::SqliteConnection); - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +43 | struct Bar(Connection, Connection); + | ^^^^^^^^^^^^^^^^^^^^^^^^ | = help: example: `struct MyDatabase(diesel::SqliteConnection);` error: `database` attribute can only be used on structs - --> tests/ui-fail-nightly/database-syntax.rs:25:1 + --> tests/ui-fail-nightly/database-syntax.rs:46:1 | -25 | union Baz { } +46 | union Baz { } | ^^^^^^^^^^^^^^ error: `database` attribute cannot be applied to structs with generics - --> tests/ui-fail-nightly/database-syntax.rs:28:9 + --> tests/ui-fail-nightly/database-syntax.rs:49:9 | -28 | struct E<'r>(&'r str); +49 | struct E<'r>(&'r str); | ^^^^ error: `database` attribute cannot be applied to structs with generics - --> tests/ui-fail-nightly/database-syntax.rs:31:9 + --> tests/ui-fail-nightly/database-syntax.rs:52:9 | -31 | struct F(T); +52 | struct F(T); | ^^^ diff --git a/contrib/sync_db_pools/codegen/tests/ui-fail-nightly/database-types.stderr b/contrib/sync_db_pools/codegen/tests/ui-fail-nightly/database-types.stderr index 1e4e92ac8d..64be8774fe 100644 --- a/contrib/sync_db_pools/codegen/tests/ui-fail-nightly/database-types.stderr +++ b/contrib/sync_db_pools/codegen/tests/ui-fail-nightly/database-types.stderr @@ -4,7 +4,6 @@ error[E0277]: the trait bound `Unknown: Poolable` is not satisfied 6 | struct A(Unknown); | ^^^^^^^ the trait `Poolable` is not implemented for `Unknown` | - = help: the trait `Poolable` is implemented for `SqliteConnection` note: required by a bound in `rocket_sync_db_pools::Connection` --> $WORKSPACE/contrib/sync_db_pools/lib/src/connection.rs | @@ -17,7 +16,6 @@ error[E0277]: the trait bound `Vec: Poolable` is not satisfied 9 | struct B(Vec); | ^^^^^^^^ the trait `Poolable` is not implemented for `Vec` | - = help: the trait `Poolable` is implemented for `SqliteConnection` note: required by a bound in `rocket_sync_db_pools::Connection` --> $WORKSPACE/contrib/sync_db_pools/lib/src/connection.rs | @@ -30,7 +28,6 @@ error[E0277]: the trait bound `Unknown: Poolable` is not satisfied 6 | struct A(Unknown); | ^^^^^^^ the trait `Poolable` is not implemented for `Unknown` | - = help: the trait `Poolable` is implemented for `SqliteConnection` note: required by a bound in `ConnectionPool` --> $WORKSPACE/contrib/sync_db_pools/lib/src/connection.rs | @@ -43,7 +40,6 @@ error[E0277]: the trait bound `Vec: Poolable` is not satisfied 9 | struct B(Vec); | ^^^^^^^^ the trait `Poolable` is not implemented for `Vec` | - = help: the trait `Poolable` is implemented for `SqliteConnection` note: required by a bound in `ConnectionPool` --> $WORKSPACE/contrib/sync_db_pools/lib/src/connection.rs | diff --git a/contrib/sync_db_pools/codegen/tests/ui-fail-stable/database-syntax.stderr b/contrib/sync_db_pools/codegen/tests/ui-fail-stable/database-syntax.stderr index 6e5323157d..9523d0ca86 100644 --- a/contrib/sync_db_pools/codegen/tests/ui-fail-stable/database-syntax.stderr +++ b/contrib/sync_db_pools/codegen/tests/ui-fail-stable/database-syntax.stderr @@ -1,56 +1,56 @@ error: unexpected end of input, expected string literal - --> tests/ui-fail-stable/database-syntax.rs:6:1 - | -6 | #[database] - | ^^^^^^^^^^^ - | - = note: this error originates in the attribute macro `database` (in Nightly builds, run with -Z macro-backtrace for more info) + --> tests/ui-fail-stable/database-syntax.rs:27:1 + | +27 | #[database] + | ^^^^^^^^^^^ + | + = note: this error originates in the attribute macro `database` (in Nightly builds, run with -Z macro-backtrace for more info) error: expected string literal - --> tests/ui-fail-stable/database-syntax.rs:9:12 - | -9 | #[database(1)] - | ^ + --> tests/ui-fail-stable/database-syntax.rs:30:12 + | +30 | #[database(1)] + | ^ error: expected string literal - --> tests/ui-fail-stable/database-syntax.rs:12:12 + --> tests/ui-fail-stable/database-syntax.rs:33:12 | -12 | #[database(123)] +33 | #[database(123)] | ^^^ error: unexpected token - --> tests/ui-fail-stable/database-syntax.rs:15:20 + --> tests/ui-fail-stable/database-syntax.rs:36:20 | -15 | #[database("hello" "hi")] +36 | #[database("hello" "hi")] | ^^^^ error: `database` attribute can only be used on structs - --> tests/ui-fail-stable/database-syntax.rs:19:1 + --> tests/ui-fail-stable/database-syntax.rs:40:1 | -19 | enum Foo { } +40 | enum Foo { } | ^^^^ error: `database` attribute can only be applied to structs with exactly one unnamed field --- help: example: `struct MyDatabase(diesel::SqliteConnection);` - --> tests/ui-fail-stable/database-syntax.rs:22:11 + --> tests/ui-fail-stable/database-syntax.rs:43:11 | -22 | struct Bar(diesel::SqliteConnection, diesel::SqliteConnection); - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +43 | struct Bar(Connection, Connection); + | ^^^^^^^^^^^^^^^^^^^^^^^^ error: `database` attribute can only be used on structs - --> tests/ui-fail-stable/database-syntax.rs:25:1 + --> tests/ui-fail-stable/database-syntax.rs:46:1 | -25 | union Baz { } +46 | union Baz { } | ^^^^^ error: `database` attribute cannot be applied to structs with generics - --> tests/ui-fail-stable/database-syntax.rs:28:9 + --> tests/ui-fail-stable/database-syntax.rs:49:9 | -28 | struct E<'r>(&'r str); +49 | struct E<'r>(&'r str); | ^ error: `database` attribute cannot be applied to structs with generics - --> tests/ui-fail-stable/database-syntax.rs:31:9 + --> tests/ui-fail-stable/database-syntax.rs:52:9 | -31 | struct F(T); +52 | struct F(T); | ^ diff --git a/contrib/sync_db_pools/codegen/tests/ui-fail-stable/database-types.stderr b/contrib/sync_db_pools/codegen/tests/ui-fail-stable/database-types.stderr index 7a4cead305..c699597e88 100644 --- a/contrib/sync_db_pools/codegen/tests/ui-fail-stable/database-types.stderr +++ b/contrib/sync_db_pools/codegen/tests/ui-fail-stable/database-types.stderr @@ -4,7 +4,6 @@ error[E0277]: the trait bound `Unknown: Poolable` is not satisfied 6 | struct A(Unknown); | ^^^^^^^ the trait `Poolable` is not implemented for `Unknown` | - = help: the trait `Poolable` is implemented for `SqliteConnection` note: required by a bound in `rocket_sync_db_pools::Connection` --> $WORKSPACE/contrib/sync_db_pools/lib/src/connection.rs | @@ -17,7 +16,6 @@ error[E0277]: the trait bound `Vec: Poolable` is not satisfied 9 | struct B(Vec); | ^^^ the trait `Poolable` is not implemented for `Vec` | - = help: the trait `Poolable` is implemented for `SqliteConnection` note: required by a bound in `rocket_sync_db_pools::Connection` --> $WORKSPACE/contrib/sync_db_pools/lib/src/connection.rs | @@ -30,7 +28,6 @@ error[E0277]: the trait bound `Unknown: Poolable` is not satisfied 6 | struct A(Unknown); | ^^^^^^^ the trait `Poolable` is not implemented for `Unknown` | - = help: the trait `Poolable` is implemented for `SqliteConnection` note: required by a bound in `ConnectionPool` --> $WORKSPACE/contrib/sync_db_pools/lib/src/connection.rs | @@ -43,7 +40,6 @@ error[E0277]: the trait bound `Vec: Poolable` is not satisfied 9 | struct B(Vec); | ^^^ the trait `Poolable` is not implemented for `Vec` | - = help: the trait `Poolable` is implemented for `SqliteConnection` note: required by a bound in `ConnectionPool` --> $WORKSPACE/contrib/sync_db_pools/lib/src/connection.rs | diff --git a/contrib/sync_db_pools/codegen/tests/ui-fail/database-syntax.rs b/contrib/sync_db_pools/codegen/tests/ui-fail/database-syntax.rs index 20af9ee185..1add1d0d6c 100644 --- a/contrib/sync_db_pools/codegen/tests/ui-fail/database-syntax.rs +++ b/contrib/sync_db_pools/codegen/tests/ui-fail/database-syntax.rs @@ -1,25 +1,46 @@ use rocket_sync_db_pools::database; -#[allow(unused_imports)] -use rocket_sync_db_pools::diesel; +struct Connection; +struct Manager; + +use rocket::{Rocket, Build}; +use rocket_sync_db_pools::{r2d2, Poolable, PoolResult}; + +impl r2d2::ManageConnection for Manager { + type Connection = Connection; + type Error = std::convert::Infallible; + + fn connect(&self) -> Result { Ok(Connection) } + fn is_valid(&self, conn: &mut Self::Connection) -> Result<(), Self::Error> { Ok(()) } + fn has_broken(&self, conn: &mut Self::Connection) -> bool { true } +} + +impl Poolable for Connection { + type Manager = Manager; + type Error = std::convert::Infallible; + + fn pool(db_name: &str, rocket: &Rocket) -> PoolResult { + todo!() + } +} #[database] -struct A(diesel::SqliteConnection); +struct A(Connection); #[database(1)] -struct B(diesel::SqliteConnection); +struct B(Connection); #[database(123)] -struct C(diesel::SqliteConnection); +struct C(Connection); #[database("hello" "hi")] -struct D(diesel::SqliteConnection); +struct D(Connection); #[database("test")] enum Foo { } #[database("test")] -struct Bar(diesel::SqliteConnection, diesel::SqliteConnection); +struct Bar(Connection, Connection); #[database("test")] union Baz { } diff --git a/contrib/sync_db_pools/lib/src/poolable.rs b/contrib/sync_db_pools/lib/src/poolable.rs index 4e7a34c1a9..0451de60bb 100644 --- a/contrib/sync_db_pools/lib/src/poolable.rs +++ b/contrib/sync_db_pools/lib/src/poolable.rs @@ -1,3 +1,4 @@ +#[allow(unused)] use std::time::Duration; use r2d2::ManageConnection; diff --git a/core/codegen/tests/ui-fail-stable/async-entry.stderr b/core/codegen/tests/ui-fail-stable/async-entry.stderr index d7467aca76..dc1d54d2f2 100644 --- a/core/codegen/tests/ui-fail-stable/async-entry.stderr +++ b/core/codegen/tests/ui-fail-stable/async-entry.stderr @@ -135,8 +135,9 @@ error[E0308]: mismatched types --> tests/ui-fail-stable/async-entry.rs:24:21 | 24 | async fn main() { - | ^ expected `()` because of default return type - | _____________________| + | ^ + | | + | _____________________expected `()` because of default return type | | 25 | | rocket::build() 26 | | } diff --git a/examples/databases/Cargo.toml b/examples/databases/Cargo.toml index 927f64d344..926105d2e8 100644 --- a/examples/databases/Cargo.toml +++ b/examples/databases/Cargo.toml @@ -7,8 +7,8 @@ publish = false [dependencies] rocket = { path = "../../core/lib", features = ["json"] } -diesel = { version = "2.0.0", features = ["sqlite", "r2d2"] } -diesel_migrations = "2.0.0" +diesel = "2" +diesel_migrations = "2" [dependencies.sqlx] version = "0.6.0" @@ -17,7 +17,7 @@ features = ["macros", "offline", "migrate"] [dependencies.rocket_db_pools] path = "../../contrib/db_pools/lib/" -features = ["sqlx_sqlite"] +features = ["sqlx_sqlite", "diesel_mysql"] [dependencies.rocket_sync_db_pools] path = "../../contrib/sync_db_pools/lib/" diff --git a/examples/databases/Rocket.toml b/examples/databases/Rocket.toml index 1409c8841d..278fdbd997 100644 --- a/examples/databases/Rocket.toml +++ b/examples/databases/Rocket.toml @@ -7,3 +7,6 @@ url = "db/sqlx/db.sqlite" [default.databases.diesel] url = "db/diesel/db.sqlite" timeout = 10 + +[default.databases.diesel_mysql] +url = "mysql://user:password@127.0.0.1/database" diff --git a/examples/databases/db/diesel/mysql-migrations/20210329150332_create_posts_table/down.sql b/examples/databases/db/diesel/mysql-migrations/20210329150332_create_posts_table/down.sql new file mode 100644 index 0000000000..1651d89549 --- /dev/null +++ b/examples/databases/db/diesel/mysql-migrations/20210329150332_create_posts_table/down.sql @@ -0,0 +1 @@ +DROP TABLE posts; diff --git a/examples/databases/db/diesel/mysql-migrations/20210329150332_create_posts_table/up.sql b/examples/databases/db/diesel/mysql-migrations/20210329150332_create_posts_table/up.sql new file mode 100644 index 0000000000..005109e20e --- /dev/null +++ b/examples/databases/db/diesel/mysql-migrations/20210329150332_create_posts_table/up.sql @@ -0,0 +1,6 @@ +CREATE TABLE posts ( + id INTEGER AUTO_INCREMENT PRIMARY KEY, + title VARCHAR(255) NOT NULL, + text TEXT NOT NULL, + published BOOLEAN NOT NULL DEFAULT FALSE +); diff --git a/examples/databases/src/diesel_mysql.rs b/examples/databases/src/diesel_mysql.rs new file mode 100644 index 0000000000..6c6c7751f0 --- /dev/null +++ b/examples/databases/src/diesel_mysql.rs @@ -0,0 +1,97 @@ +use rocket::fairing::AdHoc; +use rocket::response::{Debug, status::Created}; +use rocket::serde::{Serialize, Deserialize, json::Json}; + +use rocket_db_pools::{Database, Connection}; +use rocket_db_pools::diesel::{MysqlPool, prelude::*}; + +type Result> = std::result::Result; + +#[derive(Database)] +#[database("diesel_mysql")] +struct Db(MysqlPool); + +#[derive(Debug, Clone, Deserialize, Serialize, Queryable, Insertable)] +#[serde(crate = "rocket::serde")] +#[diesel(table_name = posts)] +struct Post { + #[serde(skip_deserializing)] + id: Option, + title: String, + text: String, + #[serde(skip_deserializing)] + published: bool, +} + +diesel::table! { + posts (id) { + id -> Nullable, + title -> Text, + text -> Text, + published -> Bool, + } +} + +#[post("/", data = "")] +async fn create(mut db: Connection, mut post: Json) -> Result>> { + diesel::sql_function!(fn last_insert_id() -> BigInt); + + let post = db.transaction(|mut conn| Box::pin(async move { + diesel::insert_into(posts::table) + .values(&*post) + .execute(&mut conn) + .await?; + + post.id = Some(posts::table + .select(last_insert_id()) + .first(&mut conn) + .await?); + + Ok::<_, diesel::result::Error>(post) + })).await?; + + Ok(Created::new("/").body(post)) +} + +#[get("/")] +async fn list(mut db: Connection) -> Result>>> { + let ids = posts::table + .select(posts::id) + .load(&mut db) + .await?; + + Ok(Json(ids)) +} + +#[get("/")] +async fn read(mut db: Connection, id: i64) -> Option> { + posts::table + .filter(posts::id.eq(id)) + .first(&mut db) + .await + .map(Json) + .ok() +} + +#[delete("/")] +async fn delete(mut db: Connection, id: i64) -> Result> { + let affected = diesel::delete(posts::table) + .filter(posts::id.eq(id)) + .execute(&mut db) + .await?; + + Ok((affected == 1).then(|| ())) +} + +#[delete("/")] +async fn destroy(mut db: Connection) -> Result<()> { + diesel::delete(posts::table).execute(&mut db).await?; + Ok(()) +} + +pub fn stage() -> AdHoc { + AdHoc::on_ignite("Diesel SQLite Stage", |rocket| async { + rocket.attach(Db::init()) + .mount("/diesel-async/", routes![list, read, create, delete, destroy]) + }) +} diff --git a/examples/databases/src/diesel_sqlite.rs b/examples/databases/src/diesel_sqlite.rs index c64d8edf6a..1427729a27 100644 --- a/examples/databases/src/diesel_sqlite.rs +++ b/examples/databases/src/diesel_sqlite.rs @@ -3,9 +3,7 @@ use rocket::fairing::AdHoc; use rocket::response::{Debug, status::Created}; use rocket::serde::{Serialize, Deserialize, json::Json}; -use rocket_sync_db_pools::diesel; - -use self::diesel::prelude::*; +use diesel::prelude::*; #[database("diesel")] struct Db(diesel::SqliteConnection); diff --git a/examples/databases/src/main.rs b/examples/databases/src/main.rs index 8748f22d1c..8491e01bb1 100644 --- a/examples/databases/src/main.rs +++ b/examples/databases/src/main.rs @@ -5,12 +5,22 @@ mod sqlx; mod diesel_sqlite; +mod diesel_mysql; mod rusqlite; +use rocket::response::Redirect; + +#[get("/")] +fn index() -> Redirect { + Redirect::to(uri!("/sqlx", sqlx::list())) +} + #[launch] fn rocket() -> _ { rocket::build() + .mount("/", routes![index]) .attach(sqlx::stage()) .attach(rusqlite::stage()) .attach(diesel_sqlite::stage()) + .attach(diesel_mysql::stage()) } diff --git a/scripts/test.sh b/scripts/test.sh index b691dab980..1532ee47c3 100755 --- a/scripts/test.sh +++ b/scripts/test.sh @@ -82,6 +82,8 @@ function test_contrib() { sqlx_sqlite sqlx_mssql mongodb + diesel_mysql + diesel_postgres ) SYNC_DB_POOLS_FEATURES=( From c2936fcb1e4f8f4907889b54a9e4e741a565a7d7 Mon Sep 17 00:00:00 2001 From: Sergio Benitez Date: Tue, 11 Jul 2023 13:18:35 -0700 Subject: [PATCH 158/166] Update 'yansi' to 1.0.0-rc. --- contrib/db_pools/lib/src/database.rs | 8 +-- contrib/dyn_templates/src/fairing.rs | 6 +- contrib/dyn_templates/src/lib.rs | 5 +- contrib/dyn_templates/src/metadata.rs | 5 +- contrib/sync_db_pools/lib/src/connection.rs | 11 ++-- core/lib/Cargo.toml | 3 +- core/lib/src/catcher/catcher.rs | 8 +-- core/lib/src/config/config.rs | 62 ++++++++++----------- core/lib/src/config/tls.rs | 5 +- core/lib/src/data/data_stream.rs | 4 +- core/lib/src/error.rs | 6 +- core/lib/src/fairing/fairings.rs | 6 +- core/lib/src/fs/server.rs | 4 +- core/lib/src/fs/temp_file.rs | 2 +- core/lib/src/log.rs | 51 +++++++---------- core/lib/src/outcome.rs | 2 +- core/lib/src/request/request.rs | 11 ++-- core/lib/src/response/debug.rs | 6 +- core/lib/src/rocket.rs | 14 ++--- core/lib/src/route/route.rs | 8 +-- core/lib/src/route/uri.rs | 5 +- core/lib/src/server.rs | 10 ++-- core/lib/src/shield/shield.rs | 4 +- core/lib/src/state.rs | 5 +- 24 files changed, 119 insertions(+), 132 deletions(-) diff --git a/contrib/db_pools/lib/src/database.rs b/contrib/db_pools/lib/src/database.rs index fc7ec870cd..4afbd1f3e4 100644 --- a/contrib/db_pools/lib/src/database.rs +++ b/contrib/db_pools/lib/src/database.rs @@ -122,10 +122,10 @@ pub trait Database: From + DerefMut + Send + Sy return Some(db); } - let dbtype = std::any::type_name::(); - let fairing = Paint::default(format!("{}::init()", dbtype)).bold(); - error!("Attempted to fetch unattached database `{}`.", Paint::default(dbtype).bold()); - info_!("`{}` fairing must be attached prior to using this database.", fairing); + let dbtype = std::any::type_name::().bold().primary(); + error!("Attempted to fetch unattached database `{}`.", dbtype); + info_!("`{}{}` fairing must be attached prior to using this database.", + dbtype.linger(), "::init()".clear()); None } } diff --git a/contrib/dyn_templates/src/fairing.rs b/contrib/dyn_templates/src/fairing.rs index 08e1c76800..8d10fec154 100644 --- a/contrib/dyn_templates/src/fairing.rs +++ b/contrib/dyn_templates/src/fairing.rs @@ -59,9 +59,9 @@ impl Fairing for TemplateFairing { let cm = rocket.state::() .expect("Template ContextManager registered in on_ignite"); - info!("{}{}:", Paint::emoji("📐 "), Paint::magenta("Templating")); - info_!("directory: {}", Paint::white(Source::from(&*cm.context().root))); - info_!("engines: {:?}", Paint::white(Engines::ENABLED_EXTENSIONS)); + info!("{}{}:", "📐 ".emoji(), "Templating".magenta()); + info_!("directory: {}", Source::from(&*cm.context().root).primary()); + info_!("engines: {:?}", Engines::ENABLED_EXTENSIONS.primary()); } #[cfg(debug_assertions)] diff --git a/contrib/dyn_templates/src/lib.rs b/contrib/dyn_templates/src/lib.rs index 4f25e5c902..8ae1040a07 100644 --- a/contrib/dyn_templates/src/lib.rs +++ b/contrib/dyn_templates/src/lib.rs @@ -181,6 +181,7 @@ use rocket::response::{self, Responder}; use rocket::http::{ContentType, Status}; use rocket::figment::{value::Value, error::Error}; use rocket::serde::Serialize; +use rocket::yansi::Paint; const DEFAULT_TEMPLATE_DIR: &str = "templates"; @@ -441,8 +442,8 @@ impl<'r> Responder<'r, 'static> for Template { impl Sentinel for Template { fn abort(rocket: &Rocket) -> bool { if rocket.state::().is_none() { - let template = rocket::yansi::Paint::default("Template").bold(); - let fairing = rocket::yansi::Paint::default("Template::fairing()").bold(); + let template = "Template".primary().bold(); + let fairing = "Template::fairing()".primary().bold(); error!("returning `{}` responder without attaching `{}`.", template, fairing); info_!("To use or query templates, you must attach `{}`.", fairing); info_!("See the `Template` documentation for more information."); diff --git a/contrib/dyn_templates/src/metadata.rs b/contrib/dyn_templates/src/metadata.rs index feceab36d3..06d722d2d1 100644 --- a/contrib/dyn_templates/src/metadata.rs +++ b/contrib/dyn_templates/src/metadata.rs @@ -4,6 +4,7 @@ use rocket::{Request, Rocket, Ignite, Sentinel}; use rocket::http::{Status, ContentType}; use rocket::request::{self, FromRequest}; use rocket::serde::Serialize; +use rocket::yansi::Paint; use crate::{Template, context::ContextManager}; @@ -126,8 +127,8 @@ impl Metadata<'_> { impl Sentinel for Metadata<'_> { fn abort(rocket: &Rocket) -> bool { if rocket.state::().is_none() { - let md = rocket::yansi::Paint::default("Metadata").bold(); - let fairing = rocket::yansi::Paint::default("Template::fairing()").bold(); + let md = "Metadata".primary().bold(); + let fairing = "Template::fairing()".primary().bold(); error!("requested `{}` guard without attaching `{}`.", md, fairing); info_!("To use or query templates, you must attach `{}`.", fairing); info_!("See the `Template` documentation for more information."); diff --git a/contrib/sync_db_pools/lib/src/connection.rs b/contrib/sync_db_pools/lib/src/connection.rs index 355a55d100..a52aae72ff 100644 --- a/contrib/sync_db_pools/lib/src/connection.rs +++ b/contrib/sync_db_pools/lib/src/connection.rs @@ -224,10 +224,13 @@ impl Sentinel for Connection { use rocket::yansi::Paint; if rocket.state::>().is_none() { - let conn = Paint::default(std::any::type_name::()).bold(); - let fairing = Paint::default(format!("{}::fairing()", conn)).wrap().bold(); - error!("requesting `{}` DB connection without attaching `{}`.", conn, fairing); - info_!("Attach `{}` to use database connection pooling.", fairing); + let conn = std::any::type_name::().primary().bold(); + error!("requesting `{}` DB connection without attaching `{}{}`.", + conn, conn.linger(), "::fairing()".clear()); + + info_!("Attach `{}{}` to use database connection pooling.", + conn.linger(), "::fairing()".clear()); + return true; } diff --git a/core/lib/Cargo.toml b/core/lib/Cargo.toml index 44de9448e3..bed0ec8b76 100644 --- a/core/lib/Cargo.toml +++ b/core/lib/Cargo.toml @@ -37,13 +37,12 @@ uuid_ = { package = "uuid", version = "1", optional = true, features = ["serde"] # Non-optional, core dependencies from here on out. futures = { version = "0.3.0", default-features = false, features = ["std"] } -yansi = "0.5" +yansi = { version = "1.0.0-rc", features = ["detect-tty"] } log = { version = "0.4", features = ["std"] } num_cpus = "1.0" time = { version = "0.3", features = ["macros", "parsing"] } memchr = "2" # TODO: Use pear instead. binascii = "0.1" -is-terminal = "0.4.3" ref-cast = "1.0" atomic = "0.5" parking_lot = "0.12" diff --git a/core/lib/src/catcher/catcher.rs b/core/lib/src/catcher/catcher.rs index 9068ea19cf..faa54758f3 100644 --- a/core/lib/src/catcher/catcher.rs +++ b/core/lib/src/catcher/catcher.rs @@ -343,14 +343,14 @@ impl From for Catcher { impl fmt::Display for Catcher { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { if let Some(ref n) = self.name { - write!(f, "{}{}{} ", Paint::cyan("("), Paint::white(n), Paint::cyan(")"))?; + write!(f, "{}{}{} ", "(".cyan(), n.primary(), ")".cyan())?; } - write!(f, "{} ", Paint::green(self.base.path()))?; + write!(f, "{} ", self.base.path().green())?; match self.code { - Some(code) => write!(f, "{}", Paint::blue(code)), - None => write!(f, "{}", Paint::blue("default")) + Some(code) => write!(f, "{}", code.blue()), + None => write!(f, "{}", "default".blue()), } } } diff --git a/core/lib/src/config/config.rs b/core/lib/src/config/config.rs index 993d7c00bf..61b25bffa7 100644 --- a/core/lib/src/config/config.rs +++ b/core/lib/src/config/config.rs @@ -4,7 +4,7 @@ use figment::{Figment, Profile, Provider, Metadata, error::Result}; use figment::providers::{Serialized, Env, Toml, Format}; use figment::value::{Map, Dict, magic::RelativePathBuf}; use serde::{Deserialize, Serialize}; -use yansi::Paint; +use yansi::{Paint, Style, Color::Primary}; use crate::log::PaintExt; use crate::config::{LogLevel, Shutdown, Ident}; @@ -383,7 +383,7 @@ impl Config { trace!("-- configuration trace information --"); for param in Self::PARAMETERS { if let Some(meta) = figment.find_metadata(param) { - let (param, name) = (Paint::blue(param), Paint::white(&meta.name)); + let (param, name) = (param.blue(), meta.name.primary()); if let Some(ref source) = meta.source { trace_!("{:?} parameter source: {} ({})", param, name, source); } else { @@ -394,52 +394,50 @@ impl Config { } pub(crate) fn pretty_print(&self, figment: &Figment) { - fn bold(val: T) -> Paint { - Paint::default(val).bold() - } + static VAL: Style = Primary.bold(); self.trace_print(figment); - launch_meta!("{}Configured for {}.", Paint::emoji("🔧 "), self.profile); - launch_meta_!("address: {}", bold(&self.address)); - launch_meta_!("port: {}", bold(&self.port)); - launch_meta_!("workers: {}", bold(self.workers)); - launch_meta_!("max blocking threads: {}", bold(self.max_blocking)); - launch_meta_!("ident: {}", bold(&self.ident)); + launch_meta!("{}Configured for {}.", "🔧 ".emoji(), self.profile.underline()); + launch_meta_!("address: {}", self.address.paint(VAL)); + launch_meta_!("port: {}", self.port.paint(VAL)); + launch_meta_!("workers: {}", self.workers.paint(VAL)); + launch_meta_!("max blocking threads: {}", self.max_blocking.paint(VAL)); + launch_meta_!("ident: {}", self.ident.paint(VAL)); match self.ip_header { - Some(ref name) => launch_meta_!("IP header: {}", bold(name)), - None => launch_meta_!("IP header: {}", bold("disabled")) + Some(ref name) => launch_meta_!("IP header: {}", name.paint(VAL)), + None => launch_meta_!("IP header: {}", "disabled".paint(VAL)) } - launch_meta_!("limits: {}", bold(&self.limits)); - launch_meta_!("temp dir: {}", bold(&self.temp_dir.relative().display())); - launch_meta_!("http/2: {}", bold(cfg!(feature = "http2"))); + launch_meta_!("limits: {}", (&self.limits).paint(VAL)); + launch_meta_!("temp dir: {}", self.temp_dir.relative().display().paint(VAL)); + launch_meta_!("http/2: {}", (cfg!(feature = "http2").paint(VAL))); match self.keep_alive { - 0 => launch_meta_!("keep-alive: {}", bold("disabled")), - ka => launch_meta_!("keep-alive: {}{}", bold(ka), bold("s")), + 0 => launch_meta_!("keep-alive: {}", "disabled".paint(VAL)), + ka => launch_meta_!("keep-alive: {}{}", ka.paint(VAL), "s".paint(VAL)), } match (self.tls_enabled(), self.mtls_enabled()) { - (true, true) => launch_meta_!("tls: {}", bold("enabled w/mtls")), - (true, false) => launch_meta_!("tls: {} w/o mtls", bold("enabled")), - (false, _) => launch_meta_!("tls: {}", bold("disabled")), + (true, true) => launch_meta_!("tls: {}", "enabled w/mtls".paint(VAL)), + (true, false) => launch_meta_!("tls: {} w/o mtls", "enabled".paint(VAL)), + (false, _) => launch_meta_!("tls: {}", "disabled".paint(VAL)), } - launch_meta_!("shutdown: {}", bold(&self.shutdown)); - launch_meta_!("log level: {}", bold(self.log_level)); - launch_meta_!("cli colors: {}", bold(&self.cli_colors)); + launch_meta_!("shutdown: {}", self.shutdown.paint(VAL)); + launch_meta_!("log level: {}", self.log_level.paint(VAL)); + launch_meta_!("cli colors: {}", self.cli_colors.paint(VAL)); // Check for now deprecated config values. for (key, replacement) in Self::DEPRECATED_KEYS { if let Some(md) = figment.find_metadata(key) { - warn!("found value for deprecated config key `{}`", Paint::white(key)); + warn!("found value for deprecated config key `{}`", key.paint(VAL)); if let Some(ref source) = md.source { - launch_meta_!("in {} {}", Paint::white(source), md.name); + launch_meta_!("in {} {}", source.paint(VAL), md.name); } if let Some(new_key) = replacement { - launch_meta_!("key has been by replaced by `{}`", Paint::white(new_key)); + launch_meta_!("key has been by replaced by `{}`", new_key.paint(VAL)); } else { launch_meta_!("key has no special meaning"); } @@ -449,10 +447,10 @@ impl Config { // Check for now removed config values. for (prefix, replacement) in Self::DEPRECATED_PROFILES { if let Some(profile) = figment.profiles().find(|p| p.starts_with(prefix)) { - warn!("found set deprecated profile `{}`", Paint::white(profile)); + warn!("found set deprecated profile `{}`", profile.paint(VAL)); if let Some(new_profile) = replacement { - launch_meta_!("profile was replaced by `{}`", Paint::white(new_profile)); + launch_meta_!("profile was replaced by `{}`", new_profile.paint(VAL)); } else { launch_meta_!("profile `{}` has no special meaning", profile); } @@ -460,11 +458,11 @@ impl Config { } #[cfg(feature = "secrets")] { - launch_meta_!("secret key: {}", bold(&self.secret_key)); + launch_meta_!("secret key: {}", self.secret_key.paint(VAL)); if !self.secret_key.is_provided() { warn!("secrets enabled without a stable `secret_key`"); launch_meta_!("disable `secrets` feature or configure a `secret_key`"); - launch_meta_!("this becomes an {} in non-debug profiles", Paint::red("error")); + launch_meta_!("this becomes an {} in non-debug profiles", "error".red()); } } } @@ -598,7 +596,7 @@ pub fn pretty_print_error(error: figment::Error) { crate::log::init_default(); error!("Failed to extract valid configuration."); for e in error { - fn w(v: T) -> Paint { Paint::white(v) } + fn w(v: T) -> yansi::Painted { Paint::new(v).primary() } match e.kind { Kind::Message(msg) => error_!("{}", msg), diff --git a/core/lib/src/config/tls.rs b/core/lib/src/config/tls.rs index 06755d7cec..41e88082a7 100644 --- a/core/lib/src/config/tls.rs +++ b/core/lib/src/config/tls.rs @@ -644,8 +644,9 @@ mod with_tls_feature { Either::Left(path) => { let path = path.relative(); let file = fs::File::open(&path).map_err(move |e| { - Error::new(e.kind(), format!("error reading TLS file `{}`: {}", - Paint::white(figment::Source::File(path)), e)) + let source = figment::Source::File(path); + let msg = format!("error reading TLS file `{}`: {}", source.primary(), e); + Error::new(e.kind(), msg) })?; Ok(Box::new(io::BufReader::new(file))) diff --git a/core/lib/src/data/data_stream.rs b/core/lib/src/data/data_stream.rs index b335547662..a9a9e07ad7 100644 --- a/core/lib/src/data/data_stream.rs +++ b/core/lib/src/data/data_stream.rs @@ -7,6 +7,7 @@ use tokio::fs::File; use tokio::io::{AsyncRead, AsyncWrite, AsyncReadExt, ReadBuf, Take}; use futures::stream::Stream; use futures::ready; +use yansi::Paint; use crate::http::hyper; use crate::ext::{PollExt, Chain}; @@ -261,8 +262,7 @@ impl AsyncRead for DataStream<'_> { StreamKind::Multipart(_) => "a multipart form field", }; - let msg = yansi::Paint::default(kind).bold(); - warn_!("Data limit reached while reading {}.", msg); + warn_!("Data limit reached while reading {}.", kind.primary().bold()); } Pin::new(&mut self.chain).poll_read(cx, buf) diff --git a/core/lib/src/error.rs b/core/lib/src/error.rs index 2dc361cccc..b68b255bcb 100644 --- a/core/lib/src/error.rs +++ b/core/lib/src/error.rs @@ -188,7 +188,7 @@ impl Error { error!("Rocket failed to launch due to the following {} collisions:", kind); for &(ref a, ref b) in collisions { - info_!("{} {} {}", a, Paint::red("collides with").italic(), b) + info_!("{} {} {}", a, "collides with".red().italic(), b) } } @@ -208,7 +208,7 @@ impl Error { } ErrorKind::InsecureSecretKey(profile) => { error!("secrets enabled in non-debug without `secret_key`"); - info_!("selected profile: {}", Paint::default(profile).bold()); + info_!("selected profile: {}", profile.primary().bold()); info_!("disable `secrets` feature or configure a `secret_key`"); "aborting due to insecure configuration" } @@ -219,7 +219,7 @@ impl Error { ErrorKind::SentinelAborts(ref failures) => { error!("Rocket failed to launch due to aborting sentinels:"); for sentry in failures { - let name = Paint::default(sentry.type_name).bold(); + let name = sentry.type_name.primary().bold(); let (file, line, col) = sentry.location; info_!("{} ({}:{}:{})", name, file, line, col); } diff --git a/core/lib/src/fairing/fairings.rs b/core/lib/src/fairing/fairings.rs index dbc78b7003..12a99c08c4 100644 --- a/core/lib/src/fairing/fairings.rs +++ b/core/lib/src/fairing/fairings.rs @@ -173,11 +173,11 @@ impl Fairings { pub fn pretty_print(&self) { let active_fairings = self.active().collect::>(); if !active_fairings.is_empty() { - launch_meta!("{}{}:", Paint::emoji("📡 "), Paint::magenta("Fairings")); + launch_meta!("{}{}:", "📡 ".emoji(), "Fairings".magenta()); for (_, fairing) in iter!(self, active_fairings.into_iter()) { - launch_meta_!("{} ({})", Paint::default(fairing.info().name).bold(), - Paint::blue(fairing.info().kind).bold()); + let (name, kind) = (fairing.info().name, fairing.info().kind); + launch_meta_!("{} ({})", name.primary().bold(), kind.blue().bold()); } } } diff --git a/core/lib/src/fs/server.rs b/core/lib/src/fs/server.rs index 267c419163..da78ec3374 100644 --- a/core/lib/src/fs/server.rs +++ b/core/lib/src/fs/server.rs @@ -147,12 +147,12 @@ impl FileServer { if !options.contains(Options::Missing) { if !options.contains(Options::IndexFile) && !path.is_dir() { let path = path.display(); - error!("FileServer path '{}' is not a directory.", Paint::white(path)); + error!("FileServer path '{}' is not a directory.", path.primary()); warn_!("Aborting early to prevent inevitable handler failure."); panic!("invalid directory: refusing to continue"); } else if !path.exists() { let path = path.display(); - error!("FileServer path '{}' is not a file.", Paint::white(path)); + error!("FileServer path '{}' is not a file.", path.primary()); warn_!("Aborting early to prevent inevitable handler failure."); panic!("invalid file: refusing to continue"); } diff --git a/core/lib/src/fs/temp_file.rs b/core/lib/src/fs/temp_file.rs index 6924b40c0f..0f0ca9f5d0 100644 --- a/core/lib/src/fs/temp_file.rs +++ b/core/lib/src/fs/temp_file.rs @@ -536,7 +536,7 @@ impl<'r> FromData<'r> for Capped> { let has_form = |ty: &ContentType| ty.is_form_data() || ty.is_form(); if req.content_type().map_or(false, has_form) { - let (tf, form) = (Paint::white("TempFile<'_>"), Paint::white("Form>")); + let (tf, form) = ("TempFile<'_>".primary(), "Form>".primary()); warn_!("Request contains a form that will not be processed."); info_!("Bare `{}` data guard writes raw, unprocessed streams to disk.", tf); info_!("Did you mean to use `{}` instead?", form); diff --git a/core/lib/src/log.rs b/core/lib/src/log.rs index f20640718f..87f5118942 100644 --- a/core/lib/src/log.rs +++ b/core/lib/src/log.rs @@ -4,9 +4,8 @@ use std::fmt; use std::str::FromStr; use std::sync::atomic::{AtomicBool, Ordering}; -use is_terminal::IsTerminal; use serde::{de, Serialize, Serializer, Deserialize, Deserializer}; -use yansi::Paint; +use yansi::{Paint, Painted, Condition}; /// Reexport the `log` crate as `private`. pub use log as private; @@ -81,8 +80,8 @@ pub enum LogLevel { Off, } -pub trait PaintExt { - fn emoji(item: &str) -> Paint<&str>; +pub trait PaintExt: Sized { + fn emoji(self) -> Painted; } // Whether a record is a special `launch_{meta,info}!` record. @@ -116,7 +115,7 @@ impl log::Log for RocketLogger { // In Rocket, we abuse targets with suffix "_" to indicate indentation. let indented = record.target().ends_with('_'); if indented { - write_out!(" {} ", Paint::default(">>").bold()); + write_out!(" {} ", ">>".bold()); } // Downgrade a physical launch `warn` to logical `info`. @@ -126,27 +125,23 @@ impl log::Log for RocketLogger { match level { log::Level::Error if !indented => { - write_out!("{} {}\n", - Paint::red("Error:").bold(), - Paint::red(record.args()).wrap()); + write_out!("{} {}\n", "Error:".red().bold(), record.args().red().wrap()); } log::Level::Warn if !indented => { - write_out!("{} {}\n", - Paint::yellow("Warning:").bold(), - Paint::yellow(record.args()).wrap()); + write_out!("{} {}\n", "Warning:".yellow().bold(), record.args().yellow().wrap()); } - log::Level::Info => write_out!("{}\n", Paint::blue(record.args()).wrap()), - log::Level::Trace => write_out!("{}\n", Paint::magenta(record.args()).wrap()), - log::Level::Warn => write_out!("{}\n", Paint::yellow(record.args()).wrap()), - log::Level::Error => write_out!("{}\n", Paint::red(record.args()).wrap()), + log::Level::Info => write_out!("{}\n", record.args().blue().wrap()), + log::Level::Trace => write_out!("{}\n", record.args().magenta().wrap()), + log::Level::Warn => write_out!("{}\n", record.args().yellow().wrap()), + log::Level::Error => write_out!("{}\n", &record.args().red().wrap()), log::Level::Debug => { - write_out!("\n{} ", Paint::blue("-->").bold()); + write_out!("\n{} ", "-->".blue().bold()); if let Some(file) = record.file() { - write_out!("{}", Paint::blue(file)); + write_out!("{}", file.blue()); } if let Some(line) = record.line() { - write_out!(":{}\n", Paint::blue(line)); + write_out!(":{}\n", line.blue()); } write_out!("\t{}\n", record.args()); @@ -171,18 +166,12 @@ pub(crate) fn init(config: &crate::Config) { ROCKET_LOGGER_SET.store(true, Ordering::Release); } - // Always disable colors if requested or if they won't work on Windows. - if !config.cli_colors || !Paint::enable_windows_ascii() { - Paint::disable(); - } + // Always disable colors if requested or if the stdout/err aren't TTYs. + let should_color = config.cli_colors && Condition::stdouterr_are_tty(); + yansi::whenever(Condition::cached(should_color)); // Set Rocket-logger specific settings only if Rocket's logger is set. if ROCKET_LOGGER_SET.load(Ordering::Acquire) { - // Rocket logs to stdout, so disable coloring if it's not a TTY. - if !std::io::stdout().is_terminal() { - Paint::disable(); - } - log::set_max_level(config.log_level.into()); } } @@ -247,10 +236,10 @@ impl<'de> Deserialize<'de> for LogLevel { } } -impl PaintExt for Paint<&str> { +impl PaintExt for &str { /// Paint::masked(), but hidden on Windows due to broken output. See #1122. - fn emoji(_item: &str) -> Paint<&str> { - #[cfg(windows)] { Paint::masked("") } - #[cfg(not(windows))] { Paint::masked(_item) } + fn emoji(self) -> Painted { + #[cfg(windows)] { Paint::new("").mask() } + #[cfg(not(windows))] { Paint::new(self).mask() } } } diff --git a/core/lib/src/outcome.rs b/core/lib/src/outcome.rs index df3dbc1ae8..44a38f89de 100644 --- a/core/lib/src/outcome.rs +++ b/core/lib/src/outcome.rs @@ -765,6 +765,6 @@ impl fmt::Debug for Outcome { impl fmt::Display for Outcome { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { let (color, string) = self.formatting(); - write!(f, "{}", Paint::default(string).fg(color)) + write!(f, "{}", string.paint(color)) } } diff --git a/core/lib/src/request/request.rs b/core/lib/src/request/request.rs index 0b44d13199..62a2bb1e62 100644 --- a/core/lib/src/request/request.rs +++ b/core/lib/src/request/request.rs @@ -1110,15 +1110,12 @@ impl fmt::Debug for Request<'_> { impl fmt::Display for Request<'_> { /// Pretty prints a Request. Primarily used by Rocket's logging. fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "{} {}", Paint::green(self.method()), Paint::blue(&self.uri))?; + write!(f, "{} {}", self.method().green(), self.uri.blue())?; // Print the requests media type when the route specifies a format. - if let Some(media_type) = self.format() { - if !media_type.is_any() { - write!(f, " {}{}{}", - Paint::yellow(media_type.top()), - Paint::yellow("/"), - Paint::yellow(media_type.sub()))?; + if let Some(mime) = self.format() { + if !mime.is_any() { + write!(f, " {}/{}", mime.top().yellow().linger(), mime.sub().clear())?; } } diff --git a/core/lib/src/response/debug.rs b/core/lib/src/response/debug.rs index 2c6e5c26a7..030b537020 100644 --- a/core/lib/src/response/debug.rs +++ b/core/lib/src/response/debug.rs @@ -78,8 +78,8 @@ impl From for Debug { 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); + warn_!("Debug: {:?}", self.0.primary()); + warn_!("Debug always responds with {}.", Status::InternalServerError.primary()); Err(Status::InternalServerError) } } @@ -87,7 +87,7 @@ impl<'r, E: std::fmt::Debug> Responder<'r, 'static> for Debug { /// 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)); + warn_!("I/O Error: {:?}", self.primary()); Err(Status::InternalServerError) } } diff --git a/core/lib/src/rocket.rs b/core/lib/src/rocket.rs index e3ceb17bb8..ef9fdfecfa 100644 --- a/core/lib/src/rocket.rs +++ b/core/lib/src/rocket.rs @@ -255,7 +255,7 @@ impl Rocket { Err(e) => { error!("invalid {} base: {}", kind, Paint::white(&base)); error_!("{}", e); - info_!("{} {}", Paint::white("in"), std::panic::Location::caller()); + info_!("{} {}", "in".primary(), std::panic::Location::caller()); panic!("aborting due to {} base error", kind); } }; @@ -600,7 +600,7 @@ fn log_items(e: &str, t: &str, items: I, base: B, origin: O) { let mut items: Vec<_> = items.collect(); if !items.is_empty() { - launch_meta!("{}{}:", Paint::emoji(e), Paint::magenta(t)); + launch_meta!("{}{}:", e.emoji(), t.magenta()); } items.sort_by_key(|i| origin(i).path().as_str().chars().count()); @@ -678,9 +678,7 @@ impl Rocket { async fn _local_launch(self) -> Rocket { let rocket = self.into_orbit(); rocket.fairings.handle_liftoff(&rocket).await; - launch_info!("{}{}", Paint::emoji("🚀 "), - Paint::default("Rocket has launched into local orbit").bold()); - + launch_info!("{}{}", "🚀 ".emoji(), "Rocket has launched locally".primary().bold()); rocket } @@ -693,9 +691,9 @@ impl Rocket { let socket_addr = SocketAddr::new(rkt.config.address, rkt.config.port); let addr = format!("{}://{}", proto, socket_addr); launch_info!("{}{} {}", - Paint::emoji("🚀 "), - Paint::default("Rocket has launched from").bold(), - Paint::default(addr).bold().underline()); + "🚀 ".emoji(), + "Rocket has launched from".bold().primary().linger(), + addr.underline()); })) .await .map(|rocket| rocket.into_ignite()) diff --git a/core/lib/src/route/route.rs b/core/lib/src/route/route.rs index 44ef2790c0..24853d9517 100644 --- a/core/lib/src/route/route.rs +++ b/core/lib/src/route/route.rs @@ -346,18 +346,18 @@ impl Route { impl fmt::Display for Route { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { if let Some(ref n) = self.name { - write!(f, "{}{}{} ", Paint::cyan("("), Paint::white(n), Paint::cyan(")"))?; + write!(f, "{}{}{} ", "(".cyan(), n.primary(), ")".cyan())?; } - write!(f, "{} ", Paint::green(&self.method))?; + write!(f, "{} ", self.method.green())?; self.uri.color_fmt(f)?; if self.rank > 1 { - write!(f, " [{}]", Paint::default(&self.rank).bold())?; + write!(f, " [{}]", self.rank.primary().bold())?; } if let Some(ref format) = self.format { - write!(f, " {}", Paint::yellow(format))?; + write!(f, " {}", format.yellow())?; } Ok(()) diff --git a/core/lib/src/route/uri.rs b/core/lib/src/route/uri.rs index e440270faa..09072ccd91 100644 --- a/core/lib/src/route/uri.rs +++ b/core/lib/src/route/uri.rs @@ -247,10 +247,9 @@ impl<'a> RouteUri<'a> { .map(|raw| raw.as_str()) .unwrap_or(unmounted.as_str()); - write!(f, "{}", Paint::blue(self.base()).underline())?; - write!(f, "{}", Paint::blue(unmounted_part))?; + write!(f, "{}{}", self.base().blue().underline(), unmounted_part.blue())?; if let Some(q) = self.unmounted().query() { - write!(f, "{}{}", Paint::yellow("?"), Paint::yellow(q))?; + write!(f, "{}{}", "?".yellow(), q.yellow())?; } Ok(()) diff --git a/core/lib/src/server.rs b/core/lib/src/server.rs index b00111357b..e3836984fd 100644 --- a/core/lib/src/server.rs +++ b/core/lib/src/server.rs @@ -31,7 +31,7 @@ async fn handle(name: Option<&str>, run: F) -> Option macro_rules! panic_info { ($name:expr, $e:expr) => {{ match $name { - Some(name) => error_!("Handler {} panicked.", Paint::white(name)), + Some(name) => error_!("Handler {} panicked.", name.primary()), None => error_!("A handler panicked.") }; @@ -129,7 +129,7 @@ impl Rocket { }; match self._send_response(response, tx).await { - Ok(()) => info_!("{}", Paint::green("Response succeeded.")), + Ok(()) => info_!("{}", "Response succeeded.".green()), Err(e) if remote_hungup(&e) => warn_!("Remote left: {}.", e), Err(e) => warn_!("Failed to write response: {}.", e), } @@ -284,7 +284,7 @@ impl Rocket { let mut response = match self.route(request, data).await { Outcome::Success(response) => response, Outcome::Forward((data, _)) if request.method() == Method::Head => { - info_!("Autohandling {} request.", Paint::default("HEAD").bold()); + info_!("Autohandling {} request.", "HEAD".primary().bold()); // Dispatch the request again with Method `GET`. request._set_method(Method::Get); @@ -334,7 +334,7 @@ impl Rocket { // Check if the request processing completed (Some) or if the // request needs to be forwarded. If it does, continue the loop // (None) to try again. - info_!("{} {}", Paint::default("Outcome:").bold(), outcome); + info_!("{} {}", "Outcome:".primary().bold(), outcome); match outcome { o@Outcome::Success(_) | o@Outcome::Failure(_) => return o, Outcome::Forward(forwarded) => (data, status) = forwarded, @@ -372,7 +372,7 @@ impl Rocket { .map(|result| result.map_err(Some)) .unwrap_or_else(|| Err(None)) } else { - let code = Paint::blue(status.code).bold(); + let code = status.code.blue().bold(); warn_!("No {} catcher registered. Using Rocket default.", code); Ok(crate::catcher::default_handler(status, req)) } diff --git a/core/lib/src/shield/shield.rs b/core/lib/src/shield/shield.rs index 1e58175feb..ea44814484 100644 --- a/core/lib/src/shield/shield.rs +++ b/core/lib/src/shield/shield.rs @@ -207,10 +207,10 @@ impl Fairing for Shield { } if !self.headers().is_empty() { - info!("{}{}:", Paint::emoji("🛡️ "), Paint::magenta("Shield")); + info!("{}{}:", "🛡️ ".emoji(), "Shield".magenta()); for header in self.headers() { - info_!("{}: {}", header.name(), Paint::default(header.value())); + info_!("{}: {}", header.name(), header.value().primary()); } if force_hsts { diff --git a/core/lib/src/state.rs b/core/lib/src/state.rs index 577e873b4c..6b5edcdf28 100644 --- a/core/lib/src/state.rs +++ b/core/lib/src/state.rs @@ -3,6 +3,7 @@ use std::ops::Deref; use std::any::type_name; use ref_cast::RefCast; +use yansi::Paint; use crate::{Phase, Rocket, Ignite, Sentinel}; use crate::request::{self, FromRequest, Request}; @@ -210,8 +211,8 @@ impl<'r, T: Send + Sync + 'static> FromRequest<'r> for &'r State { impl Sentinel for &State { fn abort(rocket: &Rocket) -> bool { if rocket.state::().is_none() { - let type_name = yansi::Paint::default(type_name::()).bold(); - error!("launching with unmanaged `{}` state.", type_name); + let type_name = type_name::(); + error!("launching with unmanaged `{}` state.", type_name.primary().bold()); info_!("Using `State` requires managing it with `.manage()`."); return true; } From c337f75f325e4e0ed756e2f716c98445c1675287 Mon Sep 17 00:00:00 2001 From: Sergio Benitez Date: Thu, 10 Aug 2023 16:22:28 -0400 Subject: [PATCH 159/166] Fix 'get_pending()' docs, functionality. The `get_pending()` method now properly decrypts private cookies that were present in the jar originally. Resolves #2591. --- core/lib/src/cookies.rs | 14 +++++++++++--- core/lib/tests/cookies-private.rs | 9 +++++---- 2 files changed, 16 insertions(+), 7 deletions(-) diff --git a/core/lib/src/cookies.rs b/core/lib/src/cookies.rs index d52a31f575..62515c055b 100644 --- a/core/lib/src/cookies.rs +++ b/core/lib/src/cookies.rs @@ -243,8 +243,9 @@ impl<'a> CookieJar<'a> { /// container with the name `name`, irrespective of whether the cookie was /// private or not. If no such cookie exists, returns `None`. /// - /// This _does not_ return cookies sent by the client in a request. To - /// retrieve such cookies, using [`CookieJar::get()`] or + /// In general, due to performance overhead, calling this method should be + /// avoided if it is known that a cookie called `name` is not pending. + /// Instead, prefer to use [`CookieJar::get()`] or /// [`CookieJar::get_private()`]. /// /// # Example @@ -268,7 +269,14 @@ impl<'a> CookieJar<'a> { } drop(ops); - self.get(name).cloned() + + #[cfg(feature = "secrets")] { + self.get_private(name).or_else(|| self.get(name).cloned()) + } + + #[cfg(not(feature = "secrets"))] { + self.get(name).cloned() + } } /// Adds `cookie` to this collection. diff --git a/core/lib/tests/cookies-private.rs b/core/lib/tests/cookies-private.rs index f7c807d651..e499ed34e8 100644 --- a/core/lib/tests/cookies-private.rs +++ b/core/lib/tests/cookies-private.rs @@ -36,6 +36,7 @@ fn cookie_get_private(jar: &CookieJar<'_>) -> String { assert_ne!(a, b.as_ref()); assert_ne!(a, c); assert_ne!(b.as_ref(), c); + assert_eq!(b, jar.get_pending("b")); format!( "{}{}{}", @@ -49,6 +50,7 @@ fn cookie_get_private(jar: &CookieJar<'_>) -> String { #[get("/oh-no")] fn cookie_get(jar: &CookieJar<'_>) -> String { let (a, b, c) = (jar.get("a"), jar.get("b"), jar.get("c")); + assert_eq!(b.cloned(), jar.get_pending("b")); format!( "{}{}{}", @@ -65,10 +67,8 @@ mod cookies_private_tests { use rocket::{Build, Rocket}; fn rocket() -> Rocket { - rocket::build().mount( - "/", - routes![cookie_add_private, cookie_get, cookie_get_private], - ) + rocket::build() + .mount("/", routes![cookie_add_private, cookie_get, cookie_get_private]) } #[test] @@ -79,6 +79,7 @@ mod cookies_private_tests { assert_eq!(cookies.iter().count(), 3); assert_eq!(cookies.get("a").unwrap().value(), "v1"); assert_eq!(cookies.get_private("b").unwrap().value(), "v2"); + assert_eq!(cookies.get_pending("b").unwrap().value(), "v2"); assert_ne!(cookies.get("b").unwrap().value(), "v2"); assert_eq!(cookies.get("c").unwrap().value(), "v3"); } From b4c8597194ab7be7fb1f688c23475762d4c46e35 Mon Sep 17 00:00:00 2001 From: Sergio Benitez Date: Fri, 11 Aug 2023 15:14:34 -0400 Subject: [PATCH 160/166] Update UI test expected results. --- .../ui-fail-stable/database-syntax.stderr | 2 +- .../tests/ui-fail-nightly/async-entry.stderr | 16 +- .../ui-fail-nightly/catch_type_errors.stderr | 64 ++++---- .../from_form_type_errors.stderr | 32 ++-- .../ui-fail-nightly/responder-types.stderr | 78 +++++----- .../ui-fail-nightly/route-type-errors.stderr | 144 +++++++++--------- .../ui-fail-nightly/typed-uri-bad-type.stderr | 124 +++++++-------- .../typed-uris-invalid-syntax.stderr | 2 +- .../uri_display_type_errors.stderr | 112 +++++++------- .../tests/ui-fail-stable/async-entry.stderr | 10 +- .../bad-ignored-segments.stderr | 4 +- .../codegen/tests/ui-fail-stable/catch.stderr | 16 +- .../tests/ui-fail-stable/from_form.stderr | 20 +-- .../route-attribute-general-syntax.stderr | 20 +-- .../route-path-bad-syntax.stderr | 42 ++--- .../ui-fail-stable/typed-uri-bad-type.stderr | 4 + .../typed-uris-bad-params.stderr | 56 +++---- .../typed-uris-invalid-syntax.stderr | 4 +- 18 files changed, 377 insertions(+), 373 deletions(-) diff --git a/contrib/sync_db_pools/codegen/tests/ui-fail-stable/database-syntax.stderr b/contrib/sync_db_pools/codegen/tests/ui-fail-stable/database-syntax.stderr index 9523d0ca86..e850902edf 100644 --- a/contrib/sync_db_pools/codegen/tests/ui-fail-stable/database-syntax.stderr +++ b/contrib/sync_db_pools/codegen/tests/ui-fail-stable/database-syntax.stderr @@ -31,7 +31,7 @@ error: `database` attribute can only be used on structs | ^^^^ error: `database` attribute can only be applied to structs with exactly one unnamed field - --- help: example: `struct MyDatabase(diesel::SqliteConnection);` + = help: example: `struct MyDatabase(diesel::SqliteConnection);` --> tests/ui-fail-stable/database-syntax.rs:43:11 | 43 | struct Bar(Connection, Connection); diff --git a/core/codegen/tests/ui-fail-nightly/async-entry.stderr b/core/codegen/tests/ui-fail-nightly/async-entry.stderr index 0c76b4b436..74374c654a 100644 --- a/core/codegen/tests/ui-fail-nightly/async-entry.stderr +++ b/core/codegen/tests/ui-fail-nightly/async-entry.stderr @@ -113,6 +113,14 @@ error[E0728]: `await` is only allowed inside `async` functions and blocks 73 | let _ = rocket::build().launch().await; | ^^^^^ only allowed inside `async` functions and blocks +error[E0277]: `main` has invalid return type `Rocket` + --> tests/ui-fail-nightly/async-entry.rs:94:20 + | +94 | async fn main() -> rocket::Rocket { + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ `main` can only return types that implement `Termination` + | + = help: consider using `()`, or a `Result` + error[E0308]: mismatched types --> tests/ui-fail-nightly/async-entry.rs:35:9 | @@ -171,11 +179,3 @@ error[E0308]: mismatched types | = note: expected struct `Rocket` found struct `std::string::String` - -error[E0277]: `main` has invalid return type `Rocket` - --> tests/ui-fail-nightly/async-entry.rs:94:20 - | -94 | async fn main() -> rocket::Rocket { - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ `main` can only return types that implement `Termination` - | - = help: consider using `()`, or a `Result` diff --git a/core/codegen/tests/ui-fail-nightly/catch_type_errors.stderr b/core/codegen/tests/ui-fail-nightly/catch_type_errors.stderr index 1f46c0f915..a461ccbdd4 100644 --- a/core/codegen/tests/ui-fail-nightly/catch_type_errors.stderr +++ b/core/codegen/tests/ui-fail-nightly/catch_type_errors.stderr @@ -7,14 +7,14 @@ error[E0277]: the trait bound `usize: Responder<'_, '_>` is not satisfied | ^^^^^ the trait `Responder<'_, '_>` is not implemented for `usize` | = help: the following other types implement trait `Responder<'r, 'o>`: - <&'o [u8] as Responder<'r, 'o>> - <&'o str as Responder<'r, 'o>> - <() as Responder<'r, 'static>> - <(ContentType, R) as Responder<'r, 'o>> - <(Status, R) as Responder<'r, 'o>> - as Responder<'r, 'o>> - as Responder<'r, 'static>> - as Responder<'r, 'static>> + as Responder<'r, 'o>> + as Responder<'r, 'static>> + as Responder<'r, 'static>> + as Responder<'r, 'o>> + as Responder<'r, 'o>> + > + as Responder<'r, 'r>> + > and $N others error[E0277]: the trait bound `bool: Responder<'_, '_>` is not satisfied @@ -26,14 +26,14 @@ error[E0277]: the trait bound `bool: Responder<'_, '_>` is not satisfied | ^^^^ the trait `Responder<'_, '_>` is not implemented for `bool` | = help: the following other types implement trait `Responder<'r, 'o>`: - <&'o [u8] as Responder<'r, 'o>> - <&'o str as Responder<'r, 'o>> - <() as Responder<'r, 'static>> - <(ContentType, R) as Responder<'r, 'o>> - <(Status, R) as Responder<'r, 'o>> - as Responder<'r, 'o>> - as Responder<'r, 'static>> - as Responder<'r, 'static>> + as Responder<'r, 'o>> + as Responder<'r, 'static>> + as Responder<'r, 'static>> + as Responder<'r, 'o>> + as Responder<'r, 'o>> + > + as Responder<'r, 'r>> + > and $N others error[E0308]: mismatched types @@ -59,14 +59,14 @@ error[E0277]: the trait bound `usize: Responder<'_, '_>` is not satisfied | ^^^^^ the trait `Responder<'_, '_>` is not implemented for `usize` | = help: the following other types implement trait `Responder<'r, 'o>`: - <&'o [u8] as Responder<'r, 'o>> - <&'o str as Responder<'r, 'o>> - <() as Responder<'r, 'static>> - <(ContentType, R) as Responder<'r, 'o>> - <(Status, R) as Responder<'r, 'o>> - as Responder<'r, 'o>> - as Responder<'r, 'static>> - as Responder<'r, 'static>> + as Responder<'r, 'o>> + as Responder<'r, 'static>> + as Responder<'r, 'static>> + as Responder<'r, 'o>> + as Responder<'r, 'o>> + > + as Responder<'r, 'r>> + > and $N others error[E0277]: the trait bound `usize: Responder<'_, '_>` is not satisfied @@ -78,12 +78,12 @@ error[E0277]: the trait bound `usize: Responder<'_, '_>` is not satisfied | ^^^^^ the trait `Responder<'_, '_>` is not implemented for `usize` | = help: the following other types implement trait `Responder<'r, 'o>`: - <&'o [u8] as Responder<'r, 'o>> - <&'o str as Responder<'r, 'o>> - <() as Responder<'r, 'static>> - <(ContentType, R) as Responder<'r, 'o>> - <(Status, R) as Responder<'r, 'o>> - as Responder<'r, 'o>> - as Responder<'r, 'static>> - as Responder<'r, 'static>> + as Responder<'r, 'o>> + as Responder<'r, 'static>> + as Responder<'r, 'static>> + as Responder<'r, 'o>> + as Responder<'r, 'o>> + > + as Responder<'r, 'r>> + > and $N others 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 2021692b31..3bee439403 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 @@ -5,14 +5,14 @@ error[E0277]: the trait bound `Unknown: FromFormField<'_>` is not satisfied | ^^^^^^^ the trait `FromFormField<'_>` is not implemented for `Unknown` | = help: the following other types implement trait `FromFormField<'v>`: - &'v [u8] - &'v str - Capped<&'v [u8]> - Capped<&'v str> - Capped> - Capped> - Capped - Cow<'v, str> + bool + isize + i8 + i16 + i32 + i64 + i128 + usize and $N others = note: required for `Unknown` to implement `FromForm<'r>` @@ -23,13 +23,13 @@ error[E0277]: the trait bound `Foo: FromFormField<'_>` is not satisfied | ^^^^^^^^^^ the trait `FromFormField<'_>` is not implemented for `Foo` | = help: the following other types implement trait `FromFormField<'v>`: - &'v [u8] - &'v str - Capped<&'v [u8]> - Capped<&'v str> - Capped> - Capped> - Capped - Cow<'v, str> + bool + isize + i8 + i16 + i32 + i64 + i128 + usize and $N others = note: required for `Foo` to implement `FromForm<'r>` diff --git a/core/codegen/tests/ui-fail-nightly/responder-types.stderr b/core/codegen/tests/ui-fail-nightly/responder-types.stderr index 84995fe04c..fd26ecfebe 100644 --- a/core/codegen/tests/ui-fail-nightly/responder-types.stderr +++ b/core/codegen/tests/ui-fail-nightly/responder-types.stderr @@ -5,14 +5,14 @@ error[E0277]: the trait bound `u8: Responder<'_, '_>` is not satisfied | ^^^^^^^^^ the trait `Responder<'_, '_>` is not implemented for `u8` | = help: the following other types implement trait `Responder<'r, 'o>`: - <&'o [u8] as Responder<'r, 'o>> - <&'o str as Responder<'r, 'o>> - <() as Responder<'r, 'static>> - <(ContentType, R) as Responder<'r, 'o>> - <(Status, R) as Responder<'r, 'o>> - as Responder<'r, 'o>> - as Responder<'r, 'static>> - as Responder<'r, 'static>> + > + > + > + > + as Responder<'r, 'o>> + as Responder<'r, 'static>> + as Responder<'r, 'static>> + as Responder<'r, 'o>> and $N others error[E0277]: the trait bound `Header<'_>: From` is not satisfied @@ -22,14 +22,14 @@ error[E0277]: the trait bound `Header<'_>: From` is not satisfied | ^^^^^^^^^ the trait `From` is not implemented for `Header<'_>` | = help: the following other types implement trait `From`: + as From>> + as From> + as From> as From<&Cookie<'_>>> + as From<&Referrer>> as From<&ExpectCt>> - as From<&Frame>> - as From<&Hsts>> as From<&NoSniff>> - as From<&Permission>> - as From<&Prefetch>> - as From<&Referrer>> + as From<&Hsts>> and $N others = note: required for `u8` to implement `Into>` note: required by a bound in `rocket::Response::<'r>::set_header` @@ -45,14 +45,14 @@ error[E0277]: the trait bound `u8: Responder<'_, '_>` is not satisfied | ^^^^^^^^^ the trait `Responder<'_, '_>` is not implemented for `u8` | = help: the following other types implement trait `Responder<'r, 'o>`: - <&'o [u8] as Responder<'r, 'o>> - <&'o str as Responder<'r, 'o>> - <() as Responder<'r, 'static>> - <(ContentType, R) as Responder<'r, 'o>> - <(Status, R) as Responder<'r, 'o>> - as Responder<'r, 'o>> - as Responder<'r, 'static>> - as Responder<'r, 'static>> + > + > + > + > + as Responder<'r, 'o>> + as Responder<'r, 'static>> + as Responder<'r, 'static>> + as Responder<'r, 'o>> and $N others error[E0277]: the trait bound `Header<'_>: From` is not satisfied @@ -62,14 +62,14 @@ error[E0277]: the trait bound `Header<'_>: From` is not satisfied | ^^^^^^^^^ the trait `From` is not implemented for `Header<'_>` | = help: the following other types implement trait `From`: + as From>> + as From> + as From> as From<&Cookie<'_>>> + as From<&Referrer>> as From<&ExpectCt>> - as From<&Frame>> - as From<&Hsts>> as From<&NoSniff>> - as From<&Permission>> - as From<&Prefetch>> - as From<&Referrer>> + as From<&Hsts>> and $N others = note: required for `u8` to implement `Into>` note: required by a bound in `rocket::Response::<'r>::set_header` @@ -85,14 +85,14 @@ error[E0277]: the trait bound `Header<'_>: From` is not sat | ^^^^^^^^^^^^ the trait `From` is not implemented for `Header<'_>` | = help: the following other types implement trait `From`: + as From>> + as From> + as From> as From<&Cookie<'_>>> + as From<&Referrer>> as From<&ExpectCt>> - as From<&Frame>> - as From<&Hsts>> as From<&NoSniff>> - as From<&Permission>> - as From<&Prefetch>> - as From<&Referrer>> + as From<&Hsts>> and $N others = note: required for `std::string::String` to implement `Into>` note: required by a bound in `rocket::Response::<'r>::set_header` @@ -108,14 +108,14 @@ error[E0277]: the trait bound `usize: Responder<'_, '_>` is not satisfied | ^^^^^ the trait `Responder<'_, '_>` is not implemented for `usize` | = help: the following other types implement trait `Responder<'r, 'o>`: - <&'o [u8] as Responder<'r, 'o>> - <&'o str as Responder<'r, 'o>> - <() as Responder<'r, 'static>> - <(ContentType, R) as Responder<'r, 'o>> - <(Status, R) as Responder<'r, 'o>> - as Responder<'r, 'o>> - as Responder<'r, 'static>> - as Responder<'r, 'static>> + > + > + > + > + as Responder<'r, 'o>> + as Responder<'r, 'static>> + as Responder<'r, 'static>> + as Responder<'r, 'o>> and $N others note: required by a bound in `route::handler::, Status, (rocket::Data<'o>, Status)>>::from` --> $WORKSPACE/core/lib/src/route/handler.rs 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 3aa69ad545..4339650ca5 100644 --- a/core/codegen/tests/ui-fail-nightly/route-type-errors.stderr +++ b/core/codegen/tests/ui-fail-nightly/route-type-errors.stderr @@ -5,14 +5,14 @@ error[E0277]: the trait bound `Q: FromParam<'_>` is not satisfied | ^ the trait `FromParam<'_>` is not implemented for `Q` | = help: the following other types implement trait `FromParam<'a>`: - &'a str - IpAddr - Ipv4Addr - Ipv6Addr - NonZeroI128 - NonZeroI16 - NonZeroI32 - NonZeroI64 + bool + isize + i8 + i16 + i32 + i64 + i128 + usize and $N others error[E0277]: the trait bound `Q: FromSegments<'_>` is not satisfied @@ -22,10 +22,10 @@ error[E0277]: the trait bound `Q: FromSegments<'_>` is not satisfied | ^ the trait `FromSegments<'_>` is not implemented for `Q` | = help: the following other types implement trait `FromSegments<'r>`: - > - >::Error> as FromSegments<'r>> as FromSegments<'r>> + > as FromSegments<'r>> + >::Error> as FromSegments<'r>> error[E0277]: the trait bound `Q: FromFormField<'_>` is not satisfied --> tests/ui-fail-nightly/route-type-errors.rs:12:12 @@ -34,14 +34,14 @@ error[E0277]: the trait bound `Q: FromFormField<'_>` is not satisfied | ^ the trait `FromFormField<'_>` is not implemented for `Q` | = help: the following other types implement trait `FromFormField<'v>`: - &'v [u8] - &'v str - Capped<&'v [u8]> - Capped<&'v str> - Capped> - Capped> - Capped - Cow<'v, str> + bool + isize + i8 + i16 + i32 + i64 + i128 + usize and $N others = note: required for `Q` to implement `FromForm<'_>` @@ -52,14 +52,14 @@ error[E0277]: the trait bound `Q: FromFormField<'_>` is not satisfied | ^ the trait `FromFormField<'_>` is not implemented for `Q` | = help: the following other types implement trait `FromFormField<'v>`: - &'v [u8] - &'v str - Capped<&'v [u8]> - Capped<&'v str> - Capped> - Capped> - Capped - Cow<'v, str> + bool + isize + i8 + i16 + i32 + i64 + i128 + usize and $N others = note: required for `Q` to implement `FromForm<'_>` @@ -70,14 +70,14 @@ error[E0277]: the trait bound `Q: FromData<'_>` is not satisfied | ^ the trait `FromData<'_>` is not implemented for `Q` | = help: the following other types implement trait `FromData<'r>`: - &'r RawStr - &'r [u8] - &'r str - Capped<&'r RawStr> - Capped<&'r [u8]> - Capped<&'r str> + rocket::Data<'r> + Cow<'_, str> Capped> + Capped> Capped> + Capped + Capped<&'r str> + Capped<&'r RawStr> and $N others error[E0277]: the trait bound `Q: FromRequest<'_>` is not satisfied @@ -87,14 +87,14 @@ error[E0277]: the trait bound `Q: FromRequest<'_>` is not satisfied | ^ the trait `FromRequest<'_>` is not implemented for `Q` | = help: the following other types implement trait `FromRequest<'r>`: - &'r ContentType - &'r Host<'r> - &'r Limits - &'r Route - &'r rocket::Config - &'r rocket::State - &'r rocket::http::Accept - &'r rocket::http::CookieJar<'r> + rocket::http::Method + Outcome>::Error), Status> + Flash<&'r rocket::http::CookieJar<'r>> + rocket::Shutdown + IpAddr + std::net::SocketAddr + std::option::Option + Result>::Error> and $N others error[E0277]: the trait bound `Q: FromParam<'_>` is not satisfied @@ -104,14 +104,14 @@ error[E0277]: the trait bound `Q: FromParam<'_>` is not satisfied | ^ the trait `FromParam<'_>` is not implemented for `Q` | = help: the following other types implement trait `FromParam<'a>`: - &'a str - IpAddr - Ipv4Addr - Ipv6Addr - NonZeroI128 - NonZeroI16 - NonZeroI32 - NonZeroI64 + bool + isize + i8 + i16 + i32 + i64 + i128 + usize and $N others error[E0277]: the trait bound `Q: FromRequest<'_>` is not satisfied @@ -121,14 +121,14 @@ error[E0277]: the trait bound `Q: FromRequest<'_>` is not satisfied | ^ the trait `FromRequest<'_>` is not implemented for `Q` | = help: the following other types implement trait `FromRequest<'r>`: - &'r ContentType - &'r Host<'r> - &'r Limits - &'r Route - &'r rocket::Config - &'r rocket::State - &'r rocket::http::Accept - &'r rocket::http::CookieJar<'r> + rocket::http::Method + Outcome>::Error), Status> + Flash<&'r rocket::http::CookieJar<'r>> + rocket::Shutdown + IpAddr + std::net::SocketAddr + std::option::Option + Result>::Error> and $N others error[E0277]: the trait bound `Q: FromParam<'_>` is not satisfied @@ -138,14 +138,14 @@ error[E0277]: the trait bound `Q: FromParam<'_>` is not satisfied | ^ the trait `FromParam<'_>` is not implemented for `Q` | = help: the following other types implement trait `FromParam<'a>`: - &'a str - IpAddr - Ipv4Addr - Ipv6Addr - NonZeroI128 - NonZeroI16 - NonZeroI32 - NonZeroI64 + bool + isize + i8 + i16 + i32 + i64 + i128 + usize and $N others error[E0277]: the trait bound `Q: FromParam<'_>` is not satisfied @@ -155,12 +155,12 @@ error[E0277]: the trait bound `Q: FromParam<'_>` is not satisfied | ^ the trait `FromParam<'_>` is not implemented for `Q` | = help: the following other types implement trait `FromParam<'a>`: - &'a str - IpAddr - Ipv4Addr - Ipv6Addr - NonZeroI128 - NonZeroI16 - NonZeroI32 - NonZeroI64 + bool + isize + i8 + i16 + i32 + i64 + i128 + usize and $N others 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 900f294759..2e47e9046f 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 @@ -17,9 +17,9 @@ error[E0277]: the trait bound `usize: FromUriParam` is not implemented for `usize` | = help: the following other types implement trait `FromUriParam`: - > - > > + > + > error[E0277]: the trait bound `usize: FromUriParam` is not satisfied --> tests/ui-fail-nightly/typed-uri-bad-type.rs:47:17 @@ -28,9 +28,9 @@ error[E0277]: the trait bound `usize: FromUriParam` is not implemented for `usize` | = help: the following other types implement trait `FromUriParam`: - > - > > + > + > error[E0277]: the trait bound `usize: FromUriParam` is not satisfied --> tests/ui-fail-nightly/typed-uri-bad-type.rs:49:22 @@ -39,9 +39,9 @@ error[E0277]: the trait bound `usize: FromUriParam` is not implemented for `usize` | = help: the following other types implement trait `FromUriParam`: - > - > > + > + > error[E0277]: the trait bound `S: FromUriParam` is not satisfied --> tests/ui-fail-nightly/typed-uri-bad-type.rs:51:30 @@ -50,14 +50,14 @@ error[E0277]: the trait bound `S: FromUriParam` | ^ the trait `FromUriParam` is not implemented for `S` | = help: the following other types implement trait `FromUriParam`: - <&'a std::path::Path as FromUriParam> - <&'a std::path::Path as FromUriParam> - <&'a std::path::Path as FromUriParam> - <&'a std::path::Path as FromUriParam> - <&'a std::path::Path as FromUriParam> - <&'a std::path::Path as FromUriParam> - <&'a str as FromUriParam> - <&'a str as FromUriParam> + > + > + > + > + > + > + > + > and $N others error[E0277]: the trait bound `i32: FromUriParam>` is not satisfied @@ -67,9 +67,9 @@ error[E0277]: the trait bound `i32: FromUriParam>` is not implemented for `i32` | = help: the following other types implement trait `FromUriParam`: + > > > - > = note: required for `std::option::Option` to implement `FromUriParam>` error[E0277]: the trait bound `std::string::String: FromUriParam>` is not satisfied @@ -79,12 +79,12 @@ error[E0277]: the trait bound `std::string::String: FromUriParam>` is not implemented for `std::string::String` | = help: the following other types implement trait `FromUriParam`: + > + > + > > > > - > - > - > = note: required for `Result` to implement `FromUriParam>` error[E0277]: the trait bound `isize: FromUriParam` is not satisfied @@ -94,9 +94,9 @@ error[E0277]: the trait bound `isize: FromUriParam` is not implemented for `isize` | = help: the following other types implement trait `FromUriParam`: + > > > - > error[E0277]: the trait bound `isize: FromUriParam` is not satisfied --> tests/ui-fail-nightly/typed-uri-bad-type.rs:60:24 @@ -105,9 +105,9 @@ error[E0277]: the trait bound `isize: FromUriParam` is not implemented for `isize` | = help: the following other types implement trait `FromUriParam`: + > > > - > error[E0277]: the trait bound `S: FromUriParam` is not satisfied --> tests/ui-fail-nightly/typed-uri-bad-type.rs:62:23 @@ -116,14 +116,14 @@ error[E0277]: the trait bound `S: FromUriParam | ^ the trait `FromUriParam` is not implemented for `S` | = help: the following other types implement trait `FromUriParam`: - <&'a std::path::Path as FromUriParam> - <&'a std::path::Path as FromUriParam> - <&'a std::path::Path as FromUriParam> - <&'a std::path::Path as FromUriParam> - <&'a std::path::Path as FromUriParam> - <&'a std::path::Path as FromUriParam> - <&'a str as FromUriParam> - <&'a str as FromUriParam> + > + > + > + > + > + > + > + > and $N others error[E0277]: the trait bound `S: FromUriParam` is not satisfied @@ -133,14 +133,14 @@ error[E0277]: the trait bound `S: FromUriParam | ^ the trait `FromUriParam` is not implemented for `S` | = help: the following other types implement trait `FromUriParam`: - <&'a std::path::Path as FromUriParam> - <&'a std::path::Path as FromUriParam> - <&'a std::path::Path as FromUriParam> - <&'a std::path::Path as FromUriParam> - <&'a std::path::Path as FromUriParam> - <&'a std::path::Path as FromUriParam> - <&'a str as FromUriParam> - <&'a str as FromUriParam> + > + > + > + > + > + > + > + > and $N others error[E0277]: the trait bound `S: Ignorable` is not satisfied @@ -150,8 +150,8 @@ error[E0277]: the trait bound `S: Ignorable` is n | ^ the trait `Ignorable` is not implemented for `S` | = help: the following other types implement trait `Ignorable

`: - Result std::option::Option + Result note: required by a bound in `assert_ignorable` --> $WORKSPACE/core/http/src/uri/fmt/uri_display.rs | @@ -165,8 +165,8 @@ error[E0277]: the trait bound `usize: Ignorable` | ^ the trait `Ignorable` is not implemented for `usize` | = help: the following other types implement trait `Ignorable

`: - Result std::option::Option + Result note: required by a bound in `assert_ignorable` --> $WORKSPACE/core/http/src/uri/fmt/uri_display.rs | @@ -180,14 +180,14 @@ error[E0277]: the trait bound `S: FromUriParam | ^ the trait `FromUriParam` is not implemented for `S` | = help: the following other types implement trait `FromUriParam`: - <&'a std::path::Path as FromUriParam> - <&'a std::path::Path as FromUriParam> - <&'a std::path::Path as FromUriParam> - <&'a std::path::Path as FromUriParam> - <&'a std::path::Path as FromUriParam> - <&'a std::path::Path as FromUriParam> - <&'a str as FromUriParam> - <&'a str as FromUriParam> + > + > + > + > + > + > + > + > and $N others error[E0277]: the trait bound `usize: FromUriParam` is not satisfied @@ -197,9 +197,9 @@ error[E0277]: the trait bound `usize: FromUriParam` is not implemented for `usize` | = help: the following other types implement trait `FromUriParam`: - > - > > + > + > error[E0277]: the trait bound `rocket::http::uri::Reference<'_>: ValidRoutePrefix` is not satisfied --> tests/ui-fail-nightly/typed-uri-bad-type.rs:77:15 @@ -211,8 +211,8 @@ error[E0277]: the trait bound `rocket::http::uri::Reference<'_>: ValidRoutePrefi | required by a bound introduced by this call | = help: the following other types implement trait `ValidRoutePrefix`: - rocket::http::uri::Absolute<'a> rocket::http::uri::Origin<'a> + rocket::http::uri::Absolute<'a> note: required by a bound in `RouteUriBuilder::with_prefix` --> $WORKSPACE/core/http/src/uri/fmt/formatter.rs | @@ -226,9 +226,9 @@ error[E0277]: the trait bound `usize: FromUriParam` is not implemented for `usize` | = help: the following other types implement trait `FromUriParam`: - > - > > + > + > error[E0277]: the trait bound `rocket::http::uri::Asterisk: ValidRoutePrefix` is not satisfied --> tests/ui-fail-nightly/typed-uri-bad-type.rs:78:15 @@ -240,8 +240,8 @@ error[E0277]: the trait bound `rocket::http::uri::Asterisk: ValidRoutePrefix` is | required by a bound introduced by this call | = help: the following other types implement trait `ValidRoutePrefix`: - rocket::http::uri::Absolute<'a> rocket::http::uri::Origin<'a> + rocket::http::uri::Absolute<'a> note: required by a bound in `RouteUriBuilder::with_prefix` --> $WORKSPACE/core/http/src/uri/fmt/formatter.rs | @@ -255,9 +255,9 @@ error[E0277]: the trait bound `usize: FromUriParam` is not implemented for `usize` | = help: the following other types implement trait `FromUriParam`: - > - > > + > + > error[E0277]: the trait bound `rocket::http::uri::Asterisk: ValidRouteSuffix>` is not satisfied --> tests/ui-fail-nightly/typed-uri-bad-type.rs:81:37 @@ -269,10 +269,10 @@ error[E0277]: the trait bound `rocket::http::uri::Asterisk: ValidRouteSuffix`: - as ValidRouteSuffix>> - as ValidRouteSuffix>> - as ValidRouteSuffix>> as ValidRouteSuffix>> + as ValidRouteSuffix>> + as ValidRouteSuffix>> + as ValidRouteSuffix>> note: required by a bound in `RouteUriBuilder::with_suffix` --> $WORKSPACE/core/http/src/uri/fmt/formatter.rs | @@ -288,9 +288,9 @@ error[E0277]: the trait bound `usize: FromUriParam` is not implemented for `usize` | = help: the following other types implement trait `FromUriParam`: - > - > > + > + > error[E0277]: the trait bound `rocket::http::uri::Origin<'_>: ValidRouteSuffix>` is not satisfied --> tests/ui-fail-nightly/typed-uri-bad-type.rs:82:37 @@ -302,10 +302,10 @@ error[E0277]: the trait bound `rocket::http::uri::Origin<'_>: ValidRouteSuffix`: - as ValidRouteSuffix>> - as ValidRouteSuffix>> - as ValidRouteSuffix>> as ValidRouteSuffix>> + as ValidRouteSuffix>> + as ValidRouteSuffix>> + as ValidRouteSuffix>> note: required by a bound in `RouteUriBuilder::with_suffix` --> $WORKSPACE/core/http/src/uri/fmt/formatter.rs | diff --git a/core/codegen/tests/ui-fail-nightly/typed-uris-invalid-syntax.stderr b/core/codegen/tests/ui-fail-nightly/typed-uris-invalid-syntax.stderr index e4726047ec..f7ae1881e5 100644 --- a/core/codegen/tests/ui-fail-nightly/typed-uris-invalid-syntax.stderr +++ b/core/codegen/tests/ui-fail-nightly/typed-uris-invalid-syntax.stderr @@ -96,7 +96,7 @@ error: unexpected token 27 | uri!(simple: id = ); | ^ -error: unexpected end of input, expected expression +error: unexpected end of input, expected an expression --> tests/ui-fail-nightly/typed-uris-invalid-syntax.rs:28:22 | 28 | uri!(simple(id = )); 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 0613f5aa5d..fb9c378940 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 @@ -5,14 +5,14 @@ error[E0277]: the trait bound `BadType: UriDisplay` is not implemented for `BadType` | = help: the following other types implement trait `UriDisplay

`: - <&T as UriDisplay

> - <&mut T as UriDisplay

> - as UriDisplay> - > - > - > - > - > + > + > + > + > + > + > + > + > and $N others = note: required for `&BadType` to implement `UriDisplay` note: required by a bound in `rocket::http::uri::fmt::Formatter::<'i, P>::write_value` @@ -28,14 +28,14 @@ error[E0277]: the trait bound `BadType: UriDisplay` is not implemented for `BadType` | = help: the following other types implement trait `UriDisplay

`: - <&T as UriDisplay

> - <&mut T as UriDisplay

> - as UriDisplay> - > - > - > - > - > + > + > + > + > + > + > + > + > and $N others = note: required for `&BadType` to implement `UriDisplay` note: required by a bound in `rocket::http::uri::fmt::Formatter::<'_, rocket::http::uri::fmt::Query>::write_named_value` @@ -51,14 +51,14 @@ error[E0277]: the trait bound `BadType: UriDisplay` is not implemented for `BadType` | = help: the following other types implement trait `UriDisplay

`: - <&T as UriDisplay

> - <&mut T as UriDisplay

> - as UriDisplay> - > - > - > - > - > + > + > + > + > + > + > + > + > and $N others = note: required for `&BadType` to implement `UriDisplay` note: required by a bound in `rocket::http::uri::fmt::Formatter::<'_, rocket::http::uri::fmt::Query>::write_named_value` @@ -74,14 +74,14 @@ error[E0277]: the trait bound `BadType: UriDisplay` is not implemented for `BadType` | = help: the following other types implement trait `UriDisplay

`: - <&T as UriDisplay

> - <&mut T as UriDisplay

> - as UriDisplay> - > - > - > - > - > + > + > + > + > + > + > + > + > and $N others = note: required for `&BadType` to implement `UriDisplay` = note: 1 redundant requirement hidden @@ -99,14 +99,14 @@ error[E0277]: the trait bound `BadType: UriDisplay` is not implemented for `BadType` | = help: the following other types implement trait `UriDisplay

`: - <&T as UriDisplay

> - <&mut T as UriDisplay

> - as UriDisplay> - > - > - > - > - > + > + > + > + > + > + > + > + > and $N others = note: required for `&BadType` to implement `UriDisplay` = note: 1 redundant requirement hidden @@ -124,14 +124,14 @@ error[E0277]: the trait bound `BadType: UriDisplay` is not implemented for `BadType` | = help: the following other types implement trait `UriDisplay

`: - <&T as UriDisplay

> - <&mut T as UriDisplay

> - as UriDisplay> - > - > - > - > - > + > + > + > + > + > + > + > + > and $N others = note: required for `&BadType` to implement `UriDisplay` = note: 1 redundant requirement hidden @@ -149,14 +149,14 @@ error[E0277]: the trait bound `BadType: UriDisplay | ^^^^^^^ the trait `UriDisplay` is not implemented for `BadType` | = help: the following other types implement trait `UriDisplay

`: - <&T as UriDisplay

> - <&mut T as UriDisplay

> - as UriDisplay> - > - > - > - > - > + > + > + > + > + > + > + > + > and $N others = note: required for `&BadType` to implement `UriDisplay` note: required by a bound in `rocket::http::uri::fmt::Formatter::<'i, P>::write_value` diff --git a/core/codegen/tests/ui-fail-stable/async-entry.stderr b/core/codegen/tests/ui-fail-stable/async-entry.stderr index dc1d54d2f2..dbd9512c43 100644 --- a/core/codegen/tests/ui-fail-stable/async-entry.stderr +++ b/core/codegen/tests/ui-fail-stable/async-entry.stderr @@ -27,7 +27,7 @@ error: [note] this function must be `async` | ^^ error: attribute cannot be applied to `main` function - --- note: this attribute generates a `main` function + = note: this attribute generates a `main` function --> tests/ui-fail-stable/async-entry.rs:49:5 | 49 | #[rocket::launch] @@ -70,7 +70,7 @@ error: [note] this function must return a value | ^^ error: attribute cannot be applied to `main` function - --- note: this attribute generates a `main` function + = note: this attribute generates a `main` function --> tests/ui-fail-stable/async-entry.rs:79:5 | 79 | #[rocket::launch] @@ -85,7 +85,7 @@ error: [note] this function cannot be `main` | ^^^^ error: attribute cannot be applied to `main` function - --- note: this attribute generates a `main` function + = note: this attribute generates a `main` function --> tests/ui-fail-stable/async-entry.rs:87:5 | 87 | #[rocket::launch] @@ -100,12 +100,12 @@ error: [note] this function cannot be `main` | ^^^^ error[E0728]: `await` is only allowed inside `async` functions and blocks - --> tests/ui-fail-stable/async-entry.rs:73:41 + --> tests/ui-fail-stable/async-entry.rs:73:42 | 72 | fn rocket() -> _ { | ------ this is not `async` 73 | let _ = rocket::build().launch().await; - | ^^^^^^ only allowed inside `async` functions and blocks + | ^^^^^ only allowed inside `async` functions and blocks error[E0308]: mismatched types --> tests/ui-fail-stable/async-entry.rs:35:9 diff --git a/core/codegen/tests/ui-fail-stable/bad-ignored-segments.stderr b/core/codegen/tests/ui-fail-stable/bad-ignored-segments.stderr index 9a79954b83..efb913cf13 100644 --- a/core/codegen/tests/ui-fail-stable/bad-ignored-segments.stderr +++ b/core/codegen/tests/ui-fail-stable/bad-ignored-segments.stderr @@ -1,12 +1,12 @@ error: parameter must be named - --- help: use a name such as `_guard` or `_param` + = help: use a name such as `_guard` or `_param` --> tests/ui-fail-stable/bad-ignored-segments.rs:6:7 | 6 | #[get("/c?<_>")] | ^^^^^^^^ error: parameter must be named - --- help: use a name such as `_guard` or `_param` + = help: use a name such as `_guard` or `_param` --> tests/ui-fail-stable/bad-ignored-segments.rs:9:21 | 9 | #[post("/d", data = "<_>")] diff --git a/core/codegen/tests/ui-fail-stable/catch.stderr b/core/codegen/tests/ui-fail-stable/catch.stderr index ac419d3126..49ded6be31 100644 --- a/core/codegen/tests/ui-fail-stable/catch.stderr +++ b/core/codegen/tests/ui-fail-stable/catch.stderr @@ -1,54 +1,54 @@ error: expected `fn` - --- help: `#[catch]` can only be used on functions + = help: `#[catch]` can only be used on functions --> tests/ui-fail-stable/catch.rs:6:1 | 6 | struct Catcher(String); | ^^^^^^ error: expected `fn` - --- help: `#[catch]` can only be used on functions + = help: `#[catch]` can only be used on functions --> tests/ui-fail-stable/catch.rs:9:7 | 9 | const CATCH: &str = "Catcher"; | ^^^^^ error: expected integer or `default`, found string literal - --- help: `#[catch]` expects a status code int or `default`: `#[catch(404)]` or `#[catch(default)]` + = help: `#[catch]` expects a status code int or `default`: `#[catch(404)]` or `#[catch(default)]` --> tests/ui-fail-stable/catch.rs:11:9 | 11 | #[catch("404")] | ^^^^^ error: unexpected keyed parameter: expected literal or identifier - --- help: `#[catch]` expects a status code int or `default`: `#[catch(404)]` or `#[catch(default)]` + = help: `#[catch]` expects a status code int or `default`: `#[catch(404)]` or `#[catch(default)]` --> tests/ui-fail-stable/catch.rs:14:9 | 14 | #[catch(code = "404")] | ^^^^ error: unexpected keyed parameter: expected literal or identifier - --- help: `#[catch]` expects a status code int or `default`: `#[catch(404)]` or `#[catch(default)]` + = help: `#[catch]` expects a status code int or `default`: `#[catch(404)]` or `#[catch(default)]` --> tests/ui-fail-stable/catch.rs:17:9 | 17 | #[catch(code = 404)] | ^^^^ error: status must be in range [100, 599] - --- help: `#[catch]` expects a status code int or `default`: `#[catch(404)]` or `#[catch(default)]` + = help: `#[catch]` expects a status code int or `default`: `#[catch(404)]` or `#[catch(default)]` --> tests/ui-fail-stable/catch.rs:20:9 | 20 | #[catch(99)] | ^^ error: status must be in range [100, 599] - --- help: `#[catch]` expects a status code int or `default`: `#[catch(404)]` or `#[catch(default)]` + = help: `#[catch]` expects a status code int or `default`: `#[catch(404)]` or `#[catch(default)]` --> tests/ui-fail-stable/catch.rs:23:9 | 23 | #[catch(600)] | ^^^ error: unexpected attribute parameter: `message` - --- help: `#[catch]` expects a status code int or `default`: `#[catch(404)]` or `#[catch(default)]` + = help: `#[catch]` expects a status code int or `default`: `#[catch(404)]` or `#[catch(default)]` --> tests/ui-fail-stable/catch.rs:26:14 | 26 | #[catch(400, message = "foo")] diff --git a/core/codegen/tests/ui-fail-stable/from_form.stderr b/core/codegen/tests/ui-fail-stable/from_form.stderr index 94daef79d3..afd9e509c6 100644 --- a/core/codegen/tests/ui-fail-stable/from_form.stderr +++ b/core/codegen/tests/ui-fail-stable/from_form.stderr @@ -69,7 +69,7 @@ error: [note] error occurred while deriving `FromForm` = note: this error originates in the derive macro `FromForm` (in Nightly builds, run with -Z macro-backtrace for more info) error: invalid form field name - --- help: field name cannot be `isindex` or contain '&', '=', '?', '.', '[', ']' + = help: field name cannot be `isindex` or contain '&', '=', '?', '.', '[', ']' --> tests/ui-fail-stable/from_form.rs:28:20 | 28 | #[field(name = "isindex")] @@ -300,7 +300,7 @@ error: [note] error occurred while deriving `FromForm` = note: this error originates in the derive macro `FromForm` (in Nightly builds, run with -Z macro-backtrace for more info) error: invalid form field name - --- help: field name cannot be `isindex` or contain '&', '=', '?', '.', '[', ']' + = help: field name cannot be `isindex` or contain '&', '=', '?', '.', '[', ']' --> tests/ui-fail-stable/from_form.rs:111:20 | 111 | #[field(name = "hello&world")] @@ -315,7 +315,7 @@ error: [note] error occurred while deriving `FromForm` = note: this error originates in the derive macro `FromForm` (in Nightly builds, run with -Z macro-backtrace for more info) error: invalid form field name - --- help: field name cannot be `isindex` or contain '&', '=', '?', '.', '[', ']' + = help: field name cannot be `isindex` or contain '&', '=', '?', '.', '[', ']' --> tests/ui-fail-stable/from_form.rs:117:20 | 117 | #[field(name = "!@#$%^&*()_")] @@ -330,7 +330,7 @@ error: [note] error occurred while deriving `FromForm` = note: this error originates in the derive macro `FromForm` (in Nightly builds, run with -Z macro-backtrace for more info) error: invalid form field name - --- help: field name cannot be `isindex` or contain '&', '=', '?', '.', '[', ']' + = help: field name cannot be `isindex` or contain '&', '=', '?', '.', '[', ']' --> tests/ui-fail-stable/from_form.rs:123:20 | 123 | #[field(name = "?")] @@ -345,7 +345,7 @@ error: [note] error occurred while deriving `FromForm` = note: this error originates in the derive macro `FromForm` (in Nightly builds, run with -Z macro-backtrace for more info) error: invalid form field name - --- help: field name cannot be `isindex` or contain '&', '=', '?', '.', '[', ']' + = help: field name cannot be `isindex` or contain '&', '=', '?', '.', '[', ']' --> tests/ui-fail-stable/from_form.rs:129:20 | 129 | #[field(name = "")] @@ -360,7 +360,7 @@ error: [note] error occurred while deriving `FromForm` = note: this error originates in the derive macro `FromForm` (in Nightly builds, run with -Z macro-backtrace for more info) error: invalid form field name - --- help: field name cannot be `isindex` or contain '&', '=', '?', '.', '[', ']' + = help: field name cannot be `isindex` or contain '&', '=', '?', '.', '[', ']' --> tests/ui-fail-stable/from_form.rs:135:20 | 135 | #[field(name = "a&b")] @@ -375,7 +375,7 @@ error: [note] error occurred while deriving `FromForm` = note: this error originates in the derive macro `FromForm` (in Nightly builds, run with -Z macro-backtrace for more info) error: invalid form field name - --- help: field name cannot be `isindex` or contain '&', '=', '?', '.', '[', ']' + = help: field name cannot be `isindex` or contain '&', '=', '?', '.', '[', ']' --> tests/ui-fail-stable/from_form.rs:141:20 | 141 | #[field(name = "a=")] @@ -404,7 +404,7 @@ error: [note] error occurred while deriving `FromForm` = note: this error originates in the derive macro `FromForm` (in Nightly builds, run with -Z macro-backtrace for more info) error: duplicate default field expression - --- help: at most one `default` or `default_with` is allowed + = help: at most one `default` or `default_with` is allowed --> tests/ui-fail-stable/from_form.rs:184:23 | 184 | #[field(default = 2)] @@ -419,7 +419,7 @@ error: [note] error occurred while deriving `FromForm` = note: this error originates in the derive macro `FromForm` (in Nightly builds, run with -Z macro-backtrace for more info) error: duplicate default expressions - --- help: only one of `default` or `default_with` must be used + = help: only one of `default` or `default_with` must be used --> tests/ui-fail-stable/from_form.rs:190:23 | 190 | #[field(default = 1, default_with = None)] @@ -440,7 +440,7 @@ error: [note] error occurred while deriving `FromForm` = note: this error originates in the derive macro `FromForm` (in Nightly builds, run with -Z macro-backtrace for more info) error: duplicate default expressions - --- help: only one of `default` or `default_with` must be used + = help: only one of `default` or `default_with` must be used --> tests/ui-fail-stable/from_form.rs:197:23 | 197 | #[field(default = 1)] 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 9ae9ff9968..e8416f8d69 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 @@ -7,28 +7,28 @@ error: missing expected parameter: `uri` = note: this error originates in the attribute macro `get` (in Nightly builds, run with -Z macro-backtrace for more info) error: expected `fn` - --- help: #[get] can only be used on functions + = help: #[get] can only be used on functions --> tests/ui-fail-stable/route-attribute-general-syntax.rs:9:1 | 9 | struct S; | ^^^^^^ error: expected `fn` - --- help: #[get] can only be used on functions + = help: #[get] can only be used on functions --> tests/ui-fail-stable/route-attribute-general-syntax.rs:12:1 | 12 | enum A { } | ^^^^ error: expected `fn` - --- help: #[get] can only be used on functions + = help: #[get] can only be used on functions --> tests/ui-fail-stable/route-attribute-general-syntax.rs:15:1 | 15 | trait Foo { } | ^^^^^ error: expected `fn` - --- help: #[get] can only be used on functions + = help: #[get] can only be used on functions --> tests/ui-fail-stable/route-attribute-general-syntax.rs:18:1 | 18 | impl S { } @@ -65,7 +65,7 @@ error: expected key/value `key = value` | ^ error: handler arguments must be named - --- help: to name an ignored handler argument, use `_name` + = help: to name an ignored handler argument, use `_name` --> tests/ui-fail-stable/route-attribute-general-syntax.rs:39:7 | 39 | fn c1(_: usize) {} @@ -168,35 +168,35 @@ error: invalid or unknown media type | ^^^^^^^^^^^ error: invalid HTTP method for route handlers - --- help: method must be one of: `GET`, `PUT`, `POST`, `DELETE`, `HEAD`, `PATCH`, `OPTIONS` + = help: method must be one of: `GET`, `PUT`, `POST`, `DELETE`, `HEAD`, `PATCH`, `OPTIONS` --> tests/ui-fail-stable/route-attribute-general-syntax.rs:95:9 | 95 | #[route(CONNECT, "/")] | ^^^^^^^ error: invalid HTTP method - --- help: method must be one of: `GET`, `PUT`, `POST`, `DELETE`, `HEAD`, `PATCH`, `OPTIONS` + = help: method must be one of: `GET`, `PUT`, `POST`, `DELETE`, `HEAD`, `PATCH`, `OPTIONS` --> tests/ui-fail-stable/route-attribute-general-syntax.rs:98:9 | 98 | #[route(FIX, "/")] | ^^^ error: expected identifier, found string literal - --- help: method must be one of: `GET`, `PUT`, `POST`, `DELETE`, `HEAD`, `PATCH`, `OPTIONS` + = help: method must be one of: `GET`, `PUT`, `POST`, `DELETE`, `HEAD`, `PATCH`, `OPTIONS` --> tests/ui-fail-stable/route-attribute-general-syntax.rs:101:9 | 101 | #[route("hi", "/")] | ^^^^ error: expected identifier, found string literal - --- help: method must be one of: `GET`, `PUT`, `POST`, `DELETE`, `HEAD`, `PATCH`, `OPTIONS` + = help: method must be one of: `GET`, `PUT`, `POST`, `DELETE`, `HEAD`, `PATCH`, `OPTIONS` --> tests/ui-fail-stable/route-attribute-general-syntax.rs:104:9 | 104 | #[route("GET", "/")] | ^^^^^ error: expected identifier, found integer literal - --- help: method must be one of: `GET`, `PUT`, `POST`, `DELETE`, `HEAD`, `PATCH`, `OPTIONS` + = help: method must be one of: `GET`, `PUT`, `POST`, `DELETE`, `HEAD`, `PATCH`, `OPTIONS` --> tests/ui-fail-stable/route-attribute-general-syntax.rs:107:9 | 107 | #[route(120, "/")] 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 7263aa202e..1aaccd5772 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,47 +1,47 @@ error: invalid route URI: expected token '/' but found 'a' at index 0 - --- help: expected URI in origin form: "/path/" + = help: expected URI in origin form: "/path/" --> tests/ui-fail-stable/route-path-bad-syntax.rs:5:7 | 5 | #[get("a")] | ^^^ error: invalid route URI: unexpected EOF: expected token '/' at index 0 - --- help: expected URI in origin form: "/path/" + = help: expected URI in origin form: "/path/" --> tests/ui-fail-stable/route-path-bad-syntax.rs:8:7 | 8 | #[get("")] | ^^ error: invalid route URI: expected token '/' but found 'a' at index 0 - --- help: expected URI in origin form: "/path/" + = help: expected URI in origin form: "/path/" --> tests/ui-fail-stable/route-path-bad-syntax.rs:11:7 | 11 | #[get("a/b/c")] | ^^^^^^^ error: route URIs cannot contain empty segments - --- note: expected "/a/b", found "/a///b" + = note: expected "/a/b", found "/a///b" --> tests/ui-fail-stable/route-path-bad-syntax.rs:14:7 | 14 | #[get("/a///b")] | ^^^^^^^^ error: route URIs cannot contain empty segments - --- note: expected "/?bat", found "/?bat&&" + = note: expected "/?bat", found "/?bat&&" --> tests/ui-fail-stable/route-path-bad-syntax.rs:17:7 | 17 | #[get("/?bat&&")] | ^^^^^^^^^ error: route URIs cannot contain empty segments - --- note: expected "/?bat", found "/?bat&&" + = note: expected "/?bat", found "/?bat&&" --> tests/ui-fail-stable/route-path-bad-syntax.rs:20:7 | 20 | #[get("/?bat&&")] | ^^^^^^^^^ error: route URIs cannot contain empty segments - --- note: expected "/a/b/", found "/a/b//" + = note: expected "/a/b/", found "/a/b//" --> tests/ui-fail-stable/route-path-bad-syntax.rs:23:7 | 23 | #[get("/a/b//")] @@ -108,68 +108,68 @@ error: [note] expected argument named `b` here | ^^ error: invalid identifier: `foo_.` - --- help: dynamic parameters must be valid identifiers - --- help: did you mean ``? + = help: dynamic parameters must be valid identifiers + = help: did you mean ``? --> tests/ui-fail-stable/route-path-bad-syntax.rs:60:7 | 60 | #[get("/")] | ^^^^^^^^^^ error: invalid identifier: `foo*` - --- help: dynamic parameters must be valid identifiers - --- help: did you mean ``? + = help: dynamic parameters must be valid identifiers + = help: did you mean ``? --> tests/ui-fail-stable/route-path-bad-syntax.rs:63:7 | 63 | #[get("/")] | ^^^^^^^^^ error: invalid identifier: `!` - --- help: dynamic parameters must be valid identifiers - --- help: did you mean ``? + = help: dynamic parameters must be valid identifiers + = help: did you mean ``? --> tests/ui-fail-stable/route-path-bad-syntax.rs:66:7 | 66 | #[get("/")] | ^^^^^^ error: invalid identifier: `name>:`? + = help: dynamic parameters must be valid identifiers + = help: did you mean ``? --> tests/ui-fail-stable/route-path-bad-syntax.rs:69:7 | 69 | #[get("/:")] | ^^^^^^^^^^^^^^ error: unexpected static parameter - --- help: parameter must be dynamic: `` + = help: parameter must be dynamic: `` --> tests/ui-fail-stable/route-path-bad-syntax.rs:74:19 | 74 | #[get("/", data = "foo")] | ^^^^^ error: parameter cannot be trailing - --- help: did you mean ``? + = help: did you mean ``? --> tests/ui-fail-stable/route-path-bad-syntax.rs:77:19 | 77 | #[get("/", data = "")] | ^^^^^^^^^ error: unexpected static parameter - --- help: parameter must be dynamic: `` + = help: parameter must be dynamic: `` --> tests/ui-fail-stable/route-path-bad-syntax.rs:80:19 | 80 | #[get("/", data = "`? + = help: dynamic parameters must be valid identifiers + = help: did you mean ``? --> tests/ui-fail-stable/route-path-bad-syntax.rs:83:19 | 83 | #[get("/", data = "")] | ^^^^^^^^^ error: handler arguments must be named - --- help: to name an ignored handler argument, use `_name` + = help: to name an ignored handler argument, use `_name` --> tests/ui-fail-stable/route-path-bad-syntax.rs:89:7 | 89 | fn k0(_: usize) {} 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 cd8c538700..179c4abee1 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 @@ -273,6 +273,8 @@ error[E0277]: the trait bound `rocket::http::uri::Asterisk: ValidRouteSuffix $WORKSPACE/core/http/src/uri/fmt/formatter.rs | + | pub fn with_suffix(self, suffix: S) -> SuffixedRouteUri + | ----------- required by a bound in this associated function | where S: ValidRouteSuffix> | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ required by this bound in `RouteUriBuilder::with_suffix` @@ -303,5 +305,7 @@ error[E0277]: the trait bound `rocket::http::uri::Origin<'_>: ValidRouteSuffix $WORKSPACE/core/http/src/uri/fmt/formatter.rs | + | pub fn with_suffix(self, suffix: S) -> SuffixedRouteUri + | ----------- required by a bound in this associated function | where S: ValidRouteSuffix> | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ required by this bound in `RouteUriBuilder::with_suffix` diff --git a/core/codegen/tests/ui-fail-stable/typed-uris-bad-params.stderr b/core/codegen/tests/ui-fail-stable/typed-uris-bad-params.stderr index 22b2d36346..b57d6dc204 100644 --- a/core/codegen/tests/ui-fail-stable/typed-uris-bad-params.stderr +++ b/core/codegen/tests/ui-fail-stable/typed-uris-bad-params.stderr @@ -5,21 +5,21 @@ error: expected identifier, found keyword `_` | ^ error: route expects 1 parameter but 2 were supplied - --- note: route `ignored` has uri "/<_>" + = note: route `ignored` has uri "/<_>" --> tests/ui-fail-stable/typed-uris-bad-params.rs:69:18 | 69 | uri!(ignored(10, "10")); | ^^ error: expected unnamed arguments due to ignored parameters - --- note: uri for route `ignored` ignores path parameters: "/<_>" + = note: uri for route `ignored` ignores path parameters: "/<_>" --> tests/ui-fail-stable/typed-uris-bad-params.rs:67:18 | 67 | uri!(ignored(num = 10)); | ^^^ error: route expects 1 parameter but 2 were supplied - --- note: route `ignored` has uri "/<_>" + = note: route `ignored` has uri "/<_>" --> tests/ui-fail-stable/typed-uris-bad-params.rs:65:18 | 65 | uri!(ignored(10, 20)); @@ -44,8 +44,8 @@ error: path parameters cannot be ignored | ^ error: invalid parameters for `has_two` route uri - --- note: uri parameters are: id: i32, name: String - --- help: missing parameter: `name` + = note: uri parameters are: id: i32, name: String + = help: missing parameter: `name` --> tests/ui-fail-stable/typed-uris-bad-params.rs:55:18 | 55 | uri!(has_two(id = 100, cookies = "hi")); @@ -58,8 +58,8 @@ error: [help] unknown parameter: `cookies` | ^^^^^^^ error: invalid parameters for `has_two` route uri - --- note: uri parameters are: id: i32, name: String - --- help: missing parameter: `name` + = note: uri parameters are: id: i32, name: String + = help: missing parameter: `name` --> tests/ui-fail-stable/typed-uris-bad-params.rs:53:18 | 53 | uri!(has_two(cookies = "hi", id = 100, id = 10, id = 10)); @@ -78,16 +78,16 @@ error: [help] duplicate parameter: `id` | ^^ error: invalid parameters for `has_two` route uri - --- note: uri parameters are: id: i32, name: String - --- help: missing parameter: `id` + = note: uri parameters are: id: i32, name: String + = help: missing parameter: `id` --> tests/ui-fail-stable/typed-uris-bad-params.rs:51:18 | 51 | uri!(has_two(name = "hi")); | ^^^^ error: invalid parameters for `has_two` route uri - --- note: uri parameters are: id: i32, name: String - --- help: missing parameter: `name` + = note: uri parameters are: id: i32, name: String + = help: missing parameter: `name` --> tests/ui-fail-stable/typed-uris-bad-params.rs:49:18 | 49 | uri!(has_two(id = 100, id = 100, )); @@ -100,7 +100,7 @@ error: [help] duplicate parameter: `id` | ^^ error: invalid parameters for `has_one_guarded` route uri - --- note: uri parameters are: id: i32 + = note: uri parameters are: id: i32 --> tests/ui-fail-stable/typed-uris-bad-params.rs:47:26 | 47 | uri!(has_one_guarded(id = 100, cookies = "hi")); @@ -113,7 +113,7 @@ error: [help] unknown parameter: `cookies` | ^^^^^^^ error: invalid parameters for `has_one_guarded` route uri - --- note: uri parameters are: id: i32 + = note: uri parameters are: id: i32 --> tests/ui-fail-stable/typed-uris-bad-params.rs:45:26 | 45 | uri!(has_one_guarded(cookies = "hi", id = 100)); @@ -126,8 +126,8 @@ error: [help] unknown parameter: `cookies` | ^^^^^^^ error: invalid parameters for `has_one` route uri - --- note: uri parameters are: id: i32 - --- help: missing parameter: `id` + = note: uri parameters are: id: i32 + = help: missing parameter: `id` --> tests/ui-fail-stable/typed-uris-bad-params.rs:43:18 | 43 | uri!(has_one(name = "hi")); @@ -140,7 +140,7 @@ error: [help] unknown parameter: `name` | ^^^^ error: invalid parameters for `has_one` route uri - --- note: uri parameters are: id: i32 + = note: uri parameters are: id: i32 --> tests/ui-fail-stable/typed-uris-bad-params.rs:41:18 | 41 | uri!(has_one(id = 100, id = 100, )); @@ -153,7 +153,7 @@ error: [help] duplicate parameter: `id` | ^^ error: invalid parameters for `has_one` route uri - --- note: uri parameters are: id: i32 + = note: uri parameters are: id: i32 --> tests/ui-fail-stable/typed-uris-bad-params.rs:39:18 | 39 | uri!(has_one(id = 100, id = 100)); @@ -166,7 +166,7 @@ error: [help] duplicate parameter: `id` | ^^ error: invalid parameters for `has_one` route uri - --- note: uri parameters are: id: i32 + = note: uri parameters are: id: i32 --> tests/ui-fail-stable/typed-uris-bad-params.rs:37:18 | 37 | uri!(has_one(name = 100, age = 50, id = 100, id = 50)); @@ -185,7 +185,7 @@ error: [help] duplicate parameter: `id` | ^^ error: invalid parameters for `has_one` route uri - --- note: uri parameters are: id: i32 + = note: uri parameters are: id: i32 --> tests/ui-fail-stable/typed-uris-bad-params.rs:35:18 | 35 | uri!(has_one(name = 100, age = 50, id = 100)); @@ -198,7 +198,7 @@ error: [help] unknown parameters: `name`, `age` | ^^^^ error: invalid parameters for `has_one` route uri - --- note: uri parameters are: id: i32 + = note: uri parameters are: id: i32 --> tests/ui-fail-stable/typed-uris-bad-params.rs:33:18 | 33 | uri!(has_one(name = 100, id = 100)); @@ -211,7 +211,7 @@ error: [help] unknown parameter: `name` | ^^^^ error: invalid parameters for `has_one` route uri - --- note: uri parameters are: id: i32 + = note: uri parameters are: id: i32 --> tests/ui-fail-stable/typed-uris-bad-params.rs:31:18 | 31 | uri!(has_one(id = 100, name = "hi")); @@ -224,49 +224,49 @@ error: [help] unknown parameter: `name` | ^^^^ error: route expects 2 parameters but 1 was supplied - --- note: route `has_two` has uri "/?" + = note: route `has_two` has uri "/?" --> tests/ui-fail-stable/typed-uris-bad-params.rs:29:18 | 29 | uri!(has_two(10)); | ^^ error: route expects 2 parameters but 3 were supplied - --- note: route `has_two` has uri "/?" + = note: route `has_two` has uri "/?" --> tests/ui-fail-stable/typed-uris-bad-params.rs:28:18 | 28 | uri!(has_two(10, "hi", "there")); | ^^ error: route expects 1 parameter but 2 were supplied - --- note: route `has_one_guarded` has uri "/" + = note: route `has_one_guarded` has uri "/" --> tests/ui-fail-stable/typed-uris-bad-params.rs:26:26 | 26 | uri!(has_one_guarded("hi", 100)); | ^^^^ error: route expects 1 parameter but 2 were supplied - --- note: route `has_one` has uri "/" + = note: route `has_one` has uri "/" --> tests/ui-fail-stable/typed-uris-bad-params.rs:25:18 | 25 | uri!(has_one("Hello", 23, )); | ^^^^^^^ error: route expects 1 parameter but 2 were supplied - --- note: route `has_one` has uri "/" + = note: route `has_one` has uri "/" --> tests/ui-fail-stable/typed-uris-bad-params.rs:24:18 | 24 | uri!(has_one(1, 23)); | ^ error: route expects 1 parameter but 0 were supplied - --- note: route `has_one` has uri "/" + = note: route `has_one` has uri "/" --> tests/ui-fail-stable/typed-uris-bad-params.rs:22:10 | 22 | uri!(has_one()); | ^^^^^^^ error: route expects 1 parameter but 0 were supplied - --- note: route `has_one` has uri "/" + = note: route `has_one` has uri "/" --> tests/ui-fail-stable/typed-uris-bad-params.rs:21:10 | 21 | uri!(has_one); diff --git a/core/codegen/tests/ui-fail-stable/typed-uris-invalid-syntax.stderr b/core/codegen/tests/ui-fail-stable/typed-uris-invalid-syntax.stderr index d50a6ab337..00b70d540a 100644 --- a/core/codegen/tests/ui-fail-stable/typed-uris-invalid-syntax.stderr +++ b/core/codegen/tests/ui-fail-stable/typed-uris-invalid-syntax.stderr @@ -96,7 +96,7 @@ error: unexpected token 27 | uri!(simple: id = ); | ^ -error: unexpected end of input, expected expression +error: unexpected end of input, expected an expression --> tests/ui-fail-stable/typed-uris-invalid-syntax.rs:28:22 | 28 | uri!(simple(id = )); @@ -145,7 +145,7 @@ error: URI suffix must contain only query and/or fragment | ^^^^^^^^^ error: route expects 2 parameters but 0 were supplied - --- note: route `simple` has uri "//" + = note: route `simple` has uri "//" --> tests/ui-fail-stable/typed-uris-invalid-syntax.rs:13:10 | 13 | uri!(simple,); From 5606b8e69369a8bb2363a8449fdd67f90b0ace07 Mon Sep 17 00:00:00 2001 From: Sergio Benitez Date: Mon, 14 Aug 2023 14:11:08 -0400 Subject: [PATCH 161/166] Clarify when 'UriDisplay' can be derived. Resolves #2595. --- core/http/src/uri/fmt/uri_display.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/core/http/src/uri/fmt/uri_display.rs b/core/http/src/uri/fmt/uri_display.rs index fcaebf02d1..fb8902ff02 100644 --- a/core/http/src/uri/fmt/uri_display.rs +++ b/core/http/src/uri/fmt/uri_display.rs @@ -198,8 +198,8 @@ use crate::uri::fmt::{Part, Path, Query, Formatter}; /// assert_eq!(uri_string, "Bob%20Smith"); /// ``` /// -/// As long as every field in the structure (or enum) implements `UriDisplay`, -/// the trait can be derived. The implementation calls +/// As long as every field in the structure (or enum for [`UriDisplay`]) +/// implements `UriDisplay`, the trait can be derived. The implementation calls /// [`Formatter::write_named_value()`] for every named field and /// [`Formatter::write_value()`] for every unnamed field. See the /// [`UriDisplay`] and [`UriDisplay`] derive documentation for full From ddeac5ddcf252d081d69f1b1cec8467ab9ec4d26 Mon Sep 17 00:00:00 2001 From: Sergio Benitez Date: Mon, 14 Aug 2023 14:16:56 -0400 Subject: [PATCH 162/166] Remove unnecessary braces. --- core/lib/fuzz/targets/collision-matching.rs | 6 +++--- core/lib/fuzz/targets/uri-normalization.rs | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/core/lib/fuzz/targets/collision-matching.rs b/core/lib/fuzz/targets/collision-matching.rs index ad350eac35..8b9db5640e 100644 --- a/core/lib/fuzz/targets/collision-matching.rs +++ b/core/lib/fuzz/targets/collision-matching.rs @@ -202,16 +202,16 @@ fn fuzz((route_a, route_b, req): TestData<'_>) { } #[cfg(all(not(honggfuzz), not(afl)))] -libfuzzer_sys::fuzz_target!(|data: TestData| { fuzz(data) }); +libfuzzer_sys::fuzz_target!(|data: TestData| fuzz(data)); #[cfg(honggbuzz)] fn main() { loop { - honggfuzz::fuzz!(|data: TestData| { fuzz(data) }); + honggfuzz::fuzz!(|data: TestData| fuzz(data)); } } #[cfg(afl)] fn main() { - afl::fuzz!(|data: TestData| { fuzz(data) }); + afl::fuzz!(|data: TestData| fuzz(data)); } diff --git a/core/lib/fuzz/targets/uri-normalization.rs b/core/lib/fuzz/targets/uri-normalization.rs index f228a2a0d0..f9704cb72b 100644 --- a/core/lib/fuzz/targets/uri-normalization.rs +++ b/core/lib/fuzz/targets/uri-normalization.rs @@ -20,4 +20,4 @@ fn fuzz(data: &str) { } } -fuzz_target!(|data: &str| { fuzz(data) }); +fuzz_target!(|data: &str| fuzz(data)); From aa7805a5f838daa906b64eae7104584414a63e81 Mon Sep 17 00:00:00 2001 From: Manuel Transfeld Date: Tue, 22 Aug 2023 23:19:37 +0200 Subject: [PATCH 163/166] Update 'sqlx' to '0.7'. --- contrib/db_pools/lib/Cargo.toml | 3 +- contrib/db_pools/lib/src/lib.rs | 11 +-- contrib/db_pools/lib/src/pool.rs | 4 +- contrib/db_pools/lib/tests/databases.rs | 7 -- contrib/sync_db_pools/lib/Cargo.toml | 4 +- .../tests/ui-fail-stable/from_form.stderr | 6 ++ ...327afd9516143806981e11f8e34d069c14472.json | 32 ++++++++ ...78d86afd6a6d36771bfeb12f331abca6279cf.json | 12 +++ ...1ceb0bad0e473701e51ef21ecb2973c76b4df.json | 20 +++++ ...82aa1bee940f0776fae3f9962639b78328858.json | 12 +++ ...9adac27b27a3cf7bf853af3a9f130b1684d91.json | 12 +++ examples/databases/Cargo.toml | 4 +- examples/databases/sqlx-data.json | 81 ------------------- examples/databases/src/sqlx.rs | 10 +-- scripts/test.sh | 1 - site/guide/6-state.md | 6 +- 16 files changed, 113 insertions(+), 112 deletions(-) create mode 100644 examples/databases/.sqlx/query-11e3096becb72f427c8d3911ef4327afd9516143806981e11f8e34d069c14472.json create mode 100644 examples/databases/.sqlx/query-3c289da9873097a11191dbedc5c78d86afd6a6d36771bfeb12f331abca6279cf.json create mode 100644 examples/databases/.sqlx/query-4415c35941e52a981b10707fe2e1ceb0bad0e473701e51ef21ecb2973c76b4df.json create mode 100644 examples/databases/.sqlx/query-668690acaca0a0c0b4ac306b14d82aa1bee940f0776fae3f9962639b78328858.json create mode 100644 examples/databases/.sqlx/query-79301b44b77802e0096efd73b1e9adac27b27a3cf7bf853af3a9f130b1684d91.json delete mode 100644 examples/databases/sqlx-data.json diff --git a/contrib/db_pools/lib/Cargo.toml b/contrib/db_pools/lib/Cargo.toml index af9efca2fb..540e5348b2 100644 --- a/contrib/db_pools/lib/Cargo.toml +++ b/contrib/db_pools/lib/Cargo.toml @@ -21,7 +21,6 @@ deadpool_redis = ["deadpool-redis", "deadpool"] sqlx_mysql = ["sqlx", "sqlx/mysql"] sqlx_postgres = ["sqlx", "sqlx/postgres"] sqlx_sqlite = ["sqlx", "sqlx/sqlite"] -sqlx_mssql = ["sqlx", "sqlx/mssql"] sqlx_macros = ["sqlx/macros"] # diesel features diesel_postgres = ["diesel-async/postgres", "diesel-async/deadpool", "diesel", "deadpool"] @@ -72,7 +71,7 @@ default-features = false optional = true [dependencies.sqlx] -version = "0.6" +version = "0.7" default-features = false features = ["runtime-tokio-rustls"] optional = true diff --git a/contrib/db_pools/lib/src/lib.rs b/contrib/db_pools/lib/src/lib.rs index d79fa4ea05..c00375c895 100644 --- a/contrib/db_pools/lib/src/lib.rs +++ b/contrib/db_pools/lib/src/lib.rs @@ -62,7 +62,7 @@ //! #[get("/")] //! async fn read(mut db: Connection, id: i64) -> Option { //! sqlx::query("SELECT content FROM logs WHERE id = ?").bind(id) -//! .fetch_one(&mut *db).await +//! .fetch_one(&mut **db).await //! .and_then(|r| Ok(Log(r.try_get(0)?))) //! .ok() //! } @@ -113,23 +113,20 @@ //! On shutdown, new connections are denied. Shutdown _does not_ wait for //! connections to be returned. //! -//! ## `sqlx` (v0.6) +//! ## `sqlx` (v0.7) //! //! | Database | Feature | [`Pool`] Type | [`Connection`] Deref | //! |----------|-----------------|----------------------|------------------------------------------| //! | Postgres | `sqlx_postgres` | [`sqlx::PgPool`] | [`sqlx::pool::PoolConnection`] | //! | MySQL | `sqlx_mysql` | [`sqlx::MySqlPool`] | [`sqlx::pool::PoolConnection`] | //! | SQLite | `sqlx_sqlite` | [`sqlx::SqlitePool`] | [`sqlx::pool::PoolConnection`] | -//! | MSSQL | `sqlx_mssql` | [`sqlx::MssqlPool`] | [`sqlx::pool::PoolConnection`] | //! //! [`sqlx::PgPool`]: https://docs.rs/sqlx/0.6/sqlx/type.PgPool.html //! [`sqlx::MySqlPool`]: https://docs.rs/sqlx/0.6/sqlx/type.MySqlPool.html //! [`sqlx::SqlitePool`]: https://docs.rs/sqlx/0.6/sqlx/type.SqlitePool.html -//! [`sqlx::MssqlPool`]: https://docs.rs/sqlx/0.6/sqlx/type.MssqlPool.html //! [`sqlx::pool::PoolConnection`]: https://docs.rs/sqlx/0.6/sqlx/pool/struct.PoolConnection.html //! [`sqlx::pool::PoolConnection`]: https://docs.rs/sqlx/0.6/sqlx/pool/struct.PoolConnection.html //! [`sqlx::pool::PoolConnection`]: https://docs.rs/sqlx/0.6/sqlx/pool/struct.PoolConnection.html -//! [`sqlx::pool::PoolConnection`]: https://docs.rs/sqlx/0.6/sqlx/pool/struct.PoolConnection.html //! //! On shutdown, new connections are denied. Shutdown waits for connections to //! be returned. @@ -163,9 +160,9 @@ //! //! ```toml //! [dependencies.sqlx] -//! version = "0.6" +//! version = "0.7" //! default-features = false -//! features = ["macros", "offline", "migrate"] +//! features = ["macros", "migrate"] //! //! [dependencies.rocket_db_pools] //! version = "=0.1.0-rc.3" diff --git a/contrib/db_pools/lib/src/pool.rs b/contrib/db_pools/lib/src/pool.rs index 49d298cba1..694a20648e 100644 --- a/contrib/db_pools/lib/src/pool.rs +++ b/contrib/db_pools/lib/src/pool.rs @@ -253,10 +253,10 @@ mod sqlx { let mut opts = config.url.parse::>().map_err(Error::Init)?; specialize(&mut opts, &config); - opts.disable_statement_logging(); + opts = opts.disable_statement_logging(); if let Ok(level) = figment.extract_inner::(rocket::Config::LOG_LEVEL) { if !matches!(level, LogLevel::Normal | LogLevel::Off) { - opts.log_statements(level.into()) + opts = opts.log_statements(level.into()) .log_slow_statements(level.into(), Duration::default()); } } diff --git a/contrib/db_pools/lib/tests/databases.rs b/contrib/db_pools/lib/tests/databases.rs index ce0bd36975..21401a2595 100644 --- a/contrib/db_pools/lib/tests/databases.rs +++ b/contrib/db_pools/lib/tests/databases.rs @@ -52,13 +52,6 @@ check_types_match!( sqlx::pool::PoolConnection, ); -check_types_match!( - "sqlx_mssql", - sqlx_mssql, - sqlx::MssqlPool, - sqlx::pool::PoolConnection, -); - check_types_match!( "mongodb", mongodb, diff --git a/contrib/sync_db_pools/lib/Cargo.toml b/contrib/sync_db_pools/lib/Cargo.toml index 6ef7081d65..8a2903f166 100644 --- a/contrib/sync_db_pools/lib/Cargo.toml +++ b/contrib/sync_db_pools/lib/Cargo.toml @@ -28,8 +28,8 @@ diesel = { version = "2.0.0", default-features = false, optional = true } postgres = { version = "0.19", optional = true } r2d2_postgres = { version = "0.18", optional = true } -rusqlite = { version = "0.27.0", optional = true } -r2d2_sqlite = { version = "0.20.0", optional = true } +rusqlite = { version = "0.29.0", optional = true } +r2d2_sqlite = { version = "0.22.0", optional = true } memcache = { version = "0.15", optional = true } r2d2-memcache = { version = "0.6", optional = true } diff --git a/core/codegen/tests/ui-fail-stable/from_form.stderr b/core/codegen/tests/ui-fail-stable/from_form.stderr index afd9e509c6..3bc006df65 100644 --- a/core/codegen/tests/ui-fail-stable/from_form.stderr +++ b/core/codegen/tests/ui-fail-stable/from_form.stderr @@ -523,6 +523,9 @@ help: the type constructed contains `{integer}` due to the type of the argument | ^^^ this argument influences the type of `Some` note: tuple variant defined here --> $RUST/core/src/option.rs + | + | Some(#[stable(feature = "rust1", since = "1.0.0")] T), + | ^^^^ error[E0308]: mismatched types --> tests/ui-fail-stable/from_form.rs:203:33 @@ -542,6 +545,9 @@ help: the type constructed contains `&'static str` due to the type of the argume | this argument influences the type of `Some` note: tuple variant defined here --> $RUST/core/src/option.rs + | + | Some(#[stable(feature = "rust1", since = "1.0.0")] T), + | ^^^^ error[E0277]: the trait bound `bool: From<&str>` is not satisfied --> tests/ui-fail-stable/from_form.rs:209:23 diff --git a/examples/databases/.sqlx/query-11e3096becb72f427c8d3911ef4327afd9516143806981e11f8e34d069c14472.json b/examples/databases/.sqlx/query-11e3096becb72f427c8d3911ef4327afd9516143806981e11f8e34d069c14472.json new file mode 100644 index 0000000000..247b40e2fd --- /dev/null +++ b/examples/databases/.sqlx/query-11e3096becb72f427c8d3911ef4327afd9516143806981e11f8e34d069c14472.json @@ -0,0 +1,32 @@ +{ + "db_name": "SQLite", + "query": "SELECT id, title, text FROM posts WHERE id = ?", + "describe": { + "columns": [ + { + "name": "id", + "ordinal": 0, + "type_info": "Int64" + }, + { + "name": "title", + "ordinal": 1, + "type_info": "Text" + }, + { + "name": "text", + "ordinal": 2, + "type_info": "Text" + } + ], + "parameters": { + "Right": 1 + }, + "nullable": [ + false, + false, + false + ] + }, + "hash": "11e3096becb72f427c8d3911ef4327afd9516143806981e11f8e34d069c14472" +} diff --git a/examples/databases/.sqlx/query-3c289da9873097a11191dbedc5c78d86afd6a6d36771bfeb12f331abca6279cf.json b/examples/databases/.sqlx/query-3c289da9873097a11191dbedc5c78d86afd6a6d36771bfeb12f331abca6279cf.json new file mode 100644 index 0000000000..a78e822ab5 --- /dev/null +++ b/examples/databases/.sqlx/query-3c289da9873097a11191dbedc5c78d86afd6a6d36771bfeb12f331abca6279cf.json @@ -0,0 +1,12 @@ +{ + "db_name": "SQLite", + "query": "INSERT INTO posts (title, text) VALUES (?, ?)", + "describe": { + "columns": [], + "parameters": { + "Right": 2 + }, + "nullable": [] + }, + "hash": "3c289da9873097a11191dbedc5c78d86afd6a6d36771bfeb12f331abca6279cf" +} diff --git a/examples/databases/.sqlx/query-4415c35941e52a981b10707fe2e1ceb0bad0e473701e51ef21ecb2973c76b4df.json b/examples/databases/.sqlx/query-4415c35941e52a981b10707fe2e1ceb0bad0e473701e51ef21ecb2973c76b4df.json new file mode 100644 index 0000000000..99a73650ca --- /dev/null +++ b/examples/databases/.sqlx/query-4415c35941e52a981b10707fe2e1ceb0bad0e473701e51ef21ecb2973c76b4df.json @@ -0,0 +1,20 @@ +{ + "db_name": "SQLite", + "query": "SELECT id FROM posts", + "describe": { + "columns": [ + { + "name": "id", + "ordinal": 0, + "type_info": "Int64" + } + ], + "parameters": { + "Right": 0 + }, + "nullable": [ + false + ] + }, + "hash": "4415c35941e52a981b10707fe2e1ceb0bad0e473701e51ef21ecb2973c76b4df" +} diff --git a/examples/databases/.sqlx/query-668690acaca0a0c0b4ac306b14d82aa1bee940f0776fae3f9962639b78328858.json b/examples/databases/.sqlx/query-668690acaca0a0c0b4ac306b14d82aa1bee940f0776fae3f9962639b78328858.json new file mode 100644 index 0000000000..d744583bc9 --- /dev/null +++ b/examples/databases/.sqlx/query-668690acaca0a0c0b4ac306b14d82aa1bee940f0776fae3f9962639b78328858.json @@ -0,0 +1,12 @@ +{ + "db_name": "SQLite", + "query": "DELETE FROM posts", + "describe": { + "columns": [], + "parameters": { + "Right": 0 + }, + "nullable": [] + }, + "hash": "668690acaca0a0c0b4ac306b14d82aa1bee940f0776fae3f9962639b78328858" +} diff --git a/examples/databases/.sqlx/query-79301b44b77802e0096efd73b1e9adac27b27a3cf7bf853af3a9f130b1684d91.json b/examples/databases/.sqlx/query-79301b44b77802e0096efd73b1e9adac27b27a3cf7bf853af3a9f130b1684d91.json new file mode 100644 index 0000000000..6aa292f3d1 --- /dev/null +++ b/examples/databases/.sqlx/query-79301b44b77802e0096efd73b1e9adac27b27a3cf7bf853af3a9f130b1684d91.json @@ -0,0 +1,12 @@ +{ + "db_name": "SQLite", + "query": "DELETE FROM posts WHERE id = ?", + "describe": { + "columns": [], + "parameters": { + "Right": 1 + }, + "nullable": [] + }, + "hash": "79301b44b77802e0096efd73b1e9adac27b27a3cf7bf853af3a9f130b1684d91" +} diff --git a/examples/databases/Cargo.toml b/examples/databases/Cargo.toml index 926105d2e8..63b50d17fc 100644 --- a/examples/databases/Cargo.toml +++ b/examples/databases/Cargo.toml @@ -11,9 +11,9 @@ diesel = "2" diesel_migrations = "2" [dependencies.sqlx] -version = "0.6.0" +version = "0.7.0" default-features = false -features = ["macros", "offline", "migrate"] +features = ["macros", "migrate"] [dependencies.rocket_db_pools] path = "../../contrib/db_pools/lib/" diff --git a/examples/databases/sqlx-data.json b/examples/databases/sqlx-data.json deleted file mode 100644 index 9dbd494590..0000000000 --- a/examples/databases/sqlx-data.json +++ /dev/null @@ -1,81 +0,0 @@ -{ - "db": "SQLite", - "11e3096becb72f427c8d3911ef4327afd9516143806981e11f8e34d069c14472": { - "query": "SELECT id, title, text FROM posts WHERE id = ?", - "describe": { - "columns": [ - { - "name": "id", - "ordinal": 0, - "type_info": "Int64" - }, - { - "name": "title", - "ordinal": 1, - "type_info": "Text" - }, - { - "name": "text", - "ordinal": 2, - "type_info": "Text" - } - ], - "parameters": { - "Right": 1 - }, - "nullable": [ - false, - false, - false - ] - } - }, - "3c289da9873097a11191dbedc5c78d86afd6a6d36771bfeb12f331abca6279cf": { - "query": "INSERT INTO posts (title, text) VALUES (?, ?)", - "describe": { - "columns": [], - "parameters": { - "Right": 2 - }, - "nullable": [] - } - }, - "4415c35941e52a981b10707fe2e1ceb0bad0e473701e51ef21ecb2973c76b4df": { - "query": "SELECT id FROM posts", - "describe": { - "columns": [ - { - "name": "id", - "ordinal": 0, - "type_info": "Int64" - } - ], - "parameters": { - "Right": 0 - }, - "nullable": [ - false - ] - } - }, - "668690acaca0a0c0b4ac306b14d82aa1bee940f0776fae3f9962639b78328858": { - "query": "DELETE FROM posts", - "describe": { - "columns": [], - "parameters": { - "Right": 0 - }, - "nullable": [] - } - }, - "79301b44b77802e0096efd73b1e9adac27b27a3cf7bf853af3a9f130b1684d91": { - "query": "DELETE FROM posts WHERE id = ?", - "describe": { - "columns": [], - "parameters": { - "Right": 1 - }, - "nullable": [] - } - } -} \ No newline at end of file diff --git a/examples/databases/src/sqlx.rs b/examples/databases/src/sqlx.rs index ffad0c7377..0f0ca67bae 100644 --- a/examples/databases/src/sqlx.rs +++ b/examples/databases/src/sqlx.rs @@ -26,7 +26,7 @@ struct Post { async fn create(mut db: Connection, post: Json) -> Result>> { // There is no support for `RETURNING`. sqlx::query!("INSERT INTO posts (title, text) VALUES (?, ?)", post.title, post.text) - .execute(&mut *db) + .execute(&mut **db) .await?; Ok(Created::new("/").body(post)) @@ -35,7 +35,7 @@ async fn create(mut db: Connection, post: Json) -> Result) -> Result>> { let ids = sqlx::query!("SELECT id FROM posts") - .fetch(&mut *db) + .fetch(&mut **db) .map_ok(|record| record.id) .try_collect::>() .await?; @@ -46,7 +46,7 @@ async fn list(mut db: Connection) -> Result>> { #[get("/")] async fn read(mut db: Connection, id: i64) -> Option> { sqlx::query!("SELECT id, title, text FROM posts WHERE id = ?", id) - .fetch_one(&mut *db) + .fetch_one(&mut **db) .map_ok(|r| Json(Post { id: Some(r.id), title: r.title, text: r.text })) .await .ok() @@ -55,7 +55,7 @@ async fn read(mut db: Connection, id: i64) -> Option> { #[delete("/")] async fn delete(mut db: Connection, id: i64) -> Result> { let result = sqlx::query!("DELETE FROM posts WHERE id = ?", id) - .execute(&mut *db) + .execute(&mut **db) .await?; Ok((result.rows_affected() == 1).then(|| ())) @@ -63,7 +63,7 @@ async fn delete(mut db: Connection, id: i64) -> Result> { #[delete("/")] async fn destroy(mut db: Connection) -> Result<()> { - sqlx::query!("DELETE FROM posts").execute(&mut *db).await?; + sqlx::query!("DELETE FROM posts").execute(&mut **db).await?; Ok(()) } diff --git a/scripts/test.sh b/scripts/test.sh index 1532ee47c3..40525a1f05 100755 --- a/scripts/test.sh +++ b/scripts/test.sh @@ -80,7 +80,6 @@ function test_contrib() { sqlx_mysql sqlx_postgres sqlx_sqlite - sqlx_mssql mongodb diesel_mysql diesel_postgres diff --git a/site/guide/6-state.md b/site/guide/6-state.md index 0eb6d65a1d..b845d3474e 100644 --- a/site/guide/6-state.md +++ b/site/guide/6-state.md @@ -264,7 +264,7 @@ in three simple steps: #[get("/")] async fn read(mut db: Connection, id: i64) -> Option { sqlx::query("SELECT content FROM logs WHERE id = ?").bind(id) - .fetch_one(&mut *db).await + .fetch_one(&mut **db).await .and_then(|r| Ok(r.try_get(0)?)) .ok() } @@ -294,9 +294,9 @@ features enabled in `Cargo.toml`: ```toml [dependencies.sqlx] -version = "0.6" +version = "0.7" default-features = false -features = ["macros", "offline", "migrate"] +features = ["macros", "migrate"] [dependencies.rocket_db_pools] version = "=0.1.0-rc.3" From fc76bf7b686749b0f449029f6a79bdd2105333b7 Mon Sep 17 00:00:00 2001 From: Sergio Benitez Date: Fri, 25 Aug 2023 15:19:15 -0700 Subject: [PATCH 164/166] Update 'databases' example README. The README now more completely documents the example. All implementations now make use of 'RETURNING'. --- ...78d86afd6a6d36771bfeb12f331abca6279cf.json | 12 ---- ...2770d89cfaf9859d0bfca78b2ca24627675b7.json | 20 ++++++ examples/databases/Cargo.toml | 2 +- examples/databases/README.md | 71 +++++++++++++++++-- examples/databases/src/diesel_mysql.rs | 2 +- examples/databases/src/diesel_sqlite.rs | 8 ++- examples/databases/src/rusqlite.rs | 10 +-- examples/databases/src/sqlx.rs | 11 +-- 8 files changed, 105 insertions(+), 31 deletions(-) delete mode 100644 examples/databases/.sqlx/query-3c289da9873097a11191dbedc5c78d86afd6a6d36771bfeb12f331abca6279cf.json create mode 100644 examples/databases/.sqlx/query-bea4ef6e25064f6b383e854f8bc2770d89cfaf9859d0bfca78b2ca24627675b7.json diff --git a/examples/databases/.sqlx/query-3c289da9873097a11191dbedc5c78d86afd6a6d36771bfeb12f331abca6279cf.json b/examples/databases/.sqlx/query-3c289da9873097a11191dbedc5c78d86afd6a6d36771bfeb12f331abca6279cf.json deleted file mode 100644 index a78e822ab5..0000000000 --- a/examples/databases/.sqlx/query-3c289da9873097a11191dbedc5c78d86afd6a6d36771bfeb12f331abca6279cf.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "db_name": "SQLite", - "query": "INSERT INTO posts (title, text) VALUES (?, ?)", - "describe": { - "columns": [], - "parameters": { - "Right": 2 - }, - "nullable": [] - }, - "hash": "3c289da9873097a11191dbedc5c78d86afd6a6d36771bfeb12f331abca6279cf" -} diff --git a/examples/databases/.sqlx/query-bea4ef6e25064f6b383e854f8bc2770d89cfaf9859d0bfca78b2ca24627675b7.json b/examples/databases/.sqlx/query-bea4ef6e25064f6b383e854f8bc2770d89cfaf9859d0bfca78b2ca24627675b7.json new file mode 100644 index 0000000000..7aad158e46 --- /dev/null +++ b/examples/databases/.sqlx/query-bea4ef6e25064f6b383e854f8bc2770d89cfaf9859d0bfca78b2ca24627675b7.json @@ -0,0 +1,20 @@ +{ + "db_name": "SQLite", + "query": "INSERT INTO posts (title, text) VALUES (?, ?) RETURNING id", + "describe": { + "columns": [ + { + "name": "id", + "ordinal": 0, + "type_info": "Int64" + } + ], + "parameters": { + "Right": 2 + }, + "nullable": [ + false + ] + }, + "hash": "bea4ef6e25064f6b383e854f8bc2770d89cfaf9859d0bfca78b2ca24627675b7" +} diff --git a/examples/databases/Cargo.toml b/examples/databases/Cargo.toml index 63b50d17fc..2ed4fa5adc 100644 --- a/examples/databases/Cargo.toml +++ b/examples/databases/Cargo.toml @@ -7,7 +7,7 @@ publish = false [dependencies] rocket = { path = "../../core/lib", features = ["json"] } -diesel = "2" +diesel = { version = "2", features = ["returning_clauses_for_sqlite_3_35"] } diesel_migrations = "2" [dependencies.sqlx] diff --git a/examples/databases/README.md b/examples/databases/README.md index 43dcdb8ff2..f9505bf6af 100644 --- a/examples/databases/README.md +++ b/examples/databases/README.md @@ -1,8 +1,69 @@ # Databases Example -This example makes use of SQLite. You'll need `sqlite3` and its development -headers installed: +This example makes use of SQLite and MySQL. You'll need `sqlite3` and a MySQL +client installed: - * **macOS:** `brew install sqlite` - * **Debian**, **Ubuntu:** `apt-get install libsqlite3-dev` - * **Arch:** `pacman -S sqlite` + * **macOS:** `brew install sqlite mysql-client` + * **Debian**, **Ubuntu:** `apt-get install libsqlite3-dev libmysqlclient-dev` + * **Arch:** `pacman -S sqlite libmysqlclient` + +## API Implementation + +This example implements a JSON-based HTTP API for a "blog" using several database drivers: + + * `sqlx` (`/sqlx`, `sqlx.rs`) + * `rusqlite` (`/rusqlite`, `rusqlite.rs`) + * `diesel` (sqlite) (`/diesel`, `diesel_sqlite.rs`) + * `diesel-async` (mysql) (`/diesel-async`, `diesel_mysql.rs`) + +The exposed API is succinctly described as follows, with +[`httpie`](https://httpie.io/) CLI examples: + + * `POST /driver`: create post via JSON with `title` and `text`; returns new + post JSON with new `id` + + http http://127.0.0.1:8000/sqlx title="Title" text="Hello, world." + > { "id": 2128, "text": "Hello, world.", "title": "Title" } + + * `GET /driver`: returns JSON array of IDs for blog posts + + http http://127.0.0.1:8000/sqlx + > [ 2128, 2129, 2130, 2131 ] + + * `GET /driver/`: returns a JSON object for the post with id `` + + http http://127.0.0.1:8000/sqlx/2128 + > { "id": 2128, "text": "Hello, world.", "title": "Title" } + + * `DELETE /driver`: delete all posts + + http delete http://127.0.0.1:8000/sqlx + + * `DELETE /driver/`: delete post with id `` + + http delete http://127.0.0.1:8000/sqlx/4 + +## Migrations + +Database migrations are stored in the respective `db/${driver}` directory. + +### `diesel` + +Diesel migrations are found in `db/diesel/migrations`. They are run +automatically. They can be run manually as well: + +```sh +cargo install diesel_cli --no-default-features --features sqlite +DATABASE_URL="db/diesel/db.sqlite" diesel migration --migration-dir db/diesel/migrations redo +``` + +### `sqlx` + +sqlx migrations are found in `db/sqlx/migrations`. They are run automatically. + +Query metadata for offline checking was prepared with the following commands: + +```sh +cargo install sqlx-cli --no-default-features --features sqlite +DATABASE_URL="sqlite:$(pwd)/db/sqlx/db.sqlite" cargo sqlx prepare +``` diff --git a/examples/databases/src/diesel_mysql.rs b/examples/databases/src/diesel_mysql.rs index 6c6c7751f0..a09db8b151 100644 --- a/examples/databases/src/diesel_mysql.rs +++ b/examples/databases/src/diesel_mysql.rs @@ -92,6 +92,6 @@ async fn destroy(mut db: Connection) -> Result<()> { pub fn stage() -> AdHoc { AdHoc::on_ignite("Diesel SQLite Stage", |rocket| async { rocket.attach(Db::init()) - .mount("/diesel-async/", routes![list, read, create, delete, destroy]) + .mount("/diesel-async", routes![list, read, create, delete, destroy]) }) } diff --git a/examples/databases/src/diesel_sqlite.rs b/examples/databases/src/diesel_sqlite.rs index 1427729a27..8e5a9da4fa 100644 --- a/examples/databases/src/diesel_sqlite.rs +++ b/examples/databases/src/diesel_sqlite.rs @@ -32,14 +32,16 @@ table! { } #[post("/", data = "")] -async fn create(db: Db, post: Json) -> Result>> { +async fn create(db: Db, mut post: Json) -> Result>> { let post_value = post.clone(); - db.run(move |conn| { + let id: Option = db.run(move |conn| { diesel::insert_into(posts::table) .values(&*post_value) - .execute(conn) + .returning(posts::id) + .get_result(conn) }).await?; + post.id = Some(id.expect("returning guarantees id present")); Ok(Created::new("/").body(post)) } diff --git a/examples/databases/src/rusqlite.rs b/examples/databases/src/rusqlite.rs index 0908246750..92674e274e 100644 --- a/examples/databases/src/rusqlite.rs +++ b/examples/databases/src/rusqlite.rs @@ -22,13 +22,15 @@ struct Post { type Result> = std::result::Result; #[post("/", data = "")] -async fn create(db: Db, post: Json) -> Result>> { +async fn create(db: Db, mut post: Json) -> Result>> { let item = post.clone(); - db.run(move |conn| { - conn.execute("INSERT INTO posts (title, text) VALUES (?1, ?2)", - params![item.title, item.text]) + let id = db.run(move |conn| { + conn.query_row("INSERT INTO posts (title, text) VALUES (?1, ?2) RETURNING id", + params![item.title, item.text], + |r| r.get(0)) }).await?; + post.id = Some(id); Ok(Created::new("/").body(post)) } diff --git a/examples/databases/src/sqlx.rs b/examples/databases/src/sqlx.rs index 0f0ca67bae..0eb6a30f4a 100644 --- a/examples/databases/src/sqlx.rs +++ b/examples/databases/src/sqlx.rs @@ -23,12 +23,13 @@ struct Post { } #[post("/", data = "")] -async fn create(mut db: Connection, post: Json) -> Result>> { - // There is no support for `RETURNING`. - sqlx::query!("INSERT INTO posts (title, text) VALUES (?, ?)", post.title, post.text) - .execute(&mut **db) - .await?; +async fn create(mut db: Connection, mut post: Json) -> Result>> { + let query = sqlx::query! { + "INSERT INTO posts (title, text) VALUES (?, ?) RETURNING id", + post.title, post.text + }; + post.id = Some(query.fetch_one(&mut **db).await?.id); Ok(Created::new("/").body(post)) } From 695cf3aab10f50afec680c556710bd2c7e4378cc Mon Sep 17 00:00:00 2001 From: Sergio Benitez Date: Fri, 25 Aug 2023 15:23:29 -0700 Subject: [PATCH 165/166] Update UI tests for latest `rustc`. --- .../tests/ui-fail-nightly/catch.stderr | 2 +- .../ui-fail-stable/catch_type_errors.stderr | 64 ++++---- .../tests/ui-fail-stable/from_form.stderr | 6 - .../from_form_type_errors.stderr | 32 ++-- .../ui-fail-stable/responder-types.stderr | 78 +++++----- .../ui-fail-stable/route-type-errors.stderr | 144 +++++++++--------- .../ui-fail-stable/typed-uri-bad-type.stderr | 124 +++++++-------- .../uri_display_type_errors.stderr | 112 +++++++------- 8 files changed, 278 insertions(+), 284 deletions(-) diff --git a/core/codegen/tests/ui-fail-nightly/catch.stderr b/core/codegen/tests/ui-fail-nightly/catch.stderr index 79cc292ca7..823904b4cf 100644 --- a/core/codegen/tests/ui-fail-nightly/catch.stderr +++ b/core/codegen/tests/ui-fail-nightly/catch.stderr @@ -75,7 +75,7 @@ note: function defined here | 30 | fn f3(_request: &Request, other: bool) { } | ^^ ------------------ ----------- -help: did you mean +help: provide the argument | 29 | f3(bool, /* bool */) | diff --git a/core/codegen/tests/ui-fail-stable/catch_type_errors.stderr b/core/codegen/tests/ui-fail-stable/catch_type_errors.stderr index c65b195ba5..9b930dbb75 100644 --- a/core/codegen/tests/ui-fail-stable/catch_type_errors.stderr +++ b/core/codegen/tests/ui-fail-stable/catch_type_errors.stderr @@ -7,14 +7,14 @@ error[E0277]: the trait bound `usize: Responder<'_, '_>` is not satisfied | ^^^^^ the trait `Responder<'_, '_>` is not implemented for `usize` | = help: the following other types implement trait `Responder<'r, 'o>`: - <&'o [u8] as Responder<'r, 'o>> - <&'o str as Responder<'r, 'o>> - <() as Responder<'r, 'static>> - <(ContentType, R) as Responder<'r, 'o>> - <(Status, R) as Responder<'r, 'o>> - as Responder<'r, 'o>> - as Responder<'r, 'static>> - as Responder<'r, 'static>> + as Responder<'r, 'o>> + as Responder<'r, 'static>> + as Responder<'r, 'static>> + as Responder<'r, 'o>> + as Responder<'r, 'o>> + > + as Responder<'r, 'r>> + > and $N others error[E0277]: the trait bound `bool: Responder<'_, '_>` is not satisfied @@ -26,14 +26,14 @@ error[E0277]: the trait bound `bool: Responder<'_, '_>` is not satisfied | ^^^^ the trait `Responder<'_, '_>` is not implemented for `bool` | = help: the following other types implement trait `Responder<'r, 'o>`: - <&'o [u8] as Responder<'r, 'o>> - <&'o str as Responder<'r, 'o>> - <() as Responder<'r, 'static>> - <(ContentType, R) as Responder<'r, 'o>> - <(Status, R) as Responder<'r, 'o>> - as Responder<'r, 'o>> - as Responder<'r, 'static>> - as Responder<'r, 'static>> + as Responder<'r, 'o>> + as Responder<'r, 'static>> + as Responder<'r, 'static>> + as Responder<'r, 'o>> + as Responder<'r, 'o>> + > + as Responder<'r, 'r>> + > and $N others error[E0308]: mismatched types @@ -59,14 +59,14 @@ error[E0277]: the trait bound `usize: Responder<'_, '_>` is not satisfied | ^^^^^ the trait `Responder<'_, '_>` is not implemented for `usize` | = help: the following other types implement trait `Responder<'r, 'o>`: - <&'o [u8] as Responder<'r, 'o>> - <&'o str as Responder<'r, 'o>> - <() as Responder<'r, 'static>> - <(ContentType, R) as Responder<'r, 'o>> - <(Status, R) as Responder<'r, 'o>> - as Responder<'r, 'o>> - as Responder<'r, 'static>> - as Responder<'r, 'static>> + as Responder<'r, 'o>> + as Responder<'r, 'static>> + as Responder<'r, 'static>> + as Responder<'r, 'o>> + as Responder<'r, 'o>> + > + as Responder<'r, 'r>> + > and $N others error[E0277]: the trait bound `usize: Responder<'_, '_>` is not satisfied @@ -78,12 +78,12 @@ error[E0277]: the trait bound `usize: Responder<'_, '_>` is not satisfied | ^^^^^ the trait `Responder<'_, '_>` is not implemented for `usize` | = help: the following other types implement trait `Responder<'r, 'o>`: - <&'o [u8] as Responder<'r, 'o>> - <&'o str as Responder<'r, 'o>> - <() as Responder<'r, 'static>> - <(ContentType, R) as Responder<'r, 'o>> - <(Status, R) as Responder<'r, 'o>> - as Responder<'r, 'o>> - as Responder<'r, 'static>> - as Responder<'r, 'static>> + as Responder<'r, 'o>> + as Responder<'r, 'static>> + as Responder<'r, 'static>> + as Responder<'r, 'o>> + as Responder<'r, 'o>> + > + as Responder<'r, 'r>> + > and $N others diff --git a/core/codegen/tests/ui-fail-stable/from_form.stderr b/core/codegen/tests/ui-fail-stable/from_form.stderr index 3bc006df65..afd9e509c6 100644 --- a/core/codegen/tests/ui-fail-stable/from_form.stderr +++ b/core/codegen/tests/ui-fail-stable/from_form.stderr @@ -523,9 +523,6 @@ help: the type constructed contains `{integer}` due to the type of the argument | ^^^ this argument influences the type of `Some` note: tuple variant defined here --> $RUST/core/src/option.rs - | - | Some(#[stable(feature = "rust1", since = "1.0.0")] T), - | ^^^^ error[E0308]: mismatched types --> tests/ui-fail-stable/from_form.rs:203:33 @@ -545,9 +542,6 @@ help: the type constructed contains `&'static str` due to the type of the argume | this argument influences the type of `Some` note: tuple variant defined here --> $RUST/core/src/option.rs - | - | Some(#[stable(feature = "rust1", since = "1.0.0")] T), - | ^^^^ error[E0277]: the trait bound `bool: From<&str>` is not satisfied --> tests/ui-fail-stable/from_form.rs:209:23 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 705ee23f13..6022e34c25 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 @@ -5,14 +5,14 @@ error[E0277]: the trait bound `Unknown: FromFormField<'_>` is not satisfied | ^^^^^^^ the trait `FromFormField<'_>` is not implemented for `Unknown` | = help: the following other types implement trait `FromFormField<'v>`: - &'v [u8] - &'v str - Capped<&'v [u8]> - Capped<&'v str> - Capped> - Capped> - Capped - Cow<'v, str> + bool + isize + i8 + i16 + i32 + i64 + i128 + usize and $N others = note: required for `Unknown` to implement `FromForm<'r>` @@ -23,13 +23,13 @@ error[E0277]: the trait bound `Foo: FromFormField<'_>` is not satisfied | ^^^ the trait `FromFormField<'_>` is not implemented for `Foo` | = help: the following other types implement trait `FromFormField<'v>`: - &'v [u8] - &'v str - Capped<&'v [u8]> - Capped<&'v str> - Capped> - Capped> - Capped - Cow<'v, str> + bool + isize + i8 + i16 + i32 + i64 + i128 + usize and $N others = note: required for `Foo` to implement `FromForm<'r>` diff --git a/core/codegen/tests/ui-fail-stable/responder-types.stderr b/core/codegen/tests/ui-fail-stable/responder-types.stderr index 4dafb9f599..1ee3af24c2 100644 --- a/core/codegen/tests/ui-fail-stable/responder-types.stderr +++ b/core/codegen/tests/ui-fail-stable/responder-types.stderr @@ -5,14 +5,14 @@ error[E0277]: the trait bound `u8: Responder<'_, '_>` is not satisfied | ^^^^^ the trait `Responder<'_, '_>` is not implemented for `u8` | = help: the following other types implement trait `Responder<'r, 'o>`: - <&'o [u8] as Responder<'r, 'o>> - <&'o str as Responder<'r, 'o>> - <() as Responder<'r, 'static>> - <(ContentType, R) as Responder<'r, 'o>> - <(Status, R) as Responder<'r, 'o>> - as Responder<'r, 'o>> - as Responder<'r, 'static>> - as Responder<'r, 'static>> + > + > + > + > + as Responder<'r, 'o>> + as Responder<'r, 'static>> + as Responder<'r, 'static>> + as Responder<'r, 'o>> and $N others error[E0277]: the trait bound `Header<'_>: From` is not satisfied @@ -22,14 +22,14 @@ error[E0277]: the trait bound `Header<'_>: From` is not satisfied | ^^^^^ the trait `From` is not implemented for `Header<'_>` | = help: the following other types implement trait `From`: + as From>> + as From> + as From> as From<&Cookie<'_>>> + as From<&Referrer>> as From<&ExpectCt>> - as From<&Frame>> - as From<&Hsts>> as From<&NoSniff>> - as From<&Permission>> - as From<&Prefetch>> - as From<&Referrer>> + as From<&Hsts>> and $N others = note: required for `u8` to implement `Into>` note: required by a bound in `rocket::Response::<'r>::set_header` @@ -45,14 +45,14 @@ error[E0277]: the trait bound `u8: Responder<'_, '_>` is not satisfied | ^^^^^ the trait `Responder<'_, '_>` is not implemented for `u8` | = help: the following other types implement trait `Responder<'r, 'o>`: - <&'o [u8] as Responder<'r, 'o>> - <&'o str as Responder<'r, 'o>> - <() as Responder<'r, 'static>> - <(ContentType, R) as Responder<'r, 'o>> - <(Status, R) as Responder<'r, 'o>> - as Responder<'r, 'o>> - as Responder<'r, 'static>> - as Responder<'r, 'static>> + > + > + > + > + as Responder<'r, 'o>> + as Responder<'r, 'static>> + as Responder<'r, 'static>> + as Responder<'r, 'o>> and $N others error[E0277]: the trait bound `Header<'_>: From` is not satisfied @@ -62,14 +62,14 @@ error[E0277]: the trait bound `Header<'_>: From` is not satisfied | ^^^^^ the trait `From` is not implemented for `Header<'_>` | = help: the following other types implement trait `From`: + as From>> + as From> + as From> as From<&Cookie<'_>>> + as From<&Referrer>> as From<&ExpectCt>> - as From<&Frame>> - as From<&Hsts>> as From<&NoSniff>> - as From<&Permission>> - as From<&Prefetch>> - as From<&Referrer>> + as From<&Hsts>> and $N others = note: required for `u8` to implement `Into>` note: required by a bound in `rocket::Response::<'r>::set_header` @@ -85,14 +85,14 @@ error[E0277]: the trait bound `Header<'_>: From` is not sat | ^^^^ the trait `From` is not implemented for `Header<'_>` | = help: the following other types implement trait `From`: + as From>> + as From> + as From> as From<&Cookie<'_>>> + as From<&Referrer>> as From<&ExpectCt>> - as From<&Frame>> - as From<&Hsts>> as From<&NoSniff>> - as From<&Permission>> - as From<&Prefetch>> - as From<&Referrer>> + as From<&Hsts>> and $N others = note: required for `std::string::String` to implement `Into>` note: required by a bound in `rocket::Response::<'r>::set_header` @@ -108,14 +108,14 @@ error[E0277]: the trait bound `usize: Responder<'_, '_>` is not satisfied | ^^^^^ the trait `Responder<'_, '_>` is not implemented for `usize` | = help: the following other types implement trait `Responder<'r, 'o>`: - <&'o [u8] as Responder<'r, 'o>> - <&'o str as Responder<'r, 'o>> - <() as Responder<'r, 'static>> - <(ContentType, R) as Responder<'r, 'o>> - <(Status, R) as Responder<'r, 'o>> - as Responder<'r, 'o>> - as Responder<'r, 'static>> - as Responder<'r, 'static>> + > + > + > + > + as Responder<'r, 'o>> + as Responder<'r, 'static>> + as Responder<'r, 'static>> + as Responder<'r, 'o>> and $N others note: required by a bound in `route::handler::, Status, (rocket::Data<'o>, Status)>>::from` --> $WORKSPACE/core/lib/src/route/handler.rs 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 a76bdfe2b9..40dd0d27cd 100644 --- a/core/codegen/tests/ui-fail-stable/route-type-errors.stderr +++ b/core/codegen/tests/ui-fail-stable/route-type-errors.stderr @@ -5,14 +5,14 @@ error[E0277]: the trait bound `Q: FromParam<'_>` is not satisfied | ^ the trait `FromParam<'_>` is not implemented for `Q` | = help: the following other types implement trait `FromParam<'a>`: - &'a str - IpAddr - Ipv4Addr - Ipv6Addr - NonZeroI128 - NonZeroI16 - NonZeroI32 - NonZeroI64 + bool + isize + i8 + i16 + i32 + i64 + i128 + usize and $N others error[E0277]: the trait bound `Q: FromSegments<'_>` is not satisfied @@ -22,10 +22,10 @@ error[E0277]: the trait bound `Q: FromSegments<'_>` is not satisfied | ^ the trait `FromSegments<'_>` is not implemented for `Q` | = help: the following other types implement trait `FromSegments<'r>`: - > - >::Error> as FromSegments<'r>> as FromSegments<'r>> + > as FromSegments<'r>> + >::Error> as FromSegments<'r>> error[E0277]: the trait bound `Q: FromFormField<'_>` is not satisfied --> tests/ui-fail-stable/route-type-errors.rs:12:12 @@ -34,14 +34,14 @@ error[E0277]: the trait bound `Q: FromFormField<'_>` is not satisfied | ^ the trait `FromFormField<'_>` is not implemented for `Q` | = help: the following other types implement trait `FromFormField<'v>`: - &'v [u8] - &'v str - Capped<&'v [u8]> - Capped<&'v str> - Capped> - Capped> - Capped - Cow<'v, str> + bool + isize + i8 + i16 + i32 + i64 + i128 + usize and $N others = note: required for `Q` to implement `FromForm<'_>` @@ -52,14 +52,14 @@ error[E0277]: the trait bound `Q: FromFormField<'_>` is not satisfied | ^ the trait `FromFormField<'_>` is not implemented for `Q` | = help: the following other types implement trait `FromFormField<'v>`: - &'v [u8] - &'v str - Capped<&'v [u8]> - Capped<&'v str> - Capped> - Capped> - Capped - Cow<'v, str> + bool + isize + i8 + i16 + i32 + i64 + i128 + usize and $N others = note: required for `Q` to implement `FromForm<'_>` @@ -70,14 +70,14 @@ error[E0277]: the trait bound `Q: FromData<'_>` is not satisfied | ^ the trait `FromData<'_>` is not implemented for `Q` | = help: the following other types implement trait `FromData<'r>`: - &'r RawStr - &'r [u8] - &'r str - Capped<&'r RawStr> - Capped<&'r [u8]> - Capped<&'r str> + rocket::Data<'r> + Cow<'_, str> Capped> + Capped> + Capped Capped> + Capped<&'r str> + Capped<&'r RawStr> and $N others error[E0277]: the trait bound `Q: FromRequest<'_>` is not satisfied @@ -87,14 +87,14 @@ error[E0277]: the trait bound `Q: FromRequest<'_>` is not satisfied | ^ the trait `FromRequest<'_>` is not implemented for `Q` | = help: the following other types implement trait `FromRequest<'r>`: - &'r ContentType - &'r Host<'r> - &'r Limits - &'r Route - &'r rocket::Config - &'r rocket::State - &'r rocket::http::Accept - &'r rocket::http::CookieJar<'r> + rocket::http::Method + Outcome>::Error), Status> + Flash<&'r rocket::http::CookieJar<'r>> + rocket::Shutdown + IpAddr + std::net::SocketAddr + std::option::Option + Result>::Error> and $N others error[E0277]: the trait bound `Q: FromParam<'_>` is not satisfied @@ -104,14 +104,14 @@ error[E0277]: the trait bound `Q: FromParam<'_>` is not satisfied | ^ the trait `FromParam<'_>` is not implemented for `Q` | = help: the following other types implement trait `FromParam<'a>`: - &'a str - IpAddr - Ipv4Addr - Ipv6Addr - NonZeroI128 - NonZeroI16 - NonZeroI32 - NonZeroI64 + bool + isize + i8 + i16 + i32 + i64 + i128 + usize and $N others error[E0277]: the trait bound `Q: FromRequest<'_>` is not satisfied @@ -121,14 +121,14 @@ error[E0277]: the trait bound `Q: FromRequest<'_>` is not satisfied | ^ the trait `FromRequest<'_>` is not implemented for `Q` | = help: the following other types implement trait `FromRequest<'r>`: - &'r ContentType - &'r Host<'r> - &'r Limits - &'r Route - &'r rocket::Config - &'r rocket::State - &'r rocket::http::Accept - &'r rocket::http::CookieJar<'r> + rocket::http::Method + Outcome>::Error), Status> + Flash<&'r rocket::http::CookieJar<'r>> + rocket::Shutdown + IpAddr + std::net::SocketAddr + std::option::Option + Result>::Error> and $N others error[E0277]: the trait bound `Q: FromParam<'_>` is not satisfied @@ -138,14 +138,14 @@ error[E0277]: the trait bound `Q: FromParam<'_>` is not satisfied | ^ the trait `FromParam<'_>` is not implemented for `Q` | = help: the following other types implement trait `FromParam<'a>`: - &'a str - IpAddr - Ipv4Addr - Ipv6Addr - NonZeroI128 - NonZeroI16 - NonZeroI32 - NonZeroI64 + bool + isize + i8 + i16 + i32 + i64 + i128 + usize and $N others error[E0277]: the trait bound `Q: FromParam<'_>` is not satisfied @@ -155,12 +155,12 @@ error[E0277]: the trait bound `Q: FromParam<'_>` is not satisfied | ^ the trait `FromParam<'_>` is not implemented for `Q` | = help: the following other types implement trait `FromParam<'a>`: - &'a str - IpAddr - Ipv4Addr - Ipv6Addr - NonZeroI128 - NonZeroI16 - NonZeroI32 - NonZeroI64 + bool + isize + i8 + i16 + i32 + i64 + i128 + usize and $N others 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 179c4abee1..5b529fc249 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 @@ -17,9 +17,9 @@ error[E0277]: the trait bound `usize: FromUriParam` is not implemented for `usize` | = help: the following other types implement trait `FromUriParam`: - > - > > + > + > error[E0277]: the trait bound `usize: FromUriParam` is not satisfied --> tests/ui-fail-stable/typed-uri-bad-type.rs:47:17 @@ -28,9 +28,9 @@ error[E0277]: the trait bound `usize: FromUriParam` is not implemented for `usize` | = help: the following other types implement trait `FromUriParam`: - > - > > + > + > error[E0277]: the trait bound `usize: FromUriParam` is not satisfied --> tests/ui-fail-stable/typed-uri-bad-type.rs:49:22 @@ -39,9 +39,9 @@ error[E0277]: the trait bound `usize: FromUriParam` is not implemented for `usize` | = help: the following other types implement trait `FromUriParam`: - > - > > + > + > error[E0277]: the trait bound `S: FromUriParam` is not satisfied --> tests/ui-fail-stable/typed-uri-bad-type.rs:51:30 @@ -50,14 +50,14 @@ error[E0277]: the trait bound `S: FromUriParam` | ^ the trait `FromUriParam` is not implemented for `S` | = help: the following other types implement trait `FromUriParam`: - <&'a std::path::Path as FromUriParam> - <&'a std::path::Path as FromUriParam> - <&'a std::path::Path as FromUriParam> - <&'a std::path::Path as FromUriParam> - <&'a std::path::Path as FromUriParam> - <&'a std::path::Path as FromUriParam> - <&'a str as FromUriParam> - <&'a str as FromUriParam> + > + > + > + > + > + > + > + > and $N others error[E0277]: the trait bound `i32: FromUriParam>` is not satisfied @@ -67,9 +67,9 @@ error[E0277]: the trait bound `i32: FromUriParam>` is not implemented for `i32` | = help: the following other types implement trait `FromUriParam`: + > > > - > = note: required for `std::option::Option` to implement `FromUriParam>` error[E0277]: the trait bound `std::string::String: FromUriParam>` is not satisfied @@ -79,12 +79,12 @@ error[E0277]: the trait bound `std::string::String: FromUriParam>` is not implemented for `std::string::String` | = help: the following other types implement trait `FromUriParam`: + > + > + > > > > - > - > - > = note: required for `Result` to implement `FromUriParam>` error[E0277]: the trait bound `isize: FromUriParam` is not satisfied @@ -94,9 +94,9 @@ error[E0277]: the trait bound `isize: FromUriParam` is not implemented for `isize` | = help: the following other types implement trait `FromUriParam`: + > > > - > error[E0277]: the trait bound `isize: FromUriParam` is not satisfied --> tests/ui-fail-stable/typed-uri-bad-type.rs:60:24 @@ -105,9 +105,9 @@ error[E0277]: the trait bound `isize: FromUriParam` is not implemented for `isize` | = help: the following other types implement trait `FromUriParam`: + > > > - > error[E0277]: the trait bound `S: FromUriParam` is not satisfied --> tests/ui-fail-stable/typed-uri-bad-type.rs:62:23 @@ -116,14 +116,14 @@ error[E0277]: the trait bound `S: FromUriParam | ^ the trait `FromUriParam` is not implemented for `S` | = help: the following other types implement trait `FromUriParam`: - <&'a std::path::Path as FromUriParam> - <&'a std::path::Path as FromUriParam> - <&'a std::path::Path as FromUriParam> - <&'a std::path::Path as FromUriParam> - <&'a std::path::Path as FromUriParam> - <&'a std::path::Path as FromUriParam> - <&'a str as FromUriParam> - <&'a str as FromUriParam> + > + > + > + > + > + > + > + > and $N others error[E0277]: the trait bound `S: FromUriParam` is not satisfied @@ -133,14 +133,14 @@ error[E0277]: the trait bound `S: FromUriParam | ^ the trait `FromUriParam` is not implemented for `S` | = help: the following other types implement trait `FromUriParam`: - <&'a std::path::Path as FromUriParam> - <&'a std::path::Path as FromUriParam> - <&'a std::path::Path as FromUriParam> - <&'a std::path::Path as FromUriParam> - <&'a std::path::Path as FromUriParam> - <&'a std::path::Path as FromUriParam> - <&'a str as FromUriParam> - <&'a str as FromUriParam> + > + > + > + > + > + > + > + > and $N others error[E0277]: the trait bound `S: Ignorable` is not satisfied @@ -150,8 +150,8 @@ error[E0277]: the trait bound `S: Ignorable` is n | ^ the trait `Ignorable` is not implemented for `S` | = help: the following other types implement trait `Ignorable

`: - Result std::option::Option + Result note: required by a bound in `assert_ignorable` --> $WORKSPACE/core/http/src/uri/fmt/uri_display.rs | @@ -165,8 +165,8 @@ error[E0277]: the trait bound `usize: Ignorable` | ^ the trait `Ignorable` is not implemented for `usize` | = help: the following other types implement trait `Ignorable

`: - Result std::option::Option + Result note: required by a bound in `assert_ignorable` --> $WORKSPACE/core/http/src/uri/fmt/uri_display.rs | @@ -180,14 +180,14 @@ error[E0277]: the trait bound `S: FromUriParam | ^ the trait `FromUriParam` is not implemented for `S` | = help: the following other types implement trait `FromUriParam`: - <&'a std::path::Path as FromUriParam> - <&'a std::path::Path as FromUriParam> - <&'a std::path::Path as FromUriParam> - <&'a std::path::Path as FromUriParam> - <&'a std::path::Path as FromUriParam> - <&'a std::path::Path as FromUriParam> - <&'a str as FromUriParam> - <&'a str as FromUriParam> + > + > + > + > + > + > + > + > and $N others error[E0277]: the trait bound `usize: FromUriParam` is not satisfied @@ -197,9 +197,9 @@ error[E0277]: the trait bound `usize: FromUriParam` is not implemented for `usize` | = help: the following other types implement trait `FromUriParam`: - > - > > + > + > error[E0277]: the trait bound `rocket::http::uri::Reference<'_>: ValidRoutePrefix` is not satisfied --> tests/ui-fail-stable/typed-uri-bad-type.rs:77:15 @@ -210,8 +210,8 @@ error[E0277]: the trait bound `rocket::http::uri::Reference<'_>: ValidRoutePrefi | required by a bound introduced by this call | = help: the following other types implement trait `ValidRoutePrefix`: - rocket::http::uri::Absolute<'a> rocket::http::uri::Origin<'a> + rocket::http::uri::Absolute<'a> note: required by a bound in `RouteUriBuilder::with_prefix` --> $WORKSPACE/core/http/src/uri/fmt/formatter.rs | @@ -225,9 +225,9 @@ error[E0277]: the trait bound `usize: FromUriParam` is not implemented for `usize` | = help: the following other types implement trait `FromUriParam`: - > - > > + > + > error[E0277]: the trait bound `rocket::http::uri::Asterisk: ValidRoutePrefix` is not satisfied --> tests/ui-fail-stable/typed-uri-bad-type.rs:78:15 @@ -238,8 +238,8 @@ error[E0277]: the trait bound `rocket::http::uri::Asterisk: ValidRoutePrefix` is | required by a bound introduced by this call | = help: the following other types implement trait `ValidRoutePrefix`: - rocket::http::uri::Absolute<'a> rocket::http::uri::Origin<'a> + rocket::http::uri::Absolute<'a> note: required by a bound in `RouteUriBuilder::with_prefix` --> $WORKSPACE/core/http/src/uri/fmt/formatter.rs | @@ -253,9 +253,9 @@ error[E0277]: the trait bound `usize: FromUriParam` is not implemented for `usize` | = help: the following other types implement trait `FromUriParam`: - > - > > + > + > error[E0277]: the trait bound `rocket::http::uri::Asterisk: ValidRouteSuffix>` is not satisfied --> tests/ui-fail-stable/typed-uri-bad-type.rs:81:37 @@ -266,10 +266,10 @@ error[E0277]: the trait bound `rocket::http::uri::Asterisk: ValidRouteSuffix`: - as ValidRouteSuffix>> - as ValidRouteSuffix>> - as ValidRouteSuffix>> as ValidRouteSuffix>> + as ValidRouteSuffix>> + as ValidRouteSuffix>> + as ValidRouteSuffix>> note: required by a bound in `RouteUriBuilder::with_suffix` --> $WORKSPACE/core/http/src/uri/fmt/formatter.rs | @@ -285,9 +285,9 @@ error[E0277]: the trait bound `usize: FromUriParam` is not implemented for `usize` | = help: the following other types implement trait `FromUriParam`: - > - > > + > + > error[E0277]: the trait bound `rocket::http::uri::Origin<'_>: ValidRouteSuffix>` is not satisfied --> tests/ui-fail-stable/typed-uri-bad-type.rs:82:37 @@ -298,10 +298,10 @@ error[E0277]: the trait bound `rocket::http::uri::Origin<'_>: ValidRouteSuffix`: - as ValidRouteSuffix>> - as ValidRouteSuffix>> - as ValidRouteSuffix>> as ValidRouteSuffix>> + as ValidRouteSuffix>> + as ValidRouteSuffix>> + as ValidRouteSuffix>> note: required by a bound in `RouteUriBuilder::with_suffix` --> $WORKSPACE/core/http/src/uri/fmt/formatter.rs | 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 65271c404a..92916acebf 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 @@ -5,14 +5,14 @@ error[E0277]: the trait bound `BadType: UriDisplay` is not implemented for `BadType` | = help: the following other types implement trait `UriDisplay

`: - <&T as UriDisplay

> - <&mut T as UriDisplay

> - as UriDisplay> - > - > - > - > - > + > + > + > + > + > + > + > + > and $N others = note: required for `&BadType` to implement `UriDisplay` note: required by a bound in `rocket::http::uri::fmt::Formatter::<'i, P>::write_value` @@ -28,14 +28,14 @@ error[E0277]: the trait bound `BadType: UriDisplay` is not implemented for `BadType` | = help: the following other types implement trait `UriDisplay

`: - <&T as UriDisplay

> - <&mut T as UriDisplay

> - as UriDisplay> - > - > - > - > - > + > + > + > + > + > + > + > + > and $N others = note: required for `&BadType` to implement `UriDisplay` note: required by a bound in `rocket::http::uri::fmt::Formatter::<'_, rocket::http::uri::fmt::Query>::write_named_value` @@ -51,14 +51,14 @@ error[E0277]: the trait bound `BadType: UriDisplay` is not implemented for `BadType` | = help: the following other types implement trait `UriDisplay

`: - <&T as UriDisplay

> - <&mut T as UriDisplay

> - as UriDisplay> - > - > - > - > - > + > + > + > + > + > + > + > + > and $N others = note: required for `&BadType` to implement `UriDisplay` note: required by a bound in `rocket::http::uri::fmt::Formatter::<'_, rocket::http::uri::fmt::Query>::write_named_value` @@ -74,14 +74,14 @@ error[E0277]: the trait bound `BadType: UriDisplay` is not implemented for `BadType` | = help: the following other types implement trait `UriDisplay

`: - <&T as UriDisplay

> - <&mut T as UriDisplay

> - as UriDisplay> - > - > - > - > - > + > + > + > + > + > + > + > + > and $N others = note: required for `&BadType` to implement `UriDisplay` = note: 1 redundant requirement hidden @@ -99,14 +99,14 @@ error[E0277]: the trait bound `BadType: UriDisplay` is not implemented for `BadType` | = help: the following other types implement trait `UriDisplay

`: - <&T as UriDisplay

> - <&mut T as UriDisplay

> - as UriDisplay> - > - > - > - > - > + > + > + > + > + > + > + > + > and $N others = note: required for `&BadType` to implement `UriDisplay` = note: 1 redundant requirement hidden @@ -124,14 +124,14 @@ error[E0277]: the trait bound `BadType: UriDisplay` is not implemented for `BadType` | = help: the following other types implement trait `UriDisplay

`: - <&T as UriDisplay

> - <&mut T as UriDisplay

> - as UriDisplay> - > - > - > - > - > + > + > + > + > + > + > + > + > and $N others = note: required for `&BadType` to implement `UriDisplay` = note: 1 redundant requirement hidden @@ -149,14 +149,14 @@ error[E0277]: the trait bound `BadType: UriDisplay | ^^^^^^^ the trait `UriDisplay` is not implemented for `BadType` | = help: the following other types implement trait `UriDisplay

`: - <&T as UriDisplay

> - <&mut T as UriDisplay

> - as UriDisplay> - > - > - > - > - > + > + > + > + > + > + > + > + > and $N others = note: required for `&BadType` to implement `UriDisplay` note: required by a bound in `rocket::http::uri::fmt::Formatter::<'i, P>::write_value` From 26a3f00f82b6ddec2bbdb3ecb0c29d6ff0da7472 Mon Sep 17 00:00:00 2001 From: Sergio Benitez Date: Fri, 25 Aug 2023 17:58:26 -0700 Subject: [PATCH 166/166] Work around bug in sqlx database example. --- examples/databases/src/sqlx.rs | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/examples/databases/src/sqlx.rs b/examples/databases/src/sqlx.rs index 0eb6a30f4a..c52cc2e779 100644 --- a/examples/databases/src/sqlx.rs +++ b/examples/databases/src/sqlx.rs @@ -24,12 +24,16 @@ struct Post { #[post("/", data = "")] async fn create(mut db: Connection, mut post: Json) -> Result>> { - let query = sqlx::query! { - "INSERT INTO posts (title, text) VALUES (?, ?) RETURNING id", - post.title, post.text - }; + // NOTE: sqlx#2543, sqlx#1648 mean we can't use the pithier `fetch_one()`. + let results = sqlx::query!( + "INSERT INTO posts (title, text) VALUES (?, ?) RETURNING id", + post.title, post.text + ) + .fetch(&mut **db) + .try_collect::>() + .await?; - post.id = Some(query.fetch_one(&mut **db).await?.id); + post.id = Some(results.first().expect("returning results").id); Ok(Created::new("/").body(post)) }