%% @copyright 2006-2007 Mikael Magnusson, 2014-2016 Jean Parpaillon %% %% @author Mikael Magnusson %% @author Jean Parpaillon %% @doc API Introspection support module %% %% See D-Bus specifications %% for introspection XML schema. %% %% @end -module(dbus_introspect). -include_lib("xmerl/include/xmerl.hrl"). -include("dbus.hrl"). -include("dbus_errors.hrl"). -export([ find_interface/2, find_method/3, find_signal/3, to_xml/1, from_xml/1, from_xml_string/1 ]). -record(state, {node :: dbus_node(), child :: dbus_node(), iface :: dbus_iface(), method :: dbus_method(), signal :: dbus_signal(), property :: dbus_property(), s = root :: atom()}). %%% %%% API %%% %% @doc Find an interface definition. %% %% @end -spec find_interface(dbus_node(), Iface :: dbus_name()) -> {ok, dbus_iface()} | {error, dbus_err()}. find_interface(#dbus_node{interfaces=Ifaces}, IfaceName) -> case find_name(IfaceName, Ifaces, fun dbus_names:bin_to_iface/1) of {value, #dbus_iface{}=I} -> {ok, I}; none -> {error, {'org.freedesktop.DBus.UnknownInterface', [IfaceName]}} end. %% @doc Find a method definition. %% @end -spec find_method(dbus_node(), Iface :: dbus_name(), Method :: dbus_name()) -> {ok, dbus_method()} | {error, dbus_err()}. find_method(#dbus_node{interfaces=Ifaces}=Node, IfaceName, MethodName) -> case find_name(IfaceName, Ifaces, fun dbus_names:bin_to_iface/1) of {value, #dbus_iface{methods=Methods}=_I} -> case find_name(MethodName, Methods, fun dbus_names:bin_to_method/1) of {value, #dbus_method{}=Method} -> {ok, Method}; none -> {error, {'org.freedesktop.DBus.UnknownMethod', {[MethodName], IfaceName, Node}}} end; none -> {error, {'org.freedesktop.DBus.UnknownInterface', [IfaceName]}} end. %% @doc Find a signal definition %% @end -spec find_signal(dbus_node(), Iface :: dbus_name(), Signal :: dbus_name()) -> {ok, dbus_signal()} | {error, dbus_err()}. find_signal(#dbus_node{interfaces=Ifaces}=Node, IfaceName, SignalName) -> case find_name(IfaceName, Ifaces, fun dbus_names:bin_to_iface/1) of {value, #dbus_iface{signals=Signals}} -> case find_name(SignalName, Signals, fun dbus_names:bin_to_signal/1) of {value, #dbus_signal{}=Signal} -> {ok, Signal}; none -> {error, {'org.freedesktop.DBus.UnknownSignal', {[SignalName], IfaceName, Node}}} end; none -> {error, {'org.freedesktop.DBus.UnknownInterface', [IfaceName]}} end. %% @doc Export a `dbus_node()' into an XML introspection document. %% @end -spec to_xml(dbus_node()) -> list(). to_xml(#dbus_node{}=Node) -> Prolog = "", lists:flatten(xmerl:export_simple([to_xmerl(Node)], xmerl_xml, [{prolog, Prolog}])). %% @doc Parse a `dbus_node() ' from an XML string. %% %% @throws {error, parse_error} %% @end -spec from_xml_string(binary()) -> dbus_node(). from_xml_string(Data) when is_binary(Data) -> Opts = [{event_fun, fun xml_event/3}, {event_state, #state{}}, skip_external_dtd], case xmerl_sax_parser:stream(Data, Opts) of {ok, #dbus_node{}=Node, _Rest} -> Node; {_Tag, _Location, Reason, _EndTags, _State} -> ?error("Error parsing introspection: ~p~n", [Reason]), throw({error, parse_error}) end. %% @doc Parse a `dbus_node()' from a filename. %% %% @throws {error, parse_error} %% @end -spec from_xml(file:filename()) -> dbus_node(). from_xml(Filename) -> Opts = [{event_fun, fun xml_event/3}, {event_state, #state{}}, skip_external_dtd], case xmerl_sax_parser:file(Filename, Opts) of {ok, #dbus_node{}=Node, _Rest} -> Node; {_Tag, _Location, Reason, _EndTags, _State} -> ?error("Error parsing introspection: ~p~n", [Reason]), throw({error, parse_error}) end. %%% %%% Priv %%% find_name(Name, Tree, _) when is_atom(Name) -> case gb_trees:lookup(Name, Tree) of {value, Iface} -> {value, Iface}; none -> gb_trees:lookup(atom_to_binary(Name, utf8), Tree) end; find_name(Name, Tree, ToKnownFun) when is_binary(Name) -> case gb_trees:lookup(Name, Tree) of {value, Iface} -> {value, Iface}; none -> case ToKnownFun(Name) of Bin when is_binary(Bin) -> none; Atom when is_atom(Atom) -> gb_trees:lookup(Atom, Tree) end end. to_binary(Item) when is_atom(Item) -> atom_to_binary(Item, utf8); to_binary(Item) when is_list(Item) -> list_to_binary(Item). to_xmerl(undefined) -> []; to_xmerl(List) when is_list(List) -> lists:map(fun(Elem) -> to_xmerl(Elem) end, List); to_xmerl(#dbus_node{}=Elem) -> {node, case to_binary(Elem#dbus_node.name) of undefined -> []; Name -> [{name, Name}] end, to_xmerl(Elem#dbus_node.elements) ++ to_xmerl(Elem#dbus_node.interfaces) }; to_xmerl(#dbus_iface{}=Elem) -> {interface, [{name, to_binary(Elem#dbus_iface.name)}], to_xmerl(Elem#dbus_iface.methods) ++ to_xmerl(Elem#dbus_iface.signals) ++ to_xmerl(Elem#dbus_iface.properties)}; to_xmerl(#dbus_method{}=Elem) -> Result = case Elem#dbus_method.result of none -> []; undefined -> []; Arg -> [to_xmerl(Arg)] end, {method, case to_binary(Elem#dbus_method.name) of undefined -> []; Name -> [{name, Name}] end, to_xmerl(Elem#dbus_method.args) ++ Result}; to_xmerl(#dbus_signal{}=Elem) -> Result = case Elem#dbus_signal.result of none -> []; undefined -> []; Arg -> [to_xmerl(Arg)] end, {signal, case list_to_binary(Elem#dbus_signal.name) of undefined -> []; Name -> [{name, Name}] end, to_xmerl(Elem#dbus_signal.args) ++ Result}; to_xmerl(#dbus_arg{}=Elem) -> {arg, case to_binary(Elem#dbus_arg.name) of undefined -> []; Name -> [{name, Name}] end ++ [{direction, Elem#dbus_arg.direction}, {type, Elem#dbus_arg.type}], []}. xml_event(startDocument, _L, S) -> S; xml_event(endDocument, _L, #state{node=Node}) -> Node; xml_event({startPrefixMapping, _Prefix, _Uri}, _L, S) -> S; xml_event({endPrefixMapping, _Prefix}, _L, S) -> S; xml_event({startElement, _, "node", _, Attrs}, _L, #state{s=root}=S) -> S#state{s=node, node=build_root_node(Attrs, #dbus_node{interfaces=gb_trees:empty()})}; xml_event({startElement, _, "node", _, Attrs}, _L, #state{s=node}=S) -> S#state{s=child_node, child=build_node(Attrs, #dbus_node{})}; xml_event({startElement, _, "interface", _, Attrs}, _L, #state{s=node}=S) -> S#state{s=iface, iface=build_iface(Attrs, #dbus_iface{methods=gb_trees:empty(), signals=gb_trees:empty(), properties=gb_trees:empty()})}; xml_event({startElement, _, "method", _, Attrs}, _L, #state{s=iface}=S) -> S#state{s=method, method=build_method(Attrs, #dbus_method{})}; xml_event({startElement, _, "signal", _, Attrs}, _L, #state{s=iface}=S) -> S#state{s=signal, signal=build_signal(Attrs, #dbus_signal{})}; xml_event({startElement, _, "property", _, Attrs}, _L, #state{s=iface}=S) -> S#state{s=property, property=build_property(Attrs, #dbus_property{})}; xml_event({startElement, _, "arg", _, Attrs}, _L, #state{s=method, method=#dbus_method{args=Args}=Method}=S) -> Arg = build_arg(Attrs, #dbus_arg{}), S#state{s=method_arg, method=Method#dbus_method{args=[Arg | Args]}}; xml_event({startElement, _, "arg", _, Attrs}, _L, #state{s=signal, signal=#dbus_signal{args=Args}=Signal}=S) -> Arg = build_arg(Attrs, #dbus_arg{}), S#state{s=signal_arg, signal=Signal#dbus_signal{args=[Arg | Args]}}; xml_event({startElement, _, "annotation", _, Attrs}, _L, #state{s=method, method=#dbus_method{annotations=Annotations}=M}=S) -> S#state{s=method_annotation, method=M#dbus_method{annotations=[ build_annotation(Attrs, {undefined, undefined}) | Annotations]}}; xml_event({startElement, _, "annotation", _, Attrs}, _L, #state{s=iface, iface=#dbus_iface{annotations=Annotations}=I}=S) -> S#state{s=iface_annotation, iface=I#dbus_iface{annotations=[ build_annotation(Attrs, {undefined, undefined}) | Annotations]}}; xml_event({startElement, _, "annotation", _, Attrs}, _L, #state{s=property, property=#dbus_property{annotations=Annotations}=P}=S) -> S#state{s=property_annotation, property=P#dbus_property{annotations=[ build_annotation(Attrs, {undefined, undefined}) | Annotations]}}; xml_event({startElement, _, "annotation", _, Attrs}, _L, #state{s=signal, signal=#dbus_signal{annotations=Annotations}=Sig}=S) -> S#state{s=signal_annotation, signal=Sig#dbus_signal{annotations=[ build_annotation(Attrs, {undefined, undefined}) | Annotations]}}; xml_event({endElement, _, "node", _}, _L, #state{s=node}=S) -> S#state{s=root}; xml_event({endElement, _, "node", _}, _L, #state{s=child_node, node=#dbus_node{elements=E}=N, child=C}=S) -> S#state{s=node, node=N#dbus_node{elements=[C | E]}, child=undefined}; xml_event({endElement, _, "interface", _}, _L, #state{s=iface, node=#dbus_node{interfaces=Ifaces}=N, iface=I}=S) -> NewNode = N#dbus_node{interfaces=gb_trees:insert(I#dbus_iface.name, I, Ifaces)}, S#state{s=node, node=NewNode, iface=undefined, method=undefined}; xml_event({endElement, _, "method", _}, _L, #state{s=method, iface=#dbus_iface{methods=Methods}=I, method=M}=S) -> NewIface = I#dbus_iface{methods=gb_trees:insert(M#dbus_method.name, end_method(M), Methods)}, S#state{s=iface, iface=NewIface, method=undefined}; xml_event({endElement, _, "signal", _}, _L, #state{s=signal, iface=#dbus_iface{signals=Signals}=I, signal=Sig}=S) -> NewIface = I#dbus_iface{signals=gb_trees:insert(Sig#dbus_signal.name, end_signal(Sig), Signals)}, S#state{s=iface, iface=NewIface, signal=undefined}; xml_event({endElement, _, "property", _}, _L, #state{s=property, iface=#dbus_iface{properties=Props}=I, property=P}=S) -> NewIface = I#dbus_iface{properties=gb_trees:insert(P#dbus_property.name, P, Props)}, S#state{s=iface, iface=NewIface, property=undefined}; xml_event({endElement, _, "arg", _}, _L, #state{s=method_arg}=S) -> S#state{s=method}; xml_event({endElement, _, "arg", _}, _L, #state{s=signal_arg}=S) -> S#state{s=signal}; xml_event({endElement, _, "annotation", _}, _L, #state{s=method_annotation}=S) -> S#state{s=method}; xml_event({endElement, _, "annotation", _}, _L, #state{s=iface_annotation}=S) -> S#state{s=iface}; xml_event({endElement, _, "annotation", _}, _L, #state{s=property_annotation}=S) -> S#state{s=property}; xml_event({endElement, _, "annotation", _}, _L, #state{s=signal_annotation}=S) -> S#state{s=signal}; xml_event({characters, _Str}, _L, S) -> S; xml_event({ignorableWhitespace, _Str}, _L, S) -> S; xml_event({processingInstruction, _Target, _Data}, _L, S) -> S; xml_event({comment, _Str}, _L, S) -> S; xml_event(startCDATA, _L, S) -> S; xml_event(endCDATA, _L, S) -> S; xml_event({startDTD, "node", "-//freedesktop//DTD D-BUS Object Introspection 1.0//EN", _SysID}, _L, S) -> S; xml_event({startDTD, _Name, PublicID, _SysID}, {_, _, L}, _S) -> throw({error, io_lib:format("(line ~p) Invalid DTD: ~p", [L, PublicID])}); xml_event(endDTD, _L, S) -> S; xml_event({startEntity, _SysID}, _L, S) -> S; xml_event({endEntity, _SysID}, _L, S) -> S; xml_event({elementDecl, _Name, _Model}, _L, S) -> S; xml_event({attributeDecl, _ElementName, _AttributeName, _Type, _Mode, _Value}, _L, S) -> S; xml_event({internalEntityDecl, _Name, _Value}, _L, S) -> S; xml_event({externalEntityDecl, _Name, _PublicId, _SystemId}, _L, S) -> S; xml_event({unparsedEntityDecl, _Name, _PublicId, _SystemId, _Ndata}, _L, S) -> S; xml_event({notationDecl, _Name, _PublicId, _SystemId}, _L, S) -> S; xml_event(E, {_,_,L}, S) -> throw({error, io_lib:format("(line ~p) Unhandled event ~p (state=~p)", [L, E, S])}). build_root_node([], Node) -> Node; build_root_node([{_, _, "name", Path} | Attrs], Node) -> build_root_node(Attrs, Node#dbus_node{name=list_to_binary(Path)}); build_root_node([{_, _, Attr, _}, _], _) -> throw({error, {invalid_node_attribute, Attr}}). %% TODO: check child node path: must be relative build_node([], Node) -> Node; build_node([{_, _, "name", Path} | Attrs], Node) -> build_node(Attrs, Node#dbus_node{name=list_to_binary(Path)}); build_node([{_, _, Attr, _}, _], _) -> throw({error, {invalid_node_attribute, Attr}}). build_iface([], Iface) -> Iface; build_iface([{_, _, "name", Name} | Attrs], Iface) -> build_iface(Attrs, Iface#dbus_iface{name=dbus_names:list_to_iface(Name)}); build_iface([{_, _, Attr, _}, _], _) -> throw({error, {invalid_interface_attribute, Attr}}). build_method([], Method) -> Method; build_method([{_, _, "name", Name} | Attrs], Method) -> build_method(Attrs, Method#dbus_method{name=dbus_names:list_to_method(Name)}); build_method([{_, _, Attr, _}, _], _) -> throw({error, {invalid_method_attribute, Attr}}). end_method(#dbus_method{args=Args}=Method) -> InArgs = lists:filter(fun (#dbus_arg{direction=in}) -> true; (#dbus_arg{direction=undefined}) -> true; (_) -> false end, Args), Signature = lists:foldl(fun (#dbus_arg{type=Type}, Acc) -> << Type/binary, Acc/binary >> end, <<>>, InArgs), case dbus_marshaller:unmarshal_signature(Signature) of {ok, Types} -> Method#dbus_method{args=lists:reverse(Args), in_sig=Signature, in_types=Types}; more -> throw({error, incomplete_signature}) end. %% TODO: Deal with omission of direction attribute build_arg([], Arg) -> Arg; build_arg([{_, _, "name", Name} | Attrs], Arg) -> build_arg(Attrs, Arg#dbus_arg{name=list_to_binary(Name)}); build_arg([{_, _, "type", Type} | Attrs], Arg) -> build_arg(Attrs, Arg#dbus_arg{type=list_to_binary(Type)}); build_arg([{_, _, "direction", "in"} | Attrs], Arg) -> build_arg(Attrs, Arg#dbus_arg{direction=in}); build_arg([{_, _, "direction", "out"} | Attrs], Arg) -> build_arg(Attrs, Arg#dbus_arg{direction=out}); build_arg([{_, _, Attr, _}, _], _) -> throw({error, {invalid_arg_attribute, Attr}}). build_signal([], Signal) -> Signal; build_signal([{_, _, "name", Name} | Attrs], Signal) -> build_signal(Attrs, Signal#dbus_signal{name=dbus_names:list_to_signal(Name)}); build_signal([{_, _, Attr, _} | _], _) -> throw({error, {invalid_signal_attribute, Attr}}). end_signal(#dbus_signal{args=Args}=Signal) -> OutArgs = lists:filter(fun (#dbus_arg{direction=out}) -> true; (#dbus_arg{direction=undefined}) -> true; (_) -> false end, Args), Signature = lists:foldl(fun (#dbus_arg{type=Type}, Acc) -> << Type/binary, Acc/binary >> end, <<>>, OutArgs), case dbus_marshaller:unmarshal_signature(Signature) of {ok, Types} -> Signal#dbus_signal{args=lists:reverse(Args), out_sig=Signature, out_types=Types}; more -> throw({error, incomplete_signature}) end. build_property([], Prop) -> Prop; build_property([{_, _, "name", Name} | Attrs], Prop) -> build_property(Attrs, Prop#dbus_property{name=list_to_binary(Name)}); build_property([{_, _, "type", Type} | Attrs], Prop) -> build_property(Attrs, Prop#dbus_property{type=list_to_binary(Type)}); build_property([{_, _, "access", "read"} | Attrs], Prop) -> build_property(Attrs, Prop#dbus_property{access=read}); build_property([{_, _, "access", "write"} | Attrs], Prop) -> build_property(Attrs, Prop#dbus_property{access=write}); build_property([{_, _, "access", "readwrite"} | Attrs], Prop) -> build_property(Attrs, Prop#dbus_property{access=readwrite}); build_property([{_, _, Attr, _} | _], _) -> throw({error, {invalid_signal_attribute, Attr}}). build_annotation([], Acc) -> Acc; build_annotation([{_, _, "name", "org.freedesktop.DBus.Deprecated"} | Attrs], {undefined, V}) -> build_annotation(Attrs, {'org.freedesktop.DBus.Deprecated', V}); build_annotation([{_, _, "name", "org.freedesktop.DBus.GLib.CSymbol"} | Attrs], {undefined, V}) -> build_annotation(Attrs, {'org.freedesktop.DBus.GLib.CSymbol', V}); build_annotation([{_, _, "name", "org.freedesktop.DBus.Method.NoReply"} | Attrs], {undefined, V}) -> build_annotation(Attrs, {'org.freedesktop.DBus.Method.NoReply', V}); build_annotation([{_, _, "name", "org.freedesktop.DBus.Property.EmitsChangedSignal"} | Attrs], {undefined, V}) -> build_annotation(Attrs, {'org.freedesktop.DBus.Property.EmitsChangedSignal', V}); build_annotation([{_, _, "name", Name} | Attrs], {undefined, V}) -> build_annotation(Attrs, {list_to_binary(Name), V}); build_annotation([{_, _, "value", "true"} | Attrs], {N, undefined}) -> build_annotation(Attrs, {N, true}); build_annotation([{_, _, "value", "false"} | Attrs], {N, undefined}) -> build_annotation(Attrs, {N, false}); build_annotation([{_, _, "value", "invalidates"} | Attrs], {N, undefined}) -> build_annotation(Attrs, {N, invalidates}); build_annotation([{_, _, "value", V} | Attrs], {N, undefined}) -> build_annotation(Attrs, {N, list_to_binary(V)}); build_annotation([{_, _, Attr, _} | _], _) -> throw({error, {invalid_annotation_attribute, Attr}}).