ruby-dbus 0.18.0.beta1 → 0.18.0.beta2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
data/lib/dbus/marshall.rb CHANGED
@@ -12,6 +12,8 @@
12
12
 
13
13
  require "socket"
14
14
 
15
+ require_relative "../dbus/type"
16
+
15
17
  # = D-Bus main module
16
18
  #
17
19
  # Module containing all the D-Bus modules and classes.
@@ -23,217 +25,128 @@ module DBus
23
25
  # = D-Bus packet unmarshaller class
24
26
  #
25
27
  # Class that handles the conversion (unmarshalling) of payload data
26
- # to Array.
28
+ # to #{::Object}s (in **plain** mode) or to {Data::Base} (in **exact** mode)
29
+ #
30
+ # Spelling note: this codebase always uses a double L
31
+ # in the "marshall" word and its inflections.
27
32
  class PacketUnmarshaller
28
- # Index pointer that points to the byte in the data that is
29
- # currently being processed.
30
- #
31
- # Used to kown what part of the buffer has been consumed by unmarshalling.
32
- # FIXME: Maybe should be accessed with a "consumed_size" method.
33
- attr_reader :idx
34
-
35
- # Create a new unmarshaller for the given data _buffer_ and _endianness_.
33
+ # Create a new unmarshaller for the given data *buffer*.
34
+ # @param buffer [String]
35
+ # @param endianness [:little,:big]
36
36
  def initialize(buffer, endianness)
37
- @buffy = buffer.dup
38
- @endianness = endianness
39
- case @endianness
40
- when BIG_END
41
- @uint32 = "N"
42
- @uint16 = "n"
43
- @double = "G"
44
- when LIL_END
45
- @uint32 = "V"
46
- @uint16 = "v"
47
- @double = "E"
48
- else
49
- raise InvalidPacketException, "Incorrect endianness #{@endianness}"
50
- end
51
- @idx = 0
37
+ # TODO: this dup can be avoided if we can prove
38
+ # that an IncompleteBufferException leaves the original *buffer* intact
39
+ buffer = buffer.dup
40
+ @raw_msg = RawMessage.new(buffer, endianness)
52
41
  end
53
42
 
54
43
  # Unmarshall the buffer for a given _signature_ and length _len_.
55
- # Return an array of unmarshalled objects
44
+ # Return an array of unmarshalled objects.
56
45
  # @param signature [Signature]
57
46
  # @param len [Integer,nil] if given, and there is not enough data
58
47
  # in the buffer, raise {IncompleteBufferException}
59
- # @return [Array<::Object>]
48
+ # @param mode [:plain,:exact]
49
+ # @return [Array<::Object,DBus::Data::Base>]
50
+ # Objects in `:plain` mode, {DBus::Data::Base} in `:exact` mode
51
+ # The array size corresponds to the number of types in *signature*.
60
52
  # @raise IncompleteBufferException
61
- def unmarshall(signature, len = nil)
62
- raise IncompleteBufferException if len && @buffy.bytesize < @idx + len
53
+ # @raise InvalidPacketException
54
+ def unmarshall(signature, len = nil, mode: :plain)
55
+ @raw_msg.want!(len) if len
63
56
 
64
57
  sigtree = Type::Parser.new(signature).parse
65
58
  ret = []
66
59
  sigtree.each do |elem|
67
- ret << do_parse(elem)
60
+ ret << do_parse(elem, mode: mode)
68
61
  end
69
62
  ret
70
63
  end
71
64
 
72
- # Align the pointer index on a byte index of _alignment_, which
73
- # must be 1, 2, 4 or 8.
74
- def align(alignment)
75
- case alignment
76
- when 1
77
- nil
78
- when 2, 4, 8
79
- bits = alignment - 1
80
- @idx = @idx + bits & ~bits
81
- raise IncompleteBufferException if @idx > @buffy.bytesize
82
- else
83
- raise ArgumentError, "Unsupported alignment #{alignment}"
84
- end
65
+ # after the headers, the body starts 8-aligned
66
+ def align_body
67
+ @raw_msg.align(8)
85
68
  end
86
69
 
87
- ###############################################################
88
- # FIXME: does anyone except the object itself call the above methods?
89
- # Yes : Message marshalling code needs to align "body" to 8 byte boundary
90
- private
70
+ # @return [Integer]
71
+ def consumed_size
72
+ @raw_msg.pos
73
+ end
91
74
 
