mirror of https://github.com/rwf2/Rocket.git
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<Path>` 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.
This commit is contained in:
parent
a82508b403
commit
0a56312607
|
@ -45,13 +45,13 @@ fn generate_matching_requests<'c>(client: &'c Client, routes: &[Route]) -> Vec<L
|
|||
}
|
||||
|
||||
fn request_for_route<'c>(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::<Vec<_>>()
|
||||
.join("/");
|
||||
|
||||
let query = route.uri.origin.query()
|
||||
let query = route.uri.uri.query()
|
||||
.map(|q| q.raw_segments())
|
||||
.into_iter()
|
||||
.flatten()
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -77,9 +77,11 @@ impl FromMeta for RouteUri {
|
|||
.help("expected URI in origin form: \"/path/<param>\"")
|
||||
})?;
|
||||
|
||||
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))
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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/");
|
||||
}
|
||||
|
|
|
@ -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",
|
||||
}
|
||||
|
|
|
@ -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 | | }
|
||||
|
|
|
@ -240,3 +240,27 @@ warning: `segment` starts with `<` but does not end with `>`
|
|||
| ^^^^^^^^
|
||||
|
|
||||
= help: perhaps you meant the dynamic parameter `<name>`?
|
||||
|
||||
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/"
|
||||
|
|
|
@ -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/")]
|
||||
| ^^^^^^^^^
|
||||
|
|
|
@ -54,7 +54,7 @@ fn h3() {}
|
|||
#[get("/<_r>/<b>")]
|
||||
fn h4() {}
|
||||
|
||||
|
||||
//
|
||||
// Check dynamic parameters are valid idents
|
||||
|
||||
#[get("/<foo_.>")]
|
||||
|
@ -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() { }
|
||||
|
|
|
@ -126,7 +126,6 @@ impl<A: IntoOwned, B: IntoOwned> IntoOwned for (A, B) {
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
impl<B: 'static + ToOwned + ?Sized> 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);
|
||||
|
||||
|
|
|
@ -180,6 +180,11 @@ impl RawStr {
|
|||
/// ```
|
||||
#[inline(always)]
|
||||
pub fn percent_decode(&self) -> Result<Cow<'_, str>, 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<Item = &'a RawStr>
|
||||
where P: Pattern<'a>
|
||||
pub fn split<'a, P>(&'a self, pat: P) -> impl DoubleEndedIterator<Item = &'a RawStr>
|
||||
where P: Pattern<'a>,
|
||||
<P as stable_pattern::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
|
||||
|
|
|
@ -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());
|
||||
|
|
|
@ -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());
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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<P: fmt::Part>(
|
|||
|
||||
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())
|
||||
}
|
||||
|
||||
/// Normalizes `self`. If `absolute`, a starting `/` is required.
|
||||
pub(crate) fn to_normalized(self, absolute: bool) -> Data<'static, fmt::Path> {
|
||||
let mut path = String::with_capacity(self.raw().len());
|
||||
let absolute = absolute || self.raw().starts_with('/');
|
||||
for (i, seg) in self.raw_segments().filter(|s| !s.is_empty()).enumerate() {
|
||||
if absolute || i != 0 { path.push('/'); }
|
||||
let _ = write!(path, "{}", seg);
|
||||
if absolute && !self.raw().starts_with('/') {
|
||||
return false;
|
||||
}
|
||||
|
||||
if path.is_empty() && absolute {
|
||||
self.raw_segments()
|
||||
.rev()
|
||||
.skip(1)
|
||||
.all(|s| !s.is_empty())
|
||||
}
|
||||
|
||||
/// 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 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<Item = &'a RawStr> {
|
||||
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<Item = &'a RawStr> {
|
||||
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::<fmt::Path>(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::<fmt::Path>(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<Data<'static, fmt::Query>> {
|
||||
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);
|
||||
query.push_str(seg.as_str());
|
||||
}
|
||||
|
||||
if query.is_empty() {
|
||||
return None;
|
||||
}
|
||||
|
||||
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<Item = &'a RawStr> {
|
||||
let query = match self.raw() {
|
||||
let query = match self.raw().trim() {
|
||||
q if q.is_empty() => None,
|
||||
q => Some(q)
|
||||
};
|
||||
|
|
|
@ -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<Query<'_>> {
|
||||
|
@ -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<P>(&mut self, path: P)
|
||||
where P: Into<Cow<'a, str>>
|
||||
{
|
||||
|
|
|
@ -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<P: Part> 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<P: Part> 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<P: Part> 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<usize>) {
|
||||
(self.len(), Some(self.len()))
|
||||
(self.num(), Some(self.num()))
|
||||
}
|
||||
|
||||
fn count(self) -> usize {
|
||||
self.len()
|
||||
self.num()
|
||||
}
|
||||
}
|
||||
)
|
||||
|
|
|
@ -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");
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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) });
|
|
@ -107,14 +107,25 @@ pub struct Catcher {
|
|||
/// The name of this catcher, if one was given.
|
||||
pub name: Option<Cow<'static, str>>,
|
||||
|
||||
/// The mount point.
|
||||
pub base: uri::Origin<'static>,
|
||||
|
||||
/// The HTTP status to match against if this route is not `default`.
|
||||
pub code: Option<u16>,
|
||||
|
||||
/// The catcher's associated error handler.
|
||||
pub handler: Box<dyn Handler>,
|
||||
|
||||
/// 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<Self, uri::Error<'static>>
|
||||
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()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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<usize>) -> 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)
|
||||
}
|
||||
|
||||
|
|
|
@ -579,9 +579,9 @@ fn log_items<T, I, B, O>(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<P: Phase> Rocket<P> {
|
|||
/// .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<Item = &Catcher> {
|
||||
match self.0.as_state_ref() {
|
||||
|
|
|
@ -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 `<a>`.
|
||||
pub dynamic: bool,
|
||||
pub trailing: bool,
|
||||
/// This is a `<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 }
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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<Segment>,
|
||||
/// Segments in the path, including base.
|
||||
pub path_segs: Vec<Segment>,
|
||||
/// Segments in the route URI, including base.
|
||||
pub uri_segments: Vec<Segment>,
|
||||
/// 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<Color>,
|
||||
/// Whether the path has a `<trailing..>` parameter.
|
||||
pub trailing_path: bool,
|
||||
pub dynamic_trail: bool,
|
||||
}
|
||||
|
||||
type Result<T, E = uri::Error<'static>> = std::result::Result<T, E>;
|
||||
|
@ -103,25 +103,36 @@ impl<'a> RouteUri<'a> {
|
|||
pub(crate) fn try_new(base: &str, uri: &str) -> Result<RouteUri<'static>> {
|
||||
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);
|
||||
|
||||
Ok(RouteUri { source, base, unmounted_origin, origin, metadata })
|
||||
let source = uri.to_string().into();
|
||||
let metadata = Metadata::from(&base, &uri);
|
||||
|
||||
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()
|
||||
fn from(base: &Origin<'_>, uri: &Origin<'_>) -> Self {
|
||||
let uri_segments = uri.path()
|
||||
.raw_segments()
|
||||
.map(Segment::from)
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let path_segs = origin.path().raw_segments()
|
||||
.map(Segment::from)
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let query_segs = origin.query()
|
||||
let query_segs = uri.query()
|
||||
.map(|q| q.raw_segments().map(Segment::from).collect::<Vec<_>>())
|
||||
.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<Origin<'b>> 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<str> for RouteUri<'_> {
|
||||
|
|
|
@ -1,16 +1,72 @@
|
|||
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<T = Self> {
|
||||
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 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.
|
||||
///
|
||||
/// This implementation is used at initialization to check if two user
|
||||
/// routes collide before launching. Format collisions works like this:
|
||||
///
|
||||
/// * 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.
|
||||
///
|
||||
/// Because query parsing is lenient, and dynamic query parameters can be
|
||||
/// missing, queries do not impact whether two routes collide.
|
||||
fn collides_with(&self, other: &Route) -> bool {
|
||||
self.method == other.method
|
||||
&& self.rank == other.rank
|
||||
&& self.uri.collides_with(&other.uri)
|
||||
&& formats_collide(self, other)
|
||||
}
|
||||
}
|
||||
|
||||
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.
|
||||
fn collides_with(&self, other: &Self) -> bool {
|
||||
self.code == other.code
|
||||
&& self.base.path().segments().eq(other.base.path().segments())
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
if !seg_a.collides_with(seg_b) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Check for `/a/<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()
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -21,28 +77,6 @@ impl Collide for MediaType {
|
|||
}
|
||||
}
|
||||
|
||||
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,
|
||||
|
@ -61,319 +95,172 @@ fn formats_collide(route: &Route, other: &Route) -> 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.
|
||||
///
|
||||
/// This implementation is used at initialization to check if two user
|
||||
/// routes collide before launching. Format collisions works like this:
|
||||
///
|
||||
/// * 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.
|
||||
///
|
||||
/// Because query parsing is lenient, and dynamic query parameters can be
|
||||
/// missing, queries do not impact whether two routes collide.
|
||||
fn collides_with(&self, other: &Route) -> bool {
|
||||
self.method == other.method
|
||||
&& self.rank == other.rank
|
||||
&& paths_collide(self, other)
|
||||
&& formats_collide(self, other)
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
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()));
|
||||
|
||||
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, 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 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.
|
||||
fn collides_with(&self, other: &Self) -> bool {
|
||||
self.code == other.code
|
||||
&& self.base.path().segments().eq(other.base.path().segments())
|
||||
}
|
||||
}
|
||||
|
||||
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())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::str::FromStr;
|
||||
|
||||
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;
|
||||
use crate::http::{Method, Method::*, MediaType};
|
||||
|
||||
type SimpleRoute = (Method, &'static str);
|
||||
|
||||
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 dummy_route(ranked: bool, method: impl Into<Option<Method>>, uri: &'static str) -> Route {
|
||||
let method = method.into().unwrap_or(Get);
|
||||
Route::ranked((!ranked).then(|| 0), method, uri, 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)
|
||||
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)+));
|
||||
}
|
||||
|
||||
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("/<a>", "/<b>"));
|
||||
assert!(unranked_collide("/<a>", "/b"));
|
||||
assert!(unranked_collide("/hello/<name>", "/hello/<person>"));
|
||||
assert!(unranked_collide("/hello/<name>/hi", "/hello/<person>/hi"));
|
||||
assert!(unranked_collide("/hello/<name>/hi/there", "/hello/<person>/hi/there"));
|
||||
assert!(unranked_collide("/<name>/hi/there", "/<person>/hi/there"));
|
||||
assert!(unranked_collide("/<name>/hi/there", "/dude/<name>/there"));
|
||||
assert!(unranked_collide("/<name>/<a>/<b>", "/<a>/<b>/<c>"));
|
||||
assert!(unranked_collide("/<name>/<a>/<b>/", "/<a>/<b>/<c>/"));
|
||||
assert!(unranked_collide("/<a..>", "/hi"));
|
||||
assert!(unranked_collide("/<a..>", "/hi/hey"));
|
||||
assert!(unranked_collide("/<a..>", "/hi/hey/hayo"));
|
||||
assert!(unranked_collide("/a/<a..>", "/a/hi/hey/hayo"));
|
||||
assert!(unranked_collide("/a/<b>/<a..>", "/a/hi/hey/hayo"));
|
||||
assert!(unranked_collide("/a/<b>/<c>/<a..>", "/a/hi/hey/hayo"));
|
||||
assert!(unranked_collide("/<b>/<c>/<a..>", "/a/hi/hey/hayo"));
|
||||
assert!(unranked_collide("/<b>/<c>/hey/hayo", "/a/hi/hey/hayo"));
|
||||
assert!(unranked_collide("/<a..>", "/foo"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn medium_param_collisions() {
|
||||
assert!(unranked_collide("/<a>", "/b"));
|
||||
assert!(unranked_collide("/hello/<name>", "/hello/bob"));
|
||||
assert!(unranked_collide("/<name>", "//bob"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn hard_param_collisions() {
|
||||
assert!(unranked_collide("/<a..>", "///a///"));
|
||||
assert!(unranked_collide("/<a..>", "//a/bcjdklfj//<c>"));
|
||||
assert!(unranked_collide("/a/<a..>", "//a/bcjdklfj//<c>"));
|
||||
assert!(unranked_collide("/a/<b>/<c..>", "//a/bcjdklfj//<c>"));
|
||||
assert!(unranked_collide("/<a..>", "/"));
|
||||
assert!(unranked_collide("/", "/<_..>"));
|
||||
assert!(unranked_collide("/a/b/<a..>", "/a/<b..>"));
|
||||
assert!(unranked_collide("/a/b/<a..>", "/a/<b>/<b..>"));
|
||||
assert!(unranked_collide("/hi/<a..>", "/hi"));
|
||||
assert!(unranked_collide("/hi/<a..>", "/hi/"));
|
||||
assert!(unranked_collide("/<a..>", "//////"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn query_collisions() {
|
||||
assert!(unranked_collide("/?<a>", "/?<a>"));
|
||||
assert!(unranked_collide("/a/?<a>", "/a/?<a>"));
|
||||
assert!(unranked_collide("/a?<a>", "/a?<a>"));
|
||||
assert!(unranked_collide("/<r>?<a>", "/<r>?<a>"));
|
||||
assert!(unranked_collide("/a/b/c?<a>", "/a/b/c?<a>"));
|
||||
assert!(unranked_collide("/<a>/b/c?<d>", "/a/b/<c>?<d>"));
|
||||
assert!(unranked_collide("/?<a>", "/"));
|
||||
assert!(unranked_collide("/a?<a>", "/a"));
|
||||
assert!(unranked_collide("/a?<a>", "/a"));
|
||||
assert!(unranked_collide("/a/b?<a>", "/a/b"));
|
||||
assert!(unranked_collide("/a/b", "/a/b?<c>"));
|
||||
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("/<a>", "/"));
|
||||
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>", "/b/<b>"));
|
||||
assert!(!unranked_collide("/t", "/test"));
|
||||
assert!(!unranked_collide("/a", "/aa"));
|
||||
assert!(!unranked_collide("/a", "/aaa"));
|
||||
assert!(!unranked_collide("/", "/a"));
|
||||
assert_no_collision!("/<a>", "/");
|
||||
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>", "/b/<b>");
|
||||
assert_no_collision!("/<a>/b", "/<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/<a>", "/hello/");
|
||||
|
||||
assert_no_collision!("/a?<b>", "/b");
|
||||
assert_no_collision!("/a/b", "/a?<b>");
|
||||
assert_no_collision!("/a/b/c?<d>", "/a/b/c/d");
|
||||
assert_no_collision!("/a/hello", "/a/?<hello>");
|
||||
assert_no_collision!("/?<a>", "/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 "/<foo..>", 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>", "/b/<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>", "/b/<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>", "/b/<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 "/", "/?<a>");
|
||||
assert_no_collision!(ranked "/a/<b>", "/a/<b>?d");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn query_non_collisions() {
|
||||
assert!(!unranked_collide("/a?<b>", "/b"));
|
||||
assert!(!unranked_collide("/a/b", "/a?<b>"));
|
||||
assert!(!unranked_collide("/a/b/c?<d>", "/a/b/c/d"));
|
||||
assert!(!unranked_collide("/a/hello", "/a/?<hello>"));
|
||||
assert!(!unranked_collide("/?<a>", "/hi"));
|
||||
}
|
||||
fn collisions() {
|
||||
assert_collision!("/a", "/a");
|
||||
assert_collision!("/hello", "/hello");
|
||||
assert_collision!("/hello/there/how/ar", "/hello/there/how/ar");
|
||||
|
||||
#[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, "/<foo..>"), (Post, "/")));
|
||||
}
|
||||
assert_collision!("/<a>", "/<b>");
|
||||
assert_collision!("/<a>", "/b");
|
||||
assert_collision!("/hello/<name>", "/hello/<person>");
|
||||
assert_collision!("/hello/<name>/hi", "/hello/<person>/hi");
|
||||
assert_collision!("/hello/<name>/hi/there", "/hello/<person>/hi/there");
|
||||
assert_collision!("/<name>/hi/there", "/<person>/hi/there");
|
||||
assert_collision!("/<name>/hi/there", "/dude/<name>/there");
|
||||
assert_collision!("/<name>/<a>/<b>", "/<a>/<b>/<c>");
|
||||
assert_collision!("/<name>/<a>/<b>/", "/<a>/<b>/<c>/");
|
||||
assert_collision!("/<a..>", "/hi");
|
||||
assert_collision!("/<a..>", "/hi/hey");
|
||||
assert_collision!("/<a..>", "/hi/hey/hayo");
|
||||
assert_collision!("/a/<a..>", "/a/hi/hey/hayo");
|
||||
assert_collision!("/a/<b>/<a..>", "/a/hi/hey/hayo");
|
||||
assert_collision!("/a/<b>/<c>/<a..>", "/a/hi/hey/hayo");
|
||||
assert_collision!("/<b>/<c>/<a..>", "/a/hi/hey/hayo");
|
||||
assert_collision!("/<b>/<c>/hey/hayo", "/a/hi/hey/hayo");
|
||||
assert_collision!("/<a..>", "/foo");
|
||||
|
||||
#[test]
|
||||
fn query_dependent_non_collisions() {
|
||||
assert!(!m_collide((Get, "/"), (Get, "/?a")));
|
||||
assert!(!m_collide((Get, "/"), (Get, "/?<a>")));
|
||||
assert!(!m_collide((Get, "/a/<b>"), (Get, "/a/<b>?d")));
|
||||
}
|
||||
assert_collision!("/", "/<a..>");
|
||||
assert_collision!("/a", "/a/<a..>");
|
||||
assert_collision!("/a/", "/a/<a..>");
|
||||
assert_collision!("/<a>/", "/a/<a..>");
|
||||
assert_collision!("/<a>", "/a/<a..>");
|
||||
|
||||
#[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>", "/b/<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>", "/b/<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>", "/b/<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_collision!("/<a>", "/b");
|
||||
assert_collision!("/hello/<name>", "/hello/bob");
|
||||
assert_collision!("/<name>", "//bob");
|
||||
|
||||
assert!(s_s_collide("/a/hi/<a..>", "/a/hi/"));
|
||||
assert!(s_s_collide("/hi/<a..>", "/hi/"));
|
||||
assert!(s_s_collide("/<a..>", "/"));
|
||||
assert_collision!("/<a..>", "///a///");
|
||||
assert_collision!("/<a..>", "//a/bcjdklfj//<c>");
|
||||
assert_collision!("/a/<a..>", "//a/bcjdklfj//<c>");
|
||||
assert_collision!("/a/<b>/<c..>", "//a/bcjdklfj//<c>");
|
||||
assert_collision!("/<a..>", "/");
|
||||
assert_collision!("/", "/<_..>");
|
||||
assert_collision!("/a/b/<a..>", "/a/<b..>");
|
||||
assert_collision!("/a/b/<a..>", "/a/<b>/<b..>");
|
||||
assert_collision!("/hi/<a..>", "/hi");
|
||||
assert_collision!("/hi/<a..>", "/hi/");
|
||||
assert_collision!("/<a..>", "//////");
|
||||
|
||||
assert_collision!("/?<a>", "/?<a>");
|
||||
assert_collision!("/a/?<a>", "/a/?<a>");
|
||||
assert_collision!("/a?<a>", "/a?<a>");
|
||||
assert_collision!("/<r>?<a>", "/<r>?<a>");
|
||||
assert_collision!("/a/b/c?<a>", "/a/b/c?<a>");
|
||||
assert_collision!("/<a>/b/c?<d>", "/a/b/<c>?<d>");
|
||||
assert_collision!("/?<a>", "/");
|
||||
assert_collision!("/a?<a>", "/a");
|
||||
assert_collision!("/a?<a>", "/a");
|
||||
assert_collision!("/a/b?<a>", "/a/b");
|
||||
assert_collision!("/a/b", "/a/b?<c>");
|
||||
|
||||
assert_collision!("/a/hi/<a..>", "/a/hi/");
|
||||
assert_collision!("/hi/<a..>", "/hi/");
|
||||
assert_collision!("/<a..>", "/");
|
||||
}
|
||||
|
||||
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<S1, S2>(m: Method, mt1: S1, mt2: S2) -> bool
|
||||
where S1: Into<Option<&'static str>>, S2: Into<Option<&'static str>>
|
||||
{
|
||||
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::<ContentType>().unwrap());
|
||||
} else {
|
||||
req.replace_header(mt_str.parse::<Accept>().unwrap());
|
||||
}
|
||||
}
|
||||
|
||||
let mut route = Route::new(m, "/", dummy_handler);
|
||||
if let Some(mt_str) = mt2.into() {
|
||||
route.format = Some(mt_str.parse::<MediaType>().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?<c>"));
|
||||
assert!(req_route_path_match("/a/b?a=b", "/<a>/b?<c>"));
|
||||
assert!(req_route_path_match("/a/b?a=b", "/<a>/<b>?<c>"));
|
||||
assert!(req_route_path_match("/a/b?a=b", "/a/<b>?<c>"));
|
||||
assert!(req_route_path_match("/?b=c", "/?<b>"));
|
||||
|
||||
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?<c>"));
|
||||
assert!(req_route_path_match("/a/b", "/a/b?<c..>"));
|
||||
assert!(req_route_path_match("/a/b?c", "/a/b?c"));
|
||||
assert!(req_route_path_match("/a/b?c", "/a/b?<c>"));
|
||||
assert!(req_route_path_match("/a/b?c=foo&d=z", "/a/b?<c>"));
|
||||
assert!(req_route_path_match("/a/b?c=foo&d=z", "/a/b?<c..>"));
|
||||
|
||||
assert!(req_route_path_match("/a/b?c=foo&d=z", "/a/b?c=foo&<c..>"));
|
||||
assert!(req_route_path_match("/a/b?c=foo&d=z", "/a/b?d=z&<c..>"));
|
||||
|
||||
assert!(!req_route_path_match("/a/b/c", "/a/b?<c>"));
|
||||
assert!(!req_route_path_match("/a?b=c", "/a/b?<c>"));
|
||||
assert!(!req_route_path_match("/?b=c", "/a/b?<c>"));
|
||||
assert!(!req_route_path_match("/?b=c", "/a?<c>"));
|
||||
|
||||
assert!(!req_route_path_match("/a/b?c=foo&d=z", "/a/b?a=b&<c..>"));
|
||||
assert!(!req_route_path_match("/a/b?c=foo&d=z", "/a/b?d=b&<c..>"));
|
||||
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&<rest..>"));
|
||||
assert!(!req_route_path_match("/a/b", "/a/b?<a>&b&<rest..>"));
|
||||
}
|
||||
|
||||
|
||||
fn catchers_collide<A, B>(a: A, ap: &str, b: B, bp: &str) -> bool
|
||||
where A: Into<Option<u16>>, B: Into<Option<u16>>
|
||||
{
|
||||
|
|
|
@ -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 (<a..>).
|
||||
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/<b..>`.
|
||||
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?<c>"));
|
||||
assert!(req_matches_route("/a/b?a=b", "/<a>/b?<c>"));
|
||||
assert!(req_matches_route("/a/b?a=b", "/<a>/<b>?<c>"));
|
||||
assert!(req_matches_route("/a/b?a=b", "/a/<b>?<c>"));
|
||||
assert!(req_matches_route("/?b=c", "/?<b>"));
|
||||
|
||||
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?<c>"));
|
||||
assert!(req_matches_route("/a/b", "/a/b?<c..>"));
|
||||
assert!(req_matches_route("/a/b?c", "/a/b?c"));
|
||||
assert!(req_matches_route("/a/b?c", "/a/b?<c>"));
|
||||
assert!(req_matches_route("/a/b?c=foo&d=z", "/a/b?<c>"));
|
||||
assert!(req_matches_route("/a/b?c=foo&d=z", "/a/b?<c..>"));
|
||||
|
||||
assert!(req_matches_route("/a/b?c=foo&d=z", "/a/b?c=foo&<c..>"));
|
||||
assert!(req_matches_route("/a/b?c=foo&d=z", "/a/b?d=z&<c..>"));
|
||||
|
||||
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?<c>"));
|
||||
assert!(!req_matches_route("/a?b=c", "/a/b?<c>"));
|
||||
assert!(!req_matches_route("/?b=c", "/a/b?<c>"));
|
||||
assert!(!req_matches_route("/?b=c", "/a?<c>"));
|
||||
|
||||
assert!(!req_matches_route("/a/b?c=foo&d=z", "/a/b?a=b&<c..>"));
|
||||
assert!(!req_matches_route("/a/b?c=foo&d=z", "/a/b?d=b&<c..>"));
|
||||
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&<rest..>"));
|
||||
assert!(!req_matches_route("/a/b", "/a/b?<a>&b&<rest..>"));
|
||||
}
|
||||
|
||||
fn req_matches_format<S1, S2>(m: Method, mt1: S1, mt2: S2) -> bool
|
||||
where S1: Into<Option<&'static str>>, S2: Into<Option<&'static str>>
|
||||
{
|
||||
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::<ContentType>().unwrap());
|
||||
} else {
|
||||
req.replace_header(mt_str.parse::<Accept>().unwrap());
|
||||
}
|
||||
}
|
||||
|
||||
let mut route = Route::new(m, "/", dummy_handler);
|
||||
if let Some(mt_str) = mt2.into() {
|
||||
route.format = Some(mt_str.parse::<MediaType>().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"));
|
||||
}
|
||||
}
|
|
@ -2,6 +2,7 @@
|
|||
|
||||
mod router;
|
||||
mod collider;
|
||||
mod matcher;
|
||||
|
||||
pub(crate) use router::*;
|
||||
pub(crate) use collider::*;
|
||||
|
|
|
@ -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(&["/<a>", "/hello//"]));
|
||||
assert!(rankless_route_collisions(&["/<a>", "/hello///"]));
|
||||
assert!(rankless_route_collisions(&["/hello///bob", "/hello/<b>"]));
|
||||
assert!(rankless_route_collisions(&["/<a..>//", "/a//<a..>"]));
|
||||
assert!(rankless_route_collisions(&["/a/<a..>//", "/a/<a..>"]));
|
||||
assert!(rankless_route_collisions(&["/a/<a..>//", "/a/b//c//d/"]));
|
||||
assert!(rankless_route_collisions(&["/<a..>//", "/a//<a..>"]));
|
||||
assert!(rankless_route_collisions(&["/a/<a..>/", "/a/bd/e/"]));
|
||||
assert!(rankless_route_collisions(&["/<a..>/", "/a/bd/e/"]));
|
||||
assert!(rankless_route_collisions(&["//", "/<foo..>"]));
|
||||
|
@ -233,6 +224,11 @@ mod test {
|
|||
|
||||
#[test]
|
||||
fn test_no_collisions() {
|
||||
assert!(!rankless_route_collisions(&["/a", "/a/"]));
|
||||
assert!(!rankless_route_collisions(&["/<a>", "/hello//"]));
|
||||
assert!(!rankless_route_collisions(&["/<a>", "/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/<d>/e"]));
|
||||
assert!(!rankless_route_collisions(&["/a/d/<b..>", "/a/b/c"]));
|
||||
|
@ -309,7 +305,6 @@ mod test {
|
|||
|
||||
let router = router_with_routes(&["/<a>/<b>"]);
|
||||
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(&["/<a>/<b>"]);
|
||||
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/<a..>"]);
|
||||
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);
|
||||
}
|
||||
})
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
/// }
|
||||
///
|
||||
|
|
|
@ -74,6 +74,10 @@ fn hello(lang: Option<Lang>, 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])
|
||||
|
|
Loading…
Reference in New Issue