diff --git a/Cargo.toml b/Cargo.toml index 6ed9b50a..eb452e1a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -2,7 +2,6 @@ members = [ "actix-codec", "actix-macros", - "actix-router", "actix-rt", "actix-server", "actix-service", @@ -17,7 +16,6 @@ members = [ [patch.crates-io] actix-codec = { path = "actix-codec" } actix-macros = { path = "actix-macros" } -actix-router = { path = "actix-router" } actix-rt = { path = "actix-rt" } actix-server = { path = "actix-server" } actix-service = { path = "actix-service" } diff --git a/actix-router/CHANGES.md b/actix-router/CHANGES.md deleted file mode 100644 index 0ace6c8f..00000000 --- a/actix-router/CHANGES.md +++ /dev/null @@ -1,114 +0,0 @@ -# Changes - -## Unreleased - 2021-xx-xx -* Introduce `ResourceDef::join`. [#380] - -[#380]: https://github.com/actix/actix-net/pull/380 - - -## 0.5.0-beta.1 - 2021-07-20 -* Fix a bug in multi-patterns where static patterns are interpreted as regex. [#366] -* Introduce `ResourceDef::pattern_iter` to get an iterator over all patterns in a multi-pattern resource. [#373] -* Fix segment interpolation leaving `Path` in unintended state after matching. [#368] -* Fix `ResourceDef` `PartialEq` implementation. [#373] -* Re-work `IntoPatterns` trait, adding a `Patterns` enum. [#372] -* Implement `IntoPatterns` for `bytestring::ByteString`. [#372] -* Rename `Path::{len => segment_count}` to be more descriptive of it's purpose. [#370] -* Rename `ResourceDef::{resource_path => resource_path_from_iter}`. [#371] -* `ResourceDef::resource_path_from_iter` now takes an `IntoIterator`. [#373] -* Rename `ResourceDef::{resource_path_named => resource_path_from_map}`. [#371] -* Rename `ResourceDef::{is_prefix_match => find_match}`. [#373] -* Rename `ResourceDef::{match_path => capture_match_info}`. [#373] -* Rename `ResourceDef::{match_path_checked => capture_match_info_fn}`. [#373] -* Remove `ResourceDef::name_mut` and introduce `ResourceDef::set_name`. [#373] -* Rename `Router::{*_checked => *_fn}`. [#373] -* Return type of `ResourceDef::name` is now `Option<&str>`. [#373] -* Return type of `ResourceDef::pattern` is now `Option<&str>`. [#373] - -[#368]: https://github.com/actix/actix-net/pull/368 -[#366]: https://github.com/actix/actix-net/pull/366 -[#368]: https://github.com/actix/actix-net/pull/368 -[#370]: https://github.com/actix/actix-net/pull/370 -[#371]: https://github.com/actix/actix-net/pull/371 -[#372]: https://github.com/actix/actix-net/pull/372 -[#373]: https://github.com/actix/actix-net/pull/373 - - -## 0.4.0 - 2021-06-06 -* When matching path parameters, `%25` is now kept in the percent-encoded form; no longer decoded to `%`. [#357] -* Path tail patterns now match new lines (`\n`) in request URL. [#360] -* Fixed a safety bug where `Path` could return a malformed string after percent decoding. [#359] -* Methods `Path::{add, add_static}` now take `impl Into>`. [#345] - -[#345]: https://github.com/actix/actix-net/pull/345 -[#357]: https://github.com/actix/actix-net/pull/357 -[#359]: https://github.com/actix/actix-net/pull/359 -[#360]: https://github.com/actix/actix-net/pull/360 - - -## 0.3.0 - 2019-12-31 -* Version was yanked previously. See https://crates.io/crates/actix-router/0.3.0 - - -## 0.2.7 - 2021-02-06 -* Add `Router::recognize_checked` [#247] - -[#247]: https://github.com/actix/actix-net/pull/247 - - -## 0.2.6 - 2021-01-09 -* Use `bytestring` version range compatible with Bytes v1.0. [#246] - -[#246]: https://github.com/actix/actix-net/pull/246 - - -## 0.2.5 - 2020-09-20 -* Fix `from_hex()` method - - -## 0.2.4 - 2019-12-31 -* Add `ResourceDef::resource_path_named()` path generation method - - -## 0.2.3 - 2019-12-25 -* Add impl `IntoPattern` for `&String` - - -## 0.2.2 - 2019-12-25 -* Use `IntoPattern` for `RouterBuilder::path()` - - -## 0.2.1 - 2019-12-25 -* Add `IntoPattern` trait -* Add multi-pattern resources - - -## 0.2.0 - 2019-12-07 -* Update http to 0.2 -* Update regex to 1.3 -* Use bytestring instead of string - - -## 0.1.5 - 2019-05-15 -* Remove debug prints - - -## 0.1.4 - 2019-05-15 -* Fix checked resource match - - -## 0.1.3 - 2019-04-22 -* Added support for `remainder match` (i.e "/path/{tail}*") - - -## 0.1.2 - 2019-04-07 -* Export `Quoter` type -* Allow to reset `Path` instance - - -## 0.1.1 - 2019-04-03 -* Get dynamic segment by name instead of iterator. - - -## 0.1.0 - 2019-03-09 -* Initial release diff --git a/actix-router/Cargo.toml b/actix-router/Cargo.toml deleted file mode 100644 index 2a2ce1cc..00000000 --- a/actix-router/Cargo.toml +++ /dev/null @@ -1,38 +0,0 @@ -[package] -name = "actix-router" -version = "0.5.0-beta.1" -authors = [ - "Nikolay Kim ", - "Ali MJ Al-Nasrawy ", - "Rob Ede ", -] -description = "Resource path matching and router" -keywords = ["actix", "router", "routing"] -repository = "https://github.com/actix/actix-net.git" -license = "MIT OR Apache-2.0" -edition = "2018" - -[lib] -name = "actix_router" -path = "src/lib.rs" - -[features] -default = ["http"] - -[dependencies] -bytestring = ">=0.1.5, <2" -firestorm = "0.4" -http = { version = "0.2.3", optional = true } -log = "0.4" -regex = "1.5" -serde = "1" - -[dev-dependencies] -criterion = { version = "0.3", features = ["html_reports"] } -firestorm = { version = "0.4", features = ["enable_system_time"] } -http = "0.2.3" -serde = { version = "1", features = ["derive"] } - -[[bench]] -name = "router" -harness = false diff --git a/actix-router/LICENSE-APACHE b/actix-router/LICENSE-APACHE deleted file mode 120000 index 965b606f..00000000 --- a/actix-router/LICENSE-APACHE +++ /dev/null @@ -1 +0,0 @@ -../LICENSE-APACHE \ No newline at end of file diff --git a/actix-router/LICENSE-MIT b/actix-router/LICENSE-MIT deleted file mode 120000 index 76219eb7..00000000 --- a/actix-router/LICENSE-MIT +++ /dev/null @@ -1 +0,0 @@ -../LICENSE-MIT \ No newline at end of file diff --git a/actix-router/benches/router.rs b/actix-router/benches/router.rs deleted file mode 100644 index a428b9f1..00000000 --- a/actix-router/benches/router.rs +++ /dev/null @@ -1,194 +0,0 @@ -//! Based on https://github.com/ibraheemdev/matchit/blob/master/benches/bench.rs - -use criterion::{black_box, criterion_group, criterion_main, Criterion}; - -macro_rules! register { - (colon) => {{ - register!(finish => ":p1", ":p2", ":p3", ":p4") - }}; - (brackets) => {{ - register!(finish => "{p1}", "{p2}", "{p3}", "{p4}") - }}; - (regex) => {{ - register!(finish => "(.*)", "(.*)", "(.*)", "(.*)") - }}; - (finish => $p1:literal, $p2:literal, $p3:literal, $p4:literal) => {{ - let arr = [ - concat!("/authorizations"), - concat!("/authorizations/", $p1), - concat!("/applications/", $p1, "/tokens/", $p2), - concat!("/events"), - concat!("/repos/", $p1, "/", $p2, "/events"), - concat!("/networks/", $p1, "/", $p2, "/events"), - concat!("/orgs/", $p1, "/events"), - concat!("/users/", $p1, "/received_events"), - concat!("/users/", $p1, "/received_events/public"), - concat!("/users/", $p1, "/events"), - concat!("/users/", $p1, "/events/public"), - concat!("/users/", $p1, "/events/orgs/", $p2), - concat!("/feeds"), - concat!("/notifications"), - concat!("/repos/", $p1, "/", $p2, "/notifications"), - concat!("/notifications/threads/", $p1), - concat!("/notifications/threads/", $p1, "/subscription"), - concat!("/repos/", $p1, "/", $p2, "/stargazers"), - concat!("/users/", $p1, "/starred"), - concat!("/user/starred"), - concat!("/user/starred/", $p1, "/", $p2), - concat!("/repos/", $p1, "/", $p2, "/subscribers"), - concat!("/users/", $p1, "/subscriptions"), - concat!("/user/subscriptions"), - concat!("/repos/", $p1, "/", $p2, "/subscription"), - concat!("/user/subscriptions/", $p1, "/", $p2), - concat!("/users/", $p1, "/gists"), - concat!("/gists"), - concat!("/gists/", $p1), - concat!("/gists/", $p1, "/star"), - concat!("/repos/", $p1, "/", $p2, "/git/blobs/", $p3), - concat!("/repos/", $p1, "/", $p2, "/git/commits/", $p3), - concat!("/repos/", $p1, "/", $p2, "/git/refs"), - concat!("/repos/", $p1, "/", $p2, "/git/tags/", $p3), - concat!("/repos/", $p1, "/", $p2, "/git/trees/", $p3), - concat!("/issues"), - concat!("/user/issues"), - concat!("/orgs/", $p1, "/issues"), - concat!("/repos/", $p1, "/", $p2, "/issues"), - concat!("/repos/", $p1, "/", $p2, "/issues/", $p3), - concat!("/repos/", $p1, "/", $p2, "/assignees"), - concat!("/repos/", $p1, "/", $p2, "/assignees/", $p3), - concat!("/repos/", $p1, "/", $p2, "/issues/", $p3, "/comments"), - concat!("/repos/", $p1, "/", $p2, "/issues/", $p3, "/events"), - concat!("/repos/", $p1, "/", $p2, "/labels"), - concat!("/repos/", $p1, "/", $p2, "/labels/", $p3), - concat!("/repos/", $p1, "/", $p2, "/issues/", $p3, "/labels"), - concat!("/repos/", $p1, "/", $p2, "/milestones/", $p3, "/labels"), - concat!("/repos/", $p1, "/", $p2, "/milestones/"), - concat!("/repos/", $p1, "/", $p2, "/milestones/", $p3), - concat!("/emojis"), - concat!("/gitignore/templates"), - concat!("/gitignore/templates/", $p1), - concat!("/meta"), - concat!("/rate_limit"), - concat!("/users/", $p1, "/orgs"), - concat!("/user/orgs"), - concat!("/orgs/", $p1), - concat!("/orgs/", $p1, "/members"), - concat!("/orgs/", $p1, "/members", $p2), - concat!("/orgs/", $p1, "/public_members"), - concat!("/orgs/", $p1, "/public_members/", $p2), - concat!("/orgs/", $p1, "/teams"), - concat!("/teams/", $p1), - concat!("/teams/", $p1, "/members"), - concat!("/teams/", $p1, "/members", $p2), - concat!("/teams/", $p1, "/repos"), - concat!("/teams/", $p1, "/repos/", $p2, "/", $p3), - concat!("/user/teams"), - concat!("/repos/", $p1, "/", $p2, "/pulls"), - concat!("/repos/", $p1, "/", $p2, "/pulls/", $p3), - concat!("/repos/", $p1, "/", $p2, "/pulls/", $p3, "/commits"), - concat!("/repos/", $p1, "/", $p2, "/pulls/", $p3, "/files"), - concat!("/repos/", $p1, "/", $p2, "/pulls/", $p3, "/merge"), - concat!("/repos/", $p1, "/", $p2, "/pulls/", $p3, "/comments"), - concat!("/user/repos"), - concat!("/users/", $p1, "/repos"), - concat!("/orgs/", $p1, "/repos"), - concat!("/repositories"), - concat!("/repos/", $p1, "/", $p2), - concat!("/repos/", $p1, "/", $p2, "/contributors"), - concat!("/repos/", $p1, "/", $p2, "/languages"), - concat!("/repos/", $p1, "/", $p2, "/teams"), - concat!("/repos/", $p1, "/", $p2, "/tags"), - concat!("/repos/", $p1, "/", $p2, "/branches"), - concat!("/repos/", $p1, "/", $p2, "/branches/", $p3), - concat!("/repos/", $p1, "/", $p2, "/collaborators"), - concat!("/repos/", $p1, "/", $p2, "/collaborators/", $p3), - concat!("/repos/", $p1, "/", $p2, "/comments"), - concat!("/repos/", $p1, "/", $p2, "/commits/", $p3, "/comments"), - concat!("/repos/", $p1, "/", $p2, "/commits"), - concat!("/repos/", $p1, "/", $p2, "/commits/", $p3), - concat!("/repos/", $p1, "/", $p2, "/readme"), - concat!("/repos/", $p1, "/", $p2, "/keys"), - concat!("/repos/", $p1, "/", $p2, "/keys", $p3), - concat!("/repos/", $p1, "/", $p2, "/downloads"), - concat!("/repos/", $p1, "/", $p2, "/downloads", $p3), - concat!("/repos/", $p1, "/", $p2, "/forks"), - concat!("/repos/", $p1, "/", $p2, "/hooks"), - concat!("/repos/", $p1, "/", $p2, "/hooks", $p3), - concat!("/repos/", $p1, "/", $p2, "/releases"), - concat!("/repos/", $p1, "/", $p2, "/releases/", $p3), - concat!("/repos/", $p1, "/", $p2, "/releases/", $p3, "/assets"), - concat!("/repos/", $p1, "/", $p2, "/stats/contributors"), - concat!("/repos/", $p1, "/", $p2, "/stats/commit_activity"), - concat!("/repos/", $p1, "/", $p2, "/stats/code_frequency"), - concat!("/repos/", $p1, "/", $p2, "/stats/participation"), - concat!("/repos/", $p1, "/", $p2, "/stats/punch_card"), - concat!("/repos/", $p1, "/", $p2, "/statuses/", $p3), - concat!("/search/repositories"), - concat!("/search/code"), - concat!("/search/issues"), - concat!("/search/users"), - concat!("/legacy/issues/search/", $p1, "/", $p2, "/", $p3, "/", $p4), - concat!("/legacy/repos/search/", $p1), - concat!("/legacy/user/search/", $p1), - concat!("/legacy/user/email/", $p1), - concat!("/users/", $p1), - concat!("/user"), - concat!("/users"), - concat!("/user/emails"), - concat!("/users/", $p1, "/followers"), - concat!("/user/followers"), - concat!("/users/", $p1, "/following"), - concat!("/user/following"), - concat!("/user/following/", $p1), - concat!("/users/", $p1, "/following", $p2), - concat!("/users/", $p1, "/keys"), - concat!("/user/keys"), - concat!("/user/keys/", $p1), - ]; - std::array::IntoIter::new(arr) - }}; -} - -fn call() -> impl Iterator { - let arr = [ - "/authorizations", - "/user/repos", - "/repos/rust-lang/rust/stargazers", - "/orgs/rust-lang/public_members/nikomatsakis", - "/repos/rust-lang/rust/releases/1.51.0", - ]; - - std::array::IntoIter::new(arr) -} - -fn compare_routers(c: &mut Criterion) { - let mut group = c.benchmark_group("Compare Routers"); - - let mut actix = actix_router::Router::::build(); - for route in register!(brackets) { - actix.path(route, true); - } - let actix = actix.finish(); - group.bench_function("actix", |b| { - b.iter(|| { - for route in call() { - let mut path = actix_router::Path::new(route); - black_box(actix.recognize(&mut path).unwrap()); - } - }); - }); - - let regex_set = regex::RegexSet::new(register!(regex)).unwrap(); - group.bench_function("regex", |b| { - b.iter(|| { - for route in call() { - black_box(regex_set.matches(route)); - } - }); - }); - - group.finish(); -} - -criterion_group!(benches, compare_routers); -criterion_main!(benches); diff --git a/actix-router/examples/flamegraph.rs b/actix-router/examples/flamegraph.rs deleted file mode 100644 index 798cc22d..00000000 --- a/actix-router/examples/flamegraph.rs +++ /dev/null @@ -1,169 +0,0 @@ -macro_rules! register { - (brackets) => {{ - register!(finish => "{p1}", "{p2}", "{p3}", "{p4}") - }}; - (finish => $p1:literal, $p2:literal, $p3:literal, $p4:literal) => {{ - let arr = [ - concat!("/authorizations"), - concat!("/authorizations/", $p1), - concat!("/applications/", $p1, "/tokens/", $p2), - concat!("/events"), - concat!("/repos/", $p1, "/", $p2, "/events"), - concat!("/networks/", $p1, "/", $p2, "/events"), - concat!("/orgs/", $p1, "/events"), - concat!("/users/", $p1, "/received_events"), - concat!("/users/", $p1, "/received_events/public"), - concat!("/users/", $p1, "/events"), - concat!("/users/", $p1, "/events/public"), - concat!("/users/", $p1, "/events/orgs/", $p2), - concat!("/feeds"), - concat!("/notifications"), - concat!("/repos/", $p1, "/", $p2, "/notifications"), - concat!("/notifications/threads/", $p1), - concat!("/notifications/threads/", $p1, "/subscription"), - concat!("/repos/", $p1, "/", $p2, "/stargazers"), - concat!("/users/", $p1, "/starred"), - concat!("/user/starred"), - concat!("/user/starred/", $p1, "/", $p2), - concat!("/repos/", $p1, "/", $p2, "/subscribers"), - concat!("/users/", $p1, "/subscriptions"), - concat!("/user/subscriptions"), - concat!("/repos/", $p1, "/", $p2, "/subscription"), - concat!("/user/subscriptions/", $p1, "/", $p2), - concat!("/users/", $p1, "/gists"), - concat!("/gists"), - concat!("/gists/", $p1), - concat!("/gists/", $p1, "/star"), - concat!("/repos/", $p1, "/", $p2, "/git/blobs/", $p3), - concat!("/repos/", $p1, "/", $p2, "/git/commits/", $p3), - concat!("/repos/", $p1, "/", $p2, "/git/refs"), - concat!("/repos/", $p1, "/", $p2, "/git/tags/", $p3), - concat!("/repos/", $p1, "/", $p2, "/git/trees/", $p3), - concat!("/issues"), - concat!("/user/issues"), - concat!("/orgs/", $p1, "/issues"), - concat!("/repos/", $p1, "/", $p2, "/issues"), - concat!("/repos/", $p1, "/", $p2, "/issues/", $p3), - concat!("/repos/", $p1, "/", $p2, "/assignees"), - concat!("/repos/", $p1, "/", $p2, "/assignees/", $p3), - concat!("/repos/", $p1, "/", $p2, "/issues/", $p3, "/comments"), - concat!("/repos/", $p1, "/", $p2, "/issues/", $p3, "/events"), - concat!("/repos/", $p1, "/", $p2, "/labels"), - concat!("/repos/", $p1, "/", $p2, "/labels/", $p3), - concat!("/repos/", $p1, "/", $p2, "/issues/", $p3, "/labels"), - concat!("/repos/", $p1, "/", $p2, "/milestones/", $p3, "/labels"), - concat!("/repos/", $p1, "/", $p2, "/milestones/"), - concat!("/repos/", $p1, "/", $p2, "/milestones/", $p3), - concat!("/emojis"), - concat!("/gitignore/templates"), - concat!("/gitignore/templates/", $p1), - concat!("/meta"), - concat!("/rate_limit"), - concat!("/users/", $p1, "/orgs"), - concat!("/user/orgs"), - concat!("/orgs/", $p1), - concat!("/orgs/", $p1, "/members"), - concat!("/orgs/", $p1, "/members", $p2), - concat!("/orgs/", $p1, "/public_members"), - concat!("/orgs/", $p1, "/public_members/", $p2), - concat!("/orgs/", $p1, "/teams"), - concat!("/teams/", $p1), - concat!("/teams/", $p1, "/members"), - concat!("/teams/", $p1, "/members", $p2), - concat!("/teams/", $p1, "/repos"), - concat!("/teams/", $p1, "/repos/", $p2, "/", $p3), - concat!("/user/teams"), - concat!("/repos/", $p1, "/", $p2, "/pulls"), - concat!("/repos/", $p1, "/", $p2, "/pulls/", $p3), - concat!("/repos/", $p1, "/", $p2, "/pulls/", $p3, "/commits"), - concat!("/repos/", $p1, "/", $p2, "/pulls/", $p3, "/files"), - concat!("/repos/", $p1, "/", $p2, "/pulls/", $p3, "/merge"), - concat!("/repos/", $p1, "/", $p2, "/pulls/", $p3, "/comments"), - concat!("/user/repos"), - concat!("/users/", $p1, "/repos"), - concat!("/orgs/", $p1, "/repos"), - concat!("/repositories"), - concat!("/repos/", $p1, "/", $p2), - concat!("/repos/", $p1, "/", $p2, "/contributors"), - concat!("/repos/", $p1, "/", $p2, "/languages"), - concat!("/repos/", $p1, "/", $p2, "/teams"), - concat!("/repos/", $p1, "/", $p2, "/tags"), - concat!("/repos/", $p1, "/", $p2, "/branches"), - concat!("/repos/", $p1, "/", $p2, "/branches/", $p3), - concat!("/repos/", $p1, "/", $p2, "/collaborators"), - concat!("/repos/", $p1, "/", $p2, "/collaborators/", $p3), - concat!("/repos/", $p1, "/", $p2, "/comments"), - concat!("/repos/", $p1, "/", $p2, "/commits/", $p3, "/comments"), - concat!("/repos/", $p1, "/", $p2, "/commits"), - concat!("/repos/", $p1, "/", $p2, "/commits/", $p3), - concat!("/repos/", $p1, "/", $p2, "/readme"), - concat!("/repos/", $p1, "/", $p2, "/keys"), - concat!("/repos/", $p1, "/", $p2, "/keys", $p3), - concat!("/repos/", $p1, "/", $p2, "/downloads"), - concat!("/repos/", $p1, "/", $p2, "/downloads", $p3), - concat!("/repos/", $p1, "/", $p2, "/forks"), - concat!("/repos/", $p1, "/", $p2, "/hooks"), - concat!("/repos/", $p1, "/", $p2, "/hooks", $p3), - concat!("/repos/", $p1, "/", $p2, "/releases"), - concat!("/repos/", $p1, "/", $p2, "/releases/", $p3), - concat!("/repos/", $p1, "/", $p2, "/releases/", $p3, "/assets"), - concat!("/repos/", $p1, "/", $p2, "/stats/contributors"), - concat!("/repos/", $p1, "/", $p2, "/stats/commit_activity"), - concat!("/repos/", $p1, "/", $p2, "/stats/code_frequency"), - concat!("/repos/", $p1, "/", $p2, "/stats/participation"), - concat!("/repos/", $p1, "/", $p2, "/stats/punch_card"), - concat!("/repos/", $p1, "/", $p2, "/statuses/", $p3), - concat!("/search/repositories"), - concat!("/search/code"), - concat!("/search/issues"), - concat!("/search/users"), - concat!("/legacy/issues/search/", $p1, "/", $p2, "/", $p3, "/", $p4), - concat!("/legacy/repos/search/", $p1), - concat!("/legacy/user/search/", $p1), - concat!("/legacy/user/email/", $p1), - concat!("/users/", $p1), - concat!("/user"), - concat!("/users"), - concat!("/user/emails"), - concat!("/users/", $p1, "/followers"), - concat!("/user/followers"), - concat!("/users/", $p1, "/following"), - concat!("/user/following"), - concat!("/user/following/", $p1), - concat!("/users/", $p1, "/following", $p2), - concat!("/users/", $p1, "/keys"), - concat!("/user/keys"), - concat!("/user/keys/", $p1), - ]; - - arr.to_vec() - }}; -} - -static PATHS: [&str; 5] = [ - "/authorizations", - "/user/repos", - "/repos/rust-lang/rust/stargazers", - "/orgs/rust-lang/public_members/nikomatsakis", - "/repos/rust-lang/rust/releases/1.51.0", -]; - -fn main() { - let mut router = actix_router::Router::::build(); - - for route in register!(brackets) { - router.path(route, true); - } - - let actix = router.finish(); - - if firestorm::enabled() { - firestorm::bench("target", || { - for &route in &PATHS { - let mut path = actix_router::Path::new(route); - actix.recognize(&mut path).unwrap(); - } - }) - .unwrap(); - } -} diff --git a/actix-router/src/de.rs b/actix-router/src/de.rs deleted file mode 100644 index 775c48b8..00000000 --- a/actix-router/src/de.rs +++ /dev/null @@ -1,723 +0,0 @@ -use serde::de::{self, Deserializer, Error as DeError, Visitor}; -use serde::forward_to_deserialize_any; - -use crate::path::{Path, PathIter}; -use crate::ResourcePath; - -macro_rules! unsupported_type { - ($trait_fn:ident, $name:expr) => { - fn $trait_fn(self, _: V) -> Result - where - V: Visitor<'de>, - { - Err(de::value::Error::custom(concat!( - "unsupported type: ", - $name - ))) - } - }; -} - -macro_rules! parse_single_value { - ($trait_fn:ident, $visit_fn:ident, $tp:tt) => { - fn $trait_fn(self, visitor: V) -> Result - where - V: Visitor<'de>, - { - if self.path.segment_count() != 1 { - Err(de::value::Error::custom( - format!( - "wrong number of parameters: {} expected 1", - self.path.segment_count() - ) - .as_str(), - )) - } else { - let v = self.path[0].parse().map_err(|_| { - de::value::Error::custom(format!( - "can not parse {:?} to a {}", - &self.path[0], $tp - )) - })?; - visitor.$visit_fn(v) - } - } - }; -} - -pub struct PathDeserializer<'de, T: ResourcePath> { - path: &'de Path, -} - -impl<'de, T: ResourcePath + 'de> PathDeserializer<'de, T> { - pub fn new(path: &'de Path) -> Self { - PathDeserializer { path } - } -} - -impl<'de, T: ResourcePath + 'de> Deserializer<'de> for PathDeserializer<'de, T> { - type Error = de::value::Error; - - fn deserialize_map(self, visitor: V) -> Result - where - V: Visitor<'de>, - { - visitor.visit_map(ParamsDeserializer { - params: self.path.iter(), - current: None, - }) - } - - fn deserialize_struct( - self, - _: &'static str, - _: &'static [&'static str], - visitor: V, - ) -> Result - where - V: Visitor<'de>, - { - self.deserialize_map(visitor) - } - - fn deserialize_unit(self, visitor: V) -> Result - where - V: Visitor<'de>, - { - visitor.visit_unit() - } - - fn deserialize_unit_struct( - self, - _: &'static str, - visitor: V, - ) -> Result - where - V: Visitor<'de>, - { - self.deserialize_unit(visitor) - } - - fn deserialize_newtype_struct( - self, - _: &'static str, - visitor: V, - ) -> Result - where - V: Visitor<'de>, - { - visitor.visit_newtype_struct(self) - } - - fn deserialize_tuple(self, len: usize, visitor: V) -> Result - where - V: Visitor<'de>, - { - if self.path.segment_count() < len { - Err(de::value::Error::custom( - format!( - "wrong number of parameters: {} expected {}", - self.path.segment_count(), - len - ) - .as_str(), - )) - } else { - visitor.visit_seq(ParamsSeq { - params: self.path.iter(), - }) - } - } - - fn deserialize_tuple_struct( - self, - _: &'static str, - len: usize, - visitor: V, - ) -> Result - where - V: Visitor<'de>, - { - if self.path.segment_count() < len { - Err(de::value::Error::custom( - format!( - "wrong number of parameters: {} expected {}", - self.path.segment_count(), - len - ) - .as_str(), - )) - } else { - visitor.visit_seq(ParamsSeq { - params: self.path.iter(), - }) - } - } - - fn deserialize_enum( - self, - _: &'static str, - _: &'static [&'static str], - visitor: V, - ) -> Result - where - V: Visitor<'de>, - { - if self.path.is_empty() { - Err(de::value::Error::custom("expected at least one parameters")) - } else { - visitor.visit_enum(ValueEnum { - value: &self.path[0], - }) - } - } - - fn deserialize_str(self, visitor: V) -> Result - where - V: Visitor<'de>, - { - if self.path.segment_count() != 1 { - Err(de::value::Error::custom( - format!( - "wrong number of parameters: {} expected 1", - self.path.segment_count() - ) - .as_str(), - )) - } else { - visitor.visit_str(&self.path[0]) - } - } - - fn deserialize_seq(self, visitor: V) -> Result - where - V: Visitor<'de>, - { - visitor.visit_seq(ParamsSeq { - params: self.path.iter(), - }) - } - - unsupported_type!(deserialize_any, "'any'"); - unsupported_type!(deserialize_bytes, "bytes"); - unsupported_type!(deserialize_option, "Option"); - unsupported_type!(deserialize_identifier, "identifier"); - unsupported_type!(deserialize_ignored_any, "ignored_any"); - - parse_single_value!(deserialize_bool, visit_bool, "bool"); - parse_single_value!(deserialize_i8, visit_i8, "i8"); - parse_single_value!(deserialize_i16, visit_i16, "i16"); - parse_single_value!(deserialize_i32, visit_i32, "i32"); - parse_single_value!(deserialize_i64, visit_i64, "i64"); - parse_single_value!(deserialize_u8, visit_u8, "u8"); - parse_single_value!(deserialize_u16, visit_u16, "u16"); - parse_single_value!(deserialize_u32, visit_u32, "u32"); - parse_single_value!(deserialize_u64, visit_u64, "u64"); - parse_single_value!(deserialize_f32, visit_f32, "f32"); - parse_single_value!(deserialize_f64, visit_f64, "f64"); - parse_single_value!(deserialize_string, visit_string, "String"); - parse_single_value!(deserialize_byte_buf, visit_string, "String"); - parse_single_value!(deserialize_char, visit_char, "char"); -} - -struct ParamsDeserializer<'de, T: ResourcePath> { - params: PathIter<'de, T>, - current: Option<(&'de str, &'de str)>, -} - -impl<'de, T: ResourcePath> de::MapAccess<'de> for ParamsDeserializer<'de, T> { - type Error = de::value::Error; - - fn next_key_seed(&mut self, seed: K) -> Result, Self::Error> - where - K: de::DeserializeSeed<'de>, - { - self.current = self.params.next().map(|ref item| (item.0, item.1)); - match self.current { - Some((key, _)) => Ok(Some(seed.deserialize(Key { key })?)), - None => Ok(None), - } - } - - fn next_value_seed(&mut self, seed: V) -> Result - where - V: de::DeserializeSeed<'de>, - { - if let Some((_, value)) = self.current.take() { - seed.deserialize(Value { value }) - } else { - Err(de::value::Error::custom("unexpected item")) - } - } -} - -struct Key<'de> { - key: &'de str, -} - -impl<'de> Deserializer<'de> for Key<'de> { - type Error = de::value::Error; - - fn deserialize_identifier(self, visitor: V) -> Result - where - V: Visitor<'de>, - { - visitor.visit_str(self.key) - } - - fn deserialize_any(self, _visitor: V) -> Result - where - V: Visitor<'de>, - { - Err(de::value::Error::custom("Unexpected")) - } - - forward_to_deserialize_any! { - bool i8 i16 i32 i64 u8 u16 u32 u64 f32 f64 char str string bytes - byte_buf option unit unit_struct newtype_struct seq tuple - tuple_struct map struct enum ignored_any - } -} - -macro_rules! parse_value { - ($trait_fn:ident, $visit_fn:ident, $tp:tt) => { - fn $trait_fn(self, visitor: V) -> Result - where - V: Visitor<'de>, - { - let v = self.value.parse().map_err(|_| { - de::value::Error::custom(format!("can not parse {:?} to a {}", self.value, $tp)) - })?; - visitor.$visit_fn(v) - } - }; -} - -struct Value<'de> { - value: &'de str, -} - -impl<'de> Deserializer<'de> for Value<'de> { - type Error = de::value::Error; - - parse_value!(deserialize_bool, visit_bool, "bool"); - parse_value!(deserialize_i8, visit_i8, "i8"); - parse_value!(deserialize_i16, visit_i16, "i16"); - parse_value!(deserialize_i32, visit_i32, "i16"); - parse_value!(deserialize_i64, visit_i64, "i64"); - parse_value!(deserialize_u8, visit_u8, "u8"); - parse_value!(deserialize_u16, visit_u16, "u16"); - parse_value!(deserialize_u32, visit_u32, "u32"); - parse_value!(deserialize_u64, visit_u64, "u64"); - parse_value!(deserialize_f32, visit_f32, "f32"); - parse_value!(deserialize_f64, visit_f64, "f64"); - parse_value!(deserialize_string, visit_string, "String"); - parse_value!(deserialize_byte_buf, visit_string, "String"); - parse_value!(deserialize_char, visit_char, "char"); - - fn deserialize_ignored_any(self, visitor: V) -> Result - where - V: Visitor<'de>, - { - visitor.visit_unit() - } - - fn deserialize_unit(self, visitor: V) -> Result - where - V: Visitor<'de>, - { - visitor.visit_unit() - } - - fn deserialize_unit_struct( - self, - _: &'static str, - visitor: V, - ) -> Result - where - V: Visitor<'de>, - { - visitor.visit_unit() - } - - fn deserialize_bytes(self, visitor: V) -> Result - where - V: Visitor<'de>, - { - visitor.visit_borrowed_bytes(self.value.as_bytes()) - } - - fn deserialize_str(self, visitor: V) -> Result - where - V: Visitor<'de>, - { - visitor.visit_borrowed_str(self.value) - } - - fn deserialize_option(self, visitor: V) -> Result - where - V: Visitor<'de>, - { - visitor.visit_some(self) - } - - fn deserialize_enum( - self, - _: &'static str, - _: &'static [&'static str], - visitor: V, - ) -> Result - where - V: Visitor<'de>, - { - visitor.visit_enum(ValueEnum { value: self.value }) - } - - fn deserialize_newtype_struct( - self, - _: &'static str, - visitor: V, - ) -> Result - where - V: Visitor<'de>, - { - visitor.visit_newtype_struct(self) - } - - fn deserialize_tuple(self, _: usize, _: V) -> Result - where - V: Visitor<'de>, - { - Err(de::value::Error::custom("unsupported type: tuple")) - } - - fn deserialize_struct( - self, - _: &'static str, - _: &'static [&'static str], - _: V, - ) -> Result - where - V: Visitor<'de>, - { - Err(de::value::Error::custom("unsupported type: struct")) - } - - fn deserialize_tuple_struct( - self, - _: &'static str, - _: usize, - _: V, - ) -> Result - where - V: Visitor<'de>, - { - Err(de::value::Error::custom("unsupported type: tuple struct")) - } - - unsupported_type!(deserialize_any, "any"); - unsupported_type!(deserialize_seq, "seq"); - unsupported_type!(deserialize_map, "map"); - unsupported_type!(deserialize_identifier, "identifier"); -} - -struct ParamsSeq<'de, T: ResourcePath> { - params: PathIter<'de, T>, -} - -impl<'de, T: ResourcePath> de::SeqAccess<'de> for ParamsSeq<'de, T> { - type Error = de::value::Error; - - fn next_element_seed(&mut self, seed: U) -> Result, Self::Error> - where - U: de::DeserializeSeed<'de>, - { - match self.params.next() { - Some(item) => Ok(Some(seed.deserialize(Value { value: item.1 })?)), - None => Ok(None), - } - } -} - -struct ValueEnum<'de> { - value: &'de str, -} - -impl<'de> de::EnumAccess<'de> for ValueEnum<'de> { - type Error = de::value::Error; - type Variant = UnitVariant; - - fn variant_seed(self, seed: V) -> Result<(V::Value, Self::Variant), Self::Error> - where - V: de::DeserializeSeed<'de>, - { - Ok((seed.deserialize(Key { key: self.value })?, UnitVariant)) - } -} - -struct UnitVariant; - -impl<'de> de::VariantAccess<'de> for UnitVariant { - type Error = de::value::Error; - - fn unit_variant(self) -> Result<(), Self::Error> { - Ok(()) - } - - fn newtype_variant_seed(self, _seed: T) -> Result - where - T: de::DeserializeSeed<'de>, - { - Err(de::value::Error::custom("not supported")) - } - - fn tuple_variant(self, _len: usize, _visitor: V) -> Result - where - V: Visitor<'de>, - { - Err(de::value::Error::custom("not supported")) - } - - fn struct_variant( - self, - _: &'static [&'static str], - _: V, - ) -> Result - where - V: Visitor<'de>, - { - Err(de::value::Error::custom("not supported")) - } -} - -#[cfg(test)] -mod tests { - use serde::{de, Deserialize}; - - use super::*; - use crate::path::Path; - use crate::router::Router; - - #[derive(Deserialize)] - struct MyStruct { - key: String, - value: String, - } - - #[derive(Deserialize)] - struct Id { - _id: String, - } - - #[derive(Debug, Deserialize)] - struct Test1(String, u32); - - #[derive(Debug, Deserialize)] - struct Test2 { - key: String, - value: u32, - } - - #[derive(Debug, Deserialize, PartialEq)] - #[serde(rename_all = "lowercase")] - enum TestEnum { - Val1, - Val2, - } - - #[derive(Debug, Deserialize)] - struct Test3 { - val: TestEnum, - } - - #[test] - fn test_request_extract() { - let mut router = Router::<()>::build(); - router.path("/{key}/{value}/", ()); - let router = router.finish(); - - let mut path = Path::new("/name/user1/"); - assert!(router.recognize(&mut path).is_some()); - - let s: MyStruct = de::Deserialize::deserialize(PathDeserializer::new(&path)).unwrap(); - assert_eq!(s.key, "name"); - assert_eq!(s.value, "user1"); - - let s: (String, String) = - de::Deserialize::deserialize(PathDeserializer::new(&path)).unwrap(); - assert_eq!(s.0, "name"); - assert_eq!(s.1, "user1"); - - let mut router = Router::<()>::build(); - router.path("/{key}/{value}/", ()); - let router = router.finish(); - - let mut path = Path::new("/name/32/"); - assert!(router.recognize(&mut path).is_some()); - - let s: Test1 = de::Deserialize::deserialize(PathDeserializer::new(&path)).unwrap(); - assert_eq!(s.0, "name"); - assert_eq!(s.1, 32); - - let s: Test2 = de::Deserialize::deserialize(PathDeserializer::new(&path)).unwrap(); - assert_eq!(s.key, "name"); - assert_eq!(s.value, 32); - - let s: (String, u8) = - de::Deserialize::deserialize(PathDeserializer::new(&path)).unwrap(); - assert_eq!(s.0, "name"); - assert_eq!(s.1, 32); - - let res: Vec = - de::Deserialize::deserialize(PathDeserializer::new(&path)).unwrap(); - assert_eq!(res[0], "name".to_owned()); - assert_eq!(res[1], "32".to_owned()); - } - - #[test] - fn test_extract_path_single() { - let mut router = Router::<()>::build(); - router.path("/{value}/", ()); - let router = router.finish(); - - let mut path = Path::new("/32/"); - assert!(router.recognize(&mut path).is_some()); - let i: i8 = de::Deserialize::deserialize(PathDeserializer::new(&path)).unwrap(); - assert_eq!(i, 32); - } - - #[test] - fn test_extract_enum() { - let mut router = Router::<()>::build(); - router.path("/{val}/", ()); - let router = router.finish(); - - let mut path = Path::new("/val1/"); - assert!(router.recognize(&mut path).is_some()); - let i: TestEnum = de::Deserialize::deserialize(PathDeserializer::new(&path)).unwrap(); - assert_eq!(i, TestEnum::Val1); - - let mut router = Router::<()>::build(); - router.path("/{val1}/{val2}/", ()); - let router = router.finish(); - - let mut path = Path::new("/val1/val2/"); - assert!(router.recognize(&mut path).is_some()); - let i: (TestEnum, TestEnum) = - de::Deserialize::deserialize(PathDeserializer::new(&path)).unwrap(); - assert_eq!(i, (TestEnum::Val1, TestEnum::Val2)); - } - - #[test] - fn test_extract_enum_value() { - let mut router = Router::<()>::build(); - router.path("/{val}/", ()); - let router = router.finish(); - - let mut path = Path::new("/val1/"); - assert!(router.recognize(&mut path).is_some()); - let i: Test3 = de::Deserialize::deserialize(PathDeserializer::new(&path)).unwrap(); - assert_eq!(i.val, TestEnum::Val1); - - let mut path = Path::new("/val3/"); - assert!(router.recognize(&mut path).is_some()); - let i: Result = - de::Deserialize::deserialize(PathDeserializer::new(&path)); - assert!(i.is_err()); - assert!(format!("{:?}", i).contains("unknown variant")); - } - - #[test] - fn test_extract_errors() { - let mut router = Router::<()>::build(); - router.path("/{value}/", ()); - let router = router.finish(); - - let mut path = Path::new("/name/"); - assert!(router.recognize(&mut path).is_some()); - - let s: Result = - de::Deserialize::deserialize(PathDeserializer::new(&path)); - assert!(s.is_err()); - assert!(format!("{:?}", s).contains("wrong number of parameters")); - - let s: Result = - de::Deserialize::deserialize(PathDeserializer::new(&path)); - assert!(s.is_err()); - assert!(format!("{:?}", s).contains("can not parse")); - - let s: Result<(String, String), de::value::Error> = - de::Deserialize::deserialize(PathDeserializer::new(&path)); - assert!(s.is_err()); - assert!(format!("{:?}", s).contains("wrong number of parameters")); - - let s: Result = - de::Deserialize::deserialize(PathDeserializer::new(&path)); - assert!(s.is_err()); - assert!(format!("{:?}", s).contains("can not parse")); - } - - // #[test] - // fn test_extract_path_decode() { - // let mut router = Router::<()>::default(); - // router.register_resource(Resource::new(ResourceDef::new("/{value}/"))); - - // macro_rules! test_single_value { - // ($value:expr, $expected:expr) => {{ - // let req = TestRequest::with_uri($value).finish(); - // let info = router.recognize(&req, &(), 0); - // let req = req.with_route_info(info); - // assert_eq!( - // *Path::::from_request(&req, &PathConfig::default()).unwrap(), - // $expected - // ); - // }}; - // } - - // test_single_value!("/%25/", "%"); - // test_single_value!("/%40%C2%A3%24%25%5E%26%2B%3D/", "@£$%^&+="); - // test_single_value!("/%2B/", "+"); - // test_single_value!("/%252B/", "%2B"); - // test_single_value!("/%2F/", "/"); - // test_single_value!("/%252F/", "%2F"); - // test_single_value!( - // "/http%3A%2F%2Flocalhost%3A80%2Ffoo/", - // "http://localhost:80/foo" - // ); - // test_single_value!("/%2Fvar%2Flog%2Fsyslog/", "/var/log/syslog"); - // test_single_value!( - // "/http%3A%2F%2Flocalhost%3A80%2Ffile%2F%252Fvar%252Flog%252Fsyslog/", - // "http://localhost:80/file/%2Fvar%2Flog%2Fsyslog" - // ); - - // let req = TestRequest::with_uri("/%25/7/?id=test").finish(); - - // let mut router = Router::<()>::default(); - // router.register_resource(Resource::new(ResourceDef::new("/{key}/{value}/"))); - // let info = router.recognize(&req, &(), 0); - // let req = req.with_route_info(info); - - // let s = Path::::from_request(&req, &PathConfig::default()).unwrap(); - // assert_eq!(s.key, "%"); - // assert_eq!(s.value, 7); - - // let s = Path::<(String, String)>::from_request(&req, &PathConfig::default()).unwrap(); - // assert_eq!(s.0, "%"); - // assert_eq!(s.1, "7"); - // } - - // #[test] - // fn test_extract_path_no_decode() { - // let mut router = Router::<()>::default(); - // router.register_resource(Resource::new(ResourceDef::new("/{value}/"))); - - // let req = TestRequest::with_uri("/%25/").finish(); - // let info = router.recognize(&req, &(), 0); - // let req = req.with_route_info(info); - // assert_eq!( - // *Path::::from_request(&req, &&PathConfig::default().disable_decoding()) - // .unwrap(), - // "%25" - // ); - // } -} diff --git a/actix-router/src/lib.rs b/actix-router/src/lib.rs deleted file mode 100644 index 463e59e4..00000000 --- a/actix-router/src/lib.rs +++ /dev/null @@ -1,149 +0,0 @@ -//! Resource path matching and router. - -#![deny(rust_2018_idioms, nonstandard_style)] -#![doc(html_logo_url = "https://actix.rs/img/logo.png")] -#![doc(html_favicon_url = "https://actix.rs/favicon.ico")] - -mod de; -mod path; -mod resource; -mod router; - -pub use self::de::PathDeserializer; -pub use self::path::Path; -pub use self::resource::ResourceDef; -pub use self::router::{ResourceInfo, Router, RouterBuilder}; - -// TODO: this trait is necessary, document it -// see impl Resource for ServiceRequest -pub trait Resource { - fn resource_path(&mut self) -> &mut Path; -} - -pub trait ResourcePath { - fn path(&self) -> &str; -} - -impl ResourcePath for String { - fn path(&self) -> &str { - self.as_str() - } -} - -impl<'a> ResourcePath for &'a str { - fn path(&self) -> &str { - self - } -} - -impl ResourcePath for bytestring::ByteString { - fn path(&self) -> &str { - &*self - } -} - -/// One or many patterns. -#[derive(Debug, Clone, PartialEq, Eq, Hash)] -pub enum Patterns { - Single(String), - List(Vec), -} - -impl Patterns { - pub fn is_empty(&self) -> bool { - match self { - Patterns::Single(_) => false, - Patterns::List(pats) => pats.is_empty(), - } - } -} - -/// Helper trait for type that could be converted to one or more path pattern. -pub trait IntoPatterns { - fn patterns(&self) -> Patterns; -} - -impl IntoPatterns for String { - fn patterns(&self) -> Patterns { - Patterns::Single(self.clone()) - } -} - -impl<'a> IntoPatterns for &'a String { - fn patterns(&self) -> Patterns { - Patterns::Single((*self).clone()) - } -} - -impl<'a> IntoPatterns for &'a str { - fn patterns(&self) -> Patterns { - Patterns::Single((*self).to_owned()) - } -} - -impl IntoPatterns for bytestring::ByteString { - fn patterns(&self) -> Patterns { - Patterns::Single(self.to_string()) - } -} - -impl IntoPatterns for Patterns { - fn patterns(&self) -> Patterns { - self.clone() - } -} - -impl> IntoPatterns for Vec { - fn patterns(&self) -> Patterns { - let mut patterns = self.iter().map(|v| v.as_ref().to_owned()); - - match patterns.size_hint() { - (1, _) => Patterns::Single(patterns.next().unwrap()), - _ => Patterns::List(patterns.collect()), - } - } -} - -macro_rules! array_patterns_single (($tp:ty) => { - impl IntoPatterns for [$tp; 1] { - fn patterns(&self) -> Patterns { - Patterns::Single(self[0].to_owned()) - } - } -}); - -macro_rules! array_patterns_multiple (($tp:ty, $str_fn:expr, $($num:tt) +) => { - // for each array length specified in $num - $( - impl IntoPatterns for [$tp; $num] { - fn patterns(&self) -> Patterns { - Patterns::List(self.iter().map($str_fn).collect()) - } - } - )+ -}); - -array_patterns_single!(&str); -array_patterns_multiple!(&str, |&v| v.to_owned(), 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16); - -array_patterns_single!(String); -array_patterns_multiple!(String, |v| v.clone(), 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16); - -#[cfg(feature = "http")] -mod url; - -#[cfg(feature = "http")] -pub use self::url::{Quoter, Url}; - -#[cfg(feature = "http")] -mod http_impls { - use http::Uri; - - use super::ResourcePath; - - impl ResourcePath for Uri { - fn path(&self) -> &str { - self.path() - } - } -} diff --git a/actix-router/src/path.rs b/actix-router/src/path.rs deleted file mode 100644 index e29591f9..00000000 --- a/actix-router/src/path.rs +++ /dev/null @@ -1,220 +0,0 @@ -use std::borrow::Cow; -use std::ops::Index; - -use firestorm::profile_method; -use serde::de; - -use crate::{de::PathDeserializer, Resource, ResourcePath}; - -#[derive(Debug, Clone)] -pub(crate) enum PathItem { - Static(Cow<'static, str>), - Segment(u16, u16), -} - -impl Default for PathItem { - fn default() -> Self { - Self::Static(Cow::Borrowed("")) - } -} - -/// Resource path match information. -/// -/// If resource path contains variable patterns, `Path` stores them. -#[derive(Debug, Clone, Default)] -pub struct Path { - path: T, - pub(crate) skip: u16, - pub(crate) segments: Vec<(Cow<'static, str>, PathItem)>, -} - -impl Path { - pub fn new(path: T) -> Path { - Path { - path, - skip: 0, - segments: Vec::new(), - } - } - - /// Get reference to inner path instance. - #[inline] - pub fn get_ref(&self) -> &T { - &self.path - } - - /// Get mutable reference to inner path instance. - #[inline] - pub fn get_mut(&mut self) -> &mut T { - &mut self.path - } - - /// Path. - #[inline] - pub fn path(&self) -> &str { - profile_method!(path); - - let skip = self.skip as usize; - let path = self.path.path(); - if skip <= path.len() { - &path[skip..] - } else { - "" - } - } - - /// Set new path. - #[inline] - pub fn set(&mut self, path: T) { - self.skip = 0; - self.path = path; - self.segments.clear(); - } - - /// Reset state. - #[inline] - pub fn reset(&mut self) { - self.skip = 0; - self.segments.clear(); - } - - /// Skip first `n` chars in path. - #[inline] - pub fn skip(&mut self, n: u16) { - self.skip += n; - } - - pub(crate) fn add(&mut self, name: impl Into>, value: PathItem) { - profile_method!(add); - - match value { - PathItem::Static(s) => self.segments.push((name.into(), PathItem::Static(s))), - PathItem::Segment(begin, end) => self.segments.push(( - name.into(), - PathItem::Segment(self.skip + begin, self.skip + end), - )), - } - } - - #[doc(hidden)] - pub fn add_static( - &mut self, - name: impl Into>, - value: impl Into>, - ) { - self.segments - .push((name.into(), PathItem::Static(value.into()))); - } - - /// Check if there are any matched patterns. - #[inline] - pub fn is_empty(&self) -> bool { - self.segments.is_empty() - } - - /// Returns number of interpolated segments. - #[inline] - pub fn segment_count(&self) -> usize { - self.segments.len() - } - - /// Get matched parameter by name without type conversion - pub fn get(&self, name: &str) -> Option<&str> { - profile_method!(get); - - for (seg_name, val) in self.segments.iter() { - if name == seg_name { - return match val { - PathItem::Static(ref s) => Some(&s), - PathItem::Segment(s, e) => { - Some(&self.path.path()[(*s as usize)..(*e as usize)]) - } - }; - } - } - - None - } - - /// Get unprocessed part of the path - pub fn unprocessed(&self) -> &str { - &self.path.path()[(self.skip as usize)..] - } - - /// Get matched parameter by name. - /// - /// If keyed parameter is not available empty string is used as default value. - pub fn query(&self, key: &str) -> &str { - profile_method!(query); - - if let Some(s) = self.get(key) { - s - } else { - "" - } - } - - /// Return iterator to items in parameter container. - pub fn iter(&self) -> PathIter<'_, T> { - PathIter { - idx: 0, - params: self, - } - } - - /// Try to deserialize matching parameters to a specified type `U` - pub fn load<'de, U: serde::Deserialize<'de>>(&'de self) -> Result { - profile_method!(load); - de::Deserialize::deserialize(PathDeserializer::new(self)) - } -} - -#[derive(Debug)] -pub struct PathIter<'a, T> { - idx: usize, - params: &'a Path, -} - -impl<'a, T: ResourcePath> Iterator for PathIter<'a, T> { - type Item = (&'a str, &'a str); - - #[inline] - fn next(&mut self) -> Option<(&'a str, &'a str)> { - if self.idx < self.params.segment_count() { - let idx = self.idx; - let res = match self.params.segments[idx].1 { - PathItem::Static(ref s) => &s, - PathItem::Segment(s, e) => &self.params.path.path()[(s as usize)..(e as usize)], - }; - self.idx += 1; - return Some((&self.params.segments[idx].0, res)); - } - None - } -} - -impl<'a, T: ResourcePath> Index<&'a str> for Path { - type Output = str; - - fn index(&self, name: &'a str) -> &str { - self.get(name) - .expect("Value for parameter is not available") - } -} - -impl Index for Path { - type Output = str; - - fn index(&self, idx: usize) -> &str { - match self.segments[idx].1 { - PathItem::Static(ref s) => &s, - PathItem::Segment(s, e) => &self.path.path()[(s as usize)..(e as usize)], - } - } -} - -impl Resource for Path { - fn resource_path(&mut self) -> &mut Self { - self - } -} diff --git a/actix-router/src/resource.rs b/actix-router/src/resource.rs deleted file mode 100644 index 61ff587a..00000000 --- a/actix-router/src/resource.rs +++ /dev/null @@ -1,1803 +0,0 @@ -use std::{ - borrow::{Borrow, Cow}, - collections::HashMap, - hash::{BuildHasher, Hash, Hasher}, - mem, -}; - -use firestorm::{profile_fn, profile_method, profile_section}; -use regex::{escape, Regex, RegexSet}; - -use crate::{ - path::{Path, PathItem}, - IntoPatterns, Patterns, Resource, ResourcePath, -}; - -const MAX_DYNAMIC_SEGMENTS: usize = 16; - -/// Regex flags to allow '.' in regex to match '\n' -/// -/// See the docs under: https://docs.rs/regex/1/regex/#grouping-and-flags -const REGEX_FLAGS: &str = "(?s-m)"; - -/// Describes the set of paths that match to a resource. -/// -/// `ResourceDef`s are effectively a way to transform the a custom resource pattern syntax into -/// suitable regular expressions from which to check matches with paths and capture portions of a -/// matched path into variables. Common cases are on a fast path that avoids going through the -/// regex engine. -/// -/// -/// # Static Resources -/// A static resource is the most basic type of definition. Pass a regular string to -/// [new][Self::new]. Conforming paths must match the string exactly. -/// -/// ## Examples -/// ``` -/// # use actix_router::ResourceDef; -/// let resource = ResourceDef::new("/home"); -/// -/// assert!(resource.is_match("/home")); -/// -/// assert!(!resource.is_match("/home/new")); -/// assert!(!resource.is_match("/homes")); -/// assert!(!resource.is_match("/search")); -/// ``` -/// -/// -/// # Dynamic Segments -/// Also known as "path parameters". Resources can define sections of a pattern that be extracted -/// from a conforming path, if it conforms to (one of) the resource pattern(s). -/// -/// The marker for a dynamic segment is curly braces wrapping an identifier. For example, -/// `/user/{id}` would match paths like `/user/123` or `/user/james` and be able to extract the user -/// IDs "123" and "james", respectively. -/// -/// However, this resource pattern (`/user/{id}`) would, not cover `/user/123/stars` (unless -/// constructed as a prefix; see next section) since the default pattern for segments matches all -/// characters until it finds a `/` character (or the end of the path). Custom segment patterns are -/// covered further down. -/// -/// Dynamic segments do not need to be delimited by `/` characters, they can be defined within a -/// path segment. For example, `/rust-is-{opinion}` can match the paths `/rust-is-cool` and -/// `/rust-is-hard`. -/// -/// For information on capturing segment values from paths or other custom resource types, -/// see [`capture_match_info`][Self::capture_match_info] -/// and [`capture_match_info_fn`][Self::capture_match_info_fn]. -/// -/// A resource definition can contain at most 16 dynamic segments. -/// -/// ## Examples -/// ``` -/// use actix_router::{Path, ResourceDef}; -/// -/// let resource = ResourceDef::prefix("/user/{id}"); -/// -/// assert!(resource.is_match("/user/123")); -/// assert!(!resource.is_match("/user")); -/// assert!(!resource.is_match("/user/")); -/// -/// let mut path = Path::new("/user/123"); -/// resource.capture_match_info(&mut path); -/// assert_eq!(path.get("id").unwrap(), "123"); -/// ``` -/// -/// -/// # Prefix Resources -/// A prefix resource is defined as pattern that can match just the start of a path. -/// -/// This library chooses to restrict that definition slightly. In particular, when matching, the -/// prefix must be separated from the remaining part of the path by a `/` character, either at the -/// end of the prefix pattern or at the start of the the remaining slice. In practice, this is not -/// much of a limitation. -/// -/// Prefix resources can contain dynamic segments. -/// -/// ## Examples -/// ``` -/// # use actix_router::ResourceDef; -/// let resource = ResourceDef::prefix("/home"); -/// assert!(resource.is_match("/home")); -/// assert!(resource.is_match("/home/new")); -/// assert!(!resource.is_match("/homes")); -/// -/// let resource = ResourceDef::prefix("/user/{id}/"); -/// assert!(resource.is_match("/user/123/")); -/// assert!(resource.is_match("/user/123/stars")); -/// ``` -/// -/// -/// # Custom Regex Segments -/// Dynamic segments can be customised to only match a specific regular expression. It can be -/// helpful to do this if resource definitions would otherwise conflict and cause one to -/// be inaccessible. -/// -/// The regex used when capturing segment values can be specified explicitly using this syntax: -/// `{name:regex}`. For example, `/user/{id:\d+}` will only match paths where the user ID -/// is numeric. -/// -/// By default, dynamic segments use this regex: `[^/]+`. This shows why it is the case, as shown in -/// the earlier section, that segments capture a slice of the path up to the next `/` character. -/// -/// Custom regex segments can be used in static and prefix resource definition variants. -/// -/// ## Examples -/// ``` -/// # use actix_router::ResourceDef; -/// let resource = ResourceDef::new(r"/user/{id:\d+}"); -/// assert!(resource.is_match("/user/123")); -/// assert!(resource.is_match("/user/314159")); -/// assert!(!resource.is_match("/user/abc")); -/// ``` -/// -/// -/// # Tail Segments -/// As a shortcut to defining a custom regex for matching _all_ remaining characters (not just those -/// up until a `/` character), there is a special pattern to match (and capture) the remaining -/// path portion. -/// -/// To do this, use the segment pattern: `{name}*`. Since a tail segment also has a name, values are -/// extracted in the same way as non-tail dynamic segments. -/// -/// ## Examples -/// ```rust -/// # use actix_router::{Path, ResourceDef}; -/// let resource = ResourceDef::new("/blob/{tail}*"); -/// assert!(resource.is_match("/blob/HEAD/Cargo.toml")); -/// assert!(resource.is_match("/blob/HEAD/README.md")); -/// -/// let mut path = Path::new("/blob/main/LICENSE"); -/// resource.capture_match_info(&mut path); -/// assert_eq!(path.get("tail").unwrap(), "main/LICENSE"); -/// ``` -/// -/// -/// # Multi-Pattern Resources -/// For resources that can map to multiple distinct paths, it may be suitable to use -/// multi-pattern resources by passing an array/vec to [`new`][Self::new]. They will be combined -/// into a regex set which is usually quicker to check matches on than checking each -/// pattern individually. -/// -/// Multi-pattern resources can contain dynamic segments just like single pattern ones. -/// However, take care to use consistent and semantically-equivalent segment names; it could affect -/// expectations in the router using these definitions and cause runtime panics. -/// -/// ## Examples -/// ```rust -/// # use actix_router::ResourceDef; -/// let resource = ResourceDef::new(["/home", "/index"]); -/// assert!(resource.is_match("/home")); -/// assert!(resource.is_match("/index")); -/// ``` -/// -/// -/// # Trailing Slashes -/// It should be noted that this library takes no steps to normalize intra-path or trailing slashes. -/// As such, all resource definitions implicitly expect a pre-processing step to normalize paths if -/// they you wish to accommodate "recoverable" path errors. Below are several examples of -/// resource-path pairs that would not be compatible. -/// -/// ## Examples -/// ```rust -/// # use actix_router::ResourceDef; -/// assert!(!ResourceDef::new("/root").is_match("/root/")); -/// assert!(!ResourceDef::new("/root/").is_match("/root")); -/// assert!(!ResourceDef::prefix("/root/").is_match("/root")); -/// ``` -#[derive(Clone, Debug)] -pub struct ResourceDef { - id: u16, - - /// Optional name of resource. - name: Option, - - /// Pattern that generated the resource definition. - /// - /// `None` when pattern type is `DynamicSet`. - patterns: Patterns, - - /// Pattern type. - pat_type: PatternType, - - /// List of segments that compose the pattern, in order. - /// - /// `None` when pattern type is `DynamicSet`. - segments: Option>, -} - -#[derive(Debug, Clone, PartialEq)] -enum PatternSegment { - /// Literal slice of pattern. - Const(String), - - /// Name of dynamic segment. - Var(String), -} - -#[derive(Clone, Debug)] -#[allow(clippy::large_enum_variant)] -enum PatternType { - /// Single constant/literal segment. - Static(String), - - /// Single constant/literal prefix segment. - Prefix(String), - - /// Single regular expression and list of dynamic segment names. - Dynamic(Regex, Vec<&'static str>), - - /// Regular expression set and list of component expressions plus dynamic segment names. - DynamicSet(RegexSet, Vec<(Regex, Vec<&'static str>)>), -} - -impl ResourceDef { - /// Constructs a new resource definition from patterns. - /// - /// Multi-pattern resources can be constructed by providing a slice (or vec) of patterns. - /// - /// # Panics - /// Panics if path pattern is malformed. - /// - /// # Examples - /// ``` - /// use actix_router::ResourceDef; - /// - /// let resource = ResourceDef::new("/user/{id}"); - /// assert!(resource.is_match("/user/123")); - /// assert!(!resource.is_match("/user/123/stars")); - /// assert!(!resource.is_match("user/1234")); - /// assert!(!resource.is_match("/foo")); - /// - /// let resource = ResourceDef::new(["/profile", "/user/{id}"]); - /// assert!(resource.is_match("/profile")); - /// assert!(resource.is_match("/user/123")); - /// assert!(!resource.is_match("user/123")); - /// assert!(!resource.is_match("/foo")); - /// ``` - pub fn new(paths: T) -> Self { - profile_method!(new); - - match paths.patterns() { - Patterns::Single(pattern) => ResourceDef::from_single_pattern(&pattern, false), - - // since zero length pattern sets are possible - // just return a useless `ResourceDef` - Patterns::List(patterns) if patterns.is_empty() => ResourceDef { - id: 0, - name: None, - patterns: Patterns::List(patterns), - pat_type: PatternType::DynamicSet(RegexSet::empty(), Vec::new()), - segments: None, - }, - - Patterns::List(patterns) => { - let mut re_set = Vec::with_capacity(patterns.len()); - let mut pattern_data = Vec::new(); - - for pattern in &patterns { - match ResourceDef::parse(&pattern, false, true) { - (PatternType::Dynamic(re, names), _) => { - re_set.push(re.as_str().to_owned()); - pattern_data.push((re, names)); - } - _ => unreachable!(), - } - } - - let pattern_re_set = RegexSet::new(re_set).unwrap(); - - ResourceDef { - id: 0, - name: None, - patterns: Patterns::List(patterns), - pat_type: PatternType::DynamicSet(pattern_re_set, pattern_data), - segments: None, - } - } - } - } - - /// Constructs a new resource definition using a string pattern that performs prefix matching. - /// - /// More specifically, the regular expressions generated for matching are different when using - /// this method vs using `new`; they will not be appended with the `$` meta-character that - /// matches the end of an input. - /// - /// Although it will compile and run correctly, it is meaningless to construct a prefix - /// resource definition with a tail segment; use [`new`][Self::new] in this case. - /// - /// # Panics - /// Panics if path regex pattern is malformed. - /// - /// # Examples - /// ``` - /// use actix_router::ResourceDef; - /// - /// let resource = ResourceDef::prefix("/user/{id}"); - /// assert!(resource.is_match("/user/123")); - /// assert!(resource.is_match("/user/123/stars")); - /// assert!(!resource.is_match("user/123")); - /// assert!(!resource.is_match("user/123/stars")); - /// assert!(!resource.is_match("/foo")); - /// - /// let resource = ResourceDef::prefix("user/{id}"); - /// assert!(resource.is_match("user/123")); - /// assert!(resource.is_match("user/123/stars")); - /// assert!(!resource.is_match("/user/123")); - /// assert!(!resource.is_match("/user/123/stars")); - /// assert!(!resource.is_match("foo")); - /// ``` - pub fn prefix(path: &str) -> Self { - profile_method!(prefix); - ResourceDef::from_single_pattern(path, true) - } - - /// Constructs a new resource definition using a string pattern that performs prefix matching, - /// inserting a `/` to beginning of the pattern if absent and pattern is not empty. - /// - /// # Panics - /// Panics if path regex pattern is malformed. - /// - /// # Examples - /// ``` - /// use actix_router::ResourceDef; - /// - /// let resource = ResourceDef::root_prefix("user/{id}"); - /// - /// assert_eq!(&resource, &ResourceDef::prefix("/user/{id}")); - /// assert_eq!(&resource, &ResourceDef::root_prefix("/user/{id}")); - /// assert_ne!(&resource, &ResourceDef::new("user/{id}")); - /// assert_ne!(&resource, &ResourceDef::new("/user/{id}")); - /// - /// assert!(resource.is_match("/user/123")); - /// assert!(!resource.is_match("user/123")); - /// ``` - pub fn root_prefix(path: &str) -> Self { - profile_method!(root_prefix); - ResourceDef::prefix(&insert_slash(path)) - } - - /// Returns a numeric resource ID. - /// - /// If not explicitly set using [`set_id`][Self::set_id], this will return `0`. - /// - /// # Examples - /// ``` - /// # use actix_router::ResourceDef; - /// let mut resource = ResourceDef::new("/root"); - /// assert_eq!(resource.id(), 0); - /// - /// resource.set_id(42); - /// assert_eq!(resource.id(), 42); - /// ``` - pub fn id(&self) -> u16 { - self.id - } - - /// Set numeric resource ID. - /// - /// # Examples - /// ``` - /// # use actix_router::ResourceDef; - /// let mut resource = ResourceDef::new("/root"); - /// resource.set_id(42); - /// assert_eq!(resource.id(), 42); - /// ``` - pub fn set_id(&mut self, id: u16) { - self.id = id; - } - - /// Returns resource definition name, if set. - /// - /// # Examples - /// ``` - /// # use actix_router::ResourceDef; - /// let mut resource = ResourceDef::new("/root"); - /// assert!(resource.name().is_none()); - /// - /// resource.set_name("root"); - /// assert_eq!(resource.name().unwrap(), "root"); - pub fn name(&self) -> Option<&str> { - self.name.as_deref() - } - - /// Assigns a new name to the resource. - /// - /// # Panics - /// Panics if `name` is an empty string. - /// - /// # Examples - /// ``` - /// # use actix_router::ResourceDef; - /// let mut resource = ResourceDef::new("/root"); - /// resource.set_name("root"); - /// assert_eq!(resource.name().unwrap(), "root"); - /// ``` - pub fn set_name(&mut self, name: impl Into) { - let name = name.into(); - - if name.is_empty() { - panic!("resource name should not be empty"); - } - - self.name = Some(name) - } - - /// Returns `true` if pattern type is prefix. - /// - /// # Examples - /// ``` - /// # use actix_router::ResourceDef; - /// assert!(ResourceDef::prefix("/user").is_prefix()); - /// assert!(!ResourceDef::new("/user").is_prefix()); - /// ``` - pub fn is_prefix(&self) -> bool { - match &self.pat_type { - PatternType::Prefix(_) => true, - PatternType::Dynamic(re, _) if !re.as_str().ends_with('$') => true, - _ => false, - } - } - - /// Returns the pattern string that generated the resource definition. - /// - /// Returns `None` if definition was constructed with multiple patterns. - /// See [`patterns_iter`][Self::pattern_iter]. - /// - /// # Examples - /// ``` - /// # use actix_router::ResourceDef; - /// let mut resource = ResourceDef::new("/user/{id}"); - /// assert_eq!(resource.pattern().unwrap(), "/user/{id}"); - /// - /// let mut resource = ResourceDef::new(["/profile", "/user/{id}"]); - /// assert!(resource.pattern().is_none()); - pub fn pattern(&self) -> Option<&str> { - match &self.patterns { - Patterns::Single(pattern) => Some(pattern.as_str()), - Patterns::List(_) => None, - } - } - - /// Returns iterator of pattern strings that generated the resource definition. - /// - /// # Examples - /// ``` - /// # use actix_router::ResourceDef; - /// let mut resource = ResourceDef::new("/root"); - /// let mut iter = resource.pattern_iter(); - /// assert_eq!(iter.next().unwrap(), "/root"); - /// assert!(iter.next().is_none()); - /// - /// let mut resource = ResourceDef::new(["/root", "/backup"]); - /// let mut iter = resource.pattern_iter(); - /// assert_eq!(iter.next().unwrap(), "/root"); - /// assert_eq!(iter.next().unwrap(), "/backup"); - /// assert!(iter.next().is_none()); - pub fn pattern_iter(&self) -> impl Iterator { - struct PatternIter<'a> { - patterns: &'a Patterns, - list_idx: usize, - done: bool, - } - - impl<'a> Iterator for PatternIter<'a> { - type Item = &'a str; - - fn next(&mut self) -> Option { - match &self.patterns { - Patterns::Single(pattern) => { - if self.done { - return None; - } - - self.done = true; - Some(pattern.as_str()) - } - Patterns::List(patterns) if patterns.is_empty() => None, - Patterns::List(patterns) => match patterns.get(self.list_idx) { - Some(pattern) => { - self.list_idx += 1; - Some(pattern.as_str()) - } - None => { - // fast path future call - self.done = true; - None - } - }, - } - } - - fn size_hint(&self) -> (usize, Option) { - match &self.patterns { - Patterns::Single(_) => (1, Some(1)), - Patterns::List(patterns) => (patterns.len(), Some(patterns.len())), - } - } - } - - PatternIter { - patterns: &self.patterns, - list_idx: 0, - done: false, - } - } - - /// Joins two resources. - /// - /// Resulting resource is prefix if `other` is prefix. - /// - /// # Examples - /// ``` - /// # use actix_router::ResourceDef; - /// let joined = ResourceDef::prefix("/root").join(&ResourceDef::prefix("/seg")); - /// assert_eq!(joined, ResourceDef::prefix("/root/seg")); - /// ``` - pub fn join(&self, other: &ResourceDef) -> ResourceDef { - let patterns = self - .pattern_iter() - .flat_map(move |this| other.pattern_iter().map(move |other| (this, other))) - .map(|(this, other)| [this, other].join("")) - .collect::>(); - - match patterns.len() { - 1 => ResourceDef::from_single_pattern(&patterns[0], other.is_prefix()), - _ => ResourceDef::new(patterns), - } - } - - /// Returns `true` if `path` matches this resource. - /// - /// The behavior of this method depends on how the `ResourceDef` was constructed. For example, - /// static resources will not be able to match as many paths as dynamic and prefix resources. - /// See [`ResourceDef`] struct docs for details on resource definition types. - /// - /// This method will always agree with [`find_match`][Self::find_match] on whether the path - /// matches or not. - /// - /// # Examples - /// ``` - /// use actix_router::ResourceDef; - /// - /// // static resource - /// let resource = ResourceDef::new("/user"); - /// assert!(resource.is_match("/user")); - /// assert!(!resource.is_match("/users")); - /// assert!(!resource.is_match("/user/123")); - /// assert!(!resource.is_match("/foo")); - /// - /// // dynamic resource - /// let resource = ResourceDef::new("/user/{user_id}"); - /// assert!(resource.is_match("/user/123")); - /// assert!(!resource.is_match("/user/123/stars")); - /// - /// // prefix resource - /// let resource = ResourceDef::prefix("/root"); - /// assert!(resource.is_match("/root")); - /// assert!(resource.is_match("/root/leaf")); - /// assert!(!resource.is_match("/roots")); - /// - /// // more examples are shown in the `ResourceDef` struct docs - /// ``` - #[inline] - pub fn is_match(&self, path: &str) -> bool { - profile_method!(is_match); - - // this function could be expressed as: - // `self.find_match(path).is_some()` - // but this skips some checks and uses potentially faster regex methods - - match self.pat_type { - PatternType::Static(ref s) => s == path, - - PatternType::Prefix(ref prefix) if prefix == path => true, - PatternType::Prefix(ref prefix) => is_strict_prefix(prefix, path), - - // dynamic prefix - PatternType::Dynamic(ref re, _) if !re.as_str().ends_with('$') => { - match re.find(path) { - // prefix matches exactly - Some(m) if m.end() == path.len() => true, - - // prefix matches part - Some(m) => is_strict_prefix(m.as_str(), path), - - // prefix does not match - None => false, - } - } - - PatternType::Dynamic(ref re, _) => re.is_match(path), - PatternType::DynamicSet(ref re, _) => re.is_match(path), - } - } - - /// Tries to match `path` to this resource, returning the position in the path where the - /// match ends. - /// - /// This method will always agree with [`is_match`][Self::is_match] on whether the path matches - /// or not. - /// - /// # Examples - /// ``` - /// use actix_router::ResourceDef; - /// - /// // static resource - /// let resource = ResourceDef::new("/user"); - /// assert_eq!(resource.find_match("/user"), Some(5)); - /// assert!(resource.find_match("/user/").is_none()); - /// assert!(resource.find_match("/user/123").is_none()); - /// assert!(resource.find_match("/foo").is_none()); - /// - /// // constant prefix resource - /// let resource = ResourceDef::prefix("/user"); - /// assert_eq!(resource.find_match("/user"), Some(5)); - /// assert_eq!(resource.find_match("/user/"), Some(5)); - /// assert_eq!(resource.find_match("/user/123"), Some(5)); - /// - /// // dynamic prefix resource - /// let resource = ResourceDef::prefix("/user/{id}"); - /// assert_eq!(resource.find_match("/user/123"), Some(9)); - /// assert_eq!(resource.find_match("/user/1234/"), Some(10)); - /// assert_eq!(resource.find_match("/user/12345/stars"), Some(11)); - /// assert!(resource.find_match("/user/").is_none()); - /// - /// // multi-pattern resource - /// let resource = ResourceDef::new(["/user/{id}", "/profile/{id}"]); - /// assert_eq!(resource.find_match("/user/123"), Some(9)); - /// assert_eq!(resource.find_match("/profile/1234"), Some(13)); - /// ``` - pub fn find_match(&self, path: &str) -> Option { - profile_method!(find_match); - - match &self.pat_type { - PatternType::Static(segment) if path == segment => Some(segment.len()), - PatternType::Static(_) => None, - - PatternType::Prefix(prefix) if path == prefix => Some(prefix.len()), - PatternType::Prefix(prefix) if is_strict_prefix(prefix, path) => Some(prefix.len()), - PatternType::Prefix(_) => None, - - // dynamic prefix - PatternType::Dynamic(ref re, _) if !re.as_str().ends_with('$') => { - match re.find(path) { - // prefix matches exactly - Some(m) if m.end() == path.len() => Some(m.end()), - - // prefix matches part - Some(m) if is_strict_prefix(m.as_str(), path) => Some(m.end()), - - // prefix does not match - _ => None, - } - } - - PatternType::Dynamic(re, _) => re.find(path).map(|m| m.end()), - - PatternType::DynamicSet(re, params) => { - let idx = re.matches(path).into_iter().next()?; - let (ref pattern, _) = params[idx]; - pattern.find(path).map(|m| m.end()) - } - } - } - - /// Collects dynamic segment values into `path`. - /// - /// Returns `true` if `path` matches this resource. - /// - /// # Examples - /// ``` - /// use actix_router::{Path, ResourceDef}; - /// - /// let resource = ResourceDef::prefix("/user/{id}"); - /// let mut path = Path::new("/user/123/stars"); - /// assert!(resource.capture_match_info(&mut path)); - /// assert_eq!(path.get("id").unwrap(), "123"); - /// assert_eq!(path.unprocessed(), "/stars"); - /// - /// let resource = ResourceDef::new("/blob/{path}*"); - /// let mut path = Path::new("/blob/HEAD/Cargo.toml"); - /// assert!(resource.capture_match_info(&mut path)); - /// assert_eq!(path.get("path").unwrap(), "HEAD/Cargo.toml"); - /// assert_eq!(path.unprocessed(), ""); - /// ``` - pub fn capture_match_info(&self, path: &mut Path) -> bool { - profile_method!(capture_match_info); - self.capture_match_info_fn(path, |_, _| true, ()) - } - - /// Collects dynamic segment values into `resource` after matching paths and executing - /// check function. - /// - /// The check function is given a reference to the passed resource and optional arbitrary data. - /// This is useful if you want to conditionally match on some non-path related aspect of the - /// resource type. - /// - /// Returns `true` if resource path matches this resource definition _and_ satisfies the - /// given check function. - /// - /// # Examples - /// ``` - /// use actix_router::{Path, ResourceDef}; - /// - /// fn try_match(resource: &ResourceDef, path: &mut Path<&str>) -> bool { - /// let admin_allowed = std::env::var("ADMIN_ALLOWED").ok(); - /// - /// resource.capture_match_info_fn( - /// path, - /// // when env var is not set, reject when path contains "admin" - /// |res, admin_allowed| !res.path().contains("admin"), - /// &admin_allowed - /// ) - /// } - /// - /// let resource = ResourceDef::prefix("/user/{id}"); - /// - /// // path matches; segment values are collected into path - /// let mut path = Path::new("/user/james/stars"); - /// assert!(try_match(&resource, &mut path)); - /// assert_eq!(path.get("id").unwrap(), "james"); - /// assert_eq!(path.unprocessed(), "/stars"); - /// - /// // path matches but fails check function; no segments are collected - /// let mut path = Path::new("/user/admin/stars"); - /// assert!(!try_match(&resource, &mut path)); - /// assert_eq!(path.unprocessed(), "/user/admin/stars"); - /// ``` - pub fn capture_match_info_fn( - &self, - resource: &mut R, - check_fn: F, - user_data: U, - ) -> bool - where - R: Resource, - T: ResourcePath, - F: FnOnce(&R, U) -> bool, - { - profile_method!(capture_match_info_fn); - - let mut segments = <[PathItem; MAX_DYNAMIC_SEGMENTS]>::default(); - let path = resource.resource_path(); - let path_str = path.path(); - - let (matched_len, matched_vars) = match &self.pat_type { - PatternType::Static(_) | PatternType::Prefix(_) => { - profile_section!(pattern_static_or_prefix); - - match self.find_match(path_str) { - Some(len) => (len, None), - None => return false, - } - } - - PatternType::Dynamic(re, names) => { - profile_section!(pattern_dynamic); - - let captures = { - profile_section!(pattern_dynamic_regex_exec); - - match re.captures(path.path()) { - Some(captures) => captures, - _ => return false, - } - }; - - { - profile_section!(pattern_dynamic_extract_captures); - - for (no, name) in names.iter().enumerate() { - if let Some(m) = captures.name(&name) { - segments[no] = PathItem::Segment(m.start() as u16, m.end() as u16); - } else { - log::error!( - "Dynamic path match but not all segments found: {}", - name - ); - return false; - } - } - }; - - (captures[0].len(), Some(names)) - } - - PatternType::DynamicSet(re, params) => { - profile_section!(pattern_dynamic_set); - - let path = path.path(); - let (pattern, names) = match re.matches(path).into_iter().next() { - Some(idx) => ¶ms[idx], - _ => return false, - }; - - let captures = match pattern.captures(path.path()) { - Some(captures) => captures, - _ => return false, - }; - - for (no, name) in names.iter().enumerate() { - if let Some(m) = captures.name(&name) { - segments[no] = PathItem::Segment(m.start() as u16, m.end() as u16); - } else { - log::error!("Dynamic path match but not all segments found: {}", name); - return false; - } - } - - (captures[0].len(), Some(names)) - } - }; - - if !check_fn(resource, user_data) { - return false; - } - - // Modify `path` to skip matched part and store matched segments - let path = resource.resource_path(); - - if let Some(vars) = matched_vars { - for i in 0..vars.len() { - path.add(vars[i], mem::take(&mut segments[i])); - } - } - - path.skip(matched_len as u16); - - true - } - - /// Assembles resource path using a closure that maps variable segment names to values. - fn build_resource_path(&self, path: &mut String, mut vars: F) -> bool - where - F: FnMut(&str) -> Option, - I: AsRef, - { - for el in match self.segments { - Some(ref segments) => segments, - None => return false, - } { - match *el { - PatternSegment::Const(ref val) => path.push_str(val), - PatternSegment::Var(ref name) => match vars(name) { - Some(val) => path.push_str(val.as_ref()), - _ => return false, - }, - } - } - - true - } - - /// Assembles full resource path from iterator of dynamic segment values. - /// - /// Returns `true` on success. - /// - /// Resource paths can not be built from multi-pattern resources; this call will always return - /// false and will not add anything to the string buffer. - /// - /// # Examples - /// ``` - /// # use actix_router::ResourceDef; - /// let mut s = String::new(); - /// let resource = ResourceDef::new("/user/{id}/post/{title}"); - /// - /// assert!(resource.resource_path_from_iter(&mut s, &["123", "my-post"])); - /// assert_eq!(s, "/user/123/post/my-post"); - /// ``` - pub fn resource_path_from_iter(&self, path: &mut String, values: I) -> bool - where - I: IntoIterator, - I::Item: AsRef, - { - profile_method!(resource_path_from_iter); - let mut iter = values.into_iter(); - self.build_resource_path(path, |_| iter.next()) - } - - /// Assembles resource path from map of dynamic segment values. - /// - /// Returns `true` on success. - /// - /// Resource paths can not be built from multi-pattern resources; this call will always return - /// false and will not add anything to the string buffer. - /// - /// # Examples - /// ``` - /// # use std::collections::HashMap; - /// # use actix_router::ResourceDef; - /// let mut s = String::new(); - /// let resource = ResourceDef::new("/user/{id}/post/{title}"); - /// - /// let mut map = HashMap::new(); - /// map.insert("id", "123"); - /// map.insert("title", "my-post"); - /// - /// assert!(resource.resource_path_from_map(&mut s, &map)); - /// assert_eq!(s, "/user/123/post/my-post"); - /// ``` - pub fn resource_path_from_map( - &self, - path: &mut String, - values: &HashMap, - ) -> bool - where - K: Borrow + Eq + Hash, - V: AsRef, - S: BuildHasher, - { - profile_method!(resource_path_from_map); - self.build_resource_path(path, |name| values.get(name).map(AsRef::::as_ref)) - } - - /// Parse path pattern and create a new instance. - fn from_single_pattern(pattern: &str, is_prefix: bool) -> Self { - profile_method!(from_single_pattern); - - let pattern = pattern.to_owned(); - let (pat_type, segments) = ResourceDef::parse(&pattern, is_prefix, false); - - ResourceDef { - id: 0, - name: None, - patterns: Patterns::Single(pattern), - pat_type, - segments: Some(segments), - } - } - - /// Parses a dynamic segment definition from a pattern. - /// - /// The returned tuple includes: - /// - the segment descriptor, either `Var` or `Tail` - /// - the segment's regex to check values against - /// - the remaining, unprocessed string slice - /// - whether the parsed parameter represents a tail pattern - /// - /// # Panics - /// Panics if given patterns does not contain a dynamic segment. - fn parse_param(pattern: &str) -> (PatternSegment, String, &str, bool) { - profile_method!(parse_param); - - const DEFAULT_PATTERN: &str = "[^/]+"; - const DEFAULT_PATTERN_TAIL: &str = ".*"; - - let mut params_nesting = 0usize; - let close_idx = pattern - .find(|c| match c { - '{' => { - params_nesting += 1; - false - } - '}' => { - params_nesting -= 1; - params_nesting == 0 - } - _ => false, - }) - .unwrap_or_else(|| { - panic!(r#"path "{}" contains malformed dynamic segment"#, pattern) - }); - - let (mut param, mut unprocessed) = pattern.split_at(close_idx + 1); - - // remove outer curly brackets - param = ¶m[1..param.len() - 1]; - - let tail = unprocessed == "*"; - - let (name, pattern) = match param.find(':') { - Some(idx) => { - if tail { - panic!("custom regex is not supported for tail match"); - } - - let (name, pattern) = param.split_at(idx); - (name, &pattern[1..]) - } - None => ( - param, - if tail { - unprocessed = &unprocessed[1..]; - DEFAULT_PATTERN_TAIL - } else { - DEFAULT_PATTERN - }, - ), - }; - - let segment = PatternSegment::Var(name.to_string()); - let regex = format!(r"(?P<{}>{})", &name, &pattern); - - (segment, regex, unprocessed, tail) - } - - /// Parse `pattern` using `is_prefix` and `force_dynamic` flags. - /// - /// Parameters: - /// - `is_prefix`: Use `true` if `pattern` should be treated as a prefix; i.e., a conforming - /// path will be a match even if it has parts remaining to process - /// - `force_dynamic`: Use `true` to disallow the return of static and prefix segments. - /// - /// The returned tuple includes: - /// - the pattern type detected, either `Static`, `Prefix`, or `Dynamic` - /// - a list of segment descriptors from the pattern - fn parse( - pattern: &str, - is_prefix: bool, - force_dynamic: bool, - ) -> (PatternType, Vec) { - profile_method!(parse); - - let mut unprocessed = pattern; - - if !force_dynamic && unprocessed.find('{').is_none() && !unprocessed.ends_with('*') { - // pattern is static - - let tp = if is_prefix { - PatternType::Prefix(unprocessed.to_owned()) - } else { - PatternType::Static(unprocessed.to_owned()) - }; - - return (tp, vec![PatternSegment::Const(unprocessed.to_owned())]); - } - - let mut segments = Vec::new(); - let mut re = format!("{}^", REGEX_FLAGS); - let mut dyn_segment_count = 0; - let mut has_tail_segment = false; - - while let Some(idx) = unprocessed.find('{') { - let (prefix, rem) = unprocessed.split_at(idx); - - segments.push(PatternSegment::Const(prefix.to_owned())); - re.push_str(&escape(prefix)); - - let (param_pattern, re_part, rem, tail) = Self::parse_param(rem); - - if tail { - has_tail_segment = true; - } - - segments.push(param_pattern); - re.push_str(&re_part); - - unprocessed = rem; - dyn_segment_count += 1; - } - - if is_prefix && has_tail_segment { - // tail segments in prefixes have no defined semantics - - #[cfg(not(test))] - log::warn!( - "Prefix resources should not have tail segments. \ - Use `ResourceDef::new` constructor. \ - This may become a panic in the future." - ); - - // panic in tests to make this case detectable - #[cfg(test)] - panic!("prefix resource definitions should not have tail segments"); - } - - if unprocessed.ends_with('*') { - // unnamed tail segment - - #[cfg(not(test))] - log::warn!( - "Tail segments must have names. \ - Consider `.../{{tail}}*`. \ - This may become a panic in the future." - ); - - // panic in tests to make this case detectable - #[cfg(test)] - panic!("tail segments must have names"); - } else if !has_tail_segment && !unprocessed.is_empty() { - // prevent `Const("")` element from being added after last dynamic segment - - segments.push(PatternSegment::Const(unprocessed.to_owned())); - re.push_str(&escape(unprocessed)); - } - - if dyn_segment_count > MAX_DYNAMIC_SEGMENTS { - panic!( - "Only {} dynamic segments are allowed, provided: {}", - MAX_DYNAMIC_SEGMENTS, dyn_segment_count - ); - } - - if !is_prefix && !has_tail_segment { - re.push('$'); - } - - let re = match Regex::new(&re) { - Ok(re) => re, - Err(err) => panic!("Wrong path pattern: \"{}\" {}", pattern, err), - }; - - // `Bok::leak(Box::new(name))` is an intentional memory leak. In typical applications the - // routing table is only constructed once (per worker) so leak is bounded. If you are - // constructing `ResourceDef`s more than once in your application's lifecycle you would - // expect a linear increase in leaked memory over time. - let names = re - .capture_names() - .filter_map(|name| name.map(|name| Box::leak(Box::new(name.to_owned())).as_str())) - .collect(); - - (PatternType::Dynamic(re, names), segments) - } -} - -impl Eq for ResourceDef {} - -impl PartialEq for ResourceDef { - fn eq(&self, other: &ResourceDef) -> bool { - self.patterns == other.patterns - && match &self.pat_type { - PatternType::Static(_) => matches!(&other.pat_type, PatternType::Static(_)), - PatternType::Prefix(_) => matches!(&other.pat_type, PatternType::Prefix(_)), - PatternType::Dynamic(re, _) => match &other.pat_type { - PatternType::Dynamic(other_re, _) => re.as_str() == other_re.as_str(), - _ => false, - }, - PatternType::DynamicSet(_, _) => { - matches!(&other.pat_type, PatternType::DynamicSet(..)) - } - } - } -} - -impl Hash for ResourceDef { - fn hash(&self, state: &mut H) { - self.patterns.hash(state); - } -} - -impl<'a> From<&'a str> for ResourceDef { - fn from(path: &'a str) -> ResourceDef { - ResourceDef::new(path) - } -} - -impl From for ResourceDef { - fn from(path: String) -> ResourceDef { - ResourceDef::new(path) - } -} - -pub(crate) fn insert_slash(path: &str) -> Cow<'_, str> { - profile_fn!(insert_slash); - - if !path.is_empty() && !path.starts_with('/') { - let mut new_path = String::with_capacity(path.len() + 1); - new_path.push('/'); - new_path.push_str(path); - Cow::Owned(new_path) - } else { - Cow::Borrowed(path) - } -} - -/// Returns true if `prefix` acts as a proper prefix (i.e., separated by a slash) in `path`. -/// -/// The `strict` refers to the fact that this will return `false` if `prefix == path`. -fn is_strict_prefix(prefix: &str, path: &str) -> bool { - path.starts_with(prefix) && (prefix.ends_with('/') || path[prefix.len()..].starts_with('/')) -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn equivalence() { - assert_eq!( - ResourceDef::root_prefix("/root"), - ResourceDef::prefix("/root") - ); - assert_eq!( - ResourceDef::root_prefix("root"), - ResourceDef::prefix("/root") - ); - assert_eq!( - ResourceDef::root_prefix("/{id}"), - ResourceDef::prefix("/{id}") - ); - assert_eq!( - ResourceDef::root_prefix("{id}"), - ResourceDef::prefix("/{id}") - ); - - assert_eq!(ResourceDef::new("/"), ResourceDef::new(["/"])); - assert_eq!(ResourceDef::new("/"), ResourceDef::new(vec!["/"])); - - assert_ne!(ResourceDef::new(""), ResourceDef::prefix("")); - assert_ne!(ResourceDef::new("/"), ResourceDef::prefix("/")); - assert_ne!(ResourceDef::new("/{id}"), ResourceDef::prefix("/{id}")); - } - - #[test] - fn parse_static() { - let re = ResourceDef::new(""); - - assert!(!re.is_prefix()); - - assert!(re.is_match("")); - assert!(!re.is_match("/")); - assert_eq!(re.find_match(""), Some(0)); - assert_eq!(re.find_match("/"), None); - - let re = ResourceDef::new("/"); - assert!(re.is_match("/")); - assert!(!re.is_match("")); - assert!(!re.is_match("/foo")); - - let re = ResourceDef::new("/name"); - assert!(re.is_match("/name")); - assert!(!re.is_match("/name1")); - assert!(!re.is_match("/name/")); - assert!(!re.is_match("/name~")); - - let mut path = Path::new("/name"); - assert!(re.capture_match_info(&mut path)); - assert_eq!(path.unprocessed(), ""); - - assert_eq!(re.find_match("/name"), Some(5)); - assert_eq!(re.find_match("/name1"), None); - assert_eq!(re.find_match("/name/"), None); - assert_eq!(re.find_match("/name~"), None); - - let re = ResourceDef::new("/name/"); - assert!(re.is_match("/name/")); - assert!(!re.is_match("/name")); - assert!(!re.is_match("/name/gs")); - - let re = ResourceDef::new("/user/profile"); - assert!(re.is_match("/user/profile")); - assert!(!re.is_match("/user/profile/profile")); - - let mut path = Path::new("/user/profile"); - assert!(re.capture_match_info(&mut path)); - assert_eq!(path.unprocessed(), ""); - } - - #[test] - fn parse_param() { - let re = ResourceDef::new("/user/{id}"); - assert!(re.is_match("/user/profile")); - assert!(re.is_match("/user/2345")); - assert!(!re.is_match("/user/2345/")); - assert!(!re.is_match("/user/2345/sdg")); - - let mut path = Path::new("/user/profile"); - assert!(re.capture_match_info(&mut path)); - assert_eq!(path.get("id").unwrap(), "profile"); - assert_eq!(path.unprocessed(), ""); - - let mut path = Path::new("/user/1245125"); - assert!(re.capture_match_info(&mut path)); - assert_eq!(path.get("id").unwrap(), "1245125"); - assert_eq!(path.unprocessed(), ""); - - let re = ResourceDef::new("/v{version}/resource/{id}"); - assert!(re.is_match("/v1/resource/320120")); - assert!(!re.is_match("/v/resource/1")); - assert!(!re.is_match("/resource")); - - let mut path = Path::new("/v151/resource/adage32"); - assert!(re.capture_match_info(&mut path)); - assert_eq!(path.get("version").unwrap(), "151"); - assert_eq!(path.get("id").unwrap(), "adage32"); - assert_eq!(path.unprocessed(), ""); - - let re = ResourceDef::new("/{id:[[:digit:]]{6}}"); - assert!(re.is_match("/012345")); - assert!(!re.is_match("/012")); - assert!(!re.is_match("/01234567")); - assert!(!re.is_match("/XXXXXX")); - - let mut path = Path::new("/012345"); - assert!(re.capture_match_info(&mut path)); - assert_eq!(path.get("id").unwrap(), "012345"); - assert_eq!(path.unprocessed(), ""); - } - - #[allow(clippy::cognitive_complexity)] - #[test] - fn dynamic_set() { - let re = ResourceDef::new(vec![ - "/user/{id}", - "/v{version}/resource/{id}", - "/{id:[[:digit:]]{6}}", - "/static", - ]); - assert!(re.is_match("/user/profile")); - assert!(re.is_match("/user/2345")); - assert!(!re.is_match("/user/2345/")); - assert!(!re.is_match("/user/2345/sdg")); - - let mut path = Path::new("/user/profile"); - assert!(re.capture_match_info(&mut path)); - assert_eq!(path.get("id").unwrap(), "profile"); - assert_eq!(path.unprocessed(), ""); - - let mut path = Path::new("/user/1245125"); - assert!(re.capture_match_info(&mut path)); - assert_eq!(path.get("id").unwrap(), "1245125"); - assert_eq!(path.unprocessed(), ""); - - assert!(re.is_match("/v1/resource/320120")); - assert!(!re.is_match("/v/resource/1")); - assert!(!re.is_match("/resource")); - - let mut path = Path::new("/v151/resource/adage32"); - assert!(re.capture_match_info(&mut path)); - assert_eq!(path.get("version").unwrap(), "151"); - assert_eq!(path.get("id").unwrap(), "adage32"); - - assert!(re.is_match("/012345")); - assert!(!re.is_match("/012")); - assert!(!re.is_match("/01234567")); - assert!(!re.is_match("/XXXXXX")); - - assert!(re.is_match("/static")); - assert!(!re.is_match("/a/static")); - assert!(!re.is_match("/static/a")); - - let mut path = Path::new("/012345"); - assert!(re.capture_match_info(&mut path)); - assert_eq!(path.get("id").unwrap(), "012345"); - - let re = ResourceDef::new([ - "/user/{id}", - "/v{version}/resource/{id}", - "/{id:[[:digit:]]{6}}", - ]); - assert!(re.is_match("/user/profile")); - assert!(re.is_match("/user/2345")); - assert!(!re.is_match("/user/2345/")); - assert!(!re.is_match("/user/2345/sdg")); - - let re = ResourceDef::new([ - "/user/{id}".to_string(), - "/v{version}/resource/{id}".to_string(), - "/{id:[[:digit:]]{6}}".to_string(), - ]); - assert!(re.is_match("/user/profile")); - assert!(re.is_match("/user/2345")); - assert!(!re.is_match("/user/2345/")); - assert!(!re.is_match("/user/2345/sdg")); - } - - #[test] - fn parse_tail() { - let re = ResourceDef::new("/user/-{id}*"); - - let mut path = Path::new("/user/-profile"); - assert!(re.capture_match_info(&mut path)); - assert_eq!(path.get("id").unwrap(), "profile"); - - let mut path = Path::new("/user/-2345"); - assert!(re.capture_match_info(&mut path)); - assert_eq!(path.get("id").unwrap(), "2345"); - - let mut path = Path::new("/user/-2345/"); - assert!(re.capture_match_info(&mut path)); - assert_eq!(path.get("id").unwrap(), "2345/"); - - let mut path = Path::new("/user/-2345/sdg"); - assert!(re.capture_match_info(&mut path)); - assert_eq!(path.get("id").unwrap(), "2345/sdg"); - } - - #[test] - fn static_tail() { - let re = ResourceDef::new("/user{tail}*"); - assert!(re.is_match("/users")); - assert!(re.is_match("/user-foo")); - assert!(re.is_match("/user/profile")); - assert!(re.is_match("/user/2345")); - assert!(re.is_match("/user/2345/")); - assert!(re.is_match("/user/2345/sdg")); - assert!(!re.is_match("/foo/profile")); - - let re = ResourceDef::new("/user/{tail}*"); - assert!(re.is_match("/user/profile")); - assert!(re.is_match("/user/2345")); - assert!(re.is_match("/user/2345/")); - assert!(re.is_match("/user/2345/sdg")); - assert!(!re.is_match("/foo/profile")); - } - - #[test] - fn dynamic_tail() { - let re = ResourceDef::new("/user/{id}/{tail}*"); - assert!(!re.is_match("/user/2345")); - let mut path = Path::new("/user/2345/sdg"); - assert!(re.capture_match_info(&mut path)); - assert_eq!(path.get("id").unwrap(), "2345"); - assert_eq!(path.get("tail").unwrap(), "sdg"); - assert_eq!(path.unprocessed(), ""); - } - - #[test] - fn newline_patterns_and_paths() { - let re = ResourceDef::new("/user/a\nb"); - assert!(re.is_match("/user/a\nb")); - assert!(!re.is_match("/user/a\nb/profile")); - - let re = ResourceDef::new("/a{x}b/test/a{y}b"); - let mut path = Path::new("/a\nb/test/a\nb"); - assert!(re.capture_match_info(&mut path)); - assert_eq!(path.get("x").unwrap(), "\n"); - assert_eq!(path.get("y").unwrap(), "\n"); - - let re = ResourceDef::new("/user/{tail}*"); - assert!(re.is_match("/user/a\nb/")); - - let re = ResourceDef::new("/user/{id}*"); - let mut path = Path::new("/user/a\nb/a\nb"); - assert!(re.capture_match_info(&mut path)); - assert_eq!(path.get("id").unwrap(), "a\nb/a\nb"); - - let re = ResourceDef::new("/user/{id:.*}"); - let mut path = Path::new("/user/a\nb/a\nb"); - assert!(re.capture_match_info(&mut path)); - assert_eq!(path.get("id").unwrap(), "a\nb/a\nb"); - } - - #[cfg(feature = "http")] - #[test] - fn parse_urlencoded_param() { - use std::convert::TryFrom; - - let re = ResourceDef::new("/user/{id}/test"); - - let mut path = Path::new("/user/2345/test"); - assert!(re.capture_match_info(&mut path)); - assert_eq!(path.get("id").unwrap(), "2345"); - - let mut path = Path::new("/user/qwe%25/test"); - assert!(re.capture_match_info(&mut path)); - assert_eq!(path.get("id").unwrap(), "qwe%25"); - - let uri = http::Uri::try_from("/user/qwe%25/test").unwrap(); - let mut path = Path::new(uri); - assert!(re.capture_match_info(&mut path)); - assert_eq!(path.get("id").unwrap(), "qwe%25"); - } - - #[test] - fn prefix_static() { - let re = ResourceDef::prefix("/name"); - - assert!(re.is_prefix()); - - assert!(re.is_match("/name")); - assert!(re.is_match("/name/")); - assert!(re.is_match("/name/test/test")); - assert!(!re.is_match("/name1")); - assert!(!re.is_match("/name~")); - - let mut path = Path::new("/name"); - assert!(re.capture_match_info(&mut path)); - assert_eq!(path.unprocessed(), ""); - - let mut path = Path::new("/name/test"); - assert!(re.capture_match_info(&mut path)); - assert_eq!(path.unprocessed(), "/test"); - - assert_eq!(re.find_match("/name"), Some(5)); - assert_eq!(re.find_match("/name/"), Some(5)); - assert_eq!(re.find_match("/name/test/test"), Some(5)); - assert_eq!(re.find_match("/name1"), None); - assert_eq!(re.find_match("/name~"), None); - - let re = ResourceDef::prefix("/name/"); - assert!(re.is_match("/name/")); - assert!(re.is_match("/name/gs")); - assert!(!re.is_match("/name")); - - let mut path = Path::new("/name/gs"); - assert!(re.capture_match_info(&mut path)); - assert_eq!(path.unprocessed(), "gs"); - - let re = ResourceDef::root_prefix("name/"); - assert!(re.is_match("/name/")); - assert!(re.is_match("/name/gs")); - assert!(!re.is_match("/name")); - - let mut path = Path::new("/name/gs"); - assert!(re.capture_match_info(&mut path)); - assert_eq!(path.unprocessed(), "gs"); - } - - #[test] - fn prefix_dynamic() { - let re = ResourceDef::prefix("/{name}/"); - - assert!(re.is_prefix()); - - assert!(re.is_match("/name/")); - assert!(re.is_match("/name/gs")); - assert!(!re.is_match("/name")); - - assert_eq!(re.find_match("/name/"), Some(6)); - assert_eq!(re.find_match("/name/gs"), Some(6)); - assert_eq!(re.find_match("/name"), None); - - let mut path = Path::new("/test2/"); - assert!(re.capture_match_info(&mut path)); - assert_eq!(&path["name"], "test2"); - assert_eq!(&path[0], "test2"); - assert_eq!(path.unprocessed(), ""); - - let mut path = Path::new("/test2/subpath1/subpath2/index.html"); - assert!(re.capture_match_info(&mut path)); - assert_eq!(&path["name"], "test2"); - assert_eq!(&path[0], "test2"); - assert_eq!(path.unprocessed(), "subpath1/subpath2/index.html"); - - let resource = ResourceDef::prefix("/user"); - // input string shorter than prefix - assert!(resource.find_match("/foo").is_none()); - } - - #[test] - fn build_path_list() { - let mut s = String::new(); - let resource = ResourceDef::new("/user/{item1}/test"); - assert!(resource.resource_path_from_iter(&mut s, &mut (&["user1"]).iter())); - assert_eq!(s, "/user/user1/test"); - - let mut s = String::new(); - let resource = ResourceDef::new("/user/{item1}/{item2}/test"); - assert!(resource.resource_path_from_iter(&mut s, &mut (&["item", "item2"]).iter())); - assert_eq!(s, "/user/item/item2/test"); - - let mut s = String::new(); - let resource = ResourceDef::new("/user/{item1}/{item2}"); - assert!(resource.resource_path_from_iter(&mut s, &mut (&["item", "item2"]).iter())); - assert_eq!(s, "/user/item/item2"); - - let mut s = String::new(); - let resource = ResourceDef::new("/user/{item1}/{item2}/"); - assert!(resource.resource_path_from_iter(&mut s, &mut (&["item", "item2"]).iter())); - assert_eq!(s, "/user/item/item2/"); - - let mut s = String::new(); - assert!(!resource.resource_path_from_iter(&mut s, &mut (&["item"]).iter())); - - let mut s = String::new(); - assert!(resource.resource_path_from_iter(&mut s, &mut (&["item", "item2"]).iter())); - assert_eq!(s, "/user/item/item2/"); - assert!(!resource.resource_path_from_iter(&mut s, &mut (&["item"]).iter())); - - let mut s = String::new(); - assert!(resource.resource_path_from_iter(&mut s, &mut vec!["item", "item2"].iter())); - assert_eq!(s, "/user/item/item2/"); - } - - #[test] - fn multi_pattern_cannot_build_path() { - let resource = ResourceDef::new(["/user/{id}", "/profile/{id}"]); - let mut s = String::new(); - assert!(!resource.resource_path_from_iter(&mut s, &mut ["123"].iter())); - } - - #[test] - fn multi_pattern_capture_segment_values() { - let resource = ResourceDef::new(["/user/{id}", "/profile/{id}"]); - - let mut path = Path::new("/user/123"); - assert!(resource.capture_match_info(&mut path)); - assert!(path.get("id").is_some()); - - let mut path = Path::new("/profile/123"); - assert!(resource.capture_match_info(&mut path)); - assert!(path.get("id").is_some()); - - let resource = ResourceDef::new(["/user/{id}", "/profile/{uid}"]); - - let mut path = Path::new("/user/123"); - assert!(resource.capture_match_info(&mut path)); - assert!(path.get("id").is_some()); - assert!(path.get("uid").is_none()); - - let mut path = Path::new("/profile/123"); - assert!(resource.capture_match_info(&mut path)); - assert!(path.get("id").is_none()); - assert!(path.get("uid").is_some()); - } - - #[test] - fn dynamic_prefix_proper_segmentation() { - let resource = ResourceDef::prefix(r"/id/{id:\d{3}}"); - - assert!(resource.is_match("/id/123")); - assert!(resource.is_match("/id/123/foo")); - assert!(!resource.is_match("/id/1234")); - assert!(!resource.is_match("/id/123a")); - - assert_eq!(resource.find_match("/id/123"), Some(7)); - assert_eq!(resource.find_match("/id/123/foo"), Some(7)); - assert_eq!(resource.find_match("/id/1234"), None); - assert_eq!(resource.find_match("/id/123a"), None); - } - - #[test] - fn build_path_map() { - let resource = ResourceDef::new("/user/{item1}/{item2}/"); - - let mut map = HashMap::new(); - map.insert("item1", "item"); - - let mut s = String::new(); - assert!(!resource.resource_path_from_map(&mut s, &map)); - - map.insert("item2", "item2"); - - let mut s = String::new(); - assert!(resource.resource_path_from_map(&mut s, &map)); - assert_eq!(s, "/user/item/item2/"); - } - - #[test] - fn build_path_tail() { - let resource = ResourceDef::new("/user/{item1}*"); - - let mut s = String::new(); - assert!(!resource.resource_path_from_iter(&mut s, &mut (&[""; 0]).iter())); - - let mut s = String::new(); - assert!(resource.resource_path_from_iter(&mut s, &mut (&["user1"]).iter())); - assert_eq!(s, "/user/user1"); - - let mut s = String::new(); - let mut map = HashMap::new(); - map.insert("item1", "item"); - assert!(resource.resource_path_from_map(&mut s, &map)); - assert_eq!(s, "/user/item"); - } - - #[test] - fn consistent_match_length() { - let result = Some(5); - - let re = ResourceDef::prefix("/abc/"); - assert_eq!(re.find_match("/abc/def"), result); - - let re = ResourceDef::prefix("/{id}/"); - assert_eq!(re.find_match("/abc/def"), result); - } - - #[test] - fn join() { - // test joined defs match the same paths as each component separately - - fn seq_find_match(re1: &ResourceDef, re2: &ResourceDef, path: &str) -> Option { - let len1 = re1.find_match(path)?; - let len2 = re2.find_match(&path[len1..])?; - Some(len1 + len2) - } - - macro_rules! join_test { - ($pat1:expr, $pat2:expr => $($test:expr),+) => {{ - let pat1 = $pat1; - let pat2 = $pat2; - $({ - let _path = $test; - let (re1, re2) = (ResourceDef::prefix(pat1), ResourceDef::new(pat2)); - let _seq = seq_find_match(&re1, &re2, _path); - let _join = re1.join(&re2).find_match(_path); - assert_eq!( - _seq, _join, - "patterns: prefix {:?}, {:?}; mismatch on \"{}\"; seq={:?}; join={:?}", - pat1, pat2, _path, _seq, _join - ); - assert!(!re1.join(&re2).is_prefix()); - - let (re1, re2) = (ResourceDef::prefix(pat1), ResourceDef::prefix(pat2)); - let _seq = seq_find_match(&re1, &re2, _path); - let _join = re1.join(&re2).find_match(_path); - assert_eq!( - _seq, _join, - "patterns: prefix {:?}, prefix {:?}; mismatch on \"{}\"; seq={:?}; join={:?}", - pat1, pat2, _path, _seq, _join - ); - assert!(re1.join(&re2).is_prefix()); - })+ - }} - } - - join_test!("", "" => "", "/hello", "/"); - join_test!("/user", "" => "", "/user", "/user/123", "/user11", "user", "user/123"); - join_test!("", "/user"=> "", "/user", "foo", "/user11", "user", "user/123"); - join_test!("/user", "/xx"=> "", "", "/", "/user", "/xx", "/userxx", "/user/xx"); - } - - #[test] - fn match_methods_agree() { - macro_rules! match_methods_agree { - ($pat:expr => $($test:expr),+) => {{ - match_methods_agree!(finish $pat, ResourceDef::new($pat), $($test),+); - }}; - (prefix $pat:expr => $($test:expr),+) => {{ - match_methods_agree!(finish $pat, ResourceDef::prefix($pat), $($test),+); - }}; - (finish $pat:expr, $re:expr, $($test:expr),+) => {{ - let re = $re; - $({ - let _is = re.is_match($test); - let _find = re.find_match($test).is_some(); - assert_eq!( - _is, _find, - "pattern: {:?}; mismatch on \"{}\"; is={}; find={}", - $pat, $test, _is, _find - ); - })+ - }} - } - - match_methods_agree!("" => "", "/", "/foo"); - match_methods_agree!("/" => "", "/", "/foo"); - match_methods_agree!("/user" => "user", "/user", "/users", "/user/123", "/foo"); - match_methods_agree!("/v{v}" => "v", "/v", "/v1", "/v222", "/foo"); - match_methods_agree!(["/v{v}", "/version/{v}"] => "/v", "/v1", "/version", "/version/1", "/foo"); - - match_methods_agree!("/path{tail}*" => "/path", "/path1", "/path/123"); - match_methods_agree!("/path/{tail}*" => "/path", "/path1", "/path/123"); - - match_methods_agree!(prefix "" => "", "/", "/foo"); - match_methods_agree!(prefix "/user" => "user", "/user", "/users", "/user/123", "/foo"); - match_methods_agree!(prefix r"/id/{id:\d{3}}" => "/id/123", "/id/1234"); - } - - #[test] - #[should_panic] - fn invalid_dynamic_segment_delimiter() { - ResourceDef::new("/user/{username"); - } - - #[test] - #[should_panic] - fn invalid_dynamic_segment_name() { - ResourceDef::new("/user/{}"); - } - - #[test] - #[should_panic] - fn invalid_too_many_dynamic_segments() { - // valid - ResourceDef::new("/{a}/{b}/{c}/{d}/{e}/{f}/{g}/{h}/{i}/{j}/{k}/{l}/{m}/{n}/{o}/{p}"); - - // panics - ResourceDef::new( - "/{a}/{b}/{c}/{d}/{e}/{f}/{g}/{h}/{i}/{j}/{k}/{l}/{m}/{n}/{o}/{p}/{q}", - ); - } - - #[test] - #[should_panic] - fn invalid_custom_regex_for_tail() { - ResourceDef::new(r"/{tail:\d+}*"); - } - - #[test] - #[should_panic] - fn invalid_unnamed_tail_segment() { - ResourceDef::new("/*"); - } - - #[test] - #[should_panic] - fn prefix_plus_tail_match_is_allowed() { - ResourceDef::prefix("/user/{id}*"); - } -} diff --git a/actix-router/src/router.rs b/actix-router/src/router.rs deleted file mode 100644 index f5deb858..00000000 --- a/actix-router/src/router.rs +++ /dev/null @@ -1,281 +0,0 @@ -use firestorm::profile_method; - -use crate::{IntoPatterns, Resource, ResourceDef, ResourcePath}; - -#[derive(Debug, Copy, Clone, PartialEq)] -pub struct ResourceId(pub u16); - -/// Information about current resource -#[derive(Clone, Debug)] -pub struct ResourceInfo { - resource: ResourceId, -} - -/// Resource router. -// T is the resource itself -// U is any other data needed for routing like method guards -pub struct Router { - routes: Vec<(ResourceDef, T, Option)>, -} - -impl Router { - pub fn build() -> RouterBuilder { - RouterBuilder { - resources: Vec::new(), - } - } - - pub fn recognize(&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 deleted file mode 100644 index e08a7171..00000000 --- a/actix-router/src/url.rs +++ /dev/null @@ -1,288 +0,0 @@ -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]); - } - } -}