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/data.rb ADDED
@@ -0,0 +1,725 @@
1
+ # frozen_string_literal: true
2
+
3
+ # This file is part of the ruby-dbus project
4
+ # Copyright (C) 2022 Martin Vidner
5
+ #
6
+ # This library is free software; you can redistribute it and/or
7
+ # modify it under the terms of the GNU Lesser General Public
8
+ # License, version 2.1 as published by the Free Software Foundation.
9
+ # See the file "COPYING" for the exact licensing terms.
10
+
11
+ module DBus
12
+ # FIXME: in general, when an API gives me, a user, a choice,
13
+ # remember to make it easy for the case of:
14
+ # "I don't CARE, I don't WANT to care, WHY should I care?"
15
+
16
+ # Exact/explicit representation of D-Bus data types:
17
+ #
18
+ # - {Boolean}
19
+ # - {Byte}, {Int16}, {Int32}, {Int64}, {UInt16}, {UInt32}, {UInt64}
20
+ # - {Double}
21
+ # - {String}, {ObjectPath}, {Signature}
22
+ # - {Array}, {DictEntry}, {Struct}
23
+ # - {UnixFD}
24
+ # - {Variant}
25
+ #
26
+ # The common base type is {Base}.
27
+ #
28
+ # There are other intermediate classes in the inheritance hierarchy, using
29
+ # the names the specification uses, but they are an implementation detail:
30
+ #
31
+ # - A value is either {Basic} or a {Container}.
32
+ # - Basic values are either {Fixed}-size or {StringLike}.
33
+ module Data
34
+ # Given a plain Ruby *value* and wanting a D-Bus *type*,
35
+ # construct an appropriate {Data::Base} instance.
36
+ #
37
+ # @param type [SingleCompleteType,Type]
38
+ # @param value [::Object]
39
+ # @return [Data::Base]
40
+ # @raise TypeError
41
+ def make_typed(type, value)
42
+ type = DBus.type(type) unless type.is_a?(Type)
43
+ data_class = Data::BY_TYPE_CODE[type.sigtype]
44
+ # not nil because DBus.type validates
45
+
46
+ data_class.from_typed(value, member_types: type.members)
47
+ end
48
+ module_function :make_typed
49
+
50
+ # The base class for explicitly typed values.
51
+ #
52
+ # A value is either {Basic} or a {Container}.
53
+ # {Basic} values are either {Fixed}-size or {StringLike}.
54
+ class Base
55
+ # @!method self.basic?
56
+ # @return [Boolean]
57
+
58
+ # @!method self.fixed?
59
+ # @return [Boolean]
60
+
61
+ # @return appropriately-typed, valid value
62
+ attr_reader :value
63
+
64
+ # @!method type
65
+ # @abstract
66
+ # Note that for Variants type=="v",
67
+ # for the specific see {Variant#member_type}
68
+ # @return [Type] the exact type of this value
69
+
70
+ # Child classes must validate *value*.
71
+ def initialize(value)
72
+ @value = value
73
+ end
74
+
75
+ def ==(other)
76
+ @value == if other.is_a?(Base)
77
+ other.value
78
+ else
79
+ other
80
+ end
81
+ end
82
+
83
+ # Hash key equality
84
+ # See https://ruby-doc.org/core-3.0.0/Object.html#method-i-eql-3F
85
+ alias eql? ==
86
+ end
87
+
88
+ # A value that is not a {Container}.
89
+ class Basic < Base
90
+ def self.basic?
91
+ true
92
+ end
93
+
94
+ # @return [Type]
95
+ def self.type
96
+ # memoize
97
+ @type ||= Type.new(type_code).freeze
98
+ end
99
+
100
+ def type
101
+ # The basic types can do this, unlike the containers
102
+ self.class.type
103
+ end
104
+
105
+ # @param value [::Object]
106
+ # @param member_types [::Array<Type>] (ignored, will be empty)
107
+ # @return [Basic]
108
+ def self.from_typed(value, member_types:) # rubocop:disable Lint/UnusedMethodArgument
109
+ # assert member_types.empty?
110
+ new(value)
111
+ end
112
+ end
113
+
114
+ # A value that has a fixed size (unlike {StringLike}).
115
+ class Fixed < Basic
116
+ def self.fixed?
117
+ true
118
+ end
119
+
120
+ # most Fixed types are valid
121
+ # whatever bits from the wire are used to initialize them
122
+ # @param mode [:plain,:exact]
123
+ def self.from_raw(value, mode:)
124
+ return value if mode == :plain
125
+
126
+ new(value)
127
+ end
128
+
129
+ # @param endianness [:little,:big]
130
+ def marshall(endianness)
131
+ [value].pack(self.class.format[endianness])
132
+ end
133
+ end
134
+
135
+ # {DBus::Data::String}, {DBus::Data::ObjectPath}, or {DBus::Data::Signature}.
136
+ class StringLike < Basic
137
+ def self.fixed?
138
+ false
139
+ end
140
+
141
+ def initialize(value)
142
+ if value.is_a?(self.class)
143
+ value = value.value
144
+ else
145
+ self.class.validate_raw!(value)
146
+ end
147
+
148
+ super(value)
149
+ end
150
+ end
151
+
152
+ # Contains one or more other values.
153
+ class Container < Base
154
+ def self.basic?
155
+ false
156
+ end
157
+
158
+ def self.fixed?
159
+ false
160
+ end
161
+ end
162
+
163
+ # Format strings for String#unpack, both little- and big-endian.
164
+ Format = ::Struct.new(:little, :big)
165
+
166
+ # Represents integers
167
+ class Int < Fixed
168
+ # @param value [::Integer,DBus::Data::Int]
169
+ # @raise RangeError
170
+ def initialize(value)
171
+ value = value.value if value.is_a?(self.class)
172
+ r = self.class.range
173
+ raise RangeError, "#{value.inspect} is not a member of #{r}" unless r.member?(value)
174
+
175
+ super(value)
176
+ end
177
+
178
+ def self.range
179
+ raise NotImplementedError, "Abstract"
180
+ end
181
+ end
182
+
183
+ # Byte.
184
+ #
185
+ # TODO: a specialized ByteArray for `ay` may be useful,
186
+ # to save memory and for natural handling
187
+ class Byte < Int
188
+ def self.type_code
189
+ "y"
190
+ end
191
+
192
+ def self.alignment
193
+ 1
194
+ end
195
+ FORMAT = Format.new("C", "C")
196
+ def self.format
197
+ FORMAT
198
+ end
199
+
200
+ def self.range
201
+ (0..255)
202
+ end
203
+ end
204
+
205
+ # Boolean: encoded as a {UInt32} but only 0 and 1 are valid.
206
+ class Boolean < Fixed
207
+ def self.type_code
208
+ "b"
209
+ end
210
+
211
+ def self.alignment
212
+ 4
213
+ end
214
+ FORMAT = Format.new("L<", "L>")
215
+ def self.format
216
+ FORMAT
217
+ end
218
+
219
+ def self.validate_raw!(value)
220
+ return if [0, 1].member?(value)
221
+
222
+ raise InvalidPacketException, "BOOLEAN must be 0 or 1, found #{value}"
223
+ end
224
+
225
+ def self.from_raw(value, mode:)
226
+ validate_raw!(value)
227
+
228
+ value = value == 1
229
+ return value if mode == :plain
230
+
231
+ new(value)
232
+ end
233
+
234
+ # Accept any *value*, store its Ruby truth value
235
+ # (excepting another instance of this class, where use its {#value}).
236
+ #
237
+ # So new(0).value is true.
238
+ # @param value [::Object,DBus::Data::Boolean]
239
+ def initialize(value)
240
+ value = value.value if value.is_a?(self.class)
241
+ super(value ? true : false)
242
+ end
243
+
244
+ # @param endianness [:little,:big]
245
+ def marshall(endianness)
246
+ int = value ? 1 : 0
247
+ [int].pack(UInt32.format[endianness])
248
+ end
249
+ end
250
+
251
+ # Signed 16 bit integer.
252
+ class Int16 < Int
253
+ def self.type_code
254
+ "n"
255
+ end
256
+
257
+ def self.alignment
258
+ 2
259
+ end
260
+
261
+ FORMAT = Format.new("s<", "s>")
262
+ def self.format
263
+ FORMAT
264
+ end
265
+
266
+ def self.range
267
+ (-32_768..32_767)
268
+ end
269
+ end
270
+
271
+ # Unsigned 16 bit integer.
272
+ class UInt16 < Int
273
+ def self.type_code
274
+ "q"
275
+ end
276
+
277
+ def self.alignment
278
+ 2
279
+ end
280
+
281
+ FORMAT = Format.new("S<", "S>")
282
+ def self.format
283
+ FORMAT
284
+ end
285
+
286
+ def self.range
287
+ (0..65_535)
288
+ end
289
+ end
290
+
291
+ # Signed 32 bit integer.
292
+ class Int32 < Int
293
+ def self.type_code
294
+ "i"
295
+ end
296
+
297
+ def self.alignment
298
+ 4
299
+ end
300
+
301
+ FORMAT = Format.new("l<", "l>")
302
+ def self.format
303
+ FORMAT
304
+ end
305
+
306
+ def self.range
307
+ (-2_147_483_648..2_147_483_647)
308
+ end
309
+ end
310
+
311
+ # Unsigned 32 bit integer.
312
+ class UInt32 < Int
313
+ def self.type_code
314
+ "u"
315
+ end
316
+
317
+ def self.alignment
318
+ 4
319
+ end
320
+
321
+ FORMAT = Format.new("L<", "L>")
322
+ def self.format
323
+ FORMAT
324
+ end
325
+
326
+ def self.range
327
+ (0..4_294_967_295)
328
+ end
329
+ end
330
+
331
+ # Unix file descriptor, not implemented yet.
332
+ class UnixFD < UInt32
333
+ def self.type_code
334
+ "h"
335
+ end
336
+ end
337
+
338
+ # Signed 64 bit integer.
339
+ class Int64 < Int
340
+ def self.type_code
341
+ "x"
342
+ end
343
+
344
+ def self.alignment
345
+ 8
346
+ end
347
+
348
+ FORMAT = Format.new("q<", "q>")
349
+ def self.format
350
+ FORMAT
351
+ end
352
+
353
+ def self.range
354
+ (-9_223_372_036_854_775_808..9_223_372_036_854_775_807)
355
+ end
356
+ end
357
+
358
+ # Unsigned 64 bit integer.
359
+ class UInt64 < Int
360
+ def self.type_code
361
+ "t"
362
+ end
363
+
364
+ def self.alignment
365
+ 8
366
+ end
367
+
368
+ FORMAT = Format.new("Q<", "Q>")
369
+ def self.format
370
+ FORMAT
371
+ end
372
+
373
+ def self.range
374
+ (0..18_446_744_073_709_551_615)
375
+ end
376
+ end
377
+
378
+ # Double-precision floating point number.
379
+ class Double < Fixed
380
+ def self.type_code
381
+ "d"
382
+ end
383
+
384
+ def self.alignment
385
+ 8
386
+ end
387
+
388
+ FORMAT = Format.new("E", "G")
389
+ def self.format
390
+ FORMAT
391
+ end
392
+
393
+ # @param value [#to_f,DBus::Data::Double]
394
+ # @raise TypeError,ArgumentError
395
+ def initialize(value)
396
+ value = value.value if value.is_a?(self.class)
397
+ value = Kernel.Float(value)
398
+ super(value)
399
+ end
400
+ end
401
+
402
+ # UTF-8 encoded string.
403
+ class String < StringLike
404
+ def self.type_code
405
+ "s"
406
+ end
407
+
408
+ def self.alignment
409
+ 4
410
+ end
411
+
412
+ def self.size_class
413
+ UInt32
414
+ end
415
+
416
+ def self.validate_raw!(value)
417
+ value.each_codepoint do |cp|
418
+ raise InvalidPacketException, "Invalid string, contains NUL" if cp.zero?
419
+ end
420
+ rescue ArgumentError
421
+ raise InvalidPacketException, "Invalid string, not in UTF-8"
422
+ end
423
+
424
+ def self.from_raw(value, mode:)
425
+ value.force_encoding(Encoding::UTF_8)
426
+ if mode == :plain
427
+ validate_raw!(value)
428
+ return value
429
+ end
430
+
431
+ new(value)
432
+ end
433
+ end
434
+
435
+ # See also {DBus::ObjectPath}
436
+ class ObjectPath < StringLike
437
+ def self.type_code
438
+ "o"
439
+ end
440
+
441
+ def self.alignment
442
+ 4
443
+ end
444
+
445
+ def self.size_class
446
+ UInt32
447
+ end
448
+
449
+ # @raise InvalidPacketException
450
+ def self.validate_raw!(value)
451
+ DBus::ObjectPath.new(value)
452
+ rescue DBus::Error => e
453
+ raise InvalidPacketException, e.message
454
+ end
455
+
456
+ def self.from_raw(value, mode:)
457
+ if mode == :plain
458
+ validate_raw!(value)
459
+ return value
460
+ end
461
+
462
+ new(value)
463
+ end
464
+ end
465
+
466
+ # Signature string, zero or more single complete types.
467
+ # See also {DBus::Type}
468
+ class Signature < StringLike
469
+ def self.type_code
470
+ "g"
471
+ end
472
+
473
+ def self.alignment
474
+ 1
475
+ end
476
+
477
+ def self.size_class
478
+ Byte
479
+ end
480
+
481
+ # @return [Array<Type>]
482
+ def self.validate_raw!(value)
483
+ DBus.types(value)
484
+ rescue Type::SignatureException => e
485
+ raise InvalidPacketException, "Invalid signature: #{e.message}"
486
+ end
487
+
488
+ def self.from_raw(value, mode:)
489
+ if mode == :plain
490
+ _types = validate_raw!(value)
491
+ return value
492
+ end
493
+
494
+ new(value)
495
+ end
496
+ end
497
+
498
+ # An Array, or a Dictionary (Hash).
499
+ class Array < Container
500
+ def self.type_code
501
+ "a"
502
+ end
503
+
504
+ def self.alignment
505
+ 4
506
+ end
507
+
508
+ # @return [Type]
509
+ attr_reader :member_type
510
+
511
+ def type
512
+ return @type if @type
513
+
514
+ # TODO: reconstructing the type is cumbersome; have #initialize take *type* instead?
515
+ # TODO: or rather add Type::Array[t]
516
+ @type = Type.new("a")
517
+ @type << member_type
518
+ @type
519
+ end
520
+
521
+ # TODO: check that Hash keys are basic types
522
+ # @param mode [:plain,:exact]
523
+ # @param member_type [Type]
524
+ # @param hash [Boolean] are we unmarshalling an ARRAY of DICT_ENTRY
525
+ # @return [Data::Array]
526
+ def self.from_items(value, mode:, member_type:, hash: false)
527
+ value = Hash[value] if hash
528
+ return value if mode == :plain
529
+
530
+ new(value, member_type: member_type)
531
+ end
532
+
533
+ # @param value [::Object]
534
+ # @param member_types [::Array<Type>]
535
+ # @return [Data::Array]
536
+ def self.from_typed(value, member_types:)
537
+ # TODO: validation
538
+ member_type = member_types.first
539
+
540
+ # TODO: Dict??
541
+ items = value.map do |i|
542
+ Data.make_typed(member_type, i)
543
+ end
544
+
545
+ new(items) # initialize(::Array<Data::Base>)
546
+ end
547
+
548
+ # FIXME: should Data::Array be mutable?
549
+ # if it is, is its type mutable too?
550
+
551
+ # TODO: specify type or guess type?
552
+ # Data is the exact type, so its constructor should be exact
553
+ # and guesswork should be clearly labeled
554
+ # @param member_type [SingleCompleteType,Type]
555
+ def initialize(value, member_type:)
556
+ member_type = DBus.type(member_type) unless member_type.is_a?(Type)
557
+ # TODO: copy from another Data::Array
558
+ @member_type = member_type
559
+ @type = nil
560
+ super(value)
561
+ end
562
+ end
563
+
564
+ # A fixed size, heterogenerous tuple.
565
+ #
566
+ # (The item count is fixed, not the byte size.)
567
+ class Struct < Container
568
+ def self.type_code
569
+ "r"
570
+ end
571
+
572
+ def self.alignment
573
+ 8
574
+ end
575
+
576
+ # @return [::Array<Type>]
577
+ attr_reader :member_types
578
+
579
+ def type
580
+ return @type if @type
581
+
582
+ # TODO: reconstructing the type is cumbersome; have #initialize take *type* instead?
583
+ # TODO: or rather add Type::Struct[t1, t2, ...]
584
+ @type = Type.new(self.class.type_code, abstract: true)
585
+ @member_types.each do |member_type|
586
+ @type << member_type
587
+ end
588
+ @type
589
+ end
590
+
591
+ # @param value [::Array]
592
+ def self.from_items(value, mode:, member_types:)
593
+ value.freeze
594
+ return value if mode == :plain
595
+
596
+ new(value, member_types: member_types)
597
+ end
598
+
599
+ # @param value [::Object] (#size, #each)
600
+ # @param member_types [::Array<Type>]
601
+ # @return [Struct]
602
+ def self.from_typed(value, member_types:)
603
+ # TODO: validation
604
+ raise unless value.size == member_types.size
605
+
606
+ @member_types = member_types
607
+
608
+ items = member_types.zip(value).map do |item_type, item|
609
+ Data.make_typed(item_type, item)
610
+ end
611
+
612
+ new(items, member_types: member_types) # initialize(::Array<Data::Base>)
613
+ end
614
+
615
+ def initialize(value, member_types:)
616
+ @member_types = member_types
617
+ @type = nil
618
+ super(value)
619
+ end
620
+ end
621
+
622
+ # A generic type
623
+ class Variant < Container
624
+ def self.type_code
625
+ "v"
626
+ end
627
+
628
+ def self.alignment
629
+ 1
630
+ end
631
+
632
+ # @param member_type [Type]
633
+ def self.from_items(value, mode:, member_type:)
634
+ return value if mode == :plain
635
+
636
+ new(value, member_type: member_type)
637
+ end
638
+
639
+ # @param value [::Object]
640
+ # @param member_types [::Array<Type>]
641
+ # @return [Variant]
642
+ def self.from_typed(value, member_types:) # rubocop:disable Lint/UnusedMethodArgument
643
+ # assert member_types.empty?
644
+
645
+ # decide on type of value
646
+ new(value)
647
+ end
648
+
649
+ # Note that for Variants type=="v",
650
+ # for the specific see {Variant#member_type}
651
+ # @return [Type] the exact type of this value
652
+ def type
653
+ "v"
654
+ end
655
+
656
+ # @return [Type]
657
+ attr_reader :member_type
658
+
659
+ def self.guess_type(value)
660
+ sct, = PacketMarshaller.make_variant(value)
661
+ DBus.type(sct)
662
+ end
663
+
664
+ # @param member_type [Type,nil]
665
+ def initialize(value, member_type:)
666
+ # TODO: validate that the given *member_type* matches *value*
667
+ if value.is_a?(self.class)
668
+ # Copy the contained value instead of boxing it more
669
+ # TODO: except perhaps for round-tripping in exact mode?
670
+ @member_type = value.member_type
671
+ value = value.value
672
+ else
673
+ @member_type = member_type || self.class.guess_type(value)
674
+ end
675
+ super(value)
676
+ end
677
+ end
678
+
679
+ # Dictionary/Hash entry.
680
+ # TODO: shouldn't instantiate?
681
+ class DictEntry < Container
682
+ def self.type_code
683
+ "e"
684
+ end
685
+
686
+ def self.alignment
687
+ 8
688
+ end
689
+
690
+ # @return [::Array<Type>]
691
+ attr_reader :member_types
692
+
693
+ def type
694
+ return @type if @type
695
+
696
+ # TODO: reconstructing the type is cumbersome; have #initialize take *type* instead?
697
+ @type = Type.new(self.class.type_code, abstract: true)
698
+ @member_types.each do |member_type|
699
+ @type << member_type
700
+ end
701
+ @type
702
+ end
703
+
704
+ # @param value [::Array]
705
+ def self.from_items(value, mode:, member_types:) # rubocop:disable Lint/UnusedMethodArgument
706
+ value.freeze
707
+ # DictEntry ignores the :exact mode
708
+ value
709
+ end
710
+
711
+ def initialize(value, member_types:)
712
+ @member_types = member_types
713
+ @type = nil
714
+ super(value)
715
+ end
716
+ end
717
+
718
+ consts = constants.map { |c_sym| const_get(c_sym) }
719
+ classes = consts.find_all { |c| c.respond_to?(:type_code) }
720
+ by_type_code = classes.map { |cl| [cl.type_code, cl] }.to_h
721
+
722
+ # { "b" => Data::Boolean, "s" => Data::String, ...}
723
+ BY_TYPE_CODE = by_type_code
724
+ end
725
+ end
@@ -240,6 +240,7 @@ module DBus
240
240
  class Property
241
241
  # @return [String] The name of the property, for example FooBar.
242
242
  attr_reader :name
243
+ # @return [SingleCompleteType]
243
244
  attr_reader :type
244
245
  # @return [Symbol] :read :write or :readwrite
245
246
  attr_reader :access