From 1a3e13cfb2e8601394b4a29f4f060d6ecfafa6f9 Mon Sep 17 00:00:00 2001 From: Nelson Vides Date: Wed, 12 Aug 2026 16:12:03 +0200 Subject: [PATCH] Optimize URI.encode_www_form/1 and RFC 3986 query encoding Both encoders were built on a binary comprehension whose body calls `percent/2` through a captured predicate: for <>, into: "" do case percent(byte, &char_unreserved?/1) do "%20" -> "+" percent -> percent end end `for ... into: ""` over a binary generator compiles to a native Erlang bitstring comprehension, so the comprehension itself is fine, but in the body it pays per byte a `make_fun3` to build the captured predicate, a call to `percent/2` plus an indirect call through it, a freshly allocated one- or three-byte binary that is discarded immediately, an `is_eq_exact` against the literal "%20", and a runtime-sized `bs_create_bin` to append the result. Rewrite it as a tail-recursive loop that writes the escape straight into the accumulator. The unreserved test moves into a `defguardp` so it inlines instead of going through a closure, `+` is handled by its own clause rather than by comparing the produced binary against "%20", and the appended segments have compile-time-known sizes. --- lib/elixir/lib/uri.ex | 38 ++++++++++++++++++++++------- lib/elixir/test/elixir/uri_test.exs | 30 +++++++++++++++++++++++ 2 files changed, 59 insertions(+), 9 deletions(-) diff --git a/lib/elixir/lib/uri.ex b/lib/elixir/lib/uri.ex index faf5562d996..bf27076ac84 100644 --- a/lib/elixir/lib/uri.ex +++ b/lib/elixir/lib/uri.ex @@ -168,8 +168,8 @@ defmodule URI do end defp encode_kv_pair({key, value}, :rfc3986) do - encode(Kernel.to_string(key), &char_unreserved?/1) <> - "=" <> encode(Kernel.to_string(value), &char_unreserved?/1) + encode_unreserved(Kernel.to_string(key), :percent) <> + "=" <> encode_unreserved(Kernel.to_string(value), :percent) end defp encode_kv_pair({key, value}, :www_form) do @@ -340,6 +340,10 @@ defmodule URI do character in @reserved_characters end + defguardp unreserved_char?(character) + when character in ?0..?9 or character in ?a..?z or character in ?A..?Z or + character in ~c"~_-." + @doc """ Checks if `character` is an unreserved one in a URI. @@ -357,7 +361,7 @@ defmodule URI do """ @spec char_unreserved?(byte) :: boolean def char_unreserved?(character) do - character in ?0..?9 or character in ?a..?z or character in ?A..?Z or character in ~c"~_-." + unreserved_char?(character) end @doc """ @@ -432,15 +436,31 @@ defmodule URI do """ @spec encode_www_form(binary) :: binary + def encode_www_form(<<>>), do: "" + def encode_www_form(string) when is_binary(string) do - for <>, into: "" do - case percent(byte, &char_unreserved?/1) do - "%20" -> "+" - percent -> percent - end - end + encode_unreserved(string, "", :www_form) end + # Matching upfront keeps the empty case from allocating the accumulator. + defp encode_unreserved(<<>>, _mode), do: "" + + defp encode_unreserved(string, mode), do: encode_unreserved(string, "", mode) + + defp encode_unreserved(<>, acc, :www_form) do + encode_unreserved(rest, <>, :www_form) + end + + defp encode_unreserved(<>, acc, mode) when unreserved_char?(byte) do + encode_unreserved(rest, <>, mode) + end + + defp encode_unreserved(<>, acc, mode) do + encode_unreserved(rest, <>, mode) + end + + defp encode_unreserved(<<>>, acc, _mode), do: acc + defp percent(char, predicate) do if predicate.(char) do <> diff --git a/lib/elixir/test/elixir/uri_test.exs b/lib/elixir/test/elixir/uri_test.exs index 678baddbe13..685f9fe65e6 100644 --- a/lib/elixir/test/elixir/uri_test.exs +++ b/lib/elixir/test/elixir/uri_test.exs @@ -17,9 +17,34 @@ defmodule URITest do end test "encode_www_form/1" do + assert URI.encode_www_form("") == "" assert URI.encode_www_form("4test ~1.x") == "4test+~1.x" assert URI.encode_www_form("poll:146%") == "poll%3A146%25" assert URI.encode_www_form("/\n+/ゆ") == "%2F%0A%2B%2F%E3%82%86" + + for byte <- 0..255, input <- [<>, <>] do + assert URI.encode_www_form(input) == www_form_reference(input) + end + end + + defp www_form_reference(input) do + for <>, into: "" do + cond do + byte == ?\s -> "+" + unreserved_reference?(byte) -> <> + true -> "%" <> Base.encode16(<>) + end + end + end + + defp rfc3986_reference(input) do + for <>, into: "" do + if unreserved_reference?(byte), do: <>, else: "%" <> Base.encode16(<>) + end + end + + defp unreserved_reference?(byte) do + byte in ?0..?9 or byte in ?a..?z or byte in ?A..?Z or byte in ~c"~_-." end test "encode_query/1,2" do @@ -39,6 +64,11 @@ defmodule URITest do assert URI.encode_query([{"foo[]", "+=/?&# Ñ"}], :www_form) == "foo%5B%5D=%2B%3D%2F%3F%26%23+%C3%91" + for byte <- 0..255, input <- [<>, <>] do + expected = rfc3986_reference(input) + assert URI.encode_query([{input, input}], :rfc3986) == expected <> "=" <> expected + end + assert_raise ArgumentError, fn -> URI.encode_query([{"foo", ~c"bar"}]) end