nl 0.1.0 → 0.2.0

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: b834199d097a768959ee9b81d4f8e08e504048b9bb537aefbbb3c737dcbc166b
4
- data.tar.gz: 62fbdaaeb0ea9c4a5aefba149476f32dde99b16e210252566340b9627282bd4e
3
+ metadata.gz: 52aff4b073cd86a4ce19bb2feee6f92de27bf253869e4f6f4bea673c90d7dade
4
+ data.tar.gz: 50fef7586bbdd1b050a20303fb106a29510fab9c6ea7df80631e1a727a36ea10
5
5
  SHA512:
6
- metadata.gz: 218aabff17cc161fbb064f22e50c78c11770bc70df76636434bd648af2e3592bbafdb491b61daf8164be306f5d5bfe1e8e0ad968963c9c17756fcb06e826ed47
7
- data.tar.gz: 4ff4dc4caf73c20e1b93e090689684534f8f8b184f15538eac47451293c4705918fbea24583949e1454882a850379341784d1226f3c7f8421d362c6b184fcc3e
6
+ metadata.gz: bfb9af6ebc834dfd1e098220d74b0064c1bfc9fe38ecea9972cfe1dac6daae9058d046e8bfcb5a32ff2ca8da62a73e64e13b0efbe94627f9eb79f1f2f72f8045
7
+ data.tar.gz: 34584295bf54077889280d22be9937382914121e78607bbfe82649a8785e469f96d96136bbd41fa7a14878391bae780db71b0dc24f0c514759b92e3f69598ccc
data/CHANGELOG.md CHANGED
@@ -2,4 +2,8 @@
2
2
 
3
3
  ## Unreleased
4
4
 
5
+ ## v0.2.0 (2026-03-02)
6
+
7
+ ## v0.1.0 (2025-04-20)
8
+
5
9
  - Initial release
data/Rakefile CHANGED
@@ -1,4 +1,3 @@
1
- require 'bundler/gem_tasks'
2
1
  require 'rspec/core/rake_task'
3
2
 
4
3
  RSpec::Core::RakeTask.new(:spec)
data/lib/nl/encoder.rb CHANGED
@@ -16,12 +16,12 @@ module Nl
16
16
 
17
17
  def put_string(value)
18
18
  reserve(value.bytesize)
19
- @position = @buffer.set_string(value, @position)
19
+ @position += @buffer.set_string(value, @position)
20
20
  end
21
21
 
22
22
  def put_zstring(value)
23
23
  reserve(value.bytesize + 1)
24
- @position = @buffer.set_string(value, @position)
24
+ @position += @buffer.set_string(value, @position)
25
25
  @position = @buffer.set_value(:U8, @position, 0)
26
26
  end
27
27
 
data/lib/nl/endian.rb CHANGED
@@ -20,16 +20,18 @@ module Nl
20
20
  when 4; [U32, S32]
21
21
  when 8; [U64, S64]
22
22
  else raise "Unsupported 'int' size"
23
- end
23
+ end
24
24
  ULONG, SLONG = case SIZEOF_LONG
25
25
  when 4; [U32, S32]
26
26
  when 8; [U64, S64]
27
27
  else raise "Unsupported 'long' size"
28
- end
28
+ end
29
29
  ULLONG, SLLONG = case SIZEOF_LLONG
30
30
  when 8; [U64, S64]
31
31
  else raise "Unsupported 'long long' size"
32
- end
32
+ end
33
+
34
+ INT, LONG, LLONG = SINT, SLONG, SLLONG
33
35
  end
34
36
  end
35
37
  end
data/lib/nl/family.rb CHANGED
@@ -1,14 +1,24 @@
1
+ #--
2
+ # rbs_inline: enabled
1
3
  require_relative 'socket'
2
4
 
3
5
  module Nl
4
6
  class Family
5
- def initialize(socket)
7
+ #--
8
+ # @rbs socket: Socket
9
+ # @rbs protocol: Protocol
10
+ # @rbs return: instance
11
+ def initialize(socket, protocol: self.class::PROTOCOL)
6
12
  @socket = socket
13
+ @protocol = protocol
7
14
  end
8
15
 
