New Upstream Release - ruby-dbus

Ready changes

Summary

Merged new upstream version: 0.22.1 (was: 0.20.0).

Resulting package

Built on 2023-05-20T06:44 (took 4m44s)

The resulting binary packages can be installed (if you have the apt repository enabled) by running one of:

apt install -t fresh-releases ruby-dbus

Lintian Result

Diff

diff --git a/NEWS.md b/NEWS.md
index 147d60b..5154f3e 100644
--- a/NEWS.md
+++ b/NEWS.md
@@ -2,6 +2,227 @@
 
 ## Unreleased
 
+## Ruby D-Bus 0.22.1 - 2023-05-17
+
+Bug fixes:
+ * Fix OBS building by disabling IPv6 tests, [#134][].
+
+[#134]: https://github.com/mvidner/ruby-dbus/pull/134
+
+## Ruby D-Bus 0.22.0 - 2023-05-08
+
+Features:
+ * Enable using nokogiri without rexml (by Dominik Andreas Schorpp, [#132][])
+
+Bug fixes:
+ * Respect DBUS_SYSTEM_BUS_ADDRESS environment variable.
+
+Other:
+ * For NameRequestError, mention who is the other owner.
+ * Session bus autolaunch still does not work, but: don't try launchd except
+   on macOS, and improve the error message.
+ * examples/gdbus split off to its own repository,
+   https://github.com/mvidner/dbus-gui-gtk
+
+[#132]: https://github.com/mvidner/ruby-dbus/pull/132
+
+## Ruby D-Bus 0.21.0 - 2023-04-08
+
+Features:
+ * Respect env RUBY_DBUS_ENDIANNESS=B (or =l) for outgoing messages.
+
+Bug fixes:
+ * Reduce socket buffer allocations ([#129][]).
+ * Message#marshall speedup: don't marshall the body twice.
+
+[#129]: https://github.com/mvidner/ruby-dbus/pull/129
+
+## Ruby D-Bus 0.20.0 - 2023-03-21
+
+Features:
+ * For EXTERNAL authentication, try also without the user id, to work with
+   containers ([#126][]).
+ * Thread safety, as long as the non-main threads only send signals.
+
+[#126]: https://github.com/mvidner/ruby-dbus/issues/126
+
+## Ruby D-Bus 0.19.0 - 2023-01-18
+
+API:
+ * Added a ObjectManager mix-in to implement the service-side
+   [ObjectManager][objmgr] interface.
+
+Bug fixes:
+ * dbus_attr_accessor and friends validate the signature ([#120][]).
+ * Declare the Introspectable interface in exported objects([#99][]).
+ * Do reply with an error when calling a nonexisting object
+   with an existing path prefix([#121][]).
+
+[#120]: https://github.com/mvidner/ruby-dbus/issues/120
+[#99]: https://github.com/mvidner/ruby-dbus/issues/99
+[#121]: https://github.com/mvidner/ruby-dbus/issues/121
+[objmgr]: https://dbus.freedesktop.org/doc/dbus-specification.html#standard-interfaces-objectmanager
+
+## Ruby D-Bus 0.18.1 - 2022-07-13
+
+No changes since 0.18.0.beta8.
+Repeating the most important changes since 0.17.0:
+
+API:
+ * Introduced DBus::Data classes, use them in Properties.Get,
+   Properties.GetAll to return correct types as declared ([#97][]).
+ * Introduced Object#dbus_properties_changed to send correctly typed property
+   values ([#115][]). Avoid calling PropertiesChanged directly as it will
+   guess the types.
+ * Service side `emits_changed_signal` to control emission of
+   PropertiesChanged: can be assigned within `dbus_interface` or as an option
+   when declaring properties ([#117][]).
+ * DBus.variant(type, value) is deprecated in favor of
+   Data::Variant.new(value, member_type:)
+ * Added type factories
+   * Type::Array[type]
+   * Type::Hash[key_type, value_type]
+   * Type::Struct[type1, type2...]
+
+Bug fixes:
+ * Fix Object.dbus_reader to work with attr_accessor and automatically produce
+   dbus_properties_changed for properties that are read-write at
+   implementation side and read-only at D-Bus side ([#96][])
+ * Service-side properties: Fix Properties.Get, Properties.GetAll
+   to use the specific property signature, not the generic
+   Variant ([#97][], [#105][], [#109][]).
+ * Client-side properties: When calling Properties.Set in
+   ProxyObjectInterface#[]=, use the correct type ([#108][]).
+ * Added thorough tests (`spec/data/marshall.yaml`) to detect nearly all
+   invalid data at unmarshalling time.
+
+Requirements:
+ * Require Ruby 2.4, because of RuboCop 1.0.
+
+## Ruby D-Bus 0.18.0.beta8 - 2022-06-21
+
+Bug fixes:
+ * Introduced Object#dbus_properties_changed to send correctly typed property
+   values ([#115][]). Avoid calling PropertiesChanged directly as it will
+   guess the types.
+ * Fix Object.dbus_reader to work with attr_accessor and automatically produce
+   dbus_properties_changed for properties that are read-write at
+   implementation side and read-only at D-Bus side ([#96][])
+
+[#96]: https://github.com/mvidner/ruby-dbus/issues/96
+
+API:
+ * Service side `emits_changed_signal` to control emission of
+   PropertiesChanged: can be assigned within `dbus_interface` or as an option
+   when declaring properties ([#117][]).
+
+[#115]: https://github.com/mvidner/ruby-dbus/issues/115
+[#117]: https://github.com/mvidner/ruby-dbus/pull/117
+
+## Ruby D-Bus 0.18.0.beta7 - 2022-05-29
+
+API:
+ * DBus.variant(type, value) is deprecated in favor of
+   Data::Variant.new(value, member_type:)
+
+Bug fixes:
+ * Client-side properties: When calling Properties.Set in
+   ProxyObjectInterface#[]=, use the correct type ([#108][]).
+
+[#108]: https://github.com/mvidner/ruby-dbus/issues/108
+
+## Ruby D-Bus 0.18.0.beta6 - 2022-05-25
+
+API:
+ * Data::Base#value returns plain Ruby types;
+   Data::Container#exact_value contains Data::Base ([#114][]).
+ * Data::Base#initialize and .from_typed allow plain or exact values, validate
+   argument types.
+ * Implement #== (converting) and #eql? (strict) for Data::Base and DBus::Type.
+
+[#114]: https://github.com/mvidner/ruby-dbus/pull/114
+
+## Ruby D-Bus 0.18.0.beta5 - 2022-04-27
+
+API:
+ * DBus::Type instances are frozen.
+ * Data::Container classes (Array, Struct, DictEntry, but not Variant)
+   constructors (#initialize, .from_items, .from_typed) changed to have
+   a *type* argument instead of *member_type* or *member_types*.
+ * Added type factories
+   * Type::Array[type]
+   * Type::Hash[key_type, value_type]
+   * Type::Struct[type1, type2...]
+
+Bug fixes:
+ * Properties containing Variants would return them doubly wrapped ([#111][]).
+
+[#111]: https://github.com/mvidner/ruby-dbus/pull/111
+
+## Ruby D-Bus 0.18.0.beta4 - 2022-04-21
+
+Bug fixes:
+ * Service-side properties: Fix Properties.Get, Properties.GetAll for
+   properties that contain arrays, on other than outermost level ([#109][]).
+ * Sending variants: fixed make_variant to correctly guess the signature
+   for UInt64 and number-keyed hashes/dictionaries.
+
+[#109]: https://github.com/mvidner/ruby-dbus/pull/109
+
+## Ruby D-Bus 0.18.0.beta3 - 2022-04-10
+
+Bug fixes:
+ * Service-side properties: Fix Properties.Get, Properties.GetAll for Array,
+   Dict, and Variant types ([#105][]).
+
+[#105]: https://github.com/mvidner/ruby-dbus/pull/105
+
+## Ruby D-Bus 0.18.0.beta2 - 2022-04-04
+
+API:
+ * Renamed the DBus::Type::Type class to DBus::Type
+   (which was previously a module).
+ * Introduced DBus::Data classes, use them in Properties.Get,
+   Properties.GetAll to return correct types as declared (still [#97][]).
+
+Bug fixes:
+ * Signature validation: Ensure DBus.type produces a valid Type
+ * Detect more malformed messages: non-NUL padding bytes, variants with
+   multiple or no value.
+ * Added thorough tests (`spec/data/marshall.yaml`) to detect nearly all
+   invalid data at unmarshalling time.
+
+## Ruby D-Bus 0.18.0.beta1 - 2022-02-24
+
+API:
+ * D-Bus structs have been passed as Ruby arrays. Now these arrays are frozen.
+ * Ruby structs can be used as D-Bus structs.
+
+Bug fixes:
+ * Returning the value for o.fd.DBus.Properties.Get, use the specific property
+   signature, not the generic Variant ([#97][]).
+
+Requirements:
+ * Require Ruby 2.4, because of RuboCop 1.0.
+
+[#97]: https://github.com/mvidner/ruby-dbus/issues/97
+
+## Ruby D-Bus 0.17.0 - 2022-02-11
+
+API:
+ * Export properties with `dbus_attr_accessor`, `dbus_reader` etc. ([#86][]).
+
+Bug fixes:
+ * Depend on rexml which is separate since Ruby 3.0 ([#87][],
+   by Toshiaki Asai).
+   Nokogiri is faster but bigger so it remains optional.
+ * Fix connection in case ~/.dbus-keyrings has multiple cookies, showing
+   as "Oops: undefined method `zero?' for nil:NilClass".
+ * Add the missing name to the root introspection node.
+
+[#86]: https://github.com/mvidner/ruby-dbus/pull/86
+[#87]: https://github.com/mvidner/ruby-dbus/pull/87
+
 ## Ruby D-Bus 0.16.0 - 2019-10-15
 
 API:
@@ -264,7 +485,7 @@ Bug fixes:
  * Handle more ways which tell us that a bus connection has died.
 
 [#3]: https://github.com/mvidner/ruby-dbus/issue/3
-[bsc#617350]: https://bugzilla.novell.com/show_bug.cgi?id=617350
+[bsc#617350]: https://bugzilla.suse.com/show_bug.cgi?id=617350
 
 ## Ruby D-Bus 0.3.0 - 2010-03-28
 
@@ -332,8 +553,8 @@ Bug fixes:
  * Fixed an endless sleep in DBus::Main.run ([bsc#537401][]).
  * Added details to PacketMarshaller exceptions ([bsc#538050][]).
 
-[bsc#537401]: https://bugzilla.novell.com/show_bug.cgi?id=537401
-[bsc#538050]: https://bugzilla.novell.com/show_bug.cgi?id=538050
+[bsc#537401]: https://bugzilla.suse.com/show_bug.cgi?id=537401
+[bsc#538050]: https://bugzilla.suse.com/show_bug.cgi?id=538050
 
 ## Ruby D-Bus "I'm not dead" 0.2.9 - 2009-08-26
 
diff --git a/README.md b/README.md
index c3d3286..1a4df00 100644
--- a/README.md
+++ b/README.md
@@ -11,14 +11,13 @@ Ruby D-Bus is a pure Ruby library for writing clients and services for D-Bus.
 [![Coverage Status][CS img]][Coverage Status]
 
 [Gem Version]: https://rubygems.org/gems/ruby-dbus
-[Build Status]: https://travis-ci.org/mvidner/ruby-dbus
-[travis pull requests]: https://travis-ci.org/mvidner/ruby-dbus/pull_requests
+[Build Status]: https://github.com/mvidner/ruby-dbus/actions?query=branch%3Amaster
 [Dependency Status]: https://gemnasium.com/mvidner/ruby-dbus
 [Code Climate]: https://codeclimate.com/github/mvidner/ruby-dbus
 [Coverage Status]: https://coveralls.io/r/mvidner/ruby-dbus
 
 [GV img]: https://badge.fury.io/rb/ruby-dbus.png
-[BS img]: https://travis-ci.org/mvidner/ruby-dbus.png?branch=master
+[BS img]: https://github.com/mvidner/ruby-dbus/workflows/CI/badge.svg?branch=master
 [DS img]: https://gemnasium.com/mvidner/ruby-dbus.png
 [CC img]: https://codeclimate.com/github/mvidner/ruby-dbus.png
 [CS img]: https://coveralls.io/repos/mvidner/ruby-dbus/badge.png?branch=master
@@ -32,7 +31,6 @@ via [UPower](http://upower.freedesktop.org/docs/UPower.html#UPower:OnBattery)
     sysbus = DBus.system_bus
     upower_service   = sysbus["org.freedesktop.UPower"]
     upower_object    = upower_service["/org/freedesktop/UPower"]
-    upower_object.introspect
     upower_interface = upower_object["org.freedesktop.UPower"]
     on_battery       = upower_interface["OnBattery"]
     if on_battery
@@ -43,7 +41,7 @@ via [UPower](http://upower.freedesktop.org/docs/UPower.html#UPower:OnBattery)
 
 ## Requirements
 
-- Ruby 2.0 or newer.
+- Ruby 2.4 or newer.
 
 
 ## Installation
diff --git a/Rakefile b/Rakefile
index ff421c2..2700379 100755
--- a/Rakefile
+++ b/Rakefile
@@ -1,7 +1,8 @@
 #! /usr/bin/env ruby
+# frozen_string_literal: true
+
 require "rake"
 require "fileutils"
-include FileUtils
 require "tmpdir"
 require "rspec/core/rake_task"
 begin
@@ -9,6 +10,12 @@ begin
 rescue LoadError
   nil
 end
+begin
+  require "yard"
+rescue LoadError
+  nil
+end
+
 require "packaging"
 
 Packaging.configuration do |conf|
@@ -38,12 +45,6 @@ RSpec::Core::RakeTask.new("bare:spec")
   end
 end
 
-if ENV["TRAVIS"]
-  require "coveralls/rake/task"
-  Coveralls::RakeTask.new
-  task default: "coveralls:push"
-end
-
 # remove tarball implementation and create gem for this gemfile
 Rake::Task[:tarball].clear
 
@@ -68,4 +69,13 @@ namespace :doc do
   end
 end
 
-RuboCop::RakeTask.new if Object.const_defined? :RuboCop
+if Object.const_defined? :RuboCop
+  RuboCop::RakeTask.new
+else
+  desc "Run RuboCop (dummy)"
+  task :rubocop do
+    warn "RuboCop not installed"
+  end
+end
+
+YARD::Rake::YardocTask.new if Object.const_defined? :YARD
diff --git a/VERSION b/VERSION
index 04a373e..a723ece 100644
--- a/VERSION
+++ b/VERSION
@@ -1 +1 @@
-0.16.0
+0.22.1
diff --git a/debian/changelog b/debian/changelog
index c220fae..4d8f63c 100644
--- a/debian/changelog
+++ b/debian/changelog
@@ -1,11 +1,14 @@
-ruby-dbus (0.16.0-2) UNRELEASED; urgency=medium
+ruby-dbus (0.22.1-1) UNRELEASED; urgency=medium
 
   * Update watch file format version to 4.
   * Apply multi-arch hints.
     + ruby-dbus: Add :any qualifier for ruby dependency.
   * Update standards version to 4.5.1, no changes needed.
+  * New upstream release.
+  * New upstream release.
+  * New upstream release.
 
- -- Debian Janitor <janitor@jelmer.uk>  Tue, 19 Jan 2021 22:41:27 -0000
+ -- Debian Janitor <janitor@jelmer.uk>  Sat, 20 May 2023 06:40:44 -0000
 
 ruby-dbus (0.16.0-1) unstable; urgency=medium
 
diff --git a/doc/Reference.md b/doc/Reference.md
index 28b8d2d..3757f0f 100644
--- a/doc/Reference.md
+++ b/doc/Reference.md
@@ -153,7 +153,7 @@ To receive signals for a specific object and interface, use
 
 D-Bus booleans, numbers, strings, arrays and dictionaries become their straightforward Ruby counterparts.
 
-Structs become arrays.
+Structs become frozen arrays.
 
 Object paths become strings.
 
@@ -166,12 +166,21 @@ D-Bus has stricter typing than Ruby, so the library must decide
 which D-Bus type to choose. Most of the time the choice is dictated
 by the D-Bus signature.
 
+For exact representation of D-Bus data types, use subclasses
+of {DBus::Data::Base}, such as {DBus::Data::Int16} or {DBus::Data::UInt64}.
+
 ##### Variants
 
 If the signature expects a Variant
 (which is the case for all Properties!) then an explicit mechanism is needed.
 
-1. A pair [{DBus::Type::Type}, value] specifies to marshall *value* as
+1. Any {DBus::Data::Base}.
+
+2. A {DBus::Data::Variant} made by {DBus.variant}(signature, value).
+   (Formerly this produced the type+value pair below, now it is just an alias
+   to the Variant constructor.)
+
+3. A pair [{DBus::Type}, value] specifies to marshall *value* as
    that specified type.
    The pair can be produced by {DBus.variant}(signature, value) which
    gives the  same result as [{DBus.type}(signature), value].
@@ -179,22 +188,29 @@ If the signature expects a Variant
    ISSUE: using something else than cryptic signatures is even more painful
    than remembering the signatures!
 
-        foo_i["Bar"] = DBus.variant("au", [0, 1, 1, 2, 3, 5, 8])
+   `foo_i["Bar"] = DBus.variant("au", [0, 1, 1, 2, 3, 5, 8])`
 
-2. Other values are tried to fit one of these:
+4. Other values are tried to fit one of these:
    Boolean, Double, Array of Variants, Hash of String keyed Variants,
    String, Int32, Int64.
 
-3. **Deprecated:** A pair [String, value], where String is a valid
+5. **Deprecated:** A pair [String, value], where String is a valid
    signature of a single complete type, marshalls value as that
-   type. This will hit you when you rely on method (2) but happen to have
+   type. This will hit you when you rely on method (4) but happen to have
    a particular string value in an array.
 
+##### Structs
+
+If a **STRUCT** `(...)` is expected you may pass
+
+- an [Array](https://ruby-doc.org/core/Array.html) (frozen is fine)
+- a [Struct](https://ruby-doc.org/core/Struct.html)
+
 ##### Byte Arrays
 
 If a byte array (`ay`) is expected you can pass a String too.
 The bytes sent are according to the string's
-[encoding](http://ruby-doc.org/core-2.0.0/Encoding.html).
+[encoding](http://ruby-doc.org/core/Encoding.html).
 
 ##### nil
 
@@ -244,14 +260,97 @@ When you want to provide a DBus API.
 (check that client and service side have their counterparts)
 
 ### Basic
+
 #### Exporting a Method
+
 ##### Interfaces
+
 ##### Methods
+
 ##### Bus Names
+
 ##### Errors
+
 #### Exporting Properties
+
+Similar to plain Ruby attributes, declared with
+
+- {https://docs.ruby-lang.org/en/3.1/Module.html#method-i-attr_accessor attr_accessor}
+- {https://docs.ruby-lang.org/en/3.1/Module.html#method-i-attr_reader attr_reader}
+- {https://docs.ruby-lang.org/en/3.1/Module.html#method-i-attr_writer attr_writer}
+
+These methods declare the attributes and export them as properties:
+
+- {DBus::Object.dbus_attr_accessor}
+- {DBus::Object.dbus_attr_reader}
+- {DBus::Object.dbus_attr_writer}
+
+For making properties out of Ruby methods (which are not attributes), use:
+
+- {DBus::Object.dbus_accessor}
+- {DBus::Object.dbus_reader}
+- {DBus::Object.dbus_writer}
+
+Note that the properties are declared in the Ruby naming convention with
+`snake_case` and D-Bus sees them `CamelCased`. Use the `dbus_name` argument
+for overriding this.
+
+&nbsp;
+
+    class Note < DBus::Object
+      dbus_interface "net.vidner.Example.Properties" do
+        # A read-write property "Title",
+        # with `title` and `title=` accessing @title.
+        dbus_attr_accessor :title, DBus::Type::STRING
+
+        # A read-only property "Author"
+        # (type specified via DBus signature)
+        # with `author` reading `@author`
+        dbus_attr_reader :author, "s"
+
+        # A read-only property `Clock`
+        def clock
+          Time.now.to_s
+        end
+        dbus_reader :clock, "s"
+
+        # Name mapping: `CreationTime`
+        def creation_time
+          "1993-01-01 00:00:00 +0100"
+        end
+        dbus_reader :creation_time, "s"
+
+        dbus_attr_accessor :book_volume, DBus::Type::VARIANT, dbus_name: "Volume"
+      end
+
+      dbus_interface "net.vidner.Example.Audio" do
+        dbus_attr_accessor :speaker_volume, DBus::Type::BYTE, dbus_name: "Volume"
+      end
+
+      # Must assign values because `nil` would crash our connection
+      def initialize(opath)
+        super
+        @title = "Ahem"
+        @author = "Martin"
+        @book_volume = 1
+        @speaker_volume = 11
+      end
+    end
+
+    obj = Note.new("/net/vidner/Example/Properties")
+
+    bus = DBus::SessionBus.instance
+    service = bus.request_service("net.vidner.Example")
+    service.export(obj)
+
+    main = DBus::Main.new
+    main << bus
+    main.run
+
 ### Advanced
+
 #### Inheritance
+
 #### Names
 
 Specification Conformance
diff --git a/examples/doc/_extract_examples b/examples/doc/_extract_examples
index 2f34aad..76aaefa 100755
--- a/examples/doc/_extract_examples
+++ b/examples/doc/_extract_examples
@@ -1,9 +1,14 @@
 #!/usr/bin/env ruby
+# frozen_string_literal: true
+
 if ARGV[0].nil?
   puts "Usage: #{$PROGRAM_NAME} file.md"
   exit
 end
 
+base_url = "https://github.com/mvidner/ruby-dbus/blob/master/"
+base_url += ARGV[0].gsub("../", "")
+
 File.open(ARGV[0]) do |f|
   title = nil
   setup = ""
@@ -20,7 +25,9 @@ File.open(ARGV[0]) do |f|
           setup = example
         else
           File.open("#{basename}.rb", "w") do |e|
+            anchor = title.downcase.gsub(/ +/, "-")
             e.write setup
+            e.write "# #{base_url}##{anchor}\n"
             e.write example
             e.chmod(0o755)
           end
diff --git a/examples/gdbus/gdbus b/examples/gdbus/gdbus
deleted file mode 100755
index 3d1cf64..0000000
--- a/examples/gdbus/gdbus
+++ /dev/null
@@ -1,257 +0,0 @@
-#!/usr/bin/env ruby
-#
-# This is a quite complex example using internal lower level API.
-# Not a good starting point, but might be usefull if you want to do tricky
-# stuff.
-# -- Arnaud
-
-require "dbus"
-require "gtk2"
-
-ENABLE_SYSTEM = false
-
-class MethodCallWindow
-  def initialize(pwindow, intf, meth)
-    @intf = intf
-    @meth = meth
-    @entries = []
-    @dialog = Gtk::Dialog.new(meth.name, pwindow,
-                              Gtk::Dialog::MODAL | Gtk::Dialog::NO_SEPARATOR,
-                              [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK],
-                              [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
-
-    @meth.params.each do |param|
-      shbox = Gtk::HBox.new(true, 0)
-      label = Gtk::Label.new("#{param[0]} (#{param[1]})")
-      input = Gtk::Entry.new
-      @entries << input
-      shbox.pack_start(label, true, true, 0)
-      shbox.pack_start(input, true, true, 0)
-      @dialog.vbox.pack_start(shbox, true, true, 0)
-      @dialog.vbox.show_all
-    end
-  end
-
-  def run
-    on_ok if @dialog.run == Gtk::Dialog::RESPONSE_OK
-    @dialog.destroy
-  end
-
-  def on_ok
-    bus = @intf.object.bus
-    m = DBus::Message.new(DBus::Message::METHOD_CALL)
-    m.path = @intf.object.path
-    m.interface = @intf.name
-    m.destination = @intf.object.destination
-    m.member = @meth.name
-    m.sender = bus.unique_name
-    @meth.params.each_with_index do |param, idx|
-      entry = @entries[idx]
-      data = nil
-      case param[1]
-      when "u", "i"
-        data = entry.text.to_i
-      when "s"
-        data = entry.text
-      when /^a/
-        begin
-          data = eval(entry.text)
-        rescue
-          puts "Incorrect data: #{data}"
-        end
-      end
-      m.add_param(param[1], data)
-    end
-    bus.send_sync_or_async(m) do |retm|
-      if retm.is_a?(DBus::Error)
-        puts "Error: #{retm.inspect}"
-      else
-        puts "Method #{m.member} returns: #{retm.params.inspect}"
-      end
-    end
-  end
-end
-
-class DBusUI
-  def initialize
-    @glade = Gtk::Builder.new
-    @glade << "gdbus.glade"
-
-    @sessiontreeview = @glade.get_object("sessiontreeview")
-    setup_treeview_renderer(@sessiontreeview, "D-Bus Objects")
-    @sessiontreeview.selection.signal_connect("changed") do |selection|
-      on_treeview_selection_changed(selection)
-    end
-
-    @systemtreeview = @glade.get_object("systemtreeview")
-    setup_treeview_renderer(@systemtreeview, "D-Bus Objects")
-    @systemtreeview.selection.signal_connect("changed") do |selection|
-      on_treeview_selection_changed(selection)
-    end
-
-    @methsigtreeview = @glade.get_object("methsigtreeview")
-    # ierk
-    setup_methodview_renderer(@methsigtreeview)
-    @methsigtreeview.signal_connect("row-activated") do |view, path, column|
-      on_method_activated(view, path, column)
-    end
-
-    @window = @glade.get_object("window1")
-    @window.show_all
-    start_buses
-  end
-
-  def beautify_method(meth)
-    # Damn, this need to be rewritten :p
-    s = meth.name + "("
-    if meth.is_a?(DBus::Method)
-      s += (meth.params.collect { |a| "in #{a[0]}:#{a[1]}" } +
-            meth.rets.collect { |a| "out #{a[0]}:#{a[1]}" }).join(", ")
-    elsif meth.is_a?(DBus::Signal)
-      s += (meth.params.collect { |a| "in #{a[0]}:#{a[1]}" }).join(", ")
-    end
-    s += ")"
-    s
-  end
-
-  def on_treeview_selection_changed(selection)
-    selected = selection.selected
-    model = Gtk::ListStore.new(String, String, DBus::Method,
-                               DBus::ProxyObjectInterface)
-    @methsigtreeview.model = model
-    if selected
-      if (intf = selected[1])
-        intf.methods.keys.sort.each do |mi|
-          m = intf.methods[mi]
-          subiter = model.append
-          subiter[0] = beautify_method(m)
-          subiter[1] = "M"
-          subiter[2] = m
-          subiter[3] = intf
-        end
-        intf.signals.keys.sort.each do |mi|
-          m = intf.signals[mi]
-          subiter = model.append
-          subiter[0] = beautify_method(m)
-          subiter[1] = "S"
-          subiter[2] = m
-          subiter[3] = intf
-        end
-      end
-    end
-  end
-
-  def on_method_activated(view, path, _column)
-    name = view.model.get_iter(path)[0]
-    puts "Clicked on: #{name.inspect}"
-    type = view.model.get_iter(path)[1]
-    if type == "M"
-      method = view.model.get_iter(path)[2]
-      intf = view.model.get_iter(path)[3]
-      MethodCallWindow.new(@window, intf, method).run
-    elsif type == "S"
-      signal = view.model.get_iter(path)[2]
-      intf = view.model.get_iter(path)[3]
-      mr = DBus::MatchRule.new.from_signal(intf, signal)
-      puts "*** Registering matchrule: #{mr} ***"
-      intf.object.bus.add_match(mr) do |sig|
-        puts "Got #{sig.member}(#{sig.params.join(",")})"
-      end
-    end
-  end
-
-  def on_sessiontreeview_row_activated(view, path, _column)
-    name = view.model.get_iter(path)[0]
-    puts "Clicked on: #{name.inspect}"
-  end
-
-  def on_window_delete_event(_window, _event)
-    Gtk.main_quit
-  end
-
-  def setup_methodview_renderer(treeview)
-    renderer = Gtk::CellRendererText.new
-    _col_offset = treeview.insert_column(-1, "T", renderer, "text" => 1)
-    col_offset = treeview.insert_column(-1, "Name", renderer, "text" => 0)
-    column = treeview.get_column(col_offset - 1)
-    column.clickable = true
-  end
-
-  def setup_treeview_renderer(treeview, str)
-    renderer = Gtk::CellRendererText.new
-    col_offset = treeview.insert_column(-1, str, renderer, "text" => 0)
-    column = treeview.get_column(col_offset - 1)
-    column.clickable = true
-  end
-
-  def process_input(bus)
-    # THIS is the bad ass loop
-    # we should return to the glib main loop from time to time. Anyone with a
-    # proper way to handle it ?
-    bus.update_buffer
-    bus.messages.each do |msg|
-      bus.process(msg)
-    end
-  end
-
-  def start_buses
-    # call glibize to get dbus messages from the glib mainloop
-    DBus::SessionBus.instance.glibize
-    DBus::SystemBus.instance.glibize if ENABLE_SYSTEM
-
-    DBus::SessionBus.instance.proxy.ListNames do |_msg, names|
-      fill_treeview(DBus::SessionBus.instance, @sessiontreeview, names)
-    end
-
-    return unless ENABLE_SYSTEM
-    DBus::SystemBus.instance.proxy.ListNames do |_msg, names|
-      fill_treeview(DBus::SystemBus.instance, @systemtreeview, names)
-    end
-  end
-
-  def walk_node(model, iter, node)
-    node.each_pair do |key, val|
-      subiter = model.append(iter)
-      subiter[0] = key
-      walk_node(model, subiter, val)
-    end
-
-    return if node.object.nil?
-    node.object.interfaces.sort.each do |ifname|
-      subiter = model.append(iter)
-      subiter[0] = ifname
-      subiter[1] = node.object[ifname]
-    end
-  end
-
-  def introspect_services(model, bus)
-    el = @introspect_array.shift
-    if el !~ /^:/
-      iter = model.append(nil)
-      iter[0] = el
-      puts "introspecting: #{el}"
-      begin
-        service = bus.service(el).introspect
-        walk_node(model, iter, service.root)
-      rescue Exception => e
-        puts "DBus Error:"
-        puts e.backtrace.join("\n")
-      end
-    end
-
-    !@introspect_array.empty?
-  end
-
-  def fill_treeview(bus, treeview, array)
-    model = Gtk::TreeStore.new(String, DBus::ProxyObjectInterface)
-    treeview.model = model
-    @introspect_array = array.sort
-    Gtk.idle_add { introspect_services(model, bus) }
-  end
-
-  def main
-    Gtk.main
-  end
-end
-
-DBusUI.new.main
diff --git a/examples/gdbus/gdbus.glade b/examples/gdbus/gdbus.glade
deleted file mode 100644
index a90fd2e..0000000
--- a/examples/gdbus/gdbus.glade
+++ /dev/null
@@ -1,98 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<interface>
-  <!-- interface-requires gtk+ 2.6 -->
-  <!-- interface-naming-policy toplevel-contextual -->
-  <object class="GtkWindow" id="window1">
-    <property name="visible">True</property>
-    <property name="can_focus">False</property>
-    <property name="title" translatable="yes">GD-Bus</property>
-    <property name="default_width">500</property>
-    <property name="default_height">400</property>
-    <signal name="delete-event" handler="on_window_delete_event" swapped="no"/>
-    <child>
-      <object class="GtkHPaned" id="hpaned1">
-        <property name="visible">True</property>
-        <property name="can_focus">True</property>
-        <child>
-          <object class="GtkNotebook" id="notebook1">
-            <property name="visible">True</property>
-            <property name="can_focus">True</property>
-            <child>
-              <object class="GtkScrolledWindow" id="scrolledwindow3">
-                <property name="visible">True</property>
-                <property name="can_focus">True</property>
-                <property name="shadow_type">in</property>
-                <child>
-                  <object class="GtkTreeView" id="sessiontreeview">
-                    <property name="visible">True</property>
-                    <property name="can_focus">True</property>
-                    <signal name="row-activated" handler="on_sessiontreeview_row_activated" swapped="no"/>
-                  </object>
-                </child>
-              </object>
-            </child>
-            <child type="tab">
-              <object class="GtkLabel" id="label1">
-                <property name="visible">True</property>
-                <property name="can_focus">False</property>
-                <property name="label" translatable="yes">Session</property>
-              </object>
-              <packing>
-                <property name="tab_fill">False</property>
-              </packing>
-            </child>
-            <child>
-              <object class="GtkScrolledWindow" id="scrolledwindow5">
-                <property name="visible">True</property>
-                <property name="can_focus">True</property>
-                <property name="shadow_type">in</property>
-                <child>
-                  <object class="GtkTreeView" id="systemtreeview">
-                    <property name="visible">True</property>
-                    <property name="can_focus">True</property>
-                  </object>
-                </child>
-              </object>
-              <packing>
-                <property name="position">1</property>
-              </packing>
-            </child>
-            <child type="tab">
-              <object class="GtkLabel" id="label2">
-                <property name="visible">True</property>
-                <property name="can_focus">False</property>
-                <property name="label" translatable="yes">System</property>
-              </object>
-              <packing>
-                <property name="position">1</property>
-                <property name="tab_fill">False</property>
-              </packing>
-            </child>
-          </object>
-          <packing>
-            <property name="resize">False</property>
-            <property name="shrink">True</property>
-          </packing>
-        </child>
-        <child>
-          <object class="GtkScrolledWindow" id="scrolledwindow4">
-            <property name="visible">True</property>
-            <property name="can_focus">True</property>
-            <property name="shadow_type">in</property>
-            <child>
-              <object class="GtkTreeView" id="methsigtreeview">
-                <property name="visible">True</property>
-                <property name="can_focus">True</property>
-                <signal name="row-activated" handler="on_method_activated" swapped="no"/>
-              </object>
-            </child>
-          </object>
-          <packing>
-            <property name="resize">False</property>
-            <property name="shrink">True</property>
-          </packing>
-        </child>
-      </object>
-    </child>
-  </object>
-</interface>
diff --git a/examples/gdbus/launch.sh b/examples/gdbus/launch.sh
deleted file mode 100755
index f6c61f7..0000000
--- a/examples/gdbus/launch.sh
+++ /dev/null
@@ -1,4 +0,0 @@
-#!/bin/sh
-set -e
-# for the lazy typer
-ruby -w -I ../../lib gdbus
diff --git a/examples/no-introspect/nm-test.rb b/examples/no-introspect/nm-test.rb
index c91a97a..86c4af5 100755
--- a/examples/no-introspect/nm-test.rb
+++ b/examples/no-introspect/nm-test.rb
@@ -1,4 +1,6 @@
 #!/usr/bin/env ruby
+# frozen_string_literal: true
+
 #
 # Trivial network interface lister using NetworkManager.
 # NetworkManager does not support introspection, so the api is not that sexy.
diff --git a/examples/no-introspect/tracker-test.rb b/examples/no-introspect/tracker-test.rb
index 4c1ad0d..4fd98f6 100755
--- a/examples/no-introspect/tracker-test.rb
+++ b/examples/no-introspect/tracker-test.rb
@@ -1,4 +1,6 @@
 #!/usr/bin/env ruby
+# frozen_string_literal: true
+
 #
 # Trivial network interface lister using NetworkManager.
 # NetworkManager does not support introspection, so the api is not that sexy.
@@ -11,4 +13,4 @@ tracker_service = bus.service("org.freedesktop.Tracker")
 tracker_manager = tracker_service.object("/org/freedesktop/tracker")
 poi = DBus::ProxyObjectInterface.new(tracker_manager, "org.freedesktop.Tracker.Files")
 poi.define_method("GetMetadataForFilesInFolder", "in live_query_id:i, in uri:s, in fields:as, out values:aas")
-p poi.GetMetadataForFilesInFolder(-1, ENV["HOME"] + "/Desktop", ["File:Name", "File:Size"])
+p poi.GetMetadataForFilesInFolder(-1, "#{ENV["HOME"]}/Desktop", ["File:Name", "File:Size"])
diff --git a/examples/rhythmbox/playpause.rb b/examples/rhythmbox/playpause.rb
index 8740af4..7ee9bd7 100755
--- a/examples/rhythmbox/playpause.rb
+++ b/examples/rhythmbox/playpause.rb
@@ -1,4 +1,5 @@
 #!/usr/bin/env ruby
+# frozen_string_literal: true
 
 require "dbus"
 bus = DBus::SessionBus.instance
@@ -13,7 +14,7 @@ mr.type = "signal"
 mr.interface = "org.gnome.Rhythmbox.Player"
 mr.path = "/org/gnome/Rhythmbox/Player"
 bus.add_match(mr) do |msg, first_param|
-  print msg.member + " "
+  print "#{msg.member} "
   puts first_param
 end
 
diff --git a/examples/service/call_service.rb b/examples/service/call_service.rb
index 82280ee..0496c6f 100755
--- a/examples/service/call_service.rb
+++ b/examples/service/call_service.rb
@@ -1,4 +1,5 @@
 #!/usr/bin/env ruby
+# frozen_string_literal: true
 
 require "dbus"
 
@@ -14,7 +15,7 @@ player.test_variant(["s", "coucou"])
 player.on_signal("SomethingJustHappened") do |u, v|
   puts "SomethingJustHappened: #{u} #{v}"
 end
-player.hello("8=======D", "(_._)")
+player.hello("Hey", "there!")
 p player["org.ruby.AnotherInterface"].Reverse("Hello world!")
 
 main = DBus::Main.new
diff --git a/examples/service/complex-property.rb b/examples/service/complex-property.rb
new file mode 100755
index 0000000..00383a5
--- /dev/null
+++ b/examples/service/complex-property.rb
@@ -0,0 +1,21 @@
+#!/usr/bin/env ruby
+# frozen_string_literal: true
+
+require "dbus"
+
+# Complex property
+class Test < DBus::Object
+  dbus_interface "net.vidner.Scratch" do
+    dbus_attr_reader :progress, "(stttt)"
+  end
+
+  def initialize(opath)
+    @progress = ["working", 1, 0, 100, 42].freeze
+    super(opath)
+  end
+end
+
+bus = DBus::SessionBus.instance
+svc = bus.request_service("net.vidner.Scratch")
+svc.export(Test.new("/net/vidner/Scratch"))
+DBus::Main.new.tap { |m| m << bus }.run
diff --git a/examples/service/service_newapi.rb b/examples/service/service_newapi.rb
index aba1676..348b1af 100755
--- a/examples/service/service_newapi.rb
+++ b/examples/service/service_newapi.rb
@@ -1,7 +1,7 @@
 #!/usr/bin/env ruby
+# frozen_string_literal: true
 
 require "dbus"
-require "thread"
 Thread.abort_on_exception = true
 
 class Test < DBus::Object
diff --git a/examples/simple/call_introspect.rb b/examples/simple/call_introspect.rb
index 22d633f..1b1755b 100755
--- a/examples/simple/call_introspect.rb
+++ b/examples/simple/call_introspect.rb
@@ -1,4 +1,5 @@
 #!/usr/bin/env ruby
+# frozen_string_literal: true
 
 require "dbus"
 
diff --git a/examples/simple/get_id.rb b/examples/simple/get_id.rb
index a7bd7cd..2b6739d 100755
--- a/examples/simple/get_id.rb
+++ b/examples/simple/get_id.rb
@@ -1,11 +1,14 @@
 #! /usr/bin/env ruby
+# frozen_string_literal: true
 
 # find the library without external help
-$LOAD_PATH.unshift File.expand_path("../../../lib", __FILE__)
+$LOAD_PATH.unshift File.expand_path("../../lib", __dir__)
 
 require "dbus"
 
-bus = DBus::SystemBus.instance
+busname = ARGV.fetch(0, "system")
+bus = busname == "session" ? DBus::SessionBus.instance : DBus::SystemBus.instance
+
 driver_svc = bus["org.freedesktop.DBus"]
 # p driver_svc
 driver_obj = driver_svc["/"]
@@ -14,4 +17,4 @@ driver_ifc = driver_obj["org.freedesktop.DBus"]
 # p driver_ifc
 
 bus_id = driver_ifc.GetId
-puts "The system bus id is #{bus_id}"
+puts "The #{busname} bus id is #{bus_id}"
diff --git a/examples/simple/properties.rb b/examples/simple/properties.rb
index 06f9543..a221d61 100755
--- a/examples/simple/properties.rb
+++ b/examples/simple/properties.rb
@@ -1,4 +1,6 @@
 #! /usr/bin/env ruby
+# frozen_string_literal: true
+
 require "dbus"
 
 bus = DBus::SystemBus.instance
diff --git a/examples/utils/listnames.rb b/examples/utils/listnames.rb
index 65601a8..2bcd7eb 100755
--- a/examples/utils/listnames.rb
+++ b/examples/utils/listnames.rb
@@ -1,4 +1,5 @@
 #!/usr/bin/env ruby
+# frozen_string_literal: true
 
 require "dbus"
 
diff --git a/examples/utils/notify.rb b/examples/utils/notify.rb
index db21d76..89f47d3 100755
--- a/examples/utils/notify.rb
+++ b/examples/utils/notify.rb
@@ -1,4 +1,5 @@
 #!/usr/bin/env ruby
+# frozen_string_literal: true
 
 require "dbus"
 
diff --git a/lib/dbus.rb b/lib/dbus.rb
index ccb7a4d..57ab271 100644
--- a/lib/dbus.rb
+++ b/lib/dbus.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
 # dbus.rb - Module containing the low-level D-Bus implementation
 #
 # This file is part of the ruby-dbus project
@@ -8,10 +10,27 @@
 # License, version 2.1 as published by the Free Software Foundation.
 # See the file "COPYING" for the exact licensing terms.
 
+module DBus
+  # Byte signifying big endianness.
+  BIG_END = "B"
+  # Byte signifying little endianness.
+  LIL_END = "l"
+
+  # Byte signifying the host's endianness.
+  HOST_END = if [0x01020304].pack("L").unpack1("V") == 0x01020304
+               LIL_END
+             else
+               BIG_END
+             end
+end
+# ^ That's because dbus/message needs HOST_END early
+
 require_relative "dbus/api_options"
 require_relative "dbus/auth"
 require_relative "dbus/bus"
 require_relative "dbus/bus_name"
+require_relative "dbus/data"
+require_relative "dbus/emits_changed_signal"
 require_relative "dbus/error"
 require_relative "dbus/introspect"
 require_relative "dbus/logger"
@@ -20,34 +39,24 @@ require_relative "dbus/matchrule"
 require_relative "dbus/message"
 require_relative "dbus/message_queue"
 require_relative "dbus/object"
+require_relative "dbus/object_manager"
 require_relative "dbus/object_path"
+require_relative "dbus/platform"
 require_relative "dbus/proxy_object"
 require_relative "dbus/proxy_object_factory"
 require_relative "dbus/proxy_object_interface"
+require_relative "dbus/raw_message"
 require_relative "dbus/type"
 require_relative "dbus/xml"
 
 require "socket"
-require "thread"
-
 # = D-Bus main module
 #
 # Module containing all the D-Bus modules and classes.
 module DBus
-  # Default socket name for the system bus.
-  SystemSocketName = "unix:path=/var/run/dbus/system_bus_socket".freeze
-
-  # Byte signifying big endianness.
-  BIG_END = "B".freeze
-  # Byte signifying little endianness.
-  LIL_END = "l".freeze
-
-  # Byte signifying the host's endianness.
-  HOST_END = if [0x01020304].pack("L").unpack("V")[0] == 0x01020304
-               LIL_END
-             else
-               BIG_END
-             end
+  # Comparing symbols is faster than strings
+  # @return [:little,:big]
+  HOST_ENDIANNESS = RawMessage.endianness(HOST_END)
 
   # General exceptions.
 
@@ -69,4 +78,4 @@ module DBus
   # Exception raised when invalid introspection data is parsed/used.
   class InvalidIntrospectionData < Exception
   end
-end # module DBus
+end
diff --git a/lib/dbus/api_options.rb b/lib/dbus/api_options.rb
index 1e7ed82..891d65d 100644
--- a/lib/dbus/api_options.rb
+++ b/lib/dbus/api_options.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
 # This file is part of the ruby-dbus project
 # Copyright (C) 2016 Martin Vidner
 #
@@ -9,6 +11,13 @@
 module DBus
   class ApiOptions
     # https://github.com/mvidner/ruby-dbus/issues/30
+    # @return [Boolean]
+    #   - true: a proxy (client-side) method will return an array
+    #     even for the most common case where the method is declared
+    #     to have only one 'out parameter'
+    #   - false: a proxy (client-side) method will return
+    #     - one value for the only 'out parameter'
+    #     - an array with more 'out parameters'
     attr_accessor :proxy_method_returns_array
 
     A0 = ApiOptions.new
diff --git a/lib/dbus/auth.rb b/lib/dbus/auth.rb
index 53ef711..5503539 100644
--- a/lib/dbus/auth.rb
+++ b/lib/dbus/auth.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
 # This file is part of the ruby-dbus project
 # Copyright (C) 2007 Arnaud Cornet and Paul van Tilburg
 #
@@ -6,262 +8,350 @@
 # License, version 2.1 as published by the Free Software Foundation.
 # See the file "COPYING" for the exact licensing terms.
 
-require "rbconfig"
-
 module DBus
   # Exception raised when authentication fails somehow.
-  class AuthenticationFailed < Exception
+  class AuthenticationFailed < StandardError
   end
 
-  # = General class for authentication.
-  class Authenticator
-    # Returns the name of the authenticator.
-    def name
-      self.class.to_s.upcase.sub(/.*::/, "")
-    end
-  end
+  # The Authentication Protocol.
+  # https://dbus.freedesktop.org/doc/dbus-specification.html#auth-protocol
+  #
+  # @api private
+  module Authentication
+    # Base class of authentication mechanisms
+    class Mechanism
+      # @!method call(challenge)
+      # @abstract
+      # Replies to server *challenge*, or sends an initial response if the challenge is `nil`.
+      # @param challenge [String,nil]
+      # @return [Array(Symbol,String)] pair [action, response], where
+      #   - [:MechContinue, response] caller should send "DATA response" and go to :WaitingForData
+      #   - [:MechOk,       response] caller should send "DATA response" and go to :WaitingForOk
+      #   - [:MechError,    message]  caller should send "ERROR message" and go to :WaitingForData
 
-  # = Anonymous authentication class
-  class Anonymous < Authenticator
-    def authenticate
-      "527562792044427573" # Hex encoded version of "Ruby DBus"
+      # Uppercase mechanism name, as sent to the server
+      # @return [String]
+      def name
+        self.class.to_s.upcase.sub(/.*::/, "")
+      end
     end
-  end
 
-  # = External authentication class
-  #
-  # Class for 'external' type authentication.
-  class External < Authenticator
-    # Performs the authentication.
-    def authenticate
-      # Take the user id (eg integer 1000) make a string out of it "1000", take
-      # each character and determin hex value "1" => 0x31, "0" => 0x30. You
-      # obtain for "1000" => 31303030 This is what the server is expecting.
-      # Why? I dunno. How did I come to that conclusion? by looking at rbus
-      # code. I have no idea how he found that out.
-      Process.uid.to_s.split(//).map { |d| d.ord.to_s(16) }.join
+    # Anonymous authentication class.
+    # https://dbus.freedesktop.org/doc/dbus-specification.html#auth-mechanisms-anonymous
+    class Anonymous < Mechanism
+      def call(_challenge)
+        [:MechOk, "Ruby DBus"]
+      end
     end
-  end
 
-  # = Authentication class using SHA1 crypto algorithm
-  #
-  # Class for 'CookieSHA1' type authentication.
-  # Implements the AUTH DBUS_COOKIE_SHA1 mechanism.
-  class DBusCookieSHA1 < Authenticator
-    # the autenticate method (called in stage one of authentification)
-    def authenticate
-      require "etc"
-      # number of retries we have for auth
-      @retries = 1
-      hex_encode(Etc.getlogin).to_s # server expects it to be binary
+    # Class for 'external' type authentication.
+    # https://dbus.freedesktop.org/doc/dbus-specification.html#auth-mechanisms-external
+    class External < Mechanism
+      # Performs the authentication.
+      def call(_challenge)
+        [:MechOk, Process.uid.to_s]
+      end
     end
 
-    # returns the modules name
-    def name
-      "DBUS_COOKIE_SHA1"
+    # A variant of EXTERNAL that doesn't say our UID.
+    # Seen busctl do this and it worked across a container boundary.
+    class ExternalWithoutUid < External
+      def name
+        "EXTERNAL"
+      end
+
+      def call(_challenge)
+        [:MechContinue, nil]
+      end
     end
 
-    # handles the interesting crypto stuff, check the rbus-project for more info: http://rbus.rubyforge.org/
-    def data(hexdata)
-      require "digest/sha1"
-      data = hex_decode(hexdata)
-      # name of cookie file, id of cookie in file, servers random challenge
-      context, id, s_challenge = data.split(" ")
-      # Random client challenge
-      c_challenge = 1.upto(s_challenge.bytesize / 2).map { rand(255).to_s }.join
-      # Search cookie file for id
-      path = File.join(ENV["HOME"], ".dbus-keyrings", context)
-      DBus.logger.debug "path: #{path.inspect}"
-      File.foreach(path) do |line|
-        if line.index(id).zero?
-          # Right line of file, read cookie
-          cookie = line.split(" ")[2].chomp
-          DBus.logger.debug "cookie: #{cookie.inspect}"
-          # Concatenate and encrypt
-          to_encrypt = [s_challenge, c_challenge, cookie].join(":")
-          sha = Digest::SHA1.hexdigest(to_encrypt)
-          # the almighty tcp server wants everything hex encoded
-          hex_response = hex_encode("#{c_challenge} #{sha}")
-          # Return response
-          response = [:AuthOk, hex_response]
-          return response
-        end
+    # Implements the AUTH DBUS_COOKIE_SHA1 mechanism.
+    # https://dbus.freedesktop.org/doc/dbus-specification.html#auth-mechanisms-sha
+    class DBusCookieSHA1 < Mechanism
+      # returns the modules name
+      def name
+        "DBUS_COOKIE_SHA1"
       end
-      # a little rescue magic
-      unless @retries <= 0
+
+      # First we are called with nil and we reply with our username.
+      # Then we prove that we can read that user's cookie file.
+      def call(challenge)
+        if challenge.nil?
+          require "etc"
+          # number of retries we have for auth
+          @retries = 1
+          return [:MechContinue, Etc.getlogin]
+        end
+
+        require "digest/sha1"
+        # name of cookie file, id of cookie in file, servers random challenge
+        context, id, s_challenge = challenge.split(" ")
+        # Random client challenge
+        c_challenge = 1.upto(s_challenge.bytesize / 2).map { rand(255).to_s }.join
+        # Search cookie file for id
+        path = File.join(ENV["HOME"], ".dbus-keyrings", context)
+        DBus.logger.debug "path: #{path.inspect}"
+        File.foreach(path) do |line|
+          if line.start_with?(id)
+            # Right line of file, read cookie
+            cookie = line.split(" ")[2].chomp
+            DBus.logger.debug "cookie: #{cookie.inspect}"
+            # Concatenate and encrypt
+            to_encrypt = [s_challenge, c_challenge, cookie].join(":")
+            sha = Digest::SHA1.hexdigest(to_encrypt)
+            # Return response
+            response = [:MechOk, "#{c_challenge} #{sha}"]
+            return response
+          end
+        end
+        return if @retries <= 0
+
+        # a little rescue magic
         puts "ERROR: Could not auth, will now exit."
         puts "ERROR: Unable to locate cookie, retry in 1 second."
         @retries -= 1
         sleep 1
-        data(hexdata)
+        call(challenge)
       end
     end
 
-    # encode plain to hex
-    def hex_encode(plain)
-      return nil if plain.nil?
-      plain.to_s.unpack("H*")[0]
-    end
+    # Declare client state transitions, for ease of code reading.
+    # It is just a pair.
+    NextState = Struct.new(:state, :command_words)
 
-    # decode hex to plain
-    def hex_decode(encoded)
-      encoded.scan(/[[:xdigit:]]{2}/).map { |h| h.hex.chr }.join
-    end
-  end # DBusCookieSHA1 class ends here
+    # Authenticates the connection before messages can be exchanged.
+    class Client
+      # @return [Boolean] have we negotiated Unix file descriptor passing
+      # NOTE: not implemented yet in upper layers
+      attr_reader :unix_fd
 
-  # Note: this following stuff is tested with External authenticator only!
+      # @return [String]
+      attr_reader :address_uuid
 
-  # = Authentication client class.
-  #
-  # Class tha performs the actional authentication.
-  class Client
-    # Create a new authentication client.
-    def initialize(socket)
-      @socket = socket
-      @state = nil
-      @auth_list = [External, DBusCookieSHA1, Anonymous]
-    end
+      # Create a new authentication client.
+      # @param mechs [Array<Mechanism,Class>,nil] custom list of auth Mechanism objects or classes
+      def initialize(socket, mechs = nil)
+        @unix_fd = false
+        @address_uuid = nil
 
-    # Start the authentication process.
-    def authenticate
-      if RbConfig::CONFIG["target_os"] =~ /freebsd/
-        @socket.sendmsg(0.chr, 0, nil, [:SOCKET, :SCM_CREDS, ""])
-      else
-        @socket.write(0.chr)
-      end
-      next_authenticator
-      @state = :Starting
-      while @state != :Authenticated
-        r = next_state
-        return r if !r
+        @socket = socket
+        @state = nil
+        @auth_list = mechs || [
+          External,
+          DBusCookieSHA1,
+          ExternalWithoutUid,
+          Anonymous
+        ]
       end
-      true
-    end
 
-    ##########
+      # Start the authentication process.
+      # @return [void]
+      # @raise [AuthenticationFailed]
+      def authenticate
+        DBus.logger.debug "Authenticating"
+        send_nul_byte
 
-    private
+        use_next_mechanism
 
-    ##########
+        @state, command = next_state_via_mechanism.to_a
+        send(command)
 
-    # Send an authentication method _meth_ with arguments _args_ to the
-    # server.
-    def send(meth, *args)
-      o = ([meth] + args).join(" ")
-      @socket.write(o + "\r\n")
-    end
+        loop do
+          DBus.logger.debug "auth STATE: #{@state}"
+          words = next_msg
 
-    # Try authentication using the next authenticator.
-    def next_authenticator
-      raise AuthenticationFailed if @auth_list.empty?
-      @authenticator = @auth_list.shift.new
-      auth_msg = ["AUTH", @authenticator.name, @authenticator.authenticate]
-      DBus.logger.debug "auth_msg: #{auth_msg.inspect}"
-      send(auth_msg)
-    rescue AuthenticationFailed
-      @socket.close
-      raise
-    end
+          @state, command = next_state(words).to_a
+          break if [:TerminatedOk, :TerminatedError].include? @state
+
+          send(command)
+        end
 
-    # Read data (a buffer) from the bus until CR LF is encountered.
-    # Return the buffer without the CR LF characters.
-    def next_msg
-      data = ""
-      crlf = "\r\n"
-      left = 1024 # 1024 byte, no idea if it's ever getting bigger
-      while left > 0
-        buf = @socket.read(left > 1 ? 1 : left)
-        break if buf.nil?
-        left -= buf.bytesize
-        data += buf
-        break if data.include? crlf # crlf means line finished, the TCP socket keeps on listening, so we break
+        raise AuthenticationFailed, command.first if @state == :TerminatedError
+
+        send("BEGIN")
       end
-      readline = data.chomp.split(" ")
-      DBus.logger.debug "readline: #{readline.inspect}"
-      readline
-    end
 
-    #     # Read data (a buffer) from the bus until CR LF is encountered.
-    #     # Return the buffer without the CR LF characters.
-    #     def next_msg
-    #       @socket.readline.chomp.split(" ")
-    #     end
-
-    # Try to reach the next state based on the current state.
-    def next_state
-      msg = next_msg
-      if @state == :Starting
-        DBus.logger.debug ":Starting msg: #{msg[0].inspect}"
-        case msg[0]
-        when "OK"
-          @state = :WaitingForOk
-        when "CONTINUE"
-          @state = :WaitingForData
-        when "REJECTED" # needed by tcp, unix-path/abstract doesn't get here
-          @state = :WaitingForData
+      ##########
+
+      private
+
+      ##########
+
+      # The authentication protocol requires a nul byte
+      # that may carry credentials.
+      # @return [void]
+      def send_nul_byte
+        if Platform.freebsd?
+          @socket.sendmsg(0.chr, 0, nil, [:SOCKET, :SCM_CREDS, ""])
+        else
+          @socket.write(0.chr)
         end
       end
-      DBus.logger.debug "state: #{@state}"
-      case @state
-      when :WaitingForData
-        DBus.logger.debug ":WaitingForData msg: #{msg[0].inspect}"
-        case msg[0]
-        when "DATA"
-          chall = msg[1]
-          resp, chall = @authenticator.data(chall)
-          DBus.logger.debug ":WaitingForData/DATA resp: #{resp.inspect}"
-          case resp
-          when :AuthContinue
-            send("DATA", chall)
-            @state = :WaitingForData
-          when :AuthOk
-            send("DATA", chall)
-            @state = :WaitingForOk
-          when :AuthError
-            send("ERROR")
-            @state = :WaitingForData
-          end
-        when "REJECTED"
-          next_authenticator
-          @state = :WaitingForData
-        when "ERROR"
-          send("CANCEL")
-          @state = :WaitingForReject
-        when "OK"
-          send("BEGIN")
-          @state = :Authenticated
-        else
-          send("ERROR")
-          @state = :WaitingForData
+
+      # encode plain to hex
+      # @param plain [String,nil]
+      # @return [String,nil]
+      def hex_encode(plain)
+        return nil if plain.nil?
+
+        plain.unpack1("H*")
+      end
+
+      # decode hex to plain
+      # @param encoded [String,nil]
+      # @return [String,nil]
+      def hex_decode(encoded)
+        return nil if encoded.nil?
+
+        [encoded].pack("H*")
+      end
+
+      # Send a string to the socket; good place for test mocks.
+      def write_line(str)
+        DBus.logger.debug "auth_write: #{str.inspect}"
+        @socket.write(str)
+      end
+
+      # Send *words* to the server as a single CRLF terminated string.
+      # @param words [Array<String>,String]
+      def send(words)
+        joined = Array(words).compact.join(" ")
+        write_line("#{joined}\r\n")
+      end
+
+      # Try authentication using the next mechanism.
+      # @raise [AuthenticationFailed] if there are no more left
+      # @return [void]
+      def use_next_mechanism
+        raise AuthenticationFailed, "Authentication mechanisms exhausted" if @auth_list.empty?
+
+        @mechanism = @auth_list.shift
+        @mechanism = @mechanism.new if @mechanism.is_a? Class
+      rescue AuthenticationFailed
+        # TODO: make this caller's responsibility
+        @socket.close
+        raise
+      end
+
+      # Read data (a buffer) from the bus until CR LF is encountered.
+      # Return the buffer without the CR LF characters.
+      # @return [Array<String>] received words
+      def next_msg
+        read_line.chomp.split(" ")
+      end
+
+      # Read a line from the socket; good place for test mocks.
+      # @return [String] CRLF (\r\n) terminated
+      def read_line
+        # TODO: probably can simply call @socket.readline
+        data = ""
+        crlf = "\r\n"
+        left = 1024 # 1024 byte, no idea if it's ever getting bigger
+        while left.positive?
+          buf = @socket.read(left > 1 ? 1 : left)
+          break if buf.nil?
+
+          left -= buf.bytesize
+          data += buf
+          break if data.include? crlf # crlf means line finished, the TCP socket keeps on listening, so we break
         end
-      when :WaitingForOk
-        DBus.logger.debug ":WaitingForOk msg: #{msg[0].inspect}"
-        case msg[0]
-        when "OK"
-          send("BEGIN")
-          @state = :Authenticated
-        when "REJECT"
-          next_authenticator
-          @state = :WaitingForData
-        when "DATA", "ERROR"
-          send("CANCEL")
-          @state = :WaitingForReject
+        DBus.logger.debug "auth_read: #{data.inspect}"
+        data
+      end
+
+      #     # Read data (a buffer) from the bus until CR LF is encountered.
+      #     # Return the buffer without the CR LF characters.
+      #     def next_msg
+      #       @socket.readline.chomp.split(" ")
+      #     end
+
+      # @param hex_challenge [String,nil] (nil when the server said "DATA\r\n")
+      # @param use_data [Boolean] say DATA instead of AUTH
+      # @return [NextState]
+      def next_state_via_mechanism(hex_challenge = nil, use_data: false)
+        challenge = hex_decode(hex_challenge)
+
+        action, response = @mechanism.call(challenge)
+        DBus.logger.debug "auth mechanism action: #{action.inspect}"
+
+        command = use_data ? ["DATA"] : ["AUTH", @mechanism.name]
+
+        case action
+        when :MechError
+          NextState.new(:WaitingForData, ["ERROR", response])
+        when :MechContinue
+          NextState.new(:WaitingForData, command + [hex_encode(response)])
+        when :MechOk
+          NextState.new(:WaitingForOk, command + [hex_encode(response)])
         else
-          send("ERROR")
-          @state = :WaitingForOk
+          raise AuthenticationFailed, "internal error, unknown action #{action.inspect} " \
+                                      "from our mechanism #{@mechanism.inspect}"
         end
-      when :WaitingForReject
-        DBus.logger.debug ":WaitingForReject msg: #{msg[0].inspect}"
-        case msg[0]
-        when "REJECT"
-          next_authenticator
-          @state = :WaitingForOk
+      end
+
+      # Try to reach the next state based on the current state.
+      # @param received_words [Array<String>]
+      # @return [NextState]
+      def next_state(received_words)
+        msg = received_words
+
+        case @state
+        when :WaitingForData
+          case msg[0]
+          when "DATA"
+            next_state_via_mechanism(msg[1], use_data: true)
+          when "REJECTED"
+            use_next_mechanism
+            next_state_via_mechanism
+          when "ERROR"
+            NextState.new(:WaitingForReject, ["CANCEL"])
+          when "OK"
+            @address_uuid = msg[1]
+            # NextState.new(:TerminatedOk, [])
+            NextState.new(:WaitingForAgreeUnixFD, ["NEGOTIATE_UNIX_FD"])
+          else
+            NextState.new(:WaitingForData, ["ERROR"])
+          end
+        when :WaitingForOk
+          case msg[0]
+          when "OK"
+            @address_uuid = msg[1]
+            # NextState.new(:TerminatedOk, [])
+            NextState.new(:WaitingForAgreeUnixFD, ["NEGOTIATE_UNIX_FD"])
+          when "REJECTED"
+            use_next_mechanism
+            next_state_via_mechanism
+          when "DATA", "ERROR"
+            NextState.new(:WaitingForReject, ["CANCEL"])
+          else
+            # we don't understand server's response but still wait for a successful auth completion
+            NextState.new(:WaitingForOk, ["ERROR"])
+          end
+        when :WaitingForReject
+          case msg[0]
+          when "REJECTED"
+            use_next_mechanism
+            next_state_via_mechanism
+          else
+            # TODO: spec says to close socket, clarify
+            NextState.new(:TerminatedError, ["Unknown server reply #{msg[0].inspect} when expecting REJECTED"])
+          end
+        when :WaitingForAgreeUnixFD
+          case msg[0]
+          when "AGREE_UNIX_FD"
+            @unix_fd = true
+            NextState.new(:TerminatedOk, [])
+          when "ERROR"
+            @unix_fd = false
+            NextState.new(:TerminatedOk, [])
+          else
+            # TODO: spec says to close socket, clarify
+            NextState.new(:TerminatedError, ["Unknown server reply #{msg[0].inspect} to NEGOTIATE_UNIX_FD"])
+          end
         else
-          @socket.close
-          return false
+          raise "Internal error: unhandled state #{@state.inspect}"
         end
       end
-      true
-    end # def next_state
-  end # class Client
-end # module D-Bus
+    end
+  end
+end
diff --git a/lib/dbus/bus.rb b/lib/dbus/bus.rb
index fdb239c..46fcec5 100644
--- a/lib/dbus/bus.rb
+++ b/lib/dbus/bus.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
 # dbus.rb - Module containing the low-level D-Bus implementation
 #
 # This file is part of the ruby-dbus project
@@ -9,7 +11,6 @@
 # See the file "COPYING" for the exact licensing terms.
 
 require "socket"
-require "thread"
 require "singleton"
 
 # = D-Bus main module
@@ -17,7 +18,7 @@ require "singleton"
 # Module containing all the D-Bus modules and classes.
 module DBus
   # This represents a remote service. It should not be instantiated directly
-  # Use {Bus#service}
+  # Use {Connection#service}
   class Service
     # The service name.
     attr_reader :name
@@ -48,6 +49,7 @@ module DBus
     end
 
     # Retrieves an object at the given _path_.
+    # @param path [ObjectPath]
     # @return [ProxyObject]
     def [](path)
       object(path, api: ApiOptions::A1)
@@ -55,9 +57,11 @@ module DBus
 
     # Retrieves an object at the given _path_
     # whose methods always return an array.
+    # @param path [ObjectPath]
+    # @param api [ApiOptions]
     # @return [ProxyObject]
     def object(path, api: ApiOptions::A0)
-      node = get_node(path, _create = true)
+      node = get_node(path, create: true)
       if node.object.nil? || node.object.api != api
         node.object = ProxyObject.new(
           @bus, @name, path,
@@ -67,51 +71,94 @@ module DBus
       node.object
     end
 
-    # Export an object _obj_ (an DBus::Object subclass instance).
+    # Export an object
+    # @param obj [DBus::Object]
     def export(obj)
       obj.service = self
-      get_node(obj.path, true).object = obj
+      get_node(obj.path, create: true).object = obj
+      object_manager_for(obj)&.object_added(obj)
     end
 
     # Undo exporting an object _obj_.
     # Raises ArgumentError if it is not a DBus::Object.
     # Returns the object, or false if _obj_ was not exported.
+    # @param obj [DBus::Object]
     def unexport(obj)
       raise ArgumentError, "DBus::Service#unexport() expects a DBus::Object argument" unless obj.is_a?(DBus::Object)
       return false unless obj.path
+
       last_path_separator_idx = obj.path.rindex("/")
       parent_path = obj.path[1..last_path_separator_idx - 1]
       node_name = obj.path[last_path_separator_idx + 1..-1]
 
-      parent_node = get_node(parent_path, false)
+      parent_node = get_node(parent_path, create: false)
       return false unless parent_node
+
+      object_manager_for(obj)&.object_removed(obj)
       obj.service = nil
       parent_node.delete(node_name).object
     end
 
-    # Get the object node corresponding to the given _path_. if _create_ is
-    # true, the the nodes in the path are created if they do not already exist.
-    def get_node(path, create = false)
+    # Get the object node corresponding to the given *path*.
+    # @param path [ObjectPath]
+    # @param create [Boolean] if true, the the {Node}s in the path are created
+    #   if they do not already exist.
+    # @return [Node,nil]
+    def get_node(path, create: false)
       n = @root
       path.sub(%r{^/}, "").split("/").each do |elem|
         if !(n[elem])
           return nil if !create
+
           n[elem] = Node.new(elem)
         end
         n = n[elem]
       end
-      if n.nil?
-        DBus.logger.debug "Warning, unknown object #{path}"
-      end
       n
     end
 
+    # Find the (closest) parent of *object*
+    # implementing the ObjectManager interface, or nil
+    # @return [DBus::Object,nil]
+    def object_manager_for(object)
+      path = object.path
+      node_chain = get_node_chain(path)
+      om_node = node_chain.reverse_each.find do |node|
+        node.object&.is_a? DBus::ObjectManager
+      end
+      om_node&.object
+    end
+
+    # All objects (not paths) under this path (except itself).
+    # @param path [ObjectPath]
+    # @return [Array<DBus::Object>]
+    # @raise ArgumentError if the *path* does not exist
+    def descendants_for(path)
+      node = get_node(path, create: false)
+      raise ArgumentError, "Object path #{path} doesn't exist" if node.nil?
+
+      node.descendant_objects
+    end
+
     #########
 
     private
 
     #########
 
+    # @raise ArgumentError if the *path* does not exist
+    def get_node_chain(path)
+      n = @root
+      result = [n]
+      path.sub(%r{^/}, "").split("/").each do |elem|
+        n = n[elem]
+        raise ArgumentError, "Object path #{path} doesn't exist" if n.nil?
+
+        result.push(n)
+      end
+      result
+    end
+
     # Perform a recursive retrospection on the given current _node_
     # on the given _path_.
     def rec_introspect(node, path)
@@ -120,13 +167,14 @@ module DBus
       subnodes.each do |nodename|
         subnode = node[nodename] = Node.new(nodename)
         subpath = if path == "/"
-                    "/" + nodename
+                    "/#{nodename}"
                   else
-                    path + "/" + nodename
+                    "#{path}/#{nodename}"
                   end
         rec_introspect(subnode, subpath)
       end
       return if intfs.empty?
+
       node.object = ProxyObjectFactory.new(xml, @bus, @name, path).build
     end
   end
@@ -135,34 +183,34 @@ module DBus
   #
   # Class representing a node on an object path.
   class Node < Hash
-    # The D-Bus object contained by the node.
+    # @return [DBus::Object,DBus::ProxyObject,nil]
+    #   The D-Bus object contained by the node.
     attr_accessor :object
+
     # The name of the node.
+    # @return [String] the last component of its object path, or "/"
     attr_reader :name
 
     # Create a new node with a given _name_.
     def initialize(name)
+      super()
       @name = name
       @object = nil
     end
 
     # Return an XML string representation of the node.
     # It is shallow, not recursing into subnodes
-    def to_xml
+    # @param node_opath [String]
+    def to_xml(node_opath)
       xml = '<!DOCTYPE node PUBLIC "-//freedesktop//DTD D-BUS Object Introspection 1.0//EN"
 "http://www.freedesktop.org/standards/dbus/1.0/introspect.dtd">
-<node>
 '
-      each_pair do |k, _v|
-        xml += "<node name=\"#{k}\" />"
+      xml += "<node name=\"#{node_opath}\">\n"
+      each_key do |k|
+        xml += "  <node name=\"#{k}\" />\n"
       end
-      if @object
-        @object.intfs.each_pair do |_k, v|
-          xml += %(<interface name="#{v.name}">\n)
-          v.methods.each_value { |m| xml += m.to_xml }
-          v.signals.each_value { |m| xml += m.to_xml }
-          xml += "</interface>\n"
-        end
+      @object&.intfs&.each_value do |v|
+        xml += v.to_xml
       end
       xml += "</node>"
       xml
@@ -180,9 +228,21 @@ module DBus
       if !@object.nil?
         s += format("%x ", @object.object_id)
       end
-      s + "{" + keys.collect { |k| "#{k} => #{self[k].sub_inspect}" }.join(",") + "}"
+      contents_sub_inspect = keys
+                             .map { |k| "#{k} => #{self[k].sub_inspect}" }
+                             .join(",")
+      "#{s}{#{contents_sub_inspect}}"
     end
-  end # class Inspect
+
+    # All objects (not paths) under this path (except itself).
+    # @return [Array<DBus::Object>]
+    def descendant_objects
+      children_objects = values.map(&:object).compact
+      descendants = values.map(&:descendant_objects)
+      flat_descendants = descendants.reduce([], &:+)
+      children_objects + flat_descendants
+    end
+  end
 
   # FIXME: rename Connection to Bus?
 
@@ -205,18 +265,25 @@ module DBus
     def initialize(path)
       @message_queue = MessageQueue.new(path)
       @unique_name = nil
+
+      # @return [Hash{Integer => Proc}]
+      #   key: message serial
+      #   value: block to be run when the reply to that message is received
       @method_call_replies = {}
+
+      # @return [Hash{Integer => Message}]
+      #   for debugging only: messages for which a reply was not received yet;
+      #   key == value.serial
       @method_call_msgs = {}
       @signal_matchrules = {}
       @proxy = nil
-      @object_root = Node.new("/")
     end
 
     # Dispatch all messages that are available in the queue,
     # but do not block on the queue.
     # Called by a main loop when something is available in the queue
     def dispatch_message_queue
-      while (msg = @message_queue.pop(:non_block)) # FIXME: EOFError
+      while (msg = @message_queue.pop(blocking: false)) # FIXME: EOFError
         process(msg)
       end
     end
@@ -251,10 +318,13 @@ module DBus
 <node>
   <interface name="org.freedesktop.DBus.Introspectable">
     <method name="Introspect">
-      <arg name="data" direction="out" type="s"/>
+      <arg direction="out" type="s"/>
     </method>
   </interface>
   <interface name="org.freedesktop.DBus">
+    <method name="Hello">
+      <arg direction="out" type="s"/>
+    </method>
     <method name="RequestName">
       <arg direction="in" type="s"/>
       <arg direction="in" type="u"/>
@@ -269,8 +339,8 @@ module DBus
       <arg direction="in" type="u"/>
       <arg direction="out" type="u"/>
     </method>
-    <method name="Hello">
-      <arg direction="out" type="s"/>
+    <method name="UpdateActivationEnvironment">
+      <arg direction="in" type="a{ss}"/>
     </method>
     <method name="NameHasOwner">
       <arg direction="in" type="s"/>
@@ -304,12 +374,29 @@ module DBus
       <arg direction="in" type="s"/>
       <arg direction="out" type="u"/>
     </method>
+    <method name="GetAdtAuditSessionData">
+      <arg direction="in" type="s"/>
+      <arg direction="out" type="ay"/>
+    </method>
     <method name="GetConnectionSELinuxSecurityContext">
       <arg direction="in" type="s"/>
       <arg direction="out" type="ay"/>
     </method>
     <method name="ReloadConfig">
     </method>
+    <method name="GetId">
+      <arg direction="out" type="s"/>
+    </method>
+    <method name="GetConnectionCredentials">
+      <arg direction="in" type="s"/>
+      <arg direction="out" type="a{sv}"/>
+    </method>
+    <property name="Features" type="as" access="read">
+      <annotation name="org.freedesktop.DBus.Property.EmitsChangedSignal" value="const"/>
+    </property>
+    <property name="Interfaces" type="as" access="read">
+      <annotation name="org.freedesktop.DBus.Property.EmitsChangedSignal" value="const"/>
+    </property>
     <signal name="NameOwnerChanged">
       <arg type="s"/>
       <arg type="s"/>
@@ -323,7 +410,7 @@ module DBus
     </signal>
   </interface>
 </node>
-'.freeze
+'
     # This apostroph is for syntax highlighting editors confused by above xml: "
 
     # @api private
@@ -338,6 +425,7 @@ module DBus
       if reply_handler.nil?
         send_sync(message) do |rmsg|
           raise rmsg if rmsg.is_a?(Error)
+
           ret = rmsg.params
         end
       else
@@ -412,7 +500,15 @@ module DBus
       proxy.RequestName(name, NAME_FLAG_REPLACE_EXISTING) do |rmsg, r|
         # check and report errors first
         raise rmsg if rmsg.is_a?(Error)
-        raise NameRequestError unless r == REQUEST_NAME_REPLY_PRIMARY_OWNER
+
+        details = if r == REQUEST_NAME_REPLY_IN_QUEUE
+                    other = proxy.GetNameOwner(name).first
+                    other_creds = proxy.GetConnectionCredentials(other).first
+                    "already owned by #{other}, #{other_creds.inspect}"
+                  else
+                    "error code #{r}"
+                  end
+        raise NameRequestError, "Could not request #{name}, #{details}" unless r == REQUEST_NAME_REPLY_PRIMARY_OWNER
       end
       @service = Service.new(name, self)
       @service
@@ -442,44 +538,52 @@ module DBus
     end
 
     # @api private
-    # Send a message _m_ on to the bus. This is done synchronously, thus
+    # Send a message _msg_ on to the bus. This is done synchronously, thus
     # the call will block until a reply message arrives.
-    def send_sync(m, &retc) # :yields: reply/return message
-      return if m.nil? # check if somethings wrong
-      @message_queue.push(m)
-      @method_call_msgs[m.serial] = m
-      @method_call_replies[m.serial] = retc
+    # @param msg [Message]
+    # @param retc [Proc] the reply handler
+    # @yieldparam rmsg [MethodReturnMessage] the reply
+    # @yieldreturn [Array<Object>] the reply (out) parameters
+    def send_sync(msg, &retc) # :yields: reply/return message
+      return if msg.nil? # check if somethings wrong
+
+      @message_queue.push(msg)
+      @method_call_msgs[msg.serial] = msg
+      @method_call_replies[msg.serial] = retc
 
       retm = wait_for_message
       return if retm.nil? # check if somethings wrong
 
       process(retm)
-      while @method_call_replies.key? m.serial
+      while @method_call_replies.key? msg.serial
         retm = wait_for_message
         process(retm)
       end
     rescue EOFError
-      new_err = DBus::Error.new("Connection dropped after we sent #{m.inspect}")
+      new_err = DBus::Error.new("Connection dropped after we sent #{msg.inspect}")
       raise new_err
     end
 
     # @api private
     # Specify a code block that has to be executed when a reply for
-    # message _m_ is received.
-    def on_return(m, &retc)
+    # message _msg_ is received.
+    # @param msg [Message]
+    def on_return(msg, &retc)
       # Have a better exception here
-      if m.message_type != Message::METHOD_CALL
+      if msg.message_type != Message::METHOD_CALL
         raise "on_return should only get method_calls"
       end
-      @method_call_msgs[m.serial] = m
-      @method_call_replies[m.serial] = retc
+
+      @method_call_msgs[msg.serial] = msg
+      @method_call_replies[msg.serial] = retc
     end
 
     # Asks bus to send us messages matching mr, and execute slot when
     # received
-    def add_match(mr, &slot)
+    # @param match_rule [MatchRule,#to_s]
+    def add_match(match_rule, &slot)
       # check this is a signal.
-      mrs = mr.to_s
+      mrs = match_rule.to_s
       DBus.logger.debug "#{@signal_matchrules.size} rules, adding #{mrs.inspect}"
       # don't ask for the same match if we override it
       unless @signal_matchrules.key?(mrs)
@@ -489,69 +593,76 @@ module DBus
       @signal_matchrules[mrs] = slot
     end
 
-    def remove_match(mr)
-      mrs = mr.to_s
+    # @param match_rule [MatchRule,#to_s]
+    def remove_match(match_rule)
+      mrs = match_rule.to_s
       rule_existed = @signal_matchrules.delete(mrs).nil?
       # don't remove nonexisting matches.
       return if rule_existed
+
       # FIXME: if we do try, the Error.MatchRuleNotFound is *not* raised
       # and instead is reported as "no return code for nil"
       proxy.RemoveMatch(mrs)
     end
 
     # @api private
-    # Process a message _m_ based on its type.
-    def process(m)
-      return if m.nil? # check if somethings wrong
-      case m.message_type
+    # Process a message _msg_ based on its type.
+    # @param msg [Message]
+    def process(msg)
+      return if msg.nil? # check if somethings wrong
+
+      case msg.message_type
       when Message::ERROR, Message::METHOD_RETURN
-        raise InvalidPacketException if m.reply_serial.nil?
-        mcs = @method_call_replies[m.reply_serial]
+        raise InvalidPacketException if msg.reply_serial.nil?
+
+        mcs = @method_call_replies[msg.reply_serial]
         if !mcs
-          DBus.logger.debug "no return code for mcs: #{mcs.inspect} m: #{m.inspect}"
+          DBus.logger.debug "no return code for mcs: #{mcs.inspect} msg: #{msg.inspect}"
         else
-          if m.message_type == Message::ERROR
-            mcs.call(Error.new(m))
+          if msg.message_type == Message::ERROR
+            mcs.call(Error.new(msg))
           else
-            mcs.call(m)
+            mcs.call(msg)
           end
-          @method_call_replies.delete(m.reply_serial)
-          @method_call_msgs.delete(m.reply_serial)
+          @method_call_replies.delete(msg.reply_serial)
+          @method_call_msgs.delete(msg.reply_serial)
         end
       when DBus::Message::METHOD_CALL
-        if m.path == "/org/freedesktop/DBus"
+        if msg.path == "/org/freedesktop/DBus"
           DBus.logger.debug "Got method call on /org/freedesktop/DBus"
         end
-        node = @service.get_node(m.path)
-        if !node
-          reply = Message.error(m, "org.freedesktop.DBus.Error.UnknownObject",
-                                "Object #{m.path} doesn't exist")
-          @message_queue.push(reply)
-        # handle introspectable as an exception:
-        elsif m.interface == "org.freedesktop.DBus.Introspectable" &&
-              m.member == "Introspect"
-          reply = Message.new(Message::METHOD_RETURN).reply_to(m)
+        node = @service.get_node(msg.path, create: false)
+        # introspect a known path even if there is no object on it
+        if node &&
+           msg.interface == "org.freedesktop.DBus.Introspectable" &&
+           msg.member == "Introspect"
+          reply = Message.new(Message::METHOD_RETURN).reply_to(msg)
           reply.sender = @unique_name
-          reply.add_param(Type::STRING, node.to_xml)
+          xml = node.to_xml(msg.path)
+          reply.add_param(Type::STRING, xml)
           @message_queue.push(reply)
+        # dispatch for an object
+        elsif node&.object
+          node.object.dispatch(msg)
         else
-          obj = node.object
-          return if obj.nil? # FIXME, pushes no reply
-          obj.dispatch(m) if obj
+          reply = Message.error(msg, "org.freedesktop.DBus.Error.UnknownObject",
+                                "Object #{msg.path} doesn't exist")
+          @message_queue.push(reply)
         end
       when DBus::Message::SIGNAL
         # the signal can match multiple different rules
         # clone to allow new signale handlers to be registered
         @signal_matchrules.dup.each do |mrs, slot|
-          if DBus::MatchRule.new.from_s(mrs).match(m)
-            slot.call(m)
+          if DBus::MatchRule.new.from_s(mrs).match(msg)
+            slot.call(msg)
           end
         end
       else
-        DBus.logger.debug "Unknown message type: #{m.message_type}"
+        # spec(Message Format): Unknown types must be ignored.
+        DBus.logger.debug "Unknown message type: #{msg.message_type}"
       end
-    rescue Exception => ex
-      raise m.annotate_exception(ex)
+    rescue Exception => e
+      raise msg.annotate_exception(e)
     end
 
     # Retrieves the Service with the given _name_.
@@ -566,6 +677,11 @@ module DBus
     # @api private
     # Emit a signal event for the given _service_, object _obj_, interface
     # _intf_ and signal _sig_ with arguments _args_.
+    # @param service [Service]
+    # @param obj [DBus::Object]
+    # @param intf [Interface]
+    # @param sig [Signal]
+    # @param args arguments for the signal
     def emit(service, obj, intf, sig, *args)
       m = Message.new(DBus::Message::SIGNAL)
       m.path = obj.path
@@ -596,7 +712,7 @@ module DBus
       end
       @service = Service.new(@unique_name, self)
     end
-  end # class Connection
+  end
 
   # = D-Bus session bus class
   #
@@ -614,7 +730,8 @@ module DBus
     def self.session_bus_address
       ENV["DBUS_SESSION_BUS_ADDRESS"] ||
         address_from_file ||
-        "launchd:env=DBUS_LAUNCHD_SESSION_BUS_SOCKET"
+        ("launchd:env=DBUS_LAUNCHD_SESSION_BUS_SOCKET" if Platform.macos?) ||
+        (raise NotImplementedError, "Cannot find session bus; sorry, haven't figured out autolaunch yet")
     end
 
     def self.address_from_file
@@ -622,6 +739,7 @@ module DBus
       # traditional dbus uses /var/lib/dbus/machine-id
       machine_id_path = Dir["{/etc,/var/lib/dbus,/var/db/dbus}/machine-id"].first
       return nil unless machine_id_path
+
       machine_id = File.read(machine_id_path).chomp
 
       display = ENV["DISPLAY"][/:(\d+)\.?/, 1]
@@ -643,6 +761,9 @@ module DBus
     include Singleton
   end
 
+  # Default socket name for the system bus.
+  SYSTEM_BUS_ADDRESS = "unix:path=/var/run/dbus/system_bus_socket"
+
   # = D-Bus system bus class
   #
   # The system bus is a system-wide bus mostly used for global or
@@ -653,9 +774,13 @@ module DBus
   class ASystemBus < Connection
     # Get the default system bus.
     def initialize
-      super(SystemSocketName)
+      super(self.class.system_bus_address)
       send_hello
     end
+
+    def self.system_bus_address
+      ENV["DBUS_SYSTEM_BUS_ADDRESS"] || SYSTEM_BUS_ADDRESS
+    end
   end
 
   # = D-Bus remote (TCP) bus class
@@ -668,7 +793,7 @@ module DBus
   # (for Unix-socket) unix:path=/tmp/my_funky_bus_socket
   #
   # you'll need to take care about authentification then, more info here:
-  # http://github.com/pangdudu/ruby-dbus/blob/master/README.rdoc
+  # https://gitlab.com/pangdudu/ruby-dbus/-/blob/master/README.rdoc
   class RemoteBus < Connection
     # Get the remote bus.
     def initialize(socket_name)
@@ -728,6 +853,7 @@ module DBus
       while !@quitting && !@buses.empty?
         ready = IO.select(@buses.keys, [], [], 5) # timeout 5 seconds
         next unless ready # timeout exceeds so continue unless quitting
+
         ready.first.each do |socket|
           b = @buses[socket]
           begin
@@ -742,5 +868,5 @@ module DBus
         end
       end
     end
-  end # class Main
-end # module DBus
+  end
+end
diff --git a/lib/dbus/bus_name.rb b/lib/dbus/bus_name.rb
index 29dabca..ba2ce96 100644
--- a/lib/dbus/bus_name.rb
+++ b/lib/dbus/bus_name.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
 # This file is part of the ruby-dbus project
 # Copyright (C) 2019 Martin Vidner
 #
@@ -7,21 +9,23 @@
 # See the file "COPYING" for the exact licensing terms.
 
 module DBus
-  # A {::String} that validates at initialization time
+  # D-Bus: a name for a connection, like ":1.3" or "org.example.ManagerManager".
+  # Implemented as a {::String} that validates at initialization time.
   # @see https://dbus.freedesktop.org/doc/dbus-specification.html#message-protocol-names-bus
   class BusName < String
     # @raise Error if not a valid bus name
-    def initialize(s)
-      unless self.class.valid?(s)
-        raise DBus::Error, "Invalid bus name #{s.inspect}"
+    def initialize(name)
+      unless self.class.valid?(name)
+        raise DBus::Error, "Invalid bus name #{name.inspect}"
       end
+
       super
     end
 
-    def self.valid?(s)
-      s.size <= 255 &&
-        (s =~ /\A:[A-Za-z0-9_-]+(\.[A-Za-z0-9_-]+)+\z/ ||
-         s =~ /\A[A-Za-z_-][A-Za-z0-9_-]*(\.[A-Za-z_-][A-Za-z0-9_-]*)+\z/)
+    def self.valid?(name)
+      name.size <= 255 &&
+        (name =~ /\A:[A-Za-z0-9_-]+(\.[A-Za-z0-9_-]+)+\z/ ||
+         name =~ /\A[A-Za-z_-][A-Za-z0-9_-]*(\.[A-Za-z_-][A-Za-z0-9_-]*)+\z/)
     end
   end
 end
diff --git a/lib/dbus/core_ext/class/attribute.rb b/lib/dbus/core_ext/class/attribute.rb
index d6b6030..f450186 100644
--- a/lib/dbus/core_ext/class/attribute.rb
+++ b/lib/dbus/core_ext/class/attribute.rb
@@ -2,7 +2,7 @@
 # copied from activesupport/core_ext from Rails, MIT license
 # https://github.com/rails/rails/tree/9794e85351243cac6d4e78adaba634b8e4ecad0a/activesupport/lib/active_support/core_ext
 
-require "dbus/core_ext/module/redefine_method"
+require_relative "../module/redefine_method"
 
 class Class
   # Declare a class-level attribute whose value is inheritable by subclasses.
diff --git a/lib/dbus/data.rb b/lib/dbus/data.rb
new file mode 100644
index 0000000..da8d5ec
--- /dev/null
+++ b/lib/dbus/data.rb
@@ -0,0 +1,821 @@
+# frozen_string_literal: true
+
+# This file is part of the ruby-dbus project
+# Copyright (C) 2022 Martin Vidner
+#
+# This library is free software; you can redistribute it and/or
+# modify it under the terms of the GNU Lesser General Public
+# License, version 2.1 as published by the Free Software Foundation.
+# See the file "COPYING" for the exact licensing terms.
+
+module DBus
+  # FIXME: in general, when an API gives me, a user, a choice,
+  # remember to make it easy for the case of:
+  # "I don't CARE, I don't WANT to care, WHY should I care?"
+
+  # Exact/explicit representation of D-Bus data types:
+  #
+  # - {Boolean}
+  # - {Byte}, {Int16}, {Int32}, {Int64}, {UInt16}, {UInt32}, {UInt64}
+  # - {Double}
+  # - {String}, {ObjectPath}, {Signature}
+  # - {Array}, {DictEntry}, {Struct}
+  # - {UnixFD}
+  # - {Variant}
+  #
+  # The common base type is {Base}.
+  #
+  # There are other intermediate classes in the inheritance hierarchy, using
+  # the names the specification uses, but they are an implementation detail:
+  #
+  # - A value is either {Basic} or a {Container}.
+  # - Basic values are either {Fixed}-size or {StringLike}.
+  module Data
+    # Given a plain Ruby *value* and wanting a D-Bus *type*,
+    # construct an appropriate {Data::Base} instance.
+    #
+    # @param type [SingleCompleteType,Type]
+    # @param value [::Object,Data::Base] a plain value; exact values also allowed
+    # @return [Data::Base]
+    # @raise TypeError
+    def make_typed(type, value)
+      type = DBus.type(type) unless type.is_a?(Type)
+      data_class = Data::BY_TYPE_CODE[type.sigtype]
+      # not nil because DBus.type validates
+
+      data_class.from_typed(value, type: type)
+    end
+    module_function :make_typed
+
+    # The base class for explicitly typed values.
+    #
+    # A value is either {Basic} or a {Container}.
+    # {Basic} values are either {Fixed}-size or {StringLike}.
+    class Base
+      # @!method self.basic?
+      # @return [Boolean]
+
+      # @!method self.fixed?
+      # @return [Boolean]
+
+      # @return [::Object] a valid value, plain-Ruby typed.
+      # @see Data::Container#exact_value
+      attr_reader :value
+
+      # @!method self.type_code
+      # @return [String] a single-character string, for example "a" for arrays
+
+      # @!method type
+      # @abstract
+      # Note that for Variants type=="v",
+      # for the specific see {Variant#member_type}
+      # @return [Type] the exact type of this value
+
+      # @!method self.from_typed(value, type:)
+      # @param value [::Object]
+      # @param type [Type]
+      # @return [Base]
+      # @api private
+      # Use {Data.make_typed} instead.
+      # Construct an instance of the specific subclass, with a type further
+      # specified in the *type* argument.
+
+      # Child classes must validate *value*.
+      def initialize(value)
+        @value = value
+      end
+
+      def ==(other)
+        @value == if other.is_a?(Base)
+                    other.value
+                  else
+                    other
+                  end
+      end
+
+      # Hash key equality
+      # See https://ruby-doc.org/core-3.0.0/Object.html#method-i-eql-3F
+      # Stricter than #== (RSpec: eq), 1==1.0 but 1.eql(1.0)->false
+      def eql?(other)
+        return false unless other.class == self.class
+
+        other.value.eql?(@value)
+        # TODO: this should work, now check derived classes, exact_value
+      end
+
+      # @param type [Type]
+      def self.assert_type_matches_class(type)
+        raise ArgumentError, "Expecting #{type_code.inspect} for class #{self}, got #{type.sigtype.inspect}" \
+          unless type.sigtype == type_code
+      end
+    end
+
+    # A value that is not a {Container}.
+    class Basic < Base
+      def self.basic?
+        true
+      end
+
+      # @return [Type]
+      def self.type
+        # memoize
+        @type ||= Type.new(type_code).freeze
+      end
+
+      def type
+        # The basic types can do this, unlike the containers
+        self.class.type
+      end
+
+      # @param value [::Object]
+      # @param type [Type]
+      # @return [Basic]
+      def self.from_typed(value, type:)
+        assert_type_matches_class(type)
+        new(value)
+      end
+    end
+
+    # A value that has a fixed size (unlike {StringLike}).
+    class Fixed < Basic
+      def self.fixed?
+        true
+      end
+
+      # most Fixed types are valid
+      # whatever bits from the wire are used to initialize them
+      # @param mode [:plain,:exact]
+      def self.from_raw(value, mode:)
+        return value if mode == :plain
+
+        new(value)
+      end
+
+      # @param endianness [:little,:big]
+      def marshall(endianness)
+        [value].pack(self.class.format[endianness])
+      end
+    end
+
+    # Format strings for String#unpack, both little- and big-endian.
+    Format = ::Struct.new(:little, :big)
+
+    # Represents integers
+    class Int < Fixed
+      # @!method self.range
+      # @return [Range] the full range of allowed values
+
+      # @param value [::Integer,DBus::Data::Int]
+      # @raise RangeError
+      def initialize(value)
+        value = value.value if value.is_a?(self.class)
+        r = self.class.range
+        raise RangeError, "#{value.inspect} is not a member of #{r}" unless r.member?(value)
+
+        super(value)
+      end
+    end
+
+    # Byte.
+    #
+    # TODO: a specialized ByteArray for `ay` may be useful,
+    # to save memory and for natural handling
+    class Byte < Int
+      def self.type_code
+        "y"
+      end
+
+      def self.alignment
+        1
+      end
+      FORMAT = Format.new("C", "C")
+      def self.format
+        FORMAT
+      end
+
+      def self.range
+        (0..255)
+      end
+    end
+
+    # Boolean: encoded as a {UInt32} but only 0 and 1 are valid.
+    class Boolean < Fixed
+      def self.type_code
+        "b"
+      end
+
+      def self.alignment
+        4
+      end
+      FORMAT = Format.new("L<", "L>")
+      def self.format
+        FORMAT
+      end
+
+      def self.validate_raw!(value)
+        return if [0, 1].member?(value)
+
+        raise InvalidPacketException, "BOOLEAN must be 0 or 1, found #{value}"
+      end
+
+      def self.from_raw(value, mode:)
+        validate_raw!(value)
+
+        value = value == 1
+        return value if mode == :plain
+
+        new(value)
+      end
+
+      # Accept any *value*, store its Ruby truth value
+      # (excepting another instance of this class, where use its {#value}).
+      #
+      # So new(0).value is true.
+      # @param value [::Object,DBus::Data::Boolean]
+      def initialize(value)
+        value = value.value if value.is_a?(self.class)
+        super(value ? true : false)
+      end
+
+      # @param endianness [:little,:big]
+      def marshall(endianness)
+        int = value ? 1 : 0
+        [int].pack(UInt32.format[endianness])
+      end
+    end
+
+    # Signed 16 bit integer.
+    class Int16 < Int
+      def self.type_code
+        "n"
+      end
+
+      def self.alignment
+        2
+      end
+
+      FORMAT = Format.new("s<", "s>")
+      def self.format
+        FORMAT
+      end
+
+      def self.range
+        (-32_768..32_767)
+      end
+    end
+
+    # Unsigned 16 bit integer.
+    class UInt16 < Int
+      def self.type_code
+        "q"
+      end
+
+      def self.alignment
+        2
+      end
+
+      FORMAT = Format.new("S<", "S>")
+      def self.format
+        FORMAT
+      end
+
+      def self.range
+        (0..65_535)
+      end
+    end
+
+    # Signed 32 bit integer.
+    class Int32 < Int
+      def self.type_code
+        "i"
+      end
+
+      def self.alignment
+        4
+      end
+
+      FORMAT = Format.new("l<", "l>")
+      def self.format
+        FORMAT
+      end
+
+      def self.range
+        (-2_147_483_648..2_147_483_647)
+      end
+    end
+
+    # Unsigned 32 bit integer.
+    class UInt32 < Int
+      def self.type_code
+        "u"
+      end
+
+      def self.alignment
+        4
+      end
+
+      FORMAT = Format.new("L<", "L>")
+      def self.format
+        FORMAT
+      end
+
+      def self.range
+        (0..4_294_967_295)
+      end
+    end
+
+    # Unix file descriptor, not implemented yet.
+    class UnixFD < UInt32
+      def self.type_code
+        "h"
+      end
+    end
+
+    # Signed 64 bit integer.
+    class Int64 < Int
+      def self.type_code
+        "x"
+      end
+
+      def self.alignment
+        8
+      end
+
+      FORMAT = Format.new("q<", "q>")
+      def self.format
+        FORMAT
+      end
+
+      def self.range
+        (-9_223_372_036_854_775_808..9_223_372_036_854_775_807)
+      end
+    end
+
+    # Unsigned 64 bit integer.
+    class UInt64 < Int
+      def self.type_code
+        "t"
+      end
+
+      def self.alignment
+        8
+      end
+
+      FORMAT = Format.new("Q<", "Q>")
+      def self.format
+        FORMAT
+      end
+
+      def self.range
+        (0..18_446_744_073_709_551_615)
+      end
+    end
+
+    # Double-precision floating point number.
+    class Double < Fixed
+      def self.type_code
+        "d"
+      end
+
+      def self.alignment
+        8
+      end
+
+      FORMAT = Format.new("E", "G")
+      def self.format
+        FORMAT
+      end
+
+      # @param value [#to_f,DBus::Data::Double]
+      # @raise TypeError,ArgumentError
+      def initialize(value)
+        value = value.value if value.is_a?(self.class)
+        value = Kernel.Float(value)
+        super(value)
+      end
+    end
+
+    # {DBus::Data::String}, {DBus::Data::ObjectPath}, or {DBus::Data::Signature}.
+    class StringLike < Basic
+      def self.fixed?
+        false
+      end
+
+      def initialize(value)
+        if value.is_a?(self.class)
+          value = value.value
+        else
+          self.class.validate_raw!(value)
+        end
+
+        super(value)
+      end
+    end
+
+    # UTF-8 encoded string.
+    class String < StringLike
+      def self.type_code
+        "s"
+      end
+
+      def self.alignment
+        4
+      end
+
+      def self.size_class
+        UInt32
+      end
+
+      def self.validate_raw!(value)
+        value.each_codepoint do |cp|
+          raise InvalidPacketException, "Invalid string, contains NUL" if cp.zero?
+        end
+      rescue ArgumentError
+        raise InvalidPacketException, "Invalid string, not in UTF-8"
+      end
+
+      def self.from_raw(value, mode:)
+        value.force_encoding(Encoding::UTF_8)
+        if mode == :plain
+          validate_raw!(value)
+          return value
+        end
+
+        new(value)
+      end
+    end
+
+    # See also {DBus::ObjectPath}
+    class ObjectPath < StringLike
+      def self.type_code
+        "o"
+      end
+
+      def self.alignment
+        4
+      end
+
+      def self.size_class
+        UInt32
+      end
+
+      # @raise InvalidPacketException
+      def self.validate_raw!(value)
+        DBus::ObjectPath.new(value)
+      rescue DBus::Error => e
+        raise InvalidPacketException, e.message
+      end
+
+      def self.from_raw(value, mode:)
+        if mode == :plain
+          validate_raw!(value)
+          return value
+        end
+
+        new(value)
+      end
+    end
+
+    # Signature string, zero or more single complete types.
+    # See also {DBus::Type}
+    class Signature < StringLike
+      def self.type_code
+        "g"
+      end
+
+      def self.alignment
+        1
+      end
+
+      def self.size_class
+        Byte
+      end
+
+      # @return [::Array<Type>]
+      def self.validate_raw!(value)
+        DBus.types(value)
+      rescue Type::SignatureException => e
+        raise InvalidPacketException, "Invalid signature: #{e.message}"
+      end
+
+      def self.from_raw(value, mode:)
+        if mode == :plain
+          _types = validate_raw!(value)
+          return value
+        end
+
+        new(value)
+      end
+    end
+
+    # Contains one or more other values.
+    class Container < Base
+      def self.basic?
+        false
+      end
+
+      def self.fixed?
+        false
+      end
+
+      # For containers, the type varies among instances
+      # @see Base#type
+      attr_reader :type
+
+      # @return something that is, or contains, {Data::Base}.
+      #   Er, this docs kinda sucks.
+      def exact_value
+        @value
+      end
+
+      def value
+        @value.map(&:value)
+      end
+
+      # Hash key equality
+      # See https://ruby-doc.org/core-3.0.0/Object.html#method-i-eql-3F
+      # Stricter than #== (RSpec: eq), 1==1.0 but 1.eql(1.0)->false
+      def eql?(other)
+        return false unless other.class == self.class
+
+        other.exact_value.eql?(exact_value)
+      end
+
+      # def ==(other)
+      #   eql?(other) || super
+      # end
+    end
+
+    # An Array, or a Dictionary (Hash).
+    class Array < Container
+      def self.type_code
+        "a"
+      end
+
+      def self.alignment
+        4
+      end
+
+      # TODO: check that Hash keys are basic types
+      # @param mode [:plain,:exact]
+      # @param type [Type]
+      # @param hash [Boolean] are we unmarshalling an ARRAY of DICT_ENTRY
+      # @return [Data::Array]
+      def self.from_items(value, mode:, type:, hash: false)
+        value = Hash[value] if hash
+        return value if mode == :plain
+
+        new(value, type: type)
+      end
+
+      # @param value [::Object]
+      # @param type [Type]
+      # @return [Data::Array]
+      def self.from_typed(value, type:)
+        new(value, type: type) # initialize(::Array<Data::Base>)
+      end
+
+      def value
+        v = super
+        if type.child.sigtype == Type::DICT_ENTRY
+          # BTW this makes a copy so mutating it is pointless
+          v.to_h
+        else
+          v
+        end
+      end
+
+      # FIXME: should Data::Array be mutable?
+      # if it is, is its type mutable too?
+
+      # TODO: specify type or guess type?
+      # Data is the exact type, so its constructor should be exact
+      # and guesswork should be clearly labeled
+
+      # @param value [Data::Array,Enumerable]
+      # @param type [SingleCompleteType,Type]
+      def initialize(value, type:)
+        type = Type::Factory.make_type(type)
+        self.class.assert_type_matches_class(type)
+        @type = type
+
+        typed_value = case value
+                      when Data::Array
+                        unless value.type == type
+                          raise ArgumentError,
+                                "Specified type is #{type.inspect} but value type is #{value.type.inspect}"
+                        end
+
+                        value.exact_value
+                      else
+                        # TODO: Dict??
+                        value.map do |i|
+                          Data.make_typed(type.child, i)
+                        end
+                      end
+        super(typed_value)
+      end
+    end
+
+    # A fixed size, heterogenerous tuple.
+    #
+    # (The item count is fixed, not the byte size.)
+    class Struct < Container
+      def self.type_code
+        "r"
+      end
+
+      def self.alignment
+        8
+      end
+
+      # @param value [::Array]
+      def self.from_items(value, mode:, type:)
+        value.freeze
+        return value if mode == :plain
+
+        new(value, type: type)
+      end
+
+      # @param value [::Object] (#size, #each)
+      # @param type [Type]
+      # @return [Struct]
+      def self.from_typed(value, type:)
+        new(value, type: type)
+      end
+
+      # @param value [Data::Struct,Enumerable]
+      # @param type [SingleCompleteType,Type]
+      def initialize(value, type:)
+        type = Type::Factory.make_type(type)
+        self.class.assert_type_matches_class(type)
+        @type = type
+
+        typed_value = case value
+                      when self.class
+                        unless value.type == type
+                          raise ArgumentError,
+                                "Specified type is #{type.inspect} but value type is #{value.type.inspect}"
+                        end
+
+                        value.exact_value
+                      else
+                        member_types = type.members
+                        unless value.size == member_types.size
+                          raise ArgumentError, "Specified type has #{member_types.size} members " \
+                                               "but value has #{value.size} members"
+                        end
+
+                        member_types.zip(value).map do |item_type, item|
+                          Data.make_typed(item_type, item)
+                        end
+                      end
+        super(typed_value)
+      end
+
+      def ==(other)
+        case other
+        when ::Struct
+          @value.size == other.size &&
+            @value.zip(other.to_a).all? { |i, other_i| i == other_i }
+        else
+          super
+        end
+      end
+    end
+
+    # Dictionary/Hash entry.
+    # TODO: shouldn't instantiate?
+    class DictEntry < Struct
+      def self.type_code
+        "e"
+      end
+
+      # @param value [::Array]
+      def self.from_items(value, mode:, type:) # rubocop:disable Lint/UnusedMethodArgument
+        value.freeze
+        # DictEntry ignores the :exact mode
+        value
+      end
+
+      # @param value [::Object] (#size, #each)
+      # @param type [Type]
+      # @return [DictEntry]
+      def self.from_typed(value, type:)
+        new(value, type: type)
+      end
+    end
+
+    # A generic type.
+    #
+    # Implementation note: @value is a {Data::Base}.
+    class Variant < Container
+      def self.type_code
+        "v"
+      end
+
+      def self.alignment
+        1
+      end
+
+      def value
+        @value.value
+      end
+
+      # @param member_type [Type]
+      def self.from_items(value, mode:, member_type:)
+        return value if mode == :plain
+
+        new(value, member_type: member_type)
+      end
+
+      # @param value [::Object]
+      # @param type [Type]
+      # @return [Variant]
+      def self.from_typed(value, type:)
+        assert_type_matches_class(type)
+
+        # nil: decide on type of value
+        new(value, member_type: nil)
+      end
+
+      # @return [Type]
+      def self.type
+        # memoize
+        @type ||= Type.new(type_code).freeze
+      end
+
+      # Note that for Variants type.to_s=="v",
+      # for the specific see {Variant#member_type}
+      # @return [Type] the exact type of this value
+      def type
+        self.class.type
+      end
+
+      # @return [Type]
+      attr_reader :member_type
+
+      # Determine the type of *value*
+      # @param value [::Object]
+      # @return [Type]
+      # @api private
+      # See also {PacketMarshaller.make_variant}
+      def self.guess_type(value)
+        sct, = PacketMarshaller.make_variant(value)
+        DBus.type(sct)
+      end
+
+      # @param member_type [SingleCompleteType,Type,nil]
+      def initialize(value, member_type:)
+        member_type = Type::Factory.make_type(member_type) if member_type
+        # TODO: validate that the given *member_type* matches *value*
+        case value
+        when Data::Variant
+          # Copy the contained value instead of boxing it more
+          # TODO: except perhaps for round-tripping in exact mode?
+          @member_type = value.member_type
+          value = value.exact_value
+        when Data::Base
+          @member_type = member_type || value.type
+          raise ArgumentError, "Variant type #{@member_type} does not match value type #{value.type}" \
+            unless @member_type == value.type
+        else
+          @member_type = member_type || self.class.guess_type(value)
+          value = Data.make_typed(@member_type, value)
+        end
+        super(value)
+      end
+
+      # Internal helpers to keep the {DBus.variant} method working.
+      # Formerly it returned just a pair of [DBus.type(string_type), value]
+      # so let's provide [0], [1], .first, .last
+      def [](index)
+        case index
+        when 0
+          member_type
+        when 1
+          value
+        else
+          raise ArgumentError, "DBus.variant can only be indexed with 0 or 1, seen #{index.inspect}"
+        end
+      end
+
+      # @see #[]
+      def first
+        self[0]
+      end
+
+      # @see #[]
+      def last
+        self[1]
+      end
+    end
+
+    consts = constants.map { |c_sym| const_get(c_sym) }
+    classes = consts.find_all { |c| c.respond_to?(:type_code) }
+    by_type_code = classes.map { |cl| [cl.type_code, cl] }.to_h
+
+    # { "b" => Data::Boolean, "s" => Data::String, ...}
+    BY_TYPE_CODE = by_type_code
+  end
+end
diff --git a/lib/dbus/emits_changed_signal.rb b/lib/dbus/emits_changed_signal.rb
new file mode 100644
index 0000000..c7e4591
--- /dev/null
+++ b/lib/dbus/emits_changed_signal.rb
@@ -0,0 +1,83 @@
+# frozen_string_literal: true
+
+# This file is part of the ruby-dbus project
+# Copyright (C) 2022 Martin Vidner
+#
+# This library is free software; you can redistribute it and/or
+# modify it under the terms of the GNU Lesser General Public
+# License, version 2.1 as published by the Free Software Foundation.
+# See the file "COPYING" for the exact licensing terms.
+
+module DBus
+  # Describes the behavior of PropertiesChanged signal, for a single property
+  # or for an entire interface.
+  #
+  # The possible values are:
+  #
+  # - *true*: the signal is emitted with the value included.
+  # - *:invalidates*: the signal is emitted but the value is not included
+  #   in the signal.
+  # - *:const*: the property never changes value during the lifetime
+  #   of the object it belongs to, and hence the signal
+  #   is never emitted for it (but clients can cache the value)
+  # - *false*: the signal won't be emitted (clients should re-Get the property value)
+  #
+  # The default is:
+  # - for an interface: *true*
+  # - for a property: what the parent interface specifies
+  #
+  # @see DBus::Object.emits_changed_signal
+  # @see DBus::Object.dbus_attr_accessor
+  # @see https://dbus.freedesktop.org/doc/dbus-specification.html#introspection-format
+  #
+  # Immutable once constructed.
+  class EmitsChangedSignal
+    # @return [true,false,:const,:invalidates]
+    attr_reader :value
+
+    # @param value [true,false,:const,:invalidates,nil]
+    #   See class-level description above, {EmitsChangedSignal}.
+    # @param interface [Interface,nil]
+    #   If the (property-level) *value* is unspecified (nil), this is the
+    #   containing {Interface} to get the value from.
+    def initialize(value, interface: nil)
+      if value.nil?
+        raise ArgumentError, "Both arguments are nil" if interface.nil?
+
+        @value = interface.emits_changed_signal.value
+      else
+        expecting = [true, false, :const, :invalidates]
+        unless expecting.include?(value)
+          raise ArgumentError, "Expecting one of #{expecting.inspect}. Seen #{value.inspect}"
+        end
+
+        @value = value
+      end
+
+      freeze
+    end
+
+    # Return introspection XML string representation
+    # @return [String]
+    def to_xml
+      return "" if @value == true
+
+      "    <annotation name=\"org.freedesktop.DBus.Property.EmitsChangedSignal\" value=\"#{@value}\"/>\n"
+    end
+
+    def to_s
+      @value.to_s
+    end
+
+    def ==(other)
+      if other.is_a?(self.class)
+        other.value == @value
+      else
+        other == value
+      end
+    end
+    alias eql? ==
+
+    DEFAULT_ECS = EmitsChangedSignal.new(true)
+  end
+end
diff --git a/lib/dbus/error.rb b/lib/dbus/error.rb
index f271824..4d58629 100644
--- a/lib/dbus/error.rb
+++ b/lib/dbus/error.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
 # error.rb
 #
 # This file is part of the ruby-dbus project
@@ -32,7 +34,7 @@ module DBus
       end
       # TODO: validate error name
     end
-  end # class Error
+  end
 
   # @example raise a generic error
   #   raise DBus.error, "message"
@@ -43,4 +45,4 @@ module DBus
     DBus::Error.new(nil, name)
   end
   module_function :error
-end # module DBus
+end
diff --git a/lib/dbus/introspect.rb b/lib/dbus/introspect.rb
index 8194a27..92460ed 100644
--- a/lib/dbus/introspect.rb
+++ b/lib/dbus/introspect.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
 # dbus/introspection.rb - module containing a low-level D-Bus introspection implementation
 #
 # This file is part of the ruby-dbus project
@@ -10,9 +12,9 @@
 
 module DBus
   # Regular expressions that should match all method names.
-  METHOD_SIGNAL_RE = /^[A-Za-z][A-Za-z0-9_]*$/
+  METHOD_SIGNAL_RE = /^[A-Za-z][A-Za-z0-9_]*$/.freeze
   # Regular expressions that should match all interface names.
-  INTERFACE_ELEMENT_RE = /^[A-Za-z][A-Za-z0-9_]*$/
+  INTERFACE_ELEMENT_RE = /^[A-Za-z][A-Za-z0-9_]*$/.freeze
 
   # Exception raised when an invalid class definition is encountered.
   class InvalidClassDefinition < Exception
@@ -24,21 +26,42 @@ module DBus
   # method call instantiates and configures this class for us.
   #
   # It also is the local definition of interface exported by the program.
-  # At the client side, see ProxyObjectInterface
+  # At the client side, see {ProxyObjectInterface}.
   class Interface
-    # The name of the interface. String
+    # @return [String] The name of the interface.
     attr_reader :name
-    # The methods that are part of the interface. Hash: Symbol => DBus::Method
+    # @return [Hash{Symbol => DBus::Method}] The methods that are part of the interface.
     attr_reader :methods
-    # The signals that are part of the interface. Hash: Symbol => Signal
+    # @return [Hash{Symbol => Signal}] The signals that are part of the interface.
     attr_reader :signals
 
+    # @return [Hash{Symbol => Property}]
+    attr_reader :properties
+
+    # @return [EmitsChangedSignal]
+    attr_reader :emits_changed_signal
+
     # Creates a new interface with a given _name_.
     def initialize(name)
       validate_name(name)
       @name = name
       @methods = {}
       @signals = {}
+      @properties = {}
+      @emits_changed_signal = EmitsChangedSignal::DEFAULT_ECS
+    end
+
+    # Helper for {Object.emits_changed_signal=}.
+    # @api private
+    def emits_changed_signal=(ecs)
+      raise TypeError unless ecs.is_a? EmitsChangedSignal
+      # equal?: object identity
+      unless @emits_changed_signal.equal?(EmitsChangedSignal::DEFAULT_ECS) ||
+             @emits_changed_signal.value == ecs.value
+        raise "emits_change_signal was assigned more than once"
+      end
+
+      @emits_changed_signal = ecs
     end
 
     # Validates a service _name_.
@@ -47,33 +70,57 @@ module DBus
       raise InvalidIntrospectionData if name =~ /^\./ || name =~ /\.$/
       raise InvalidIntrospectionData if name =~ /\.\./
       raise InvalidIntrospectionData if name !~ /\./
+
       name.split(".").each do |element|
         raise InvalidIntrospectionData if element !~ INTERFACE_ELEMENT_RE
       end
     end
 
-    # Helper method for defining a method _m_.
-    def define(m)
-      if m.is_a?(Method)
-        @methods[m.name.to_sym] = m
-      elsif m.is_a?(Signal)
-        @signals[m.name.to_sym] = m
-      end
+    # Add _ifc_el_ as a known {Method}, {Signal} or {Property}
+    # @param ifc_el [InterfaceElement]
+    def define(ifc_el)
+      name = ifc_el.name.to_sym
+      category = case ifc_el
+                 when Method
+                   @methods
+                 when Signal
+                   @signals
+                 when Property
+                   @properties
+                 end
+      category[name] = ifc_el
     end
+    alias declare define
     alias << define
 
     # Defines a method with name _id_ and a given _prototype_ in the
     # interface.
+    # Better name: declare_method
     def define_method(id, prototype)
       m = Method.new(id)
       m.from_prototype(prototype)
       define(m)
     end
-  end # class Interface
+    alias declare_method define_method
+
+    # Return introspection XML string representation of the property.
+    # @return [String]
+    def to_xml
+      xml = "  <interface name=\"#{name}\">\n"
+      xml += emits_changed_signal.to_xml
+      methods.each_value { |m| xml += m.to_xml }
+      signals.each_value { |m| xml += m.to_xml }
+      properties.each_value { |m| xml += m.to_xml }
+      xml += "  </interface>\n"
+      xml
+    end
+  end
 
   # = A formal parameter has a name and a type
   class FormalParameter
+    # @return [#to_s]
     attr_reader :name
+    # @return [SingleCompleteType]
     attr_reader :type
 
     def initialize(name, type)
@@ -95,14 +142,16 @@ module DBus
   # This is a generic class for entities that are part of the interface
   # such as methods and signals.
   class InterfaceElement
-    # The name of the interface element. Symbol
+    # @return [Symbol] The name of the interface element
     attr_reader :name
-    # The parameters of the interface element. Array: FormalParameter
+
+    # @return [Array<FormalParameter>] The parameters of the interface element
     attr_reader :params
 
     # Validates element _name_.
     def validate_name(name)
       return if (name =~ METHOD_SIGNAL_RE) && (name.bytesize <= 255)
+
       raise InvalidMethodName, name
     end
 
@@ -123,13 +172,13 @@ module DBus
     def add_param(name_signature_pair)
       add_fparam(*name_signature_pair)
     end
-  end # class InterfaceElement
+  end
 
   # = D-Bus interface method class
   #
   # This is a class representing methods that are part of an interface.
   class Method < InterfaceElement
-    # The list of return values for the method. Array: FormalParameter
+    # @return [Array<FormalParameter>] The list of return values for the method
     attr_reader :rets
 
     # Creates a new method interface element with the given _name_.
@@ -139,15 +188,19 @@ module DBus
     end
 
     # Add a return value _name_ and _signature_.
+    # @param name [#to_s]
+    # @param signature [SingleCompleteType]
     def add_return(name, signature)
       @rets << FormalParameter.new(name, signature)
     end
 
     # Add parameter types by parsing the given _prototype_.
+    # @param prototype [Prototype]
     def from_prototype(prototype)
       prototype.split(/, */).each do |arg|
         arg = arg.split(" ")
         raise InvalidClassDefinition if arg.size != 2
+
         dir, arg = arg
         if arg =~ /:/
           arg = arg.split(":")
@@ -166,20 +219,21 @@ module DBus
     end
 
     # Return an XML string representation of the method interface elment.
+    # @return [String]
     def to_xml
-      xml = %(<method name="#{@name}">\n)
+      xml = "    <method name=\"#{@name}\">\n"
       @params.each do |param|
-        name = param.name ? %(name="#{param.name}" ) : ""
-        xml += %(<arg #{name}direction="in" type="#{param.type}"/>\n)
+        name = param.name ? "name=\"#{param.name}\" " : ""
+        xml += "      <arg #{name}direction=\"in\" type=\"#{param.type}\"/>\n"
       end
       @rets.each do |param|
-        name = param.name ? %(name="#{param.name}" ) : ""
-        xml += %(<arg #{name}direction="out" type="#{param.type}"/>\n)
+        name = param.name ? "name=\"#{param.name}\" " : ""
+        xml += "      <arg #{name}direction=\"out\" type=\"#{param.type}\"/>\n"
       end
-      xml += %(</method>\n)
+      xml += "    </method>\n"
       xml
     end
-  end # class Method
+  end
 
   # = D-Bus interface signal class
   #
@@ -201,13 +255,61 @@ module DBus
 
     # Return an XML string representation of the signal interface elment.
     def to_xml
-      xml = %(<signal name="#{@name}">\n)
+      xml = "    <signal name=\"#{@name}\">\n"
       @params.each do |param|
-        name = param.name ? %(name="#{param.name}" ) : ""
-        xml += %(<arg #{name}type="#{param.type}"/>\n)
+        name = param.name ? "name=\"#{param.name}\" " : ""
+        xml += "      <arg #{name}type=\"#{param.type}\"/>\n"
       end
-      xml += %(</signal>\n)
+      xml += "    </signal>\n"
       xml
     end
-  end # class Signal
-end # module DBus
+  end
+
+  # An (exported) property
+  # https://dbus.freedesktop.org/doc/dbus-specification.html#standard-interfaces-properties
+  class Property
+    # @return [Symbol] The name of the property, for example FooBar.
+    attr_reader :name
+    # @return [Type]
+    attr_reader :type
+    # @return [Symbol] :read :write or :readwrite
+    attr_reader :access
+
+    # @return [Symbol,nil] What to call at Ruby side.
+    #   (Always without the trailing `=`)
+    #   It is `nil` IFF representing a client-side proxy.
+    attr_reader :ruby_name
+
+    def initialize(name, type, access, ruby_name:)
+      @name = name.to_sym
+      type = DBus.type(type) unless type.is_a?(Type)
+      @type = type
+      @access = access
+      @ruby_name = ruby_name
+    end
+
+    # @return [Boolean]
+    def readable?
+      access == :read || access == :readwrite
+    end
+
+    # @return [Boolean]
+    def writable?
+      access == :write || access == :readwrite
+    end
+
+    # Return introspection XML string representation of the property.
+    def to_xml
+      "    <property type=\"#{@type}\" name=\"#{@name}\" access=\"#{@access}\"/>\n"
+    end
+
+    # @param xml_node [AbstractXML::Node]
+    # @return [Property]
+    def self.from_xml(xml_node)
+      name = xml_node["name"].to_sym
+      type = xml_node["type"]
+      access = xml_node["access"].to_sym
+      new(name, type, access, ruby_name: nil)
+    end
+  end
+end
diff --git a/lib/dbus/logger.rb b/lib/dbus/logger.rb
index 87d05f8..facf526 100644
--- a/lib/dbus/logger.rb
+++ b/lib/dbus/logger.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
 # dbus/logger.rb - debug logging
 #
 # This file is part of the ruby-dbus project
@@ -16,7 +18,7 @@ module DBus
   # with DEBUG if $DEBUG is set, otherwise INFO.
   def logger
     unless defined? @logger
-      @logger = Logger.new(STDERR)
+      @logger = Logger.new($stderr)
       @logger.level = $DEBUG ? Logger::DEBUG : Logger::INFO
     end
     @logger
diff --git a/lib/dbus/marshall.rb b/lib/dbus/marshall.rb
index 3a314a5..d18d892 100644
--- a/lib/dbus/marshall.rb
+++ b/lib/dbus/marshall.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
 # dbus.rb - Module containing the low-level D-Bus implementation
 #
 # This file is part of the ruby-dbus project
@@ -10,6 +12,8 @@
 
 require "socket"
 
+require_relative "../dbus/type"
+
 # = D-Bus main module
 #
 # Module containing all the D-Bus modules and classes.
@@ -21,211 +25,134 @@ module DBus
   # = D-Bus packet unmarshaller class
   #
   # Class that handles the conversion (unmarshalling) of payload data
-  # to Array.
+  # to #{::Object}s (in **plain** mode) or to {Data::Base} (in **exact** mode)
+  #
+  # Spelling note: this codebase always uses a double L
+  # in the "marshall" word and its inflections.
   class PacketUnmarshaller
-    # Index pointer that points to the byte in the data that is
-    # currently being processed.
-    #
-    # Used to kown what part of the buffer has been consumed by unmarshalling.
-    # FIXME: Maybe should be accessed with a "consumed_size" method.
-    attr_reader :idx
-
-    # Create a new unmarshaller for the given data _buffer_ and _endianness_.
+    # Create a new unmarshaller for the given data *buffer*.
+    # @param buffer [String]
+    # @param endianness [:little,:big]
     def initialize(buffer, endianness)
-      @buffy = buffer.dup
-      @endianness = endianness
-      if @endianness == BIG_END
-        @uint32 = "N"
-        @uint16 = "n"
-        @double = "G"
-      elsif @endianness == LIL_END
-        @uint32 = "V"
-        @uint16 = "v"
-        @double = "E"
-      else
-        raise InvalidPacketException, "Incorrect endianness #{@endianness}"
-      end
-      @idx = 0
+      # TODO: this dup can be avoided if we can prove
+      # that an IncompleteBufferException leaves the original *buffer* intact
+      buffer = buffer.dup
+      @raw_msg = RawMessage.new(buffer, endianness)
     end
 
     # Unmarshall the buffer for a given _signature_ and length _len_.
-    # Return an array of unmarshalled objects
-    def unmarshall(signature, len = nil)
-      if !len.nil?
-        if @buffy.bytesize < @idx + len
-          raise IncompleteBufferException
-        end
-      end
+    # Return an array of unmarshalled objects.
+    # @param signature [Signature]
+    # @param len [Integer,nil] if given, and there is not enough data
+    #   in the buffer, raise {IncompleteBufferException}
+    # @param mode [:plain,:exact]
+    # @return [Array<::Object,DBus::Data::Base>]
+    #    Objects in `:plain` mode, {DBus::Data::Base} in `:exact` mode
+    #    The array size corresponds to the number of types in *signature*.
+    # @raise IncompleteBufferException
+    # @raise InvalidPacketException
+    def unmarshall(signature, len = nil, mode: :plain)
+      @raw_msg.want!(len) if len
+
       sigtree = Type::Parser.new(signature).parse
       ret = []
       sigtree.each do |elem|
-        ret << do_parse(elem)
+        ret << do_parse(elem, mode: mode)
       end
       ret
     end
 
-    # Align the pointer index on a byte index of _a_, where a
-    # must be 1, 2, 4 or 8.
-    def align(a)
-      case a
-      when 1
-        nil
-      when 2, 4, 8
-        bits = a - 1
-        @idx = @idx + bits & ~bits
-        raise IncompleteBufferException if @idx > @buffy.bytesize
-      else
-        raise "Unsupported alignment #{a}"
-      end
+    # after the headers, the body starts 8-aligned
+    def align_body
+      @raw_msg.align(8)
     end
 
-    ###############################################################
-    # FIXME: does anyone except the object itself call the above methods?
-    # Yes : Message marshalling code needs to align "body" to 8 byte boundary
-    private
-
-    # Retrieve the next _nbytes_ number of bytes from the buffer.
-    def read(nbytes)
-      raise IncompleteBufferException if @idx + nbytes > @buffy.bytesize
-      ret = @buffy.slice(@idx, nbytes)
-      @idx += nbytes
-      ret
+    # @return [Integer]
+    def consumed_size
+      @raw_msg.pos
     end
 
-    # Read the string length and string itself from the buffer.
-    # Return the string.
-    def read_string
-      align(4)
-      str_sz = read(4).unpack(@uint32)[0]
-      ret = @buffy.slice(@idx, str_sz)
-      raise IncompleteBufferException if @idx + str_sz + 1 > @buffy.bytesize
-      @idx += str_sz
-      if @buffy[@idx].ord != 0
-        raise InvalidPacketException, "String is not nul-terminated"
-      end
-      @idx += 1
-      # no exception, see check above
-      ret
-    end
+    private
 
-    # Read the signature length and signature itself from the buffer.
-    # Return the signature.
-    def read_signature
-      str_sz = read(1).unpack("C")[0]
-      ret = @buffy.slice(@idx, str_sz)
-      raise IncompleteBufferException if @idx + str_sz + 1 >= @buffy.bytesize
-      @idx += str_sz
-      if @buffy[@idx].ord != 0
-        raise InvalidPacketException, "Type is not nul-terminated"
-      end
-      @idx += 1
-      # no exception, see check above
-      ret
+    # @param data_class [Class] a subclass of Data::Base (specific?)
+    # @return [::Integer,::Float]
+    def aligned_read_value(data_class)
+      @raw_msg.align(data_class.alignment)
+      bytes = @raw_msg.read(data_class.alignment)
+      bytes.unpack1(data_class.format[@raw_msg.endianness])
     end
 
     # Based on the _signature_ type, retrieve a packet from the buffer
     # and return it.
-    def do_parse(signature)
+    # @param signature [Type]
+    # @param mode [:plain,:exact]
+    # @return [Data::Base]
+    def do_parse(signature, mode: :plain)
+      # FIXME: better naming for packet vs value
       packet = nil
-      case signature.sigtype
-      when Type::BYTE
-        packet = read(1).unpack("C")[0]
-      when Type::UINT16
-        align(2)
-        packet = read(2).unpack(@uint16)[0]
-      when Type::INT16
-        align(2)
-        packet = read(2).unpack(@uint16)[0]
-        if (packet & 0x8000) != 0
-          packet -= 0x10000
-        end
-      when Type::UINT32, Type::UNIX_FD
-        align(4)
-        packet = read(4).unpack(@uint32)[0]
-      when Type::INT32
-        align(4)
-        packet = read(4).unpack(@uint32)[0]
-        if (packet & 0x80000000) != 0
-          packet -= 0x100000000
-        end
-      when Type::UINT64
-        align(8)
-        packet_l = read(4).unpack(@uint32)[0]
-        packet_h = read(4).unpack(@uint32)[0]
-        packet = if @endianness == LIL_END
-                   packet_l + packet_h * 2**32
-                 else
-                   packet_l * 2**32 + packet_h
-                 end
-      when Type::INT64
-        align(8)
-        packet_l = read(4).unpack(@uint32)[0]
-        packet_h = read(4).unpack(@uint32)[0]
-        packet = if @endianness == LIL_END
-                   packet_l + packet_h * 2**32
-                 else
-                   packet_l * 2**32 + packet_h
-                 end
-        if (packet & 0x8000000000000000) != 0
-          packet -= 0x10000000000000000
-        end
-      when Type::DOUBLE
-        align(8)
-        packet = read(8).unpack(@double)[0]
-      when Type::BOOLEAN
-        align(4)
-        v = read(4).unpack(@uint32)[0]
-        raise InvalidPacketException if ![0, 1].member?(v)
-        packet = (v == 1)
-      when Type::ARRAY
-        align(4)
-        # checks please
-        array_sz = read(4).unpack(@uint32)[0]
-        raise InvalidPacketException if array_sz > 67_108_864
-
-        align(signature.child.alignment)
-        raise IncompleteBufferException if @idx + array_sz > @buffy.bytesize
-
-        packet = []
-        start_idx = @idx
-        while @idx - start_idx < array_sz
-          packet << do_parse(signature.child)
-        end
+      data_class = Data::BY_TYPE_CODE[signature.sigtype]
 
-        if signature.child.sigtype == Type::DICT_ENTRY
-          packet = Hash[packet]
-        end
-      when Type::STRUCT
-        align(8)
-        packet = []
-        signature.members.each do |elem|
-          packet << do_parse(elem)
-        end
-      when Type::VARIANT
-        string = read_signature
-        # error checking please
-        sig = Type::Parser.new(string).parse[0]
-        align(sig.alignment)
-        packet = do_parse(sig)
-      when Type::OBJECT_PATH
-        packet = read_string
-      when Type::STRING
-        packet = read_string
-        packet.force_encoding("UTF-8")
-      when Type::SIGNATURE
-        packet = read_signature
-      when Type::DICT_ENTRY
-        align(8)
-        key = do_parse(signature.members[0])
-        value = do_parse(signature.members[1])
-        packet = [key, value]
-      else
+      if data_class.nil?
         raise NotImplementedError,
               "sigtype: #{signature.sigtype} (#{signature.sigtype.chr})"
       end
+
+      if data_class.fixed?
+        value = aligned_read_value(data_class)
+        packet = data_class.from_raw(value, mode: mode)
+      elsif data_class.basic?
+        size = aligned_read_value(data_class.size_class)
+        # @raw_msg.align(data_class.alignment)
+        # ^ is not necessary because we've just read a suitably-aligned *size*
+        value = @raw_msg.read(size)
+        nul = @raw_msg.read(1)
+        if nul != "\u0000"
+          raise InvalidPacketException, "#{data_class} is not NUL-terminated"
+        end
+
+        packet = data_class.from_raw(value, mode: mode)
+      else
+        @raw_msg.align(data_class.alignment)
+        case signature.sigtype
+        when Type::STRUCT, Type::DICT_ENTRY
+          values = signature.members.map do |child_sig|
+            do_parse(child_sig, mode: mode)
+          end
+          packet = data_class.from_items(values, mode: mode, type: signature)
+
+        when Type::VARIANT
+          data_sig = do_parse(Data::Signature.type, mode: :exact) # -> Data::Signature
+          types = Type::Parser.new(data_sig.value).parse # -> Array<Type>
+          unless types.size == 1
+            raise InvalidPacketException, "VARIANT must contain 1 value, #{types.size} found"
+          end
+
+          type = types.first
+          value = do_parse(type, mode: mode)
+          packet = data_class.from_items(value, mode: mode, member_type: type)
+
+        when Type::ARRAY
+          array_bytes = aligned_read_value(Data::UInt32)
+          if array_bytes > 67_108_864
+            raise InvalidPacketException, "ARRAY body longer than 64MiB"
+          end
+
+          # needed here because of empty arrays
+          @raw_msg.align(signature.child.alignment)
+
+          items = []
+          end_pos = @raw_msg.pos + array_bytes
+          while @raw_msg.pos < end_pos
+            item = do_parse(signature.child, mode: mode)
+            items << item
+          end
+          is_hash = signature.child.sigtype == Type::DICT_ENTRY
+          packet = data_class.from_items(items, mode: mode, type: signature, hash: is_hash)
+        end
+      end
       packet
-    end # def do_parse
-  end # class PacketUnmarshaller
+    end
+  end
 
   # D-Bus packet marshaller class
   #
@@ -234,40 +161,35 @@ module DBus
   class PacketMarshaller
     # The current or result packet.
     # FIXME: allow access only when marshalling is finished
+    # @return [String]
     attr_reader :packet
 
+    # @return [:little,:big]
+    attr_reader :endianness
+
     # Create a new marshaller, setting the current packet to the
     # empty packet.
-    def initialize(offset = 0)
+    def initialize(offset = 0, endianness: HOST_ENDIANNESS)
+      @endianness = endianness
       @packet = ""
       @offset = offset # for correct alignment of nested marshallers
     end
 
-    # Round _n_ up to the specified power of two, _a_
-    def num_align(n, a)
-      case a
+    # Round _num_ up to the specified power of two, _alignment_
+    def num_align(num, alignment)
+      case alignment
       when 1, 2, 4, 8
-        bits = a - 1
-        n + bits & ~bits
+        bits = alignment - 1
+        num + bits & ~bits
       else
-        raise "Unsupported alignment"
+        raise ArgumentError, "Unsupported alignment #{alignment}"
       end
     end
 
-    # Align the buffer with NULL (\0) bytes on a byte length of _a_.
-    def align(a)
-      @packet = @packet.ljust(num_align(@offset + @packet.bytesize, a) - @offset, 0.chr)
-    end
-
-    # Append the the string _str_ itself to the packet.
-    def append_string(str)
-      align(4)
-      @packet += [str.bytesize].pack("L") + [str].pack("Z*")
-    end
-
-    # Append the the signature _signature_ itself to the packet.
-    def append_signature(str)
-      @packet += str.bytesize.chr + str + "\0"
+    # Align the buffer with NULL (\0) bytes on a byte length of _alignment_.
+    def align(alignment)
+      pad_count = num_align(@offset + @packet.bytesize, alignment) - @offset
+      @packet = @packet.ljust(pad_count, 0.chr)
     end
 
     # Append the array type _type_ to the packet and allow for appending
@@ -282,7 +204,9 @@ module DBus
       yield
       sz = @packet.bytesize - contentidx
       raise InvalidPacketException if sz > 67_108_864
-      @packet[sizeidx...sizeidx + 4] = [sz].pack("L")
+
+      sz_data = Data::UInt32.new(sz)
+      @packet[sizeidx...sizeidx + 4] = sz_data.marshall(endianness)
     end
 
     # Align and allow for appending struct fields.
@@ -294,110 +218,129 @@ module DBus
     # Append a value _val_ to the packet based on its _type_.
     #
     # Host native endianness is used, declared in Message#marshall
+    #
+    # @param type [SingleCompleteType] (or Integer or {Type})
+    # @param val [::Object]
     def append(type, val)
       raise TypeException, "Cannot send nil" if val.nil?
 
       type = type.chr if type.is_a?(Integer)
       type = Type::Parser.new(type).parse[0] if type.is_a?(String)
-      case type.sigtype
-      when Type::BYTE
-        @packet += val.chr
-      when Type::UINT32, Type::UNIX_FD
-        align(4)
-        @packet += [val].pack("L")
-      when Type::UINT64
-        align(8)
-        @packet += [val].pack("Q")
-      when Type::INT64
-        align(8)
-        @packet += [val].pack("q")
-      when Type::INT32
-        align(4)
-        @packet += [val].pack("l")
-      when Type::UINT16
-        align(2)
-        @packet += [val].pack("S")
-      when Type::INT16
-        align(2)
-        @packet += [val].pack("s")
-      when Type::DOUBLE
-        align(8)
-        @packet += [val].pack("d")
-      when Type::BOOLEAN
-        align(4)
-        @packet += if val
-                     [1].pack("L")
-                   else
-                     [0].pack("L")
-                   end
-      when Type::OBJECT_PATH
-        append_string(val)
-      when Type::STRING
-        append_string(val)
-      when Type::SIGNATURE
-        append_signature(val)
-      when Type::VARIANT
-        vartype = nil
-        if val.is_a?(Array) && val.size == 2
-          if val[0].is_a?(DBus::Type::Type)
-            vartype, vardata = val
-          elsif val[0].is_a?(String)
-            begin
-              parsed = Type::Parser.new(val[0]).parse
-              vartype = parsed[0] if parsed.size == 1
-              vardata = val[1]
-            rescue Type::SignatureException
-              # no assignment
+      # type is [Type] now
+      data_class = Data::BY_TYPE_CODE[type.sigtype]
+      if data_class.nil?
+        raise NotImplementedError,
+              "sigtype: #{type.sigtype} (#{type.sigtype.chr})"
+      end
+
+      if data_class.fixed?
+        align(data_class.alignment)
+        data = data_class.new(val)
+        @packet += data.marshall(endianness)
+      elsif data_class.basic?
+        val = val.value if val.is_a?(Data::Basic)
+        align(data_class.size_class.alignment)
+        size_data = data_class.size_class.new(val.bytesize)
+        @packet += size_data.marshall(endianness)
+        # Z* makes a binary string, as opposed to interpolation
+        @packet += [val].pack("Z*")
+      else
+        case type.sigtype
+
+        when Type::VARIANT
+          append_variant(val)
+        when Type::ARRAY
+          val = val.exact_value if val.is_a?(Data::Array)
+          append_array(type.child, val)
+        when Type::STRUCT, Type::DICT_ENTRY
+          val = val.exact_value if val.is_a?(Data::Struct) || val.is_a?(Data::DictEntry)
+          unless val.is_a?(Array) || val.is_a?(Struct)
+            type_name = Type::TYPE_MAPPING[type.sigtype].first
+            raise TypeException, "#{type_name} expects an Array or Struct, seen #{val.class}"
+          end
+
+          if type.sigtype == Type::DICT_ENTRY && val.size != 2
+            raise TypeException, "DICT_ENTRY expects a pair"
+          end
+
+          if type.members.size != val.size
+            type_name = Type::TYPE_MAPPING[type.sigtype].first
+            raise TypeException, "#{type_name} has #{val.size} elements but type info for #{type.members.size}"
+          end
+
+          struct do
+            type.members.zip(val).each do |t, v|
+              append(t, v)
             end
           end
+        else
+          raise NotImplementedError,
+                "sigtype: #{type.sigtype} (#{type.sigtype.chr})"
         end
-        if vartype.nil?
-          vartype, vardata = PacketMarshaller.make_variant(val)
-          vartype = Type::Parser.new(vartype).parse[0]
-        end
+      end
+    end
 
-        append_signature(vartype.to_s)
-        align(vartype.alignment)
-        sub = PacketMarshaller.new(@offset + @packet.bytesize)
-        sub.append(vartype, vardata)
-        @packet += sub.packet
-      when Type::ARRAY
-        if val.is_a?(Hash)
-          raise TypeException, "Expected an Array but got a Hash" if type.child.sigtype != Type::DICT_ENTRY
-          # Damn ruby rocks here
-          val = val.to_a
-        end
-        # If string is recieved and ay is expected, explode the string
-        if val.is_a?(String) && type.child.sigtype == Type::BYTE
-          val = val.bytes
-        end
-        if !val.is_a?(Enumerable)
-          raise TypeException, "Expected an Enumerable of #{type.child.inspect} but got a #{val.class}"
-        end
-        array(type.child) do
-          val.each do |elem|
-            append(type.child, elem)
+    def append_variant(val)
+      vartype = nil
+      if val.is_a?(DBus::Data::Variant)
+        vartype = val.member_type
+        vardata = val.exact_value
+      elsif val.is_a?(DBus::Data::Container)
+        vartype = val.type
+        vardata = val.exact_value
+      elsif val.is_a?(DBus::Data::Base)
+        vartype = val.type
+        vardata = val.value
+      elsif val.is_a?(Array) && val.size == 2
+        case val[0]
+        when Type
+          vartype, vardata = val
+        # Ambiguous but easy to use, because Type
+        # cannot construct "as" "a{sv}" easily
+        when String
+          begin
+            parsed = Type::Parser.new(val[0]).parse
+            vartype = parsed[0] if parsed.size == 1
+            vardata = val[1]
+          rescue Type::SignatureException
+            # no assignment
           end
         end
-      when Type::STRUCT, Type::DICT_ENTRY
-        # TODO: use duck typing, val.respond_to?
-        raise TypeException, "Struct/DE expects an Array" if !val.is_a?(Array)
-        if type.sigtype == Type::DICT_ENTRY && val.size != 2
-          raise TypeException, "Dict entry expects a pair"
-        end
-        if type.members.size != val.size
-          raise TypeException, "Struct/DE has #{val.size} elements but type info for #{type.members.size}"
-        end
-        struct do
-          type.members.zip(val).each do |t, v|
-            append(t, v)
-          end
+      end
+      if vartype.nil?
+        vartype, vardata = PacketMarshaller.make_variant(val)
+        vartype = Type::Parser.new(vartype).parse[0]
+      end
+
+      append(Data::Signature.type, vartype.to_s)
+      align(vartype.alignment)
+      sub = PacketMarshaller.new(@offset + @packet.bytesize, endianness: endianness)
+      sub.append(vartype, vardata)
+      @packet += sub.packet
+    end
+
+    # @param child_type [Type]
+    def append_array(child_type, val)
+      if val.is_a?(Hash)
+        raise TypeException, "Expected an Array but got a Hash" if child_type.sigtype != Type::DICT_ENTRY
+
+        # Damn ruby rocks here
+        val = val.to_a
+      end
+      # If string is received and ay is expected, explode the string
+      if val.is_a?(String) && child_type.sigtype == Type::BYTE
+        val = val.bytes
+      end
+      if !val.is_a?(Enumerable)
+        raise TypeException, "Expected an Enumerable of #{child_type.inspect} but got a #{val.class}"
+      end
+
+      array(child_type) do
+        val.each do |elem|
+          append(child_type, elem)
         end
-      else
-        raise NotImplementedError,
-              "sigtype: #{type.sigtype} (#{type.sigtype.chr})"
       end
-    end # def append
+    end
 
     # Make a [signature, value] pair for a variant
     def self.make_variant(value)
@@ -417,17 +360,25 @@ module DBus
       elsif value.is_a? Hash
         h = {}
         value.each_key { |k| h[k] = make_variant(value[k]) }
-        ["a{sv}", h]
+        key_type = if value.empty?
+                     "s"
+                   else
+                     t, = make_variant(value.first.first)
+                     t
+                   end
+        ["a{#{key_type}v}", h]
       elsif value.respond_to? :to_str
         ["s", value.to_str]
       elsif value.respond_to? :to_int
         i = value.to_int
-        if -2_147_483_648 <= i && i < 2_147_483_648
+        if Data::Int32.range.cover?(i)
           ["i", i]
-        else
+        elsif Data::Int64.range.cover?(i)
           ["x", i]
+        else
+          ["t", i]
         end
       end
     end
-  end # class PacketMarshaller
-end # module DBus
+  end
+end
diff --git a/lib/dbus/matchrule.rb b/lib/dbus/matchrule.rb
index ef92140..4e4a69b 100644
--- a/lib/dbus/matchrule.rb
+++ b/lib/dbus/matchrule.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
 # This file is part of the ruby-dbus project
 # Copyright (C) 2007 Arnaud Cornet and Paul van Tilburg
 #
@@ -27,7 +29,7 @@ module DBus
     attr_accessor :path
     # The destination filter.
     attr_accessor :destination
-    # The type type that is matched.
+    # @return [String] The type type that is matched.
     attr_reader :type
 
     # Create a new match rule
@@ -35,13 +37,14 @@ module DBus
       @sender = @interface = @member = @path = @destination = @type = nil
     end
 
-    # Set the message types to filter to type _t_.
+    # Set the message types to filter to type _typ_.
     # Possible message types are: signal, method_call, method_return, and error.
-    def type=(t)
-      if !["signal", "method_call", "method_return", "error"].member?(t)
-        raise MatchRuleException, t
+    def type=(typ)
+      if !["signal", "method_call", "method_return", "error"].member?(typ)
+        raise MatchRuleException, typ
       end
-      @type = t
+
+      @type = typ
     end
 
     # Returns a match rule string version of the object. E.g.:
@@ -58,11 +61,13 @@ module DBus
     def from_s(str)
       str.split(",").each do |eq|
         next unless eq =~ /^(.*)='([^']*)'$/
+
         # "
         name = Regexp.last_match(1)
         val = Regexp.last_match(2)
         raise MatchRuleException, name unless FILTERS.member?(name.to_sym)
-        method(name + "=").call(val)
+
+        method("#{name}=").call(val)
       end
       self
     end
@@ -80,18 +85,13 @@ module DBus
 
     # Determines whether a message _msg_ matches the match rule.
     def match(msg)
-      if @type
-        if { Message::SIGNAL => "signal", Message::METHOD_CALL => "method_call",
-             Message::METHOD_RETURN => "method_return",
-             Message::ERROR => "error" }[msg.message_type] != @type
-          return false
-        end
-      end
+      return false if @type && @type != msg.message_type_s
       return false if @interface && @interface != msg.interface
       return false if @member && @member != msg.member
       return false if @path && @path != msg.path
+
       # FIXME: sender and destination are ignored
       true
     end
-  end # class MatchRule
-end # module D-Bus
+  end
+end
diff --git a/lib/dbus/message.rb b/lib/dbus/message.rb
index 726037e..99bf62b 100644
--- a/lib/dbus/message.rb
+++ b/lib/dbus/message.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
 # dbus.rb - Module containing the low-level D-Bus implementation
 #
 # This file is part of the ruby-dbus project
@@ -8,6 +10,8 @@
 # License, version 2.1 as published by the Free Software Foundation.
 # See the file "COPYING" for the exact licensing terms.
 
+require_relative "raw_message"
+
 # = D-Bus main module
 #
 # Module containing all the D-Bus modules and classes.
@@ -27,7 +31,7 @@ module DBus
     # Mutex that protects updates on the serial number.
     @@serial_mutex = Mutex.new
     # Type of a message (by specification).
-    MESSAGE_SIGNATURE = "yyyyuua(yv)".freeze
+    MESSAGE_SIGNATURE = "yyyyuua(yv)"
 
     # FIXME: following message type constants should be under Message::Type IMO
     # well, yeah sure
@@ -43,6 +47,9 @@ module DBus
     # Signal message type.
     SIGNAL = 4
 
+    # Names used by signal match rules
+    TYPE_NAMES = ["invalid", "method_call", "method_return", "error", "signal"].freeze
+
     # Message flag signyfing that no reply is expected.
     NO_REPLY_EXPECTED = 0x1
     # Message flag signifying that no automatic start is required/must be
@@ -66,11 +73,13 @@ module DBus
     attr_accessor :sender
     # The signature of the message contents.
     attr_accessor :signature
-    # The serial number of the message this message is a reply for.
+    # @return [Integer] (u32)
+    #   The serial number of the message this message is a reply for.
     attr_accessor :reply_serial
     # The protocol.
     attr_reader :protocol
-    # The serial of the message.
+    # @return [Integer] (u32)
+    #   The serial of the message.
     attr_reader :serial
     # The parameters of the message.
     attr_reader :params
@@ -105,22 +114,28 @@ module DBus
       "error_name=#{error_name}"
     end
 
-    # Create a regular reply to a message _m_.
-    def self.method_return(m)
-      MethodReturnMessage.new.reply_to(m)
+    # @return [String] name of message type, as used in match rules:
+    #    "method_call", "method_return", "signal", "error"
+    def message_type_s
+      TYPE_NAMES[message_type] || "unknown_type_#{message_type}"
+    end
+
+    # Create a regular reply to a message _msg_.
+    def self.method_return(msg)
+      MethodReturnMessage.new.reply_to(msg)
     end
 
-    # Create an error reply to a message _m_.
-    def self.error(m, error_name, description = nil)
-      ErrorMessage.new(error_name, description).reply_to(m)
+    # Create an error reply to a message _msg_.
+    def self.error(msg, error_name, description = nil)
+      ErrorMessage.new(error_name, description).reply_to(msg)
     end
 
-    # Mark this message as a reply to a another message _m_, taking
-    # the serial number of _m_ as reply serial and the sender of _m_ as
+    # Mark this message as a reply to a another message _msg_, taking
+    # the serial number of _msg_ as reply serial and the sender of _msg_ as
     # destination.
-    def reply_to(m)
-      @reply_serial = m.serial
-      @destination = m.sender
+    def reply_to(msg)
+      @reply_serial = msg.serial
+      @destination = msg.sender
       self
     end
 
@@ -131,6 +146,10 @@ module DBus
       @params << [type, val]
     end
 
+    # "l" or "B"
+    ENDIANNESS_CHAR = ENV.fetch("RUBY_DBUS_ENDIANNESS", HOST_END)
+    ENDIANNESS = RawMessage.endianness(ENDIANNESS_CHAR)
+
     # FIXME: what are these? a message element constant enumeration?
     # See method below, in a message, you have and array of optional parameters
     # that come with an index, to determine their meaning. The values are in
@@ -152,14 +171,14 @@ module DBus
         raise InvalidDestinationName
       end
 
-      params = PacketMarshaller.new
-      @params.each do |param|
-        params.append(param[0], param[1])
+      params_marshaller = PacketMarshaller.new(endianness: ENDIANNESS)
+      @params.each do |type, value|
+        params_marshaller.append(type, value)
       end
-      @body_length = params.packet.bytesize
+      @body_length = params_marshaller.packet.bytesize
 
-      marshaller = PacketMarshaller.new
-      marshaller.append(Type::BYTE, HOST_END)
+      marshaller = PacketMarshaller.new(endianness: ENDIANNESS)
+      marshaller.append(Type::BYTE, ENDIANNESS_CHAR.ord)
       marshaller.append(Type::BYTE, @message_type)
       marshaller.append(Type::BYTE, @flags)
       marshaller.append(Type::BYTE, @protocol)
@@ -178,10 +197,8 @@ module DBus
       marshaller.append("a(yv)", headers)
 
       marshaller.align(8)
-      @params.each do |param|
-        marshaller.append(param[0], param[1])
-      end
-      marshaller.packet
+
+      marshaller.packet + params_marshaller.packet
     end
 
     # Unmarshall a packet contained in the buffer _buf_ and set the
@@ -191,13 +208,7 @@ module DBus
     #   the detected message (self) and
     #   the index pointer of the buffer where the message data ended.
     def unmarshall_buffer(buf)
-      buf = buf.dup
-      endianness = if buf[0] == "l"
-                     LIL_END
-                   else
-                     BIG_END
-                   end
-      pu = PacketUnmarshaller.new(buf, endianness)
+      pu = PacketUnmarshaller.new(buf, RawMessage.endianness(buf[0]))
       mdata = pu.unmarshall(MESSAGE_SIGNATURE)
       _, @message_type, @flags, @protocol, @body_length, @serial,
         headers = mdata
@@ -222,21 +233,21 @@ module DBus
           @signature = struct[1]
         end
       end
-      pu.align(8)
-      if @body_length > 0 && @signature
+      pu.align_body
+      if @body_length.positive? && @signature
         @params = pu.unmarshall(@signature, @body_length)
       end
-      [self, pu.idx]
+      [self, pu.consumed_size]
     end
 
     # Make a new exception from ex, mark it as being caused by this message
     # @api private
-    def annotate_exception(ex)
-      new_ex = ex.exception("#{ex}; caused by #{self}")
-      new_ex.set_backtrace(ex.backtrace)
-      new_ex
+    def annotate_exception(exc)
+      new_exc = exc.exception("#{exc}; caused by #{self}")
+      new_exc.set_backtrace(exc.backtrace)
+      new_exc
     end
-  end # class Message
+  end
 
   class MethodReturnMessage < Message
     def initialize
@@ -251,17 +262,17 @@ module DBus
       add_param(Type::STRING, description) unless description.nil?
     end
 
-    def self.from_exception(ex)
-      name = if ex.is_a? DBus::Error
-               ex.name
+    def self.from_exception(exc)
+      name = if exc.is_a? DBus::Error
+               exc.name
              else
                "org.freedesktop.DBus.Error.Failed"
-               # ex.class.to_s # RuntimeError is not a valid name, has no dot
+               # exc.class.to_s # RuntimeError is not a valid name, has no dot
              end
-      description = ex.message
+      description = exc.message
       msg = new(name, description)
-      msg.add_param(DBus.type("as"), ex.backtrace)
+      msg.add_param(DBus.type("as"), exc.backtrace)
       msg
     end
   end
-end # module DBus
+end
diff --git a/lib/dbus/message_queue.rb b/lib/dbus/message_queue.rb
index 3d51a04..ff59ed9 100644
--- a/lib/dbus/message_queue.rb
+++ b/lib/dbus/message_queue.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
 # This file is part of the ruby-dbus project
 # Copyright (C) 2007 Arnaud Cornet and Paul van Tilburg
 # Copyright (C) 2009-2014 Martin Vidner
@@ -16,35 +18,49 @@ module DBus
     # The socket that is used to connect with the bus.
     attr_reader :socket
 
+    # The buffer size for messages.
+    MSG_BUF_SIZE = 4096
+
     def initialize(address)
+      DBus.logger.debug "MessageQueue: #{address}"
       @address = address
       @buffer = ""
+      # Reduce allocations by using a single buffer for our socket
+      @read_buffer = String.new(capacity: MSG_BUF_SIZE)
       @is_tcp = false
+      @mutex = Mutex.new
       connect
     end
 
-    # @param non_block [Boolean] if true, return nil instead of waiting
+    # @param blocking [Boolean]
+    #   true:  wait to return a {Message};
+    #   false: may return `nil`
     # @return [Message,nil] one message or nil if unavailable
     # @raise EOFError
     # @todo failure modes
-    def pop(non_block = false)
-      buffer_from_socket_nonblock
-      message = message_from_buffer_nonblock
-      unless non_block
-        # we can block
-        while message.nil?
-          r, _d, _d = IO.select([@socket])
-          if r && r[0] == @socket
-            buffer_from_socket_nonblock
-            message = message_from_buffer_nonblock
+    def pop(blocking: true)
+      # FIXME: this is not enough, the R/W test deadlocks on shared connections
+      @mutex.synchronize do
+        buffer_from_socket_nonblock
+        message = message_from_buffer_nonblock
+        if blocking
+          # we can block
+          while message.nil?
+            r, _d, _d = IO.select([@socket])
+            if r && r[0] == @socket
+              buffer_from_socket_nonblock
+              message = message_from_buffer_nonblock
+            end
           end
         end
+        message
       end
-      message
     end
 
     def push(message)
-      @socket.write(message.marshall)
+      @mutex.synchronize do
+        @socket.write(message.marshall)
+      end
     end
     alias << push
 
@@ -54,7 +70,7 @@ module DBus
     def connect
       addresses = @address.split ";"
       # connect to first one that succeeds
-      worked = addresses.find do |a|
+      addresses.find do |a|
         transport, keyvaluestring = a.split ":"
         kv_list = keyvaluestring.split ","
         kv_hash = {}
@@ -74,29 +90,29 @@ module DBus
           # ignore, report?
         end
       end
-      worked
       # returns the address that worked or nil.
       # how to report failure?
     end
 
     # Connect to a bus over tcp and initialize the connection.
     def connect_to_tcp(params)
-      # check if the path is sufficient
-      if params.key?("host") && params.key?("port")
+      host = params["host"]
+      port = params["port"]
+      if host && port
         begin
           # initialize the tcp socket
-          @socket = TCPSocket.new(params["host"], params["port"].to_i)
+          @socket = TCPSocket.new(host, port.to_i)
           @socket.fcntl(Fcntl::F_SETFD, Fcntl::FD_CLOEXEC)
           init_connection
           @is_tcp = true
         rescue Exception => e
           puts "Oops:", e
-          puts "Error: Could not establish connection to: #{@path}, will now exit."
+          puts "Error: Could not establish connection to: #{host}:#{port}, will now exit."
           exit(1) # a little harsh
         end
       else
         # Danger, Will Robinson: the specified "path" is not usable
-        puts "Error: supplied path: #{@path}, unusable! sorry."
+        puts "Error: supplied params: #{@params}, unusable! sorry."
       end
     end
 
@@ -125,7 +141,7 @@ module DBus
 
     # Initialize the connection to the bus.
     def init_connection
-      client = Client.new(@socket)
+      client = Authentication::Client.new(@socket)
       client.authenticate
     end
 
@@ -135,6 +151,7 @@ module DBus
     # @return [Message,nil] the message or nil if unavailable
     def message_from_buffer_nonblock
       return nil if @buffer.empty?
+
       ret = nil
       begin
         ret, size = Message.new.unmarshall_buffer(@buffer)
@@ -145,15 +162,12 @@ module DBus
       ret
     end
 
-    # The buffer size for messages.
-    MSG_BUF_SIZE = 4096
-
     # Fill (append) the buffer from data that might be available on the
     # socket.
     # @return [void]
     # @raise EOFError
     def buffer_from_socket_nonblock
-      @buffer += @socket.read_nonblock(MSG_BUF_SIZE)
+      @buffer += @socket.read_nonblock(MSG_BUF_SIZE, @read_buffer)
     rescue EOFError
       raise # the caller expects it
     rescue Errno::EAGAIN
@@ -161,6 +175,7 @@ module DBus
     rescue Exception => e
       puts "Oops:", e
       raise if @is_tcp # why?
+
       puts "WARNING: read_nonblock failed, falling back to .recv"
       @buffer += @socket.recv(MSG_BUF_SIZE)
     end
diff --git a/lib/dbus/object.rb b/lib/dbus/object.rb
index cd55d20..2ed2641 100644
--- a/lib/dbus/object.rb
+++ b/lib/dbus/object.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
 # This file is part of the ruby-dbus project
 # Copyright (C) 2007 Arnaud Cornet and Paul van Tilburg
 #
@@ -6,10 +8,11 @@
 # License, version 2.1 as published by the Free Software Foundation.
 # See the file "COPYING" for the exact licensing terms.
 
-require "thread"
-require "dbus/core_ext/class/attribute"
+require_relative "core_ext/class/attribute"
 
 module DBus
+  PROPERTY_INTERFACE = "org.freedesktop.DBus.Properties"
+
   # Exported object type
   # = Exportable D-Bus object class
   #
@@ -18,6 +21,7 @@ module DBus
   class Object
     # The path of the object.
     attr_reader :path
+
     # The interfaces that the object supports. Hash: String => Interface
     my_class_attribute :intfs
     self.intfs = {}
@@ -41,11 +45,13 @@ module DBus
       when Message::METHOD_CALL
         reply = nil
         begin
-          if !intfs[msg.interface]
+          iface = intfs[msg.interface]
+          if !iface
             raise DBus.error("org.freedesktop.DBus.Error.UnknownMethod"),
                   "Interface \"#{msg.interface}\" of object \"#{msg.path}\" doesn't exist"
           end
-          meth = intfs[msg.interface].methods[msg.member.to_sym]
+          member_sym = msg.member.to_sym
+          meth = iface.methods[member_sym]
           if !meth
             raise DBus.error("org.freedesktop.DBus.Error.UnknownMethod"),
                   "Method \"#{msg.member}\" on interface \"#{msg.interface}\" of object \"#{msg.path}\" doesn't exist"
@@ -55,11 +61,12 @@ module DBus
           retdata = [*retdata]
 
           reply = Message.method_return(msg)
-          meth.rets.zip(retdata).each do |rsig, rdata|
-            reply.add_param(rsig.type, rdata)
+          rsigs = meth.rets.map(&:type)
+          rsigs.zip(retdata).each do |rsig, rdata|
+            reply.add_param(rsig, rdata)
           end
-        rescue => ex
-          dbus_msg_exc = msg.annotate_exception(ex)
+        rescue StandardError => e
+          dbus_msg_exc = msg.annotate_exception(e)
           reply = ErrorMessage.from_exception(dbus_msg_exc).reply_to(msg)
         end
         @service.bus.message_queue.push(reply)
@@ -68,62 +75,417 @@ module DBus
 
     # Select (and create) the interface that the following defined methods
     # belong to.
-    def self.dbus_interface(s)
+    # @param name [String] interface name like "org.example.ManagerManager"
+    # @see https://dbus.freedesktop.org/doc/dbus-specification.html#message-protocol-names-interface
+    def self.dbus_interface(name)
       @@intfs_mutex.synchronize do
-        @@cur_intf = intfs[s]
+        @@cur_intf = intfs[name]
         if !@@cur_intf
-          @@cur_intf = Interface.new(s)
+          @@cur_intf = Interface.new(name) # validates the name
           # As this is a mutable class_attr, we cannot use
-          #   self.intfs[s] = @@cur_intf                      # Hash#[]=
+          #   self.intfs[name] = @@cur_intf                      # Hash#[]=
           # as that would modify parent class attr in place.
           # Using the setter lets a subclass have the new value
           # while the superclass keeps the old one.
-          self.intfs = intfs.merge(s => @@cur_intf)
+          self.intfs = intfs.merge(name => @@cur_intf)
+        end
+        begin
+          yield
+        ensure
+          @@cur_intf = nil
         end
-        yield
-        @@cur_intf = nil
       end
     end
 
-    # Dummy undefined interface class.
-    class UndefinedInterface < ScriptError
+    # Forgetting to declare the interface for a method/signal/property
+    # is a ScriptError.
+    class UndefinedInterface < ScriptError # rubocop:disable Lint/InheritException
       def initialize(sym)
-        super "No interface specified for #{sym}"
+        super "No interface specified for #{sym}. Enclose it in dbus_interface."
+      end
+    end
+
+    # Declare the behavior of PropertiesChanged signal,
+    # common for all properties in this interface
+    # (individual properties may override it)
+    # @example
+    #   self.emits_changed_signal = :invalidates
+    # @param [true,false,:const,:invalidates] value
+    def self.emits_changed_signal=(value)
+      raise UndefinedInterface, :emits_changed_signal if @@cur_intf.nil?
+
+      @@cur_intf.emits_changed_signal = EmitsChangedSignal.new(value)
+    end
+
+    # A read-write property accessing an instance variable.
+    # A combination of `attr_accessor` and {.dbus_accessor}.
+    #
+    # PropertiesChanged signal will be emitted whenever `foo_bar=` is used
+    # but not when @foo_bar is written directly.
+    #
+    # @param ruby_name [Symbol] :foo_bar is exposed as FooBar;
+    #   use dbus_name to override
+    # @param type [Type,SingleCompleteType]
+    #   a signature like "s" or "a(uus)" or Type::STRING
+    # @param dbus_name [String] if not given it is made
+    #   by CamelCasing the ruby_name. foo_bar becomes FooBar
+    #   to convert the Ruby convention to the DBus convention.
+    # @param emits_changed_signal [true,false,:const,:invalidates]
+    #   see {EmitsChangedSignal}; if unspecified, ask the interface.
+    # @return [void]
+    def self.dbus_attr_accessor(ruby_name, type, dbus_name: nil, emits_changed_signal: nil)
+      attr_accessor(ruby_name)
+
+      dbus_accessor(ruby_name, type, dbus_name: dbus_name, emits_changed_signal: emits_changed_signal)
+    end
+
+    # A read-only property accessing an instance variable.
+    # A combination of `attr_reader` and {.dbus_reader}.
+    #
+    # Whenever the property value gets changed from "inside" the object,
+    # you should emit the `PropertiesChanged` signal by calling
+    # {#dbus_properties_changed}.
+    #
+    #   dbus_properties_changed(interface_name, {dbus_name.to_s => value}, [])
+    #
+    # or, omitting the value in the signal,
+    #
+    #   dbus_properties_changed(interface_name, {}, [dbus_name.to_s])
+    #
+    # @param  (see .dbus_attr_accessor)
+    # @return (see .dbus_attr_accessor)
+    def self.dbus_attr_reader(ruby_name, type, dbus_name: nil, emits_changed_signal: nil)
+      attr_reader(ruby_name)
+
+      dbus_reader(ruby_name, type, dbus_name: dbus_name, emits_changed_signal: emits_changed_signal)
+    end
+
+    # A write-only property accessing an instance variable.
+    # A combination of `attr_writer` and {.dbus_writer}.
+    #
+    # @param  (see .dbus_attr_accessor)
+    # @return (see .dbus_attr_accessor)
+    def self.dbus_attr_writer(ruby_name, type, dbus_name: nil, emits_changed_signal: nil)
+      attr_writer(ruby_name)
+
+      dbus_writer(ruby_name, type, dbus_name: dbus_name, emits_changed_signal: emits_changed_signal)
+    end
+
+    # A read-write property using a pair of reader/writer methods
+    # (which must already exist).
+    # (To directly access an instance variable, use {.dbus_attr_accessor} instead)
+    #
+    # Uses {.dbus_watcher} to set up the PropertiesChanged signal.
+    #
+    # @param  (see .dbus_attr_accessor)
+    # @return (see .dbus_attr_accessor)
+    def self.dbus_accessor(ruby_name, type, dbus_name: nil, emits_changed_signal: nil)
+      raise UndefinedInterface, ruby_name if @@cur_intf.nil?
+
+      dbus_name = make_dbus_name(ruby_name, dbus_name: dbus_name)
+      property = Property.new(dbus_name, type, :readwrite, ruby_name: ruby_name)
+      @@cur_intf.define(property)
+
+      dbus_watcher(ruby_name, dbus_name: dbus_name, emits_changed_signal: emits_changed_signal)
+    end
+
+    # A read-only property accessing a reader method (which must already exist).
+    # (To directly access an instance variable, use {.dbus_attr_reader} instead)
+    #
+    # At the D-Bus side the property is read only but it makes perfect sense to
+    # implement it with a read-write attr_accessor. In that case this method
+    # uses {.dbus_watcher} to set up the PropertiesChanged signal.
+    #
+    #   attr_accessor :foo_bar
+    #   dbus_reader :foo_bar, "s"
+    #
+    # If the property value should change by other means than its attr_writer,
+    # you should emit the `PropertiesChanged` signal by calling
+    # {#dbus_properties_changed}.
+    #
+    #   dbus_properties_changed(interface_name, {dbus_name.to_s => value}, [])
+    #
+    # or, omitting the value in the signal,
+    #
+    #   dbus_properties_changed(interface_name, {}, [dbus_name.to_s])
+    #
+    # @param  (see .dbus_attr_accessor)
+    # @return (see .dbus_attr_accessor)
+    def self.dbus_reader(ruby_name, type, dbus_name: nil, emits_changed_signal: nil)
+      raise UndefinedInterface, ruby_name if @@cur_intf.nil?
+
+      dbus_name = make_dbus_name(ruby_name, dbus_name: dbus_name)
+      property = Property.new(dbus_name, type, :read, ruby_name: ruby_name)
+      @@cur_intf.define(property)
+
+      ruby_name_eq = "#{ruby_name}=".to_sym
+      return unless method_defined?(ruby_name_eq)
+
+      dbus_watcher(ruby_name, dbus_name: dbus_name, emits_changed_signal: emits_changed_signal)
+    end
+
+    # A write-only property accessing a writer method (which must already exist).
+    # (To directly access an instance variable, use {.dbus_attr_writer} instead)
+    #
+    # Uses {.dbus_watcher} to set up the PropertiesChanged signal.
+    #
+    # @param  (see .dbus_attr_accessor)
+    # @return (see .dbus_attr_accessor)
+    def self.dbus_writer(ruby_name, type, dbus_name: nil, emits_changed_signal: nil)
+      raise UndefinedInterface, ruby_name if @@cur_intf.nil?
+
+      dbus_name = make_dbus_name(ruby_name, dbus_name: dbus_name)
+      property = Property.new(dbus_name, type, :write, ruby_name: ruby_name)
+      @@cur_intf.define(property)
+
+      dbus_watcher(ruby_name, dbus_name: dbus_name, emits_changed_signal: emits_changed_signal)
+    end
+
+    # Enables automatic sending of the PropertiesChanged signal.
+    # For *ruby_name* `foo_bar`, wrap `foo_bar=` so that it sends
+    # the signal for FooBar.
+    # The original version remains as `_original_foo_bar=`.
+    #
+    # @param ruby_name [Symbol] :foo_bar and :foo_bar= both mean the same thing
+    # @param dbus_name [String] if not given it is made
+    #   by CamelCasing the ruby_name. foo_bar becomes FooBar
+    #   to convert the Ruby convention to the DBus convention.
+    # @param emits_changed_signal [true,false,:const,:invalidates]
+    #   see {EmitsChangedSignal}; if unspecified, ask the interface.
+    # @return [void]
+    def self.dbus_watcher(ruby_name, dbus_name: nil, emits_changed_signal: nil)
+      raise UndefinedInterface, ruby_name if @@cur_intf.nil?
+
+      interface_name = @@cur_intf.name
+
+      ruby_name = ruby_name.to_s.sub(/=$/, "").to_sym
+      ruby_name_eq = "#{ruby_name}=".to_sym
+      original_ruby_name_eq = "_original_#{ruby_name_eq}"
+
+      dbus_name = make_dbus_name(ruby_name, dbus_name: dbus_name)
+
+      emits_changed_signal = EmitsChangedSignal.new(emits_changed_signal, interface: @@cur_intf)
+
+      # the argument order is alias_method(new_name, existing_name)
+      alias_method original_ruby_name_eq, ruby_name_eq
+      define_method ruby_name_eq do |value|
+        result = public_send(original_ruby_name_eq, value)
+
+        case emits_changed_signal.value
+        when true
+          # signature: "interface:s, changed_props:a{sv}, invalidated_props:as"
+          dbus_properties_changed(interface_name, { dbus_name.to_s => value }, [])
+        when :invalidates
+          dbus_properties_changed(interface_name, {}, [dbus_name.to_s])
+        when :const
+          # Oh my, seeing a value change of a supposedly constant property.
+          # Maybe should have raised at declaration time, don't make a fuss now.
+        when false
+          # Do nothing
+        end
+
+        result
       end
     end
 
     # Defines an exportable method on the object with the given name _sym_,
     # _prototype_ and the code in a block.
-    def self.dbus_method(sym, protoype = "", &block)
+    # @param prototype [Prototype]
+    def self.dbus_method(sym, prototype = "", &block)
       raise UndefinedInterface, sym if @@cur_intf.nil?
-      @@cur_intf.define(Method.new(sym.to_s).from_prototype(protoype))
-      define_method(Object.make_method_name(@@cur_intf.name, sym.to_s), &block)
+
+      @@cur_intf.define(Method.new(sym.to_s).from_prototype(prototype))
+
+      ruby_name = Object.make_method_name(@@cur_intf.name, sym.to_s)
+      # ::Module#define_method(name) { body }
+      define_method(ruby_name, &block)
     end
 
     # Emits a signal from the object with the given _interface_, signal
     # _sig_ and arguments _args_.
+    # @param intf [Interface]
+    # @param sig [Signal]
+    # @param args arguments for the signal
     def emit(intf, sig, *args)
       @service.bus.emit(@service, self, intf, sig, *args)
     end
 
     # Defines a signal for the object with a given name _sym_ and _prototype_.
-    def self.dbus_signal(sym, protoype = "")
+    def self.dbus_signal(sym, prototype = "")
       raise UndefinedInterface, sym if @@cur_intf.nil?
+
       cur_intf = @@cur_intf
-      signal = Signal.new(sym.to_s).from_prototype(protoype)
-      cur_intf.define(Signal.new(sym.to_s).from_prototype(protoype))
+      signal = Signal.new(sym.to_s).from_prototype(prototype)
+      cur_intf.define(Signal.new(sym.to_s).from_prototype(prototype))
+
+      # ::Module#define_method(name) { body }
       define_method(sym.to_s) do |*args|
         emit(cur_intf, signal, *args)
       end
     end
 
-    ####################################################################
-
     # Helper method that returns a method name generated from the interface
     # name _intfname_ and method name _methname_.
     # @api private
     def self.make_method_name(intfname, methname)
       "#{intfname}%%#{methname}"
     end
+
+    # TODO: borrow a proven implementation
+    # @param str [String]
+    # @return [String]
+    # @api private
+    def self.camelize(str)
+      str.split(/_/).map(&:capitalize).join("")
+    end
+
+    # Make a D-Bus conventional name, CamelCased.
+    # @param ruby_name [String,Symbol] eg :do_something
+    # @param dbus_name [String,Symbol,nil] use this if given
+    # @return [Symbol] eg DoSomething
+    def self.make_dbus_name(ruby_name, dbus_name: nil)
+      dbus_name ||= camelize(ruby_name.to_s)
+      dbus_name.to_sym
+    end
+
+    # Use this instead of calling PropertiesChanged directly. This one
+    # considers not only the PC signature (which says that all property values
+    # are variants) but also the specific property type.
+    # @param interface_name [String] interface name like "org.example.ManagerManager"
+    # @param changed_props [Hash{String => ::Object}]
+    #   changed properties (D-Bus names) and their values.
+    # @param invalidated_props [Array<String>]
+    #   names of properties whose changed value is not specified
+    def dbus_properties_changed(interface_name, changed_props, invalidated_props)
+      typed_changed_props = changed_props.map do |dbus_name, value|
+        property = dbus_lookup_property(interface_name, dbus_name)
+        type = property.type
+        typed_value = Data.make_typed(type, value)
+        variant = Data::Variant.new(typed_value, member_type: type)
+        [dbus_name, variant]
+      end.to_h
+      PropertiesChanged(interface_name, typed_changed_props, invalidated_props)
+    end
+
+    # @param interface_name [String]
+    # @param property_name [String]
+    # @return [Property]
+    # @raise [DBus::Error]
+    # @api private
+    def dbus_lookup_property(interface_name, property_name)
+      # what should happen for unknown properties
+      # plasma: InvalidArgs (propname), UnknownInterface (interface)
+      # systemd: UnknownProperty
+      interface = intfs[interface_name]
+      if !interface
+        raise DBus.error("org.freedesktop.DBus.Error.UnknownProperty"),
+              "Property '#{interface_name}.#{property_name}' (on object '#{@path}') not found: no such interface"
+      end
+
+      property = interface.properties[property_name.to_sym]
+      if !property
+        raise DBus.error("org.freedesktop.DBus.Error.UnknownProperty"),
+              "Property '#{interface_name}.#{property_name}' (on object '#{@path}') not found"
+      end
+
+      property
+    end
+
+    # Generates information about interfaces and properties of the object
+    #
+    # Returns a hash containing interfaces names as keys. Each value is the
+    # same hash that would be returned by the
+    # org.freedesktop.DBus.Properties.GetAll() method for that combination of
+    # object path and interface. If an interface has no properties, the empty
+    # hash is returned.
+    #
+    # @return [Hash{String => Hash{String => Data::Base}}] interface -> property -> value
+    def interfaces_and_properties
+      get_all_method = self.class.make_method_name("org.freedesktop.DBus.Properties", :GetAll)
+
+      intfs.keys.each_with_object({}) do |interface, hash|
+        hash[interface] = public_send(get_all_method, interface).first
+      end
+    end
+
+    ####################################################################
+
+    # use the above defined methods to declare the property-handling
+    # interfaces and methods
+
+    dbus_interface PROPERTY_INTERFACE do
+      dbus_method :Get, "in interface_name:s, in property_name:s, out value:v" do |interface_name, property_name|
+        property = dbus_lookup_property(interface_name, property_name)
+
+        if property.readable?
+          ruby_name = property.ruby_name
+          value = public_send(ruby_name)
+          # may raise, DBus.error or https://ruby-doc.com/core-3.1.0/TypeError.html
+          typed_value = Data.make_typed(property.type, value)
+          [typed_value]
+        else
+          raise DBus.error("org.freedesktop.DBus.Error.PropertyWriteOnly"),
+                "Property '#{interface_name}.#{property_name}' (on object '#{@path}') is not readable"
+        end
+      end
+
+      dbus_method :Set, "in interface_name:s, in property_name:s, in val:v" do |interface_name, property_name, value|
+        property = dbus_lookup_property(interface_name, property_name)
+
+        if property.writable?
+          ruby_name_eq = "#{property.ruby_name}="
+          # TODO: declare dbus_method :Set to take :exact argument
+          # and type check it here before passing its :plain value
+          # to the implementation
+          public_send(ruby_name_eq, value)
+        else
+          raise DBus.error("org.freedesktop.DBus.Error.PropertyReadOnly"),
+                "Property '#{interface_name}.#{property_name}' (on object '#{@path}') is not writable"
+        end
+      end
+
+      dbus_method :GetAll, "in interface_name:s, out value:a{sv}" do |interface_name|
+        interface = intfs[interface_name]
+        if !interface
+          raise DBus.error("org.freedesktop.DBus.Error.UnknownProperty"),
+                "Properties '#{interface_name}.*' (on object '#{@path}') not found: no such interface"
+        end
+
+        p_hash = {}
+        interface.properties.each do |p_name, property|
+          next unless property.readable?
+
+          ruby_name = property.ruby_name
+          begin
+            # D-Bus spec says:
+            # > If GetAll is called with a valid interface name for which some
+            # > properties are not accessible to the caller (for example, due
+            # > to per-property access control implemented in the service),
+            # > those properties should be silently omitted from the result
+            # > array.
+            # so we will silently omit properties that fail to read.
+            # Get'ting them individually will send DBus.Error
+            value = public_send(ruby_name)
+            # may raise, DBus.error or https://ruby-doc.com/core-3.1.0/TypeError.html
+            typed_value = Data.make_typed(property.type, value)
+            p_hash[p_name.to_s] = typed_value
+          rescue StandardError
+            DBus.logger.debug "Property '#{interface_name}.#{p_name}' (on object '#{@path}')" \
+                              " has raised during GetAll, omitting it"
+          end
+        end
+
+        [p_hash]
+      end
+
+      dbus_signal :PropertiesChanged, "interface:s, changed_properties:a{sv}, invalidated_properties:as"
+    end
+
+    dbus_interface "org.freedesktop.DBus.Introspectable" do
+      dbus_method :Introspect, "out xml_data:s" do
+        # The body is not used, Connection#process handles it instead
+        # which is more efficient and handles paths without objects.
+      end
+    end
   end
 end
diff --git a/lib/dbus/object_manager.rb b/lib/dbus/object_manager.rb
new file mode 100644
index 0000000..9e29785
--- /dev/null
+++ b/lib/dbus/object_manager.rb
@@ -0,0 +1,58 @@
+# frozen_string_literal: true
+
+# This file is part of the ruby-dbus project
+# Copyright (C) 2022 José Iván López González
+# Copyright (C) 2022 Martin Vidner
+#
+# This library is free software; you can redistribute it and/or
+# modify it under the terms of the GNU Lesser General Public
+# License, version 2.1 as published by the Free Software Foundation.
+# See the file "COPYING" for the exact licensing terms.
+
+module DBus
+  # A mixin for {DBus::Object} implementing
+  # {https://dbus.freedesktop.org/doc/dbus-specification.html#standard-interfaces-objectmanager
+  # org.freedesktop.DBus.ObjectManager}.
+  #
+  # {Service#export} and {Service#unexport} will look for an ObjectManager
+  # parent in the path hierarchy. If found, it will emit InterfacesAdded
+  # or InterfacesRemoved, as appropriate.
+  module ObjectManager
+    OBJECT_MANAGER_INTERFACE = "org.freedesktop.DBus.ObjectManager"
+
+    # @return [Hash{ObjectPath => Hash{String => Hash{String => Data::Base}}}]
+    #   object -> interface -> property -> value
+    def managed_objects
+      # FIXME: also fix the "service" concept
+      descendant_objects = @service.descendants_for(path)
+      descendant_objects.each_with_object({}) do |obj, hash|
+        hash[obj.path] = obj.interfaces_and_properties
+      end
+    end
+
+    # @param object [DBus::Object]
+    # @return [void]
+    def object_added(object)
+      InterfacesAdded(object.path, object.interfaces_and_properties)
+    end
+
+    # @param object [DBus::Object]
+    # @return [void]
+    def object_removed(object)
+      InterfacesRemoved(object.path, object.intfs.keys)
+    end
+
+    def self.included(base)
+      base.class_eval do
+        dbus_interface OBJECT_MANAGER_INTERFACE do
+          dbus_method :GetManagedObjects, "out res:a{oa{sa{sv}}}" do
+            [managed_objects]
+          end
+
+          dbus_signal :InterfacesAdded, "object:o, interfaces_and_properties:a{sa{sv}}"
+          dbus_signal :InterfacesRemoved, "object:o, interfaces:as"
+        end
+      end
+    end
+  end
+end
diff --git a/lib/dbus/object_path.rb b/lib/dbus/object_path.rb
index bf23e4c..860b7dd 100644
--- a/lib/dbus/object_path.rb
+++ b/lib/dbus/object_path.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
 # This file is part of the ruby-dbus project
 # Copyright (C) 2019 Martin Vidner
 #
@@ -7,18 +9,21 @@
 # See the file "COPYING" for the exact licensing terms.
 
 module DBus
-  # A {::String} that validates at initialization time
+  # A {::String} that validates at initialization time.
+  # See also {DBus::Data::ObjectPath}
+  # @see https://dbus.freedesktop.org/doc/dbus-specification.html#message-protocol-marshaling-object-path
   class ObjectPath < String
     # @raise Error if not a valid object path
-    def initialize(s)
-      unless self.class.valid?(s)
-        raise DBus::Error, "Invalid object path #{s.inspect}"
+    def initialize(str)
+      unless self.class.valid?(str)
+        raise DBus::Error, "Invalid object path #{str.inspect}"
       end
+
       super
     end
 
-    def self.valid?(s)
-      s == "/" || s =~ %r{\A(/[A-Za-z0-9_]+)+\z}
+    def self.valid?(str)
+      str == "/" || str =~ %r{\A(/[A-Za-z0-9_]+)+\z}
     end
   end
 end
diff --git a/lib/dbus/platform.rb b/lib/dbus/platform.rb
new file mode 100644
index 0000000..5a8e7a9
--- /dev/null
+++ b/lib/dbus/platform.rb
@@ -0,0 +1,26 @@
+# frozen_string_literal: true
+
+# This file is part of the ruby-dbus project
+# Copyright (C) 2023 Martin Vidner
+#
+# This library is free software; you can redistribute it and/or
+# modify it under the terms of the GNU Lesser General Public
+# License, version 2.1 as published by the Free Software Foundation.
+# See the file "COPYING" for the exact licensing terms.
+
+require "rbconfig"
+
+module DBus
+  # Platform detection
+  module Platform
+    module_function
+
+    def freebsd?
+      RbConfig::CONFIG["target_os"] =~ /freebsd/
+    end
+
+    def macos?
+      RbConfig::CONFIG["target_os"] =~ /darwin/
+    end
+  end
+end
diff --git a/lib/dbus/proxy_object.rb b/lib/dbus/proxy_object.rb
index 987d163..2ca1a3a 100644
--- a/lib/dbus/proxy_object.rb
+++ b/lib/dbus/proxy_object.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
 # This file is part of the ruby-dbus project
 # Copyright (C) 2007 Arnaud Cornet and Paul van Tilburg
 # Copyright (C) 2009-2014 Martin Vidner
@@ -44,6 +46,7 @@ module DBus
     end
 
     # Returns the interfaces of the object.
+    # @return [Array<String>] names of the interfaces
     def interfaces
       introspect unless introspected
       @interfaces.keys
@@ -56,6 +59,7 @@ module DBus
       introspect unless introspected
       ifc = @interfaces[intfname]
       raise DBus::Error, "no such interface `#{intfname}' on object `#{@path}'" unless ifc
+
       ifc
     end
 
@@ -92,6 +96,7 @@ module DBus
           # don't overwrite instance methods!
           next if dup_meths.include?(name)
           next if self.class.instance_methods.include?(name)
+
           if univocal_meths.include? name
             univocal_meths.delete name
             dup_meths << name
@@ -121,14 +126,23 @@ module DBus
     # It uses _default_iface_ which must have been set.
     # @return [void]
     def on_signal(name, &block)
-      # TODO: improve
-      raise NoMethodError unless @default_iface && has_iface?(@default_iface)
+      unless @default_iface && has_iface?(@default_iface)
+        raise NoMethodError, "undefined signal `#{name}' for DBus interface `#{@default_iface}' on object `#{@path}'"
+      end
+
       @interfaces[@default_iface].on_signal(name, &block)
     end
 
     ####################################################
     private
 
+    # rubocop:disable Lint/MissingSuper
+    # as this should forward everything
+    #
+    # https://github.com/rubocop-hq/ruby-style-guide#no-method-missing
+    # and http://blog.marc-andre.ca/2010/11/15/methodmissing-politely/
+    # have a point to be investigated
+
     # Handles all unkown methods, mostly to route method calls to the
     # default interface.
     def method_missing(name, *args, &reply_handler)
@@ -146,9 +160,17 @@ module DBus
         # interesting, foo.method("unknown")
         # raises NameError, not NoMethodError
         raise unless e.to_s =~ /undefined method `#{name}'/
+
         # BTW e.exception("...") would preserve the class.
         raise NoMethodError, "undefined method `#{name}' for DBus interface `#{@default_iface}' on object `#{@path}'"
       end
     end
-  end # class ProxyObject
+    # rubocop:enable Lint/MissingSuper
+
+    def respond_to_missing?(name, _include_private = false)
+      @default_iface &&
+        has_iface?(@default_iface) &&
+        @interfaces[@default_iface].methods.key?(name) or super
+    end
+  end
 end
diff --git a/lib/dbus/proxy_object_factory.rb b/lib/dbus/proxy_object_factory.rb
index 69c1bcf..eeb520f 100644
--- a/lib/dbus/proxy_object_factory.rb
+++ b/lib/dbus/proxy_object_factory.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
 # This file is part of the ruby-dbus project
 # Copyright (C) 2007 Arnaud Cornet and Paul van Tilburg
 # Copyright (C) 2009-2014 Martin Vidner
@@ -22,17 +24,21 @@ module DBus
       @api = api
     end
 
-    # Investigates the sub-nodes of the proxy object _po_ based on the
+    # Investigates the sub-nodes of the proxy object _pobj_ based on the
     # introspection XML data _xml_ and sets them up recursively.
-    def self.introspect_into(po, xml)
-      intfs, po.subnodes = IntrospectXMLParser.new(xml).parse
+    # @param pobj [ProxyObject]
+    # @param xml [String]
+    def self.introspect_into(pobj, xml)
+      # intfs [Array<Interface>], subnodes [Array<String>]
+      intfs, pobj.subnodes = IntrospectXMLParser.new(xml).parse
       intfs.each do |i|
-        poi = ProxyObjectInterface.new(po, i.name)
+        poi = ProxyObjectInterface.new(pobj, i.name)
         i.methods.each_value { |m| poi.define(m) }
         i.signals.each_value { |s| poi.define(s) }
-        po[i.name] = poi
+        i.properties.each_value { |p| poi.define(p) }
+        pobj[i.name] = poi
       end
-      po.introspected = true
+      pobj.introspected = true
     end
 
     # Generates, sets up and returns the proxy object.
@@ -41,5 +47,5 @@ module DBus
       ProxyObjectFactory.introspect_into(po, @xml)
       po
     end
-  end # class ProxyObjectFactory
+  end
 end
diff --git a/lib/dbus/proxy_object_interface.rb b/lib/dbus/proxy_object_interface.rb
index c419246..eed8242 100644
--- a/lib/dbus/proxy_object_interface.rb
+++ b/lib/dbus/proxy_object_interface.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
 # This file is part of the ruby-dbus project
 # Copyright (C) 2007 Arnaud Cornet and Paul van Tilburg
 # Copyright (C) 2009-2014 Martin Vidner
@@ -13,13 +15,16 @@ module DBus
   # A class similar to the normal Interface used as a proxy for remote
   # object interfaces.
   class ProxyObjectInterface
-    # The proxied methods contained in the interface.
-    attr_accessor :methods
-    # The proxied signals contained in the interface.
-    attr_accessor :signals
-    # The proxy object to which this interface belongs.
+    # @return [Hash{String => DBus::Method}]
+    attr_reader :methods
+    # @return [Hash{String => Signal}]
+    attr_reader :signals
+    # @return [Hash{Symbol => Property}]
+    attr_reader :properties
+
+    # @return [ProxyObject] The proxy object to which this interface belongs.
     attr_reader :object
-    # The name of the interface.
+    # @return [String] The name of the interface.
     attr_reader :name
 
     # Creates a new proxy interface for the given proxy _object_
@@ -29,6 +34,7 @@ module DBus
       @name = name
       @methods = {}
       @signals = {}
+      @properties = {}
     end
 
     # Returns the string representation of the interface (the name).
@@ -36,27 +42,28 @@ module DBus
       @name
     end
 
-    # Defines a method on the interface from the Method descriptor _m_.
-    def define_method_from_descriptor(m)
-      m.params.each do |fpar|
+    # Defines a method on the interface from the Method descriptor _method_.
+    # @param method [Method]
+    def define_method_from_descriptor(method)
+      method.params.each do |fpar|
         par = fpar.type
         # This is the signature validity check
         Type::Parser.new(par).parse
       end
 
       singleton_class.class_eval do
-        define_method m.name do |*args, &reply_handler|
-          if m.params.size != args.size
-            raise ArgumentError, "wrong number of arguments (#{args.size} for #{m.params.size})"
+        define_method method.name do |*args, &reply_handler|
+          if method.params.size != args.size
+            raise ArgumentError, "wrong number of arguments (#{args.size} for #{method.params.size})"
           end
 
           msg = Message.new(Message::METHOD_CALL)
           msg.path = @object.path
           msg.interface = @name
           msg.destination = @object.destination
-          msg.member = m.name
+          msg.member = method.name
           msg.sender = @object.bus.unique_name
-          m.params.each do |fpar|
+          method.params.each do |fpar|
             par = fpar.type
             msg.add_param(par, args.shift)
           end
@@ -64,25 +71,35 @@ module DBus
           if ret.nil? || @object.api.proxy_method_returns_array
             ret
           else
-            m.rets.size == 1 ? ret.first : ret
+            method.rets.size == 1 ? ret.first : ret
           end
         end
       end
 
-      @methods[m.name] = m
+      @methods[method.name] = method
     end
 
-    # Defines a signal from the descriptor _s_.
-    def define_signal_from_descriptor(s)
-      @signals[s.name] = s
+    # Defines a signal from the descriptor _sig_.
+    # @param sig [Signal]
+    def define_signal_from_descriptor(sig)
+      @signals[sig.name] = sig
     end
 
-    # Defines a signal or method based on the descriptor _m_.
-    def define(m)
-      if m.is_a?(Method)
-        define_method_from_descriptor(m)
-      elsif m.is_a?(Signal)
-        define_signal_from_descriptor(m)
+    # @param prop [Property]
+    def define_property_from_descriptor(prop)
+      @properties[prop.name] = prop
+    end
+
+    # Defines a signal or method based on the descriptor _ifc_el_.
+    # @param ifc_el [DBus::Method,Signal,Property]
+    def define(ifc_el)
+      case ifc_el
+      when Method
+        define_method_from_descriptor(ifc_el)
+      when Signal
+        define_signal_from_descriptor(ifc_el)
+      when Property
+        define_property_from_descriptor(ifc_el)
       end
     end
 
@@ -109,7 +126,7 @@ module DBus
       end
     end
 
-    PROPERTY_INTERFACE = "org.freedesktop.DBus.Properties".freeze
+    PROPERTY_INTERFACE = "org.freedesktop.DBus.Properties"
 
     # Read a property.
     # @param propname [String]
@@ -124,10 +141,26 @@ module DBus
     end
 
     # Write a property.
-    # @param propname [String]
+    # @param property_name [String]
     # @param value [Object]
-    def []=(propname, value)
-      object[PROPERTY_INTERFACE].Set(name, propname, value)
+    def []=(property_name, value)
+      property = properties[property_name.to_sym]
+      if !property
+        raise DBus.error("org.freedesktop.DBus.Error.UnknownProperty"),
+              "Property '#{name}.#{property_name}' (on object '#{object.path}') not found"
+      end
+
+      case value
+      # accommodate former need to explicitly make a variant with the right type
+      when Data::Variant
+        variant = value
+      else
+        type = property.type
+        typed_value = Data.make_typed(type, value)
+        variant = Data::Variant.new(typed_value, member_type: type)
+      end
+
+      object[PROPERTY_INTERFACE].Set(name, property_name, variant)
     end
 
     # Read all properties at once, as a hash.
@@ -141,5 +174,5 @@ module DBus
         ret
       end
     end
-  end # class ProxyObjectInterface
+  end
 end
diff --git a/lib/dbus/raw_message.rb b/lib/dbus/raw_message.rb
new file mode 100644
index 0000000..be03e7f
--- /dev/null
+++ b/lib/dbus/raw_message.rb
@@ -0,0 +1,91 @@
+# frozen_string_literal: true
+
+# This file is part of the ruby-dbus project
+# Copyright (C) 2022 Martin Vidner
+#
+# This library is free software; you can redistribute it and/or
+# modify it under the terms of the GNU Lesser General Public
+# License, version 2.1 as published by the Free Software Foundation.
+# See the file "COPYING" for the exact licensing terms.
+
+module DBus
+  # A message while it is being parsed: a binary string,
+  # with a position cursor (*pos*), and an *endianness* tag.
+  class RawMessage
+    # @return [String]
+    # attr_reader :bytes
+
+    # @return [Integer] position in the byte buffer
+    attr_reader :pos
+
+    # @return [:little,:big]
+    attr_reader :endianness
+
+    # @param bytes [String]
+    # @param endianness [:little,:big,nil]
+    #    if not given, read the 1st byte of *bytes*
+    def initialize(bytes, endianness = nil)
+      @bytes = bytes
+      @pos = 0
+      @endianness = endianness || self.class.endianness(@bytes[0])
+    end
+
+    # Get the endiannes switch as a Symbol,
+    # which will make using it slightly more efficient
+    # @param tag_char [String]
+    # @return [:little,:big]
+    def self.endianness(tag_char)
+      case tag_char
+      when LIL_END
+        :little
+      when BIG_END
+        :big
+      else
+        raise InvalidPacketException, "Incorrect endianness #{tag_char.inspect}"
+      end
+    end
+
+    # @return [void]
+    # @raise IncompleteBufferException if there are not enough bytes remaining
+    def want!(size)
+      raise IncompleteBufferException if @pos + size > @bytes.bytesize
+    end
+
+    # @return [String]
+    # @raise IncompleteBufferException if there are not enough bytes remaining
+    # TODO: stress test this with encodings. always binary?
+    def read(size)
+      want!(size)
+      ret = @bytes.slice(@pos, size)
+      @pos += size
+      ret
+    end
+
+    # @return [String]
+    # @api private
+    def remaining_bytes
+      # This returns "" if pos is just past the end of the string,
+      # and nil if it is further.
+      @bytes[@pos..-1]
+    end
+
+    # Align the *pos* index on a multiple of *alignment*
+    # @param alignment [Integer] must be 1, 2, 4 or 8
+    # @return [void]
+    def align(alignment)
+      case alignment
+      when 1
+        nil
+      when 2, 4, 8
+        bits = alignment - 1
+        pad_size = ((@pos + bits) & ~bits) - @pos
+        pad = read(pad_size)
+        unless pad.bytes.all?(&:zero?)
+          raise InvalidPacketException, "Alignment bytes are not NUL"
+        end
+      else
+        raise ArgumentError, "Unsupported alignment #{alignment}"
+      end
+    end
+  end
+end
diff --git a/lib/dbus/type.rb b/lib/dbus/type.rb
index da4cb34..823534f 100644
--- a/lib/dbus/type.rb
+++ b/lib/dbus/type.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
 # dbus/type.rb - module containing low-level D-Bus data type information
 #
 # This file is part of the ruby-dbus project
@@ -9,13 +11,33 @@
 # See the file "COPYING" for the exact licensing terms.
 
 module DBus
-  # = D-Bus type module
+  # Like a {Signature} but containing only a single complete type.
+  #
+  # For documentation purposes only.
+  class SingleCompleteType < String; end
+
+  # Zero or more {SingleCompleteType}s; its own type code is "g".
+  # For example "ssv" for a method taking two Strings and a Variant/
+  #
+  # For documentation purposes only.
+  class Signature < String; end
+
+  # Similar to {Signature} but for {DBus::Object.define_method},
+  # contains names and direction of the parameters.
+  # For example "in query:s, in case_sensitive:b, out results:ao".
   #
-  # This module containts the constants of the types specified in the D-Bus
-  # protocol.
-  module Type
+  # For documentation purposes only.
+  class Prototype < String; end
+
+  # Represents the D-Bus types.
+  #
+  # Corresponds to {SingleCompleteType}.
+  # Instances are immutable/frozen once fully constructed.
+  #
+  # See also {DBus::Data::Signature} which is "type on the wire".
+  class Type
     # Mapping from type number to name and alignment.
-    TypeMapping = {
+    TYPE_MAPPING = {
       0 => ["INVALID", nil],
       "y" => ["BYTE", 1],
       "b" => ["BOOLEAN", 4],
@@ -36,7 +58,7 @@ module DBus
       "h" => ["UNIX_FD", 4]
     }.freeze
     # Defines the set of constants
-    TypeMapping.each_pair do |key, value|
+    TYPE_MAPPING.each_pair do |key, value|
       Type.const_set(value.first, key)
     end
 
@@ -44,87 +66,140 @@ module DBus
     class SignatureException < Exception
     end
 
-    # = D-Bus type conversion class
+    # Formerly this was a Module and there was a DBus::Type::Type class
+    # but the class got too prominent to keep its double double name.
+    # This is for backward compatibility.
+    Type = self # rubocop:disable Naming/ConstantName
+
+    # @return [String] the signature type character, eg "s" or "e".
+    attr_reader :sigtype
+    # @return [Array<Type>] contained member types.
+    attr_reader :members
+
+    # Use {DBus.type} instead, because this allows constructing
+    # incomplete or invalid types, for backward compatibility.
     #
-    # Helper class for representing a D-Bus type.
-    class Type
-      # Returns the signature type number.
-      attr_reader :sigtype
-      # Return contained member types.
-      attr_reader :members
-
-      # Create a new type instance for type number _sigtype_.
-      def initialize(sigtype)
-        if !TypeMapping.keys.member?(sigtype)
-          raise SignatureException, "Unknown key in signature: #{sigtype.chr}"
+    # @param abstract [Boolean] allow abstract types "r" and "e"
+    #   (Enabled for internal usage by {Parser}.)
+    def initialize(sigtype, abstract: false)
+      if !TYPE_MAPPING.keys.member?(sigtype)
+        case sigtype
+        when ")"
+          raise SignatureException, "STRUCT unexpectedly closed: )"
+        when "}"
+          raise SignatureException, "DICT_ENTRY unexpectedly closed: }"
+        else
+          raise SignatureException, "Unknown type code #{sigtype.inspect}"
         end
-        @sigtype = sigtype
-        @members = []
-      end
-
-      # Return the required alignment for the type.
-      def alignment
-        TypeMapping[@sigtype].last
       end
 
-      # Return a string representation of the type according to the
-      # D-Bus specification.
-      def to_s
-        case @sigtype
+      unless abstract
+        case sigtype
         when STRUCT
-          "(" + @members.collect(&:to_s).join + ")"
-        when ARRAY
-          "a" + child.to_s
+          raise SignatureException, "Abstract STRUCT, use \"(...)\" instead of \"#{STRUCT}\""
         when DICT_ENTRY
-          "{" + @members.collect(&:to_s).join + "}"
-        else
-          if !TypeMapping.keys.member?(@sigtype)
-            raise NotImplementedError
-          end
-          @sigtype.chr
+          raise SignatureException, "Abstract DICT_ENTRY, use \"{..}\" instead of \"#{DICT_ENTRY}\""
         end
       end
 
-      # Add a new member type _a_.
-      def <<(a)
-        if ![STRUCT, ARRAY, DICT_ENTRY].member?(@sigtype)
-          raise SignatureException
-        end
-        raise SignatureException if @sigtype == ARRAY && !@members.empty?
-        if @sigtype == DICT_ENTRY
-          if @members.size == 2
-            raise SignatureException, "Dict entries have exactly two members"
-          end
-          if @members.empty?
-            if [STRUCT, ARRAY, DICT_ENTRY].member?(a.sigtype)
-              raise SignatureException, "Dict entry keys must be basic types"
-            end
-          end
-        end
-        @members << a
+      @sigtype = sigtype.freeze
+      @members = [] # not frozen yet, Parser#parse_one or Factory will do it
+      freeze
+    end
+
+    # A Type is equal to
+    # - another Type with the same string representation
+    # - a String ({SingleCompleteType}) describing the type
+    def ==(other)
+      case other
+      when ::String
+        to_s == other
+      else
+        eql?(other)
       end
+    end
+
+    # A Type is eql? to
+    # - another Type with the same string representation
+    #
+    # Hash key equality
+    # See https://ruby-doc.org/core-3.0.0/Object.html#method-i-eql-3F
+    def eql?(other)
+      return false unless other.is_a?(Type)
+
+      @sigtype == other.sigtype && @members == other.members
+    end
+
+    # Return the required alignment for the type.
+    def alignment
+      TYPE_MAPPING[@sigtype].last
+    end
 
-      # Return the first contained member type.
-      def child
-        @members[0]
+    # Return a string representation of the type according to the
+    # D-Bus specification.
+    def to_s
+      case @sigtype
+      when STRUCT
+        "(#{@members.collect(&:to_s).join})"
+      when ARRAY
+        "a#{child}"
+      when DICT_ENTRY
+        "{#{@members.collect(&:to_s).join}}"
+      else
+        @sigtype.chr
       end
+    end
+
+    # Add a new member type _item_.
+    # @param item [Type]
+    def <<(item)
+      raise ArgumentError unless item.is_a?(Type)
 
-      def inspect
-        s = TypeMapping[@sigtype].first
-        if [STRUCT, ARRAY].member?(@sigtype)
-          s += ": " + @members.inspect
+      if ![STRUCT, ARRAY, DICT_ENTRY].member?(@sigtype)
+        raise SignatureException
+      end
+      raise SignatureException if @sigtype == ARRAY && !@members.empty?
+
+      if @sigtype == DICT_ENTRY
+        case @members.size
+        when 2
+          raise SignatureException, "DICT_ENTRY must have 2 subtypes, found 3 or more in #{@signature}"
+        when 0
+          if [STRUCT, ARRAY, DICT_ENTRY, VARIANT].member?(item.sigtype)
+            raise SignatureException, "DICT_ENTRY key must be basic (non-container)"
+          end
         end
-        s
       end
-    end # class Type
+      @members << item
+    end
+
+    # Return the first contained member type.
+    def child
+      @members[0]
+    end
+
+    def inspect
+      s = TYPE_MAPPING[@sigtype].first
+      if [STRUCT, ARRAY, DICT_ENTRY].member?(@sigtype)
+        s += ": #{@members.inspect}"
+      end
+      s
+    end
 
     # = D-Bus type parser class
     #
     # Helper class to parse a type signature in the protocol.
+    # @api private
     class Parser
       # Create a new parser for the given _signature_.
+      # @param signature [Signature]
       def initialize(signature)
         @signature = signature
+        if signature.size > 255
+          msg = "Potential signature is longer than 255 characters (#{@signature.size}): #{@signature}"
+          raise SignatureException, msg
+        end
+
         @idx = 0
       end
 
@@ -135,57 +210,214 @@ module DBus
         c
       end
 
-      # Parse one character _c_ of the signature.
-      def parse_one(c)
+      # Parse one character _char_ of the signature.
+      # @param for_array [Boolean] are we parsing an immediate child of an ARRAY
+      # @return [Type]
+      def parse_one(char, for_array: false)
         res = nil
-        case c
+        case char
         when "a"
           res = Type.new(ARRAY)
-          c = nextchar
-          raise SignatureException, "Parse error in #{@signature}" if c.nil?
-          child = parse_one(c)
+          char = nextchar
+          raise SignatureException, "Empty ARRAY in #{@signature}" if char.nil?
+
+          child = parse_one(char, for_array: true)
           res << child
         when "("
-          res = Type.new(STRUCT)
-          while (c = nextchar) && c != ")"
-            res << parse_one(c)
+          res = Type.new(STRUCT, abstract: true)
+          while (char = nextchar) && char != ")"
+            res << parse_one(char)
           end
-          raise SignatureException, "Parse error in #{@signature}" if c.nil?
+          raise SignatureException, "STRUCT not closed in #{@signature}" if char.nil?
+          raise SignatureException, "Empty STRUCT in #{@signature}" if res.members.empty?
         when "{"
-          res = Type.new(DICT_ENTRY)
-          while (c = nextchar) && c != "}"
-            res << parse_one(c)
+          raise SignatureException, "DICT_ENTRY not an immediate child of an ARRAY" unless for_array
+
+          res = Type.new(DICT_ENTRY, abstract: true)
+
+          # key type, value type
+          2.times do |i|
+            char = nextchar
+            raise SignatureException, "DICT_ENTRY not closed in #{@signature}" if char.nil?
+
+            raise SignatureException, "DICT_ENTRY must have 2 subtypes, found #{i} in #{@signature}" if char == "}"
+
+            res << parse_one(char)
           end
-          raise SignatureException, "Parse error in #{@signature}" if c.nil?
+
+          # closing "}"
+          char = nextchar
+          raise SignatureException, "DICT_ENTRY not closed in #{@signature}" if char.nil?
+
+          raise SignatureException, "DICT_ENTRY must have 2 subtypes, found 3 or more in #{@signature}" if char != "}"
         else
-          res = Type.new(c)
+          res = Type.new(char)
         end
+        res.members.freeze
         res
       end
 
       # Parse the entire signature, return a DBus::Type object.
+      # @return [Array<Type>]
       def parse
         @idx = 0
         ret = []
         while (c = nextchar)
           ret << parse_one(c)
         end
-        ret
+        ret.freeze
+      end
+
+      # Parse one {SingleCompleteType}
+      # @return [Type]
+      def parse1
+        c = nextchar
+        raise SignatureException, "Empty signature, expecting a Single Complete Type" if c.nil?
+
+        t = parse_one(c)
+        raise SignatureException, "Has more than a Single Complete Type: #{@signature}" unless nextchar.nil?
+
+        t.freeze
+      end
+    end
+
+    class Factory
+      # @param type [Type,SingleCompleteType,Class]
+      # @see from_plain_class
+      # @return [Type] (frozen)
+      def self.make_type(type)
+        case type
+        when Type
+          type
+        when String
+          DBus.type(type)
+        when Class
+          from_plain_class(type)
+        else
+          msg = "Expecting DBus::Type, DBus::SingleCompleteType(aka ::String), or Class, got #{type.inspect}"
+          raise ArgumentError, msg
+        end
       end
-    end # class Parser
-  end # module Type
+
+      # Make a {Type} corresponding to some plain classes:
+      # - String
+      # - Float
+      # - DBus::ObjectPath
+      # - DBus::Signature, DBus::SingleCompleteType
+      # @param klass [Class]
+      # @return [Type] (frozen)
+      def self.from_plain_class(klass)
+        @signature_type ||= DBus.type(SIGNATURE)
+        @class_to_type ||= {
+          DBus::ObjectPath => DBus.type(OBJECT_PATH),
+          DBus::Signature => @signature_type,
+          DBus::SingleCompleteType => @signature_type,
+          String => DBus.type(STRING),
+          Float => DBus.type(DOUBLE)
+        }
+        t = @class_to_type[klass]
+        raise ArgumentError, "Cannot convert plain class #{klass} to a D-Bus type" if t.nil?
+
+        t
+      end
+    end
+
+    # Syntactic helper for constructing an array Type.
+    # You may be looking for {Data::Array} instead.
+    # @example
+    #   t = Type::Array[Type::INT16]
+    class ArrayFactory < Factory
+      # @param member_type [Type,SingleCompleteType]
+      # @return [Type] (frozen)
+      def self.[](member_type)
+        t = Type.new(ARRAY)
+        t << make_type(member_type)
+        t.members.freeze
+        t
+      end
+    end
+
+    # @example
+    #   t = Type::Array[Type::INT16]
+    Array = ArrayFactory
+
+    # Syntactic helper for constructing a hash Type.
+    # You may be looking for {Data::Array} and {Data::DictEntry} instead.
+    # @example
+    #   t = Type::Hash[Type::STRING, Type::VARIANT]
+    class HashFactory < Factory
+      # @param key_type [Type,SingleCompleteType]
+      # @param value_type [Type,SingleCompleteType]
+      # @return [Type] (frozen)
+      def self.[](key_type, value_type)
+        t = Type.new(ARRAY)
+        de = Type.new(DICT_ENTRY, abstract: true)
+        de << make_type(key_type)
+        de << make_type(value_type)
+        de.members.freeze
+        t << de
+        t.members.freeze
+        t
+      end
+    end
+
+    # @example
+    #   t = Type::Hash[Type::INT16]
+    Hash = HashFactory
+
+    # Syntactic helper for constructing a struct Type.
+    # You may be looking for {Data::Struct} instead.
+    # @example
+    #   t = Type::Struct[Type::INT16, Type::STRING]
+    class StructFactory < Factory
+      # @param member_types [::Array<Type,SingleCompleteType>]
+      # @return [Type] (frozen)
+      def self.[](*member_types)
+        raise ArgumentError if member_types.empty?
+
+        t = Type.new(STRUCT, abstract: true)
+        member_types.each do |mt|
+          t << make_type(mt)
+        end
+        t.members.freeze
+        t
+      end
+    end
+
+    # @example
+    #   t = Type::Struct[Type::INT16, Type::STRING]
+    Struct = StructFactory
+  end
 
   # shortcuts
 
-  # Parse a String to a DBus::Type::Type
+  # Parse a String to a valid {DBus::Type}.
+  # This is prefered to {Type#initialize} which allows
+  # incomplete or invalid types.
+  # @param string_type [SingleCompleteType]
+  # @return [DBus::Type] (frozen)
+  # @raise SignatureException
   def type(string_type)
-    Type::Parser.new(string_type).parse[0]
+    Type::Parser.new(string_type).parse1
   end
   module_function :type
 
+  # Parse a String to zero or more {DBus::Type}s.
+  # @param string_type [Signature]
+  # @return [Array<DBus::Type>] (frozen)
+  # @raise SignatureException
+  def types(string_type)
+    Type::Parser.new(string_type).parse
+  end
+  module_function :types
+
   # Make an explicit [Type, value] pair
+  # @param string_type [SingleCompleteType]
+  # @param value [::Object]
+  # @return [Array(DBus::Type::Type,::Object)]
+  # @deprecated Use {Data::Variant#initialize} instead
   def variant(string_type, value)
-    [type(string_type), value]
+    Data::Variant.new(value, member_type: string_type)
   end
   module_function :variant
-end # module DBus
+end
diff --git a/lib/dbus/xml.rb b/lib/dbus/xml.rb
index 3b4b401..fe6ec82 100644
--- a/lib/dbus/xml.rb
+++ b/lib/dbus/xml.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
 # dbus/xml.rb - introspection parser, rexml/nokogiri abstraction
 #
 # This file is part of the ruby-dbus project
@@ -9,11 +11,17 @@
 # License, version 2.1 as published by the Free Software Foundation.
 # See the file "COPYING" for the exact licensing terms.
 
-# TODO: check if it is slow, make replaceable
-require "rexml/document"
+# Our gemspec says rexml is needed and nokogiri is optional
+# but in fact either will do
+
 begin
   require "nokogiri"
 rescue LoadError
+  begin
+    require "rexml/document"
+  rescue LoadError
+    raise LoadError, "cannot load nokogiri OR rexml/document"
+  end
 end
 
 module DBus
@@ -26,14 +34,23 @@ module DBus
       attr_accessor :backend
     end
     # Creates a new parser for XML data in string _xml_.
+    # @param xml [String]
     def initialize(xml)
       @xml = xml
     end
 
     class AbstractXML
+      # @!method initialize(xml)
+      # @abstract
+
+      # @!method each(xpath)
+      # @abstract
+      # yields nodes which match xpath of type AbstractXML::Node
+
       def self.have_nokogiri?
         Object.const_defined?("Nokogiri")
       end
+
       class Node
         def initialize(node)
           @node = node
@@ -46,12 +63,6 @@ module DBus
         # yields child nodes which match xpath of type AbstractXML::Node
         def each(xpath); end
       end
-      # required methods
-      # initialize parser with xml string
-      def initialize(xml); end
-
-      # yields nodes which match xpath of type AbstractXML::Node
-      def each(xpath); end
     end
 
     class NokogiriParser < AbstractXML
@@ -64,7 +75,9 @@ module DBus
           @node.search(path).each { |node| block.call NokogiriNode.new(node) }
         end
       end
+
       def initialize(xml)
+        super()
         @doc = Nokogiri.XML(xml)
       end
 
@@ -83,7 +96,9 @@ module DBus
           @node.elements.each(path) { |node| block.call REXMLNode.new(node) }
         end
       end
+
       def initialize(xml)
+        super()
         @doc = REXML::Document.new(xml)
       end
 
@@ -125,6 +140,10 @@ module DBus
           parse_methsig(se, s)
           i << s
         end
+        e.each("property") do |pe|
+          p = Property.from_xml(pe)
+          i << p
+        end
       end
       d = Time.now - t
       if d > 2
@@ -136,28 +155,30 @@ module DBus
     ######################################################################
     private
 
-    # Parses a method signature XML element _e_ and initialises
-    # method/signal _m_.
-    def parse_methsig(e, m)
-      e.each("arg") do |ae|
+    # Parses a method signature XML element *elem* and initialises
+    # method/signal *methsig*.
+    # @param elem [AbstractXML::Node]
+    def parse_methsig(elem, methsig)
+      elem.each("arg") do |ae|
         name = ae["name"]
         dir = ae["direction"]
         sig = ae["type"]
-        if m.is_a?(DBus::Signal)
+        case methsig
+        when DBus::Signal
           # Direction can only be "out", ignore it
-          m.add_fparam(name, sig)
-        elsif m.is_a?(DBus::Method)
+          methsig.add_fparam(name, sig)
+        when DBus::Method
           case dir
           # This is a method, so dir defaults to "in"
           when "in", nil
-            m.add_fparam(name, sig)
+            methsig.add_fparam(name, sig)
           when "out"
-            m.add_return(name, sig)
+            methsig.add_return(name, sig)
           end
         else
           raise NotImplementedError, dir
         end
       end
     end
-  end # class IntrospectXMLParser
-end # module DBus
+  end
+end
diff --git a/ruby-dbus.gemspec b/ruby-dbus.gemspec
index fcf8c27..8693cf7 100644
--- a/ruby-dbus.gemspec
+++ b/ruby-dbus.gemspec
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
 # -*- ruby -*-
 require "rubygems"
 
@@ -18,15 +20,18 @@ GEMSPEC = Gem::Specification.new do |s|
   ]
   s.require_path = "lib"
 
-  s.required_ruby_version = ">= 2.0.0"
+  s.required_ruby_version = ">= 2.4.0"
 
-  # This is optional
+  # Either of rexml and nokogiri is required
+  # but AFAIK gemspec cannot express that.
+  # Nokogiri is recommended as rexml is dead slow.
+  s.add_runtime_dependency "rexml"
   # s.add_runtime_dependency "nokogiri"
 
-  s.add_development_dependency "coveralls"
   s.add_development_dependency "packaging_rake_tasks"
   s.add_development_dependency "rake"
   s.add_development_dependency "rspec", "~> 3"
-  s.add_development_dependency "rubocop", "= 0.41.2"
+  s.add_development_dependency "rubocop", "= 1.0"
   s.add_development_dependency "simplecov"
+  s.add_development_dependency "simplecov-lcov"
 end
diff --git a/spec/async_spec.rb b/spec/async_spec.rb
index b527370..9b69482 100755
--- a/spec/async_spec.rb
+++ b/spec/async_spec.rb
@@ -1,4 +1,6 @@
 #!/usr/bin/env rspec
+# frozen_string_literal: true
+
 # Test the binding of dbus concepts to ruby concepts
 require_relative "spec_helper"
 require "dbus"
diff --git a/spec/auth_spec.rb b/spec/auth_spec.rb
new file mode 100755
index 0000000..b25e606
--- /dev/null
+++ b/spec/auth_spec.rb
@@ -0,0 +1,225 @@
+#!/usr/bin/env rspec
+# frozen_string_literal: true
+
+require_relative "spec_helper"
+require "dbus"
+
+describe DBus::Authentication::Client do
+  let(:socket) { instance_double("Socket") }
+  let(:subject) { described_class.new(socket) }
+
+  before(:each) do
+    allow(Process).to receive(:uid).and_return(999)
+    allow(subject).to receive(:send_nul_byte)
+  end
+
+  describe "#next_state" do
+    it "raises when I forget to handle a state" do
+      subject.instance_variable_set(:@state, :Denmark)
+      expect { subject.__send__(:next_state, []) }.to raise_error(RuntimeError, /unhandled state :Denmark/)
+    end
+  end
+
+  def expect_protocol(pairs)
+    pairs.each do |we_say, server_says|
+      expect(subject).to receive(:write_line).with(we_say)
+      next if server_says.nil?
+
+      expect(subject).to receive(:read_line).and_return(server_says)
+    end
+  end
+
+  context "with ANONYMOUS" do
+    let(:subject) { described_class.new(socket, [DBus::Authentication::Anonymous]) }
+
+    it "authentication passes" do
+      expect_protocol [
+        ["AUTH ANONYMOUS 527562792044427573\r\n", "OK ffffffffffffffffffffffffffffffff\r\n"],
+        ["NEGOTIATE_UNIX_FD\r\n", "ERROR not for anonymous\r\n"],
+        ["BEGIN\r\n"]
+      ]
+
+      expect { subject.authenticate }.to_not raise_error
+    end
+  end
+
+  context "with EXTERNAL" do
+    let(:subject) { described_class.new(socket, [DBus::Authentication::External]) }
+
+    it "authentication passes, and address_uuid is set" do
+      expect_protocol [
+        ["AUTH EXTERNAL 393939\r\n", "OK ffffffffffffffffffffffffffffffff\r\n"],
+        ["NEGOTIATE_UNIX_FD\r\n", "AGREE_UNIX_FD\r\n"],
+        ["BEGIN\r\n"]
+      ]
+
+      expect { subject.authenticate }.to_not raise_error
+      expect(subject.address_uuid).to eq "ffffffffffffffffffffffffffffffff"
+    end
+
+    context "when the server says superfluous things before an OK" do
+      it "authentication passes" do
+        expect_protocol [
+          ["AUTH EXTERNAL 393939\r\n", "WOULD_YOU_LIKE_SOME_TEA\r\n"],
+          ["ERROR\r\n", "OK ffffffffffffffffffffffffffffffff\r\n"],
+          ["NEGOTIATE_UNIX_FD\r\n", "AGREE_UNIX_FD\r\n"],
+          ["BEGIN\r\n"]
+        ]
+
+        expect { subject.authenticate }.to_not raise_error
+      end
+    end
+
+    context "when the server messes up NEGOTIATE_UNIX_FD" do
+      it "authentication fails orderly" do
+        expect_protocol [
+          ["AUTH EXTERNAL 393939\r\n", "OK ffffffffffffffffffffffffffffffff\r\n"],
+          ["NEGOTIATE_UNIX_FD\r\n", "I_DONT_NEGOTIATE_WITH_TENORISTS\r\n"]
+        ]
+
+        allow(socket).to receive(:close) # want to get rid of this
+        # TODO: quote the server error message?
+        expect { subject.authenticate }.to raise_error(DBus::AuthenticationFailed, /Unknown server reply/)
+      end
+    end
+
+    context "when the server replies with ERROR" do
+      it "authentication fails orderly" do
+        expect_protocol [
+          ["AUTH EXTERNAL 393939\r\n", "ERROR something failed\r\n"],
+          ["CANCEL\r\n", "REJECTED DBUS_COOKIE_SHA1\r\n"]
+        ]
+
+        allow(socket).to receive(:close) # want to get rid of this
+        # TODO: quote the server error message?
+        expect { subject.authenticate }.to raise_error(DBus::AuthenticationFailed, /exhausted/)
+      end
+    end
+  end
+
+  context "with EXTERNAL without uid" do
+    let(:subject) do
+      described_class.new(socket, [DBus::Authentication::External, DBus::Authentication::ExternalWithoutUid])
+    end
+
+    it "authentication passes" do
+      expect_protocol [
+        ["AUTH EXTERNAL 393939\r\n", "REJECTED EXTERNAL\r\n"],
+        # this succeeds when we connect to a privileged container,
+        # where outside-non-root becomes inside-root
+        ["AUTH EXTERNAL\r\n", "DATA\r\n"],
+        ["DATA\r\n", "OK ffffffffffffffffffffffffffffffff\r\n"],
+        ["NEGOTIATE_UNIX_FD\r\n", "AGREE_UNIX_FD\r\n"],
+        ["BEGIN\r\n"]
+      ]
+
+      expect { subject.authenticate }.to_not raise_error
+    end
+  end
+
+  context "with a rejected mechanism and then EXTERNAL" do
+    let(:rejected_mechanism) do
+      double("Mechanism", name: "WIMP", call: [:MechContinue, "I expect to be rejected"])
+    end
+
+    let(:subject) { described_class.new(socket, [rejected_mechanism, DBus::Authentication::External]) }
+
+    it "authentication eventually passes" do
+      expect_protocol [
+        [/^AUTH WIMP .*\r\n/, "REJECTED EXTERNAL\r\n"],
+        ["AUTH EXTERNAL 393939\r\n", "OK ffffffffffffffffffffffffffffffff\r\n"],
+        ["NEGOTIATE_UNIX_FD\r\n", "AGREE_UNIX_FD\r\n"],
+        ["BEGIN\r\n"]
+      ]
+
+      expect { subject.authenticate }.to_not raise_error
+    end
+  end
+
+  context "with a DATA-using mechanism" do
+    let(:mechanism) do
+      double("Mechanism", name: "CHALLENGE_ME", call: [:MechContinue, "1"])
+    end
+
+    # try it twice to test calling #use_next_mechanism
+    let(:subject) { described_class.new(socket, [mechanism, mechanism]) }
+
+    it "authentication fails orderly when the server says ERROR" do
+      expect_protocol [
+        ["AUTH CHALLENGE_ME 31\r\n", "ERROR something failed\r\n"],
+        ["CANCEL\r\n", "REJECTED DBUS_COOKIE_SHA1\r\n"],
+        ["AUTH CHALLENGE_ME 31\r\n", "ERROR something failed\r\n"],
+        ["CANCEL\r\n", "REJECTED DBUS_COOKIE_SHA1\r\n"]
+      ]
+
+      allow(socket).to receive(:close) # want to get rid of this
+      # TODO: quote the server error message?
+      expect { subject.authenticate }.to raise_error(DBus::AuthenticationFailed, /exhausted/)
+    end
+
+    it "authentication fails orderly when the server says ERROR and then changes its mind" do
+      expect_protocol [
+        ["AUTH CHALLENGE_ME 31\r\n", "ERROR something failed\r\n"],
+        ["CANCEL\r\n", "I_CHANGED_MY_MIND please come back\r\n"]
+      ]
+
+      allow(socket).to receive(:close) # want to get rid of this
+      # TODO: quote the server error message?
+      expect { subject.authenticate }.to raise_error(DBus::AuthenticationFailed, /Unknown.*MIND.*REJECTED/)
+    end
+
+    it "authentication passes when the server says superfluous things before DATA" do
+      expect_protocol [
+        ["AUTH CHALLENGE_ME 31\r\n", "WOULD_YOU_LIKE_SOME_TEA\r\n"],
+        ["ERROR\r\n", "DATA\r\n"],
+        ["DATA 31\r\n", "OK ffffffffffffffffffffffffffffffff\r\n"],
+        ["NEGOTIATE_UNIX_FD\r\n", "AGREE_UNIX_FD\r\n"],
+        ["BEGIN\r\n"]
+      ]
+
+      expect { subject.authenticate }.to_not raise_error
+    end
+
+    it "authentication passes when the server decides not to need the DATA" do
+      expect_protocol [
+        ["AUTH CHALLENGE_ME 31\r\n", "OK ffffffffffffffffffffffffffffffff\r\n"],
+        ["NEGOTIATE_UNIX_FD\r\n", "AGREE_UNIX_FD\r\n"],
+        ["BEGIN\r\n"]
+      ]
+
+      expect { subject.authenticate }.to_not raise_error
+    end
+  end
+
+  context "with a mechanism returning :MechError" do
+    let(:fallible_mechanism) do
+      double(name: "FALLIBLE", call: [:MechError, "not my best day"])
+    end
+
+    let(:subject) { described_class.new(socket, [fallible_mechanism]) }
+
+    it "authentication fails orderly" do
+      expect_protocol [
+        ["ERROR not my best day\r\n", "REJECTED DBUS_COOKIE_SHA1\r\n"]
+      ]
+
+      allow(socket).to receive(:close) # want to get rid of thise
+      expect { subject.authenticate }.to raise_error(DBus::AuthenticationFailed, /exhausted/)
+    end
+  end
+
+  context "with a badly implemented mechanism" do
+    let(:buggy_mechanism) do
+      double(name: "buggy", call: [:smurf, nil])
+    end
+
+    let(:subject) { described_class.new(socket, [buggy_mechanism]) }
+
+    it "authentication fails before protoxol is exchanged" do
+      expect(subject).to_not receive(:write_line)
+      expect(subject).to_not receive(:read_line)
+
+      expect { subject.authenticate }.to raise_error(DBus::AuthenticationFailed, /smurf/)
+    end
+  end
+end
diff --git a/spec/binding_spec.rb b/spec/binding_spec.rb
index f5f123e..443c95a 100755
--- a/spec/binding_spec.rb
+++ b/spec/binding_spec.rb
@@ -1,4 +1,6 @@
 #!/usr/bin/env rspec
+# frozen_string_literal: true
+
 # Test the binding of dbus concepts to ruby concepts
 require_relative "spec_helper"
 
diff --git a/spec/bus_and_xml_backend_spec.rb b/spec/bus_and_xml_backend_spec.rb
index 1ef6768..9c4b15b 100755
--- a/spec/bus_and_xml_backend_spec.rb
+++ b/spec/bus_and_xml_backend_spec.rb
@@ -1,9 +1,13 @@
 #!/usr/bin/env rspec
+# frozen_string_literal: true
+
 # Test the bus class
 require_relative "spec_helper"
 
 require "rubygems"
-require "nokogiri"
+# If we have nokogiri, rexml is normally omitted
+# but here we include it for test coverage
+require "rexml"
 require "dbus"
 
 describe "BusAndXmlBackendTest" do
diff --git a/spec/bus_driver_spec.rb b/spec/bus_driver_spec.rb
index cbd98b3..56f52cf 100755
--- a/spec/bus_driver_spec.rb
+++ b/spec/bus_driver_spec.rb
@@ -1,4 +1,6 @@
 #!/usr/bin/env rspec
+# frozen_string_literal: true
+
 require_relative "spec_helper"
 require "dbus"
 
diff --git a/spec/bus_name_spec.rb b/spec/bus_name_spec.rb
index 2a26f65..1dd7acc 100755
--- a/spec/bus_name_spec.rb
+++ b/spec/bus_name_spec.rb
@@ -1,4 +1,6 @@
 #!/usr/bin/env rspec
+# frozen_string_literal: true
+
 require_relative "spec_helper"
 require "dbus"
 
@@ -17,7 +19,7 @@ describe DBus::BusName do
       expect(described_class.valid?("Empty.Last.Component.")).to be_falsey
       expect(described_class.valid?("Invalid.Ch@r@cter")).to be_falsey
       expect(described_class.valid?("/Invalid-Character")).to be_falsey
-      long_name = "a." + ("long." * 100) + "name"
+      long_name = "a.#{"long." * 100}name"
       expect(described_class.valid?(long_name)).to be_falsey
       expect(described_class.valid?("org.7_zip.Archiver")).to be_falsey
     end
diff --git a/spec/bus_spec.rb b/spec/bus_spec.rb
index 7dceb59..7206be0 100755
--- a/spec/bus_spec.rb
+++ b/spec/bus_spec.rb
@@ -1,4 +1,6 @@
 #!/usr/bin/env rspec
+# frozen_string_literal: true
+
 # Test the bus class
 require_relative "spec_helper"
 
diff --git a/spec/byte_array_spec.rb b/spec/byte_array_spec.rb
index b9052d2..1bb1df9 100755
--- a/spec/byte_array_spec.rb
+++ b/spec/byte_array_spec.rb
@@ -1,4 +1,6 @@
 #!/usr/bin/env rspec
+# frozen_string_literal: true
+
 require_relative "spec_helper"
 
 require "dbus"
diff --git a/spec/client_robustness_spec.rb b/spec/client_robustness_spec.rb
index d4dc519..a9d60f2 100755
--- a/spec/client_robustness_spec.rb
+++ b/spec/client_robustness_spec.rb
@@ -1,4 +1,6 @@
 #!/usr/bin/env rspec
+# frozen_string_literal: true
+
 # Test that a client survives various error cases
 require_relative "spec_helper"
 require "dbus"
@@ -12,14 +14,14 @@ describe "ClientRobustnessTest" do
   context "when the bus name is invalid" do
     it "tells the user the bus name is invalid" do
       # user mistake, should be "org.ruby.service"
-      expect { @bus.service(".org.ruby.service") }.to raise_error(DBus::Error)
+      expect { @bus.service(".org.ruby.service") }.to raise_error(DBus::Error, /Invalid bus name/)
     end
   end
 
   context "when the object path is invalid" do
     it "tells the user the path is invalid" do
       # user mistake, should be "/org/ruby/MyInstance"
-      expect { @svc.object("org.ruby.MyInstance") }.to raise_error(DBus::Error)
+      expect { @svc.object("org.ruby.MyInstance") }.to raise_error(DBus::Error, /Invalid object path/)
     end
   end
 end
diff --git a/spec/data/marshall.yaml b/spec/data/marshall.yaml
new file mode 100644
index 0000000..a3b1fb4
--- /dev/null
+++ b/spec/data/marshall.yaml
@@ -0,0 +1,1667 @@
+---
+# Test data for marshalling and unmarshalling of D-Bus values.
+# The intent is to be implementation independent.
+# More importantly, it should work both ways, for marshalling and unmarshalling.
+#
+# This file is a list of test cases.
+#
+# Each test case is a dictionary:
+# - sig: the signature of the data
+# - end: endianness of the byte buffer ("big" or "little")
+# - buf: the byte buffer. Logically it is an array of bytes but YAML
+#        would write that in base-64, obscuring the contents.
+#        So we write it as nested lists of integers (bytes), or UTF-8 strings.
+#        The nesting only matters for test case readability, the data is
+#        flattened before use.
+# - val: the unmarshalled value (for valid buffers)
+# - exc: exception name (for invalid buffers)
+# - msg: exception message substring (for invalid buffers)
+# - marshall: true (default) or false,
+# - unmarshall: true (default) or false, for test cases that only work one way
+#               or are expected to fail
+
+- sig: "y"
+  end: little
+  buf:
+  - 0
+  val: 0
+- sig: "y"
+  end: little
+  buf:
+  - 128
+  val: 128
+- sig: "y"
+  end: little
+  buf:
+  - 255
+  val: 255
+- sig: "y"
+  end: big
+  buf:
+  - 0
+  val: 0
+- sig: "y"
+  end: big
+  buf:
+  - 128
+  val: 128
+- sig: "y"
+  end: big
+  buf:
+  - 255
+  val: 255
+- sig: b
+  end: little
+  buf: [1, 0, 0, 0]
+  val: true
+- sig: b
+  end: little
+  buf: [0, 0, 0, 0]
+  val: false
+- sig: b
+  end: big
+  buf: [0, 0, 0, 1]
+  val: true
+- sig: b
+  end: big
+  buf: [0, 0, 0, 0]
+  val: false
+- sig: b
+  end: little
+  buf:
+  - 0
+  - 255
+  - 255
+  - 0
+  exc: DBus::InvalidPacketException
+  msg: BOOLEAN must be 0 or 1, found
+- sig: b
+  end: big
+  buf:
+  - 0
+  - 255
+  - 255
+  - 0
+  exc: DBus::InvalidPacketException
+  msg: BOOLEAN must be 0 or 1, found
+- sig: "n"
+  end: little
+  buf:
+  - 0
+  - 0
+  val: 0
+- sig: "n"
+  end: little
+  buf:
+  - 255
+  - 127
+  val: 32767
+- sig: "n"
+  end: little
+  buf:
+  - 0
+  - 128
+  val: -32768
+- sig: "n"
+  end: little
+  buf:
+  - 255
+  - 255
+  val: -1
+- sig: "n"
+  end: big
+  buf:
+  - 0
+  - 0
+  val: 0
+- sig: "n"
+  end: big
+  buf:
+  - 127
+  - 255
+  val: 32767
+- sig: "n"
+  end: big
+  buf:
+  - 128
+  - 0
+  val: -32768
+- sig: "n"
+  end: big
+  buf:
+  - 255
+  - 255
+  val: -1
+- sig: q
+  end: little
+  buf:
+  - 0
+  - 0
+  val: 0
+- sig: q
+  end: little
+  buf:
+  - 255
+  - 127
+  val: 32767
+- sig: q
+  end: little
+  buf:
+  - 0
+  - 128
+  val: 32768
+- sig: q
+  end: little
+  buf:
+  - 255
+  - 255
+  val: 65535
+- sig: q
+  end: big
+  buf:
+  - 0
+  - 0
+  val: 0
+- sig: q
+  end: big
+  buf:
+  - 127
+  - 255
+  val: 32767
+- sig: q
+  end: big
+  buf:
+  - 128
+  - 0
+  val: 32768
+- sig: q
+  end: big
+  buf:
+  - 255
+  - 255
+  val: 65535
+- sig: i
+  end: little
+  buf:
+  - 0
+  - 0
+  - 0
+  - 0
+  val: 0
+- sig: i
+  end: little
+  buf:
+  - 255
+  - 255
+  - 255
+  - 127
+  val: 2147483647
+- sig: i
+  end: little
+  buf:
+  - 0
+  - 0
+  - 0
+  - 128
+  val: -2147483648
+- sig: i
+  end: little
+  buf:
+  - 255
+  - 255
+  - 255
+  - 255
+  val: -1
+- sig: i
+  end: big
+  buf:
+  - 0
+  - 0
+  - 0
+  - 0
+  val: 0
+- sig: i
+  end: big
+  buf:
+  - 127
+  - 255
+  - 255
+  - 255
+  val: 2147483647
+- sig: i
+  end: big
+  buf:
+  - 128
+  - 0
+  - 0
+  - 0
+  val: -2147483648
+- sig: i
+  end: big
+  buf:
+  - 255
+  - 255
+  - 255
+  - 255
+  val: -1
+- sig: u
+  end: little
+  buf:
+  - 0
+  - 0
+  - 0
+  - 0
+  val: 0
+- sig: u
+  end: little
+  buf:
+  - 255
+  - 255
+  - 255
+  - 127
+  val: 2147483647
+- sig: u
+  end: little
+  buf:
+  - 0
+  - 0
+  - 0
+  - 128
+  val: 2147483648
+- sig: u
+  end: little
+  buf:
+  - 255
+  - 255
+  - 255
+  - 255
+  val: 4294967295
+- sig: u
+  end: big
+  buf:
+  - 0
+  - 0
+  - 0
+  - 0
+  val: 0
+- sig: u
+  end: big
+  buf:
+  - 127
+  - 255
+  - 255
+  - 255
+  val: 2147483647
+- sig: u
+  end: big
+  buf:
+  - 128
+  - 0
+  - 0
+  - 0
+  val: 2147483648
+- sig: u
+  end: big
+  buf:
+  - 255
+  - 255
+  - 255
+  - 255
+  val: 4294967295
+- sig: h
+  end: little
+  buf:
+  - 0
+  - 0
+  - 0
+  - 0
+  val: 0
+- sig: h
+  end: little
+  buf:
+  - 255
+  - 255
+  - 255
+  - 127
+  val: 2147483647
+- sig: h
+  end: little
+  buf:
+  - 0
+  - 0
+  - 0
+  - 128
+  val: 2147483648
+- sig: h
+  end: little
+  buf:
+  - 255
+  - 255
+  - 255
+  - 255
+  val: 4294967295
+- sig: h
+  end: big
+  buf:
+  - 0
+  - 0
+  - 0
+  - 0
+  val: 0
+- sig: h
+  end: big
+  buf:
+  - 127
+  - 255
+  - 255
+  - 255
+  val: 2147483647
+- sig: h
+  end: big
+  buf:
+  - 128
+  - 0
+  - 0
+  - 0
+  val: 2147483648
+- sig: h
+  end: big
+  buf:
+  - 255
+  - 255
+  - 255
+  - 255
+  val: 4294967295
+- sig: x
+  end: little
+  buf:
+  - 0
+  - 0
+  - 0
+  - 0
+  - 0
+  - 0
+  - 0
+  - 0
+  val: 0
+- sig: x
+  end: little
+  buf:
+  - 255
+  - 255
+  - 255
+  - 255
+  - 255
+  - 255
+  - 255
+  - 127
+  val: 9223372036854775807
+- sig: x
+  end: little
+  buf:
+  - 0
+  - 0
+  - 0
+  - 0
+  - 0
+  - 0
+  - 0
+  - 128
+  val: -9223372036854775808
+- sig: x
+  end: little
+  buf:
+  - 255
+  - 255
+  - 255
+  - 255
+  - 255
+  - 255
+  - 255
+  - 255
+  val: -1
+- sig: x
+  end: big
+  buf:
+  - 0
+  - 0
+  - 0
+  - 0
+  - 0
+  - 0
+  - 0
+  - 0
+  val: 0
+- sig: x
+  end: big
+  buf:
+  - 127
+  - 255
+  - 255
+  - 255
+  - 255
+  - 255
+  - 255
+  - 255
+  val: 9223372036854775807
+- sig: x
+  end: big
+  buf:
+  - 128
+  - 0
+  - 0
+  - 0
+  - 0
+  - 0
+  - 0
+  - 0
+  val: -9223372036854775808
+- sig: x
+  end: big
+  buf:
+  - 255
+  - 255
+  - 255
+  - 255
+  - 255
+  - 255
+  - 255
+  - 255
+  val: -1
+- sig: t
+  end: little
+  buf:
+  - 0
+  - 0
+  - 0
+  - 0
+  - 0
+  - 0
+  - 0
+  - 0
+  val: 0
+- sig: t
+  end: little
+  buf:
+  - 255
+  - 255
+  - 255
+  - 255
+  - 255
+  - 255
+  - 255
+  - 127
+  val: 9223372036854775807
+- sig: t
+  end: little
+  buf:
+  - 0
+  - 0
+  - 0
+  - 0
+  - 0
+  - 0
+  - 0
+  - 128
+  val: 9223372036854775808
+- sig: t
+  end: little
+  buf:
+  - 255
+  - 255
+  - 255
+  - 255
+  - 255
+  - 255
+  - 255
+  - 255
+  val: 18446744073709551615
+- sig: t
+  end: big
+  buf:
+  - 0
+  - 0
+  - 0
+  - 0
+  - 0
+  - 0
+  - 0
+  - 0
+  val: 0
+- sig: t
+  end: big
+  buf:
+  - 127
+  - 255
+  - 255
+  - 255
+  - 255
+  - 255
+  - 255
+  - 255
+  val: 9223372036854775807
+- sig: t
+  end: big
+  buf:
+  - 128
+  - 0
+  - 0
+  - 0
+  - 0
+  - 0
+  - 0
+  - 0
+  val: 9223372036854775808
+- sig: t
+  end: big
+  buf:
+  - 255
+  - 255
+  - 255
+  - 255
+  - 255
+  - 255
+  - 255
+  - 255
+  val: 18446744073709551615
+- sig: d
+  end: little
+  buf:
+  - 0
+  - 0
+  - 0
+  - 0
+  - 0
+  - 0
+  - 0
+  - 0
+  val: 0.0
+- sig: d
+  end: little
+  buf:
+  - 0
+  - 0
+  - 0
+  - 0
+  - 0
+  - 0
+  - 0
+  - 128
+  val: -0.0
+- sig: d
+  end: little
+  buf:
+  - 0
+  - 0
+  - 0
+  - 0
+  - 0
+  - 0
+  - 0
+  - "@"
+  val: 2.0
+- sig: d
+  end: big
+  buf:
+  - 0
+  - 0
+  - 0
+  - 0
+  - 0
+  - 0
+  - 0
+  - 0
+  val: 0.0
+- sig: d
+  end: big
+  buf:
+  - 128
+  - 0
+  - 0
+  - 0
+  - 0
+  - 0
+  - 0
+  - 0
+  val: -0.0
+- sig: d
+  end: big
+  buf:
+  - "@"
+  - 0
+  - 0
+  - 0
+  - 0
+  - 0
+  - 0
+  - 0
+  val: 2.0
+- sig: s
+  end: little
+  buf:
+  - 0
+  - 0
+  - 0
+  - 0
+  - 0
+  val: ''
+- sig: s
+  end: little
+  buf:
+  - 2
+  - 0
+  - 0
+  - 0
+  - 197
+  - 152
+  - 0
+  val: Ř
+- sig: s
+  end: little
+  buf:
+  - 3
+  - 0
+  - 0
+  - 0
+  - 239
+  - 191
+  - 191
+  - 0
+  val: "\uFFFF"
+- sig: s
+  end: big
+  buf:
+  - 0
+  - 0
+  - 0
+  - 0
+  - 0
+  val: ''
+- sig: s
+  end: big
+  buf:
+  - 0
+  - 0
+  - 0
+  - 2
+  - 197
+  - 152
+  - 0
+  val: Ř
+- sig: s
+  end: big
+  buf:
+  - 0
+  - 0
+  - 0
+  - 3
+  - 239
+  - 191
+  - 191
+  - 0
+  val: "\uFFFF"
+- sig: s
+  end: big
+  buf:
+  - 0
+  - 0
+  - 0
+  - 4
+  - 244
+  - 143
+  - 191
+  - 191
+  - 0
+  val: "\U0010FFFF"
+- sig: s
+  end: little
+  buf:
+  - 0
+  - 0
+  - 0
+  - 0
+  - U
+  exc: DBus::InvalidPacketException
+  msg: not NUL-terminated
+- sig: s
+  end: little
+  buf:
+  - 1
+  - 0
+  - 0
+  - 0
+  - "@U"
+  exc: DBus::InvalidPacketException
+  msg: not NUL-terminated
+- sig: s
+  end: little
+  buf:
+  - 0
+  - 0
+  - 0
+  - 0
+  exc: DBus::IncompleteBufferException
+  msg: ''
+- sig: s
+  end: little
+  buf:
+  - 0
+  - 0
+  - 0
+  exc: DBus::IncompleteBufferException
+  msg: ''
+- sig: s
+  end: little
+  buf:
+  - 0
+  - 0
+  exc: DBus::IncompleteBufferException
+  msg: ''
+- sig: s
+  end: little
+  buf:
+  - 0
+  exc: DBus::IncompleteBufferException
+  msg: ''
+# NUL in the middle
+- sig: s
+  end: little
+  buf:
+  - 3
+  - 0
+  - 0
+  - 0
+  - a
+  - 0
+  - b
+  - 0
+  exc: DBus::InvalidPacketException
+  msg: Invalid string
+# invalid UTF-8
+- sig: s
+  end: little
+  buf:
+  - 4
+  - 0
+  - 0
+  - 0
+  - 255
+  - 255
+  - 255
+  - 255
+  - 0
+  exc: DBus::InvalidPacketException
+  msg: Invalid string
+# overlong sequence encoding an "A"
+- sig: s
+  end: little
+  buf:
+  - 2
+  - 0
+  - 0
+  - 0
+  - 0xC1
+  - 0x81
+  - 0
+  exc: DBus::InvalidPacketException
+  msg: Invalid string
+# first codepoint outside UTF-8, U+110000
+- sig: s
+  end: little
+  buf:
+  - 4
+  - 0
+  - 0
+  - 0
+  - 0xF4
+  - 0x90
+  - 0xC0
+  - 0xC0
+  - 0
+  exc: DBus::InvalidPacketException
+  msg: Invalid string
+- sig: o
+  end: little
+  buf:
+  - 1
+  - 0
+  - 0
+  - 0
+  - "/"
+  - 0
+  val: "/"
+- sig: o
+  end: little
+  buf:
+  - 32
+  - 0
+  - 0
+  - 0
+  - "/99Numbers/_And_Underscores/anyw"
+  - 0
+  val: "/99Numbers/_And_Underscores/anyw"
+# no size limit like for other names; 512 characters are fine
+- sig: o
+  end: little
+  buf:
+  - [0, 2, 0, 0]
+  - "/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"
+  - "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"
+  - "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"
+  - "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"
+  - "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"
+  - "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"
+  - "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"
+  - "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"
+  - 0
+  val: "/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"
+- sig: o
+  end: big
+  buf:
+  - 0
+  - 0
+  - 0
+  - 1
+  - "/"
+  - 0
+  val: "/"
+- sig: o
+  end: big
+  buf:
+  - 0
+  - 0
+  - 0
+  - " /99Numbers/_And_Underscores/anyw"
+  - 0
+  val: "/99Numbers/_And_Underscores/anyw"
+- sig: o
+  end: little
+  buf:
+  - 0
+  - 0
+  - 0
+  - 0
+  - U
+  exc: DBus::InvalidPacketException
+  msg: not NUL-terminated
+- sig: o
+  end: little
+  buf:
+  - 1
+  - 0
+  - 0
+  - 0
+  - "/U"
+  exc: DBus::InvalidPacketException
+  msg: not NUL-terminated
+- sig: o
+  end: little
+  buf:
+  - 0
+  - 0
+  - 0
+  - 0
+  exc: DBus::IncompleteBufferException
+  msg: ''
+- sig: o
+  end: little
+  buf:
+  - 0
+  - 0
+  - 0
+  exc: DBus::IncompleteBufferException
+  msg: ''
+- sig: o
+  end: little
+  buf:
+  - 0
+  - 0
+  exc: DBus::IncompleteBufferException
+  msg: ''
+- sig: o
+  end: little
+  buf:
+  - 0
+  exc: DBus::IncompleteBufferException
+  msg: ''
+- sig: o
+  end: little
+  buf:
+  - 0
+  - 0
+  - 0
+  - 0
+  - 0
+  exc: DBus::InvalidPacketException
+  msg: Invalid object path
+- sig: o
+  end: big
+  buf:
+  - 0
+  - 0
+  - 0
+  - 0
+  - 0
+  exc: DBus::InvalidPacketException
+  msg: Invalid object path
+- sig: o
+  end: big
+  buf:
+  - 0
+  - 0
+  - 0
+  - 5
+  - "/_//_"
+  - 0
+  exc: DBus::InvalidPacketException
+  msg: Invalid object path
+- sig: o
+  end: big
+  buf:
+  - 0
+  - 0
+  - 0
+  - 5
+  - "/_/_/"
+  - 0
+  exc: DBus::InvalidPacketException
+  msg: Invalid object path
+- sig: o
+  end: big
+  buf:
+  - 0
+  - 0
+  - 0
+  - 5
+  - "/_/_ "
+  - 0
+  exc: DBus::InvalidPacketException
+  msg: Invalid object path
+- sig: o
+  end: big
+  buf:
+  - 0
+  - 0
+  - 0
+  - 5
+  - "/_/_-"
+  - 0
+  exc: DBus::InvalidPacketException
+  msg: Invalid object path
+# NUL in the middle
+- sig: o
+  end: big
+  buf:
+  - 0
+  - 0
+  - 0
+  - 5
+  - "/_/_"
+  - 0
+  - 0
+  exc: DBus::InvalidPacketException
+  msg: Invalid object path
+# accented a
+- sig: o
+  end: big
+  buf:
+  - 0
+  - 0
+  - 0
+  - 5
+  - "/_/"
+  - 195
+  - 161
+  - 0
+  exc: DBus::InvalidPacketException
+  msg: Invalid object path
+- sig: g
+  end: little
+  buf:
+  - 0
+  - 0
+  val: ''
+- sig: g
+  end: big
+  buf:
+  - 0
+  - 0
+  val: ''
+- sig: g
+  end: little
+  buf:
+  - 1
+  - b
+  - 0
+  val: b
+- sig: g
+  end: big
+  buf:
+  - 1
+  - b
+  - 0
+  val: b
+- sig: g
+  end: big
+  buf:
+  - 0
+  - U
+  exc: DBus::InvalidPacketException
+  msg: not NUL-terminated
+- sig: g
+  end: big
+  buf:
+  - 1
+  - bU
+  exc: DBus::InvalidPacketException
+  msg: not NUL-terminated
+- sig: g
+  end: little
+  buf:
+  - 0
+  exc: DBus::IncompleteBufferException
+  msg: ''
+- sig: g
+  end: big
+  buf:
+  - 1
+  - "!"
+  - 0
+  exc: DBus::InvalidPacketException
+  msg: Invalid signature
+- sig: g
+  end: big
+  buf:
+  - 1
+  - r
+  - 0
+  exc: DBus::InvalidPacketException
+  msg: Invalid signature
+- sig: g
+  end: big
+  buf:
+  - 2
+  - ae
+  - 0
+  exc: DBus::InvalidPacketException
+  msg: Invalid signature
+- sig: g
+  end: big
+  buf:
+  - 1
+  - a
+  - 0
+  exc: DBus::InvalidPacketException
+  msg: Invalid signature
+# dict_entry with other than 2 members
+- sig: g
+  end: big
+  buf:
+  - 3
+  - a{}
+  - 0
+  exc: DBus::InvalidPacketException
+  msg: Invalid signature
+- sig: g
+  end: big
+  buf:
+  - 4
+  - a{s}
+  - 0
+  exc: DBus::InvalidPacketException
+  msg: Invalid signature
+- sig: g
+  end: big
+  buf:
+  - 6
+  - a{sss}
+  - 0
+  exc: DBus::InvalidPacketException
+  msg: Invalid signature
+# dict_entry with non-basic key
+- sig: g
+  end: big
+  buf:
+  - 5
+  - a{vs}
+  - 0
+  exc: DBus::InvalidPacketException
+  msg: Invalid signature
+# dict_entry outside array
+- sig: g
+  end: big
+  buf:
+  - 4
+  - "{sv}"
+  - 0
+  exc: DBus::InvalidPacketException
+  msg: Invalid signature
+# dict_entry not immediately in an array
+- sig: g
+  end: big
+  buf:
+  - 7
+  - a({sv})
+  - 0
+  exc: DBus::InvalidPacketException
+  msg: Invalid signature
+# NUL in the middle
+- sig: g
+  end: big
+  buf:
+  - 3
+  - a
+  - 0
+  - "y"
+  - 0
+  exc: DBus::InvalidPacketException
+  msg: Invalid signature
+
+# ARRAYs
+
+# marshalling format:
+# - (alignment of data_bytes)
+# - UINT32 data_bytes (without any alignment padding)
+# - (alignment of ITEM_TYPE, even if the array is empty)
+# - ITEM_TYPE item1
+# - (alignment of ITEM_TYPE)
+# - ITEM_TYPE item2...
+
+# Here we repeat the STRINGs test data (without the trailing NUL)
+# but the outcomes are different
+- sig: ay
+  end: little
+  buf:
+  - 0
+  - 0
+  - 0
+  - 0
+  val: []
+- sig: ay
+  end: little
+  buf:
+  - 2
+  - 0
+  - 0
+  - 0
+  - 197
+  - 152
+  val:
+  - 197
+  - 152
+- sig: ay
+  end: little
+  buf:
+  - 3
+  - 0
+  - 0
+  - 0
+  - 239
+  - 191
+  - 191
+  val:
+  - 239
+  - 191
+  - 191
+- sig: ay
+  end: big
+  buf:
+  - 0
+  - 0
+  - 0
+  - 0
+  val: []
+- sig: ay
+  end: big
+  buf:
+  - 0
+  - 0
+  - 0
+  - 2
+  - 197
+  - 152
+  val:
+  - 197
+  - 152
+- sig: ay
+  end: big
+  buf:
+  - 0
+  - 0
+  - 0
+  - 3
+  - 239
+  - 191
+  - 191
+  val:
+  - 239
+  - 191
+  - 191
+- sig: ay
+  end: big
+  buf:
+  - 0
+  - 0
+  - 0
+  - 4
+  - 244
+  - 143
+  - 191
+  - 191
+  val:
+  - 244
+  - 143
+  - 191
+  - 191
+- sig: ay
+  end: little
+  buf:
+  - 3
+  - 0
+  - 0
+  - 0
+  - a
+  - 0
+  - b
+  val:
+  - 97
+  - 0
+  - 98
+- sig: ay
+  end: little
+  buf:
+  - 4
+  - 0
+  - 0
+  - 0
+  - 255
+  - 255
+  - 255
+  - 255
+  val:
+  - 255
+  - 255
+  - 255
+  - 255
+- sig: ay
+  end: little
+  buf:
+  - 2
+  - 0
+  - 0
+  - 0
+  - 193
+  - 129
+  val:
+  - 193
+  - 129
+- sig: ay
+  end: little
+  buf:
+  - 4
+  - 0
+  - 0
+  - 0
+  - 244
+  - 144
+  - 192
+  - 192
+  val:
+  - 244
+  - 144
+  - 192
+  - 192
+
+# With basic types, by the time we have found the message to be invalid,
+# it is nevertheless well-formed and we could read the next message.
+# However, an overlong array (body longer than 64MiB) is a good enough
+# reason to drop the connection, which is what InvalidPacketException
+# does, right? Doesn't it?
+# Well it does, by crashing the entire process.
+# That should be made more graceful.
+- sig: ay
+  end: little
+  buf:
+  - 1
+  - 0
+  - 0
+  - 4
+  exc: DBus::InvalidPacketException
+  msg: ARRAY body longer than 64MiB
+- sig: ay
+  end: little
+  buf:
+  - 2
+  - 0
+  - 0
+  - 0
+  - 170
+  exc: DBus::IncompleteBufferException
+  msg: ''
+- sig: ay
+  end: little
+  buf:
+  - 0
+  - 0
+  - 0
+  exc: DBus::IncompleteBufferException
+  msg: ''
+- sig: ay
+  end: little
+  buf:
+  - 0
+  - 0
+  exc: DBus::IncompleteBufferException
+  msg: ''
+- sig: ay
+  end: little
+  buf:
+  - 0
+  exc: DBus::IncompleteBufferException
+  msg: ''
+- sig: at
+  end: little
+  buf:
+  # body size
+  - [0, 0, 0, 0]
+  # padding
+  - [0, 0, 0, 0]
+  val: []
+- sig: at
+  end: little
+  buf:
+  # body size
+  - [16, 0, 0, 0]
+  # padding
+  - [0, 0, 0, 0]
+  # item
+  - [1, 0, 0, 0, 0, 0, 0, 0]
+  # item
+  - [2, 0, 0, 0, 0, 0, 0, 0]
+  val:
+  - 1
+  - 2
+- sig: at
+  end: little
+  buf:
+  # body size, missing padding
+  - [0, 0, 0, 0]
+  exc: DBus::IncompleteBufferException
+  msg: ''
+- sig: at
+  end: little
+  buf:
+  # body size
+  - [0, 0, 0, 0]
+  # nonzero padding
+  - [0xDE, 0xAD, 0xBE, 0xEF]
+  exc: DBus::InvalidPacketException
+  msg: ''
+- sig: at
+  end: little
+  buf:
+  # body size
+  - [8, 0, 0, 0]
+  # padding
+  - [0, 0, 0, 0]
+  # incomplete item
+  - 170
+  exc: DBus::IncompleteBufferException
+  msg: ''
+
+# arrays of nontrivial types let us demonstrate the padding of their elements
+- sig: a(qq)
+  end: little
+  buf:
+  # body size
+  - [0, 0, 0, 0]
+  # padding
+  - [0, 0, 0, 0]
+  val: []
+- sig: a(qq)
+  end: little
+  buf:
+  # body size
+  - [12, 0, 0, 0]
+  # padding
+  - [0, 0, 0, 0]
+  # item
+  - [1, 0, 2, 0]
+  # padding
+  - [0, 0, 0, 0]
+  # item
+  - [3, 0, 4, 0]
+  val:
+  - - 1
+    - 2
+  - - 3
+    - 4
+# This illustrates that the specification is wrong in asserting that
+# the body size is divisible by the item count
+- sig: a(qq)
+  end: little
+  buf:
+  # body size
+  - [20, 0, 0, 0]
+  # padding
+  - [0, 0, 0, 0]
+  # item
+  - [5, 0, 6, 0]
+  # padding
+  - [0, 0, 0, 0]
+  # item
+  - [7, 0, 8, 0]
+  # padding
+  - [0, 0, 0, 0]
+  # item
+  - [9, 0, 10, 0]
+  val:
+  - - 5
+    - 6
+  - - 7
+    - 8
+  - - 9
+    - 10
+- sig: a(qq)
+  end: little
+  buf:
+  # body size, missing padding
+  - [0, 0, 0, 0]
+  exc: DBus::IncompleteBufferException
+  msg: ''
+- sig: a(qq)
+  end: little
+  buf:
+  # body size
+  - [0, 0, 0, 0]
+  # nonzero padding
+  - [0xDE, 0xAD, 0xBE, 0xEF]
+  exc: DBus::InvalidPacketException
+  msg: ''
+- sig: a{yq}
+  end: little
+  buf:
+  # body size
+  - [0, 0, 0, 0]
+  # padding
+  - [0, 0, 0, 0]
+  val: {}
+- sig: a{yq}
+  end: little
+  buf:
+  # body size
+  - [12, 0, 0, 0]
+  # dict_entry padding
+  - [0, 0, 0, 0]
+  # key, padding, value
+  - [1, 0, 2, 0]
+  # dict_entry padding
+  - [0, 0, 0, 0]
+  # key, padding, value
+  - [3, 0, 4, 0]
+  val:
+    1: 2
+    3: 4
+- sig: a{yq}
+  end: big
+  buf:
+  # body size
+  - [0, 0, 0, 12]
+  # dict_entry padding
+  - [0, 0, 0, 0]
+  # key, padding, value
+  - [1, 0, 0, 2]
+  # dict_entry padding
+  - [0, 0, 0, 0]
+  # key, padding, value
+  - [3, 0, 0, 4]
+  val:
+    1: 2
+    3: 4
+- sig: a{yq}
+  end: little
+  buf:
+  # body size, missing padding
+  - [0, 0, 0, 0]
+  exc: DBus::IncompleteBufferException
+  msg: ''
+- sig: a{yq}
+  end: little
+  buf:
+  # body size
+  - [0, 0, 0, 0]
+  # nonzero padding
+  - [0xDE, 0xAD, 0xBE, 0xEF]
+  exc: DBus::InvalidPacketException
+  msg: ''
+- sig: a{oq}
+  end: little
+  buf:
+  # body size
+  - [0, 0, 0, 0]
+  # padding
+  - [0, 0, 0, 0]
+  val: {}
+- sig: a{oq}
+  end: little
+  buf:
+  # body size
+  - [26, 0, 0, 0]
+  # dict_entry padding
+  - [0, 0, 0, 0]
+  # key, padding, value
+  - [2, 0, 0, 0, "/7", 0]
+  - 0
+  - [7, 0]
+  # dict_entry padding
+  - [0, 0, 0, 0, 0, 0]
+  # key, padding, value
+  - [2, 0, 0, 0, "/9", 0]
+  - 0
+  - [9, 0]
+  val:
+    /7: 7
+    /9: 9
+- sig: "(qq)"
+  end: little
+  buf:
+  - 1
+  - 0
+  - 2
+  - 0
+  val:
+  - 1
+  - 2
+- sig: "(qq)"
+  end: big
+  buf:
+  - 0
+  - 3
+  - 0
+  - 4
+  val:
+  - 3
+  - 4
+- sig: v
+  end: little
+  buf:
+  # signature
+  - [1, "y", 0]
+  # value
+  - 255
+  val: 255
+  marshall: false
+- sig: v
+  end: little
+  buf:
+  # signature
+  - [1, "u", 0]
+  # padding
+  - 0
+  # value
+  - [1, 0, 0, 0]
+  val: 1
+  marshall: false
+# nested variant
+- sig: v
+  end: little
+  buf:
+  # signature
+  - [1, "v", 0]
+  # value:
+  # signature
+  - [1, "y", 0]
+  # value
+  - 255
+  val: 255
+  marshall: false
+# the signature has no type
+- sig: v
+  end: little
+  buf:
+  # signature
+  - [0, 0]
+  exc: DBus::InvalidPacketException
+  msg: 1 value, 0 found
+# the signature has more than one type
+- sig: v
+  end: little
+  buf:
+  # signature
+  - [2, "yy", 0]
+  # data
+  - 255
+  - 255
+  exc: DBus::InvalidPacketException
+  msg: 1 value, 2 found
+# a variant nested 69 levels
+- sig: v
+  end: little
+  buf:
+  - [1, "v", 0,   1, "v", 0,   1, "v", 0,   1, "v", 0]
+  - [1, "v", 0,   1, "v", 0,   1, "v", 0,   1, "v", 0]
+  - [1, "v", 0,   1, "v", 0,   1, "v", 0,   1, "v", 0]
+  - [1, "v", 0,   1, "v", 0,   1, "v", 0,   1, "v", 0]
+
+  - [1, "v", 0,   1, "v", 0,   1, "v", 0,   1, "v", 0]
+  - [1, "v", 0,   1, "v", 0,   1, "v", 0,   1, "v", 0]
+  - [1, "v", 0,   1, "v", 0,   1, "v", 0,   1, "v", 0]
+  - [1, "v", 0,   1, "v", 0,   1, "v", 0,   1, "v", 0]
+
+  - [1, "v", 0,   1, "v", 0,   1, "v", 0,   1, "v", 0]
+  - [1, "v", 0,   1, "v", 0,   1, "v", 0,   1, "v", 0]
+  - [1, "v", 0,   1, "v", 0,   1, "v", 0,   1, "v", 0]
+  - [1, "v", 0,   1, "v", 0,   1, "v", 0,   1, "v", 0]
+
+  - [1, "v", 0,   1, "v", 0,   1, "v", 0,   1, "v", 0]
+  - [1, "v", 0,   1, "v", 0,   1, "v", 0,   1, "v", 0]
+  - [1, "v", 0,   1, "v", 0,   1, "v", 0,   1, "v", 0]
+  - [1, "v", 0,   1, "v", 0,   1, "v", 0,   1, "v", 0]
+
+  - [1, "v", 0,   1, "v", 0,   1, "v", 0,   1, "v", 0]
+
+  - [1, "y", 0]
+  - 255
+  exc: DBus::InvalidPacketException
+  msg: nested too deep
+  unmarshall: false
diff --git a/spec/data_spec.rb b/spec/data_spec.rb
new file mode 100755
index 0000000..044caa7
--- /dev/null
+++ b/spec/data_spec.rb
@@ -0,0 +1,673 @@
+#!/usr/bin/env rspec
+# frozen_string_literal: true
+
+require_relative "spec_helper"
+require "dbus"
+
+# The from_raw methods are tested in packet_unmarshaller_spec.rb
+
+RSpec.shared_examples "#== and #eql? work for basic types" do |*args|
+  plain_a = args.fetch(0, 22)
+  plain_b = args.fetch(1, 222)
+
+  context "with #{plain_a.inspect} and #{plain_b.inspect}" do
+    describe "#eql?" do
+      it "returns true for same class and value" do
+        a = described_class.new(plain_a)
+        b = described_class.new(plain_a)
+        expect(a).to eql(b)
+      end
+
+      it "returns false for same class, different value" do
+        a = described_class.new(plain_a)
+        b = described_class.new(plain_b)
+        expect(a).to_not eql(b)
+      end
+
+      it "returns false for same value but plain class" do
+        a = described_class.new(plain_a)
+        b = plain_a
+        expect(a).to_not eql(b)
+      end
+    end
+
+    describe "#==" do
+      it "returns true for same class and value" do
+        a = described_class.new(plain_a)
+        b = described_class.new(plain_a)
+        expect(a).to eq(b)
+      end
+
+      it "returns false for same class, different value" do
+        a = described_class.new(plain_a)
+        b = described_class.new(plain_b)
+        expect(a).to_not eq(b)
+      end
+
+      it "returns true for same value but plain class" do
+        a = described_class.new(plain_a)
+        b = plain_a
+        expect(a).to eq(b)
+      end
+    end
+  end
+end
+
+RSpec.shared_examples "#== and #eql? work for container types (1 value)" do |plain_a, a_kwargs|
+  a1 = described_class.new(plain_a, **a_kwargs)
+  a2 = described_class.new(plain_a, **a_kwargs)
+
+  context "with #{plain_a.inspect}, #{a_kwargs.inspect}" do
+    describe "#eql?" do
+      it "returns true for same class and value" do
+        expect(a1).to eql(a2)
+      end
+
+      it "returns false for same value but plain class" do
+        expect(a1).to_not eql(plain_a)
+      end
+    end
+
+    describe "#==" do
+      it "returns true for same class and value" do
+        expect(a1).to eq(a2)
+      end
+
+      it "returns true for same value but plain class" do
+        expect(a1).to eq(plain_a)
+      end
+    end
+  end
+end
+
+RSpec.shared_examples "#== and #eql? work for container types (inequal)" do |plain_a, a_kwargs, plain_b, b_kwargs|
+  # RSpec note: if the shared_examples is used via include_examples more than
+  # once in a single context, `let` would take value from just one of them.
+  # So use plain assignment.
+  a = described_class.new(plain_a, **a_kwargs)
+  b = described_class.new(plain_b, **b_kwargs)
+
+  include_examples "#== and #eql? work for container types (1 value)", plain_a, a_kwargs
+
+  context "with #{plain_a.inspect}, #{a_kwargs.inspect} and #{plain_b.inspect}, #{b_kwargs.inspect}" do
+    describe "#eql?" do
+      it "returns false for same class, different value" do
+        expect(a).to_not eql(b)
+      end
+    end
+
+    describe "#==" do
+      it "returns false for same class, different value" do
+        expect(a).to_not eq(b)
+      end
+    end
+  end
+end
+
+RSpec.shared_examples "#== and #eql? work for container types (equal)" do |plain_a, a_kwargs, plain_b, b_kwargs|
+  a = described_class.new(plain_a, **a_kwargs)
+  b = described_class.new(plain_b, **b_kwargs)
+
+  include_examples "#== and #eql? work for container types (1 value)", plain_a, a_kwargs
+
+  context "with #{plain_a.inspect}, #{a_kwargs.inspect} and #{plain_b.inspect}, #{b_kwargs.inspect}" do
+    describe "#eql?" do
+      it "returns true for same class, differently expressed value" do
+        expect(a).to eql(b)
+      end
+    end
+
+    describe "#==" do
+      it "returns true for same class, differently expressed value" do
+        expect(a).to eq(b)
+      end
+    end
+
+    describe "#==" do
+      it "returns true for plain, differently expressed value" do
+        expect(a).to eq(plain_b)
+        expect(b).to eq(plain_a)
+      end
+    end
+  end
+end
+
+RSpec.shared_examples "constructor accepts numeric range" do |min, max|
+  describe "#initialize" do
+    it "accepts the min value #{min}" do
+      expect(described_class.new(min).value).to eql(min)
+    end
+
+    it "accepts the max value #{max}" do
+      expect(described_class.new(max).value).to eql(max)
+    end
+
+    it "raises on too small a value #{min - 1}" do
+      expect { described_class.new(min - 1) }.to raise_error(RangeError)
+    end
+
+    it "raises on too big a value #{max + 1}" do
+      expect { described_class.new(max + 1) }.to raise_error(RangeError)
+    end
+
+    it "raises on nil" do
+      expect { described_class.new(nil) }.to raise_error(RangeError)
+    end
+  end
+end
+
+RSpec.shared_examples "constructor accepts plain or typed values" do |plain_list|
+  describe "#initialize" do
+    Array(plain_list).each do |plain|
+      it "accepts the plain value #{plain.inspect}" do
+        expect(described_class.new(plain).value).to eql(plain)
+        expect(described_class.new(plain)).to eq(plain)
+      end
+
+      it "accepts the typed value #{plain.inspect}" do
+        typed = described_class.new(plain)
+        expect(described_class.new(typed).value).to eql(plain)
+        expect(described_class.new(typed)).to eq(plain)
+      end
+    end
+  end
+end
+
+# FIXME: decide eq and eql here
+RSpec.shared_examples "constructor (kwargs) accepts values" do |list|
+  describe "#initialize" do
+    list.each do |value, kwargs_hash|
+      it "accepts the plain value #{value.inspect}, #{kwargs_hash.inspect}" do
+        expect(described_class.new(value, **kwargs_hash)).to eq(value)
+      end
+
+      it "accepts the typed value #{value.inspect}, #{kwargs_hash.inspect}" do
+        typed = described_class.new(value, **kwargs_hash)
+        expect(described_class.new(typed, **kwargs_hash)).to eq(value)
+      end
+    end
+  end
+end
+
+RSpec.shared_examples "constructor rejects values from this list" do |bad_list|
+  describe "#initialize" do
+    bad_list.each do |(value, exc_class, msg_substr)|
+      it "rejects #{value.inspect} with #{exc_class}: #{msg_substr}" do
+        msg_re = Regexp.try_convert(msg_substr) || Regexp.new(Regexp.quote(msg_substr))
+        expect { described_class.new(value) }.to raise_error(exc_class, msg_re)
+      end
+    end
+  end
+end
+
+RSpec.shared_examples "constructor (kwargs) rejects values" do |bad_list|
+  describe "#initialize" do
+    bad_list.each do |(value, kwargs_hash, exc_class, msg_substr)|
+      it "rejects #{value.inspect}, #{kwargs_hash.inspect} with #{exc_class}: #{msg_substr}" do
+        msg_re = Regexp.try_convert(msg_substr) || Regexp.new(Regexp.quote(msg_substr))
+        expect { described_class.new(value, **kwargs_hash) }.to raise_error(exc_class, msg_re)
+      end
+    end
+  end
+end
+
+# TODO: Look at conversions? to_str, to_int?
+
+describe DBus::Data do
+  T = DBus::Type unless const_defined? "T"
+
+  # test initialization, from user code, or from packet (from_raw)
+  # remember to unpack if initializing from Data::Base
+  # #value should recurse inside so that the user doesnt have to
+  # Kick InvalidPacketException out of here?
+
+  describe DBus::Data::Byte do
+    include_examples "#== and #eql? work for basic types"
+    include_examples "constructor accepts numeric range", 0, 2**8 - 1
+    include_examples "constructor accepts plain or typed values", 42
+  end
+
+  describe DBus::Data::Int16 do
+    include_examples "#== and #eql? work for basic types"
+    include_examples "constructor accepts numeric range", -2**15, 2**15 - 1
+    include_examples "constructor accepts plain or typed values", 42
+  end
+
+  describe DBus::Data::UInt16 do
+    include_examples "#== and #eql? work for basic types"
+    include_examples "constructor accepts numeric range", 0, 2**16 - 1
+    include_examples "constructor accepts plain or typed values", 42
+  end
+
+  describe DBus::Data::Int32 do
+    include_examples "#== and #eql? work for basic types"
+    include_examples "constructor accepts numeric range", -2**31, 2**31 - 1
+    include_examples "constructor accepts plain or typed values", 42
+  end
+
+  describe DBus::Data::UInt32 do
+    include_examples "#== and #eql? work for basic types"
+    include_examples "constructor accepts numeric range", 0, 2**32 - 1
+    include_examples "constructor accepts plain or typed values", 42
+  end
+
+  describe DBus::Data::Int64 do
+    include_examples "#== and #eql? work for basic types"
+    include_examples "constructor accepts numeric range", -2**63, 2**63 - 1
+    include_examples "constructor accepts plain or typed values", 42
+  end
+
+  describe DBus::Data::UInt64 do
+    include_examples "#== and #eql? work for basic types"
+    include_examples "constructor accepts numeric range", 0, 2**64 - 1
+    include_examples "constructor accepts plain or typed values", 42
+  end
+
+  describe DBus::Data::Boolean do
+    describe "#initialize" do
+      it "accepts false and true" do
+        expect(described_class.new(false).value).to eq(false)
+        expect(described_class.new(true).value).to eq(true)
+      end
+
+      it "accepts truth value of other objects" do
+        expect(described_class.new(nil).value).to eq(false)
+        expect(described_class.new(0).value).to eq(true) # !
+        expect(described_class.new(1).value).to eq(true)
+        expect(described_class.new(Time.now).value).to eq(true)
+      end
+    end
+
+    include_examples "#== and #eql? work for basic types", false, true
+    include_examples "constructor accepts plain or typed values", false
+  end
+
+  describe DBus::Data::Double do
+    include_examples "#== and #eql? work for basic types"
+    include_examples "constructor accepts plain or typed values", Math::PI
+
+    describe "#initialize" do
+      it "raises on values that can't be made a Float" do
+        expect { described_class.new(nil) }.to raise_error(TypeError)
+        expect { described_class.new("one") }.to raise_error(ArgumentError)
+        expect { described_class.new(/itsaregexp/) }.to raise_error(TypeError)
+      end
+    end
+  end
+
+  describe "basic, string-like types" do
+    describe DBus::Data::String do
+      # TODO: what about strings with good codepoints but encoded in
+      # let's say Encoding::ISO8859_2?
+      good = [
+        "",
+        "Ř",
+        # a Noncharacter, but well-formed Unicode
+        # https://www.unicode.org/versions/corrigendum9.html
+        "\uffff",
+        # maximal UTF-8 codepoint U+10FFFF
+        "\u{10ffff}"
+      ]
+
+      bad = [
+        # NUL in the middle
+        # FIXME: InvalidPacketException is wrong here, it should be ArgumentError
+        ["a\x00b", DBus::InvalidPacketException, "contains NUL"],
+        # invalid UTF-8
+        ["\xFF\xFF\xFF\xFF", DBus::InvalidPacketException, "not in UTF-8"],
+        # overlong sequence encoding an "A"
+        ["\xC1\x81", DBus::InvalidPacketException, "not in UTF-8"],
+        # first codepoint outside UTF-8, U+110000
+        ["\xF4\x90\xC0\xC0", DBus::InvalidPacketException, "not in UTF-8"]
+      ]
+
+      include_examples "#== and #eql? work for basic types", "foo", "bar"
+      include_examples "constructor accepts plain or typed values", good
+      include_examples "constructor rejects values from this list", bad
+
+      describe ".alignment" do
+        # this overly specific test avoids a redundant alignment call
+        # in the production code
+        it "returns the correct value" do
+          expect(described_class.alignment).to eq 4
+        end
+      end
+    end
+
+    describe DBus::Data::ObjectPath do
+      good = [
+        "/"
+        # TODO: others
+      ]
+
+      bad = [
+        ["", DBus::InvalidPacketException, "Invalid object path"]
+        # TODO: others
+      ]
+
+      include_examples "#== and #eql? work for basic types", "/foo", "/bar"
+      include_examples "constructor accepts plain or typed values", good
+      include_examples "constructor rejects values from this list", bad
+
+      describe ".alignment" do
+        # this overly specific test avoids a redundant alignment call
+        # in the production code
+        it "returns the correct value" do
+          expect(described_class.alignment).to eq 4
+        end
+      end
+    end
+
+    describe DBus::Data::Signature do
+      good = [
+        "",
+        "i",
+        "ii"
+        # TODO: others
+      ]
+
+      bad = [
+        ["!", DBus::InvalidPacketException, "Unknown type code"]
+        # TODO: others
+      ]
+
+      include_examples "#== and #eql? work for basic types", "aah", "aaaaah"
+      include_examples "constructor accepts plain or typed values", good
+      include_examples "constructor rejects values from this list", bad
+
+      describe ".alignment" do
+        # this overly specific test avoids a redundant alignment call
+        # in the production code
+        it "returns the correct value" do
+          expect(described_class.alignment).to eq 1
+        end
+      end
+    end
+  end
+
+  describe "containers" do
+    describe DBus::Data::Array do
+      aq = DBus::Data::Array.new([1, 2, 3], type: "aq")
+
+      good = [
+        [[1, 2, 3], { type: "aq" }],
+        [[1, 2, 3], { type: T::Array[T::UINT16] }],
+        [[1, 2, 3], { type: T::Array["q"] }],
+        [[DBus::Data::UInt16.new(1), DBus::Data::UInt16.new(2), DBus::Data::UInt16.new(3)], { type: T::Array["q"] }]
+        # TODO: others
+      ]
+
+      bad = [
+        # undesirable type guessing
+        [[1, 2, 3], { type: nil }, ArgumentError, /Expecting DBus::Type.*got nil/],
+        [[1, 2, 3], { type: "!" }, DBus::Type::SignatureException, "Unknown type code"],
+        [aq, { type: "q" }, ArgumentError, "Expecting \"a\""],
+        [aq, { type: "ao" }, ArgumentError,
+         "Specified type is ARRAY: [OBJECT_PATH] but value type is ARRAY: [UINT16]"]
+        # TODO: how to handle these?
+        # [{1 => 2, 3 => 4}, { type: "aq" }, ArgumentError, "?"],
+        # [/i am not an array/, { type: "aq" }, ArgumentError, "?"],
+      ]
+
+      include_examples "#== and #eql? work for container types (inequal)",
+                       [1, 2, 3], { type: "aq" },
+                       [3, 2, 1], { type: "aq" }
+
+      include_examples "#== and #eql? work for container types (inequal)",
+                       [[1, 2, 3]], { type: "aaq" },
+                       [[3, 2, 1]], { type: "aaq" }
+
+      include_examples "constructor (kwargs) accepts values", good
+      include_examples "constructor (kwargs) rejects values", bad
+
+      describe ".from_typed" do
+        it "creates new instance from given object and type" do
+          type = T::Array[String]
+          expect(described_class.from_typed(["test", "lest"], type: type)).to be_a(described_class)
+        end
+      end
+    end
+
+    describe DBus::Data::Struct do
+      three_words = ::Struct.new(:a, :b, :c)
+
+      qqq = T::Struct[T::UINT16, T::UINT16, T::UINT16]
+      integers = [1, 2, 3]
+      uints = [DBus::Data::UInt16.new(1), DBus::Data::UInt16.new(2), DBus::Data::UInt16.new(3)]
+
+      # TODO: all the reasonable initialization params
+      # need to be normalized into one/few internal representation.
+      # So check what is the result
+      #
+      # Internally, it must be Data::Base
+      # Perhaps distinguish #value => Data::Base
+      # and #plain_value => plain Ruby
+      #
+      # but then, can they mutate?
+      #
+      # TODO: also check data ownership: reasonable to own the data?
+      # can make it explicit?
+      good = [
+        # from plain array; various *type* styles
+        [integers, { type: DBus.type("(qqq)") }],
+        [integers, { type: T::Struct["q", "q", "q"] }],
+        [integers, { type: T::Struct[T::UINT16, T::UINT16, T::UINT16] }],
+        [integers, { type: T::Struct[*DBus.types("qqq")] }],
+        # plain array of data
+        [uints, { type: qqq }],
+        # ::Struct
+        [three_words.new(*integers), { type: qqq }],
+        [three_words.new(*uints), { type: qqq }]
+        # TODO: others
+      ]
+
+      # check these only when canonicalizing @value, because that will
+      # type-check the value deeply
+      _bad_but_valid = [
+        # STRUCT specific: member count mismatch
+        [[1, 2], { type: qqq }, ArgumentError, "???"],
+        [[1, 2, 3, 4], { type: qqq }, ArgumentError, "???"]
+        # TODO: others
+      ]
+
+      include_examples "#== and #eql? work for container types (inequal)",
+                       [1, 2, 3], { type: qqq },
+                       [3, 2, 1], { type: qqq }
+
+      include_examples "#== and #eql? work for container types (equal)",
+                       three_words.new(*integers), { type: qqq },
+                       [1, 2, 3], { type: qqq }
+
+      include_examples "constructor (kwargs) accepts values", good
+      # include_examples "constructor (kwargs) rejects values", bad
+
+      describe ".from_typed" do
+        it "creates new instance from given object and type" do
+          type = T::Struct[T::STRING, T::STRING]
+          expect(described_class.from_typed(["test", "lest"].freeze, type: type))
+            .to be_a(described_class)
+        end
+      end
+
+      describe "#initialize" do
+        it "converts type to Type" do
+          value = [1, 2, 3]
+          type = "(uuu)"
+          result = described_class.new(value, type: type)
+          expect(result.type).to be_a DBus::Type
+        end
+
+        it "checks that type matches class" do
+          value = [1, 2, 3]
+          type = T::Array[T::INT32]
+          expect { described_class.new(value, type: type) }
+            .to raise_error(ArgumentError, /Expecting "r"/)
+        end
+
+        it "checks type of a Data::Struct value" do
+          value1 = [1, 2, 3]
+          type1 = "(uuu)"
+          result1 = described_class.new(value1, type: type1)
+
+          value2 = result1
+          type2 = "(xxx)"
+          expect { described_class.new(value2, type: type2) }
+            .to raise_error(ArgumentError, /value type is STRUCT.*UINT32/)
+        end
+
+        it "checks that size of type and value match" do
+          value = [1, 2, 3, 4]
+          type = "(uuu)"
+          expect { described_class.new(value, type: type) }
+            .to raise_error(ArgumentError, /type has 3 members.*value has 4 members/)
+        end
+
+        it "converts value to ::Array of Data::Base" do
+          value = three_words.new(*integers)
+          type = T::Struct[T::INT32, T::INT32, T::INT32]
+          result = described_class.new(value, type: type)
+
+          expect(result.exact_value).to be_an(::Array)
+          expect(result.exact_value[0]).to be_a(DBus::Data::Base)
+        end
+      end
+    end
+
+    describe DBus::Data::DictEntry do
+      describe ".from_typed" do
+        it "creates new instance from given object and type" do
+          type = T::Hash[String, T::INT16].child
+          expect(described_class.from_typed(["test", 12], type: type))
+            .to be_a(described_class)
+        end
+      end
+
+      describe "#initialize" do
+        it "checks that type matches class" do
+          value = [1, 2]
+          type = T::Array[T::INT32]
+
+          expect { described_class.new(value, type: type) }
+            .to raise_error(ArgumentError, /Expecting "e"/)
+        end
+
+        it "checks type of a Data::DictEntry value" do
+          value1 = [1, 2]
+          type1 = T::Hash[T::UINT32, T::UINT32].child
+          result1 = described_class.new(value1, type: type1)
+
+          value2 = result1
+          type2 = T::Hash[T::UINT64, T::UINT64].child
+          expect { described_class.new(value2, type: type2) }
+            .to raise_error(ArgumentError, /value type is DICT_ENTRY.*UINT32/)
+        end
+
+        it "checks that size of type and value match" do
+          value = [1, 2, 3]
+          type = T::Hash[T::UINT32, T::UINT32].child
+          expect { described_class.new(value, type: type) }
+            .to raise_error(ArgumentError, /type has 2 members.*value has 3 members/)
+        end
+
+        it "converts value to ::Array of Data::Base" do
+          two_words = ::Struct.new(:k, :v)
+          value = two_words.new(1, 2)
+          type = T::Hash[T::UINT32, T::UINT32].child
+          result = described_class.new(value, type: type)
+
+          expect(result.exact_value).to be_an(::Array)
+          expect(result.exact_value[0]).to be_a(DBus::Data::Base)
+        end
+
+        it "takes a plain value" do
+          input = ["test", 23]
+
+          type = T::Hash[String, T::INT16].child
+          value = described_class.new(input, type: type)
+
+          expect(value).to be_a(described_class)
+          expect(value.type.to_s).to eq "{sn}"
+          expect(value.value).to eql input
+        end
+      end
+    end
+
+    describe DBus::Data::Variant do
+      describe ".from_typed" do
+        it "creates new instance from given object and type" do
+          type = DBus.type(T::VARIANT)
+          value = described_class.from_typed("test", type: type)
+          expect(value).to be_a(described_class)
+          expect(value.type.to_s).to eq "v"
+          expect(value.member_type.to_s).to eq "s"
+        end
+      end
+
+      describe "#initialize" do
+        it "takes a plain value" do
+          input = 42
+
+          type = DBus.type(T::INT16)
+          value = described_class.new(input, member_type: type)
+          expect(value).to be_a(described_class)
+          expect(value.type.to_s).to eq "v"
+          expect(value.member_type.to_s).to eq "n"
+          expect(value.value).to eq 42
+        end
+
+        # FIXME: verify that @value has the correct class
+        it "takes an exact value" do
+          input = DBus::Data::Int16.new(42)
+
+          type = DBus.type(T::INT16)
+          value = described_class.new(input, member_type: type)
+          expect(value).to be_a(described_class)
+          expect(value.type.to_s).to eq "v"
+          expect(value.member_type.to_s).to eq "n"
+          expect(value.value).to eq 42
+        end
+
+        it "checks the type of the exact value" do
+          input = DBus::Data::UInt16.new(42)
+
+          type = DBus.type(T::INT16)
+          expect { described_class.new(input, member_type: type) }
+            .to raise_error(ArgumentError, /Variant type n does not match value type q/)
+        end
+      end
+
+      include_examples "#== and #eql? work for container types (1 value)",
+                       "/foo", { member_type: DBus.type(T::STRING) }
+
+      describe "DBus.variant compatibility" do
+        let(:v) { DBus.variant("o", "/foo") }
+
+        describe "#[]" do
+          it "returns the type for 0" do
+            expect(v[0]).to eq DBus.type(DBus::Type::OBJECT_PATH)
+          end
+
+          it "returns the value for 1" do
+            expect(v[1]).to eq DBus::ObjectPath.new("/foo")
+          end
+
+          it "returns an error for other indices" do
+            expect { v[2] }.to raise_error(ArgumentError, /DBus.variant can only be indexed with 0 or 1/)
+          end
+        end
+
+        describe "#first" do
+          it "returns the type" do
+            expect(v.first).to eq DBus.type(DBus::Type::OBJECT_PATH)
+          end
+        end
+
+        describe "#last" do
+          it "returns the value" do
+            expect(v.last).to eq DBus::ObjectPath.new("/foo")
+          end
+        end
+      end
+    end
+  end
+end
diff --git a/spec/emits_changed_signal_spec.rb b/spec/emits_changed_signal_spec.rb
new file mode 100755
index 0000000..04668ab
--- /dev/null
+++ b/spec/emits_changed_signal_spec.rb
@@ -0,0 +1,58 @@
+#!/usr/bin/env rspec
+# frozen_string_literal: true
+
+require_relative "spec_helper"
+require "dbus"
+
+describe DBus::EmitsChangedSignal do
+  describe "#initialize" do
+    it "accepts a simple value" do
+      expect(described_class.new(:const).value).to eq :const
+    end
+
+    it "avoids nil by asking the interface" do
+      ifc = DBus::Interface.new("org.example.Foo")
+      ifc.emits_changed_signal = described_class.new(:invalidates)
+
+      expect(described_class.new(nil, interface: ifc).value).to eq :invalidates
+    end
+
+    it "fails for unknown value" do
+      expect { described_class.new(:huh) }.to raise_error(ArgumentError, /Seen :huh/)
+    end
+
+    it "fails for 2 nils" do
+      expect { described_class.new(nil, interface: nil) }.to raise_error(ArgumentError, /Both/)
+    end
+  end
+
+  describe "#==" do
+    it "is true for two different objects with the same value" do
+      const_a = described_class.new(:const)
+      const_b = described_class.new(:const)
+      expect(const_a == const_b).to be true
+    end
+  end
+
+  describe "#to_xml" do
+    it "uses a string value" do
+      expect(described_class.new(:const).to_xml)
+        .to eq "    <annotation name=\"org.freedesktop.DBus.Property.EmitsChangedSignal\" value=\"const\"/>\n"
+    end
+  end
+
+  describe "#to_s" do
+    it "uses a string value" do
+      expect(described_class.new(:const).to_s).to eq "const"
+    end
+  end
+end
+
+describe DBus::Interface do
+  describe ".emits_changed_signal=" do
+    it "only allows an EmitsChangedSignal as argument" do
+      ifc = described_class.new("org.ruby.Interface")
+      expect { ifc.emits_changed_signal = :const }.to raise_error(TypeError)
+    end
+  end
+end
diff --git a/spec/err_msg_spec.rb b/spec/err_msg_spec.rb
index 18829f3..1db4046 100755
--- a/spec/err_msg_spec.rb
+++ b/spec/err_msg_spec.rb
@@ -1,4 +1,6 @@
 #!/usr/bin/env rspec
+# frozen_string_literal: true
+
 # should report it missing on org.ruby.SampleInterface
 # (on object...) instead of on DBus::Proxy::ObjectInterface
 require_relative "spec_helper"
diff --git a/spec/introspect_xml_parser_spec.rb b/spec/introspect_xml_parser_spec.rb
index ec638a7..b1f2548 100755
--- a/spec/introspect_xml_parser_spec.rb
+++ b/spec/introspect_xml_parser_spec.rb
@@ -1,4 +1,6 @@
 #!/usr/bin/env rspec
+# frozen_string_literal: true
+
 require_relative "spec_helper"
 require "dbus"
 
diff --git a/spec/introspection_spec.rb b/spec/introspection_spec.rb
index 1f6c8c6..364596b 100755
--- a/spec/introspection_spec.rb
+++ b/spec/introspection_spec.rb
@@ -1,4 +1,6 @@
 #!/usr/bin/env rspec
+# frozen_string_literal: true
+
 require_relative "spec_helper"
 require "dbus"
 
diff --git a/spec/main_loop_spec.rb b/spec/main_loop_spec.rb
index 309b101..18d2a6d 100755
--- a/spec/main_loop_spec.rb
+++ b/spec/main_loop_spec.rb
@@ -1,4 +1,6 @@
 #!/usr/bin/env rspec
+# frozen_string_literal: true
+
 # Test the main loop
 require_relative "spec_helper"
 require "dbus"
@@ -70,7 +72,7 @@ describe "MainLoopTest" do
     @obj.on_signal "LongTaskEnd"
   end
 
-  it "tests loop quit" do
+  it "tests loop quit", slow: true do
     test_loop_quit 1
   end
 
diff --git a/spec/node_spec.rb b/spec/node_spec.rb
new file mode 100755
index 0000000..c8112ab
--- /dev/null
+++ b/spec/node_spec.rb
@@ -0,0 +1,69 @@
+#!/usr/bin/env rspec
+# frozen_string_literal: true
+
+require_relative "spec_helper"
+require "dbus"
+
+describe DBus::Node do
+  describe "#inspect" do
+    # the behavior needs improvement
+    it "shows the node, poorly" do
+      parent = described_class.new("parent")
+      parent.object = DBus::Object.new("/parent")
+
+      3.times do |i|
+        child_name = "child#{i}"
+        child = described_class.new(child_name)
+        parent[child_name] = child
+      end
+
+      expect(parent.inspect).to match(/<DBus::Node [0-9a-f]+ {child0 => {},child1 => {},child2 => {}}>/)
+    end
+  end
+
+  describe "#descendant_objects" do
+    let(:manager_path) { "/org/example/FooManager" }
+    let(:child_paths) do
+      [
+        # note that "/org/example/FooManager/good"
+        # is a path under a managed object but there is no object there
+        "/org/example/FooManager/good/1",
+        "/org/example/FooManager/good/2",
+        "/org/example/FooManager/good/3",
+        "/org/example/FooManager/bad/1",
+        "/org/example/FooManager/bad/2"
+      ]
+    end
+
+    let(:non_child_paths) do
+      [
+        "/org/example/BarManager/good/1",
+        "/org/example/BarManager/good/2"
+      ]
+    end
+
+    context "on the bus" do
+      let(:bus) { DBus::ASessionBus.new }
+      let(:service) do
+        # if we used org.ruby.service it would be a name collision
+        # ... which would not break the test for lucky reasons
+        bus.request_service("org.ruby.service.scratch")
+      end
+
+      before do
+        service.export(DBus::Object.new(manager_path))
+        non_child_paths.each do |p|
+          service.export(DBus::Object.new(p))
+        end
+      end
+
+      it "returns just the descendants of the specified objects" do
+        child_exported_objects = child_paths.map { |p| DBus::Object.new(p) }
+        child_exported_objects.each { |obj| service.export(obj) }
+
+        node = service.get_node(manager_path, create: false)
+        expect(node.descendant_objects).to eq child_exported_objects
+      end
+    end
+  end
+end
diff --git a/spec/object_manager_spec.rb b/spec/object_manager_spec.rb
new file mode 100755
index 0000000..3dd1829
--- /dev/null
+++ b/spec/object_manager_spec.rb
@@ -0,0 +1,33 @@
+#!/usr/bin/env rspec
+# frozen_string_literal: true
+
+require_relative "spec_helper"
+require "dbus"
+
+describe DBus::ObjectManager do
+  describe "GetManagedObjects" do
+    let(:bus) { DBus::ASessionBus.new }
+    let(:service) { bus["org.ruby.service"] }
+    let(:obj) { service["/org/ruby/MyInstance"] }
+    let(:parent_iface) { obj["org.ruby.TestParent"] }
+    let(:om_iface) { obj["org.freedesktop.DBus.ObjectManager"] }
+
+    it "returns the interfaces and properties of currently managed objects" do
+      c1_opath = parent_iface.New("child1")
+      c2_opath = parent_iface.New("child2")
+
+      parent_iface.Delete(c1_opath)
+      expected_gmo = {
+        "/org/ruby/MyInstance/child2" => {
+          "org.freedesktop.DBus.Introspectable" => {},
+          "org.freedesktop.DBus.Properties" => {},
+          "org.ruby.TestChild" => { "Name" => "Child2" }
+        }
+      }
+      expect(om_iface.GetManagedObjects).to eq(expected_gmo)
+
+      parent_iface.Delete(c2_opath)
+      expect(om_iface.GetManagedObjects).to eq({})
+    end
+  end
+end
diff --git a/spec/object_path_spec.rb b/spec/object_path_spec.rb
index 82e18b1..85c022c 100755
--- a/spec/object_path_spec.rb
+++ b/spec/object_path_spec.rb
@@ -1,4 +1,6 @@
 #!/usr/bin/env rspec
+# frozen_string_literal: true
+
 require_relative "spec_helper"
 require "dbus"
 
@@ -18,6 +20,7 @@ describe DBus::ObjectPath do
       expect(described_class.valid?("/EmptyLastComponent/")).to be_falsey
       expect(described_class.valid?("/Invalid Character")).to be_falsey
       expect(described_class.valid?("/Invalid-Character")).to be_falsey
+      expect(described_class.valid?("/InválídCháráctér")).to be_falsey
     end
   end
 end
diff --git a/spec/object_spec.rb b/spec/object_spec.rb
new file mode 100755
index 0000000..f4f0f15
--- /dev/null
+++ b/spec/object_spec.rb
@@ -0,0 +1,148 @@
+#!/usr/bin/env rspec
+# frozen_string_literal: true
+
+require_relative "spec_helper"
+require "dbus"
+
+class ObjectTest < DBus::Object
+  T = DBus::Type unless const_defined? "T"
+
+  dbus_interface "org.ruby.ServerTest" do
+    dbus_attr_writer :write_me, T::Struct[String, String]
+
+    attr_accessor :read_only_for_dbus
+
+    dbus_reader :read_only_for_dbus, T::STRING, emits_changed_signal: :invalidates
+  end
+end
+
+describe DBus::Object do
+  describe ".dbus_attr_writer" do
+    describe "the declared assignment method" do
+      # Slightly advanced RSpec:
+      # https://rspec.info/documentation/3.9/rspec-expectations/RSpec/Matchers.html#satisfy-instance_method
+      let(:a_struct_in_a_variant) do
+        satisfying { |x| x.is_a?(DBus::Data::Variant) && x.member_type.to_s == "(ss)" }
+        # ^ This formatting keeps the matcher on a single line
+        # which enables RSpec to cite it if it fails, instead of saying "block".
+      end
+
+      it "emits PropertyChanged with correctly typed argument" do
+        obj = ObjectTest.new("/test")
+        expect(obj).to receive(:PropertiesChanged).with(
+          "org.ruby.ServerTest",
+          {
+            "WriteMe" => a_struct_in_a_variant
+          },
+          []
+        )
+        # bug: call PC with simply the assigned value,
+        # which will need type guessing
+        obj.write_me = ["two", "strings"]
+      end
+    end
+  end
+
+  describe ".dbus_accessor" do
+    it "can only be used within a dbus_interface" do
+      expect do
+        ObjectTest.instance_exec do
+          dbus_accessor :foo, DBus::Type::STRING
+        end
+      end.to raise_error(DBus::Object::UndefinedInterface)
+    end
+  end
+
+  describe ".dbus_reader" do
+    it "can only be used within a dbus_interface" do
+      expect do
+        ObjectTest.instance_exec do
+          dbus_reader :foo, DBus::Type::STRING
+        end
+      end.to raise_error(DBus::Object::UndefinedInterface)
+    end
+
+    it "fails when the signature is invalid" do
+      expect do
+        ObjectTest.instance_exec do
+          dbus_interface "org.ruby.ServerTest" do
+            dbus_reader :foo2, "!"
+          end
+        end
+      end.to raise_error(DBus::Type::SignatureException)
+    end
+  end
+
+  describe ".dbus_reader, when paired with attr_accessor" do
+    describe "the declared assignment method" do
+      it "emits PropertyChanged" do
+        obj = ObjectTest.new("/test")
+        expect(obj).to receive(:PropertiesChanged).with(
+          "org.ruby.ServerTest",
+          {},
+          ["ReadOnlyForDbus"]
+        )
+        obj.read_only_for_dbus = "myvalue"
+      end
+    end
+  end
+
+  describe ".dbus_writer" do
+    it "can only be used within a dbus_interface" do
+      expect do
+        ObjectTest.instance_exec do
+          dbus_writer :foo, DBus::Type::STRING
+        end
+      end.to raise_error(DBus::Object::UndefinedInterface)
+    end
+  end
+
+  describe ".dbus_watcher" do
+    it "can only be used within a dbus_interface" do
+      expect do
+        ObjectTest.instance_exec do
+          dbus_watcher :foo
+        end
+      end.to raise_error(DBus::Object::UndefinedInterface)
+    end
+  end
+
+  describe ".dbus_method" do
+    it "can only be used within a dbus_interface" do
+      expect do
+        ObjectTest.instance_exec do
+          dbus_method :foo do
+          end
+        end
+      end.to raise_error(DBus::Object::UndefinedInterface)
+    end
+  end
+
+  describe ".emits_changed_signal" do
+    it "raises UndefinedInterface when so" do
+      expect { ObjectTest.emits_changed_signal = false }
+        .to raise_error DBus::Object::UndefinedInterface
+    end
+
+    it "assigns to the current interface" do
+      ObjectTest.instance_exec do
+        dbus_interface "org.ruby.Interface" do
+          self.emits_changed_signal = false
+        end
+      end
+      ecs = ObjectTest.intfs["org.ruby.Interface"].emits_changed_signal
+      expect(ecs).to eq false
+    end
+
+    it "only can be assigned once" do
+      expect do
+        Class.new(DBus::Object) do
+          dbus_interface "org.ruby.Interface" do
+            self.emits_changed_signal = false
+            self.emits_changed_signal = :invalidates
+          end
+        end
+      end.to raise_error(RuntimeError, /assigned more than once/)
+    end
+  end
+end
diff --git a/spec/packet_marshaller_spec.rb b/spec/packet_marshaller_spec.rb
new file mode 100755
index 0000000..28bf95d
--- /dev/null
+++ b/spec/packet_marshaller_spec.rb
@@ -0,0 +1,41 @@
+#!/usr/bin/env rspec
+# frozen_string_literal: true
+
+require_relative "spec_helper"
+require "dbus"
+require "ostruct"
+require "yaml"
+
+data_dir = File.expand_path("data", __dir__)
+marshall_yaml_s = File.read("#{data_dir}/marshall.yaml")
+marshall_yaml = YAML.safe_load(marshall_yaml_s)
+
+describe DBus::PacketMarshaller do
+  context "marshall.yaml" do
+    marshall_yaml.each do |test|
+      t = OpenStruct.new(test)
+      next if t.marshall == false
+      # skip test cases for invalid unmarshalling
+      next if t.val.nil?
+
+      # while the marshaller can use only native endianness, skip the other
+      endianness = t.end.to_sym
+
+      signature = t.sig
+      expected = buffer_from_yaml(t.buf)
+
+      it "writes a '#{signature}' with value #{t.val.inspect} (#{endianness})" do
+        subject = described_class.new(endianness: endianness)
+        subject.append(signature, t.val)
+        expect(subject.packet).to eq(expected)
+      end
+
+      it "writes a '#{signature}' with typed value #{t.val.inspect} (#{endianness})" do
+        subject = described_class.new(endianness: endianness)
+        typed_val = DBus::Data.make_typed(signature, t.val)
+        subject.append(signature, typed_val)
+        expect(subject.packet).to eq(expected)
+      end
+    end
+  end
+end
diff --git a/spec/packet_unmarshaller_spec.rb b/spec/packet_unmarshaller_spec.rb
new file mode 100755
index 0000000..2992cb9
--- /dev/null
+++ b/spec/packet_unmarshaller_spec.rb
@@ -0,0 +1,248 @@
+#!/usr/bin/env rspec
+# frozen_string_literal: true
+
+require_relative "spec_helper"
+require "dbus"
+require "ostruct"
+require "yaml"
+
+data_dir = File.expand_path("data", __dir__)
+marshall_yaml_s = File.read("#{data_dir}/marshall.yaml")
+marshall_yaml = YAML.safe_load(marshall_yaml_s)
+
+# Helper to access PacketUnmarshaller internals.
+# Add it to its public API?
+# @param p_u [PacketUnmarshaller]
+# @return [String] the binary string with unconsumed data
+def remaining_buffer(p_u)
+  raw_msg = p_u.instance_variable_get(:@raw_msg)
+  raw_msg.remaining_bytes
+end
+
+RSpec.shared_examples "parses good data" do |cases|
+  describe "parses all the instances of good test data" do
+    cases.each_with_index do |(buffer, endianness, expected), i|
+      it "parses plain data ##{i}" do
+        buffer = String.new(buffer, encoding: Encoding::BINARY)
+        subject = described_class.new(buffer, endianness)
+
+        results = subject.unmarshall(signature, mode: :plain)
+        # unmarshall works on multiple signatures but we use one
+        expect(results).to be_an(Array)
+        expect(results.size).to eq(1)
+        result = results.first
+
+        expect(result).to eq(expected)
+
+        expect(remaining_buffer(subject)).to be_empty
+      end
+
+      it "parses exact data ##{i}" do
+        buffer = String.new(buffer, encoding: Encoding::BINARY)
+        subject = described_class.new(buffer, endianness)
+
+        results = subject.unmarshall(signature, mode: :exact)
+        # unmarshall works on multiple signatures but we use one
+        expect(results).to be_an(Array)
+        expect(results.size).to eq(1)
+        result = results.first
+
+        expect(result).to be_a(DBus::Data::Base)
+        expect(result.value).to eq(expected)
+
+        expect(remaining_buffer(subject)).to be_empty
+      end
+    end
+  end
+end
+
+# this is necessary because we do an early switch on the signature
+RSpec.shared_examples "reports empty data" do
+  it "reports empty data" do
+    [:big, :little].each do |endianness|
+      subject = described_class.new("", endianness)
+      expect { subject.unmarshall(signature) }.to raise_error(DBus::IncompleteBufferException)
+    end
+  end
+end
+
+describe DBus::PacketUnmarshaller do
+  context "marshall.yaml" do
+    marshall_yaml.each do |test|
+      t = OpenStruct.new(test)
+      signature = t.sig
+      buffer = buffer_from_yaml(t.buf)
+      endianness = t.end.to_sym
+
+      # successful parse
+      if !t.val.nil?
+        expected = t.val
+
+        it "parses a '#{signature}' to get #{t.val.inspect} (plain)" do
+          subject = described_class.new(buffer, endianness)
+          results = subject.unmarshall(signature, mode: :plain)
+          # unmarshall works on multiple signatures but we use one
+          expect(results).to be_an(Array)
+          expect(results.size).to eq(1)
+          result = results.first
+
+          expect(result).to eq(expected)
+          expect(remaining_buffer(subject)).to be_empty
+        end
+
+        it "parses a '#{t.sig}' to get #{t.val.inspect} (exact)" do
+          subject = described_class.new(buffer, endianness)
+          results = subject.unmarshall(signature, mode: :exact)
+          # unmarshall works on multiple signatures but we use one
+          expect(results).to be_an(Array)
+          expect(results.size).to eq(1)
+          result = results.first
+
+          expect(result).to be_a(DBus::Data::Base)
+          expect(result.value).to eq(expected)
+
+          expect(remaining_buffer(subject)).to be_empty
+        end
+      elsif t.exc
+        next if t.unmarshall == false
+
+        exc_class = DBus.const_get(t.exc)
+        msg_re = Regexp.new(Regexp.escape(t.msg))
+
+        # TODO: InvalidPacketException is never rescued.
+        # The other end is sending invalid data. Can we do better than crashing?
+        # When we can test with peer connections, try it out.
+        it "parses a '#{signature}' to report a #{t.exc}" do
+          subject = described_class.new(buffer, endianness)
+          expect { subject.unmarshall(signature, mode: :plain) }.to raise_error(exc_class, msg_re)
+
+          subject = described_class.new(buffer, endianness)
+          expect { subject.unmarshall(signature, mode: :exact) }.to raise_error(exc_class, msg_re)
+        end
+      end
+    end
+  end
+
+  context "BYTEs" do
+    let(:signature) { "y" }
+    include_examples "reports empty data"
+  end
+
+  context "BOOLEANs" do
+    let(:signature) { "b" }
+    include_examples "reports empty data"
+  end
+
+  context "INT16s" do
+    let(:signature) { "n" }
+    include_examples "reports empty data"
+  end
+
+  context "UINT16s" do
+    let(:signature) { "q" }
+    include_examples "reports empty data"
+  end
+
+  context "INT32s" do
+    let(:signature) { "i" }
+    include_examples "reports empty data"
+  end
+
+  context "UINT32s" do
+    let(:signature) { "u" }
+    include_examples "reports empty data"
+  end
+
+  context "UNIX_FDs" do
+    let(:signature) { "h" }
+    include_examples "reports empty data"
+  end
+
+  context "INT64s" do
+    let(:signature) { "x" }
+    include_examples "reports empty data"
+  end
+
+  context "UINT64s" do
+    let(:signature) { "t" }
+    include_examples "reports empty data"
+  end
+
+  context "DOUBLEs" do
+    let(:signature) { "d" }
+    # See https://en.wikipedia.org/wiki/Double-precision_floating-point_format
+    # for binary representations
+    # TODO: figure out IEEE754 comparisons
+    good = [
+      # But == cant distinguish -0.0
+      ["\x00\x00\x00\x00\x00\x00\x00\x80", :little, -0.0],
+      # But NaN == NaN is false!
+      # ["\xff\xff\xff\xff\xff\xff\xff\xff", :little, Float::NAN],
+      ["\x80\x00\x00\x00\x00\x00\x00\x00", :big, -0.0]
+      # ["\xff\xff\xff\xff\xff\xff\xff\xff", :big, Float::NAN]
+    ]
+    include_examples "parses good data", good
+    include_examples "reports empty data"
+  end
+
+  context "STRINGs" do
+    let(:signature) { "s" }
+    include_examples "reports empty data"
+  end
+
+  context "OBJECT_PATHs" do
+    let(:signature) { "o" }
+    include_examples "reports empty data"
+  end
+
+  context "SIGNATUREs" do
+    let(:signature) { "g" }
+    include_examples "reports empty data"
+  end
+
+  context "ARRAYs" do
+    context "of BYTEs" do
+      let(:signature) { "ay" }
+      include_examples "reports empty data"
+    end
+
+    context "of UINT64s" do
+      let(:signature) { "at" }
+      include_examples "reports empty data"
+    end
+
+    context "of STRUCT of 2 UINT16s" do
+      let(:signature) { "a(qq)" }
+      include_examples "reports empty data"
+    end
+
+    context "of DICT_ENTRIES" do
+      let(:signature) { "a{yq}" }
+      include_examples "reports empty data"
+    end
+  end
+
+  context "STRUCTs" do
+    # TODO: this is invalid but does not raise
+    context "(generic 'r' struct)" do
+      let(:signature) { "r" }
+    end
+
+    context "of two shorts" do
+      let(:signature) { "(qq)" }
+      include_examples "reports empty data"
+    end
+  end
+
+  # makes sense here? or in array? remember invalid sigs are rejected elsewhere
+  context "DICT_ENTRYs" do
+    context "(generic 'e' dict_entry)" do
+      let(:signature) { "e" }
+    end
+  end
+
+  context "VARIANTs" do
+    let(:signature) { "v" }
+    include_examples "reports empty data"
+  end
+end
diff --git a/spec/platform_spec.rb b/spec/platform_spec.rb
new file mode 100755
index 0000000..3c2e2f8
--- /dev/null
+++ b/spec/platform_spec.rb
@@ -0,0 +1,14 @@
+#!/usr/bin/env rspec
+# frozen_string_literal: true
+
+require_relative "spec_helper"
+require "dbus"
+
+describe DBus::Platform do
+  describe ".macos?" do
+    # code coverage chasing, as other tests mock it out
+    it "doesn't crash" do
+      expect { described_class.macos? }.to_not raise_error
+    end
+  end
+end
diff --git a/spec/property_spec.rb b/spec/property_spec.rb
index a22ac5d..deb9989 100755
--- a/spec/property_spec.rb
+++ b/spec/property_spec.rb
@@ -1,11 +1,21 @@
 #!/usr/bin/env rspec
+# frozen_string_literal: true
+
 require_relative "spec_helper"
 require "dbus"
 
+# FIXME: factor out DBus::TestFixtures::Value in spec_helper
+require "ostruct"
+require "yaml"
+
+data_dir = File.expand_path("data", __dir__)
+marshall_yaml_s = File.read("#{data_dir}/marshall.yaml")
+marshall_yaml = YAML.safe_load(marshall_yaml_s)
+
 describe "PropertyTest" do
   before(:each) do
-    session_bus = DBus::ASessionBus.new
-    @svc = session_bus.service("org.ruby.service")
+    @session_bus = DBus::ASessionBus.new
+    @svc = @session_bus.service("org.ruby.service")
     @obj = @svc.object("/org/ruby/MyInstance")
     @iface = @obj["org.ruby.SampleInterface"]
   end
@@ -21,6 +31,10 @@ describe "PropertyTest" do
     expect(iface["ReadMe"]).to eq("READ ME")
   end
 
+  it "gets an error when reading a property whose implementation raises" do
+    expect { @iface["Explosive"] }.to raise_error(DBus::Error, /Something failed/)
+  end
+
   it "tests property nonreading" do
     expect { @iface["WriteMe"] }.to raise_error(DBus::Error, /not readable/)
   end
@@ -31,7 +45,7 @@ describe "PropertyTest" do
   end
 
   # https://github.com/mvidner/ruby-dbus/pull/19
-  it "tests service select timeout" do
+  it "tests service select timeout", slow: true do
     @iface["ReadOrWriteMe"] = "VALUE"
     expect(@iface["ReadOrWriteMe"]).to eq("VALUE")
     # wait for the service to become idle
@@ -46,7 +60,7 @@ describe "PropertyTest" do
 
   it "tests get all" do
     all = @iface.all_properties
-    expect(all.keys.sort).to eq(["ReadMe", "ReadOrWriteMe"])
+    expect(all.keys.sort).to eq(["MyArray", "MyByte", "MyDict", "MyStruct", "MyVariant", "ReadMe", "ReadOrWriteMe"])
   end
 
   it "tests get all on a V1 object" do
@@ -54,7 +68,7 @@ describe "PropertyTest" do
     iface = obj["org.ruby.SampleInterface"]
 
     all = iface.all_properties
-    expect(all.keys.sort).to eq(["ReadMe", "ReadOrWriteMe"])
+    expect(all.keys.sort).to eq(["MyArray", "MyByte", "MyDict", "MyStruct", "MyVariant", "ReadMe", "ReadOrWriteMe"])
   end
 
   it "tests unknown property reading" do
@@ -64,4 +78,177 @@ describe "PropertyTest" do
   it "tests unknown property writing" do
     expect { @iface["Spoon"] = "FPRK" }.to raise_error(DBus::Error, /not found/)
   end
+
+  it "errors for a property on an unknown interface" do
+    # our idiomatic way would error out on interface lookup already,
+    # so do it the low level way
+    prop_if = @obj[DBus::PROPERTY_INTERFACE]
+    expect { prop_if.Get("org.ruby.NoSuchInterface", "SomeProperty") }.to raise_error(DBus::Error) do |e|
+      expect(e.name).to match(/UnknownProperty/)
+      expect(e.message).to match(/no such interface/)
+    end
+  end
+
+  it "errors for GetAll on an unknown interface" do
+    # no idiomatic way?
+    # so do it the low level way
+    prop_if = @obj[DBus::PROPERTY_INTERFACE]
+    expect { prop_if.GetAll("org.ruby.NoSuchInterface") }.to raise_error(DBus::Error) do |e|
+      expect(e.name).to match(/UnknownProperty/)
+      expect(e.message).to match(/no such interface/)
+    end
+  end
+
+  it "receives a PropertiesChanged signal", slow: true do
+    received = {}
+
+    # TODO: for client side, provide a helper on_properties_changed,
+    # or automate it even more in ProxyObject, ProxyObjectInterface
+    prop_if = @obj[DBus::PROPERTY_INTERFACE]
+    prop_if.on_signal("PropertiesChanged") do |_interface_name, changed_props, _invalidated_props|
+      received.merge!(changed_props)
+    end
+
+    @iface["ReadOrWriteMe"] = "VALUE"
+    @iface.SetTwoProperties("REAMDE", 255)
+
+    # loop to process the signal. complicated :-( see signal_spec.rb
+    loop = DBus::Main.new
+    loop << @session_bus
+    quitter = Thread.new do
+      sleep 1
+      loop.quit
+    end
+    loop.run
+    # quitter has told loop.run to quit
+    quitter.join
+
+    expect(received["ReadOrWriteMe"]).to eq("VALUE")
+    expect(received["ReadMe"]).to eq("REAMDE")
+    expect(received["MyByte"]).to eq(255)
+  end
+
+  context "a struct-typed property" do
+    it "gets read as a struct, not an array (#97)" do
+      struct = @iface["MyStruct"]
+      expect(struct).to be_frozen
+    end
+
+    it "Get returns the correctly typed value (check with dbus-send)" do
+      # As big as the DBus::Data branch is,
+      # it still does not handle the :exact mode on the client/proxy side.
+      # So we resort to parsing dbus-send output.
+      cmd = "dbus-send --print-reply " \
+            "--dest=org.ruby.service " \
+            "/org/ruby/MyInstance " \
+            "org.freedesktop.DBus.Properties.Get " \
+            "string:org.ruby.SampleInterface " \
+            "string:MyStruct"
+      reply = `#{cmd}`
+      expect(reply).to match(/variant\s+struct {\s+string "three"\s+string "strings"\s+string "in a struct"\s+}/)
+    end
+
+    it "GetAll returns the correctly typed value (check with dbus-send)" do
+      cmd = "dbus-send --print-reply " \
+            "--dest=org.ruby.service " \
+            "/org/ruby/MyInstance " \
+            "org.freedesktop.DBus.Properties.GetAll " \
+            "string:org.ruby.SampleInterface "
+      reply = `#{cmd}`
+      expect(reply).to match(/variant\s+struct {\s+string "three"\s+string "strings"\s+string "in a struct"\s+}/)
+    end
+  end
+
+  context "an array-typed property" do
+    it "gets read as an array" do
+      val = @iface["MyArray"]
+      expect(val).to eq([42, 43])
+    end
+  end
+
+  context "a dict-typed property" do
+    it "gets read as a hash" do
+      val = @iface["MyDict"]
+      expect(val).to eq({
+                          "one" => 1,
+                          "two" => "dva",
+                          "three" => [3, 3, 3]
+                        })
+    end
+
+    it "Get returns the correctly typed value (check with dbus-send)" do
+      cmd = "dbus-send --print-reply " \
+            "--dest=org.ruby.service " \
+            "/org/ruby/MyInstance " \
+            "org.freedesktop.DBus.Properties.Get " \
+            "string:org.ruby.SampleInterface " \
+            "string:MyDict"
+      reply = `#{cmd}`
+      # a bug about variant nesting lead to a "variant variant int32 1" value
+      match_rx = /variant \s+ array \s \[ \s+
+         dict \s entry\( \s+
+            string \s "one" \s+
+            variant \s+ int32 \s 1 \s+
+         \)/x
+      expect(reply).to match(match_rx)
+    end
+  end
+
+  context "a variant-typed property" do
+    it "gets read at all" do
+      obj = @svc.object("/org/ruby/MyDerivedInstance")
+      iface = obj["org.ruby.SampleInterface"]
+      val = iface["MyVariant"]
+      expect(val).to eq([42, 43])
+    end
+  end
+
+  context "a byte-typed property" do
+    # Slightly advanced RSpec:
+    # https://rspec.info/documentation/3.9/rspec-expectations/RSpec/Matchers.html#satisfy-instance_method
+    let(:a_byte_in_a_variant) do
+      satisfying { |x| x.is_a?(DBus::Data::Variant) && x.member_type.to_s == DBus::Type::BYTE }
+      # ^ This formatting keeps the matcher on a single line
+      # which enables RSpec to cite it if it fails, instead of saying "block".
+    end
+
+    let(:prop_iface) { @obj[DBus::PROPERTY_INTERFACE] }
+
+    it "gets set with a correct type (#108)" do
+      expect(prop_iface).to receive(:Set).with(
+        "org.ruby.SampleInterface",
+        "MyByte",
+        a_byte_in_a_variant
+      )
+      @iface["MyByte"] = 1
+    end
+
+    it "gets set with a correct type (#108), when using the DBus.variant workaround" do
+      expect(prop_iface).to receive(:Set).with(
+        "org.ruby.SampleInterface",
+        "MyByte",
+        a_byte_in_a_variant
+      )
+      @iface["MyByte"] = DBus.variant("y", 1)
+    end
+  end
+
+  context "marshall.yaml round-trip via a VARIANT property" do
+    marshall_yaml.each do |test|
+      t = OpenStruct.new(test)
+      next if t.val.nil?
+
+      # Round trips do not work yet because the properties
+      # must present a plain Ruby value so the exact D-Bus type is lost.
+      # Round trips will work once users can declare accepting DBus::Data
+      # in properties and method arguments.
+      it "Sets #{t.sig.inspect}:#{t.val.inspect} and Gets something back" do
+        before = DBus::Data.make_typed(t.sig, t.val)
+        expect { @iface["MyVariant"] = before }.to_not raise_error
+        expect { _after = @iface["MyVariant"] }.to_not raise_error
+        # round-trip:
+        # expect(after).to eq(before.value)
+      end
+    end
+  end
 end
diff --git a/spec/proxy_object_interface_spec.rb b/spec/proxy_object_interface_spec.rb
new file mode 100755
index 0000000..4861f75
--- /dev/null
+++ b/spec/proxy_object_interface_spec.rb
@@ -0,0 +1,35 @@
+#!/usr/bin/env rspec
+# frozen_string_literal: true
+
+require_relative "spec_helper"
+require "dbus"
+
+describe DBus::ProxyObjectInterface do
+  # TODO: tag tests that need a service, eg "needs-service"
+  # TODO: maybe remove this and rely on a packaged tool
+  around(:each) do |example|
+    with_private_bus do
+      with_service_by_activation(&example)
+    end
+  end
+
+  let(:bus) { DBus::ASessionBus.new }
+
+  context "when calling org.ruby.service" do
+    let(:svc) { bus["org.ruby.service"] }
+
+    # This is white box testing, knowing the implementation
+    # A better way would be structuring it according to the D-Bus Spec
+    # Or testing the service side doing the right thing? (What if our bugs cancel out)
+    describe "#define_method_from_descriptor" do
+      it "can call a method with multiple OUT arguments" do
+        obj = svc["/org/ruby/MyInstance"]
+        ifc = obj["org.ruby.SampleInterface"]
+
+        even, odd = ifc.EvenOdd([3, 1, 4, 1, 5, 9, 2, 6])
+        expect(even).to eq [4, 2, 6]
+        expect(odd).to eq [3, 1, 1, 5, 9]
+      end
+    end
+  end
+end
diff --git a/spec/proxy_object_spec.rb b/spec/proxy_object_spec.rb
index ff7aa44..5f064e2 100755
--- a/spec/proxy_object_spec.rb
+++ b/spec/proxy_object_spec.rb
@@ -1,4 +1,6 @@
 #!/usr/bin/env rspec
+# frozen_string_literal: true
+
 require_relative "spec_helper"
 require "dbus"
 
diff --git a/spec/raw_message_spec.rb b/spec/raw_message_spec.rb
new file mode 100755
index 0000000..99b477a
--- /dev/null
+++ b/spec/raw_message_spec.rb
@@ -0,0 +1,32 @@
+#!/usr/bin/env rspec
+# frozen_string_literal: true
+
+require_relative "spec_helper"
+require "dbus"
+
+# Pedantic full coverage test.
+# The happy paths are covered via calling classes
+describe DBus::RawMessage do
+  describe ".endianness" do
+    it "returns :little for 'l'" do
+      expect(described_class.endianness("l")).to eq :little
+    end
+
+    it "returns :big for 'B'" do
+      expect(described_class.endianness("B")).to eq :big
+    end
+
+    it "raises for other strings" do
+      expect { described_class.endianness("m") }
+        .to raise_error(DBus::InvalidPacketException, /Incorrect endianness/)
+    end
+  end
+
+  describe "#align" do
+    it "raises for values other than 1 2 4 8" do
+      subject = described_class.new("l")
+      expect { subject.align(3) }.to raise_error(ArgumentError)
+      expect { subject.align(16) }.to raise_error(ArgumentError)
+    end
+  end
+end
diff --git a/spec/server_robustness_spec.rb b/spec/server_robustness_spec.rb
index 6fa4c20..f65421f 100755
--- a/spec/server_robustness_spec.rb
+++ b/spec/server_robustness_spec.rb
@@ -1,4 +1,6 @@
 #!/usr/bin/env rspec
+# frozen_string_literal: true
+
 # Test that a server survives various error cases
 require_relative "spec_helper"
 require "dbus"
@@ -14,7 +16,7 @@ describe "ServerRobustnessTest" do
   it "tests no such path with introspection" do
     obj = @svc.object "/org/ruby/NotMyInstance"
     expect { obj.introspect }.to raise_error(DBus::Error) do |e|
-      expect(e).to_not match(/timeout/)
+      expect(e.message).to_not match(/timeout/)
     end
   end
 
@@ -23,7 +25,19 @@ describe "ServerRobustnessTest" do
     ifc = DBus::ProxyObjectInterface.new(obj, "org.ruby.SampleInterface")
     ifc.define_method("the_answer", "out n:i")
     expect { ifc.the_answer }.to raise_error(DBus::Error) do |e|
-      expect(e).to_not match(/timeout/)
+      expect(e.message).to_not match(/timeout/)
+    end
+  end
+
+  context "an existing path without an object" do
+    let(:obj) { @svc.object "/org" }
+
+    it "errors without a timeout" do
+      ifc = DBus::ProxyObjectInterface.new(obj, "org.ruby.SampleInterface")
+      ifc.define_method("the_answer", "out n:i")
+      expect { ifc.the_answer }.to raise_error(DBus::Error) do |e|
+        expect(e.message).to_not match(/timeout/)
+      end
     end
   end
 
@@ -31,7 +45,7 @@ describe "ServerRobustnessTest" do
     obj = @svc.object "/org/ruby/MyInstance"
     obj.default_iface = "org.ruby.SampleInterface"
     expect { obj.will_raise }.to raise_error(DBus::Error) do |e|
-      expect(e).to_not match(/timeout/)
+      expect(e.message).to_not match(/timeout/)
     end
   end
 
@@ -39,7 +53,7 @@ describe "ServerRobustnessTest" do
     obj = @svc.object "/org/ruby/MyInstance"
     obj.default_iface = "org.ruby.SampleInterface"
     expect { obj.will_raise_name_error }.to raise_error(DBus::Error) do |e|
-      expect(e).to_not match(/timeout/)
+      expect(e.message).to_not match(/timeout/)
     end
   end
 
@@ -49,7 +63,7 @@ describe "ServerRobustnessTest" do
     ifc = DBus::ProxyObjectInterface.new(obj, "org.ruby.SampleInterface")
     ifc.define_method("not_the_answer", "out n:i")
     expect { ifc.not_the_answer }.to raise_error(DBus::Error) do |e|
-      expect(e).to_not match(/timeout/)
+      expect(e.message).to_not match(/timeout/)
     end
   end
 
@@ -58,7 +72,7 @@ describe "ServerRobustnessTest" do
     ifc = DBus::ProxyObjectInterface.new(obj, "org.ruby.NoSuchInterface")
     ifc.define_method("the_answer", "out n:i")
     expect { ifc.the_answer }.to raise_error(DBus::Error) do |e|
-      expect(e).to_not match(/timeout/)
+      expect(e.message).to_not match(/timeout/)
     end
   end
 end
diff --git a/spec/server_spec.rb b/spec/server_spec.rb
index 0d8ab41..9886dde 100755
--- a/spec/server_spec.rb
+++ b/spec/server_spec.rb
@@ -1,4 +1,6 @@
 #!/usr/bin/env rspec
+# frozen_string_literal: true
+
 # Test that a server survives various error cases
 require_relative "spec_helper"
 require "dbus"
diff --git a/spec/service_newapi.rb b/spec/service_newapi.rb
index 537b83e..77edf1b 100755
--- a/spec/service_newapi.rb
+++ b/spec/service_newapi.rb
@@ -1,25 +1,57 @@
 #!/usr/bin/env ruby
-# -*- coding: utf-8 -*-
+# frozen_string_literal: true
 
 require_relative "spec_helper"
 SimpleCov.command_name "Service Tests" if Object.const_defined? "SimpleCov"
 # find the library without external help
-$LOAD_PATH.unshift File.expand_path("../../lib", __FILE__)
+$LOAD_PATH.unshift File.expand_path("../lib", __dir__)
 
 require "dbus"
 
-PROPERTY_INTERFACE = "org.freedesktop.DBus.Properties".freeze
+PROPERTY_INTERFACE = "org.freedesktop.DBus.Properties"
+
+class TestChild < DBus::Object
+  def initialize(opath)
+    @name = opath.split("/").last.capitalize
+    super
+  end
+
+  dbus_interface "org.ruby.TestChild" do
+    dbus_attr_reader :name, "s"
+  end
+end
 
 class Test < DBus::Object
-  INTERFACE = "org.ruby.SampleInterface".freeze
+  Point2D = Struct.new(:x, :y)
+
+  attr_writer :main_loop
+
+  include DBus::ObjectManager
+
+  INTERFACE = "org.ruby.SampleInterface"
   def initialize(path)
     super path
     @read_me = "READ ME"
     @read_or_write_me = "READ OR WRITE ME"
+    @my_struct = ["three", "strings", "in a struct"].freeze
+    @my_array = [42, 43]
+    @my_dict = {
+      "one" => 1,
+      "two" => "dva",
+      "three" => [3, 3, 3]
+    }
+    @my_variant = @my_array.dup
+    # 201 is a RET instruction for ZX Spectrum which has turned 40 recently
+    @my_byte = 201
+    @main_loop = nil
   end
 
   # Create an interface aggregating all upcoming dbus_method defines.
   dbus_interface INTERFACE do
+    dbus_method :quit, "" do
+      @main_loop&.quit
+    end
+
     dbus_method :hello, "in name:s, in name2:s" do |name, name2|
       puts "hello(#{name}, #{name2})"
     end
@@ -59,6 +91,58 @@ class Test < DBus::Object
     dbus_method :mirror_byte_array, "in bytes:ay, out mirrored:ay" do |bytes|
       [bytes]
     end
+
+    dbus_method :Coordinates, "out coords:(dd)" do
+      coords = [3.0, 4.0].freeze
+      [coords]
+    end
+
+    dbus_method :Coordinates2, "out coords:(dd)" do
+      coords = Point2D.new(5.0, 12.0)
+      [coords]
+    end
+
+    # Two OUT arguments
+    dbus_method :EvenOdd, "in numbers:ai, out even:ai, out odd:ai" do |numbers|
+      even, odd = numbers.partition(&:even?)
+      [even, odd]
+    end
+
+    # Properties:
+    # ReadMe:string, returns "READ ME" at first, then what WriteMe received
+    # WriteMe:string
+    # ReadOrWriteMe:string, returns "READ OR WRITE ME" at first
+    dbus_attr_accessor :read_or_write_me, "s"
+    dbus_attr_reader :read_me, "s"
+
+    def write_me=(value)
+      @read_me = value
+    end
+    dbus_writer :write_me, "s"
+
+    dbus_attr_writer :password, "s"
+
+    # a property that raises when client tries to read it
+    def explosive
+      raise "Something failed"
+    end
+    dbus_reader :explosive, "s"
+
+    dbus_attr_accessor :my_struct, "(sss)"
+    dbus_attr_accessor :my_array, "aq"
+    dbus_attr_accessor :my_dict, "a{sv}"
+    dbus_attr_accessor :my_variant, "v"
+
+    dbus_attr_accessor :my_byte, "y"
+
+    # to test dbus_properties_changed
+    dbus_method :SetTwoProperties, "in read_me:s, in byte:y" do |read_me, byte|
+      @read_me = read_me
+      @my_byte = byte
+      dbus_properties_changed(INTERFACE,
+                              { "ReadMe" => read_me, "MyByte" => byte },
+                              [])
+    end
   end
 
   # closing and reopening the same interface
@@ -90,6 +174,21 @@ class Test < DBus::Object
     end
   end
 
+  dbus_interface "org.ruby.TestParent" do
+    dbus_method :New, "in name:s, out opath:o" do |name|
+      child = TestChild.new("#{path}/#{name}")
+      @service.export(child)
+      [child.path]
+    end
+
+    dbus_method :Delete, "in opath:o" do |opath|
+      raise ArgumentError unless opath.start_with?(path)
+
+      obj = @service.get_node(opath)&.object
+      @service.unexport(obj)
+    end
+  end
+
   dbus_interface "org.ruby.Duplicates" do
     dbus_method :the_answer, "out answer:i" do
       [0]
@@ -118,72 +217,6 @@ class Test < DBus::Object
     dbus_signal :LongTaskStart
     dbus_signal :LongTaskEnd
   end
-
-  # Properties:
-  # ReadMe:string, returns "READ ME" at first, then what WriteMe received
-  # WriteMe:string
-  # ReadOrWriteMe:string, returns "READ OR WRITE ME" at first
-  dbus_interface PROPERTY_INTERFACE do
-    dbus_method :Get, "in interface:s, in propname:s, out value:v" do |interface, propname|
-      unless interface == INTERFACE
-        raise DBus.error("org.freedesktop.DBus.Error.UnknownInterface"),
-              "Interface '#{interface}' not found on object '#{@path}'"
-      end
-
-      case propname
-      when "ReadMe"
-        [@read_me]
-      when "ReadOrWriteMe"
-        [@read_or_write_me]
-      when "WriteMe"
-        raise DBus.error("org.freedesktop.DBus.Error.InvalidArgs"),
-              "Property '#{interface}.#{propname}' (on object '#{@path}') is not readable"
-      else
-        # what should happen for unknown properties
-        # plasma: InvalidArgs (propname), UnknownInterface (interface)
-        raise DBus.error("org.freedesktop.DBus.Error.InvalidArgs"),
-              "Property '#{interface}.#{propname}' not found on object '#{@path}'"
-      end
-    end
-
-    dbus_method :Set, "in interface:s, in propname:s, in  value:v" do |interface, propname, value|
-      unless interface == INTERFACE
-        raise DBus.error("org.freedesktop.DBus.Error.UnknownInterface"),
-              "Interface '#{interface}' not found on object '#{@path}'"
-      end
-
-      case propname
-      when "ReadMe"
-        raise DBus.error("org.freedesktop.DBus.Error.InvalidArgs"),
-              "Property '#{interface}.#{propname}' (on object '#{@path}') is not writable"
-      when "ReadOrWriteMe"
-        @read_or_write_me = value
-        self.PropertiesChanged(interface, { propname => value }, [])
-      when "WriteMe"
-        @read_me = value
-        self.PropertiesChanged(interface, { "ReadMe" => value }, [])
-      else
-        raise DBus.error("org.freedesktop.DBus.Error.InvalidArgs"),
-              "Property '#{interface}.#{propname}' not found on object '#{@path}'"
-      end
-    end
-
-    dbus_method :GetAll, "in interface:s, out value:a{sv}" do |interface|
-      unless interface == INTERFACE
-        raise DBus.error("org.freedesktop.DBus.Error.UnknownInterface"),
-              "Interface '#{interface}' not found on object '#{@path}'"
-      end
-
-      [
-        {
-          "ReadMe" => @read_me,
-          "ReadOrWriteMe" => @read_or_write_me
-        }
-      ]
-    end
-
-    dbus_signal :PropertiesChanged, "interface:s, changed_properties:a{sv}, invalidated_properties:as"
-  end
 end
 
 class Derived < Test
@@ -224,6 +257,7 @@ end
 puts "listening, with ruby-#{RUBY_VERSION}"
 main = DBus::Main.new
 main << bus
+myobj.main_loop = main
 begin
   main.run
 rescue SystemCallError
diff --git a/spec/service_spec.rb b/spec/service_spec.rb
new file mode 100755
index 0000000..dfbda61
--- /dev/null
+++ b/spec/service_spec.rb
@@ -0,0 +1,18 @@
+#!/usr/bin/env rspec
+# frozen_string_literal: true
+
+require_relative "spec_helper"
+require "dbus"
+
+describe "DBus::Service (server role)" do
+  let(:bus) { DBus::ASessionBus.new }
+  # This is the client role, but the server role API is bad
+  # and for the one test there is no difference
+  let(:service) { bus["org.ruby.service"] }
+
+  describe "#descendants_for" do
+    it "raises for not existing path" do
+      expect { service.descendants_for("/notthere") }.to raise_error(ArgumentError)
+    end
+  end
+end
diff --git a/spec/session_bus_spec.rb b/spec/session_bus_spec.rb
index 3a0cc1d..fc987d9 100755
--- a/spec/session_bus_spec.rb
+++ b/spec/session_bus_spec.rb
@@ -1,7 +1,26 @@
 #!/usr/bin/env rspec
+# frozen_string_literal: true
+
 require_relative "spec_helper"
 require "dbus"
 
+describe DBus::ASystemBus do
+  describe "#initialize" do
+    it "will use DBUS_SYSTEM_BUS_ADDRESS or the well known address" do
+      expect(ENV)
+        .to receive(:[])
+        .with("DBUS_SYSTEM_BUS_ADDRESS")
+        .and_return(nil)
+      expect(DBus::MessageQueue)
+        .to receive(:new)
+        .with("unix:path=/var/run/dbus/system_bus_socket")
+      expect_any_instance_of(described_class).to receive(:send_hello)
+
+      described_class.new
+    end
+  end
+end
+
 describe DBus::ASessionBus do
   subject(:dbus_session_bus_address) { "unix:abstract=/tmp/dbus-foo,guid=123" }
 
@@ -16,6 +35,22 @@ describe DBus::ASessionBus do
       ENV["DBUS_SESSION_BUS_ADDRESS"] = dbus_session_bus_address
       expect(DBus::ASessionBus.session_bus_address).to eq(dbus_session_bus_address)
     end
+
+    it "uses launchd on macOS when ENV and file fail" do
+      ENV["DBUS_SESSION_BUS_ADDRESS"] = nil
+      expect(described_class).to receive(:address_from_file).and_return(nil)
+      expect(DBus::Platform).to receive(:macos?).and_return(true)
+
+      expect(described_class.session_bus_address).to start_with "launchd:"
+    end
+
+    it "raises a readable exception when all addresses fail" do
+      ENV["DBUS_SESSION_BUS_ADDRESS"] = nil
+      expect(described_class).to receive(:address_from_file).and_return(nil)
+      expect(DBus::Platform).to receive(:macos?).and_return(false)
+
+      expect { described_class.session_bus_address }.to raise_error(NotImplementedError, /Cannot find session bus/)
+    end
   end
 
   describe "#address_from_file" do
@@ -23,7 +58,7 @@ describe DBus::ASessionBus do
 
     before do
       # mocks of files for address_from_file method
-      machine_id_path = File.expand_path("/etc/machine-id", __FILE__)
+      machine_id_path = File.expand_path("/etc/machine-id", __dir__)
       expect(Dir).to receive(:[]).with(any_args) { [machine_id_path] }
       expect(File).to receive(:read).with(machine_id_path) { "baz" }
       expect(File).to receive(:exist?).with(session_bus_file_path) { true }
diff --git a/spec/session_bus_spec_manual.rb b/spec/session_bus_spec_manual.rb
index 60fd4d2..4374c8f 100755
--- a/spec/session_bus_spec_manual.rb
+++ b/spec/session_bus_spec_manual.rb
@@ -1,4 +1,6 @@
 #!/usr/bin/env rspec
+# frozen_string_literal: true
+
 require_relative "spec_helper"
 require "dbus"
 
diff --git a/spec/signal_spec.rb b/spec/signal_spec.rb
index d160ec2..7e449d5 100755
--- a/spec/signal_spec.rb
+++ b/spec/signal_spec.rb
@@ -1,4 +1,6 @@
 #!/usr/bin/env rspec
+# frozen_string_literal: true
+
 # Test the signal handlers
 require_relative "spec_helper"
 require "dbus"
@@ -29,7 +31,7 @@ describe "SignalHandlerTest" do
   end
 
   # testing for commit 017c83 (kkaempf)
-  it "tests overriding a handler" do
+  it "tests overriding a handler", slow: true do
     DBus.logger.debug "Inside test_overriding_a_handler"
     counter = 0
 
@@ -52,7 +54,7 @@ describe "SignalHandlerTest" do
     expect(counter).to eq(1)
   end
 
-  it "tests on signal overload" do
+  it "tests on signal overload", slow: true do
     DBus.logger.debug "Inside test_on_signal_overload"
     counter = 0
     started = false
@@ -74,7 +76,7 @@ describe "SignalHandlerTest" do
     expect { @intf.on_signal "to", "many", "yarrrrr!" }.to raise_error(ArgumentError)
   end
 
-  it "is possible to add signal handlers from within handlers" do
+  it "is possible to add signal handlers from within handlers", slow: true do
     ended = false
     @intf.on_signal "LongTaskStart" do
       @intf.on_signal "LongTaskEnd" do
@@ -101,4 +103,14 @@ describe "SignalHandlerTest" do
   it "tests removing a nonexistent rule" do
     @obj.on_signal "DoesNotExist"
   end
+
+  describe DBus::ProxyObject do
+    describe "#on_signal" do
+      it "raises a descriptive error when the default_iface is wrong" do
+        @obj.default_iface = "org.ruby.NoSuchInterface"
+        expect { @obj.on_signal("Foo") {} }
+          .to raise_error(NoMethodError, /undefined signal.*interface `org.ruby.NoSuchInterface'/)
+      end
+    end
+  end
 end
diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb
index 73aa847..92c8640 100644
--- a/spec/spec_helper.rb
+++ b/spec/spec_helper.rb
@@ -1,28 +1,44 @@
+# frozen_string_literal: true
+
 coverage = if ENV["COVERAGE"]
              ENV["COVERAGE"] == "true"
            else
              # heuristics: enable for interactive builds (but not in OBS)
-             # or in Travis
-             ENV["DISPLAY"] || ENV["TRAVIS"]
+             ENV["DISPLAY"]
            end
 
 if coverage
   require "simplecov"
-  SimpleCov.root File.expand_path("../..", __FILE__)
+  SimpleCov.root File.expand_path("..", __dir__)
 
   # do not cover specs
   SimpleCov.add_filter "_spec.rb"
   # do not cover the activesupport helpers
   SimpleCov.add_filter "/core_ext/"
+  # measure all if/else branches on a line
+  SimpleCov.enable_coverage :branch
 
-  # use coveralls for on-line code coverage reporting at Travis CI
-  if ENV["TRAVIS"]
-    require "coveralls"
-  end
   SimpleCov.start
+
+  # additionally use the LCOV format for on-line code coverage reporting at CI
+  if ENV["COVERAGE_LCOV"] == "true"
+    require "simplecov-lcov"
+
+    SimpleCov::Formatter::LcovFormatter.config do |c|
+      c.report_with_single_file = true
+      # this is the default Coveralls GitHub Action location
+      # https://github.com/marketplace/actions/coveralls-github-action
+      c.single_report_path = "coverage/lcov.info"
+    end
+
+    SimpleCov.formatter = SimpleCov::Formatter::MultiFormatter.new [
+      SimpleCov::Formatter::HTMLFormatter,
+      SimpleCov::Formatter::LcovFormatter
+    ]
+  end
 end
 
-$LOAD_PATH.unshift File.expand_path("../../lib", __FILE__)
+$LOAD_PATH.unshift File.expand_path("../lib", __dir__)
 
 if Object.const_defined? "RSpec"
   # http://betterspecs.org/#expect
@@ -36,7 +52,7 @@ end
 require "tempfile"
 require "timeout"
 
-TOPDIR = File.expand_path("../..", __FILE__)
+TOPDIR = File.expand_path("..", __dir__)
 
 # path of config file for a private bus
 def config_file_path
@@ -104,3 +120,15 @@ def with_service_by_activation(&block)
 
   system "pkill -f #{exec}"
 end
+
+# Make a binary string from readable YAML pieces; see data/marshall.yaml
+def buffer_from_yaml(parts)
+  strings = parts.flatten.map do |part|
+    if part.is_a? Integer
+      part.chr
+    else
+      part
+    end
+  end
+  strings.join.force_encoding(Encoding::BINARY)
+end
diff --git a/spec/thread_safety_spec.rb b/spec/thread_safety_spec.rb
index b731063..9fd1b9e 100755
--- a/spec/thread_safety_spec.rb
+++ b/spec/thread_safety_spec.rb
@@ -1,30 +1,75 @@
 #!/usr/bin/env rspec
+# frozen_string_literal: true
+
 # Test thread safety
 require_relative "spec_helper"
 require "dbus"
 
-describe "ThreadSafetyTest" do
-  it "tests thread competition" do
-    print "Thread competition: "
-    jobs = []
-    5.times do
-      jobs << Thread.new do
-        Thread.current.abort_on_exception = true
+class TestSignalRace < DBus::Object
+  dbus_interface "org.ruby.ServerTest" do
+    dbus_signal :signal_without_arguments
+  end
+end
+
+# Run *count* threads all doing *body*, wait for their finish
+def race_threads(count, &body)
+  jobs = count.times.map do |j|
+    Thread.new do
+      Thread.current.abort_on_exception = true
+
+      body.call(j)
+    end
+  end
+  jobs.each(&:join)
+end
+
+# Repeat *count* times: { random sleep, *body* }, printing progress
+def repeat_with_jitter(count, &body)
+  count.times do |i|
+    sleep 0.1 * rand
+    print "#{i} "
+    $stdout.flush
+
+    body.call
+  end
+end
 
+describe "thread safety" do
+  context "R/W: when the threads call methods with return values" do
+    it "it works with separate bus connections" do
+      race_threads(5) do |_j|
         # use separate connections to avoid races
         bus = DBus::ASessionBus.new
         svc = bus.service("org.ruby.service")
         obj = svc.object("/org/ruby/MyInstance")
         obj.default_iface = "org.ruby.SampleInterface"
 
-        10.times do |i|
-          print "#{i} "
-          $stdout.flush
+        repeat_with_jitter(10) do
           expect(obj.the_answer[0]).to eq(42)
-          sleep 0.1 * rand
         end
       end
+      puts
+    end
+  end
+
+  context "W/O: when the threads only send signals" do
+    it "it works with a shared separate bus connection" do
+      race_threads(5) do |j|
+        # shared connection
+        bus = DBus::SessionBus.instance
+        # hackish: we do not actually request the name
+        svc = DBus::Service.new("org.ruby.server-test#{j}", bus)
+
+        obj = TestSignalRace.new "/org/ruby/Foo"
+        svc.export obj
+
+        repeat_with_jitter(10) do
+          obj.signal_without_arguments
+        end
+
+        svc.unexport(obj)
+      end
+      puts
     end
-    jobs.each(&:join)
   end
 end
diff --git a/spec/tools/dbus-launch-simple b/spec/tools/dbus-launch-simple
index 33d48c2..5925366 100755
--- a/spec/tools/dbus-launch-simple
+++ b/spec/tools/dbus-launch-simple
@@ -16,9 +16,9 @@ my_dbus_launch () {
     # wait for the daemon to print the info
     TRIES=0
     while [ ! -s $AF -o ! -s $PF ]; do
-	sleep 0.1
-	TRIES=`expr $TRIES + 1`
-	if [ $TRIES -gt 100 ]; then echo "dbus-daemon failed?"; exit 1; fi
+        sleep 0.1
+        TRIES=`expr $TRIES + 1`
+        if [ $TRIES -gt 100 ]; then echo "dbus-daemon failed?"; exit 1; fi
     done
     DBUS_SESSION_BUS_PID=$(cat $PF)
     export DBUS_SESSION_BUS_ADDRESS=$(cat $AF)
@@ -26,10 +26,10 @@ my_dbus_launch () {
 #    dbus-monitor &
 }
 
-my_dbus_launch
-
 # Clean up at exit.
 trap "kill \$KILLS; rm -rf \$RM_FILES" EXIT TERM INT
 
+my_dbus_launch
+
 # run the payload; the return value is passed on
 "$@"
diff --git a/spec/tools/dbus-limited-session.conf b/spec/tools/dbus-limited-session.conf
index 3ade98b..c4ee928 100644
--- a/spec/tools/dbus-limited-session.conf
+++ b/spec/tools/dbus-limited-session.conf
@@ -7,7 +7,31 @@
   <!-- Our well-known bus type, don't change this -->
   <type>session</type>
 
+  <!-- Authentication:
+       This was useful during refactoring, but meanwhile RSpec mocking has
+       replaced it. -->
+  <!-- Explicitly list all known authentication mechanisms,
+       their order is not important.
+       By default the daemon allows all but this lets me disable some. -->
+  <auth>EXTERNAL</auth>
+  <auth>DBUS_COOKIE_SHA1</auth>
+  <auth>ANONYMOUS</auth>
+  <!-- Insecure, other users could call us and exploit debug APIs/bugs -->
+  <!--
+  <allow_anonymous/>
+  -->
+
+  <!-- Give clients a variety of addresses to connect to -->
   <listen>unix:tmpdir=/tmp</listen>
+  <listen>unix:dir=/tmp</listen>
+  <!-- runtime will happily steal the actual session bus! -->
+  <!--
+  <listen>unix:runtime=yes</listen>
+  -->
+  <!-- openSUSE Build Service does not set up IPv6 at build time -->
+  <!--
+  <listen>tcp:host=%3a%3a1,family=ipv6</listen>
+  -->
   <listen>tcp:host=127.0.0.1</listen>
 
   <standard_session_servicedirs />
@@ -24,5 +48,10 @@
   <!-- Do not increase the limits.
        Instead, lower some so that we can test resource leaks. -->
   <limit name="max_match_rules_per_connection">50</limit><!-- was 512 -->
+  <limit name="reply_timeout">5000</limit><!-- 5 seconds -->
 
+  <!--
+dbus-daemon[1700]: [session uid=1001 pid=1700] Unable to set up new connection: Failed to get AppArmor confinement information of socket peer: Protocol not available
+  -->
+  <apparmor mode="disabled"/>
 </busconfig>
diff --git a/spec/type_spec.rb b/spec/type_spec.rb
index 132c21a..369cf19 100755
--- a/spec/type_spec.rb
+++ b/spec/type_spec.rb
@@ -1,18 +1,226 @@
 #!/usr/bin/env rspec
+# frozen_string_literal: true
+
 require_relative "spec_helper"
 require "dbus"
 
 describe DBus do
   describe ".type" do
-    ["i", "ai", "a(ii)", "aai"].each do |s|
-      it "parses some type #{s}" do
-        expect(DBus.type(s).to_s).to be_eql s
+    good = [
+      "i",
+      "ai",
+      "a(ii)",
+      "aai"
+    ]
+
+    context "valid single types" do
+      good.each do |s|
+        it "#{s.inspect} is parsed" do
+          expect(DBus.type(s).to_s).to eq(s)
+        end
+      end
+    end
+
+    bad = [
+      ["\x00", "Unknown type code"],
+      ["!", "Unknown type code"],
+
+      # ARRAY related
+      ["a", "Empty ARRAY"],
+      ["aa", "Empty ARRAY"],
+
+      # STRUCT related
+      ["r", "Abstract STRUCT"],
+      ["()", "Empty STRUCT"],
+      ["(ii", "STRUCT not closed"],
+      ["a{i)", "STRUCT unexpectedly closed"],
+
+      # TODO: deep nesting arrays, structs, combined
+
+      # DICT_ENTRY related
+      ["e", "Abstract DICT_ENTRY"],
+      ["a{}", "DICT_ENTRY must have 2 subtypes, found 0"],
+      ["a{s}", "DICT_ENTRY must have 2 subtypes, found 1"],
+      ["a{sss}", "DICT_ENTRY must have 2 subtypes, found 3"],
+      ["a{vs}", "DICT_ENTRY key must be basic (non-container)"],
+      ["{sv}", "DICT_ENTRY not an immediate child of an ARRAY"],
+      ["a({sv})", "DICT_ENTRY not an immediate child of an ARRAY"],
+      ["a{s", "DICT_ENTRY not closed"],
+      ["a{sv", "DICT_ENTRY not closed"],
+      ["}", "DICT_ENTRY unexpectedly closed"],
+
+      # Too long
+      ["(#{"y" * 254})", "longer than 255"],
+
+      # not Single Complete Types
+      ["", "expecting a Single Complete Type"],
+      ["ii", "more than a Single Complete Type"]
+    ]
+    context "invalid single types" do
+      bad.each.each do |s, msg|
+        it "#{s.inspect} raises an exception mentioning: #{msg}" do
+          rx = Regexp.new(Regexp.quote(msg))
+          expect { DBus.type(s) }.to raise_error(DBus::Type::SignatureException, rx)
+        end
+      end
+    end
+  end
+
+  describe ".types" do
+    good = [
+      "",
+      "ii"
+    ]
+
+    context "valid signatures" do
+      good.each do |s|
+        it "#{s.inspect} is parsed" do
+          expect(DBus.types(s).map(&:to_s).join).to eq(s)
+        end
       end
     end
+  end
+
+  describe DBus::Type do
+    let(:as1) { DBus.type("as") }
+    let(:as2) { DBus.type("as") }
+    let(:aas) { DBus.type("aas") }
+
+    describe "#==" do
+      it "is true for same types" do
+        expect(as1).to eq(as2)
+      end
+
+      it "is true for a type and its string representation" do
+        expect(as1).to eq("as")
+      end
+
+      it "is false for different types" do
+        expect(as1).to_not eq(aas)
+      end
+
+      it "is false for a type and a different string" do
+        expect(as1).to_not eq("aas")
+      end
+    end
+
+    describe "#eql?" do
+      it "is true for same types" do
+        expect(as1).to eql(as2)
+      end
+
+      it "is false for a type and its string representation" do
+        expect(as1).to_not eql("as")
+      end
+
+      it "is false for different types" do
+        expect(as1).to_not eql(aas)
+      end
+
+      it "is false for a type and a different string" do
+        expect(as1).to_not eql("aas")
+      end
+    end
+
+    describe "#<<" do
+      it "raises if the argument is not a Type" do
+        t = DBus::Type.new(DBus::Type::ARRAY)
+        expect { t << "s" }.to raise_error(ArgumentError)
+      end
+
+      # TODO: the following raise checks do not occur in practice, as there are
+      # parallel checks in the parses. The code could be simplified?
+      it "raises if adding too much to an array" do
+        t = DBus::Type.new(DBus::Type::ARRAY)
+        b = DBus::Type.new(DBus::Type::BOOLEAN)
+        t << b
+        expect { t << b }.to raise_error(DBus::Type::SignatureException)
+      end
+
+      it "raises if adding too much to a dict_entry" do
+        t = DBus::Type.new(DBus::Type::DICT_ENTRY, abstract: true)
+        b = DBus::Type.new(DBus::Type::BOOLEAN)
+        t << b
+        t << b
+        expect { t << b }.to raise_error(DBus::Type::SignatureException)
+      end
+
+      it "raises if adding to a non-container" do
+        t = DBus::Type.new(DBus::Type::STRING)
+        b = DBus::Type.new(DBus::Type::BOOLEAN)
+        expect { t << b }.to raise_error(DBus::Type::SignatureException)
+
+        t = DBus::Type.new(DBus::Type::VARIANT)
+        expect { t << b }.to raise_error(DBus::Type::SignatureException)
+      end
+    end
+
+    describe DBus::Type::Array do
+      describe ".[]" do
+        it "takes Type argument" do
+          t = DBus::Type::Array[DBus::Type.new("s")]
+          expect(t.to_s).to eq "as"
+        end
+
+        it "takes 's':String argument" do
+          t = DBus::Type::Array["s"]
+          expect(t.to_s).to eq "as"
+        end
+
+        it "takes String:Class argument" do
+          t = DBus::Type::Array[String]
+          expect(t.to_s).to eq "as"
+        end
+
+        it "rejects Integer:Class argument" do
+          expect { DBus::Type::Array[Integer] }.to raise_error(ArgumentError)
+        end
+
+        it "rejects /./:Regexp argument" do
+          expect { DBus::Type::Array[/./] }.to raise_error(ArgumentError)
+        end
+      end
+    end
+
+    describe DBus::Type::Hash do
+      describe ".[]" do
+        it "takes Type arguments" do
+          t = DBus::Type::Hash[DBus::Type.new("s"), DBus::Type.new("v")]
+          expect(t.to_s).to eq "a{sv}"
+        end
+
+        it "takes 's':String arguments" do
+          t = DBus::Type::Hash["s", "v"]
+          expect(t.to_s).to eq "a{sv}"
+        end
+
+        it "takes String:Class argument" do
+          t = DBus::Type::Hash[String, DBus::Type::VARIANT]
+          expect(t.to_s).to eq "a{sv}"
+        end
+      end
+    end
+
+    describe DBus::Type::Struct do
+      describe ".[]" do
+        it "takes Type arguments" do
+          t = DBus::Type::Struct[DBus::Type.new("s"), DBus::Type.new("v")]
+          expect(t.to_s).to eq "(sv)"
+        end
+
+        it "takes 's':String arguments" do
+          t = DBus::Type::Struct["s", "v"]
+          expect(t.to_s).to eq "(sv)"
+        end
+
+        it "takes String:Class argument" do
+          t = DBus::Type::Struct[String, DBus::Type::VARIANT]
+          expect(t.to_s).to eq "(sv)"
+        end
 
-    ["aa", "(ii", "ii)", "hrmp"].each do |s|
-      it "raises exception for invalid type #{s}" do
-        expect { DBus.type(s).to_s }.to raise_error DBus::Type::SignatureException
+        it "raises on no arguments" do
+          expect { DBus::Type::Struct[] }.to raise_error(ArgumentError)
+        end
       end
     end
   end
diff --git a/spec/value_spec.rb b/spec/value_spec.rb
index f78f221..8204201 100755
--- a/spec/value_spec.rb
+++ b/spec/value_spec.rb
@@ -1,5 +1,6 @@
 #!/usr/bin/env rspec
-# -*- coding: utf-8 -*-
+# frozen_string_literal: true
+
 require_relative "spec_helper"
 require "dbus"
 
@@ -91,4 +92,18 @@ describe "ValueTest" do
   it "aligns short integers correctly" do
     expect(@obj.i16_plus(10, -30)[0]).to eq(-20)
   end
+
+  context "structs" do
+    it "they are returned as FROZEN arrays" do
+      struct = @obj.Coordinates[0]
+      expect(struct).to be_an(Array)
+      expect(struct).to be_frozen
+    end
+
+    it "they are returned also from structs" do
+      struct = @obj.Coordinates2[0]
+      expect(struct).to be_an(Array)
+      expect(struct).to be_frozen
+    end
+  end
 end
diff --git a/spec/variant_spec.rb b/spec/variant_spec.rb
index 01a38fa..571df43 100755
--- a/spec/variant_spec.rb
+++ b/spec/variant_spec.rb
@@ -1,4 +1,6 @@
 #!/usr/bin/env rspec
+# frozen_string_literal: true
+
 # Test marshalling variants according to ruby types
 require_relative "spec_helper"
 require "dbus"
@@ -9,8 +11,8 @@ describe "VariantTest" do
     @svc = @bus.service("org.ruby.service")
   end
 
-  def make_variant(a)
-    DBus::PacketMarshaller.make_variant(a)
+  def make_variant(val)
+    DBus::PacketMarshaller.make_variant(val)
   end
 
   it "tests make variant scalar" do
diff --git a/spec/zzz_quit_spec.rb b/spec/zzz_quit_spec.rb
new file mode 100755
index 0000000..51e63a5
--- /dev/null
+++ b/spec/zzz_quit_spec.rb
@@ -0,0 +1,16 @@
+#!/usr/bin/env rspec
+# frozen_string_literal: true
+
+require_relative "spec_helper"
+require "dbus"
+
+describe "Quit the service" do
+  it "Tells the service to quit and waits, to collate coverage data" do
+    session_bus = DBus::ASessionBus.new
+    @svc = session_bus.service("org.ruby.service")
+    @obj = @svc.object("/org/ruby/MyInstance")
+    @obj.default_iface = "org.ruby.SampleInterface"
+    @obj.quit
+    sleep 3
+  end
+end

Debdiff

[The following lists of changes regard files as different if they have different names, permissions or owners.]

Files in second set of .debs but not in first

-rw-r--r--  root/root   /usr/lib/ruby/vendor_ruby/dbus/data.rb
-rw-r--r--  root/root   /usr/lib/ruby/vendor_ruby/dbus/emits_changed_signal.rb
-rw-r--r--  root/root   /usr/lib/ruby/vendor_ruby/dbus/object_manager.rb
-rw-r--r--  root/root   /usr/lib/ruby/vendor_ruby/dbus/platform.rb
-rw-r--r--  root/root   /usr/lib/ruby/vendor_ruby/dbus/raw_message.rb
-rw-r--r--  root/root   /usr/share/doc/ruby-dbus/rdoc/DBus/Authentication.html
-rw-r--r--  root/root   /usr/share/doc/ruby-dbus/rdoc/DBus/Authentication/Anonymous.html
-rw-r--r--  root/root   /usr/share/doc/ruby-dbus/rdoc/DBus/Authentication/Client.html
-rw-r--r--  root/root   /usr/share/doc/ruby-dbus/rdoc/DBus/Authentication/DBusCookieSHA1.html
-rw-r--r--  root/root   /usr/share/doc/ruby-dbus/rdoc/DBus/Authentication/External.html
-rw-r--r--  root/root   /usr/share/doc/ruby-dbus/rdoc/DBus/Authentication/ExternalWithoutUid.html
-rw-r--r--  root/root   /usr/share/doc/ruby-dbus/rdoc/DBus/Authentication/Mechanism.html
-rw-r--r--  root/root   /usr/share/doc/ruby-dbus/rdoc/DBus/Data.html
-rw-r--r--  root/root   /usr/share/doc/ruby-dbus/rdoc/DBus/Data/Array.html
-rw-r--r--  root/root   /usr/share/doc/ruby-dbus/rdoc/DBus/Data/Base.html
-rw-r--r--  root/root   /usr/share/doc/ruby-dbus/rdoc/DBus/Data/Basic.html
-rw-r--r--  root/root   /usr/share/doc/ruby-dbus/rdoc/DBus/Data/Boolean.html
-rw-r--r--  root/root   /usr/share/doc/ruby-dbus/rdoc/DBus/Data/Byte.html
-rw-r--r--  root/root   /usr/share/doc/ruby-dbus/rdoc/DBus/Data/Container.html
-rw-r--r--  root/root   /usr/share/doc/ruby-dbus/rdoc/DBus/Data/DictEntry.html
-rw-r--r--  root/root   /usr/share/doc/ruby-dbus/rdoc/DBus/Data/Double.html
-rw-r--r--  root/root   /usr/share/doc/ruby-dbus/rdoc/DBus/Data/Fixed.html
-rw-r--r--  root/root   /usr/share/doc/ruby-dbus/rdoc/DBus/Data/Int.html
-rw-r--r--  root/root   /usr/share/doc/ruby-dbus/rdoc/DBus/Data/Int16.html
-rw-r--r--  root/root   /usr/share/doc/ruby-dbus/rdoc/DBus/Data/Int32.html
-rw-r--r--  root/root   /usr/share/doc/ruby-dbus/rdoc/DBus/Data/Int64.html
-rw-r--r--  root/root   /usr/share/doc/ruby-dbus/rdoc/DBus/Data/ObjectPath.html
-rw-r--r--  root/root   /usr/share/doc/ruby-dbus/rdoc/DBus/Data/Signature.html
-rw-r--r--  root/root   /usr/share/doc/ruby-dbus/rdoc/DBus/Data/String.html
-rw-r--r--  root/root   /usr/share/doc/ruby-dbus/rdoc/DBus/Data/StringLike.html
-rw-r--r--  root/root   /usr/share/doc/ruby-dbus/rdoc/DBus/Data/Struct.html
-rw-r--r--  root/root   /usr/share/doc/ruby-dbus/rdoc/DBus/Data/UInt16.html
-rw-r--r--  root/root   /usr/share/doc/ruby-dbus/rdoc/DBus/Data/UInt32.html
-rw-r--r--  root/root   /usr/share/doc/ruby-dbus/rdoc/DBus/Data/UInt64.html
-rw-r--r--  root/root   /usr/share/doc/ruby-dbus/rdoc/DBus/Data/UnixFD.html
-rw-r--r--  root/root   /usr/share/doc/ruby-dbus/rdoc/DBus/Data/Variant.html
-rw-r--r--  root/root   /usr/share/doc/ruby-dbus/rdoc/DBus/EmitsChangedSignal.html
-rw-r--r--  root/root   /usr/share/doc/ruby-dbus/rdoc/DBus/ObjectManager.html
-rw-r--r--  root/root   /usr/share/doc/ruby-dbus/rdoc/DBus/Platform.html
-rw-r--r--  root/root   /usr/share/doc/ruby-dbus/rdoc/DBus/Property.html
-rw-r--r--  root/root   /usr/share/doc/ruby-dbus/rdoc/DBus/Prototype.html
-rw-r--r--  root/root   /usr/share/doc/ruby-dbus/rdoc/DBus/RawMessage.html
-rw-r--r--  root/root   /usr/share/doc/ruby-dbus/rdoc/DBus/Signature.html
-rw-r--r--  root/root   /usr/share/doc/ruby-dbus/rdoc/DBus/SingleCompleteType.html
-rw-r--r--  root/root   /usr/share/doc/ruby-dbus/rdoc/DBus/Type/ArrayFactory.html
-rw-r--r--  root/root   /usr/share/doc/ruby-dbus/rdoc/DBus/Type/Factory.html
-rw-r--r--  root/root   /usr/share/doc/ruby-dbus/rdoc/DBus/Type/HashFactory.html
-rw-r--r--  root/root   /usr/share/doc/ruby-dbus/rdoc/DBus/Type/StructFactory.html
-rw-r--r--  root/root   /usr/share/rubygems-integration/all/specifications/ruby-dbus-0.22.1.gemspec
-rwxr-xr-x  root/root   /usr/share/doc/ruby-dbus/examples/service/complex-property.rb

Files in first set of .debs but not in second

-rw-r--r--  root/root   /usr/share/doc/ruby-dbus/examples/gdbus/gdbus.glade
-rw-r--r--  root/root   /usr/share/doc/ruby-dbus/rdoc/DBus/Anonymous.html
-rw-r--r--  root/root   /usr/share/doc/ruby-dbus/rdoc/DBus/Authenticator.html
-rw-r--r--  root/root   /usr/share/doc/ruby-dbus/rdoc/DBus/Client.html
-rw-r--r--  root/root   /usr/share/doc/ruby-dbus/rdoc/DBus/DBusCookieSHA1.html
-rw-r--r--  root/root   /usr/share/doc/ruby-dbus/rdoc/DBus/External.html
-rw-r--r--  root/root   /usr/share/doc/ruby-dbus/rdoc/DBus/Type/Type.html
-rw-r--r--  root/root   /usr/share/rubygems-integration/all/specifications/ruby-dbus-0.16.0.gemspec
-rwxr-xr-x  root/root   /usr/share/doc/ruby-dbus/examples/gdbus/gdbus
-rwxr-xr-x  root/root   /usr/share/doc/ruby-dbus/examples/gdbus/launch.sh

No differences were encountered in the control files

More details

Full run details