92
- # Retrieve the next _nbytes_ number of bytes from the buffer.
93
- def read(nbytes)
94
- raise IncompleteBufferException if @idx + nbytes > @buffy.bytesize
75
+ private
95
76
 
96
- ret = @buffy.slice(@idx, nbytes)
97
- @idx += nbytes
98
- ret
77
+ # @param data_class [Class] a subclass of Data::Base (specific?)
78
+ # @return [::Integer,::Float]
79
+ def aligned_read_value(data_class)
80
+ @raw_msg.align(data_class.alignment)
81
+ bytes = @raw_msg.read(data_class.alignment)
82
+ bytes.unpack1(data_class.format[@raw_msg.endianness])
99
83
  end
100
84
 
101
- # Read the string length and string itself from the buffer.
102
- # Return the string.
103
- def read_string
104
- align(4)
105
- str_sz = read(4).unpack1(@uint32)
106
- ret = @buffy.slice(@idx, str_sz)
107
- raise IncompleteBufferException if @idx + str_sz + 1 > @buffy.bytesize
85
+ # Based on the _signature_ type, retrieve a packet from the buffer
86
+ # and return it.
87
+ # @param signature [Type]
88
+ # @param mode [:plain,:exact]
89
+ # @return [Data::Base]
90
+ def do_parse(signature, mode: :plain)
91
+ # FIXME: better naming for packet vs value
92
+ packet = nil
93
+ data_class = Data::BY_TYPE_CODE[signature.sigtype]
108
94
 
109
- @idx += str_sz
110
- if @buffy[@idx].ord != 0
111
- raise InvalidPacketException, "String is not nul-terminated"
95
+ if data_class.nil?
96
+ raise NotImplementedError,
97
+ "sigtype: #{signature.sigtype} (#{signature.sigtype.chr})"
112
98
  end
113
99
 
114
- @idx += 1
115
- # no exception, see check above
116
- ret
117
- end
100
+ if data_class.fixed?
101
+ value = aligned_read_value(data_class)
102
+ packet = data_class.from_raw(value, mode: mode)
103
+ elsif data_class.basic?
104
+ size = aligned_read_value(data_class.size_class)
105
+ value = @raw_msg.read(size)
106
+ nul = @raw_msg.read(1)
107
+ if nul != "\u0000"
108
+ raise InvalidPacketException, "#{data_class} is not NUL-terminated"
109
+ end
118
110
 
119
- # Read the signature length and signature itself from the buffer.
120
- # Return the signature.
121
- def read_signature
122
- str_sz = read(1).unpack1("C")
123
- ret = @buffy.slice(@idx, str_sz)
124
- raise IncompleteBufferException if @idx + str_sz + 1 >= @buffy.bytesize
111
+ packet = data_class.from_raw(value, mode: mode)
112
+ else
113
+ @raw_msg.align(data_class.alignment)
114
+ case signature.sigtype
115
+ when Type::STRUCT, Type::DICT_ENTRY
116
+ values = signature.members.map do |child_sig|
117
+ do_parse(child_sig, mode: mode)
118
+ end
119
+ packet = data_class.from_items(values, mode: mode, member_types: signature.members)
125
120
 
126
- @idx += str_sz
127
- if @buffy[@idx].ord != 0
128
- raise InvalidPacketException, "Type is not nul-terminated"
129
- end
121
+ when Type::VARIANT
122
+ data_sig = do_parse(Data::Signature.type, mode: :exact) # -> Data::Signature
123
+ types = Type::Parser.new(data_sig.value).parse # -> Array<Type>
124
+ unless types.size == 1
125
+ raise InvalidPacketException, "VARIANT must contain 1 value, #{types.size} found"
126
+ end
130
127
 
131
- @idx += 1
132
- # no exception, see check above
133
- ret
134
- end
128
+ type = types.first
129
+ value = do_parse(type, mode: mode)
130
+ packet = data_class.from_items(value, mode: mode, member_type: type)
135
131
 
