Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions lib/decode_error.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
defmodule Exgencode.DecodeError do

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This could probably be also used in some other cases that now just fail generically?

@moduledoc """
Raised during decoding when an `:offset_to` field points to an invalid
location - either backwards (before the current cursor, into already-parsed
bytes) or past the end of the binary.
"""
defexception [:message]
end
23 changes: 19 additions & 4 deletions lib/exgencode.ex
Original file line number Diff line number Diff line change
Expand Up @@ -346,6 +346,12 @@ defmodule Exgencode do
{field_name, props[:decode]}
end)

offset_targets =
for {field_name, props} <- field_list, props[:offset_to] != nil, into: %{} do
{props[:offset_to], field_name}
end
|> Macro.escape()

struct_fields =
for {field_name, props} <- field_list, props[:type] not in [:constant, :skip] do
{field_name, props[:default]}
Expand Down Expand Up @@ -376,15 +382,24 @@ defmodule Exgencode do
end

def decode(pdu, binary, version) do
do_decode(pdu, binary, unquote(fields_for_decodes), version)
do_decode(pdu, binary, unquote(fields_for_decodes), version, bit_size(binary))
end

defp do_decode(pdu, binary, [{field, decode_fun} | rest], version) do
defp do_decode(pdu, binary, [{field, decode_fun} | rest], version, init_bits) do
binary =
Exgencode.EncodeDecode.skip_to_offset(
pdu,
field,
binary,
init_bits,
unquote(offset_targets)
)

{new_pdu, rest_binary} = decode_fun.(version).(pdu, binary)
do_decode(new_pdu, rest_binary, rest, version)
do_decode(new_pdu, rest_binary, rest, version, init_bits)
end

defp do_decode(pdu, rest_bin, [], _) do
defp do_decode(pdu, rest_bin, [], _, _) do
{pdu, rest_bin}
end
end
Expand Down
37 changes: 37 additions & 0 deletions lib/exgencode/encode_decode.ex
Original file line number Diff line number Diff line change
Expand Up @@ -303,6 +303,43 @@ defmodule Exgencode.EncodeDecode do
wrap_conditional_decode(props, basic_fun)
end

def skip_to_offset(pdu, field, binary, init_bits, offset_targets) do
case offset_targets do
%{^field => offset_field} ->
do_skip_to_offset(pdu, field, offset_field, binary, init_bits)

_ ->
binary
end
end

defp do_skip_to_offset(pdu, field, offset_field, binary, init_bits) do
case Map.get(pdu, offset_field) do
offset when offset in [nil, 0] ->
binary

offset ->
cursor_bits = init_bits - bit_size(binary)
target_bits = offset * 8

cond do
target_bits == cursor_bits ->
binary

target_bits < cursor_bits ->
raise Exgencode.DecodeError, "offset field #{inspect(offset_field)} of #{inspect(field)} cannot point backwards!"

target_bits - cursor_bits > bit_size(binary) ->
raise Exgencode.DecodeError, "offset_field #{inspect(offset_field)} cannot point outside binary!"

true ->
gap_bits = target_bits - cursor_bits
<<_padding::size(gap_bits), rest::bitstring>> = binary
rest
end
end
end

def wrap_custom_encode(field_name, encode_fun) do
quote do
fn pdu ->
Expand Down
27 changes: 23 additions & 4 deletions lib/exgencode/validator.ex
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,8 @@ defmodule Exgencode.Validator do
end

def validate_pdu(pdu_name, fields) do
validate_offset_ordering(pdu_name, fields)

total_size =
fields
|> Enum.reject(fn {_field_name, props} -> props[:type] == :variable end)
Expand All @@ -143,9 +145,26 @@ defmodule Exgencode.Validator do
)
end

defp raise_argument_error(pdu_name, field_name, msg) do

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why was this removed?

raise ArgumentError,
"Badly defined field #{inspect(field_name)} in #{inspect(pdu_name |> Macro.to_string())} - " <>
msg
defp validate_offset_ordering(pdu_name, fields) do
ordered_names = Enum.map(fields, fn {field_name, _props} -> field_name end)

