Repository: pallets/werkzeug

Issue:
v2.2.0 breaks previous URL map logic for url-encoded path segments containing %2F
Version 2.2.0 seems to change the way url-encoded path segments (specifically paths which contain the escape sequence for `/`, `%2F`) are handled in rules/maps in the routing module.

Here's a self-contained example demonstrating the difference in routing behavior, save this in `werkzeug_test.py` for example:
```python
from werkzeug.routing import Map, Rule
from werkzeug.wrappers import Request, Response


class PathTester:

    URL_MAP = Map(
        [
            Rule(
                "/<path:some_path>/some_route",
                endpoint="simple_route",
            ),
            Rule(
                "/<path:some_path>/<segment_a>/<segment_b>",
                endpoint="complex_route",
            ),
        ]
    )

    def on_simple_route(self, request, some_path):
        return Response("Matched simple route")

    def on_complex_route(self, request, some_path, segment_a, segment_b):
        return Response("Matched complex route")

    def dispatch_request(self, request):
        adapter = self.URL_MAP.bind_to_environ(request.environ)
        endpoint, values = adapter.match()
        return getattr(self, f"on_{endpoint}")(request, **values)

    def wsgi_app(self, environ, start_response):
        request = Request(environ)
        response = self.dispatch_request(request)
        return response(environ, start_response)

    def __call__(self, environ, start_response):
        return self.wsgi_app(environ, start_response)


if __name__ == "__main__":
    from werkzeug.serving import run_simple
    run_simple("127.0.0.1", 5000, PathTester())
```

Running this example using werkzeug 2.1.2:
```shell
$ pip install --upgrade Werkzeug==2.1.2
$ python werkzeug_test.py

# … in another terminal

$ curl http://localhost:5000/my%2Fpath/some_route
Matched simple route
```

Now, running this example using werkzeug 2.2.0:
```shell
$ pip install --upgrade Werkzeug==2.2.0
$ python werkzeug_test.py

# … in another terminal

$ curl http://localhost:5000/my%2Fpath/some_route
Matched complex route
```

Environment:
- Python version: 3.9.13
- Werkzeug version: 2.2.0



Discussion:
We're experiencing a similar issue with a change of behavior in the routing where a trailing slash triggers a 404 in Flask, while before an url ending at '/' was correctly redirected to the canonical URL for the endpoint.
I believe the issue is related, we did some troubleshooting and it seems the issue started after the commit at https://github.com/pallets/werkzeug/commit/88125beebc6463e571cef3ca84eccfad0c818ae3
Having this same issue as well. When starting the process, it hangs with 100% of usage dedicated to the function parse_rules.

Resolving patch:
diff --git a/src/werkzeug/routing/rules.py b/src/werkzeug/routing/rules.py
--- a/src/werkzeug/routing/rules.py
+++ b/src/werkzeug/routing/rules.py
@@ -592,8 +592,12 @@ def _parse_rule(self, rule: str) -> t.Iterable[RulePart]:
 
             data = match.groupdict()
             if data["static"]:
-                static_weights.append((static_parts, -len(data["static"])))
-                static_parts += 1
+                # Only static parts beyond the `/` count towards the
+                # weighting (more static parts mean the rule should be
+                # weighted first).
+                if data["static"] != "/":
+                    static_weights.append((static_parts, -len(data["static"])))
+                    static_parts += 1
                 self._trace.append((False, data["static"]))
 
             if data["variable"] is not None:


Test patch:
diff --git a/tests/test_routing.py b/tests/test_routing.py
--- a/tests/test_routing.py
+++ b/tests/test_routing.py
@@ -1394,6 +1394,18 @@ def test_rule_websocket_methods():
     r.Rule("/ws", endpoint="ws", websocket=True, methods=["get", "head", "options"])
 
 
+def test_path_weighting():
+    m = r.Map(
+        [
+            r.Rule("/<path:path>/c", endpoint="simple"),
+            r.Rule("/<path:path>/<a>/<b>", endpoint="complex"),
+        ]
+    )
+    a = m.bind("localhost", path_info="/a/b/c")
+
+    assert a.match() == ("simple", {"path": "a/b"})
+
+
 def test_newline_match():
     m = r.Map([r.Rule("/hello", endpoint="hello")])
     a = m.bind("localhost")