1 /// Thin OO wrapper around DBus types
2 module ddbus.thin;
3 
4 import ddbus.c_lib;
5 import ddbus.conv;
6 import ddbus.util;
7 import std.string;
8 import std.typecons;
9 import std.exception;
10 import std.traits;
11 import std.conv;
12 import std.range;
13 import std.algorithm;
14 
15 class DBusException : Exception {
16   this(DBusError *err) {
17     super(err.message.fromStringz().idup);
18   }
19 }
20 
21 T wrapErrors(T)(T delegate(DBusError *err) del) {
22   DBusError error;
23   dbus_error_init(&error);
24   T ret = del(&error);
25   if(dbus_error_is_set(&error)) {
26     auto ex = new DBusException(&error);
27     dbus_error_free(&error);
28     throw ex;
29   }
30   return ret;
31 }
32 
33 /// Structure allowing typeless parameters
34 struct DBusAny {
35   /// DBus type of the value (never 'v'), see typeSig!T
36   int type;
37   /// Child signature for Arrays & Tuples
38   char[] signature;
39   /// If true, this value will get serialized as variant value, otherwise it is serialized like it wasn't in a DBusAny wrapper.
40   /// Same functionality as Variant!T but with dynamic types if true.
41   bool explicitVariant;
42 
43   union
44   {
45     ///
46     byte int8;
47     ///
48     short int16;
49     ///
50     ushort uint16;
51     ///
52     int int32;
53     ///
54     uint uint32;
55     ///
56     long int64;
57     ///
58     ulong uint64;
59     ///
60     double float64;
61     ///
62     string str;
63     ///
64     bool boolean;
65     ///
66     DBusAny[] array;
67     ///
68     DBusAny[] tuple;
69     ///
70     DictionaryEntry!(DBusAny, DBusAny)* entry;
71     ///
72     ubyte[] binaryData;
73   }
74 
75   /// Manually creates a DBusAny object using a type, signature and implicit specifier.
76   this(int type, char[] signature, bool explicit) {
77     this.type = type;
78     this.signature = signature;
79     this.explicitVariant = explicit;
80   }
81 
82   /// Automatically creates a DBusAny object with fitting parameters from a D type or Variant!T.
83   /// Pass a `Variant!T` to make this an explicit variant.
84   this(T)(T value) {
85     static if(is(T == byte) || is(T == ubyte)) {
86       this(typeCode!byte, null, false);
87       int8 = cast(byte) value;
88     } else static if(is(T == short)) {
89       this(typeCode!short, null, false);
90       int16 = cast(short) value;
91     } else static if(is(T == ushort)) {
92       this(typeCode!ushort, null, false);
93       uint16 = cast(ushort) value;
94     } else static if(is(T == int)) {
95       this(typeCode!int, null, false);
96       int32 = cast(int) value;
97     } else static if(is(T == uint)) {
98       this(typeCode!uint, null, false);
99       uint32 = cast(uint) value;
100     } else static if(is(T == long)) {
101       this(typeCode!long, null, false);
102       int64 = cast(long) value;
103     } else static if(is(T == ulong)) {
104       this(typeCode!ulong, null, false);
105       uint64 = cast(ulong) value;
106     } else static if(is(T == double)) {
107       this(typeCode!double, null, false);
108       float64 = cast(double) value;
109     } else static if(isSomeString!T) {
110       this(typeCode!string, null, false);
111       str = value.to!string;
112     } else static if(is(T == bool)) {
113       this(typeCode!bool, null, false);
114       boolean = cast(bool) value;
115     } else static if(is(T == Variant!R, R)) {
116       static if(is(R == DBusAny)) {
117         type = value.data.type;
118         signature = value.data.signature;
119         explicitVariant = true;
120         if(type == 'a' || type == 'r') {
121           if(signature == ['y'])
122             binaryData = value.data.binaryData;
123           else
124             array = value.data.array;
125         } else if(type == 's')
126           str = value.data.str;
127         else if(type == 'e')
128           entry = value.data.entry;
129         else
130           uint64 = value.data.uint64;
131       } else {
132         this(value.data);
133         explicitVariant = true;
134       }
135     } else static if(is(T : DictionaryEntry!(K, V), K, V)) {
136       this('e', null, false);
137       entry = new DictionaryEntry!(DBusAny, DBusAny)();
138       static if(is(K == DBusAny))
139         entry.key = value.key;
140       else
141         entry.key = DBusAny(value.key);
142       static if(is(V == DBusAny))
143         entry.value = value.value;
144       else
145         entry.value = DBusAny(value.value);
146     } else static if(is(T == ubyte[]) || is(T == byte[])) {
147       this('a', ['y'], false);
148       binaryData = cast(ubyte[]) value;
149     } else static if(isInputRange!T) {
150       this.type = 'a';
151       static assert(!is(ElementType!T == DBusAny), "Array must consist of the same type, use Variant!DBusAny or DBusAny(tuple(...)) instead");
152       static assert(typeSig!(ElementType!T) != "y");
153       this.signature = typeSig!(ElementType!T).dup;
154       this.explicitVariant = false;
155       foreach(elem; value)
156         array ~= DBusAny(elem);
157     } else static if(isTuple!T) {
158       this.type = 'r';
159       this.signature = ['('];
160       this.explicitVariant = false;
161       foreach(index, R; value.Types) {
162         auto var = DBusAny(value[index]);
163         tuple ~= var;
164         if(var.explicitVariant)
165           this.signature ~= 'v';
166         else {
167           if (var.type != 'r')
168             this.signature ~= cast(char) var.type;
169           if(var.type == 'a' || var.type == 'r')
170             this.signature ~= var.signature;
171         }
172       }
173       this.signature ~= ')';
174     } else static if(isAssociativeArray!T) {
175       this(value.byDictionaryEntries);
176     } else static assert(false, T.stringof ~ " not convertible to a Variant");
177   }
178 
179   ///
180   string toString() const {
181     string valueStr;
182     switch(type) {
183     case typeCode!byte:
184       valueStr = int8.to!string;
185       break;
186     case typeCode!short:
187       valueStr = int16.to!string;
188       break;
189     case typeCode!ushort:
190       valueStr = uint16.to!string;
191       break;
192     case typeCode!int:
193       valueStr = int32.to!string;
194       break;
195     case typeCode!uint:
196       valueStr = uint32.to!string;
197       break;
198     case typeCode!long:
199       valueStr = int64.to!string;
200       break;
201     case typeCode!ulong:
202       valueStr = uint64.to!string;
203       break;
204     case typeCode!double:
205       valueStr = float64.to!string;
206       break;
207     case typeCode!string:
208       valueStr = '"' ~ str ~ '"';
209       break;
210     case typeCode!bool:
211       valueStr = boolean ? "true" : "false";
212       break;
213     case 'a':
214       import std.digest.digest : toHexString;
215 
216       if(signature == ['y'])
217         valueStr = "binary(" ~ binaryData.toHexString ~ ')';
218       else
219         valueStr = '[' ~ array.map!(a => a.toString).join(", ") ~ ']';
220       break;
221     case 'r':
222       valueStr = '(' ~ array.map!(a => a.toString).join(", ") ~ ')';
223       break;
224     case 'e':
225       valueStr = entry.key.toString ~ ": " ~ entry.value.toString;
226       break;
227     default:
228       valueStr = "unknown";
229       break;
230     }
231     return "DBusAny(" ~ cast(char) type
232       ~ ", \"" ~ signature.idup
233       ~ "\", " ~ (explicitVariant ? "explicit" : "implicit")
234       ~ ", " ~ valueStr ~ ")";
235   }
236 
237   /// If the value is an array of DictionaryEntries this will return a HashMap
238   DBusAny[DBusAny] toAA() {
239     enforce(type == 'a' && signature && signature[0] == '{');
240     DBusAny[DBusAny] aa;
241     foreach(val; array) {
242       enforce(val.type == 'e');
243       aa[val.entry.key] = val.entry.value;
244     }
245     return aa;
246   }
247 
248   /// Converts a basic type, a tuple or an array to the D type with type checking. Tuples can get converted to an array too.
249   T to(T)() {
250     static if(is(T == Variant!R, R)) {
251       static if(is(R == DBusAny)) {
252         auto v = to!R;
253         v.explicitVariant = false;
254         return Variant!R(v);
255       } else
256         return Variant!R(to!R);
257     } else static if(is(T == DBusAny)) {
258       return this;
259     } else static if(isIntegral!T || isFloatingPoint!T) {
260       switch(type) {
261       case typeCode!byte:
262         return cast(T) int8;
263       case typeCode!short:
264         return cast(T) int16;
265       case typeCode!ushort:
266         return cast(T) uint16;
267       case typeCode!int:
268         return cast(T) int32;
269       case typeCode!uint:
270         return cast(T) uint32;
271       case typeCode!long:
272         return cast(T) int64;
273       case typeCode!ulong:
274         return cast(T) uint64;
275       case typeCode!double:
276         return cast(T) float64;
277       default:
278         throw new Exception("Can't convert type " ~ cast(char) type ~ " to " ~ T.stringof);
279       }
280     } else static if(is(T == bool)) {
281       if(type == 'b')
282         return boolean;
283       else
284         throw new Exception("Can't convert type " ~ cast(char) type ~ " to " ~ T.stringof);
285     } else static if(isSomeString!T) {
286       if(type == 's')
287         return str.to!T;
288       else
289         throw new Exception("Can't convert type " ~ cast(char) type ~ " to " ~ T.stringof);
290     } else static if(isDynamicArray!T) {
291       if(type != 'a' && type != 'r')
292         throw new Exception("Can't convert type " ~ cast(char) type ~ " to an array");
293       T ret;
294       if(signature == ['y']) {
295         static if(isIntegral!(ElementType!T))
296           foreach(elem; binaryData)
297             ret ~= elem.to!(ElementType!T);
298       } else
299         foreach(elem; array)
300           ret ~= elem.to!(ElementType!T);
301       return ret;
302     } else static if(isTuple!T) {
303       if(type != 'r')
304         throw new Exception("Can't convert type " ~ cast(char) type ~ " to " ~ T.stringof);
305       T ret;
306       enforce(ret.Types.length == tuple.length, "Tuple length mismatch");
307       foreach(index, T; ret.Types)
308         ret[index] = tuple[index].to!T;
309       return ret;
310     } else static if(isAssociativeArray!T) {
311       if(type != 'a' || !signature || signature[0] != '{')
312         throw new Exception("Can't convert type " ~ cast(char) type ~ " to " ~ T.stringof);
313       T ret;
314       foreach(pair; array) {
315         enforce(pair.type == 'e');
316         ret[pair.entry.key.to!(KeyType!T)] = pair.entry.value.to!(ValueType!T);
317       }
318       return ret;
319     } else static assert(false, "Can't convert variant to " ~ T.stringof);
320   }
321 
322   bool opEquals(ref in DBusAny b) const {
323     if(b.type != type || b.explicitVariant != explicitVariant)
324       return false;
325     if((type == 'a' || type == 'r') && b.signature != signature)
326       return false;
327     if(type == 'a' && signature == ['y'])
328       return binaryData == b.binaryData;
329     if(type == 'a')
330       return array == b.array;
331     else if(type == 'r')
332       return tuple == b.tuple;
333     else if(type == 's')
334       return str == b.str;
335     else if(type == 'e')
336       return entry == b.entry || (entry && b.entry && *entry == *b.entry);
337     else
338       return uint64 == b.uint64;
339   }
340 }
341 
342 unittest {
343   import dunit.toolkit;
344   DBusAny set(string member, T)(DBusAny v, T value) {
345     mixin("v." ~ member ~ " = value;");
346     return v;
347   }
348 
349   void test(T)(T value, DBusAny b) {
350     assertEqual(DBusAny(value), b);
351     assertEqual(b.to!T, value);
352     b.toString();
353   }
354 
355   test(cast(ubyte) 184, set!"int8"(DBusAny('y', null, false), cast(byte) 184));
356   test(cast(short) 184, set!"int16"(DBusAny('n', null, false), cast(short) 184));
357   test(cast(ushort) 184, set!"uint16"(DBusAny('q', null, false), cast(ushort) 184));
358   test(cast(int) 184, set!"int32"(DBusAny('i', null, false), cast(int) 184));
359   test(cast(uint) 184, set!"uint32"(DBusAny('u', null, false), cast(uint) 184));
360   test(cast(long) 184, set!"int64"(DBusAny('x', null, false), cast(long) 184));
361   test(cast(ulong) 184, set!"uint64"(DBusAny('t', null, false), cast(ulong) 184));
362   test(true, set!"boolean"(DBusAny('b', null, false), true));
363   test(cast(ubyte[]) [1, 2, 3], set!"binaryData"(DBusAny('a', ['y'], false), cast(ubyte[]) [1, 2, 3]));
364 
365   test(variant(cast(ubyte) 184), set!"int8"(DBusAny('y', null, true), cast(byte) 184));
366   test(variant(cast(short) 184), set!"int16"(DBusAny('n', null, true), cast(short) 184));
367   test(variant(cast(ushort) 184), set!"uint16"(DBusAny('q', null, true), cast(ushort) 184));
368   test(variant(cast(int) 184), set!"int32"(DBusAny('i', null, true), cast(int) 184));
369   test(variant(cast(uint) 184), set!"uint32"(DBusAny('u', null, true), cast(uint) 184));
370   test(variant(cast(long) 184), set!"int64"(DBusAny('x', null, true), cast(long) 184));
371   test(variant(cast(ulong) 184), set!"uint64"(DBusAny('t', null, true), cast(ulong) 184));
372   test(variant(true), set!"boolean"(DBusAny('b', null, true), true));
373   test(variant(cast(ubyte[]) [1, 2, 3]), set!"binaryData"(DBusAny('a', ['y'], true), cast(ubyte[]) [1, 2, 3]));
374 
375   test(variant(DBusAny(5)), set!"int32"(DBusAny('i', null, true), 5));
376 
377   test([1, 2, 3], set!"array"(DBusAny('a', ['i'], false), [DBusAny(1), DBusAny(2), DBusAny(3)]));
378   test(variant([1, 2, 3]), set!"array"(DBusAny('a', ['i'], true), [DBusAny(1), DBusAny(2), DBusAny(3)]));
379 
380   test(tuple("a", 4, [1, 2]), set!"tuple"(DBusAny('r', "(siai)".dup, false), [DBusAny("a"), DBusAny(4), DBusAny([1, 2])]));
381   test(tuple("a", variant(4), variant([1, 2])), set!"tuple"(DBusAny('r', "(svv)".dup, false), [DBusAny("a"), DBusAny(variant(4)), DBusAny(variant([1, 2]))]));
382 
383   test(["a": "b"], set!"array"(DBusAny('a', "{ss}".dup, false), [DBusAny(DictionaryEntry!(DBusAny, DBusAny)(DBusAny("a"), DBusAny("b")))]));
384   test([variant("a"): 4], set!"array"(DBusAny('a', "{vi}".dup, false), [DBusAny(DictionaryEntry!(DBusAny, DBusAny)(DBusAny(variant("a")), DBusAny(4)))]));
385 }
386 
387 /// Marks the data as variant on serialization
388 struct Variant(T) {
389   ///
390   T data;
391 }
392 
393 Variant!T variant(T)(T data) {
394   return Variant!T(data);
395 }
396 
397 enum MessageType {
398   Invalid = 0,
399   Call, Return, Error, Signal
400 }
401 
402 struct Message {
403   DBusMessage *msg;
404 
405   this(string dest, string path, string iface, string method) {
406     msg = dbus_message_new_method_call(dest.toStringz(), path.toStringz(), iface.toStringz(), method.toStringz());
407   }
408 
409   this(DBusMessage *m) {
410     msg = m;
411   }
412 
413   this(this) {
414     dbus_message_ref(msg);
415   }
416 
417   ~this() {
418     dbus_message_unref(msg);
419   }
420 
421   void build(TS...)(TS args) if(allCanDBus!TS) {
422     DBusMessageIter iter;
423     dbus_message_iter_init_append(msg, &iter);
424     buildIter(&iter, args);
425   }
426 
427   /**
428      Reads the first argument of the message.
429      Note that this creates a new iterator every time so calling it multiple times will always
430      read the first argument. This is suitable for single item returns.
431      To read multiple arguments use readTuple.
432   */
433   T read(T)() if(canDBus!T) {
434     DBusMessageIter iter;
435     dbus_message_iter_init(msg, &iter);
436     return readIter!T(&iter);
437   }
438   alias read to;
439 
440   Tup readTuple(Tup)() if(isTuple!Tup && allCanDBus!(Tup.Types)) {
441     DBusMessageIter iter;
442     dbus_message_iter_init(msg, &iter);
443     Tup ret;
444     readIterTuple(&iter, ret);
445     return ret;
446   }
447 
448   Message createReturn() {
449     return Message(dbus_message_new_method_return(msg));
450   }
451 
452   MessageType type() {
453     return cast(MessageType)dbus_message_get_type(msg);
454   }
455 
456   bool isCall() {
457     return type() == MessageType.Call;
458   }
459 
460   // Various string members
461   // TODO: make a mixin to avoid this copy-paste
462   string signature() {
463     const(char)* cStr = dbus_message_get_signature(msg);
464     assert(cStr != null);
465     return cStr.fromStringz().idup;
466   }
467   string path() {
468     const(char)* cStr = dbus_message_get_path(msg);
469     assert(cStr != null);
470     return cStr.fromStringz().idup;
471   }
472   string iface() {
473     const(char)* cStr = dbus_message_get_interface(msg);
474     assert(cStr != null);
475     return cStr.fromStringz().idup;
476   }
477   string member() {
478     const(char)* cStr = dbus_message_get_member(msg);
479     assert(cStr != null);
480     return cStr.fromStringz().idup;
481   }
482   string sender() {
483     const(char)* cStr = dbus_message_get_sender(msg);
484     assert(cStr != null);
485     return cStr.fromStringz().idup;
486   }
487 }
488 
489 unittest {
490   import dunit.toolkit;
491   auto msg = Message("org.example.test", "/test","org.example.testing","testMethod");
492   msg.path().assertEqual("/test");
493 }
494 
495 struct Connection {
496   DBusConnection *conn;
497   this(DBusConnection *connection) {
498     conn = connection;
499   }
500 
501   this(this) {
502     dbus_connection_ref(conn);
503   }
504 
505   ~this() {
506     dbus_connection_unref(conn);
507   }
508 
509   void close() {
510     dbus_connection_close(conn);
511   }
512 
513   void send(Message msg) {
514     dbus_connection_send(conn,msg.msg, null);
515   }
516 
517   void sendBlocking(Message msg) {
518     send(msg);
519     dbus_connection_flush(conn);
520   }
521 
522   Message sendWithReplyBlocking(Message msg, int timeout = -1) {
523     DBusMessage *dbusMsg = msg.msg;
524     dbus_message_ref(dbusMsg);
525     DBusMessage *reply = wrapErrors((err) {
526         auto ret = dbus_connection_send_with_reply_and_block(conn,dbusMsg,timeout,err);
527         dbus_message_unref(dbusMsg);
528         return ret;
529       });
530     return Message(reply);
531   }
532 }
533 
534 Connection connectToBus(DBusBusType bus = DBusBusType.DBUS_BUS_SESSION) {
535   DBusConnection *conn = wrapErrors((err) { return dbus_bus_get(bus,err); });
536   return Connection(conn);
537 }
538 
539 unittest {
540   import dunit.toolkit;
541   // This test will only pass if DBus is installed.
542   Connection conn = connectToBus();
543   conn.conn.assertTruthy();
544   // We can only count on no system bus on OSX
545   version(OSX) {
546     connectToBus(DBusBusType.DBUS_BUS_SYSTEM).assertThrow!DBusException();
547   }
548 }