ruby-dbus 0.18.0.beta1 → 0.18.0.beta4

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,744 @@
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
+ # @!method self.range
169
+ # @return [Range] the full range of allowed values
170
+
171
+ # @param value [::Integer,DBus::Data::Int]
172
+ # @raise RangeError
173
+ def initialize(value)
174
+ value = value.value if value.is_a?(self.class)
175
+ r = self.class.range
176
+ raise RangeError, "#{value.inspect} is not a member of #{r}" unless r.member?(value)
177
+
178
+ super(value)
179
+ end
180
+ end
181
+
182
+ # Byte.
183
+ #
184
+ # TODO: a specialized ByteArray for `ay` may be useful,
185
+ # to save memory and for natural handling
186
+ class Byte < Int
187
+ def self.type_code
188
+ "y"
189
+ end
190
+
191
+ def self.alignment
192
+ 1
193
+ end
194
+ FORMAT = Format.new("C", "C")
195
+ def self.format
196
+ FORMAT
197
+ end
198
+
199
+ def self.range
200
+ (0..255)
201
+ end
202
+ end
203
+
204
+ # Boolean: encoded as a {UInt32} but only 0 and 1 are valid.
205
+ class Boolean < Fixed
206
+ def self.type_code
207
+ "b"
208
+ end
209
+
210
+ def self.alignment
211
+ 4
212
+ end
213
+ FORMAT = Format.new("L<", "L>")
214
+ def self.format
215
+ FORMAT
216
+ end
217
+
218
+ def self.validate_raw!(value)
219
+ return if [0, 1].member?(value)
220
+
221
+ raise InvalidPacketException, "BOOLEAN must be 0 or 1, found #{value}"
222
+ end
223
+
224
+ def self.from_raw(value, mode:)
225
+ validate_raw!(value)
226
+
227
+ value = value == 1
228
+ return value if mode == :plain
229
+
230
+ new(value)
231
+ end
232
+
233
+ # Accept any *value*, store its Ruby truth value
234
+ # (excepting another instance of this class, where use its {#value}).
235
+ #
236
+ # So new(0).value is true.
237
+ # @param value [::Object,DBus::Data::Boolean]
238
+ def initialize(value)
239
+ value = value.value if value.is_a?(self.class)
240
+ super(value ? true : false)
241
+ end
242
+
243
+ # @param endianness [:little,:big]
244
+ def marshall(endianness)
245
+ int = value ? 1 : 0
246
+ [int].pack(UInt32.format[endianness])
247
+ end
248
+ end
249
+
250
+ # Signed 16 bit integer.
251
+ class Int16 < Int
252
+ def self.type_code
253
+ "n"
254
+ end
255
+
256
+ def self.alignment
257
+ 2
258
+ end
259
+
260
+ FORMAT = Format.new("s<", "s>")
261
+ def self.format
262
+ FORMAT
263
+ end
264
+
265
+ def self.range
266
+ (-32_768..32_767)
267
+ end
268
+ end
269
+
270
+ # Unsigned 16 bit integer.
271
+ class UInt16 < Int
272
+ def self.type_code
273
+ "q"
274
+ end
275
+
276
+ def self.alignment
277
+ 2
278
+ end
279
+
280
+ FORMAT = Format.new("S<", "S>")
281
+ def self.format
282
+ FORMAT
283
+ end
284
+
285
+ def self.range
286
+ (0..65_535)
287
+ end
288
+ end
289
+
290
+ # Signed 32 bit integer.
291
+ class Int32 < Int
292
+ def self.type_code
293
+ "i"
294
+ end
295
+
296
+ def self.alignment
297
+ 4
298
+ end
299
+
300
+ FORMAT = Format.new("l<", "l>")
301
+ def self.format
302
+ FORMAT
303
+ end
304
+
305
+ def self.range
306
+ (-2_147_483_648..2_147_483_647)
307
+ end
308
+ end
309
+
310
+ # Unsigned 32 bit integer.
311
+ class UInt32 < Int
312
+ def self.type_code
313
+ "u"
314
+ end
315
+
316
+ def self.alignment
317
+ 4
318
+ end
319
+
320
+ FORMAT = Format.new("L<", "L>")
321
+ def self.format
322
+ FORMAT
323
+ end
324
+
325
+ def self.range
326
+ (0..4_294_967_295)
327
+ end
328
+ end
329
+
330
+ # Unix file descriptor, not implemented yet.
331
+ class UnixFD < UInt32
332
+ def self.type_code
333
+ "h"
334
+ end
335
+ end
336
+
337
+ # Signed 64 bit integer.
338
+ class Int64 < Int
339
+ def self.type_code
340
+ "x"
341
+ end
342
+
343
+ def self.alignment
344
+ 8
345
+ end
346
+
347
+ FORMAT = Format.new("q<", "q>")
348
+ def self.format
349
+ FORMAT
350
+ end
351
+
352
+ def self.range
353
+ (-9_223_372_036_854_775_808..9_223_372_036_854_775_807)
354
+ end
355
+ end
356
+
357
+ # Unsigned 64 bit integer.
358
+ class UInt64 < Int
359
+ def self.type_code
360
+ "t"
361
+ end
362
+
363
+ def self.alignment
364
+ 8
365
+ end
366
+
367
+ FORMAT = Format.new("Q<", "Q>")
368
+ def self.format
369
+ FORMAT
370
+ end
371
+
372
+ def self.range
373
+ (0..18_446_744_073_709_551_615)
374
+ end
375
+ end
376
+
377
+ # Double-precision floating point number.
378
+ class Double < Fixed
379
+ def self.type_code
380
+ "d"
381
+ end
382
+
383
+ def self.alignment
384
+ 8
385
+ end
386
+
387
+ FORMAT = Format.new("E", "G")
388
+ def self.format
389
+ FORMAT
390
+ end
391
+
392
+ # @param value [#to_f,DBus::Data::Double]
393
+ # @raise TypeError,ArgumentError
394
+ def initialize(value)
395
+ value = value.value if value.is_a?(self.class)
396
+ value = Kernel.Float(value)
397
+ super(value)
398
+ end
399
+ end
400
+
401
+ # UTF-8 encoded string.
402
+ class String < StringLike
403
+ def self.type_code
404
+ "s"
405
+ end
406
+
407
+ def self.alignment
408
+ 4
409
+ end
410
+
411
+ def self.size_class
412
+ UInt32
413
+ end
414
+
415
+ def self.validate_raw!(value)
416
+ value.each_codepoint do |cp|
417
+ raise InvalidPacketException, "Invalid string, contains NUL" if cp.zero?
418
+ end
419
+ rescue ArgumentError
420
+ raise InvalidPacketException, "Invalid string, not in UTF-8"
421
+ end
422
+
423
+ def self.from_raw(value, mode:)
424
+ value.force_encoding(Encoding::UTF_8)
425
+ if mode == :plain
426
+ validate_raw!(value)
427
+ return value
428
+ end
429
+
430
+ new(value)
431
+ end
432
+ end
433
+
434
+ # See also {DBus::ObjectPath}
435
+ class ObjectPath < StringLike
436
+ def self.type_code
437
+ "o"
438
+ end
439
+
440
+ def self.alignment
441
+ 4
442
+ end
443
+
444
+ def self.size_class
445
+ UInt32
446
+ end
447
+
448
+ # @raise InvalidPacketException
449
+ def self.validate_raw!(value)
450
+ DBus::ObjectPath.new(value)
451
+ rescue DBus::Error => e
452
+ raise InvalidPacketException, e.message
453
+ end
454
+
455
+ def self.from_raw(value, mode:)
456
+ if mode == :plain
457
+ validate_raw!(value)
458
+ return value
459
+ end
460
+
461
+ new(value)
462
+ end
463
+ end
464
+
465
+ # Signature string, zero or more single complete types.
466
+ # See also {DBus::Type}
467
+ class Signature < StringLike
468
+ def self.type_code
469
+ "g"
470
+ end
471
+
472
+ def self.alignment
473
+ 1
474
+ end
475
+
476
+ def self.size_class
477
+ Byte
478
+ end
479
+
480
+ # @return [Array<Type>]
481
+ def self.validate_raw!(value)
482
+ DBus.types(value)
483
+ rescue Type::SignatureException => e
484
+ raise InvalidPacketException, "Invalid signature: #{e.message}"
485
+ end
486
+
487
+ def self.from_raw(value, mode:)
488
+ if mode == :plain
489
+ _types = validate_raw!(value)
490
+ return value
491
+ end
492
+
493
+ new(value)
494
+ end
495
+ end
496
+
497
+ # An Array, or a Dictionary (Hash).
498
+ class Array < Container
499
+ def self.type_code
500
+ "a"
501
+ end
502
+
503
+ def self.alignment
504
+ 4
505
+ end
506
+
507
+ # @return [Type]
508
+ attr_reader :member_type
509
+
510
+ def type
511
+ return @type if @type
512
+
513
+ # TODO: reconstructing the type is cumbersome; have #initialize take *type* instead?
514
+ # TODO: or rather add Type::Array[t]
515
+ @type = Type.new("a")
516
+ @type << member_type
517
+ @type
518
+ end
519
+
520
+ # TODO: check that Hash keys are basic types
521
+ # @param mode [:plain,:exact]
522
+ # @param member_type [Type]
523
+ # @param hash [Boolean] are we unmarshalling an ARRAY of DICT_ENTRY
524
+ # @return [Data::Array]
525
+ def self.from_items(value, mode:, member_type:, hash: false)
526
+ value = Hash[value] if hash
527
+ return value if mode == :plain
528
+
529
+ new(value, member_type: member_type)
530
+ end
531
+
532
+ # @param value [::Object]
533
+ # @param member_types [::Array<Type>]
534
+ # @return [Data::Array]
535
+ def self.from_typed(value, member_types:)
536
+ # TODO: validation
537
+ member_type = member_types.first
538
+
539
+ # TODO: Dict??
540
+ items = value.map do |i|
541
+ Data.make_typed(member_type, i)
542
+ end
543
+
544
+ new(items, member_type: member_type) # initialize(::Array<Data::Base>)
545
+ end
546
+
547
+ # FIXME: should Data::Array be mutable?
548
+ # if it is, is its type mutable too?
549
+
550
+ # TODO: specify type or guess type?
551
+ # Data is the exact type, so its constructor should be exact
552
+ # and guesswork should be clearly labeled
553
+ # @param member_type [SingleCompleteType,Type]
554
+ def initialize(value, member_type:)
555
+ member_type = DBus.type(member_type) unless member_type.is_a?(Type)
556
+ # TODO: copy from another Data::Array
557
+ @member_type = member_type
558
+ @type = nil
559
+ super(value)
560
+ end
561
+ end
562
+
563
+ # A fixed size, heterogenerous tuple.
564
+ #
565
+ # (The item count is fixed, not the byte size.)
566
+ class Struct < Container
567
+ def self.type_code
568
+ "r"
569
+ end
570
+
571
+ def self.alignment
572
+ 8
573
+ end
574
+
575
+ # @return [::Array<Type>]
576
+ attr_reader :member_types
577
+
578
+ def type
579
+ return @type if @type
580
+
581
+ # TODO: reconstructing the type is cumbersome; have #initialize take *type* instead?
582
+ # TODO: or rather add Type::Struct[t1, t2, ...]
583
+ @type = Type.new(self.class.type_code, abstract: true)
584
+ @member_types.each do |member_type|
585
+ @type << member_type
586
+ end
587
+ @type
588
+ end
589
+
590
+ # @param value [::Array]
591
+ def self.from_items(value, mode:, member_types:)
592
+ value.freeze
593
+ return value if mode == :plain
594
+
595
+ new(value, member_types: member_types)
596
+ end
597
+
598
+ # @param value [::Object] (#size, #each)
599
+ # @param member_types [::Array<Type>]
600
+ # @return [Struct]
601
+ def self.from_typed(value, member_types:)
602
+ # TODO: validation
603
+ raise unless value.size == member_types.size
604
+
605
+ items = member_types.zip(value).map do |item_type, item|
606
+ Data.make_typed(item_type, item)
607
+ end
608
+
609
+ new(items, member_types: member_types) # initialize(::Array<Data::Base>)
610
+ end
611
+
612
+ def initialize(value, member_types:)
613
+ @member_types = member_types
614
+ @type = nil
615
+ super(value)
616
+ end
617
+ end
618
+
619
+ # A generic type
620
+ class Variant < Container
621
+ def self.type_code
622
+ "v"
623
+ end
624
+
625
+ def self.alignment
626
+ 1
627
+ end
628
+
629
+ # @param member_type [Type]
630
+ def self.from_items(value, mode:, member_type:)
631
+ return value if mode == :plain
632
+
633
+ new(value, member_type: member_type)
634
+ end
635
+
636
+ # @param value [::Object]
637
+ # @param member_types [::Array<Type>]
638
+ # @return [Variant]
639
+ def self.from_typed(value, member_types:) # rubocop:disable Lint/UnusedMethodArgument
640
+ # assert member_types.empty?
641
+
642
+ # decide on type of value
643
+ new(value, member_type: nil)
644
+ end
645
+
646
+ # @return [Type]
647
+ def self.type
648
+ # memoize
649
+ @type ||= Type.new(type_code).freeze
650
+ end
651
+
652
+ # Note that for Variants type.to_s=="v",
653
+ # for the specific see {Variant#member_type}
654
+ # @return [Type] the exact type of this value
655
+ def type
656
+ self.class.type
657
+ end
658
+
659
+ # @return [Type]
660
+ attr_reader :member_type
661
+
662
+ def self.guess_type(value)
663
+ sct, = PacketMarshaller.make_variant(value)
664
+ DBus.type(sct)
665
+ end
666
+
667
+ # @param member_type [Type,nil]
668
+ def initialize(value, member_type:)
669
+ # TODO: validate that the given *member_type* matches *value*
670
+ if value.is_a?(self.class)
671
+ # Copy the contained value instead of boxing it more
672
+ # TODO: except perhaps for round-tripping in exact mode?
673
+ @member_type = value.member_type
674
+ value = value.value
675
+ else
676
+ @member_type = member_type || self.class.guess_type(value)
677
+ end
678
+ super(value)
679
+ end
680
+ end
681
+
682
+ # Dictionary/Hash entry.
683
+ # TODO: shouldn't instantiate?
684
+ class DictEntry < Container
685
+ def self.type_code
686
+ "e"
687
+ end
688
+
689
+ def self.alignment
690
+ 8
691
+ end
692
+
693
+ # @return [::Array<Type>]
694
+ attr_reader :member_types
695
+
696
+ def type
697
+ return @type if @type
698
+
699
+ # TODO: reconstructing the type is cumbersome; have #initialize take *type* instead?
700
+ @type = Type.new(self.class.type_code, abstract: true)
701
+ @member_types.each do |member_type|
702
+ @type << member_type
703
+ end
704
+ @type
705
+ end
706
+
707
+ # @param value [::Array]
708
+ def self.from_items(value, mode:, member_types:) # rubocop:disable Lint/UnusedMethodArgument
709
+ value.freeze
710
+ # DictEntry ignores the :exact mode
711
+ value
712
+ end
713
+
714
+ # @param value [::Object] (#size, #each)
715
+ # @param member_types [::Array<Type>]
716
+ # @return [DictEntry]
717
+ def self.from_typed(value, member_types:)
718
+ # assert member_types.size == 2
719
+ # TODO: duplicated from Struct. Inherit/delegate?
720
+ # TODO: validation
721
+ raise unless value.size == member_types.size
722
+
723
+ items = member_types.zip(value).map do |item_type, item|
724
+ Data.make_typed(item_type, item)
725
+ end
726
+
727
+ new(items, member_types: member_types) # initialize(::Array<Data::Base>)
728
+ end
729
+
730
+ def initialize(value, member_types:)
731
+ @member_types = member_types
732
+ @type = nil
733
+ super(value)
734
+ end
735
+ end
736
+
737
+ consts = constants.map { |c_sym| const_get(c_sym) }
738
+ classes = consts.find_all { |c| c.respond_to?(:type_code) }
739
+ by_type_code = classes.map { |cl| [cl.type_code, cl] }.to_h
740
+
741
+ # { "b" => Data::Boolean, "s" => Data::String, ...}
742
+ BY_TYPE_CODE = by_type_code
743
+ end
744
+ end