(&self, resource: &mut R) -> Option<(&T, ResourceId)>
+ where
+ R: Resource,
+ P: ResourcePath,
+ {
+ profile_method!(recognize);
+
+ for item in self.routes.iter() {
+ if item.0.capture_match_info(resource.resource_path()) {
+ return Some((&item.1, ResourceId(item.0.id())));
+ }
+ }
+
+ None
+ }
+
+ pub fn recognize_mut(&mut self, resource: &mut R) -> Option<(&mut T, ResourceId)>
+ where
+ R: Resource,
+ P: ResourcePath,
+ {
+ profile_method!(recognize_mut);
+
+ for item in self.routes.iter_mut() {
+ if item.0.capture_match_info(resource.resource_path()) {
+ return Some((&mut item.1, ResourceId(item.0.id())));
+ }
+ }
+
+ None
+ }
+
+ pub fn recognize_fn(&self, resource: &mut R, check: F) -> Option<(&T, ResourceId)>
+ where
+ F: Fn(&R, &Option) -> bool,
+ R: Resource,
+ P: ResourcePath,
+ {
+ profile_method!(recognize_checked);
+
+ for item in self.routes.iter() {
+ if item.0.capture_match_info_fn(resource, &check, &item.2) {
+ return Some((&item.1, ResourceId(item.0.id())));
+ }
+ }
+
+ None
+ }
+
+ pub fn recognize_mut_fn(
+ &mut self,
+ resource: &mut R,
+ check: F,
+ ) -> Option<(&mut T, ResourceId)>
+ where
+ F: Fn(&R, &Option) -> bool,
+ R: Resource,
+ P: ResourcePath,
+ {
+ profile_method!(recognize_mut_checked);
+
+ for item in self.routes.iter_mut() {
+ if item.0.capture_match_info_fn(resource, &check, &item.2) {
+ return Some((&mut item.1, ResourceId(item.0.id())));
+ }
+ }
+
+ None
+ }
+}
+
+pub struct RouterBuilder {
+ resources: Vec<(ResourceDef, T, Option)>,
+}
+
+impl RouterBuilder {
+ /// Register resource for specified path.
+ pub fn path(
+ &mut self,
+ path: P,
+ resource: T,
+ ) -> &mut (ResourceDef, T, Option) {
+ profile_method!(path);
+
+ self.resources
+ .push((ResourceDef::new(path), resource, None));
+ self.resources.last_mut().unwrap()
+ }
+
+ /// Register resource for specified path prefix.
+ pub fn prefix(&mut self, prefix: &str, resource: T) -> &mut (ResourceDef, T, Option) {
+ profile_method!(prefix);
+
+ self.resources
+ .push((ResourceDef::prefix(prefix), resource, None));
+ self.resources.last_mut().unwrap()
+ }
+
+ /// Register resource for ResourceDef
+ pub fn rdef(&mut self, rdef: ResourceDef, resource: T) -> &mut (ResourceDef, T, Option) {
+ profile_method!(rdef);
+
+ self.resources.push((rdef, resource, None));
+ self.resources.last_mut().unwrap()
+ }
+
+ /// Finish configuration and create router instance.
+ pub fn finish(self) -> Router {
+ Router {
+ routes: self.resources,
+ }
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use crate::path::Path;
+ use crate::router::{ResourceId, Router};
+
+ #[allow(clippy::cognitive_complexity)]
+ #[test]
+ fn test_recognizer_1() {
+ let mut router = Router::::build();
+ router.path("/name", 10).0.set_id(0);
+ router.path("/name/{val}", 11).0.set_id(1);
+ router.path("/name/{val}/index.html", 12).0.set_id(2);
+ router.path("/file/{file}.{ext}", 13).0.set_id(3);
+ router.path("/v{val}/{val2}/index.html", 14).0.set_id(4);
+ router.path("/v/{tail:.*}", 15).0.set_id(5);
+ router.path("/test2/{test}.html", 16).0.set_id(6);
+ router.path("/{test}/index.html", 17).0.set_id(7);
+ let mut router = router.finish();
+
+ let mut path = Path::new("/unknown");
+ assert!(router.recognize_mut(&mut path).is_none());
+
+ let mut path = Path::new("/name");
+ let (h, info) = router.recognize_mut(&mut path).unwrap();
+ assert_eq!(*h, 10);
+ assert_eq!(info, ResourceId(0));
+ assert!(path.is_empty());
+
+ let mut path = Path::new("/name/value");
+ let (h, info) = router.recognize_mut(&mut path).unwrap();
+ assert_eq!(*h, 11);
+ assert_eq!(info, ResourceId(1));
+ assert_eq!(path.get("val").unwrap(), "value");
+ assert_eq!(&path["val"], "value");
+
+ let mut path = Path::new("/name/value2/index.html");
+ let (h, info) = router.recognize_mut(&mut path).unwrap();
+ assert_eq!(*h, 12);
+ assert_eq!(info, ResourceId(2));
+ assert_eq!(path.get("val").unwrap(), "value2");
+
+ let mut path = Path::new("/file/file.gz");
+ let (h, info) = router.recognize_mut(&mut path).unwrap();
+ assert_eq!(*h, 13);
+ assert_eq!(info, ResourceId(3));
+ assert_eq!(path.get("file").unwrap(), "file");
+ assert_eq!(path.get("ext").unwrap(), "gz");
+
+ let mut path = Path::new("/vtest/ttt/index.html");
+ let (h, info) = router.recognize_mut(&mut path).unwrap();
+ assert_eq!(*h, 14);
+ assert_eq!(info, ResourceId(4));
+ assert_eq!(path.get("val").unwrap(), "test");
+ assert_eq!(path.get("val2").unwrap(), "ttt");
+
+ let mut path = Path::new("/v/blah-blah/index.html");
+ let (h, info) = router.recognize_mut(&mut path).unwrap();
+ assert_eq!(*h, 15);
+ assert_eq!(info, ResourceId(5));
+ assert_eq!(path.get("tail").unwrap(), "blah-blah/index.html");
+
+ let mut path = Path::new("/test2/index.html");
+ let (h, info) = router.recognize_mut(&mut path).unwrap();
+ assert_eq!(*h, 16);
+ assert_eq!(info, ResourceId(6));
+ assert_eq!(path.get("test").unwrap(), "index");
+
+ let mut path = Path::new("/bbb/index.html");
+ let (h, info) = router.recognize_mut(&mut path).unwrap();
+ assert_eq!(*h, 17);
+ assert_eq!(info, ResourceId(7));
+ assert_eq!(path.get("test").unwrap(), "bbb");
+ }
+
+ #[test]
+ fn test_recognizer_2() {
+ let mut router = Router::::build();
+ router.path("/index.json", 10);
+ router.path("/{source}.json", 11);
+ let mut router = router.finish();
+
+ let mut path = Path::new("/index.json");
+ let (h, _) = router.recognize_mut(&mut path).unwrap();
+ assert_eq!(*h, 10);
+
+ let mut path = Path::new("/test.json");
+ let (h, _) = router.recognize_mut(&mut path).unwrap();
+ assert_eq!(*h, 11);
+ }
+
+ #[test]
+ fn test_recognizer_with_prefix() {
+ let mut router = Router::::build();
+ router.path("/name", 10).0.set_id(0);
+ router.path("/name/{val}", 11).0.set_id(1);
+ let mut router = router.finish();
+
+ let mut path = Path::new("/name");
+ path.skip(5);
+ assert!(router.recognize_mut(&mut path).is_none());
+
+ let mut path = Path::new("/test/name");
+ path.skip(5);
+ let (h, _) = router.recognize_mut(&mut path).unwrap();
+ assert_eq!(*h, 10);
+
+ let mut path = Path::new("/test/name/value");
+ path.skip(5);
+ let (h, id) = router.recognize_mut(&mut path).unwrap();
+ assert_eq!(*h, 11);
+ assert_eq!(id, ResourceId(1));
+ assert_eq!(path.get("val").unwrap(), "value");
+ assert_eq!(&path["val"], "value");
+
+ // same patterns
+ let mut router = Router::::build();
+ router.path("/name", 10);
+ router.path("/name/{val}", 11);
+ let mut router = router.finish();
+
+ let mut path = Path::new("/name");
+ path.skip(6);
+ assert!(router.recognize_mut(&mut path).is_none());
+
+ let mut path = Path::new("/test2/name");
+ path.skip(6);
+ let (h, _) = router.recognize_mut(&mut path).unwrap();
+ assert_eq!(*h, 10);
+
+ let mut path = Path::new("/test2/name-test");
+ path.skip(6);
+ assert!(router.recognize_mut(&mut path).is_none());
+
+ let mut path = Path::new("/test2/name/ttt");
+ path.skip(6);
+ let (h, _) = router.recognize_mut(&mut path).unwrap();
+ assert_eq!(*h, 11);
+ assert_eq!(&path["val"], "ttt");
+ }
+}
diff --git a/actix-router/src/url.rs b/actix-router/src/url.rs
new file mode 100644
index 000000000..e08a7171a
--- /dev/null
+++ b/actix-router/src/url.rs
@@ -0,0 +1,288 @@
+use crate::ResourcePath;
+
+#[allow(dead_code)]
+const GEN_DELIMS: &[u8] = b":/?#[]@";
+#[allow(dead_code)]
+const SUB_DELIMS_WITHOUT_QS: &[u8] = b"!$'()*,";
+#[allow(dead_code)]
+const SUB_DELIMS: &[u8] = b"!$'()*,+?=;";
+#[allow(dead_code)]
+const RESERVED: &[u8] = b":/?#[]@!$'()*,+?=;";
+#[allow(dead_code)]
+const UNRESERVED: &[u8] = b"abcdefghijklmnopqrstuvwxyz
+ ABCDEFGHIJKLMNOPQRSTUVWXYZ
+ 1234567890
+ -._~";
+const ALLOWED: &[u8] = b"abcdefghijklmnopqrstuvwxyz
+ ABCDEFGHIJKLMNOPQRSTUVWXYZ
+ 1234567890
+ -._~
+ !$'()*,";
+const QS: &[u8] = b"+&=;b";
+
+#[inline]
+fn bit_at(array: &[u8], ch: u8) -> bool {
+ array[(ch >> 3) as usize] & (1 << (ch & 7)) != 0
+}
+
+#[inline]
+fn set_bit(array: &mut [u8], ch: u8) {
+ array[(ch >> 3) as usize] |= 1 << (ch & 7)
+}
+
+thread_local! {
+ static DEFAULT_QUOTER: Quoter = Quoter::new(b"@:", b"%/+");
+}
+
+#[derive(Default, Clone, Debug)]
+pub struct Url {
+ uri: http::Uri,
+ path: Option,
+}
+
+impl Url {
+ pub fn new(uri: http::Uri) -> Url {
+ let path = DEFAULT_QUOTER.with(|q| q.requote(uri.path().as_bytes()));
+
+ Url { uri, path }
+ }
+
+ pub fn with_quoter(uri: http::Uri, quoter: &Quoter) -> Url {
+ Url {
+ path: quoter.requote(uri.path().as_bytes()),
+ uri,
+ }
+ }
+
+ pub fn uri(&self) -> &http::Uri {
+ &self.uri
+ }
+
+ pub fn path(&self) -> &str {
+ if let Some(ref s) = self.path {
+ s
+ } else {
+ self.uri.path()
+ }
+ }
+
+ #[inline]
+ pub fn update(&mut self, uri: &http::Uri) {
+ self.uri = uri.clone();
+ self.path = DEFAULT_QUOTER.with(|q| q.requote(uri.path().as_bytes()));
+ }
+
+ #[inline]
+ pub fn update_with_quoter(&mut self, uri: &http::Uri, quoter: &Quoter) {
+ self.uri = uri.clone();
+ self.path = quoter.requote(uri.path().as_bytes());
+ }
+}
+
+impl ResourcePath for Url {
+ #[inline]
+ fn path(&self) -> &str {
+ self.path()
+ }
+}
+
+pub struct Quoter {
+ safe_table: [u8; 16],
+ protected_table: [u8; 16],
+}
+
+impl Quoter {
+ pub fn new(safe: &[u8], protected: &[u8]) -> Quoter {
+ let mut q = Quoter {
+ safe_table: [0; 16],
+ protected_table: [0; 16],
+ };
+
+ // prepare safe table
+ for i in 0..128 {
+ if ALLOWED.contains(&i) {
+ set_bit(&mut q.safe_table, i);
+ }
+ if QS.contains(&i) {
+ set_bit(&mut q.safe_table, i);
+ }
+ }
+
+ for ch in safe {
+ set_bit(&mut q.safe_table, *ch)
+ }
+
+ // prepare protected table
+ for ch in protected {
+ set_bit(&mut q.safe_table, *ch);
+ set_bit(&mut q.protected_table, *ch);
+ }
+
+ q
+ }
+
+ pub fn requote(&self, val: &[u8]) -> Option {
+ let mut has_pct = 0;
+ let mut pct = [b'%', 0, 0];
+ let mut idx = 0;
+ let mut cloned: Option> = None;
+
+ let len = val.len();
+ while idx < len {
+ let ch = val[idx];
+
+ if has_pct != 0 {
+ pct[has_pct] = val[idx];
+ has_pct += 1;
+ if has_pct == 3 {
+ has_pct = 0;
+ let buf = cloned.as_mut().unwrap();
+
+ if let Some(ch) = restore_ch(pct[1], pct[2]) {
+ if ch < 128 {
+ if bit_at(&self.protected_table, ch) {
+ buf.extend_from_slice(&pct);
+ idx += 1;
+ continue;
+ }
+
+ if bit_at(&self.safe_table, ch) {
+ buf.push(ch);
+ idx += 1;
+ continue;
+ }
+ }
+ buf.push(ch);
+ } else {
+ buf.extend_from_slice(&pct[..]);
+ }
+ }
+ } else if ch == b'%' {
+ has_pct = 1;
+ if cloned.is_none() {
+ let mut c = Vec::with_capacity(len);
+ c.extend_from_slice(&val[..idx]);
+ cloned = Some(c);
+ }
+ } else if let Some(ref mut cloned) = cloned {
+ cloned.push(ch)
+ }
+ idx += 1;
+ }
+
+ cloned.map(|data| String::from_utf8_lossy(&data).into_owned())
+ }
+}
+
+#[inline]
+fn from_hex(v: u8) -> Option {
+ if (b'0'..=b'9').contains(&v) {
+ Some(v - 0x30) // ord('0') == 0x30
+ } else if (b'A'..=b'F').contains(&v) {
+ Some(v - 0x41 + 10) // ord('A') == 0x41
+ } else if (b'a'..=b'f').contains(&v) {
+ Some(v - 0x61 + 10) // ord('a') == 0x61
+ } else {
+ None
+ }
+}
+
+#[inline]
+fn restore_ch(d1: u8, d2: u8) -> Option {
+ from_hex(d1).and_then(|d1| from_hex(d2).map(move |d2| d1 << 4 | d2))
+}
+
+#[cfg(test)]
+mod tests {
+ use http::Uri;
+ use std::convert::TryFrom;
+
+ use super::*;
+ use crate::{Path, ResourceDef};
+
+ const PROTECTED: &[u8] = b"%/+";
+
+ fn match_url(pattern: &'static str, url: impl AsRef) -> Path {
+ let re = ResourceDef::new(pattern);
+ let uri = Uri::try_from(url.as_ref()).unwrap();
+ let mut path = Path::new(Url::new(uri));
+ assert!(re.capture_match_info(&mut path));
+ path
+ }
+
+ fn percent_encode(data: &[u8]) -> String {
+ data.iter().map(|c| format!("%{:02X}", c)).collect()
+ }
+
+ #[test]
+ fn test_parse_url() {
+ let re = "/user/{id}/test";
+
+ let path = match_url(re, "/user/2345/test");
+ assert_eq!(path.get("id").unwrap(), "2345");
+
+ // "%25" should never be decoded into '%' to guarantee the output is a valid
+ // percent-encoded format
+ let path = match_url(re, "/user/qwe%25/test");
+ assert_eq!(path.get("id").unwrap(), "qwe%25");
+
+ let path = match_url(re, "/user/qwe%25rty/test");
+ assert_eq!(path.get("id").unwrap(), "qwe%25rty");
+ }
+
+ #[test]
+ fn test_protected_chars() {
+ let encoded = percent_encode(PROTECTED);
+ let path = match_url("/user/{id}/test", format!("/user/{}/test", encoded));
+ assert_eq!(path.get("id").unwrap(), &encoded);
+ }
+
+ #[test]
+ fn test_non_protecteed_ascii() {
+ let nonprotected_ascii = ('\u{0}'..='\u{7F}')
+ .filter(|&c| c.is_ascii() && !PROTECTED.contains(&(c as u8)))
+ .collect::();
+ let encoded = percent_encode(nonprotected_ascii.as_bytes());
+ let path = match_url("/user/{id}/test", format!("/user/{}/test", encoded));
+ assert_eq!(path.get("id").unwrap(), &nonprotected_ascii);
+ }
+
+ #[test]
+ fn test_valid_utf8_multibyte() {
+ let test = ('\u{FF00}'..='\u{FFFF}').collect::();
+ let encoded = percent_encode(test.as_bytes());
+ let path = match_url("/a/{id}/b", format!("/a/{}/b", &encoded));
+ assert_eq!(path.get("id").unwrap(), &test);
+ }
+
+ #[test]
+ fn test_invalid_utf8() {
+ let invalid_utf8 = percent_encode((0x80..=0xff).collect::>().as_slice());
+ let uri = Uri::try_from(format!("/{}", invalid_utf8)).unwrap();
+ let path = Path::new(Url::new(uri));
+
+ // We should always get a valid utf8 string
+ assert!(String::from_utf8(path.path().as_bytes().to_owned()).is_ok());
+ }
+
+ #[test]
+ fn test_from_hex() {
+ let hex = b"0123456789abcdefABCDEF";
+
+ for i in 0..256 {
+ let c = i as u8;
+ if hex.contains(&c) {
+ assert!(from_hex(c).is_some())
+ } else {
+ assert!(from_hex(c).is_none())
+ }
+ }
+
+ let expected = [
+ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 10, 11, 12, 13, 14, 15,
+ ];
+ for i in 0..hex.len() {
+ assert_eq!(from_hex(hex[i]).unwrap(), expected[i]);
+ }
+ }
+}
diff --git a/actix-test/CHANGES.md b/actix-test/CHANGES.md
index fa554ba2e..dc76ba3fd 100644
--- a/actix-test/CHANGES.md
+++ b/actix-test/CHANGES.md
@@ -1,6 +1,7 @@
# Changes
## Unreleased - 2021-xx-xx
+* Minimum supported Rust version (MSRV) is now 1.51.
## 0.1.0-beta.3 - 2021-06-20
diff --git a/actix-web-actors/CHANGES.md b/actix-web-actors/CHANGES.md
index bf642ef95..084e7b272 100644
--- a/actix-web-actors/CHANGES.md
+++ b/actix-web-actors/CHANGES.md
@@ -1,6 +1,7 @@
# Changes
## Unreleased - 2021-xx-xx
+* Minimum supported Rust version (MSRV) is now 1.51.
## 4.0.0-beta.6 - 2021-06-26
diff --git a/actix-web-actors/README.md b/actix-web-actors/README.md
index 5f8f78bde..2858d3f20 100644
--- a/actix-web-actors/README.md
+++ b/actix-web-actors/README.md
@@ -4,7 +4,7 @@
[](https://crates.io/crates/actix-web-actors)
[](https://docs.rs/actix-web-actors/4.0.0-beta.6)
-[](https://blog.rust-lang.org/2020/03/12/Rust-1.46.html)
+[](https://blog.rust-lang.org/2020/03/12/Rust-1.51.html)

[](https://deps.rs/crate/actix-web-actors/4.0.0-beta.6)
@@ -14,4 +14,4 @@
## Documentation & Resources
- [API Documentation](https://docs.rs/actix-web-actors)
-- Minimum supported Rust version: 1.46 or later
+- Minimum supported Rust version: 1.51 or later
diff --git a/actix-web-codegen/CHANGES.md b/actix-web-codegen/CHANGES.md
index a8a901f72..f0a56b30f 100644
--- a/actix-web-codegen/CHANGES.md
+++ b/actix-web-codegen/CHANGES.md
@@ -1,6 +1,10 @@
# Changes
## Unreleased - 2021-xx-xx
+* In routing macros, paths are now validated at compile time. [#2350]
+* Minimum supported Rust version (MSRV) is now 1.51.
+
+[#2350]: https://github.com/actix/actix-web/pull/2350
## 0.5.0-beta.3 - 2021-06-17
diff --git a/actix-web-codegen/Cargo.toml b/actix-web-codegen/Cargo.toml
index 4d0fd5e26..66f7acf6d 100644
--- a/actix-web-codegen/Cargo.toml
+++ b/actix-web-codegen/Cargo.toml
@@ -17,6 +17,7 @@ proc-macro = true
quote = "1"
syn = { version = "1", features = ["full", "parsing"] }
proc-macro2 = "1"
+actix-router = "0.5.0-beta.1"
[dev-dependencies]
actix-rt = "2.2"
diff --git a/actix-web-codegen/README.md b/actix-web-codegen/README.md
index 96e4cb51f..e69cfbbe5 100644
--- a/actix-web-codegen/README.md
+++ b/actix-web-codegen/README.md
@@ -4,7 +4,7 @@
[](https://crates.io/crates/actix-web-codegen)
[](https://docs.rs/actix-web-codegen/0.5.0-beta.3)
-[](https://blog.rust-lang.org/2020/03/12/Rust-1.46.html)
+[](https://blog.rust-lang.org/2020/03/12/Rust-1.51.html)

[](https://deps.rs/crate/actix-web-codegen/0.5.0-beta.3)
@@ -14,7 +14,7 @@
## Documentation & Resources
- [API Documentation](https://docs.rs/actix-web-codegen)
-- Minimum supported Rust version: 1.46 or later.
+- Minimum supported Rust version: 1.51 or later.
## Compile Testing
diff --git a/actix-web-codegen/src/route.rs b/actix-web-codegen/src/route.rs
index 747042527..c2f851a0e 100644
--- a/actix-web-codegen/src/route.rs
+++ b/actix-web-codegen/src/route.rs
@@ -3,6 +3,7 @@ extern crate proc_macro;
use std::collections::HashSet;
use std::convert::TryFrom;
+use actix_router::ResourceDef;
use proc_macro::TokenStream;
use proc_macro2::{Span, TokenStream as TokenStream2};
use quote::{format_ident, quote, ToTokens, TokenStreamExt};
@@ -101,6 +102,7 @@ impl Args {
match arg {
NestedMeta::Lit(syn::Lit::Str(lit)) => match path {
None => {
+ let _ = ResourceDef::new(lit.value());
path = Some(lit);
}
_ => {
diff --git a/actix-web-codegen/tests/trybuild.rs b/actix-web-codegen/tests/trybuild.rs
index 12e848cf3..54bc1caec 100644
--- a/actix-web-codegen/tests/trybuild.rs
+++ b/actix-web-codegen/tests/trybuild.rs
@@ -1,4 +1,4 @@
-#[rustversion::stable(1.46)] // MSRV
+#[rustversion::stable(1.51)] // MSRV
#[test]
fn compile_macros() {
let t = trybuild::TestCases::new();
@@ -10,6 +10,7 @@ fn compile_macros() {
t.compile_fail("tests/trybuild/route-missing-method-fail.rs");
t.compile_fail("tests/trybuild/route-duplicate-method-fail.rs");
t.compile_fail("tests/trybuild/route-unexpected-method-fail.rs");
+ t.compile_fail("tests/trybuild/route-malformed-path-fail.rs");
t.pass("tests/trybuild/docstring-ok.rs");
}
diff --git a/actix-web-codegen/tests/trybuild/route-malformed-path-fail.rs b/actix-web-codegen/tests/trybuild/route-malformed-path-fail.rs
new file mode 100644
index 000000000..1258a6f2f
--- /dev/null
+++ b/actix-web-codegen/tests/trybuild/route-malformed-path-fail.rs
@@ -0,0 +1,33 @@
+use actix_web_codegen::get;
+
+#[get("/{")]
+async fn zero() -> &'static str {
+ "malformed resource def"
+}
+
+#[get("/{foo")]
+async fn one() -> &'static str {
+ "malformed resource def"
+}
+
+#[get("/{}")]
+async fn two() -> &'static str {
+ "malformed resource def"
+}
+
+#[get("/*")]
+async fn three() -> &'static str {
+ "malformed resource def"
+}
+
+#[get("/{tail:\\d+}*")]
+async fn four() -> &'static str {
+ "malformed resource def"
+}
+
+#[get("/{a}/{b}/{c}/{d}/{e}/{f}/{g}/{h}/{i}/{j}/{k}/{l}/{m}/{n}/{o}/{p}/{q}")]
+async fn five() -> &'static str {
+ "malformed resource def"
+}
+
+fn main() {}
diff --git a/actix-web-codegen/tests/trybuild/route-malformed-path-fail.stderr b/actix-web-codegen/tests/trybuild/route-malformed-path-fail.stderr
new file mode 100644
index 000000000..93c510109
--- /dev/null
+++ b/actix-web-codegen/tests/trybuild/route-malformed-path-fail.stderr
@@ -0,0 +1,42 @@
+error: custom attribute panicked
+ --> $DIR/route-malformed-path-fail.rs:3:1
+ |
+3 | #[get("/{")]
+ | ^^^^^^^^^^^^
+ |
+ = help: message: pattern "{" contains malformed dynamic segment
+
+error: custom attribute panicked
+ --> $DIR/route-malformed-path-fail.rs:8:1
+ |
+8 | #[get("/{foo")]
+ | ^^^^^^^^^^^^^^^
+ |
+ = help: message: pattern "{foo" contains malformed dynamic segment
+
+error: custom attribute panicked
+ --> $DIR/route-malformed-path-fail.rs:13:1
+ |
+13 | #[get("/{}")]
+ | ^^^^^^^^^^^^^
+ |
+ = help: message: Wrong path pattern: "/{}" regex parse error:
+ ((?s-m)^/(?P<>[^/]+))$
+ ^
+ error: empty capture group name
+
+error: custom attribute panicked
+ --> $DIR/route-malformed-path-fail.rs:23:1
+ |
+23 | #[get("/{tail:\\d+}*")]
+ | ^^^^^^^^^^^^^^^^^^^^^^^
+ |
+ = help: message: custom regex is not supported for tail match
+
+error: custom attribute panicked
+ --> $DIR/route-malformed-path-fail.rs:28:1
+ |
+28 | #[get("/{a}/{b}/{c}/{d}/{e}/{f}/{g}/{h}/{i}/{j}/{k}/{l}/{m}/{n}/{o}/{p}/{q}")]
+ | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+ |
+ = help: message: Only 16 dynamic segments are allowed, provided: 17
diff --git a/awc/README.md b/awc/README.md
index dd08c6e10..fe91383ca 100644
--- a/awc/README.md
+++ b/awc/README.md
@@ -12,7 +12,7 @@
- [API Documentation](https://docs.rs/awc)
- [Example Project](https://github.com/actix/examples/tree/HEAD/security/awc_https)
-- Minimum Supported Rust Version (MSRV): 1.46.0
+- Minimum Supported Rust Version (MSRV): 1.51.0
## Example
diff --git a/clippy.toml b/clippy.toml
index eb66960ac..829dd1c59 100644
--- a/clippy.toml
+++ b/clippy.toml
@@ -1 +1 @@
-msrv = "1.46"
+msrv = "1.51"
diff --git a/docs/graphs/net-only.dot b/docs/graphs/net-only.dot
index bee0185ab..8a58ec2b8 100644
--- a/docs/graphs/net-only.dot
+++ b/docs/graphs/net-only.dot
@@ -4,7 +4,7 @@ digraph {
subgraph cluster_net {
label="actix-net"
"actix-codec" "actix-macros" "actix-rt" "actix-server" "actix-service"
- "actix-tls" "actix-tracing" "actix-utils" "actix-router"
+ "actix-tls" "actix-tracing" "actix-utils"
}
subgraph cluster_other {
@@ -25,7 +25,6 @@ digraph {
"actix-tls" -> { "tokio-util" }[color="#009900"]
"actix-server" -> { "actix-service" "actix-rt" "actix-utils" "tokio" }
"actix-rt" -> { "actix-macros" "tokio" }
- "actix-router" -> { "bytestring" }
"local-channel" -> { "local-waker" }
diff --git a/docs/graphs/web-focus.dot b/docs/graphs/web-focus.dot
index 2c6e2779b..63b3eaa82 100644
--- a/docs/graphs/web-focus.dot
+++ b/docs/graphs/web-focus.dot
@@ -10,6 +10,7 @@ digraph {
"web-actors"
"web-codegen"
"http-test"
+ "router"
{ rank=same; "multipart" "web-actors" "http-test" };
{ rank=same; "files" "awc" "web" };
@@ -36,7 +37,7 @@ digraph {
"rt" -> { "macros" }
{ rank=same; "utils" "codec" };
- { rank=same; "rt" "macros" "service" "router" };
+ { rank=same; "rt" "macros" "service" };
// actix
diff --git a/docs/graphs/web-only.dot b/docs/graphs/web-only.dot
index b0decd818..ee74c292b 100644
--- a/docs/graphs/web-only.dot
+++ b/docs/graphs/web-only.dot
@@ -10,9 +10,10 @@ digraph {
"actix-web-codegen"
"actix-http-test"
"actix-test"
+ "actix-router"
}
- "actix-web" -> { "actix-web-codegen" "actix-http" }
+ "actix-web" -> { "actix-web-codegen" "actix-http" "actix-router" }
"awc" -> { "actix-http" }
"actix-web-actors" -> { "actix" "actix-web" "actix-http" }
"actix-multipart" -> { "actix-web" }
diff --git a/src/app.rs b/src/app.rs
index 5cff20568..da5b45f3a 100644
--- a/src/app.rs
+++ b/src/app.rs
@@ -334,7 +334,7 @@ where
U: AsRef,
{
let mut rdef = ResourceDef::new(url.as_ref());
- *rdef.name_mut() = name.as_ref().to_string();
+ rdef.set_name(name.as_ref());
self.external.push(rdef);
self
}
diff --git a/src/app_service.rs b/src/app_service.rs
index 3c1b78474..ce52543b8 100644
--- a/src/app_service.rs
+++ b/src/app_service.rs
@@ -291,7 +291,7 @@ impl Service for AppRouting {
actix_service::always_ready!();
fn call(&self, mut req: ServiceRequest) -> Self::Future {
- let res = self.router.recognize_checked(&mut req, |req, guards| {
+ let res = self.router.recognize_fn(&mut req, |req, guards| {
if let Some(ref guards) = guards {
for f in guards {
if !f.check(req.head()) {
diff --git a/src/config.rs b/src/config.rs
index b072ace16..9e77c0f96 100644
--- a/src/config.rs
+++ b/src/config.rs
@@ -249,7 +249,7 @@ impl ServiceConfig {
U: AsRef,
{
let mut rdef = ResourceDef::new(url.as_ref());
- *rdef.name_mut() = name.as_ref().to_string();
+ rdef.set_name(name.as_ref());
self.external.push(rdef);
self
}
diff --git a/src/dev.rs b/src/dev.rs
index b8d95efbb..0817d902f 100644
--- a/src/dev.rs
+++ b/src/dev.rs
@@ -28,11 +28,22 @@ pub use actix_service::{
use crate::http::header::ContentEncoding;
use actix_http::{Response, ResponseBuilder};
-pub(crate) fn insert_leading_slash(mut patterns: Vec) -> Vec {
- for path in &mut patterns {
- if !path.is_empty() && !path.starts_with('/') {
- path.insert(0, '/');
- };
+use actix_router::Patterns;
+
+pub(crate) fn ensure_leading_slash(mut patterns: Patterns) -> Patterns {
+ match &mut patterns {
+ Patterns::Single(pat) => {
+ if !pat.is_empty() && !pat.starts_with('/') {
+ pat.insert(0, '/');
+ };
+ }
+ Patterns::List(pats) => {
+ for pat in pats {
+ if !pat.is_empty() && !pat.starts_with('/') {
+ pat.insert(0, '/');
+ };
+ }
+ }
}
patterns
diff --git a/src/http/header/content_disposition.rs b/src/http/header/content_disposition.rs
index 9f67baffb..6e75fde92 100644
--- a/src/http/header/content_disposition.rs
+++ b/src/http/header/content_disposition.rs
@@ -457,7 +457,7 @@ impl Header for ContentDisposition {
fn parse(msg: &T) -> Result {
if let Some(h) = msg.headers().get(&Self::name()) {
- Self::from_raw(&h)
+ Self::from_raw(h)
} else {
Err(crate::error::ParseError::Header)
}
diff --git a/src/lib.rs b/src/lib.rs
index 714c759cf..e7cf46361 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -53,7 +53,7 @@
//! * SSL support using OpenSSL or Rustls
//! * Middlewares ([Logger, Session, CORS, etc](https://actix.rs/docs/middleware/))
//! * Includes an async [HTTP client](https://docs.rs/awc/)
-//! * Runs on stable Rust 1.46+
+//! * Runs on stable Rust 1.51+
//!
//! # Crate Features
//! * `cookies` - cookies support (enabled by default)
diff --git a/src/middleware/logger.rs b/src/middleware/logger.rs
index bbb0e3dc4..9574b02f7 100644
--- a/src/middleware/logger.rs
+++ b/src/middleware/logger.rs
@@ -341,7 +341,6 @@ where
) -> Poll