16
+ #--
17
+ # @rbs () -> instance
18
+ # | [R] () { (instance) -> R } -> R
9
19
  def self.open
10
20
  begin
11
- socket = Socket.new(self::PROTONUM)
21
+ socket = Socket.new(self::PROTOCOL.protonum)
12
22
  socket.bind(Socket.sockaddr_nl(0, 0))
13
23
  if block_given?
14
24
  yield new(socket)
@@ -20,144 +30,8 @@ module Nl
20
30
  end
21
31
  end
22
32
 
23
- def exchange_message(type, request_class, reply_class, args)
24
- self.class::PROTOCOL.exchange_message(@socket, type, request_class, reply_class, args)
25
- end
26
-
27
- class Message
28
- attr_accessor :header, :fixed_header, :attributes
29
-
30
- def initialize(header, fixed_header = nil, attributes = [])
31
- @header = header
32
- @fixed_header = fixed_header
33
- @attributes = attributes
34
- end
35
-
36
- def self.from_params(params)
37
- header_params = params.slice(*self::HEADER_PARAMS)
38
- attribute_params = params.slice(*self::ATTRIBUTE_PARAMS)
39
- # TODO: Reject unknown params
40
-
41
- header = Core::NlMsgHdr.new(0, self::TYPE, nil, nil, nil)
42
- fixed_header = self::FIXED_HEADER&.new(**header_params)
43
- attributes = self::ATTRIBUTE_SET.build_attributes(**attribute_params)
44
- new(header, fixed_header, attributes)
45
- end
46
-
47
- def append_attribute(attribute)
48
- @attributes << attribute
49
- end
50
-
51
- def encode(encoder)
52
- validate!
53
-
54
- encoder.measure(Endian::Host::U16) do
55
- @header.encode(encoder)
56
- @fixed_header.encode(encoder) if @fixed_header
57
- @attributes.each do |attr|
58
- attr.encode(encoder)
59
- end
60
- end
61
- end
62
-
63
- def self.decode(decoder, header)
64
- unless self::TYPE == header.type
65
- raise "Expected message type #{self::TYPE}, got #{header.type}"
66
- end
67
-
68
- if fixed_header_class = self::FIXED_HEADER
69
- fixed_header = fixed_header_class.decode(decoder)
70
- end
71
-
72
- attributes = self::ATTRIBUTE_SET.decode(decoder)
73
-
74
- new(header, fixed_header, attributes).tap(&:validate!)
75
- end
76
-
77
- def validate!
78
- unless self.class::TYPE == header.type
79
- raise "Expected message type #{self.class::FIXED_HEADER}, got #{header.type}"
80
- end
81
- # TODO: Validate fixed header and attributes
82
- # @fixed_header&.validate!
83
- # @attributes.each(&:validate!)
84
- end
85
-
86
- def to_h
87
- to_h_rec(@attributes, @fixed_header&.to_h || {})
88
- end
89
-
90
- # FIXME:
91
- private def to_h_rec(attributes, init = {})
92
- attributes.each_with_object(init) do |attr, h|
93
- if attr.class::DATATYPE.is_a?(Protocols::Raw::DataTypes::NestedAttributes)
94
- h[attr.class::NAME] = to_h_rec(attr.value)
95
- else
96
- h[attr.class::NAME] = attr.value
97
- end
98
- end
99
- end
100
- end
101
-
102
- class AttributeSet
103
- Attribute = Struct.new(:value)
104
- class Attribute
105
- def self.decode(decoder)
106
- value = self::DATATYPE.decode(decoder)
107
- new(value)
108
- end
109
-
110
- def encode(encoder)
111
- self.class::DATATYPE.encode(encoder, self.value)
112
- end
113
- end
114
-
115
- class << self
116
- private def decode1(decoder)
117
- nlattr = Core::NlAttr.decode(decoder)
118
- attr = decoder.limit(nlattr.len - Core::NLA_HDRLEN) do
119
- if attr_class = self::BY_TYPE[nlattr.type & Core::NLA_TYPE_MASK]
120
- attr_class.decode(decoder)
121
- else
122
- decoder.skip
123
- nil
124
- end
125
- end
126
- decoder.align_to(Core::NLA_ALIGNTO)
127
- attr
128
- end
129
-
130
- def decode(decoder)
131
- attrs = []
132
- while decoder.available?
133
- attr = decode1(decoder)
134
- attrs << attr
135
- end
136
- attrs.compact
137
- end
138
-
139
- private def encode1(encoder, attr)
140
- nlattr = Core::NlAttr.new(attr.class::TYPE, 0)
141
- encoder.measure(Endian::Host::U16) do
142
- nlattr.encode(encoder)
143
- attr.encode(encoder)
144
- end
145
- encoder.align_to(Core::NLA_ALIGNTO)
146
- end
147
-
148
- def encode(encoder, attrs)
149
- attrs.each do |attr|
150
- encode1(encoder, attr)
151
- end
152
- end
153
-
154
- def build_attributes(**params)
155
- params.map do |name, value|
156
- attr_class = self::BY_NAME[name] or raise "Unknown attribute #{name}"
157
- attr_class.new(value)
158
- end
159
- end
160
- end
33
+ private def exchange_message(type, request_class, reply_class, args, &block)
34
+ @protocol.exchange_message(@socket, type, request_class, reply_class, args, &block)
161
35
  end
