# frozen_string_literal: true # dbus.rb - Module containing the low-level D-Bus implementation # # This file is part of the ruby-dbus project # Copyright (C) 2007 Arnaud Cornet and Paul van Tilburg # # This library is free software; you can redistribute it and/or # modify it under the terms of the GNU Lesser General Public # License, version 2.1 as published by the Free Software Foundation. # See the file "COPYING" for the exact licensing terms. require "socket" require_relative "../dbus/type" # = D-Bus main module # # Module containing all the D-Bus modules and classes. module DBus # Exception raised when an invalid packet is encountered. class InvalidPacketException < Exception end # = D-Bus packet unmarshaller class # # Class that handles the conversion (unmarshalling) of payload data # 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 # Create a new unmarshaller for the given data *buffer*. # @param buffer [String] # @param endianness [:little,:big] def initialize(buffer, endianness) # 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. # @param signature [Signature] # @param len [Integer,nil] if given, and there is not enough data # in the buffer, raise {IncompleteBufferException} # @param mode [:plain,:exact] # @return [Array<::Object,DBus::Data::Base>] # Objects in `:plain` mode, {DBus::Data::Base} in `:exact` mode # The array size corresponds to the number of types in *signature*. # @raise IncompleteBufferException # @raise InvalidPacketException def unmarshall(signature, len = nil, mode: :plain) @raw_msg.want!(len) if len sigtree = Type::Parser.new(signature).parse ret = [] sigtree.each do |elem| ret << do_parse(elem, mode: mode) end ret end # after the headers, the body starts 8-aligned def align_body @raw_msg.align(8) end # @return [Integer] def consumed_size @raw_msg.pos end private # @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. # @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 data_class = Data::BY_TYPE_CODE[signature.sigtype] 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 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) 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 type = types.first value = do_parse(type, mode: mode) packet = data_class.from_items(value, mode: mode, member_type: type) when Type::ARRAY array_bytes = aligned_read_value(Data::UInt32) if array_bytes > 67_108_864 raise InvalidPacketException, "ARRAY body longer than 64MiB" end # needed here because of empty arrays @raw_msg.align(signature.child.alignment) items = [] end_pos = @raw_msg.pos + array_bytes while @raw_msg.pos < end_pos item = do_parse(signature.child, mode: mode) items << item end is_hash = signature.child.sigtype == Type::DICT_ENTRY packet = data_class.from_items(items, mode: mode, member_type: signature.child, hash: is_hash) end end packet end end # D-Bus packet marshaller class # # Class that handles the conversion (marshalling) of Ruby objects to # (binary) payload data. 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, endianness: HOST_ENDIANNESS) @endianness = endianness @packet = "" @offset = offset # for correct alignment of nested marshallers end # Round _num_ up to the specified power of two, _alignment_ def num_align(num, alignment) case alignment when 1, 2, 4, 8 bits = alignment - 1 num + bits & ~bits else raise ArgumentError, "Unsupported alignment #{alignment}" end end # Align the buffer with NULL (\0) bytes on a byte length of _alignment_. def align(alignment) pad_count = num_align(@offset + @packet.bytesize, alignment) - @offset @packet = @packet.ljust(pad_count, 0.chr) end # Append the array type _type_ to the packet and allow for appending # the child elements. def array(type) # Thanks to Peter Rullmann for this line align(4) sizeidx = @packet.bytesize @packet += "ABCD" align(type.alignment) contentidx = @packet.bytesize yield sz = @packet.bytesize - contentidx raise InvalidPacketException if sz > 67_108_864 sz_data = Data::UInt32.new(sz) @packet[sizeidx...sizeidx + 4] = sz_data.marshall(endianness) end # Align and allow for appending struct fields. def struct align(8) yield end # 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) # type is [Type] now data_class = Data::BY_TYPE_CODE[type.sigtype] if data_class.nil? raise NotImplementedError, "sigtype: #{type.sigtype} (#{type.sigtype.chr})" end if data_class.fixed? align(data_class.alignment) data = data_class.new(val) @packet += data.marshall(endianness) elsif data_class.basic? val = val.value if val.is_a?(Data::Basic) align(data_class.size_class.alignment) size_data = data_class.size_class.new(val.bytesize) @packet += size_data.marshall(endianness) # Z* makes a binary string, as opposed to interpolation @packet += [val].pack("Z*") else case type.sigtype when Type::VARIANT append_variant(val) when Type::ARRAY 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.sigtype == Type::DICT_ENTRY && val.size != 2 raise TypeException, "DICT_ENTRY expects a pair" end if type.members.size != val.size type_name = Type::TYPE_MAPPING[type.sigtype].first raise TypeException, "#{type_name} has #{val.size} elements but type info for #{type.members.size}" end struct do type.members.zip(val).each do |t, v| append(t, v) end end else raise NotImplementedError, "sigtype: #{type.sigtype} (#{type.sigtype.chr})" end end end def append_variant(val) vartype = nil 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 vartype = parsed[0] if parsed.size == 1 vardata = val[1] rescue Type::SignatureException # no assignment end end end if vartype.nil? vartype, vardata = PacketMarshaller.make_variant(val) vartype = Type::Parser.new(vartype).parse[0] end append(Data::Signature.type, vartype.to_s) align(vartype.alignment) sub = PacketMarshaller.new(@offset + @packet.bytesize, endianness: endianness) sub.append(vartype, vardata) @packet += sub.packet end # @param child_type [Type] def append_array(child_type, val) if val.is_a?(Hash) raise TypeException, "Expected an Array but got a Hash" if child_type.sigtype != Type::DICT_ENTRY # Damn ruby rocks here val = val.to_a end # If string is recieved and ay is expected, explode the string if val.is_a?(String) && child_type.sigtype == Type::BYTE val = val.bytes end if !val.is_a?(Enumerable) raise TypeException, "Expected an Enumerable of #{child_type.inspect} but got a #{val.class}" end array(child_type) do val.each do |elem| append(child_type, elem) end end end # Make a [signature, value] pair for a variant def self.make_variant(value) # TODO: mix in _make_variant to String, Integer... if value == true ["b", true] elsif value == false ["b", false] elsif value.nil? ["b", nil] elsif value.is_a? Float ["d", value] elsif value.is_a? Symbol ["s", value.to_s] elsif value.is_a? Array ["av", value.map { |i| make_variant(i) }] elsif value.is_a? Hash h = {} value.each_key { |k| h[k] = make_variant(value[k]) } ["a{sv}", h] elsif value.respond_to? :to_str ["s", value.to_str] elsif value.respond_to? :to_int i = value.to_int if (-2_147_483_648...2_147_483_648).cover?(i) ["i", i] else ["x", i] end end end end end