
From code review Co-authored-by: Radosław Rowicki <35342116+radrow@users.noreply.github.com>
150 lines
5.6 KiB
Erlang
150 lines
5.6 KiB
Erlang
%%%-------------------------------------------------------------------
|
|
%%% @copyright (C) 2018, Aeternity Anstalt
|
|
%%% @doc
|
|
%%% Functions for serializing generic objects to/from binary format.
|
|
%%% @end
|
|
%%%-------------------------------------------------------------------
|
|
|
|
-module(aeserialization).
|
|
|
|
-export([ decode_fields/2
|
|
, deserialize/5
|
|
, deserialize_tag_and_vsn/1
|
|
, encode_fields/2
|
|
, serialize/4 ]).
|
|
|
|
%%%===================================================================
|
|
%%% Types
|
|
%%%===================================================================
|
|
|
|
-export_type([ template/0
|
|
, fields/0
|
|
]).
|
|
|
|
-type template() :: [{field_name(), type()}].
|
|
-type field_name() :: atom().
|
|
-type type() :: 'int'
|
|
| 'bool'
|
|
| 'binary'
|
|
| 'id' %% As defined in aec_id.erl
|
|
| [type()] %% Length one in the type. This means a list of any length.
|
|
| #{items := [{field_name(), type()}]} %% Record with named fields represented as a map. Encoded as a list in the given order.
|
|
| tuple(). %% Any arity, containing type(). This means a static size array.
|
|
|
|
-type encodable_term() :: non_neg_integer()
|
|
| binary()
|
|
| boolean()
|
|
| [encodable_term()] %% Of any length
|
|
| #{atom() => encodable_term()}
|
|
| tuple() %% Any arity, containing encodable_term().
|
|
| aeser_id:id().
|
|
|
|
-type fields() :: [{field_name(), encodable_term()}].
|
|
|
|
%%%===================================================================
|
|
%%% API
|
|
%%%===================================================================
|
|
|
|
-spec serialize(non_neg_integer(), non_neg_integer(), template(), fields()) -> binary().
|
|
serialize(Tag, Vsn, Template, Fields) ->
|
|
List = encode_fields([{tag, int}, {vsn, int}|Template],
|
|
[{tag, Tag}, {vsn, Vsn}|Fields]),
|
|
aeser_rlp:encode(List).
|
|
|
|
%% Type isn't strictly necessary, but will give a better error reason
|
|
-spec deserialize(atom(), non_neg_integer(), non_neg_integer(),
|
|
template(), binary()) -> fields().
|
|
deserialize(Type, Tag, Vsn, Template0, Binary) ->
|
|
Decoded = aeser_rlp:decode(Binary),
|
|
Template = [{tag, int}, {vsn, int}|Template0],
|
|
case decode_fields(Template, Decoded) of
|
|
[{tag, Tag}, {vsn, Vsn}|Left] ->
|
|
Left;
|
|
Other ->
|
|
error({illegal_serialization, Type, Vsn,
|
|
Other, Binary, Decoded, Template})
|
|
end.
|
|
|
|
-spec deserialize_tag_and_vsn(binary()) ->
|
|
{non_neg_integer(), non_neg_integer(), fields()}.
|
|
deserialize_tag_and_vsn(Binary) ->
|
|
[TagBin, VsnBin|Fields] = aeser_rlp:decode(Binary),
|
|
Template = [{tag, int}, {vsn, int}],
|
|
[{tag, Tag}, {vsn, Vsn}] = decode_fields(Template, [TagBin, VsnBin]),
|
|
{Tag, Vsn, Fields}.
|
|
|
|
encode_fields([{Field, Type}|TypesLeft],
|
|
[{Field, Val}|FieldsLeft]) ->
|
|
try encode_field(Type, Val) of
|
|
Encoded -> [Encoded | encode_fields(TypesLeft, FieldsLeft)]
|
|
catch error:{illegal, T, V} ->
|
|
error({illegal_field, Field, Type, Val, T, V})
|
|
end;
|
|
encode_fields([], []) ->
|
|
[];
|
|
encode_fields(Template, Values) ->
|
|
error({illegal_template_or_values, Template, Values}).
|
|
|
|
decode_fields([{Field, Type}|TypesLeft],
|
|
[Bin |FieldsLeft]) ->
|
|
try decode_field(Type, Bin) of
|
|
Decoded -> [{Field, Decoded} | decode_fields(TypesLeft, FieldsLeft)]
|
|
catch error:{illegal, T, V} ->
|
|
error({illegal_field, Field, Type, Bin, T, V})
|
|
end;
|
|
decode_fields([], []) ->
|
|
[];
|
|
decode_fields(Template, Values) ->
|
|
error({illegal_template_or_values, Template, Values}).
|
|
|
|
%%%===================================================================
|
|
%%% Internal functions
|
|
%%%===================================================================
|
|
|
|
encode_field([Type], L) when is_list(L) ->
|
|
[encode_field(Type, X) || X <- L];
|
|
encode_field(#{items := Items}, Map) ->
|
|
lists:map(
|
|
fun({K, Type}) ->
|
|
V = maps:get(K, Map),
|
|
encode_field(Type, V)
|
|
end, Items);
|
|
encode_field(Type, T) when tuple_size(Type) =:= tuple_size(T) ->
|
|
Zipped = lists:zip(tuple_to_list(Type), tuple_to_list(T)),
|
|
[encode_field(X, Y) || {X, Y} <- Zipped];
|
|
encode_field(int, X) when is_integer(X), X >= 0 ->
|
|
binary:encode_unsigned(X);
|
|
encode_field(binary, X) when is_binary(X) -> X;
|
|
encode_field(bool, true) -> <<1:8>>;
|
|
encode_field(bool, false) -> <<0:8>>;
|
|
encode_field(id, Val) ->
|
|
try aeser_id:encode(Val)
|
|
catch _:_ -> error({illegal, id, Val})
|
|
end;
|
|
encode_field(Type, Val) -> error({illegal, Type, Val}).
|
|
|
|
decode_field([Type], List) when is_list(List) ->
|
|
[decode_field(Type, X) || X <- List];
|
|
decode_field(#{items := Items}, List) when length(List) =:= length(Items) ->
|
|
Zipped = lists:zip(Items, List),
|
|
lists:foldl(
|
|
fun({{K, Type}, V}, Map) ->
|
|
maps:is_key(K, Map) andalso error(badarg, duplicate_field),
|
|
Map#{ K => decode_field(Type, V) }
|
|
end, #{}, Zipped);
|
|
decode_field(Type, List) when length(List) =:= tuple_size(Type) ->
|
|
Zipped = lists:zip(tuple_to_list(Type), List),
|
|
list_to_tuple([decode_field(X, Y) || {X, Y} <- Zipped]);
|
|
decode_field(int, <<0:8, X/binary>> = B) when X =/= <<>> ->
|
|
error({illegal, int, B});
|
|
decode_field(int, X) when is_binary(X) -> binary:decode_unsigned(X);
|
|
decode_field(binary, X) when is_binary(X) -> X;
|
|
decode_field(bool, <<1:8>>) -> true;
|
|
decode_field(bool, <<0:8>>) -> false;
|
|
decode_field(id, Val) ->
|
|
try aeser_id:decode(Val)
|
|
catch _:_ -> error({illegal, id, Val})
|
|
end;
|
|
decode_field(Type, X) -> error({illegal, Type, X}).
|
|
|