nl 0.2.4 → 0.4.1

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.
Files changed (72) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +21 -0
  3. data/Rakefile +7 -0
  4. data/lib/nl/async/dispatcher.rb +199 -0
  5. data/lib/nl/async/driver.rb +37 -0
  6. data/lib/nl/async/mailbox.rb +81 -0
  7. data/lib/nl/async/operation.rb +277 -0
  8. data/lib/nl/async.rb +13 -0
  9. data/lib/nl/attribute_set.rb +173 -0
  10. data/lib/nl/bitfield32.rb +86 -0
  11. data/lib/nl/blocking_transport.rb +131 -0
  12. data/lib/nl/connection.rb +61 -0
  13. data/lib/nl/datagram.rb +22 -0
  14. data/lib/nl/datatypes.rb +454 -0
  15. data/lib/nl/decoder.rb +13 -1
  16. data/lib/nl/endian.rb +10 -0
  17. data/lib/nl/error.rb +10 -0
  18. data/lib/nl/exchange.rb +124 -0
  19. data/lib/nl/family.rb +117 -23
  20. data/lib/nl/genl/client.rb +104 -0
  21. data/lib/nl/genl/protocol.rb +59 -0
  22. data/lib/nl/genl/wire.rb +94 -0
  23. data/lib/nl/genl.rb +60 -73
  24. data/lib/nl/notification.rb +112 -0
  25. data/lib/nl/notification_router.rb +92 -0
  26. data/lib/nl/raw/client.rb +83 -0
  27. data/lib/nl/raw/protocol.rb +112 -0
  28. data/lib/nl/raw/wire.rb +146 -0
  29. data/lib/nl/raw.rb +104 -0
  30. data/lib/nl/sequence_allocator.rb +34 -0
  31. data/lib/nl/socket.rb +15 -12
  32. data/lib/nl/structured_payload.rb +61 -0
  33. data/lib/nl/sub_message.rb +94 -0
  34. data/lib/nl/version.rb +1 -1
  35. data/lib/nl.rb +10 -12
  36. data/sig/generated/nl/async/dispatcher.rbs +53 -0
  37. data/sig/generated/nl/async/driver.rbs +21 -0
  38. data/sig/generated/nl/async/mailbox.rbs +32 -0
  39. data/sig/generated/nl/async/operation.rbs +123 -0
  40. data/sig/generated/nl/async.rbs +9 -0
  41. data/sig/generated/nl/attribute_set.rbs +50 -0
  42. data/sig/generated/nl/bitfield32.rbs +41 -0
  43. data/sig/generated/nl/blocking_transport.rbs +33 -0
  44. data/sig/generated/nl/connection.rbs +26 -0
  45. data/sig/generated/nl/datagram.rbs +11 -0
  46. data/sig/generated/nl/datatypes.rbs +181 -0
  47. data/sig/generated/nl/decoder.rbs +38 -0
  48. data/sig/generated/nl/encoder.rbs +29 -0
  49. data/sig/generated/nl/endian.rbs +28 -0
  50. data/sig/generated/nl/error.rbs +27 -0
  51. data/sig/generated/nl/exchange.rbs +75 -0
  52. data/sig/generated/nl/family.rbs +71 -0
  53. data/sig/generated/nl/genl/client.rbs +74 -0
  54. data/sig/generated/nl/genl/protocol.rbs +37 -0
  55. data/sig/generated/nl/genl/wire.rbs +113 -0
  56. data/sig/generated/nl/genl.rbs +50 -0
  57. data/sig/generated/nl/notification.rbs +53 -0
  58. data/sig/generated/nl/notification_router.rbs +30 -0
  59. data/sig/generated/nl/raw/client.rbs +50 -0
  60. data/sig/generated/nl/raw/protocol.rbs +122 -0
  61. data/sig/generated/nl/raw/wire.rbs +151 -0
  62. data/sig/generated/nl/raw.rbs +70 -0
  63. data/sig/generated/nl/sequence_allocator.rbs +15 -0
  64. data/sig/generated/nl/socket.rbs +40 -0
  65. data/sig/generated/nl/structured_payload.rbs +25 -0
  66. data/sig/generated/nl/sub_message.rbs +74 -0
  67. data/sig/generated/nl/version.rbs +5 -0
  68. data/sig/generated/nl.rbs +2 -0
  69. metadata +65 -9
  70. data/lib/nl/core.rb +0 -94
  71. data/lib/nl/protocols/genl.rb +0 -55
  72. data/lib/nl/protocols/raw.rb +0 -395
