diff --git a/lib/plug/router.ex b/lib/plug/router.ex index 0c754b74..aca7d0e7 100644 --- a/lib/plug/router.ex +++ b/lib/plug/router.ex @@ -74,6 +74,14 @@ defmodule Plug.Router do The above will match `/hello/foo.json` but not `/hello/foo`. Other delimiters such as `-`, `@` may be used to denote suffixes. + Identifier matching can be escaped using the `\` character: + + get "/hello/\\:greet" do + send_resp(conn, 200, "hello") + end + + The above will only match `/hello/:greet`. + Routes allow for globbing which will match the remaining parts of a route. A glob match is done with the `*` character followed by the variable name. Typically you prefix the variable name with @@ -90,6 +98,15 @@ defmodule Plug.Router do send_resp(conn, 200, "route after /hello: #{inspect glob}") end + Similarly to `:identifiers`, globs are also escaped using the + `\` character: + + get "/hello/\\*glob" do + send_resp(conn, 200, "this is not a glob route") + end + + The above will only match `/hello/*glob`. + Opposite to `:identifiers`, globs do not allow prefix nor suffix matches. diff --git a/lib/plug/router/utils.ex b/lib/plug/router/utils.ex index bea788e8..b24a6979 100644 --- a/lib/plug/router/utils.ex +++ b/lib/plug/router/utils.ex @@ -127,7 +127,7 @@ defmodule Plug.Router.Utils do including the known parameters. """ def build_path_clause(path, guard, context \\ nil) when is_binary(path) do - compiled = :binary.compile_pattern([":", "*"]) + compiled = :binary.compile_pattern(["\\:", "\\*", ":", "*"]) {params, match, guards, post_match} = path @@ -148,6 +148,15 @@ defmodule Plug.Router.Utils do [] -> build_path_clause(rest, params, [segment | match], guards, post_match, context, compiled) + [{prefix_size, match_length}] when match_length == 2 -> + suffix_size = byte_size(segment) - prefix_size - 2 + + <> = + segment + + escaped_segment = [prefix <> <> <> suffix | match] + build_path_clause(rest, params, escaped_segment, guards, post_match, context, compiled) + [{prefix_size, _}] -> suffix_size = byte_size(segment) - prefix_size - 1 <> = segment diff --git a/test/plug/router/utils_test.exs b/test/plug/router/utils_test.exs index 9314696d..a95781b5 100644 --- a/test/plug/router/utils_test.exs +++ b/test/plug/router/utils_test.exs @@ -60,6 +60,20 @@ defmodule Plug.Router.UtilsTest do build_path_match("foo/bar:username") end + test "build match with escaped identifiers" do + assert quote(@opts, do: {[], ["foo", ":"]}) == build_path_match("foo/\\:") + assert quote(@opts, do: {[], ["foo", ":id"]}) == build_path_match("/foo/\\:id") + assert quote(@opts, do: {[], ["foo", ":username"]}) == build_path_match("foo/\\:username") + + assert quote(@opts, do: {[:id, :post_id], ["foo", id, ":name", post_id]}) == + build_path_match("/foo/:id/\\:name/:post_id") + + assert quote(@opts, do: {[], ["foo", "bar-:id"]}) == build_path_match("/foo/bar-\\:id") + + assert quote(@opts, do: {[], ["foo", "bar:batchDelete"]}) == + build_path_match("foo/bar\\:batchDelete") + end + test "build match only with glob" do assert quote(@opts, do: {[:bar], bar}) == build_path_match("*bar") assert quote(@opts, do: {[:glob], glob}) == build_path_match("/*glob") @@ -70,6 +84,14 @@ defmodule Plug.Router.UtilsTest do assert quote(@opts, do: {[:glob], ["foo" | glob]}) == build_path_match("foo/*glob") end + test "build match with escaped glob" do + assert quote(@opts, do: {[], ["*bar"]}) == build_path_match("\\*bar") + assert quote(@opts, do: {[], ["*glob"]}) == build_path_match("/\\*glob") + + assert quote(@opts, do: {[], ["foo", "*bar"]}) == build_path_match("/foo/\\*bar") + assert quote(@opts, do: {[], ["foo", "*glob"]}) == build_path_match("foo/\\*glob") + end + test "build invalid match with empty matches" do assert_raise Plug.Router.InvalidSpecError, "invalid dynamic path. The characters : and * must be immediately followed by lowercase letters or underscore, got: :",