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
35 changes: 35 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
25 changes: 19 additions & 6 deletions lib/cbor.ex
Original file line number Diff line number Diff line change
Expand Up @@ -92,12 +92,15 @@ 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
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

Expand All @@ -110,22 +113,32 @@ defmodule CBOR do
iex> CBOR.decode(<<162, 97, 97, 1, 97, 98, 130, 2, 3>>)
{:ok, %{"a" => 1, "b" => [2, 3]}, ""}

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> 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
def decode(binary, opts \\ []) do
try do
perform_decoding(binary)
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
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), do: {:error, :cannot_decode_non_binary_values}
defp perform_decoding(_value, _opts), do: {:error, :cannot_decode_non_binary_values}
end
70 changes: 38 additions & 32 deletions lib/cbor/decoder.ex
Original file line number Diff line number Diff line change
@@ -1,26 +1,26 @@
defmodule CBOR.Decoder do
def decode(binary) do
decode(binary, header(binary))
def decode(binary, opts) do
decode(binary, header(binary), opts)
end

def decode(_binary, {mt, :indefinite, rest}) 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}) 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))
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
Expand Down Expand Up @@ -65,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
Expand Down Expand Up @@ -125,7 +125,13 @@ defmodule CBOR.Decoder do
end
end

defp decode_other(value, {inner, rest}), do: {decode_tag(value, inner), 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"}
Expand Down
2 changes: 1 addition & 1 deletion lib/cbor/tag.ex
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
defmodule CBOR.Tag do
@enforce_keys [:tag, :value]
defstruct [:tag, :value]
end
end
2 changes: 1 addition & 1 deletion test/cbor/decoder_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down