1 /// Thin OO wrapper around DBus types
2 module ddbus.thin;
3 
4 import core.time : Duration;
5 
6 import ddbus.attributes : isAllowedField;
7 import ddbus.c_lib;
8 import ddbus.conv;
9 import ddbus.exception : TypeMismatchException;
10 import ddbus.util;
11 
12 import std.meta : ApplyRight, Filter, staticIndexOf;
13 import std..string;
14 import std.typecons;
15 import std.exception;
16 import std.traits;
17 import std.conv;
18 import std.range;
19 import std.algorithm;
20 
21 // This import is public for backwards compatibility
22 public import ddbus.exception : wrapErrors, DBusException;
23 
24 struct ObjectPath {
25   private string _value;
26 
27   this(string objPath) pure @safe {
28     enforce(isValid(objPath));
29     _value = objPath;
30   }
31 
32   string toString() const {
33     return _value;
34   }
35 
36   /++
37     Returns the string representation of this ObjectPath.
38    +/
39   string value() const pure @nogc nothrow @safe {
40     return _value;
41   }
42 
43   size_t toHash() const pure @nogc nothrow @trusted {
44     return hashOf(_value);
45   }
46 
47   bool opEquals(ref const typeof(this) b) const pure @nogc nothrow @safe {
48     return _value == b._value;
49   }
50 
51   ObjectPath opBinary(string op : "~")(string rhs) const pure @safe {
52     if (!rhs.startsWith("/")) {
53       return opBinary!"~"(ObjectPath("/" ~ rhs));
54     } else {
55       return opBinary!"~"(ObjectPath(rhs));
56     }
57   }
58 
59   ObjectPath opBinary(string op : "~")(ObjectPath rhs) const pure @safe
60   in {
61     assert(ObjectPath.isValid(_value) && ObjectPath.isValid(rhs._value));
62   }
63   out (v) {
64     assert(ObjectPath.isValid(v._value));
65   }
66   do {
67     ObjectPath ret;
68 
69     if (_value == "/") {
70       ret._value = rhs._value;
71     } else {
72       ret._value = _value ~ rhs._value;
73     }
74 
75     return ret;
76   }
77 
78   void opOpAssign(string op : "~")(string rhs) pure @safe {
79     _value = opBinary!"~"(rhs)._value;
80   }
81 
82   void opOpAssign(string op : "~")(ObjectPath rhs) pure @safe {
83     _value = opBinary!"~"(rhs)._value;
84   }
85 
86   /++
87     Returns: `false` for empty strings or strings that don't match the
88     pattern `(/[0-9A-Za-z_]+)+|/`.
89    +/
90   static bool isValid(string objPath) pure @nogc nothrow @safe {
91     import std.ascii : isAlphaNum;
92 
93     if (!objPath.length) {
94       return false;
95     }
96 
97     if (objPath == "/") {
98       return true;
99     }
100 
101     if (objPath[0] != '/' || objPath[$ - 1] == '/') {
102       return false;
103     }
104 
105     // .representation to avoid unicode exceptions -> @nogc & nothrow
106     return objPath.representation.splitter('/').drop(1).all!(a => a.length
107         && a.all!(c => c.isAlphaNum || c == '_'));
108   }
109 }
110 
111 unittest {
112   import dunit.toolkit;
113 
114   ObjectPath("some.invalid/object_path").assertThrow();
115   ObjectPath("/path/with/TrailingSlash/").assertThrow();
116   ObjectPath("/path/without/TrailingSlash").assertNotThrown();
117   string path = "/org/freedesktop/DBus";
118   auto obj = ObjectPath(path);
119   obj.value.assertEqual(path);
120   obj.toHash().assertEqual(path.hashOf);
121 }
122 
123 unittest {
124   import dunit.toolkit;
125 
126   ObjectPath a = ObjectPath("/org/freedesktop");
127   a.assertEqual(ObjectPath("/org/freedesktop"));
128   a ~= ObjectPath("/UPower");
129   a.assertEqual(ObjectPath("/org/freedesktop/UPower"));
130   a ~= "Device";
131   a.assertEqual(ObjectPath("/org/freedesktop/UPower/Device"));
132   (a ~ "0").assertEqual(ObjectPath("/org/freedesktop/UPower/Device/0"));
133   a.assertEqual(ObjectPath("/org/freedesktop/UPower/Device"));
134 }
135 
136 /// Structure allowing typeless parameters
137 struct DBusAny {
138   /// DBus type of the value (never 'v'), see typeSig!T
139   int type;
140   /// Child signature for Arrays & Tuples
141   string signature;
142   /// If true, this value will get serialized as variant value, otherwise it is serialized like it wasn't in a DBusAny wrapper.
143   /// Same functionality as Variant!T but with dynamic types if true.
144   bool explicitVariant;
145 
146   union {
147     ///
148     byte int8;
149     ///
150     short int16;
151     ///
152     ushort uint16;
153     ///
154     int int32;
155     ///
156     uint uint32;
157     ///
158     long int64;
159     ///
160     ulong uint64;
161     ///
162     double float64;
163     ///
164     string str;
165     ///
166     bool boolean;
167     ///
168     ObjectPath obj;
169     ///
170     DBusAny[] array;
171     ///
172     alias tuple = array;
173     ///
174     DictionaryEntry!(DBusAny, DBusAny)* entry;
175     ///
176     ubyte[] binaryData;
177   }
178 
179   /++
180     Manually creates a DBusAny object using a type, signature and explicit
181     variant specifier.
182 
183     Direct use of this constructor from user code should be avoided.
184    +/
185   this(int type, string signature, bool explicit) {
186     this.type = type;
187     this.signature = signature;
188     this.explicitVariant = explicit;
189   }
190 
191   /++
192     Automatically creates a DBusAny object with fitting parameters from a D
193     type or Variant!T.
194 
195     Pass a `Variant!T` to make this an explicit variant.
196    +/
197   this(T)(T value) {
198     static if (is(T == byte) || is(T == ubyte)) {
199       this(typeCode!byte, null, false);
200       int8 = cast(byte) value;
201     } else static if (is(T == short)) {
202       this(typeCode!short, null, false);
203       int16 = cast(short) value;
204     } else static if (is(T == ushort)) {
205       this(typeCode!ushort, null, false);
206       uint16 = cast(ushort) value;
207     } else static if (is(T == int)) {
208       this(typeCode!int, null, false);
209       int32 = cast(int) value;
210     } else static if (is(T == uint)) {
211       this(typeCode!uint, null, false);
212       uint32 = cast(uint) value;
213     } else static if (is(T == long)) {
214       this(typeCode!long, null, false);
215       int64 = cast(long) value;
216     } else static if (is(T == ulong)) {
217       this(typeCode!ulong, null, false);
218       uint64 = cast(ulong) value;
219     } else static if (is(T == double)) {
220       this(typeCode!double, null, false);
221       float64 = cast(double) value;
222     } else static if (isSomeString!T) {
223       this(typeCode!string, null, false);
224       str = value.to!string;
225     } else static if (is(T == bool)) {
226       this(typeCode!bool, null, false);
227       boolean = cast(bool) value;
228     } else static if (is(T == ObjectPath)) {
229       this(typeCode!ObjectPath, null, false);
230       obj = value;
231     } else static if (is(T == Variant!R, R)) {
232       static if (is(R == DBusAny)) {
233         type = value.data.type;
234         signature = value.data.signature;
235         explicitVariant = true;
236         if (type == 'a' || type == 'r') {
237           if (signature == ['y']) {
238             binaryData = value.data.binaryData;
239           } else {
240             array = value.data.array;
241           }
242         } else if (type == 's') {
243           str = value.data.str;
244         } else if (type == 'e') {
245           entry = value.data.entry;
246         } else {
247           uint64 = value.data.uint64;
248         }
249       } else {
250         this(value.data);
251         explicitVariant = true;
252       }
253     } else static if (is(T : DictionaryEntry!(K, V), K, V)) {
254       this('e', null, false);
255       entry = new DictionaryEntry!(DBusAny, DBusAny)();
256       static if (is(K == DBusAny)) {
257         entry.key = value.key;
258       } else {
259         entry.key = DBusAny(value.key);
260       }
261       static if (is(V == DBusAny)) {
262         entry.value = value.value;
263       } else {
264         entry.value = DBusAny(value.value);
265       }
266     } else static if (is(T == ubyte[]) || is(T == byte[])) {
267       this('a', ['y'], false);
268       binaryData = cast(ubyte[]) value;
269     } else static if (isInputRange!T) {
270       this.type = 'a';
271 
272       static assert(!is(ElementType!T == DBusAny),
273           "Array must consist of the same type, use Variant!DBusAny or DBusAny(tuple(...)) instead");
274 
275       static assert(.typeSig!(ElementType!T) != "y");
276 
277       this.signature = .typeSig!(ElementType!T);
278       this.explicitVariant = false;
279 
280       foreach (elem; value) {
281         array ~= DBusAny(elem);
282       }
283     } else static if (isTuple!T) {
284       this.type = 'r';
285       this.signature = ['('];
286       this.explicitVariant = false;
287 
288       foreach (index, R; value.Types) {
289         auto var = DBusAny(value[index]);
290         tuple ~= var;
291 
292         if (var.explicitVariant) {
293           this.signature ~= 'v';
294         } else {
295           if (var.type != 'r') {
296             this.signature ~= cast(char) var.type;
297           }
298 
299           if (var.type == 'a' || var.type == 'r') {
300             this.signature ~= var.signature;
301           }
302         }
303       }
304 
305       this.signature ~= ')';
306     } else static if (is(T == struct) && canDBus!T) {
307       this.type = 'r';
308       this.signature = ['('];
309       this.explicitVariant = false;
310       foreach (index, R; Fields!T) {
311         static if (isAllowedField!(value.tupleof[index])) {
312           auto var = DBusAny(value.tupleof[index]);
313           tuple ~= var;
314           if (var.explicitVariant)
315             this.signature ~= 'v';
316           else {
317             if (var.type != 'r')
318               this.signature ~= cast(char) var.type;
319             if (var.type == 'a' || var.type == 'r')
320               this.signature ~= var.signature;
321           }
322         }
323       }
324       this.signature ~= ')';
325     } else static if (isAssociativeArray!T) {
326       this(value.byDictionaryEntries);
327     } else {
328       static assert(false, T.stringof ~ " not convertible to a Variant");
329     }
330   }
331 
332   ///
333   string toString() const {
334     string valueStr;
335     switch (type) {
336     case typeCode!byte:
337       valueStr = int8.to!string;
338       break;
339     case typeCode!short:
340       valueStr = int16.to!string;
341       break;
342     case typeCode!ushort:
343       valueStr = uint16.to!string;
344       break;
345     case typeCode!int:
346       valueStr = int32.to!string;
347       break;
348     case typeCode!uint:
349       valueStr = uint32.to!string;
350       break;
351     case typeCode!long:
352       valueStr = int64.to!string;
353       break;
354     case typeCode!ulong:
355       valueStr = uint64.to!string;
356       break;
357     case typeCode!double:
358       valueStr = float64.to!string;
359       break;
360     case typeCode!string:
361       valueStr = '"' ~ str ~ '"';
362       break;
363     case typeCode!ObjectPath:
364       valueStr = '"' ~ obj.to!string ~ '"';
365       break;
366     case typeCode!bool:
367       valueStr = boolean ? "true" : "false";
368       break;
369     case 'a':
370       import std.digest.digest : toHexString;
371 
372       if (signature == ['y']) {
373         valueStr = "binary(" ~ binaryData.toHexString ~ ')';
374       } else {
375         valueStr = '[' ~ array.map!(a => a.toString).join(", ") ~ ']';
376       }
377 
378       break;
379     case 'r':
380       valueStr = '(' ~ tuple.map!(a => a.toString).join(", ") ~ ')';
381       break;
382     case 'e':
383       valueStr = entry.key.toString ~ ": " ~ entry.value.toString;
384       break;
385     default:
386       valueStr = "unknown";
387       break;
388     }
389 
390     return "DBusAny(" ~ cast(char) type ~ ", \"" ~ signature.idup ~ "\", " ~ (explicitVariant
391         ? "explicit" : "implicit") ~ ", " ~ valueStr ~ ")";
392   }
393 
394   /++
395     Get the value stored in the DBusAny object.
396 
397     Parameters:
398       T = The requested type. The currently stored value must match the
399         requested type exactly.
400 
401     Returns:
402       The current value of the DBusAny object.
403 
404     Throws:
405       TypeMismatchException if the DBus type of the current value of the
406       DBusAny object is not the same as the DBus type used to represent T.
407   +/
408   T get(T)() @property const
409       if (staticIndexOf!(T, BasicTypes) >= 0) {
410     enforce(type == typeCode!T, new TypeMismatchException(
411         "Cannot get a " ~ T.stringof ~ " from a DBusAny with" ~ " a value of DBus type '" ~ typeSig ~ "'.",
412         typeCode!T, type));
413 
414     static if (isIntegral!T) {
415       enum memberName = (isUnsigned!T ? "uint" : "int") ~ (T.sizeof * 8).to!string;
416       return __traits(getMember, this, memberName);
417     } else static if (is(T == double)) {
418       return float64;
419     } else static if (is(T == string)) {
420       return str;
421     } else static if (is(T == ObjectPath)) {
422       return obj;
423     } else static if (is(T == bool)) {
424       return boolean;
425     } else {
426       static assert(false);
427     }
428   }
429 
430   /// ditto
431   T get(T)() @property const
432       if (is(T == const(DBusAny)[])) {
433     enforce((type == 'a' && signature != "y") || type == 'r', new TypeMismatchException(
434         "Cannot get a " ~ T.stringof ~ " from a DBusAny with" ~ " a value of DBus type '" ~ this.typeSig ~ "'.",
435         'a', type));
436 
437     return array;
438   }
439 
440   /// ditto
441   T get(T)() @property const
442       if (is(T == const(ubyte)[])) {
443     enforce(type == 'a' && signature == "y", new TypeMismatchException(
444         "Cannot get a " ~ T.stringof ~ " from a DBusAny with" ~ " a value of DBus type '" ~ this.typeSig ~ "'.",
445         'a', type));
446 
447     return binaryData;
448   }
449 
450   /// If the value is an array of DictionaryEntries this will return a HashMap
451   deprecated("Please use to!(V[K])") DBusAny[DBusAny] toAA() {
452     enforce(type == 'a' && signature && signature[0] == '{');
453     DBusAny[DBusAny] aa;
454 
455     foreach (val; array) {
456       enforce(val.type == 'e');
457       aa[val.entry.key] = val.entry.value;
458     }
459 
460     return aa;
461   }
462 
463   /++
464     Get the DBus type signature of the value stored in the DBusAny object.
465 
466     Returns:
467       The type signature of the value stored in this DBusAny object.
468    +/
469   string typeSig() @property const pure nothrow @safe {
470     if (type == 'a') {
471       return "a" ~ signature;
472     } else if (type == 'r') {
473       return signature;
474     } else if (type == 'e') {
475       return () @trusted{
476         return "{" ~ entry.key.signature ~ entry.value.signature ~ "}";
477       }();
478     } else {
479       return [cast(char) type];
480     }
481   }
482 
483   /++
484     Converts a basic type, a tuple or an array to the D type with type checking.
485 
486     Tuples can be converted to an array of DBusAny, but not to any other array.
487    +/
488   T to(T)() @property const pure {
489     // Just use `get` if possible
490     static if (canDBus!T && __traits(compiles, get!T)) {
491       if (this.typeSig == .typeSig!T)
492         return get!T;
493     }
494 
495     // If we get here, we need some type conversion
496     static if (is(T == Variant!R, R)) {
497       static if (is(R == DBusAny)) {
498         auto v = to!R;
499         v.explicitVariant = false;
500         return Variant!R(v);
501       } else {
502         return Variant!R(to!R);
503       }
504     } else static if (is(T == DBusAny)) {
505       return this;
506     } else {
507       // In here are all static if blocks that may fall through to the throw
508       // statement at the bottom of this block.
509 
510       static if (is(T == DictionaryEntry!(K, V), K, V)) {
511         if (type == 'e') {
512           static if (is(T == typeof(entry))) {
513             return entry;
514           } else {
515             return DictionaryEntry(entry.key.to!K, entry.value.to!V);
516           }
517         }
518       } else static if (isAssociativeArray!T) {
519         if (type == 'a' && (!array.length || array[0].type == 'e')) {
520           alias K = Unqual!(KeyType!T);
521           alias V = Unqual!(ValueType!T);
522           V[K] ret;
523 
524           foreach (pair; array) {
525             assert(pair.type == 'e');
526             ret[pair.entry.key.to!K] = pair.entry.value.to!V;
527           }
528 
529           return cast(T) ret;
530         }
531       } else static if (isDynamicArray!T && !isSomeString!T) {
532         alias E = Unqual!(ElementType!T);
533 
534         if (typeSig == "ay") {
535           auto data = get!(const(ubyte)[]);
536           static if (is(E == ubyte) || is(E == byte)) {
537             return cast(T) data.dup;
538           } else {
539             return cast(T) data.map!(elem => elem.to!E).array;
540           }
541         } else if (type == 'a' || (type == 'r' && is(E == DBusAny))) {
542           return cast(T) get!(const(DBusAny)[]).map!(elem => elem.to!E).array;
543         }
544       } else static if (isTuple!T) {
545         if (type == 'r') {
546           T ret;
547 
548           foreach (i, T; ret.Types) {
549             ret[i] = tuple[i].to!T;
550           }
551 
552           return ret;
553         }
554       } else static if (is(T == struct) && canDBus!T) {
555         if (type == 'r') {
556           T ret;
557           size_t j;
558 
559           foreach (i, F; Fields!T) {
560             static if (isAllowedField!(ret.tupleof[i])) {
561               ret.tupleof[i] = tuple[j++].to!F;
562             }
563           }
564 
565           return ret;
566         }
567       } else {
568         alias isPreciselyConvertible = ApplyRight!(isImplicitlyConvertible, T);
569 
570         template isUnpreciselyConvertible(S) {
571           enum isUnpreciselyConvertible = !isPreciselyConvertible!S
572               && __traits(compiles, get!S.to!T);
573         }
574 
575         // Try to be precise
576         foreach (B; Filter!(isPreciselyConvertible, BasicTypes)) {
577           if (type == typeCode!B)
578             return get!B;
579         }
580 
581         // Try to convert
582         foreach (B; Filter!(isUnpreciselyConvertible, BasicTypes)) {
583           if (type == typeCode!B)
584             return get!B.to!T;
585         }
586       }
587 
588       throw new ConvException("Cannot convert from DBus type '" ~ this.typeSig ~ "' to "
589           ~ T.stringof);
590     }
591   }
592 
593   bool opEquals(ref in DBusAny b) const {
594     if (b.type != type || b.explicitVariant != explicitVariant) {
595       return false;
596     }
597 
598     if ((type == 'a' || type == 'r') && b.signature != signature) {
599       return false;
600     }
601 
602     if (type == 'a' && signature == ['y']) {
603       return binaryData == b.binaryData;
604     }
605 
606     if (type == 'a') {
607       return array == b.array;
608     } else if (type == 'r') {
609       return tuple == b.tuple;
610     } else if (type == 's') {
611       return str == b.str;
612     } else if (type == 'o') {
613       return obj == b.obj;
614     } else if (type == 'e') {
615       return entry == b.entry || (entry && b.entry && *entry == *b.entry);
616     } else {
617       return uint64 == b.uint64;
618     }
619   }
620 }
621 
622 unittest {
623   import dunit.toolkit;
624 
625   DBusAny set(string member, T)(DBusAny v, T value) {
626     mixin("v." ~ member ~ " = value;");
627     return v;
628   }
629 
630   void test(T)(T value, DBusAny b) {
631     assertEqual(DBusAny(value), b);
632     assertEqual(b.to!T, value);
633     b.toString();
634   }
635 
636   test(cast(ubyte) 184, set!"int8"(DBusAny('y', null, false), cast(byte) 184));
637   test(cast(short) 184, set!"int16"(DBusAny('n', null, false), cast(short) 184));
638   test(cast(ushort) 184, set!"uint16"(DBusAny('q', null, false), cast(ushort) 184));
639   test(cast(int) 184, set!"int32"(DBusAny('i', null, false), cast(int) 184));
640   test(cast(uint) 184, set!"uint32"(DBusAny('u', null, false), cast(uint) 184));
641   test(cast(long) 184, set!"int64"(DBusAny('x', null, false), cast(long) 184));
642   test(cast(ulong) 184, set!"uint64"(DBusAny('t', null, false), cast(ulong) 184));
643   test(1.84, set!"float64"(DBusAny('d', null, false), 1.84));
644   test(true, set!"boolean"(DBusAny('b', null, false), true));
645   test("abc", set!"str"(DBusAny('s', null, false), "abc"));
646   test(ObjectPath("/foo/Bar"), set!"obj"(DBusAny('o', null, false), ObjectPath("/foo/Bar")));
647   test(cast(ubyte[])[1, 2, 3], set!"binaryData"(DBusAny('a', ['y'], false),
648       cast(ubyte[])[1, 2, 3]));
649 
650   test(variant(cast(ubyte) 184), set!"int8"(DBusAny('y', null, true), cast(byte) 184));
651   test(variant(cast(short) 184), set!"int16"(DBusAny('n', null, true), cast(short) 184));
652   test(variant(cast(ushort) 184), set!"uint16"(DBusAny('q', null, true), cast(ushort) 184));
653   test(variant(cast(int) 184), set!"int32"(DBusAny('i', null, true), cast(int) 184));
654   test(variant(cast(uint) 184), set!"uint32"(DBusAny('u', null, true), cast(uint) 184));
655   test(variant(cast(long) 184), set!"int64"(DBusAny('x', null, true), cast(long) 184));
656   test(variant(cast(ulong) 184), set!"uint64"(DBusAny('t', null, true), cast(ulong) 184));
657   test(variant(1.84), set!"float64"(DBusAny('d', null, true), 1.84));
658   test(variant(true), set!"boolean"(DBusAny('b', null, true), true));
659   test(variant("abc"), set!"str"(DBusAny('s', null, true), "abc"));
660   test(variant(ObjectPath("/foo/Bar")), set!"obj"(DBusAny('o', null, true),
661       ObjectPath("/foo/Bar")));
662   test(variant(cast(ubyte[])[1, 2, 3]), set!"binaryData"(DBusAny('a', ['y'],
663       true), cast(ubyte[])[1, 2, 3]));
664 
665   test(variant(DBusAny(5)), set!"int32"(DBusAny('i', null, true), 5));
666 
667   test([1, 2, 3], set!"array"(DBusAny('a', ['i'], false), [DBusAny(1), DBusAny(2), DBusAny(3)]));
668   test(variant([1, 2, 3]), set!"array"(DBusAny('a', ['i'], true), [DBusAny(1),
669       DBusAny(2), DBusAny(3)]));
670 
671   test(tuple("a", 4, [1, 2]), set!"tuple"(DBusAny('r', "(siai)".dup, false),
672       [DBusAny("a"), DBusAny(4), DBusAny([1, 2])]));
673   test(tuple("a", variant(4), variant([1, 2])), set!"tuple"(DBusAny('r',
674       "(svv)", false), [DBusAny("a"), DBusAny(variant(4)), DBusAny(variant([1, 2]))]));
675 
676   test(["a" : "b"], set!"array"(DBusAny('a', "{ss}", false),
677       [DBusAny(DictionaryEntry!(DBusAny, DBusAny)(DBusAny("a"), DBusAny("b")))]));
678   test([variant("a") : 4], set!"array"(DBusAny('a', "{vi}", false),
679       [DBusAny(DictionaryEntry!(DBusAny, DBusAny)(DBusAny(variant("a")), DBusAny(4)))]));
680 }
681 
682 /// Marks the data as variant on serialization
683 struct Variant(T) {
684   ///
685   T data;
686 }
687 
688 Variant!T variant(T)(T data) {
689   return Variant!T(data);
690 }
691 
692 enum MessageType {
693   Invalid = 0,
694   Call,
695   Return,
696   Error,
697   Signal
698 }
699 
700 struct Message {
701   DBusMessage* msg;
702 
703   this(string dest, string path, string iface, string method) {
704     msg = dbus_message_new_method_call(dest.toStringz(), path.toStringz(),
705         iface.toStringz(), method.toStringz());
706   }
707 
708   this(DBusMessage* m) {
709     msg = m;
710   }
711 
712   this(this) {
713     dbus_message_ref(msg);
714   }
715 
716   ~this() {
717     dbus_message_unref(msg);
718   }
719 
720   void build(TS...)(TS args)
721       if (allCanDBus!TS) {
722     DBusMessageIter iter;
723     dbus_message_iter_init_append(msg, &iter);
724     buildIter(&iter, args);
725   }
726 
727   /**
728      Reads the first argument of the message.
729      Note that this creates a new iterator every time so calling it multiple times will always
730      read the first argument. This is suitable for single item returns.
731      To read multiple arguments use readTuple.
732   */
733   T read(T)()
734       if (canDBus!T) {
735     DBusMessageIter iter;
736     dbus_message_iter_init(msg, &iter);
737     return readIter!T(&iter);
738   }
739 
740   alias read to;
741 
742   Tup readTuple(Tup)()
743       if (isTuple!Tup && allCanDBus!(Tup.Types)) {
744     DBusMessageIter iter;
745     dbus_message_iter_init(msg, &iter);
746     Tup ret;
747     readIterTuple(&iter, ret);
748     return ret;
749   }
750 
751   Message createReturn() {
752     return Message(dbus_message_new_method_return(msg));
753   }
754 
755   MessageType type() {
756     return cast(MessageType) dbus_message_get_type(msg);
757   }
758 
759   bool isCall() {
760     return type() == MessageType.Call;
761   }
762 
763   // Various string members
764   // TODO: make a mixin to avoid this copy-paste
765   string signature() {
766     const(char)* cStr = dbus_message_get_signature(msg);
767     assert(cStr != null);
768     return cStr.fromStringz().idup;
769   }
770 
771   string path() {
772     const(char)* cStr = dbus_message_get_path(msg);
773     assert(cStr != null);
774     return cStr.fromStringz().idup;
775   }
776 
777   string iface() {
778     const(char)* cStr = dbus_message_get_interface(msg);
779     assert(cStr != null);
780     return cStr.fromStringz().idup;
781   }
782 
783   string member() {
784     const(char)* cStr = dbus_message_get_member(msg);
785     assert(cStr != null);
786     return cStr.fromStringz().idup;
787   }
788 
789   string sender() {
790     const(char)* cStr = dbus_message_get_sender(msg);
791     assert(cStr != null);
792     return cStr.fromStringz().idup;
793   }
794 }
795 
796 unittest {
797   import dunit.toolkit;
798 
799   auto msg = Message("org.example.test", "/test", "org.example.testing", "testMethod");
800   msg.path().assertEqual("/test");
801 }
802 
803 struct Connection {
804   DBusConnection* conn;
805   this(DBusConnection* connection) {
806     conn = connection;
807   }
808 
809   this(this) {
810     dbus_connection_ref(conn);
811   }
812 
813   ~this() {
814     dbus_connection_unref(conn);
815   }
816 
817   void close() {
818     dbus_connection_close(conn);
819   }
820 
821   void send(Message msg) {
822     dbus_connection_send(conn, msg.msg, null);
823   }
824 
825   void sendBlocking(Message msg) {
826     send(msg);
827     dbus_connection_flush(conn);
828   }
829 
830   Message sendWithReplyBlocking(Message msg, int timeout = -1) {
831     DBusMessage* dbusMsg = msg.msg;
832     dbus_message_ref(dbusMsg);
833     DBusMessage* reply = wrapErrors((err) {
834       auto ret = dbus_connection_send_with_reply_and_block(conn, dbusMsg, timeout, err);
835       dbus_message_unref(dbusMsg);
836       return ret;
837     });
838     return Message(reply);
839   }
840 
841   Message sendWithReplyBlocking(Message msg, Duration timeout) {
842     return sendWithReplyBlocking(msg, timeout.total!"msecs"().to!int);
843   }
844 }
845 
846 unittest {
847   import dunit.toolkit;
848   import ddbus.attributes : dbusMarshaling, MarshalingFlag;
849 
850   struct S1 {
851     private int a;
852     private @(Yes.DBusMarshal) double b;
853     string s;
854   }
855 
856   @dbusMarshaling(MarshalingFlag.manualOnly)
857   struct S2 {
858     int h, i;
859     @(Yes.DBusMarshal) int j, k;
860     int* p;
861   }
862 
863   @dbusMarshaling(MarshalingFlag.includePrivateFields)
864   struct S3 {
865     private Variant!int c;
866     string d;
867     S1 e;
868     S2 f;
869     @(No.DBusMarshal) uint g;
870   }
871 
872   Message msg = Message("org.example.wow", "/wut", "org.test.iface", "meth3");
873 
874   __gshared int dummy;
875 
876   enum testStruct = S3(variant(5), "blah", S1(-7, 63.5, "test"), S2(84, -123,
877         78, 432, &dummy), 16);
878 
879   // Non-marshaled fields should appear as freshly initialized
880   enum expectedResult = S3(variant(5), "blah", S1(int.init, 63.5, "test"),
881         S2(int.init, int.init, 78, 432, null), uint.init);
882 
883   // Test struct conversion in building/reading messages
884   msg.build(testStruct);
885   msg.read!S3().assertEqual(expectedResult);
886 
887   // Test struct conversion in DBusAny
888   DBusAny(testStruct).to!S3.assertEqual(expectedResult);
889 }
890 
891 Connection connectToBus(DBusBusType bus = DBusBusType.DBUS_BUS_SESSION) {
892   DBusConnection* conn = wrapErrors((err) { return dbus_bus_get(bus, err); });
893   return Connection(conn);
894 }
895 
896 unittest {
897   import dunit.toolkit;
898 
899   // This test will only pass if DBus is installed.
900   Connection conn = connectToBus();
901   conn.conn.assertTruthy();
902   // We can only count on no system bus on OSX
903   version (OSX) {
904     connectToBus(DBusBusType.DBUS_BUS_SYSTEM).assertThrow!DBusException();
905   }
906 }