fields
|> Enum.filter(fn {_field_name, props} -> props[:offset_to] != nil end)
|> Enum.reduce(MapSet.new(), fn {field_name, props}, seen_targets ->
target = props[:offset_to]

if MapSet.member?(seen_targets, target),
do: raise(ArgumentError, "#{inspect(pdu_name |> Macro.to_string())} multiple offset fields pointing to field #{inspect(target)} is unsupported!")

offset_index = Enum.find_index(ordered_names, &(&1 == field_name))
target_index = Enum.find_index(ordered_names, &(&1 == target))

if not is_nil(target_index) and target_index < offset_index,
do: raise(ArgumentError, "#{inspect(pdu_name |> Macro.to_string())} #{inspect(target)}: backward offsets are unsupported!")

MapSet.put(seen_targets, target)
end)

:ok
end
end
112 changes: 108 additions & 4 deletions test/exgencode_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -87,11 +87,15 @@ defmodule ExgencodeTest do

nested_pdu = %TestPdu.NestedVersionedMsg{nested: %TestPdu.VersionedMsg{}}
binary = <<2::size(16), 10::size(16)>>
assert {^nested_pdu, <<>>} = Exgencode.Pdu.decode(%TestPdu.NestedVersionedMsg{}, binary, "1.0.0")

assert {^nested_pdu, <<>>} =
Exgencode.Pdu.decode(%TestPdu.NestedVersionedMsg{}, binary, "1.0.0")

nested_pdu = %TestPdu.NestedVersionedMsg{nested: %TestPdu.VersionedMsg{newerField: 111}}
binary = <<2::size(16), 10::size(16), 111::size(8)>>
assert {^nested_pdu, <<>>} = Exgencode.Pdu.decode(%TestPdu.NestedVersionedMsg{}, binary, "2.0.0")

assert {^nested_pdu, <<>>} =
Exgencode.Pdu.decode(%TestPdu.NestedVersionedMsg{}, binary, "2.0.0")
end

test "versioned encode/decode symmetry" do
Expand Down Expand Up @@ -569,14 +573,14 @@ defmodule ExgencodeTest do
test "custom size function" do
pdu = %TestPdu.CustomSizeFunPdu{custom: {5, 1024}}

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What happened here, why did the behaviour change?

assert <<2, 8, 5, 0, 0, 0, 4, 0, 8, 4>> = Exgencode.Pdu.encode(pdu)
assert <<2, 8, 5, 0, 0, 0, 4, 0, 4>> = Exgencode.Pdu.encode(pdu)

offsets = Exgencode.Pdu.set_offsets(pdu)

assert {offsets, <<>>} ==
Exgencode.Pdu.decode(
%TestPdu.CustomSizeFunPdu{},
<<2, 8, 5, 0, 0, 0, 4, 0, 8, 4>>,
<<2, 8, 5, 0, 0, 0, 4, 0, 4>>,
nil
)
end
Expand All @@ -585,4 +589,104 @@ defmodule ExgencodeTest do
pdu = %TestPdu.OffsetMadnessPdu{}
assert <<3, 4, 0, 11, 5, 0>> = Exgencode.Pdu.encode(pdu)
end

test "decode reaches offset_to : skips gap" do
binary = <<7, 0, 3, "abc", 0, 0xDE, 0xAD, 0xBE, 0xEF>>

assert {%TestPdu.OffsetMsg{
offset_to_footer: 7,
name_length: 3,
name: "abc",
footer: 0xDEADBEEF
}, <<>>} == Exgencode.Pdu.decode(%TestPdu.OffsetMsg{}, binary)
end

test "decode with zero-length gap (offset == cursor) is unchanged" do
binary = <<6, 0, 3, "abc", 0xDE, 0xAD, 0xBE, 0xEF>>

assert {%TestPdu.OffsetMsg{
offset_to_footer: 6,
name_length: 3,
name: "abc",
footer: 0xDEADBEEF
}, <<>>} == Exgencode.Pdu.decode(%TestPdu.OffsetMsg{}, binary)
end

test "decode with offset 0 leaves the target field absent" do
binary = <<0, 0, 3, "abc">>

assert {%TestPdu.OffsetMsg{
offset_to_footer: 0,
name_length: 3,
name: "abc",
footer: nil
}, <<>>} == Exgencode.Pdu.decode(%TestPdu.OffsetMsg{}, binary)
end