136
- # Based on the _signature_ type, retrieve a packet from the buffer
137
- # and return it.
138
- def do_parse(signature)
139
- packet = nil
140
- case signature.sigtype
141
- when Type::BYTE
142
- packet = read(1).unpack1("C")
143
- when Type::UINT16
144
- align(2)
145
- packet = read(2).unpack1(@uint16)
146
- when Type::INT16
147
- align(2)
148
- packet = read(2).unpack1(@uint16)
149
- if (packet & 0x8000) != 0
150
- packet -= 0x10000
151
- end
152
- when Type::UINT32, Type::UNIX_FD
153
- align(4)
154
- packet = read(4).unpack1(@uint32)
155
- when Type::INT32
156
- align(4)
157
- packet = read(4).unpack1(@uint32)
158
- if (packet & 0x80000000) != 0
159
- packet -= 0x100000000
160
- end
161
- when Type::UINT64
162
- align(8)
163
- packet_l = read(4).unpack1(@uint32)
164
- packet_h = read(4).unpack1(@uint32)
165
- packet = if @endianness == LIL_END
166
- packet_l + packet_h * 2**32
167
- else
168
- packet_l * 2**32 + packet_h
169
- end
170
- when Type::INT64
171
- align(8)
172
- packet_l = read(4).unpack1(@uint32)
173
- packet_h = read(4).unpack1(@uint32)
174
- packet = if @endianness == LIL_END
175
- packet_l + packet_h * 2**32
176
- else
177
- packet_l * 2**32 + packet_h
178
- end
179
- if (packet & 0x8000000000000000) != 0
180
- packet -= 0x10000000000000000
181
- end
182
- when Type::DOUBLE
183
- align(8)
184
- packet = read(8).unpack1(@double)
185
- when Type::BOOLEAN
186
- align(4)
187
- v = read(4).unpack1(@uint32)
188
- raise InvalidPacketException if ![0, 1].member?(v)
189
-
190
- packet = (v == 1)
191
- when Type::ARRAY
192
- align(4)
193
- # checks please
194
- array_sz = read(4).unpack1(@uint32)
195
- raise InvalidPacketException if array_sz > 67_108_864
196
-
197
- align(signature.child.alignment)
198
- raise IncompleteBufferException if @idx + array_sz > @buffy.bytesize
199
-
200
- packet = []
201
- start_idx = @idx
202
- while @idx - start_idx < array_sz
203
- packet << do_parse(signature.child)
204
- end
132
+ when Type::ARRAY
133
+ array_bytes = aligned_read_value(Data::UInt32)
134
+ if array_bytes > 67_108_864
135
+ raise InvalidPacketException, "ARRAY body longer than 64MiB"
136
+ end
205
137
 
206
- if signature.child.sigtype == Type::DICT_ENTRY
207
- packet = Hash[packet]
208
- end
209
- when Type::STRUCT
210
- align(8)
211
- packet = []
212
- signature.members.each do |elem|
213
- packet << do_parse(elem)
138
+ # needed here because of empty arrays
139
+ @raw_msg.align(signature.child.alignment)
140
+
141
+ items = []
142
+ end_pos = @raw_msg.pos + array_bytes
143
+ while @raw_msg.pos < end_pos
144
+ item = do_parse(signature.child, mode: mode)
145
+ items << item
146
+ end
147
+ is_hash = signature.child.sigtype == Type::DICT_ENTRY
148
+ packet = data_class.from_items(items, mode: mode, member_type: signature.child, hash: is_hash)
214
149
  end
215
- packet.freeze
216
- when Type::VARIANT
217
- string = read_signature
218
- # error checking please
219
- sig = Type::Parser.new(string).parse[0]
220
- align(sig.alignment)
221
- packet = do_parse(sig)
222
- when Type::OBJECT_PATH
223
- packet = read_string
224
- when Type::STRING
225
- packet = read_string
226
- packet.force_encoding("UTF-8")
227
- when Type::SIGNATURE
228
- packet = read_signature
229
- when Type::DICT_ENTRY
230
- align(8)
231
- key = do_parse(signature.members[0])
232
- value = do_parse(signature.members[1])
233
- packet = [key, value]
234
- else
235
- raise NotImplementedError,
236
- "sigtype: #{signature.sigtype} (#{signature.sigtype.chr})"
237
150
  end
238
151
  packet
239
152
  end
@@ -246,11 +159,16 @@ module DBus
246
159
  class PacketMarshaller
247
160
  # The current or result packet.
248
161
  # FIXME: allow access only when marshalling is finished
162
+ # @return [String]
249
163
  attr_reader :packet
250
164
 
