Skip to content

Commit 1c895fc

Browse files
authored
fix: remove quadratic decoding of repeated elements. (#287)
1 parent 91f21be commit 1c895fc

3 files changed

Lines changed: 66 additions & 1 deletion

File tree

lib/protox/define_decoder.ex

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,17 +63,38 @@ defmodule Protox.DefineDecoder do
6363
make_parse_key_value_body(fields, vars, opts)
6464

6565
unknown_fields_name = Keyword.fetch!(opts, :unknown_fields_name)
66+
finish_decode = make_finish_decode(fields, vars.msg, unknown_fields_name)
6667

6768
quote do
6869
@spec parse_key_value(binary(), struct()) :: struct()
6970
defp parse_key_value(<<>>, msg) do
70-
%{msg | unquote(unknown_fields_name) => Enum.reverse(msg.unquote(unknown_fields_name))}
71+
unquote(finish_decode)
7172
end
7273

7374
defp parse_key_value(bytes, msg), do: unquote(parse_key_value_body)
7475
end
7576
end
7677

78+
defp make_finish_decode(fields, msg_var, unknown_fields_name) do
79+
repeated_field_names =
80+
fields
81+
|> Enum.filter(&(&1.kind in [:packed, :unpacked]))
82+
|> Enum.map(& &1.name)
83+
|> Enum.uniq()
84+
85+
updates =
86+
Enum.map(repeated_field_names, fn field_name ->
87+
{field_name, quote(do: Enum.reverse(unquote(msg_var).unquote(field_name)))}
88+
end) ++
89+
[
90+
{unknown_fields_name, quote(do: Enum.reverse(unquote(msg_var).unquote(unknown_fields_name)))}
91+
]
92+
93+
quote do
94+
%{unquote(msg_var) | unquote_splicing(updates)}
95+
end
96+
end
97+
7798
defp make_parse_key_value_body(fields, vars, opts) do
7899
# Fragment to parse unknown fields. Those are identified with an unknown tag.
79100
unknown_tag_clause =
@@ -312,6 +333,19 @@ defmodule Protox.DefineDecoder do
312333
quote(do: {unquote(field.name), unquote(value)})
313334
end
314335

336+
defp make_update_field(value, %Field{kind: kind} = field, vars, wrap_value) when kind in [:packed, :unpacked] do
337+
update_value =
338+
if wrap_value do
339+
quote(do: [unquote(value) | unquote(vars.msg).unquote(field.name)])
340+
else
341+
quote(do: Enum.reverse(unquote(value), unquote(vars.msg).unquote(field.name)))
342+
end
343+
344+
quote do
345+
{unquote(field.name), unquote(update_value)}
346+
end
347+
end
348+
315349
defp make_update_field(value, %Field{} = field, vars, wrap_value) do
316350
value = maybe_wrap(value, wrap_value)
317351

test/protox/decode_test.exs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,12 @@ defmodule Protox.DecodeTest do
4848
<<218, 2, 2, 1, 0>> <> <<218, 2, 1, 1, 218, 2, 1, 0>>,
4949
%TestAllTypesProto3{repeated_bool: [true, false, true, false]}
5050
},
51+
{
52+
"Repeated bool, unpacked and packed, should be concatenated (2)",
53+
# unpacked <> packed
54+
<<218, 2, 1, 1, 218, 2, 1, 0>> <> <<218, 2, 2, 1, 0>>,
55+
%TestAllTypesProto3{repeated_bool: [true, false, true, false]}
56+
},
5157
{
5258
"Repeated uint32",
5359
<<138, 2, 6, 0, 1, 2, 3, 144, 78>>,

test/protox/generate_test.exs

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -142,4 +142,29 @@ defmodule Protox.GenerateTest do
142142
assert Code.compile_file(namespace_directory_message1_tmp_file) != []
143143
assert Code.compile_file(namespace_sub_directory_message_tmp_file) != []
144144
end
145+
146+
test "Generate repeated-field decoders with prepend-and-reverse" do
147+
file = Path.join(__DIR__, "../samples/google/test_messages_proto3.proto")
148+
generated_file_name = "generated_repeated_decode.ex"
149+
150+
{:ok, files_content} =
151+
Protox.Generate.generate_module_code([file], generated_file_name, false, [
152+
"./test/samples/google"
153+
])
154+
155+
assert [%Protox.Generate.FileContent{name: ^generated_file_name, content: content}] =
156+
files_content
157+
158+
content = IO.iodata_to_binary(content)
159+
160+
refute content =~ "msg.repeated_bool ++"
161+
refute content =~ "msg.repeated_string ++"
162+
refute content =~ "msg.repeated_nested_message ++"
163+
164+
assert content =~
165+
~r/repeated_string:\s*\[\s*Protox\.Decode\.validate_string!\(delimited\)\s*\|\s*msg\.repeated_string\s*\]/s
166+
167+
assert content =~ "repeated_string: Enum.reverse(msg.repeated_string)"
168+
assert content =~ "repeated_bool: Enum.reverse(msg.repeated_bool)"
169+
end
145170
end

0 commit comments

Comments
 (0)