162
36
  end
163
37
  end
data/lib/nl/genl.rb CHANGED
@@ -48,18 +48,40 @@ module Nl
48
48
  end
49
49
  include Constants
50
50
 
51
- GenlMsgHdr ||= Data.define(:cmd, :version, :reserved)
52
- class GenlMsgHdr
53
- GENLMSGHDR_FMT = Ractor.make_shareable([
54
- Endian::Host::U8,
55
- Endian::Host::U8,
56
- Endian::Host::U16,
57
- ])
58
- private_constant :GENLMSGHDR_FMT
51
+ class Connection
52
+ def self.open(resolver:)
53
+ conn = new(resolver:)
54
+ if block_given?
55
+ begin
56
+ yield conn
57
+ ensure
58
+ conn.close
59
+ end
60
+ else
61
+ conn
62
+ end
63
+ end
64
+
65
+ def initialize(resolver:)
66
+ @socket = Socket.new(Core::NETLINK_GENERIC)
67
+ @socket.bind(Socket.sockaddr_nl(0, 0))
68
+ @resolver = resolver
69
+ @id_cache = {}
70
+ end
71
+
72
+ def open(family_class)
73
+ proto = family_class::PROTOCOL
74
+ id = @id_cache[proto.name] ||= begin
75
+ proto.family_id
76
+ rescue NotImplementedError
77
+ @resolver.call(@socket, proto.name)
78
+ end
79
+ family_class.new(@socket, protocol: Protocols::Genl.new(proto.name, family_id: id))
80
+ end
59
81
 
60
- def self.parse(buffer, offset)
61
- new(*buffer.get_values(GENLMSGHDR_FMT, offset))
82
+ def close
83
+ @socket.close
62
84
  end
63
85
  end
64
86
  end
65
- end
87
+ end
@@ -1,14 +1,54 @@
1
1
  module Nl
2
2
  module Protocols
3
3
  # The Generic Netlink protocol
4
- class Genl < Raw # TODO: Implement
5
- def initialize(name)
6
- super(name, NETLINK_GENERIC)
4
+ class Genl < Raw
5
+ GenlMsgHdr = ::Struct.new(:cmd, :version, :reserved)
6
+ class GenlMsgHdr
7
+ FORMAT = Ractor.make_shareable([
8
+ Endian::Host::U8,
9
+ Endian::Host::U8,
10
+ Endian::Host::U16,
11
+ ])
12
+
13
+ def self.decode(decoder)
14
+ new(*decoder.get_values(FORMAT))
15
+ end
16
+
17
+ def encode(encoder)
18
+ encoder.put_values(FORMAT, to_a)
19
+ end
20
+ end
21
+
22
+ def initialize(name, family_id: nil)
23
+ super(name, Core::NETLINK_GENERIC)
24
+ @family_id = family_id || default_family_id(name)
25
+ end
26
+
27
+ def family_id
28
+ @family_id or raise NotImplementedError, "Genetlink family ID for '#{name}' must be resolved via nlctrl"
29
+ end
30
+
31
+ def encode_message(encoder, message)
32
+ cmd = message.nlmsg_header.type
33
+ encoder.measure(Endian::Host::U16) do
34
+ message.nlmsg_header.type = family_id
35
+ message.nlmsg_header.encode(encoder)
36
+ message.nlmsg_header.type = cmd
37
+ GenlMsgHdr.new(cmd, 1, 0).encode(encoder)
38
+ message.fixed_header&.encode(encoder)
39
+ message.attributes.encode(encoder)
40
+ end
41
+ end
42
+
43
+ class Message < Raw::Message
44
+ def self.decode(decoder, header)
45
+ genlhdr = GenlMsgHdr.decode(decoder)
46
+ super(decoder, header, type: genlhdr.cmd)
47
+ end
7
48
  end
