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.
+
+
+
+ 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