ctypes 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 +7 -0
- data/.standard.yml +3 -0
- data/CODE_OF_CONDUCT.md +132 -0
- data/CONTRIBUTING.md +55 -0
- data/Gemfile +14 -0
- data/LICENSE.txt +21 -0
- data/MAINTAINERS.md +3 -0
- data/README.md +390 -0
- data/Rakefile +10 -0
- data/SECURITY.md +57 -0
- data/ctypes.gemspec +40 -0
- data/lib/ctypes/array.rb +180 -0
- data/lib/ctypes/bitfield/builder.rb +246 -0
- data/lib/ctypes/bitfield.rb +278 -0
- data/lib/ctypes/bitmap.rb +154 -0
- data/lib/ctypes/enum/builder.rb +85 -0
- data/lib/ctypes/enum.rb +201 -0
- data/lib/ctypes/exporter.rb +50 -0
- data/lib/ctypes/helpers.rb +190 -0
- data/lib/ctypes/importers/castxml/loader.rb +150 -0
- data/lib/ctypes/importers/castxml.rb +59 -0
- data/lib/ctypes/importers.rb +7 -0
- data/lib/ctypes/int.rb +147 -0
- data/lib/ctypes/missing_bytes_error.rb +24 -0
- data/lib/ctypes/pad.rb +56 -0
- data/lib/ctypes/pretty_print_helpers.rb +31 -0
- data/lib/ctypes/string.rb +154 -0
- data/lib/ctypes/struct/builder.rb +242 -0
- data/lib/ctypes/struct.rb +529 -0
- data/lib/ctypes/terminated.rb +65 -0
- data/lib/ctypes/type.rb +195 -0
- data/lib/ctypes/union/builder.rb +220 -0
- data/lib/ctypes/union.rb +637 -0
- data/lib/ctypes/version.rb +8 -0
- data/lib/ctypes.rb +102 -0
- data/sig/ctypes.rbs +4 -0
- metadata +92 -0
@@ -0,0 +1,278 @@
|
|
1
|
+
# encoding: ASCII-8BIT
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
# SPDX-FileCopyrightText: 2025 Cisco
|
5
|
+
# SPDX-License-Identifier: MIT
|
6
|
+
|
7
|
+
module CTypes
|
8
|
+
# define a bit-field type for use in structures
|
9
|
+
# @example 8-bit integer split into three multibit values
|
10
|
+
# class MyBits < CTypes::Bitfield
|
11
|
+
# # declare bit layout in right-to-left order
|
12
|
+
# layout do
|
13
|
+
# unsigned :bit # single bit for a
|
14
|
+
# skip 1 # skip one bit
|
15
|
+
# unsigned :two, 2 # two bits for this field
|
16
|
+
# signed :nibble, 4 # four bit nibble as a signed int
|
17
|
+
# end
|
18
|
+
# end
|
19
|
+
#
|
20
|
+
# # size is the number of bytes required by the layout, but can be larger
|
21
|
+
# # when requested in #layout
|
22
|
+
# MyBits.size # => 1
|
23
|
+
#
|
24
|
+
# # The bits are packed right-to-left
|
25
|
+
# MyBits.pack({bit: 1}) # => "\x01" (0b00000001)
|
26
|
+
# MyBits.pack({two: 3}) # => "\x0c" (0b00001100)
|
27
|
+
# MyBits.pack({nibble: -1}) # => "\xf0" (0b11110000)
|
28
|
+
#
|
29
|
+
# # unpack a value and access individual fields
|
30
|
+
# value = MyBits.unpack("\xf1") # => #<Bitfield bit: 1, two: 0, nibble: -1>
|
31
|
+
# value.bit # => 1
|
32
|
+
# value.two # => 0
|
33
|
+
# value.nibble # => -1
|
34
|
+
#
|
35
|
+
# # update the value and repack
|
36
|
+
# value.two = 1
|
37
|
+
# value.nibble = 0
|
38
|
+
# value.to_binstr # => "\x05" (0b00000101)
|
39
|
+
class Bitfield
|
40
|
+
extend Type
|
41
|
+
using PrettyPrintHelpers
|
42
|
+
|
43
|
+
# describe the layout of the bitfield
|
44
|
+
#
|
45
|
+
# @example right-to-left bit layout
|
46
|
+
# layout do
|
47
|
+
# unsigned :bit # single bit for a
|
48
|
+
# skip 1 # skip one bit
|
49
|
+
# unsigned :two, 2 # two bits for this field
|
50
|
+
# signed :nibble, 4 # four bit nibble as a signed int
|
51
|
+
# end
|
52
|
+
#
|
53
|
+
# @example explicit bit layout
|
54
|
+
# layout do
|
55
|
+
# field :bit, offset: 0, bits: 1
|
56
|
+
# field :two, offset: 2, bits: 2
|
57
|
+
# field :nibble, offset: 4, bits: 4
|
58
|
+
# end
|
59
|
+
#
|
60
|
+
# @example create a two-byte bitfield, but only one bit declared
|
61
|
+
# layout do
|
62
|
+
# size 2 # bitfield will take two bytes
|
63
|
+
# field :bit, offset: 9, bits: 1
|
64
|
+
# end
|
65
|
+
#
|
66
|
+
def self.layout(&block)
|
67
|
+
raise Error, "no block given" unless block
|
68
|
+
builder = Builder.new(&block)
|
69
|
+
builder.instance_eval(&block)
|
70
|
+
apply_layout(builder)
|
71
|
+
end
|
72
|
+
|
73
|
+
# @api private
|
74
|
+
def self.builder
|
75
|
+
Builder.new
|
76
|
+
end
|
77
|
+
|
78
|
+
# @api private
|
79
|
+
def self.apply_layout(builder)
|
80
|
+
@type, @bits, @dry_type, @endian, @layout = builder.result
|
81
|
+
|
82
|
+
# clear all existing field accessors
|
83
|
+
@fields ||= {}
|
84
|
+
@fields.each { |_, methods| remove_method(*methods) }
|
85
|
+
@fields.clear
|
86
|
+
|
87
|
+
@bits.each do |name, _|
|
88
|
+
@fields[name] = attr_accessor(name)
|
89
|
+
end
|
90
|
+
|
91
|
+
self
|
92
|
+
end
|
93
|
+
private_class_method :apply_layout
|
94
|
+
|
95
|
+
# get the size of the bitfield in bytes
|
96
|
+
def self.size
|
97
|
+
@type.size
|
98
|
+
end
|
99
|
+
|
100
|
+
# check if bitfield is a fixed size
|
101
|
+
def self.fixed_size?
|
102
|
+
true
|
103
|
+
end
|
104
|
+
|
105
|
+
# check if bitfield is greedy
|
106
|
+
def self.greedy?
|
107
|
+
false
|
108
|
+
end
|
109
|
+
|
110
|
+
# pack a ruby hash containing bitfield values into a binary string
|
111
|
+
# @param value [Hash] value to be encoded
|
112
|
+
# @param endian [Symbol] optional endian override
|
113
|
+
# @param validate [Boolean] set to false to disable value validation
|
114
|
+
# @return [::String] binary encoding for value
|
115
|
+
def self.pack(value, endian: default_endian, validate: true)
|
116
|
+
value = value.to_hash.freeze
|
117
|
+
value = @dry_type[value] unless validate == false
|
118
|
+
out = 0
|
119
|
+
@bits.each do |(name, offset, mask, _)|
|
120
|
+
out |= (value[name] & mask) << offset
|
121
|
+
end
|
122
|
+
@type.pack(out, endian:, validate:)
|
123
|
+
end
|
124
|
+
|
125
|
+
# convert a String containing the binary represention of a c type into the
|
126
|
+
# equivalent ruby type
|
127
|
+
#
|
128
|
+
# @param buf [::String] bytes that make up the type
|
129
|
+
# @param endian [Symbol] endian of data within buf
|
130
|
+
# @return [Bitfield] unpacked bitfield
|
131
|
+
def self.unpack_one(buf, endian: default_endian)
|
132
|
+
value, rest = @type.unpack_one(buf, endian:)
|
133
|
+
out = _new
|
134
|
+
@bits.each do |(name, offset, mask, signed, var)|
|
135
|
+
v = (value >> offset) & mask
|
136
|
+
v |= (-1 << signed) | v if signed && v[signed - 1] == 1
|
137
|
+
out.instance_variable_set(var, v)
|
138
|
+
end
|
139
|
+
|
140
|
+
[out, rest]
|
141
|
+
end
|
142
|
+
|
143
|
+
# get the list of fields defined in this bitfield
|
144
|
+
def self.fields
|
145
|
+
@fields.keys
|
146
|
+
end
|
147
|
+
|
148
|
+
# check if the bitfield declared a specific field
|
149
|
+
def self.has_field?(name)
|
150
|
+
@fields.has_key?(name)
|
151
|
+
end
|
152
|
+
|
153
|
+
def self.pretty_print(q)
|
154
|
+
q.ctype("bitfield", @endian) do
|
155
|
+
q.seplist(@layout, -> { q.breakable(";") }) do |cmd|
|
156
|
+
q.text(cmd)
|
157
|
+
end
|
158
|
+
end
|
159
|
+
end
|
160
|
+
class << self
|
161
|
+
alias_method :inspect, :pretty_inspect # :nodoc:
|
162
|
+
end
|
163
|
+
|
164
|
+
# generate ruby code needed to create this type
|
165
|
+
def self.export_type(q)
|
166
|
+
q << "bitfield {"
|
167
|
+
q.break
|
168
|
+
q.nest(2) do
|
169
|
+
@layout.each do |cmd|
|
170
|
+
q << cmd
|
171
|
+
q.break
|
172
|
+
end
|
173
|
+
end
|
174
|
+
q << "}"
|
175
|
+
q << ".with_endian(%p)" % [@endian] if @endian
|
176
|
+
end
|
177
|
+
|
178
|
+
class << self
|
179
|
+
# @method _new
|
180
|
+
# allocate an uninitialized instance of the bitfield
|
181
|
+
# @return [Bitfield] uninitialized bitfield instance
|
182
|
+
# @api private
|
183
|
+
alias_method :_new, :new
|
184
|
+
private :_new
|
185
|
+
end
|
186
|
+
|
187
|
+
# allocate an instance of the Bitfield and initialize default values
|
188
|
+
# @param fields [Hash] values to set
|
189
|
+
# @return [Bitfield]
|
190
|
+
def self.new(fields = nil)
|
191
|
+
buf = fields.nil? ? ("\0" * size) : pack(fields)
|
192
|
+
unpack(buf)
|
193
|
+
end
|
194
|
+
|
195
|
+
def self.==(other)
|
196
|
+
return true if super
|
197
|
+
return false unless other.is_a?(Class) && other < Bitfield
|
198
|
+
other.field_layout == @bits &&
|
199
|
+
other.default_endian == default_endian &&
|
200
|
+
other.size == size
|
201
|
+
end
|
202
|
+
|
203
|
+
# @api private
|
204
|
+
def self.field_layout # :nodoc:
|
205
|
+
@bits
|
206
|
+
end
|
207
|
+
|
208
|
+
# set a bitfield value
|
209
|
+
# @param k [Symbol] field name
|
210
|
+
# @param v value
|
211
|
+
def []=(k, v)
|
212
|
+
has_field!(k)
|
213
|
+
instance_variable_set(:"@#{k}", v)
|
214
|
+
end
|
215
|
+
|
216
|
+
# get an bitfield value
|
217
|
+
# @param k [Symbol] field name
|
218
|
+
# @return value
|
219
|
+
def [](k)
|
220
|
+
has_field!(k)
|
221
|
+
instance_variable_get(:"@#{k}")
|
222
|
+
end
|
223
|
+
|
224
|
+
def has_key?(name)
|
225
|
+
self.class.has_field?(name)
|
226
|
+
end
|
227
|
+
|
228
|
+
def has_field!(name) # :nodoc:
|
229
|
+
raise UnknownFieldError, "unknown field: %p" % name unless
|
230
|
+
self.class.has_field?(name)
|
231
|
+
end
|
232
|
+
private :has_field!
|
233
|
+
|
234
|
+
def to_h(shallow: false)
|
235
|
+
out = {}
|
236
|
+
self.class.field_layout.each do |name, _, _, _, var|
|
237
|
+
out[name] = instance_variable_get(var)
|
238
|
+
end
|
239
|
+
out
|
240
|
+
end
|
241
|
+
alias_method :to_hash, :to_h
|
242
|
+
|
243
|
+
def pretty_print(q) # :nodoc:
|
244
|
+
q.group(4, "bitfield {", "}") do
|
245
|
+
q.seplist(self.class.field_layout, -> { q.breakable("") }) do |name, _|
|
246
|
+
q.text(".#{name} = ")
|
247
|
+
q.pp(instance_variable_get(:"@#{name}"))
|
248
|
+
q.text(", ")
|
249
|
+
end
|
250
|
+
end
|
251
|
+
end
|
252
|
+
alias_method :inspect, :pretty_inspect # :nodoc:
|
253
|
+
|
254
|
+
# return the binary representation of this Bitfield instance
|
255
|
+
# @return [::String] binary representation of struct
|
256
|
+
def to_binstr(endian: self.class.default_endian)
|
257
|
+
self.class.pack(to_h, endian:)
|
258
|
+
end
|
259
|
+
|
260
|
+
# determine if this instance of the bitfield is equal to another instance
|
261
|
+
#
|
262
|
+
# @note this implementation also supports Hash equality through {to_h}
|
263
|
+
def ==(other)
|
264
|
+
case other
|
265
|
+
when self.class
|
266
|
+
self.class.field_layout.all? do |name, _, _, _, var|
|
267
|
+
instance_variable_get(var) == other[name]
|
268
|
+
end
|
269
|
+
when Hash
|
270
|
+
other == to_h
|
271
|
+
else
|
272
|
+
super
|
273
|
+
end
|
274
|
+
end
|
275
|
+
end
|
276
|
+
end
|
277
|
+
|
278
|
+
require_relative "bitfield/builder"
|
@@ -0,0 +1,154 @@
|
|
1
|
+
# encoding: ASCII-8BIT
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
# SPDX-FileCopyrightText: 2025 Cisco
|
5
|
+
# SPDX-License-Identifier: MIT
|
6
|
+
|
7
|
+
module CTypes
|
8
|
+
# map bits in an integer to specific flags
|
9
|
+
#
|
10
|
+
# This class enables the mapping of individual bits in an integer to specific
|
11
|
+
# flags that each bit represents. This class only supports single bit
|
12
|
+
# fields. See `Bitfield` if you need to unpack multibit values.
|
13
|
+
#
|
14
|
+
# @example 8-bit bitmap, the long way
|
15
|
+
# b = Bitmap.new(
|
16
|
+
# bits: Enum.new({a: 0, b: 1, c: 7}),
|
17
|
+
# type: CTypes::UInt8)
|
18
|
+
#
|
19
|
+
# # pack some values
|
20
|
+
# b.pack([]) # => "\x00"
|
21
|
+
# b.pack([:a]) # => "\x01"
|
22
|
+
# b.pack([:b]) # => "\x02"
|
23
|
+
# b.pack([:c]) # => "\x80"
|
24
|
+
# b.pack([:c, :a]) # => "\x81"
|
25
|
+
# b.pack([:a, :c]) # => "\x81"
|
26
|
+
#
|
27
|
+
# # unpack some values
|
28
|
+
# b.unpack("\x00") # => []
|
29
|
+
# b.unpack("\x01") # => [:a]
|
30
|
+
# b.unpack("\x02") # => [:b]
|
31
|
+
# b.unpack("\x03") # => [:a, :b]
|
32
|
+
# b.unpack("\x80") # => [:c]
|
33
|
+
#
|
34
|
+
# @example 8-bit bitmap, using helpers
|
35
|
+
# include CTypes::Helpers # for bitmap and uint8
|
36
|
+
# b = bitmap(uint8, {a: 0, b: 1, c: 7})
|
37
|
+
# b.pack([:a, :c]) # => "\x81"
|
38
|
+
# b.unpack("\x03") # => [:a, :b]
|
39
|
+
class Bitmap
|
40
|
+
extend Forwardable
|
41
|
+
include Type
|
42
|
+
|
43
|
+
# create a new Bitmap
|
44
|
+
# @param bits [Enum] mapping of bit position to name
|
45
|
+
# @param type [Type] ctype to encode value as; defaults to uint32
|
46
|
+
def initialize(bits:, type: Helpers.uint32)
|
47
|
+
raise Error, "bits must be an Enum instance: %p" % bits unless
|
48
|
+
bits.is_a?(Enum)
|
49
|
+
|
50
|
+
@type = type
|
51
|
+
@bits = bits
|
52
|
+
@bits_max = type.size * 8
|
53
|
+
@bits_constraint = Dry::Types["integer"]
|
54
|
+
.constrained(gteq: 0, lt: @bits_max)
|
55
|
+
@dry_type = Dry::Types["array"].of(@bits.dry_type).default([].freeze)
|
56
|
+
end
|
57
|
+
def_delegators :@type, :greedy?
|
58
|
+
|
59
|
+
def size
|
60
|
+
@type.size
|
61
|
+
end
|
62
|
+
|
63
|
+
def fixed_size?
|
64
|
+
@type.fixed_size?
|
65
|
+
end
|
66
|
+
|
67
|
+
# pack a ruby Array containing symbol names for each bit into a binary
|
68
|
+
# string
|
69
|
+
# @param value [::Array] Array of bits to be set, by name or right-to-left
|
70
|
+
# index
|
71
|
+
# @param endian [Symbol] optional endian override
|
72
|
+
# @param validate [Boolean] set to false to disable value validation
|
73
|
+
# @return [::String] binary encoding for value
|
74
|
+
#
|
75
|
+
# @example packing with named values
|
76
|
+
# b = CTypes::Helpers.bitmap(uint8, {a: 0, b: 1, c: 7})
|
77
|
+
# b.pack([:a, :c]) # => "\x81"
|
78
|
+
#
|
79
|
+
# @example packing explicit bits
|
80
|
+
# b = CTypes::Helpers.bitmap(uint8, {a: 0, b: 1, c: 7})
|
81
|
+
# b.pack([0, 7]) # => "\x81" (0b10000000)
|
82
|
+
# b.pack([0, 6]) # => Dry::Types::ConstraintError
|
83
|
+
# b.pack([0, 6], validate: false) # => "\x41" (0b01000001)
|
84
|
+
#
|
85
|
+
def pack(value, endian: default_endian, validate: true)
|
86
|
+
value = @dry_type[value] unless validate == false
|
87
|
+
mapping = @bits.mapping
|
88
|
+
bits = value.inject(0) do |out, v|
|
89
|
+
bit = case v
|
90
|
+
when Integer
|
91
|
+
v
|
92
|
+
when /\Abit_(\d+)\z/
|
93
|
+
$1.to_i
|
94
|
+
when Symbol
|
95
|
+
mapping[v]
|
96
|
+
else
|
97
|
+
raise Error, "unknown bitmap value: %p" % v
|
98
|
+
end
|
99
|
+
@bits_constraint[bit]
|
100
|
+
out |= 1 << bit
|
101
|
+
end
|
102
|
+
@type.pack(bits, endian: @type.endian || endian, validate: validate)
|
103
|
+
end
|
104
|
+
|
105
|
+
# convert a String containing the binary represention of a c type into the
|
106
|
+
# equivalent ruby type
|
107
|
+
#
|
108
|
+
# @param buf [::String] bytes that make up the type
|
109
|
+
# @param endian [Symbol] endian of data within buf
|
110
|
+
# @return [::Array] unpacked bitfield
|
111
|
+
#
|
112
|
+
# @example
|
113
|
+
# b = bitmap(uint8, {a: 0, b: 1, c: 7})
|
114
|
+
# b.unpack("\x00") # => []
|
115
|
+
# b.unpack("\x01") # => [:a]
|
116
|
+
# b.unpack("\x81") # => [:a, :c]
|
117
|
+
#
|
118
|
+
# @example allow unlabelled bits in the value
|
119
|
+
# b = bitmap(uint8, {a: 0, b: 1, c: 7})
|
120
|
+
# b.unpack("\x04") # => Dry::Types::ConstraintError
|
121
|
+
#
|
122
|
+
# # create a new permissive bitmap from the initial bitmap
|
123
|
+
# bp = b.permissive
|
124
|
+
# bp.unpack("\x04") # => [:bit_2]
|
125
|
+
#
|
126
|
+
# # known bits are still unpacked with the proper name
|
127
|
+
# bp.unpack("\x05") # => [:a, :bit_2]
|
128
|
+
#
|
129
|
+
def unpack_one(buf, endian: default_endian)
|
130
|
+
value, rest = @type.unpack_one(buf, endian: @type.endian || endian)
|
131
|
+
bits = []
|
132
|
+
@bits_max.times do |bit|
|
133
|
+
next if value & (1 << bit) == 0
|
134
|
+
v = @bits.dry_type[bit]
|
135
|
+
v = :"bit_#{v}" if v.is_a?(Integer)
|
136
|
+
bits << v
|
137
|
+
end
|
138
|
+
[bits, rest]
|
139
|
+
end
|
140
|
+
|
141
|
+
# get a permissive version of this bitmap
|
142
|
+
# @return [Bitmap] permissive version of the bitmap.
|
143
|
+
#
|
144
|
+
# @example
|
145
|
+
# b = CTypes::Helpers.bitmap(uint8, {a: 0})
|
146
|
+
# b.unpack("\x04") # => Dry::Types::ConstraintError
|
147
|
+
#
|
148
|
+
# b = CTypes::Helpers.bitmap(uint8, {a: 0}).permissive
|
149
|
+
# b.unpack("\x04") # => [:bit_2]
|
150
|
+
def permissive
|
151
|
+
Bitmap.new(type: @type, bits: @bits.permissive)
|
152
|
+
end
|
153
|
+
end
|
154
|
+
end
|
@@ -0,0 +1,85 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# SPDX-FileCopyrightText: 2025 Cisco
|
4
|
+
# SPDX-License-Identifier: MIT
|
5
|
+
|
6
|
+
module CTypes
|
7
|
+
# {Enum} layout builder
|
8
|
+
#
|
9
|
+
# This class is used to define the name/value pairs for {Enum} types. The
|
10
|
+
# builder supports dynamic numbering similar to that used in C. The
|
11
|
+
# {Builder} is used internally by {Enum}, and should not be used directly.
|
12
|
+
#
|
13
|
+
# @example Supported syntax and dynamic numbering
|
14
|
+
# CTypes::Enum.new do |builder|
|
15
|
+
# # automatic numbering
|
16
|
+
# builder << :a # :a will have value 0
|
17
|
+
# builder << :b # :b will have value 1
|
18
|
+
# builder << {c: 10} # :c will have value 10
|
19
|
+
# builder << :d # :d will have value 11
|
20
|
+
#
|
21
|
+
# # bulk assignment
|
22
|
+
# builder << %i[e f g] # :e == 12, :f == 11, :g == 12
|
23
|
+
#
|
24
|
+
# # bulk assignment with values
|
25
|
+
# builder << {z: 25, y: 24, x: 23}
|
26
|
+
# builder << :max # :max == 26
|
27
|
+
# end
|
28
|
+
class Enum::Builder
|
29
|
+
def initialize(&block)
|
30
|
+
@map = {}
|
31
|
+
@next = 0
|
32
|
+
@default = nil
|
33
|
+
block.call(self)
|
34
|
+
end
|
35
|
+
attr_reader :map
|
36
|
+
attr_accessor :default
|
37
|
+
|
38
|
+
# append new key/value pairs to the {Enum}
|
39
|
+
# @param value name or name/value pairs
|
40
|
+
#
|
41
|
+
# @example
|
42
|
+
# CTypes::Enum.new do |builder|
|
43
|
+
# # automatic numbering
|
44
|
+
# builder << :a # :a will have value 0
|
45
|
+
# builder << :b # :b will have value 1
|
46
|
+
# builder << {c: 10} # :c will have value 10
|
47
|
+
# builder << :d # :d will have value 11
|
48
|
+
#
|
49
|
+
# # bulk assignment
|
50
|
+
# builder << %i[e f g] # :e == 12, :f == 11, :g == 12
|
51
|
+
#
|
52
|
+
# # bulk assignment with values
|
53
|
+
# builder << {z: 25, y: 24, x: 23}
|
54
|
+
# builder << :max # :max == 26
|
55
|
+
# end
|
56
|
+
def <<(value)
|
57
|
+
case value
|
58
|
+
when Hash
|
59
|
+
value.each_pair { |k, v| set(k, v) }
|
60
|
+
when ::Array
|
61
|
+
value.each { |v| set(v, @next) }
|
62
|
+
else
|
63
|
+
set(value, @next)
|
64
|
+
end
|
65
|
+
self
|
66
|
+
end
|
67
|
+
|
68
|
+
def push(*values)
|
69
|
+
values.each { |v| self << v }
|
70
|
+
end
|
71
|
+
|
72
|
+
private
|
73
|
+
|
74
|
+
def set(key, value)
|
75
|
+
key = key.to_sym
|
76
|
+
raise Error, "duplicate key %p" % key if
|
77
|
+
@map.has_key?(key)
|
78
|
+
raise Error, "value must be Integer: %p" unless
|
79
|
+
value.is_a?(Integer)
|
80
|
+
@map[key] = value
|
81
|
+
@next = value + 1 if value >= @next
|
82
|
+
@default ||= key
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
data/lib/ctypes/enum.rb
ADDED
@@ -0,0 +1,201 @@
|
|
1
|
+
# encoding: ASCII-8BIT
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
# SPDX-FileCopyrightText: 2025 Cisco
|
5
|
+
# SPDX-License-Identifier: MIT
|
6
|
+
require "forwardable"
|
7
|
+
|
8
|
+
module CTypes
|
9
|
+
# Pack & unpack C enum values
|
10
|
+
#
|
11
|
+
# @example 32-bit contiguous enum
|
12
|
+
# state = Enum.new(%i[start stop block])
|
13
|
+
# state.pack(:block) # => "\2\0\0\0"
|
14
|
+
# state.unpack("\0\0\0\0") # => :start
|
15
|
+
#
|
16
|
+
# @example 8-bit contiguous enum
|
17
|
+
# state = Enum.new(UInt8, %i[start stop block])
|
18
|
+
# state.pack(:block) # => "\2"
|
19
|
+
# state.unpack("\1") # => :stop
|
20
|
+
#
|
21
|
+
# @example sparse enum
|
22
|
+
# state = Enum.new do |e|
|
23
|
+
# e << %i{a b c}
|
24
|
+
# e << {d: 32}
|
25
|
+
# end
|
26
|
+
# state.pack(:d) # => "\x20\x00\x00\x00"
|
27
|
+
# state.unpack("\x02\x00\x00\x00") # => :c
|
28
|
+
#
|
29
|
+
class Enum
|
30
|
+
include Type
|
31
|
+
using PrettyPrintHelpers
|
32
|
+
extend Forwardable
|
33
|
+
|
34
|
+
# @example contiguous 32-bit enum
|
35
|
+
# Enum.new([:a, :b, :c])
|
36
|
+
# Enum.new(%i[a b c])
|
37
|
+
#
|
38
|
+
# @example contiguous 8-bit enum
|
39
|
+
# Enum.new(:uint8, %i[a b c])
|
40
|
+
#
|
41
|
+
# @example sparse enum
|
42
|
+
# Enum.new({a: 46, b: 789})
|
43
|
+
#
|
44
|
+
# @example 16-bit sparse enum
|
45
|
+
# Enum.new(UInt16, {a: 46, b: 789})
|
46
|
+
#
|
47
|
+
# @example 8-bit sparse enum
|
48
|
+
# Enum.new(Uint8) do |e|
|
49
|
+
# e << %i{zero one two} # define 0, 1, 2
|
50
|
+
# e << {eighty: 80} # skip 3-79 (incl), define 80
|
51
|
+
# e << :eighty_one
|
52
|
+
# e << {a: 100, b: 200} # define multiple sparse values
|
53
|
+
# end
|
54
|
+
#
|
55
|
+
# @example dynamically generated enum
|
56
|
+
# Enum.new do |e|
|
57
|
+
# # declare state_0 through state_31
|
58
|
+
# 32.times do |i|
|
59
|
+
# e << "state_#{i}"
|
60
|
+
# end
|
61
|
+
# end
|
62
|
+
#
|
63
|
+
# @see Builder
|
64
|
+
def initialize(type = Helpers.uint32, values = nil, permissive: false,
|
65
|
+
&block)
|
66
|
+
builder = if block
|
67
|
+
Builder.new(&block)
|
68
|
+
else
|
69
|
+
if values.nil?
|
70
|
+
values = type
|
71
|
+
type = Helpers.uint32
|
72
|
+
end
|
73
|
+
|
74
|
+
Builder.new { |b| b << values }
|
75
|
+
end
|
76
|
+
|
77
|
+
@dry_type = Dry::Types["symbol"]
|
78
|
+
.default(builder.default)
|
79
|
+
.enum(builder.map)
|
80
|
+
@type = type
|
81
|
+
@size = @type.size
|
82
|
+
@dry_type = @dry_type.lax if permissive
|
83
|
+
end
|
84
|
+
attr_reader :size, :type
|
85
|
+
def_delegators :@type, :signed?, :greedy?
|
86
|
+
|
87
|
+
# encode a ruby type into a String containing the binary representation of
|
88
|
+
# the enum
|
89
|
+
#
|
90
|
+
# @param value [Symbol, Integer] value to be encoded
|
91
|
+
# @param endian [Symbol] endian to pack with
|
92
|
+
# @param validate [Boolean] set to false to disable value validation
|
93
|
+
# @return [::String] binary encoding for value
|
94
|
+
#
|
95
|
+
# @example
|
96
|
+
# e = Enum.new(%i[stopped running blocked])
|
97
|
+
# e.pack(:running) # => "\1\0\0\0"
|
98
|
+
# e.pack(2) # => "\2\0\0\0"
|
99
|
+
def pack(value, endian: default_endian, validate: true)
|
100
|
+
value = @dry_type[value] if validate
|
101
|
+
out = @dry_type.mapping[value]
|
102
|
+
out ||= case value
|
103
|
+
when /\Aunknown_(\h+)\z/
|
104
|
+
out = $1.to_i(16)
|
105
|
+
when Integer
|
106
|
+
value
|
107
|
+
else
|
108
|
+
raise Error, "unknown enum value: %p" % value
|
109
|
+
end
|
110
|
+
|
111
|
+
@type.pack(out, endian: @type.endian || endian, validate:)
|
112
|
+
end
|
113
|
+
|
114
|
+
# convert a String containing the binary represention of a c enum into the
|
115
|
+
# ruby value
|
116
|
+
#
|
117
|
+
# @param buf [String] bytes that make up the type
|
118
|
+
# @param endian [Symbol] endian of data within buf
|
119
|
+
# @return [Array(Symbol, ::String)] decoded type, and remaining bytes
|
120
|
+
# @see Type#unpack
|
121
|
+
#
|
122
|
+
# @example
|
123
|
+
# e = Enum.new(%i[stopped running blocked])
|
124
|
+
# e.unpack("\1\0\0\0") # => :running
|
125
|
+
def unpack_one(buf, endian: default_endian)
|
126
|
+
value, rest = @type.unpack_one(buf, endian: @type.endian || endian)
|
127
|
+
out = @dry_type[value]
|
128
|
+
out = ("unknown_%0#{@size * 2}x" % value).to_sym unless out.is_a?(Symbol)
|
129
|
+
[out, rest]
|
130
|
+
end
|
131
|
+
|
132
|
+
# @api private
|
133
|
+
def mapping
|
134
|
+
@dry_type.mapping
|
135
|
+
end
|
136
|
+
|
137
|
+
def pretty_print(q) # :nodoc:
|
138
|
+
q.group(2, "enum(", ")") do
|
139
|
+
if @type != Helpers.uint32
|
140
|
+
q.pp(@type)
|
141
|
+
q.comma_breakable
|
142
|
+
end
|
143
|
+
q.group(0, "{", "}") do
|
144
|
+
q.seplist(@dry_type.mapping) do |name, value|
|
145
|
+
q.text("#{name}: #{value}")
|
146
|
+
end
|
147
|
+
end
|
148
|
+
end
|
149
|
+
end
|
150
|
+
alias_method :inspect, :pretty_inspect # :nodoc:
|
151
|
+
|
152
|
+
def export_type(q)
|
153
|
+
q << "enum("
|
154
|
+
if @type != UInt32
|
155
|
+
q << @type
|
156
|
+
q << ", "
|
157
|
+
end
|
158
|
+
q << "{"
|
159
|
+
q.break
|
160
|
+
|
161
|
+
q.nest(2) do
|
162
|
+
@dry_type.mapping.each do |name, value|
|
163
|
+
q << "#{name}: #{value},"
|
164
|
+
q.break
|
165
|
+
end
|
166
|
+
end
|
167
|
+
q << "})"
|
168
|
+
q << ".permissive" if @dry_type.is_a?(Dry::Types::Lax)
|
169
|
+
end
|
170
|
+
|
171
|
+
def ==(other)
|
172
|
+
return false unless other.is_a?(Enum)
|
173
|
+
other.type == @type && other.mapping == @dry_type.mapping
|
174
|
+
end
|
175
|
+
|
176
|
+
def default_value
|
177
|
+
# with `.lax` added to dry_type for permissive enum, the standard
|
178
|
+
# `dry_type[]` doesn't work for a default. Instead, we're going to try
|
179
|
+
# whatever key is set for 0, and failing that, just the first value
|
180
|
+
# defined for the enum
|
181
|
+
self[0] || @dry_type.mapping.first.first
|
182
|
+
end
|
183
|
+
|
184
|
+
def permissive
|
185
|
+
Enum.new(@type, @dry_type.mapping, permissive: true)
|
186
|
+
end
|
187
|
+
|
188
|
+
# lookup the key for a given Integer value in this Enum
|
189
|
+
#
|
190
|
+
# @example
|
191
|
+
# e = Enum.new(%i[a b c])
|
192
|
+
# e[1] # => :b
|
193
|
+
# e[5] # => nil
|
194
|
+
def [](value)
|
195
|
+
@inverted_mapping ||= @dry_type.mapping.invert
|
196
|
+
@inverted_mapping[value]
|
197
|
+
end
|
198
|
+
end
|
199
|
+
end
|
200
|
+
|
201
|
+
require_relative "enum/builder"
|