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