165
+ # @return [:little,:big]
166
+ attr_reader :endianness
167
+
251
168
  # Create a new marshaller, setting the current packet to the
252
169
  # empty packet.
253
- def initialize(offset = 0)
170
+ def initialize(offset = 0, endianness: HOST_ENDIANNESS)
171
+ @endianness = endianness
254
172
  @packet = ""
255
173
  @offset = offset # for correct alignment of nested marshallers
256
174
  end
@@ -272,17 +190,6 @@ module DBus
272
190
  @packet = @packet.ljust(pad_count, 0.chr)
273
191
  end
274
192
 
275
- # Append the the string _str_ itself to the packet.
276
- def append_string(str)
277
- align(4)
278
- @packet += [str.bytesize].pack("L") + [str].pack("Z*")
279
- end
280
-
281
- # Append the the signature _signature_ itself to the packet.
282
- def append_signature(str)
283
- @packet += "#{str.bytesize.chr}#{str}\u0000"
284
- end
285
-
286
193
  # Append the array type _type_ to the packet and allow for appending
287
194
  # the child elements.
288
195
  def array(type)
@@ -296,7 +203,8 @@ module DBus
296
203
  sz = @packet.bytesize - contentidx
297
204
  raise InvalidPacketException if sz > 67_108_864
298
205
 
299
- @packet[sizeidx...sizeidx + 4] = [sz].pack("L")
206
+ sz_data = Data::UInt32.new(sz)
207
+ @packet[sizeidx...sizeidx + 4] = sz_data.marshall(endianness)
300
208
  end
301
209
 
302
210
  # Align and allow for appending struct fields.
@@ -308,84 +216,78 @@ module DBus
308
216
  # Append a value _val_ to the packet based on its _type_.
309
217
  #
310
218
  # Host native endianness is used, declared in Message#marshall
219
+ #
220
+ # @param type [SingleCompleteType] (or Integer or {Type})
221
+ # @param val [::Object]
311
222
  def append(type, val)
312
223
  raise TypeException, "Cannot send nil" if val.nil?
313
224
 
314
225
  type = type.chr if type.is_a?(Integer)
315
226
  type = Type::Parser.new(type).parse[0] if type.is_a?(String)
316
- case type.sigtype
317
- when Type::BYTE
318
- @packet += val.chr
319
- when Type::UINT32, Type::UNIX_FD
320
- align(4)
321
- @packet += [val].pack("L")
322
- when Type::UINT64
323
- align(8)
324
- @packet += [val].pack("Q")
325
- when Type::INT64
326
- align(8)
327
- @packet += [val].pack("q")
328
- when Type::INT32
329
- align(4)
330
- @packet += [val].pack("l")
331
- when Type::UINT16
332
- align(2)
333
- @packet += [val].pack("S")
334
- when Type::INT16
335
- align(2)
336
- @packet += [val].pack("s")
337
- when Type::DOUBLE
338
- align(8)
339
- @packet += [val].pack("d")
340
- when Type::BOOLEAN
341
- align(4)
342
- @packet += if val
343
- [1].pack("L")
344
- else
345
- [0].pack("L")
346
- end
347
- when Type::OBJECT_PATH
348
- append_string(val)
349
- when Type::STRING
350
- append_string(val)
351
- when Type::SIGNATURE
352
- append_signature(val)
353
- when Type::VARIANT
354
- append_variant(val)
355
- when Type::ARRAY
356
- append_array(type.child, val)
357
- when Type::STRUCT, Type::DICT_ENTRY
358
- unless val.is_a?(Array) || val.is_a?(Struct)
359
- type_name = Type::TYPE_MAPPING[type.sigtype].first
360
- raise TypeException, "#{type_name} expects an Array or Struct"
361
- end
227
+ # type is [Type] now
228
+ data_class = Data::BY_TYPE_CODE[type.sigtype]
229
+ if data_class.nil?
230
+ raise NotImplementedError,
231
+ "sigtype: #{type.sigtype} (#{type.sigtype.chr})"
232
+ end
362
233
 
