From f0b64eb4cda0a13f995b942a677adf6eb32cd37a Mon Sep 17 00:00:00 2001 From: Sam Morrow Date: Wed, 6 Jul 2022 15:19:01 -0700 Subject: [PATCH 1/2] added option for custom tag decoder function --- lib/cbor.ex | 42 +++++++++++++++++++++++++++++++++++++----- lib/cbor/decoder.ex | 19 ++++++++++++++----- 2 files changed, 51 insertions(+), 10 deletions(-) diff --git a/lib/cbor.ex b/lib/cbor.ex index b2527f7..f2f7bc5 100644 --- a/lib/cbor.ex +++ b/lib/cbor.ex @@ -97,19 +97,29 @@ defmodule CBOR do def encode(value), do: CBOR.Encoder.encode_into(value, <<>>) @doc """ - Converts a CBOR encoded binary into native elixir data structures + Converts a CBOR encoded binary into native elixir data structures with a specified default + decoder function. The function added should take a tag and a value, and the caller can + specify how to decode the value associated to a tag input into the function. ## Examples - iex> CBOR.decode(<<130, 101, 72, 101, 108, 108, 111, 102, 87, 111, 114, 108, 100, 33>>) - {:ok, ["Hello", "World!"], ""} + iex(1)> bin_tuple = CBOR.encode(%CBOR.Tag{tag: 50, value: {1, 2, "tuple"}}) + <<216, 50, 131, 1, 2, 101, 116, 117, 112, 108, 101>> - iex> CBOR.decode(<<130, 1, 130, 2, 3>>) - {:ok, [1, [2, 3]], ""} + iex(2)> tuple_decoder = fn (tag, value) -> case tag, do: (50 -> List.to_tuple(value); _ -> CBOR.Decoder.default_decode_tag(tag, value)) end + #Function<43.65746770/2 in :erl_eval.expr/5> # Note that non-matched tags are decoded using CBOR.Decoder.default_decode_tag/2 iex> CBOR.decode(<<162, 97, 97, 1, 97, 98, 130, 2, 3>>) {:ok, %{"a" => 1, "b" => [2, 3]}, ""} + iex(3)> CBOR.decode(bin_tuple, tuple_decoder) + {:ok, {1, 2, "tuple"}, ""} + + iex(4)> bin_non_tuple = CBOR.encode(%CBOR.Tag{tag: 123, value: "Hello, World!"}) + <<216, 123, 109, 72, 101, 108, 108, 111, 44, 32, 87, 111, 114, 108, 100, 33>> + + iex(5)> CBOR.decode(bin_non_tuple, tuple_decoder) + {:ok, %CBOR.Tag{tag: 123, value: "Hello, World!"}, ""} """ @spec decode(binary()) :: {:ok, any(), binary()} | {:error, atom} def decode(binary) do @@ -120,6 +130,19 @@ defmodule CBOR do end end + @doc """ + Converts a CBOR encoded binary into native elixir data structures + + """ + @spec decode(binary(), fun()) :: {:ok, any(), binary()} | {:error, atom} + def decode(binary, default_decode) do + try do + perform_decoding(binary, default_decode) + rescue + FunctionClauseError -> {:error, :cbor_function_clause_error} + end + end + defp perform_decoding(binary) when is_binary(binary) do case CBOR.Decoder.decode(binary) do {value, rest} -> {:ok, value, rest} @@ -128,4 +151,13 @@ defmodule CBOR do end defp perform_decoding(_value), do: {:error, :cannot_decode_non_binary_values} + + defp perform_decoding(binary, default_decode) when is_binary(binary) do + case CBOR.Decoder.decode(binary, default_decode) do + {value, rest} -> {:ok, value, rest} + _other -> {:error, :cbor_decoder_error} + end + end + + defp perform_decoding(_value, _function), do: {:error, :cannot_decode_non_binary_values} end diff --git a/lib/cbor/decoder.ex b/lib/cbor/decoder.ex index ed6519c..0351410 100644 --- a/lib/cbor/decoder.ex +++ b/lib/cbor/decoder.ex @@ -1,9 +1,14 @@ defmodule CBOR.Decoder do def decode(binary) do - decode(binary, header(binary)) + decode(binary, header(binary), &default_decode_tag/2) end - def decode(_binary, {mt, :indefinite, rest}) do + # default_decoder set by caller to decode tags without a set decode value + def decode(binary, decode_default) do + decode(binary, header(binary), decode_default) + end + + def decode(_binary, {mt, :indefinite, rest}, _decode_default) do case mt do 2 -> mark_as_bytes(decode_string_indefinite(rest, 2, [])) 3 -> decode_string_indefinite(rest, 3, []) @@ -12,7 +17,7 @@ defmodule CBOR.Decoder do end end - def decode(bin, {mt, value, rest}) do + def decode(bin, {mt, value, rest}, decode_default) do case mt do 0 -> {value, rest} 1 -> {-value - 1, rest} @@ -20,7 +25,7 @@ defmodule CBOR.Decoder do 3 -> decode_string(rest, value) 4 -> decode_array(value, rest) 5 -> decode_map(value, rest) - 6 -> decode_other(value, decode(rest)) + 6 -> decode_other(value, decode(rest), decode_default) 7 -> decode_float(bin, value, rest) end end @@ -125,7 +130,7 @@ defmodule CBOR.Decoder do end end - defp decode_other(value, {inner, rest}), do: {decode_tag(value, inner), rest} + defp decode_other(tag, {value, rest}, decode_default), do: {decode_default.(tag, value), rest} def decode_non_finite(0, 0), do: %CBOR.Tag{tag: :float, value: :inf} def decode_non_finite(1, 0), do: %CBOR.Tag{tag: :float, value: :"-inf"} @@ -139,6 +144,10 @@ defmodule CBOR.Decoder do value * 5192296858534827628530496329220096.0 end + def default_decode_tag(tag, value) do + decode_tag(tag, value) + end + defp decode_tag(0, value), do: decode_datetime(value) defp decode_tag(3, value), do: -decode_tag(2, value) - 1 From e718a1a9d12070b6fcce9d14087f83aa952382b2 Mon Sep 17 00:00:00 2001 From: Sam Morrow Date: Thu, 7 Jul 2022 16:25:56 -0700 Subject: [PATCH 2/2] Replaced optional decoder with an optional keyword --- README.md | 35 +++++++++++++++++ lib/cbor.ex | 63 +++++++++++------------------- lib/cbor/decoder.ex | 79 ++++++++++++++++++-------------------- lib/cbor/tag.ex | 2 +- test/cbor/decoder_test.exs | 2 +- 5 files changed, 97 insertions(+), 84 deletions(-) diff --git a/README.md b/README.md index 9acf84b..2960cb9 100644 --- a/README.md +++ b/README.md @@ -99,6 +99,41 @@ defimpl CBOR.Encoder, for: Money do end ``` +## Custom Decoding + +If you want to decode something that is not supported out of the box you can add a custom tag decoder function with the `tag_decoder` option into `CBOR.decode/1`. The function should take in a `CBOR.Tag` struct and convert the value based on the tag. An example for decoding Tuples and Atoms with a custom tags is shown below. + +```elixir +# Tag 50 represents Tuples, tag 51 represents Atoms. Tag numbers chosen arbitrarily. +defmodule TupleDecoder do + def tag_decoder(tag_struct) do + case tag_struct.tag do + 50 -> + List.to_tuple(tag_struct.value) + 51 -> + String.to_atom(tag_struct.value) + _ -> + tag_struct + end + end +end + +iex(1)> bin_tuple = CBOR.encode(%CBOR.Tag{tag: 50, value: {%CBOR.Tag{tag: 51, value: "atom"}, %CBOR.Tag{tag: 50, value: {"nested_tuple", 1, 2}}}}) + +iex(2)> CBOR.decode(bin_tuple, tag_decoder: TupleDecoder.tag_decoder) +{:ok, {:atom, {"nested_tuple", 1, 2}}, ""} + +iex(3)> CBOR.decode(bin_tuple) +{:ok, + %CBOR.Tag{ + tag: 50, + value: [ + %CBOR.Tag{tag: 51, value: "atom"}, + %CBOR.Tag{tag: 50, value: ["nested_tuple", 1, 2]} + ] + }, ""} +``` + ### Documentation Documentation can be generated with [ExDoc](https://github.com/elixir-lang/ex_doc) diff --git a/lib/cbor.ex b/lib/cbor.ex index f2f7bc5..f9bf45d 100644 --- a/lib/cbor.ex +++ b/lib/cbor.ex @@ -92,72 +92,53 @@ defmodule CBOR do iex> CBOR.encode(%{"a" => 1, "b" => [2, 3]}) <<162, 97, 97, 1, 97, 98, 130, 2, 3>> + iex> CBOR.encode(%CBOR.Tag{tag: 50, value: {1, 2, %CBOR.Tag{tag: 50, value: {"nested_tuple", 1, 2}}}}) + <<216, 50, 131, 1, 2, 216, 50, 131, 108, 110, 101, 115, 116, 101, 100, 95, 116, 117, 112, 108, 101, 1, 2>> """ @spec encode(any()) :: binary() def encode(value), do: CBOR.Encoder.encode_into(value, <<>>) @doc """ - Converts a CBOR encoded binary into native elixir data structures with a specified default - decoder function. The function added should take a tag and a value, and the caller can - specify how to decode the value associated to a tag input into the function. + Converts a CBOR encoded binary into native elixir data structures. Allows passing in a custom tag decoder that takes in a CBOR.Tag + struct and outputs a converted value. ## Examples - iex(1)> bin_tuple = CBOR.encode(%CBOR.Tag{tag: 50, value: {1, 2, "tuple"}}) - <<216, 50, 131, 1, 2, 101, 116, 117, 112, 108, 101>> + iex> CBOR.decode(<<130, 101, 72, 101, 108, 108, 111, 102, 87, 111, 114, 108, 100, 33>>) + {:ok, ["Hello", "World!"], ""} - iex(2)> tuple_decoder = fn (tag, value) -> case tag, do: (50 -> List.to_tuple(value); _ -> CBOR.Decoder.default_decode_tag(tag, value)) end - #Function<43.65746770/2 in :erl_eval.expr/5> # Note that non-matched tags are decoded using CBOR.Decoder.default_decode_tag/2 + iex> CBOR.decode(<<130, 1, 130, 2, 3>>) + {:ok, [1, [2, 3]], ""} iex> CBOR.decode(<<162, 97, 97, 1, 97, 98, 130, 2, 3>>) {:ok, %{"a" => 1, "b" => [2, 3]}, ""} - iex(3)> CBOR.decode(bin_tuple, tuple_decoder) - {:ok, {1, 2, "tuple"}, ""} + iex(1)> tuple_decoder = fn(tag_struct) -> case tag_struct.tag, do: (50 -> List.to_tuple(tag_struct.value); _ -> tag_struct) end + iex(2)> CBOR.decode(<<216, 50, 131, 1, 2, 216, 50, 131, 108, 110, 101, 115, 116, 101, 100, 95, 116, 117, 112, 108, 101, 1, 2>>, tag_decoder: tuple_decoder) + {:ok, {1, 2, {"nested_tuple", 1, 2}}, ""} - iex(4)> bin_non_tuple = CBOR.encode(%CBOR.Tag{tag: 123, value: "Hello, World!"}) - <<216, 123, 109, 72, 101, 108, 108, 111, 44, 32, 87, 111, 114, 108, 100, 33>> - - iex(5)> CBOR.decode(bin_non_tuple, tuple_decoder) - {:ok, %CBOR.Tag{tag: 123, value: "Hello, World!"}, ""} + iex> CBOR.decode(<<216, 50, 131, 1, 2, 216, 50, 131, 108, 110, 101, 115, 116, 101, 100, 95, 116, 117, 112, 108, 101, 1, 2>>) + {:ok, + %CBOR.Tag{ + tag: 50, + value: [1, 2, %CBOR.Tag{tag: 50, value: ["nested_tuple", 1, 2]}] + }, ""} """ @spec decode(binary()) :: {:ok, any(), binary()} | {:error, atom} - def decode(binary) do - try do - perform_decoding(binary) - rescue - FunctionClauseError -> {:error, :cbor_function_clause_error} - end - end - - @doc """ - Converts a CBOR encoded binary into native elixir data structures - - """ - @spec decode(binary(), fun()) :: {:ok, any(), binary()} | {:error, atom} - def decode(binary, default_decode) do + def decode(binary, opts \\ []) do try do - perform_decoding(binary, default_decode) + perform_decoding(binary, opts) rescue FunctionClauseError -> {:error, :cbor_function_clause_error} end end - defp perform_decoding(binary) when is_binary(binary) do - case CBOR.Decoder.decode(binary) do - {value, rest} -> {:ok, value, rest} - _other -> {:error, :cbor_decoder_error} - end - end - - defp perform_decoding(_value), do: {:error, :cannot_decode_non_binary_values} - - defp perform_decoding(binary, default_decode) when is_binary(binary) do - case CBOR.Decoder.decode(binary, default_decode) do + defp perform_decoding(binary, opts) when is_binary(binary) do + case CBOR.Decoder.decode(binary, opts) do {value, rest} -> {:ok, value, rest} _other -> {:error, :cbor_decoder_error} end end - defp perform_decoding(_value, _function), do: {:error, :cannot_decode_non_binary_values} + defp perform_decoding(_value, _opts), do: {:error, :cannot_decode_non_binary_values} end diff --git a/lib/cbor/decoder.ex b/lib/cbor/decoder.ex index 0351410..76bbb98 100644 --- a/lib/cbor/decoder.ex +++ b/lib/cbor/decoder.ex @@ -1,31 +1,26 @@ defmodule CBOR.Decoder do - def decode(binary) do - decode(binary, header(binary), &default_decode_tag/2) + def decode(binary, opts) do + decode(binary, header(binary), opts) end - # default_decoder set by caller to decode tags without a set decode value - def decode(binary, decode_default) do - decode(binary, header(binary), decode_default) - end - - def decode(_binary, {mt, :indefinite, rest}, _decode_default) do + def decode(_binary, {mt, :indefinite, rest}, opts) do case mt do 2 -> mark_as_bytes(decode_string_indefinite(rest, 2, [])) 3 -> decode_string_indefinite(rest, 3, []) - 4 -> decode_array_indefinite(rest, []) - 5 -> decode_map_indefinite(rest, %{}) + 4 -> decode_array_indefinite(rest, [], opts) + 5 -> decode_map_indefinite(rest, %{}, opts) end end - def decode(bin, {mt, value, rest}, decode_default) do + def decode(bin, {mt, value, rest}, opts) do case mt do 0 -> {value, rest} 1 -> {-value - 1, rest} 2 -> mark_as_bytes(decode_string(rest, value)) 3 -> decode_string(rest, value) - 4 -> decode_array(value, rest) - 5 -> decode_map(value, rest) - 6 -> decode_other(value, decode(rest), decode_default) + 4 -> decode_array(value, rest, opts) + 5 -> decode_map(value, rest, opts) + 6 -> decode_other(value, decode(rest, opts), opts) 7 -> decode_float(bin, value, rest) end end @@ -70,38 +65,38 @@ defmodule CBOR.Decoder do end end - defp decode_array(0, rest), do: {[], rest} - defp decode_array(len, rest), do: decode_array(len, [], rest) - defp decode_array(0, acc, bin), do: {Enum.reverse(acc), bin} - defp decode_array(len, acc, bin) do - {value, bin_rest} = decode(bin) - decode_array(len - 1, [value|acc], bin_rest) + defp decode_array(0, rest, _opts), do: {[], rest} + defp decode_array(len, rest, opts), do: decode_array(len, [], rest, opts) + defp decode_array(0, acc, bin, _opts), do: {Enum.reverse(acc), bin} + defp decode_array(len, acc, bin, opts) do + {value, bin_rest} = decode(bin, opts) + decode_array(len - 1, [value|acc], bin_rest, opts) end - defp decode_array_indefinite(<<0xff, new_rest::binary>>, acc) do + defp decode_array_indefinite(<<0xff, new_rest::binary>>, acc, _opts) do {Enum.reverse(acc), new_rest} end - defp decode_array_indefinite(rest, acc) do - {value, new_rest} = decode(rest) - decode_array_indefinite(new_rest, [value | acc]) + defp decode_array_indefinite(rest, acc, opts) do + {value, new_rest} = decode(rest, opts) + decode_array_indefinite(new_rest, [value | acc], opts) end - defp decode_map(0, rest), do: {%{}, rest} - defp decode_map(len, rest), do: decode_map(len, %{}, rest) - defp decode_map(0, acc, bin), do: {acc, bin} - defp decode_map(len, acc, bin) do - {key, key_rest} = decode(bin) - {value, bin_rest} = decode(key_rest) + defp decode_map(0, rest, _opts), do: {%{}, rest} + defp decode_map(len, rest, opts), do: decode_map(len, %{}, rest, opts) + defp decode_map(0, acc, bin, _opts), do: {acc, bin} + defp decode_map(len, acc, bin, opts) do + {key, key_rest} = decode(bin, opts) + {value, bin_rest} = decode(key_rest, opts) - decode_map(len - 1, Map.put(acc, key, value), bin_rest) + decode_map(len - 1, Map.put(acc, key, value), bin_rest, opts) end - defp decode_map_indefinite(<<0xff, new_rest::binary>>, acc), do: {acc, new_rest} - defp decode_map_indefinite(rest, acc) do - {key, key_rest} = decode(rest) - {value, new_rest} = decode(key_rest) - decode_map_indefinite(new_rest, Map.put(acc, key, value)) + defp decode_map_indefinite(<<0xff, new_rest::binary>>, acc, _opts), do: {acc, new_rest} + defp decode_map_indefinite(rest, acc, opts) do + {key, key_rest} = decode(rest, opts) + {value, new_rest} = decode(key_rest, opts) + decode_map_indefinite(new_rest, Map.put(acc, key, value), opts) end defp decode_float(bin, value, rest) do @@ -130,7 +125,13 @@ defmodule CBOR.Decoder do end end - defp decode_other(tag, {value, rest}, decode_default), do: {decode_default.(tag, value), rest} + defp decode_other(tag, {value, rest}, opts) do + decoded_tag = decode_tag(tag, value) + case opts == [] do + true -> {decoded_tag, rest} + false -> {Keyword.get(opts, :tag_decoder).(decoded_tag), rest} + end + end def decode_non_finite(0, 0), do: %CBOR.Tag{tag: :float, value: :inf} def decode_non_finite(1, 0), do: %CBOR.Tag{tag: :float, value: :"-inf"} @@ -144,10 +145,6 @@ defmodule CBOR.Decoder do value * 5192296858534827628530496329220096.0 end - def default_decode_tag(tag, value) do - decode_tag(tag, value) - end - defp decode_tag(0, value), do: decode_datetime(value) defp decode_tag(3, value), do: -decode_tag(2, value) - 1 diff --git a/lib/cbor/tag.ex b/lib/cbor/tag.ex index 994c9a2..1b60844 100644 --- a/lib/cbor/tag.ex +++ b/lib/cbor/tag.ex @@ -1,4 +1,4 @@ defmodule CBOR.Tag do @enforce_keys [:tag, :value] defstruct [:tag, :value] -end \ No newline at end of file +end diff --git a/test/cbor/decoder_test.exs b/test/cbor/decoder_test.exs index 5cde477..8881f62 100644 --- a/test/cbor/decoder_test.exs +++ b/test/cbor/decoder_test.exs @@ -8,7 +8,7 @@ defmodule CBOR.DecoderTest do test "too little data" do assert_raise(FunctionClauseError, fn -> - CBOR.Decoder.decode("") == 1 + CBOR.Decoder.decode("", []) == 1 end) end