diff options
author | Anders Svensson <[email protected]> | 2011-05-18 18:29:12 +0200 |
---|---|---|
committer | Anders Svensson <[email protected]> | 2011-05-18 18:29:12 +0200 |
commit | 3c15ff32e89e401b4dde2b8acc9699be2614b996 (patch) | |
tree | 184dc988fb2ab3af04a532bc59cc794a8d74fbd3 /lib/diameter/src/compiler | |
parent | b1e768e86593178810c8a0b3c38443dcf6be5181 (diff) | |
download | otp-3c15ff32e89e401b4dde2b8acc9699be2614b996.tar.gz otp-3c15ff32e89e401b4dde2b8acc9699be2614b996.tar.bz2 otp-3c15ff32e89e401b4dde2b8acc9699be2614b996.zip |
Initial commit of the diameter application.
The application provides an implementation of the Diameter protocol
as defined in RFC 3588.
Diffstat (limited to 'lib/diameter/src/compiler')
-rw-r--r-- | lib/diameter/src/compiler/.gitignore | 3 | ||||
-rw-r--r-- | lib/diameter/src/compiler/Makefile | 141 | ||||
-rw-r--r-- | lib/diameter/src/compiler/diameter_codegen.erl | 788 | ||||
-rw-r--r-- | lib/diameter/src/compiler/diameter_forms.hrl | 52 | ||||
-rw-r--r-- | lib/diameter/src/compiler/diameter_make.erl | 120 | ||||
-rw-r--r-- | lib/diameter/src/compiler/diameter_spec_scan.erl | 157 | ||||
-rw-r--r-- | lib/diameter/src/compiler/diameter_spec_util.erl | 1052 | ||||
-rw-r--r-- | lib/diameter/src/compiler/modules.mk | 27 |
8 files changed, 2340 insertions, 0 deletions
diff --git a/lib/diameter/src/compiler/.gitignore b/lib/diameter/src/compiler/.gitignore new file mode 100644 index 0000000000..d9f072e262 --- /dev/null +++ b/lib/diameter/src/compiler/.gitignore @@ -0,0 +1,3 @@ + +/depend.mk + diff --git a/lib/diameter/src/compiler/Makefile b/lib/diameter/src/compiler/Makefile new file mode 100644 index 0000000000..8512eb515a --- /dev/null +++ b/lib/diameter/src/compiler/Makefile @@ -0,0 +1,141 @@ +# +# %CopyrightBegin% +# +# Copyright Ericsson AB 2010-2011. All Rights Reserved. +# +# The contents of this file are subject to the Erlang Public License, +# Version 1.1, (the "License"); you may not use this file except in +# compliance with the License. You should have received a copy of the +# Erlang Public License along with this software. If not, it can be +# retrieved online at http://www.erlang.org/. +# +# Software distributed under the License is distributed on an "AS IS" +# basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See +# the License for the specific language governing rights and limitations +# under the License. +# +# %CopyrightEnd% +# +# + +ifneq ($(ERL_TOP),) +include $(ERL_TOP)/make/target.mk +EBIN = ../../ebin +include $(ERL_TOP)/make/$(TARGET)/otp.mk +else +include $(DIAMETER_TOP)/make/target.mk +EBIN = ../../ebin +include $(DIAMETER_TOP)/make/$(TARGET)/rules.mk +endif + + +# ---------------------------------------------------- +# Application version +# ---------------------------------------------------- +include ../../vsn.mk +VSN=$(DIAMETER_VSN) + +# ---------------------------------------------------- +# Release directory specification +# ---------------------------------------------------- + +RELSYSDIR = $(RELEASE_PATH)/lib/diameter-$(VSN) + +INCDIR = ../../include + +# ---------------------------------------------------- +# Target Specs +# ---------------------------------------------------- + +include modules.mk + +ERL_FILES = \ + $(MODULES:%=%.erl) + +TARGET_FILES = \ + $(MODULES:%=$(EBIN)/%.$(EMULATOR)) + +# ---------------------------------------------------- +# FLAGS +# ---------------------------------------------------- + +ifeq ($(TYPE),debug) +ERL_COMPILE_FLAGS += -Ddebug +endif + +include ../app/diameter.mk + +ERL_COMPILE_FLAGS += \ + $(DIAMETER_ERL_COMPILE_FLAGS) \ + -I$(INCDIR) + +# ---------------------------------------------------- +# Targets +# ---------------------------------------------------- + +debug: + @${MAKE} TYPE=debug opt + +opt: $(TARGET_FILES) + +clean: + rm -f $(TARGET_FILES) + rm -f errs core *~ + rm -f depend.mk + +docs: + +info: + @echo "" + @echo "ERL_FILES = $(ERL_FILES)" + @echo "HRL_FILES = $(HRL_FILES)" + @echo "" + @echo "TARGET_FILES = $(TARGET_FILES)" + @echo "" + +# ---------------------------------------------------- +# Special Build Targets +# ---------------------------------------------------- + +# Invoked from ../app to add modules to the app file. +$(APP_TARGET): force + M=`echo $(MODULES) | sed -e 's/^ *//' -e 's/ *$$//' -e 'y/ /,/'`; \ + echo "/%COMPILER_MODULES%/s//$$M/;w;q" | tr ';' '\n' \ + | ed -s $@ + +# ---------------------------------------------------- +# Release Target +# ---------------------------------------------------- +ifneq ($(ERL_TOP),) +include $(ERL_TOP)/make/otp_release_targets.mk +else +include $(DIAMETER_TOP)/make/release_targets.mk +endif + +release_spec: opt + $(INSTALL_DIR) $(RELSYSDIR)/ebin + $(INSTALL_DATA) $(TARGET_FILES) $(RELSYSDIR)/ebin + $(INSTALL_DIR) $(RELSYSDIR)/src + $(INSTALL_DIR) $(RELSYSDIR)/src/compiler + $(INSTALL_DATA) $(ERL_FILES) $(HRL_FILES) $(RELSYSDIR)/src/compiler + +release_docs_spec: + +force: + +# ---------------------------------------------------- +# Dependencies +# ---------------------------------------------------- + +depend: depend.mk + +# Generate dependencies makefile. +depend.mk: ../app/depend.sed $(ERL_FILES) modules.mk Makefile + for f in $(MODULES); do \ + sed -f $< $$f.erl | sed "s@/@/$$f@"; \ + done \ + > $@ + +-include depend.mk + +.PHONY: clean debug depend docs force info opt release_docs_spec release_spec diff --git a/lib/diameter/src/compiler/diameter_codegen.erl b/lib/diameter/src/compiler/diameter_codegen.erl new file mode 100644 index 0000000000..213ba0d22c --- /dev/null +++ b/lib/diameter/src/compiler/diameter_codegen.erl @@ -0,0 +1,788 @@ +%% +%% %CopyrightBegin% +%% +%% Copyright Ericsson AB 2010-2011. All Rights Reserved. +%% +%% The contents of this file are subject to the Erlang Public License, +%% Version 1.1, (the "License"); you may not use this file except in +%% compliance with the License. You should have received a copy of the +%% Erlang Public License along with this software. If not, it can be +%% retrieved online at http://www.erlang.org/. +%% +%% Software distributed under the License is distributed on an "AS IS" +%% basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See +%% the License for the specific language governing rights and limitations +%% under the License. +%% +%% %CopyrightEnd% +%% + +-module(diameter_codegen). + +%% +%% This module generates .erl and .hrl files for encode/decode +%% modules from the orddict parsed from a .dia (aka spec) file by +%% dis_spec_util. The generated code is very simple (one-liners), the +%% generated functions being called by code included from dis_gen.hrl +%% in order to encode/decode messages and AVPs. The orddict itself is +%% returned by dict/0 in the generated module and dis_spec_util calls +%% this function when importing spec files. (That is, beam has to be +%% compiled from an imported spec file before it can be imported.) +%% + +-export([from_spec/4]). + +%% Internal exports (for test). +-export([file/1, + file/2, + file/3]). + +-include_lib("diameter/src/app/diameter_internal.hrl"). +-include("diameter_forms.hrl"). + +%% Generated functions that could have no generated clauses will have +%% a trailing ?UNEXPECTED clause that should never execute. +-define(UNEXPECTED(N), {?clause, [?VAR('_') || _ <- lists:seq(1,N)], + [], + [?APPLY(erlang, + error, + [?TERM({unexpected, getr(module)})])]}). + + +from_spec(File, Spec, Opts, Mode) -> + Outdir = proplists:get_value(outdir, Opts, "."), + putr(verbose, lists:member(verbose, Opts)), + putr(debug, lists:member(debug, Opts)), + codegen(File, Spec, Outdir, Mode). + +%% Optional reports when running verbosely. +report(What, Data) -> + report(getr(verbose), What, Data), + Data. + +report(true, Tag, Data) -> + io:format(">>~n>> ~p ~p~n", [Tag, Data]); +report(false, _, _) -> + ok. + +putr(Key, Value) -> + put({?MODULE, Key}, Value). + +getr(Key) -> + get({?MODULE, Key}). + +%% =========================================================================== +%% =========================================================================== + +%% Generate from parsed spec in a file. + +file(F) -> + file(F, spec). + +file(F, Mode) -> + file(F, ".", Mode). + +file(F, Outdir, Mode) -> + {ok, [Spec]} = file:consult(F), + from_spec(F, Spec, Outdir, Mode). + +%% =========================================================================== +%% =========================================================================== + +choose(true, X, _) -> X; +choose(false, _, X) -> X. + +get_value(Key, Plist) -> + proplists:get_value(Key, Plist, []). + +write(Path, [C|_] = Spec) + when is_integer(C) -> + w(Path, Spec, "~s"); +write(Path, Spec) -> + w(Path, Spec, "~p."). + +w(Path, Spec, Fmt) -> + {ok, Fd} = file:open(Path, [write]), + io:fwrite(Fd, Fmt ++ "~n", [Spec]), + file:close(Fd). + +codegen(File, Spec, Outdir, Mode) -> + Mod = mod(File, orddict:find(name, Spec)), + Path = filename:join(Outdir, Mod), %% minus extension + gen(Mode, Spec, Mod, Path), + ok. + +mod(File, error) -> + filename:rootname(filename:basename(File)); +mod(_, {ok, Mod}) -> + atom_to_list(Mod). + +gen(spec, Spec, _Mod, Path) -> + write(Path ++ ".spec", Spec); + +gen(hrl, Spec, Mod, Path) -> + gen_hrl(Path ++ ".hrl", Mod, Spec); + +gen(erl = Mode, Spec, Mod, Path) + when is_list(Mod) -> + gen(Mode, Spec, list_to_atom(Mod), Path); + +gen(erl, Spec, Mod, Path) -> + putr(module, Mod), %% used by ?UNEXPECTED. + + Forms = [{?attribute, module, Mod}, + {?attribute, compile, [{parse_transform, diameter_exprecs}]}, + {?attribute, compile, [nowarn_unused_function]}, + {?attribute, export, [{name, 0}, + {id, 0}, + {vendor_id, 0}, + {vendor_name, 0}, + {decode_avps, 2}, %% in diameter_gen.hrl + {encode_avps, 2}, %% + {msg_name, 2}, + {msg_header, 1}, + {rec2msg, 1}, + {msg2rec, 1}, + {name2rec, 1}, + {avp_name, 2}, + {avp_arity, 2}, + {avp_header, 1}, + {avp, 3}, + {grouped_avp, 3}, + {enumerated_avp, 3}, + {empty_value, 1}, + {dict, 0}]}, + %% diameter.hrl is included for #diameter_avp + {?attribute, include_lib, "diameter/include/diameter.hrl"}, + {?attribute, include_lib, "diameter/include/diameter_gen.hrl"}, + f_name(Mod), + f_id(Spec), + f_vendor_id(Spec), + f_vendor_name(Spec), + f_msg_name(Spec), + f_msg_header(Spec), + f_rec2msg(Spec), + f_msg2rec(Spec), + f_name2rec(Spec), + f_avp_name(Spec), + f_avp_arity(Spec), + f_avp_header(Spec), + f_avp(Spec), + f_enumerated_avp(Spec), + f_empty_value(Spec), + f_dict(Spec), + {eof, ?LINE}], + + gen_erl(Path, insert_hrl_forms(Spec, Forms)). + +gen_erl(Path, Forms) -> + getr(debug) andalso write(Path ++ ".forms", Forms), + write(Path ++ ".erl", + header() ++ erl_prettypr:format(erl_syntax:form_list(Forms))). + +insert_hrl_forms(Spec, Forms) -> + {H,T} = lists:splitwith(fun is_header/1, Forms), + H ++ make_hrl_forms(Spec) ++ T. + +is_header({attribute, _, export, _}) -> + false; +is_header(_) -> + true. + +make_hrl_forms(Spec) -> + {_Prefix, MsgRecs, GrpRecs, ImportedGrpRecs} + = make_record_forms(Spec), + + RecordForms = MsgRecs ++ GrpRecs ++ lists:flatmap(fun({_,Fs}) -> Fs end, + ImportedGrpRecs), + + RecNames = lists:map(fun({attribute,_,record,{N,_}}) -> N end, + RecordForms), + + %% export_records is used by the diameter_exprecs parse transform. + [{?attribute, export_records, RecNames} | RecordForms]. + +make_record_forms(Spec) -> + Prefix = prefix(Spec), + + MsgRecs = a_record(Prefix, fun msg_proj/1, get_value(messages, Spec)), + GrpRecs = a_record(Prefix, fun grp_proj/1, get_value(grouped, Spec)), + + ImportedGrpRecs = [{M, a_record(Prefix, fun grp_proj/1, Gs)} + || {M,Gs} <- get_value(import_groups, Spec)], + + {Prefix, MsgRecs, GrpRecs, ImportedGrpRecs}. + +msg_proj({Name, _, _, _, Avps}) -> + {Name, Avps}. + +grp_proj({Name, _, _, Avps}) -> + {Name, Avps}. + +%% a_record/3 + +a_record(Prefix, ProjF, L) -> + lists:map(fun(T) -> a_record(ProjF(T), Prefix) end, L). + +a_record({Nm, Avps}, Prefix) -> + Name = list_to_atom(Prefix ++ atom_to_list(Nm)), + Fields = lists:map(fun field/1, Avps), + {?attribute, record, {Name, Fields}}. + +field(Avp) -> + {Name, Arity} = avp_info(Avp), + if 1 == Arity -> + {?record_field, ?ATOM(Name)}; + true -> + {?record_field, ?ATOM(Name), ?NIL} + end. + +%%% ------------------------------------------------------------------------ +%%% # name/0 +%%% ------------------------------------------------------------------------ + +f_name(Name) -> + {?function, name, 0, + [{?clause, [], [], [?ATOM(Name)]}]}. + +%%% ------------------------------------------------------------------------ +%%% # id/0 +%%% ------------------------------------------------------------------------ + +f_id(Spec) -> + Id = orddict:fetch(id, Spec), + {?function, id, 0, + [{?clause, [], [], [?INTEGER(Id)]}]}. + +%%% ------------------------------------------------------------------------ +%%% # vendor_id/0 +%%% ------------------------------------------------------------------------ + +f_vendor_id(Spec) -> + {Id, _} = orddict:fetch(vendor, Spec), + {?function, vendor_id, 0, + [{?clause, [], [], [?INTEGER(Id)]}]}. + +%%% ------------------------------------------------------------------------ +%%% # vendor_name/0 +%%% ------------------------------------------------------------------------ + +f_vendor_name(Spec) -> + {_, Name} = orddict:fetch(vendor, Spec), + {?function, vendor_name, 0, + [{?clause, [], [], [?ATOM(Name)]}]}. + +%%% ------------------------------------------------------------------------ +%%% # msg_name/1 +%%% ------------------------------------------------------------------------ + +f_msg_name(Spec) -> + {?function, msg_name, 2, msg_name(Spec)}. + +%% Return the empty name for any unknown command to which +%% DIAMETER_COMMAND_UNSUPPORTED should be replied. + +msg_name(Spec) -> + lists:flatmap(fun c_msg_name/1, + proplists:get_value(command_codes, Spec, [])) + ++ [{?clause, [?VAR('_'), ?VAR('_')], [], [?ATOM('')]}]. + +c_msg_name({Code, Req, Ans}) -> + [{?clause, [?INTEGER(Code), ?ATOM(true)], + [], + [?ATOM(mname(Req))]}, + {?clause, [?INTEGER(Code), ?ATOM(false)], + [], + [?ATOM(mname(Ans))]}]. + +mname({N, _Abbr}) -> + N; +mname(N) -> + N. + +%%% ------------------------------------------------------------------------ +%%% # msg2rec/1 +%%% ------------------------------------------------------------------------ + +f_msg2rec(Spec) -> + {?function, msg2rec, 1, msg2rec(Spec)}. + +msg2rec(Spec) -> + Pre = prefix(Spec), + Dict = dict:from_list(lists:flatmap(fun msgs/1, + get_value(command_codes, Spec))), + lists:flatmap(fun(T) -> msg2rec(T, Dict, Pre) end, + get_value(messages, Spec)) + ++ [?UNEXPECTED(1)]. + +msgs({_Code, Req, Ans}) -> + [{mname(Req), Req}, {mname(Ans), Ans}]. + +msg2rec({N,_,_,_,_}, Dict, Pre) -> + c_msg2rec(fetch_names(N, Dict), Pre). + +fetch_names(Name, Dict) -> + case dict:find(Name, Dict) of + {ok, N} -> + N; + error -> + Name + end. + +c_msg2rec({N,A}, Pre) -> + [c_name2rec(N, N, Pre), c_name2rec(A, N, Pre)]; +c_msg2rec(N, Pre) -> + [c_name2rec(N, N, Pre)]. + +%%% ------------------------------------------------------------------------ +%%% # rec2msg/1 +%%% ------------------------------------------------------------------------ + +f_rec2msg(Spec) -> + {?function, rec2msg, 1, rec2msg(Spec)}. + +rec2msg(Spec) -> + Pre = prefix(Spec), + lists:map(fun(T) -> c_rec2msg(T, Pre) end, get_value(messages, Spec)) + ++ [?UNEXPECTED(1)]. + +c_rec2msg({N,_,_,_,_}, Pre) -> + {?clause, [?ATOM(rec_name(N, Pre))], [], [?ATOM(N)]}. + +%%% ------------------------------------------------------------------------ +%%% # name2rec/1 +%%% ------------------------------------------------------------------------ + +f_name2rec(Spec) -> + {?function, name2rec, 1, name2rec(Spec)}. + +name2rec(Spec) -> + Pre = prefix(Spec), + Groups = get_value(grouped, Spec) + ++ lists:flatmap(fun avps/1, get_value(import_groups, Spec)), + lists:map(fun({N,_,_,_}) -> c_name2rec(N, N, Pre) end, Groups) + ++ [{?clause, [?VAR('T')], [], [?CALL(msg2rec, [?VAR('T')])]}]. + +c_name2rec(Name, Rname, Pre) -> + {?clause, [?ATOM(Name)], [], [?ATOM(rec_name(Rname, Pre))]}. + +avps({_Mod, Avps}) -> + Avps. + +%%% ------------------------------------------------------------------------ +%%% # avp_name/1 +%%% ------------------------------------------------------------------------ + +f_avp_name(Spec) -> + {?function, avp_name, 2, avp_name(Spec)}. + +%% 3588, 4.1: +%% +%% AVP Code +%% The AVP Code, combined with the Vendor-Id field, identifies the +%% attribute uniquely. AVP numbers 1 through 255 are reserved for +%% backward compatibility with RADIUS, without setting the Vendor-Id +%% field. AVP numbers 256 and above are used for Diameter, which are +%% allocated by IANA (see Section 11.1). + +avp_name(Spec) -> + Avps = get_value(avp_types, Spec) + ++ lists:flatmap(fun avps/1, get_value(import_avps, Spec)), + {Vid, _} = orddict:fetch(vendor, Spec), + Vs = lists:flatmap(fun({V,Ns}) -> [{N,V} || N <- Ns] end, + get_value(avp_vendor_id, Spec)), + + lists:map(fun(T) -> c_avp_name(T, Vid, Vs) end, Avps) + ++ [{?clause, [?VAR('_'), ?VAR('_')], [], [?ATOM('AVP')]}]. + +c_avp_name({Name, Code, Type, Flags, _Encr}, Vid, Vs) -> + c_avp_name({Name, Type}, + Code, + lists:member('V', Flags), + Vid, + proplists:get_value(Name, Vs)). + +c_avp_name(T, Code, false, _, undefined = U) -> + {?clause, [?INTEGER(Code), ?ATOM(U)], + [], + [?TERM(T)]}; + +c_avp_name(T, Code, true, Vid, V) + when is_integer(Vid) -> + {?clause, [?INTEGER(Code), ?INTEGER(choose(V == undefined, Vid, V))], + [], + [?TERM(T)]}. + +%%% ------------------------------------------------------------------------ +%%% # avp_arity/2 +%%% ------------------------------------------------------------------------ + +f_avp_arity(Spec) -> + {?function, avp_arity, 2, avp_arity(Spec)}. + +avp_arity(Spec) -> + Msgs = get_value(messages, Spec), + Groups = get_value(grouped, Spec) + ++ lists:flatmap(fun avps/1, get_value(import_groups, Spec)), + c_avp_arity(Msgs ++ Groups) + ++ [{?clause, [?VAR('_'), ?VAR('_')], [], [?INTEGER(0)]}]. + +c_avp_arity(L) + when is_list(L) -> + lists:flatmap(fun c_avp_arity/1, L); + +c_avp_arity({N,_,_,_,As}) -> + c_avp_arity(N,As); +c_avp_arity({N,_,_,As}) -> + c_avp_arity(N,As). + +c_avp_arity(Name, Avps) -> + lists:map(fun(A) -> c_arity(Name, A) end, Avps). + +c_arity(Name, Avp) -> + {AvpName, Arity} = avp_info(Avp), + {?clause, [?ATOM(Name), ?ATOM(AvpName)], [], [?TERM(Arity)]}. + +%%% ------------------------------------------------------------------------ +%%% # avp/3 +%%% ------------------------------------------------------------------------ + +f_avp(Spec) -> + {?function, avp, 3, avp(Spec) ++ [?UNEXPECTED(3)]}. + +avp(Spec) -> + Native = get_value(avp_types, Spec), + Custom = get_value(custom_types, Spec), + Imported = get_value(import_avps, Spec), + avp([{N,T} || {N,_,T,_,_} <- Native], Imported, Custom). + +avp(Native, Imported, Custom) -> + Dict = orddict:from_list(Native), + + report(native, Dict), + report(imported, Imported), + report(custom, Custom), + + CustomNames = lists:flatmap(fun({_,Ns}) -> Ns end, Custom), + + lists:map(fun c_base_avp/1, + lists:filter(fun({N,_}) -> + false == lists:member(N, CustomNames) + end, + Native)) + ++ lists:flatmap(fun c_imported_avp/1, Imported) + ++ lists:flatmap(fun(C) -> c_custom_avp(C, Dict) end, Custom). + +c_base_avp({AvpName, T}) -> + {?clause, [?VAR('T'), ?VAR('Data'), ?ATOM(AvpName)], + [], + [base_avp(AvpName, T)]}. + +base_avp(AvpName, 'Enumerated') -> + ?CALL(enumerated_avp, [?VAR('T'), ?ATOM(AvpName), ?VAR('Data')]); + +base_avp(AvpName, 'Grouped') -> + ?CALL(grouped_avp, [?VAR('T'), ?ATOM(AvpName), ?VAR('Data')]); + +base_avp(_, Type) -> + ?APPLY(diameter_types, Type, [?VAR('T'), ?VAR('Data')]). + +c_imported_avp({Mod, Avps}) -> + lists:map(fun(A) -> imported_avp(Mod, A) end, Avps). + +imported_avp(_Mod, {AvpName, _, 'Grouped' = T, _, _}) -> + c_base_avp({AvpName, T}); + +imported_avp(Mod, {AvpName, _, _, _, _}) -> + {?clause, [?VAR('T'), ?VAR('Data'), ?ATOM(AvpName)], + [], + [?APPLY(Mod, avp, [?VAR('T'), + ?VAR('Data'), + ?ATOM(AvpName)])]}. + +c_custom_avp({Mod, Avps}, Dict) -> + lists:map(fun(N) -> custom_avp(Mod, N, orddict:fetch(N, Dict)) end, Avps). + +custom_avp(Mod, AvpName, Type) -> + {?clause, [?VAR('T'), ?VAR('Data'), ?ATOM(AvpName)], + [], + [?APPLY(Mod, AvpName, [?VAR('T'), ?ATOM(Type), ?VAR('Data')])]}. + +%%% ------------------------------------------------------------------------ +%%% # enumerated_avp/3 +%%% ------------------------------------------------------------------------ + +f_enumerated_avp(Spec) -> + {?function, enumerated_avp, 3, enumerated_avp(Spec) ++ [?UNEXPECTED(3)]}. + +enumerated_avp(Spec) -> + lists:flatmap(fun c_enumerated_avp/1, get_value(enums, Spec)). + +c_enumerated_avp({AvpName, Values}) -> + lists:flatmap(fun(V) -> c_enumerated_avp(AvpName, V) end, Values). + +c_enumerated_avp(AvpName, {I,_}) -> + [{?clause, [?ATOM(decode), ?ATOM(AvpName), ?TERM(<<I:32/integer>>)], + [], + [?TERM(I)]}, + {?clause, [?ATOM(encode), ?ATOM(AvpName), ?INTEGER(I)], + [], + [?TERM(<<I:32/integer>>)]}]. + +%%% ------------------------------------------------------------------------ +%%% msg_header/1 +%%% ------------------------------------------------------------------------ + +f_msg_header(Spec) -> + {?function, msg_header, 1, msg_header(Spec) ++ [?UNEXPECTED(1)]}. + +msg_header(Spec) -> + ApplId = orddict:fetch(id, Spec), + + lists:map(fun({M,C,F,_,_}) -> c_msg_header(M, C, F, ApplId) end, + get_value(messages, Spec)). + +%% Note that any application id in the message header spec is ignored. + +c_msg_header(Name, Code, Flags, ApplId) -> + {?clause, [?ATOM(Name)], + [], + [?TERM({Code, encode_msg_flags(Flags), ApplId})]}. + +encode_msg_flags(Flags) -> + lists:foldl(fun emf/2, 0, Flags). + +emf('REQ', N) -> N bor 2#10000000; +emf('PXY', N) -> N bor 2#01000000; +emf('ERR', N) -> N bor 2#00100000. + +%%% ------------------------------------------------------------------------ +%%% # avp_header/1 +%%% ------------------------------------------------------------------------ + +f_avp_header(Spec) -> + {?function, avp_header, 1, avp_header(Spec) ++ [?UNEXPECTED(1)]}. + +avp_header(Spec) -> + Native = get_value(avp_types, Spec), + Imported = get_value(import_avps, Spec), + {Vid, _} = orddict:fetch(vendor, Spec), + Vs = lists:flatmap(fun({V,Ns}) -> [{N,V} || N <- Ns] end, + get_value(avp_vendor_id, Spec)), + + lists:flatmap(fun(A) -> c_avp_header({Vid, Vs}, A) end, + Native ++ Imported). + +c_avp_header({Vid, Vs}, {Name, Code, _Type, Flags, _Encr}) -> + [{?clause, [?ATOM(Name)], + [], + [?TERM({Code, encode_avp_flags(Flags), vid(Name, Flags, Vs, Vid)})]}]; + +c_avp_header({_, Vs}, {Mod, Avps}) -> + lists:map(fun(A) -> c_avp_header(Vs, Mod, A) end, Avps). + +c_avp_header(Vs, Mod, {Name, _, _, Flags, _}) -> + Apply = ?APPLY(Mod, avp_header, [?ATOM(Name)]), + {?clause, [?ATOM(Name)], + [], + [case proplists:get_value(Name, Vs) of + undefined -> + Apply; + Vid -> + true = lists:member('V', Flags), %% sanity check + ?CALL(setelement, [?INTEGER(3), Apply, ?INTEGER(Vid)]) + end]}. + +encode_avp_flags(Fs) -> + lists:foldl(fun eaf/2, 0, Fs). + +eaf('V', F) -> 2#10000000 bor F; +eaf('M', F) -> 2#01000000 bor F; +eaf('P', F) -> 2#00100000 bor F. + +vid(Name, Flags, Vs, Vid) -> + v(lists:member('V', Flags), Name, Vs, Vid). + +v(true, Name, Vs, Vid) -> + proplists:get_value(Name, Vs, Vid); +v(false, _, _, _) -> + undefined. + +%%% ------------------------------------------------------------------------ +%%% # empty_value/0 +%%% ------------------------------------------------------------------------ + +f_empty_value(Spec) -> + {?function, empty_value, 1, empty_value(Spec)}. + +empty_value(Spec) -> + Groups = get_value(grouped, Spec) + ++ lists:flatmap(fun avps/1, get_value(import_groups, Spec)), + Enums = get_value(enums, Spec) + ++ lists:flatmap(fun avps/1, get_value(import_enums, Spec)), + lists:map(fun c_empty_value/1, Groups ++ Enums) + ++ [{?clause, [?VAR('Name')], [], [?CALL(empty, [?VAR('Name')])]}]. + +c_empty_value({Name, _, _, _}) -> + {?clause, [?ATOM(Name)], + [], + [?CALL(empty_group, [?ATOM(Name)])]}; + +c_empty_value({Name, _}) -> + {?clause, [?ATOM(Name)], + [], + [?TERM(<<0:32/integer>>)]}. + +%%% ------------------------------------------------------------------------ +%%% # dict/0 +%%% ------------------------------------------------------------------------ + +f_dict(Spec) -> + {?function, dict, 0, + [{?clause, [], [], [?TERM(Spec)]}]}. + +%%% ------------------------------------------------------------------------ +%%% # gen_hrl/3 +%%% ------------------------------------------------------------------------ + +gen_hrl(Path, Mod, Spec) -> + {ok, Fd} = file:open(Path, [write]), + + {Prefix, MsgRecs, GrpRecs, ImportedGrpRecs} + = make_record_forms(Spec), + + file:write(Fd, hrl_header(Mod)), + + forms("Message records", Fd, MsgRecs), + forms("Grouped AVP records", Fd, GrpRecs), + + lists:foreach(fun({M,Fs}) -> + forms("Grouped AVP records from " ++ atom_to_list(M), + Fd, + Fs) + end, + ImportedGrpRecs), + + PREFIX = to_upper(Prefix), + + write("ENUM Macros", + Fd, + m_enums(PREFIX, false, get_value(enums, Spec))), + write("RESULT CODE Macros", + Fd, + m_enums(PREFIX, false, get_value(result_codes, Spec))), + + lists:foreach(fun({M,Es}) -> + write("ENUM Macros from " ++ atom_to_list(M), + Fd, + m_enums(PREFIX, true, Es)) + end, + get_value(import_enums, Spec)), + + file:close(Fd). + +forms(_, _, []) -> + ok; +forms(Banner, Fd, Forms) -> + write(Banner, Fd, prettypr(Forms)). + +write(_, _, []) -> + ok; +write(Banner, Fd, Str) -> + banner(Fd, Banner), + io:fwrite(Fd, "~s~n", [Str]). + +prettypr(Forms) -> + erl_prettypr:format(erl_syntax:form_list(Forms)). + +banner(Fd, Heading) -> + file:write(Fd, banner(Heading)). + +banner(Heading) -> + ("\n\n" + "%%% -------------------------------------------------------\n" + "%%% " ++ Heading ++ ":\n" + "%%% -------------------------------------------------------\n\n"). + +z(S) -> + string:join(string:tokens(S, "\s\t"), "\s"). + +m_enums(Prefix, Wrap, Enums) -> + lists:map(fun(T) -> m_enum(Prefix, Wrap, T) end, Enums). + +m_enum(Prefix, B, {Name, Values}) -> + P = Prefix ++ to_upper(Name) ++ "_", + lists:map(fun({I,A}) -> + N = ["'", P, to_upper(z(atom_to_list(A))), "'"], + wrap(B, + N, + ["-define(", N, ", ", integer_to_list(I), ").\n"]) + end, + Values). + +wrap(true, Name, Def) -> + ["-ifndef(", Name, ").\n", Def, "-endif.\n"]; +wrap(false, _, Def) -> + Def. + +to_upper(A) when is_atom(A) -> + to_upper(atom_to_list(A)); +to_upper(S) -> + lists:map(fun tu/1, S). + +tu(C) when C >= $a, C =< $z -> + C + $A - $a; +tu(C) -> + C. + +header() -> + ("%% -------------------------------------------------------------------\n" + "%% This is a generated file.\n" + "%% -------------------------------------------------------------------\n" + "\n" + "%%\n" + "%% Copyright (c) Ericsson AB. All rights reserved.\n" + "%%\n" + "%% The information in this document is the property of Ericsson.\n" + "%%\n" + "%% Except as specifically authorized in writing by Ericsson, the\n" + "%% receiver of this document shall keep the information contained\n" + "%% herein confidential and shall protect the same in whole or in\n" + "%% part from disclosure and dissemination to third parties.\n" + "%%\n" + "%% Disclosure and disseminations to the receivers employees shall\n" + "%% only be made on a strict need to know basis.\n" + "%%\n\n"). + +hrl_header(Name) -> + header() ++ "-hrl_name('" ++ Name ++ ".hrl').\n". + +%% avp_info/1 + +avp_info(Entry) -> %% {Name, Arity} + case Entry of + {'<',A,'>'} -> {A, 1}; + {A} -> {A, 1}; + [A] -> {A, {0,1}}; + {Q,T} -> + {A,_} = avp_info(T), + {A, arity(Q)} + end. + +%% Normalize arity to 1 or {N,X} where N is an integer. A record field +%% for an AVP is list-valued iff the normalized arity is not 1. +arity('*' = Inf) -> {0, Inf}; +arity({'*', N}) -> {0, N}; +arity({1,1}) -> 1; +arity(T) -> T. + +prefix(Spec) -> + case orddict:find(prefix, Spec) of + {ok, P} -> + atom_to_list(P) ++ "_"; + error -> + "" + end. + +rec_name(Name, Prefix) -> + list_to_atom(Prefix ++ atom_to_list(Name)). diff --git a/lib/diameter/src/compiler/diameter_forms.hrl b/lib/diameter/src/compiler/diameter_forms.hrl new file mode 100644 index 0000000000..4125e2331c --- /dev/null +++ b/lib/diameter/src/compiler/diameter_forms.hrl @@ -0,0 +1,52 @@ +%% +%% %CopyrightBegin% +%% +%% Copyright Ericsson AB 2010. All Rights Reserved. +%% +%% The contents of this file are subject to the Erlang Public License, +%% Version 1.1, (the "License"); you may not use this file except in +%% compliance with the License. You should have received a copy of the +%% Erlang Public License along with this software. If not, it can be +%% retrieved online at http://www.erlang.org/. +%% +%% Software distributed under the License is distributed on an "AS IS" +%% basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See +%% the License for the specific language governing rights and limitations +%% under the License. +%% +%% %CopyrightEnd% +%% + +%% +%% Macros used when building abstract code. +%% + +%% Form tag with line number. +-define(F(T), T, ?LINE). +%% Yes, that's right. The replacement is to the first unmatched ')'. + +-define(attribute, ?F(attribute)). +-define(clause, ?F(clause)). +-define(function, ?F(function)). +-define(call, ?F(call)). +-define('fun', ?F('fun')). +-define(generate, ?F(generate)). +-define(lc, ?F(lc)). +-define(match, ?F(match)). +-define(remote, ?F(remote)). +-define(record, ?F(record)). +-define(record_field, ?F(record_field)). +-define(record_index, ?F(record_index)). +-define(tuple, ?F(tuple)). + +-define(ATOM(T), {atom, ?LINE, T}). +-define(INTEGER(N), {integer, ?LINE, N}). +-define(VAR(V), {var, ?LINE, V}). +-define(NIL, {nil, ?LINE}). + +-define(CALL(F,A), {?call, ?ATOM(F), A}). +-define(APPLY(M,F,A), {?call, {?remote, ?ATOM(M), ?ATOM(F)}, A}). +-define(FIELDS(Fs), [{?record_field, ?ATOM(F), V} || {F,V} <- Fs]). + +%% Literal term. +-define(TERM(T), erl_parse:abstract(T, ?LINE)). diff --git a/lib/diameter/src/compiler/diameter_make.erl b/lib/diameter/src/compiler/diameter_make.erl new file mode 100644 index 0000000000..4431b88c4d --- /dev/null +++ b/lib/diameter/src/compiler/diameter_make.erl @@ -0,0 +1,120 @@ +%% +%% %CopyrightBegin% +%% +%% Copyright Ericsson AB 2010-2011. All Rights Reserved. +%% +%% The contents of this file are subject to the Erlang Public License, +%% Version 1.1, (the "License"); you may not use this file except in +%% compliance with the License. You should have received a copy of the +%% Erlang Public License along with this software. If not, it can be +%% retrieved online at http://www.erlang.org/. +%% +%% Software distributed under the License is distributed on an "AS IS" +%% basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See +%% the License for the specific language governing rights and limitations +%% under the License. +%% +%% %CopyrightEnd% +%% + +%% +%% Driver for the encoder generator utility. +%% + +-module(diameter_make). + +-export([spec/0, + hrl/0, + erl/0]). + +-spec spec() -> no_return(). +-spec hrl() -> no_return(). +-spec erl() -> no_return(). + +spec() -> + make(spec). + +hrl() -> + make(hrl). + +erl() -> + make(erl). + +%% make/1 + +make(Mode) -> + Args = init:get_plain_arguments(), + Opts = try options(Args) catch throw: help -> help(Mode) end, + Files = proplists:get_value(files, Opts, []), + lists:foreach(fun(F) -> from_file(F, Mode, Opts) end, Files), + halt(0). + +%% from_file/3 + +from_file(F, Mode, Opts) -> + try to_spec(F, Mode, Opts) of + Spec -> + from_spec(F, Spec, Mode, Opts) + catch + error: Reason -> + io:format("==> ~p parse failure:~n~p~n", + [F, {Reason, erlang:get_stacktrace()}]), + halt(1) + end. + +%% to_spec/2 + +%% Try to read the input as an already parsed file or else parse it. +to_spec(F, spec, Opts) -> + diameter_spec_util:parse(F, Opts); +to_spec(F, _, _) -> + {ok, [Spec]} = file:consult(F), + Spec. + +%% from_spec/4 + +from_spec(File, Spec, Mode, Opts) -> + try + diameter_codegen:from_spec(File, Spec, Opts, Mode) + catch + error: Reason -> + io:format("==> ~p codegen failure:~n~p~n~p~n", + [Mode, File, {Reason, erlang:get_stacktrace()}]), + halt(1) + end. + +%% options/1 + +options(["-v" | Rest]) -> + [verbose | options(Rest)]; + +options(["-o", Outdir | Rest]) -> + [{outdir, Outdir} | options(Rest)]; + +options(["-i", Incdir | Rest]) -> + [{include, Incdir} | options(Rest)]; + +options(["-h" | _]) -> + throw(help); + +options(["--" | Fs]) -> + [{files, Fs}]; + +options(["-" ++ _ = Opt | _]) -> + io:fwrite("==> unknown option: ~s~n", [Opt]), + throw(help); + +options(Fs) -> %% trailing arguments + options(["--" | Fs]). + +%% help/1 + +help(M) -> + io:fwrite("Usage: ~p ~p [Options] [--] File ...~n" + "Options:~n" + " -v verbose output~n" + " -h shows this help message~n" + " -o OutDir where to put the output files~n" + " -i IncludeDir where to search for beams to import~n", + [?MODULE, M]), + halt(1). diff --git a/lib/diameter/src/compiler/diameter_spec_scan.erl b/lib/diameter/src/compiler/diameter_spec_scan.erl new file mode 100644 index 0000000000..bc0448882a --- /dev/null +++ b/lib/diameter/src/compiler/diameter_spec_scan.erl @@ -0,0 +1,157 @@ +%% +%% %CopyrightBegin% +%% +%% Copyright Ericsson AB 2010-2011. All Rights Reserved. +%% +%% The contents of this file are subject to the Erlang Public License, +%% Version 1.1, (the "License"); you may not use this file except in +%% compliance with the License. You should have received a copy of the +%% Erlang Public License along with this software. If not, it can be +%% retrieved online at http://www.erlang.org/. +%% +%% Software distributed under the License is distributed on an "AS IS" +%% basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See +%% the License for the specific language governing rights and limitations +%% under the License. +%% +%% %CopyrightEnd% +%% + +-module(diameter_spec_scan). + +%% +%% Functions used by the spec file parser in diameter_spec_util. +%% + +-export([split/1, + split/2, + parse/1]). + +%%% ----------------------------------------------------------- +%%% # parse/1 +%%% +%%% Output: list of Token +%%% +%%% Token = '{' | '}' | '<' | '>' | '[' | ']' +%%% | '*' | '::=' | ':' | ',' | '-' +%%% | {name, string()} +%%% | {tag, atom()} +%%% | {number, integer() >= 0} +%%% +%%% Tokenize a string. Fails if the string does not parse. +%%% ----------------------------------------------------------- + +parse(S) -> + parse(S, []). + +%% parse/2 + +parse(S, Acc) -> + acc(split(S), Acc). + +acc({T, Rest}, Acc) -> + parse(Rest, [T | Acc]); +acc("", Acc) -> + lists:reverse(Acc). + +%%% ----------------------------------------------------------- +%%% # split/2 +%%% +%%% Output: {list() of Token, Rest} +%%% +%%% Extract a specified number of tokens from a string. Returns a list +%%% of length less than the specified number if there are less than +%%% this number of tokens to be parsed. +%%% ----------------------------------------------------------- + +split(Str, N) + when N >= 0 -> + split(N, Str, []). + +split(0, Str, Acc) -> + {lists:reverse(Acc), Str}; + +split(N, Str, Acc) -> + case split(Str) of + {T, Rest} -> + split(N-1, Rest, [T|Acc]); + "" = Rest -> + {lists:reverse(Acc), Rest} + end. + +%%% ----------------------------------------------------------- +%%% # split/1 +%%% +%%% Output: {Token, Rest} | "" +%%% +%%% Extract the next token from a string. +%%% ----------------------------------------------------------- + +split("" = Rest) -> + Rest; + +split("::=" ++ T) -> + {'::=', T}; + +split([H|T]) + when H == ${; H == $}; + H == $<; H == $>; + H == $[; H == $]; + H == $*; H == $:; H == $,; H == $- -> + {list_to_atom([H]), T}; + +split([H|T]) when $A =< H, H =< $Z; + $0 =< H, H =< $9 -> + {P, Rest} = splitwith(fun is_name_ch/1, [H], T), + Tok = try + {number, read_int(P)} + catch + error:_ -> + {name, P} + end, + {Tok, Rest}; + +split([H|T]) when $a =< H, H =< $z -> + {P, Rest} = splitwith(fun is_name_ch/1, [H], T), + {{tag, list_to_atom(P)}, Rest}; + +split([H|T]) when H == $\t; + H == $\s; + H == $\n -> + split(T). + +%% read_int/1 + +read_int([$0,X|S]) + when X == $X; + X == $x -> + {ok, [N], []} = io_lib:fread("~16u", S), + N; + +read_int(S) -> + list_to_integer(S). + +%% splitwith/3 + +splitwith(Fun, Acc, S) -> + split([] /= S andalso Fun(hd(S)), Fun, Acc, S). + +split(true, Fun, Acc, [H|T]) -> + splitwith(Fun, [H|Acc], T); +split(false, _, Acc, S) -> + {lists:reverse(Acc), S}. + +is_name_ch(C) -> + is_alphanum(C) orelse C == $- orelse C == $_. + +is_alphanum(C) -> + is_lower(C) orelse is_upper(C) orelse is_digit(C). + +is_lower(C) -> + $a =< C andalso C =< $z. + +is_upper(C) -> + $A =< C andalso C =< $Z. + +is_digit(C) -> + $0 =< C andalso C =< $9. diff --git a/lib/diameter/src/compiler/diameter_spec_util.erl b/lib/diameter/src/compiler/diameter_spec_util.erl new file mode 100644 index 0000000000..322d53a199 --- /dev/null +++ b/lib/diameter/src/compiler/diameter_spec_util.erl @@ -0,0 +1,1052 @@ +%% +%% %CopyrightBegin% +%% +%% Copyright Ericsson AB 2010-2011. All Rights Reserved. +%% +%% The contents of this file are subject to the Erlang Public License, +%% Version 1.1, (the "License"); you may not use this file except in +%% compliance with the License. You should have received a copy of the +%% Erlang Public License along with this software. If not, it can be +%% retrieved online at http://www.erlang.org/. +%% +%% Software distributed under the License is distributed on an "AS IS" +%% basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See +%% the License for the specific language governing rights and limitations +%% under the License. +%% +%% %CopyrightEnd% +%% + +%% +%% This module turns a .dia (aka spec) file into the orddict that +%% diameter_codegen.erl in turn morphs into .erl and .hrl files for +%% encode and decode of Diameter messages and AVPs. +%% + +-module(diameter_spec_util). + +-export([parse/2]). + +-define(ERROR(T), erlang:error({T, ?MODULE, ?LINE})). +-define(ATOM, list_to_atom). + +%% parse/1 +%% +%% Output: orddict() + +parse(Path, Options) -> + put({?MODULE, verbose}, lists:member(verbose, Options)), + {ok, B} = file:read_file(Path), + Chunks = chunk(B), + Spec = make_spec(Chunks), + true = enums_defined(Spec), %% sanity checks + true = groups_defined(Spec), %% + true = customs_defined(Spec), %% + Full = import_enums(import_groups(import_avps(insert_codes(Spec), + Options))), + true = v_flags_set(Spec), + Full. + +%% Optional reports when running verbosely. +report(What, Data) -> + report(get({?MODULE, verbose}), What, Data). + +report(true, Tag, Data) -> + io:format("##~n## ~p ~p~n", [Tag, Data]); +report(false, _, _) -> + ok. + +%% chunk/1 + +chunk(B) -> + chunkify(normalize(binary_to_list(B))). + +%% normalize/1 +%% +%% Replace CR NL by NL, multiple NL by one, tab by space, and strip +%% comments and leading/trailing space from each line. Precludes +%% semicolons being used for any other purpose than comments. + +normalize(Str) -> + nh(Str, []). + +nh([], Acc) -> + lists:reverse(Acc); + +%% Trim leading whitespace. +nh(Str, Acc) -> + nb(trim(Str), Acc). + +%% tab -> space +nb([$\t|Rest], Acc) -> + nb(Rest, [$\s|Acc]); + +%% CR NL -> NL +nb([$\r,$\n|Rest], Acc) -> + nt(Rest, Acc); + +%% Gobble multiple newlines before starting over again. +nb([$\n|Rest], Acc) -> + nt(Rest, Acc); + +%% Comment. +nb([$;|Rest], Acc) -> + nb(lists:dropwhile(fun(C) -> C /= $\n end, Rest), Acc); + +%% Just an ordinary character. Boring ... +nb([C|Rest], Acc) -> + nb(Rest, [C|Acc]); + +nb([] = Str, Acc) -> + nt(Str, Acc). + +%% Discard a subsequent newline. +nt(T, [$\n|_] = Acc) -> + nh(T, trim(Acc)); + +%% Trim whitespace from the end of the line before continuing. +nt(T, Acc) -> + nh(T, [$\n|trim(Acc)]). + +trim(S) -> + lists:dropwhile(fun(C) -> lists:member(C, "\s\t") end, S). + +%% chunkify/1 +%% +%% Split the spec file into pieces delimited by lines starting with +%% @Tag. Returns a list of {Tag, Args, Chunk} where Chunk is the +%% string extending to the next delimiter. Note that leading +%% whitespace has already been stripped. + +chunkify(Str) -> + %% Drop characters to the start of the first chunk. + {_, Rest} = split_chunk([$\n|Str]), + chunkify(Rest, []). + +chunkify([], Acc) -> + lists:reverse(Acc); + +chunkify(Rest, Acc) -> + {H,T} = split_chunk(Rest), + chunkify(T, [split_tag(H) | Acc]). + +split_chunk(Str) -> + split_chunk(Str, []). + +split_chunk([] = Rest, Acc) -> + {lists:reverse(Acc), Rest}; +split_chunk([$@|Rest], [$\n|_] = Acc) -> + {lists:reverse(Acc), Rest}; +split_chunk([C|Rest], Acc) -> + split_chunk(Rest, [C|Acc]). + +%% Expect a tag and its arguments on a single line. +split_tag(Str) -> + {L, Rest} = get_until($\n, Str), + [{tag, Tag} | Toks] = diameter_spec_scan:parse(L), + {Tag, Toks, trim(Rest)}. + +get_until(EndT, L) -> + {H, [EndT | T]} = lists:splitwith(fun(C) -> C =/= EndT end, L), + {H,T}. + +%% ------------------------------------------------------------------------ +%% make_spec/1 +%% +%% Turn chunks into spec. + +make_spec(Chunks) -> + lists:foldl(fun(T,A) -> report(chunk, T), chunk(T,A) end, + orddict:new(), + Chunks). + +chunk({T, [X], []}, Dict) + when T == name; + T == prefix -> + store(T, atomize(X), Dict); + +chunk({id = T, [{number, I}], []}, Dict) -> + store(T, I, Dict); + +chunk({vendor = T, [{number, I}, N], []}, Dict) -> + store(T, {I, atomize(N)}, Dict); + +%% inherits -> [{Mod, [AvpName, ...]}, ...] +chunk({inherits = T, [_,_|_] = Args, []}, Acc) -> + Mods = [atomize(A) || A <- Args], + append_list(T, [{M,[]} || M <- Mods], Acc); +chunk({inherits = T, [Mod], Body}, Acc) -> + append(T, {atomize(Mod), parse_avp_names(Body)}, Acc); + +%% avp_types -> [{AvpName, Code, Type, Flags, Encr}, ...] +chunk({avp_types = T, [], Body}, Acc) -> + store(T, parse_avp_types(Body), Acc); + +%% custom_types -> [{Mod, [AvpName, ...]}, ...] +chunk({custom_types = T, [Mod], Body}, Dict) -> + [_|_] = Avps = parse_avp_names(Body), + append(T, {atomize(Mod), Avps}, Dict); + +%% messages -> [{MsgName, Code, Type, Appl, Avps}, ...] +chunk({messages = T, [], Body}, Acc) -> + store(T, parse_messages(Body), Acc); + +%% grouped -> [{AvpName, Code, Vendor, Avps}, ...] +chunk({grouped = T, [], Body}, Acc) -> + store(T, parse_groups(Body), Acc); + +%% avp_vendor_id -> [{Id, [AvpName, ...]}, ...] +chunk({avp_vendor_id = T, [{number, I}], Body}, Dict) -> + [_|_] = Names = parse_avp_names(Body), + append(T, {I, Names}, Dict); + +%% enums -> [{AvpName, [{Value, Name}, ...]}, ...] +chunk({enum, [N], Str}, Dict) -> + append(enums, {atomize(N), parse_enums(Str)}, Dict); + +%% result_codes -> [{ResultName, [{Value, Name}, ...]}, ...] +chunk({result_code, [N], Str}, Dict) -> + append(result_codes, {atomize(N), parse_enums(Str)}, Dict); + +%% commands -> [{Name, Abbrev}, ...] +chunk({commands = T, [], Body}, Dict) -> + store(T, parse_commands(Body), Dict); + +chunk(T, _) -> + ?ERROR({unknown_tag, T}). + +store(Key, Value, Dict) -> + error == orddict:find(Key, Dict) orelse ?ERROR({duplicate, Key}), + orddict:store(Key, Value, Dict). +append(Key, Value, Dict) -> + orddict:append(Key, Value, Dict). +append_list(Key, Values, Dict) -> + orddict:append_list(Key, Values, Dict). + +atomize({tag, T}) -> + T; +atomize({name, T}) -> + ?ATOM(T). + +get_value(Keys, Spec) + when is_list(Keys) -> + [get_value(K, Spec) || K <- Keys]; +get_value(Key, Spec) -> + proplists:get_value(Key, Spec, []). + +%% ------------------------------------------------------------------------ +%% enums_defined/1 +%% groups_defined/1 +%% customs_defined/1 +%% +%% Ensure that every local enum/grouped/custom is defined as an avp +%% with an appropriate type. + +enums_defined(Spec) -> + is_defined(Spec, 'Enumerated', enums). + +groups_defined(Spec) -> + is_defined(Spec, 'Grouped', grouped). + +is_defined(Spec, Type, Key) -> + Avps = get_value(avp_types, Spec), + lists:all(fun(T) -> true = is_local(name(Key, T), Type, Avps) end, + get_value(Key, Spec)). + +name(enums, {N,_}) -> N; +name(grouped, {N,_,_,_}) -> N. + +is_local(Name, Type, Avps) -> + case lists:keyfind(Name, 1, Avps) of + {Name, _, Type, _, _} -> + true; + {Name, _, T, _, _} -> + ?ERROR({avp_has_wrong_type, Name, Type, T}); + false -> + ?ERROR({avp_not_defined, Name, Type}) + end. + +customs_defined(Spec) -> + Avps = get_value(avp_types, Spec), + lists:all(fun(A) -> true = is_local(A, Avps) end, + lists:flatmap(fun last/1, get_value(custom_types, Spec))). + +is_local(Name, Avps) -> + case lists:keyfind(Name, 1, Avps) of + {Name, _, T, _, _} when T == 'Grouped'; + T == 'Enumerated' -> + ?ERROR({avp_has_invalid_custom_type, Name, T}); + {Name, _, _, _, _} -> + true; + false -> + ?ERROR({avp_not_defined, Name}) + end. + +last({_,Xs}) -> Xs. + +%% ------------------------------------------------------------------------ +%% v_flags_set/1 + +v_flags_set(Spec) -> + Avps = get_value(avp_types, Spec) + ++ lists:flatmap(fun last/1, get_value(import_avps, Spec)), + Vs = lists:flatmap(fun last/1, get_value(avp_vendor_id, Spec)), + + lists:all(fun(N) -> vset(N, Avps) end, Vs). + +vset(Name, Avps) -> + A = lists:keyfind(Name, 1, Avps), + false == A andalso ?ERROR({avp_not_defined, Name}), + {Name, _Code, _Type, Flags, _Encr} = A, + lists:member('V', Flags) orelse ?ERROR({v_flag_not_set, A}). + +%% ------------------------------------------------------------------------ +%% insert_codes/1 + +insert_codes(Spec) -> + [Msgs, Cmds] = get_value([messages, commands], Spec), + + %% Code -> [{Name, Flags}, ...] + Dict = lists:foldl(fun({N,C,Fs,_,_}, D) -> dict:append(C,{N,Fs},D) end, + dict:new(), + Msgs), + + %% list() of {Code, {ReqName, ReqAbbr}, {AnsName, AnsAbbr}} + %% If the name and abbreviation are the same then the 2-tuples + %% are replaced by the common atom()-valued name. + Codes = dict:fold(fun(C,Ns,A) -> [make_code(C, Ns, Cmds) | A] end, + [], + dict:erase(-1, Dict)), %% answer-message + + orddict:store(command_codes, Codes, Spec). + +make_code(Code, [_,_] = Ns, Cmds) -> + {Req, Ans} = make_names(Ns, lists:map(fun({_,Fs}) -> + lists:member('REQ', Fs) + end, + Ns)), + {Code, abbrev(Req, Cmds), abbrev(Ans, Cmds)}; + +make_code(Code, Cs, _) -> + ?ERROR({missing_request_or_answer, Code, Cs}). + +%% 3.3. Diameter Command Naming Conventions +%% +%% Diameter command names typically includes one or more English words +%% followed by the verb Request or Answer. Each English word is +%% delimited by a hyphen. A three-letter acronym for both the request +%% and answer is also normally provided. + +make_names([{Rname,_},{Aname,_}], [true, false]) -> + {Rname, Aname}; +make_names([{Aname,_},{Rname,_}], [false, true]) -> + {Rname, Aname}; +make_names([_,_] = Names, _) -> + ?ERROR({inconsistent_command_flags, Names}). + +abbrev(Name, Cmds) -> + case abbr(Name, get_value(Name, Cmds)) of + Name -> + Name; + Abbr -> + {Name, Abbr} + end. + +%% No explicit abbreviation: construct. +abbr(Name, []) -> + ?ATOM(abbr(string:tokens(atom_to_list(Name), "-"))); + +%% Abbreviation was specified. +abbr(_Name, Abbr) -> + Abbr. + +%% No hyphens: already abbreviated. +abbr([Abbr]) -> + Abbr; + +%% XX-Request/Answer ==> XXR/XXA +abbr([[_,_] = P, T]) + when T == "Request"; + T == "Answer" -> + P ++ [hd(T)]; + +%% XXX-...-YYY-Request/Answer ==> X...YR/X...YA +abbr([_,_|_] = L) -> + lists:map(fun erlang:hd/1, L). + +%% ------------------------------------------------------------------------ +%% import_avps/2 + +import_avps(Spec, Options) -> + Msgs = get_value(messages, Spec), + Groups = get_value(grouped, Spec), + + %% Messages and groups require AVP's referenced by them. + NeededAvps + = ordsets:from_list(lists:flatmap(fun({_,_,_,_,As}) -> + [avp_name(A) || A <- As] + end, + Msgs) + ++ lists:flatmap(fun({_,_,_,As}) -> + [avp_name(A) || A <- As] + end, + Groups)), + MissingAvps = missing_avps(NeededAvps, Spec), + + report(needed, NeededAvps), + report(missing, MissingAvps), + + Import = inherit(get_value(inherits, Spec), Options), + + report(imported, Import), + + ImportedAvps = lists:map(fun({N,_,_,_,_}) -> N end, + lists:flatmap(fun last/1, Import)), + + Unknown = MissingAvps -- ImportedAvps, + + [] == Unknown orelse ?ERROR({undefined_avps, Unknown}), + + orddict:store(import_avps, Import, orddict:erase(inherits, Spec)). + +%% missing_avps/2 +%% +%% Given a list of AVP names and parsed spec, return the list of +%% AVP's that aren't defined in this spec. + +missing_avps(NeededNames, Spec) -> + Avps = get_value(avp_types, Spec), + Groups = lists:map(fun({N,_,_,As}) -> + {N, [avp_name(A) || A <- As]} + end, + get_value(grouped, Spec)), + Names = ordsets:from_list(['AVP' | lists:map(fun({N,_,_,_,_}) -> N end, + Avps)]), + missing_avps(NeededNames, [], {Names, Groups}). + +avp_name({'<',A,'>'}) -> A; +avp_name({A}) -> A; +avp_name([A]) -> A; +avp_name({_, A}) -> avp_name(A). + +missing_avps(NeededNames, MissingNames, {Names, _} = T) -> + missing(ordsets:filter(fun(N) -> lists:member(N, NeededNames) end, Names), + ordsets:union(NeededNames, MissingNames), + T). + +%% Nothing found locally. +missing([], MissingNames, _) -> + MissingNames; + +%% Or not. Keep looking for for the AVP's needed by the found AVP's of +%% type Grouped. +missing(FoundNames, MissingNames, {_, Groups} = T) -> + NeededNames = lists:flatmap(fun({N,As}) -> + choose(lists:member(N, FoundNames), As, []) + end, + Groups), + missing_avps(ordsets:from_list(NeededNames), + ordsets:subtract(MissingNames, FoundNames), + T). + +%% inherit/2 + +inherit(Inherits, Options) -> + Dirs = [D || {include, D} <- Options] ++ ["."], + lists:foldl(fun(T,A) -> find_avps(T, A, Dirs) end, [], Inherits). + +find_avps({Mod, AvpNames}, Acc, Path) -> + report(inherit_from, Mod), + Avps = avps_from_beam(find_beam(Mod, Path), Mod), %% could be empty + [{Mod, lists:sort(find_avps(AvpNames, Avps))} | Acc]. + +find_avps([], Avps) -> + Avps; +find_avps(Names, Avps) -> + lists:filter(fun({N,_,_,_,_}) -> lists:member(N, Names) end, Avps). + +%% find_beam/2 + +find_beam(Mod, Dirs) + when is_atom(Mod) -> + find_beam(atom_to_list(Mod), Dirs); +find_beam(Mod, Dirs) -> + Beam = Mod ++ code:objfile_extension(), + case try_path(Dirs, Beam) of + {value, Path} -> + Path; + false -> + ?ERROR({beam_not_on_path, Beam, Dirs}) + end. + +try_path([D|Ds], Fname) -> + Path = filename:join(D, Fname), + case file:read_file_info(Path) of + {ok, _} -> + {value, Path}; + _ -> + try_path(Ds, Fname) + end; +try_path([], _) -> + false. + +%% avps_from_beam/2 + +avps_from_beam(Path, Mod) -> + report(beam, Path), + ok = load_module(code:is_loaded(Mod), Mod, Path), + orddict:fetch(avp_types, Mod:dict()). + +load_module(false, Mod, Path) -> + R = filename:rootname(Path, code:objfile_extension()), + {module, Mod} = code:load_abs(R), + ok; +load_module({file, _}, _, _) -> + ok. + +choose(true, X, _) -> X; +choose(false, _, X) -> X. + +%% ------------------------------------------------------------------------ +%% import_groups/1 +%% import_enums/1 + +import_groups(Spec) -> + orddict:store(import_groups, import(grouped, Spec), Spec). + +import_enums(Spec) -> + orddict:store(import_enums, import(enums, Spec), Spec). + +import(Key, Spec) -> + lists:flatmap(fun(T) -> import_key(Key, T) end, + get_value(import_avps, Spec)). + +import_key(Key, {Mod, Avps}) -> + Imports = lists:flatmap(fun(T) -> + choose(lists:keymember(element(1,T), + 1, + Avps), + [T], + []) + end, + get_value(Key, Mod:dict())), + if Imports == [] -> + []; + true -> + [{Mod, Imports}] + end. + +%% ------------------------------------------------------------------------ +%% parse_enums/1 +%% +%% Enums are specified either as the integer value followed by the +%% name or vice-versa. In the former case the name of the enum is +%% taken to be the string up to the end of line, which may contain +%% whitespace. In the latter case the integer may be parenthesized, +%% specified in hex and followed by an inline comment. This is +%% historical and will likely be changed to require a precise input +%% format. +%% +%% Output: list() of {integer(), atom()} + +parse_enums(Str) -> + lists:flatmap(fun(L) -> parse_enum(trim(L)) end, string:tokens(Str, "\n")). + +parse_enum([]) -> + []; + +parse_enum(Str) -> + REs = [{"^(0[xX][0-9A-Fa-f]+|[0-9]+)\s+(.*?)\s*$", 1, 2}, + {"^(.+?)\s+(0[xX][0-9A-Fa-f]+|[0-9]+)(\s+.*)?$", 2, 1}, + {"^(.+?)\s+\\((0[xX][0-9A-Fa-f]+|[0-9]+)\\)(\s+.*)?$", 2, 1}], + parse_enum(Str, REs). + +parse_enum(Str, REs) -> + try lists:foreach(fun(R) -> enum(Str, R) end, REs) of + ok -> + ?ERROR({bad_enum, Str}) + catch + throw: {enum, T} -> + [T] + end. + +enum(Str, {Re, I, N}) -> + case re:run(Str, Re, [{capture, all_but_first, list}]) of + {match, Vs} -> + T = list_to_tuple(Vs), + throw({enum, {to_int(element(I,T)), ?ATOM(element(N,T))}}); + nomatch -> + ok + end. + +to_int([$0,X|Hex]) + when X == $x; + X == $X -> + {ok, [I], _} = io_lib:fread("~#", "16#" ++ Hex), + I; +to_int(I) -> + list_to_integer(I). + +%% ------------------------------------------------------------------------ +%% parse_messages/1 +%% +%% Parse according to the ABNF for message specifications in 3.2 of +%% RFC 3588 (shown below). We require all message and AVP names to +%% start with a digit or uppercase character, except for the base +%% answer-message, which is treated as a special case. Allowing names +%% that start with a digit is more than the RFC specifies but the name +%% doesn't affect what's sent over the wire. (Certains 3GPP standards +%% use names starting with a digit. eg 3GPP-Charging-Id in TS32.299.) + +%% +%% Sadly, not even the RFC follows this grammar. In particular, except +%% in the example in 3.2, it wraps each command-name in angle brackets +%% ('<' '>') which makes parsing a sequence of specifications require +%% lookahead: after 'optional' avps have been parsed, it's not clear +%% whether a '<' is a 'fixed' or whether it's the start of a +%% subsequent message until we see whether or not '::=' follows the +%% closing '>'. Require the grammar as specified. +%% +%% Output: list of {Name, Code, Flags, ApplId, Avps} +%% +%% Name = atom() +%% Code = integer() +%% Flags = integer() +%% ApplId = [] | [integer()] +%% Avps = see parse_avps/1 + +parse_messages(Str) -> + p_cmd(trim(Str), []). + +%% command-def = command-name "::=" diameter-message +%% +%% command-name = diameter-name +%% +%% diameter-name = ALPHA *(ALPHA / DIGIT / "-") +%% +%% diameter-message = header [ *fixed] [ *required] [ *optional] +%% [ *fixed] +%% +%% header = "<" Diameter-Header:" command-id +%% [r-bit] [p-bit] [e-bit] [application-id]">" +%% +%% The header spec (and example that follows it) is slightly mangled +%% and, given the examples in the RFC should as follows: +%% +%% header = "<" "Diameter Header:" command-id +%% [r-bit] [p-bit] [e-bit] [application-id]">" +%% +%% This is what's required/parsed below, modulo whitespace. This is +%% also what's specified in the current draft standard at +%% http://ftp.ietf.org/drafts/wg/dime. +%% +%% Note that the grammar specifies the order fixed, required, +%% optional. In practise there seems to be little difference between +%% the latter two since qualifiers can be used to change the +%% semantics. For example 1*[XXX] and *1{YYY} specify 1 or more of the +%% optional avp XXX and 0 or 1 of the required avp YYY, making the +%% iotional avp required and the required avp optional. The current +%% draft addresses this somewhat by requiring that min for a qualifier +%% on an optional avp must be 0 if present. It doesn't say anything +%% about required avps however, so specifying a min of 0 would still +%% be possible. The draft also does away with the trailing *fixed. +%% +%% What will be parsed here will treat required and optional +%% interchangeably. That is. only require that required/optional +%% follow and preceed fixed, not that optional avps must follow +%% required ones. We already have several specs for which this parsing +%% is necessary and there seems to be no harm in accepting it. + +p_cmd("", Acc) -> + lists:reverse(Acc); + +p_cmd(Str, Acc) -> + {Next, Rest} = split_def(Str), + report(command, Next), + p_cmd(Rest, [p_cmd(Next) | Acc]). + +p_cmd("answer-message" ++ Str) -> + p_header([{name, 'answer-message'} | diameter_spec_scan:parse(Str)]); + +p_cmd(Str) -> + p_header(diameter_spec_scan:parse(Str)). + +%% p_header/1 + +p_header(['<', {name, _} = N, '>' | Toks]) -> + p_header([N | Toks]); + +p_header([{name, 'answer-message' = N}, '::=', + '<', {name, "Diameter"}, {name, "Header"}, ':', {tag, code}, + ',', {name, "ERR"}, '[', {name, "PXY"}, ']', '>' + | Toks]) -> + {N, -1, ['ERR', 'PXY'], [], parse_avps(Toks)}; + +p_header([{name, Name}, '::=', + '<', {name, "Diameter"}, {name, "Header"}, ':', {number, Code} + | Toks]) -> + {Flags, Rest} = p_flags(Toks), + {ApplId, [C|_] = R} = p_appl(Rest), + '>' == C orelse ?ERROR({invalid_flag, {Name, Code, Flags, ApplId}, R}), + {?ATOM(Name), Code, Flags, ApplId, parse_avps(tl(R))}; + +p_header(Toks) -> + ?ERROR({invalid_header, Toks}). + +%% application-id = 1*DIGIT +%% +%% command-id = 1*DIGIT +%% ; The Command Code assigned to the command +%% +%% r-bit = ", REQ" +%% ; If present, the 'R' bit in the Command +%% ; Flags is set, indicating that the message +%% ; is a request, as opposed to an answer. +%% +%% p-bit = ", PXY" +%% ; If present, the 'P' bit in the Command +%% ; Flags is set, indicating that the message +%% ; is proxiable. +%% +%% e-bit = ", ERR" +%% ; If present, the 'E' bit in the Command +%% ; Flags is set, indicating that the answer +%% ; message contains a Result-Code AVP in +%% ; the "protocol error" class. + +p_flags(Toks) -> + lists:foldl(fun p_flags/2, {[], Toks}, ["REQ", "PXY", "ERR"]). + +p_flags(N, {Acc, [',', {name, N} | Toks]}) -> + {[?ATOM(N) | Acc], Toks}; + +p_flags(_, T) -> + T. + +%% The RFC doesn't specify ',' before application-id but this seems a +%% bit inconsistent. Accept a comma if it exists. +p_appl([',', {number, I} | Toks]) -> + {[I], Toks}; +p_appl([{number, I} | Toks]) -> + {[I], Toks}; +p_appl(Toks) -> + {[], Toks}. + +%% parse_avps/1 +%% +%% Output: list() of Avp | {Qual, Avp} +%% +%% Qual = '*' | {Min, '*'} | {'*', Max} | {Min, Max} +%% Avp = {'<', Name, '>'} | {Name} | [Name] +%% +%% Min, Max = integer() >= 0 + +parse_avps(Toks) -> + p_avps(Toks, ['<', '|', '<'], []). +%% The list corresponds to the delimiters expected at the front, middle +%% and back of the avp specification, '|' representing '{' and '['. + +%% fixed = [qual] "<" avp-spec ">" +%% ; Defines the fixed position of an AVP +%% +%% required = [qual] "{" avp-spec "}" +%% ; The AVP MUST be present and can appear +%% ; anywhere in the message. +%% +%% optional = [qual] "[" avp-name "]" +%% ; The avp-name in the 'optional' rule cannot +%% ; evaluate to any AVP Name which is included +%% ; in a fixed or required rule. The AVP can +%% ; appear anywhere in the message. +%% +%% qual = [min] "*" [max] +%% ; See ABNF conventions, RFC 2234 Section 6.6. +%% ; The absence of any qualifiers depends on whether +%% ; it precedes a fixed, required, or optional +%% ; rule. If a fixed or required rule has no +%% ; qualifier, then exactly one such AVP MUST +%% ; be present. If an optional rule has no +%% ; qualifier, then 0 or 1 such AVP may be +%% ; present. +%% ; +%% ; NOTE: "[" and "]" have a different meaning +%% ; than in ABNF (see the optional rule, above). +%% ; These braces cannot be used to express +%% ; optional fixed rules (such as an optional +%% ; ICV at the end). To do this, the convention +%% ; is '0*1fixed'. +%% +%% min = 1*DIGIT +%% ; The minimum number of times the element may +%% ; be present. The default value is zero. +%% +%% max = 1*DIGIT +%% ; The maximum number of times the element may +%% ; be present. The default value is infinity. A +%% ; value of zero implies the AVP MUST NOT be +%% ; present. +%% +%% avp-spec = diameter-name +%% ; The avp-spec has to be an AVP Name, defined +%% ; in the base or extended Diameter +%% ; specifications. +%% +%% avp-name = avp-spec / "AVP" +%% ; The string "AVP" stands for *any* arbitrary +%% ; AVP Name, which does not conflict with the +%% ; required or fixed position AVPs defined in +%% ; the command code definition. +%% + +p_avps([], _, Acc) -> + lists:reverse(Acc); + +p_avps(Toks, Delim, Acc) -> + {Qual, Rest} = p_qual(Toks), + {Avp, R, D} = p_avp(Rest, Delim), + T = if Qual == false -> + Avp; + true -> + {Qual, Avp} + end, + p_avps(R, D, [T | Acc]). + +p_qual([{number, Min}, '*', {number, Max} | Toks]) -> + {{Min, Max}, Toks}; +p_qual([{number, Min}, '*' = Max | Toks]) -> + {{Min, Max}, Toks}; +p_qual(['*' = Min, {number, Max} | Toks]) -> + {{Min, Max}, Toks}; +p_qual(['*' = Q | Toks]) -> + {Q, Toks}; +p_qual(Toks) -> + {false, Toks}. + +p_avp([B, {name, Name}, E | Toks], [_|_] = Delim) -> + {avp(B, ?ATOM(Name), E), + Toks, + delim(choose(B == '<', B, '|'), Delim)}; +p_avp(Toks, Delim) -> + ?ERROR({invalid_avp, Toks, Delim}). + +avp('<' = B, Name, '>' = E) -> + {B, Name, E}; +avp('{', Name, '}') -> + {Name}; +avp('[', Name, ']') -> + [Name]; +avp(B, Name, E) -> + ?ERROR({invalid_avp, B, Name, E}). + +delim(B, D) -> + if B == hd(D) -> D; true -> tl(D) end. + +%% split_def/1 +%% +%% Strip one command definition off head of a string. + +split_def(Str) -> + sdh(Str, []). + +%% Look for the "::=" starting off the definition. +sdh("", _) -> + ?ERROR({missing, '::='}); +sdh("::=" ++ Rest, Acc) -> + sdb(Rest, [$=,$:,$:|Acc]); +sdh([C|Rest], Acc) -> + sdh(Rest, [C|Acc]). + +%% Look for the "::=" starting off the following definition. +sdb("::=" ++ _ = Rest, Acc) -> + sdt(trim(Acc), Rest); +sdb("" = Rest, Acc) -> + sd(Acc, Rest); +sdb([C|Rest], Acc) -> + sdb(Rest, [C|Acc]). + +%% Put name characters of the subsequent specification back into Rest. +sdt([C|Acc], Rest) + when C /= $\n, C /= $\s -> + sdt(Acc, [C|Rest]); + +sdt(Acc, Rest) -> + sd(Acc, Rest). + +sd(Acc, Rest) -> + {trim(lists:reverse(Acc)), Rest}. +%% Note that Rest is already trimmed of leading space. + +%% ------------------------------------------------------------------------ +%% parse_groups/1 +%% +%% Parse according to the ABNF for message specifications in 4.4 of +%% RFC 3588 (shown below). Again, allow names starting with a digit +%% and also require "AVP Header" without "-" since this is what +%% the RFC uses in all examples. +%% +%% Output: list of {Name, Code, Vendor, Avps} +%% +%% Name = atom() +%% Code = integer() +%% Vendor = [] | [integer()] +%% Avps = see parse_avps/1 + +parse_groups(Str) -> + p_group(trim(Str), []). + +%% grouped-avp-def = name "::=" avp +%% +%% name-fmt = ALPHA *(ALPHA / DIGIT / "-") +%% +%% name = name-fmt +%% ; The name has to be the name of an AVP, +%% ; defined in the base or extended Diameter +%% ; specifications. +%% +%% avp = header [ *fixed] [ *required] [ *optional] +%% [ *fixed] +%% +%% header = "<" "AVP-Header:" avpcode [vendor] ">" +%% +%% avpcode = 1*DIGIT +%% ; The AVP Code assigned to the Grouped AVP +%% +%% vendor = 1*DIGIT +%% ; The Vendor-ID assigned to the Grouped AVP. +%% ; If absent, the default value of zero is +%% ; used. + +p_group("", Acc) -> + lists:reverse(Acc); + +p_group(Str, Acc) -> + {Next, Rest} = split_def(Str), + report(group, Next), + p_group(Rest, [p_group(diameter_spec_scan:parse(Next)) | Acc]). + +p_group([{name, Name}, '::=', '<', {name, "AVP"}, {name, "Header"}, + ':', {number, Code} + | Toks]) -> + {Id, [C|_] = R} = p_vendor(Toks), + C == '>' orelse ?ERROR({invalid_group_header, R}), + {?ATOM(Name), Code, Id, parse_avps(tl(R))}; + +p_group(Toks) -> + ?ERROR({invalid_group, Toks}). + +p_vendor([{number, I} | Toks]) -> + {[I], Toks}; +p_vendor(Toks) -> + {[], Toks}. + +%% ------------------------------------------------------------------------ +%% parse_avp_names/1 + +parse_avp_names(Str) -> + [p_name(N) || N <- diameter_spec_scan:parse(Str)]. + +p_name({name, N}) -> + ?ATOM(N); +p_name(T) -> + ?ERROR({invalid_avp_name, T}). + +%% ------------------------------------------------------------------------ +%% parse_avp_types/1 +%% +%% Output: list() of {Name, Code, Type, Flags, Encr} + +parse_avp_types(Str) -> + p_avp_types(Str, []). + +p_avp_types(Str, Acc) -> + p_type(diameter_spec_scan:split(Str, 3), Acc). + +p_type({[],[]}, Acc) -> + lists:reverse(Acc); + +p_type({[{name, Name}, {number, Code}, {name, Type}], Str}, Acc) -> + {Flags, Encr, Rest} = try + p_avp_flags(trim(Str), []) + catch + throw: {?MODULE, Reason} -> + ?ERROR({invalid_avp_type, Reason}) + end, + p_avp_types(Rest, [{?ATOM(Name), Code, ?ATOM(type(Type)), Flags, Encr} + | Acc]); + +p_type(T, _) -> + ?ERROR({invalid_avp_type, T}). + +p_avp_flags([C|Str], Acc) + when C == $M; + C == $P; + C == $V -> + p_avp_flags(Str, [?ATOM([C]) | Acc]); +%% Could support lowercase here if there's a use for distinguishing +%% between Must and Should in the future in deciding whether or not +%% to set a flag. + +p_avp_flags([$-|Str], Acc) -> + %% Require encr on same line as flags if specified. + {H,T} = lists:splitwith(fun(C) -> C /= $\n end, Str), + + {[{name, [$X|X]} | Toks], Rest} = diameter_spec_scan:split([$X|H], 2), + + "" == X orelse throw({?MODULE, {invalid_avp_flag, Str}}), + + Encr = case Toks of + [] -> + "-"; + [{_, E}] -> + (E == "Y" orelse E == "N") + orelse throw({?MODULE, {invalid_encr, E}}), + E + end, + + Flags = ordsets:from_list(lists:reverse(Acc)), + + {Flags, ?ATOM(Encr), Rest ++ T}; + +p_avp_flags(Str, Acc) -> + p_avp_flags([$-|Str], Acc). + +type("DiamIdent") -> "DiameterIdentity"; %% RFC 3588 +type("DiamURI") -> "DiameterURI"; %% RFC 3588 +type("IPFltrRule") -> "IPFilterRule"; %% RFC 4005 +type("QoSFltrRule") -> "QoSFilterRule"; %% RFC 4005 +type(N) + when N == "OctetString"; + N == "Integer32"; + N == "Integer64"; + N == "Unsigned32"; + N == "Unsigned64"; + N == "Float32"; + N == "Float64"; + N == "Grouped"; + N == "Enumerated"; + N == "Address"; + N == "Time"; + N == "UTF8String"; + N == "DiameterIdentity"; + N == "DiameterURI"; + N == "IPFilterRule"; + N == "QoSFilterRule" -> + N; +type(N) -> + ?ERROR({invalid_avp_type, N}). + +%% ------------------------------------------------------------------------ +%% parse_commands/1 + +parse_commands(Str) -> + p_abbr(diameter_spec_scan:parse(Str), []). + + p_abbr([{name, Name}, {name, Abbrev} | Toks], Acc) + when length(Abbrev) < length(Name) -> + p_abbr(Toks, [{?ATOM(Name), ?ATOM(Abbrev)} | Acc]); + +p_abbr([], Acc) -> + lists:reverse(Acc); + +p_abbr(T, _) -> + ?ERROR({invalid_command, T}). diff --git a/lib/diameter/src/compiler/modules.mk b/lib/diameter/src/compiler/modules.mk new file mode 100644 index 0000000000..17a311dacf --- /dev/null +++ b/lib/diameter/src/compiler/modules.mk @@ -0,0 +1,27 @@ +#-*-makefile-*- ; force emacs to enter makefile-mode + +# %CopyrightBegin% +# +# Copyright Ericsson AB 2010-2011. All Rights Reserved. +# +# The contents of this file are subject to the Erlang Public License, +# Version 1.1, (the "License"); you may not use this file except in +# compliance with the License. You should have received a copy of the +# Erlang Public License along with this software. If not, it can be +# retrieved online at http://www.erlang.org/. +# +# Software distributed under the License is distributed on an "AS IS" +# basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See +# the License for the specific language governing rights and limitations +# under the License. +# +# %CopyrightEnd% + +MODULES = \ + diameter_codegen \ + diameter_spec_scan \ + diameter_spec_util + +HRL_FILES = \ + diameter_forms.hrl + |