363
- if type.sigtype == Type::DICT_ENTRY && val.size != 2
364
- raise TypeException, "DICT_ENTRY expects a pair"
365
- end
234
+ if data_class.fixed?
235
+ align(data_class.alignment)
236
+ data = data_class.new(val)
237
+ @packet += data.marshall(endianness)
238
+ elsif data_class.basic?
239
+ val = val.value if val.is_a?(Data::Basic)
240
+ align(data_class.size_class.alignment)
241
+ size_data = data_class.size_class.new(val.bytesize)
242
+ @packet += size_data.marshall(endianness)
243
+ # Z* makes a binary string, as opposed to interpolation
244
+ @packet += [val].pack("Z*")
245
+ else
246
+ case type.sigtype
247
+
248
+ when Type::VARIANT
249
+ append_variant(val)
250
+ when Type::ARRAY
251
+ append_array(type.child, val)
252
+ when Type::STRUCT, Type::DICT_ENTRY
253
+ val = val.value if val.is_a?(Data::Struct)
254
+ unless val.is_a?(Array) || val.is_a?(Struct)
255
+ type_name = Type::TYPE_MAPPING[type.sigtype].first
256
+ raise TypeException, "#{type_name} expects an Array or Struct, seen #{val.class}"
257
+ end
366
258
 
367
- if type.members.size != val.size
368
- type_name = Type::TYPE_MAPPING[type.sigtype].first
369
- raise TypeException, "#{type_name} has #{val.size} elements but type info for #{type.members.size}"
370
- end
259
+ if type.sigtype == Type::DICT_ENTRY && val.size != 2
260
+ raise TypeException, "DICT_ENTRY expects a pair"
261
+ end
262
+
263
+ if type.members.size != val.size
264
+ type_name = Type::TYPE_MAPPING[type.sigtype].first
265
+ raise TypeException, "#{type_name} has #{val.size} elements but type info for #{type.members.size}"
266
+ end
371
267
 
372
- struct do
373
- type.members.zip(val).each do |t, v|
374
- append(t, v)
268
+ struct do
269
+ type.members.zip(val).each do |t, v|
270
+ append(t, v)
271
+ end
375
272
  end
273
+ else
274
+ raise NotImplementedError,
275
+ "sigtype: #{type.sigtype} (#{type.sigtype.chr})"
376
276
  end
377
- else
378
- raise NotImplementedError,
379
- "sigtype: #{type.sigtype} (#{type.sigtype.chr})"
380
277
  end
381
278
  end
382
279
 
383
280
  def append_variant(val)
384
281
  vartype = nil
385
- if val.is_a?(Array) && val.size == 2
282
+ if val.is_a?(DBus::Data::Base)
283
+ vartype = val.type # FIXME: box or unbox another variant?
284
+ vardata = val.value
285
+ elsif val.is_a?(Array) && val.size == 2
386
286
  case val[0]
387
- when DBus::Type::Type
287
+ when Type
388
288
  vartype, vardata = val
289
+ # Ambiguous but easy to use, because Type
290
+ # cannot construct "as" "a{sv}" easily
389
291
  when String
390
292
  begin
391
293
  parsed = Type::Parser.new(val[0]).parse
@@ -401,14 +303,14 @@ module DBus
401
303
  vartype = Type::Parser.new(vartype).parse[0]
402
304
  end
403
305
 
404
- append_signature(vartype.to_s)
306
+ append(Data::Signature.type, vartype.to_s)
405
307
  align(vartype.alignment)
406
- sub = PacketMarshaller.new(@offset + @packet.bytesize)
308
+ sub = PacketMarshaller.new(@offset + @packet.bytesize, endianness: endianness)
407
309
  sub.append(vartype, vardata)
408
310
  @packet += sub.packet
409
311
  end
410
312
 
411
- # @param child_type [DBus::Type::Type]
313
+ # @param child_type [Type]
412
314
  def append_array(child_type, val)
413
315
  if val.is_a?(Hash)
414
316
  raise TypeException, "Expected an Array but got a Hash" if child_type.sigtype != Type::DICT_ENTRY
data/lib/dbus/message.rb CHANGED
@@ -172,7 +172,7 @@ module DBus
172
172
  @body_length = params.packet.bytesize
173
173
 
174
174
  marshaller = PacketMarshaller.new
175
- marshaller.append(Type::BYTE, HOST_END)
175
+ marshaller.append(Type::BYTE, HOST_END.ord)
176
176
  marshaller.append(Type::BYTE, @message_type)
177
177
  marshaller.append(Type::BYTE, @flags)
178
178
  marshaller.append(Type::BYTE, @protocol)
