actix-router-0.5.3/.cargo_vcs_info.json0000644000000001520000000000100134650ustar { "git": { "sha1": "b342b8fc82317365dd9e43659601345d86ba065d" }, "path_in_vcs": "actix-router" }actix-router-0.5.3/CHANGES.md000064400000000000000000000236041046102023000136560ustar 00000000000000# Changes ## Unreleased ## 0.5.3 - Add `unicode` crate feature (on-by-default) to switch between `regex` and `regex-lite` as a trade-off between full unicode support and binary size. - Minimum supported Rust version (MSRV) is now 1.72. ## 0.5.2 - Minimum supported Rust version (MSRV) is now 1.68 due to transitive `time` dependency. ## 0.5.1 - Correct typo in error string for `i32` deserialization. [#2876] - Minimum supported Rust version (MSRV) is now 1.59 due to transitive `time` dependency. [#2876]: https://github.com/actix/actix-web/pull/2876 ## 0.5.0 ### Added - Add `Path::as_str`. [#2590] - Add `ResourceDef::set_name`. [#373][net#373] - Add `RouterBuilder::push`. [#2612] - Implement `IntoPatterns` for `bytestring::ByteString`. [#372][net#372] - Introduce `ResourceDef::join`. [#380][net#380] - Introduce `ResourceDef::pattern_iter` to get an iterator over all patterns in a multi-pattern resource. [#373][net#373] - `Resource` is now implemented for `&mut Path<_>` and `RefMut>`. [#2568] - Support `build_resource_path` on multi-pattern resources. [#2356] - Support multi-pattern prefixes and joins. [#2356] ### Changed - Change signature of `ResourceDef::capture_match_info_fn` to remove `user_data` parameter. [#2612] - Deprecate `Path::path`. [#2590] - Disallow prefix routes with tail segments. [#379][net#379] - Enforce path separators on dynamic prefixes. [#378][net#378] - Minimum supported Rust version (MSRV) is now 1.54. - Prefix segments now always end with with a segment delimiter or end-of-input. [#2355] - Prefix segments with trailing slashes define a trailing empty segment. [#2355] - `Quoter::requote` now returns `Option>`. [#2613] - Re-work `IntoPatterns` trait, adding a `Patterns` enum. [#372][net#372] - Rename `Path::{len => segment_count}` to be more descriptive of its purpose. [#370][net#370] - Rename `ResourceDef::{is_prefix_match => find_match}`. [#373][net#373] - Rename `ResourceDef::{match_path => capture_match_info}`. [#373][net#373] - Rename `ResourceDef::{match_path_checked => capture_match_info_fn}`. [#373][net#373] - Rename `ResourceDef::{resource_path => resource_path_from_iter}`. [#371][net#371] - Rename `ResourceDef::{resource_path_named => resource_path_from_map}`. [#371][net#371] - Rename `Router::{*_checked => *_fn}`. [#373][net#373] - Replace `Option` with `U` in `Router` API. [#2612] - `Resource` trait now uses an associated type, `Path`, instead of a generic parameter. [#2568] - `ResourceDef::pattern` now returns the first pattern in multi-pattern resources. [#2356] - `ResourceDef::resource_path_from_iter` now takes an `IntoIterator`. [#373][net#373] - Return type of `ResourceDef::name` is now `Option<&str>`. [#373][net#373] - Return type of `ResourceDef::pattern` is now `Option<&str>`. [#373][net#373] ### Fixed - Fix `ResourceDef`'s `PartialEq` implementation. [#373][net#373] - Fix segment interpolation leaving `Path` in unintended state after matching. [#368][net#368] - Improve malformed path error message. [#384][net#384] - `PathDeserializer` now decodes all percent encoded characters in dynamic segments. [#2566] - Relax bounds on `Router::recognize*` and `ResourceDef::capture_match_info`. [#2612] - Static patterns in multi-patterns are no longer interpreted as regex. [#366][net#366] ### Removed - `ResourceDef::name_mut`. [#373][net#373] - Unused `ResourceInfo`. [#2612] [#2355]: https://github.com/actix/actix-web/pull/2355 [#2356]: https://github.com/actix/actix-web/pull/2356 [#2566]: https://github.com/actix/actix-net/pull/2566 [#2568]: https://github.com/actix/actix-web/pull/2568 [#2590]: https://github.com/actix/actix-web/pull/2590 [#2612]: https://github.com/actix/actix-web/pull/2612 [#2613]: https://github.com/actix/actix-web/pull/2613 [net#366]: https://github.com/actix/actix-net/pull/366 [net#368]: https://github.com/actix/actix-net/pull/368 [net#368]: https://github.com/actix/actix-net/pull/368 [net#370]: https://github.com/actix/actix-net/pull/370 [net#371]: https://github.com/actix/actix-net/pull/371 [net#372]: https://github.com/actix/actix-net/pull/372 [net#373]: https://github.com/actix/actix-net/pull/373 [net#378]: https://github.com/actix/actix-net/pull/378 [net#379]: https://github.com/actix/actix-net/pull/379 [net#380]: https://github.com/actix/actix-net/pull/380 [net#384]: https://github.com/actix/actix-net/pull/384
0.5.0 Pre-Releases ## 0.5.0-rc.3 - Remove unused `ResourceInfo`. [#2612] - Add `RouterBuilder::push`. [#2612] - Change signature of `ResourceDef::capture_match_info_fn` to remove `user_data` parameter. [#2612] - Replace `Option` with `U` in `Router` API. [#2612] - Relax bounds on `Router::recognize*` and `ResourceDef::capture_match_info`. [#2612] - `Quoter::requote` now returns `Option>`. [#2613] [#2612]: https://github.com/actix/actix-web/pull/2612 [#2613]: https://github.com/actix/actix-web/pull/2613 ## 0.5.0-rc.2 - Add `Path::as_str`. [#2590] - Deprecate `Path::path`. [#2590] [#2590]: https://github.com/actix/actix-web/pull/2590 ## 0.5.0-rc.1 - `Resource` trait now have an associated type, `Path`, instead of the generic parameter. [#2568] - `Resource` is now implemented for `&mut Path<_>` and `RefMut>`. [#2568] [#2568]: https://github.com/actix/actix-web/pull/2568 ## 0.5.0-beta.4 - `PathDeserializer` now decodes all percent encoded characters in dynamic segments. [#2566] - Minimum supported Rust version (MSRV) is now 1.54. [#2566]: https://github.com/actix/actix-net/pull/2566 ## 0.5.0-beta.3 - Minimum supported Rust version (MSRV) is now 1.52. ## 0.5.0-beta.2 - Introduce `ResourceDef::join`. [#380][net#380] - Disallow prefix routes with tail segments. [#379][net#379] - Enforce path separators on dynamic prefixes. [#378][net#378] - Improve malformed path error message. [#384][net#384] - Prefix segments now always end with with a segment delimiter or end-of-input. [#2355] - Prefix segments with trailing slashes define a trailing empty segment. [#2355] - Support multi-pattern prefixes and joins. [#2356] - `ResourceDef::pattern` now returns the first pattern in multi-pattern resources. [#2356] - Support `build_resource_path` on multi-pattern resources. [#2356] - Minimum supported Rust version (MSRV) is now 1.51. [net#378]: https://github.com/actix/actix-net/pull/378 [net#379]: https://github.com/actix/actix-net/pull/379 [net#380]: https://github.com/actix/actix-net/pull/380 [net#384]: https://github.com/actix/actix-net/pull/384 [#2355]: https://github.com/actix/actix-web/pull/2355 [#2356]: https://github.com/actix/actix-web/pull/2356 ## 0.5.0-beta.1 - Fix a bug in multi-patterns where static patterns are interpreted as regex. [#366][net#366] - Introduce `ResourceDef::pattern_iter` to get an iterator over all patterns in a multi-pattern resource. [#373][net#373] - Fix segment interpolation leaving `Path` in unintended state after matching. [#368][net#368] - Fix `ResourceDef` `PartialEq` implementation. [#373][net#373] - Re-work `IntoPatterns` trait, adding a `Patterns` enum. [#372][net#372] - Implement `IntoPatterns` for `bytestring::ByteString`. [#372][net#372] - Rename `Path::{len => segment_count}` to be more descriptive of it's purpose. [#370][net#370] - Rename `ResourceDef::{resource_path => resource_path_from_iter}`. [#371][net#371] - `ResourceDef::resource_path_from_iter` now takes an `IntoIterator`. [#373][net#373] - Rename `ResourceDef::{resource_path_named => resource_path_from_map}`. [#371][net#371] - Rename `ResourceDef::{is_prefix_match => find_match}`. [#373][net#373] - Rename `ResourceDef::{match_path => capture_match_info}`. [#373][net#373] - Rename `ResourceDef::{match_path_checked => capture_match_info_fn}`. [#373][net#373] - Remove `ResourceDef::name_mut` and introduce `ResourceDef::set_name`. [#373][net#373] - Rename `Router::{*_checked => *_fn}`. [#373][net#373] - Return type of `ResourceDef::name` is now `Option<&str>`. [#373][net#373] - Return type of `ResourceDef::pattern` is now `Option<&str>`. [#373][net#373] [net#368]: https://github.com/actix/actix-net/pull/368 [net#366]: https://github.com/actix/actix-net/pull/366 [net#368]: https://github.com/actix/actix-net/pull/368 [net#370]: https://github.com/actix/actix-net/pull/370 [net#371]: https://github.com/actix/actix-net/pull/371 [net#372]: https://github.com/actix/actix-net/pull/372 [net#373]: https://github.com/actix/actix-net/pull/373
## 0.4.0 - When matching path parameters, `%25` is now kept in the percent-encoded form; no longer decoded to `%`. [#357][net#357] - Path tail patterns now match new lines (`\n`) in request URL. [#360][net#360] - Fixed a safety bug where `Path` could return a malformed string after percent decoding. [#359][net#359] - Methods `Path::{add, add_static}` now take `impl Into>`. [#345][net#345] [net#345]: https://github.com/actix/actix-net/pull/345 [net#357]: https://github.com/actix/actix-net/pull/357 [net#359]: https://github.com/actix/actix-net/pull/359 [net#360]: https://github.com/actix/actix-net/pull/360 ## 0.3.0 - Version was yanked previously. See https://crates.io/crates/actix-router/0.3.0 ## 0.2.7 - Add `Router::recognize_checked` [#247][net#247] [net#247]: https://github.com/actix/actix-net/pull/247 ## 0.2.6 - Use `bytestring` version range compatible with Bytes v1.0. [#246][net#246] [net#246]: https://github.com/actix/actix-net/pull/246 ## 0.2.5 - Fix `from_hex()` method ## 0.2.4 - Add `ResourceDef::resource_path_named()` path generation method ## 0.2.3 - Add impl `IntoPattern` for `&String` ## 0.2.2 - Use `IntoPattern` for `RouterBuilder::path()` ## 0.2.1 - Add `IntoPattern` trait - Add multi-pattern resources ## 0.2.0 - Update http to 0.2 - Update regex to 1.3 - Use bytestring instead of string ## 0.1.5 - Remove debug prints ## 0.1.4 - Fix checked resource match ## 0.1.3 - Added support for `remainder match` (i.e "/path/{tail}\*") ## 0.1.2 - Export `Quoter` type - Allow to reset `Path` instance ## 0.1.1 - Get dynamic segment by name instead of iterator. ## 0.1.0 - Initial release actix-router-0.5.3/Cargo.toml0000644000000033200000000000100114630ustar # THIS FILE IS AUTOMATICALLY GENERATED BY CARGO # # When uploading crates to the registry Cargo will automatically # "normalize" Cargo.toml files for maximal compatibility # with all versions of Cargo and also rewrite `path` dependencies # to registry (e.g., crates.io) dependencies. # # If you are reading this file be aware that the original Cargo.toml # will likely look very different (and much more reasonable). # See Cargo.toml.orig for the original contents. [package] edition = "2021" name = "actix-router" version = "0.5.3" authors = [ "Nikolay Kim ", "Ali MJ Al-Nasrawy ", "Rob Ede ", ] description = "Resource path matching and router" readme = "README.md" keywords = [ "actix", "router", "routing", ] license = "MIT OR Apache-2.0" repository = "https://github.com/actix/actix-web" [lib] name = "actix_router" path = "src/lib.rs" [[bench]] name = "router" harness = false required-features = ["unicode"] [[bench]] name = "quoter" harness = false [dependencies.bytestring] version = ">=0.1.5, <2" [dependencies.cfg-if] version = "1" [dependencies.http] version = "0.2.7" optional = true [dependencies.regex] version = "1.5" optional = true [dependencies.regex-lite] version = "0.1" [dependencies.serde] version = "1" [dependencies.tracing] version = "0.1.30" features = ["log"] default-features = false [dev-dependencies.criterion] version = "0.5" features = ["html_reports"] [dev-dependencies.http] version = "0.2.7" [dev-dependencies.percent-encoding] version = "2.1" [dev-dependencies.serde] version = "1" features = ["derive"] [features] default = [ "http", "unicode", ] http = ["dep:http"] unicode = ["dep:regex"] actix-router-0.5.3/Cargo.toml.orig000064400000000000000000000020461046102023000151500ustar 00000000000000[package] name = "actix-router" version = "0.5.3" 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-web" license = "MIT OR Apache-2.0" edition = "2021" [lib] name = "actix_router" path = "src/lib.rs" [features] default = ["http", "unicode"] http = ["dep:http"] unicode = ["dep:regex"] [dependencies] bytestring = ">=0.1.5, <2" cfg-if = "1" http = { version = "0.2.7", optional = true } regex = { version = "1.5", optional = true } regex-lite = "0.1" serde = "1" tracing = { version = "0.1.30", default-features = false, features = ["log"] } [dev-dependencies] criterion = { version = "0.5", features = ["html_reports"] } http = "0.2.7" serde = { version = "1", features = ["derive"] } percent-encoding = "2.1" [[bench]] name = "router" harness = false required-features = ["unicode"] [[bench]] name = "quoter" harness = false actix-router-0.5.3/LICENSE-APACHE000064400000000000000000000261201046102023000142040ustar 00000000000000 Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "{}" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright 2017-NOW Actix Team Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. actix-router-0.5.3/LICENSE-MIT000064400000000000000000000020421046102023000137110ustar 00000000000000Copyright (c) 2017-NOW Actix Team Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. actix-router-0.5.3/README.md000064400000000000000000000015711046102023000135420ustar 00000000000000# `actix-router` [![crates.io](https://img.shields.io/crates/v/actix-router?label=latest)](https://crates.io/crates/actix-router) [![Documentation](https://docs.rs/actix-router/badge.svg?version=0.5.3)](https://docs.rs/actix-router/0.5.3) ![Version](https://img.shields.io/badge/rustc-1.72+-ab6000.svg) ![MIT or Apache 2.0 licensed](https://img.shields.io/crates/l/actix-router.svg)
[![dependency status](https://deps.rs/crate/actix-router/0.5.3/status.svg)](https://deps.rs/crate/actix-router/0.5.3) [![Download](https://img.shields.io/crates/d/actix-router.svg)](https://crates.io/crates/actix-router) [![Chat on Discord](https://img.shields.io/discord/771444961383153695?label=chat&logo=discord)](https://discord.gg/NWpN5mmg3x) Resource path matching and router. actix-router-0.5.3/benches/quoter.rs000064400000000000000000000030221046102023000155500ustar 00000000000000use std::{borrow::Cow, fmt::Write as _}; use criterion::{black_box, criterion_group, criterion_main, Criterion}; fn compare_quoters(c: &mut Criterion) { let mut group = c.benchmark_group("Compare Quoters"); let quoter = actix_router::Quoter::new(b"", b""); let path_quoted = (0..=0x7f).fold(String::new(), |mut buf, c| { write!(&mut buf, "%{:02X}", c).unwrap(); buf }); let path_unquoted = ('\u{00}'..='\u{7f}').collect::(); group.bench_function("quoter_unquoted", |b| { b.iter(|| { for _ in 0..10 { black_box(quoter.requote(path_unquoted.as_bytes())); } }); }); group.bench_function("percent_encode_unquoted", |b| { b.iter(|| { for _ in 0..10 { let decode = percent_encoding::percent_decode(path_unquoted.as_bytes()); black_box(Into::>::into(decode)); } }); }); group.bench_function("quoter_quoted", |b| { b.iter(|| { for _ in 0..10 { black_box(quoter.requote(path_quoted.as_bytes())); } }); }); group.bench_function("percent_encode_quoted", |b| { b.iter(|| { for _ in 0..10 { let decode = percent_encoding::percent_decode(path_quoted.as_bytes()); black_box(Into::>::into(decode)); } }); }); group.finish(); } criterion_group!(benches, compare_quoters); criterion_main!(benches); actix-router-0.5.3/benches/router.rs000064400000000000000000000212111046102023000155510ustar 00000000000000//! 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), ]; IntoIterator::into_iter(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", ]; IntoIterator::into_iter(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); actix-router-0.5.3/src/de.rs000064400000000000000000000561211046102023000140110ustar 00000000000000use std::borrow::Cow; use serde::{ de::{self, Deserializer, Error as DeError, Visitor}, forward_to_deserialize_any, }; use crate::{ path::{Path, PathIter}, Quoter, ResourcePath, }; thread_local! { static FULL_QUOTER: Quoter = Quoter::new(b"", b""); } macro_rules! unsupported_type { ($trait_fn:ident, $name:expr) => { fn $trait_fn(self, _: V) -> Result where V: Visitor<'de>, { Err(de::Error::custom(concat!("unsupported type: ", $name))) } }; } macro_rules! parse_single_value { ($trait_fn:ident) => { 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 { Value { value: &self.path[0], } .$trait_fn(visitor) } } }; } macro_rules! parse_value { ($trait_fn:ident, $visit_fn:ident, $tp:tt) => { fn $trait_fn(self, visitor: V) -> Result where V: Visitor<'de>, { let decoded = FULL_QUOTER .with(|q| q.requote_str_lossy(self.value)) .map(Cow::Owned) .unwrap_or(Cow::Borrowed(self.value)); let v = decoded.parse().map_err(|_| { de::value::Error::custom(format!("can not parse {:?} to a {}", self.value, $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_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_option, "Option"); unsupported_type!(deserialize_identifier, "identifier"); unsupported_type!(deserialize_ignored_any, "ignored_any"); parse_single_value!(deserialize_bool); parse_single_value!(deserialize_i8); parse_single_value!(deserialize_i16); parse_single_value!(deserialize_i32); parse_single_value!(deserialize_i64); parse_single_value!(deserialize_u8); parse_single_value!(deserialize_u16); parse_single_value!(deserialize_u32); parse_single_value!(deserialize_u64); parse_single_value!(deserialize_f32); parse_single_value!(deserialize_f64); parse_single_value!(deserialize_str); parse_single_value!(deserialize_string); parse_single_value!(deserialize_bytes); parse_single_value!(deserialize_byte_buf); parse_single_value!(deserialize_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 } } 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, "i32"); 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_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_str(self, visitor: V) -> Result where V: Visitor<'de>, { match FULL_QUOTER.with(|q| q.requote_str_lossy(self.value)) { Some(s) => visitor.visit_string(s), None => visitor.visit_borrowed_str(self.value), } } fn deserialize_bytes(self, visitor: V) -> Result where V: Visitor<'de>, { match FULL_QUOTER.with(|q| q.requote_str_lossy(self.value)) { Some(s) => visitor.visit_byte_buf(s.into()), None => visitor.visit_borrowed_bytes(self.value.as_bytes()), } } fn deserialize_byte_buf(self, visitor: V) -> Result where V: Visitor<'de>, { self.deserialize_bytes(visitor) } fn deserialize_string(self, visitor: V) -> Result where V: Visitor<'de>, { self.deserialize_str(visitor) } 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::Deserialize; use super::*; use crate::{router::Router, ResourceDef}; #[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 deserialize_path_decode_string() { let rdef = ResourceDef::new("/{key}"); let mut path = Path::new("/%25"); rdef.capture_match_info(&mut path); let de = PathDeserializer::new(&path); let segment: String = serde::Deserialize::deserialize(de).unwrap(); assert_eq!(segment, "%"); let mut path = Path::new("/%2F"); rdef.capture_match_info(&mut path); let de = PathDeserializer::new(&path); let segment: String = serde::Deserialize::deserialize(de).unwrap(); assert_eq!(segment, "/") } #[test] fn deserialize_path_decode_seq() { let rdef = ResourceDef::new("/{key}/{value}"); let mut path = Path::new("/%30%25/%30%2F"); rdef.capture_match_info(&mut path); let de = PathDeserializer::new(&path); let segment: (String, String) = serde::Deserialize::deserialize(de).unwrap(); assert_eq!(segment.0, "0%"); assert_eq!(segment.1, "0/"); } #[test] fn deserialize_path_decode_map() { #[derive(Deserialize)] struct Vals { key: String, value: String, } let rdef = ResourceDef::new("/{key}/{value}"); let mut path = Path::new("/%25/%2F"); rdef.capture_match_info(&mut path); let de = PathDeserializer::new(&path); let vals: Vals = serde::Deserialize::deserialize(de).unwrap(); assert_eq!(vals.key, "%"); assert_eq!(vals.value, "/"); } #[test] fn deserialize_borrowed() { #[derive(Debug, Deserialize)] struct Params<'a> { val: &'a str, } let rdef = ResourceDef::new("/{val}"); let mut path = Path::new("/X"); rdef.capture_match_info(&mut path); let de = PathDeserializer::new(&path); let params: Params<'_> = serde::Deserialize::deserialize(de).unwrap(); assert_eq!(params.val, "X"); let de = PathDeserializer::new(&path); let params: &str = serde::Deserialize::deserialize(de).unwrap(); assert_eq!(params, "X"); let mut path = Path::new("/%2F"); rdef.capture_match_info(&mut path); let de = PathDeserializer::new(&path); assert!( as serde::Deserialize>::deserialize(de).is_err()); let de = PathDeserializer::new(&path); assert!(<&str as serde::Deserialize>::deserialize(de).is_err()); } // #[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" // ); // } } actix-router-0.5.3/src/lib.rs000064400000000000000000000013021046102023000141560ustar 00000000000000//! Resource path matching and router. #![deny(rust_2018_idioms, nonstandard_style)] #![warn(future_incompatible)] #![doc(html_logo_url = "https://actix.rs/img/logo.png")] #![doc(html_favicon_url = "https://actix.rs/favicon.ico")] #![cfg_attr(docsrs, feature(doc_auto_cfg))] mod de; mod path; mod pattern; mod quoter; mod regex_set; mod resource; mod resource_path; mod router; #[cfg(feature = "http")] mod url; #[cfg(feature = "http")] pub use self::url::Url; pub use self::{ de::PathDeserializer, path::Path, pattern::{IntoPatterns, Patterns}, quoter::Quoter, resource::ResourceDef, resource_path::{Resource, ResourcePath}, router::{ResourceId, Router, RouterBuilder}, }; actix-router-0.5.3/src/path.rs000064400000000000000000000145761046102023000143650ustar 00000000000000use std::{ borrow::Cow, ops::{DerefMut, Index}, }; use serde::{de, Deserialize}; 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 { /// Full path representation. path: T, /// Number of characters in `path` that have been processed into `segments`. pub(crate) skip: u16, /// List of processed dynamic segments; name->value pairs. pub(crate) segments: Vec<(Cow<'static, str>, PathItem)>, } impl Path { pub fn new(path: T) -> Path { Path { path, skip: 0, segments: Vec::new(), } } /// Returns reference to inner path instance. #[inline] pub fn get_ref(&self) -> &T { &self.path } /// Returns mutable reference to inner path instance. #[inline] pub fn get_mut(&mut self) -> &mut T { &mut self.path } /// Returns full path as a string. #[inline] pub fn as_str(&self) -> &str { self.path.path() } /// Returns unprocessed part of the path. /// /// Returns empty string if no more is to be processed. #[inline] pub fn unprocessed(&self) -> &str { // clamp skip to path length let skip = (self.skip as usize).min(self.as_str().len()); &self.path.path()[skip..] } /// Returns unprocessed part of the path. #[doc(hidden)] #[deprecated(since = "0.6.0", note = "Use `.as_str()` or `.unprocessed()`.")] #[inline] pub fn path(&self) -> &str { 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.path = path; self.skip = 0; 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) { match value { PathItem::Static(seg) => self.segments.push((name.into(), PathItem::Static(seg))), 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> { 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 } /// Returns matched parameter by name. /// /// If keyed parameter is not available empty string is used as default value. pub fn query(&self, key: &str) -> &str { self.get(key).unwrap_or_default() } /// Return iterator to items in parameter container. pub fn iter(&self) -> PathIter<'_, T> { PathIter { idx: 0, params: self, } } /// Deserializes matching parameters to a specified type `U`. /// /// # Errors /// /// Returns error when dynamic path segments cannot be deserialized into a `U` type. pub fn load<'de, U: Deserialize<'de>>(&'de self) -> Result { 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 { type Path = T; fn resource_path(&mut self) -> &mut Path { self } } impl Resource for T where T: DerefMut>, P: ResourcePath, { type Path = P; fn resource_path(&mut self) -> &mut Path { &mut *self } } #[cfg(test)] mod tests { use std::cell::RefCell; use super::*; #[allow(clippy::needless_borrow)] #[test] fn deref_impls() { let mut foo = Path::new("/foo"); let _ = (&mut foo).resource_path(); let foo = RefCell::new(foo); let _ = foo.borrow_mut().resource_path(); } } actix-router-0.5.3/src/pattern.rs000064400000000000000000000043151046102023000150740ustar 00000000000000/// 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 patterns. pub trait IntoPatterns { fn patterns(&self) -> Patterns; } impl IntoPatterns for String { fn patterns(&self) -> Patterns { Patterns::Single(self.clone()) } } impl IntoPatterns for &String { fn patterns(&self) -> Patterns { (*self).patterns() } } impl IntoPatterns for str { fn patterns(&self) -> Patterns { Patterns::Single(self.to_owned()) } } impl IntoPatterns for &str { fn patterns(&self) -> Patterns { (*self).patterns() } } 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 space-separated $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); actix-router-0.5.3/src/quoter.rs000064400000000000000000000125621046102023000147410ustar 00000000000000/// Partial percent-decoding. /// /// Performs percent-decoding on a slice but can selectively skip decoding certain sequences. /// /// # Examples /// ``` /// # use actix_router::Quoter; /// // + is set as a protected character and will not be decoded... /// let q = Quoter::new(&[], b"+"); /// /// // ...but the other encoded characters (like the hyphen below) will. /// assert_eq!(q.requote(b"/a%2Db%2Bc").unwrap(), b"/a-b%2Bc"); /// ``` pub struct Quoter { /// Simple bit-map of protected values in the 0-127 ASCII range. protected_table: AsciiBitmap, } impl Quoter { /// Constructs a new `Quoter` instance given a set of protected ASCII bytes. /// /// The first argument is ignored but is kept for backward compatibility. /// /// # Panics /// Panics if any of the `protected` bytes are not in the 0-127 ASCII range. pub fn new(_: &[u8], protected: &[u8]) -> Quoter { let mut protected_table = AsciiBitmap::default(); // prepare protected table for &ch in protected { protected_table.set_bit(ch); } Quoter { protected_table } } /// Decodes the next escape sequence, if any, and advances `val`. #[inline(always)] fn decode_next<'a>(&self, val: &mut &'a [u8]) -> Option<(&'a [u8], u8)> { for i in 0..val.len() { if let (prev, [b'%', p1, p2, rem @ ..]) = val.split_at(i) { if let Some(ch) = hex_pair_to_char(*p1, *p2) // ignore protected ascii bytes .filter(|&ch| !(ch < 128 && self.protected_table.bit_at(ch))) { *val = rem; return Some((prev, ch)); } } } None } /// Partially percent-decodes the given bytes. /// /// Escape sequences of the protected set are *not* decoded. /// /// Returns `None` when no modification to the original bytes was required. /// /// Invalid/incomplete percent-encoding sequences are passed unmodified. pub fn requote(&self, val: &[u8]) -> Option> { let mut remaining = val; // early return indicates that no percent-encoded sequences exist and we can skip allocation let (pre, decoded_char) = self.decode_next(&mut remaining)?; // decoded output will always be shorter than the input let mut decoded = Vec::::with_capacity(val.len()); // push first segment and decoded char decoded.extend_from_slice(pre); decoded.push(decoded_char); // decode and push rest of segments and decoded chars while let Some((prev, ch)) = self.decode_next(&mut remaining) { // this ugly conditional achieves +50% perf in cases where this is a tight loop. if !prev.is_empty() { decoded.extend_from_slice(prev); } decoded.push(ch); } decoded.extend_from_slice(remaining); Some(decoded) } pub(crate) fn requote_str_lossy(&self, val: &str) -> Option { self.requote(val.as_bytes()) .map(|data| String::from_utf8_lossy(&data).into_owned()) } } /// Decode a ASCII hex-encoded pair to an integer. /// /// Returns `None` if either portion of the decoded pair does not evaluate to a valid hex value. /// /// - `0x33 ('3'), 0x30 ('0') => 0x30 ('0')` /// - `0x34 ('4'), 0x31 ('1') => 0x41 ('A')` /// - `0x36 ('6'), 0x31 ('1') => 0x61 ('a')` #[inline(always)] fn hex_pair_to_char(d1: u8, d2: u8) -> Option { let d_high = char::from(d1).to_digit(16)?; let d_low = char::from(d2).to_digit(16)?; // left shift high nibble by 4 bits Some((d_high as u8) << 4 | (d_low as u8)) } #[derive(Debug, Default, Clone)] struct AsciiBitmap { array: [u8; 16], } impl AsciiBitmap { /// Sets bit in given bit-map to 1=true. /// /// # Panics /// Panics if `ch` index is out of bounds. fn set_bit(&mut self, ch: u8) { self.array[(ch >> 3) as usize] |= 0b1 << (ch & 0b111) } /// Returns true if bit to true in given bit-map. /// /// # Panics /// Panics if `ch` index is out of bounds. fn bit_at(&self, ch: u8) -> bool { self.array[(ch >> 3) as usize] & (0b1 << (ch & 0b111)) != 0 } } #[cfg(test)] mod tests { use super::*; #[test] fn custom_quoter() { let q = Quoter::new(b"", b"+"); assert_eq!(q.requote(b"/a%25c").unwrap(), b"/a%c"); assert_eq!(q.requote(b"/a%2Bc"), None); let q = Quoter::new(b"%+", b"/"); assert_eq!(q.requote(b"/a%25b%2Bc").unwrap(), b"/a%b+c"); assert_eq!(q.requote(b"/a%2fb"), None); assert_eq!(q.requote(b"/a%2Fb"), None); assert_eq!(q.requote(b"/a%0Ab").unwrap(), b"/a\nb"); assert_eq!(q.requote(b"/a%FE\xffb").unwrap(), b"/a\xfe\xffb"); assert_eq!(q.requote(b"/a\xfe\xffb"), None); } #[test] fn non_ascii() { let q = Quoter::new(b"%+", b"/"); assert_eq!(q.requote(b"/a%FE\xffb").unwrap(), b"/a\xfe\xffb"); assert_eq!(q.requote(b"/a\xfe\xffb"), None); } #[test] fn invalid_sequences() { let q = Quoter::new(b"%+", b"/"); assert_eq!(q.requote(b"/a%2x%2X%%"), None); assert_eq!(q.requote(b"/a%20%2X%%").unwrap(), b"/a %2X%%"); } #[test] fn quoter_no_modification() { let q = Quoter::new(b"", b""); assert_eq!(q.requote(b"/abc/../efg"), None); } } actix-router-0.5.3/src/regex_set.rs000064400000000000000000000035451046102023000154100ustar 00000000000000//! Abstraction over `regex` and `regex-lite` depending on whether we have `unicode` crate feature //! enabled. use cfg_if::cfg_if; #[cfg(feature = "unicode")] pub(crate) use regex::{escape, Regex}; #[cfg(not(feature = "unicode"))] pub(crate) use regex_lite::{escape, Regex}; #[cfg(feature = "unicode")] #[derive(Debug, Clone)] pub(crate) struct RegexSet(regex::RegexSet); #[cfg(not(feature = "unicode"))] #[derive(Debug, Clone)] pub(crate) struct RegexSet(Vec); impl RegexSet { /// Create a new regex set. /// /// # Panics /// /// Panics if any path patterns are malformed. pub(crate) fn new(re_set: Vec) -> Self { cfg_if! { if #[cfg(feature = "unicode")] { Self(regex::RegexSet::new(re_set).unwrap()) } else { Self(re_set.iter().map(|re| Regex::new(re).unwrap()).collect()) } } } /// Create a new empty regex set. pub(crate) fn empty() -> Self { cfg_if! { if #[cfg(feature = "unicode")] { Self(regex::RegexSet::empty()) } else { Self(Vec::new()) } } } /// Returns true if regex set matches `path`. pub(crate) fn is_match(&self, path: &str) -> bool { cfg_if! { if #[cfg(feature = "unicode")] { self.0.is_match(path) } else { self.0.iter().any(|re| re.is_match(path)) } } } /// Returns index within `path` of first match. pub(crate) fn first_match_idx(&self, path: &str) -> Option { cfg_if! { if #[cfg(feature = "unicode")] { self.0.matches(path).into_iter().next() } else { Some(self.0.iter().enumerate().find(|(_, re)| re.is_match(path))?.0) } } } } actix-router-0.5.3/src/resource.rs000064400000000000000000001734071046102023000152570ustar 00000000000000use std::{ borrow::{Borrow, Cow}, collections::HashMap, hash::{BuildHasher, Hash, Hasher}, mem, }; use tracing::error; use crate::{ path::PathItem, regex_set::{escape, Regex, RegexSet}, 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. /// /// /// # Pattern Format and Matching Behavior /// Resource pattern is defined as a string of zero or more _segments_ where each segment is /// preceded by a slash `/`. /// /// This means that pattern string __must__ either be empty or begin with a slash (`/`). This also /// implies that a trailing slash in pattern defines an empty segment. For example, the pattern /// `"/user/"` has two segments: `["user", ""]` /// /// A key point to understand is that `ResourceDef` matches segments, not strings. Segments are /// matched individually. For example, the pattern `/user/` is not considered a prefix for the path /// `/user/123/456`, because the second segment doesn't match: `["user", ""]` /// vs `["user", "123", "456"]`. /// /// This definition is consistent with the definition of absolute URL path in /// [RFC 3986 §3.3](https://datatracker.ietf.org/doc/html/rfc3986#section-3.3) /// /// /// # Static Resources /// A static resource is the most basic type of definition. Pass a pattern to [new][Self::new]. /// Conforming paths must match the pattern exactly. /// /// ## Examples /// ``` /// # use actix_router::ResourceDef; /// let resource = ResourceDef::new("/home"); /// /// assert!(resource.is_match("/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, up to a /// segment boundary. /// /// Prefix patterns with a trailing slash may have an unexpected, though correct, behavior. /// They define and therefore require an empty segment in order to match. It is easier to understand /// this behavior after reading the [matching behavior section]. Examples are given below. /// /// The empty pattern (`""`), as a prefix, matches any path. /// /// 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")); /// /// // prefix pattern with a trailing slash /// 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/stars")); /// assert!(!resource.is_match("/user/123")); /// ``` /// /// # 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. /// /// The regex could potentially match multiple segments. If this is not wanted, then care must be /// taken to avoid matching a slash `/`. It is guaranteed, however, that the match ends at a /// segment boundary; the pattern `r"(/|$)` is always appended to the regex. /// /// 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 /// ``` /// # 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 /// ``` /// # 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 /// you wish to accommodate "recoverable" path errors. Below are several examples of resource-path /// pairs that would not be compatible. /// /// ## Examples /// ``` /// # 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")); /// ``` /// /// [matching behavior section]: #pattern-format-and-matching-behavior #[derive(Clone, Debug)] pub struct ResourceDef { id: u16, /// Optional name of resource. name: Option, /// Pattern that generated the resource definition. patterns: Patterns, is_prefix: bool, /// Pattern type. pat_type: PatternType, /// List of segments that compose the pattern, in order. segments: Vec, } #[derive(Debug, Clone, PartialEq)] enum PatternSegment { /// Literal slice of pattern. Const(String), /// Name of dynamic segment. Var(String), } #[derive(Debug, Clone)] #[allow(clippy::large_enum_variant)] enum PatternType { /// Single constant/literal segment. Static(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 any path patterns are 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 { Self::construct(paths, false) } /// Constructs a new resource definition using a 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 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")); /// ``` pub fn prefix(paths: T) -> Self { ResourceDef::construct(paths, true) } /// Constructs a new resource definition using a string pattern that performs prefix matching, /// ensuring a leading `/` if pattern is not empty. /// /// # Panics /// Panics if path 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 { ResourceDef::prefix(insert_slash(path).into_owned()) } /// 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(); assert!(!name.is_empty(), "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 { self.is_prefix } /// Returns the pattern string that generated the resource definition. /// /// If definition is constructed with multiple patterns, the first pattern is returned. To get /// all patterns, use [`patterns_iter`][Self::pattern_iter]. If resource has 0 patterns, /// returns `None`. /// /// # 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_eq!(resource.pattern(), Some("/profile")); pub fn pattern(&self) -> Option<&str> { match &self.patterns { Patterns::Single(pattern) => Some(pattern.as_str()), Patterns::List(patterns) => patterns.first().map(AsRef::as_ref), } } /// 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)| { let mut pattern = String::with_capacity(this.len() + other.len()); pattern.push_str(this); pattern.push_str(other); pattern }) .collect::>(); match patterns.len() { 1 => ResourceDef::construct(&patterns[0], other.is_prefix()), _ => ResourceDef::construct(patterns, other.is_prefix()), } } /// 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 { // 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(pattern) => self.static_match(pattern, path).is_some(), PatternType::Dynamic(re, _) => re.is_match(path), PatternType::DynamicSet(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 { match &self.pat_type { PatternType::Static(pattern) => self.static_match(pattern, path), PatternType::Dynamic(re, _) => Some(re.captures(path)?[1].len()), PatternType::DynamicSet(re, params) => { let idx = re.first_match_idx(path)?; let (ref pattern, _) = params[idx]; Some(pattern.captures(path)?[1].len()) } } } /// Collects dynamic segment values into `resource`. /// /// 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, resource: &mut R) -> bool { self.capture_match_info_fn(resource, |_| 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").is_ok(); /// /// resource.capture_match_info_fn( /// path, /// // when env var is not set, reject when path contains "admin" /// |path| !(!admin_allowed && path.as_str().contains("admin")), /// ) /// } /// /// 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) -> bool where R: Resource, F: FnOnce(&R) -> bool, { let mut segments = <[PathItem; MAX_DYNAMIC_SEGMENTS]>::default(); let path = resource.resource_path(); let path_str = path.unprocessed(); let (matched_len, matched_vars) = match &self.pat_type { PatternType::Static(pattern) => match self.static_match(pattern, path_str) { Some(len) => (len, None), None => return false, }, PatternType::Dynamic(re, names) => { let captures = match re.captures(path.unprocessed()) { 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 { error!("Dynamic path match but not all segments found: {}", name); return false; } } (captures[1].len(), Some(names)) } PatternType::DynamicSet(re, params) => { let path = path.unprocessed(); let (pattern, names) = match re.first_match_idx(path) { 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 { error!("Dynamic path match but not all segments found: {}", name); return false; } } (captures[1].len(), Some(names)) } }; if !check_fn(resource) { 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 segment in &self.segments { match segment { PatternSegment::Const(val) => path.push_str(val), PatternSegment::Var(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. /// /// For multi-pattern resources, the first pattern is used under the assumption that it would be /// equivalent to any other choice. /// /// # 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, { 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. /// /// For multi-pattern resources, the first pattern is used under the assumption that it would be /// equivalent to any other choice. /// /// # 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, { self.build_resource_path(path, |name| values.get(name)) } /// Returns true if `prefix` acts as a proper prefix (i.e., separated by a slash) in `path`. fn static_match(&self, pattern: &str, path: &str) -> Option { let rem = path.strip_prefix(pattern)?; match self.is_prefix { // resource is not a prefix so an exact match is needed false if rem.is_empty() => Some(pattern.len()), // resource is a prefix so rem should start with a path delimiter true if rem.is_empty() || rem.starts_with('/') => Some(pattern.len()), // otherwise, no match _ => None, } } fn construct(paths: T, is_prefix: bool) -> Self { let patterns = paths.patterns(); let (pat_type, segments) = match &patterns { Patterns::Single(pattern) => ResourceDef::parse(pattern, is_prefix, false), // since zero length pattern sets are possible // just return a useless `ResourceDef` Patterns::List(patterns) if patterns.is_empty() => ( PatternType::DynamicSet(RegexSet::empty(), Vec::new()), Vec::new(), ), Patterns::List(patterns) => { let mut re_set = Vec::with_capacity(patterns.len()); let mut pattern_data = Vec::new(); let mut segments = None; for pattern in patterns { match ResourceDef::parse(pattern, is_prefix, true) { (PatternType::Dynamic(re, names), segs) => { re_set.push(re.as_str().to_owned()); pattern_data.push((re, names)); segments.get_or_insert(segs); } _ => unreachable!(), } } let pattern_re_set = RegexSet::new(re_set); let segments = segments.unwrap_or_default(); ( PatternType::DynamicSet(pattern_re_set, pattern_data), segments, ) } }; ResourceDef { id: 0, name: None, patterns, is_prefix, pat_type, 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) { 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#"pattern "{}" 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) => { assert!(!tail, "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) { if !force_dynamic && pattern.find('{').is_none() && !pattern.ends_with('*') { // pattern is static return ( PatternType::Static(pattern.to_owned()), vec![PatternSegment::Const(pattern.to_owned())], ); } let mut unprocessed = pattern; 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))] tracing::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))] tracing::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)); } assert!( dyn_segment_count <= MAX_DYNAMIC_SEGMENTS, "Only {} dynamic segments are allowed, provided: {}", MAX_DYNAMIC_SEGMENTS, dyn_segment_count ); // Store the pattern in capture group #1 to have context info outside it let mut re = format!("({})", re); // Ensure the match ends at a segment boundary if !has_tail_segment { if is_prefix { re.push_str(r"(/|$)"); } else { 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 && self.is_prefix == other.is_prefix } } 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> { 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) } } #[cfg(test)] mod tests { use super::*; use crate::Path; #[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 dynamic_set_prefix() { let re = ResourceDef::prefix(vec!["/u/{id}", "/{id:[[:digit:]]{3}}"]); assert_eq!(re.find_match("/u/abc"), Some(6)); assert_eq!(re.find_match("/u/abc/123"), Some(6)); assert_eq!(re.find_match("/s/user/profile"), None); assert_eq!(re.find_match("/123"), Some(4)); assert_eq!(re.find_match("/123/456"), Some(4)); assert_eq!(re.find_match("/12345"), None); let mut path = Path::new("/151/res"); assert!(re.capture_match_info(&mut path)); assert_eq!(path.get("id").unwrap(), "151"); assert_eq!(path.unprocessed(), "/res"); } #[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() { 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/gs")); assert!(!re.is_match("/name")); let mut path = Path::new("/name/gs"); assert!(!re.capture_match_info(&mut path)); 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/gs")); assert!(!re.is_match("/name")); let mut path = Path::new("/name/gs"); assert!(!re.capture_match_info(&mut path)); } #[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(5)); assert_eq!(re.find_match("/name/gs"), Some(5)); assert_eq!(re.find_match("/name"), Some(5)); assert_eq!(re.find_match(""), 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 prefix_empty() { let re = ResourceDef::prefix(""); assert!(re.is_prefix()); assert!(re.is_match("")); assert!(re.is_match("/")); assert!(re.is_match("/name/test/test")); } #[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, #[allow(clippy::useless_vec)] &mut vec!["item", "item2"].iter() )); assert_eq!(s, "/user/item/item2/"); } #[test] fn multi_pattern_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())); assert_eq!(s, "/user/123"); } #[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 prefix_trailing_slash() { // The prefix "/abc/" matches two segments: ["user", ""] // These are not prefixes let re = ResourceDef::prefix("/abc/"); assert_eq!(re.find_match("/abc/def"), None); assert_eq!(re.find_match("/abc//def"), Some(5)); let re = ResourceDef::prefix("/{id}/"); assert_eq!(re.find_match("/abc/def"), None); assert_eq!(re.find_match("/abc//def"), Some(5)); } #[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"); join_test!(["/ver/{v}", "/v{v}"], ["/req/{req}", "/{req}"] => "/v1/abc", "/ver/1/abc", "/v1/req/abc", "/ver/1/req/abc", "/v1/abc/def", "/ver1/req/abc/def", "", "/", "/v1/"); } #[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"); match_methods_agree!(["/v{v}", "/ver/{v}"] => "", "s/v", "/v1", "/v1/xx", "/ver/i3/5", "/ver/1"); } #[test] #[should_panic] fn duplicate_segment_name() { ResourceDef::new("/user/{id}/post/{id}"); } #[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_disallowed() { ResourceDef::prefix("/user/{id}*"); } } actix-router-0.5.3/src/resource_path.rs000064400000000000000000000013251046102023000162600ustar 00000000000000use crate::Path; // TODO: this trait is necessary, document it // see impl Resource for ServiceRequest pub trait Resource { /// Type of resource's path returned in `resource_path`. type Path: ResourcePath; 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 } } #[cfg(feature = "http")] impl ResourcePath for http::Uri { fn path(&self) -> &str { self.path() } } actix-router-0.5.3/src/router.rs000064400000000000000000000230761046102023000147440ustar 00000000000000use crate::{IntoPatterns, Resource, ResourceDef}; #[derive(Debug, Copy, Clone, PartialEq, Eq)] pub struct ResourceId(pub u16); /// Resource router. /// /// It matches a [routing resource](Resource) to an ordered list of _routes_. Each is defined by a /// single [`ResourceDef`] and contains two types of custom data: /// 1. The route _value_, of the generic type `T`. /// 1. Some _context_ data, of the generic type `U`, which is only provided to the check function in /// [`recognize_fn`](Self::recognize_fn). This parameter defaults to `()` and can be omitted if /// not required. pub struct Router { routes: Vec<(ResourceDef, T, U)>, } impl Router { /// Constructs new `RouterBuilder` with empty route list. pub fn build() -> RouterBuilder { RouterBuilder { routes: Vec::new() } } /// Finds the value in the router that matches a given [routing resource](Resource). /// /// The match result, including the captured dynamic segments, in the `resource`. pub fn recognize(&self, resource: &mut R) -> Option<(&T, ResourceId)> where R: Resource, { self.recognize_fn(resource, |_, _| true) } /// Same as [`recognize`](Self::recognize) but returns a mutable reference to the matched value. pub fn recognize_mut(&mut self, resource: &mut R) -> Option<(&mut T, ResourceId)> where R: Resource, { self.recognize_mut_fn(resource, |_, _| true) } /// Finds the value in the router that matches a given [routing resource](Resource) and passes /// an additional predicate check using context data. /// /// Similar to [`recognize`](Self::recognize). However, before accepting the route as matched, /// the `check` closure is executed, passing the resource and each route's context data. If the /// closure returns true then the match result is stored into `resource` and a reference to /// the matched _value_ is returned. pub fn recognize_fn(&self, resource: &mut R, mut check: F) -> Option<(&T, ResourceId)> where R: Resource, F: FnMut(&R, &U) -> bool, { for (rdef, val, ctx) in self.routes.iter() { if rdef.capture_match_info_fn(resource, |res| check(res, ctx)) { return Some((val, ResourceId(rdef.id()))); } } None } /// Same as [`recognize_fn`](Self::recognize_fn) but returns a mutable reference to the matched /// value. pub fn recognize_mut_fn( &mut self, resource: &mut R, mut check: F, ) -> Option<(&mut T, ResourceId)> where R: Resource, F: FnMut(&R, &U) -> bool, { for (rdef, val, ctx) in self.routes.iter_mut() { if rdef.capture_match_info_fn(resource, |res| check(res, ctx)) { return Some((val, ResourceId(rdef.id()))); } } None } } /// Builder for an ordered [routing](Router) list. pub struct RouterBuilder { routes: Vec<(ResourceDef, T, U)>, } impl RouterBuilder { /// Adds a new route to the end of the routing list. /// /// Returns mutable references to elements of the new route. pub fn push( &mut self, rdef: ResourceDef, val: T, ctx: U, ) -> (&mut ResourceDef, &mut T, &mut U) { self.routes.push((rdef, val, ctx)); #[allow(clippy::map_identity)] // map is used to distribute &mut-ness to tuple elements self.routes .last_mut() .map(|(rdef, val, ctx)| (rdef, val, ctx)) .unwrap() } /// Finish configuration and create router instance. pub fn finish(self) -> Router { Router { routes: self.routes, } } } /// Convenience methods provided when context data impls [`Default`] impl RouterBuilder where U: Default, { /// Registers resource for specified path. pub fn path(&mut self, path: impl IntoPatterns, val: T) -> (&mut ResourceDef, &mut T, &mut U) { self.push(ResourceDef::new(path), val, U::default()) } /// Registers resource for specified path prefix. pub fn prefix( &mut self, prefix: impl IntoPatterns, val: T, ) -> (&mut ResourceDef, &mut T, &mut U) { self.push(ResourceDef::prefix(prefix), val, U::default()) } /// Registers resource for [`ResourceDef`]. pub fn rdef(&mut self, rdef: ResourceDef, val: T) -> (&mut ResourceDef, &mut T, &mut U) { self.push(rdef, val, U::default()) } } #[cfg(test)] mod tests { use crate::{ path::Path, 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("/v2/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(), "2"); 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(); // test skip beyond path length 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"); } } actix-router-0.5.3/src/url.rs000064400000000000000000000076301046102023000142240ustar 00000000000000use crate::{Quoter, ResourcePath}; thread_local! { static DEFAULT_QUOTER: Quoter = Quoter::new(b"", b"%/+"); } #[derive(Debug, Clone, Default)] pub struct Url { uri: http::Uri, path: Option, } impl Url { #[inline] pub fn new(uri: http::Uri) -> Url { let path = DEFAULT_QUOTER.with(|q| q.requote_str_lossy(uri.path())); Url { uri, path } } #[inline] pub fn new_with_quoter(uri: http::Uri, quoter: &Quoter) -> Url { Url { path: quoter.requote_str_lossy(uri.path()), uri, } } /// Returns URI. #[inline] pub fn uri(&self) -> &http::Uri { &self.uri } /// Returns path. #[inline] pub fn path(&self) -> &str { match self.path { Some(ref path) => path, _ => self.uri.path(), } } #[inline] pub fn update(&mut self, uri: &http::Uri) { self.uri = uri.clone(); self.path = DEFAULT_QUOTER.with(|q| q.requote_str_lossy(uri.path())); } #[inline] pub fn update_with_quoter(&mut self, uri: &http::Uri, quoter: &Quoter) { self.uri = uri.clone(); self.path = quoter.requote_str_lossy(uri.path()); } } impl ResourcePath for Url { #[inline] fn path(&self) -> &str { self.path() } } #[cfg(test)] mod tests { use std::fmt::Write as _; use http::Uri; 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() .fold(String::with_capacity(data.len() * 3), |mut buf, c| { write!(&mut buf, "%{:02X}", c).unwrap(); buf }) } #[test] fn parse_url() { let re = "/user/{id}/test"; let path = match_url(re, "/user/2345/test"); assert_eq!(path.get("id").unwrap(), "2345"); } #[test] fn protected_chars() { let re = "/user/{id}/test"; let encoded = percent_encode(PROTECTED); let path = match_url(re, format!("/user/{}/test", encoded)); // characters in captured segment remain unencoded assert_eq!(path.get("id").unwrap(), &encoded); // "%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 non_protected_ascii() { let non_protected_ascii = ('\u{0}'..='\u{7F}') .filter(|&c| c.is_ascii() && !PROTECTED.contains(&(c as u8))) .collect::(); let encoded = percent_encode(non_protected_ascii.as_bytes()); let path = match_url("/user/{id}/test", format!("/user/{}/test", encoded)); assert_eq!(path.get("id").unwrap(), &non_protected_ascii); } #[test] fn valid_utf8_multi_byte() { 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 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.as_str().as_bytes().to_owned()).is_ok()); } }