From c618698b0d5ed03f812493fc6bb346aa974197b1 Mon Sep 17 00:00:00 2001 From: Raul Zavaczki Date: Tue, 30 Sep 2025 14:22:34 +0300 Subject: [PATCH 1/6] Allow colon to be escaped when building the path segments --- lib/plug/router/utils.ex | 27 ++++++++++++++++++++++++++- test/plug/router/utils_test.exs | 13 +++++++++++++ 2 files changed, 39 insertions(+), 1 deletion(-) diff --git a/lib/plug/router/utils.ex b/lib/plug/router/utils.ex index bea788e8..ae1728da 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,31 @@ 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 + + case matched do + "\\:" -> + escaped_segment = prefix <> ":" <> suffix + + build_path_clause( + rest, + params, + [escaped_segment | match], + guards, + post_match, + context, + compiled + ) + + _ -> + raise Plug.Router.InvalidSpecError, + "the path segment contains invalid characters, got: " <> inspect(segment) + end + [{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..273177de 100644 --- a/test/plug/router/utils_test.exs +++ b/test/plug/router/utils_test.exs @@ -60,6 +60,19 @@ defmodule Plug.Router.UtilsTest do build_path_match("foo/bar:username") end + test "build match with escaped identifiers" do + 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") From 9d7ac81ffe88288e8029003d45b9aa6e7abdd485 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Tue, 30 Sep 2025 13:27:50 +0200 Subject: [PATCH 2/6] Update lib/plug/router/utils.ex --- lib/plug/router/utils.ex | 26 +++----------------------- 1 file changed, 3 insertions(+), 23 deletions(-) diff --git a/lib/plug/router/utils.ex b/lib/plug/router/utils.ex index ae1728da..4451ec2b 100644 --- a/lib/plug/router/utils.ex +++ b/lib/plug/router/utils.ex @@ -149,29 +149,9 @@ 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 - - case matched do - "\\:" -> - escaped_segment = prefix <> ":" <> suffix - - build_path_clause( - rest, - params, - [escaped_segment | match], - guards, - post_match, - context, - compiled - ) - - _ -> - raise Plug.Router.InvalidSpecError, - "the path segment contains invalid characters, got: " <> inspect(segment) - end + <> = segment + escaped_segment = [prefix <> ":" <> suffix | match] + build_path_clause(rest, params, match, guards, post_match, context, compiled) [{prefix_size, _}] -> suffix_size = byte_size(segment) - prefix_size - 1 From 74d6bab8bcc4eaf7dc03089be8d004129656c660 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Tue, 30 Sep 2025 13:30:07 +0200 Subject: [PATCH 3/6] Update lib/plug/router/utils.ex --- lib/plug/router/utils.ex | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/plug/router/utils.ex b/lib/plug/router/utils.ex index 4451ec2b..dffb0de7 100644 --- a/lib/plug/router/utils.ex +++ b/lib/plug/router/utils.ex @@ -149,6 +149,7 @@ 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, match, guards, post_match, context, compiled) From f0b2c1bb845e7321ffd43a456f35a133a6ed04ab Mon Sep 17 00:00:00 2001 From: Zavaczki Raul Date: Tue, 30 Sep 2025 14:33:24 +0300 Subject: [PATCH 4/6] Update lib/plug/router/utils.ex MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: José Valim --- lib/plug/router/utils.ex | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/plug/router/utils.ex b/lib/plug/router/utils.ex index dffb0de7..e90ebcef 100644 --- a/lib/plug/router/utils.ex +++ b/lib/plug/router/utils.ex @@ -150,8 +150,8 @@ defmodule Plug.Router.Utils do [{prefix_size, match_length}] when match_length == 2 -> suffix_size = byte_size(segment) - prefix_size - 2 - <> = segment - escaped_segment = [prefix <> ":" <> suffix | match] + <> = segment + escaped_segment = [prefix <> <> <> suffix | match] build_path_clause(rest, params, match, guards, post_match, context, compiled) [{prefix_size, _}] -> From 2a6c3fc78429ba416f8fd5f9ee5655b5e0bb8ab1 Mon Sep 17 00:00:00 2001 From: Raul Zavaczki Date: Tue, 30 Sep 2025 14:51:50 +0300 Subject: [PATCH 5/6] feedback --- lib/plug/router.ex | 17 +++++++++++++++++ lib/plug/router/utils.ex | 9 ++++++--- test/plug/router/utils_test.exs | 9 +++++++++ 3 files changed, 32 insertions(+), 3 deletions(-) diff --git a/lib/plug/router.ex b/lib/plug/router.ex index 0c754b74..9fb40275 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 through 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 e90ebcef..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 @@ -150,9 +150,12 @@ defmodule Plug.Router.Utils do [{prefix_size, match_length}] when match_length == 2 -> suffix_size = byte_size(segment) - prefix_size - 2 - <> = segment + + <> = + segment + escaped_segment = [prefix <> <> <> suffix | match] - build_path_clause(rest, params, match, guards, post_match, context, compiled) + build_path_clause(rest, params, escaped_segment, guards, post_match, context, compiled) [{prefix_size, _}] -> suffix_size = byte_size(segment) - prefix_size - 1 diff --git a/test/plug/router/utils_test.exs b/test/plug/router/utils_test.exs index 273177de..a95781b5 100644 --- a/test/plug/router/utils_test.exs +++ b/test/plug/router/utils_test.exs @@ -61,6 +61,7 @@ defmodule Plug.Router.UtilsTest do 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") @@ -83,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: :", From 2bd0f7d7c20f0b1bdf93c6d0987657ad4331ad5f Mon Sep 17 00:00:00 2001 From: Raul Zavaczki Date: Tue, 30 Sep 2025 14:56:02 +0300 Subject: [PATCH 6/6] wording --- lib/plug/router.ex | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/plug/router.ex b/lib/plug/router.ex index 9fb40275..aca7d0e7 100644 --- a/lib/plug/router.ex +++ b/lib/plug/router.ex @@ -98,7 +98,7 @@ defmodule Plug.Router do send_resp(conn, 200, "route after /hello: #{inspect glob}") end - Similarly to `:identifiers`, globs are also escaped through the + Similarly to `:identifiers`, globs are also escaped using the `\` character: get "/hello/\\*glob" do