Current section

Files

Jump to
amqp10_common src amqp10_framing.erl
Raw

src/amqp10_framing.erl

%% This Source Code Form is subject to the terms of the Mozilla Public
%% License, v. 2.0. If a copy of the MPL was not distributed with this
%% file, You can obtain one at https://mozilla.org/MPL/2.0/.
%%
%% Copyright (c) 2007-2025 Broadcom. All Rights Reserved. The term “Broadcom” refers to Broadcom Inc. and/or its subsidiaries. All rights reserved.
%%
-module(amqp10_framing).
-export([version/0,
encode/1,
encode_described/3,
encode_bin/1,
decode/1,
decode_bin/1,
decode_bin/2,
symbol_for/1,
number_for/1,
pprint/1]).
%% debug
-export([fill_from_list/2, fill_from_map/2]).
-include("amqp10_framing.hrl").
-type amqp10_frame() :: #'v1_0.header'{} |
#'v1_0.delivery_annotations'{} |
#'v1_0.message_annotations'{} |
#'v1_0.properties'{} |
#'v1_0.application_properties'{} |
#'v1_0.data'{} |
#'v1_0.amqp_sequence'{} |
#'v1_0.amqp_value'{} |
#'v1_0.footer'{} |
#'v1_0.received'{} |
#'v1_0.accepted'{} |
#'v1_0.rejected'{} |
#'v1_0.released'{} |
#'v1_0.modified'{} |
#'v1_0.source'{} |
#'v1_0.target'{} |
#'v1_0.delete_on_close'{} |
#'v1_0.delete_on_no_links'{} |
#'v1_0.delete_on_no_messages'{} |
#'v1_0.delete_on_no_links_or_messages'{} |
#'v1_0.sasl_mechanisms'{} |
#'v1_0.sasl_init'{} |
#'v1_0.sasl_challenge'{} |
#'v1_0.sasl_response'{} |
#'v1_0.sasl_outcome'{} |
#'v1_0.attach'{} |
#'v1_0.flow'{} |
#'v1_0.transfer'{} |
#'v1_0.disposition'{} |
#'v1_0.detach'{} |
#'v1_0.end'{} |
#'v1_0.close'{} |
#'v1_0.error'{} |
#'v1_0.coordinator'{} |
#'v1_0.declare'{} |
#'v1_0.discharge'{} |
#'v1_0.declared'{} |
#'v1_0.transactional_state'{}.
version() ->
{1, 0, 0}.
%% These are essentially in lieu of code generation ..
fill_from_list(Record, Fields) ->
{Res, _} = lists:foldl(
fun (Field, {Record1, Num}) ->
DecodedField = decode(Field),
{setelement(Num, Record1, DecodedField),
Num + 1}
end,
{Record, 2}, Fields),
Res.
fill_from_map(Record, Fields) ->
{Res, _} = lists:foldl(
fun (Key, {Record1, Num}) ->
case proplists:get_value(Key, Fields) of
undefined ->
{Record1, Num+1};
Value ->
{setelement(Num, Record1, decode(Value)), Num+1}
end
end,
{Record, 2}, keys(Record)),
Res.
fill_from(F = #'v1_0.data'{}, Field) ->
F#'v1_0.data'{content = Field};
fill_from(F = #'v1_0.amqp_value'{}, Field) ->
F#'v1_0.amqp_value'{content = Field}.
keys(Record) ->
[{symbol, symbolify(K)} || K <- amqp10_framing0:fields(Record)].
symbolify(FieldName) when is_atom(FieldName) ->
re:replace(atom_to_list(FieldName), "_", "-", [{return,binary}, global]).
%% TODO: in fields of composite types with multiple=true, "a null
%% value and a zero-length array (with a correct type for its
%% elements) both describe an absence of a value and should be treated
%% as semantically identical." (see section 1.3)
decode({described, Descriptor, {list, Fields} = Type}) ->
case amqp10_framing0:record_for(Descriptor) of
#'v1_0.flow'{} = Flow ->
amqp10_composite:flow(Flow, Fields);
#'v1_0.transfer'{} = Transfer ->
amqp10_composite:transfer(Transfer, Fields);
#'v1_0.disposition'{} = Disposition ->
amqp10_composite:disposition(Disposition, Fields);
#'v1_0.header'{} = Header ->
amqp10_composite:header(Header, Fields);
#'v1_0.properties'{} = Properties ->
amqp10_composite:properties(Properties, Fields);
#'v1_0.amqp_sequence'{} ->
#'v1_0.amqp_sequence'{content = [decode(F) || F <- Fields]};
#'v1_0.amqp_value'{} ->
#'v1_0.amqp_value'{content = Type};
Else ->
fill_from_list(Else, Fields)
end;
decode({described, Descriptor, {map, Fields} = Type}) ->
case amqp10_framing0:record_for(Descriptor) of
#'v1_0.application_properties'{} ->
#'v1_0.application_properties'{content = decode_map(Fields)};
#'v1_0.delivery_annotations'{} ->
#'v1_0.delivery_annotations'{content = decode_annotations(Fields)};
#'v1_0.message_annotations'{} ->
#'v1_0.message_annotations'{content = decode_annotations(Fields)};
#'v1_0.footer'{} ->
#'v1_0.footer'{content = decode_annotations(Fields)};
#'v1_0.amqp_value'{} ->
#'v1_0.amqp_value'{content = Type};
Else ->
fill_from_map(Else, Fields)
end;
decode({described, Descriptor, {binary, Field} = Type}) ->
case amqp10_framing0:record_for(Descriptor) of
#'v1_0.amqp_value'{} ->
#'v1_0.amqp_value'{content = Type};
#'v1_0.data'{} ->
#'v1_0.data'{content = Field}
end;
decode({described, Descriptor, Field}) ->
fill_from(amqp10_framing0:record_for(Descriptor), Field);
decode(null) ->
undefined;
decode(Other) ->
Other.
decode_map(Fields) ->
[{decode(K), decode(V)} || {K, V} <- Fields].
%% "The annotations type is a map where the keys are restricted to be of type symbol
%% or of type ulong. All ulong keys, and all symbolic keys except those beginning
%% with "x-" are reserved." [3.2.10]
%% Since we already parse annotations here and neither the client nor server uses
%% reserved keys, we perform strict validation and throw if any reserved keys are used.
decode_annotations(Fields) ->
lists:map(fun({{symbol, <<"x-", _/binary>>} = K, V}) ->
{K, decode(V)};
({ReservedKey, _V}) ->
throw({reserved_annotation_key, ReservedKey})
end, Fields).
-spec encode_described(list | map | binary | annotations | '*',
non_neg_integer(),
amqp10_frame()) ->
amqp10_binary_generator:amqp10_described().
encode_described(list, CodeNumber,
#'v1_0.amqp_sequence'{content = Content}) ->
{described, {ulong, CodeNumber},
{list, lists:map(fun encode/1, Content)}};
encode_described(list, CodeNumber, Rec) ->
L = if is_record(Rec, 'v1_0.flow') orelse
is_record(Rec, 'v1_0.transfer') orelse
is_record(Rec, 'v1_0.disposition') orelse
is_record(Rec, 'v1_0.header') orelse
is_record(Rec, 'v1_0.properties') ->
encode_fields_omit_trailing_null(Rec, true, tuple_size(Rec), []);
true ->
encode_fields(Rec, 2, tuple_size(Rec))
end,
{described, {ulong, CodeNumber}, {list, L}};
encode_described(map, CodeNumber,
#'v1_0.application_properties'{content = Content}) ->
{described, {ulong, CodeNumber}, {map, Content}};
encode_described(map, CodeNumber,
#'v1_0.delivery_annotations'{content = Content}) ->
{described, {ulong, CodeNumber}, {map, Content}};
encode_described(map, CodeNumber,
#'v1_0.message_annotations'{content = Content}) ->
{described, {ulong, CodeNumber}, {map, Content}};
encode_described(map, CodeNumber,
#'v1_0.footer'{content = Content}) ->
{described, {ulong, CodeNumber}, {map, Content}};
encode_described(binary, CodeNumber, #'v1_0.data'{content = Content}) ->
{described, {ulong, CodeNumber}, {binary, Content}};
encode_described('*', CodeNumber, #'v1_0.amqp_value'{content = Content}) ->
{described, {ulong, CodeNumber}, Content};
encode_described(annotations, CodeNumber, Frame) ->
encode_described(map, CodeNumber, Frame).
encode_fields(_, N, Size) when N > Size ->
[];
encode_fields(Tup, N, Size) ->
[encode(element(N, Tup)) | encode_fields(Tup, N + 1, Size)].
encode_fields_omit_trailing_null(_, _, 1, L) ->
L;
encode_fields_omit_trailing_null(Tup, Omit, N, L) ->
case element(N, Tup) of
undefined when Omit ->
encode_fields_omit_trailing_null(Tup, Omit, N - 1, L);
Val ->
encode_fields_omit_trailing_null(Tup, false, N - 1, [encode(Val) | L])
end.
encode(X) ->
amqp10_framing0:encode(X).
-spec encode_bin(term()) -> iodata().
encode_bin(X) ->
amqp10_binary_generator:generate(encode(X)).
-spec decode_bin(binary()) -> [term()].
decode_bin(Binary) ->
[decode(Section) || Section <- amqp10_binary_parser:parse_many(Binary, [])].
-spec decode_bin(binary(), amqp10_binary_parser:opts()) -> [term()].
decode_bin(Binary, Opts) ->
lists:map(fun({Pos = {pos, _}, Section}) ->
{Pos, decode(Section)};
(Section) ->
decode(Section)
end, amqp10_binary_parser:parse_many(Binary, Opts)).
symbol_for(X) ->
amqp10_framing0:symbol_for(X).
number_for(X) ->
amqp10_framing0:number_for(X).
pprint(Thing) when is_tuple(Thing) ->
case amqp10_framing0:fields(Thing) of
unknown -> Thing;
Names -> [T|L] = tuple_to_list(Thing),
{T, lists:zip(Names, [pprint(I) || I <- L])}
end;
pprint(Other) -> Other.
-ifdef(TEST).
-include_lib("eunit/include/eunit.hrl").
encode_decode_test_() ->
Data = [{{symbol, <<"x-my key">>}, {binary, <<"my value">>}}],
Test = fun(M) -> [M] = decode_bin(iolist_to_binary(encode_bin(M))) end,
[
fun() -> Test(#'v1_0.application_properties'{content = Data}) end,
fun() -> Test(#'v1_0.delivery_annotations'{content = Data}) end,
fun() -> Test(#'v1_0.message_annotations'{content = Data}) end,
fun() -> Test(#'v1_0.footer'{content = Data}) end
].
encode_decode_amqp_sequence_test() ->
L = [{utf8, <<"k">>},
{binary, <<"v">>}],
F = #'v1_0.amqp_sequence'{content = L},
[F] = decode_bin(iolist_to_binary(encode_bin(F))),
ok.
-endif.