diff options
author | Anders Svensson <[email protected]> | 2017-06-11 13:59:02 +0200 |
---|---|---|
committer | Anders Svensson <[email protected]> | 2017-06-13 13:50:07 +0200 |
commit | 205521d3927ed6f53c9a6fa3095f8a879bdca929 (patch) | |
tree | 7eda323ec84c0cb19bd55dd95735636bc08c6c11 /lib/diameter/src/base/diameter_gen.erl | |
parent | cac106defc5060c5e485480e8003b992482d751d (diff) | |
download | otp-205521d3927ed6f53c9a6fa3095f8a879bdca929.tar.gz otp-205521d3927ed6f53c9a6fa3095f8a879bdca929.tar.bz2 otp-205521d3927ed6f53c9a6fa3095f8a879bdca929.zip |
Move (most of) diameter_gen.hrl to diameter_gen.erl
To remove the requirement that dictionary modules be recompiled whenever
the encode/decode implementation changes. The included diameter_gen.hrl
now only contains trivial functions that call info diameter_gen.erl.
Diffstat (limited to 'lib/diameter/src/base/diameter_gen.erl')
-rw-r--r-- | lib/diameter/src/base/diameter_gen.erl | 709 |
1 files changed, 709 insertions, 0 deletions
diff --git a/lib/diameter/src/base/diameter_gen.erl b/lib/diameter/src/base/diameter_gen.erl new file mode 100644 index 0000000000..e832832876 --- /dev/null +++ b/lib/diameter/src/base/diameter_gen.erl @@ -0,0 +1,709 @@ +%% +%% %CopyrightBegin% +%% +%% Copyright Ericsson AB 2010-2017. All Rights Reserved. +%% +%% Licensed under the Apache License, Version 2.0 (the "License"); +%% you may not use this file except in compliance with the License. +%% You may obtain a copy of the License at +%% +%% http://www.apache.org/licenses/LICENSE-2.0 +%% +%% Unless required by applicable law or agreed to in writing, software +%% distributed under the License is distributed on an "AS IS" BASIS, +%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +%% See the License for the specific language governing permissions and +%% limitations under the License. +%% +%% %CopyrightEnd% +%% + +%% +%% This file contains code that encode/decode modules generated by +%% diameter_codegen.erl calls to implement the functionality. This +%% code does most of the work, the generated code being kept simple. +%% + +-module(diameter_gen). + +-export([encode_avps/3, + decode_avps/3, + grouped_avp/4, + empty_group/2, + empty/2]). + +-include_lib("diameter/include/diameter.hrl"). + +-define(THROW(T), throw({?MODULE, T})). + +-type parent_name() :: atom(). %% parent = Message or AVP +-type parent_record() :: tuple(). %% +-type avp_name() :: atom(). +-type avp_record() :: tuple(). +-type avp_values() :: [{avp_name(), term()}]. + +-type non_grouped_avp() :: #diameter_avp{}. +-type grouped_avp() :: nonempty_improper_list(#diameter_avp{}, [avp()]). +-type avp() :: non_grouped_avp() | grouped_avp(). + +%% --------------------------------------------------------------------------- +%% # encode_avps/3 +%% --------------------------------------------------------------------------- + +-spec encode_avps(parent_name(), parent_record() | avp_values(), map()) + -> iolist() + | no_return(). + +encode_avps(Name, Vals, #{module := Mod} = Opts) -> + try + encode(Name, Vals, Opts, Mod) + catch + throw: {?MODULE, Reason} -> + diameter_lib:log({encode, error}, + ?MODULE, + ?LINE, + {Reason, Name, Vals, Mod}), + erlang:error(list_to_tuple(Reason ++ [Name])); + error: Reason -> + Stack = erlang:get_stacktrace(), + diameter_lib:log({encode, failure}, + ?MODULE, + ?LINE, + {Reason, Name, Vals, Mod, Stack}), + erlang:error({encode_failure, Reason, Name, Stack}) + end. + +%% encode/4 + +encode(Name, Vals, #{ordered_encode := false} = Opts, Mod) + when is_list(Vals) -> + lists:map(fun({F,V}) -> encode(Name, F, V, Opts, Mod) end, Vals); + +encode(Name, Vals, Opts, Mod) + when is_list(Vals) -> + encode(Name, Mod:'#set-'(Vals, newrec(Mod, Name)), Opts, Mod); + +encode(Name, Rec, Opts, Mod) -> + [encode(Name, F, V, Opts, Mod) || {F,V} <- Mod:'#get-'(Rec)]. + +%% encode/5 + +encode(Name, AvpName, Values, Opts, Mod) -> + enc(Name, AvpName, Mod:avp_arity(Name, AvpName), Values, Opts, Mod). + +%% enc/6 + +enc(_, AvpName, 1, undefined, _, _) -> + ?THROW([mandatory_avp_missing, AvpName]); + +enc(Name, AvpName, 1, Value, Opts, Mod) -> + enc(Name, AvpName, [Value], Opts, Mod); + +enc(_, _, {0,_}, [], _, _) -> + []; + +enc(_, AvpName, _, T, _, _) + when not is_list(T) -> + ?THROW([repeated_avp_as_non_list, AvpName, T]); + +enc(_, AvpName, {Min, _}, L, _, _) + when length(L) < Min -> + ?THROW([repeated_avp_insufficient_arity, AvpName, Min, L]); + +enc(_, AvpName, {_, Max}, L, _, _) + when Max < length(L) -> + ?THROW([repeated_avp_excessive_arity, AvpName, Max, L]); + +enc(Name, AvpName, _, Values, Opts, Mod) -> + enc(Name, AvpName, Values, Opts, Mod). + +%% enc/5 + +enc(Name, 'AVP', Values, Opts, Mod) -> + [enc_AVP(Name, A, Opts, Mod) || A <- Values]; + +enc(_, AvpName, Values, Opts, Mod) -> + enc(AvpName, Values, Opts, Mod). + +%% enc/4 + +enc(AvpName, Values, Opts, Mod) -> + H = Mod:avp_header(AvpName), + [diameter_codec:pack_data(H, Mod:avp(encode, V, AvpName, Opts)) + || V <- Values]. + +%% enc_AVP/4 + +%% No value: assume AVP data is already encoded. The normal case will +%% be when this is passed back from #diameter_packet.errors as a +%% consequence of a failed decode. Any AVP can be encoded this way +%% however, which side-steps any arity checks for known AVP's and +%% could potentially encode something unfortunate. +enc_AVP(_, #diameter_avp{value = undefined} = A, Opts, _) -> + diameter_codec:pack_avp(A, Opts); + +%% Missing name for value encode. +enc_AVP(_, #diameter_avp{name = N, value = V}, _, _) + when N == undefined; + N == 'AVP' -> + ?THROW([value_with_nameless_avp, N, V]); + +%% Or not. Ensure that 'AVP' is the appropriate field. Note that if we +%% don't know this AVP at all then the encode will fail. +enc_AVP(Name, #diameter_avp{name = AvpName, value = Data}, Opts, Mod) -> + 0 == Mod:avp_arity(Name, AvpName) + orelse ?THROW([known_avp_as_AVP, Name, AvpName, Data]), + enc(AvpName, [Data], Opts, Mod); + +%% The backdoor ... +enc_AVP(_, {AvpName, Value}, Opts, Mod) -> + enc(AvpName, [Value], Opts, Mod); + +%% ... and the side door. +enc_AVP(_Name, {_Dict, _AvpName, _Data} = T, Opts, _) -> + diameter_codec:pack_avp(#diameter_avp{data = T}, Opts). + +%% --------------------------------------------------------------------------- +%% # decode_avps/3 +%% --------------------------------------------------------------------------- + +-spec decode_avps(parent_name(), [#diameter_avp{}], map()) + -> {parent_record(), [avp()], Failed} + when Failed :: [{5000..5999, #diameter_avp{}}]. + +decode_avps(Name, Recs, #{module := Mod} = Opts) -> + {Avps, {Rec, Failed}} + = mapfoldl(fun(T,A) -> decode(Name, Opts, Mod, T, A) end, + {newrec(Mod, Name), []}, + Recs), + {Rec, Avps, Failed ++ missing(Rec, Name, Failed, Opts, Mod)}. +%% Append 5005 errors so that errors are reported in the order +%% encountered. Failed-AVP should typically contain the first +%% encountered error accordg to the RFC. + +%% mapfoldl/3 +%% +%% Like lists:mapfoldl/3, but don't reverse the list. + +mapfoldl(F, Acc, List) -> + mapfoldl(F, Acc, List, []). + +mapfoldl(F, Acc0, [T|Rest], List) -> + {B, Acc} = F(T, Acc0), + mapfoldl(F, Acc, Rest, [B|List]); +mapfoldl(_, Acc, [], List) -> + {List, Acc}. + +%% 3588: +%% +%% DIAMETER_MISSING_AVP 5005 +%% The request did not contain an AVP that is required by the Command +%% Code definition. If this value is sent in the Result-Code AVP, a +%% Failed-AVP AVP SHOULD be included in the message. The Failed-AVP +%% AVP MUST contain an example of the missing AVP complete with the +%% Vendor-Id if applicable. The value field of the missing AVP +%% should be of correct minimum length and contain zeros. + +missing(Rec, Name, Failed, Opts, Mod) -> + Avps = lists:foldl(fun({_, #diameter_avp{code = C, vendor_id = V}}, A) -> + maps:put({C,V}, true, A) + end, + maps:new(), + Failed), + missing(Mod:avp_arity(Name), tl(tuple_to_list(Rec)), Avps, Opts, Mod, []). + +missing([{Name, Arity} | As], [Value | Vs], Avps, Opts, Mod, Acc) -> + missing(As, + Vs, + Avps, + Opts, + Mod, + case + [H || missing_arity(Arity, Value), + {C,_,V} = H <- [Mod:avp_header(Name)], + not maps:is_key({C,V}, Avps)] + of + [H] -> + [{5005, empty_avp(Name, H, Opts, Mod)} | Acc]; + [] -> + Acc + end); + +missing([], [], _, _, _, Acc) -> + Acc. + +%% Maximum arities have already been checked in building the record. + +missing_arity(1, V) -> + V == undefined; +missing_arity({0, _}, _) -> + false; +missing_arity({1, _}, L) -> + [] == L; +missing_arity({Min, _}, L) -> + not has_prefix(Min, L). + +%% Compare a non-negative integer and the length of a list without +%% computing the length. +has_prefix(0, _) -> + true; +has_prefix(_, []) -> + false; +has_prefix(N, [_|L]) -> + has_prefix(N-1, L). + +%% empty_avp/4 + +empty_avp(Name, {Code, Flags, VId}, Opts, Mod) -> + {Name, Type} = Mod:avp_name(Code, VId), + #diameter_avp{name = Name, + code = Code, + vendor_id = VId, + is_mandatory = 0 /= (Flags band 2#01000000), + need_encryption = 0 /= (Flags band 2#00100000), + data = Mod:empty_value(Name, Opts), + type = Type}. + +%% 3588, ch 7: +%% +%% The Result-Code AVP describes the error that the Diameter node +%% encountered in its processing. In case there are multiple errors, +%% the Diameter node MUST report only the first error it encountered +%% (detected possibly in some implementation dependent order). The +%% specific errors that can be described by this AVP are described in +%% the following section. + +%% decode/5 + +decode(Name, + Opts, + Mod, + #diameter_avp{code = Code, vendor_id = Vid} + = Avp, + Acc) -> + decode(Name, Opts, Mod, Mod:avp_name(Code, Vid), Avp, Acc). + +%% decode/6 + +%% AVP not in dictionary. +decode(Name, Opts, Mod, 'AVP', Avp, Acc) -> + decode_AVP(Name, Avp, Opts, Mod, Acc); + +%% 6733, 4.4: +%% +%% Receivers of a Grouped AVP that does not have the 'M' (mandatory) +%% bit set and one or more of the encapsulated AVPs within the group +%% has the 'M' (mandatory) bit set MAY simply be ignored if the +%% Grouped AVP itself is unrecognized. The rule applies even if the +%% encapsulated AVP with its 'M' (mandatory) bit set is further +%% encapsulated within other sub-groups, i.e., other Grouped AVPs +%% embedded within the Grouped AVP. +%% +%% The first sentence is slightly mangled, but take it to mean this: +%% +%% An unrecognized AVP of type Grouped that does not set the 'M' bit +%% MAY be ignored even if one of its encapsulated AVPs sets the 'M' +%% bit. +%% +%% The text above is a change from RFC 3588, which instead says this: +%% +%% Further, if any of the AVPs encapsulated within a Grouped AVP has +%% the 'M' (mandatory) bit set, the Grouped AVP itself MUST also +%% include the 'M' bit set. +%% +%% Both of these texts have problems. If the AVP is unknown then its +%% type is unknown since the type isn't sent over the wire, so the +%% 6733 text becomes a non-statement: don't know that the AVP not +%% setting the M-bit is of type Grouped, therefore can't know that its +%% data consists of encapsulated AVPs, therefore can't but ignore that +%% one of these might set the M-bit. It should be no worse if we know +%% the AVP to have type Grouped. +%% +%% Similarly, for the 3588 text: if we receive an AVP that doesn't set +%% the M-bit and don't know that the AVP has type Grouped then we +%% can't realize that its data contains an AVP that sets the M-bit, so +%% can't regard the AVP as erroneous on this account. Again, it should +%% be no worse if the type is known to be Grouped, but in this case +%% the RFC forces us to regard the AVP as erroneous. This is +%% inconsistent, and the 3588 text has never been enforced. +%% +%% So, if an AVP doesn't set the M-bit then we're free to ignore it, +%% regardless of the AVP's type. If we know the type to be Grouped +%% then we must ignore the M-bit on an encapsulated AVP. That means +%% packing such an encapsulated AVP into an 'AVP' field if need be, +%% not regarding the lack of a specific field as an error as is +%% otherwise the case. (The lack of an AVP-specific field being how we +%% defined the RFC's "unrecognized", which is slightly stronger than +%% "not defined".) + +decode(Name, Opts0, Mod, {AvpName, Type}, Avp, Acc) -> + #diameter_avp{data = Data, is_mandatory = M} + = Avp, + + %% Whether or not to ignore an M-bit on an encapsulated AVP, or on + %% all AVPs with the service_opt() strict_mbit. + Opts1 = set_strict(Type, M, Opts0), + + %% Whether or not we're decoding within Failed-AVP and should + %% ignore decode errors. + #{dictionary := AppMod, failed_avp := Failed} + = Opts + = set_failed(Name, Opts1), %% Not AvpName or else a failed Failed-AVP + %% decode is packed into 'AVP'. + + %% Reset the dictionary for best-effort decode of Failed-AVP. + DecMod = if Failed -> + AppMod; + true -> + Mod + end, + + %% On decode, a Grouped AVP is represented as a #diameter_avp{} + %% list with AVP as head and component AVPs as tail. On encode, + %% data can be a list of component AVPs. + + try avp_decode(Data, AvpName, Opts, DecMod, Mod) of + {Rec, As} when Type == 'Grouped' -> + A = Avp#diameter_avp{name = AvpName, + value = Rec, + type = Type}, + {[A|As], pack_avp(Name, A, Opts, Mod, Acc)}; + + V when Type /= 'Grouped' -> + A = Avp#diameter_avp{name = AvpName, + value = V, + type = Type}, + {A, pack_avp(Name, A, Opts, Mod, Acc)} + catch + throw: {?MODULE, {grouped, Error, ComponentAvps}} -> + decode_error(Name, + Error, + ComponentAvps, + Opts, + Mod, + Avp#diameter_avp{name = AvpName, + data = trim(Avp#diameter_avp.data), + type = Type}, + Acc); + + error: Reason -> + decode_error(Name, + Reason, + Opts, + Mod, + Avp#diameter_avp{name = AvpName, + data = trim(Avp#diameter_avp.data), + type = Type}, + Acc) + end. + +%% avp_decode/5 + +avp_decode(Data, AvpName, Opts, Mod, Mod) -> + Mod:avp(decode, Data, AvpName, Opts); + +avp_decode(Data, AvpName, Opts, Mod, _) -> + Mod:avp(decode, Data, AvpName, Opts, Mod). + +%% trim/1 +%% +%% Remove any extra bit that was added in diameter_codec to induce a +%% 5014 error. + +trim(#diameter_avp{data = Data} = Avp) -> + Avp#diameter_avp{data = trim(Data)}; + +trim({5014, Bin}) -> + Bin; + +trim(Avps) + when is_list(Avps) -> + lists:map(fun trim/1, Avps); + +trim(Avp) -> + Avp. + +%% decode_error/7 + +decode_error(Name, [_ | Rec], _, #{failed_avp := true} = Opts, Mod, Avp, Acc) -> + decode_AVP(Name, Avp#diameter_avp{value = Rec}, Opts, Mod, Acc); + +decode_error(Name, _, _, #{failed_avp := true} = Opts, Mod, Avp, Acc) -> + decode_AVP(Name, Avp, Opts, Mod, Acc); + +decode_error(_, [Error | _], ComponentAvps, _, _, Avp, Acc) -> + decode_error(Error, Avp, Acc, ComponentAvps); + +decode_error(_, Error, ComponentAvps, _, _, Avp, Acc) -> + decode_error(Error, Avp, Acc, ComponentAvps). + +%% decode_error/5 + +decode_error(Name, _Reason, #{failed_avp := true} = Opts, Mod, Avp, Acc) -> + decode_AVP(Name, Avp, Opts, Mod, Acc); + +decode_error(Name, Reason, Opts, Mod, Avp, {Rec, Failed}) -> + Stack = diameter_lib:get_stacktrace(), + diameter_lib:log(decode_error, + ?MODULE, + ?LINE, + {Reason, Name, Avp#diameter_avp.name, Mod, Stack}), + {Avp, {Rec, [rc(Reason, Avp, Opts, Mod) | Failed]}}. + +%% decode_error/4 + +decode_error({RC, ErrorData}, Avp, {Rec, Failed}, ComponentAvps) -> + E = Avp#diameter_avp{data = [ErrorData]}, + {[Avp | trim(ComponentAvps)], {Rec, [{RC, E} | Failed]}}. + +%% set_strict/3 + +%% Set false as soon as we see a Grouped AVP that doesn't set the +%% M-bit, to ignore the M-bit on an encapsulated AVP. +set_strict('Grouped', false = M, #{strict_mbit := true} = Opts) -> + Opts#{strict_mbit := M}; +set_strict(_, _, Opts) -> + Opts. + +%% set_failed/2 +%% +%% Set true as soon as we see Failed-AVP. Matching on 'Failed-AVP' +%% assumes that this is the RFC AVP. Strictly, this doesn't need to be +%% the case. + +set_failed('Failed-AVP', #{failed_avp := false} = Opts) -> + Opts#{failed_avp := true}; +set_failed(_, Opts) -> + Opts. + +%% decode_AVP/5 +%% +%% Don't know this AVP: see if it can be packed in an 'AVP' field +%% undecoded. Note that the type field is 'undefined' in this case. + +decode_AVP(Name, Avp, Opts, Mod, Acc) -> + {trim(Avp), pack_AVP(Name, Avp, Opts, Mod, Acc)}. + +%% rc/2 + +%% diameter_types will raise an error of this form to communicate +%% DIAMETER_INVALID_AVP_LENGTH (5014). A module specified to a +%% @custom_types tag in a dictionary file can also raise an error of +%% this form. +rc({'DIAMETER', 5014 = RC, _}, #diameter_avp{name = AvpName} = Avp, Opts, Mod) -> + {RC, Avp#diameter_avp{data = Mod:empty_value(AvpName, Opts)}}; + +%% 3588: +%% +%% DIAMETER_INVALID_AVP_VALUE 5004 +%% The request contained an AVP with an invalid value in its data +%% portion. A Diameter message indicating this error MUST include +%% the offending AVPs within a Failed-AVP AVP. +rc(_, Avp, _, _) -> + {5004, Avp}. + +%% pack_avp/5 + +pack_avp(Name, #diameter_avp{name = AvpName} = Avp, Opts, Mod, Acc) -> + pack_avp(Name, Mod:avp_arity(Name, AvpName), Avp, Opts, Mod, Acc). + +%% pack_avp/6 + +pack_avp(Name, 0, Avp, Opts, Mod, Acc) -> + pack_AVP(Name, Avp, Opts, Mod, Acc); + +pack_avp(_, Arity, #diameter_avp{name = AvpName} = Avp, _Opts, Mod, Acc) -> + pack(Arity, AvpName, Avp, Mod, Acc). + +%% pack_AVP/5 + +%% Length failure was induced because of a header/payload length +%% mismatch. The AVP Length is reset to match the received data if +%% this AVP is encoded in an answer message, since the length is +%% computed. +%% +%% Data is a truncated header if command_code = undefined, otherwise +%% payload bytes. The former is padded to the length of a header if +%% the AVP reaches an outgoing encode in diameter_codec. +%% +%% RFC 6733 says that an AVP returned with 5014 can contain a minimal +%% payload for the AVP's type, but in this case we don't know the +%% type. + +pack_AVP(_, #diameter_avp{data = {5014 = RC, Data}} = Avp, _, _, Acc) -> + {Rec, Failed} = Acc, + {Rec, [{RC, Avp#diameter_avp{data = Data}} | Failed]}; + +pack_AVP(Name, Avp, Opts, Mod, Acc) -> + pack_arity(Name, pack_arity(Name, Opts, Mod, Avp), Avp, Mod, Acc). + +%% pack_arity/5 + +pack_arity(_, 0, #diameter_avp{is_mandatory = M} = Avp, _, Acc) -> + {Rec, Failed} = Acc, + {Rec, [{if M -> 5001; true -> 5008 end, Avp} | Failed]}; + +pack_arity(_, Arity, Avp, Mod, Acc) -> + pack(Arity, 'AVP', Avp, Mod, Acc). + +%% Give Failed-AVP special treatment since (1) it'll contain any +%% unrecognized mandatory AVP's and (2) the RFC 3588 grammar failed to +%% allow for Failed-AVP in an answer-message. + +pack_arity(Name, + #{strict_mbit := Strict, + failed_avp := Failed}, + Mod, + #diameter_avp{is_mandatory = M, + name = AvpName}) -> + + %% Not testing just Name /= 'Failed-AVP' means we're changing the + %% packing of AVPs nested within Failed-AVP, but the point of + %% ignoring errors within Failed-AVP is to decode as much as + %% possible, and failing because a mandatory AVP couldn't be + %% packed into a dedicated field defeats that point. + + if Failed == true; + Name == 'Failed-AVP'; + Name == 'answer-message', AvpName == 'Failed-AVP'; + not M; + not Strict -> + Mod:avp_arity(Name, 'AVP'); + true -> + 0 + end. + +%% 3588: +%% +%% DIAMETER_AVP_UNSUPPORTED 5001 +%% The peer received a message that contained an AVP that is not +%% recognized or supported and was marked with the Mandatory bit. A +%% Diameter message with this error MUST contain one or more Failed- +%% AVP AVP containing the AVPs that caused the failure. +%% +%% DIAMETER_AVP_NOT_ALLOWED 5008 +%% A message was received with an AVP that MUST NOT be present. The +%% Failed-AVP AVP MUST be included and contain a copy of the +%% offending AVP. + +%% pack/5 + +pack(Arity, FieldName, Avp, Mod, {Rec, _} = Acc) -> + pack(Mod:'#get-'(FieldName, Rec), Arity, FieldName, Avp, Mod, Acc). + +%% pack/6 + +pack(undefined, 1, 'AVP' = F, Avp, Mod, {Rec, Failed}) -> %% unlikely + {Mod:'#set-'({F, Avp}, Rec), Failed}; + +pack(undefined, 1, F, #diameter_avp{value = V}, Mod, {Rec, Failed}) -> + {Mod:'#set-'({F, V}, Rec), Failed}; + +%% 3588: +%% +%% DIAMETER_AVP_OCCURS_TOO_MANY_TIMES 5009 +%% A message was received that included an AVP that appeared more +%% often than permitted in the message definition. The Failed-AVP +%% AVP MUST be included and contain a copy of the first instance of +%% the offending AVP that exceeded the maximum number of occurrences +%% + +pack(_, 1, _, Avp, _, {Rec, Failed}) -> + {Rec, [{5009, Avp} | Failed]}; + +pack(L, {_, Max}, F, Avp, Mod, {Rec, Failed}) -> + case '*' /= Max andalso has_prefix(Max+1, L) of + true -> + {Rec, [{5009, Avp} | Failed]}; + false when F == 'AVP' -> + {Mod:'#set-'({F, [Avp | L]}, Rec), Failed}; + false -> + {Mod:'#set-'({F, [Avp#diameter_avp.value | L]}, Rec), Failed} + end. + +%% --------------------------------------------------------------------------- +%% # grouped_avp/3 +%% --------------------------------------------------------------------------- + +-spec grouped_avp(decode, avp_name(), binary() | {5014, binary()}, term()) + -> {avp_record(), [avp()]}; + (encode, avp_name(), avp_record() | avp_values(), term()) + -> iolist() + | no_return(). + +%% Length error induced by diameter_codec:collect_avps/1: the AVP +%% length in the header was too short (insufficient for the extracted +%% header) or too long (past the end of the message). An empty payload +%% is sufficient according to the RFC text for 5014. +grouped_avp(decode, _Name, {5014 = RC, _Bin}, _) -> + ?THROW({grouped, {RC, []}, []}); + +grouped_avp(decode, Name, Data, Opts) -> + grouped_decode(Name, diameter_codec:collect_avps(Data), Opts); + +grouped_avp(encode, Name, Data, Opts) -> + encode_avps(Name, Data, Opts). + +%% grouped_decode/2 +%% +%% Note that Grouped is the only AVP type that doesn't just return a +%% decoded value, also returning the list of component diameter_avp +%% records. + +%% Length error in trailing component AVP. +grouped_decode(_Name, {Error, Acc}, _) -> + {5014, Avp} = Error, + ?THROW({grouped, Error, [Avp | Acc]}); + +%% 7.5. Failed-AVP AVP + +%% In the case where the offending AVP is embedded within a Grouped AVP, +%% the Failed-AVP MAY contain the grouped AVP, which in turn contains +%% the single offending AVP. The same method MAY be employed if the +%% grouped AVP itself is embedded in yet another grouped AVP and so on. +%% In this case, the Failed-AVP MAY contain the grouped AVP hierarchy up +%% to the single offending AVP. This enables the recipient to detect +%% the location of the offending AVP when embedded in a group. + +%% An error in decoding a component AVP throws the first faulty +%% component, which the catch in d/3 wraps in the Grouped AVP in +%% question. A partially decoded record is only used when ignoring +%% errors in Failed-AVP. +grouped_decode(Name, ComponentAvps, Opts) -> + {Rec, Avps, Es} = decode_avps(Name, ComponentAvps, Opts), + [] == Es orelse ?THROW({grouped, [{_,_} = hd(Es) | Rec], Avps}), + {Rec, Avps}. + +%% --------------------------------------------------------------------------- +%% # empty_group/2 +%% --------------------------------------------------------------------------- + +empty_group(Name, #{module := Mod} = Opts) -> + list_to_binary([z(F, A, Opts, Mod) || {F,A} <- Mod:avp_arity(Name)]). + +z(Name, 1, Opts, Mod) -> + z(Name, Opts, Mod); +z(_, {0,_}, _, _) -> + []; +z(Name, {Min, _}, Opts, Mod) -> + binary:copy(z(Name, Opts, Mod), Min). + +z('AVP', _, _) -> + <<0:64>>; %% minimal header +z(Name, Opts, Mod) -> + Bin = diameter_codec:pack_data(Mod:avp_header(Name), + Mod:empty_value(Name, Opts)), + Sz = iolist_size(Bin), + <<0:Sz/unit:8>>. + +%% --------------------------------------------------------------------------- +%% # empty/2 +%% --------------------------------------------------------------------------- + +empty(Name, #{module := Mod} = Opts) -> + Mod:avp(encode, zero, Name, Opts). + +%% ------------------------------------------------------------------------------ + +newrec(Mod, Name) -> + Mod:'#new-'(Mod:name2rec(Name)). |