8
49
 
9
- def parse(buffer, offset)
10
- nlmsg = super(buffer, offset)
11
- genlmsg = GenlMsgHdr.parse(nlmsg.data, 0)
50
+ private def default_family_id(name)
51
+ Nl::Genl::GENL_ID_CTRL if name == 'nlctrl'
12
52
  end
13
53
  end
14
54
  end
@@ -1,3 +1,6 @@
1
+ require_relative '../encoder'
2
+ require_relative '../decoder'
3
+
1
4
  module Nl
2
5
  module Protocols
3
6
  # The raw Netlink protocol
@@ -26,7 +29,7 @@ module Nl
26
29
  end
27
30
 
28
31
  def send_message(socket, message)
29
- seq_pid = socket.complete(message.header)
32
+ seq_pid = socket.complete(message.nlmsg_header)
30
33
  encoder = Encoder.new
31
34
  encode_message(encoder, message)
32
35
  socket.sendmsg(encoder.buffer.get_string, 0, Socket.sockaddr_nl(0, 0))
@@ -75,34 +78,187 @@ module Nl
75
78
  # @param reply_class [Class] Reply message class
76
79
  # @param args [Hash] Request arguments
77
80
  def exchange_message(socket, type, request_class, reply_class, args)
78
- flags = Core::NLM_F_REQUEST | Core::NLM_F_ACK
79
- flags |= Core::NLM_F_DUMP if type == :dump
81
+ flags = Core::NLM_F_REQUEST
82
+ flags |= type == :dump ? Core::NLM_F_DUMP : Core::NLM_F_ACK
80
83
 
81
84
  request = request_class.from_params(args)
82
- request.header.flags = flags
85
+ request.nlmsg_header.flags = flags
83
86
  seq_pid = send_message(socket, request)
84
87
 
85
- result = []
88
+ result = [] unless block_given?
86
89
 
87
90
  done = false
88
- acked = false
89
91
  begin
90
92
  recv_message(socket, seq_pid, reply_class) do |message|
91
93
  case message
92
- when Done
94
+ when Done, Ack
93
95
  done = true
94
96
  when Exception
95
- raise
96
- when Ack
97
- acked = true
97
+ raise message
98
98
  else
99
- result << message
100
- done = true if type == :do
99
+ if block_given?
100
+ yield message
101
+ else
102
+ result << message
103
+ end
101
104
  end
102
105
  end
103
106
  end until done
104
107
 
