diff --git a/NEWS.md b/NEWS.md index 9438443..3ecec12 100644 --- a/NEWS.md +++ b/NEWS.md @@ -5,11 +5,15 @@ 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 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/lib/dbus.rb b/lib/dbus.rb index 2b487c1..aa90be7 100644 --- a/lib/dbus.rb +++ b/lib/dbus.rb @@ -14,6 +14,7 @@ require_relative "dbus/auth" require_relative "dbus/bus" require_relative "dbus/bus_name" +require_relative "dbus/data" require_relative "dbus/error" require_relative "dbus/introspect" require_relative "dbus/logger" @@ -26,6 +27,7 @@ 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" @@ -49,6 +51,10 @@ module DBus BIG_END end + # Comparing symbols is faster than strings + # @return [:little,:big] + HOST_ENDIANNESS = RawMessage.endianness(HOST_END) + # General exceptions. # Exception raised when there is a problem with a type (may be unknown or diff --git a/lib/dbus/data.rb b/lib/dbus/data.rb new file mode 100644 index 0000000..fd67754 --- /dev/null +++ b/lib/dbus/data.rb @@ -0,0 +1,717 @@ +# frozen_string_literal: true + +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] + # @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, member_types: type.members) + 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 appropriately-typed, valid value + attr_reader :value + + # @!method type + # @abstract + # Note that for Variants type=="v", + # for the specific see {Variant#member_type} + # @return [Type] the exact type of this value + + # 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 + alias eql? == + 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 member_types [::Array] (ignored, will be empty) + # @return [Basic] + def self.from_typed(value, member_types:) # rubocop:disable Lint/UnusedMethodArgument + # assert member_types.empty? + 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 + + # {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 + + # Contains one or more other values. + class Container < Base + def self.basic? + false + end + + def self.fixed? + false + end + end + + # Format strings for String#unpack, both little- and big-endian. + Format = ::Struct.new(:little, :big) + + # Represents integers + class Int < Fixed + # @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 + + def self.range + raise NotImplementedError, "Abstract" + 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 + + # 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] + 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 + + # An Array, or a Dictionary (Hash). + class Array < Container + def self.type_code + "a" + end + + def self.alignment + 4 + end + + # @return [Type] + attr_reader :member_type + + def type + return @type if @type + + # TODO: reconstructing the type is cumbersome; have #initialize take *type* instead? + # TODO: or rather add Type::Array[t] + @type = Type.new("a") + @type << member_type + @type + end + + # TODO: check that Hash keys are basic types + # @param mode [:plain,:exact] + # @param member_type [Type] + # @param hash [Boolean] are we unmarshalling an ARRAY of DICT_ENTRY + # @return [Data::Array] + def self.from_items(value, mode:, member_type:, hash: false) + value = Hash[value] if hash + return value if mode == :plain + + new(value, member_type: member_type) + end + + # @param value [::Object] + # @param member_types [::Array] + # @return [Data::Array] + def self.from_typed(value, member_types:) + # TODO: validation + member_type = member_types.first + + # TODO: Dict?? + items = value.map do |i| + Data.make_typed(member_type, i) + end + + new(items) # initialize(::Array) + 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 member_type [SingleCompleteType,Type] + def initialize(value, member_type:) + member_type = DBus.type(member_type) unless member_type.is_a?(Type) + # TODO: copy from another Data::Array + @member_type = member_type + @type = nil + super(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 + + # @return [::Array] + attr_reader :member_types + + def type + return @type if @type + + # TODO: reconstructing the type is cumbersome; have #initialize take *type* instead? + # TODO: or rather add Type::Struct[t1, t2, ...] + @type = Type.new(self.class.type_code, abstract: true) + @member_types.each do |member_type| + @type << member_type + end + @type + end + + # @param value [::Array] + def self.from_items(value, mode:, member_types:) + value.freeze + return value if mode == :plain + + new(value, member_types: member_types) + end + + # @param value [::Object] (#size, #each) + # @param member_types [::Array] + # @return [Struct] + def self.from_typed(value, member_types:) + # TODO: validation + raise unless value.size == member_types.size + + @member_types = member_types + + items = member_types.zip(value).map do |item_type, item| + Data.make_typed(item_type, item) + end + + new(items, member_types: member_types) # initialize(::Array) + end + + def initialize(value, member_types:) + @member_types = member_types + @type = nil + super(value) + end + end + + # A generic type + class Variant < Container + def self.type_code + "v" + end + + def self.alignment + 1 + 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 member_types [::Array] + # @return [Variant] + def self.from_typed(value, member_types:) # rubocop:disable Lint/UnusedMethodArgument + # assert member_types.empty? + + # decide on type of value + new(value) + end + + # Note that for Variants type=="v", + # for the specific see {Variant#member_type} + # @return [Type] the exact type of this value + def type + "v" + end + + # @return [Type] + attr_reader :member_type + + def self.guess_type(value) + sct, = PacketMarshaller.make_variant(value) + DBus.type(sct) + end + + # @param member_type [Type,nil] + def initialize(value, member_type:) + # TODO: validate that the given *member_type* matches *value* + if value.is_a?(self.class) + # 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.value + else + @member_type = member_type || self.class.guess_type(value) + end + super(value) + end + end + + # Dictionary/Hash entry. + # TODO: shouldn't instantiate? + class DictEntry < Container + def self.type_code + "e" + end + + def self.alignment + 8 + end + + # @return [::Array] + attr_reader :member_types + + def type + return @type if @type + + # TODO: reconstructing the type is cumbersome; have #initialize take *type* instead? + @type = Type.new(self.class.type_code, abstract: true) + @member_types.each do |member_type| + @type << member_type + end + @type + end + + # @param value [::Array] + def self.from_items(value, mode:, member_types:) # rubocop:disable Lint/UnusedMethodArgument + value.freeze + # DictEntry ignores the :exact mode + value + end + + def initialize(value, member_types:) + @member_types = member_types + @type = nil + super(value) + 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/introspect.rb b/lib/dbus/introspect.rb index f068ec9..ee0f96d 100644 --- a/lib/dbus/introspect.rb +++ b/lib/dbus/introspect.rb @@ -240,6 +240,7 @@ def to_xml class Property # @return [String] The name of the property, for example FooBar. attr_reader :name + # @return [SingleCompleteType] attr_reader :type # @return [Symbol] :read :write or :readwrite attr_reader :access diff --git a/lib/dbus/marshall.rb b/lib/dbus/marshall.rb index d4d5d8e..4627430 100644 --- a/lib/dbus/marshall.rb +++ b/lib/dbus/marshall.rb @@ -12,6 +12,8 @@ require "socket" +require_relative "../dbus/type" + # = D-Bus main module # # Module containing all the D-Bus modules and classes. @@ -23,236 +25,128 @@ class InvalidPacketException < Exception # = 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) - # forward compatibility with new tests - endianness = BIG_END if endianness == :big - endianness = LIL_END if endianness == :little - - @buffy = buffer.dup - @endianness = endianness - case @endianness - when BIG_END - @uint32 = "N" - @uint16 = "n" - @double = "G" - when 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 + # 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} - # @return [Array<::Object>] + # @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 - def unmarshall(signature, len = nil) - raise IncompleteBufferException if len && @buffy.bytesize < @idx + len + # @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 _alignment_, which - # must be 1, 2, 4 or 8. - def align(alignment) - case alignment - when 1 - nil - when 2, 4, 8 - bits = alignment - 1 - pad_size = ((@idx + bits) & ~bits) - @idx - 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 - - ############################################################### - # 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 + # after the headers, the body starts 8-aligned + def align_body + @raw_msg.align(8) end - # Read the string length and string itself from the buffer. - # Return the string. - def read_string - align(4) - str_sz = read(4).unpack1(@uint32) - 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 + # @return [Integer] + def consumed_size + @raw_msg.pos end - # Read the signature length and signature itself from the buffer. - # Return the signature. - def read_signature - str_sz = read(1).unpack1("C") - 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 + private - @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).unpack1("C") - when Type::UINT16 - align(2) - packet = read(2).unpack1(@uint16) - when Type::INT16 - align(2) - packet = read(2).unpack1(@uint16) - if (packet & 0x8000) != 0 - packet -= 0x10000 - end - when Type::UINT32, Type::UNIX_FD - align(4) - packet = read(4).unpack1(@uint32) - when Type::INT32 - align(4) - packet = read(4).unpack1(@uint32) - if (packet & 0x80000000) != 0 - packet -= 0x100000000 - end - when Type::UINT64 - align(8) - packet_l = read(4).unpack1(@uint32) - packet_h = read(4).unpack1(@uint32) - 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).unpack1(@uint32) - packet_h = read(4).unpack1(@uint32) - 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).unpack1(@double) - when Type::BOOLEAN - align(4) - v = read(4).unpack1(@uint32) - unless [0, 1].member?(v) - raise InvalidPacketException, "BOOLEAN must be 0 or 1, found #{v}" - end + data_class = Data::BY_TYPE_CODE[signature.sigtype] - packet = (v == 1) - when Type::ARRAY - align(4) - # checks please - array_sz = read(4).unpack1(@uint32) - if array_sz > 67_108_864 - raise InvalidPacketException, "ARRAY body longer than 64MiB" + 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) + value = @raw_msg.read(size) + nul = @raw_msg.read(1) + if nul != "\u0000" + raise InvalidPacketException, "#{data_class} is not NUL-terminated" end - align(signature.child.alignment) - raise IncompleteBufferException if @idx + array_sz > @buffy.bytesize + 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, member_types: signature.members) - packet = [] - start_idx = @idx - while @idx - start_idx < array_sz - packet << do_parse(signature.child) - end + when Type::VARIANT + data_sig = do_parse(Data::Signature.type, mode: :exact) # -> Data::Signature + types = Type::Parser.new(data_sig.value).parse # -> Array + unless types.size == 1 + raise InvalidPacketException, "VARIANT must contain 1 value, #{types.size} found" + end - 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 - packet.freeze - when Type::VARIANT - string = read_signature - # error checking please - sigs = Type::Parser.new(string).parse - unless sigs.size == 1 - raise InvalidPacketException, "VARIANT must contain 1 value, #{sigs.size} found" - end + type = types.first + value = do_parse(type, mode: mode) + packet = data_class.from_items(value, mode: mode, member_type: type) - sig = sigs.first - 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 - raise NotImplementedError, - "sigtype: #{signature.sigtype} (#{signature.sigtype.chr})" + 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, member_type: signature.child, hash: is_hash) + end end packet end @@ -265,11 +159,16 @@ def do_parse(signature) 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 @@ -291,17 +190,6 @@ def align(alignment) @packet = @packet.ljust(pad_count, 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}\u0000" - end - # Append the array type _type_ to the packet and allow for appending # the child elements. def array(type) @@ -315,7 +203,8 @@ def array(type) 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. @@ -327,84 +216,78 @@ def struct # 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 - append_variant(val) - when Type::ARRAY - append_array(type.child, val) - when Type::STRUCT, Type::DICT_ENTRY - 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" - end + # 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 type.sigtype == Type::DICT_ENTRY && val.size != 2 - raise TypeException, "DICT_ENTRY expects a pair" - 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 + append_array(type.child, val) + when Type::STRUCT, Type::DICT_ENTRY + val = val.value if val.is_a?(Data::Struct) + 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.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 + 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) + 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 - else - raise NotImplementedError, - "sigtype: #{type.sigtype} (#{type.sigtype.chr})" end end def append_variant(val) vartype = nil - if val.is_a?(Array) && val.size == 2 + if val.is_a?(DBus::Data::Base) + vartype = val.type # FIXME: box or unbox another variant? + 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 @@ -420,9 +303,9 @@ def append_variant(val) vartype = Type::Parser.new(vartype).parse[0] end - append_signature(vartype.to_s) + append(Data::Signature.type, vartype.to_s) align(vartype.alignment) - sub = PacketMarshaller.new(@offset + @packet.bytesize) + sub = PacketMarshaller.new(@offset + @packet.bytesize, endianness: endianness) sub.append(vartype, vardata) @packet += sub.packet end diff --git a/lib/dbus/message.rb b/lib/dbus/message.rb index 53e7932..26888ba 100644 --- a/lib/dbus/message.rb +++ b/lib/dbus/message.rb @@ -172,7 +172,7 @@ def marshall @body_length = params.packet.bytesize marshaller = PacketMarshaller.new - marshaller.append(Type::BYTE, HOST_END) + marshaller.append(Type::BYTE, HOST_END.ord) marshaller.append(Type::BYTE, @message_type) marshaller.append(Type::BYTE, @flags) marshaller.append(Type::BYTE, @protocol) @@ -204,13 +204,7 @@ def marshall # 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 @@ -235,11 +229,11 @@ def unmarshall_buffer(buf) @signature = struct[1] end end - pu.align(8) + 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 diff --git a/lib/dbus/object.rb b/lib/dbus/object.rb index 731dd14..60cd75f 100644 --- a/lib/dbus/object.rb +++ b/lib/dbus/object.rb @@ -61,15 +61,7 @@ def dispatch(msg) retdata = [*retdata] reply = Message.method_return(msg) - if iface.name == PROPERTY_INTERFACE && member_sym == :Get - # Use the specific property type instead of the generic variant - # returned by Get. - # TODO: GetAll and Set still missing - property = dbus_lookup_property(msg.params[0], msg.params[1]) - rsigs = [property.type] - else - rsigs = meth.rets.map(&:type) - end + rsigs = meth.rets.map(&:type) rsigs.zip(retdata).each do |rsig, rdata| reply.add_param(rsig, rdata) end @@ -345,7 +337,9 @@ def dbus_lookup_property(interface_name, property_name) if property.readable? ruby_name = property.ruby_name value = public_send(ruby_name) - [value] + # 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" @@ -357,6 +351,9 @@ def 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"), @@ -385,7 +382,10 @@ def dbus_lookup_property(interface_name, property_name) # > array. # so we will silently omit properties that fail to read. # Get'ting them individually will send DBus.Error - p_hash[p_name.to_s] = public_send(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) + 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" diff --git a/lib/dbus/object_path.rb b/lib/dbus/object_path.rb index 774e641..860b7dd 100644 --- a/lib/dbus/object_path.rb +++ b/lib/dbus/object_path.rb @@ -9,7 +9,8 @@ # 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 diff --git a/lib/dbus/raw_message.rb b/lib/dbus/raw_message.rb new file mode 100644 index 0000000..c3c5967 --- /dev/null +++ b/lib/dbus/raw_message.rb @@ -0,0 +1,83 @@ +# frozen_string_literal: true + +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 47c45f0..de2fca4 100644 --- a/lib/dbus/type.rb +++ b/lib/dbus/type.rb @@ -34,6 +34,9 @@ class Prototype < String; end # This module containts the constants of the types specified in the D-Bus # protocol. # + # Corresponds to {SingleCompleteType}. + # + # See also {DBus::Data::Signature} class Type # Mapping from type number to name and alignment. TYPE_MAPPING = { @@ -70,9 +73,9 @@ class SignatureException < Exception # This is for backward compatibility. Type = self # rubocop:disable Naming/ConstantName - # Returns the signature type number. + # @return [String] the signature type character, eg "s" or "e". attr_reader :sigtype - # Return contained member types. + # @return [Array] contained member types. attr_reader :members # Use {DBus.type} instead, because this allows constructing @@ -188,6 +191,7 @@ def nextchar # 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 char @@ -279,6 +283,9 @@ def types(string_type) module_function :types # Make an explicit [Type, value] pair + # @param string_type [SingleCompleteType] + # @param value [::Object] + # @return [Array(DBus::Type::Type,::Object)] def variant(string_type, value) [type(string_type), value] end diff --git a/spec/data/marshall.yaml b/spec/data/marshall.yaml new file mode 100644 index 0000000..e3abf53 --- /dev/null +++ b/spec/data/marshall.yaml @@ -0,0 +1,1639 @@ +--- +# 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: "(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..66c4b22 --- /dev/null +++ b/spec/data_spec.rb @@ -0,0 +1,298 @@ +#!/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 "constructor accepts numeric range" do |min, max| + describe "#initialize" do + it "accepts the min value #{min}" do + expect(described_class.new(min).value).to eq(min) + end + + it "accepts the max value #{max}" do + expect(described_class.new(max).value).to eq(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 eq(plain) + end + + it "accepts the typed value #{plain.inspect}" do + typed = described_class.new(plain) + expect(described_class.new(typed).value).to eq(plain) + end + end + end +end + +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).value).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).value).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.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.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 + # 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 "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 "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 "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 "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 "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 "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 "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 "constructor accepts plain or typed values", false + end + + describe DBus::Data::Double do + 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 "constructor accepts plain or typed values", good + include_examples "constructor rejects values from this list", bad + end + + describe DBus::Data::ObjectPath do + good = [ + "/" + # TODO: others + ] + + bad = [ + ["", DBus::InvalidPacketException, "Invalid object path"] + # TODO: others + ] + + include_examples "constructor accepts plain or typed values", good + include_examples "constructor rejects values from this list", bad + end + + describe DBus::Data::Signature do + good = [ + "", + "i", + "ii" + # TODO: others + ] + + bad = [ + ["!", DBus::InvalidPacketException, "Unknown type code"] + # TODO: others + ] + + include_examples "constructor accepts plain or typed values", good + include_examples "constructor rejects values from this list", bad + end + end + + describe "containers" do + describe DBus::Data::Array do + good = [ + # [[1, 2, 3], member_type: nil], + [[1, 2, 3], { member_type: "q" }], + [[1, 2, 3], { member_type: DBus::Type::UINT16 }], + [[1, 2, 3], { member_type: DBus.type("q") }], + [[DBus::Data::UInt16.new(1), DBus::Data::UInt16.new(2), DBus::Data::UInt16.new(3)], { member_type: "q" }] + # TODO: others + ] + + bad = [ + # undesirable type guessing + ## [[1, 2, 3], { member_type: nil }, DBus::InvalidPacketException, "Unknown type code"], + ## [[1, 2, 3], { member_type: "!" }, DBus::InvalidPacketException, "Unknown type code"] + # TODO: others + ] + + include_examples "constructor (kwargs) accepts values", good + include_examples "constructor (kwargs) rejects values", bad + end + + describe DBus::Data::Struct do + three_words = ::Struct.new(:a, :b, :c) + + qqq = ["q", "q", "q"] + 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 m_t styles + [integers, { member_types: ["q", "q", "q"] }], + [integers, { member_types: [DBus::Type::UINT16, DBus::Type::UINT16, DBus::Type::UINT16] }], + [integers, { member_types: DBus.types("qqq") }], + # plain array of data + [uints, { member_types: DBus.types("qqq") }], + # ::Struct + [three_words.new(*integers), { member_types: qqq }], + [three_words.new(*uints), { member_types: qqq }] + # TODO: others + ] + + _bad_but_valid = [ + # Wrong member_types arg: + # hmm this is another reason to pass the type + # as the entire struct type, not the members: + # empty struct will be caught naturally + [integers, { member_types: [] }, ArgumentError, "???"], + [integers, { member_types: ["!"] }, DBus::InvalidPacketException, "Unknown type code"], + # STRUCT specific: member count mismatch + [[1, 2], { member_types: DBus.types("qqq") }, ArgumentError, "???"], + [[1, 2, 3, 4], { member_types: DBus.types("qqq") }, ArgumentError, "???"] + # TODO: others + ] + + include_examples "constructor (kwargs) accepts values", good + # include_examples "constructor (kwargs) rejects values", bad + end + + describe DBus::Data::Variant do + end + + describe DBus::Data::DictEntry do + end + end +end diff --git a/spec/packet_marshaller_spec.rb b/spec/packet_marshaller_spec.rb new file mode 100755 index 0000000..3994a8a --- /dev/null +++ b/spec/packet_marshaller_spec.rb @@ -0,0 +1,34 @@ +#!/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 + end + end +end diff --git a/spec/packet_unmarshaller_spec.rb b/spec/packet_unmarshaller_spec.rb index c4fa555..90509a8 100755 --- a/spec/packet_unmarshaller_spec.rb +++ b/spec/packet_unmarshaller_spec.rb @@ -3,42 +3,61 @@ 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 pu [PacketUnmarshaller] +# @param p_u [PacketUnmarshaller] # @return [String] the binary string with unconsumed data def remaining_buffer(p_u) - buf = p_u.instance_variable_get(:@buffy) - # This returns "" if idx is just past the end of the string, - # and nil if it is further. - buf[p_u.idx..-1] + 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, value), i| - it "parses data ##{i}" 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) - # NOTE: it's [value], not value. + results = subject.unmarshall(signature, mode: :plain) # unmarshall works on multiple signatures but we use one - expect(subject.unmarshall(signature)).to eq([value]) + 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 - end - end -end -RSpec.shared_examples "reports bad data" do |cases| - describe "reports all the instances of bad test data" do - cases.each_with_index do |(buffer, endianness, exc_class, msg_re), i| - it "reports data ##{i}" do + it "parses exact data ##{i}" do buffer = String.new(buffer, encoding: Encoding::BINARY) subject = described_class.new(buffer, endianness) - expect { subject.unmarshall(signature) }.to raise_error(exc_class, msg_re) + + 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) + if expected.is_a?(Hash) + expect(result.value.size).to eq(expected.size) + result.value.each_key do |result_key| + expect(result.value[result_key]).to eq(expected[result_key.value]) + end + else + expect(result.value).to eq(expected) + end + + expect(remaining_buffer(subject)).to be_empty end end end @@ -55,153 +74,111 @@ def remaining_buffer(p_u) 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) + if expected.is_a?(Hash) + expect(result.value.size).to eq(expected.size) + result.value.each_key do |result_key| + expect(result.value[result_key]).to eq(expected[result_key.value]) + end + else + expect(result.value).to eq(expected) + end + + 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" } - good = [ - ["\x00", :little, 0x00], - ["\x80", :little, 0x80], - ["\xff", :little, 0xff], - ["\x00", :big, 0x00], - ["\x80", :big, 0x80], - ["\xff", :big, 0xff] - ] - include_examples "parses good data", good include_examples "reports empty data" end context "BOOLEANs" do let(:signature) { "b" } - - good = [ - ["\x01\x00\x00\x00", :little, true], - ["\x00\x00\x00\x00", :little, false], - ["\x00\x00\x00\x01", :big, true], - ["\x00\x00\x00\x00", :big, false] - ] - include_examples "parses good data", good - - # 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. - bad = [ - ["\x00\xff\xff\x00", :little, DBus::InvalidPacketException, /BOOLEAN must be 0 or 1, found/], - ["\x00\xff\xff\x00", :big, DBus::InvalidPacketException, /BOOLEAN must be 0 or 1, found/] - ] - include_examples "reports bad data", bad include_examples "reports empty data" end context "INT16s" do let(:signature) { "n" } - - good = [ - ["\x00\x00", :little, 0], - ["\xff\x7f", :little, 32_767], - ["\x00\x80", :little, -32_768], - ["\xff\xff", :little, -1], - ["\x00\x00", :big, 0], - ["\x7f\xff", :big, 32_767], - ["\x80\x00", :big, -32_768], - ["\xff\xff", :big, -1] - ] - include_examples "parses good data", good include_examples "reports empty data" end context "UINT16s" do let(:signature) { "q" } - - good = [ - ["\x00\x00", :little, 0], - ["\xff\x7f", :little, 32_767], - ["\x00\x80", :little, 32_768], - ["\xff\xff", :little, 65_535], - ["\x00\x00", :big, 0], - ["\x7f\xff", :big, 32_767], - ["\x80\x00", :big, 32_768], - ["\xff\xff", :big, 65_535] - ] - include_examples "parses good data", good include_examples "reports empty data" end context "INT32s" do let(:signature) { "i" } - good = [ - ["\x00\x00\x00\x00", :little, 0], - ["\xff\xff\xff\x7f", :little, 2_147_483_647], - ["\x00\x00\x00\x80", :little, -2_147_483_648], - ["\xff\xff\xff\xff", :little, -1], - ["\x00\x00\x00\x00", :big, 0], - ["\x7f\xff\xff\xff", :big, 2_147_483_647], - ["\x80\x00\x00\x00", :big, -2_147_483_648], - ["\xff\xff\xff\xff", :big, -1] - ] - include_examples "parses good data", good include_examples "reports empty data" end context "UINT32s" do let(:signature) { "u" } - good = [ - ["\x00\x00\x00\x00", :little, 0], - ["\xff\xff\xff\x7f", :little, 2_147_483_647], - ["\x00\x00\x00\x80", :little, 2_147_483_648], - ["\xff\xff\xff\xff", :little, 4_294_967_295], - ["\x00\x00\x00\x00", :big, 0], - ["\x7f\xff\xff\xff", :big, 2_147_483_647], - ["\x80\x00\x00\x00", :big, 2_147_483_648], - ["\xff\xff\xff\xff", :big, 4_294_967_295] - ] - include_examples "parses good data", good include_examples "reports empty data" end context "UNIX_FDs" do let(:signature) { "h" } - good = [ - ["\x00\x00\x00\x00", :little, 0], - ["\xff\xff\xff\x7f", :little, 2_147_483_647], - ["\x00\x00\x00\x80", :little, 2_147_483_648], - ["\xff\xff\xff\xff", :little, 4_294_967_295], - ["\x00\x00\x00\x00", :big, 0], - ["\x7f\xff\xff\xff", :big, 2_147_483_647], - ["\x80\x00\x00\x00", :big, 2_147_483_648], - ["\xff\xff\xff\xff", :big, 4_294_967_295] - ] - include_examples "parses good data", good include_examples "reports empty data" end context "INT64s" do let(:signature) { "x" } - good = [ - ["\x00\x00\x00\x00\x00\x00\x00\x00", :little, 0], - ["\xff\xff\xff\xff\xff\xff\xff\x7f", :little, 9_223_372_036_854_775_807], - ["\x00\x00\x00\x00\x00\x00\x00\x80", :little, -9_223_372_036_854_775_808], - ["\xff\xff\xff\xff\xff\xff\xff\xff", :little, -1], - ["\x00\x00\x00\x00\x00\x00\x00\x00", :big, 0], - ["\x7f\xff\xff\xff\xff\xff\xff\xff", :big, 9_223_372_036_854_775_807], - ["\x80\x00\x00\x00\x00\x00\x00\x00", :big, -9_223_372_036_854_775_808], - ["\xff\xff\xff\xff\xff\xff\xff\xff", :big, -1] - ] - include_examples "parses good data", good include_examples "reports empty data" end context "UINT64s" do let(:signature) { "t" } - good = [ - ["\x00\x00\x00\x00\x00\x00\x00\x00", :little, 0], - ["\xff\xff\xff\xff\xff\xff\xff\x7f", :little, 9_223_372_036_854_775_807], - ["\x00\x00\x00\x00\x00\x00\x00\x80", :little, 9_223_372_036_854_775_808], - ["\xff\xff\xff\xff\xff\xff\xff\xff", :little, 18_446_744_073_709_551_615], - ["\x00\x00\x00\x00\x00\x00\x00\x00", :big, 0], - ["\x7f\xff\xff\xff\xff\xff\xff\xff", :big, 9_223_372_036_854_775_807], - ["\x80\x00\x00\x00\x00\x00\x00\x00", :big, 9_223_372_036_854_775_808], - ["\xff\xff\xff\xff\xff\xff\xff\xff", :big, 18_446_744_073_709_551_615] - ] - include_examples "parses good data", good include_examples "reports empty data" end @@ -211,15 +188,11 @@ def remaining_buffer(p_u) # for binary representations # TODO: figure out IEEE754 comparisons good = [ - ["\x00\x00\x00\x00\x00\x00\x00\x00", :little, 0.0], # But == cant distinguish -0.0 ["\x00\x00\x00\x00\x00\x00\x00\x80", :little, -0.0], - ["\x00\x00\x00\x00\x00\x00\x00\x40", :little, 2.0], # But NaN == NaN is false! # ["\xff\xff\xff\xff\xff\xff\xff\xff", :little, Float::NAN], - ["\x00\x00\x00\x00\x00\x00\x00\x00", :big, 0.0], - ["\x80\x00\x00\x00\x00\x00\x00\x00", :big, -0.0], - ["\x40\x00\x00\x00\x00\x00\x00\x00", :big, 2.0] + ["\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 @@ -228,287 +201,37 @@ def remaining_buffer(p_u) context "STRINGs" do let(:signature) { "s" } - good = [ - ["\x00\x00\x00\x00\x00", :little, ""], - ["\x02\x00\x00\x00\xC5\x98\x00", :little, "Ř"], - ["\x03\x00\x00\x00\xEF\xBF\xBF\x00", :little, "\uffff"], - ["\x00\x00\x00\x00\x00", :big, ""], - ["\x00\x00\x00\x02\xC5\x98\x00", :big, "Ř"], - ["\x00\x00\x00\x03\xEF\xBF\xBF\x00", :big, "\uffff"], - # maximal UTF-8 codepoint U+10FFFF - ["\x00\x00\x00\x04\xF4\x8F\xBF\xBF\x00", :big, "\u{10ffff}"] - ] - _bad_but_valid = [ - # NUL in the middle - ["\x03\x00\x00\x00a\x00b\x00", :little, DBus::InvalidPacketException, /Invalid string/], - # invalid UTF-8 - ["\x04\x00\x00\x00\xFF\xFF\xFF\xFF\x00", :little, DBus::InvalidPacketException, /Invalid string/], - # overlong sequence encoding an "A" - ["\x02\x00\x00\x00\xC1\x81\x00", :little, DBus::InvalidPacketException, /Invalid string/], - # first codepoint outside UTF-8, U+110000 - ["\x04\x00\x00\x00\xF4\x90\xC0\xC0\x00", :little, DBus::InvalidPacketException, /Invalid string/] - - ] - bad = [ - ["\x00\x00\x00\x00\x55", :little, DBus::InvalidPacketException, /not NUL-terminated/], - ["\x01\x00\x00\x00@\x55", :little, DBus::InvalidPacketException, /not NUL-terminated/], - ["\x00\x00\x00\x00", :little, DBus::IncompleteBufferException, /./], - ["\x00\x00\x00", :little, DBus::IncompleteBufferException, /./], - ["\x00\x00", :little, DBus::IncompleteBufferException, /./], - ["\x00", :little, DBus::IncompleteBufferException, /./] - ] - include_examples "parses good data", good - include_examples "reports bad data", bad include_examples "reports empty data" end context "OBJECT_PATHs" do let(:signature) { "o" } - long_path = "/#{"A" * 511}" - good = [ - ["\x01\x00\x00\x00/\x00", :little, "/"], - ["\x20\x00\x00\x00/99Numbers/_And_Underscores/anyw\x00", :little, "/99Numbers/_And_Underscores/anyw"], - # no size limit like for other names - ["\x00\x02\x00\x00#{long_path}\x00", :little, long_path], - ["\x00\x00\x00\x01/\x00", :big, "/"], - ["\x00\x00\x00\x20/99Numbers/_And_Underscores/anyw\x00", :big, "/99Numbers/_And_Underscores/anyw"] - ] - _bad_but_valid = [ - ["\x00\x00\x00\x00\x00", :little, DBus::InvalidPacketException, /Invalid object path/], - ["\x00\x00\x00\x00\x00", :big, DBus::InvalidPacketException, /Invalid object path/], - ["\x00\x00\x00\x05/_//_\x00", :big, DBus::InvalidPacketException, /Invalid object path/], - ["\x00\x00\x00\x05/_/_/\x00", :big, DBus::InvalidPacketException, /Invalid object path/], - ["\x00\x00\x00\x05/_/_ \x00", :big, DBus::InvalidPacketException, /Invalid object path/], - ["\x00\x00\x00\x05/_/_-\x00", :big, DBus::InvalidPacketException, /Invalid object path/], - # NUL in the middle - ["\x00\x00\x00\x05/_/_\x00\x00", :big, DBus::InvalidPacketException, /Invalid object path/], - # accented a - ["\x00\x00\x00\x05/_/\xC3\xA1\x00", :big, DBus::InvalidPacketException, /Invalid object path/] - ] - bad = [ - # string-like baddies - ["\x00\x00\x00\x00\x55", :little, DBus::InvalidPacketException, /not NUL-terminated/], - ["\x01\x00\x00\x00/\x55", :little, DBus::InvalidPacketException, /not NUL-terminated/], - ["\x00\x00\x00\x00", :little, DBus::IncompleteBufferException, /./], - ["\x00\x00\x00", :little, DBus::IncompleteBufferException, /./], - ["\x00\x00", :little, DBus::IncompleteBufferException, /./], - ["\x00", :little, DBus::IncompleteBufferException, /./] - ] - include_examples "parses good data", good - include_examples "reports bad data", bad include_examples "reports empty data" end context "SIGNATUREs" do let(:signature) { "g" } - good = [ - ["\x00\x00", :little, ""], - ["\x00\x00", :big, ""], - ["\x01b\x00", :little, "b"], - ["\x01b\x00", :big, "b"] - ] - _bad_but_valid = [ - ["\x01!\x00", :big, DBus::InvalidPacketException, /Invalid signature/], - ["\x01r\x00", :big, DBus::InvalidPacketException, /Invalid signature/], - ["\x02ae\x00", :big, DBus::InvalidPacketException, /Invalid signature/], - ["\x01a\x00", :big, DBus::InvalidPacketException, /Invalid signature/], - # dict_entry with other than 2 members - ["\x03a{}\x00", :big, DBus::InvalidPacketException, /Invalid signature/], - ["\x04a{s}\x00", :big, DBus::InvalidPacketException, /Invalid signature/], - ["\x06a{sss}\x00", :big, DBus::InvalidPacketException, /Invalid signature/], - # dict_entry with non-basic key - ["\x05a{vs}\x00", :big, DBus::InvalidPacketException, /Invalid signature/], - # dict_entry outside array - ["\x04{sv}\x00", :big, DBus::InvalidPacketException, /Invalid signature/], - # dict_entry outside array - ["\x07a({sv})\x00", :big, DBus::InvalidPacketException, /Invalid signature/], - # NUL in the middle - ["\x03a\x00y\x00", :big, DBus::InvalidPacketException, /Invalid signature/] - ] - bad = [ - # string-like baddies - ["\x00\x55", :big, DBus::InvalidPacketException, /not NUL-terminated/], - ["\x01b\x55", :big, DBus::InvalidPacketException, /not NUL-terminated/], - ["\x00", :little, DBus::IncompleteBufferException, /./] - ] - include_examples "parses good data", good - include_examples "reports bad data", bad include_examples "reports empty data" end context "ARRAYs" do - # 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... context "of BYTEs" do - # TODO: will want to special-case this - # and represent them as binary strings let(:signature) { "ay" } - - # Here we repeat the STRINGs test data (without the trailing NUL) - # but the outcomes are different - good = [ - ["\x00\x00\x00\x00", :little, []], - ["\x02\x00\x00\x00\xC5\x98", :little, [0xC5, 0x98]], - ["\x03\x00\x00\x00\xEF\xBF\xBF", :little, [0xEF, 0xBF, 0xBF]], - ["\x00\x00\x00\x00", :big, []], - ["\x00\x00\x00\x02\xC5\x98", :big, [0xC5, 0x98]], - ["\x00\x00\x00\x03\xEF\xBF\xBF", :big, [0xEF, 0xBF, 0xBF]], - # maximal UTF-8 codepoint U+10FFFF - ["\x00\x00\x00\x04\xF4\x8F\xBF\xBF", :big, [0xF4, 0x8F, 0xBF, 0xBF]], - # NUL in the middle - ["\x03\x00\x00\x00a\x00b", :little, [0x61, 0, 0x62]], - # invalid UTF-8 - ["\x04\x00\x00\x00\xFF\xFF\xFF\xFF", :little, [0xFF, 0xFF, 0xFF, 0xFF]], - # overlong sequence encoding an "A" - ["\x02\x00\x00\x00\xC1\x81", :little, [0xC1, 0x81]], - # first codepoint outside UTF-8, U+110000 - ["\x04\x00\x00\x00\xF4\x90\xC0\xC0", :little, [0xF4, 0x90, 0xC0, 0xC0]] - ] - bad = [ - # 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. - - ["\x01\x00\x00\x04", :little, DBus::InvalidPacketException, /ARRAY body longer than 64MiB/], - - ["\x02\x00\x00\x00\xAA", :little, DBus::IncompleteBufferException, /./], - ["\x00\x00\x00", :little, DBus::IncompleteBufferException, /./], - ["\x00\x00", :little, DBus::IncompleteBufferException, /./], - ["\x00", :little, DBus::IncompleteBufferException, /./] - ] - include_examples "parses good data", good - include_examples "reports bad data", bad include_examples "reports empty data" end context "of UINT64s" do let(:signature) { "at" } - - good = [ - [ - # body size, padding - "\x00\x00\x00\x00" \ - "\x00\x00\x00\x00", :little, [] - ], - [ - # body size, padding, item, item - "\x10\x00\x00\x00" \ - "\x00\x00\x00\x00" \ - "\x01\x00\x00\x00\x00\x00\x00\x00" \ - "\x02\x00\x00\x00\x00\x00\x00\x00", :little, [1, 2] - ] - ] - bad = [ - # missing padding - ["\x00\x00\x00\x00", :little, DBus::IncompleteBufferException, /./], - [ - # (zero) body size, non-zero padding, (no items) - "\x00\x00\x00\x00" \ - "\xDE\xAD\xBE\xEF", :little, DBus::InvalidPacketException, /./ - ] - ] - include_examples "parses good data", good - include_examples "reports bad data", bad include_examples "reports empty data" end - # arrays let us demonstrate the padding of their elements context "of STRUCT of 2 UINT16s" do let(:signature) { "a(qq)" } - - good = [ - [ - # body size, padding - "\x00\x00\x00\x00" \ - "\x00\x00\x00\x00", :little, [] - ], - [ - # body size, padding, item, padding, item - "\x0C\x00\x00\x00" \ - "\x00\x00\x00\x00" \ - "\x01\x00\x02\x00" \ - "\x00\x00\x00\x00" \ - "\x03\x00\x04\x00", :little, [[1, 2], [3, 4]] - ], - [ - # body size, padding, item, padding, item, padding, item - "\x14\x00\x00\x00" \ - "\x00\x00\x00\x00" \ - "\x05\x00\x06\x00" \ - "\x00\x00\x00\x00" \ - "\x07\x00\x08\x00" \ - "\x00\x00\x00\x00" \ - "\x09\x00\x0A\x00", :little, [[5, 6], [7, 8], [9, 10]] - ] - ] - bad = [ - # missing padding - ["\x00\x00\x00\x00", :little, DBus::IncompleteBufferException, /./], - [ - # (zero) body size, non-zero padding, (no items) - "\x00\x00\x00\x00" \ - "\xDE\xAD\xBE\xEF", :little, DBus::InvalidPacketException, /./ - ] - ] - include_examples "parses good data", good - include_examples "reports bad data", bad include_examples "reports empty data" end context "of DICT_ENTRIES" do let(:signature) { "a{yq}" } - - good = [ - [ - # body size, padding - "\x00\x00\x00\x00" \ - "\x00\x00\x00\x00", :little, {} - ], - [ - # 4 body size, - # 4 (dict_entry) padding, - # 1 key, 1 padding, 2 value - # 4 (dict_entry) padding, - # 1 key, 1 padding, 2 value - "\x0C\x00\x00\x00" \ - "\x00\x00\x00\x00" \ - "\x01\x00\x02\x00" \ - "\x00\x00\x00\x00" \ - "\x03\x00\x04\x00", :little, { 1 => 2, 3 => 4 } - ], - [ - # 4 body size, - # 4 (dict_entry) padding, - # 1 key, 1 padding, 2 value - # 4 (dict_entry) padding, - # 1 key, 1 padding, 2 value - "\x00\x00\x00\x0C" \ - "\x00\x00\x00\x00" \ - "\x01\x00\x00\x02" \ - "\x00\x00\x00\x00" \ - "\x03\x00\x00\x04", :big, { 1 => 2, 3 => 4 } - ] - ] - bad = [ - # missing padding - ["\x00\x00\x00\x00", :little, DBus::IncompleteBufferException, /./], - [ - # (zero) body size, non-zero padding, (no items) - "\x00\x00\x00\x00" \ - "\xDE\xAD\xBE\xEF", :little, DBus::InvalidPacketException, /./ - ] - ] - include_examples "parses good data", good - include_examples "reports bad data", bad include_examples "reports empty data" end end @@ -521,12 +244,6 @@ def remaining_buffer(p_u) context "of two shorts" do let(:signature) { "(qq)" } - - good = [ - ["\x01\x00\x02\x00", :little, [1, 2]], - ["\x00\x03\x00\x04", :big, [3, 4]] - ] - include_examples "parses good data", good include_examples "reports empty data" end end @@ -540,37 +257,6 @@ def remaining_buffer(p_u) context "VARIANTs" do let(:signature) { "v" } - - good = [ - ["\x01y\x00\xFF", :little, 255], - [ - # signature, padding, value - "\x01u\x00" \ - "\x00" \ - "\x01\x00\x00\x00", :little, 1 - ], - # nested variant - [ - "\x01v\x00" \ - "\x01y\x00\xFF", :little, 255 - ] - ] - _bad_but_valid = [ - # variant nested too deep - [ - "#{"\x01v\x00" * 70}" \ - "\x01y\x00\xFF", :little, DBus::InvalidPacketException, /nested too deep/ - ] - ] - bad = [ - # IDEA: test other libraries by sending them an empty variant? - # the signature has no type - ["\x00\x00", :little, DBus::InvalidPacketException, /1 value, 0 found/], - # the signature has more than one type - ["\x02yy\x00\xFF\xFF", :little, DBus::InvalidPacketException, /1 value, 2 found/] - ] - include_examples "parses good data", good - include_examples "reports bad data", bad include_examples "reports empty data" end end diff --git a/spec/property_spec.rb b/spec/property_spec.rb index e2356ad..60b52ff 100755 --- a/spec/property_spec.rb +++ b/spec/property_spec.rb @@ -122,5 +122,29 @@ 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 end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index d4f42c4..da5dd28 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -118,3 +118,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