@@ -0,0 +1,454 @@
1
+ require_relative 'raw/wire'
2
+ require_relative 'bitfield32'
3
+
4
+ module Nl
5
+ # @rbs!
6
+ # interface _DataType
7
+ # def encode: (Encoder, untyped, ?context: untyped) -> void
8
+ # def decode: (Decoder, ?context: untyped, ?nlattr_type_flags: Integer) -> untyped
9
+ # def nlattr_type_flags: (?untyped, ?context: untyped) -> Integer
10
+ # def coerce: (untyped, ?context: untyped) -> untyped
11
+ # end
12
+
13
+ module DataTypes
14
+ class Base
15
+ def initialize(check: nil)
16
+ @check = check
17
+ end
18
+
19
+ def nlattr_type_flags(_value = nil, context: nil)
20
+ 0
21
+ end
22
+
23
+ def coerce(value, context: nil)
24
+ value
25
+ end
26
+
27
+ private def checked(value)
28
+ @check&.call(value)
29
+ value
30
+ end
31
+ end
32
+
33
+ class Scalar < Base
34
+ def initialize(type, check:)
35
+ super(check:)
36
+ @type = type
37
+ end
38
+
39
+ def encode(encoder, value, context: nil)
40
+ encoder.put_value(@type, checked(value || 0))
41
+ end
42
+
43
+ def decode(decoder, context: nil, nlattr_type_flags: 0)
44
+ checked(decoder.get_value(@type))
45
+ end
46
+ end
47
+
48
+ class VariableInteger < Base
49
+ UINT32_RANGE = (0...2**32)
50
+ UINT64_RANGE = (0...2**64)
51
+ SINT32_RANGE = (-(2**31)...2**31)
52
+ SINT64_RANGE = (-(2**63)...2**63)
53
+
54
+ def initialize(byte_order, signed:, check:)
55
+ super(check:)
56
+ prefix = signed ? 'S' : 'U'
57
+ @type32 = byte_order.const_get("#{prefix}32")
58
+ @type64 = byte_order.const_get("#{prefix}64")
59
+ @range32 = signed ? SINT32_RANGE : UINT32_RANGE
60
+ @range64 = signed ? SINT64_RANGE : UINT64_RANGE
61
+ end
62
+
63
+ def encode(encoder, value, context: nil)
64
+ value = checked(value || 0)
65
+ unless @range64.cover?(value)
66
+ raise RangeError, "integer #{value.inspect} is outside the #{@range64.begin}...#{@range64.end} range"
67
+ end
68
+
69
+ type = @range32.cover?(value) ? @type32 : @type64
70
+ encoder.put_value(type, value)
71
+ end
72
+
73
+ def decode(decoder, context: nil, nlattr_type_flags: 0)
74
+ type = case decoder.remaining
75
+ when 4 then @type32
76
+ when 8 then @type64
77
+ else
78
+ raise Decoder::Error, "variable integer payload must be 4 or 8 bytes, got #{decoder.remaining}"
79
+ end
80
+ checked(decoder.get_value(type))
81
+ end
82
+ end
83
+
84
+ class String < Base
85
+ def initialize(check:)
86
+ super(check:)
87
+ end
88
+
89
+ def encode(encoder, value, context: nil)
90
+ encoder.put_zstring(checked(value))
91
+ end
92
+
93
+ def decode(decoder, context: nil, nlattr_type_flags: 0)
94
+ checked(decoder.get_zstring)
95
+ end
96
+ end
97
+
98
+ class Binary < Base
99
+ def initialize(length: nil, check:)
100
+ super(check:)
101
+ @length = length
102
+ end
103
+
104
+ def encode(encoder, value, context: nil)
105
+ value = checked(value)
106
+ if @length && value.bytesize != @length
107
+ raise ArgumentError, "binary value must be exactly #{@length} bytes, got #{value.bytesize}"
108
+ end
109
+ encoder.put_string(value)
110
+ end
111
+
112
+ def decode(decoder, context: nil, nlattr_type_flags: 0)
113
+ checked(decoder.get_string(@length || decoder.remaining))
114
+ end
115
+ end
116
+
117
+ # An array whose elements are packed consecutively without per-element
118
+ # headers or alignment padding.
119
+ class PackedArray < Base
120
+ def initialize(sub_type, check: nil)
121
+ super(check:)
122
+ @sub_type = sub_type
123
+ end
124
+
125
+ def encode(encoder, values, context: nil)
126
+ values = coerce(values, context:)
127
+ if @check
128
+ temporary = Encoder.new
129
+ values.each { @sub_type.encode(temporary, it, context:) }
130
+ encoder.put_string(checked(temporary.buffer.get_string))
131
+ else
132
+ values.each { @sub_type.encode(encoder, it, context:) }
133
+ end
134
+ end
135
+
136
+ def coerce(values, context: nil)
137
+ values.map { @sub_type.coerce(it, context:) }
138
+ end
139
+
140
+ def decode(decoder, context: nil, nlattr_type_flags: 0)
141
+ if @check
142
+ payload = checked(decoder.get_string)
143
+ decoder = Decoder.new(IO::Buffer.for(payload))
144
+ end
145
+
146
+ result = []
147
+ result << @sub_type.decode(decoder, context:, nlattr_type_flags: 0) while decoder.available?
148
+ result
149
+ end
150
+ end
151
+
152
+ class Struct < Base
153
+ def initialize(type, check: nil, consume_remaining: false)
154
+ super(check:)
155
+ @type = type
156
+ @consume_remaining = consume_remaining
157
+ end
158
+
159
+ def coerce(value, context: nil)
160
+ return value unless value.is_a?(Hash)
161
+
162
+ unknown = value.keys - @type::MEMBERS.keys
163
+ raise ArgumentError, "unknown struct members: #{unknown.join(', ')}" unless unknown.empty?
164
+
165
+ @type.new(*@type::MEMBERS.map { |name, datatype| datatype.coerce(value[name], context:) })
166
+ end
167
+
168
+ def encode(encoder, value, context: nil)
169
+ unless value.is_a?(@type)
170
+ raise TypeError, "value must be an instance of #{@type}"
171
+ end
172
+
173
+ if @check
174
+ temporary = Encoder.new
175
+ value.encode(temporary)
176
+ encoder.put_string(checked(temporary.buffer.get_string))
177
+ else
178
+ value.encode(encoder)
179
+ end
180
+ end
181
+
182
+ def decode(decoder, context: nil, nlattr_type_flags: 0)
183
+ unless @check
184
+ value = @type.decode(decoder)
185
+ decoder.skip if @consume_remaining
186
+ return value
187
+ end
188
+
189
+ payload = checked(decoder.get_string)
190
+ nested = Decoder.new(IO::Buffer.for(payload))
191
+ nested.limit(payload.bytesize) do
192
+ value = @type.decode(it)
193
+ it.skip if @consume_remaining
194
+ value
195
+ end
196
+ end
197
+ end
198
+
199
+ class Flag < Base
200
+ def encode(encoder, value, context: nil)
201
+ # flag attribute has no payload; presence encodes true
202
+ end
203
+
204
+ def decode(decoder, context: nil, nlattr_type_flags: 0)
205
+ true
206
+ end
207
+ end
208
+
209
+ class Bitfield32 < Base
210
+ def encode(encoder, value, context: nil)
211
+ unless value.is_a?(Nl::Bitfield32)
212
+ raise TypeError, 'bitfield32 value must be an Nl::Bitfield32'
213
+ end
214
+
215
+ encoder.put_value(Endian::Host::U32, value.value)
216
+ encoder.put_value(Endian::Host::U32, value.selector)
217
+ end
218
+
219
+ def decode(decoder, context: nil, nlattr_type_flags: 0)
220
+ value = decoder.get_value(Endian::Host::U32)
221
+ selector = decoder.get_value(Endian::Host::U32)
222
+ Nl::Bitfield32.new(value, selector)
223
+ end
224
+ end
225
+
226
+ class Pad < Base
227
+ def initialize(length = nil)
228
+ @length = length
229
+ end
230
+
231
+ def encode(encoder, _value, context: nil)
232
+ encoder.put_string(?\0.b * @length) if @length
233
+ end
234
+
235
+ def decode(decoder, context: nil, nlattr_type_flags: 0)
236
+ @length ? decoder.skip(@length) : decoder.skip
237
+ nil
238
+ end
239
+ end
240
+
241
+ class NestedAttributes < Base
242
+ def initialize(attribute_set, selector_bindings: [])
243
+ @attribute_set = attribute_set
244
+ @selector_bindings = selector_bindings
245
+ end
246
+
247
+ def nlattr_type_flags(_value = nil, context: nil)
248
+ Raw::NLA_F_NESTED
249
+ end
250
+
251
+ def coerce(value, context: nil)
252
+ value.is_a?(Hash) ? @attribute_set.build_attributes(value, external_selectors: selectors(context)) : value
253
+ end
254
+
255
+ def encode(encoder, value, context: nil)
256
+ unless value.is_a?(@attribute_set)
257
+ raise TypeError, "value must be an instance of #{@attribute_set}"
258
+ end
259
+
260
+ value.encode(encoder, external_selectors: selectors(context))
261
+ end
262
+
263
+ def decode(decoder, context: nil, nlattr_type_flags: 0)
264
+ @attribute_set.decode(decoder, external_selectors: selectors(context))
265
+ end
266
+
267
+ private def selectors(context)
268
+ @selector_bindings.map { it.read(context) }
269
+ end
270
+ end
271
+
272
+ class SubMessage < Base
273
+ Format = Data.define(:type, :selector_bindings, :nested)
274
+
275
+ def initialize(selector, formats)
276
+ @selector = selector
277
+ @formats = formats.transform_values do |format|
278
+ format.is_a?(Format) ? format : Format.new(*format)
279
+ end.freeze
280
+ end
281
+
282
+ def nlattr_type_flags(value = nil, context:)
283
+ if value.is_a?(RawSubMessage)
284
+ @selector.read(context)
285
+ return value.nlattr_type_flags
286
+ end
287
+
288
+ format = @selector.select(context, @formats)
289
+ unless value.is_a?(format.type)
290
+ raise TypeError, "value must be an instance of #{format.type}"
291
+ end
292
+ format.nested ? Raw::NLA_F_NESTED : 0
293
+ end
294
+
295
+ def coerce(value, context:)
296
+ if value.is_a?(RawSubMessage)
297
+ @selector.read(context) # ensure selector value is present
298
+ return value
299
+ end
300
+
301
+ format = @selector.select(context, @formats)
302
+ return value if value.is_a?(format.type)
303
+ return format.type.from_params(value, external_selectors: selectors(format, context)) if value.is_a?(Hash)
304
+
305
+ raise TypeError, "value must be a Hash or an instance of #{format.type}"
306
+ end
307
+
308
+ def encode(encoder, value, context: nil)
309
+ if value.is_a?(RawSubMessage)
310
+ @selector.read(context)
311
+ return value.encode(encoder)
312
+ end
313
+
314
+ format = @selector.select(context, @formats)
315
+ unless value.is_a?(format.type)
316
+ raise TypeError, "value must be an instance of #{format.type}"
317
+ end
318
+ value.encode(encoder, external_selectors: selectors(format, context))
319
+ end
320
+
321
+ def decode(decoder, context:, nlattr_type_flags: 0)
322
+ format = @selector.select(context, @formats) do
323
+ return RawSubMessage.new(
324
+ decoder.get_string,
325
+ nlattr_type_flags:,
326
+ )
327
+ end
328
+ format.type.decode(decoder, external_selectors: selectors(format, context))
329
+ end
330
+
331
+ private def selectors(format, context)
332
+ format.selector_bindings.map { it.read(context) }
333
+ end
334
+ end
335
+
336
+ # Nested attributes whose type numbers are keys rather than members of
337
+ # an attribute set. Each level is returned as an Integer-keyed Hash;
338
+ # values at the innermost level are decoded as @attribute_set.
339
+ class NestTypeValue < Base
340
+ def initialize(attribute_set, levels, selector_bindings: [])
341
+ raise ArgumentError, 'levels must be positive' unless levels.positive?
342
+
343
+ @attribute_set = attribute_set
344
+ @levels = levels
345
+ @selector_bindings = selector_bindings
346
+ end
347
+
348
+ def encode(encoder, value, context: nil)
349
+ encode_level(encoder, value, @levels, selectors(context))
350
+ end
351
+
352
+ def coerce(value = nil, context: nil, **keywords)
353
+ value = keywords unless keywords.empty?
354
+ coerce_level(value, @levels, selectors(context))
355
+ end
356
+
357
+ def decode(decoder, context: nil, nlattr_type_flags: 0)
358
+ decode_level(decoder, @levels, selectors(context))
359
+ end
360
+
361
+ private def encode_level(encoder, values, levels, external_selectors)
362
+ values.each do |type, value|
363
+ nlattr = Raw::NlAttr.new(0, type | Raw::NLA_F_NESTED)
364
+ encoder.measure(Endian::Host::U16) do
365
+ nlattr.encode(encoder)
366
+ if levels == 1
367
+ if external_selectors.empty?
368
+ value.encode(encoder)
369
+ else
370
+ value.encode(encoder, external_selectors:)
371
+ end
372
+ else
373
+ encode_level(encoder, value, levels - 1, external_selectors)
374
+ end
375
+ end
376
+ encoder.align_to(Raw::NLA_ALIGNTO)
377
+ end
378
+ end
379
+
380
+ private def coerce_level(values, levels, external_selectors)
381
+ values.transform_values do |value|
382
+ if levels == 1
383
+ if value.is_a?(Hash)
384
+ external_selectors.empty? ? @attribute_set.build_attributes(value) :
385
+ @attribute_set.build_attributes(value, external_selectors:)
386
+ else
387
+ value
388
+ end
389
+ else
390
+ coerce_level(value, levels - 1, external_selectors)
391
+ end
392
+ end
393
+ end
394
+
395
+ private def decode_level(decoder, levels, external_selectors)
396
+ result = {}
397
+ while decoder.available?(Raw::NLA_HDRLEN)
398
+ nlattr = Raw::NlAttr.decode(decoder)
399
+ type = nlattr.type & Raw::NLA_TYPE_MASK
400
+ value = decoder.limit(nlattr.len - Raw::NLA_HDRLEN) do
401
+ if levels == 1
402
+ external_selectors.empty? ? @attribute_set.decode(decoder) :
403
+ @attribute_set.decode(decoder, external_selectors:)
404
+ else
405
+ decode_level(decoder, levels - 1, external_selectors)
406
+ end
407
+ end
408
+ decoder.align_to(Raw::NLA_ALIGNTO)
409
+ result[type] = value
410
+ end
411
+ result
412
+ end
413
+
414
+ private def selectors(context)
415
+ @selector_bindings.map { it.read(context) }
416
+ end
417
+ end
418
+
419
+ class IndexedArray < Base
420
+ def initialize(sub_type)
421
+ @sub_type = sub_type
422
+ end
423
+
424
+ def encode(encoder, values, context: nil)
425
+ values.each_with_index do |value, i|
426
+ nlattr = Raw::NlAttr.new(0, i + 1)
427
+ encoder.measure(Endian::Host::U16) do
428
+ nlattr.encode(encoder)
429
+ @sub_type.encode(encoder, value, context:)
430
+ end
431
+ encoder.align_to(Raw::NLA_ALIGNTO)
432
+ end
433
+ end
434
+
435
+ def coerce(values, context: nil)
436
+ values.map { @sub_type.coerce(it, context:) }
437
+ end
438
+
439
+ def decode(decoder, context: nil, nlattr_type_flags: 0)
440
+ result = []
441
+ while decoder.available?
442
+ nlattr = Raw::NlAttr.decode(decoder)
443
+ flags = nlattr.type & (Raw::NLA_F_NESTED | Raw::NLA_F_NET_BYTEORDER)
444
+ element = decoder.limit(nlattr.len - Raw::NLA_HDRLEN) do
445
+ @sub_type.decode(decoder, context:, nlattr_type_flags: flags)
446
+ end
447
+ decoder.align_to(Raw::NLA_ALIGNTO)
448
+ result << element
449
+ end
450
+ result
451
+ end
452
+ end
453
+ end
454
+ end
data/lib/nl/decoder.rb CHANGED
@@ -18,6 +18,10 @@ module Nl
18
18
  @position + size <= @limit
