# frozen_string_literal: true # dbus/introspection.rb - module containing a low-level D-Bus introspection implementation # # This file is part of the ruby-dbus project # 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. module DBus # Regular expressions that should match all method names. METHOD_SIGNAL_RE = /^[A-Za-z][A-Za-z0-9_]*$/.freeze # Regular expressions that should match all interface names. INTERFACE_ELEMENT_RE = /^[A-Za-z][A-Za-z0-9_]*$/.freeze # Exception raised when an invalid class definition is encountered. class InvalidClassDefinition < Exception end # = D-Bus interface class # # This class is the interface descriptor. In most cases, the Introspect() # method call instantiates and configures this class for us. # # It also is the local definition of interface exported by the program. # At the client side, see {ProxyObjectInterface}. class Interface # @return [String] The name of the interface. attr_reader :name # @return [Hash{Symbol => DBus::Method}] The methods that are part of the interface. attr_reader :methods # @return [Hash{Symbol => Signal}] The signals that are part of the interface. attr_reader :signals # @return [Hash{Symbol => Property}] attr_reader :properties # @return [EmitsChangedSignal] attr_reader :emits_changed_signal # Creates a new interface with a given _name_. def initialize(name) validate_name(name) @name = name @methods = {} @signals = {} @properties = {} @emits_changed_signal = EmitsChangedSignal::DEFAULT_ECS end # Helper for {Object.emits_changed_signal=}. # @api private def emits_changed_signal=(ecs) raise TypeError unless ecs.is_a? EmitsChangedSignal # equal?: object identity unless @emits_changed_signal.equal?(EmitsChangedSignal::DEFAULT_ECS) || @emits_changed_signal.value == ecs.value raise "emits_change_signal was assigned more than once" end @emits_changed_signal = ecs end # Validates a service _name_. def validate_name(name) raise InvalidIntrospectionData if name.bytesize > 255 raise InvalidIntrospectionData if name =~ /^\./ || name =~ /\.$/ raise InvalidIntrospectionData if name =~ /\.\./ raise InvalidIntrospectionData if name !~ /\./ name.split(".").each do |element| raise InvalidIntrospectionData if element !~ INTERFACE_ELEMENT_RE end end # Add _ifc_el_ as a known {Method}, {Signal} or {Property} # @param ifc_el [InterfaceElement] def define(ifc_el) name = ifc_el.name.to_sym category = case ifc_el when Method @methods when Signal @signals when Property @properties end category[name] = ifc_el end alias declare define alias << define # Defines a method with name _id_ and a given _prototype_ in the # interface. # Better name: declare_method def define_method(id, prototype) m = Method.new(id) m.from_prototype(prototype) define(m) end alias declare_method define_method # Return introspection XML string representation of the property. # @return [String] def to_xml xml = " \n" xml += emits_changed_signal.to_xml methods.each_value { |m| xml += m.to_xml } signals.each_value { |m| xml += m.to_xml } properties.each_value { |m| xml += m.to_xml } xml += " \n" xml end end # = A formal parameter has a name and a type class FormalParameter # @return [#to_s] attr_reader :name # @return [SingleCompleteType] attr_reader :type def initialize(name, type) @name = name @type = type end # backward compatibility, deprecated def [](index) case index when 0 then name when 1 then type end end end # = D-Bus interface element class # # This is a generic class for entities that are part of the interface # such as methods and signals. class InterfaceElement # @return [Symbol] The name of the interface element attr_reader :name # @return [Array] The parameters of the interface element attr_reader :params # Validates element _name_. def validate_name(name) return if (name =~ METHOD_SIGNAL_RE) && (name.bytesize <= 255) raise InvalidMethodName, name end # Creates a new element with the given _name_. def initialize(name) validate_name(name.to_s) @name = name @params = [] end # Adds a formal parameter with _name_ and _signature_ # (See also Message#add_param which takes signature+value) def add_fparam(name, signature) @params << FormalParameter.new(name, signature) end # Deprecated, for backward compatibility def add_param(name_signature_pair) add_fparam(*name_signature_pair) end end # = D-Bus interface method class # # This is a class representing methods that are part of an interface. class Method < InterfaceElement # @return [Array] The list of return values for the method attr_reader :rets # Creates a new method interface element with the given _name_. def initialize(name) super(name) @rets = [] end # Add a return value _name_ and _signature_. # @param name [#to_s] # @param signature [SingleCompleteType] def add_return(name, signature) @rets << FormalParameter.new(name, signature) end # Add parameter types by parsing the given _prototype_. # @param prototype [Prototype] def from_prototype(prototype) prototype.split(/, */).each do |arg| arg = arg.split(" ") raise InvalidClassDefinition if arg.size != 2 dir, arg = arg if arg =~ /:/ arg = arg.split(":") name, sig = arg else sig = arg end case dir when "in" add_fparam(name, sig) when "out" add_return(name, sig) end end self end # Return an XML string representation of the method interface elment. # @return [String] def to_xml xml = " \n" @params.each do |param| name = param.name ? "name=\"#{param.name}\" " : "" xml += " \n" end @rets.each do |param| name = param.name ? "name=\"#{param.name}\" " : "" xml += " \n" end xml += " \n" xml end end # = D-Bus interface signal class # # This is a class representing signals that are part of an interface. class Signal < InterfaceElement # Add parameter types based on the given _prototype_. def from_prototype(prototype) prototype.split(/, */).each do |arg| if arg =~ /:/ arg = arg.split(":") name, sig = arg else sig = arg end add_fparam(name, sig) end self end # Return an XML string representation of the signal interface elment. def to_xml xml = " \n" @params.each do |param| name = param.name ? "name=\"#{param.name}\" " : "" xml += " \n" end xml += " \n" xml end end # An (exported) property # https://dbus.freedesktop.org/doc/dbus-specification.html#standard-interfaces-properties class Property # @return [Symbol] The name of the property, for example FooBar. attr_reader :name # @return [Type] attr_reader :type # @return [Symbol] :read :write or :readwrite attr_reader :access # @return [Symbol,nil] What to call at Ruby side. # (Always without the trailing `=`) # It is `nil` IFF representing a client-side proxy. attr_reader :ruby_name def initialize(name, type, access, ruby_name:) @name = name.to_sym type = DBus.type(type) unless type.is_a?(Type) @type = type @access = access @ruby_name = ruby_name end # @return [Boolean] def readable? access == :read || access == :readwrite end # @return [Boolean] def writable? access == :write || access == :readwrite end # Return introspection XML string representation of the property. def to_xml " \n" end # @param xml_node [AbstractXML::Node] # @return [Property] def self.from_xml(xml_node) name = xml_node["name"].to_sym type = xml_node["type"] access = xml_node["access"].to_sym new(name, type, access, ruby_name: nil) end end end