test "multiple offsets with offset_to gap" do
binary = <<4, 7, 0, 2, "XY", 0, 0, 0xBE, 0xEF>>

assert {%TestPdu.MultiOffsetMsg{
offset_to_a: 4,
offset_to_b: 8,
len_a: 2,
field_a: "XY",
field_b: 0xBEEF
}, <<>>} == Exgencode.Pdu.decode(%TestPdu.MultiOffsetMsg{}, binary)
end

test "decode raises on a backwards offset" do
binary = <<4, 0, 3, "abc", 0xDE, 0xAD, 0xBE, 0xEF>>

assert_raise Exgencode.DecodeError, ~r/points backwards/, fn ->
Exgencode.Pdu.decode(%TestPdu.OffsetMsg{}, binary)
end
end

test "decode raises on an offset past the end of the binary" do
binary = <<20, 0, 3, "abc", 0, 0xDE, 0xAD, 0xBE, 0xEF>>

assert_raise Exgencode.DecodeError, ~r/points past end/, fn ->
Exgencode.Pdu.decode(%TestPdu.OffsetMsg{}, binary)
end
end

test "offsets in a nested subrecord stay relative to that subrecord" do
binary = <<9, 7, 0, 3, "abc", 0, 0xDE, 0xAD, 0xBE, 0xEF>>

assert {%TestPdu.NestedOffsetMsg{
header: 9,
sub: %TestPdu.OffsetMsg{
offset_to_footer: 7,
name_length: 3,
name: "abc",
footer: 0xDEADBEEF
}
}, <<>>} == Exgencode.Pdu.decode(%TestPdu.NestedOffsetMsg{}, binary)
end

test "defining two offset fields to the same target is rejected at compile time" do
assert_raise ArgumentError, ~r/Multiple offset fields point to/, fn ->
defmodule BadDup do
import Exgencode

defpdu DuplicateOffsetPdu,
off_a: [size: 8, offset_to: :target],
off_b: [size: 8, offset_to: :target],
target: [size: 8, conditional: :off_a]
end
end
end

test "defining an offset field after its target is rejected at compile time" do
assert_raise ArgumentError, ~r/must be defined before its target/, fn ->
defmodule BadOrder do
import Exgencode

defpdu BackwardsOffsetPdu,
target: [size: 8],
off: [size: 8, offset_to: :target]
end
end
end
end
20 changes: 18 additions & 2 deletions test/helpers/test_pdu.ex
Original file line number Diff line number Diff line change
Expand Up @@ -157,9 +157,8 @@ defmodule Exgencode.TestPdu do
<<vals::size*8, rest::binary>> = val
{struct(pdu, %{custom: {size, vals}}), rest}
end,
size: fn %CustomSizeFunPdu{custom: {size, _vals}} -> size * 8 end
size: fn %CustomSizeFunPdu{custom: {size, _vals}} -> (size + 1) * 8 end
],
anotherWhyNot: [offset_to: :oneMore, size: 8],
oneMore: [default: 4, size: 8]

defpdu OffsetMadnessPdu,
Expand All @@ -168,4 +167,21 @@ defmodule Exgencode.TestPdu do
somethingIrrelevant: [default: 11, size: 16],
anotherOffset: [offset_to: :somethingElse, size: 8],
somethingElse: [default: 0, size: 8]

defpdu OffsetMsg,
offset_to_footer: [size: 8, offset_to: :footer, default: 0x00],
name_length: [size: 16, default: 0],
name: [type: :variable, size: :name_length, conditional: :name_length],
footer: [size: 32, conditional: :offset_to_footer]

defpdu MultiOffsetMsg,
offset_to_a: [size: 8, offset_to: :field_a, default: 0x00],
offset_to_b: [size: 8, offset_to: :field_b, default: 0x00],
len_a: [size: 16, default: 0],
field_a: [type: :variable, size: :len_a, conditional: :len_a],
field_b: [size: 16, conditional: :offset_to_b]

defpdu NestedOffsetMsg,
header: [size: 8, default: 0],
sub: [type: :subrecord, default: %OffsetMsg{}]
end
Loading