19
19
  end
20
20
 
21
+ def remaining
22
+ @limit - @position
23
+ end
24
+
21
25
  def limit(size)
22
26
  orig_limit = @limit
23
27
  @limit = @position + size
@@ -38,12 +42,20 @@ module Nl
38
42
 
39
43
  def get_string(length = @limit - @position)
40
44
  nposition = @position + length
41
- raise OutOfBounds if nposition > @limit
45
+ raise OutOfBounds if length.negative? || nposition > @limit
42
46
  value = @buffer.get_string(@position, length)
43
47
  @position = nposition
44
48
  value
45
49
  end
46
50
 
51
+ def get_buffer(length = @limit - @position)
52
+ nposition = @position + length
53
+ raise OutOfBounds if length.negative? || nposition > @limit
54
+ value = @buffer.slice(@position, length)
55
+ @position = nposition
56
+ value
57
+ end
58
+
47
59
  def get_zstring(unterminated_ok: false)
48
60
  nposition = @position
49
61
  nul_found = false
data/lib/nl/endian.rb CHANGED
@@ -1,17 +1,27 @@
1
+ # Byte-order helpers
2
+
1
3
  module Nl
2
4
  module Endian
5
+ # sizeof(int)
3
6
  SIZEOF_INT = [1].pack('i!').bytesize
