diff options
Diffstat (limited to 'lib/diameter/src')
26 files changed, 2894 insertions, 1785 deletions
diff --git a/lib/diameter/src/Makefile b/lib/diameter/src/Makefile index eea2aa894d..2ec016ecbc 100644 --- a/lib/diameter/src/Makefile +++ b/lib/diameter/src/Makefile @@ -54,14 +54,16 @@ VPATH = .:base:compiler:transport:gen include modules.mk +# Modules generated from dictionary specifications. DICT_MODULES = $(DICTS:%=gen/diameter_gen_%) DICT_ERLS = $(DICT_MODULES:%=%.erl) DICT_HRLS = $(DICT_MODULES:%=%.hrl) # Modules to build before compiling dictionaries. -COMPILER_MODULES = $(filter compiler/%, $(CT_MODULES)) +COMPILER_MODULES = $(notdir $(filter compiler/%, $(CT_MODULES))) \ + $(DICT_YRL) -# All handwritten modules. +# All handwritten modules from which a depend.mk is generated. MODULES = \ $(RT_MODULES) \ $(CT_MODULES) @@ -74,11 +76,12 @@ APP_MODULES = \ # Modules for which to build beams. TARGET_MODULES = \ $(APP_MODULES) \ - $(CT_MODULES) + $(CT_MODULES) \ + $(DICT_YRL:%=gen/%) # What to build for the 'opt' target. TARGET_FILES = \ - $(patsubst %,$(EBIN)/%.$(EMULATOR),$(notdir $(TARGET_MODULES))) \ + $(patsubst %, $(EBIN)/%.$(EMULATOR), $(notdir $(TARGET_MODULES))) \ $(APP_TARGET) \ $(APPUP_TARGET) @@ -125,6 +128,10 @@ opt: $(TARGET_FILES) debug: @$(MAKE) TYPE=debug opt +# The dictionary parser. +gen/$(DICT_YRL).erl: compiler/$(DICT_YRL).yrl + $(ERLC) -Werror -o $(@D) $< + # Generate the app file. $(APP_TARGET): $(APP_SRC) ../vsn.mk modules.mk M=`echo $(notdir $(APP_MODULES)) | tr ' ' ,`; \ @@ -146,6 +153,8 @@ info: @echo ======================================== @$(call list,DICTS) @echo + @$(call list,DICT_YRL) + @echo @$(call list,RT_MODULES) @echo @$(call list,CT_MODULES) @@ -164,7 +173,7 @@ info: @echo ======================================== clean: - rm -f $(TARGET_FILES) $(DICT_ERLS) $(DICT_HRLS) + rm -f $(TARGET_FILES) gen/* rm -f depend.mk # ---------------------------------------------------- @@ -192,8 +201,9 @@ release_spec: opt $(MAKE) $(TARGET_DIRS:%/=release_src_%) $(TARGET_DIRS:%/=release_src_%): release_src_%: - $(INSTALL_DATA) $(filter $*/%,$(TARGET_MODULES:%=%.erl) \ - $(INTERNAL_HRLS)) \ + $(INSTALL_DATA) $(filter $*/%, $(TARGET_MODULES:%=%.erl) \ + $(INTERNAL_HRLS)) \ + $(filter $*/%, compiler/$(DICT_YRL).yrl) \ $(RELSYSDIR)/src/$* release_docs_spec: @@ -207,7 +217,7 @@ gen/diameter_gen_base_accounting.hrl gen/diameter_gen_relay.hrl: \ $(EBIN)/diameter_gen_base_rfc3588.$(EMULATOR) gen/diameter_gen_base_rfc3588.erl gen/diameter_gen_base_rfc3588.hrl: \ - $(COMPILER_MODULES:compiler/%=$(EBIN)/%.$(EMULATOR)) + $(COMPILER_MODULES:%=$(EBIN)/%.$(EMULATOR)) $(DICT_MODULES:gen/%=$(EBIN)/%.$(EMULATOR)): \ $(INCDIR)/diameter.hrl \ @@ -224,11 +234,13 @@ depend.mk: depend.sed $(MODULES:%=%.erl) Makefile -include depend.mk -.PRECIOUS: $(DICT_ERLS) $(DICT_HRLS) .PHONY: app clean depend dict info release_subdir .PHONY: debug opt release_docs_spec release_spec .PHONY: $(TARGET_DIRS:%/=%) $(TARGET_DIRS:%/=release_src_%) +# Keep intermediate files. +.SECONDARY: $(DICT_ERLS) $(DICT_HRLS) gen/$(DICT_YRL:%=%.erl) + # ---------------------------------------------------- # Targets using secondary expansion (make >= 3.81) # ---------------------------------------------------- @@ -237,4 +249,6 @@ depend.mk: depend.sed $(MODULES:%=%.erl) Makefile # Make beams from a subdirectory. $(TARGET_DIRS:%/=%): \ - $$(patsubst $$@/%,$(EBIN)/%.$(EMULATOR),$$(filter $$@/%,$(TARGET_MODULES))) + $$(patsubst $$@/%, \ + $(EBIN)/%.$(EMULATOR), \ + $$(filter $$@/%, $(TARGET_MODULES) compiler/$(DICT_YRL))) diff --git a/lib/diameter/src/base/diameter.erl b/lib/diameter/src/base/diameter.erl index 2f721421d8..336f0c1f2d 100644 --- a/lib/diameter/src/base/diameter.erl +++ b/lib/diameter/src/base/diameter.erl @@ -38,17 +38,47 @@ service_info/2]). %% Start/stop the application. In a "real" application this should -%% typically be a consequence of specifying diameter in a release file -%% rather than by calling start/stop explicitly. +%% typically be a consequence of a release file rather than by calling +%% start/stop explicitly. -export([start/0, stop/0]). +-export_type([evaluable/0, + app_alias/0, + service_name/0, + capability/0, + peer_filter/0, + service_opt/0, + application_opt/0, + app_module/0, + transport_ref/0, + transport_opt/0, + transport_pred/0, + call_opt/0]). + +-export_type(['OctetString'/0, + 'Integer32'/0, + 'Integer64'/0, + 'Unsigned32'/0, + 'Unsigned64'/0, + 'Float32'/0, + 'Float64'/0, + 'Grouped'/0, + 'Address'/0, + 'Time'/0, + 'UTF8String'/0, + 'DiameterIdentity'/0, + 'DiameterURI'/0, + 'Enumerated'/0, + 'IPFilterRule'/0, + 'QoSFilterRule'/0]). + +-include_lib("diameter/include/diameter.hrl"). -include("diameter_internal.hrl"). --include("diameter_types.hrl"). -%%% -------------------------------------------------------------------------- -%%% start/0 -%%% -------------------------------------------------------------------------- +%% --------------------------------------------------------------------------- +%% start/0 +%% --------------------------------------------------------------------------- -spec start() -> ok @@ -57,9 +87,9 @@ start() -> application:start(?APPLICATION). -%%% -------------------------------------------------------------------------- -%%% stop/0 -%%% -------------------------------------------------------------------------- +%% --------------------------------------------------------------------------- +%% stop/0 +%% --------------------------------------------------------------------------- -spec stop() -> ok @@ -68,9 +98,9 @@ start() -> stop() -> application:stop(?APPLICATION). -%%% -------------------------------------------------------------------------- -%%% start_service/2 -%%% -------------------------------------------------------------------------- +%% --------------------------------------------------------------------------- +%% start_service/2 +%% --------------------------------------------------------------------------- -spec start_service(service_name(), [service_opt()]) -> ok @@ -80,9 +110,9 @@ start_service(SvcName, Opts) when is_list(Opts) -> diameter_config:start_service(SvcName, Opts). -%%% -------------------------------------------------------------------------- -%%% stop_service/1 -%%% -------------------------------------------------------------------------- +%% --------------------------------------------------------------------------- +%% stop_service/1 +%% --------------------------------------------------------------------------- -spec stop_service(service_name()) -> ok @@ -91,9 +121,9 @@ start_service(SvcName, Opts) stop_service(SvcName) -> diameter_config:stop_service(SvcName). -%%% -------------------------------------------------------------------------- -%%% services/0 -%%% -------------------------------------------------------------------------- +%% --------------------------------------------------------------------------- +%% services/0 +%% --------------------------------------------------------------------------- -spec services() -> [service_name()]. @@ -101,9 +131,9 @@ stop_service(SvcName) -> services() -> [Name || {Name, _} <- diameter_service:services()]. -%%% -------------------------------------------------------------------------- -%%% service_info/2 -%%% -------------------------------------------------------------------------- +%% --------------------------------------------------------------------------- +%% service_info/2 +%% --------------------------------------------------------------------------- -spec service_info(service_name(), atom() | [atom()]) -> any(). @@ -111,9 +141,9 @@ services() -> service_info(SvcName, Option) -> diameter_service:info(SvcName, Option). -%%% -------------------------------------------------------------------------- -%%% add_transport/3 -%%% -------------------------------------------------------------------------- +%% --------------------------------------------------------------------------- +%% add_transport/3 +%% --------------------------------------------------------------------------- -spec add_transport(service_name(), {listen|connect, [transport_opt()]}) -> {ok, transport_ref()} @@ -123,9 +153,9 @@ add_transport(SvcName, {T, Opts} = Cfg) when is_list(Opts), (T == connect orelse T == listen) -> diameter_config:add_transport(SvcName, Cfg). -%%% -------------------------------------------------------------------------- -%%% remove_transport/2 -%%% -------------------------------------------------------------------------- +%% --------------------------------------------------------------------------- +%% remove_transport/2 +%% --------------------------------------------------------------------------- -spec remove_transport(service_name(), transport_pred()) -> ok | {error, term()}. @@ -133,12 +163,9 @@ add_transport(SvcName, {T, Opts} = Cfg) remove_transport(SvcName, Pred) -> diameter_config:remove_transport(SvcName, Pred). -%%% -------------------------------------------------------------------------- -%%% # subscribe(SvcName) -%%% -%%% Description: Subscribe to #diameter_event{} messages for the specified -%%% service. -%%% -------------------------------------------------------------------------- +%% --------------------------------------------------------------------------- +%% subscribe/1 +%% --------------------------------------------------------------------------- -spec subscribe(service_name()) -> true. @@ -146,9 +173,9 @@ remove_transport(SvcName, Pred) -> subscribe(SvcName) -> diameter_service:subscribe(SvcName). -%%% -------------------------------------------------------------------------- -%%% # unsubscribe(SvcName) -%%% -------------------------------------------------------------------------- +%% --------------------------------------------------------------------------- +%% unsubscribe/1 +%% --------------------------------------------------------------------------- -spec unsubscribe(service_name()) -> true. @@ -156,9 +183,9 @@ subscribe(SvcName) -> unsubscribe(SvcName) -> diameter_service:unsubscribe(SvcName). -%%% ---------------------------------------------------------- -%%% # session_id/1 -%%% ---------------------------------------------------------- +%% --------------------------------------------------------------------------- +%% session_id/1 +%% --------------------------------------------------------------------------- -spec session_id('DiameterIdentity'()) -> 'OctetString'(). @@ -166,9 +193,9 @@ unsubscribe(SvcName) -> session_id(Ident) -> diameter_session:session_id(Ident). -%%% ---------------------------------------------------------- -%%% # origin_state_id/0 -%%% ---------------------------------------------------------- +%% --------------------------------------------------------------------------- +%% origin_state_id/0 +%% --------------------------------------------------------------------------- -spec origin_state_id() -> 'Unsigned32'(). @@ -176,9 +203,9 @@ session_id(Ident) -> origin_state_id() -> diameter_session:origin_state_id(). -%%% -------------------------------------------------------------------------- -%%% # call/[34] -%%% -------------------------------------------------------------------------- +%% --------------------------------------------------------------------------- +%% call/3,4 +%% --------------------------------------------------------------------------- -spec call(service_name(), app_alias(), any(), [call_opt()]) -> any(). @@ -188,3 +215,125 @@ call(SvcName, App, Message, Options) -> call(SvcName, App, Message) -> call(SvcName, App, Message, []). + +%% =========================================================================== + +%% Diameter basic types + +-type 'OctetString'() :: iolist(). +-type 'Integer32'() :: -2147483647..2147483647. +-type 'Integer64'() :: -9223372036854775807..9223372036854775807. +-type 'Unsigned32'() :: 0..4294967295. +-type 'Unsigned64'() :: 0..18446744073709551615. +-type 'Float32'() :: '-infinity' | float() | infinity. +-type 'Float64'() :: '-infinity' | float() | infinity. +-type 'Grouped'() :: list() | tuple(). + +%% Diameter derived types + +-type 'Address'() + :: inet:ip_address() + | string(). + +-type 'Time'() :: {{integer(), 1..12, 1..31}, + {0..23, 0..59, 0..59}}. +-type 'UTF8String'() :: iolist(). +-type 'DiameterIdentity'() :: 'OctetString'(). +-type 'DiameterURI'() :: 'OctetString'(). +-type 'Enumerated'() :: 'Integer32'(). +-type 'IPFilterRule'() :: 'OctetString'(). +-type 'QoSFilterRule'() :: 'OctetString'(). + +%% The handle to a service. + +-type service_name() + :: any(). + +%% Capabilities options/avps on start_service/2 and/or add_transport/2 + +-type capability() + :: {'Origin-Host', 'DiameterIdentity'()} + | {'Origin-Realm', 'DiameterIdentity'()} + | {'Host-IP-Address', ['Address'()]} + | {'Vendor-Id', 'Unsigned32'()} + | {'Product-Name', 'UTF8String'()} + | {'Supported-Vendor-Id', ['Unsigned32'()]} + | {'Auth-Application-Id', ['Unsigned32'()]} + | {'Vendor-Specific-Application-Id', ['Grouped'()]} + | {'Firmware-Revision', 'Unsigned32'()}. + +%% Filters for call/4 + +-type peer_filter() + :: none + | host + | realm + | {host, any|'DiameterIdentity'()} + | {realm, any|'DiameterIdentity'()} + | {eval, evaluable()} + | {neg, peer_filter()} + | {all, [peer_filter()]} + | {any, [peer_filter()]}. + +-type evaluable() + :: {module(), atom(), list()} + | fun() + | maybe_improper_list(evaluable(), list()). + +%% Options passed to start_service/2 + +-type service_opt() + :: capability() + | {application, [application_opt()]}. + +-type application_opt() + :: {alias, app_alias()} + | {dictionary, module()} + | {module, app_module()} + | {state, any()} + | {call_mutates_state, boolean()} + | {answer_errors, callback|report|discard}. + +-type app_alias() + :: any(). + +-type app_module() + :: module() + | maybe_improper_list(module(), list()) + | #diameter_callback{}. + +%% Identifier returned by add_transport/2 + +-type transport_ref() + :: reference(). + +%% Options passed to add_transport/2 + +-type transport_opt() + :: {transport_module, atom()} + | {transport_config, any()} + | {applications, [app_alias()]} + | {capabilities, [capability()]} + | {capabilities_cb, evaluable()} + | {watchdog_timer, 'Unsigned32'() | {module(), atom(), list()}} + | {reconnect_timer, 'Unsigned32'()} + | {private, any()}. + +%% Predicate passed to remove_transport/2 + +-type transport_pred() + :: fun((reference(), connect|listen, list()) -> boolean()) + | fun((reference(), list()) -> boolean()) + | fun((list()) -> boolean()) + | reference() + | list() + | {connect|listen, transport_pred()} + | {atom(), atom(), list()}. + +%% Options passed to call/4 + +-type call_opt() + :: {extra, list()} + | {filter, peer_filter()} + | {timeout, 'Unsigned32'()} + | detach. diff --git a/lib/diameter/src/base/diameter_callback.erl b/lib/diameter/src/base/diameter_callback.erl index 6d5c8cdca1..90431099b0 100644 --- a/lib/diameter/src/base/diameter_callback.erl +++ b/lib/diameter/src/base/diameter_callback.erl @@ -18,11 +18,58 @@ %% %% -%% A minimal application callback module. +%% A diameter callback module that can redirect selected callbacks, +%% providing reasonable default implementations otherwise. +%% +%% To order alternate callbacks, configure a #diameter_callback record +%% as the Diameter application callback in question. The record has +%% one field for each callback function as well as 'default' and +%% 'extra' fields. A function-specific field can be set to a +%% diameter:evaluable() in order to redirect the callback +%% corresponding to that field, or to 'false' to request the default +%% callback implemented in this module. If neither of these fields are +%% set then the 'default' field determines the form of the callback: a +%% module name results in the usual callback as if the module had been +%% configured directly as the callback module, a diameter_evaluable() +%% in a callback applied to the atom-valued callback name and argument +%% list. For all callbacks not to this module, the 'extra' field is a +%% list of additional arguments, following arguments supplied by +%% diameter but preceeding those of the diameter:evaluable() being +%% applied. +%% +%% For example, the following config to diameter:start_service/2, in +%% an 'application' tuple, would result in only a mymod:peer_down/3 +%% callback, this module implementing the remaining callbacks. +%% +%% {module, #diameter_callback{peer_down = {mymod, down, []}}} +%% +%% Equivalently, this can also be specified with a [Mod | Args] +%% field/value list as follows. +%% +%% {module, [diameter_callback, {peer_down, {mymod, down, []}}]} +%% +%% The following would result in this module suppying peer_up and +%% peer_down callback, others taking place in module mymod. +%% +%% {module, #diameter_callback{peer_up = false, +%% peer_down = false, +%% default = mymod}} +%% +%% The following would result in all callbacks taking place as +%% calls to mymod:diameter/2. +%% +%% {module, #diameter_callback{default = {mymod, diameter, []}}} +%% +%% The following are equivalent and result in all callbacks being +%% provided by this module. +%% +%% {module, #diameter_callback{}} +%% {module, diameter_callback} %% -module(diameter_callback). +%% Default callbacks when no aleternate is specified. -export([peer_up/3, peer_down/3, pick_peer/4, @@ -32,6 +79,16 @@ handle_answer/4, handle_error/4]). +%% Callbacks taking a #diameter_callback record. +-export([peer_up/4, + peer_down/4, + pick_peer/5, + prepare_request/4, + prepare_retransmit/4, + handle_request/4, + handle_answer/5, + handle_error/5]). + -include_lib("diameter/include/diameter.hrl"). %%% ---------------------------------------------------------- @@ -41,51 +98,137 @@ peer_up(_Svc, _Peer, State) -> State. +peer_up(Svc, Peer, State, D) -> + cb(peer_up, + [Svc, Peer, State], + D#diameter_callback.peer_up, + D). + %%% ---------------------------------------------------------- %%% # peer_down/3 %%% ---------------------------------------------------------- -peer_down(_SvcName, _Peer, State) -> +peer_down(_Svc, _Peer, State) -> State. +peer_down(Svc, Peer, State, D) -> + cb(peer_down, + [Svc, Peer, State], + D#diameter_callback.peer_down, + D). + %%% ---------------------------------------------------------- %%% # pick_peer/4 %%% ---------------------------------------------------------- -pick_peer([Peer|_], _, _SvcName, _State) -> - {ok, Peer}. +pick_peer([Peer|_], _, _Svc, _State) -> + {ok, Peer}; +pick_peer([], _, _Svc, _State) -> + false. + +pick_peer(PeersL, PeersR, Svc, State, D) -> + cb(pick_peer, + [PeersL, PeersR, Svc, State], + D#diameter_callback.pick_peer, + D). %%% ---------------------------------------------------------- %%% # prepare_request/3 %%% ---------------------------------------------------------- -prepare_request(Pkt, _SvcName, _Peer) -> +prepare_request(Pkt, _Svc, _Peer) -> {send, Pkt}. +prepare_request(Pkt, Svc, Peer, D) -> + cb(prepare_request, + [Pkt, Svc, Peer], + D#diameter_callback.prepare_request, + D). + %%% ---------------------------------------------------------- %%% # prepare_retransmit/3 %%% ---------------------------------------------------------- -prepare_retransmit(Pkt, _SvcName, _Peer) -> +prepare_retransmit(Pkt, _Svc, _Peer) -> {send, Pkt}. +prepare_retransmit(Pkt, Svc, Peer, D) -> + cb(prepare_retransmit, + [Pkt, Svc, Peer], + D#diameter_callback.prepare_retransmit, + D). + %%% ---------------------------------------------------------- %%% # handle_request/3 %%% ---------------------------------------------------------- -handle_request(_Pkt, _SvcName, _Peer) -> +handle_request(_Pkt, _Svc, _Peer) -> {protocol_error, 3001}. %% DIAMETER_COMMAND_UNSUPPORTED +handle_request(Pkt, Svc, Peer, D) -> + cb(handle_request, + [Pkt, Svc, Peer], + D#diameter_callback.handle_request, + D). + %%% ---------------------------------------------------------- %%% # handle_answer/4 %%% ---------------------------------------------------------- -handle_answer(#diameter_packet{msg = Ans}, _Req, _SvcName, _Peer) -> - Ans. +handle_answer(#diameter_packet{msg = Ans, errors = []}, _Req, _Svc, _Peer) -> + Ans; +handle_answer(#diameter_packet{msg = Ans, errors = Es}, _Req, _Svc, _Peer) -> + [Ans | Es]. + +handle_answer(Pkt, Req, Svc, Peer, D) -> + cb(handle_answer, + [Pkt, Req, Svc, Peer], + D#diameter_callback.handle_answer, + D). %%% --------------------------------------------------------------------------- %%% # handle_error/4 %%% --------------------------------------------------------------------------- -handle_error(Reason, _Req, _SvcName, _Peer) -> +handle_error(Reason, _Req, _Svc, _Peer) -> {error, Reason}. + +handle_error(Reason, Req, Svc, Peer, D) -> + cb(handle_error, + [Reason, Req, Svc, Peer], + D#diameter_callback.handle_error, + D). + +%% =========================================================================== + +%% cb/4 + +%% Unspecified callback: use default field to determine something +%% appropriate. +cb(CB, Args, undefined, D) -> + cb(CB, Args, D); + +%% Explicitly requested default. +cb(CB, Args, false, _) -> + apply(?MODULE, CB, Args); + +%% A specified callback. +cb(_, Args, F, #diameter_callback{extra = X}) -> + diameter_lib:eval([[F|X] | Args]). + +%% cb/3 + +%% No user-supplied default: call ours. +cb(CB, Args, #diameter_callback{default = undefined}) -> + apply(?MODULE, CB, Args); + +%% Default is a module name: make the usual callback. +cb(CB, Args, #diameter_callback{default = M, + extra = X}) + when is_atom(M) -> + apply(M, CB, Args ++ X); + +%% Default is something else: apply if to callback name and arguments. +cb(CB, Args, #diameter_callback{default = F, + extra = X}) -> + diameter_lib:eval([F, CB, Args | X]). diff --git a/lib/diameter/src/base/diameter_capx.erl b/lib/diameter/src/base/diameter_capx.erl index 842a9e6103..6c4d60ee9b 100644 --- a/lib/diameter/src/base/diameter_capx.erl +++ b/lib/diameter/src/base/diameter_capx.erl @@ -54,7 +54,6 @@ -include_lib("diameter/include/diameter.hrl"). -include("diameter_internal.hrl"). --include("diameter_types.hrl"). -include("diameter_gen_base_rfc3588.hrl"). -define(SUCCESS, 2001). %% DIAMETER_SUCCESS @@ -75,13 +74,17 @@ build_CER(Caps) -> try_it([fun bCER/1, Caps]). -spec recv_CER(#diameter_base_CER{}, #diameter_service{}) - -> tried({['Unsigned32'()], #diameter_caps{}, #diameter_base_CEA{}}). + -> tried({[diameter:'Unsigned32'()], + #diameter_caps{}, + #diameter_base_CEA{}}). recv_CER(CER, Svc) -> try_it([fun rCER/2, CER, Svc]). -spec recv_CEA(#diameter_base_CEA{}, #diameter_service{}) - -> tried({['Unsigned32'()], ['Unsigned32'()], #diameter_caps{}}). + -> tried({[diameter:'Unsigned32'()], + [diameter:'Unsigned32'()], + #diameter_caps{}}). recv_CEA(CEA, Svc) -> try_it([fun rCEA/2, CEA, Svc]). diff --git a/lib/diameter/src/base/diameter_config.erl b/lib/diameter/src/base/diameter_config.erl index a6b48fe65b..9253af0de2 100644 --- a/lib/diameter/src/base/diameter_config.erl +++ b/lib/diameter/src/base/diameter_config.erl @@ -605,6 +605,13 @@ app_acc({application, Opts}, Acc) -> app_acc(_, Acc) -> Acc. +init_mod(#diameter_callback{} = R) -> + init_mod([diameter_callback, R]); +init_mod([diameter_callback, #diameter_callback{}] = L) -> + L; +init_mod([diameter_callback = M | L]) + when is_list(L) -> + [M, init_cb(L)]; init_mod(M) when is_atom(M) -> [M]; @@ -614,6 +621,14 @@ init_mod([M|_] = L) init_mod(M) -> ?THROW({module, M}). +init_cb(List) -> + Fields = record_info(fields, diameter_callback), + Defaults = lists:zip(Fields, tl(tuple_to_list(#diameter_callback{}))), + Values = [V || F <- Fields, + D <- [proplists:get_value(F, Defaults)], + V <- [proplists:get_value(F, List, D)]], + #diameter_callback{} = list_to_tuple([diameter_callback | Values]). + init_mutable(M) when M == true; M == false -> diff --git a/lib/diameter/src/base/diameter_peer_fsm.erl b/lib/diameter/src/base/diameter_peer_fsm.erl index fae5d763dc..99644814d2 100644 --- a/lib/diameter/src/base/diameter_peer_fsm.erl +++ b/lib/diameter/src/base/diameter_peer_fsm.erl @@ -46,7 +46,6 @@ -include_lib("diameter/include/diameter.hrl"). -include("diameter_internal.hrl"). --include("diameter_types.hrl"). -include("diameter_gen_base_rfc3588.hrl"). -define(GOAWAY, ?'DIAMETER_BASE_DISCONNECT-CAUSE_DO_NOT_WANT_TO_TALK_TO_YOU'). @@ -78,7 +77,8 @@ parent :: pid(), transport :: pid(), service :: #diameter_service{}, - dpr = false :: false | {'Unsigned32'(), 'Unsigned32'()}}). + dpr = false :: false | {diameter:'Unsigned32'(), + diameter:'Unsigned32'()}}). %% | hop by hop and end to end identifiers %% There are non-3588 states possible as a consequence of 5.6.1 of the diff --git a/lib/diameter/src/base/diameter_service.erl b/lib/diameter/src/base/diameter_service.erl index 7adcf1c265..0893956f97 100644 --- a/lib/diameter/src/base/diameter_service.erl +++ b/lib/diameter/src/base/diameter_service.erl @@ -64,7 +64,6 @@ -include_lib("diameter/include/diameter.hrl"). -include("diameter_internal.hrl"). --include("diameter_types.hrl"). -define(STATE_UP, up). -define(STATE_DOWN, down). @@ -117,7 +116,7 @@ {pid :: match(pid()), type :: match(connect | accept), ref :: match(reference()), %% key into diameter_config - options :: match([transport_opt()]), %% as passed to start_transport + options :: match([diameter:transport_opt()]),%% from start_transport op_state = ?STATE_DOWN :: match(?STATE_DOWN | ?STATE_UP), started = now(), %% at process start conn = false :: match(boolean() | pid())}). @@ -126,7 +125,7 @@ %% Record representing a peer_fsm process. -record(conn, {pid :: pid(), - apps :: [{0..16#FFFFFFFF, app_alias()}], %% {Id, Alias} + apps :: [{0..16#FFFFFFFF, diameter:app_alias()}], %% {Id, Alias} caps :: #diameter_caps{}, started = now(), %% at process start peer :: pid()}). %% key into peerT @@ -137,16 +136,16 @@ handler :: match(pid()), %% request process transport :: match(pid()), %% peer process caps :: match(#diameter_caps{}), - app :: match(app_alias()), %% #diameter_app.alias + app :: match(diameter:app_alias()), %% #diameter_app.alias dictionary :: match(module()), %% #diameter_app.dictionary - module :: match(nonempty_improper_list(module(), list())), + module :: match([module() | list()]), %% #diameter_app.module - filter :: match(peer_filter()), + filter :: match(diameter:peer_filter()), packet :: match(#diameter_packet{})}). %% Record call/4 options are parsed into. -record(options, - {filter = none :: peer_filter(), + {filter = none :: diameter:peer_filter(), extra = [] :: list(), timeout = ?DEFAULT_TIMEOUT :: 0..16#FFFFFFFF, detach = false :: boolean()}). @@ -647,11 +646,6 @@ mod_state(Alias) -> mod_state(Alias, ModS) -> put({?MODULE, mod_state, Alias}, ModS). -%% have_transport/2 - -have_transport(SvcName, Ref) -> - [] /= diameter_config:have_transport(SvcName, Ref). - %%% --------------------------------------------------------------------------- %%% # shutdown/2 %%% --------------------------------------------------------------------------- @@ -1131,7 +1125,7 @@ start_tc(Tc, T, _) -> %% tc_timeout/2 tc_timeout({Ref, _Type, _Opts} = T, #state{service_name = SvcName} = S) -> - tc(have_transport(SvcName, Ref), T, S). + tc(diameter_config:have_transport(SvcName, Ref), T, S). tc(true, {Ref, Type, Opts}, #state{service_name = SvcName} = S) -> @@ -1159,7 +1153,7 @@ close(Pid, #state{service_name = SvcName, options = Opts} = fetch(PeerT, Pid), - c(Pid, have_transport(SvcName, Ref), Opts). + c(Pid, diameter_config:have_transport(SvcName, Ref), Opts). %% Tell watchdog to (maybe) die later ... c(Pid, true, Opts) -> @@ -1490,7 +1484,7 @@ pd([], _) -> send_request(TPid, #diameter_packet{bin = Bin} = Pkt, Req, Timeout) when node() == node(TPid) -> %% Store the outgoing request before sending to avoid a race with - %% reply reception. + %% reply reception. TRef = store_request(TPid, Bin, Req, Timeout), send(TPid, Pkt), TRef; @@ -1946,7 +1940,7 @@ reply(Msg, Dict, TPid, #diameter_packet{errors = Es, when [] == Es; is_record(hd(Msg), diameter_header) -> Pkt = diameter_codec:encode(Dict, make_answer_packet(Msg, ReqPkt)), - incr(send, Pkt, Dict, TPid), %% count result codes in sent answers + incr(send, Pkt, TPid), %% count result codes in sent answers send(TPid, Pkt#diameter_packet{transport_data = TD}); %% Or not: set Result-Code and Failed-AVP AVP's. @@ -2218,12 +2212,11 @@ a(#diameter_packet{errors = []} SvcName, AE, #request{transport = TPid, - dictionary = Dict, caps = Caps, packet = P} = Req) -> try - incr(in, Pkt, Dict, TPid) + incr(in, Pkt, TPid) of _ -> cb(Req, handle_answer, [Pkt, msg(P), SvcName, {TPid, Caps}]) @@ -2254,18 +2247,17 @@ e(Pkt, SvcName, discard, Req) -> %% Increment a stats counter for an incoming or outgoing message. %% TODO: fix -incr(_, #diameter_packet{msg = undefined}, _, _) -> +incr(_, #diameter_packet{msg = undefined}, _) -> ok; -incr(Dir, Pkt, Dict, TPid) +incr(Dir, Pkt, TPid) when is_pid(TPid) -> #diameter_packet{header = #diameter_header{is_error = E} = Hdr, msg = Rec} = Pkt, - D = choose(E, ?BASE, Dict), - RC = int(get_avp_value(D, 'Result-Code', Rec)), + RC = int(get_avp_value(?BASE, 'Result-Code', Rec)), PE = is_protocol_error(RC), %% Check that the E bit is set only for 3xxx result codes. @@ -2273,7 +2265,7 @@ incr(Dir, Pkt, Dict, TPid) orelse (E andalso PE) orelse x({invalid_error_bit, RC}, answer, [Dir, Pkt]), - Ctr = rc_counter(D, Rec, RC), + Ctr = rc_counter(Rec, RC), is_tuple(Ctr) andalso incr(TPid, {diameter_codec:msg_id(Hdr), Dir, Ctr}). @@ -2291,11 +2283,11 @@ incr(TPid, Counter) -> %% Maintain statistics assuming one or the other, not both, which is %% surely the intent of the RFC. -rc_counter(_, _, RC) +rc_counter(_, RC) when is_integer(RC) -> {'Result-Code', RC}; -rc_counter(D, Rec, _) -> - rcc(get_avp_value(D, 'Experimental-Result', Rec)). +rc_counter(Rec, _) -> + rcc(get_avp_value(?BASE, 'Experimental-Result', Rec)). %% Outgoing answers may be in any of the forms messages can be sent %% in. Incoming messages will be records. We're assuming here that the @@ -2355,8 +2347,8 @@ rt(#request{packet = #diameter_packet{msg = undefined}}, _) -> false; %% TODO: Not what we should do. %% ... or not. -rt(#request{packet = #diameter_packet{msg = Msg}, dictionary = D} = Req, S) -> - find_transport(get_destination(Msg, D), Req, S). +rt(#request{packet = #diameter_packet{msg = Msg}} = Req, S) -> + find_transport(get_destination(Msg), Req, S). %%% --------------------------------------------------------------------------- %%% # report_status/5 @@ -2468,12 +2460,12 @@ find_transport({alias, Alias}, Msg, Opts, #state{service = Svc} = S) -> find_transport(#diameter_app{} = App, Msg, Opts, S) -> ft(App, Msg, Opts, S). -ft(#diameter_app{module = Mod, dictionary = D} = App, Msg, Opts, S) -> +ft(#diameter_app{module = Mod} = App, Msg, Opts, S) -> #options{filter = Filter, extra = Xtra} = Opts, pick_peer(App#diameter_app{module = Mod ++ Xtra}, - get_destination(Msg, D), + get_destination(Msg), Filter, S); ft(false = No, _, _, _) -> @@ -2509,11 +2501,11 @@ find_transport([_,_] = RH, Filter, S). -%% get_destination/2 +%% get_destination/1 -get_destination(Msg, Dict) -> - [str(get_avp_value(Dict, 'Destination-Realm', Msg)), - str(get_avp_value(Dict, 'Destination-Host', Msg))]. +get_destination(Msg) -> + [str(get_avp_value(?BASE, 'Destination-Realm', Msg)), + str(get_avp_value(?BASE, 'Destination-Host', Msg))]. %% This is not entirely correct. The avp could have an arity 1, in %% which case an empty list is a DiameterIdentity of length 0 rather diff --git a/lib/diameter/src/base/diameter_session.erl b/lib/diameter/src/base/diameter_session.erl index bb91e97f39..4c468f207c 100644 --- a/lib/diameter/src/base/diameter_session.erl +++ b/lib/diameter/src/base/diameter_session.erl @@ -26,8 +26,6 @@ %% towards diameter_sup -export([init/0]). --include("diameter_types.hrl"). - -define(INT64, 16#FFFFFFFFFFFFFFFF). -define(INT32, 16#FFFFFFFF). @@ -73,7 +71,7 @@ %% consumed (see Section 6.2) SHOULD be silently discarded. -spec sequence() - -> 'Unsigned32'(). + -> diameter:'Unsigned32'(). sequence() -> Instr = {_Pos = 2, _Incr = 1, _Threshold = ?INT32, _SetVal = 0}, @@ -97,7 +95,7 @@ sequence() -> %% counter retained in non-volatile memory across restarts. -spec origin_state_id() - -> 'Unsigned32'(). + -> diameter:'Unsigned32'(). origin_state_id() -> ets:lookup_element(diameter_sequence, origin_state_id, 2). @@ -130,8 +128,8 @@ origin_state_id() -> %% <optional value> is implementation specific but may include a modem's %% device Id, a layer 2 address, timestamp, etc. --spec session_id('DiameterIdentity'()) - -> 'OctetString'(). +-spec session_id(diameter:'DiameterIdentity'()) + -> diameter:'OctetString'(). %% Note that Session-Id has type UTF8String and that any OctetString %% is a UTF8String. diff --git a/lib/diameter/src/base/diameter_types.erl b/lib/diameter/src/base/diameter_types.erl index 6b1b1b8d39..9ae289034c 100644 --- a/lib/diameter/src/base/diameter_types.erl +++ b/lib/diameter/src/base/diameter_types.erl @@ -42,8 +42,23 @@ 'IPFilterRule'/2, 'QoSFilterRule'/2]). +%% Functions taking the AVP name in question as second parameter. +-export(['OctetString'/3, + 'Integer32'/3, + 'Integer64'/3, + 'Unsigned32'/3, + 'Unsigned64'/3, + 'Float32'/3, + 'Float64'/3, + 'Address'/3, + 'Time'/3, + 'UTF8String'/3, + 'DiameterIdentity'/3, + 'DiameterURI'/3, + 'IPFilterRule'/3, + 'QoSFilterRule'/3]). + -include_lib("diameter/include/diameter.hrl"). --include("diameter_internal.hrl"). -define(UINT(N,X), ((0 =< X) andalso (X < 1 bsl N))). -define(SINT(N,X), ((-1*(1 bsl (N-1)) < X) andalso (X < 1 bsl (N-1)))). @@ -433,6 +448,50 @@ uenc([C | Rest], Acc) -> 'Time'(encode, zero) -> <<0:32>>. +%% ------------------------------------------------------------------------- + +'OctetString'(M, _, Data) -> + 'OctetString'(M, Data). + +'Integer32'(M, _, Data) -> + 'Integer32'(M, Data). + +'Integer64'(M, _, Data) -> + 'Integer64'(M, Data). + +'Unsigned32'(M, _, Data) -> + 'Unsigned32'(M, Data). + +'Unsigned64'(M, _, Data) -> + 'Unsigned64'(M, Data). + +'Float32'(M, _, Data) -> + 'Float32'(M, Data). + +'Float64'(M, _, Data) -> + 'Float64'(M, Data). + +'Address'(M, _, Data) -> + 'Address'(M, Data). + +'Time'(M, _, Data) -> + 'Time'(M, Data). + +'UTF8String'(M, _, Data) -> + 'UTF8String'(M, Data). + +'DiameterIdentity'(M, _, Data) -> + 'DiameterIdentity'(M, Data). + +'DiameterURI'(M, _, Data) -> + 'DiameterURI'(M, Data). + +'IPFilterRule'(M, _, Data) -> + 'IPFilterRule'(M, Data). + +'QoSFilterRule'(M, _, Data) -> + 'QoSFilterRule'(M, Data). + %% =========================================================================== %% =========================================================================== diff --git a/lib/diameter/src/base/diameter_types.hrl b/lib/diameter/src/base/diameter_types.hrl deleted file mode 100644 index 02bf8a74dd..0000000000 --- a/lib/diameter/src/base/diameter_types.hrl +++ /dev/null @@ -1,139 +0,0 @@ -%% -%% %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% -%% - -%% -%% Types for function specifications, primarily in diameter.erl. This -%% has nothing specifically to do with diameter_types.erl. -%% - --type evaluable() - :: {module(), atom(), list()} - | fun() - | nonempty_improper_list(evaluable(), list()). %% [evaluable() | Args] - --type app_alias() - :: any(). - --type service_name() - :: any(). - -%% Diameter basic types - --type 'OctetString'() :: iolist(). --type 'Integer32'() :: -2147483647..2147483647. --type 'Integer64'() :: -9223372036854775807..9223372036854775807. --type 'Unsigned32'() :: 0..4294967295. --type 'Unsigned64'() :: 0..18446744073709551615. --type 'Float32'() :: '-infinity' | float() | infinity. --type 'Float64'() :: '-infinity' | float() | infinity. --type 'Grouped'() :: list() | tuple(). - -%% Diameter derived types - --type 'Address'() - :: inet:ip_address() - | string(). - --type 'Time'() :: {{integer(), 1..12, 1..31}, - {0..23, 0..59, 0..59}}. --type 'UTF8String'() :: iolist(). --type 'DiameterIdentity'() :: 'OctetString'(). --type 'DiameterURI'() :: 'OctetString'(). --type 'Enumerated'() :: 'Integer32'(). --type 'IPFilterRule'() :: 'OctetString'(). --type 'QoSFilterRule'() :: 'OctetString'(). - -%% Capabilities options/avps on start_service/2 and/or add_transport/2 - --type capability() - :: {'Origin-Host', 'DiameterIdentity'()} - | {'Origin-Realm', 'DiameterIdentity'()} - | {'Host-IP-Address', ['Address'()]} - | {'Vendor-Id', 'Unsigned32'()} - | {'Product-Name', 'UTF8String'()} - | {'Supported-Vendor-Id', ['Unsigned32'()]} - | {'Auth-Application-Id', ['Unsigned32'()]} - | {'Vendor-Specific-Application-Id', ['Grouped'()]} - | {'Firmware-Revision', 'Unsigned32'()}. - -%% Filters for call/4 - --type peer_filter() - :: none - | host - | realm - | {host, any|'DiameterIdentity'()} - | {realm, any|'DiameterIdentity'()} - | {eval, evaluable()} - | {neg, peer_filter()} - | {all, [peer_filter()]} - | {any, [peer_filter()]}. - -%% Options passed to start_service/2 - --type service_opt() - :: capability() - | {application, [application_opt()]}. - --type application_opt() - :: {alias, app_alias()} - | {dictionary, module()} - | {module, app_module()} - | {state, any()} - | {call_mutates_state, boolean()} - | {answer_errors, callback|report|discard}. - --type app_module() - :: module() - | nonempty_improper_list(module(), list()). %% list with module() head - -%% Identifier returned by add_transport/2 - --type transport_ref() - :: reference(). - -%% Options passed to add_transport/2 - --type transport_opt() - :: {transport_module, atom()} - | {transport_config, any()} - | {applications, [app_alias()]} - | {capabilities, [capability()]} - | {watchdog_timer, 'Unsigned32'() | {module(), atom(), list()}} - | {reconnect_timer, 'Unsigned32'()} - | {private, any()}. - -%% Predicate passed to remove_transport/2 - --type transport_pred() - :: fun((reference(), connect|listen, list()) -> boolean()) - | fun((reference(), list()) -> boolean()) - | fun((list()) -> boolean()) - | reference() - | list() - | {connect|listen, transport_pred()} - | {atom(), atom(), list()}. - -%% Options passed to call/4 - --type call_opt() - :: {extra, list()} - | {filter, peer_filter()} - | {timeout, 'Unsigned32'()} - | detach. diff --git a/lib/diameter/src/compiler/diameter_codegen.erl b/lib/diameter/src/compiler/diameter_codegen.erl index 0fd4a0b301..1e31c40afe 100644 --- a/lib/diameter/src/compiler/diameter_codegen.erl +++ b/lib/diameter/src/compiler/diameter_codegen.erl @@ -20,17 +20,18 @@ -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.) +%% This module generates erl/hrl files for encode/decode modules +%% from the orddict parsed from a dictionary file (.dia) by +%% diameter_dict_util. The generated code is simple (one-liners), +%% the generated functions being called by code included iin the +%% generated modules from diameter_gen.hrl. The orddict itself is +%% returned by dict/0 in the generated module and diameter_dict_util +%% calls this function when importing dictionaries as a consequence +%% of @inherits sections. That is, @inherits introduces a dependency +%% on the beam file of another dictionary. %% --export([from_spec/4]). +-export([from_dict/4]). %% Internal exports (for test). -export([file/1, @@ -38,17 +39,23 @@ file/3]). -include("diameter_forms.hrl"). +-include("diameter_vsn.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)})])]}). +-define(S, atom_to_list). +-define(A, list_to_atom). +-define(Atom(T), ?ATOM(?A(T))). -from_spec(File, Spec, Opts, Mode) -> +%% =========================================================================== + +-spec from_dict(File, Spec, Opts, Mode) + -> ok + when File :: string(), + Spec :: orddict:orddict(), + Opts :: list(), + Mode :: spec | erl | hrl. + +from_dict(File, Spec, Opts, Mode) -> Outdir = proplists:get_value(outdir, Opts, "."), putr(verbose, lists:member(verbose, Opts)), putr(debug, lists:member(debug, Opts)), @@ -73,7 +80,7 @@ getr(Key) -> %% =========================================================================== %% =========================================================================== -%% Generate from parsed spec in a file. +%% Generate from parsed dictionary in a file. file(F) -> file(F, spec). @@ -83,55 +90,46 @@ file(F, Mode) -> file(F, Outdir, Mode) -> {ok, [Spec]} = file:consult(F), - from_spec(F, Spec, Outdir, Mode). + from_dict(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."). +write(Path, Str) -> + w(Path, Str, "~s"). -w(Path, Spec, Fmt) -> +write_term(Path, T) -> + w(Path, T, "~p."). + +w(Path, T, Fmt) -> {ok, Fd} = file:open(Path, [write]), - io:fwrite(Fd, Fmt ++ "~n", [Spec]), + io:fwrite(Fd, Fmt ++ "~n", [T]), 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), + gen(Mode, Spec, ?A(Mod), Path), ok. mod(File, error) -> filename:rootname(filename:basename(File)); mod(_, {ok, Mod}) -> - atom_to_list(Mod). + Mod. gen(spec, Spec, _Mod, Path) -> - write(Path ++ ".spec", Spec); + write_term(Path ++ ".spec", [?VERSION | 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, compile, [{parse_transform, diameter_nowarn}]}, {?attribute, export, [{name, 0}, {id, 0}, {vendor_id, 0}, @@ -175,7 +173,7 @@ gen(erl, Spec, Mod, Path) -> gen_erl(Path, insert_hrl_forms(Spec, Forms)). gen_erl(Path, Forms) -> - getr(debug) andalso write(Path ++ ".forms", Forms), + getr(debug) andalso write_term(Path ++ ".forms", Forms), write(Path ++ ".erl", header() ++ erl_prettypr:format(erl_syntax:form_list(Forms))). @@ -224,16 +222,16 @@ 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)), + Name = list_to_atom(Prefix ++ 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)}; + {?record_field, ?Atom(Name)}; true -> - {?record_field, ?ATOM(Name), ?NIL} + {?record_field, ?Atom(Name), ?NIL} end. %%% ------------------------------------------------------------------------ @@ -256,25 +254,33 @@ c_id({ok, Id}) -> {?clause, [], [], [?INTEGER(Id)]}; c_id(error) -> - ?UNEXPECTED(0). + ?BADARG(0). %%% ------------------------------------------------------------------------ %%% # vendor_id/0 %%% ------------------------------------------------------------------------ f_vendor_id(Spec) -> - {Id, _} = orddict:fetch(vendor, Spec), {?function, vendor_id, 0, - [{?clause, [], [], [?INTEGER(Id)]}]}. + [{?clause, [], [], [b_vendor_id(orddict:find(vendor, Spec))]}]}. + +b_vendor_id({ok, {Id, _}}) -> + ?INTEGER(Id); +b_vendor_id(error) -> + ?APPLY(erlang, error, [?TERM(undefined)]). %%% ------------------------------------------------------------------------ %%% # vendor_name/0 %%% ------------------------------------------------------------------------ f_vendor_name(Spec) -> - {_, Name} = orddict:fetch(vendor, Spec), {?function, vendor_name, 0, - [{?clause, [], [], [?ATOM(Name)]}]}. + [{?clause, [], [], [b_vendor_name(orddict:find(vendor, Spec))]}]}. + +b_vendor_name({ok, {_, Name}}) -> + ?Atom(Name); +b_vendor_name(error) -> + ?APPLY(erlang, error, [?TERM(undefined)]). %%% ------------------------------------------------------------------------ %%% # msg_name/1 @@ -287,22 +293,18 @@ f_msg_name(Spec) -> %% DIAMETER_COMMAND_UNSUPPORTED should be replied. msg_name(Spec) -> - lists:flatmap(fun c_msg_name/1, - proplists:get_value(command_codes, 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))]}, + [?Atom(Req)]}, {?clause, [?INTEGER(Code), ?ATOM(false)], [], - [?ATOM(mname(Ans))]}]. - -mname({N, _Abbr}) -> - N; -mname(N) -> - N. + [?Atom(Ans)]}]. %%% ------------------------------------------------------------------------ %%% # msg2rec/1 @@ -313,30 +315,11 @@ f_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. + lists:map(fun(T) -> c_msg2rec(T, Pre) end, get_value(messages, Spec)) + ++ [?BADARG(1)]. -c_msg2rec({N,A}, Pre) -> - [c_name2rec(N, N, Pre), c_name2rec(A, N, Pre)]; -c_msg2rec(N, Pre) -> - [c_name2rec(N, N, Pre)]. +c_msg2rec({N,_,_,_,_}, Pre) -> + c_name2rec(N, Pre). %%% ------------------------------------------------------------------------ %%% # rec2msg/1 @@ -348,10 +331,10 @@ f_rec2msg(Spec) -> rec2msg(Spec) -> Pre = prefix(Spec), lists:map(fun(T) -> c_rec2msg(T, Pre) end, get_value(messages, Spec)) - ++ [?UNEXPECTED(1)]. + ++ [?BADARG(1)]. c_rec2msg({N,_,_,_,_}, Pre) -> - {?clause, [?ATOM(rec_name(N, Pre))], [], [?ATOM(N)]}. + {?clause, [?Atom(rec_name(N, Pre))], [], [?Atom(N)]}. %%% ------------------------------------------------------------------------ %%% # name2rec/1 @@ -364,11 +347,11 @@ 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) + lists:map(fun({N,_,_,_}) -> c_name2rec(N, Pre) end, Groups) ++ [{?clause, [?VAR('T')], [], [?CALL(msg2rec, [?VAR('T')])]}]. -c_name2rec(Name, Rname, Pre) -> - {?clause, [?ATOM(Name)], [], [?ATOM(rec_name(Rname, Pre))]}. +c_name2rec(Name, Pre) -> + {?clause, [?Atom(Name)], [], [?Atom(rec_name(Name, Pre))]}. avps({_Mod, Avps}) -> Avps. @@ -390,32 +373,47 @@ f_avp_name(Spec) -> %% 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)), + Avps = get_value(avp_types, Spec), + Imported = get_value(import_avps, Spec), + Vid = orddict:find(vendor, Spec), + Vs = vendor_id_map(Spec), - lists:map(fun(T) -> c_avp_name(T, Vid, Vs) end, Avps) + lists:map(fun(T) -> c_avp_name(T, Vs, Vid) end, Avps) + ++ lists:flatmap(fun(T) -> c_imported_avp_name(T, Vs) end, Imported) ++ [{?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({Name, Code, Type, Flags}, Vs, Vid) -> + c_avp_name_(?TERM({?A(Name), ?A(Type)}), + ?INTEGER(Code), + vid(Name, Flags, Vs, Vid)). -c_avp_name(T, Code, false, _, undefined = U) -> - {?clause, [?INTEGER(Code), ?ATOM(U)], +%% Note that an imported AVP's vendor id is determined by +%% avp_vendor_id in the inheriting module and vendor in the inherited +%% module. In particular, avp_vendor_id in the inherited module is +%% ignored so can't just call Mod:avp_header/1 to retrieve the vendor +%% id. A vendor id specified in @grouped is equivalent to one +%% specified as avp_vendor_id. + +c_imported_avp_name({Mod, Avps}, Vs) -> + lists:map(fun(A) -> c_avp_name(A, Vs, {module, Mod}) end, Avps). + +c_avp_name_(T, Code, undefined = U) -> + {?clause, [Code, ?ATOM(U)], [], - [?TERM(T)]}; + [T]}; -c_avp_name(T, Code, true, Vid, V) - when is_integer(Vid) -> - {?clause, [?INTEGER(Code), ?INTEGER(choose(V == undefined, Vid, V))], +c_avp_name_(T, Code, Vid) -> + {?clause, [Code, ?INTEGER(Vid)], [], - [?TERM(T)]}. + [T]}. + +vendor_id_map(Spec) -> + lists:flatmap(fun({V,Ns}) -> [{N,V} || N <- Ns] end, + get_value(avp_vendor_id, Spec)) + ++ lists:flatmap(fun({_,_,[],_}) -> []; + ({N,_,[V],_}) -> [{N,V}] + end, + get_value(grouped, Spec)). %%% ------------------------------------------------------------------------ %%% # avp_arity/2 @@ -445,60 +443,75 @@ c_avp_arity(Name, Avps) -> c_arity(Name, Avp) -> {AvpName, Arity} = avp_info(Avp), - {?clause, [?ATOM(Name), ?ATOM(AvpName)], [], [?TERM(Arity)]}. + {?clause, [?Atom(Name), ?Atom(AvpName)], [], [?TERM(Arity)]}. %%% ------------------------------------------------------------------------ %%% # avp/3 %%% ------------------------------------------------------------------------ f_avp(Spec) -> - {?function, avp, 3, avp(Spec) ++ [?UNEXPECTED(3)]}. + {?function, avp, 3, avp(Spec) ++ [?BADARG(3)]}. avp(Spec) -> - Native = get_value(avp_types, Spec), - Custom = get_value(custom_types, Spec), - Imported = get_value(import_avps, Spec), - Enums = get_value(enums, Spec), - avp([{N,T} || {N,_,T,_,_} <- Native], Imported, Custom, Enums). + Native = get_value(avp_types, Spec), + CustomMods = get_value(custom_types, Spec), + TypeMods = get_value(codecs, Spec), + Imported = get_value(import_avps, Spec), + Enums = get_value(enum, Spec), -avp(Native, Imported, Custom, Enums) -> - Dict = orddict:from_list(Native), + Custom = lists:map(fun({M,As}) -> {M, custom_types, As} end, + CustomMods) + ++ lists:map(fun({M,As}) -> {M, codecs, As} end, + TypeMods), + avp(types(Native), Imported, Custom, Enums). + +types(Avps) -> + lists:map(fun({N,_,T,_}) -> {N,T} end, Avps). - report(native, Dict), +avp(Native, Imported, Custom, Enums) -> + report(native, Native), report(imported, Imported), report(custom, Custom), - CustomNames = lists:flatmap(fun({_,Ns}) -> Ns end, Custom), + TypeDict = lists:foldl(fun({N,_,T,_}, D) -> orddict:store(N,T,D) end, + orddict:from_list(Native), + lists:flatmap(fun avps/1, Imported)), + + 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, + lists:filter(fun({N,_}) -> not_in(CustomNames, N) end, Native)) - ++ lists:flatmap(fun(I) -> cs_imported_avp(I, Enums) end, Imported) - ++ lists:flatmap(fun(C) -> cs_custom_avp(C, Dict) end, Custom). + ++ lists:flatmap(fun(I) -> cs_imported_avp(I, Enums, CustomNames) end, + Imported) + ++ lists:flatmap(fun(C) -> cs_custom_avp(C, TypeDict) end, Custom). + +not_in(List, X) -> + not lists:member(X, List). c_base_avp({AvpName, T}) -> - {?clause, [?VAR('T'), ?VAR('Data'), ?ATOM(AvpName)], + {?clause, [?VAR('T'), ?VAR('Data'), ?Atom(AvpName)], [], - [base_avp(AvpName, T)]}. + [b_base_avp(AvpName, T)]}. -base_avp(AvpName, 'Enumerated') -> - ?CALL(enumerated_avp, [?VAR('T'), ?ATOM(AvpName), ?VAR('Data')]); +b_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')]); +b_base_avp(AvpName, "Grouped") -> + ?CALL(grouped_avp, [?VAR('T'), ?Atom(AvpName), ?VAR('Data')]); -base_avp(_, Type) -> - ?APPLY(diameter_types, Type, [?VAR('T'), ?VAR('Data')]). +b_base_avp(_, Type) -> + ?APPLY(diameter_types, ?A(Type), [?VAR('T'), ?VAR('Data')]). -cs_imported_avp({Mod, Avps}, Enums) -> - lists:map(fun(A) -> imported_avp(Mod, A, Enums) end, Avps). +cs_imported_avp({Mod, Avps}, Enums, CustomNames) -> + lists:map(fun(A) -> imported_avp(Mod, A, Enums) end, + lists:filter(fun({N,_,_,_}) -> not_in(CustomNames, N) end, + Avps)). -imported_avp(_Mod, {AvpName, _, 'Grouped' = T, _, _}, _) -> +imported_avp(_Mod, {AvpName, _, "Grouped" = T, _}, _) -> c_base_avp({AvpName, T}); -imported_avp(Mod, {AvpName, _, 'Enumerated' = T, _, _}, Enums) -> +imported_avp(Mod, {AvpName, _, "Enumerated" = T, _}, Enums) -> case lists:keymember(AvpName, 1, Enums) of true -> c_base_avp({AvpName, T}); @@ -506,34 +519,40 @@ imported_avp(Mod, {AvpName, _, 'Enumerated' = T, _, _}, Enums) -> c_imported_avp(Mod, AvpName) end; -imported_avp(Mod, {AvpName, _, _, _, _}, _) -> +imported_avp(Mod, {AvpName, _, _, _}, _) -> c_imported_avp(Mod, AvpName). c_imported_avp(Mod, AvpName) -> - {?clause, [?VAR('T'), ?VAR('Data'), ?ATOM(AvpName)], + {?clause, [?VAR('T'), ?VAR('Data'), ?Atom(AvpName)], [], [?APPLY(Mod, avp, [?VAR('T'), ?VAR('Data'), - ?ATOM(AvpName)])]}. + ?Atom(AvpName)])]}. -cs_custom_avp({Mod, Avps}, Dict) -> - lists:map(fun(N) -> c_custom_avp(Mod, N, orddict:fetch(N, Dict)) end, +cs_custom_avp({Mod, Key, Avps}, Dict) -> + lists:map(fun(N) -> c_custom_avp(Mod, Key, N, orddict:fetch(N, Dict)) end, Avps). -c_custom_avp(Mod, AvpName, Type) -> - {?clause, [?VAR('T'), ?VAR('Data'), ?ATOM(AvpName)], +c_custom_avp(Mod, Key, AvpName, Type) -> + {F,A} = custom(Key, AvpName, Type), + {?clause, [?VAR('T'), ?VAR('Data'), ?Atom(AvpName)], [], - [?APPLY(Mod, AvpName, [?VAR('T'), ?ATOM(Type), ?VAR('Data')])]}. + [?APPLY(?A(Mod), ?A(F), [?VAR('T'), ?Atom(A), ?VAR('Data')])]}. + +custom(custom_types, AvpName, Type) -> + {AvpName, Type}; +custom(codecs, AvpName, Type) -> + {Type, AvpName}. %%% ------------------------------------------------------------------------ %%% # enumerated_avp/3 %%% ------------------------------------------------------------------------ f_enumerated_avp(Spec) -> - {?function, enumerated_avp, 3, enumerated_avp(Spec) ++ [?UNEXPECTED(3)]}. + {?function, enumerated_avp, 3, enumerated_avp(Spec) ++ [?BADARG(3)]}. enumerated_avp(Spec) -> - Enums = get_value(enums, Spec), + Enums = get_value(enum, Spec), lists:flatmap(fun cs_enumerated_avp/1, Enums) ++ lists:flatmap(fun({M,Es}) -> enumerated_avp(M, Es, Enums) end, get_value(import_enums, Spec)). @@ -554,11 +573,11 @@ cs_enumerated_avp(false, _, _) -> cs_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>>)], +c_enumerated_avp(AvpName, {_,I}) -> + [{?clause, [?ATOM(decode), ?Atom(AvpName), ?TERM(<<I:32/integer>>)], [], [?TERM(I)]}, - {?clause, [?ATOM(encode), ?ATOM(AvpName), ?INTEGER(I)], + {?clause, [?ATOM(encode), ?Atom(AvpName), ?INTEGER(I)], [], [?TERM(<<I:32/integer>>)]}]. @@ -567,7 +586,7 @@ c_enumerated_avp(AvpName, {I,_}) -> %%% ------------------------------------------------------------------------ f_msg_header(Spec) -> - {?function, msg_header, 1, msg_header(Spec) ++ [?UNEXPECTED(1)]}. + {?function, msg_header, 1, msg_header(Spec) ++ [?BADARG(1)]}. msg_header(Spec) -> msg_header(get_value(messages, Spec), Spec). @@ -582,7 +601,7 @@ msg_header(Msgs, Spec) -> %% Note that any application id in the message header spec is ignored. c_msg_header(Name, Code, Flags, ApplId) -> - {?clause, [?ATOM(Name)], + {?clause, [?Atom(Name)], [], [?TERM({Code, encode_msg_flags(Flags), ApplId})]}. @@ -598,50 +617,61 @@ emf('ERR', N) -> N bor 2#00100000. %%% ------------------------------------------------------------------------ f_avp_header(Spec) -> - {?function, avp_header, 1, avp_header(Spec) ++ [?UNEXPECTED(1)]}. + {?function, avp_header, 1, avp_header(Spec) ++ [?BADARG(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)), + Vid = orddict:find(vendor, Spec), + Vs = vendor_id_map(Spec), - lists:flatmap(fun(A) -> c_avp_header({Vid, Vs}, A) end, + lists:flatmap(fun(A) -> c_avp_header(A, Vs, Vid) end, Native ++ Imported). -c_avp_header({Vid, Vs}, {Name, Code, _Type, Flags, _Encr}) -> - [{?clause, [?ATOM(Name)], +c_avp_header({Name, Code, _Type, Flags}, Vs, Vid) -> + [{?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({Mod, Avps}, Vs, _Vid) -> + lists:map(fun(A) -> c_imported_avp_header(A, Mod, Vs) end, Avps). -c_avp_header(Vs, Mod, {Name, _, _, Flags, _}) -> - Apply = ?APPLY(Mod, avp_header, [?ATOM(Name)]), - {?clause, [?ATOM(Name)], +%% Note that avp_vendor_id in the inherited dictionary is ignored. The +%% value must be changed in the inheriting dictionary. This is +%% consistent with the semantics of avp_name/2. + +c_imported_avp_header({Name, _Code, _Type, _Flags}, Mod, Vs) -> + 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. +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(lists:member($V, Flags), Name, Vs, Vid). + +v(true = T, Name, Vs, {module, Mod}) -> + v(T, Name, Vs, {ok, {Mod:vendor_id(), Mod:vendor_name()}}); v(true, Name, Vs, Vid) -> - proplists:get_value(Name, Vs, Vid); + case proplists:get_value(Name, Vs) of + undefined -> + {ok, {Id, _}} = Vid, + Id; + Id -> + Id + end; v(false, _, _, _) -> undefined. @@ -656,19 +686,19 @@ empty_value(Spec) -> Imported = lists:flatmap(fun avps/1, get_value(import_enums, Spec)), Groups = get_value(grouped, Spec) ++ lists:flatmap(fun avps/1, get_value(import_groups, Spec)), - Enums = [T || {N,_} = T <- get_value(enums, Spec), + Enums = [T || {N,_} = T <- get_value(enum, Spec), not lists:keymember(N, 1, Imported)] ++ Imported, lists:map(fun c_empty_value/1, Groups ++ Enums) ++ [{?clause, [?VAR('Name')], [], [?CALL(empty, [?VAR('Name')])]}]. c_empty_value({Name, _, _, _}) -> - {?clause, [?ATOM(Name)], + {?clause, [?Atom(Name)], [], - [?CALL(empty_group, [?ATOM(Name)])]}; + [?CALL(empty_group, [?Atom(Name)])]}; c_empty_value({Name, _}) -> - {?clause, [?ATOM(Name)], + {?clause, [?Atom(Name)], [], [?TERM(<<0:32/integer>>)]}. @@ -678,7 +708,7 @@ c_empty_value({Name, _}) -> f_dict(Spec) -> {?function, dict, 0, - [{?clause, [], [], [?TERM(Spec)]}]}. + [{?clause, [], [], [?TERM([?VERSION | Spec])]}]}. %%% ------------------------------------------------------------------------ %%% # gen_hrl/3 @@ -706,10 +736,10 @@ gen_hrl(Path, Mod, Spec) -> write("ENUM Macros", Fd, - m_enums(PREFIX, false, get_value(enums, Spec))), + m_enums(PREFIX, false, get_value(enum, Spec))), write("DEFINE Macros", Fd, - m_enums(PREFIX, false, get_value(defines, Spec))), + m_enums(PREFIX, false, get_value(define, Spec))), lists:foreach(fun({M,Es}) -> write("ENUM Macros from " ++ atom_to_list(M), @@ -751,8 +781,8 @@ m_enums(Prefix, Wrap, 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))), "'"], + lists:map(fun({A,I}) -> + N = ["'", P, to_upper(z(A)), "'"], wrap(B, N, ["-define(", N, ", ", integer_to_list(I), ").\n"]) @@ -794,34 +824,34 @@ header() -> "%%\n\n"). hrl_header(Name) -> - header() ++ "-hrl_name('" ++ Name ++ ".hrl').\n". + header() ++ "-hrl_name('" ++ ?S(Name) ++ ".hrl').\n". %% avp_info/1 avp_info(Entry) -> %% {Name, Arity} case Entry of - {'<',A,'>'} -> {A, 1}; - {A} -> {A, 1}; - [A] -> {A, {0,1}}; + {{A}} -> {A, 1}; + {A} -> {A, 1}; + [A] -> {A, {0,1}}; {Q,T} -> {A,_} = avp_info(T), - {A, arity(Q)} + {A, arity(T,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. +arity({{_}}, '*' = Inf) -> {0, Inf}; +arity([_], '*' = Inf) -> {0, Inf}; +arity({_}, '*' = Inf) -> {1, Inf}; +arity(_, {_,_} = Q) -> Q. prefix(Spec) -> case orddict:find(prefix, Spec) of {ok, P} -> - atom_to_list(P) ++ "_"; + P ++ "_"; error -> "" end. rec_name(Name, Prefix) -> - list_to_atom(Prefix ++ atom_to_list(Name)). + Prefix ++ Name. diff --git a/lib/diameter/src/compiler/diameter_dict_parser.yrl b/lib/diameter/src/compiler/diameter_dict_parser.yrl new file mode 100644 index 0000000000..6fd4cedd23 --- /dev/null +++ b/lib/diameter/src/compiler/diameter_dict_parser.yrl @@ -0,0 +1,324 @@ +%% -*- erlang -*- +%% +%% %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% +%% + +%% +%% A grammar for dictionary specification. +%% + +Nonterminals + application_id avp avp_code avp_def avp_defs avp_flags avp_header + avp_header_tok avp_name avp_names avp_ref avp_spec avp_type + avp_vendor avps bit bits command_def command_id diameter_name + dictionary enum_def enum_defs group_def group_defs header header_tok + ident idents message_defs module qual section sections. + +Terminals + avp_types avp_vendor_id codecs custom_types define enum grouped + id inherits messages name prefix vendor + number word + '{' '}' '<' '>' '[' ']' '*' '::=' ':' ',' '-' + code + 'answer-message' + 'AVP' 'AVP-Header' + 'Diameter' 'Diameter-Header' 'Header' + 'REQ' 'PXY' 'ERR'. + +Rootsymbol dictionary. + +Endsymbol '$end'. + +%% =========================================================================== + +dictionary -> sections : '$1'. + +sections -> '$empty' : []. +sections -> section sections : ['$1' | '$2']. + +section -> name ident : ['$1', '$2']. +section -> prefix ident : ['$1', '$2']. +section -> id number : ['$1', '$2']. +section -> vendor number ident : ['$1', '$2', '$3']. +section -> inherits module avp_names : ['$1', '$2' | '$3']. +section -> avp_types avp_defs : ['$1' | '$2']. +section -> avp_vendor_id number avp_names : ['$1', '$2' | '$3']. +section -> custom_types module avp_names : ['$1', '$2' | '$3']. +section -> codecs module avp_names : ['$1', '$2' | '$3']. +section -> messages message_defs : ['$1' | '$2']. +section -> grouped group_defs : ['$1' | '$2']. +section -> enum ident enum_defs : ['$1', '$2' | '$3']. +section -> define ident enum_defs : ['$1', '$2' | '$3']. + +%% ===================================== + +module -> ident : '$1'. + +avp_names -> idents : '$1'. %% Note: not 'AVP' + +avp_defs -> '$empty' : []. +avp_defs -> avp_def avp_defs : ['$1' | '$2']. + +avp_def -> ident number avp_type avp_flags : ['$1', '$2', '$3', '$4']. + +avp_type -> ident : '$1'. + +idents -> '$empty' : []. +idents -> ident idents : ['$1' | '$2']. + +avp_flags -> '-' : + {_, Lineno} = '$1', + {word, Lineno, ""}. +avp_flags -> ident : + '$1'. +%% 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. + +ident -> word : '$1'. + +%% Don't bother mapping reserved words to make these usable in this +%% context. That an AVP can't be named Diameter-Header is probably no +%% great loss, and that it can't be named AVP may even save someone +%% from themselves. (Temporarily at least.) + +group_defs -> '$empty' : []. +group_defs -> group_def group_defs : ['$1' | '$2']. + +message_defs -> '$empty' : []. +message_defs -> command_def message_defs : ['$1' | '$2']. + +enum_defs -> '$empty' : []. +enum_defs -> enum_def enum_defs : ['$1' | '$2']. + +enum_def -> ident number : ['$1', '$2']. + +%% ===================================== +%% 3.2. Command Code ABNF specification +%% +%% Every Command Code defined MUST include a corresponding ABNF +%% specification, which is used to define the AVPs that MUST or MAY be +%% present when sending the message. The following format is used in +%% the definition: + +%% command-def = <command-name> "::=" diameter-message +%% +%% command-name = diameter-name +%% +%% diameter-name = ALPHA *(ALPHA / DIGIT / "-") +%% +%% diameter-message = header [ *fixed] [ *required] [ *optional] + +%% answer-message is a special case. +command_def -> 'answer-message' '::=' '<' header_tok ':' code + ',' 'ERR' '[' 'PXY' ']' '>' + avps + : ['$1', false | '$13']. + +command_def -> diameter_name '::=' header avps + : ['$1', '$3' | '$4']. +%% Ensure the order fixed/required/optional by semantic checks rather +%% than grammatically since the latter requires more lookahead: don't +%% know until after a leading qual which of the three it is that's +%% being parsed. + +diameter_name -> ident : '$1'. + +%% header = "<" "Diameter Header:" command-id +%% [r-bit] [p-bit] [e-bit] [application-id] ">" +%% +%% 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. +%% +%% application-id = 1*DIGIT + +header -> '<' header_tok ':' command_id bits application_id '>' + : ['$4', '$5', '$6']. + +command_id -> number : '$1'. + +%% Accept both the form of the base definition and the typo (fixed in +%% 3588bis) of the grammar. +header_tok -> 'Diameter' 'Header'. +header_tok -> 'Diameter-Header'. + +bits -> '$empty' : []. +bits -> ',' bit bits : ['$2' | '$3']. + +%% ERR only makes sense for answer-message so don't allow it here +%% (despite 3588). +bit -> 'REQ' : '$1'. +bit -> 'PXY' : '$1'. + +application_id -> '$empty' : false. +application_id -> number : '$1'. + +%% 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. +%% ; +%% ; NOTE: "[" and "]" have a slightly different +%% ; meaning than in ABNF (RFC 5234]). 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'. + +avps -> '$empty' : []. +avps -> avp avps : ['$1' | '$2']. + +avp -> avp_ref : [false | '$1']. +avp -> qual avp_ref : ['$1' | '$2']. + +avp_ref -> '<' avp_spec '>' : [$<, '$2']. +avp_ref -> '{' avp_name '}' : [${, '$2']. +avp_ref -> '[' avp_name ']' : [$[, '$2']. +%% Note that required can be an avp_name, not just avp_spec. 'AVP' +%% is specified as required by Failed-AVP for example. + +%% qual = [min] "*" [max] +%% ; See ABNF conventions, RFC 5234 Section 4. +%% ; 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. If an optional rule has a qualifier, +%% ; then the value of min MUST be 0 if present. +%% +%% min = 1*DIGIT +%% ; The minimum number of times the element may +%% ; be present. If absent, the default value is zero +%% ; for fixed and optional rules and one for required +%% ; rules. The value MUST be at least one for for +%% ; required rules. +%% +%% max = 1*DIGIT +%% ; The maximum number of times the element may +%% ; be present. If absent, the default value is +%% ; infinity. A value of zero implies the AVP MUST +%% ; NOT be present. + +qual -> number '*' number : {'$1', '$3'}. +qual -> number '*' : {'$1', true}. +qual -> '*' number : {true, '$2'}. +qual -> '*' : true. + +%% avp-spec = diameter-name +%% ; The avp-spec has to be an AVP Name, defined +%% ; in the base or extended Diameter +%% ; specifications. + +avp_spec -> diameter_name : '$1'. + +%% avp-name = avp-spec / "AVP" +%% ; The string "AVP" stands for *any* arbitrary AVP +%% ; Name, not otherwise listed in that command code +%% ; definition. Addition this AVP is recommended for +%% ; all command ABNFs to allow for extensibility. + +avp_name -> 'AVP' : '$1'. +avp_name -> avp_spec : '$1'. + +%% The following is a definition of a fictitious command code: +%% +%% Example-Request ::= < Diameter Header: 9999999, REQ, PXY > +%% { User-Name } +%% * { Origin-Host } +%% * [ AVP ] + +%% ===================================== +%% 4.4. Grouped AVP Values +%% +%% The Diameter protocol allows AVP values of type 'Grouped'. This +%% implies that the Data field is actually a sequence of AVPs. It is +%% possible to include an AVP with a Grouped type within a Grouped type, +%% that is, to nest them. AVPs within an AVP of type Grouped have the +%% same padding requirements as non-Grouped AVPs, as defined in Section +%% 4. +%% +%% The AVP Code numbering space of all AVPs included in a Grouped AVP is +%% the same as for non-grouped AVPs. 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. +%% +%% Every Grouped AVP defined MUST include a corresponding grammar, using +%% ABNF [RFC5234] (with modifications), as defined below. + +%% 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] + +group_def -> ident '::=' avp_header avps : ['$1', '$3' | '$4']. + +%% 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. + +avp_header -> '<' avp_header_tok ':' avp_code avp_vendor '>' + : ['$4', '$5']. + +avp_header_tok -> 'AVP-Header'. +avp_header_tok -> 'AVP' 'Header'. + +avp_code -> number : '$1'. + +avp_vendor -> '$empty' : false. +avp_vendor -> number : '$1'. diff --git a/lib/diameter/src/compiler/diameter_dict_scanner.erl b/lib/diameter/src/compiler/diameter_dict_scanner.erl new file mode 100644 index 0000000000..45189376fb --- /dev/null +++ b/lib/diameter/src/compiler/diameter_dict_scanner.erl @@ -0,0 +1,276 @@ +%% +%% %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_dict_scanner). + +%% +%% A scanner for dictionary files of the form expected by yecc. +%% + +-export([scan/1, + format_error/1]). + +-export([is_name/1]). + +%% ----------------------------------------------------------- +%% # scan/1 +%% ----------------------------------------------------------- + +-spec scan(string() | binary()) + -> {ok, [Token]} + | {error, {string(), string(), Lineno}} + when Token :: {word, Lineno, string()} + | {number, Lineno, non_neg_integer()} + | {Symbol, Lineno}, + Lineno :: pos_integer(), + Symbol :: '{' | '}' | '<' | '>' | '[' | ']' + | '*' | '::=' | ':' | ',' | '-' + | avp_types + | avp_vendor_id + | codecs + | custom_types + | define + | grouped + | id + | inherits + | messages + | name + | prefix + | vendor + | '$end' + | code + | 'answer-message' + | 'AVP' + | 'AVP-Header' + | 'Diameter' + | 'Diameter-Header' + | 'Header' + | 'REQ' + | 'PXY' + | 'ERR'. + +scan(B) + when is_binary(B) -> + scan(binary_to_list(B)); +scan(S) -> + scan(S, {1, []}). + +scan(S, {Lineno, Acc}) -> + case split(S) of + '$end' = E -> + {ok, lists:reverse([{E, Lineno} | Acc])}; + {Tok, Rest} -> + scan(Rest, acc(Tok, Lineno, Acc)); + Reason when is_list(Reason) -> + {error, {Reason, S, Lineno}} + end. + +%% format_error/1 + +format_error({Reason, Input, Lineno}) -> + io_lib:format("~s at line ~p: ~s", + [Reason, Lineno, head(Input, [], 20, true)]). + +%% is_name/1 + +is_name([H|T]) -> + is_alphanum(H) andalso lists:all(fun is_name_ch/1, T). + +%% =========================================================================== + +head(Str, Acc, N, _) + when [] == Str; + 0 == N; + $\r == hd(Str); + $\n == hd(Str) -> + lists:reverse(Acc); +head([C|Rest], Acc, N, true = T) %% skip leading whitespace + when C == $\s; + C == $\t; + C == $\f; + C == $\v -> + head(Rest, Acc, N, T); +head([C|Rest], Acc, N, _) -> + head(Rest, [C|Acc], N-1, false). + +acc(endline, Lineno, Acc) -> + {Lineno + 1, Acc}; +acc(T, Lineno, Acc) -> + {Lineno, [tok(T, Lineno) | Acc]}. + +tok({Cat, Sym}, Lineno) -> + {Cat, Lineno, Sym}; +tok(Sym, Lineno) -> + {Sym, Lineno}. + +%% # split/1 +%% +%% Output: {Token, Rest} | atom() + +%% Finito. +split("") -> + '$end'; + +%% Skip comments. This precludes using semicolon for any other purpose. +split([$;|T]) -> + split(lists:dropwhile(fun(C) -> not is_eol_ch(C) end, T)); + +%% Beginning of a section. +split([$@|T]) -> + {Name, Rest} = lists:splitwith(fun is_name_ch/1, T), + case section(Name) of + false -> + "Unknown section"; + 'end' -> + '$end'; + A -> + {A, Rest} + end; + +split("::=" ++ T) -> + {'::=', T}; + +split([H|T]) + when H == ${; H == $}; + H == $<; H == $>; + H == $[; H == $]; + H == $*; H == $:; H == $,; H == $- -> + {list_to_atom([H]), T}; + +%% RFC 3588 requires various names to begin with a letter but 3GPP (for +%% one) abuses this. (eg 3GPP-Charging-Id in TS32.299.) +split([H|_] = L) when $0 =< H, H =< $9 -> + {P, Rest} = splitwith(fun is_name_ch/1, L), + Tok = try + {number, read_int(P)} + catch + error:_ -> + word(P) + end, + {Tok, Rest}; + +split([H|_] = L) when $a =< H, H =< $z; + $A =< H, H =< $Z -> + {P, Rest} = splitwith(fun is_name_ch/1, L), + {word(P), Rest}; + +split([$'|T]) -> + case lists:splitwith(fun(C) -> not lists:member(C, "'\r\n") end, T) of + {[_|_] = A, [$'|Rest]} -> + {{word, A}, Rest}; + {[], [$'|_]} -> + "Empty string"; + _ -> %% not terminated on same line + "Unterminated string" + end; + +%% Line ending of various forms. +split([$\r,$\n|T]) -> + {endline, T}; +split([C|T]) + when C == $\r; + C == $\n -> + {endline, T}; + +%% Ignore whitespace. +split([C|T]) + when C == $\s; + C == $\t; + C == $\f; + C == $\v -> + split(T); + +split(_) -> + "Unexpected character". + +%% word/1 + +%% Reserved words significant in parsing ... +word(S) + when S == "answer-message"; + S == "code"; + S == "AVP"; + S == "AVP-Header"; + S == "Diameter"; + S == "Diameter-Header"; + S == "Header"; + S == "REQ"; + S == "PXY"; + S == "ERR" -> + list_to_atom(S); + +%% ... or not. +word(S) -> + {word, S}. + +%% section/1 + +section(N) + when N == "avp_types"; + N == "avp_vendor_id"; + N == "codecs"; + N == "custom_types"; + N == "define"; + N == "end"; + N == "enum"; + N == "grouped"; + N == "id"; + N == "inherits"; + N == "messages"; + N == "name"; + N == "prefix"; + N == "vendor" -> + list_to_atom(N); +section(_) -> + false. + +%% 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, [H|T]) -> + {SH, ST} = lists:splitwith(Fun, T), + {[H|SH], ST}. + +is_eol_ch(C) -> + C == $\n orelse C == $\r. + +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_dict_util.erl b/lib/diameter/src/compiler/diameter_dict_util.erl new file mode 100644 index 0000000000..e4cd29ab7f --- /dev/null +++ b/lib/diameter/src/compiler/diameter_dict_util.erl @@ -0,0 +1,1343 @@ +%% +%% %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 dictionary 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_dict_util). + +-export([parse/2, + format_error/1, + format/1]). + +-include("diameter_vsn.hrl"). + +-define(RETURN(T), throw({T, ?MODULE, ?LINE})). +-define(RETURN(T, Args), ?RETURN({T, Args})). + +-define(A, list_to_atom). +-define(L, atom_to_list). +-define(I, integer_to_list). +-define(F, io_lib:format). + +%% =========================================================================== +%% parse/2 +%% =========================================================================== + +-spec parse(File, Opts) + -> {ok, orddict:orddict()} + | {error, term()} + when File :: {path, string()} + | iolist() + | binary(), + Opts :: list(). + +parse(File, Opts) -> + putr(verbose, lists:member(verbose, Opts)), + try + {ok, do_parse(File, Opts)} + catch + {Reason, ?MODULE, _Line} -> + {error, Reason} + after + eraser(verbose) + end. + +%% =========================================================================== +%% format_error/1 +%% =========================================================================== + +format_error({read, Reason}) -> + file:format_error(Reason); +format_error({scan, Reason}) -> + diameter_dict_scanner:format_error(Reason); +format_error({parse, {Line, _Mod, Reason}}) -> + lists:flatten(["Line ", ?I(Line), ", ", Reason]); + +format_error(T) -> + {Fmt, As} = fmt(T), + lists:flatten(io_lib:format(Fmt, As)). + +fmt({avp_code_already_defined = E, [Code, false, Name, Line, L]}) -> + {fmt(E), [Code, "", Name, Line, L]}; +fmt({avp_code_already_defined = E, [Code, Vid, Name, Line, L]}) -> + {fmt(E), [Code, ?F("/~p", [Vid]), Name, Line, L]}; + +fmt({uint32_out_of_range = E, [id | T]}) -> + {fmt(E), ["@id", "application identifier" | T]}; +fmt({uint32_out_of_range = E, [K | T]}) + when K == vendor; + K == avp_vendor_id -> + {fmt(E), [?F("@~p", [K]), "vendor id" | T]}; +fmt({uint32_out_of_range = E, [K, Name | T]}) + when K == enum; + K == define -> + {fmt(E), [?F("@~p ~s", [K, Name]), "value" | T]}; +fmt({uint32_out_of_range = E, [avp_types, Name | T]}) -> + {fmt(E), ["AVP " ++ Name, "AVP code" | T]}; +fmt({uint32_out_of_range = E, [grouped, Name | T]}) -> + {fmt(E), ["Grouped AVP " ++ Name | T]}; +fmt({uint32_out_of_range = E, [messages, Name | T]}) -> + {fmt(E), ["Message " ++ Name, "command code" | T]}; + +fmt({Reason, As}) -> + {fmt(Reason), As}; + +fmt(avp_code_already_defined) -> + "AVP ~p~s (~s) at line ~p already defined at line ~p"; + +fmt(uint32_out_of_range) -> + "~s specifies ~s ~p at line ~p that is out of range for a value of " + "Diameter type Unsigned32"; + +fmt(imported_avp_already_defined) -> + "AVP ~s imported by @inherits ~p at line ~p defined at line ~p"; +fmt(duplicate_import) -> + "AVP ~s is imported by more than one @inherits, both at line ~p " + "and at line ~p"; + +fmt(duplicate_section) -> + "Section @~s at line ~p already declared at line ~p"; + +fmt(already_declared) -> + "Section @~p ~s at line ~p already declared at line ~p"; + +fmt(inherited_avp_already_defined) -> + "AVP ~s inherited at line ~p defined in @avp_types at line ~p"; +fmt(avp_already_defined) -> + "AVP ~s at line ~p already in @~p at line ~p"; +fmt(key_already_defined) -> + "Value for ~s:~s in @~p at line ~p already provided at line ~p"; + +fmt(messages_without_id) -> + "@messages at line ~p but @id not declared"; + +fmt(avp_name_already_defined) -> + "AVP ~s at line ~p already defined at line ~p"; +fmt(avp_has_unknown_type) -> + "AVP ~s at line ~p defined with unknown type ~s"; +fmt(avp_has_invalid_flag) -> + "AVP ~s at line ~p specifies invalid flag ~c"; +fmt(avp_has_duplicate_flag) -> + "AVP ~s at line ~p specifies duplicate flag ~c"; +fmt(avp_has_vendor_id) -> + "AVP ~s at line ~p does not specify V flag " + "but is assigned vendor id ~p at line ~p"; +fmt(avp_has_no_vendor) -> + "AVP ~s at line ~p specifies V flag " + "but neither @vendor_avp_id nor @vendor supplies a value"; + +fmt(group_already_defined) -> + "Group ~s at line ~p already defined at line ~p"; +fmt(grouped_avp_code_mismatch) -> + "AVP ~s at line ~p has with code ~p " + "but @avp_types specifies ~p at line ~p"; +fmt(grouped_avp_has_wrong_type) -> + "Grouped AVP ~s at line ~p defined with type ~s at line ~p"; +fmt(grouped_avp_not_defined) -> + "Grouped AVP ~s on line ~p not defined in @avp_types"; +fmt(grouped_vendor_id_without_flag) -> + "Grouped AVP ~s at line ~p has vendor id " + "but definition at line ~p does not specify V flag"; +fmt(grouped_vendor_id_mismatch) -> + "Grouped AVP ~s at line ~p has vendor id ~p " + "but ~p specified at line ~p"; + +fmt(message_name_already_defined) -> + "Message ~s at line ~p already defined at line ~p"; +fmt(message_code_already_defined) -> + "~s message with code ~p at line ~p already defined at line ~p"; +fmt(message_has_duplicate_flag) -> + "Message ~s has duplicate flag ~s at line ~p"; +fmt(message_application_id_mismatch) -> + "Message ~s has application id ~p at line ~p " + "but @id specifies ~p at line ~p"; + +fmt(invalid_avp_order) -> + "AVP reference ~c~s~c at line ~p breaks fixed/required/optional order"; +fmt(required_avp_has_zero_max_arity) -> + "Required AVP has maximum arity 0 at line ~p"; +fmt(required_avp_has_zero_min_arity) -> + "Required AVP has minimum arity 0 at line ~p"; +fmt(optional_avp_has_nonzero_min_arity) -> + "Optional AVP has non-zero minimum arity at line ~p"; +fmt(qualifier_has_min_greater_than_max) -> + "Qualifier ~p*~p at line ~p has Min > Max"; +fmt(avp_already_referenced) -> + "AVP ~s at line ~p already referenced at line ~p"; + +fmt(message_missing) -> + "~s message at line ~p but no ~s message is defined"; + +fmt(requested_avp_not_found) -> + "@inherit ~s at line ~p requests AVP ~s at line ~p " + "but module does not define that AVP"; + +fmt(enumerated_avp_has_wrong_local_type) -> + "Enumerated AVP ~s in @enum at line ~p defined with type ~s at line ~p"; +fmt(enumerated_avp_has_wrong_inherited_type) -> + "Enumerated AVP ~s in @enum at line ~p " + "inherited with type ~s from module ~s at line ~p"; +fmt(enumerated_avp_not_defined) -> + "Enumerated AVP ~s in @enum at line ~p neither defined nor inherited"; + +fmt(avp_not_defined) -> + "AVP ~s referenced at line ~p neither defined nor inherited"; + +fmt(recompile) -> + "Module ~p appears to have been compiler with an incompatible " + "version of the dictionary compiler and must be recompiled"; +fmt(not_loaded) -> + "Module ~p is not on the code path or could not be loaded"; +fmt(no_dict) -> + "Module ~p does not appear to be a diameter dictionary". + +%% =========================================================================== +%% format/1 +%% +%% Turn dict/0 output back into a dictionary file (with line ending = $\n). + +-spec format(Dict) + -> iolist() + when Dict :: orddict:orddict(). + +-define(KEYS, [id, name, prefix, vendor, + inherits, codecs, custom_types, + avp_types, + messages, + grouped, + enum, define]). + +format(Dict) -> + Io = orddict:fold(fun io/3, [], Dict), + [S || {_,S} <- lists:sort(fun keysort/2, Io)]. + +keysort({A,_}, {B,_}) -> + [HA, HB] = [H || K <- [A,B], + H <- [lists:takewhile(fun(X) -> X /= K end, ?KEYS)]], + HA < HB. + +%% =========================================================================== + +-define(INDENT, " "). +-define(SP, " "). +-define(NL, $\n). + +%% io/3 + +io(K, _, Acc) + when K == command_codes; + K == import_avps; + K == import_groups; + K == import_enums -> + Acc; + +io(Key, Body, Acc) -> + [{Key, io(Key, Body)} | Acc]. + +%% io/2 + +io(K, Id) + when K == id; + K == name; + K == prefix -> + [?NL, section(K), ?SP, tok(Id)]; + +io(vendor = K, {Id, Name}) -> + [?NL, section(K) | [[?SP, tok(X)] || X <- [Id, Name]]]; + +io(avp_types = K, Body) -> + [?NL, ?NL, section(K), ?NL, [body(K,A) || A <- Body]]; + +io(K, Body) + when K == messages; + K == grouped -> + [?NL, ?NL, section(K), [body(K,A) || A <- Body]]; + +io(K, Body) + when K == avp_vendor_id; + K == inherits; + K == custom_types; + K == codecs; + K == enum; + K == define -> + [[?NL, pairs(K, T)] || T <- Body]. + +pairs(K, {Id, Avps}) -> + [?NL, section(K), ?SP, tok(Id), ?NL, [[?NL, body(K, A)] || A <- Avps]]. + +body(K, AvpName) + when K == avp_vendor_id; + K == inherits; + K == custom_types; + K == codecs -> + [?INDENT, word(AvpName)]; + +body(K, {Name, N}) + when K == enum; + K == define -> + [?INDENT, word(Name), ?SP, ?I(N)]; + +body(avp_types = K, {Name, Code, Type, ""}) -> + body(K, {Name, Code, Type, "-"}); +body(avp_types, {Name, Code, Type, Flags}) -> + [?NL, ?INDENT, word(Name), + [[?SP, ?SP, S] || S <- [?I(Code), Type, Flags]]]; + +body(messages, {"answer-message", _, _, [], Avps}) -> + [?NL, ?NL, ?INDENT, + "answer-message ::= < Diameter Header: code, ERR [PXY] >", + f_avps(Avps)]; +body(messages, {Name, Code, Flags, ApplId, Avps}) -> + [?NL, ?NL, ?INDENT, word(Name), " ::= ", header(Code, Flags, ApplId), + f_avps(Avps)]; + +body(grouped, {Name, Code, Vid, Avps}) -> + [?NL, ?NL, ?INDENT, word(Name), " ::= ", avp_header(Code, Vid), + f_avps(Avps)]. + +header(Code, Flags, ApplId) -> + ["< Diameter Header: ", + ?I(Code), + [[", ", ?L(F)] || F <- Flags], + [[" ", ?I(N)] || N <- ApplId], + " >"]. + +avp_header(Code, Vid) -> + ["< AVP Header: ", + ?I(Code), + [[" ", ?I(V)] || V <- Vid], + " >"]. + +f_avps(L) -> + [[?NL, ?INDENT, ?INDENT, f_avp(A)] || A <- L]. + +f_avp({Q, A}) -> + [D | _] = Avp = f_delim(A), + f_avp(f_qual(D, Q), Avp); +f_avp(A) -> + f_avp("", f_delim(A)). + +f_delim({{A}}) -> + [$<, word(A), $>]; +f_delim({A}) -> + [${, word(A), $}]; +f_delim([A]) -> + [$[, word(A), $]]. + +f_avp(Q, [L, Avp, R]) -> + Len = length(lists:flatten([Q])), + [io_lib:format("~*s", [-1*max(Len+1, 6) , Q]), L, " ", Avp, " ", R]. + +f_qual(${, '*') -> + "1*"; %% Equivalent to "*" but the more common/obvious rendition +f_qual(_, '*') -> + "*"; +f_qual(_, {'*', N}) -> + [$*, ?I(N)]; +f_qual(_, {N, '*'}) -> + [?I(N), $*]; +f_qual(_, {M,N}) -> + [?I(M), $*, ?I(N)]. + +section(Key) -> + ["@", ?L(Key)]. + +tok(N) + when is_integer(N) -> + ?I(N); +tok(N) -> + word(N). + +word(Str) -> + word(diameter_dict_scanner:is_name(Str), Str). + +word(true, Str) -> + Str; +word(false, Str) -> + [$', Str, $']. + +%% =========================================================================== + +do_parse(File, Opts) -> + Bin = do([fun read/1, File], read), + Toks = do([fun diameter_dict_scanner:scan/1, Bin], scan), + Tree = do([fun diameter_dict_parser:parse/1, Toks], parse), + make_dict(Tree, Opts). + +do([F|A], E) -> + case apply(F,A) of + {ok, T} -> + T; + {error, Reason} -> + ?RETURN({E, Reason}) + end. + +read({path, Path}) -> + file:read_file(Path); +read(File) -> + {ok, iolist_to_binary([File])}. + +make_dict(Parse, Opts) -> + make_orddict(pass4(pass3(pass2(pass1(reset(make_dict(Parse), + Opts))), + Opts))). + +%% make_orddict/1 + +make_orddict(Dict) -> + dict:fold(fun mo/3, + orddict:from_list([{K,[]} || K <- [avp_types, + messages, + grouped, + inherits, + custom_types, + codecs, + avp_vendor_id, + enum, + define]]), + Dict). + +mo(K, Sects, Dict) + when is_atom(K) -> + orddict:store(K, make(K, Sects), Dict); + +mo(_, _, Dict) -> + Dict. + +make(K, [[_Line, {_, _, X}]]) + when K == id; + K == name; + K == prefix -> + X; + +make(vendor, [[_Line, {_, _, Id}, {_, _, Name}]]) -> + {Id, Name}; + +make(K, T) + when K == command_codes; + K == import_avps; + K == import_groups; + K == import_enums -> + T; + +make(K, Sects) -> + post(K, foldl(fun([_L|B], A) -> make(K,B,A) end, + [], + Sects)). + +post(avp_types, L) -> + lists:sort(L); + +post(K, L) + when K == grouped; + K == messages; + K == enum; + K == define -> + lists:reverse(L); + +post(_, L) -> + L. + +make(K, [{_,_,Name} | Body], Acc) + when K == enum; + K == define; + K == avp_vendor_id; + K == custom_types; + K == inherits; + K == codecs -> + [{Name, mk(K, Body)} | Acc]; + +make(K, Body, Acc) -> + foldl(fun(T,A) -> [mk(K, T) | A] end, Acc, Body). + +mk(avp_types, [{_,_,Name}, {_,_,Code}, {_,_,Type}, {_,_,Flags}]) -> + {Name, Code, type(Type), Flags}; + +mk(messages, [{'answer-message' = A, _}, false | Avps]) -> + {?L(A), -1, ['ERR', 'PXY'], [], make_body(Avps)}; + +mk(messages, [{_,_,Name}, [{_,_,Code}, Flags, ApplId] | Avps]) -> + {Name, + Code, + lists:map(fun({F,_}) -> F end, Flags), + opt(ApplId), + make_body(Avps)}; + +mk(grouped, [{_,_,Name}, [{_,_,Code}, Vid] | Avps]) -> + {Name, Code, opt(Vid), make_body(Avps)}; + +mk(K, Body) + when K == enum; + K == define -> + lists:map(fun([{_,_,Name}, {_,_,Value}]) -> {Name, Value} end, Body); + +mk(K, Avps) + when K == avp_vendor_id; + K == custom_types; + K == inherits; + K == codecs -> + lists:map(fun({_,_,N}) -> N end, Avps). + +opt(false) -> + []; +opt({_,_,X}) -> + [X]. + +make_body(Avps) -> + lists:map(fun avp/1, Avps). + +avp([false, D, Avp]) -> + avp(D, Avp); +avp([Q, D, Avp]) -> + case {qual(D, Q), avp(D, Avp)} of + {{0,1}, A} when D == $[ -> + A; + {{1,1}, A} -> + A; + T -> + T + end. +%% Could just store the qualifier as a pair in all cases but the more +%% compact form is easier to parse visually so live with a bit of +%% mapping. Ditto the use of '*'. + +avp(D, {'AVP', _}) -> + delim(D, "AVP"); +avp(D, {_, _, Name}) -> + delim(D, Name). + +delim($<, N) -> + {{N}}; +delim(${, N) -> + {N}; +delim($[, N) -> + [N]. + +%% There's a difference between max = 0 and not specifying an AVP: +%% reception of an AVP with max = 0 will always be an error, otherwise +%% it depends on the existence of 'AVP' and the M flag. + +qual(${, {{_,L,0}, _}) -> + ?RETURN(required_avp_has_zero_min_arity, [L]); +qual(${, {_, {_,L,0}}) -> + ?RETURN(required_avp_has_zero_max_arity, [L]); + +qual($[, {{_,L,N}, _}) + when 0 < N -> + ?RETURN(optional_avp_has_nonzero_min_arity, [L]); + +qual(_, {{_,L,Min}, {_,_,Max}}) + when Min > Max -> + ?RETURN(qualifier_has_min_greater_than_max, [Min, Max, L]); + +qual(_, true) -> + '*'; + +qual(${, {true, {_,_,N}}) -> + {1, N}; +qual(_, {true, {_,_,N}}) -> + {0, N}; + +qual(D, {{_,_,N}, true}) + when D == ${, N == 1; + D /= ${, N == 0 -> + '*'; +qual(_, {{_,_,N}, true}) -> + {N, '*'}; + +qual(_, {{_,_,Min}, {_,_,Max}}) -> + {Min, Max}. + +%% Optional reports when running verbosely. +report(What, [F | A]) + when is_function(F) -> + report(What, apply(F, A)); +report(What, Data) -> + report(getr(verbose), What, Data). + +report(true, Tag, Data) -> + io:format("##~n## ~p ~p~n", [Tag, Data]); +report(false, _, _) -> + ok. + +%% ------------------------------------------------------------------------ +%% make_dict/1 +%% +%% Turn a parsed dictionary into an dict. + +make_dict(Parse) -> + foldl(fun(T,A) -> + report(section, T), + section(T,A) + end, + dict:new(), + Parse). + +section([{T, L} | Rest], Dict) + when T == name; + T == prefix; + T == id; + T == vendor -> + case find(T, Dict) of + [] -> + dict:store(T, [[L | Rest]], Dict); + [[Line | _]] -> + ?RETURN(duplicate_section, [T, L, Line]) + end; + +section([{T, L} | Rest], Dict) + when T == avp_types; + T == messages; + T == grouped; + T == inherits; + T == custom_types; + T == codecs; + T == avp_vendor_id; + T == enum; + T == define -> + dict:append(T, [L | Rest], Dict). + +%% =========================================================================== +%% reset/2 +%% +%% Reset sections from options. + +reset(Dict, Opts) -> + foldl([fun reset/3, Opts], Dict, [name, prefix, inherits]). + +reset(K, Dict, Opts) -> + foldl(fun opt/2, Dict, [T || {A,_} = T <- Opts, A == K]). + +opt({inherits = Key, "-"}, Dict) -> + dict:erase(Key, Dict); +opt({inherits = Key, Mod}, Dict) -> + dict:append(Key, [0, {word, 0, Mod}], Dict); +opt({Key, Val}, Dict) -> + dict:store(Key, [0, {word, 0, Val}], Dict); +opt(_, Dict) -> + Dict. + +%% =========================================================================== +%% pass1/1 +%% +%% Explode sections into additional dictionary entries plus semantic +%% checks. + +pass1(Dict) -> + true = no_messages_without_id(Dict), + + foldl(fun(K,D) -> foldl([fun p1/3, K], D, find(K,D)) end, + Dict, + [id, + vendor, + avp_types, %% must precede inherits, grouped, enum + avp_vendor_id, + custom_types, + codecs, + inherits, + grouped, + messages, + enum, + define]). + +%% Multiple sections are allowed as long as their bodies don't +%% overlap. (Except enum/define.) + +p1([_Line, N], Dict, id = K) -> + true = is_uint32(N, [K]), + Dict; + +p1([_Line, Id, _Name], Dict, vendor = K) -> + true = is_uint32(Id, [K]), + Dict; + +p1([_Line, X | Body], Dict, K) + when K == avp_vendor_id; + K == custom_types; + K == codecs; + K == inherits -> + foldl([fun explode/4, X, K], Dict, Body); + +p1([_Line, X | Body], Dict, K) + when K == define; + K == enum -> + {_, L, Name} = X, + foldl([fun explode2/4, X, K], + store_new({K, Name}, + [L, Body], + Dict, + [K, Name, L], + already_declared), + Body); + +p1([_Line | Body], Dict, K) + when K == avp_types; + K == grouped; + K == messages -> + foldl([fun explode/3, K], Dict, Body). + +no_messages_without_id(Dict) -> + case find(messages, Dict) of + [] -> + true; + [[Line | _] | _] -> + [] /= find(id, Dict) orelse ?RETURN(messages_without_id, [Line]) + end. + +%% Note that the AVP's in avp_vendor_id, custom_types, codecs and +%% enum can all be inherited, as can the AVP content of messages and +%% grouped AVP's. Check that the referenced AVP's exist after +%% importing definitions. + +%% explode/4 +%% +%% {avp_vendor_id, AvpName} -> [Lineno, Id::integer()] +%% {custom_types|codecs|inherits, AvpName} -> [Lineno, Mod::string()] + +explode({_, Line, AvpName}, Dict, {_, _, X} = T, K) -> + true = K /= avp_vendor_id orelse is_uint32(T, [K]), + true = K /= inherits orelse avp_not_local(AvpName, Line, Dict), + + store_new({key(K), AvpName}, + [Line, X], + Dict, + [AvpName, Line, K], + avp_already_defined). + +%% explode2/4 + +%% {define, {Name, Key}} -> [Lineno, Value::integer(), enum|define] + +explode2([{_, Line, Key}, {_, _, Value} = T], Dict, {_, _, Name}, K) -> + true = is_uint32(T, [K, Name]), + + store_new({key(K), {Name, Key}}, + [Line, Value, K], + Dict, + [Name, Key, K, Line], + key_already_defined). + +%% key/1 +%% +%% Conflate keys that are equivalent as far as uniqueness of +%% definition goes. + +key(K) + when K == enum; + K == define -> + define; +key(K) + when K == custom_types; + K == codecs -> + custom; +key(K) -> + K. + +%% explode/3 + +%% {avp_types, AvpName} -> [Line | Toks] +%% {avp_types, {Code, IsReq}} -> [Line, AvpName] +%% +%% where AvpName = string() +%% Code = integer() +%% IsReq = boolean() + +explode([{_, Line, Name} | Toks], Dict0, avp_types = K) -> + %% Each AVP can be defined only once. + Dict = store_new({K, Name}, + [Line | Toks], + Dict0, + [Name, Line], + avp_name_already_defined), + + [{number, _, _Code} = C, {word, _, Type}, {word, _, _Flags}] = Toks, + + true = avp_type_known(Type, Name, Line), + true = is_uint32(C, [K, Name]), + + Dict; + +%% {grouped, Name} -> [Line, HeaderTok | AvpToks] +%% {grouped, {Name, AvpName}} -> [Line, Qual, Delim] +%% +%% where Name = string() +%% AvpName = string() +%% Qual = {Q, Q} | boolean() +%% Q = true | NumberTok +%% Delim = $< | ${ | $[ + +explode([{_, Line, Name}, Header | Avps], Dict0, grouped = K) -> + Dict = store_new({K, Name}, + [Line, Header | Avps], + Dict0, + [Name, Line], + group_already_defined), + + [{_,_, Code} = C, Vid] = Header, + {DefLine, {_, _, Flags}} = grouped_flags(Name, Code, Dict0, Line), + V = lists:member($V, Flags), + + true = is_uint32(C, [K, Name, "AVP code"]), + true = is_uint32(Vid, [K, Name, "vendor id"]), + false = vendor_id_mismatch(Vid, V, Name, Dict0, Line, DefLine), + + explode_avps(Avps, Dict, K, Name); + +%% {messages, Name} -> [Line, HeaderTok | AvpToks] +%% {messages, {Code, IsReq}} -> [Line, NameTok] +%% {messages, Code} -> [[Line, NameTok, IsReq]] +%% {messages, {Name, Flag}} -> [Line] +%% {messages, {Name, AvpName}} -> [Line, Qual, Delim] +%% +%% where Name = string() +%% Code = integer() +%% IsReq = boolean() +%% Flag = 'REQ' | 'PXY' +%% AvpName = string() +%% Qual = true | {Q,Q} +%% Q = true | NumberTok +%% Delim = $< | ${ | ${ + +explode([{'answer-message' = A, Line}, false = H | Avps], + Dict0, + messages = K) -> + Name = ?L(A), + Dict1 = store_new({K, Name}, + [Line, H, Avps], + Dict0, + [Name, Line], + message_name_already_defined), + + explode_avps(Avps, Dict1, K, Name); + +explode([{_, Line, MsgName} = M, Header | Avps], + Dict0, + messages = K) -> + %% There can be at most one message with a given name. + Dict1 = store_new({K, MsgName}, + [Line, Header | Avps], + Dict0, + [MsgName, Line], + message_name_already_defined), + + [{_, _, Code} = C, Bits, ApplId] = Header, + + %% Don't check any application id since it's required to be + %% the same as @id. + true = is_uint32(C, [K, MsgName]), + + %% An application id specified as part of the message definition + %% has to agree with @id. The former is parsed just because RFC + %% 3588 specifies it. + false = application_id_mismatch(ApplId, Dict1, MsgName), + + IsReq = lists:keymember('REQ', 1, Bits), + + %% For each command code, there can be at most one request and + %% one answer. + Dict2 = store_new({K, {Code, IsReq}}, + [Line, M], + Dict1, + [choose(IsReq, "Request", "Answer"), Code, Line], + message_code_already_defined), + + %% For each message, each flag can occur at most once. + Dict3 = foldl(fun({F,L},D) -> + store_new({K, {MsgName, F}}, + [L], + D, + [MsgName, ?L(F)], + message_has_duplicate_flag) + end, + Dict2, + Bits), + + dict:append({K, Code}, + [Line, M, IsReq], + explode_avps(Avps, Dict3, K, MsgName)). + +%% explode_avps/4 +%% +%% Ensure required AVP order and sane qualifiers. Can't check for AVP +%% names until after they've been imported. +%% +%% RFC 3588 allows a trailing fixed while 3588bis doesn't. Parse the +%% former. + +explode_avps(Avps, Dict, Key, Name) -> + xa("<{[<", Avps, Dict, Key, Name). + +xa(_, [], Dict, _, _) -> + Dict; + +xa(Ds, [[Qual, D, {'AVP', Line}] | Avps], Dict, Key, Name) -> + xa(Ds, [[Qual, D, {word, Line, "AVP"}] | Avps], Dict, Key, Name); + +xa([], [[_Qual, D, {_, Line, Name}] | _], _, _, _) -> + ?RETURN(invalid_avp_order, [D, Name, close(D), Line]); + +xa([D|_] = Ds, [[Qual, D, {_, Line, AvpName}] | Avps], Dict, Key, Name) -> + xa(Ds, + Avps, + store_new({Key, {Name, AvpName}}, + [Line, Qual, D], + Dict, + [Name, Line], + avp_already_referenced), + Key, + Name); + +xa([_|Ds], Avps, Dict, Key, Name) -> + xa(Ds, Avps, Dict, Key, Name). + +close($<) -> $>; +close(${) -> $}; +close($[) -> $]. + +%% is_uint32/2 + +is_uint32(false, _) -> + true; +is_uint32({Line, _, N}, Args) -> + N < 1 bsl 32 orelse ?RETURN(uint32_out_of_range, Args ++ [N, Line]). +%% Can't call diameter_types here since it may not exist yet. + +%% application_id_mismatch/3 + +application_id_mismatch({number, Line, Id}, Dict, MsgName) -> + [[_, {_, L, I}]] = dict:fetch(id, Dict), + + I /= Id andalso ?RETURN(message_application_id_mismatch, + [MsgName, Id, Line, I, L]); + +application_id_mismatch(false = No, _, _) -> + No. + +%% avp_not_local/3 + +avp_not_local(Name, Line, Dict) -> + A = find({avp_types, Name}, Dict), + + [] == A orelse ?RETURN(inherited_avp_already_defined, + [Name, Line, hd(A)]). + +%% avp_type_known/3 + +avp_type_known(Type, Name, Line) -> + false /= type(Type) + orelse ?RETURN(avp_has_unknown_type, [Name, Line, Type]). + +%% vendor_id_mismatch/6 +%% +%% Require a vendor id specified on a group to match any specified +%% in @avp_vendor_id. Note that both locations for the value are +%% equivalent, both in the value being attributed to a locally +%% defined AVP and ignored when imported from another dictionary. + +vendor_id_mismatch({_,_,_}, false, Name, _, Line, DefLine) -> + ?RETURN(grouped_vendor_id_without_flag, [Name, Line, DefLine]); + +vendor_id_mismatch({_, _, I}, true, Name, Dict, Line, _) -> + case vendor_id(Name, Dict) of + {avp_vendor_id, L, N} -> + I /= N andalso + ?RETURN(grouped_vendor_id_mismatch, [Name, Line, I, N, L]); + _ -> + false + end; + +vendor_id_mismatch(_, _, _, _, _, _) -> + false. + +%% grouped_flags/4 + +grouped_flags(Name, Code, Dict, Line) -> + case find({avp_types, Name}, Dict) of + [L, {_, _, Code}, {_, _, "Grouped"}, Flags] -> + {L, Flags}; + [_, {_, L, C}, {_, _, "Grouped"}, _Flags] -> + ?RETURN(grouped_avp_code_mismatch, [Name, Line, Code, C, L]); + [_, _Code, {_, L, T}, _] -> + ?RETURN(grouped_avp_has_wrong_type, [Name, Line, T, L]); + [] -> + ?RETURN(grouped_avp_not_defined, [Name, Line]) + end. + +%% vendor_id/2 + +%% Look for a vendor id in @avp_vendor_id, then @vendor. +vendor_id(Name, Dict) -> + case find({avp_vendor_id, Name}, Dict) of + [Line, Id] when is_integer(Id) -> + {avp_vendor_id, Line, Id}; + [] -> + vendor(Dict) + end. + +vendor(Dict) -> + case find(vendor, Dict) of + [[_Line, {_, _, Id}, {_, _, _}]] -> + {vendor, Id}; + [] -> + false + end. + +%% find/2 + +find(Key, Dict) -> + case dict:find(Key, Dict) of + {ok, L} when is_list(L) -> + L; + error -> + [] + end. + +%% store_new/5 + +store_new(Key, Value, Dict, Args, Err) -> + case dict:find(Key, Dict) of + {ok, [L | _]} -> + ?RETURN(Err, Args ++ [L]); + error -> + dict:store(Key, Value, Dict) + end. + +%% type/1 + +type("DiamIdent") -> + "DiameterIdentity"; +type("DiamURI") -> + "DiameterURI"; +type(T) + when T == "OctetString"; + T == "Integer32"; + T == "Integer64"; + T == "Unsigned32"; + T == "Unsigned64"; + T == "Float32"; + T == "Float64"; + T == "Grouped"; + T == "Enumerated"; + T == "Address"; + T == "Time"; + T == "UTF8String"; + T == "DiameterIdentity"; + T == "DiameterURI"; + T == "IPFilterRule"; + T == "QoSFilterRule" -> + T; +type(_) -> + false. + +%% =========================================================================== +%% pass2/1 +%% +%% More explosion, but that requires the previous pass to write its +%% entries. + +pass2(Dict) -> + foldl(fun(K,D) -> foldl([fun p2/3, K], D, find(K,D)) end, + Dict, + [avp_types]). + +p2([_Line | Body], Dict, avp_types) -> + foldl(fun explode_avps/2, Dict, Body); + +p2([], Dict, _) -> + Dict. + +explode_avps([{_, Line, Name} | Toks], Dict) -> + [{number, _, Code}, {word, _, _Type}, {word, _, Flags}] = Toks, + + true = avp_flags_valid(Flags, Name, Line), + + Vid = avp_vendor_id(Flags, Name, Line, Dict), + + %% An AVP is uniquely defined by its AVP code and vendor id (if any). + %% Ensure there are no duplicate. + store_new({avp_types, {Code, Vid}}, + [Line, Name], + Dict, + [Code, Vid, Name, Line], + avp_code_already_defined). + +%% avp_flags_valid/3 + +avp_flags_valid(Flags, Name, Line) -> + Bad = lists:filter(fun(C) -> not lists:member(C, "MVP") end, Flags), + [] == Bad + orelse ?RETURN(avp_has_invalid_flag, [Name, Line, hd(Bad)]), + + Dup = Flags -- "MVP", + [] == Dup + orelse ?RETURN(avp_has_duplicate_flag, [Name, Line, hd(Dup)]). + +%% avp_vendor_id/4 + +avp_vendor_id(Flags, Name, Line, Dict) -> + V = lists:member($V, Flags), + + case vendor_id(Name, Dict) of + {avp_vendor_id, _, I} when V -> + I; + {avp_vendor_id, L, I} -> + ?RETURN(avp_has_vendor_id, [Name, Line, I, L]); + {vendor, I} when V -> + I; + false when V -> + ?RETURN(avp_has_no_vendor, [Name, Line]); + _ -> + false + end. + +%% =========================================================================== +%% pass3/2 +%% +%% Import AVPs. + +pass3(Dict, Opts) -> + import_enums(import_groups(import_avps(insert_codes(Dict), Opts))). + +%% insert_codes/1 +%% +%% command_codes -> [{Code, ReqNameTok, AnsNameTok}] + +insert_codes(Dict) -> + dict:store(command_codes, + dict:fold(fun make_code/3, [], Dict), + Dict). + +make_code({messages, Code}, Names, Acc) + when is_integer(Code) -> + [mk_code(Code, Names) | Acc]; +make_code(_, _, Acc) -> + Acc. + +mk_code(Code, [[_, _, false] = Ans, [_, _, true] = Req]) -> + mk_code(Code, [Req, Ans]); + +mk_code(Code, [[_, {_,_,Req}, true], [_, {_,_,Ans}, false]]) -> + {Code, Req, Ans}; + +mk_code(_Code, [[Line, _Name, IsReq]]) -> + ?RETURN(message_missing, [choose(IsReq, "Request", "Answer"), + Line, + choose(IsReq, "answer", "request")]). + +%% import_avps/2 + +import_avps(Dict, Opts) -> + Import = inherit(Dict, Opts), + report(imported, Import), + + %% pass4/1 tests that all referenced AVP's are either defined + %% or imported. + + dict:store(import_avps, + lists:map(fun({M, _, As}) -> {M, [A || {_,A} <- As]} end, + lists:reverse(Import)), + foldl(fun explode_imports/2, Dict, Import)). + +explode_imports({Mod, Line, Avps}, Dict) -> + foldl([fun xi/4, Mod, Line], Dict, Avps). + +xi({L, {Name, _Code, _Type, _Flags} = A}, Dict, Mod, Line) -> + store_new({avp_types, Name}, + [0, Mod, Line, L, A], + store_new({import, Name}, + [Line], + Dict, + [Name, Line], + duplicate_import), + [Name, Mod, Line], + imported_avp_already_defined). + +%% import_groups/1 +%% import_enums/1 +%% +%% For each inherited module, store the content of imported AVP's of +%% type grouped/enumerated in a new key. + +import_groups(Dict) -> + dict:store(import_groups, import(grouped, Dict), Dict). + +import_enums(Dict) -> + dict:store(import_enums, import(enum, Dict), Dict). + +import(Key, Dict) -> + flatmap([fun import_key/2, Key], dict:fetch(import_avps, Dict)). + +import_key({Mod, Avps}, Key) -> + As = lists:flatmap(fun(T) -> + N = element(1,T), + choose(lists:keymember(N, 1, Avps), [T], []) + end, + orddict:fetch(Key, dict(Mod))), + if As == [] -> + []; + true -> + [{Mod, As}] + end. + +%% ------------------------------------------------------------------------ +%% inherit/2 +%% +%% Return a {Mod, Line, [{Lineno, Avp}]} list, where Mod is a module +%% name, Line points to the corresponding @inherit and each Avp is +%% from Mod:dict(). Lineno is 0 if the import is implicit. + +inherit(Dict, Opts) -> + code:add_pathsa([D || {include, D} <- Opts]), + foldl(fun inherit_avps/2, [], find(inherits, Dict)). +%% Note that the module order of the returned lists is reversed +%% relative to @inherits. + +inherit_avps([Line, {_,_,M} | Names], Acc) -> + Mod = ?A(M), + report(inherit_from, Mod), + case find_avps(Names, avps_from_module(Mod)) of + {_, [{_, L, N} | _]} -> + ?RETURN(requested_avp_not_found, [Mod, Line, N, L]); + {Found, []} -> + [{Mod, Line, lists:sort(Found)} | Acc] + end. + +%% Import everything not defined locally ... +find_avps([], Avps) -> + {[{0, A} || A <- Avps], []}; + +%% ... or specified AVPs. +find_avps(Names, Avps) -> + foldl(fun acc_avp/2, {[], Names}, Avps). + +acc_avp({Name, _Code, _Type, _Flags} = A, {Found, Not} = Acc) -> + case lists:keyfind(Name, 3, Not) of + {_, Line, Name} -> + {[{Line, A} | Found], lists:keydelete(Name, 3, Not)}; + false -> + Acc + end. + +%% avps_from_module/2 + +avps_from_module(Mod) -> + orddict:fetch(avp_types, dict(Mod)). + +dict(Mod) -> + try Mod:dict() of + [?VERSION | Dict] -> + Dict; + _ -> + ?RETURN(recompile, [Mod]) + catch + error: _ -> + ?RETURN(choose(false == code:is_loaded(Mod), + not_loaded, + no_dict), + [Mod]) + end. + +%% =========================================================================== +%% pass4/1 +%% +%% Sanity checks. + +pass4(Dict) -> + dict:fold(fun(K, V, _) -> p4(K, V, Dict) end, ok, Dict), + Dict. + +%% Ensure enum AVP's have type Enumerated. +p4({enum, Name}, [Line | _], Dict) + when is_list(Name) -> + true = is_enumerated_avp(Name, Dict, Line); + +%% Ensure all referenced AVP's are either defined locally or imported. +p4({K, {Name, AvpName}}, [Line | _], Dict) + when (K == grouped orelse K == messages), + is_list(Name), + is_list(AvpName), + AvpName /= "AVP" -> + true = avp_is_defined(AvpName, Dict, Line); + +%% Ditto. +p4({K, AvpName}, [Line | _], Dict) + when K == avp_vendor_id; + K == custom_types; + K == codecs -> + true = avp_is_defined(AvpName, Dict, Line); + +p4(_, _, _) -> + ok. + +%% has_enumerated_type/3 + +is_enumerated_avp(Name, Dict, Line) -> + case find({avp_types, Name}, Dict) of + [_Line, _Code, {_, _, "Enumerated"}, _Flags] -> %% local + true; + [_Line, _Code, {_, L, T}, _] -> + ?RETURN(enumerated_avp_has_wrong_local_type, + [Name, Line, T, L]); + [0, _, _, _, {_Name, _Code, "Enumerated", _Flags}] -> %% inherited + true; + [0, Mod, LM, LA, {_Name, _Code, Type, _Flags}] -> + ?RETURN(enumerated_avp_has_wrong_inherited_type, + [Name, Line, Type, Mod, choose(0 == LA, LM, LA)]); + [] -> + ?RETURN(enumerated_avp_not_defined, [Name, Line]) + end. + +avp_is_defined(Name, Dict, Line) -> + case find({avp_types, Name}, Dict) of + [_Line, _Code, _Type, _Flags] -> %% local + true; + [0, _, _, _, {Name, _Code, _Type, _Flags}] -> %% inherited + true; + [] -> + ?RETURN(avp_not_defined, [Name, Line]) + end. + +%% =========================================================================== + +putr(Key, Value) -> + put({?MODULE, Key}, Value). + +getr(Key) -> + get({?MODULE, Key}). + +eraser(Key) -> + erase({?MODULE, Key}). + +choose(true, X, _) -> X; +choose(false, _, X) -> X. + +foldl(F, Acc, List) -> + lists:foldl(fun(T,A) -> eval([F,T,A]) end, Acc, List). + +flatmap(F, List) -> + lists:flatmap(fun(T) -> eval([F,T]) end, List). + +eval([[F|X] | A]) -> + eval([F | A ++ X]); +eval([F|A]) -> + apply(F,A). diff --git a/lib/diameter/src/compiler/diameter_exprecs.erl b/lib/diameter/src/compiler/diameter_exprecs.erl index 5e120d6f44..191f53f29d 100644 --- a/lib/diameter/src/compiler/diameter_exprecs.erl +++ b/lib/diameter/src/compiler/diameter_exprecs.erl @@ -96,41 +96,15 @@ -export([parse_transform/2]). -%% 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(VAR(V), {var, ?LINE, V}). - --define(CALL(F,A), {?call, ?ATOM(F), A}). --define(APPLY(M,F,A), {?call, {?remote, ?ATOM(M), ?ATOM(F)}, A}). +-include("diameter_forms.hrl"). %% parse_transform/2 parse_transform(Forms, _Options) -> Rs = [R || {attribute, _, record, R} <- Forms], - case lists:append([E || {attribute, _, export_records, E} <- Forms]) of - [] -> - Forms; - Es -> - {H,T} = lists:splitwith(fun is_head/1, Forms), - H ++ [a_export(Es) | f_accessors(Es, Rs)] ++ T - end. + Es = lists:append([E || {attribute, _, export_records, E} <- Forms]), + {H,T} = lists:splitwith(fun is_head/1, Forms), + H ++ [a_export(Es) | f_accessors(Es, Rs)] ++ T. is_head(T) -> not lists:member(element(1,T), [function, eof]). @@ -200,7 +174,7 @@ fname(Op, Rname) -> '#info-/2'(Exports) -> {?function, fname(info), 2, - lists:map(fun 'info-'/1, Exports)}. + lists:map(fun 'info-'/1, Exports) ++ [?BADARG(2)]}. 'info-'(R) -> {?clause, [?ATOM(R), ?VAR('Info')], @@ -209,7 +183,7 @@ fname(Op, Rname) -> '#new-/1'(Exports) -> {?function, fname(new), 1, - lists:map(fun 'new-'/1, Exports)}. + lists:map(fun 'new-'/1, Exports) ++ [?BADARG(1)]}. 'new-'(R) -> {?clause, [?ATOM(R)], @@ -218,7 +192,7 @@ fname(Op, Rname) -> '#new-/2'(Exports) -> {?function, fname(new), 2, - lists:map(fun 'new--'/1, Exports)}. + lists:map(fun 'new--'/1, Exports) ++ [?BADARG(2)]}. 'new--'(R) -> {?clause, [?ATOM(R), ?VAR('Vals')], @@ -227,7 +201,7 @@ fname(Op, Rname) -> '#get-/2'(Exports) -> {?function, fname(get), 2, - lists:map(fun 'get-'/1, Exports)}. + lists:map(fun 'get-'/1, Exports) ++ [?BADARG(2)]}. 'get-'(R) -> {?clause, [?VAR('Attrs'), @@ -237,7 +211,7 @@ fname(Op, Rname) -> '#set-/2'(Exports) -> {?function, fname(set), 2, - lists:map(fun 'set-'/1, Exports)}. + lists:map(fun 'set-'/1, Exports) ++ [?BADARG(2)]}. 'set-'(R) -> {?clause, [?VAR('Vals'), {?match, {?record, R, []}, ?VAR('Rec')}], diff --git a/lib/diameter/src/compiler/diameter_forms.hrl b/lib/diameter/src/compiler/diameter_forms.hrl index d93131df34..4cd86c32aa 100644 --- a/lib/diameter/src/compiler/diameter_forms.hrl +++ b/lib/diameter/src/compiler/diameter_forms.hrl @@ -21,6 +21,13 @@ %% Macros used when building abstract code. %% +%% Generated functions that could have no generated clauses will have +%% a trailing ?BADARG clause that should never execute as called +%% by diameter. +-define(BADARG(N), {?clause, [?VAR('_') || _ <- lists:seq(1,N)], + [], + [?APPLY(erlang, error, [?ATOM(badarg)])]}). + %% Form tag with line number. -define(F(T), T, ?LINE). %% Yes, that's right. The replacement is to the first unmatched ')'. diff --git a/lib/diameter/src/compiler/diameter_make.erl b/lib/diameter/src/compiler/diameter_make.erl index 5380ee56ca..16e30c1ffb 100644 --- a/lib/diameter/src/compiler/diameter_make.erl +++ b/lib/diameter/src/compiler/diameter_make.erl @@ -20,59 +20,113 @@ %% %% Module alternative to diameterc for dictionary compilation. %% -%% Eg. 1> diameter_make:dict("mydict.dia"). +%% Eg. 1> diameter_make:codec("mydict.dia"). %% -%% $ erl -noshell \ +%% $ erl -noinput \ %% -boot start_clean \ -%% -s diameter_make dict mydict.dia \ +%% -eval 'ok = diameter_make:codec("mydict.dia")' \ %% -s init stop %% -module(diameter_make). --export([dict/1, +-export([codec/1, + codec/2, + dict/1, dict/2, - spec/1, - spec/2]). + format/1, + reformat/1]). --type opt() :: {outdir|include|name|prefix|inherits, string()} +-export_type([opt/0]). + +-type opt() :: {include|outdir|name|prefix|inherits, string()} | verbose | debug. -%% dict/1-2 +%% =========================================================================== + +%% codec/1-2 +%% +%% Parse a dictionary file and generate a codec module. + +-spec codec(Path, [opt()]) + -> ok + | {error, Reason} + when Path :: string(), + Reason :: string(). + +codec(File, Opts) -> + case dict(File, Opts) of + {ok, Dict} -> + make(File, + Opts, + Dict, + [spec || _ <- [1], lists:member(debug, Opts)] ++ [erl, hrl]); + {error, _} = E -> + E + end. + +codec(File) -> + codec(File, []). + +%% dict/2 +%% +%% Parse a dictionary file and return the orddict that a codec module +%% returns from dict/0. -spec dict(string(), [opt()]) - -> ok. + -> {ok, orddict:orddict()} + | {error, string()}. -dict(File, Opts) -> - make(File, - Opts, - spec(File, Opts), - [spec || _ <- [1], lists:member(debug, Opts)] ++ [erl, hrl]). +dict(Path, Opts) -> + case diameter_dict_util:parse({path, Path}, Opts) of + {ok, _} = Ok -> + Ok; + {error = E, Reason} -> + {E, diameter_dict_util:format_error(Reason)} + end. dict(File) -> dict(File, []). -%% spec/2 +%% format/1 +%% +%% Turn an orddict returned by dict/1-2 back into a dictionary file +%% in the form of an iolist(). + +-spec format(orddict:orddict()) + -> iolist(). + +format(Dict) -> + diameter_dict_util:format(Dict). --spec spec(string(), [opt()]) - -> orddict:orddict(). +%% reformat/1 +%% +%% Parse a dictionary file and return its formatted equivalent. -spec(File, Opts) -> - diameter_spec_util:parse(File, Opts). +-spec reformat(File) + -> {ok, iolist()} + | {error, Reason} + when File :: string(), + Reason :: string(). -spec(File) -> - spec(File, []). +reformat(File) -> + case dict(File) of + {ok, Dict} -> + {ok, format(Dict)}; + {error, _} = No -> + No + end. %% =========================================================================== make(_, _, _, []) -> ok; -make(File, Opts, Spec, [Mode | Rest]) -> - try diameter_codegen:from_spec(File, Spec, Opts, Mode) of - ok -> - make(File, Opts, Spec, Rest) +make(File, Opts, Dict, [Mode | Rest]) -> + try + ok = diameter_codegen:from_dict(File, Dict, Opts, Mode), + make(File, Opts, Dict, Rest) catch error: Reason -> - {error, {Reason, Mode, erlang:get_stacktrace()}} + erlang:error({Reason, Mode, erlang:get_stacktrace()}) end. diff --git a/lib/diameter/src/compiler/diameter_nowarn.erl b/lib/diameter/src/compiler/diameter_nowarn.erl new file mode 100644 index 0000000000..6c17af6563 --- /dev/null +++ b/lib/diameter/src/compiler/diameter_nowarn.erl @@ -0,0 +1,41 @@ +%% +%% %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% +%% + +%% +%% A parse transform to work around dialyzer currently not +%% understanding nowarn_unused_function except on individual +%% functions. The include of diameter_gen.hrl by generated dictionary +%% modules contains code that may not be called depending on the +%% dictionary. (The relay dictionary for example.) +%% +%% Even called functions may contain cases that aren't used for a +%% particular dictionary. This also causes dialyzer to complain but +%% there's no way to silence it in this case. +%% + +-module(diameter_nowarn). + +-export([parse_transform/2]). + +parse_transform(Forms, _Options) -> + [{attribute, ?LINE, compile, {nowarn_unused_function, {F,A}}} + || {function, _, F, A, _} <- Forms] + ++ Forms. +%% Note that dialyzer also doesn't understand {nowarn_unused_function, FAs} +%% with FAs a list of tuples. diff --git a/lib/diameter/src/compiler/diameter_spec_scan.erl b/lib/diameter/src/compiler/diameter_spec_scan.erl deleted file mode 100644 index bc0448882a..0000000000 --- a/lib/diameter/src/compiler/diameter_spec_scan.erl +++ /dev/null @@ -1,157 +0,0 @@ -%% -%% %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 deleted file mode 100644 index 62536bf06d..0000000000 --- a/lib/diameter/src/compiler/diameter_spec_util.erl +++ /dev/null @@ -1,1089 +0,0 @@ -%% -%% %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, Opts) -> - put({?MODULE, verbose}, lists:member(verbose, Opts)), - {ok, B} = file:read_file(Path), - Chunks = chunk(B), - Spec = reset(make_spec(Chunks), Opts, [name, prefix, inherits]), - true = groups_defined(Spec), %% sanity checks - true = customs_defined(Spec), %% - Full = import_enums(import_groups(import_avps(insert_codes(Spec), Opts))), - true = enums_defined(Full), %% sanity checks - true = v_flags_set(Spec), - Full. - -reset(Spec, Opts, Keys) -> - lists:foldl(fun(K,S) -> - reset([{A,?ATOM(V)} || {A,V} <- Opts, A == K], S) - end, - Spec, - Keys). - -reset(L, Spec) - when is_list(L) -> - lists:foldl(fun reset/2, Spec, L); - -reset({inherits = Key, '-'}, Spec) -> - orddict:erase(Key, Spec); -reset({inherits = Key, Dict}, Spec) -> - orddict:append(Key, Dict, Spec); -reset({Key, Atom}, Spec) -> - orddict:store(Key, Atom, Spec); -reset(_, Spec) -> - Spec. - -%% 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); - -%% defines -> [{DefineName, [{Value, Name}, ...]}, ...] -chunk({define, [N], Str}, Dict) -> - append(defines, {atomize(N), parse_enums(Str)}, Dict); -chunk({result_code, [_] = N, Str}, Dict) -> %% backwards compatibility - chunk({define, N, 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) -> - Avps = get_value(avp_types, Spec), - Import = get_value(import_enums, Spec), - lists:all(fun({N,_}) -> - true = enum_defined(N, Avps, Import) - end, - get_value(enums, Spec)). - -enum_defined(Name, Avps, Import) -> - case lists:keyfind(Name, 1, Avps) of - {Name, _, 'Enumerated', _, _} -> - true; - {Name, _, T, _, _} -> - ?ERROR({avp_has_wrong_type, Name, 'Enumerated', T}); - false -> - lists:any(fun({_,Is}) -> lists:keymember(Name, 1, Is) end, Import) - orelse ?ERROR({avp_not_defined, Name, 'Enumerated'}) - end. -%% Note that an AVP is imported only if referenced by a message or -%% grouped AVP, so the final branch will fail if an enum definition is -%% extended without this being the case. - -groups_defined(Spec) -> - Avps = get_value(avp_types, Spec), - lists:all(fun({N,_,_,_}) -> true = group_defined(N, Avps) end, - get_value(grouped, Spec)). - -group_defined(Name, Avps) -> - case lists:keyfind(Name, 1, Avps) of - {Name, _, 'Grouped', _, _} -> - true; - {Name, _, T, _, _} -> - ?ERROR({avp_has_wrong_type, Name, 'Grouped', T}); - false -> - ?ERROR({avp_not_defined, Name, 'Grouped'}) - end. - -customs_defined(Spec) -> - Avps = get_value(avp_types, Spec), - lists:all(fun(A) -> true = custom_defined(A, Avps) end, - lists:flatmap(fun last/1, get_value(custom_types, Spec))). - -custom_defined(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 -%% -%% For each inherited module, store the content of imported AVP's of -%% type grouped/enumerated in a new key. - -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/diameter_vsn.hrl b/lib/diameter/src/compiler/diameter_vsn.hrl new file mode 100644 index 0000000000..024d047adc --- /dev/null +++ b/lib/diameter/src/compiler/diameter_vsn.hrl @@ -0,0 +1,22 @@ +%% +%% %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% +%% + +%% The version of the format of the return value of dict/0 in +%% generated dictionary modules. +-define(VERSION, 1). diff --git a/lib/diameter/src/dict/base_rfc3588.dia b/lib/diameter/src/dict/base_rfc3588.dia index f7a0b717cd..acd7fffd00 100644 --- a/lib/diameter/src/dict/base_rfc3588.dia +++ b/lib/diameter/src/dict/base_rfc3588.dia @@ -136,14 +136,14 @@ [ Origin-State-Id ] answer-message ::= < Diameter Header: code, ERR [PXY] > - 0*1 < Session-Id > - { Origin-Host } - { Origin-Realm } - { Result-Code } - [ Origin-State-Id ] - [ Error-Reporting-Host ] - [ Proxy-Info ] - * [ AVP ] + 0*1 < Session-Id > + { Origin-Host } + { Origin-Realm } + { Result-Code } + [ Origin-State-Id ] + [ Error-Reporting-Host ] + [ Proxy-Info ] + * [ AVP ] RAR ::= < Diameter Header: 258, REQ, PXY > < Session-Id > @@ -312,14 +312,14 @@ @enum Termination-Cause - DIAMETER_LOGOUT 1 - DIAMETER_SERVICE_NOT_PROVIDED 2 - DIAMETER_BAD_ANSWER 3 - DIAMETER_ADMINISTRATIVE 4 - DIAMETER_LINK_BROKEN 5 - DIAMETER_AUTH_EXPIRED 6 - DIAMETER_USER_MOVED 7 - DIAMETER_SESSION_TIMEOUT 8 + LOGOUT 1 + SERVICE_NOT_PROVIDED 2 + BAD_ANSWER 3 + ADMINISTRATIVE 4 + LINK_BROKEN 5 + AUTH_EXPIRED 6 + USER_MOVED 7 + SESSION_TIMEOUT 8 @enum Session-Server-Failover @@ -343,14 +343,53 @@ @define Result-Code -;; 7.1.1. Informational + ;; 7.1.1. Informational + MULTI_ROUND_AUTH 1001 + + ;; 7.1.2. Success + SUCCESS 2001 + LIMITED_SUCCESS 2002 + + ;; 7.1.3. Protocol Errors + COMMAND_UNSUPPORTED 3001 + UNABLE_TO_DELIVER 3002 + REALM_NOT_SERVED 3003 + TOO_BUSY 3004 + LOOP_DETECTED 3005 + REDIRECT_INDICATION 3006 + APPLICATION_UNSUPPORTED 3007 + INVALID_HDR_BITS 3008 + INVALID_AVP_BITS 3009 + UNKNOWN_PEER 3010 + + ;; 7.1.4. Transient Failures + AUTHENTICATION_REJECTED 4001 + OUT_OF_SPACE 4002 + ELECTION_LOST 4003 + + ;; 7.1.5. Permanent Failures + AVP_UNSUPPORTED 5001 + UNKNOWN_SESSION_ID 5002 + AUTHORIZATION_REJECTED 5003 + INVALID_AVP_VALUE 5004 + MISSING_AVP 5005 + RESOURCES_EXCEEDED 5006 + CONTRADICTING_AVPS 5007 + AVP_NOT_ALLOWED 5008 + AVP_OCCURS_TOO_MANY_TIMES 5009 + NO_COMMON_APPLICATION 5010 + UNSUPPORTED_VERSION 5011 + UNABLE_TO_COMPLY 5012 + INVALID_BIT_IN_HEADER 5013 + INVALID_AVP_LENGTH 5014 + INVALID_MESSAGE_LENGTH 5015 + INVALID_AVP_BIT_COMBO 5016 + NO_COMMON_SECURITY 5017 + + ;; With a prefix for backwards compatibility. DIAMETER_MULTI_ROUND_AUTH 1001 - -;; 7.1.2. Success DIAMETER_SUCCESS 2001 DIAMETER_LIMITED_SUCCESS 2002 - -;; 7.1.3. Protocol Errors DIAMETER_COMMAND_UNSUPPORTED 3001 DIAMETER_UNABLE_TO_DELIVER 3002 DIAMETER_REALM_NOT_SERVED 3003 @@ -361,13 +400,9 @@ DIAMETER_INVALID_HDR_BITS 3008 DIAMETER_INVALID_AVP_BITS 3009 DIAMETER_UNKNOWN_PEER 3010 - -;; 7.1.4. Transient Failures DIAMETER_AUTHENTICATION_REJECTED 4001 DIAMETER_OUT_OF_SPACE 4002 - ELECTION_LOST 4003 - -;; 7.1.5. Permanent Failures + DIAMETER_ELECTION_LOST 4003 DIAMETER_AVP_UNSUPPORTED 5001 DIAMETER_UNKNOWN_SESSION_ID 5002 DIAMETER_AUTHORIZATION_REJECTED 5003 @@ -412,3 +447,15 @@ ;; E2E-Sequence ::= <AVP Header: 300 > 2* { AVP } + +;; Backwards compatibility. +@define Termination-Cause + + DIAMETER_LOGOUT 1 + DIAMETER_SERVICE_NOT_PROVIDED 2 + DIAMETER_BAD_ANSWER 3 + DIAMETER_ADMINISTRATIVE 4 + DIAMETER_LINK_BROKEN 5 + DIAMETER_AUTH_EXPIRED 6 + DIAMETER_USER_MOVED 7 + DIAMETER_SESSION_TIMEOUT 8 diff --git a/lib/diameter/src/dict/relay.dia b/lib/diameter/src/dict/relay.dia index c22293209b..294014b093 100644 --- a/lib/diameter/src/dict/relay.dia +++ b/lib/diameter/src/dict/relay.dia @@ -21,5 +21,3 @@ @name diameter_gen_relay @prefix diameter_relay @vendor 0 IETF - -@inherits diameter_gen_base_rfc3588 diff --git a/lib/diameter/src/gen/.gitignore b/lib/diameter/src/gen/.gitignore index d490642eb7..3f32313f56 100644 --- a/lib/diameter/src/gen/.gitignore +++ b/lib/diameter/src/gen/.gitignore @@ -1,2 +1,2 @@ - +/diameter_dict_parser.erl /diameter_gen*rl diff --git a/lib/diameter/src/modules.mk b/lib/diameter/src/modules.mk index c7cbe598af..11d354e57e 100644 --- a/lib/diameter/src/modules.mk +++ b/lib/diameter/src/modules.mk @@ -24,10 +24,15 @@ DICTS = \ base_accounting \ relay +# The yecc grammar for the dictionary parser. +DICT_YRL = \ + diameter_dict_parser + # Handwritten (runtime) modules included in the app file. RT_MODULES = \ base/diameter \ base/diameter_app \ + base/diameter_callback \ base/diameter_capx \ base/diameter_config \ base/diameter_codec \ @@ -57,13 +62,13 @@ RT_MODULES = \ # Handwritten (compile time) modules not included in the app file. CT_MODULES = \ - base/diameter_callback \ base/diameter_dbg \ base/diameter_info \ compiler/diameter_codegen \ compiler/diameter_exprecs \ - compiler/diameter_spec_scan \ - compiler/diameter_spec_util \ + compiler/diameter_nowarn \ + compiler/diameter_dict_scanner \ + compiler/diameter_dict_util \ compiler/diameter_make # Released hrl files in ../include intended for public consumption. @@ -74,8 +79,8 @@ EXTERNAL_HRLS = \ # Released hrl files intended for private use. INTERNAL_HRLS = \ base/diameter_internal.hrl \ - base/diameter_types.hrl \ - compiler/diameter_forms.hrl + compiler/diameter_forms.hrl \ + compiler/diameter_vsn.hrl # Released files relative to ../bin. BINS = \ diff --git a/lib/diameter/src/transport/diameter_sctp.erl b/lib/diameter/src/transport/diameter_sctp.erl index 209f8c01c1..68b0342cd5 100644 --- a/lib/diameter/src/transport/diameter_sctp.erl +++ b/lib/diameter/src/transport/diameter_sctp.erl @@ -546,10 +546,10 @@ send(Sock, AssocId, Stream, Bin) -> %% recv/2 %% Association established ... -recv({[], #sctp_assoc_change{state = comm_up, - outbound_streams = OS, - inbound_streams = IS, - assoc_id = Id}}, +recv({_, #sctp_assoc_change{state = comm_up, + outbound_streams = OS, + inbound_streams = IS, + assoc_id = Id}}, #transport{assoc_id = undefined, mode = {T, _}, socket = Sock} @@ -562,7 +562,7 @@ recv({[], #sctp_assoc_change{state = comm_up, streams = {IS, OS}}); %% ... or not: try the next address. -recv({[], #sctp_assoc_change{} = E}, +recv({_, #sctp_assoc_change{} = E}, #transport{assoc_id = undefined, socket = Sock, mode = {connect = C, {[RA|RAs], RP, Es}}} @@ -570,7 +570,7 @@ recv({[], #sctp_assoc_change{} = E}, S#transport{mode = {C, connect(Sock, RAs, RP, [{RA,E} | Es])}}; %% Lost association after establishment. -recv({[], #sctp_assoc_change{}}, _) -> +recv({_, #sctp_assoc_change{}}, _) -> stop; %% Inbound Diameter message. @@ -580,7 +580,7 @@ recv({[#sctp_sndrcvinfo{stream = Id}], Bin}, #transport{parent = Pid}) bin = Bin}), ok; -recv({[], #sctp_shutdown_event{assoc_id = Id}}, +recv({_, #sctp_shutdown_event{assoc_id = Id}}, #transport{assoc_id = Id}) -> stop; @@ -593,10 +593,10 @@ recv({[], #sctp_shutdown_event{assoc_id = Id}}, %% disabled by default so don't handle it. We could simply disable %% events we don't react to but don't. -recv({[], #sctp_paddr_change{}}, _) -> +recv({_, #sctp_paddr_change{}}, _) -> ok; -recv({[], #sctp_pdapi_event{}}, _) -> +recv({_, #sctp_pdapi_event{}}, _) -> ok. %% up/1 |