1 module ddbus.router;
2 
3 import ddbus.thin;
4 import ddbus.c_lib;
5 import ddbus.util;
6 import std.string;
7 import std.typecons;
8 import core.memory;
9 import std.array;
10 import std.algorithm;
11 import std.format;
12 
13 struct MessagePattern {
14   string path;
15   string iface;
16   string method;
17   bool signal;
18 
19   this(Message msg) {
20     path = msg.path();
21     iface = msg.iface();
22     method = msg.member();
23     signal = (msg.type() == MessageType.Signal);
24   }
25 
26   this(string path, string iface, string method, bool signal = false) {
27     this.path = path;
28     this.iface = iface;
29     this.method = method;
30     this.signal = signal;
31   }
32 
33   size_t toHash() const @safe nothrow {
34     size_t hash = 0;
35     auto stringHash = &(typeid(path).getHash);
36     hash += stringHash(&path);
37     hash += stringHash(&iface);
38     hash += stringHash(&method);
39     hash += (signal?1:0);
40     return hash;
41   }
42 
43   bool opEquals(ref const this s) const @safe pure nothrow {
44     return (path == s.path) && (iface == s.iface) && (method == s.method) && (signal == s.signal);
45   }
46 }
47 
48 unittest {
49   import dunit.toolkit;
50   auto msg = Message("org.example.test", "/test","org.example.testing","testMethod");
51   auto patt= new MessagePattern(msg);
52   patt.assertEqual(patt);
53   patt.signal.assertFalse();
54   patt.path.assertEqual("/test");
55 }
56 
57 struct MessageHandler {
58   alias HandlerFunc = void delegate(Message call, Connection conn);
59   HandlerFunc func;
60   string[] argSig;
61   string[] retSig;
62 }
63 
64 class MessageRouter {
65   MessageHandler[MessagePattern] callTable;
66 
67   bool handle(Message msg, Connection conn) {
68     MessageType type = msg.type();
69     if(type != MessageType.Call && type != MessageType.Signal)
70       return false;
71     auto pattern = MessagePattern(msg);
72     // import std.stdio; debug writeln("Handling ", pattern);
73 
74     if(pattern.iface == "org.freedesktop.DBus.Introspectable" &&
75       pattern.method == "Introspect" && !pattern.signal) {
76       handleIntrospect(pattern.path, msg, conn);
77       return true;
78     }
79 
80     MessageHandler* handler = (pattern in callTable);
81     if(handler is null) return false;
82 
83     // Check for matching argument types
84     version(DDBusNoChecking) {
85 
86     } else {
87       if(!equal(join(handler.argSig), msg.signature())) {
88         return false;
89       }
90     }
91 
92     handler.func(msg,conn);
93     return true;
94   }
95 
96   void setHandler(Ret, Args...)(MessagePattern patt, Ret delegate(Args) handler) {
97     void handlerWrapper(Message call, Connection conn) {
98       Tuple!Args args = call.readTuple!(Tuple!Args)();
99       auto retMsg = call.createReturn();
100       static if(!is(Ret == void)) {
101         Ret ret = handler(args.expand);
102         static if (is(Ret == Tuple!T, T...))
103           retMsg.build!T(ret.expand);
104         else
105           retMsg.build(ret);
106       } else {
107         handler(args.expand);
108       }
109       if(!patt.signal)
110         conn.send(retMsg);
111     }
112     static string[] args = typeSigArr!Args;
113     static if(is(Ret==void)) {
114       static string[] ret = [];
115     } else {
116       static string[] ret = typeSigReturn!Ret;
117     }
118     MessageHandler handleStruct = {func: &handlerWrapper, argSig: args, retSig: ret};
119     callTable[patt] = handleStruct;
120   }
121 
122   static string introspectHeader = `<!DOCTYPE node PUBLIC "-//freedesktop//DTD D-BUS Object Introspection 1.0//EN" "http://www.freedesktop.org/standards/dbus/1.0/introspect.dtd">
123 <node name="%s">`;
124 
125   string introspectXML(string path) {
126     auto methods = callTable.byKey().filter!(a => (a.path == path) && !a.signal)().array()
127       // .schwartzSort!((a) => a.iface, "a<b")();
128       .sort!((a,b) => a.iface < b.iface)();
129     auto ifaces = methods.groupBy();
130     auto app = appender!string;
131     formattedWrite(app,introspectHeader,path);
132     foreach(iface; ifaces) {
133       formattedWrite(app,`<interface name="%s">`,iface.front.iface);
134       foreach(methodPatt; iface.array()) {
135         formattedWrite(app,`<method name="%s">`,methodPatt.method);
136         auto handler = callTable[methodPatt];
137         foreach(arg; handler.argSig) {
138           formattedWrite(app,`<arg type="%s" direction="in"/>`,arg);
139         }
140         foreach(arg; handler.retSig) {
141           formattedWrite(app,`<arg type="%s" direction="out"/>`,arg);
142         }
143         app.put("</method>");
144       }
145       app.put("</interface>");
146     }
147 
148     string childPath = path;
149     if(!childPath.endsWith("/")) {
150       childPath ~= "/";
151     }
152     auto children = callTable.byKey().filter!(a => (a.path.startsWith(childPath)) && !a.signal)()
153       .map!((s) => s.path.chompPrefix(childPath))
154       .map!((s) => s.splitter('/').front)
155       .array().sort().uniq();
156     foreach(child; children) {
157       formattedWrite(app,`<node name="%s"/>`,child);
158     }
159 
160     app.put("</node>");
161     return app.data;
162   }
163 
164   void handleIntrospect(string path, Message call, Connection conn) {
165     auto retMsg = call.createReturn();
166     retMsg.build(introspectXML(path));
167     conn.sendBlocking(retMsg);
168   }
169 }
170 
171 extern(C) private DBusHandlerResult filterFunc(DBusConnection *dConn, DBusMessage *dMsg, void *routerP) {
172   MessageRouter router = cast(MessageRouter)routerP;
173   dbus_message_ref(dMsg);
174   Message msg = Message(dMsg);
175   dbus_connection_ref(dConn);
176   Connection conn = Connection(dConn);
177   bool handled = router.handle(msg, conn);
178   if(handled) {
179     return DBusHandlerResult.DBUS_HANDLER_RESULT_HANDLED;
180   } else {
181     return DBusHandlerResult.DBUS_HANDLER_RESULT_NOT_YET_HANDLED;
182   }
183 }
184 
185 extern(C) private void unrootUserData(void *userdata) {
186   GC.removeRoot(userdata);
187 }
188 
189 void registerRouter(Connection conn, MessageRouter router) {
190   void *routerP = cast(void*)router;
191   GC.addRoot(routerP);
192   dbus_connection_add_filter(conn.conn, &filterFunc, routerP, &unrootUserData);
193 }
194 
195 unittest{
196   import dunit.toolkit;
197   auto router = new MessageRouter();
198   // set up test messages
199   MessagePattern patt = MessagePattern("/root","ca.thume.test","test");
200   router.setHandler!(int,int)(patt,(int p) {return 6;});
201   patt = MessagePattern("/root","ca.thume.tester","lolwut");
202   router.setHandler!(void,int,string)(patt,(int p, string p2) {});
203   patt = MessagePattern("/root/wat","ca.thume.tester","lolwut");
204   router.setHandler!(int,int)(patt,(int p) {return 6;});
205   patt = MessagePattern("/root/bar","ca.thume.tester","lolwut");
206   router.setHandler!(Variant!DBusAny,int)(patt,(int p) {return variant(DBusAny(p));});
207   patt = MessagePattern("/root/foo","ca.thume.tester","lolwut");
208   router.setHandler!(Tuple!(string,string,int),int,Variant!DBusAny)(patt,(int p, Variant!DBusAny any) {Tuple!(string,string,int) ret; ret[0] = "a"; ret[1] = "b"; ret[2] = p; return ret;});
209   patt = MessagePattern("/troll","ca.thume.tester","wow");
210   router.setHandler!(void)(patt,{return;});
211 
212   static assert(!__traits(compiles, {
213     patt = MessagePattern("/root/bar","ca.thume.tester","lolwut");
214     router.setHandler!(void, DBusAny)(patt,(DBusAny wrongUsage){return;});
215   }));
216 
217   // TODO: these tests rely on nondeterministic hash map ordering
218   static string introspectResult = `<!DOCTYPE node PUBLIC "-//freedesktop//DTD D-BUS Object Introspection 1.0//EN" "http://www.freedesktop.org/standards/dbus/1.0/introspect.dtd">
219 <node name="/root"><interface name="ca.thume.test"><method name="test"><arg type="i" direction="in"/><arg type="i" direction="out"/></method></interface><interface name="ca.thume.tester"><method name="lolwut"><arg type="i" direction="in"/><arg type="s" direction="in"/></method></interface><node name="bar"/><node name="foo"/><node name="wat"/></node>`;
220   router.introspectXML("/root").assertEqual(introspectResult);
221   static string introspectResult2 = `<!DOCTYPE node PUBLIC "-//freedesktop//DTD D-BUS Object Introspection 1.0//EN" "http://www.freedesktop.org/standards/dbus/1.0/introspect.dtd">
222 <node name="/root/foo"><interface name="ca.thume.tester"><method name="lolwut"><arg type="i" direction="in"/><arg type="v" direction="in"/><arg type="s" direction="out"/><arg type="s" direction="out"/><arg type="i" direction="out"/></method></interface></node>`;
223   router.introspectXML("/root/foo").assertEqual(introspectResult2);
224   router.introspectXML("/").assertEndsWith(`<node name="/"><node name="root"/><node name="troll"/></node>`);
225 }