7
+ # sizeof(long)
4
8
  SIZEOF_LONG = [1].pack('l!').bytesize
9
+ # sizeof(long long)
5
10
  SIZEOF_LLONG = [1].pack('q!').bytesize
6
11
 
12
+ # Little-endian scalar types.
7
13
  module Little
8
14
  U8, S8, U16, S16, U32, S32, U64, S64, F32, F64 = :U8, :S8, :u16, :s16, :u32, :s32, :u64, :s64, :f32, :f64
9
15
  end
10
16
 
17
+ # Big-endian scalar types.
11
18
  module Big
12
19
  U8, S8, U16, S16, U32, S32, U64, S64, F32, F64 = :U8, :S8, :U16, :S16, :U32, :S32, :U64, :S64, :F32, :F64
13
20
  end
14
21
 
22
+ # Host-endian scalar types.
23
+ #
24
+ # This module includes either {Little} or {Big} depending on the host's native byte order.
15
25
  module Host
16
26
  include (IO::Buffer::HOST_ENDIAN == IO::Buffer::LITTLE_ENDIAN ? Little : Big)
17
27
 
data/lib/nl/error.rb ADDED
@@ -0,0 +1,10 @@
1
+ module Nl
2
+ class Error < StandardError; end
3
+ class ProtocolViolation < Error; end
4
+ class ExhaustedSequenceNumber < Error; end
5
+ class TimeoutError < Error; end
6
+ class ClosedError < Error; end
7
+ class NotificationLossError < Error; end
8
+ class UnknownMulticastGroupError < Error; end
9
+ class UnresolvedMulticastGroupError < Error; end
10
+ end
@@ -0,0 +1,124 @@
1
+ require_relative 'error'
2
+ require_relative 'raw/protocol'
3
+
4
+ module Nl
5
+ # State machine for one Netlink request/reply exchange.
6
+ #
7
+ # Dump exchanges are known to be multipart from the request and remain open
8
+ # through NLMSG_DONE. Some Linux Generic Netlink dump handlers construct
9
+ # replies with genlmsg_iput(), which leaves NLM_F_MULTI unset despite that
10
+ # multipart lifetime. Dump mode therefore accepts data with or without the
11
+ # flag; reply mode still uses it to detect a multipart do response.
12
+ #
13
+ # Some operations also return multipart data for a do request, although YNL
14
+ # cannot declare such a reply. Reply mode handles this kernel behavior by
15
+ # retaining the first data message, draining the remainder, and completing
16
+ # on NLMSG_DONE instead of ACK.
17
+ class Exchange
18
+ Item = Data.define(:value)
19
+ Complete = Data.define
20
+ COMPLETE = Complete.new
21
+ Failure = Data.define(:exception)
22
+
23
+ MODES = %i[dump no_reply reply].freeze
24
+ private_constant :MODES
25
+
26
+ attr_reader :mode
27
+
28
+ # @rbs (mode: :dump | :no_reply | :reply) -> void
29
+ def initialize(mode:)
30
+ raise ArgumentError, "unknown exchange mode: #{mode.inspect}" unless MODES.include?(mode)
31
+
32
+ @mode = mode
33
+ @reply = nil
34
+ # Receive states:
35
+ # - :dump accepts and emits every data message, then completes on DONE.
36
+ # - :no_reply rejects data and completes on ACK.
37
+ # - :initial waits for the first data message of a reply operation.
38
+ # - :single retains that message and completes once ACK is received.
39
+ # - :multi retains the first message, drains the rest, and
40
+ # completes on DONE. Every data message must carry NLM_F_MULTI.
41
+ @state = mode == :reply ? :initial : mode
42
+ @acked = false
43
+ @cancelled = false
44
+ @complete = false
45
+ @result = nil
46
+ @mutex = Mutex.new
47
+ end
48
+
49
+ def accept(frame)
50
+ @mutex.synchronize do
51
+ return if @complete
52
+
53
+ case frame
54
+ when Raw::UnknownFrame
55
+ nil
56
+ when Raw::ErrorFrame
57
+ @cancelled ? complete(nil) : fail_with(SystemCallError.new(frame.errno))
58
+ when Raw::DoneFrame
59
+ if frame.errno && !@cancelled
60
+ fail_with(SystemCallError.new(frame.errno))
61
+ else
62
+ complete(@reply)
63
+ end
64
+ when Raw::AckFrame
65
+ accept_ack
66
+ when Raw::DataFrame
67
+ accept_reply(frame)
68
+ else
69
+ raise ArgumentError, "unexpected exchange input: #{frame.inspect}"
70
+ end
71
+ end
72
+ end
73
+
74
+ def cancel
75
+ @mutex.synchronize { @cancelled = true unless @complete }
76
+ nil
77
+ end
78
+
79
+ def cancelled? = @mutex.synchronize { @cancelled }
80
+ def complete? = @mutex.synchronize { @complete }
81
+ def result = @mutex.synchronize { @result }
82
+
83
+ private def accept_ack
84
+ @acked = true
85
+ if @cancelled || @state == :no_reply || @state == :single
86
+ complete(@reply)
87
+ end
88
+ end
89
+
90
+ private def accept_reply(frame)
91
+ return if @cancelled
92
+
93
+ multipart = (frame.header.flags.to_i & Raw::NLM_F_MULTI) != 0
94
+ case @state
95
+ when :dump
96
+ return Item.new(frame.message)
97
+ when :no_reply
98
+ return fail_with(ProtocolViolation.new('unexpected data message in a no-reply Netlink response'))
99
+ when :initial
100
+ @state = multipart ? :multi : :single
101
+ when :single
102
+ return fail_with(ProtocolViolation.new('more than one data message in a non-multipart Netlink response'))
103
+ when :multi
104
+ unless multipart
105
+ return fail_with(ProtocolViolation.new('multipart Netlink response contains data without NLM_F_MULTI'))
106
+ end
107
+ end
108
+
109
+ @reply ||= frame.message
110
+ complete(frame.message) if @acked && @state == :single
111
+ end
112
+
113
+ private def complete(value)
114
+ @complete = true
115
+ @result = value
116
+ COMPLETE
117
+ end
118
+
119
+ private def fail_with(exception)
120
+ @complete = true
121
+ Failure.new(exception)
122
+ end
123
+ end
124
+ end