@@ -204,13 +204,7 @@ module DBus
204
204
  # the detected message (self) and
205
205
  # the index pointer of the buffer where the message data ended.
206
206
  def unmarshall_buffer(buf)
207
- buf = buf.dup
208
- endianness = if buf[0] == "l"
209
- LIL_END
210
- else
211
- BIG_END
212
- end
213
- pu = PacketUnmarshaller.new(buf, endianness)
207
+ pu = PacketUnmarshaller.new(buf, RawMessage.endianness(buf[0]))
214
208
  mdata = pu.unmarshall(MESSAGE_SIGNATURE)
215
209
  _, @message_type, @flags, @protocol, @body_length, @serial,
216
210
  headers = mdata
@@ -235,11 +229,11 @@ module DBus
235
229
  @signature = struct[1]
236
230
  end
237
231
  end
238
- pu.align(8)
232
+ pu.align_body
239
233
  if @body_length.positive? && @signature
240
234
  @params = pu.unmarshall(@signature, @body_length)
241
235
  end
242
- [self, pu.idx]
236
+ [self, pu.consumed_size]
243
237
  end
244
238
 
245
239
  # Make a new exception from ex, mark it as being caused by this message
data/lib/dbus/object.rb CHANGED
@@ -61,15 +61,7 @@ module DBus
61
61
  retdata = [*retdata]
62
62
 
63
63
  reply = Message.method_return(msg)
64
- if iface.name == PROPERTY_INTERFACE && member_sym == :Get
65
- # Use the specific property type instead of the generic variant
66
- # returned by Get.
67
- # TODO: GetAll and Set still missing
68
- property = dbus_lookup_property(msg.params[0], msg.params[1])
69
- rsigs = [property.type]
70
- else
71
- rsigs = meth.rets.map(&:type)
72
- end
64
+ rsigs = meth.rets.map(&:type)
73
65
  rsigs.zip(retdata).each do |rsig, rdata|
74
66
  reply.add_param(rsig, rdata)
75
67
  end
@@ -345,7 +337,9 @@ module DBus
345
337
  if property.readable?
346
338
  ruby_name = property.ruby_name
347
339
  value = public_send(ruby_name)
348
- [value]
340
+ # may raise, DBus.error or https://ruby-doc.com/core-3.1.0/TypeError.html
341
+ typed_value = Data.make_typed(property.type, value)
342
+ [typed_value]
349
343
  else
350
344
  raise DBus.error("org.freedesktop.DBus.Error.PropertyWriteOnly"),
351
345
  "Property '#{interface_name}.#{property_name}' (on object '#{@path}') is not readable"
@@ -357,6 +351,9 @@ module DBus
357
351
 
358
352
  if property.writable?
359
353
  ruby_name_eq = "#{property.ruby_name}="
354
+ # TODO: declare dbus_method :Set to take :exact argument
355
+ # and type check it here before passing its :plain value
356
+ # to the implementation
360
357
  public_send(ruby_name_eq, value)
361
358
  else
362
359
  raise DBus.error("org.freedesktop.DBus.Error.PropertyReadOnly"),
@@ -385,7 +382,10 @@ module DBus
385
382
  # > array.
386
383
  # so we will silently omit properties that fail to read.
387
384
  # Get'ting them individually will send DBus.Error
388
- p_hash[p_name.to_s] = public_send(ruby_name)
385
+ value = public_send(ruby_name)
386
+ # may raise, DBus.error or https://ruby-doc.com/core-3.1.0/TypeError.html
387
+ typed_value = Data.make_typed(property.type, value)
388
+ p_hash[p_name.to_s] = typed_value
389
389
  rescue StandardError
390
390
  DBus.logger.debug "Property '#{interface_name}.#{p_name}' (on object '#{@path}')" \
391
391
  " has raised during GetAll, omitting it"
@@ -9,7 +9,8 @@
9
9
  # See the file "COPYING" for the exact licensing terms.
10
10
 
11
11
  module DBus
12
- # A {::String} that validates at initialization time
12
+ # A {::String} that validates at initialization time.
13
+ # See also {DBus::Data::ObjectPath}
13
14
  # @see https://dbus.freedesktop.org/doc/dbus-specification.html#message-protocol-marshaling-object-path
14
15
  class ObjectPath < String
15
16
  # @raise Error if not a valid object path