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