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
data/lib/ctypes/union.rb
ADDED
@@ -0,0 +1,637 @@
|
|
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
|
+
# This class represents a C union in ruby. It provides methods for unpacking
|
9
|
+
# unions from their binary representation into a modifiable Ruby instance,
|
10
|
+
# and methods for repacking them into binary.
|
11
|
+
#
|
12
|
+
# The ruby representation of a union does not get the same memory overlap
|
13
|
+
# benefits as experienced in C. As a result, the ruby {Union} must unpack
|
14
|
+
# the binary string each time a different union member is accessed. To
|
15
|
+
# support modification, this also means every time we switch between union
|
16
|
+
# members, any active union member must be packed into binary format, then
|
17
|
+
# unpacked at the new member. **This is a significant performance penalty
|
18
|
+
# when working with read-write Unions.**
|
19
|
+
#
|
20
|
+
# To get arond the performance penalty, you can do one of the following:
|
21
|
+
# - do not swap between multiple union members unless absolutely necessary
|
22
|
+
# - {Union#freeze} the unpacked union instance to eliminate the repacking
|
23
|
+
# performance hit
|
24
|
+
# - figure out a memory-overlayed union implementation in ruby that doesn't
|
25
|
+
# require unpack for every member access (it would be welcomed)
|
26
|
+
#
|
27
|
+
# @example
|
28
|
+
# # encoding: ASCII-8BIT
|
29
|
+
# require_relative "./lib/ctypes"
|
30
|
+
#
|
31
|
+
# # subclass Union to define a union
|
32
|
+
# class Msg < CTypes::Union
|
33
|
+
# layout do
|
34
|
+
# # this message uses network-byte order
|
35
|
+
# endian :big
|
36
|
+
#
|
37
|
+
# # create enum for the message type used in members
|
38
|
+
# type = enum(uint8, {invalid: 0, hello: 1, read: 2})
|
39
|
+
#
|
40
|
+
# # define union members
|
41
|
+
# member :hello, struct({type:, version: string})
|
42
|
+
# member :read, struct({type:, offset: uint64, len: uint64})
|
43
|
+
# member :type, type
|
44
|
+
# member :raw, string
|
45
|
+
# end
|
46
|
+
# end
|
47
|
+
#
|
48
|
+
# # unpack a message and access member values
|
49
|
+
# msg = Msg.unpack("\x02" +
|
50
|
+
# "\xfe\xfe\xfe\xfe\xfe\xfe\xfe\xfe" +
|
51
|
+
# "\xab\xab\xab\xab\xab\xab\xab\xab")
|
52
|
+
# msg.type # => :read
|
53
|
+
# msg.read.offset # => 0xfefefefefefefefe
|
54
|
+
# msg.read.len # => 0xabababababababab
|
55
|
+
#
|
56
|
+
# # create new messages
|
57
|
+
# Msg.pack({hello: {type: :hello, version: "v1.0"}})
|
58
|
+
# # => "\1v1.0\0\0\0\0\0\0\0\0\0\0\0\0"
|
59
|
+
# Msg.pack({read: {type: :read, offset: 0xffff, len: 0x1000}})
|
60
|
+
# # => "\2\0\0\0\0\0\0\xFF\xFF\0\0\0\0\0\0\x10\0"
|
61
|
+
#
|
62
|
+
# # work with a message instance to create a message
|
63
|
+
# msg = Msg.new
|
64
|
+
# msg.hello.type = :hello
|
65
|
+
# msg.hello.version = "v1.0"
|
66
|
+
# msg.to_binstr # => "\1v1.0\0\0\0\0\0\0\0\0\0\0\0\0"
|
67
|
+
#
|
68
|
+
class Union
|
69
|
+
extend Type
|
70
|
+
using PrettyPrintHelpers
|
71
|
+
|
72
|
+
# define the layout of this union
|
73
|
+
# @see Builder
|
74
|
+
#
|
75
|
+
# @example
|
76
|
+
# class Msg < CTypes::Union
|
77
|
+
# layout do
|
78
|
+
# # this message uses network-byte order
|
79
|
+
# endian :big
|
80
|
+
#
|
81
|
+
# # create enum for the message type used in members
|
82
|
+
# type = enum(uint8, {invalid: 0, hello: 1, read: 2})
|
83
|
+
#
|
84
|
+
# # define union members
|
85
|
+
# member :hello, struct({type:, version: string})
|
86
|
+
# member :read, struct({type:, offset: uint64, len: uint64})
|
87
|
+
# member :type, type
|
88
|
+
# member :raw, string
|
89
|
+
# end
|
90
|
+
# end
|
91
|
+
def self.layout(&block)
|
92
|
+
raise Error, "no block given" unless block
|
93
|
+
builder = Builder.new(&block)
|
94
|
+
builder.instance_eval(&block)
|
95
|
+
apply_layout(builder)
|
96
|
+
end
|
97
|
+
|
98
|
+
# get an instance of the {Union::Builder}
|
99
|
+
def self.builder
|
100
|
+
Builder.new
|
101
|
+
end
|
102
|
+
|
103
|
+
# @api private
|
104
|
+
def self.apply_layout(builder)
|
105
|
+
@name, @fields, @dry_type, @size, @fixed_size, @endian = builder.result
|
106
|
+
|
107
|
+
@field_accessors ||= []
|
108
|
+
remove_method(*@field_accessors.flatten)
|
109
|
+
@field_accessors.clear
|
110
|
+
@field_types = {}
|
111
|
+
@greedy = false
|
112
|
+
|
113
|
+
@fields.each do |field|
|
114
|
+
# split out the array; we do it this way because we want to reference
|
115
|
+
# the original fields array when assigning @field_types
|
116
|
+
name, type = field
|
117
|
+
|
118
|
+
# the union will be flagged as greedy if size is not defined by a Proc,
|
119
|
+
# and the field type is greedy
|
120
|
+
@greedy ||= type.greedy? unless @size.is_a?(Proc)
|
121
|
+
|
122
|
+
case name
|
123
|
+
when Symbol
|
124
|
+
@field_accessors += [
|
125
|
+
define_method(name) { self[name] },
|
126
|
+
define_method(:"#{name}=") { |v| self[name] = v }
|
127
|
+
]
|
128
|
+
@field_types[name] = field
|
129
|
+
when ::Array
|
130
|
+
name.each do |n|
|
131
|
+
@field_accessors += [
|
132
|
+
define_method(n) { self[n] },
|
133
|
+
define_method(:"#{n}=") { |v| self[n] = v }
|
134
|
+
]
|
135
|
+
@field_types[n] = field
|
136
|
+
end
|
137
|
+
else
|
138
|
+
raise Error, "unsupported field name type: %p", name
|
139
|
+
end
|
140
|
+
end
|
141
|
+
end
|
142
|
+
private_class_method :apply_layout
|
143
|
+
|
144
|
+
# encode a ruby Hash into a String containing the binary representation of
|
145
|
+
# the Union
|
146
|
+
#
|
147
|
+
# @param value [Hash] value to be encoded; must have size <= 1
|
148
|
+
# @param endian [Symbol] endian to pack with
|
149
|
+
# @param validate [Boolean] set to false to disable value validation
|
150
|
+
# @param pad_bytes [String] bytes to used to pad; defaults to null bytes
|
151
|
+
# @note do not provide multiple member values to this method; only zero or
|
152
|
+
# one member values are supported.
|
153
|
+
# @return [::String] binary encoding for value
|
154
|
+
#
|
155
|
+
# @example
|
156
|
+
# include CTypes::Helpers
|
157
|
+
# t = union(word: uint32, bytes: array(uint8, 4))
|
158
|
+
# t.pack({word: 0xfeedface}) # => "\xCE\xFA\xED\xFE"
|
159
|
+
# t.pack({word: 0xfeedface}, endian: :big)
|
160
|
+
# # => "\xFE\xED\xFA\xCE"
|
161
|
+
#
|
162
|
+
# t.pack({bytes: [1, 2, 3, 4]}) # => "\x01\x02\x03\x04"
|
163
|
+
# t.pack({bytes: [1, 2, 3, 4]}, endian: :big)
|
164
|
+
# # => "\x01\x02\x03\x04"
|
165
|
+
#
|
166
|
+
# t.pack({bytes: [1, 2]}) # => "\x01\x02\x00\x00"
|
167
|
+
# t.pack({bytes: []}) # => "\x00\x00\x00\x00"
|
168
|
+
# t.pack({bytes: nil}) # => "\x00\x00\x00\x00"
|
169
|
+
# t.pack({}) # => "\x00\x00\x00\x00"
|
170
|
+
# t.pack({word: 20, bytes: []}) # => CTypes::Error
|
171
|
+
#
|
172
|
+
# @example using pad_bytes
|
173
|
+
# t = union { member :u8, uint8, member :u32, uint32 }
|
174
|
+
# t.pack({u8: 0}, pad_bytes: "ABCD") # => "\0BCD"
|
175
|
+
def self.pack(value, endian: default_endian, validate: true, pad_bytes: nil)
|
176
|
+
value = value.to_hash.freeze
|
177
|
+
unknown_keys = value&.keys
|
178
|
+
members = @fields.filter_map do |name, type|
|
179
|
+
case name
|
180
|
+
when Symbol
|
181
|
+
unknown_keys.delete(name)
|
182
|
+
[name, type, value[name]] if value.has_key?(name)
|
183
|
+
when ::Array
|
184
|
+
unknown_keys.reject! { |k| name.include?(k) }
|
185
|
+
[name, type, value.slice(*name)] if name.any? { |n| value.has_key?(n) }
|
186
|
+
else
|
187
|
+
raise Error, "unsupported field name type: %p" % [name]
|
188
|
+
end
|
189
|
+
end
|
190
|
+
|
191
|
+
# raise an error if they provided multiple member values
|
192
|
+
if members.size > 1
|
193
|
+
raise Error, <<~MSG % [members.map { |name, _| name }]
|
194
|
+
conflicting values for Union#pack; only supply one union member: %p
|
195
|
+
MSG
|
196
|
+
|
197
|
+
# raise an error if they provided extra keys that aren't for a member
|
198
|
+
elsif !unknown_keys.empty?
|
199
|
+
raise Error, "unknown member names: %p" % [unknown_keys]
|
200
|
+
|
201
|
+
# if they didn't provide any key, use the first member
|
202
|
+
elsif members.empty?
|
203
|
+
name, type = @fields.first
|
204
|
+
members << [name, type, type.default_value]
|
205
|
+
end
|
206
|
+
|
207
|
+
# we have a single member value to pack; let's grab the type & value and
|
208
|
+
# pack it
|
209
|
+
_, type, val = members[0]
|
210
|
+
out = if type.respond_to?(:ancestors) && type.ancestors.include?(Union)
|
211
|
+
type.pack(val, endian: type.endian || endian, validate:, pad_bytes:)
|
212
|
+
else
|
213
|
+
type.pack(val, endian: type.endian || endian, validate:)
|
214
|
+
end
|
215
|
+
|
216
|
+
# @size has two different behaviors. When @size is a proc, then the size
|
217
|
+
# is an absolute length. Otherwise, size is a minimum length. To start
|
218
|
+
# with, let's calculate a minimum length.
|
219
|
+
#
|
220
|
+
# @size has two different behaviors. When @size is a Proc, it represents
|
221
|
+
# an absolute length. Otherwise, size represents a minimum length. We
|
222
|
+
# do this to support unions with greedy members without the union itself
|
223
|
+
# being greedy.
|
224
|
+
#
|
225
|
+
# So grab the minimum length of the output string and expand the output
|
226
|
+
# to be at least that long.
|
227
|
+
min_length = if @size.is_a?(Proc)
|
228
|
+
|
229
|
+
# Run the size proc with a Union made of our output. Yes we end up
|
230
|
+
# unpacking what we just packed in this case, but it's the cost of
|
231
|
+
# supporting pack of dynamically sized unions.
|
232
|
+
begin
|
233
|
+
instance_exec(new(buf: out, endian:).freeze, &@size)
|
234
|
+
|
235
|
+
# so there's a chance that the packed union value has fewer bytes
|
236
|
+
# than what is required to evaluate the size proc. If we get a missing
|
237
|
+
# bytes error, let's pad the out string with the needed number of
|
238
|
+
# bytes. This may happen multiple times as we have no idea what is
|
239
|
+
# in the size proc.
|
240
|
+
rescue CTypes::MissingBytesError => ex
|
241
|
+
out << if pad_bytes && out.size < pad_bytes.size
|
242
|
+
pad_bytes.byteslice(out.size, ex.need)
|
243
|
+
else
|
244
|
+
"\0" * ex.need
|
245
|
+
end
|
246
|
+
retry
|
247
|
+
end
|
248
|
+
else
|
249
|
+
@size
|
250
|
+
end
|
251
|
+
|
252
|
+
# when we need to pad the output, use pad_bytes first
|
253
|
+
if out.size < min_length && pad_bytes
|
254
|
+
out << pad_bytes.byteslice(out.size, min_length - out.size)
|
255
|
+
end
|
256
|
+
|
257
|
+
# if we still need more bytes, pad with zeros
|
258
|
+
if out.size < min_length
|
259
|
+
out << "\0" * (min_length - out.size)
|
260
|
+
end
|
261
|
+
|
262
|
+
# Now, if @size is a Proc only return the absolute length bytes of our
|
263
|
+
# output string. Everything else gets the full output string
|
264
|
+
@size.is_a?(Proc) ? out.byteslice(0, min_length) : out
|
265
|
+
end
|
266
|
+
|
267
|
+
# convert a String containing the binary represention of a c union into
|
268
|
+
# a ruby type
|
269
|
+
#
|
270
|
+
# @param buf [String] bytes that make up the type
|
271
|
+
# @param endian [Symbol] endian of data within buf
|
272
|
+
# @return [::Array(Union, ::String)] Union, and remaining bytes
|
273
|
+
#
|
274
|
+
# @see Type#unpack
|
275
|
+
#
|
276
|
+
# @example
|
277
|
+
# class Msg < CTypes::Union
|
278
|
+
# layout do
|
279
|
+
# type = enum(uint8, {invalid: 0, hello: 1, read: 2})
|
280
|
+
# member :hello, struct({type:, version: string})
|
281
|
+
# member :read, struct({type:, offset: uint64, len: uint64})
|
282
|
+
# member :type, type
|
283
|
+
# end
|
284
|
+
# end
|
285
|
+
#
|
286
|
+
# msg, rest = Msg.unpack_one("\x02" +
|
287
|
+
# "\xfe\xfe\xfe\xfe\xfe\xfe\xfe\xfe" +
|
288
|
+
# "\xab\xab\xab\xab\xab\xab\xab\xab" +
|
289
|
+
# "extra_bytes")
|
290
|
+
# msg.type # => :read
|
291
|
+
# msg.read.offset # => 0xfefefefefefefefe
|
292
|
+
# msg.read.len # => 0xabababababababab
|
293
|
+
# rest # => "extra_bytes"
|
294
|
+
def self.unpack_one(buf, endian: default_endian)
|
295
|
+
size = if @size.is_a?(Proc)
|
296
|
+
instance_exec(new(buf:, endian:).freeze, &@size)
|
297
|
+
else
|
298
|
+
@size
|
299
|
+
end
|
300
|
+
|
301
|
+
raise missing_bytes_error(input: buf, need: size) if buf.size < size
|
302
|
+
if fixed_size? || @size.is_a?(Proc)
|
303
|
+
buf, rest = buf.byteslice(0, size), buf.byteslice(size..)
|
304
|
+
else
|
305
|
+
rest = ""
|
306
|
+
end
|
307
|
+
|
308
|
+
[new(buf:, endian:), rest]
|
309
|
+
end
|
310
|
+
|
311
|
+
# @api private
|
312
|
+
def self.unpack_field(field:, buf:, endian:)
|
313
|
+
name, type = @field_types[field]
|
314
|
+
raise UnknownMemberError, "unknown member: %p" % [field] unless type
|
315
|
+
|
316
|
+
case name
|
317
|
+
when Symbol
|
318
|
+
{name => type.with_endian(endian).unpack(buf)}
|
319
|
+
when ::Array
|
320
|
+
type.with_endian(endian).unpack(buf, endian:)
|
321
|
+
end
|
322
|
+
end
|
323
|
+
|
324
|
+
# return the struct name
|
325
|
+
# @api private
|
326
|
+
def self.type_name
|
327
|
+
@name
|
328
|
+
end
|
329
|
+
|
330
|
+
# @api private
|
331
|
+
def self.greedy?
|
332
|
+
@greedy
|
333
|
+
end
|
334
|
+
|
335
|
+
# check if this is a fixed-size Union
|
336
|
+
def self.fixed_size?
|
337
|
+
@fixed_size
|
338
|
+
end
|
339
|
+
|
340
|
+
# return minimum size of the Union
|
341
|
+
# @see Type.size
|
342
|
+
def self.size
|
343
|
+
@size
|
344
|
+
end
|
345
|
+
|
346
|
+
# return the size of a member
|
347
|
+
# @see Type.size
|
348
|
+
def self.sizeof(member)
|
349
|
+
@field_types[member][1].size
|
350
|
+
end
|
351
|
+
|
352
|
+
# get the list of members in this Union
|
353
|
+
#
|
354
|
+
# @return [::Array<Symbol>] member names
|
355
|
+
def self.fields
|
356
|
+
@field_types.keys
|
357
|
+
end
|
358
|
+
|
359
|
+
# get the list of members in this Union
|
360
|
+
#
|
361
|
+
# @return array of field layout
|
362
|
+
def self.field_layout
|
363
|
+
@fields
|
364
|
+
end
|
365
|
+
|
366
|
+
# check if the Union has a given member
|
367
|
+
# @param member [Symbol] member name
|
368
|
+
def self.has_field?(member)
|
369
|
+
@field_types.has_key?(member)
|
370
|
+
end
|
371
|
+
|
372
|
+
# check if another Union subclass has the same members as this Union
|
373
|
+
def self.==(other)
|
374
|
+
return true if super
|
375
|
+
return false unless other.is_a?(Class) && other < Union
|
376
|
+
other.field_layout == @fields &&
|
377
|
+
other.default_endian == default_endian &&
|
378
|
+
other.size == size
|
379
|
+
end
|
380
|
+
|
381
|
+
def self.pretty_print(q) # :nodoc:
|
382
|
+
q.ctype("union", @endian) do
|
383
|
+
q.seplist(@fields, -> { q.breakable("; ") }) do |name, type|
|
384
|
+
case name
|
385
|
+
when Symbol
|
386
|
+
q.text("member %p, " % name)
|
387
|
+
when ::Array
|
388
|
+
q.text("member ")
|
389
|
+
end
|
390
|
+
q.pp(type)
|
391
|
+
end
|
392
|
+
end
|
393
|
+
end
|
394
|
+
|
395
|
+
class << self
|
396
|
+
alias_method :inspect, :pretty_inspect # :nodoc:
|
397
|
+
end
|
398
|
+
|
399
|
+
# @api private
|
400
|
+
def self.export_type(q)
|
401
|
+
q << "CTypes::Union.builder()"
|
402
|
+
q.break
|
403
|
+
q.nest(2) do
|
404
|
+
q << ".endian(%p)\n" % [@endian] if @endian
|
405
|
+
@fields.each do |name, type|
|
406
|
+
case name
|
407
|
+
when Symbol
|
408
|
+
q << ".member(%p, " % [name]
|
409
|
+
q << type
|
410
|
+
q << ")"
|
411
|
+
when ::Array
|
412
|
+
q << ".member("
|
413
|
+
q << type
|
414
|
+
q << ")"
|
415
|
+
else
|
416
|
+
raise Error, "unsupported field name type: %p" % [name]
|
417
|
+
end
|
418
|
+
q.break
|
419
|
+
end
|
420
|
+
q << ".build()"
|
421
|
+
end
|
422
|
+
end
|
423
|
+
|
424
|
+
# @param buf [String] binary String containing Union memory
|
425
|
+
# @param endian [Symbol] byte-order of buf
|
426
|
+
def initialize(
|
427
|
+
buf: "\0" * self.class.size,
|
428
|
+
endian: self.class.default_endian
|
429
|
+
)
|
430
|
+
@buf = buf
|
431
|
+
@endian = endian
|
432
|
+
end
|
433
|
+
|
434
|
+
# freeze the values within the Union
|
435
|
+
#
|
436
|
+
# This is used to eliminate the pack/unpack performance penalty when
|
437
|
+
# accessing multiple members in a read-only Union. By freezing the Union
|
438
|
+
# we can avoid packing the existing member when accessing another memeber.
|
439
|
+
def freeze
|
440
|
+
@frozen = true
|
441
|
+
self
|
442
|
+
end
|
443
|
+
|
444
|
+
def frozen?
|
445
|
+
@frozen == true
|
446
|
+
end
|
447
|
+
|
448
|
+
# get a member value
|
449
|
+
#
|
450
|
+
# @param member [Symbol] member name
|
451
|
+
# @note only the value for the most recently accessed member is cached
|
452
|
+
# @note WARNING: accessing any member will erase any modifications made to
|
453
|
+
# other members of the union
|
454
|
+
#
|
455
|
+
# @example
|
456
|
+
# include CTypes::Helpers
|
457
|
+
# t = union(word: uint32, bytes: array(uint8, 4), str: string)
|
458
|
+
# u = t.unpack("hello world")
|
459
|
+
# u[:str] # => "hello world"
|
460
|
+
# u[:word] # => 1819043176
|
461
|
+
# u[:bytes] # => [104, 101, 108, 108]
|
462
|
+
#
|
463
|
+
# @example nested struct
|
464
|
+
# include CTypes::Helpers
|
465
|
+
# t = union(a: struct(a: uint8, b: uint8, c: uint16), raw: string)
|
466
|
+
# u = t.unpack("\x01\x02\xed\xfe")
|
467
|
+
# u.a # => #<struct a=1, b=2, c=0xfeed>
|
468
|
+
#
|
469
|
+
# @example ERROR: wiping modified member value by accident
|
470
|
+
# include CTypes::Helpers
|
471
|
+
# t = union(word: uint32, bytes: array(uint8, 4))
|
472
|
+
# u = t.new
|
473
|
+
# u[:bytes] = [1,2,3]
|
474
|
+
# u[:word] # ERROR!!! erases changes to bytes
|
475
|
+
# u[:bytes] # => [0, 0, 0, 0]
|
476
|
+
#
|
477
|
+
def [](name)
|
478
|
+
v = active_field(name)[name]
|
479
|
+
unless frozen? ||
|
480
|
+
v.is_a?(Integer) ||
|
481
|
+
v.is_a?(TrueClass) ||
|
482
|
+
v.is_a?(FalseClass)
|
483
|
+
@changed = true
|
484
|
+
end
|
485
|
+
v
|
486
|
+
end
|
487
|
+
|
488
|
+
# set a member value
|
489
|
+
# @param member [Symbol] member name
|
490
|
+
# @param value member value
|
491
|
+
# @note WARNING: modifying any member will erase any modifications made to
|
492
|
+
# other members of the union
|
493
|
+
#
|
494
|
+
# @example
|
495
|
+
# include CTypes::Helpers
|
496
|
+
# t = union(word: uint32, bytes: array(uint8, 4), str: string)
|
497
|
+
# u = t.new
|
498
|
+
# u[:bytes] = [1,2,3,4]
|
499
|
+
# u.to_binstr # => "\x01\x02\x03\x04"
|
500
|
+
# u[:bytes] = [1,2,3]
|
501
|
+
# u.to_binstr # => "\x01\x02\x03\x00"
|
502
|
+
def []=(name, value)
|
503
|
+
raise FrozenError, "can't modify frozen Union: %p" % [self] if frozen?
|
504
|
+
active_field(name)[name] = value
|
505
|
+
@changed = true
|
506
|
+
end
|
507
|
+
|
508
|
+
def has_key?(name)
|
509
|
+
self.class.has_field?(name)
|
510
|
+
end
|
511
|
+
|
512
|
+
# return a Hash representation of the Union
|
513
|
+
# @param shallow [Boolean] set to true to disable deep traversal
|
514
|
+
# @return [Hash]
|
515
|
+
#
|
516
|
+
# @example
|
517
|
+
# include CTypes::Helpers
|
518
|
+
# t = union(bytes: array(uint8, 4),
|
519
|
+
# str: string,
|
520
|
+
# nested: struct(a: uint8, b: uint8, c: uint16))
|
521
|
+
# u = t.unpack("hello world")
|
522
|
+
# u.to_h # => {:bytes=>[104, 101, 108, 108],
|
523
|
+
# # :str=>"hello world",
|
524
|
+
# # :nested=>{
|
525
|
+
# # :a=>104, :b=>101, :c=>27756
|
526
|
+
# # }}
|
527
|
+
def to_h(shallow: false)
|
528
|
+
# grab the cached active field, or the default one
|
529
|
+
out = @active_field || active_field
|
530
|
+
out = out.is_a?(Hash) ? out.dup : out.to_h
|
531
|
+
|
532
|
+
# now convert all the values to hashes unless we're doing a shallow to_h
|
533
|
+
unless shallow
|
534
|
+
out.transform_values! do |v|
|
535
|
+
case v
|
536
|
+
when ::Array
|
537
|
+
v
|
538
|
+
else
|
539
|
+
v.respond_to?(:to_h) ? v.to_h : v
|
540
|
+
end
|
541
|
+
end
|
542
|
+
end
|
543
|
+
|
544
|
+
out
|
545
|
+
end
|
546
|
+
alias_method :to_hash, :to_h
|
547
|
+
|
548
|
+
def pretty_print(q) # :nodoc:
|
549
|
+
# before printing, apply any changes to the buffer
|
550
|
+
apply_changes!
|
551
|
+
|
552
|
+
active = to_h
|
553
|
+
|
554
|
+
open = if (name = self.class.type_name || self.class.name)
|
555
|
+
"union #{name} {"
|
556
|
+
else
|
557
|
+
"union {"
|
558
|
+
end
|
559
|
+
q.group(4, open, "}") do
|
560
|
+
q.seplist(self.class.fields, -> { q.breakable("") }) do |name|
|
561
|
+
q.text(".#{name} = ")
|
562
|
+
unless active.has_key?(name)
|
563
|
+
begin
|
564
|
+
v = self.class.unpack_field(field: name, buf: @buf, endian: @endian)
|
565
|
+
active.merge!(v)
|
566
|
+
rescue Error => ex
|
567
|
+
active[name] = "[unpack failed: %p]" % [ex]
|
568
|
+
end
|
569
|
+
end
|
570
|
+
|
571
|
+
q.pp(active[name])
|
572
|
+
q.text(", ")
|
573
|
+
end
|
574
|
+
end
|
575
|
+
end
|
576
|
+
alias_method :inspect, :pretty_inspect # :nodoc:
|
577
|
+
|
578
|
+
# return the binary representation of this Union
|
579
|
+
#
|
580
|
+
# This method calls [Union.pack] on the most recentlu accessed member of
|
581
|
+
# the Union. If no member has been accessed, it returns the original
|
582
|
+
# String it was initialized with
|
583
|
+
#
|
584
|
+
# @return [String] binary representation of union
|
585
|
+
#
|
586
|
+
# @example
|
587
|
+
# include CTypes::Helpers
|
588
|
+
# t = union(word: uint32, bytes: array(uint8, 4), str: string)
|
589
|
+
# u = t.new
|
590
|
+
# u.bytes = [1,2,3,4]
|
591
|
+
# u.to_binstr # => "\x01\x02\x03\x04"
|
592
|
+
#
|
593
|
+
# @example accessing member after modifying other member resets members
|
594
|
+
# include CTypes::Helpers
|
595
|
+
# t = union(word: uint32, bytes: array(uint8, 4), str: string)
|
596
|
+
# u = t.new
|
597
|
+
# u.bytes = [1,2,3,4]
|
598
|
+
# u.to_binstr # => "\x01\x02\x03\x04"
|
599
|
+
# u.word # => ERROR: resets changes to .bytes
|
600
|
+
# u.to_binstr # => "\x00\x00\x00\x00"
|
601
|
+
def to_binstr(endian: @endian)
|
602
|
+
endian ||= self.class.default_endian
|
603
|
+
|
604
|
+
if endian != @endian
|
605
|
+
self.class.pack((@active_field || active_field).to_h, endian:)
|
606
|
+
else
|
607
|
+
apply_changes!
|
608
|
+
@buf.dup
|
609
|
+
end
|
610
|
+
end
|
611
|
+
|
612
|
+
# @api private
|
613
|
+
def active_field(name = nil)
|
614
|
+
name ||= self.class.fields.first
|
615
|
+
|
616
|
+
unless @active_field&.has_key?(name)
|
617
|
+
apply_changes!
|
618
|
+
@active_field = nil
|
619
|
+
@active_field = self.class
|
620
|
+
.unpack_field(field: name, buf: @buf, endian: @endian)
|
621
|
+
end
|
622
|
+
@active_field
|
623
|
+
end
|
624
|
+
private :active_field
|
625
|
+
|
626
|
+
# @api private
|
627
|
+
def apply_changes!
|
628
|
+
return false if frozen?
|
629
|
+
return false unless @active_field && @changed
|
630
|
+
@buf = self.class.pack(@active_field, pad_bytes: @buf)
|
631
|
+
@changed = false
|
632
|
+
true
|
633
|
+
end
|
634
|
+
private :apply_changes!
|
635
|
+
end
|
636
|
+
end
|
637
|
+
require_relative "union/builder"
|