aboutsummaryrefslogblamecommitdiffstats
path: root/lib/diameter/src/base/diameter_gen.erl
blob: 6f11583868b6ff0564d626fe8ae444939c1dc7a1 (plain) (tree)













































































































































































                                                                              
                             
                                                                 
                                                
                         



                                                                   

                                                              
                     













                                                     

            









                                                                          






















                                                               
          
 




                                   
 



                                              
























                                                                       




















                                                                       





































































                                                                             

                                 

                 


                                                                      































































                                                                             
                                                                              










                                                                        
                 



                                                                           
                                                                
                                          
                                    


                                  

                                                            


                 
                                                                       
                                             
                                                                 


































                                                                      

                                                                               






































                                                                           

                                                              

                                      







                                                                        
 











                                                                         
 
               



























                                                                      

         




                                                                     









                                                                         



























                                     






















































































                                                                                 
%%
%% %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, AM, Failed}}
        = mapfoldl(fun(T,A) -> decode(Name, Opts, Mod, T, A) end,
                   {newrec(Mod, Name), #{}, []},
                   Recs),
    %% AM counts the number of top-level AVPs, which missing/4 then
    %% uses when adding 5005 errors.
    {Rec, Avps, Failed ++ missing(Name, Opts, Mod, AM)}.

%% Append 5005 errors so that errors are reported in the order
%% encountered. Failed-AVP should typically contain the first
%% error encountered.

%% 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}.

%% missing/4

%% 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(Name, Opts, Mod, AM) ->
    lists:foldl(fun(T,A) -> missing(T, AM, Opts, Mod, A) end,
                [],
                Mod:avp_arity(Name)).

%% missing/5

missing({Name, Arity}, AM, Opts, Mod, Acc) ->
    case missing(Name, Arity, AM) of
        true  -> [{5005, empty_avp(Name, Opts, Mod)} | Acc];
        false -> Acc
    end.

%% missing/3

missing(Name, Arity, AM) ->
    'AVP' /= Name andalso too_few(Name, Arity, AM).

%% too_few/3
%%
%% Maximum arities have already been checked during the decode.

too_few(_, {0, _}, _) ->
    false;

too_few(FieldName, 1, AM) ->
    not maps:is_key(FieldName, AM);

too_few(FieldName, {M, _}, AM) ->
    maps:get(FieldName, AM, 0) < M.

%% empty_avp/3

empty_avp(Name, Opts, Mod) ->
    {Code, Flags, VId} = Mod:avp_header(Name),
    {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,
       {Rec, AM, Failed}) ->
    T = Mod:avp_name(Code, Vid),
    decode(Name, Opts, Mod, T, Avp, {Rec, incr(field(T), AM), Failed}).

%% field/1

field({AvpName, _Type}) ->
    AvpName;

field('AVP' = A) ->
    A.

%% incr/2

incr(Key, Map) ->
    maps:update_with(Key, fun incr/1, 1, Map).

%% incr/1

incr(N) ->
    N + 1.

%% 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,

    %% 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/6

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, AM, Failed}) ->
    Stack = diameter_lib:get_stacktrace(),
    AvpName = Avp#diameter_avp.name,
    diameter_lib:log(decode_error,
                     ?MODULE,
                     ?LINE,
                     {Reason, Name, AvpName, Mod, Stack}),
    {Avp, {Rec, AM, [rc(Reason, Avp, Opts, Mod) | Failed]}}.

%% decode_error/4

decode_error({RC, ErrorData}, Avp, {Rec, AM, Failed}, ComponentAvps) ->
    E = Avp#diameter_avp{data = [ErrorData]},
    {[Avp | trim(ComponentAvps)], {Rec, AM, [{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} = A, Opts, Mod) ->
    {RC, A#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, AM, Failed} = Acc,
    {Rec, AM, [{RC, Avp#diameter_avp{data = Data}} | Failed]};

pack_AVP(Name, Avp, Opts, Mod, Acc) ->
    Arity = pack_arity(Name, Opts, Mod, Avp),
    if 0 == Arity ->
            M = Avp#diameter_avp.is_mandatory,
            {Rec, AM, Failed} = Acc,
            {Rec, AM, [{if M -> 5001; true -> 5008 end, Avp} | Failed]};
       true ->
            pack(Arity, 'AVP', Avp, Mod, Acc)
    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_arity/4

%% 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.

%% pack/5

pack(Arity, F, Avp, Mod, {Rec, AM, Failed}) ->
    case too_many(F, Arity, AM) of
        true  -> {Rec, AM, [{5009, Avp} | Failed]};
        false -> {set(Arity, F, value(F, Avp), Mod, Rec), AM, Failed}
    end.

%% 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
%%

%% too_many/3

too_many(_, {_, '*'}, _) ->
    false;

too_many(FieldName, {_, M}, Map) ->
    too_many(FieldName, M, Map);

too_many(FieldName, M, Map) ->
    #{FieldName := N} = Map,
    M < N.

%% set/5

set(1, F, Value, Mod, Rec) ->
    Mod:'#set-'({F, Value}, Rec);

set(_, F, V, Mod, Rec) ->
    Vs = Mod:'#get-'(F, Rec),
    Mod:'#set-'({F, [V|Vs]}, Rec).

%% value/2

value('AVP', Avp) ->
    Avp;

value(_, #diameter_avp{value = V}) ->
    V.

%% ---------------------------------------------------------------------------
%% # 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)).