├── .gitignore ├── .rustfmt.toml ├── fuzz ├── .gitignore ├── Cargo.toml └── fuzz_targets │ └── insert_and_match.rs ├── .github ├── DOCS.md └── workflows │ ├── test.yml │ ├── safety.yml │ └── check.yml ├── LICENSE ├── Cargo.toml ├── LICENSE.httprouter ├── tests ├── merge.rs ├── remove.rs ├── insert.rs └── match.rs ├── examples └── hyper.rs ├── src ├── error.rs ├── escape.rs ├── router.rs ├── lib.rs ├── params.rs └── tree.rs ├── README.md └── benches └── bench.rs /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | Cargo.lock 3 | -------------------------------------------------------------------------------- /.rustfmt.toml: -------------------------------------------------------------------------------- 1 | edition="2018" 2 | reorder_imports=true 3 | -------------------------------------------------------------------------------- /fuzz/.gitignore: -------------------------------------------------------------------------------- 1 | 2 | target 3 | corpus 4 | artifacts 5 | -------------------------------------------------------------------------------- /.github/DOCS.md: -------------------------------------------------------------------------------- 1 | Workflows adapted from https://github.com/jonhoo/rust-ci-conf. 2 | -------------------------------------------------------------------------------- /fuzz/Cargo.toml: -------------------------------------------------------------------------------- 1 | 2 | [package] 3 | name = "matchit-fuzz" 4 | version = "0.0.0" 5 | authors = ["Automatically generated"] 6 | publish = false 7 | edition = "2018" 8 | 9 | [package.metadata] 10 | cargo-fuzz = true 11 | 12 | [dependencies] 13 | libfuzzer-sys = "0.4" 14 | 15 | [dependencies.matchit] 16 | path = ".." 17 | 18 | # Prevent this from interfering with workspaces 19 | [workspace] 20 | members = ["."] 21 | 22 | [[bin]] 23 | name = "insert_and_match" 24 | path = "fuzz_targets/insert_and_match.rs" 25 | test = false 26 | doc = false 27 | -------------------------------------------------------------------------------- /fuzz/fuzz_targets/insert_and_match.rs: -------------------------------------------------------------------------------- 1 | #![no_main] 2 | use libfuzzer_sys::fuzz_target; 3 | 4 | fuzz_target!(|data: (Vec<(String, i32)>, String, Option)| { 5 | let mut matcher = matchit::Node::new(); 6 | 7 | for (key, item) in data.0 { 8 | if matcher.insert(key, item).is_err() { 9 | return; 10 | } 11 | } 12 | 13 | match data.2 { 14 | None => { 15 | let _ = matcher.at(&data.1); 16 | } 17 | Some(b) => { 18 | let _ = matcher.path_ignore_case(&data.1, b); 19 | } 20 | } 21 | }); 22 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 Ibraheem Ahmed 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "matchit" 3 | version = "0.9.0" 4 | license = "MIT AND BSD-3-Clause" 5 | authors = ["Ibraheem Ahmed "] 6 | edition = "2021" 7 | rust-version = "1.66" 8 | description = "A high performance, zero-copy URL router." 9 | categories = ["network-programming", "algorithms"] 10 | keywords = ["router", "path", "tree", "match", "url"] 11 | repository = "https://github.com/ibraheemdev/matchit" 12 | readme = "README.md" 13 | 14 | [dependencies] 15 | 16 | [dev-dependencies] 17 | # Benchmarks 18 | criterion = "0.5" 19 | actix-router = "0.5" 20 | regex = "1" 21 | route-recognizer = "0.3" 22 | gonzales = "0.0.3-beta" 23 | path-tree = "0.8" 24 | routefinder = "0.5" 25 | wayfind = "0.8" 26 | 27 | # Examples 28 | tower = { version = "0.5.2", features = ["make", "util"] } 29 | tokio = { version = "1", features = ["full"] } 30 | http-body-util = "0.1" 31 | hyper = { version = "1", features = ["http1", "server"] } 32 | hyper-util = { version = "0.1", features = ["tokio"] } 33 | 34 | [features] 35 | default = [] 36 | __test_helpers = [] 37 | 38 | [[bench]] 39 | name = "bench" 40 | harness = false 41 | 42 | [profile.release] 43 | lto = true 44 | opt-level = 3 45 | codegen-units = 1 46 | -------------------------------------------------------------------------------- /LICENSE.httprouter: -------------------------------------------------------------------------------- 1 | BSD 3-Clause License 2 | 3 | Copyright (c) 2013, Julien Schmidt 4 | All rights reserved. 5 | 6 | Redistribution and use in source and binary forms, with or without 7 | modification, are permitted provided that the following conditions are met: 8 | 9 | 1. Redistributions of source code must retain the above copyright notice, this 10 | list of conditions and the following disclaimer. 11 | 12 | 2. Redistributions in binary form must reproduce the above copyright notice, 13 | this list of conditions and the following disclaimer in the documentation 14 | and/or other materials provided with the distribution. 15 | 16 | 3. Neither the name of the copyright holder nor the names of its 17 | contributors may be used to endorse or promote products derived from 18 | this software without specific prior written permission. 19 | 20 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 21 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 22 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 23 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 24 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 25 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 26 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 27 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 28 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 29 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 30 | -------------------------------------------------------------------------------- /tests/merge.rs: -------------------------------------------------------------------------------- 1 | use matchit::{InsertError, Router}; 2 | 3 | #[test] 4 | fn merge_ok() { 5 | let mut root = Router::new(); 6 | assert!(root.insert("/foo", "foo").is_ok()); 7 | assert!(root.insert("/bar/{id}", "bar").is_ok()); 8 | 9 | let mut child = Router::new(); 10 | assert!(child.insert("/baz", "baz").is_ok()); 11 | assert!(child.insert("/xyz/{id}", "xyz").is_ok()); 12 | 13 | assert!(root.merge(child).is_ok()); 14 | 15 | assert_eq!(root.at("/foo").map(|m| *m.value), Ok("foo")); 16 | assert_eq!(root.at("/bar/1").map(|m| *m.value), Ok("bar")); 17 | assert_eq!(root.at("/baz").map(|m| *m.value), Ok("baz")); 18 | assert_eq!(root.at("/xyz/2").map(|m| *m.value), Ok("xyz")); 19 | } 20 | 21 | #[test] 22 | fn merge_conflict() { 23 | let mut root = Router::new(); 24 | assert!(root.insert("/foo", "foo").is_ok()); 25 | assert!(root.insert("/bar", "bar").is_ok()); 26 | 27 | let mut child = Router::new(); 28 | assert!(child.insert("/foo", "changed").is_ok()); 29 | assert!(child.insert("/bar", "changed").is_ok()); 30 | assert!(child.insert("/baz", "baz").is_ok()); 31 | 32 | let errors = root.merge(child).unwrap_err(); 33 | 34 | assert_eq!( 35 | errors.first(), 36 | Some(&InsertError::Conflict { 37 | with: "/foo".into() 38 | }) 39 | ); 40 | 41 | assert_eq!( 42 | errors.get(1), 43 | Some(&InsertError::Conflict { 44 | with: "/bar".into() 45 | }) 46 | ); 47 | 48 | assert_eq!(root.at("/foo").map(|m| *m.value), Ok("foo")); 49 | assert_eq!(root.at("/bar").map(|m| *m.value), Ok("bar")); 50 | assert_eq!(root.at("/baz").map(|m| *m.value), Ok("baz")); 51 | } 52 | 53 | #[test] 54 | fn merge_nested() { 55 | let mut root = Router::new(); 56 | assert!(root.insert("/foo", "foo").is_ok()); 57 | 58 | let mut child = Router::new(); 59 | assert!(child.insert("/foo/bar", "bar").is_ok()); 60 | 61 | assert!(root.merge(child).is_ok()); 62 | 63 | assert_eq!(root.at("/foo").map(|m| *m.value), Ok("foo")); 64 | assert_eq!(root.at("/foo/bar").map(|m| *m.value), Ok("bar")); 65 | } 66 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | # This is the main CI workflow that runs the test suite on all pushes to main and all pull requests. 2 | # It runs the following jobs: 3 | # - required: runs the test suite on ubuntu with stable and beta rust toolchains 4 | # requirements of this crate, and its dependencies 5 | # - os-check: runs the test suite on mac and windows 6 | # See check.yml for information about how the concurrency cancellation and workflow triggering works 7 | permissions: 8 | contents: read 9 | on: 10 | push: 11 | branches: [master] 12 | pull_request: 13 | concurrency: 14 | group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }} 15 | cancel-in-progress: true 16 | name: test 17 | jobs: 18 | required: 19 | runs-on: ubuntu-latest 20 | timeout-minutes: 15 21 | name: ubuntu / ${{ matrix.toolchain }} 22 | strategy: 23 | matrix: 24 | # run on stable and beta to ensure that tests won't break on the next version of the rust 25 | # toolchain 26 | toolchain: [stable, beta] 27 | steps: 28 | - uses: actions/checkout@v4 29 | with: 30 | submodules: true 31 | - name: Install ${{ matrix.toolchain }} 32 | uses: dtolnay/rust-toolchain@master 33 | with: 34 | toolchain: ${{ matrix.toolchain }} 35 | - name: cargo generate-lockfile 36 | # enable this ci template to run regardless of whether the lockfile is checked in or not 37 | if: hashFiles('Cargo.lock') == '' 38 | run: cargo generate-lockfile 39 | # https://twitter.com/jonhoo/status/1571290371124260865 40 | - name: cargo test --locked 41 | run: cargo test --locked --all-features --all-targets 42 | # https://github.com/rust-lang/cargo/issues/6669 43 | - name: cargo test --doc 44 | run: cargo test --locked --all-features --doc 45 | os-check: 46 | # run cargo test on mac and windows 47 | runs-on: ${{ matrix.os }} 48 | timeout-minutes: 15 49 | name: ${{ matrix.os }} / stable 50 | strategy: 51 | fail-fast: false 52 | matrix: 53 | os: [macos-latest, windows-latest] 54 | steps: 55 | - uses: actions/checkout@v4 56 | with: 57 | submodules: true 58 | - name: Install stable 59 | uses: dtolnay/rust-toolchain@stable 60 | - name: cargo generate-lockfile 61 | if: hashFiles('Cargo.lock') == '' 62 | run: cargo generate-lockfile 63 | - name: cargo test 64 | run: cargo test --locked --all-features --all-targets 65 | -------------------------------------------------------------------------------- /.github/workflows/safety.yml: -------------------------------------------------------------------------------- 1 | # Runs: 2 | # - miri - detects undefined behavior and memory leaks 3 | # - address sanitizer - detects memory errors 4 | # - leak sanitizer - detects memory leaks 5 | # See check.yml for information about how the concurrency cancellation and workflow triggering works 6 | permissions: 7 | contents: read 8 | on: 9 | push: 10 | branches: [master] 11 | pull_request: 12 | concurrency: 13 | group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }} 14 | cancel-in-progress: true 15 | name: safety 16 | jobs: 17 | sanitizers: 18 | runs-on: ubuntu-latest 19 | timeout-minutes: 15 20 | steps: 21 | - uses: actions/checkout@v4 22 | with: 23 | submodules: true 24 | - name: Install nightly 25 | uses: dtolnay/rust-toolchain@nightly 26 | - run: | 27 | # to get the symbolizer for debug symbol resolution 28 | sudo apt install llvm 29 | # to fix buggy leak analyzer: 30 | # https://github.com/japaric/rust-san#unrealiable-leaksanitizer 31 | # ensure there's a profile.dev section 32 | if ! grep -qE '^[ \t]*[profile.dev]' Cargo.toml; then 33 | echo >> Cargo.toml 34 | echo '[profile.dev]' >> Cargo.toml 35 | fi 36 | # remove pre-existing opt-levels in profile.dev 37 | sed -i '/^\s*\[profile.dev\]/,/^\s*\[/ {/^\s*opt-level/d}' Cargo.toml 38 | # now set opt-level to 1 39 | sed -i '/^\s*\[profile.dev\]/a opt-level = 1' Cargo.toml 40 | cat Cargo.toml 41 | name: Enable debug symbols 42 | - name: cargo test -Zsanitizer=address 43 | # only --lib --tests b/c of https://github.com/rust-lang/rust/issues/53945 44 | run: cargo test --lib --tests --all-features --target x86_64-unknown-linux-gnu 45 | env: 46 | ASAN_OPTIONS: "detect_odr_violation=0:detect_leaks=0" 47 | RUSTFLAGS: "-Z sanitizer=address" 48 | - name: cargo test -Zsanitizer=leak 49 | if: always() 50 | run: cargo test --all-features --target x86_64-unknown-linux-gnu 51 | env: 52 | LSAN_OPTIONS: "suppressions=lsan-suppressions.txt" 53 | RUSTFLAGS: "-Z sanitizer=leak" 54 | miri: 55 | runs-on: ubuntu-latest 56 | timeout-minutes: 15 57 | steps: 58 | - uses: actions/checkout@v4 59 | with: 60 | submodules: true 61 | - run: | 62 | echo "NIGHTLY=nightly-$(curl -s https://rust-lang.github.io/rustup-components-history/x86_64-unknown-linux-gnu/miri)" >> $GITHUB_ENV 63 | - name: Install ${{ env.NIGHTLY }} 64 | uses: dtolnay/rust-toolchain@master 65 | with: 66 | toolchain: ${{ env.NIGHTLY }} 67 | components: miri 68 | - name: cargo miri test 69 | run: cargo miri test --all-features 70 | -------------------------------------------------------------------------------- /examples/hyper.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashMap; 2 | use std::future; 3 | use std::sync::Arc; 4 | 5 | use http_body_util::Full; 6 | use hyper::body::{Bytes, Incoming}; 7 | use hyper::server::conn::http1::Builder as ConnectionBuilder; 8 | use hyper::{Method, Request, Response, StatusCode}; 9 | use hyper_util::rt::TokioIo; 10 | use tokio::net::TcpListener; 11 | use tower::service_fn; 12 | use tower::Service as _; 13 | 14 | type Body = Full; 15 | 16 | // GET / 17 | async fn index(_req: Request) -> hyper::Result> { 18 | Ok(Response::new(Body::from("Hello, world!"))) 19 | } 20 | 21 | // GET /blog 22 | async fn blog(_req: Request) -> hyper::Result> { 23 | Ok(Response::new(Body::from("..."))) 24 | } 25 | 26 | // 404 handler 27 | async fn not_found(_req: Request) -> hyper::Result> { 28 | Ok(Response::builder() 29 | .status(StatusCode::NOT_FOUND) 30 | .body(Body::default()) 31 | .unwrap()) 32 | } 33 | 34 | // We can use `BoxCloneSyncService` to erase the type of each handler service. 35 | type Service = tower::util::BoxCloneSyncService, Response, hyper::Error>; 36 | 37 | // We use a `HashMap` to hold a `Router` for each HTTP method. This allows us 38 | // to register the same route for multiple methods. 39 | type Router = HashMap>; 40 | 41 | async fn route(router: Arc, req: Request) -> hyper::Result> { 42 | // find the subrouter for this request method 43 | let Some(router) = router.get(req.method()) else { 44 | // if there are no routes for this method, respond with 405 Method Not Allowed 45 | return Ok(Response::builder() 46 | .status(StatusCode::METHOD_NOT_ALLOWED) 47 | .body(Body::default()) 48 | .unwrap()); 49 | }; 50 | 51 | // find the service for this request path 52 | let Ok(found) = router.at(req.uri().path()) else { 53 | // if we there is no matching service, call the 404 handler 54 | return not_found(req).await; 55 | }; 56 | 57 | let mut service = found.value.clone(); 58 | 59 | future::poll_fn(|cx| service.poll_ready(cx)).await?; 60 | 61 | service.call(req).await 62 | } 63 | 64 | #[tokio::main] 65 | async fn main() { 66 | // Create a router and register our routes. 67 | let mut router = Router::new(); 68 | 69 | // GET / => `index` 70 | router 71 | .entry(Method::GET) 72 | .or_default() 73 | .insert("/", Service::new(service_fn(index))) 74 | .unwrap(); 75 | 76 | // GET /blog => `blog` 77 | router 78 | .entry(Method::GET) 79 | .or_default() 80 | .insert("/blog", Service::new(service_fn(blog))) 81 | .unwrap(); 82 | 83 | let listener = TcpListener::bind(("127.0.0.1", 3000)).await.unwrap(); 84 | 85 | // boilerplate for the hyper service 86 | let router = Arc::new(router); 87 | 88 | loop { 89 | let router = router.clone(); 90 | let (tcp, _) = listener.accept().await.unwrap(); 91 | tokio::task::spawn(async move { 92 | if let Err(err) = ConnectionBuilder::new() 93 | .serve_connection( 94 | TokioIo::new(tcp), 95 | hyper::service::service_fn(|request| route(router.clone(), request)), 96 | ) 97 | .await 98 | { 99 | println!("Error serving connection: {err:?}"); 100 | } 101 | }); 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /.github/workflows/check.yml: -------------------------------------------------------------------------------- 1 | # This workflow runs whenever a PR is opened or updated, or a commit is pushed to main. It runs 2 | # several checks: 3 | # - fmt: checks that the code is formatted according to rustfmt 4 | # - clippy: checks that the code does not contain any clippy warnings 5 | # - doc: checks that the code can be documented without errors 6 | # - hack: check combinations of feature flags 7 | # - msrv: check that the msrv specified in the crate is correct 8 | permissions: 9 | contents: read 10 | # This configuration allows maintainers of this repo to create a branch and pull request based on 11 | # the new branch. Restricting the push trigger to the main branch ensures that the PR only gets 12 | # built once. 13 | on: 14 | push: 15 | branches: [master] 16 | pull_request: 17 | # If new code is pushed to a PR branch, then cancel in progress workflows for that PR. Ensures that 18 | # we don't waste CI time, and returns results quicker https://github.com/jonhoo/rust-ci-conf/pull/5 19 | concurrency: 20 | group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }} 21 | cancel-in-progress: true 22 | name: check 23 | jobs: 24 | fmt: 25 | runs-on: ubuntu-latest 26 | name: stable / fmt 27 | steps: 28 | - uses: actions/checkout@v4 29 | with: 30 | submodules: true 31 | - name: Install stable 32 | uses: dtolnay/rust-toolchain@stable 33 | with: 34 | components: rustfmt 35 | - name: cargo fmt --check 36 | run: cargo fmt --check 37 | clippy: 38 | runs-on: ubuntu-latest 39 | name: ${{ matrix.toolchain }} / clippy 40 | permissions: 41 | contents: read 42 | checks: write 43 | strategy: 44 | fail-fast: false 45 | matrix: 46 | # Get early warning of new lints which are regularly introduced in beta channels. 47 | toolchain: [stable, beta] 48 | steps: 49 | - uses: actions/checkout@v4 50 | with: 51 | submodules: true 52 | - name: Install ${{ matrix.toolchain }} 53 | uses: dtolnay/rust-toolchain@master 54 | with: 55 | toolchain: ${{ matrix.toolchain }} 56 | components: clippy 57 | - name: cargo clippy 58 | uses: giraffate/clippy-action@v1 59 | with: 60 | clippy_flags: --workspace --all-targets --all-features -- -Dclippy::all 61 | reporter: 'github-pr-check' 62 | github_token: ${{ secrets.GITHUB_TOKEN }} 63 | semver: 64 | runs-on: ubuntu-latest 65 | name: semver 66 | steps: 67 | - uses: actions/checkout@v4 68 | with: 69 | submodules: true 70 | - name: Install stable 71 | uses: dtolnay/rust-toolchain@stable 72 | with: 73 | components: rustfmt 74 | - name: cargo-semver-checks 75 | uses: obi1kenobi/cargo-semver-checks-action@v2 76 | doc: 77 | # run docs generation on nightly rather than stable. This enables features like 78 | # https://doc.rust-lang.org/beta/unstable-book/language-features/doc-cfg.html which allows an 79 | # API be documented as only available in some specific platforms. 80 | runs-on: ubuntu-latest 81 | name: nightly / doc 82 | steps: 83 | - uses: actions/checkout@v4 84 | with: 85 | submodules: true 86 | - name: Install nightly 87 | uses: dtolnay/rust-toolchain@nightly 88 | - name: cargo doc 89 | run: cargo doc --no-deps --all-features 90 | env: 91 | RUSTDOCFLAGS: --cfg docsrs 92 | hack: 93 | # cargo-hack checks combinations of feature flags to ensure that features are all additive 94 | # which is required for feature unification 95 | runs-on: ubuntu-latest 96 | name: ubuntu / stable / features 97 | steps: 98 | - uses: actions/checkout@v4 99 | with: 100 | submodules: true 101 | - name: Install stable 102 | uses: dtolnay/rust-toolchain@stable 103 | - name: cargo install cargo-hack 104 | uses: taiki-e/install-action@cargo-hack 105 | # intentionally no target specifier; see https://github.com/jonhoo/rust-ci-conf/pull/4 106 | # --feature-powerset runs for every combination of features 107 | - name: cargo hack 108 | run: cargo hack --feature-powerset check 109 | msrv: 110 | # check that we can build using the minimal rust version that is specified by this crate 111 | runs-on: ubuntu-latest 112 | # we use a matrix here just because env can't be used in job names 113 | # https://docs.github.com/en/actions/learn-github-actions/contexts#context-availability 114 | strategy: 115 | matrix: 116 | msrv: ["1.66.0"] 117 | name: ubuntu / ${{ matrix.msrv }} 118 | steps: 119 | - uses: actions/checkout@v4 120 | with: 121 | submodules: true 122 | - name: Install ${{ matrix.msrv }} 123 | uses: dtolnay/rust-toolchain@master 124 | with: 125 | toolchain: ${{ matrix.msrv }} 126 | - name: cargo +${{ matrix.msrv }} check 127 | run: cargo check 128 | -------------------------------------------------------------------------------- /src/error.rs: -------------------------------------------------------------------------------- 1 | use crate::escape::{UnescapedRef, UnescapedRoute}; 2 | use crate::tree::{denormalize_params, Node}; 3 | 4 | use std::fmt; 5 | use std::ops::Deref; 6 | 7 | /// Represents errors that can occur when inserting a new route. 8 | #[non_exhaustive] 9 | #[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)] 10 | pub enum InsertError { 11 | /// Attempted to insert a path that conflicts with an existing route. 12 | Conflict { 13 | /// The existing route that the insertion is conflicting with. 14 | with: String, 15 | }, 16 | 17 | /// Only one parameter per route segment is allowed. 18 | /// 19 | /// For example, `/foo-{bar}` and `/{bar}-foo` are valid routes, but `/{foo}-{bar}` 20 | /// is not. 21 | InvalidParamSegment, 22 | 23 | /// Parameters must be registered with a valid name and matching braces. 24 | /// 25 | /// Note you can use `{{` or `}}` to escape literal brackets. 26 | InvalidParam, 27 | 28 | /// Catch-all parameters are only allowed at the end of a path. 29 | InvalidCatchAll, 30 | } 31 | 32 | impl fmt::Display for InsertError { 33 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 34 | match self { 35 | Self::Conflict { with } => { 36 | write!( 37 | f, 38 | "Insertion failed due to conflict with previously registered route: {with}" 39 | ) 40 | } 41 | Self::InvalidParamSegment => { 42 | write!(f, "Only one parameter is allowed per path segment") 43 | } 44 | Self::InvalidParam => write!(f, "Parameters must be registered with a valid name"), 45 | Self::InvalidCatchAll => write!( 46 | f, 47 | "Catch-all parameters are only allowed at the end of a route" 48 | ), 49 | } 50 | } 51 | } 52 | 53 | impl std::error::Error for InsertError {} 54 | 55 | impl InsertError { 56 | /// Returns an error for a route conflict with the given node. 57 | /// 58 | /// This method attempts to find the full conflicting route. 59 | pub(crate) fn conflict( 60 | route: &UnescapedRoute, 61 | prefix: UnescapedRef<'_>, 62 | current: &Node, 63 | ) -> Self { 64 | let mut route = route.clone(); 65 | 66 | // The route is conflicting with the current node. 67 | if prefix.unescaped() == current.prefix.unescaped() { 68 | denormalize_params(&mut route, ¤t.remapping); 69 | return InsertError::Conflict { 70 | with: String::from_utf8(route.into_unescaped()).unwrap(), 71 | }; 72 | } 73 | 74 | // Remove the non-matching suffix from the route. 75 | route.truncate(route.len() - prefix.len()); 76 | 77 | // Add the conflicting prefix. 78 | if !route.ends_with(¤t.prefix) { 79 | route.append(¤t.prefix); 80 | } 81 | 82 | // Add the prefixes of the first conflicting child. 83 | let mut child = current.children.first(); 84 | while let Some(node) = child { 85 | route.append(&node.prefix); 86 | child = node.children.first(); 87 | } 88 | 89 | // Denormalize any route parameters. 90 | let mut last = current; 91 | while let Some(node) = last.children.first() { 92 | last = node; 93 | } 94 | denormalize_params(&mut route, &last.remapping); 95 | 96 | // Return the conflicting route. 97 | InsertError::Conflict { 98 | with: String::from_utf8(route.into_unescaped()).unwrap(), 99 | } 100 | } 101 | } 102 | 103 | /// A failed merge attempt. 104 | /// 105 | /// See [`Router::merge`](crate::Router::merge) for details. 106 | #[derive(Clone, Debug, Eq, PartialEq)] 107 | pub struct MergeError(pub(crate) Vec); 108 | 109 | impl MergeError { 110 | /// Returns a list of [`InsertError`] for every insertion that failed 111 | /// during the merge. 112 | pub fn into_errors(self) -> Vec { 113 | self.0 114 | } 115 | } 116 | 117 | impl fmt::Display for MergeError { 118 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 119 | for error in self.0.iter() { 120 | writeln!(f, "{error}")?; 121 | } 122 | 123 | Ok(()) 124 | } 125 | } 126 | 127 | impl std::error::Error for MergeError {} 128 | 129 | impl Deref for MergeError { 130 | type Target = Vec; 131 | 132 | fn deref(&self) -> &Self::Target { 133 | &self.0 134 | } 135 | } 136 | 137 | /// A failed match attempt. 138 | /// 139 | /// ``` 140 | /// use matchit::{MatchError, Router}; 141 | /// # fn main() -> Result<(), Box> { 142 | /// let mut router = Router::new(); 143 | /// router.insert("/home", "Welcome!")?; 144 | /// router.insert("/blog", "Our blog.")?; 145 | /// 146 | /// // no routes match 147 | /// if let Err(err) = router.at("/blo") { 148 | /// assert_eq!(err, MatchError::NotFound); 149 | /// } 150 | /// # Ok(()) 151 | /// # } 152 | #[derive(Debug, PartialEq, Eq, Clone, Copy)] 153 | pub enum MatchError { 154 | /// No matching route was found. 155 | NotFound, 156 | } 157 | 158 | impl fmt::Display for MatchError { 159 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 160 | write!(f, "Matching route not found") 161 | } 162 | } 163 | 164 | impl std::error::Error for MatchError {} 165 | -------------------------------------------------------------------------------- /src/escape.rs: -------------------------------------------------------------------------------- 1 | use std::{fmt, ops::Range}; 2 | 3 | /// An unescaped route that keeps track of the position of 4 | /// escaped characters, i.e. '{{' or '}}'. 5 | /// 6 | /// Note that this type dereferences to `&[u8]`. 7 | #[derive(Clone, Default)] 8 | pub struct UnescapedRoute { 9 | // The raw unescaped route. 10 | inner: Vec, 11 | escaped: Vec, 12 | } 13 | 14 | impl UnescapedRoute { 15 | /// Unescapes escaped brackets ('{{' or '}}') in a route. 16 | pub fn new(mut inner: Vec) -> UnescapedRoute { 17 | let mut escaped = Vec::new(); 18 | let mut i = 0; 19 | 20 | while let Some(&c) = inner.get(i) { 21 | if (c == b'{' && inner.get(i + 1) == Some(&b'{')) 22 | || (c == b'}' && inner.get(i + 1) == Some(&b'}')) 23 | { 24 | inner.remove(i); 25 | escaped.push(i); 26 | } 27 | 28 | i += 1; 29 | } 30 | 31 | UnescapedRoute { inner, escaped } 32 | } 33 | 34 | /// Returns true if the character at the given index was escaped. 35 | pub fn is_escaped(&self, i: usize) -> bool { 36 | self.escaped.contains(&i) 37 | } 38 | 39 | /// Replaces the characters in the given range. 40 | pub fn splice( 41 | &mut self, 42 | range: Range, 43 | replace: Vec, 44 | ) -> impl Iterator + '_ { 45 | // Ignore any escaped characters in the range being replaced. 46 | self.escaped.retain(|x| !range.contains(x)); 47 | 48 | // Update the escaped indices. 49 | let offset = (replace.len() as isize) - (range.len() as isize); 50 | for i in &mut self.escaped { 51 | if *i > range.end { 52 | *i = i.checked_add_signed(offset).unwrap(); 53 | } 54 | } 55 | 56 | self.inner.splice(range, replace) 57 | } 58 | 59 | /// Appends another route to the end of this one. 60 | pub fn append(&mut self, other: &UnescapedRoute) { 61 | for i in &other.escaped { 62 | self.escaped.push(self.inner.len() + i); 63 | } 64 | 65 | self.inner.extend_from_slice(&other.inner); 66 | } 67 | 68 | /// Truncates the route to the given length. 69 | pub fn truncate(&mut self, to: usize) { 70 | self.escaped.retain(|&x| x < to); 71 | self.inner.truncate(to); 72 | } 73 | 74 | /// Returns a reference to this route. 75 | pub fn as_ref(&self) -> UnescapedRef<'_> { 76 | UnescapedRef { 77 | inner: &self.inner, 78 | escaped: &self.escaped, 79 | offset: 0, 80 | } 81 | } 82 | 83 | /// Returns a reference to the unescaped slice. 84 | pub fn unescaped(&self) -> &[u8] { 85 | &self.inner 86 | } 87 | 88 | /// Returns the unescaped route. 89 | pub fn into_unescaped(self) -> Vec { 90 | self.inner 91 | } 92 | } 93 | 94 | impl std::ops::Deref for UnescapedRoute { 95 | type Target = [u8]; 96 | 97 | fn deref(&self) -> &Self::Target { 98 | &self.inner 99 | } 100 | } 101 | 102 | impl fmt::Debug for UnescapedRoute { 103 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 104 | fmt::Debug::fmt(std::str::from_utf8(&self.inner).unwrap(), f) 105 | } 106 | } 107 | 108 | /// A reference to an `UnescapedRoute`. 109 | #[derive(Copy, Clone)] 110 | pub struct UnescapedRef<'a> { 111 | inner: &'a [u8], 112 | escaped: &'a [usize], 113 | // An offset applied to each escaped index. 114 | offset: isize, 115 | } 116 | 117 | impl<'a> UnescapedRef<'a> { 118 | /// Converts this reference into an owned route. 119 | pub fn to_owned(self) -> UnescapedRoute { 120 | let mut escaped = Vec::new(); 121 | for &i in self.escaped { 122 | let i = i.checked_add_signed(self.offset); 123 | 124 | match i { 125 | Some(i) if i < self.inner.len() => escaped.push(i), 126 | _ => {} 127 | } 128 | } 129 | 130 | UnescapedRoute { 131 | escaped, 132 | inner: self.inner.to_owned(), 133 | } 134 | } 135 | 136 | /// Returns `true` if the character at the given index was escaped. 137 | pub fn is_escaped(&self, i: usize) -> bool { 138 | if let Some(i) = i.checked_add_signed(-self.offset) { 139 | return self.escaped.contains(&i); 140 | } 141 | 142 | false 143 | } 144 | 145 | /// Slices the route with `start..`. 146 | pub fn slice_off(&self, start: usize) -> UnescapedRef<'a> { 147 | UnescapedRef { 148 | inner: &self.inner[start..], 149 | escaped: self.escaped, 150 | offset: self.offset - (start as isize), 151 | } 152 | } 153 | 154 | /// Slices the route with `..end`. 155 | pub fn slice_until(&self, end: usize) -> UnescapedRef<'a> { 156 | UnescapedRef { 157 | inner: &self.inner[..end], 158 | escaped: self.escaped, 159 | offset: self.offset, 160 | } 161 | } 162 | 163 | /// Returns a reference to the unescaped slice. 164 | pub fn unescaped(&self) -> &[u8] { 165 | self.inner 166 | } 167 | } 168 | 169 | impl<'a> std::ops::Deref for UnescapedRef<'a> { 170 | type Target = &'a [u8]; 171 | 172 | fn deref(&self) -> &Self::Target { 173 | &self.inner 174 | } 175 | } 176 | 177 | impl fmt::Debug for UnescapedRef<'_> { 178 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 179 | f.debug_struct("UnescapedRef") 180 | .field("inner", &std::str::from_utf8(self.inner)) 181 | .field("escaped", &self.escaped) 182 | .field("offset", &self.offset) 183 | .finish() 184 | } 185 | } 186 | -------------------------------------------------------------------------------- /src/router.rs: -------------------------------------------------------------------------------- 1 | use crate::error::MergeError; 2 | use crate::tree::Node; 3 | use crate::{InsertError, MatchError, Params}; 4 | 5 | /// A zero-copy URL router. 6 | /// 7 | /// See [the crate documentation](crate) for details. 8 | #[derive(Clone, Debug)] 9 | pub struct Router { 10 | root: Node, 11 | } 12 | 13 | impl Default for Router { 14 | fn default() -> Self { 15 | Self { 16 | root: Node::default(), 17 | } 18 | } 19 | } 20 | 21 | impl Router { 22 | /// Construct a new router. 23 | pub fn new() -> Self { 24 | Self::default() 25 | } 26 | 27 | /// Insert a route into the router. 28 | /// 29 | /// # Examples 30 | /// 31 | /// ```rust 32 | /// # use matchit::Router; 33 | /// # fn main() -> Result<(), Box> { 34 | /// let mut router = Router::new(); 35 | /// router.insert("/home", "Welcome!")?; 36 | /// router.insert("/users/{id}", "A User")?; 37 | /// # Ok(()) 38 | /// # } 39 | /// ``` 40 | pub fn insert(&mut self, route: impl Into, value: T) -> Result<(), InsertError> { 41 | self.root.insert(route.into(), value) 42 | } 43 | 44 | /// Tries to find a value in the router matching the given path. 45 | /// 46 | /// # Examples 47 | /// 48 | /// ```rust 49 | /// # use matchit::Router; 50 | /// # fn main() -> Result<(), Box> { 51 | /// let mut router = Router::new(); 52 | /// router.insert("/home", "Welcome!")?; 53 | /// 54 | /// let matched = router.at("/home").unwrap(); 55 | /// assert_eq!(*matched.value, "Welcome!"); 56 | /// # Ok(()) 57 | /// # } 58 | /// ``` 59 | #[inline] 60 | pub fn at<'path>(&self, path: &'path str) -> Result, MatchError> { 61 | match self.root.at(path.as_bytes()) { 62 | Ok((value, params)) => Ok(Match { 63 | // Safety: We only expose `&mut T` through `&mut self` 64 | value: unsafe { &*value.get() }, 65 | params, 66 | }), 67 | Err(e) => Err(e), 68 | } 69 | } 70 | 71 | /// Tries to find a value in the router matching the given path, 72 | /// returning a mutable reference. 73 | /// 74 | /// # Examples 75 | /// 76 | /// ```rust 77 | /// # use matchit::Router; 78 | /// # fn main() -> Result<(), Box> { 79 | /// let mut router = Router::new(); 80 | /// router.insert("/", 1)?; 81 | /// 82 | /// *router.at_mut("/").unwrap().value += 1; 83 | /// assert_eq!(*router.at("/").unwrap().value, 2); 84 | /// # Ok(()) 85 | /// # } 86 | /// ``` 87 | #[inline] 88 | pub fn at_mut<'path>( 89 | &mut self, 90 | path: &'path str, 91 | ) -> Result, MatchError> { 92 | match self.root.at(path.as_bytes()) { 93 | Ok((value, params)) => Ok(Match { 94 | // Safety: We have `&mut self` 95 | value: unsafe { &mut *value.get() }, 96 | params, 97 | }), 98 | Err(e) => Err(e), 99 | } 100 | } 101 | 102 | /// Remove a given route from the router. 103 | /// 104 | /// Returns the value stored under the route if it was found. 105 | /// If the route was not found or invalid, `None` is returned. 106 | /// 107 | /// # Examples 108 | /// 109 | /// ```rust 110 | /// # use matchit::Router; 111 | /// let mut router = Router::new(); 112 | /// 113 | /// router.insert("/home", "Welcome!"); 114 | /// assert_eq!(router.remove("/home"), Some("Welcome!")); 115 | /// assert_eq!(router.remove("/home"), None); 116 | /// 117 | /// router.insert("/home/{id}/", "Hello!"); 118 | /// assert_eq!(router.remove("/home/{id}/"), Some("Hello!")); 119 | /// assert_eq!(router.remove("/home/{id}/"), None); 120 | /// 121 | /// router.insert("/home/{id}/", "Hello!"); 122 | /// // The route does not match. 123 | /// assert_eq!(router.remove("/home/{user}"), None); 124 | /// assert_eq!(router.remove("/home/{id}/"), Some("Hello!")); 125 | /// 126 | /// router.insert("/home/{id}/", "Hello!"); 127 | /// // Invalid route. 128 | /// assert_eq!(router.remove("/home/{id}"), None); 129 | /// assert_eq!(router.remove("/home/{id}/"), Some("Hello!")); 130 | /// ``` 131 | pub fn remove(&mut self, path: impl Into) -> Option { 132 | self.root.remove(path.into()) 133 | } 134 | 135 | /// Test helper that ensures route priorities are consistent. 136 | #[cfg(feature = "__test_helpers")] 137 | pub fn check_priorities(&self) -> Result { 138 | self.root.check_priorities() 139 | } 140 | 141 | /// Merge a given router into current one. 142 | /// 143 | /// Returns a list of [`InsertError`] for every failed insertion. 144 | /// Note that this can result in a partially successful merge if 145 | /// a subset of routes conflict. 146 | /// 147 | /// # Examples 148 | /// 149 | /// ```rust 150 | /// # use matchit::Router; 151 | /// # fn main() -> Result<(), Box> { 152 | /// let mut root = Router::new(); 153 | /// root.insert("/home", "Welcome!")?; 154 | /// 155 | /// let mut child = Router::new(); 156 | /// child.insert("/users/{id}", "A User")?; 157 | /// 158 | /// root.merge(child)?; 159 | /// assert!(root.at("/users/1").is_ok()); 160 | /// # Ok(()) 161 | /// # } 162 | /// ``` 163 | pub fn merge(&mut self, other: Self) -> Result<(), MergeError> { 164 | let mut errors = Vec::new(); 165 | other.root.for_each(|path, value| { 166 | if let Err(err) = self.insert(path, value) { 167 | errors.push(err); 168 | } 169 | }); 170 | 171 | if errors.is_empty() { 172 | Ok(()) 173 | } else { 174 | Err(MergeError(errors)) 175 | } 176 | } 177 | } 178 | 179 | /// A successful match consisting of the registered value 180 | /// and URL parameters, returned by [`Router::at`](Router::at). 181 | #[derive(Debug)] 182 | pub struct Match<'k, 'v, V> { 183 | /// The value stored under the matched node. 184 | pub value: V, 185 | 186 | /// The route parameters. See [parameters](crate#parameters) for more details. 187 | pub params: Params<'k, 'v>, 188 | } 189 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | /*! 2 | A high performance, zero-copy URL router. 3 | 4 | ```rust 5 | use matchit::Router; 6 | 7 | fn main() -> Result<(), Box> { 8 | let mut router = Router::new(); 9 | router.insert("/home", "Welcome!")?; 10 | router.insert("/users/{id}", "A User")?; 11 | 12 | let matched = router.at("/users/978")?; 13 | assert_eq!(matched.params.get("id"), Some("978")); 14 | assert_eq!(*matched.value, "A User"); 15 | 16 | Ok(()) 17 | } 18 | ``` 19 | 20 | # Parameters 21 | 22 | The router supports dynamic route segments. These can either be named or catch-all parameters. 23 | 24 | Named parameters like `/{id}` match anything until the next static segment or the end of the path. 25 | 26 | ```rust 27 | # use matchit::Router; 28 | # fn main() -> Result<(), Box> { 29 | let mut router = Router::new(); 30 | router.insert("/users/{id}", 42)?; 31 | 32 | let matched = router.at("/users/1")?; 33 | assert_eq!(matched.params.get("id"), Some("1")); 34 | 35 | let matched = router.at("/users/23")?; 36 | assert_eq!(matched.params.get("id"), Some("23")); 37 | 38 | assert!(router.at("/users").is_err()); 39 | # Ok(()) 40 | # } 41 | ``` 42 | 43 | Prefixes and suffixes within a segment are also supported. However, there may only be a single named parameter per route segment. 44 | ```rust 45 | # use matchit::Router; 46 | # fn main() -> Result<(), Box> { 47 | let mut router = Router::new(); 48 | router.insert("/images/img-{id}.png", true)?; 49 | 50 | let matched = router.at("/images/img-1.png")?; 51 | assert_eq!(matched.params.get("id"), Some("1")); 52 | 53 | assert!(router.at("/images/img-1.jpg").is_err()); 54 | # Ok(()) 55 | # } 56 | ``` 57 | 58 | Catch-all parameters start with a `*` and match anything until the end of the path. They must always be at the *end* of the route. 59 | 60 | ```rust 61 | # use matchit::Router; 62 | # fn main() -> Result<(), Box> { 63 | let mut router = Router::new(); 64 | router.insert("/{*rest}", true)?; 65 | 66 | let matched = router.at("/foo.html")?; 67 | assert_eq!(matched.params.get("rest"), Some("foo.html")); 68 | 69 | let matched = router.at("/static/bar.css")?; 70 | assert_eq!(matched.params.get("rest"), Some("static/bar.css")); 71 | 72 | // Note that this would lead to an empty parameter value. 73 | assert!(router.at("/").is_err()); 74 | # Ok(()) 75 | # } 76 | ``` 77 | 78 | The literal characters `{` and `}` may be included in a static route by escaping them with the same character. For example, the `{` character is escaped with `{{`, and the `}` character is escaped with `}}`. 79 | 80 | ```rust 81 | # use matchit::Router; 82 | # fn main() -> Result<(), Box> { 83 | let mut router = Router::new(); 84 | router.insert("/{{hello}}", true)?; 85 | router.insert("/{hello}", true)?; 86 | 87 | // Match the static route. 88 | let matched = router.at("/{hello}")?; 89 | assert!(matched.params.is_empty()); 90 | 91 | // Match the dynamic route. 92 | let matched = router.at("/hello")?; 93 | assert_eq!(matched.params.get("hello"), Some("hello")); 94 | # Ok(()) 95 | # } 96 | ``` 97 | 98 | # Conflict Rules 99 | 100 | Static and dynamic route segments are allowed to overlap. If they do, static segments will be given higher priority: 101 | 102 | ```rust 103 | # use matchit::Router; 104 | # fn main() -> Result<(), Box> { 105 | let mut router = Router::new(); 106 | router.insert("/", "Welcome!").unwrap(); // Priority: 1 107 | router.insert("/about", "About Me").unwrap(); // Priority: 1 108 | router.insert("/{*filepath}", "...").unwrap(); // Priority: 2 109 | # Ok(()) 110 | # } 111 | ``` 112 | 113 | Formally, a route consists of a list of segments separated by `/`, with an optional leading and trailing slash: `(/)/.../(/)`. 114 | 115 | Given set of routes, their overlapping segments may include, in order of priority: 116 | 117 | - Any number of static segments (`/a`, `/b`, ...). 118 | - *One* of the following: 119 | - Any number of route parameters with a suffix (`/{x}a`, `/{x}b`, ...), prioritizing the longest suffix. 120 | - Any number of route parameters with a prefix (`/a{x}`, `/b{x}`, ...), prioritizing the longest prefix. 121 | - A single route parameter with both a prefix and a suffix (`/a{x}b`). 122 | - *One* of the following; 123 | - A single standalone parameter (`/{x}`). 124 | - A single standalone catch-all parameter (`/{*rest}`). Note this only applies to the final route segment. 125 | 126 | Any other combination of route segments is considered ambiguous, and attempting to insert such a route will result in an error. 127 | 128 | The one exception to the above set of rules is that catch-all parameters are always considered to conflict with suffixed route parameters, i.e. that `/{*rest}` 129 | and `/{x}suffix` are overlapping. This is due to an implementation detail of the routing tree that may be relaxed in the future. 130 | 131 | # How does it work? 132 | 133 | The router takes advantage of the fact that URL routes generally follow a hierarchical structure. Routes are stored them in a radix trie that makes heavy use of common prefixes. 134 | 135 | ```text 136 | Priority Path Value 137 | 9 \ 1 138 | 3 ├s None 139 | 2 |├earch\ 2 140 | 1 |└upport\ 3 141 | 2 ├blog\ 4 142 | 1 | └{post} None 143 | 1 | └\ 5 144 | 2 ├about-us\ 6 145 | 1 | └team\ 7 146 | 1 └contact\ 8 147 | ``` 148 | 149 | This allows us to reduce the route search to a small number of branches. Child nodes on the same level of the tree are also prioritized 150 | by the number of children with registered values, increasing the chance of choosing the correct branch of the first try. 151 | 152 | As it turns out, this method of routing is extremely fast. See the [benchmark results](https://github.com/ibraheemdev/matchit?tab=readme-ov-file#benchmarks) for details. 153 | */ 154 | 155 | #![deny( 156 | missing_debug_implementations, 157 | missing_docs, 158 | dead_code, 159 | unsafe_op_in_unsafe_fn, 160 | rustdoc::broken_intra_doc_links 161 | )] 162 | 163 | mod error; 164 | mod escape; 165 | mod params; 166 | mod router; 167 | mod tree; 168 | 169 | pub use error::{InsertError, MatchError, MergeError}; 170 | pub use params::{Params, ParamsIter}; 171 | pub use router::{Match, Router}; 172 | 173 | #[cfg(doctest)] 174 | mod readme { 175 | #[allow(dead_code)] 176 | #[doc = include_str!("../README.md")] 177 | struct Readme; 178 | } 179 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # `matchit` 2 | 3 | [crates.io](https://crates.io/crates/matchit) 4 | [github](https://github.com/ibraheemdev/matchit) 5 | [docs.rs](https://docs.rs/matchit) 6 | 7 | A high performance, zero-copy URL router. 8 | 9 | ```rust 10 | use matchit::Router; 11 | 12 | fn main() -> Result<(), Box> { 13 | let mut router = Router::new(); 14 | router.insert("/home", "Welcome!")?; 15 | router.insert("/users/{id}", "A User")?; 16 | 17 | let matched = router.at("/users/978")?; 18 | assert_eq!(matched.params.get("id"), Some("978")); 19 | assert_eq!(*matched.value, "A User"); 20 | 21 | Ok(()) 22 | } 23 | ``` 24 | 25 | ## Parameters 26 | 27 | The router supports dynamic route segments. These can either be named or catch-all parameters. 28 | 29 | Named parameters like `/{id}` match anything until the next static segment or the end of the path. 30 | 31 | ```rust,ignore 32 | let mut router = Router::new(); 33 | router.insert("/users/{id}", 42)?; 34 | 35 | let matched = router.at("/users/1")?; 36 | assert_eq!(matched.params.get("id"), Some("1")); 37 | 38 | let matched = router.at("/users/23")?; 39 | assert_eq!(matched.params.get("id"), Some("23")); 40 | 41 | assert!(router.at("/users").is_err()); 42 | ``` 43 | 44 | Prefixes and suffixes within a segment are also supported. However, there may only be a single named parameter per route segment. 45 | ```rust,ignore 46 | let mut router = Router::new(); 47 | router.insert("/images/img-{id}.png", true)?; 48 | 49 | let matched = router.at("/images/img-1.png")?; 50 | assert_eq!(matched.params.get("id"), Some("1")); 51 | 52 | assert!(router.at("/images/img-1.jpg").is_err()); 53 | ``` 54 | 55 | Catch-all parameters start with a `*` and match anything until the end of the path. They must always be at the *end* of the route. 56 | 57 | ```rust,ignore 58 | let mut router = Router::new(); 59 | router.insert("/{*rest}", true)?; 60 | 61 | let matched = router.at("/foo.html")?; 62 | assert_eq!(matched.params.get("rest"), Some("foo.html")); 63 | 64 | let matched = router.at("/static/bar.css")?; 65 | assert_eq!(matched.params.get("rest"), Some("static/bar.css")); 66 | 67 | // Note that this would lead to an empty parameter value. 68 | assert!(router.at("/").is_err()); 69 | ``` 70 | 71 | The literal characters `{` and `}` may be included in a static route by escaping them with the same character. 72 | For example, the `{` character is escaped with `{{`, and the `}` character is escaped with `}}`. 73 | 74 | ```rust,ignore 75 | let mut router = Router::new(); 76 | router.insert("/{{hello}}", true)?; 77 | router.insert("/{hello}", true)?; 78 | 79 | // Match the static route. 80 | let matched = router.at("/{hello}")?; 81 | assert!(matched.params.is_empty()); 82 | 83 | // Match the dynamic route. 84 | let matched = router.at("/hello")?; 85 | assert_eq!(matched.params.get("hello"), Some("hello")); 86 | ``` 87 | 88 | ## Conflict Rules 89 | 90 | Static and dynamic route segments are allowed to overlap. If they do, static segments will be given higher priority: 91 | 92 | ```rust,ignore 93 | let mut router = Router::new(); 94 | router.insert("/", "Welcome!").unwrap(); // Priority: 1 95 | router.insert("/about", "About Me").unwrap(); // Priority: 1 96 | router.insert("/{*filepath}", "...").unwrap(); // Priority: 2 97 | ``` 98 | 99 | Formally, a route consists of a list of segments separated by `/`, with an optional leading and trailing slash: `(/)/.../(/)`. 100 | 101 | Given set of routes, their overlapping segments may include, in order of priority: 102 | 103 | - Any number of static segments (`/a`, `/b`, ...). 104 | - *One* of the following: 105 | - Any number of route parameters with a suffix (`/{x}a`, `/{x}b`, ...), prioritizing the longest suffix. 106 | - Any number of route parameters with a prefix (`/a{x}`, `/b{x}`, ...), prioritizing the longest prefix. 107 | - A single route parameter with both a prefix and a suffix (`/a{x}b`). 108 | - *One* of the following; 109 | - A single standalone parameter (`/{x}`). 110 | - A single standalone catch-all parameter (`/{*rest}`). Note this only applies to the final route segment. 111 | 112 | Any other combination of route segments is considered ambiguous, and attempting to insert such a route will result in an error. 113 | 114 | The one exception to the above set of rules is that catch-all parameters are always considered to conflict with suffixed route parameters, i.e. that `/{*rest}` 115 | and `/{x}suffix` are overlapping. This is due to an implementation detail of the routing tree that may be relaxed in the future. 116 | 117 | ## How does it work? 118 | 119 | The router takes advantage of the fact that URL routes generally follow a hierarchical structure. 120 | Routes are stored them in a radix trie that makes heavy use of common prefixes. 121 | 122 | ```text 123 | Priority Path Value 124 | 9 \ 1 125 | 3 ├s None 126 | 2 |├earch\ 2 127 | 1 |└upport\ 3 128 | 2 ├blog\ 4 129 | 1 | └{post} None 130 | 1 | └\ 5 131 | 2 ├about-us\ 6 132 | 1 | └team\ 7 133 | 1 └contact\ 8 134 | ``` 135 | 136 | This allows us to reduce the route search to a small number of branches. Child nodes on the same level of the tree are also 137 | prioritized by the number of children with registered values, increasing the chance of choosing the correct branch of the first try. 138 | 139 | ## Benchmarks 140 | 141 | As it turns out, this method of routing is extremely fast. Below are the benchmark results matching against 130 registered routes. 142 | You can view the benchmark code [here](https://github.com/ibraheemdev/matchit/blob/master/benches/bench.rs). 143 | 144 | ```text 145 | Compare Routers/matchit 146 | time: [2.4451 µs 2.4456 µs 2.4462 µs] 147 | 148 | Compare Routers/gonzales 149 | time: [4.2618 µs 4.2632 µs 4.2646 µs] 150 | 151 | Compare Routers/path-tree 152 | time: [4.8666 µs 4.8696 µs 4.8728 µs] 153 | 154 | Compare Routers/wayfind 155 | time: [4.9440 µs 4.9539 µs 4.9668 µs] 156 | 157 | Compare Routers/route-recognizer 158 | time: [49.203 µs 49.214 µs 49.226 µs] 159 | 160 | Compare Routers/routefinder 161 | time: [70.598 µs 70.636 µs 70.670 µs] 162 | 163 | Compare Routers/actix 164 | time: [453.91 µs 454.01 µs 454.11 µs] 165 | 166 | Compare Routers/regex 167 | time: [421.76 µs 421.82 µs 421.89 µs] 168 | ``` 169 | 170 | ## Credits 171 | 172 | A lot of the code in this package was inspired by Julien Schmidt's [`httprouter`](https://github.com/julienschmidt/httprouter). 173 | -------------------------------------------------------------------------------- /src/params.rs: -------------------------------------------------------------------------------- 1 | use std::{fmt, iter, mem, slice}; 2 | 3 | /// A single URL parameter, consisting of a key and a value. 4 | #[derive(PartialEq, Eq, Ord, PartialOrd, Default, Copy, Clone)] 5 | pub(crate) struct Param<'k, 'v> { 6 | // Keys and values are stored as byte slices internally by the router 7 | // to avoid utf8 checks when slicing. This allows us to perform utf8 8 | // validation lazily without resorting to unsafe code. 9 | pub(crate) key: &'k [u8], 10 | pub(crate) value: &'v [u8], 11 | } 12 | 13 | impl<'k, 'v> Param<'k, 'v> { 14 | const EMPTY: Param<'static, 'static> = Param { 15 | key: b"", 16 | value: b"", 17 | }; 18 | 19 | // Returns the parameter key as a string. 20 | fn key_str(&self) -> &'k str { 21 | std::str::from_utf8(self.key).unwrap() 22 | } 23 | 24 | // Returns the parameter value as a string. 25 | fn value_str(&self) -> &'v str { 26 | std::str::from_utf8(self.value).unwrap() 27 | } 28 | } 29 | 30 | impl<'k, 'v> fmt::Debug for Param<'k, 'v> { 31 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 32 | f.debug_struct("Param") 33 | .field("key", &self.key_str()) 34 | .field("value", &self.value_str()) 35 | .finish() 36 | } 37 | } 38 | 39 | /// A list of parameters returned by a route match. 40 | /// 41 | /// ```rust 42 | /// # fn main() -> Result<(), Box> { 43 | /// # let mut router = matchit::Router::new(); 44 | /// # router.insert("/users/{id}", true).unwrap(); 45 | /// let matched = router.at("/users/1")?; 46 | /// 47 | /// // Iterate through the keys and values. 48 | /// for (key, value) in matched.params.iter() { 49 | /// println!("key: {}, value: {}", key, value); 50 | /// } 51 | /// 52 | /// // Get a specific value by name. 53 | /// let id = matched.params.get("id"); 54 | /// assert_eq!(id, Some("1")); 55 | /// # Ok(()) 56 | /// # } 57 | /// ``` 58 | #[derive(PartialEq, Eq, Ord, PartialOrd, Clone)] 59 | pub struct Params<'k, 'v> { 60 | kind: ParamsKind<'k, 'v>, 61 | } 62 | 63 | impl Default for Params<'_, '_> { 64 | fn default() -> Self { 65 | Self::new() 66 | } 67 | } 68 | 69 | // Most routes have a small number of dynamic parameters, so we can avoid 70 | // heap allocations in the common case. 71 | const SMALL: usize = 3; 72 | 73 | // A list of parameters, optimized to avoid allocations when possible. 74 | #[derive(PartialEq, Eq, Ord, PartialOrd, Clone)] 75 | enum ParamsKind<'k, 'v> { 76 | Small([Param<'k, 'v>; SMALL], usize), 77 | Large(Vec>), 78 | } 79 | 80 | impl<'k, 'v> Params<'k, 'v> { 81 | /// Create an empty list of parameters. 82 | #[inline] 83 | pub fn new() -> Self { 84 | Self { 85 | kind: ParamsKind::Small([Param::EMPTY; SMALL], 0), 86 | } 87 | } 88 | 89 | /// Returns the number of parameters. 90 | pub fn len(&self) -> usize { 91 | match self.kind { 92 | ParamsKind::Small(_, len) => len, 93 | ParamsKind::Large(ref vec) => vec.len(), 94 | } 95 | } 96 | 97 | // Truncates the parameter list to the given length. 98 | pub(crate) fn truncate(&mut self, n: usize) { 99 | match &mut self.kind { 100 | ParamsKind::Small(_, len) => *len = n, 101 | ParamsKind::Large(vec) => vec.truncate(n), 102 | } 103 | } 104 | 105 | /// Returns the value of the first parameter registered under the given key. 106 | pub fn get(&self, key: impl AsRef) -> Option<&'v str> { 107 | let key = key.as_ref().as_bytes(); 108 | 109 | match &self.kind { 110 | ParamsKind::Small(arr, len) => arr 111 | .iter() 112 | .take(*len) 113 | .find(|param| param.key == key) 114 | .map(Param::value_str), 115 | ParamsKind::Large(vec) => vec 116 | .iter() 117 | .find(|param| param.key == key) 118 | .map(Param::value_str), 119 | } 120 | } 121 | 122 | /// Returns an iterator over the parameters in the list. 123 | pub fn iter(&self) -> ParamsIter<'_, 'k, 'v> { 124 | ParamsIter::new(self) 125 | } 126 | 127 | /// Returns `true` if there are no parameters in the list. 128 | pub fn is_empty(&self) -> bool { 129 | match self.kind { 130 | ParamsKind::Small(_, len) => len == 0, 131 | ParamsKind::Large(ref vec) => vec.is_empty(), 132 | } 133 | } 134 | 135 | /// Appends a key-value parameter to the list. 136 | #[inline] 137 | pub(crate) fn push(&mut self, key: &'k [u8], value: &'v [u8]) { 138 | #[cold] 139 | #[inline(never)] 140 | fn drain_to_vec(len: usize, elem: T, arr: &mut [T; SMALL]) -> Vec { 141 | let mut vec = Vec::with_capacity(len + 1); 142 | vec.extend(arr.iter_mut().map(mem::take)); 143 | vec.push(elem); 144 | vec 145 | } 146 | 147 | #[cold] 148 | #[inline(never)] 149 | fn push_slow<'k, 'v>(vec: &mut Vec>, param: Param<'k, 'v>) { 150 | vec.push(param); 151 | } 152 | 153 | let param = Param { key, value }; 154 | match &mut self.kind { 155 | ParamsKind::Small(arr, len) => { 156 | if *len >= SMALL { 157 | self.kind = ParamsKind::Large(drain_to_vec(*len, param, arr)); 158 | return; 159 | } 160 | 161 | arr[*len] = param; 162 | *len += 1; 163 | } 164 | 165 | ParamsKind::Large(vec) => push_slow(vec, param), 166 | } 167 | } 168 | 169 | // Applies a transformation function to each key. 170 | #[inline] 171 | pub(crate) fn for_each_key_mut(&mut self, f: impl Fn((usize, &mut Param<'k, 'v>))) { 172 | match &mut self.kind { 173 | ParamsKind::Small(arr, len) => arr.iter_mut().take(*len).enumerate().for_each(f), 174 | ParamsKind::Large(vec) => vec.iter_mut().enumerate().for_each(f), 175 | } 176 | } 177 | } 178 | 179 | impl fmt::Debug for Params<'_, '_> { 180 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 181 | f.debug_list().entries(self.iter()).finish() 182 | } 183 | } 184 | 185 | /// An iterator over the keys and values of a route's [parameters](crate::Params). 186 | #[derive(Debug)] 187 | pub struct ParamsIter<'ps, 'k, 'v> { 188 | kind: ParamsIterKind<'ps, 'k, 'v>, 189 | } 190 | 191 | impl<'ps, 'k, 'v> ParamsIter<'ps, 'k, 'v> { 192 | fn new(params: &'ps Params<'k, 'v>) -> Self { 193 | let kind = match ¶ms.kind { 194 | ParamsKind::Small(arr, len) => ParamsIterKind::Small(arr.iter().take(*len)), 195 | ParamsKind::Large(vec) => ParamsIterKind::Large(vec.iter()), 196 | }; 197 | Self { kind } 198 | } 199 | } 200 | 201 | #[derive(Debug)] 202 | enum ParamsIterKind<'ps, 'k, 'v> { 203 | Small(iter::Take>>), 204 | Large(slice::Iter<'ps, Param<'k, 'v>>), 205 | } 206 | 207 | impl<'k, 'v> Iterator for ParamsIter<'_, 'k, 'v> { 208 | type Item = (&'k str, &'v str); 209 | 210 | fn next(&mut self) -> Option { 211 | match self.kind { 212 | ParamsIterKind::Small(ref mut iter) => { 213 | iter.next().map(|p| (p.key_str(), p.value_str())) 214 | } 215 | ParamsIterKind::Large(ref mut iter) => { 216 | iter.next().map(|p| (p.key_str(), p.value_str())) 217 | } 218 | } 219 | } 220 | } 221 | 222 | impl ExactSizeIterator for ParamsIter<'_, '_, '_> { 223 | fn len(&self) -> usize { 224 | match self.kind { 225 | ParamsIterKind::Small(ref iter) => iter.len(), 226 | ParamsIterKind::Large(ref iter) => iter.len(), 227 | } 228 | } 229 | } 230 | 231 | #[cfg(test)] 232 | mod tests { 233 | use super::*; 234 | 235 | #[test] 236 | fn heap_alloc() { 237 | let vec = vec![ 238 | ("hello", "hello"), 239 | ("world", "world"), 240 | ("foo", "foo"), 241 | ("bar", "bar"), 242 | ("baz", "baz"), 243 | ]; 244 | 245 | let mut params = Params::new(); 246 | for (key, value) in vec.clone() { 247 | params.push(key.as_bytes(), value.as_bytes()); 248 | assert_eq!(params.get(key), Some(value)); 249 | } 250 | 251 | match params.kind { 252 | ParamsKind::Large(..) => {} 253 | _ => panic!(), 254 | } 255 | 256 | assert!(params.iter().eq(vec.clone())); 257 | } 258 | 259 | #[test] 260 | fn stack_alloc() { 261 | let vec = vec![("hello", "hello"), ("world", "world"), ("baz", "baz")]; 262 | 263 | let mut params = Params::new(); 264 | for (key, value) in vec.clone() { 265 | params.push(key.as_bytes(), value.as_bytes()); 266 | assert_eq!(params.get(key), Some(value)); 267 | } 268 | 269 | match params.kind { 270 | ParamsKind::Small(..) => {} 271 | _ => panic!(), 272 | } 273 | 274 | assert!(params.iter().eq(vec.clone())); 275 | } 276 | 277 | #[test] 278 | fn ignore_array_default() { 279 | let params = Params::new(); 280 | assert!(params.get("").is_none()); 281 | } 282 | } 283 | -------------------------------------------------------------------------------- /tests/remove.rs: -------------------------------------------------------------------------------- 1 | use matchit::Router; 2 | 3 | struct RemoveTest { 4 | routes: Vec<&'static str>, 5 | ops: Vec<(Operation, &'static str, Option<&'static str>)>, 6 | remaining: Vec<&'static str>, 7 | } 8 | 9 | enum Operation { 10 | Insert, 11 | Remove, 12 | } 13 | 14 | use Operation::*; 15 | 16 | impl RemoveTest { 17 | fn run(self) { 18 | let mut router = Router::new(); 19 | 20 | for route in self.routes.iter() { 21 | assert_eq!(router.insert(*route, route.to_owned()), Ok(()), "{route}"); 22 | } 23 | 24 | for (op, route, expected) in self.ops.iter() { 25 | match op { 26 | Insert => { 27 | assert_eq!(router.insert(*route, route), Ok(()), "{route}") 28 | } 29 | Remove => { 30 | assert_eq!(router.remove(*route), *expected, "removing {route}",) 31 | } 32 | } 33 | } 34 | 35 | for route in self.remaining { 36 | assert!(router.at(route).is_ok(), "remaining {route}"); 37 | } 38 | } 39 | } 40 | 41 | #[test] 42 | fn normalized() { 43 | RemoveTest { 44 | routes: vec![ 45 | "/x/{foo}/bar", 46 | "/x/{bar}/baz", 47 | "/{foo}/{baz}/bax", 48 | "/{foo}/{bar}/baz", 49 | "/{fod}/{baz}/{bax}/foo", 50 | "/{fod}/baz/bax/foo", 51 | "/{foo}/baz/bax", 52 | "/{bar}/{bay}/bay", 53 | "/s", 54 | "/s/s", 55 | "/s/s/s", 56 | "/s/s/s/s", 57 | "/s/s/{s}/x", 58 | "/s/s/{y}/d", 59 | ], 60 | ops: vec![ 61 | (Remove, "/x/{foo}/bar", Some("/x/{foo}/bar")), 62 | (Remove, "/x/{bar}/baz", Some("/x/{bar}/baz")), 63 | (Remove, "/{foo}/{baz}/bax", Some("/{foo}/{baz}/bax")), 64 | (Remove, "/{foo}/{bar}/baz", Some("/{foo}/{bar}/baz")), 65 | ( 66 | Remove, 67 | "/{fod}/{baz}/{bax}/foo", 68 | Some("/{fod}/{baz}/{bax}/foo"), 69 | ), 70 | (Remove, "/{fod}/baz/bax/foo", Some("/{fod}/baz/bax/foo")), 71 | (Remove, "/{foo}/baz/bax", Some("/{foo}/baz/bax")), 72 | (Remove, "/{bar}/{bay}/bay", Some("/{bar}/{bay}/bay")), 73 | (Remove, "/s", Some("/s")), 74 | (Remove, "/s/s", Some("/s/s")), 75 | (Remove, "/s/s/s", Some("/s/s/s")), 76 | (Remove, "/s/s/s/s", Some("/s/s/s/s")), 77 | (Remove, "/s/s/{s}/x", Some("/s/s/{s}/x")), 78 | (Remove, "/s/s/{y}/d", Some("/s/s/{y}/d")), 79 | ], 80 | remaining: vec![], 81 | } 82 | .run(); 83 | } 84 | 85 | #[test] 86 | fn test() { 87 | RemoveTest { 88 | routes: vec!["/home", "/home/{id}"], 89 | ops: vec![ 90 | (Remove, "/home", Some("/home")), 91 | (Remove, "/home", None), 92 | (Remove, "/home/{id}", Some("/home/{id}")), 93 | (Remove, "/home/{id}", None), 94 | ], 95 | remaining: vec![], 96 | } 97 | .run(); 98 | } 99 | 100 | #[test] 101 | fn blog() { 102 | RemoveTest { 103 | routes: vec![ 104 | "/{page}", 105 | "/posts/{year}/{month}/{post}", 106 | "/posts/{year}/{month}/index", 107 | "/posts/{year}/top", 108 | "/static/{*path}", 109 | "/favicon.ico", 110 | ], 111 | ops: vec![ 112 | (Remove, "/{page}", Some("/{page}")), 113 | ( 114 | Remove, 115 | "/posts/{year}/{month}/{post}", 116 | Some("/posts/{year}/{month}/{post}"), 117 | ), 118 | ( 119 | Remove, 120 | "/posts/{year}/{month}/index", 121 | Some("/posts/{year}/{month}/index"), 122 | ), 123 | (Remove, "/posts/{year}/top", Some("/posts/{year}/top")), 124 | (Remove, "/static/{*path}", Some("/static/{*path}")), 125 | (Remove, "/favicon.ico", Some("/favicon.ico")), 126 | ], 127 | remaining: vec![], 128 | } 129 | .run() 130 | } 131 | 132 | #[test] 133 | fn catchall() { 134 | RemoveTest { 135 | routes: vec!["/foo/{*catchall}", "/bar", "/bar/", "/bar/{*catchall}"], 136 | ops: vec![ 137 | (Remove, "/foo/{catchall}", None), 138 | (Remove, "/foo/{*catchall}", Some("/foo/{*catchall}")), 139 | (Remove, "/bar/", Some("/bar/")), 140 | (Insert, "/foo/*catchall", Some("/foo/*catchall")), 141 | (Remove, "/bar/{*catchall}", Some("/bar/{*catchall}")), 142 | ], 143 | remaining: vec!["/bar", "/foo/*catchall"], 144 | } 145 | .run(); 146 | } 147 | 148 | #[test] 149 | fn overlapping_routes() { 150 | RemoveTest { 151 | routes: vec![ 152 | "/home", 153 | "/home/{id}", 154 | "/users", 155 | "/users/{id}", 156 | "/users/{id}/posts", 157 | "/users/{id}/posts/{post_id}", 158 | "/articles", 159 | "/articles/{category}", 160 | "/articles/{category}/{id}", 161 | ], 162 | ops: vec![ 163 | (Remove, "/home", Some("/home")), 164 | (Insert, "/home", Some("/home")), 165 | (Remove, "/home/{id}", Some("/home/{id}")), 166 | (Insert, "/home/{id}", Some("/home/{id}")), 167 | (Remove, "/users", Some("/users")), 168 | (Insert, "/users", Some("/users")), 169 | (Remove, "/users/{id}", Some("/users/{id}")), 170 | (Insert, "/users/{id}", Some("/users/{id}")), 171 | (Remove, "/users/{id}/posts", Some("/users/{id}/posts")), 172 | (Insert, "/users/{id}/posts", Some("/users/{id}/posts")), 173 | ( 174 | Remove, 175 | "/users/{id}/posts/{post_id}", 176 | Some("/users/{id}/posts/{post_id}"), 177 | ), 178 | ( 179 | Insert, 180 | "/users/{id}/posts/{post_id}", 181 | Some("/users/{id}/posts/{post_id}"), 182 | ), 183 | (Remove, "/articles", Some("/articles")), 184 | (Insert, "/articles", Some("/articles")), 185 | (Remove, "/articles/{category}", Some("/articles/{category}")), 186 | (Insert, "/articles/{category}", Some("/articles/{category}")), 187 | ( 188 | Remove, 189 | "/articles/{category}/{id}", 190 | Some("/articles/{category}/{id}"), 191 | ), 192 | ( 193 | Insert, 194 | "/articles/{category}/{id}", 195 | Some("/articles/{category}/{id}"), 196 | ), 197 | ], 198 | remaining: vec![ 199 | "/home", 200 | "/home/{id}", 201 | "/users", 202 | "/users/{id}", 203 | "/users/{id}/posts", 204 | "/users/{id}/posts/{post_id}", 205 | "/articles", 206 | "/articles/{category}", 207 | "/articles/{category}/{id}", 208 | ], 209 | } 210 | .run(); 211 | } 212 | 213 | #[test] 214 | fn trailing_slash() { 215 | RemoveTest { 216 | routes: vec!["/{home}/", "/foo"], 217 | ops: vec![ 218 | (Remove, "/", None), 219 | (Remove, "/{home}", None), 220 | (Remove, "/foo/", None), 221 | (Remove, "/foo", Some("/foo")), 222 | (Remove, "/{home}", None), 223 | (Remove, "/{home}/", Some("/{home}/")), 224 | ], 225 | remaining: vec![], 226 | } 227 | .run(); 228 | } 229 | 230 | #[test] 231 | fn remove_root() { 232 | RemoveTest { 233 | routes: vec!["/"], 234 | ops: vec![(Remove, "/", Some("/"))], 235 | remaining: vec![], 236 | } 237 | .run(); 238 | } 239 | 240 | #[test] 241 | fn check_escaped_params() { 242 | RemoveTest { 243 | routes: vec![ 244 | "/foo/{id}", 245 | "/foo/{id}/bar", 246 | "/bar/{user}/{id}", 247 | "/bar/{user}/{id}/baz", 248 | "/baz/{product}/{user}/{id}", 249 | ], 250 | ops: vec![ 251 | (Remove, "/foo/{a}", None), 252 | (Remove, "/foo/{a}/bar", None), 253 | (Remove, "/bar/{a}/{b}", None), 254 | (Remove, "/bar/{a}/{b}/baz", None), 255 | (Remove, "/baz/{a}/{b}/{c}", None), 256 | ], 257 | remaining: vec![ 258 | "/foo/{id}", 259 | "/foo/{id}/bar", 260 | "/bar/{user}/{id}", 261 | "/bar/{user}/{id}/baz", 262 | "/baz/{product}/{user}/{id}", 263 | ], 264 | } 265 | .run(); 266 | } 267 | 268 | #[test] 269 | fn wildcard_suffix() { 270 | RemoveTest { 271 | routes: vec![ 272 | "/foo/{id}", 273 | "/foo/{id}/bar", 274 | "/foo/{id}bar", 275 | "/foo/{id}bar/baz", 276 | "/foo/{id}bar/baz/bax", 277 | "/bar/x{id}y", 278 | "/bar/x{id}y/", 279 | "/baz/x{id}y", 280 | "/baz/x{id}y/", 281 | ], 282 | ops: vec![ 283 | (Remove, "/foo/{id}", Some("/foo/{id}")), 284 | (Remove, "/foo/{id}bar", Some("/foo/{id}bar")), 285 | (Remove, "/foo/{id}bar/baz", Some("/foo/{id}bar/baz")), 286 | (Insert, "/foo/{id}bax", Some("/foo/{id}bax")), 287 | (Insert, "/foo/{id}bax/baz", Some("/foo/{id}bax/baz")), 288 | (Remove, "/foo/{id}bax/baz", Some("/foo/{id}bax/baz")), 289 | (Remove, "/bar/x{id}y", Some("/bar/x{id}y")), 290 | (Remove, "/baz/x{id}y/", Some("/baz/x{id}y/")), 291 | ], 292 | remaining: vec![ 293 | "/foo/{id}/bar", 294 | "/foo/{id}bar/baz/bax", 295 | "/foo/{id}bax", 296 | "/bar/x{id}y/", 297 | "/baz/x{id}y", 298 | ], 299 | } 300 | .run(); 301 | } 302 | -------------------------------------------------------------------------------- /benches/bench.rs: -------------------------------------------------------------------------------- 1 | use criterion::{black_box, criterion_group, criterion_main, Criterion}; 2 | 3 | #[allow(clippy::useless_concat)] 4 | fn compare_routers(c: &mut Criterion) { 5 | let mut group = c.benchmark_group("Compare Routers"); 6 | 7 | let paths = routes!(literal).to_vec(); 8 | 9 | let mut matchit = matchit::Router::new(); 10 | for route in routes!(brackets) { 11 | matchit.insert(route, true).unwrap(); 12 | } 13 | group.bench_function("matchit", |b| { 14 | b.iter(|| { 15 | for path in black_box(&paths) { 16 | let result = black_box(matchit.at(path).unwrap()); 17 | assert!(*result.value); 18 | } 19 | }); 20 | }); 21 | 22 | let mut wayfind = wayfind::Router::new(); 23 | for route in routes!(brackets) { 24 | wayfind.insert(route, true).unwrap(); 25 | } 26 | let wayfind_paths = paths.to_vec(); 27 | group.bench_function("wayfind", |b| { 28 | b.iter(|| { 29 | for path in black_box(&wayfind_paths) { 30 | let result = black_box(wayfind.search(path).unwrap()); 31 | assert!(*result.data); 32 | } 33 | }); 34 | }); 35 | 36 | let mut path_tree = path_tree::PathTree::new(); 37 | for route in routes!(colon) { 38 | let _ = path_tree.insert(route, true); 39 | } 40 | group.bench_function("path-tree", |b| { 41 | b.iter(|| { 42 | for path in black_box(&paths) { 43 | let result = black_box(path_tree.find(path).unwrap()); 44 | assert!(*result.0); 45 | } 46 | }); 47 | }); 48 | 49 | let registered = routes!(brackets); 50 | let gonzales = gonzales::RouterBuilder::new().build(registered); 51 | group.bench_function("gonzales", |b| { 52 | b.iter(|| { 53 | for path in black_box(&paths) { 54 | let result = black_box(gonzales.route(path).unwrap()); 55 | assert!(registered.get(result.get_index()).is_some()); 56 | } 57 | }); 58 | }); 59 | 60 | let mut actix = actix_router::Router::::build(); 61 | for route in routes!(brackets) { 62 | actix.path(route, true); 63 | } 64 | let actix = actix.finish(); 65 | group.bench_function("actix", |b| { 66 | b.iter(|| { 67 | for path in black_box(&paths) { 68 | let mut path = actix_router::Path::new(*path); 69 | let result = black_box(actix.recognize(&mut path).unwrap()); 70 | assert!(*result.0); 71 | } 72 | }); 73 | }); 74 | 75 | let regex_set = regex::RegexSet::new(routes!(regex)).unwrap(); 76 | group.bench_function("regex", |b| { 77 | b.iter(|| { 78 | for path in black_box(&paths) { 79 | let result = black_box(regex_set.matches(path)); 80 | assert!(result.matched_any()); 81 | } 82 | }); 83 | }); 84 | 85 | let mut route_recognizer = route_recognizer::Router::new(); 86 | for route in routes!(colon) { 87 | route_recognizer.add(route, true); 88 | } 89 | group.bench_function("route-recognizer", |b| { 90 | b.iter(|| { 91 | for path in black_box(&paths) { 92 | let result = black_box(route_recognizer.recognize(path).unwrap()); 93 | assert!(**result.handler()); 94 | } 95 | }); 96 | }); 97 | 98 | let mut routefinder = routefinder::Router::new(); 99 | for route in routes!(colon) { 100 | routefinder.add(route, true).unwrap(); 101 | } 102 | group.bench_function("routefinder", |b| { 103 | b.iter(|| { 104 | for path in black_box(&paths) { 105 | let result = black_box(routefinder.best_match(path).unwrap()); 106 | assert!(*result.handler()); 107 | } 108 | }); 109 | }); 110 | 111 | group.finish(); 112 | } 113 | 114 | criterion_group!(benches, compare_routers); 115 | criterion_main!(benches); 116 | 117 | macro_rules! routes { 118 | (literal) => {{ 119 | routes!(finish => "p1", "p2", "p3", "p4") 120 | }}; 121 | (colon) => {{ 122 | routes!(finish => ":p1", ":p2", ":p3", ":p4") 123 | }}; 124 | (brackets) => {{ 125 | routes!(finish => "{p1}", "{p2}", "{p3}", "{p4}") 126 | }}; 127 | (regex) => {{ 128 | routes!(finish => "(.*)", "(.*)", "(.*)", "(.*)") 129 | }}; 130 | (finish => $p1:literal, $p2:literal, $p3:literal, $p4:literal) => {{ 131 | [ 132 | concat!("/authorizations"), 133 | concat!("/authorizations/", $p1), 134 | concat!("/applications/", $p1, "/tokens/", $p2), 135 | concat!("/events"), 136 | concat!("/repos/", $p1, "/", $p2, "/events"), 137 | concat!("/networks/", $p1, "/", $p2, "/events"), 138 | concat!("/orgs/", $p1, "/events"), 139 | concat!("/users/", $p1, "/received_events"), 140 | concat!("/users/", $p1, "/received_events/public"), 141 | concat!("/users/", $p1, "/events"), 142 | concat!("/users/", $p1, "/events/public"), 143 | concat!("/users/", $p1, "/events/orgs/", $p2), 144 | concat!("/feeds"), 145 | concat!("/notifications"), 146 | concat!("/repos/", $p1, "/", $p2, "/notifications"), 147 | concat!("/notifications/threads/", $p1), 148 | concat!("/notifications/threads/", $p1, "/subscription"), 149 | concat!("/repos/", $p1, "/", $p2, "/stargazers"), 150 | concat!("/users/", $p1, "/starred"), 151 | concat!("/user/starred"), 152 | concat!("/user/starred/", $p1, "/", $p2), 153 | concat!("/repos/", $p1, "/", $p2, "/subscribers"), 154 | concat!("/users/", $p1, "/subscriptions"), 155 | concat!("/user/subscriptions"), 156 | concat!("/repos/", $p1, "/", $p2, "/subscription"), 157 | concat!("/user/subscriptions/", $p1, "/", $p2), 158 | concat!("/users/", $p1, "/gists"), 159 | concat!("/gists"), 160 | concat!("/gists/", $p1), 161 | concat!("/gists/", $p1, "/star"), 162 | concat!("/repos/", $p1, "/", $p2, "/git/blobs/", $p3), 163 | concat!("/repos/", $p1, "/", $p2, "/git/commits/", $p3), 164 | concat!("/repos/", $p1, "/", $p2, "/git/refs"), 165 | concat!("/repos/", $p1, "/", $p2, "/git/tags/", $p3), 166 | concat!("/repos/", $p1, "/", $p2, "/git/trees/", $p3), 167 | concat!("/issues"), 168 | concat!("/user/issues"), 169 | concat!("/orgs/", $p1, "/issues"), 170 | concat!("/repos/", $p1, "/", $p2, "/issues"), 171 | concat!("/repos/", $p1, "/", $p2, "/issues/", $p3), 172 | concat!("/repos/", $p1, "/", $p2, "/assignees"), 173 | concat!("/repos/", $p1, "/", $p2, "/assignees/", $p3), 174 | concat!("/repos/", $p1, "/", $p2, "/issues/", $p3, "/comments"), 175 | concat!("/repos/", $p1, "/", $p2, "/issues/", $p3, "/events"), 176 | concat!("/repos/", $p1, "/", $p2, "/labels"), 177 | concat!("/repos/", $p1, "/", $p2, "/labels/", $p3), 178 | concat!("/repos/", $p1, "/", $p2, "/issues/", $p3, "/labels"), 179 | concat!("/repos/", $p1, "/", $p2, "/milestones/", $p3, "/labels"), 180 | concat!("/repos/", $p1, "/", $p2, "/milestones/"), 181 | concat!("/repos/", $p1, "/", $p2, "/milestones/", $p3), 182 | concat!("/emojis"), 183 | concat!("/gitignore/templates"), 184 | concat!("/gitignore/templates/", $p1), 185 | concat!("/meta"), 186 | concat!("/rate_limit"), 187 | concat!("/users/", $p1, "/orgs"), 188 | concat!("/user/orgs"), 189 | concat!("/orgs/", $p1), 190 | concat!("/orgs/", $p1, "/members"), 191 | concat!("/orgs/", $p1, "/members/", $p2), 192 | concat!("/orgs/", $p1, "/public_members"), 193 | concat!("/orgs/", $p1, "/public_members/", $p2), 194 | concat!("/orgs/", $p1, "/teams"), 195 | concat!("/teams/", $p1), 196 | concat!("/teams/", $p1, "/members"), 197 | concat!("/teams/", $p1, "/members/", $p2), 198 | concat!("/teams/", $p1, "/repos"), 199 | concat!("/teams/", $p1, "/repos/", $p2, "/", $p3), 200 | concat!("/user/teams"), 201 | concat!("/repos/", $p1, "/", $p2, "/pulls"), 202 | concat!("/repos/", $p1, "/", $p2, "/pulls/", $p3), 203 | concat!("/repos/", $p1, "/", $p2, "/pulls/", $p3, "/commits"), 204 | concat!("/repos/", $p1, "/", $p2, "/pulls/", $p3, "/files"), 205 | concat!("/repos/", $p1, "/", $p2, "/pulls/", $p3, "/merge"), 206 | concat!("/repos/", $p1, "/", $p2, "/pulls/", $p3, "/comments"), 207 | concat!("/user/repos"), 208 | concat!("/users/", $p1, "/repos"), 209 | concat!("/orgs/", $p1, "/repos"), 210 | concat!("/repositories"), 211 | concat!("/repos/", $p1, "/", $p2), 212 | concat!("/repos/", $p1, "/", $p2, "/contributors"), 213 | concat!("/repos/", $p1, "/", $p2, "/languages"), 214 | concat!("/repos/", $p1, "/", $p2, "/teams"), 215 | concat!("/repos/", $p1, "/", $p2, "/tags"), 216 | concat!("/repos/", $p1, "/", $p2, "/branches"), 217 | concat!("/repos/", $p1, "/", $p2, "/branches/", $p3), 218 | concat!("/repos/", $p1, "/", $p2, "/collaborators"), 219 | concat!("/repos/", $p1, "/", $p2, "/collaborators/", $p3), 220 | concat!("/repos/", $p1, "/", $p2, "/comments"), 221 | concat!("/repos/", $p1, "/", $p2, "/commits/", $p3, "/comments"), 222 | concat!("/repos/", $p1, "/", $p2, "/commits"), 223 | concat!("/repos/", $p1, "/", $p2, "/commits/", $p3), 224 | concat!("/repos/", $p1, "/", $p2, "/readme"), 225 | concat!("/repos/", $p1, "/", $p2, "/keys"), 226 | concat!("/repos/", $p1, "/", $p2, "/keys/", $p3), 227 | concat!("/repos/", $p1, "/", $p2, "/downloads"), 228 | concat!("/repos/", $p1, "/", $p2, "/downloads/", $p3), 229 | concat!("/repos/", $p1, "/", $p2, "/forks"), 230 | concat!("/repos/", $p1, "/", $p2, "/hooks"), 231 | concat!("/repos/", $p1, "/", $p2, "/hooks/", $p3), 232 | concat!("/repos/", $p1, "/", $p2, "/releases"), 233 | concat!("/repos/", $p1, "/", $p2, "/releases/", $p3), 234 | concat!("/repos/", $p1, "/", $p2, "/releases/", $p3, "/assets"), 235 | concat!("/repos/", $p1, "/", $p2, "/stats/contributors"), 236 | concat!("/repos/", $p1, "/", $p2, "/stats/commit_activity"), 237 | concat!("/repos/", $p1, "/", $p2, "/stats/code_frequency"), 238 | concat!("/repos/", $p1, "/", $p2, "/stats/participation"), 239 | concat!("/repos/", $p1, "/", $p2, "/stats/punch_card"), 240 | concat!("/repos/", $p1, "/", $p2, "/statuses/", $p3), 241 | concat!("/search/repositories"), 242 | concat!("/search/code"), 243 | concat!("/search/issues"), 244 | concat!("/search/users"), 245 | concat!("/legacy/issues/search/", $p1, "/", $p2, "/", $p3, "/", $p4), 246 | concat!("/legacy/repos/search/", $p1), 247 | concat!("/legacy/user/search/", $p1), 248 | concat!("/legacy/user/email/", $p1), 249 | concat!("/users/", $p1), 250 | concat!("/user"), 251 | concat!("/users"), 252 | concat!("/user/emails"), 253 | concat!("/users/", $p1, "/followers"), 254 | concat!("/user/followers"), 255 | concat!("/users/", $p1, "/following"), 256 | concat!("/user/following"), 257 | concat!("/user/following/", $p1), 258 | concat!("/users/", $p1, "/following/", $p2), 259 | concat!("/users/", $p1, "/keys"), 260 | concat!("/user/keys"), 261 | concat!("/user/keys/", $p1), 262 | ] 263 | }}; 264 | } 265 | 266 | use routes; 267 | -------------------------------------------------------------------------------- /tests/insert.rs: -------------------------------------------------------------------------------- 1 | use matchit::{InsertError, Router}; 2 | 3 | struct InsertTest(Vec<(&'static str, Result<(), InsertError>)>); 4 | 5 | impl InsertTest { 6 | fn run(self) { 7 | let mut router = Router::new(); 8 | for (route, expected) in self.0 { 9 | let got = router.insert(route, route.to_owned()); 10 | assert_eq!(got, expected, "{route}"); 11 | } 12 | } 13 | } 14 | 15 | fn conflict(with: &'static str) -> InsertError { 16 | InsertError::Conflict { with: with.into() } 17 | } 18 | 19 | // Regression test for https://github.com/ibraheemdev/matchit/issues/84. 20 | #[test] 21 | fn missing_leading_slash_suffix() { 22 | InsertTest(vec![("/{foo}", Ok(())), ("/{foo}suffix", Ok(()))]).run(); 23 | InsertTest(vec![("{foo}", Ok(())), ("{foo}suffix", Ok(()))]).run(); 24 | } 25 | 26 | // Regression test for https://github.com/ibraheemdev/matchit/issues/82. 27 | #[test] 28 | fn missing_leading_slash_conflict() { 29 | InsertTest(vec![("{foo}/", Ok(())), ("foo/", Ok(()))]).run(); 30 | InsertTest(vec![("foo/", Ok(())), ("{foo}/", Ok(()))]).run(); 31 | } 32 | 33 | #[test] 34 | fn wildcard_conflict() { 35 | InsertTest(vec![ 36 | ("/cmd/{tool}/{sub}", Ok(())), 37 | ("/cmd/vet", Ok(())), 38 | ("/foo/bar", Ok(())), 39 | ("/foo/{name}", Ok(())), 40 | ("/foo/{names}", Err(conflict("/foo/{name}"))), 41 | ("/cmd/{*path}", Err(conflict("/cmd/{tool}/{sub}"))), 42 | ("/cmd/{xxx}/names", Ok(())), 43 | ("/cmd/{tool}/{xxx}/foo", Ok(())), 44 | ("/src/{*filepath}", Ok(())), 45 | ("/src/{file}", Err(conflict("/src/{*filepath}"))), 46 | ("/src/static.json", Ok(())), 47 | ("/src/$filepathx", Ok(())), 48 | ("/src/", Ok(())), 49 | ("/src/foo/bar", Ok(())), 50 | ("/src1/", Ok(())), 51 | ("/src1/{*filepath}", Ok(())), 52 | ("/src2{*filepath}", Ok(())), 53 | ("/src2/{*filepath}", Ok(())), 54 | ("/src2/", Ok(())), 55 | ("/src2", Ok(())), 56 | ("/src3", Ok(())), 57 | ("/src3/{*filepath}", Ok(())), 58 | ("/search/{query}", Ok(())), 59 | ("/search/valid", Ok(())), 60 | ("/user_{name}", Ok(())), 61 | ("/user_x", Ok(())), 62 | ("/user_{bar}", Err(conflict("/user_{name}"))), 63 | ("/id{id}", Ok(())), 64 | ("/id/{id}", Ok(())), 65 | ("/x/{id}", Ok(())), 66 | ("/x/{id}/", Ok(())), 67 | ("/x/{id}y", Ok(())), 68 | ("/x/{id}y/", Ok(())), 69 | ("/x/{id}y", Err(conflict("/x/{id}y"))), 70 | ("/x/x{id}", Err(conflict("/x/{id}y/"))), 71 | ("/x/x{id}y", Err(conflict("/x/{id}y/"))), 72 | ("/y/{id}", Ok(())), 73 | ("/y/{id}/", Ok(())), 74 | ("/y/y{id}", Ok(())), 75 | ("/y/y{id}/", Ok(())), 76 | ("/y/{id}y", Err(conflict("/y/y{id}/"))), 77 | ("/y/{id}y/", Err(conflict("/y/y{id}/"))), 78 | ("/y/x{id}y", Err(conflict("/y/y{id}/"))), 79 | ("/z/x{id}y", Ok(())), 80 | ("/z/{id}", Ok(())), 81 | ("/z/{id}y", Err(conflict("/z/x{id}y"))), 82 | ("/z/x{id}", Err(conflict("/z/x{id}y"))), 83 | ("/z/y{id}", Err(conflict("/z/x{id}y"))), 84 | ("/z/x{id}z", Err(conflict("/z/x{id}y"))), 85 | ("/z/z{id}y", Err(conflict("/z/x{id}y"))), 86 | ("/bar/{id}", Ok(())), 87 | ("/bar/x{id}y", Ok(())), 88 | ]) 89 | .run() 90 | } 91 | 92 | #[test] 93 | fn prefix_suffix_conflict() { 94 | InsertTest(vec![ 95 | ("/x1/{a}suffix", Ok(())), 96 | ("/x1/prefix{a}", Err(conflict("/x1/{a}suffix"))), 97 | ("/x1/prefix{a}suffix", Err(conflict("/x1/{a}suffix"))), 98 | ("/x1/suffix{a}prefix", Err(conflict("/x1/{a}suffix"))), 99 | ("/x1", Ok(())), 100 | ("/x1/", Ok(())), 101 | ("/x1/{a}", Ok(())), 102 | ("/x1/{a}/", Ok(())), 103 | ("/x1/{a}suffix/", Ok(())), 104 | ("/x2/{a}suffix", Ok(())), 105 | ("/x2/{a}", Ok(())), 106 | ("/x2/prefix{a}", Err(conflict("/x2/{a}suffix"))), 107 | ("/x2/prefix{a}suff", Err(conflict("/x2/{a}suffix"))), 108 | ("/x2/prefix{a}suffix", Err(conflict("/x2/{a}suffix"))), 109 | ("/x2/prefix{a}suffixy", Err(conflict("/x2/{a}suffix"))), 110 | ("/x2", Ok(())), 111 | ("/x2/", Ok(())), 112 | ("/x2/{a}suffix/", Ok(())), 113 | ("/x3/prefix{a}", Ok(())), 114 | ("/x3/{a}suffix", Err(conflict("/x3/prefix{a}"))), 115 | ("/x3/prefix{a}suffix", Err(conflict("/x3/prefix{a}"))), 116 | ("/x3/prefix{a}/", Ok(())), 117 | ("/x3/{a}", Ok(())), 118 | ("/x3/{a}/", Ok(())), 119 | ("/x4/prefix{a}", Ok(())), 120 | ("/x4/{a}", Ok(())), 121 | ("/x4/{a}suffix", Err(conflict("/x4/prefix{a}"))), 122 | ("/x4/suffix{a}p", Err(conflict("/x4/prefix{a}"))), 123 | ("/x4/suffix{a}prefix", Err(conflict("/x4/prefix{a}"))), 124 | ("/x4/prefix{a}/", Ok(())), 125 | ("/x4/{a}/", Ok(())), 126 | ("/x5/prefix1{a}", Ok(())), 127 | ("/x5/prefix2{a}", Ok(())), 128 | ("/x5/{a}suffix", Err(conflict("/x5/prefix1{a}"))), 129 | ("/x5/prefix{a}suffix", Err(conflict("/x5/prefix1{a}"))), 130 | ("/x5/prefix1{a}suffix", Err(conflict("/x5/prefix1{a}"))), 131 | ("/x5/prefix2{a}suffix", Err(conflict("/x5/prefix2{a}"))), 132 | ("/x5/prefix3{a}suffix", Err(conflict("/x5/prefix1{a}"))), 133 | ("/x5/prefix1{a}/", Ok(())), 134 | ("/x5/prefix2{a}/", Ok(())), 135 | ("/x5/prefix3{a}/", Ok(())), 136 | ("/x5/{a}", Ok(())), 137 | ("/x5/{a}/", Ok(())), 138 | ("/x6/prefix1{a}", Ok(())), 139 | ("/x6/prefix2{a}", Ok(())), 140 | ("/x6/{a}", Ok(())), 141 | ("/x6/{a}suffix", Err(conflict("/x6/prefix1{a}"))), 142 | ("/x6/prefix{a}suffix", Err(conflict("/x6/prefix1{a}"))), 143 | ("/x6/prefix1{a}suffix", Err(conflict("/x6/prefix1{a}"))), 144 | ("/x6/prefix2{a}suffix", Err(conflict("/x6/prefix2{a}"))), 145 | ("/x6/prefix3{a}suffix", Err(conflict("/x6/prefix1{a}"))), 146 | ("/x6/prefix1{a}/", Ok(())), 147 | ("/x6/prefix2{a}/", Ok(())), 148 | ("/x6/prefix3{a}/", Ok(())), 149 | ("/x6/{a}/", Ok(())), 150 | ("/x7/prefix{a}suffix", Ok(())), 151 | ("/x7/{a}suff", Err(conflict("/x7/prefix{a}suffix"))), 152 | ("/x7/{a}suffix", Err(conflict("/x7/prefix{a}suffix"))), 153 | ("/x7/{a}suffixy", Err(conflict("/x7/prefix{a}suffix"))), 154 | ("/x7/{a}prefix", Err(conflict("/x7/prefix{a}suffix"))), 155 | ("/x7/suffix{a}prefix", Err(conflict("/x7/prefix{a}suffix"))), 156 | ("/x7/prefix{a}", Err(conflict("/x7/prefix{a}suffix"))), 157 | ("/x7/another{a}", Err(conflict("/x7/prefix{a}suffix"))), 158 | ("/x7/suffix{a}", Err(conflict("/x7/prefix{a}suffix"))), 159 | ("/x7/prefix{a}/", Err(conflict("/x7/prefix{a}suffix"))), 160 | ("/x7/prefix{a}suff", Err(conflict("/x7/prefix{a}suffix"))), 161 | ("/x7/prefix{a}suffix", Err(conflict("/x7/prefix{a}suffix"))), 162 | ("/x7/prefix{a}suffixy", Err(conflict("/x7/prefix{a}suffix"))), 163 | ("/x7/prefix1{a}", Err(conflict("/x7/prefix{a}suffix"))), 164 | ("/x7/prefix{a}/", Err(conflict("/x7/prefix{a}suffix"))), 165 | ("/x7/{a}suffix/", Err(conflict("/x7/prefix{a}suffix"))), 166 | ("/x7/prefix{a}suffix/", Ok(())), 167 | ("/x7/{a}", Ok(())), 168 | ("/x7/{a}/", Ok(())), 169 | ("/x8/prefix{a}suffix", Ok(())), 170 | ("/x8/{a}", Ok(())), 171 | ("/x8/{a}suff", Err(conflict("/x8/prefix{a}suffix"))), 172 | ("/x8/{a}suffix", Err(conflict("/x8/prefix{a}suffix"))), 173 | ("/x8/{a}suffixy", Err(conflict("/x8/prefix{a}suffix"))), 174 | ("/x8/prefix{a}", Err(conflict("/x8/prefix{a}suffix"))), 175 | ("/x8/prefix{a}/", Err(conflict("/x8/prefix{a}suffix"))), 176 | ("/x8/prefix{a}suff", Err(conflict("/x8/prefix{a}suffix"))), 177 | ("/x8/prefix{a}suffix", Err(conflict("/x8/prefix{a}suffix"))), 178 | ("/x8/prefix{a}suffixy", Err(conflict("/x8/prefix{a}suffix"))), 179 | ("/x8/prefix1{a}", Err(conflict("/x8/prefix{a}suffix"))), 180 | ("/x8/prefix{a}/", Err(conflict("/x8/prefix{a}suffix"))), 181 | ("/x8/{a}suffix/", Err(conflict("/x8/prefix{a}suffix"))), 182 | ("/x8/prefix{a}suffix/", Ok(())), 183 | ("/x8/{a}/", Ok(())), 184 | ("/x9/prefix{a}", Ok(())), 185 | ("/x9/{a}suffix", Err(conflict("/x9/prefix{a}"))), 186 | ("/x9/prefix{a}suffix", Err(conflict("/x9/prefix{a}"))), 187 | ("/x9/prefixabc{a}suffix", Err(conflict("/x9/prefix{a}"))), 188 | ("/x9/pre{a}suffix", Err(conflict("/x9/prefix{a}"))), 189 | ("/x10/{a}", Ok(())), 190 | ("/x10/prefix{a}", Ok(())), 191 | ("/x10/{a}suffix", Err(conflict("/x10/prefix{a}"))), 192 | ("/x10/prefix{a}suffix", Err(conflict("/x10/prefix{a}"))), 193 | ("/x10/prefixabc{a}suffix", Err(conflict("/x10/prefix{a}"))), 194 | ("/x10/pre{a}suffix", Err(conflict("/x10/prefix{a}"))), 195 | ("/x11/{a}", Ok(())), 196 | ("/x11/{a}suffix", Ok(())), 197 | ("/x11/prx11fix{a}", Err(conflict("/x11/{a}suffix"))), 198 | ("/x11/prx11fix{a}suff", Err(conflict("/x11/{a}suffix"))), 199 | ("/x11/prx11fix{a}suffix", Err(conflict("/x11/{a}suffix"))), 200 | ("/x11/prx11fix{a}suffixabc", Err(conflict("/x11/{a}suffix"))), 201 | ("/x12/prefix{a}suffix", Ok(())), 202 | ("/x12/pre{a}", Err(conflict("/x12/prefix{a}suffix"))), 203 | ("/x12/prefix{a}", Err(conflict("/x12/prefix{a}suffix"))), 204 | ("/x12/prefixabc{a}", Err(conflict("/x12/prefix{a}suffix"))), 205 | ("/x12/pre{a}suffix", Err(conflict("/x12/prefix{a}suffix"))), 206 | ( 207 | "/x12/prefix{a}suffix", 208 | Err(conflict("/x12/prefix{a}suffix")), 209 | ), 210 | ( 211 | "/x12/prefixabc{a}suffix", 212 | Err(conflict("/x12/prefix{a}suffix")), 213 | ), 214 | ("/x12/prefix{a}suff", Err(conflict("/x12/prefix{a}suffix"))), 215 | ( 216 | "/x12/prefix{a}suffix", 217 | Err(conflict("/x12/prefix{a}suffix")), 218 | ), 219 | ( 220 | "/x12/prefix{a}suffixabc", 221 | Err(conflict("/x12/prefix{a}suffix")), 222 | ), 223 | ("/x12/{a}suff", Err(conflict("/x12/prefix{a}suffix"))), 224 | ("/x12/{a}suffix", Err(conflict("/x12/prefix{a}suffix"))), 225 | ("/x12/{a}suffixabc", Err(conflict("/x12/prefix{a}suffix"))), 226 | ("/x13/{a}", Ok(())), 227 | ("/x13/prefix{a}suffix", Ok(())), 228 | ("/x13/pre{a}", Err(conflict("/x13/prefix{a}suffix"))), 229 | ("/x13/prefix{a}", Err(conflict("/x13/prefix{a}suffix"))), 230 | ("/x13/prefixabc{a}", Err(conflict("/x13/prefix{a}suffix"))), 231 | ("/x13/pre{a}suffix", Err(conflict("/x13/prefix{a}suffix"))), 232 | ( 233 | "/x13/prefix{a}suffix", 234 | Err(conflict("/x13/prefix{a}suffix")), 235 | ), 236 | ( 237 | "/x13/prefixabc{a}suffix", 238 | Err(conflict("/x13/prefix{a}suffix")), 239 | ), 240 | ("/x13/prefix{a}suff", Err(conflict("/x13/prefix{a}suffix"))), 241 | ( 242 | "/x13/prefix{a}suffix", 243 | Err(conflict("/x13/prefix{a}suffix")), 244 | ), 245 | ( 246 | "/x13/prefix{a}suffixabc", 247 | Err(conflict("/x13/prefix{a}suffix")), 248 | ), 249 | ("/x13/{a}suff", Err(conflict("/x13/prefix{a}suffix"))), 250 | ("/x13/{a}suffix", Err(conflict("/x13/prefix{a}suffix"))), 251 | ("/x13/{a}suffixabc", Err(conflict("/x13/prefix{a}suffix"))), 252 | ("/x15/{*rest}", Ok(())), 253 | ("/x15/{a}suffix", Err(conflict("/x15/{*rest}"))), 254 | ("/x15/{a}suffix", Err(conflict("/x15/{*rest}"))), 255 | ("/x15/prefix{a}", Ok(())), 256 | ("/x16/{*rest}", Ok(())), 257 | ("/x16/prefix{a}suffix", Ok(())), 258 | ]) 259 | .run() 260 | } 261 | 262 | #[test] 263 | fn invalid_catchall() { 264 | InsertTest(vec![ 265 | ("/non-leading-{*catchall}", Ok(())), 266 | ("/foo/bar{*catchall}", Ok(())), 267 | ("/src/{*filepath}x", Err(InsertError::InvalidCatchAll)), 268 | ("/src/{*filepath}/x", Err(InsertError::InvalidCatchAll)), 269 | ("/src2/", Ok(())), 270 | ("/src2/{*filepath}/x", Err(InsertError::InvalidCatchAll)), 271 | ]) 272 | .run() 273 | } 274 | 275 | #[test] 276 | fn catchall_root_conflict() { 277 | InsertTest(vec![("/", Ok(())), ("/{*filepath}", Ok(()))]).run() 278 | } 279 | 280 | #[test] 281 | fn child_conflict() { 282 | InsertTest(vec![ 283 | ("/cmd/vet", Ok(())), 284 | ("/cmd/{tool}", Ok(())), 285 | ("/cmd/{tool}/{sub}", Ok(())), 286 | ("/cmd/{tool}/misc", Ok(())), 287 | ("/cmd/{tool}/{bad}", Err(conflict("/cmd/{tool}/{sub}"))), 288 | ("/src/AUTHORS", Ok(())), 289 | ("/src/{*filepath}", Ok(())), 290 | ("/user_x", Ok(())), 291 | ("/user_{name}", Ok(())), 292 | ("/id/{id}", Ok(())), 293 | ("/id{id}", Ok(())), 294 | ("/{id}", Ok(())), 295 | ("/{*filepath}", Err(conflict("/{id}"))), 296 | ]) 297 | .run() 298 | } 299 | 300 | #[test] 301 | fn duplicates() { 302 | InsertTest(vec![ 303 | ("/", Ok(())), 304 | ("/", Err(conflict("/"))), 305 | ("/doc/", Ok(())), 306 | ("/doc/", Err(conflict("/doc/"))), 307 | ("/src/{*filepath}", Ok(())), 308 | ("/src/{*filepath}", Err(conflict("/src/{*filepath}"))), 309 | ("/search/{query}", Ok(())), 310 | ("/search/{query}", Err(conflict("/search/{query}"))), 311 | ("/user_{name}", Ok(())), 312 | ("/user_{name}", Err(conflict("/user_{name}"))), 313 | ]) 314 | .run() 315 | } 316 | 317 | #[test] 318 | fn unnamed_param() { 319 | InsertTest(vec![ 320 | ("/{}", Err(InsertError::InvalidParam)), 321 | ("/user{}/", Err(InsertError::InvalidParam)), 322 | ("/cmd/{}/", Err(InsertError::InvalidParam)), 323 | ("/src/{*}", Err(InsertError::InvalidParam)), 324 | ]) 325 | .run() 326 | } 327 | 328 | #[test] 329 | fn double_params() { 330 | InsertTest(vec![ 331 | ("/{foo}{bar}", Err(InsertError::InvalidParamSegment)), 332 | ("/{foo}{bar}/", Err(InsertError::InvalidParamSegment)), 333 | ("/{foo}{{*bar}/", Err(InsertError::InvalidParamSegment)), 334 | ]) 335 | .run() 336 | } 337 | 338 | #[test] 339 | fn normalized_conflict() { 340 | InsertTest(vec![ 341 | ("/x/{foo}/bar", Ok(())), 342 | ("/x/{bar}/bar", Err(conflict("/x/{foo}/bar"))), 343 | ("/{y}/bar/baz", Ok(())), 344 | ("/{y}/baz/baz", Ok(())), 345 | ("/{z}/bar/bat", Ok(())), 346 | ("/{z}/bar/baz", Err(conflict("/{y}/bar/baz"))), 347 | ]) 348 | .run() 349 | } 350 | 351 | #[test] 352 | fn more_conflicts() { 353 | InsertTest(vec![ 354 | ("/con{tact}", Ok(())), 355 | ("/who/are/{*you}", Ok(())), 356 | ("/who/foo/hello", Ok(())), 357 | ("/whose/{users}/{name}", Ok(())), 358 | ("/who/are/foo", Ok(())), 359 | ("/who/are/foo/bar", Ok(())), 360 | ("/con{nection}", Err(conflict("/con{tact}"))), 361 | ( 362 | "/whose/{users}/{user}", 363 | Err(conflict("/whose/{users}/{name}")), 364 | ), 365 | ]) 366 | .run() 367 | } 368 | 369 | #[test] 370 | fn catchall_static_overlap() { 371 | InsertTest(vec![ 372 | ("/bar", Ok(())), 373 | ("/bar/", Ok(())), 374 | ("/bar/{*foo}", Ok(())), 375 | ]) 376 | .run(); 377 | 378 | InsertTest(vec![ 379 | ("/foo", Ok(())), 380 | ("/{*bar}", Ok(())), 381 | ("/bar", Ok(())), 382 | ("/baz", Ok(())), 383 | ("/baz/{split}", Ok(())), 384 | ("/", Ok(())), 385 | ("/{*bar}", Err(conflict("/{*bar}"))), 386 | ("/{*zzz}", Err(conflict("/{*bar}"))), 387 | ("/{xxx}", Err(conflict("/{*bar}"))), 388 | ]) 389 | .run(); 390 | 391 | InsertTest(vec![ 392 | ("/{*bar}", Ok(())), 393 | ("/bar", Ok(())), 394 | ("/bar/x", Ok(())), 395 | ("/bar_{x}", Ok(())), 396 | ("/bar_{x}", Err(conflict("/bar_{x}"))), 397 | ("/bar_{x}/y", Ok(())), 398 | ("/bar/{x}", Ok(())), 399 | ]) 400 | .run(); 401 | } 402 | 403 | #[test] 404 | fn duplicate_conflict() { 405 | InsertTest(vec![ 406 | ("/hey", Ok(())), 407 | ("/hey/users", Ok(())), 408 | ("/hey/user", Ok(())), 409 | ("/hey/user", Err(conflict("/hey/user"))), 410 | ]) 411 | .run() 412 | } 413 | 414 | #[test] 415 | fn invalid_param() { 416 | InsertTest(vec![ 417 | ("{", Err(InsertError::InvalidParam)), 418 | ("}", Err(InsertError::InvalidParam)), 419 | ("x{y", Err(InsertError::InvalidParam)), 420 | ("x}", Err(InsertError::InvalidParam)), 421 | ]) 422 | .run(); 423 | } 424 | 425 | #[test] 426 | fn escaped_param() { 427 | InsertTest(vec![ 428 | ("{{", Ok(())), 429 | ("}}", Ok(())), 430 | ("xx}}", Ok(())), 431 | ("}}yy", Ok(())), 432 | ("}}yy{{}}", Ok(())), 433 | ("}}yy{{}}{{}}y{{", Ok(())), 434 | ("}}yy{{}}{{}}y{{", Err(conflict("}yy{}{}y{"))), 435 | ("/{{yy", Ok(())), 436 | ("/{yy}", Ok(())), 437 | ("/foo", Ok(())), 438 | ("/foo/{{", Ok(())), 439 | ("/foo/{{/{x}", Ok(())), 440 | ("/foo/{ba{{r}", Ok(())), 441 | ("/bar/{ba}}r}", Ok(())), 442 | ("/xxx/{x{{}}y}", Ok(())), 443 | ]) 444 | .run() 445 | } 446 | 447 | #[test] 448 | fn bare_catchall() { 449 | InsertTest(vec![("{*foo}", Ok(())), ("foo/{*bar}", Ok(()))]).run() 450 | } 451 | -------------------------------------------------------------------------------- /tests/match.rs: -------------------------------------------------------------------------------- 1 | use matchit::{MatchError, Router}; 2 | 3 | // https://github.com/ibraheemdev/matchit/issues/22 4 | #[test] 5 | fn partial_overlap() { 6 | let mut x = Router::new(); 7 | x.insert("/foo_bar", "Welcome!").unwrap(); 8 | x.insert("/foo/bar", "Welcome!").unwrap(); 9 | assert_eq!(x.at("/foo/").unwrap_err(), MatchError::NotFound); 10 | 11 | let mut x = Router::new(); 12 | x.insert("/foo", "Welcome!").unwrap(); 13 | x.insert("/foo/bar", "Welcome!").unwrap(); 14 | assert_eq!(x.at("/foo/").unwrap_err(), MatchError::NotFound); 15 | } 16 | 17 | // https://github.com/ibraheemdev/matchit/issues/31 18 | #[test] 19 | fn wildcard_overlap() { 20 | let mut router = Router::new(); 21 | router.insert("/path/foo", "foo").unwrap(); 22 | router.insert("/path/{*rest}", "wildcard").unwrap(); 23 | 24 | assert_eq!(router.at("/path/foo").map(|m| *m.value), Ok("foo")); 25 | assert_eq!(router.at("/path/bar").map(|m| *m.value), Ok("wildcard")); 26 | assert_eq!(router.at("/path/foo/").map(|m| *m.value), Ok("wildcard")); 27 | 28 | let mut router = Router::new(); 29 | router.insert("/path/foo/{arg}", "foo").unwrap(); 30 | router.insert("/path/{*rest}", "wildcard").unwrap(); 31 | 32 | assert_eq!(router.at("/path/foo/myarg").map(|m| *m.value), Ok("foo")); 33 | assert_eq!( 34 | router.at("/path/foo/myarg/").map(|m| *m.value), 35 | Ok("wildcard") 36 | ); 37 | assert_eq!( 38 | router.at("/path/foo/myarg/bar/baz").map(|m| *m.value), 39 | Ok("wildcard") 40 | ); 41 | } 42 | 43 | // https://github.com/ibraheemdev/matchit/issues/12 44 | #[test] 45 | fn overlapping_param_backtracking() { 46 | let mut matcher = Router::new(); 47 | 48 | matcher.insert("/{object}/{id}", "object with id").unwrap(); 49 | matcher 50 | .insert("/secret/{id}/path", "secret with id and path") 51 | .unwrap(); 52 | 53 | let matched = matcher.at("/secret/978/path").unwrap(); 54 | assert_eq!(matched.params.get("id"), Some("978")); 55 | 56 | let matched = matcher.at("/something/978").unwrap(); 57 | assert_eq!(matched.params.get("id"), Some("978")); 58 | assert_eq!(matched.params.get("object"), Some("something")); 59 | 60 | let matched = matcher.at("/secret/978").unwrap(); 61 | assert_eq!(matched.params.get("id"), Some("978")); 62 | } 63 | 64 | #[allow(clippy::type_complexity)] 65 | struct MatchTest { 66 | routes: Vec<&'static str>, 67 | matches: Vec<( 68 | &'static str, 69 | &'static str, 70 | Result, ()>, 71 | )>, 72 | } 73 | 74 | impl MatchTest { 75 | fn run(self) { 76 | let mut router = Router::new(); 77 | 78 | for route in self.routes { 79 | assert_eq!(router.insert(route, route.to_owned()), Ok(()), "{route}"); 80 | } 81 | 82 | router.check_priorities().unwrap(); 83 | 84 | for (path, route, params) in self.matches { 85 | match router.at(path) { 86 | Ok(x) => { 87 | assert_eq!(x.value, route); 88 | 89 | let got = x.params.iter().collect::>(); 90 | assert_eq!(params.unwrap(), got); 91 | 92 | router.at_mut(path).unwrap().value.push('Z'); 93 | assert!(router.at(path).unwrap().value.contains('Z')); 94 | router.at_mut(path).unwrap().value.pop(); 95 | } 96 | Err(err) => { 97 | if let Ok(params) = params { 98 | panic!("{err} for {path} ({params:?})"); 99 | } 100 | } 101 | } 102 | } 103 | } 104 | } 105 | 106 | macro_rules! p { 107 | ($($k:expr => $v:expr),* $(,)?) => { 108 | Ok(vec![$(($k, $v)),*]) 109 | }; 110 | } 111 | 112 | // https://github.com/ibraheemdev/matchit/issues/75 113 | #[test] 114 | fn empty_route() { 115 | MatchTest { 116 | routes: vec!["", "/foo"], 117 | matches: vec![("", "", p! {}), ("/foo", "/foo", p! {})], 118 | } 119 | .run() 120 | } 121 | 122 | // https://github.com/ibraheemdev/matchit/issues/42 123 | #[test] 124 | fn bare_catchall() { 125 | MatchTest { 126 | routes: vec!["{*foo}", "foo/{*bar}"], 127 | matches: vec![ 128 | ("x/y", "{*foo}", p! { "foo" => "x/y" }), 129 | ("/x/y", "{*foo}", p! { "foo" => "/x/y" }), 130 | ("/foo/x/y", "{*foo}", p! { "foo" => "/foo/x/y" }), 131 | ("foo/x/y", "foo/{*bar}", p! { "bar" => "x/y" }), 132 | ], 133 | } 134 | .run() 135 | } 136 | 137 | // https://github.com/ibraheemdev/matchit/issues/83 138 | #[test] 139 | fn param_suffix_flag_issue() { 140 | MatchTest { 141 | routes: vec!["/foo/{foo}suffix", "/foo/{foo}/bar"], 142 | matches: vec![("/foo/barsuffix", "/foo/{foo}suffix", p! { "foo" => "bar" })], 143 | } 144 | .run() 145 | } 146 | 147 | #[test] 148 | fn normalized() { 149 | MatchTest { 150 | routes: vec![ 151 | "/x/{foo}/bar", 152 | "/x/{bar}/baz", 153 | "/{foo}/{baz}/bax", 154 | "/{foo}/{bar}/baz", 155 | "/{fod}/{baz}/{bax}/foo", 156 | "/{fod}/baz/bax/foo", 157 | "/{foo}/baz/bax", 158 | "/{bar}/{bay}/bay", 159 | "/s", 160 | "/s/s", 161 | "/s/s/s", 162 | "/s/s/s/s", 163 | "/s/s/{s}/x", 164 | "/s/s/{y}/d", 165 | ], 166 | matches: vec![ 167 | ("/x/foo/bar", "/x/{foo}/bar", p! { "foo" => "foo" }), 168 | ("/x/foo/baz", "/x/{bar}/baz", p! { "bar" => "foo" }), 169 | ( 170 | "/y/foo/baz", 171 | "/{foo}/{bar}/baz", 172 | p! { "foo" => "y", "bar" => "foo" }, 173 | ), 174 | ( 175 | "/y/foo/bax", 176 | "/{foo}/{baz}/bax", 177 | p! { "foo" => "y", "baz" => "foo" }, 178 | ), 179 | ( 180 | "/y/baz/baz", 181 | "/{foo}/{bar}/baz", 182 | p! { "foo" => "y", "bar" => "baz" }, 183 | ), 184 | ("/y/baz/bax/foo", "/{fod}/baz/bax/foo", p! { "fod" => "y" }), 185 | ( 186 | "/y/baz/b/foo", 187 | "/{fod}/{baz}/{bax}/foo", 188 | p! { "fod" => "y", "baz" => "baz", "bax" => "b" }, 189 | ), 190 | ("/y/baz/bax", "/{foo}/baz/bax", p! { "foo" => "y" }), 191 | ( 192 | "/z/bar/bay", 193 | "/{bar}/{bay}/bay", 194 | p! { "bar" => "z", "bay" => "bar" }, 195 | ), 196 | ("/s", "/s", p! {}), 197 | ("/s/s", "/s/s", p! {}), 198 | ("/s/s/s", "/s/s/s", p! {}), 199 | ("/s/s/s/s", "/s/s/s/s", p! {}), 200 | ("/s/s/s/x", "/s/s/{s}/x", p! { "s" => "s" }), 201 | ("/s/s/s/d", "/s/s/{y}/d", p! { "y" => "s" }), 202 | ], 203 | } 204 | .run() 205 | } 206 | 207 | #[test] 208 | fn blog() { 209 | MatchTest { 210 | routes: vec![ 211 | "/{page}", 212 | "/posts/{year}/{month}/{post}", 213 | "/posts/{year}/{month}/index", 214 | "/posts/{year}/top", 215 | "/static/{*path}", 216 | "/favicon.ico", 217 | ], 218 | matches: vec![ 219 | ("/about", "/{page}", p! { "page" => "about" }), 220 | ( 221 | "/posts/2021/01/rust", 222 | "/posts/{year}/{month}/{post}", 223 | p! { "year" => "2021", "month" => "01", "post" => "rust" }, 224 | ), 225 | ( 226 | "/posts/2021/01/index", 227 | "/posts/{year}/{month}/index", 228 | p! { "year" => "2021", "month" => "01" }, 229 | ), 230 | ( 231 | "/posts/2021/top", 232 | "/posts/{year}/top", 233 | p! { "year" => "2021" }, 234 | ), 235 | ( 236 | "/static/foo.png", 237 | "/static/{*path}", 238 | p! { "path" => "foo.png" }, 239 | ), 240 | ("/favicon.ico", "/favicon.ico", p! {}), 241 | ], 242 | } 243 | .run() 244 | } 245 | 246 | #[test] 247 | fn double_overlap() { 248 | MatchTest { 249 | routes: vec![ 250 | "/{object}/{id}", 251 | "/secret/{id}/path", 252 | "/secret/978", 253 | "/other/{object}/{id}/", 254 | "/other/an_object/{id}", 255 | "/other/static/path", 256 | "/other/long/static/path/", 257 | ], 258 | matches: vec![ 259 | ( 260 | "/secret/978/path", 261 | "/secret/{id}/path", 262 | p! { "id" => "978" }, 263 | ), 264 | ( 265 | "/some_object/978", 266 | "/{object}/{id}", 267 | p! { "object" => "some_object", "id" => "978" }, 268 | ), 269 | ("/secret/978", "/secret/978", p! {}), 270 | ("/super_secret/978/", "/{object}/{id}", Err(())), 271 | ( 272 | "/other/object/1/", 273 | "/other/{object}/{id}/", 274 | p! { "object" => "object", "id" => "1" }, 275 | ), 276 | ("/other/object/1/2", "/other/{object}/{id}", Err(())), 277 | ( 278 | "/other/an_object/1", 279 | "/other/an_object/{id}", 280 | p! { "id" => "1" }, 281 | ), 282 | ("/other/static/path", "/other/static/path", p! {}), 283 | ( 284 | "/other/long/static/path/", 285 | "/other/long/static/path/", 286 | p! {}, 287 | ), 288 | ], 289 | } 290 | .run() 291 | } 292 | 293 | #[test] 294 | fn catchall_off_by_one() { 295 | MatchTest { 296 | routes: vec!["/foo/{*catchall}", "/bar", "/bar/", "/bar/{*catchall}"], 297 | matches: vec![ 298 | ("/foo", "", Err(())), 299 | ("/foo/", "", Err(())), 300 | ("/foo/x", "/foo/{*catchall}", p! { "catchall" => "x" }), 301 | ("/bar", "/bar", p! {}), 302 | ("/bar/", "/bar/", p! {}), 303 | ("/bar/x", "/bar/{*catchall}", p! { "catchall" => "x" }), 304 | ], 305 | } 306 | .run() 307 | } 308 | 309 | #[test] 310 | fn overlap() { 311 | MatchTest { 312 | routes: vec![ 313 | "/foo", 314 | "/bar", 315 | "/{*bar}", 316 | "/baz", 317 | "/baz/", 318 | "/baz/x", 319 | "/baz/{xxx}", 320 | "/", 321 | "/xxx/{*x}", 322 | "/xxx/", 323 | ], 324 | matches: vec![ 325 | ("/foo", "/foo", p! {}), 326 | ("/bar", "/bar", p! {}), 327 | ("/baz", "/baz", p! {}), 328 | ("/baz/", "/baz/", p! {}), 329 | ("/baz/x", "/baz/x", p! {}), 330 | ("/???", "/{*bar}", p! { "bar" => "???" }), 331 | ("/", "/", p! {}), 332 | ("", "", Err(())), 333 | ("/xxx/y", "/xxx/{*x}", p! { "x" => "y" }), 334 | ("/xxx/", "/xxx/", p! {}), 335 | ("/xxx", "/{*bar}", p! { "bar" => "xxx" }), 336 | ], 337 | } 338 | .run() 339 | } 340 | 341 | #[test] 342 | fn missing_trailing_slash_param() { 343 | MatchTest { 344 | routes: vec!["/foo/{object}/{id}", "/foo/bar/baz", "/foo/secret/978/"], 345 | matches: vec![ 346 | ("/foo/secret/978/", "/foo/secret/978/", p! {}), 347 | ( 348 | "/foo/secret/978", 349 | "/foo/{object}/{id}", 350 | p! { "object" => "secret", "id" => "978" }, 351 | ), 352 | ], 353 | } 354 | .run() 355 | } 356 | 357 | #[test] 358 | fn extra_trailing_slash_param() { 359 | MatchTest { 360 | routes: vec!["/foo/{object}/{id}", "/foo/bar/baz", "/foo/secret/978"], 361 | matches: vec![ 362 | ("/foo/secret/978/", "", Err(())), 363 | ("/foo/secret/978", "/foo/secret/978", p! {}), 364 | ], 365 | } 366 | .run() 367 | } 368 | 369 | #[test] 370 | fn missing_trailing_slash_catch_all() { 371 | MatchTest { 372 | routes: vec!["/foo/{*bar}", "/foo/bar/baz", "/foo/secret/978/"], 373 | matches: vec![ 374 | ( 375 | "/foo/secret/978", 376 | "/foo/{*bar}", 377 | p! { "bar" => "secret/978" }, 378 | ), 379 | ("/foo/secret/978/", "/foo/secret/978/", p! {}), 380 | ], 381 | } 382 | .run() 383 | } 384 | 385 | #[test] 386 | fn extra_trailing_slash_catch_all() { 387 | MatchTest { 388 | routes: vec!["/foo/{*bar}", "/foo/bar/baz", "/foo/secret/978"], 389 | matches: vec![ 390 | ( 391 | "/foo/secret/978/", 392 | "/foo/{*bar}", 393 | p! { "bar" => "secret/978/" }, 394 | ), 395 | ("/foo/secret/978", "/foo/secret/978", p! {}), 396 | ], 397 | } 398 | .run() 399 | } 400 | 401 | #[test] 402 | fn double_overlap_trailing_slash() { 403 | MatchTest { 404 | routes: vec![ 405 | "/{object}/{id}", 406 | "/secret/{id}/path", 407 | "/secret/978/", 408 | "/other/{object}/{id}/", 409 | "/other/an_object/{id}", 410 | "/other/static/path", 411 | "/other/long/static/path/", 412 | ], 413 | matches: vec![ 414 | ("/secret/978/path/", "", Err(())), 415 | ("/object/id/", "", Err(())), 416 | ("/object/id/path", "", Err(())), 417 | ("/other/object/1", "", Err(())), 418 | ("/other/object/1/2", "", Err(())), 419 | ( 420 | "/other/an_object/1/", 421 | "/other/{object}/{id}/", 422 | p! { "object" => "an_object", "id" => "1" }, 423 | ), 424 | ( 425 | "/other/static/path/", 426 | "/other/{object}/{id}/", 427 | p! { "object" => "static", "id" => "path" }, 428 | ), 429 | ("/other/long/static/path", "", Err(())), 430 | ("/other/object/static/path", "", Err(())), 431 | ], 432 | } 433 | .run() 434 | } 435 | 436 | #[test] 437 | fn trailing_slash_overlap() { 438 | MatchTest { 439 | routes: vec!["/foo/{x}/baz/", "/foo/{x}/baz", "/foo/bar/bar"], 440 | matches: vec![ 441 | ("/foo/x/baz/", "/foo/{x}/baz/", p! { "x" => "x" }), 442 | ("/foo/x/baz", "/foo/{x}/baz", p! { "x" => "x" }), 443 | ("/foo/bar/bar", "/foo/bar/bar", p! {}), 444 | ], 445 | } 446 | .run() 447 | } 448 | 449 | #[test] 450 | fn trailing_slash() { 451 | MatchTest { 452 | routes: vec![ 453 | "/hi", 454 | "/b/", 455 | "/search/{query}", 456 | "/cmd/{tool}/", 457 | "/src/{*filepath}", 458 | "/x", 459 | "/x/y", 460 | "/y/", 461 | "/y/z", 462 | "/0/{id}", 463 | "/0/{id}/1", 464 | "/1/{id}/", 465 | "/1/{id}/2", 466 | "/aa", 467 | "/a/", 468 | "/admin", 469 | "/admin/static", 470 | "/admin/{category}", 471 | "/admin/{category}/{page}", 472 | "/doc", 473 | "/doc/rust_faq.html", 474 | "/doc/rust1.26.html", 475 | "/no/a", 476 | "/no/b", 477 | "/no/a/b/{*other}", 478 | "/api/{page}/{name}", 479 | "/api/hello/{name}/bar/", 480 | "/api/bar/{name}", 481 | "/api/baz/foo", 482 | "/api/baz/foo/bar", 483 | "/foo/{p}", 484 | ], 485 | matches: vec![ 486 | ("/hi/", "", Err(())), 487 | ("/b", "", Err(())), 488 | ("/search/rustacean/", "", Err(())), 489 | ("/cmd/vet", "", Err(())), 490 | ("/src", "", Err(())), 491 | ("/src/", "", Err(())), 492 | ("/x/", "", Err(())), 493 | ("/y", "", Err(())), 494 | ("/0/rust/", "", Err(())), 495 | ("/1/rust", "", Err(())), 496 | ("/a", "", Err(())), 497 | ("/admin/", "", Err(())), 498 | ("/doc/", "", Err(())), 499 | ("/admin/static/", "", Err(())), 500 | ("/admin/cfg/", "", Err(())), 501 | ("/admin/cfg/users/", "", Err(())), 502 | ("/api/hello/x/bar", "", Err(())), 503 | ("/api/baz/foo/", "", Err(())), 504 | ("/api/baz/bax/", "", Err(())), 505 | ("/api/bar/huh/", "", Err(())), 506 | ("/api/baz/foo/bar/", "", Err(())), 507 | ("/api/world/abc/", "", Err(())), 508 | ("/foo/pp/", "", Err(())), 509 | ("/", "", Err(())), 510 | ("/no", "", Err(())), 511 | ("/no/", "", Err(())), 512 | ("/no/a/b", "", Err(())), 513 | ("/no/a/b/", "", Err(())), 514 | ("/_", "", Err(())), 515 | ("/_/", "", Err(())), 516 | ("/api", "", Err(())), 517 | ("/api/", "", Err(())), 518 | ("/api/hello/x/foo", "", Err(())), 519 | ("/api/baz/foo/bad", "", Err(())), 520 | ("/foo/p/p", "", Err(())), 521 | ], 522 | } 523 | .run() 524 | } 525 | 526 | #[test] 527 | fn backtracking_trailing_slash() { 528 | MatchTest { 529 | routes: vec!["/a/{b}/{c}", "/a/b/{c}/d/"], 530 | matches: vec![("/a/b/c/d", "", Err(()))], 531 | } 532 | .run() 533 | } 534 | 535 | #[test] 536 | fn root_trailing_slash() { 537 | MatchTest { 538 | routes: vec!["/foo", "/bar", "/{baz}"], 539 | matches: vec![("/", "", Err(()))], 540 | } 541 | .run() 542 | } 543 | 544 | #[test] 545 | fn catchall_overlap() { 546 | MatchTest { 547 | routes: vec!["/yyy/{*x}", "/yyy{*x}"], 548 | matches: vec![ 549 | ("/yyy/y", "/yyy/{*x}", p! { "x" => "y" }), 550 | ("/yyy/", "/yyy{*x}", p! { "x" => "/" }), 551 | ], 552 | } 553 | .run(); 554 | } 555 | 556 | #[test] 557 | fn escaped() { 558 | MatchTest { 559 | routes: vec![ 560 | "/", 561 | "/{{", 562 | "/}}", 563 | "/{{x", 564 | "/}}y{{", 565 | "/xy{{", 566 | "/{{/xyz", 567 | "/{ba{{r}", 568 | "/{ba{{r}/", 569 | "/{ba{{r}/x", 570 | "/baz/{xxx}", 571 | "/baz/{xxx}/xy{{", 572 | "/baz/{xxx}/}}xy{{{{", 573 | "/{{/{x}", 574 | "/xxx/", 575 | "/xxx/{x}}{{}}}}{{}}{{{{}}y}", 576 | ], 577 | matches: vec![ 578 | ("/", "/", p! {}), 579 | ("/{", "/{{", p! {}), 580 | ("/}", "/}}", p! {}), 581 | ("/{x", "/{{x", p! {}), 582 | ("/}y{", "/}}y{{", p! {}), 583 | ("/xy{", "/xy{{", p! {}), 584 | ("/{/xyz", "/{{/xyz", p! {}), 585 | ("/foo", "/{ba{{r}", p! { "ba{r" => "foo" }), 586 | ("/{{", "/{ba{{r}", p! { "ba{r" => "{{" }), 587 | ("/{{}}/", "/{ba{{r}/", p! { "ba{r" => "{{}}" }), 588 | ("/{{}}{{/x", "/{ba{{r}/x", p! { "ba{r" => "{{}}{{" }), 589 | ("/baz/x", "/baz/{xxx}", p! { "xxx" => "x" }), 590 | ("/baz/x/xy{", "/baz/{xxx}/xy{{", p! { "xxx" => "x" }), 591 | ("/baz/x/xy{{", "", Err(())), 592 | ("/baz/x/}xy{{", "/baz/{xxx}/}}xy{{{{", p! { "xxx" => "x" }), 593 | ("/{/{{", "/{{/{x}", p! { "x" => "{{" }), 594 | ("/xxx", "/{ba{{r}", p! { "ba{r" => "xxx" }), 595 | ("/xxx/", "/xxx/", p!()), 596 | ( 597 | "/xxx/foo", 598 | "/xxx/{x}}{{}}}}{{}}{{{{}}y}", 599 | p! { "x}{}}{}{{}y" => "foo" }, 600 | ), 601 | ], 602 | } 603 | .run() 604 | } 605 | 606 | #[test] 607 | fn empty_param() { 608 | MatchTest { 609 | routes: vec![ 610 | "/y/{foo}", 611 | "/x/{foo}/z", 612 | "/z/{*foo}", 613 | "/a/x{foo}", 614 | "/b/{foo}x", 615 | ], 616 | matches: vec![ 617 | ("/y/", "", Err(())), 618 | ("/x//z", "", Err(())), 619 | ("/z/", "", Err(())), 620 | ("/a/x", "", Err(())), 621 | ("/b/x", "", Err(())), 622 | ], 623 | } 624 | .run(); 625 | } 626 | 627 | #[test] 628 | fn wildcard_suffix() { 629 | MatchTest { 630 | routes: vec!["/", "/{foo}x", "/foox", "/{foo}x/bar", "/{foo}x/bar/baz"], 631 | matches: vec![ 632 | ("/", "/", p! {}), 633 | ("/foox", "/foox", p! {}), 634 | ("/barx", "/{foo}x", p! { "foo" => "bar" }), 635 | ("/mx", "/{foo}x", p! { "foo" => "m" }), 636 | ("/mx/", "", Err(())), 637 | ("/mxm", "", Err(())), 638 | ("/mx/bar", "/{foo}x/bar", p! { "foo" => "m" }), 639 | ("/mxm/bar", "", Err(())), 640 | ("/x", "", Err(())), 641 | ("/xfoo", "", Err(())), 642 | ("/xfoox", "/{foo}x", p! { "foo" => "xfoo" }), 643 | ("/xfoox/bar", "/{foo}x/bar", p! { "foo" => "xfoo" }), 644 | ("/xfoox/bar/baz", "/{foo}x/bar/baz", p! { "foo" => "xfoo" }), 645 | ], 646 | } 647 | .run(); 648 | } 649 | 650 | #[test] 651 | fn mixed_wildcard_suffix() { 652 | MatchTest { 653 | routes: vec![ 654 | "/", 655 | "/{f}o/b", 656 | "/{f}oo/b", 657 | "/{f}ooo/b", 658 | "/{f}oooo/b", 659 | "/foo/b", 660 | "/foo/{b}", 661 | "/foo/{b}one", 662 | "/foo/{b}one/", 663 | "/foo/{b}two", 664 | "/foo/{b}/one", 665 | "/foo/{b}one/one", 666 | "/foo/{b}two/one", 667 | "/foo/{b}one/one/", 668 | "/bar/{b}one", 669 | "/bar/{b}", 670 | "/bar/{b}/baz", 671 | "/bar/{b}one/baz", 672 | "/baz/{b}/bar", 673 | "/baz/{b}one/bar", 674 | ], 675 | matches: vec![ 676 | ("/", "/", p! {}), 677 | ("/o/b", "", Err(())), 678 | ("/fo/b", "/{f}o/b", p! { "f" => "f" }), 679 | ("/foo/b", "/foo/b", p! {}), 680 | ("/fooo/b", "/{f}ooo/b", p! { "f" => "f" }), 681 | ("/foooo/b", "/{f}oooo/b", p! { "f" => "f" }), 682 | ("/foo/b/", "", Err(())), 683 | ("/foooo/b/", "", Err(())), 684 | ("/foo/bb", "/foo/{b}", p! { "b" => "bb" }), 685 | ("/foo/bone", "/foo/{b}one", p! { "b" => "b" }), 686 | ("/foo/bone/", "/foo/{b}one/", p! { "b" => "b" }), 687 | ("/foo/btwo", "/foo/{b}two", p! { "b" => "b" }), 688 | ("/foo/btwo/", "", Err(())), 689 | ("/foo/b/one", "/foo/{b}/one", p! { "b" => "b" }), 690 | ("/foo/bone/one", "/foo/{b}one/one", p! { "b" => "b" }), 691 | ("/foo/bone/one/", "/foo/{b}one/one/", p! { "b" => "b" }), 692 | ("/foo/btwo/one", "/foo/{b}two/one", p! { "b" => "b" }), 693 | ("/bar/b", "/bar/{b}", p! { "b" => "b" }), 694 | ("/bar/b/baz", "/bar/{b}/baz", p! { "b" => "b" }), 695 | ("/bar/bone", "/bar/{b}one", p! { "b" => "b" }), 696 | ("/bar/bone/baz", "/bar/{b}one/baz", p! { "b" => "b" }), 697 | ("/baz/b/bar", "/baz/{b}/bar", p! { "b" => "b" }), 698 | ("/baz/bone/bar", "/baz/{b}one/bar", p! { "b" => "b" }), 699 | ], 700 | } 701 | .run(); 702 | } 703 | 704 | #[test] 705 | fn basic() { 706 | MatchTest { 707 | routes: vec![ 708 | "/hi", 709 | "/contact", 710 | "/co", 711 | "/c", 712 | "/a", 713 | "/ab", 714 | "/doc/", 715 | "/doc/rust_faq.html", 716 | "/doc/rust1.26.html", 717 | "/ʯ", 718 | "/β", 719 | "/sd!here", 720 | "/sd$here", 721 | "/sd&here", 722 | "/sd'here", 723 | "/sd(here", 724 | "/sd)here", 725 | "/sd+here", 726 | "/sd,here", 727 | "/sd;here", 728 | "/sd=here", 729 | ], 730 | matches: vec![ 731 | ("/a", "/a", p! {}), 732 | ("", "/", Err(())), 733 | ("/hi", "/hi", p! {}), 734 | ("/contact", "/contact", p! {}), 735 | ("/co", "/co", p! {}), 736 | ("", "/con", Err(())), 737 | ("", "/cona", Err(())), 738 | ("", "/no", Err(())), 739 | ("/ab", "/ab", p! {}), 740 | ("/ʯ", "/ʯ", p! {}), 741 | ("/β", "/β", p! {}), 742 | ("/sd!here", "/sd!here", p! {}), 743 | ("/sd$here", "/sd$here", p! {}), 744 | ("/sd&here", "/sd&here", p! {}), 745 | ("/sd'here", "/sd'here", p! {}), 746 | ("/sd(here", "/sd(here", p! {}), 747 | ("/sd)here", "/sd)here", p! {}), 748 | ("/sd+here", "/sd+here", p! {}), 749 | ("/sd,here", "/sd,here", p! {}), 750 | ("/sd;here", "/sd;here", p! {}), 751 | ("/sd=here", "/sd=here", p! {}), 752 | ], 753 | } 754 | .run() 755 | } 756 | 757 | #[test] 758 | fn wildcard() { 759 | MatchTest { 760 | routes: vec![ 761 | "/", 762 | "/cmd/{tool}/", 763 | "/cmd/{tool2}/{sub}", 764 | "/cmd/whoami", 765 | "/cmd/whoami/root", 766 | "/cmd/whoami/root/", 767 | "/src", 768 | "/src/", 769 | "/src/{*filepath}", 770 | "/search/", 771 | "/search/{query}", 772 | "/search/actix-web", 773 | "/search/google", 774 | "/user_{name}", 775 | "/user_{name}/about", 776 | "/files/{dir}/{*filepath}", 777 | "/doc/", 778 | "/doc/rust_faq.html", 779 | "/doc/rust1.26.html", 780 | "/info/{user}/public", 781 | "/info/{user}/project/{project}", 782 | "/info/{user}/project/rustlang", 783 | "/aa/{*xx}", 784 | "/ab/{*xx}", 785 | "/ab/hello{*xx}", 786 | "/{cc}", 787 | "/c1/{dd}/e", 788 | "/c1/{dd}/e1", 789 | "/{cc}/cc", 790 | "/{cc}/{dd}/ee", 791 | "/{cc}/{dd}/{ee}/ff", 792 | "/{cc}/{dd}/{ee}/{ff}/gg", 793 | "/{cc}/{dd}/{ee}/{ff}/{gg}/hh", 794 | "/get/test/abc/", 795 | "/get/{param}/abc/", 796 | "/something/{paramname}/thirdthing", 797 | "/something/secondthing/test", 798 | "/get/abc", 799 | "/get/{param}", 800 | "/get/abc/123abc", 801 | "/get/abc/{param}", 802 | "/get/abc/123abc/xxx8", 803 | "/get/abc/123abc/{param}", 804 | "/get/abc/123abc/xxx8/1234", 805 | "/get/abc/123abc/xxx8/{param}", 806 | "/get/abc/123abc/xxx8/1234/ffas", 807 | "/get/abc/123abc/xxx8/1234/{param}", 808 | "/get/abc/123abc/xxx8/1234/kkdd/12c", 809 | "/get/abc/123abc/xxx8/1234/kkdd/{param}", 810 | "/get/abc/{param}/test", 811 | "/get/abc/123abd/{param}", 812 | "/get/abc/123abddd/{param}", 813 | "/get/abc/123/{param}", 814 | "/get/abc/123abg/{param}", 815 | "/get/abc/123abf/{param}", 816 | "/get/abc/123abfff/{param}", 817 | ], 818 | matches: vec![ 819 | ("/", "/", p! {}), 820 | ("/cmd/test", "/cmd/{tool}/", Err(())), 821 | ("/cmd/test/", "/cmd/{tool}/", p! { "tool" => "test" }), 822 | ( 823 | "/cmd/test/3", 824 | "/cmd/{tool2}/{sub}", 825 | p! { "tool2" => "test", "sub" => "3" }, 826 | ), 827 | ("/cmd/who", "/cmd/{tool}/", Err(())), 828 | ("/cmd/who/", "/cmd/{tool}/", p! { "tool" => "who" }), 829 | ("/cmd/whoami", "/cmd/whoami", p! {}), 830 | ("/cmd/whoami/", "/cmd/{tool}/", p! { "tool" => "whoami" }), 831 | ( 832 | "/cmd/whoami/r", 833 | "/cmd/{tool2}/{sub}", 834 | p! { "tool2" => "whoami", "sub" => "r" }, 835 | ), 836 | ("/cmd/whoami/r/", "/cmd/{tool}/{sub}", Err(())), 837 | ("/cmd/whoami/root", "/cmd/whoami/root", p! {}), 838 | ("/cmd/whoami/root/", "/cmd/whoami/root/", p! {}), 839 | ("/src", "/src", p! {}), 840 | ("/src/", "/src/", p! {}), 841 | ( 842 | "/src/some/file.png", 843 | "/src/{*filepath}", 844 | p! { "filepath" => "some/file.png" }, 845 | ), 846 | ("/search/", "/search/", p! {}), 847 | ( 848 | "/search/actix", 849 | "/search/{query}", 850 | p! { "query" => "actix" }, 851 | ), 852 | ("/search/actix-web", "/search/actix-web", p! {}), 853 | ( 854 | "/search/someth!ng+in+ünìcodé", 855 | "/search/{query}", 856 | p! { "query" => "someth!ng+in+ünìcodé" }, 857 | ), 858 | ("/search/someth!ng+in+ünìcodé/", "", Err(())), 859 | ( 860 | "/user_rustacean", 861 | "/user_{name}", 862 | p! { "name" => "rustacean" }, 863 | ), 864 | ( 865 | "/user_rustacean/about", 866 | "/user_{name}/about", 867 | p! { "name" => "rustacean" }, 868 | ), 869 | ( 870 | "/files/js/inc/framework.js", 871 | "/files/{dir}/{*filepath}", 872 | p! { "dir" => "js", "filepath" => "inc/framework.js" }, 873 | ), 874 | ( 875 | "/info/gordon/public", 876 | "/info/{user}/public", 877 | p! { "user" => "gordon" }, 878 | ), 879 | ( 880 | "/info/gordon/project/rust", 881 | "/info/{user}/project/{project}", 882 | p! { "user" => "gordon", "project" => "rust" }, 883 | ), 884 | ( 885 | "/info/gordon/project/rustlang", 886 | "/info/{user}/project/rustlang", 887 | p! { "user" => "gordon" }, 888 | ), 889 | ("/aa/", "/", Err(())), 890 | ("/aa/aa", "/aa/{*xx}", p! { "xx" => "aa" }), 891 | ("/ab/ab", "/ab/{*xx}", p! { "xx" => "ab" }), 892 | ("/ab/hello-world", "/ab/hello{*xx}", p! { "xx" => "-world" }), 893 | ("/a", "/{cc}", p! { "cc" => "a" }), 894 | ("/all", "/{cc}", p! { "cc" => "all" }), 895 | ("/d", "/{cc}", p! { "cc" => "d" }), 896 | ("/ad", "/{cc}", p! { "cc" => "ad" }), 897 | ("/dd", "/{cc}", p! { "cc" => "dd" }), 898 | ("/dddaa", "/{cc}", p! { "cc" => "dddaa" }), 899 | ("/aa", "/{cc}", p! { "cc" => "aa" }), 900 | ("/aaa", "/{cc}", p! { "cc" => "aaa" }), 901 | ("/aaa/cc", "/{cc}/cc", p! { "cc" => "aaa" }), 902 | ("/ab", "/{cc}", p! { "cc" => "ab" }), 903 | ("/abb", "/{cc}", p! { "cc" => "abb" }), 904 | ("/abb/cc", "/{cc}/cc", p! { "cc" => "abb" }), 905 | ("/allxxxx", "/{cc}", p! { "cc" => "allxxxx" }), 906 | ("/alldd", "/{cc}", p! { "cc" => "alldd" }), 907 | ("/all/cc", "/{cc}/cc", p! { "cc" => "all" }), 908 | ("/a/cc", "/{cc}/cc", p! { "cc" => "a" }), 909 | ("/c1/d/e", "/c1/{dd}/e", p! { "dd" => "d" }), 910 | ("/c1/d/e1", "/c1/{dd}/e1", p! { "dd" => "d" }), 911 | ( 912 | "/c1/d/ee", 913 | "/{cc}/{dd}/ee", 914 | p! { "cc" => "c1", "dd" => "d" }, 915 | ), 916 | ("/cc/cc", "/{cc}/cc", p! { "cc" => "cc" }), 917 | ("/ccc/cc", "/{cc}/cc", p! { "cc" => "ccc" }), 918 | ("/deedwjfs/cc", "/{cc}/cc", p! { "cc" => "deedwjfs" }), 919 | ("/acllcc/cc", "/{cc}/cc", p! { "cc" => "acllcc" }), 920 | ("/get/test/abc/", "/get/test/abc/", p! {}), 921 | ("/get/te/abc/", "/get/{param}/abc/", p! { "param" => "te" }), 922 | ( 923 | "/get/testaa/abc/", 924 | "/get/{param}/abc/", 925 | p! { "param" => "testaa" }, 926 | ), 927 | ("/get/xx/abc/", "/get/{param}/abc/", p! { "param" => "xx" }), 928 | ("/get/tt/abc/", "/get/{param}/abc/", p! { "param" => "tt" }), 929 | ("/get/a/abc/", "/get/{param}/abc/", p! { "param" => "a" }), 930 | ("/get/t/abc/", "/get/{param}/abc/", p! { "param" => "t" }), 931 | ("/get/aa/abc/", "/get/{param}/abc/", p! { "param" => "aa" }), 932 | ( 933 | "/get/abas/abc/", 934 | "/get/{param}/abc/", 935 | p! { "param" => "abas" }, 936 | ), 937 | ( 938 | "/something/secondthing/test", 939 | "/something/secondthing/test", 940 | p! {}, 941 | ), 942 | ( 943 | "/something/abcdad/thirdthing", 944 | "/something/{paramname}/thirdthing", 945 | p! { "paramname" => "abcdad" }, 946 | ), 947 | ( 948 | "/something/secondthingaaaa/thirdthing", 949 | "/something/{paramname}/thirdthing", 950 | p! { "paramname" => "secondthingaaaa" }, 951 | ), 952 | ( 953 | "/something/se/thirdthing", 954 | "/something/{paramname}/thirdthing", 955 | p! { "paramname" => "se" }, 956 | ), 957 | ( 958 | "/something/s/thirdthing", 959 | "/something/{paramname}/thirdthing", 960 | p! { "paramname" => "s" }, 961 | ), 962 | ("/c/d/ee", "/{cc}/{dd}/ee", p! { "cc" => "c", "dd" => "d" }), 963 | ( 964 | "/c/d/e/ff", 965 | "/{cc}/{dd}/{ee}/ff", 966 | p! { "cc" => "c", "dd" => "d", "ee" => "e" }, 967 | ), 968 | ( 969 | "/c/d/e/f/gg", 970 | "/{cc}/{dd}/{ee}/{ff}/gg", 971 | p! { "cc" => "c", "dd" => "d", "ee" => "e", "ff" => "f" }, 972 | ), 973 | ( 974 | "/c/d/e/f/g/hh", 975 | "/{cc}/{dd}/{ee}/{ff}/{gg}/hh", 976 | p! { "cc" => "c", "dd" => "d", "ee" => "e", "ff" => "f", "gg" => "g" }, 977 | ), 978 | ( 979 | "/cc/dd/ee/ff/gg/hh", 980 | "/{cc}/{dd}/{ee}/{ff}/{gg}/hh", 981 | p! { "cc" => "cc", "dd" => "dd", "ee" => "ee", "ff" => "ff", "gg" => "gg" }, 982 | ), 983 | ("/get/abc", "/get/abc", p! {}), 984 | ("/get/a", "/get/{param}", p! { "param" => "a" }), 985 | ("/get/abz", "/get/{param}", p! { "param" => "abz" }), 986 | ("/get/12a", "/get/{param}", p! { "param" => "12a" }), 987 | ("/get/abcd", "/get/{param}", p! { "param" => "abcd" }), 988 | ("/get/abc/123abc", "/get/abc/123abc", p! {}), 989 | ("/get/abc/12", "/get/abc/{param}", p! { "param" => "12" }), 990 | ( 991 | "/get/abc/123ab", 992 | "/get/abc/{param}", 993 | p! { "param" => "123ab" }, 994 | ), 995 | ("/get/abc/xyz", "/get/abc/{param}", p! { "param" => "xyz" }), 996 | ( 997 | "/get/abc/123abcddxx", 998 | "/get/abc/{param}", 999 | p! { "param" => "123abcddxx" }, 1000 | ), 1001 | ("/get/abc/123abc/xxx8", "/get/abc/123abc/xxx8", p! {}), 1002 | ( 1003 | "/get/abc/123abc/x", 1004 | "/get/abc/123abc/{param}", 1005 | p! { "param" => "x" }, 1006 | ), 1007 | ( 1008 | "/get/abc/123abc/xxx", 1009 | "/get/abc/123abc/{param}", 1010 | p! { "param" => "xxx" }, 1011 | ), 1012 | ( 1013 | "/get/abc/123abc/abc", 1014 | "/get/abc/123abc/{param}", 1015 | p! { "param" => "abc" }, 1016 | ), 1017 | ( 1018 | "/get/abc/123abc/xxx8xxas", 1019 | "/get/abc/123abc/{param}", 1020 | p! { "param" => "xxx8xxas" }, 1021 | ), 1022 | ( 1023 | "/get/abc/123abc/xxx8/1234", 1024 | "/get/abc/123abc/xxx8/1234", 1025 | p! {}, 1026 | ), 1027 | ( 1028 | "/get/abc/123abc/xxx8/1", 1029 | "/get/abc/123abc/xxx8/{param}", 1030 | p! { "param" => "1" }, 1031 | ), 1032 | ( 1033 | "/get/abc/123abc/xxx8/123", 1034 | "/get/abc/123abc/xxx8/{param}", 1035 | p! { "param" => "123" }, 1036 | ), 1037 | ( 1038 | "/get/abc/123abc/xxx8/78k", 1039 | "/get/abc/123abc/xxx8/{param}", 1040 | p! { "param" => "78k" }, 1041 | ), 1042 | ( 1043 | "/get/abc/123abc/xxx8/1234xxxd", 1044 | "/get/abc/123abc/xxx8/{param}", 1045 | p! { "param" => "1234xxxd" }, 1046 | ), 1047 | ( 1048 | "/get/abc/123abc/xxx8/1234/ffas", 1049 | "/get/abc/123abc/xxx8/1234/ffas", 1050 | p! {}, 1051 | ), 1052 | ( 1053 | "/get/abc/123abc/xxx8/1234/f", 1054 | "/get/abc/123abc/xxx8/1234/{param}", 1055 | p! { "param" => "f" }, 1056 | ), 1057 | ( 1058 | "/get/abc/123abc/xxx8/1234/ffa", 1059 | "/get/abc/123abc/xxx8/1234/{param}", 1060 | p! { "param" => "ffa" }, 1061 | ), 1062 | ( 1063 | "/get/abc/123abc/xxx8/1234/kka", 1064 | "/get/abc/123abc/xxx8/1234/{param}", 1065 | p! { "param" => "kka" }, 1066 | ), 1067 | ( 1068 | "/get/abc/123abc/xxx8/1234/ffas321", 1069 | "/get/abc/123abc/xxx8/1234/{param}", 1070 | p! { "param" => "ffas321" }, 1071 | ), 1072 | ( 1073 | "/get/abc/123abc/xxx8/1234/kkdd/12c", 1074 | "/get/abc/123abc/xxx8/1234/kkdd/12c", 1075 | p! {}, 1076 | ), 1077 | ( 1078 | "/get/abc/123abc/xxx8/1234/kkdd/1", 1079 | "/get/abc/123abc/xxx8/1234/kkdd/{param}", 1080 | p! { "param" => "1" }, 1081 | ), 1082 | ( 1083 | "/get/abc/123abc/xxx8/1234/kkdd/12", 1084 | "/get/abc/123abc/xxx8/1234/kkdd/{param}", 1085 | p! { "param" => "12" }, 1086 | ), 1087 | ( 1088 | "/get/abc/123abc/xxx8/1234/kkdd/12b", 1089 | "/get/abc/123abc/xxx8/1234/kkdd/{param}", 1090 | p! { "param" => "12b" }, 1091 | ), 1092 | ( 1093 | "/get/abc/123abc/xxx8/1234/kkdd/34", 1094 | "/get/abc/123abc/xxx8/1234/kkdd/{param}", 1095 | p! { "param" => "34" }, 1096 | ), 1097 | ( 1098 | "/get/abc/123abc/xxx8/1234/kkdd/12c2e3", 1099 | "/get/abc/123abc/xxx8/1234/kkdd/{param}", 1100 | p! { "param" => "12c2e3" }, 1101 | ), 1102 | ( 1103 | "/get/abc/12/test", 1104 | "/get/abc/{param}/test", 1105 | p! { "param" => "12" }, 1106 | ), 1107 | ( 1108 | "/get/abc/123abdd/test", 1109 | "/get/abc/{param}/test", 1110 | p! { "param" => "123abdd" }, 1111 | ), 1112 | ( 1113 | "/get/abc/123abdddf/test", 1114 | "/get/abc/{param}/test", 1115 | p! { "param" => "123abdddf" }, 1116 | ), 1117 | ( 1118 | "/get/abc/123ab/test", 1119 | "/get/abc/{param}/test", 1120 | p! { "param" => "123ab" }, 1121 | ), 1122 | ( 1123 | "/get/abc/123abgg/test", 1124 | "/get/abc/{param}/test", 1125 | p! { "param" => "123abgg" }, 1126 | ), 1127 | ( 1128 | "/get/abc/123abff/test", 1129 | "/get/abc/{param}/test", 1130 | p! { "param" => "123abff" }, 1131 | ), 1132 | ( 1133 | "/get/abc/123abffff/test", 1134 | "/get/abc/{param}/test", 1135 | p! { "param" => "123abffff" }, 1136 | ), 1137 | ( 1138 | "/get/abc/123abd/test", 1139 | "/get/abc/123abd/{param}", 1140 | p! { "param" => "test" }, 1141 | ), 1142 | ( 1143 | "/get/abc/123abddd/test", 1144 | "/get/abc/123abddd/{param}", 1145 | p! { "param" => "test" }, 1146 | ), 1147 | ( 1148 | "/get/abc/123/test22", 1149 | "/get/abc/123/{param}", 1150 | p! { "param" => "test22" }, 1151 | ), 1152 | ( 1153 | "/get/abc/123abg/test", 1154 | "/get/abc/123abg/{param}", 1155 | p! { "param" => "test" }, 1156 | ), 1157 | ( 1158 | "/get/abc/123abf/testss", 1159 | "/get/abc/123abf/{param}", 1160 | p! { "param" => "testss" }, 1161 | ), 1162 | ( 1163 | "/get/abc/123abfff/te", 1164 | "/get/abc/123abfff/{param}", 1165 | p! { "param" => "te" }, 1166 | ), 1167 | ], 1168 | } 1169 | .run() 1170 | } 1171 | -------------------------------------------------------------------------------- /src/tree.rs: -------------------------------------------------------------------------------- 1 | use crate::escape::{UnescapedRef, UnescapedRoute}; 2 | use crate::{InsertError, MatchError, Params}; 3 | 4 | use std::cell::UnsafeCell; 5 | use std::cmp::min; 6 | use std::collections::VecDeque; 7 | use std::ops::Range; 8 | use std::{fmt, mem}; 9 | 10 | /// A radix tree used for URL path matching. 11 | /// 12 | /// See [the crate documentation](crate) for details. 13 | pub struct Node { 14 | // This node's prefix. 15 | pub(crate) prefix: UnescapedRoute, 16 | 17 | // The priority of this node. 18 | // 19 | // Nodes with more children are higher priority and searched first. 20 | pub(crate) priority: u32, 21 | 22 | // Whether this node contains a wildcard child. 23 | pub(crate) wild_child: bool, 24 | 25 | // The first character of any static children, for fast linear search. 26 | pub(crate) indices: Vec, 27 | 28 | // The type of this node. 29 | pub(crate) node_type: NodeType, 30 | 31 | // The children of this node. 32 | pub(crate) children: Vec>, 33 | 34 | // The value stored at this node. 35 | // 36 | // See `Node::at` for why an `UnsafeCell` is necessary. 37 | pub(crate) value: Option>, 38 | 39 | // A parameter name remapping, stored at nodes that hold values. 40 | pub(crate) remapping: ParamRemapping, 41 | } 42 | 43 | /// The types of nodes a tree can hold. 44 | #[derive(PartialEq, Eq, PartialOrd, Ord, Debug, Clone)] 45 | pub(crate) enum NodeType { 46 | /// The root path. 47 | Root, 48 | 49 | /// A route parameter, e.g. '/{id}'. 50 | /// 51 | /// If `suffix` is `false`, the only child of this node is 52 | /// a static '/', allowing for a fast path when searching. 53 | /// Otherwise, the route may have static suffixes, e.g. '/{id}.png'. 54 | /// 55 | /// The leaves of a parameter node are the static suffixes 56 | /// sorted by length. This allows for a reverse linear search 57 | /// to determine the correct leaf. It would also be possible to 58 | /// use a reverse prefix-tree here, but is likely not worth the 59 | /// complexity. 60 | Param { suffix: bool }, 61 | 62 | /// A catch-all parameter, e.g. '/{*file}'. 63 | CatchAll, 64 | 65 | /// A static prefix, e.g. '/foo'. 66 | Static, 67 | } 68 | 69 | /// Safety: We expose `value` per Rust's usual borrowing rules, so we can just 70 | /// delegate these traits. 71 | unsafe impl Send for Node {} 72 | unsafe impl Sync for Node {} 73 | 74 | /// Tracks the current node and its parent during insertion. 75 | struct InsertState<'node, T> { 76 | parent: &'node mut Node, 77 | child: Option, 78 | } 79 | 80 | impl<'node, T> InsertState<'node, T> { 81 | /// Returns a reference to the parent node for the traversal. 82 | fn parent(&self) -> Option<&Node> { 83 | match self.child { 84 | None => None, 85 | Some(_) => Some(self.parent), 86 | } 87 | } 88 | 89 | /// Returns a reference to the current node in the traversal. 90 | fn node(&self) -> &Node { 91 | match self.child { 92 | None => self.parent, 93 | Some(i) => &self.parent.children[i], 94 | } 95 | } 96 | 97 | /// Returns a mutable reference to the current node in the traversal. 98 | fn node_mut(&mut self) -> &mut Node { 99 | match self.child { 100 | None => self.parent, 101 | Some(i) => &mut self.parent.children[i], 102 | } 103 | } 104 | 105 | /// Move the current node to its i'th child. 106 | fn set_child(self, i: usize) -> InsertState<'node, T> { 107 | match self.child { 108 | None => InsertState { 109 | parent: self.parent, 110 | child: Some(i), 111 | }, 112 | Some(prev) => InsertState { 113 | parent: &mut self.parent.children[prev], 114 | child: Some(i), 115 | }, 116 | } 117 | } 118 | } 119 | 120 | impl Node { 121 | // Insert a route into the tree. 122 | pub fn insert(&mut self, route: String, val: T) -> Result<(), InsertError> { 123 | let route = UnescapedRoute::new(route.into_bytes()); 124 | let (route, remapping) = normalize_params(route)?; 125 | let mut remaining = route.as_ref(); 126 | 127 | self.priority += 1; 128 | 129 | // If the tree is empty, insert the root node. 130 | if self.value.is_none() && self.children.is_empty() { 131 | let last = self.insert_route(remaining, val)?; 132 | last.remapping = remapping; 133 | self.node_type = NodeType::Root; 134 | return Ok(()); 135 | } 136 | 137 | let mut state = InsertState { 138 | parent: self, 139 | child: None, 140 | }; 141 | 142 | 'walk: loop { 143 | // Find the common prefix between the route and the current node. 144 | let len = min(remaining.len(), state.node().prefix.len()); 145 | let common_prefix = (0..len) 146 | .find(|&i| { 147 | remaining[i] != state.node().prefix[i] 148 | // Make sure not confuse the start of a wildcard with an escaped `{`. 149 | || remaining.is_escaped(i) != state.node().prefix.is_escaped(i) 150 | }) 151 | .unwrap_or(len); 152 | 153 | // If this node has a longer prefix than we need, we have to fork and extract the 154 | // common prefix into a shared parent. 155 | if state.node().prefix.len() > common_prefix { 156 | let node = state.node_mut(); 157 | 158 | // Move the non-matching suffix into a child node. 159 | let suffix = node.prefix.as_ref().slice_off(common_prefix).to_owned(); 160 | let child = Node { 161 | prefix: suffix, 162 | value: node.value.take(), 163 | indices: node.indices.clone(), 164 | wild_child: node.wild_child, 165 | children: mem::take(&mut node.children), 166 | remapping: mem::take(&mut node.remapping), 167 | priority: node.priority - 1, 168 | node_type: NodeType::Static, 169 | }; 170 | 171 | // The current node now only holds the common prefix. 172 | node.children = vec![child]; 173 | node.indices = vec![node.prefix[common_prefix]]; 174 | node.prefix = node.prefix.as_ref().slice_until(common_prefix).to_owned(); 175 | node.wild_child = false; 176 | continue; 177 | } 178 | 179 | if remaining.len() == common_prefix { 180 | let node = state.node_mut(); 181 | 182 | // This node must not already contain a value. 183 | if node.value.is_some() { 184 | return Err(InsertError::conflict(&route, remaining, node)); 185 | } 186 | 187 | // Insert the value. 188 | node.value = Some(UnsafeCell::new(val)); 189 | node.remapping = remapping; 190 | return Ok(()); 191 | } 192 | 193 | let common_remaining = remaining; 194 | 195 | // Otherwise, the route has a remaining non-matching suffix. 196 | // 197 | // We have to search deeper. 198 | remaining = remaining.slice_off(common_prefix); 199 | let next = remaining[0]; 200 | 201 | // For parameters with a suffix, we have to find the matching suffix or create a new child node. 202 | if let NodeType::Param { suffix: has_suffix } = state.node().node_type { 203 | let terminator = remaining 204 | .iter() 205 | .position(|&b| b == b'/') 206 | .map(|b| b + 1) 207 | .unwrap_or(remaining.len()); 208 | 209 | let suffix = remaining.slice_until(terminator); 210 | 211 | let mut extra_trailing_slash = false; 212 | for (i, child) in state.node().children.iter().enumerate() { 213 | // Find a matching suffix. 214 | if *child.prefix == **suffix { 215 | state = state.set_child(i); 216 | state.node_mut().priority += 1; 217 | continue 'walk; 218 | } 219 | 220 | // The suffix matches except for an extra trailing slash. 221 | if child.prefix.len() <= suffix.len() { 222 | let (common, remaining) = suffix.split_at(child.prefix.len()); 223 | if *common == *child.prefix && remaining == *b"/" { 224 | extra_trailing_slash = true; 225 | } 226 | } 227 | } 228 | 229 | // If we are inserting a conflicting suffix, and there is a static prefix that 230 | // already leads to this route parameter, we have a prefix-suffix conflict. 231 | if !extra_trailing_slash && !matches!(*suffix, b"" | b"/") { 232 | if let Some(parent) = state.parent() { 233 | if parent.prefix_wild_child_in_segment() { 234 | return Err(InsertError::conflict(&route, common_remaining, parent)); 235 | } 236 | } 237 | } 238 | 239 | // Multiple parameters within the same segment, e.g. `/{foo}{bar}`. 240 | if matches!(find_wildcard(suffix), Ok(Some(_))) { 241 | return Err(InsertError::InvalidParamSegment); 242 | } 243 | 244 | // If there is no matching suffix, create a new suffix node. 245 | let child = state.node_mut().add_suffix_child(Node { 246 | prefix: suffix.to_owned(), 247 | node_type: NodeType::Static, 248 | priority: 1, 249 | ..Node::default() 250 | }); 251 | 252 | let has_suffix = has_suffix || !matches!(*suffix, b"" | b"/"); 253 | state.node_mut().node_type = NodeType::Param { suffix: has_suffix }; 254 | 255 | state = state.set_child(child); 256 | 257 | // If this is the final route segment, insert the value. 258 | if terminator == remaining.len() { 259 | state.node_mut().value = Some(UnsafeCell::new(val)); 260 | state.node_mut().remapping = remapping; 261 | return Ok(()); 262 | } 263 | 264 | // Otherwise, the previous node will hold only the suffix and we 265 | // need to create a new child for the remaining route. 266 | remaining = remaining.slice_off(terminator); 267 | 268 | // Create a static node unless we are inserting a parameter. 269 | if remaining[0] != b'{' || remaining.is_escaped(0) { 270 | let child = state.node_mut().add_child(Node { 271 | node_type: NodeType::Static, 272 | priority: 1, 273 | ..Node::default() 274 | }); 275 | state.node_mut().indices.push(remaining[0]); 276 | state = state.set_child(child); 277 | } 278 | 279 | // Insert the remaining route. 280 | let last = state.node_mut().insert_route(remaining, val)?; 281 | last.remapping = remapping; 282 | return Ok(()); 283 | } 284 | 285 | // Find a child node that matches the next character in the route. 286 | for mut i in 0..state.node().indices.len() { 287 | if next == state.node().indices[i] { 288 | // Make sure not confuse the start of a wildcard with an escaped `{` or `}`. 289 | if matches!(next, b'{' | b'}') && !remaining.is_escaped(0) { 290 | continue; 291 | } 292 | 293 | // Continue searching in the child. 294 | i = state.node_mut().update_child_priority(i); 295 | state = state.set_child(i); 296 | continue 'walk; 297 | } 298 | } 299 | 300 | // We couldn't find a matching child. 301 | // 302 | // If we're not inserting a wildcard we have to create a static child. 303 | if (next != b'{' || remaining.is_escaped(0)) 304 | && state.node().node_type != NodeType::CatchAll 305 | { 306 | let node = state.node_mut(); 307 | 308 | let terminator = remaining 309 | .iter() 310 | .position(|&b| b == b'/') 311 | .unwrap_or(remaining.len()); 312 | 313 | if let Ok(Some(wildcard)) = find_wildcard(remaining.slice_until(terminator)) { 314 | // If we are inserting a parameter prefix and this node already has a parameter suffix, 315 | // we have a prefix-suffix conflict. 316 | if wildcard.start > 0 && node.suffix_wild_child_in_segment() { 317 | return Err(InsertError::conflict(&route, remaining, node)); 318 | } 319 | 320 | // Similarly, we are inserting a parameter suffix and this node already has a parameter 321 | // prefix, we have a prefix-suffix conflict. 322 | let suffix = remaining.slice_off(wildcard.end); 323 | if !matches!(*suffix, b"" | b"/") && node.prefix_wild_child_in_segment() { 324 | return Err(InsertError::conflict(&route, remaining, node)); 325 | } 326 | } 327 | 328 | node.indices.push(next); 329 | let child = node.add_child(Node::default()); 330 | let child = node.update_child_priority(child); 331 | 332 | // Insert into the newly created node. 333 | let last = node.children[child].insert_route(remaining, val)?; 334 | last.remapping = remapping; 335 | return Ok(()); 336 | } 337 | 338 | // We're trying to insert a wildcard. 339 | // 340 | // If this node already has a wildcard child, we have to make sure it matches. 341 | if state.node().wild_child { 342 | // Wildcards are always the last child. 343 | let wild_child = state.node().children.len() - 1; 344 | state = state.set_child(wild_child); 345 | state.node_mut().priority += 1; 346 | 347 | // Make sure the route parameter matches. 348 | if let Some(wildcard) = remaining.get(..state.node().prefix.len()) { 349 | if *wildcard != *state.node().prefix { 350 | return Err(InsertError::conflict(&route, remaining, state.node())); 351 | } 352 | } 353 | 354 | // Catch-all routes cannot have children. 355 | if state.node().node_type == NodeType::CatchAll { 356 | return Err(InsertError::conflict(&route, remaining, state.node())); 357 | } 358 | 359 | if let Some(parent) = state.parent() { 360 | // If there is a route with both a prefix and a suffix, and we are inserting a route with 361 | // a matching prefix but _without_ a suffix, we have a prefix-suffix conflict. 362 | if !parent.prefix.ends_with(b"/") 363 | && matches!(state.node().node_type, NodeType::Param { suffix: true }) 364 | { 365 | let terminator = remaining 366 | .iter() 367 | .position(|&b| b == b'/') 368 | .map(|b| b + 1) 369 | .unwrap_or(remaining.len()); 370 | 371 | if let Ok(Some(wildcard)) = find_wildcard(remaining.slice_until(terminator)) 372 | { 373 | let suffix = remaining.slice_off(wildcard.end); 374 | if matches!(*suffix, b"" | b"/") { 375 | return Err(InsertError::conflict(&route, remaining, parent)); 376 | } 377 | } 378 | } 379 | } 380 | 381 | // Continue with the wildcard node. 382 | continue 'walk; 383 | } 384 | 385 | if let Ok(Some(wildcard)) = find_wildcard(remaining) { 386 | let node = state.node(); 387 | let suffix = remaining.slice_off(wildcard.end); 388 | 389 | // If we are inserting a suffix and there is a static prefix that already leads to this 390 | // route parameter, we have a prefix-suffix conflict. 391 | if !matches!(*suffix, b"" | b"/") && node.prefix_wild_child_in_segment() { 392 | return Err(InsertError::conflict(&route, remaining, node)); 393 | } 394 | 395 | // Similarly, if we are inserting a longer prefix, and there is a route that leads to this 396 | // parameter that includes a suffix, we have a prefix-suffix conflict. 397 | if let Some(i) = common_prefix.checked_sub(1) { 398 | if common_remaining[i] != b'/' && node.suffix_wild_child_in_segment() { 399 | return Err(InsertError::conflict(&route, remaining, node)); 400 | } 401 | } 402 | } 403 | 404 | // Otherwise, create a new node for the wildcard and insert the route. 405 | let last = state.node_mut().insert_route(remaining, val)?; 406 | last.remapping = remapping; 407 | return Ok(()); 408 | } 409 | } 410 | 411 | /// Returns `true` if there is a wildcard node that contains a prefix within the current route segment, 412 | /// i.e. before the next trailing slash 413 | fn prefix_wild_child_in_segment(&self) -> bool { 414 | if matches!(self.node_type, NodeType::Root) && self.prefix.is_empty() { 415 | return false; 416 | } 417 | 418 | if self.prefix.ends_with(b"/") { 419 | self.children.iter().any(Node::prefix_wild_child_in_segment) 420 | } else { 421 | self.children.iter().any(Node::wild_child_in_segment) 422 | } 423 | } 424 | 425 | /// Returns `true` if there is a wildcard node within the current route segment, i.e. before the 426 | /// next trailing slash. 427 | fn wild_child_in_segment(&self) -> bool { 428 | if self.prefix.contains(&b'/') { 429 | return false; 430 | } 431 | 432 | if matches!(self.node_type, NodeType::Param { .. }) { 433 | return true; 434 | } 435 | 436 | self.children.iter().any(Node::wild_child_in_segment) 437 | } 438 | 439 | /// Returns `true` if there is a wildcard parameter node that contains a suffix within the current route 440 | /// segment, i.e. before a trailing slash. 441 | fn suffix_wild_child_in_segment(&self) -> bool { 442 | if matches!(self.node_type, NodeType::Param { suffix: true }) { 443 | return true; 444 | } 445 | 446 | self.children.iter().any(|child| { 447 | if child.prefix.contains(&b'/') { 448 | return false; 449 | } 450 | 451 | child.suffix_wild_child_in_segment() 452 | }) 453 | } 454 | 455 | // Insert a route at this node. 456 | // 457 | // If the route starts with a wildcard, a child node will be created for the parameter 458 | // and `wild_child` will be set on the parent. 459 | fn insert_route( 460 | &mut self, 461 | mut prefix: UnescapedRef<'_>, 462 | val: T, 463 | ) -> Result<&mut Node, InsertError> { 464 | let mut node = self; 465 | 466 | loop { 467 | // Search for a wildcard segment. 468 | let Some(wildcard) = find_wildcard(prefix)? else { 469 | // There is no wildcard, simply insert into the current node. 470 | node.value = Some(UnsafeCell::new(val)); 471 | node.prefix = prefix.to_owned(); 472 | return Ok(node); 473 | }; 474 | 475 | // Inserting a catch-all route. 476 | if prefix[wildcard.clone()][1] == b'*' { 477 | // Ensure there is no suffix after the parameter, e.g. `/foo/{*x}/bar`. 478 | if wildcard.end != prefix.len() { 479 | return Err(InsertError::InvalidCatchAll); 480 | } 481 | 482 | // Add the prefix before the wildcard into the current node. 483 | if wildcard.start > 0 { 484 | node.prefix = prefix.slice_until(wildcard.start).to_owned(); 485 | prefix = prefix.slice_off(wildcard.start); 486 | } 487 | 488 | // Add the catch-all as a child node. 489 | let child = node.add_child(Node { 490 | prefix: prefix.to_owned(), 491 | node_type: NodeType::CatchAll, 492 | value: Some(UnsafeCell::new(val)), 493 | priority: 1, 494 | ..Node::default() 495 | }); 496 | node.wild_child = true; 497 | return Ok(&mut node.children[child]); 498 | } 499 | 500 | // Otherwise, we're inserting a regular route parameter. 501 | // 502 | // Add the prefix before the wildcard into the current node. 503 | if wildcard.start > 0 { 504 | node.prefix = prefix.slice_until(wildcard.start).to_owned(); 505 | prefix = prefix.slice_off(wildcard.start); 506 | } 507 | 508 | // Find the end of this route segment. 509 | let terminator = prefix 510 | .iter() 511 | .position(|&b| b == b'/') 512 | .map(|b| b + 1) 513 | .unwrap_or(prefix.len()); 514 | 515 | let wildcard = prefix.slice_until(wildcard.len()); 516 | let suffix = prefix.slice_until(terminator).slice_off(wildcard.len()); 517 | prefix = prefix.slice_off(terminator); 518 | 519 | // Multiple parameters within the same segment, e.g. `/{foo}{bar}`. 520 | if matches!(find_wildcard(suffix), Ok(Some(_))) { 521 | return Err(InsertError::InvalidParamSegment); 522 | } 523 | 524 | // Add the parameter as a child node. 525 | let has_suffix = !matches!(*suffix, b"" | b"/"); 526 | let child = node.add_child(Node { 527 | priority: 1, 528 | node_type: NodeType::Param { suffix: has_suffix }, 529 | prefix: wildcard.to_owned(), 530 | ..Node::default() 531 | }); 532 | 533 | node.wild_child = true; 534 | node = &mut node.children[child]; 535 | 536 | // Add the static suffix until the '/', if there is one. 537 | // 538 | // Note that for '/' suffixes where `suffix: false`, this 539 | // unconditionally introduces an extra node for the '/' 540 | // without attempting to merge with the remaining route. 541 | // This makes converting a non-suffix parameter node into 542 | // a suffix one easier during insertion, but slightly hurts 543 | // performance. 544 | if !suffix.is_empty() { 545 | let child = node.add_suffix_child(Node { 546 | priority: 1, 547 | node_type: NodeType::Static, 548 | prefix: suffix.to_owned(), 549 | ..Node::default() 550 | }); 551 | 552 | node = &mut node.children[child]; 553 | } 554 | 555 | // If the route ends here, insert the value. 556 | if prefix.is_empty() { 557 | node.value = Some(UnsafeCell::new(val)); 558 | return Ok(node); 559 | } 560 | 561 | // If there is a static segment after the '/', setup the node 562 | // for the rest of the route. 563 | if prefix[0] != b'{' || prefix.is_escaped(0) { 564 | node.indices.push(prefix[0]); 565 | let child = node.add_child(Node { 566 | priority: 1, 567 | ..Node::default() 568 | }); 569 | node = &mut node.children[child]; 570 | } 571 | } 572 | } 573 | 574 | // Adds a child to this node, keeping wildcards at the end. 575 | fn add_child(&mut self, child: Node) -> usize { 576 | let len = self.children.len(); 577 | 578 | if self.wild_child && len > 0 { 579 | self.children.insert(len - 1, child); 580 | len - 1 581 | } else { 582 | self.children.push(child); 583 | len 584 | } 585 | } 586 | 587 | // Adds a suffix child to this node, keeping suffixes sorted by ascending length. 588 | fn add_suffix_child(&mut self, child: Node) -> usize { 589 | let i = self 590 | .children 591 | .partition_point(|node| node.prefix.len() >= child.prefix.len()); 592 | self.children.insert(i, child); 593 | i 594 | } 595 | 596 | // Increments priority of the given child node, reordering the children if necessary. 597 | // 598 | // Returns the new index of the node. 599 | fn update_child_priority(&mut self, i: usize) -> usize { 600 | self.children[i].priority += 1; 601 | let priority = self.children[i].priority; 602 | 603 | // Move the node to the front as necessary. 604 | let mut updated = i; 605 | while updated > 0 && self.children[updated - 1].priority < priority { 606 | self.children.swap(updated - 1, updated); 607 | updated -= 1; 608 | } 609 | 610 | // Update the position of the indices to match. 611 | if updated != i { 612 | self.indices[updated..=i].rotate_right(1); 613 | } 614 | 615 | updated 616 | } 617 | 618 | /// Removes a route from the tree, returning the value if the route already existed. 619 | /// 620 | /// The provided path should be the same as the one used to insert the route, including 621 | /// wildcards. 622 | pub fn remove(&mut self, route: String) -> Option { 623 | let route = UnescapedRoute::new(route.into_bytes()); 624 | let (route, remapping) = normalize_params(route).ok()?; 625 | let mut remaining = route.unescaped(); 626 | 627 | // Check if we are removing the root node. 628 | if remaining == self.prefix.unescaped() { 629 | let value = self.value.take().map(UnsafeCell::into_inner); 630 | 631 | // If the root node has no children, we can reset it. 632 | if self.children.is_empty() { 633 | *self = Node::default(); 634 | } 635 | 636 | return value; 637 | } 638 | 639 | let mut node = self; 640 | 'walk: loop { 641 | // Could not find a match. 642 | if remaining.len() <= node.prefix.len() { 643 | return None; 644 | } 645 | 646 | // Otherwise, the path is longer than this node's prefix, search deeper. 647 | let (prefix, rest) = remaining.split_at(node.prefix.len()); 648 | 649 | // The prefix does not match. 650 | if prefix != node.prefix.unescaped() { 651 | return None; 652 | } 653 | 654 | let next = rest[0]; 655 | remaining = rest; 656 | 657 | // If this is a parameter node, we have to find the matching suffix. 658 | if matches!(node.node_type, NodeType::Param { .. }) { 659 | let terminator = remaining 660 | .iter() 661 | .position(|&b| b == b'/') 662 | .map(|b| b + 1) 663 | .unwrap_or(remaining.len()); 664 | 665 | let suffix = &remaining[..terminator]; 666 | 667 | for (i, child) in node.children.iter().enumerate() { 668 | // Find the matching suffix. 669 | if *child.prefix == *suffix { 670 | // If this is the end of the path, remove the suffix node. 671 | if terminator == remaining.len() { 672 | return node.remove_child(i, &remapping); 673 | } 674 | 675 | // Otherwise, continue searching. 676 | remaining = &remaining[terminator - child.prefix.len()..]; 677 | node = &mut node.children[i]; 678 | continue 'walk; 679 | } 680 | } 681 | } 682 | 683 | // Find a child node that matches the next character in the route. 684 | if let Some(i) = node.indices.iter().position(|&c| c == next) { 685 | // The route matches, remove the node. 686 | if node.children[i].prefix.unescaped() == remaining { 687 | return node.remove_child(i, &remapping); 688 | } 689 | 690 | // Otherwise, continue searching. 691 | node = &mut node.children[i]; 692 | continue 'walk; 693 | } 694 | 695 | // If there is no matching wildcard child, there is no matching route. 696 | if !node.wild_child { 697 | return None; 698 | } 699 | 700 | // If the route does match, remove the node. 701 | if node.children.last_mut().unwrap().prefix.unescaped() == remaining { 702 | return node.remove_child(node.children.len() - 1, &remapping); 703 | } 704 | 705 | // Otherwise, keep searching deeper. 706 | node = node.children.last_mut().unwrap(); 707 | } 708 | } 709 | 710 | /// Remove the child node at the given index, if the route parameters match. 711 | fn remove_child(&mut self, i: usize, remapping: &ParamRemapping) -> Option { 712 | // Require an exact match to remove a route. 713 | // 714 | // For example, `/{a}` cannot be used to remove `/{b}`. 715 | if self.children[i].remapping != *remapping { 716 | return None; 717 | } 718 | 719 | // If the node does not have any children, we can remove it completely. 720 | let value = if self.children[i].children.is_empty() { 721 | // Remove the child node. 722 | let child = self.children.remove(i); 723 | 724 | match child.node_type { 725 | // Remove the index if we removed a static prefix that is 726 | // not a suffix node. 727 | NodeType::Static if !matches!(self.node_type, NodeType::Param { .. }) => { 728 | self.indices.remove(i); 729 | } 730 | 731 | // Otherwise, we removed a wildcard. 732 | _ => self.wild_child = false, 733 | } 734 | 735 | child.value 736 | } 737 | // Otherwise, remove the value but preserve the node. 738 | else { 739 | self.children[i].value.take() 740 | }; 741 | 742 | value.map(UnsafeCell::into_inner) 743 | } 744 | 745 | /// Iterates over the tree and calls the given visitor function 746 | /// with fully resolved path and its value. 747 | pub fn for_each(self, mut visitor: V) { 748 | let mut queue = VecDeque::from([(self.prefix.clone(), self)]); 749 | 750 | // Perform a BFS on the routing tree. 751 | while let Some((mut prefix, mut node)) = queue.pop_front() { 752 | denormalize_params(&mut prefix, &node.remapping); 753 | 754 | if let Some(value) = node.value.take() { 755 | let path = String::from_utf8(prefix.unescaped().to_vec()).unwrap(); 756 | visitor(path, value.into_inner()); 757 | } 758 | 759 | // Traverse the child nodes. 760 | for child in node.children { 761 | let mut prefix = prefix.clone(); 762 | prefix.append(&child.prefix); 763 | queue.push_back((prefix, child)); 764 | } 765 | } 766 | } 767 | } 768 | 769 | /// A wildcard node that was skipped during a tree search. 770 | /// 771 | /// Contains the state necessary to backtrack to the given node. 772 | struct Skipped<'node, 'path, T> { 773 | // The node that was skipped. 774 | node: &'node Node, 775 | 776 | /// The path at the time we skipped this node. 777 | path: &'path [u8], 778 | 779 | // The number of parameters that were present. 780 | params: usize, 781 | } 782 | 783 | impl Node { 784 | // Returns the node matching the given path. 785 | // 786 | // Returning an `UnsafeCell` allows us to avoid duplicating the logic between `Node::at` and 787 | // `Node::at_mut`, as Rust doesn't have a great way of abstracting over mutability. 788 | #[inline] 789 | pub fn at<'node, 'path>( 790 | &'node self, 791 | mut path: &'path [u8], 792 | ) -> Result<(&'node UnsafeCell, Params<'node, 'path>), MatchError> { 793 | let mut node = self; 794 | let mut backtracking = false; 795 | let mut params = Params::new(); 796 | let mut skipped: Vec> = Vec::new(); 797 | 798 | 'backtrack: loop { 799 | 'walk: loop { 800 | // Reached the end of the 801 | if path.len() <= node.prefix.len() { 802 | // Check for an exact match. 803 | if *path == *node.prefix { 804 | // Found the matching value. 805 | if let Some(ref value) = node.value { 806 | // Remap the keys of any route parameters we accumulated during the search. 807 | params.for_each_key_mut(|(i, param)| param.key = &node.remapping[i]); 808 | return Ok((value, params)); 809 | } 810 | } 811 | 812 | break 'walk; 813 | } 814 | 815 | // Otherwise, the path is longer than this node's prefix, search deeper. 816 | let (prefix, rest) = path.split_at(node.prefix.len()); 817 | 818 | // The prefix does not match. 819 | if *prefix != *node.prefix { 820 | break 'walk; 821 | } 822 | 823 | let previous = path; 824 | path = rest; 825 | 826 | // If we are currently backtracking, avoid searching static children 827 | // that we already searched. 828 | if !backtracking { 829 | let next = path[0]; 830 | 831 | // Find a child node that matches the next character in the path. 832 | if let Some(i) = node.indices.iter().position(|&c| c == next) { 833 | // Keep track of wildcard routes that we skip. 834 | // 835 | // We may end up needing to backtrack later in case we do not find a 836 | // match. 837 | if node.wild_child { 838 | skipped.push(Skipped { 839 | node, 840 | path: previous, 841 | params: params.len(), 842 | }); 843 | } 844 | 845 | // Continue searching. 846 | node = &node.children[i]; 847 | continue 'walk; 848 | } 849 | } 850 | 851 | // We didn't find a matching static child. 852 | // 853 | // If there are no wildcards, then there are no matching routes in the tree. 854 | if !node.wild_child { 855 | break 'walk; 856 | } 857 | 858 | // Continue searching in the wildcard child, which is kept at the end of the list. 859 | node = node.children.last().unwrap(); 860 | match node.node_type { 861 | NodeType::Param { suffix: false } => { 862 | // Check for more path segments. 863 | let terminator = match path.iter().position(|&c| c == b'/') { 864 | // Double `//` implying an empty parameter, no match. 865 | Some(0) => break 'walk, 866 | 867 | // Found another segment. 868 | Some(i) => i, 869 | 870 | // This is the last path segment. 871 | None => { 872 | // If this is the last path segment and there is a matching 873 | // value without a suffix, we have a match. 874 | let Some(ref value) = node.value else { 875 | break 'walk; 876 | }; 877 | 878 | // Store the parameter value. 879 | params.push(b"", path); 880 | 881 | // Remap the keys of any route parameters we accumulated during the search. 882 | params 883 | .for_each_key_mut(|(i, param)| param.key = &node.remapping[i]); 884 | 885 | return Ok((value, params)); 886 | } 887 | }; 888 | 889 | // Found another path segment. 890 | let (param, rest) = path.split_at(terminator); 891 | 892 | // If there is a static child, continue the search. 893 | let [child] = node.children.as_slice() else { 894 | break 'walk; 895 | }; 896 | 897 | // Store the parameter value. 898 | // Parameters are normalized so this key is irrelevant for now. 899 | params.push(b"", param); 900 | 901 | // Continue searching. 902 | path = rest; 903 | node = child; 904 | backtracking = false; 905 | continue 'walk; 906 | } 907 | 908 | NodeType::Param { suffix: true } => { 909 | // Check for more path segments. 910 | let slash = path.iter().position(|&c| c == b'/'); 911 | let terminator = match slash { 912 | // Double `//` implying an empty parameter, no match. 913 | Some(0) => break 'walk, 914 | 915 | // Found another segment. 916 | Some(i) => i + 1, 917 | 918 | // This is the last path segment. 919 | None => path.len(), 920 | }; 921 | 922 | for child in node.children.iter() { 923 | // Ensure there is a possible match with a non-zero suffix. 924 | if child.prefix.len() >= terminator { 925 | continue; 926 | } 927 | 928 | let suffix_start = terminator - child.prefix.len(); 929 | let (param, suffix) = path[..terminator].split_at(suffix_start); 930 | 931 | // Continue searching if the suffix matches. 932 | if *suffix == *child.prefix { 933 | node = child; 934 | path = &path[suffix_start..]; 935 | backtracking = false; 936 | // Parameters are normalized so this key is irrelevant for now. 937 | params.push(b"", param); 938 | continue 'walk; 939 | } 940 | } 941 | 942 | // If this is the last path segment and there is a matching 943 | // value without a suffix, we have a match. 944 | let value = match node.value { 945 | // Found the matching value. 946 | Some(ref value) if slash.is_none() => value, 947 | _ => break 'walk, 948 | }; 949 | 950 | // Store the parameter value. 951 | params.push(b"", path); 952 | 953 | // Remap the keys of any route parameters we accumulated during the search. 954 | params.for_each_key_mut(|(i, param)| param.key = &node.remapping[i]); 955 | 956 | return Ok((value, params)); 957 | } 958 | 959 | NodeType::CatchAll => { 960 | // Catch-all segments are only allowed at the end of the route, meaning 961 | // this node must contain the value. 962 | let value = match node.value { 963 | // Found the matching value. 964 | Some(ref value) => value, 965 | 966 | // Otherwise, there are no matching routes in the tree. 967 | None => return Err(MatchError::NotFound), 968 | }; 969 | 970 | // Remap the keys of any route parameters we accumulated during the search. 971 | params.for_each_key_mut(|(i, param)| param.key = &node.remapping[i]); 972 | 973 | // Store the final catch-all parameter (`{*...}`). 974 | let key = &node.prefix[2..node.prefix.len() - 1]; 975 | params.push(key, path); 976 | 977 | return Ok((value, params)); 978 | } 979 | 980 | _ => unreachable!(), 981 | } 982 | } 983 | 984 | // Try backtracking to any matching wildcard nodes that we skipped while 985 | // traversing the tree. 986 | while let Some(skipped) = skipped.pop() { 987 | if skipped.path.ends_with(path) { 988 | // Found a matching node, restore the search state. 989 | path = skipped.path; 990 | node = skipped.node; 991 | backtracking = true; 992 | params.truncate(skipped.params); 993 | continue 'backtrack; 994 | } 995 | } 996 | 997 | return Err(MatchError::NotFound); 998 | } 999 | } 1000 | 1001 | /// Test helper that ensures route priorities are consistent. 1002 | #[cfg(feature = "__test_helpers")] 1003 | pub fn check_priorities(&self) -> Result { 1004 | let mut priority: u32 = 0; 1005 | for child in &self.children { 1006 | priority += child.check_priorities()?; 1007 | } 1008 | 1009 | if self.value.is_some() { 1010 | priority += 1; 1011 | } 1012 | 1013 | if self.priority != priority { 1014 | return Err((self.priority, priority)); 1015 | } 1016 | 1017 | Ok(priority) 1018 | } 1019 | } 1020 | 1021 | /// An ordered list of route parameters keys for a specific route. 1022 | /// 1023 | /// To support conflicting routes like `/{a}/foo` and `/{b}/bar`, route parameters 1024 | /// are normalized before being inserted into the tree. Parameter remapping are 1025 | /// stored at nodes containing values, containing the "true" names of all route parameters 1026 | /// for the given route. 1027 | type ParamRemapping = Vec>; 1028 | 1029 | /// Returns `path` with normalized route parameters, and a parameter remapping 1030 | /// to store at the node for this route. 1031 | /// 1032 | /// Note that the parameter remapping may contain unescaped characters. 1033 | fn normalize_params( 1034 | mut path: UnescapedRoute, 1035 | ) -> Result<(UnescapedRoute, ParamRemapping), InsertError> { 1036 | let mut start = 0; 1037 | let mut original = ParamRemapping::new(); 1038 | 1039 | // Parameter names are normalized alphabetically. 1040 | let mut next = b'a'; 1041 | 1042 | loop { 1043 | // Find a wildcard to normalize. 1044 | let mut wildcard = match find_wildcard(path.as_ref().slice_off(start))? { 1045 | Some(wildcard) => wildcard, 1046 | // No wildcard, we are done. 1047 | None => return Ok((path, original)), 1048 | }; 1049 | 1050 | wildcard.start += start; 1051 | wildcard.end += start; 1052 | 1053 | // Ensure the parameter has a valid name. 1054 | if wildcard.len() < 2 { 1055 | return Err(InsertError::InvalidParam); 1056 | } 1057 | 1058 | // We don't need to normalize catch-all parameters, as they are always 1059 | // at the end of a route. 1060 | if path[wildcard.clone()][1] == b'*' { 1061 | start = wildcard.end; 1062 | continue; 1063 | } 1064 | 1065 | // Normalize the parameter. 1066 | let removed = path.splice(wildcard.clone(), vec![b'{', next, b'}']); 1067 | 1068 | // Preserve the original name for remapping. 1069 | let mut removed = removed.skip(1).collect::>(); 1070 | removed.pop(); 1071 | original.push(removed); 1072 | 1073 | next += 1; 1074 | if next > b'z' { 1075 | panic!("Too many route parameters."); 1076 | } 1077 | 1078 | // Continue the search after the parameter we just normalized. 1079 | start = wildcard.start + 3; 1080 | } 1081 | } 1082 | 1083 | /// Restores `route` to it's original, denormalized form. 1084 | pub(crate) fn denormalize_params(route: &mut UnescapedRoute, params: &ParamRemapping) { 1085 | let mut start = 0; 1086 | let mut i = 0; 1087 | 1088 | loop { 1089 | // Find a wildcard to denormalize. 1090 | let mut wildcard = match find_wildcard(route.as_ref().slice_off(start)).unwrap() { 1091 | Some(w) => w, 1092 | None => return, 1093 | }; 1094 | 1095 | wildcard.start += start; 1096 | wildcard.end += start; 1097 | 1098 | // Get the corresponding parameter remapping. 1099 | let mut next = match params.get(i) { 1100 | Some(param) => param.clone(), 1101 | None => return, 1102 | }; 1103 | 1104 | // Denormalize this parameter. 1105 | next.insert(0, b'{'); 1106 | next.push(b'}'); 1107 | let _ = route.splice(wildcard.clone(), next.clone()); 1108 | 1109 | i += 1; 1110 | start = wildcard.start + next.len(); 1111 | } 1112 | } 1113 | 1114 | // Searches for a wildcard segment and checks the path for invalid characters. 1115 | fn find_wildcard(path: UnescapedRef<'_>) -> Result>, InsertError> { 1116 | for (start, &c) in path.iter().enumerate() { 1117 | // Found an unescaped closing brace without a corresponding opening brace. 1118 | if c == b'}' && !path.is_escaped(start) { 1119 | return Err(InsertError::InvalidParam); 1120 | } 1121 | 1122 | // Keep going until we find an unescaped opening brace. 1123 | if c != b'{' || path.is_escaped(start) { 1124 | continue; 1125 | } 1126 | 1127 | // Ensure there is a non-empty parameter name. 1128 | if path.get(start + 1) == Some(&b'}') { 1129 | return Err(InsertError::InvalidParam); 1130 | } 1131 | 1132 | // Find the corresponding closing brace. 1133 | for (i, &c) in path.iter().enumerate().skip(start + 2) { 1134 | match c { 1135 | b'}' => { 1136 | // This closing brace was escaped, keep searching. 1137 | if path.is_escaped(i) { 1138 | continue; 1139 | } 1140 | 1141 | // Ensure catch-all parameters have a non-empty name. 1142 | if path.get(i - 1) == Some(&b'*') { 1143 | return Err(InsertError::InvalidParam); 1144 | } 1145 | 1146 | return Ok(Some(start..i + 1)); 1147 | } 1148 | // `*` and `/` are invalid in parameter names. 1149 | b'*' | b'/' => return Err(InsertError::InvalidParam), 1150 | _ => {} 1151 | } 1152 | } 1153 | 1154 | // Missing closing brace. 1155 | return Err(InsertError::InvalidParam); 1156 | } 1157 | 1158 | Ok(None) 1159 | } 1160 | 1161 | impl Clone for Node 1162 | where 1163 | T: Clone, 1164 | { 1165 | fn clone(&self) -> Node { 1166 | let value = self.value.as_ref().map(|value| { 1167 | // Safety: We only expose `&mut T` through `&mut self`. 1168 | let value = unsafe { &*value.get() }; 1169 | UnsafeCell::new(value.clone()) 1170 | }); 1171 | 1172 | Node { 1173 | value, 1174 | prefix: self.prefix.clone(), 1175 | wild_child: self.wild_child, 1176 | node_type: self.node_type.clone(), 1177 | indices: self.indices.clone(), 1178 | children: self.children.clone(), 1179 | remapping: self.remapping.clone(), 1180 | priority: self.priority, 1181 | } 1182 | } 1183 | } 1184 | 1185 | impl Default for Node { 1186 | fn default() -> Node { 1187 | Node { 1188 | remapping: ParamRemapping::new(), 1189 | prefix: UnescapedRoute::default(), 1190 | wild_child: false, 1191 | node_type: NodeType::Static, 1192 | indices: Vec::new(), 1193 | children: Vec::new(), 1194 | value: None, 1195 | priority: 0, 1196 | } 1197 | } 1198 | } 1199 | 1200 | impl fmt::Debug for Node 1201 | where 1202 | T: fmt::Debug, 1203 | { 1204 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 1205 | // Safety: We only expose `&mut T` through `&mut self`. 1206 | let value = unsafe { self.value.as_ref().map(|x| &*x.get()) }; 1207 | 1208 | let mut f = f.debug_struct("Node"); 1209 | f.field("value", &value) 1210 | .field("prefix", &self.prefix) 1211 | .field("node_type", &self.node_type) 1212 | .field("children", &self.children); 1213 | 1214 | // Extra information for debugging purposes. 1215 | #[cfg(test)] 1216 | { 1217 | let indices = self 1218 | .indices 1219 | .iter() 1220 | .map(|&x| char::from_u32(x as _)) 1221 | .collect::>(); 1222 | 1223 | let params = self 1224 | .remapping 1225 | .iter() 1226 | .map(|x| std::str::from_utf8(x).unwrap()) 1227 | .collect::>(); 1228 | 1229 | f.field("indices", &indices).field("params", ¶ms); 1230 | } 1231 | 1232 | f.finish() 1233 | } 1234 | } 1235 | --------------------------------------------------------------------------------