ruby-dbus 0.18.0.beta1 → 0.18.0.beta2

Sign up to get free protection for your applications and to get access to all the features.
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