105
- result
108
+ unless block_given?
109
+ type == :dump ? result : result.first
110
+ end
111
+ end
112
+
113
+ class AttributeSet
114
+ Attribute = Struct.new(:value)
115
+ class Attribute
116
+ def self.decode(decoder)
117
+ value = self::DATATYPE.decode(decoder)
118
+ new(value)
119
+ end
120
+
121
+ def encode(encoder)
122
+ self.class::DATATYPE.encode(encoder, self.value)
123
+ end
124
+ end
125
+
126
+ def initialize(attributes)
127
+ attr_class = self.class::Attribute
128
+
129
+ attributes.each do |attr|
130
+ unless attr.kind_of?(attr_class)
131
+ raise TypeError, "attribute must be an instance of #{attr_class}"
132
+ end
133
+ end
134
+
135
+ @attributes = Array(attributes)
136
+ end
137
+
138
+ def [](type)
139
+ case type
140
+ when Symbol
141
+ attr_class = self.class.by_name(type)
142
+ when Integer
143
+ attr_class = self.class.by_type(type)
144
+ else
145
+ raise TypeError, "attribute type must be a Symbol or an Integer"
146
+ end
147
+
148
+ # TODO: multi-attr
149
+ @attributes.find { it.kind_of?(attr_class) } rescue binding.irb
150
+ end
151
+
152
+ def <<(attr)
153
+ attr_class = self.class::Attribute
154
+ unless attr.kind_of?(attr_class)
155
+ raise TypeError, "attribute must be an instance of #{attr_class}"
156
+ end
157
+
158
+ @attributes << attr
159
+ end
160
+
161
+ private def encode1(encoder, attr)
162
+ nlattr = Core::NlAttr.new(0, attr.class::TYPE)
163
+ encoder.measure(Endian::Host::U16) do
164
+ nlattr.encode(encoder)
165
+ attr.encode(encoder)
166
+ end
167
+ encoder.align_to(Core::NLA_ALIGNTO)
168
+ end
169
+
170
+ def encode(encoder)
171
+ @attributes.each do |attr|
172
+ encode1(encoder, attr)
173
+ end
174
+ end
175
+
176
+ class << self
177
+ private def decode1(decoder)
178
+ nlattr = Core::NlAttr.decode(decoder)
179
+ attr = decoder.limit(nlattr.len - Core::NLA_HDRLEN) do
180
+ if attr_class = self::BY_TYPE[nlattr.type & Core::NLA_TYPE_MASK]
181
+ attr_class.decode(decoder)
182
+ else
183
+ decoder.skip
184
+ nil
185
+ end
186
+ end
187
+ decoder.align_to(Core::NLA_ALIGNTO)
188
+ attr
189
+ end
190
+
191
+ def decode(decoder)
192
+ attrs = []
193
+ while decoder.available?
194
+ attr = decode1(decoder)
195
+ attrs << attr
196
+ end
197
+ new(attrs.compact)
198
+ end
199
+
200
+ def build_attributes(**params)
201
+ attrs = params.map do |name, value|
202
+ attr_class = self::BY_NAME[name] or raise "Unknown attribute #{name}"
203
+ attr_class.new(value)
204
+ end
205
+ new(attrs)
206
+ end
207
+ end
208
+ end
209
+
210
+ class Message
211
+ attr_accessor :nlmsg_header, :fixed_header, :attributes
212
+
213
+ def initialize(header, fixed_header = nil, attributes = self.class::ATTRIBUTE_SET.new)
214
+ @nlmsg_header = header
215
+ @fixed_header = fixed_header
216
+ @attributes = attributes
217
+ end
218
+
219
+ def self.from_params(params)
220
+ if self::FIXED_HEADER
221
+ header_params = params.slice(*self::FIXED_HEADER.members)
222
+ fixed_header = self::FIXED_HEADER.new(**header_params)
223
+ end
224
+ attribute_params = params.slice(*self::ATTRIBUTES)
225
+ attributes = self::ATTRIBUTE_SET.build_attributes(**attribute_params)
226
+
227
+ unknown = params.keys - attribute_params.keys
228
+ unknown -= header_params.keys if header_params
229
+ unless unknown.empty?
230
+ raise ArgumentError, "unknown parameters: #{unknown.join(', ')}"
231
+ end
232
+
233
+ header = Core::NlMsgHdr.new(0, self::TYPE, nil, nil, nil)
234
+ new(header, fixed_header, attributes)
235
+ end
236
+
237
+ def append_attribute(attribute)
238
+ @attributes << attribute
239
+ end
240
+
241
+ def encode(encoder)
242
+ encoder.measure(Endian::Host::U16) do
243
+ @nlmsg_header.encode(encoder)
244
+ @fixed_header&.encode(encoder)
245
+ @attributes.encode(encoder)
246
+ end
247
+ end
248
+
249
+ def self.decode(decoder, header, type: header.type)
250
+ unless self::TYPE == type
251
+ raise "Expected message type #{self::TYPE}, got #{type}"
252
+ end
253
+
254
+ if fixed_header_class = self::FIXED_HEADER
255
+ fixed_header = fixed_header_class.decode(decoder)
256
+ end
257
+
258
+ attributes = self::ATTRIBUTE_SET.decode(decoder)
259
+
260
+ new(header, fixed_header, attributes)
261
+ end
106
262
  end
