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 typeof(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 
51   auto msg = Message("org.example.test", "/test", "org.example.testing", "testMethod");
52   auto patt = new MessagePattern(msg);
53   patt.assertEqual(patt);
54   patt.signal.assertFalse();
55   patt.path.assertEqual("/test");
56 }
57 
58 struct MessageHandler {
59   alias HandlerFunc = void delegate(Message call, Connection conn);
60   HandlerFunc func;
61   string[] argSig;
62   string[] retSig;
63 }
64 
65 class MessageRouter {
66   MessageHandler[MessagePattern] callTable;
67 
68   bool handle(Message msg, Connection conn) {
69     MessageType type = msg.type();
70     if (type != MessageType.Call && type != MessageType.Signal) {
71       return false;
72     }
73 
74     auto pattern = MessagePattern(msg);
75     // import std.stdio; debug writeln("Handling ", pattern);
76 
77     if (pattern.iface == "org.freedesktop.DBus.Introspectable"
78         && pattern.method == "Introspect" && !pattern.signal) {
79       handleIntrospect(pattern.path, msg, conn);
80       return true;
81     }
82 
83     MessageHandler* handler = (pattern in callTable);
84     if (handler is null) {
85       return false;
86     }
87 
88     // Check for matching argument types
89     version (DDBusNoChecking) {
90 
91     } else {
92       if (!equal(join(handler.argSig), msg.signature())) {
93         return false;
94       }
95     }
96 
97     handler.func(msg, conn);
98     return true;
99   }
100 
101   void setHandler(Ret, Args...)(MessagePattern patt, Ret delegate(Args) handler) {
102     void handlerWrapper(Message call, Connection conn) {
103       Tuple!Args args = call.readTuple!(Tuple!Args)();
104       auto retMsg = call.createReturn();
105 
106       static if (!is(Ret == void)) {
107         Ret ret = handler(args.expand);
108         static if (is(Ret == Tuple!T, T...)) {
109           retMsg.build!T(ret.expand);
110         } else {
111           retMsg.build(ret);
112         }
113       } else {
114         handler(args.expand);
115       }
116 
117       if (!patt.signal) {
118         conn.send(retMsg);
119       }
120     }
121 
122     static string[] args = typeSigArr!Args;
123 
124     static if (is(Ret == void)) {
125       static string[] ret = [];
126     } else {
127       static string[] ret = typeSigReturn!Ret;
128     }
129 
130     // dfmt off
131     MessageHandler handleStruct = {
132       func: &handlerWrapper,
133       argSig: args,
134       retSig: ret
135     };
136     // dfmt on
137 
138     callTable[patt] = handleStruct;
139   }
140 
141   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">
142 <node name="%s">`;
143 
144   string introspectXML(string path) {
145     // dfmt off
146     auto methods = callTable
147       .byKey()
148       .filter!(a => (a.path == path) && !a.signal)
149       .array
150       .sort!((a, b) => a.iface < b.iface)();
151     // dfmt on
152 
153     auto ifaces = methods.groupBy();
154     auto app = appender!string;
155     formattedWrite(app, introspectHeader, path);
156     foreach (iface; ifaces) {
157       formattedWrite(app, `<interface name="%s">`, iface.front.iface);
158 
159       foreach (methodPatt; iface.array()) {
160         formattedWrite(app, `<method name="%s">`, methodPatt.method);
161         auto handler = callTable[methodPatt];
162 
163         foreach (arg; handler.argSig) {
164           formattedWrite(app, `<arg type="%s" direction="in"/>`, arg);
165         }
166 
167         foreach (arg; handler.retSig) {
168           formattedWrite(app, `<arg type="%s" direction="out"/>`, arg);
169         }
170 
171         app.put("</method>");
172       }
173 
174       app.put("</interface>");
175     }
176 
177     string childPath = path;
178     if (!childPath.endsWith("/")) {
179       childPath ~= "/";
180     }
181 
182     auto children = callTable.byKey()
183       .filter!(a => (a.path.startsWith(childPath)) && !a.signal)().map!(
184           (s) => s.path.chompPrefix(childPath)).map!((s) => s.findSplit("/")[0])
185       .array().sort().uniq();
186 
187     foreach (child; children) {
188       formattedWrite(app, `<node name="%s"/>`, child);
189     }
190 
191     app.put("</node>");
192     return app.data;
193   }
194 
195   void handleIntrospect(string path, Message call, Connection conn) {
196     auto retMsg = call.createReturn();
197     retMsg.build(introspectXML(path));
198     conn.sendBlocking(retMsg);
199   }
200 }
201 
202 extern (C) private DBusHandlerResult filterFunc(DBusConnection* dConn,
203     DBusMessage* dMsg, void* routerP) {
204   MessageRouter router = cast(MessageRouter) routerP;
205   dbus_message_ref(dMsg);
206   Message msg = Message(dMsg);
207   dbus_connection_ref(dConn);
208   Connection conn = Connection(dConn);
209   bool handled = router.handle(msg, conn);
210 
211   if (handled) {
212     return DBusHandlerResult.DBUS_HANDLER_RESULT_HANDLED;
213   } else {
214     return DBusHandlerResult.DBUS_HANDLER_RESULT_NOT_YET_HANDLED;
215   }
216 }
217 
218 extern (C) private void unrootUserData(void* userdata) {
219   GC.removeRoot(userdata);
220 }
221 
222 void registerRouter(Connection conn, MessageRouter router) {
223   void* routerP = cast(void*) router;
224   GC.addRoot(routerP);
225   dbus_connection_add_filter(conn.conn, &filterFunc, routerP, &unrootUserData);
226 }
227 
228 unittest {
229   import dunit.toolkit;
230 
231   import std.typecons : BitFlags;
232   import std.variant : Algebraic;
233 
234   auto router = new MessageRouter();
235   // set up test messages
236   MessagePattern patt = MessagePattern("/root", "ca.thume.test", "test");
237   router.setHandler!(int, int)(patt, (int p) { return 6; });
238   patt = MessagePattern("/root", "ca.thume.tester", "lolwut");
239   router.setHandler!(void, int, string)(patt, (int p, string p2) {  });
240   patt = MessagePattern("/root/wat", "ca.thume.tester", "lolwut");
241   router.setHandler!(int, int)(patt, (int p) { return 6; });
242   patt = MessagePattern("/root/bar", "ca.thume.tester", "lolwut");
243   router.setHandler!(Variant!DBusAny, int)(patt, (int p) {
244     return variant(DBusAny(p));
245   });
246   patt = MessagePattern("/root/foo", "ca.thume.tester", "lolwut");
247   router.setHandler!(Tuple!(string, string, int), int,
248       Variant!DBusAny)(patt, (int p, Variant!DBusAny any) {
249     Tuple!(string, string, int) ret;
250     ret[0] = "a";
251     ret[1] = "b";
252     ret[2] = p;
253     return ret;
254   });
255   patt = MessagePattern("/troll", "ca.thume.tester", "wow");
256   router.setHandler!(void)(patt, { return; });
257 
258   patt = MessagePattern("/root/fancy", "ca.thume.tester", "crazyTest");
259   enum F : ushort {
260     a = 1,
261     b = 8,
262     c = 16
263   }
264 
265   struct S {
266     byte b;
267     ulong ul;
268     F f;
269   }
270 
271   router.setHandler!(int)(patt, (Algebraic!(ushort, BitFlags!F, S) v) {
272     if (v.type is typeid(ushort) || v.type is typeid(BitFlags!F)) {
273       return v.coerce!int;
274     } else if (v.type is typeid(S)) {
275       auto s = v.get!S;
276       final switch (s.f) {
277       case F.a:
278         return s.b;
279       case F.b:
280         return cast(int) s.ul;
281       case F.c:
282         return cast(int) s.ul + s.b;
283       }
284     }
285 
286     assert(false);
287   });
288 
289   static assert(!__traits(compiles, {
290       patt = MessagePattern("/root/bar", "ca.thume.tester", "lolwut");
291       router.setHandler!(void, DBusAny)(patt, (DBusAny wrongUsage) { return; });
292     }));
293 
294   // TODO: these tests rely on nondeterministic hash map ordering
295   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">
296 <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="fancy"/><node name="foo"/><node name="wat"/></node>`;
297   router.introspectXML("/root").assertEqual(introspectResult);
298   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">
299 <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>`;
300   router.introspectXML("/root/foo").assertEqual(introspectResult2);
301   static string introspectResult3 = `<!DOCTYPE node PUBLIC "-//freedesktop//DTD D-BUS Object Introspection 1.0//EN" "http://www.freedesktop.org/standards/dbus/1.0/introspect.dtd">
302 <node name="/root/fancy"><interface name="ca.thume.tester"><method name="crazyTest"><arg type="v" direction="in"/><arg type="i" direction="out"/></method></interface></node>`;
303   router.introspectXML("/root/fancy").assertEqual(introspectResult3);
304   router.introspectXML("/")
305     .assertEndsWith(`<node name="/"><node name="root"/><node name="troll"/></node>`);
306 }