107
263
 
108
264
  module DataTypes
@@ -150,6 +306,46 @@ module Nl
150
306
  end
151
307
  end
152
308
 
309
+ class Flag
310
+ def encode(encoder, value)
311
+ # flag attribute has no payload; presence encodes true
312
+ end
313
+
314
+ def decode(decoder)
315
+ true
316
+ end
317
+ end
318
+
319
+ # A 32-bit value paired with a selector mask (8 bytes total: value u32 + selector u32)
320
+ class Bitfield32
321
+ def encode(encoder, value)
322
+ v, selector = value.is_a?(Array) ? value : [value, 0xFFFFFFFF]
323
+ encoder.put_value(Endian::Host::U32, v)
324
+ encoder.put_value(Endian::Host::U32, selector)
325
+ end
326
+
327
+ def decode(decoder)
328
+ value = decoder.get_value(Endian::Host::U32)
329
+ selector = decoder.get_value(Endian::Host::U32)
330
+ [value, selector]
331
+ end
332
+ end
333
+
334
+ class Pad
335
+ def initialize(length = nil)
336
+ @length = length
337
+ end
338
+
339
+ def encode(encoder, _value)
340
+ encoder.put_string(?\0.b * @length) if @length
341
+ end
342
+
343
+ def decode(decoder)
344
+ @length ? decoder.skip(@length) : decoder.skip
345
+ nil
346
+ end
347
+ end
348
+
153
349
  class NestedAttributes
154
350
  def initialize(attribute_set)
155
351
  @attribute_set = attribute_set
@@ -163,6 +359,36 @@ module Nl
163
359
  @attribute_set.decode(decoder)
164
360
  end
165
361
  end
362
+
363
+ class IndexedArray
364
+ def initialize(sub_type)
365
+ @sub_type = sub_type
366
+ end
367
+
368
+ def encode(encoder, values)
369
+ values.each_with_index do |value, i|
370
+ nlattr = Core::NlAttr.new(0, i + 1)
371
+ encoder.measure(Endian::Host::U16) do
372
+ nlattr.encode(encoder)
373
+ @sub_type.encode(encoder, value)
374
+ end
375
+ encoder.align_to(Core::NLA_ALIGNTO)
376
+ end
377
+ end
378
+
379
+ def decode(decoder)
380
+ result = []
381
+ while decoder.available?
382
+ nlattr = Core::NlAttr.decode(decoder)
383
+ element = decoder.limit(nlattr.len - Core::NLA_HDRLEN) do
384
+ @sub_type.decode(decoder)
385
+ end
386
+ decoder.align_to(Core::NLA_ALIGNTO)
387
+ result << element
388
+ end
389
+ result
390
+ end
391
+ end
166
392
  end
167
393
  end
168
394
  end
data/lib/nl/version.rb CHANGED
@@ -1,3 +1,3 @@
1
1
  module Nl
2
- VERSION = '0.1.0'
2
+ VERSION = '0.2.0'
3
3
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: nl
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 0.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Kasumi Hanazuki
@@ -37,7 +37,7 @@ licenses:
37
37
  - MIT
38
38
  metadata:
39
39
  homepage_uri: https://github.com/hanazuki/nl
40
- source_code_uri: https://github.com/hanazuki/nl/tree/v0.1.0
40
+ source_code_uri: https://github.com/hanazuki/nl/tree/v0.2.0
41
41
  changelog_uri: https://github.com/hanazuki/nl/blob/master/CHANGELOG.md
42
42
  rdoc_options: []
43
43
  require_paths:
@@ -53,7 +53,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
53
53
  - !ruby/object:Gem::Version
54
54
  version: '0'
55
55
  requirements: []
56
- rubygems_version: 3.6.8
56
+ rubygems_version: 4.0.3
57
57
  specification_version: 4
58
58
  summary: Linux Netlink client
59
59
  test_files: []