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,529 @@
|
|
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 is used to represent c structures in ruby. It provides methods
|
9
|
+
# for converting structs between their byte representation and a ruby
|
10
|
+
# representation that can be modified.
|
11
|
+
#
|
12
|
+
# @note fields are not automatically aligned based on size; if there are gaps
|
13
|
+
# present between c struct fields, you'll need to manually add padding in
|
14
|
+
# the layout to reflect that alignment.
|
15
|
+
#
|
16
|
+
# @example working with a Type-Length-Value (TLV) struct
|
17
|
+
# # encoding: ASCII-8BIT
|
18
|
+
#
|
19
|
+
# # subclass Struct to define a structure
|
20
|
+
# class TLV < CTypes::Struct
|
21
|
+
# # define structure layout
|
22
|
+
# layout do
|
23
|
+
# attribute :type, enum(uint8, %i[hello read write bye])
|
24
|
+
# attribute :len, uint16.with_endian(:big)
|
25
|
+
# attribute :value, string
|
26
|
+
# size { |struct| offsetof(:value) + struct[:len] }
|
27
|
+
# end
|
28
|
+
#
|
29
|
+
# # add any class or instance methods if needed
|
30
|
+
# end
|
31
|
+
#
|
32
|
+
# # pack the struct into bytes
|
33
|
+
# bytes = TLV.pack({type: :hello, len: 5, value: "world"})
|
34
|
+
# # => "\0\0\5world"
|
35
|
+
#
|
36
|
+
# # unpack bytes into a struct instance
|
37
|
+
# t = TLV.unpack("\0\0\5world")
|
38
|
+
# # => #<TLV type=:hello len=5 value="world">
|
39
|
+
#
|
40
|
+
# # access struct fields
|
41
|
+
# t.value # => "world"
|
42
|
+
#
|
43
|
+
# # update struct fields, then convert back into bytes
|
44
|
+
# t.type = :bye
|
45
|
+
# t.value = "goodbye"
|
46
|
+
# t.len = t.value.size
|
47
|
+
# t.to_binstr # => "\3\0\7goodbye"
|
48
|
+
#
|
49
|
+
# @example nested structs
|
50
|
+
# class Attribute < CTypes::Struct
|
51
|
+
# layout do
|
52
|
+
# attribute :base, uint8
|
53
|
+
# attribute :mod, int8
|
54
|
+
# end
|
55
|
+
# end
|
56
|
+
# class Character < CTypes::Struct
|
57
|
+
# layout do
|
58
|
+
# attribute :str, Attribute
|
59
|
+
# attribute :int, Attribute
|
60
|
+
# attribute :wis, Attribute
|
61
|
+
# attribute :dex, Attribute
|
62
|
+
# attribute :con, Attribute
|
63
|
+
# end
|
64
|
+
# end
|
65
|
+
#
|
66
|
+
# ch = Character.new
|
67
|
+
# ch.str.base = 18
|
68
|
+
# ch.int.base = 8
|
69
|
+
# ch.wis.base = 3
|
70
|
+
# ch.dex.base = 13
|
71
|
+
# ch.con.base = 16
|
72
|
+
# ch.to_binstr # => "\x12\x00\x08\x00\x03\x00\x0d\x00\x10\x00"
|
73
|
+
# ch.str.mod -= 3
|
74
|
+
# ch.to_binstr # => "\x12\xFD\x08\x00\x03\x00\x0d\x00\x10\x00"
|
75
|
+
#
|
76
|
+
class Struct
|
77
|
+
extend Type
|
78
|
+
using PrettyPrintHelpers
|
79
|
+
|
80
|
+
def self.builder
|
81
|
+
Builder.new
|
82
|
+
end
|
83
|
+
|
84
|
+
# define the layout for this structure
|
85
|
+
# @see Builder
|
86
|
+
#
|
87
|
+
# @example type-length-value (TLV) struct
|
88
|
+
# class TLV < CTypes::Struct
|
89
|
+
# layout do
|
90
|
+
# attribute :type, uint16
|
91
|
+
# attribute :len, uint16
|
92
|
+
# attribute :value, string
|
93
|
+
# size { |s| offsetof(:len) + s.len }
|
94
|
+
# end
|
95
|
+
# end
|
96
|
+
def self.layout(&block)
|
97
|
+
raise Error, "no block given" unless block
|
98
|
+
builder = Builder.new
|
99
|
+
builder.instance_eval(&block)
|
100
|
+
apply_layout(builder)
|
101
|
+
end
|
102
|
+
|
103
|
+
def self.apply_layout(builder) # :nodoc:
|
104
|
+
# reset the state of the struct
|
105
|
+
@offsets = nil
|
106
|
+
@greedy = false
|
107
|
+
|
108
|
+
@name, @fields, @dry_type, @size, @endian = builder.result
|
109
|
+
|
110
|
+
@field_accessors ||= {}
|
111
|
+
remove_method(*@field_accessors.values.flatten)
|
112
|
+
@field_accessors.clear
|
113
|
+
|
114
|
+
@fields.each do |name, type|
|
115
|
+
# the struct will be flagged as greedy if size is not defined by a
|
116
|
+
# Proc, and the field type is greedy
|
117
|
+
@greedy ||= type.greedy? unless @size.is_a?(Proc)
|
118
|
+
|
119
|
+
case name
|
120
|
+
when Symbol
|
121
|
+
@field_accessors[name] = attr_accessor(name)
|
122
|
+
when ::Array
|
123
|
+
name.each { |n| @field_accessors[n] = attr_accessor(n) }
|
124
|
+
when Pad
|
125
|
+
# no op
|
126
|
+
else
|
127
|
+
raise Error, "unsupported field name type: %p", name
|
128
|
+
end
|
129
|
+
end
|
130
|
+
end
|
131
|
+
private_class_method :apply_layout
|
132
|
+
|
133
|
+
# encode a ruby Hash into a String containing the binary representation of
|
134
|
+
# the c type
|
135
|
+
#
|
136
|
+
# @param value [Hash] value to be encoded
|
137
|
+
# @param endian [Symbol] endian to pack with
|
138
|
+
# @param validate [Boolean] set to false to disable value validation
|
139
|
+
# @return [::String] binary encoding for value
|
140
|
+
#
|
141
|
+
# @example pack with default values
|
142
|
+
# include CTypes::Helpers
|
143
|
+
# t = struct(id: uint32, value: uint32)
|
144
|
+
# t.pack({}) # => "\0\0\0\0\0\0\0\0"
|
145
|
+
#
|
146
|
+
# @example pack with some fields
|
147
|
+
# include CTypes::Helpers
|
148
|
+
# t = struct(id: uint32, value: uint32)
|
149
|
+
# t.pack({value: 0xfefefefe}) # => "\x00\x00\x00\x00\xfe\xfe\xfe\xfe"
|
150
|
+
#
|
151
|
+
# @example pack with all fields
|
152
|
+
# include CTypes::Helpers
|
153
|
+
# t = struct(id: uint32, value: uint32)
|
154
|
+
# t.pack({id: 1, value: 2}) # => "\1\0\0\0\2\0\0\0"
|
155
|
+
#
|
156
|
+
# @example pack with nested struct
|
157
|
+
# include CTypes::Helpers
|
158
|
+
# t = struct do
|
159
|
+
# attribute :id, uint32
|
160
|
+
# attribute :a, struct(base: uint8, mod: uint8)
|
161
|
+
# end
|
162
|
+
# t.pack({id: 1, a: {base: 2, mod: 3}}) # => "\1\0\0\0\2\3"
|
163
|
+
#
|
164
|
+
def self.pack(value, endian: default_endian, validate: true)
|
165
|
+
value = value.to_hash.freeze
|
166
|
+
value = @dry_type[value] unless validate == false
|
167
|
+
buf = ::String.new
|
168
|
+
@fields.each do |(name, type)|
|
169
|
+
case name
|
170
|
+
when Pad
|
171
|
+
buf << type.pack(nil)
|
172
|
+
when Symbol
|
173
|
+
buf << type.pack(value[name],
|
174
|
+
endian: type.endian || endian,
|
175
|
+
validate: false)
|
176
|
+
when ::Array
|
177
|
+
buf << type.pack(value.slice(*name),
|
178
|
+
endian: type.endian || endian,
|
179
|
+
validate: false)
|
180
|
+
else
|
181
|
+
raise Error, "unsupported field name type: %p" % [name]
|
182
|
+
end
|
183
|
+
end
|
184
|
+
|
185
|
+
return buf if fixed_size? || @size.nil?
|
186
|
+
|
187
|
+
size = instance_exec(value, &@size)
|
188
|
+
if size > buf.size
|
189
|
+
buf << "\0" * (size - buf.size)
|
190
|
+
elsif size < buf.size
|
191
|
+
buf[0, size]
|
192
|
+
else
|
193
|
+
buf
|
194
|
+
end
|
195
|
+
end
|
196
|
+
|
197
|
+
# convert a String containing the binary represention of a c struct into
|
198
|
+
# a ruby type
|
199
|
+
#
|
200
|
+
# @param buf [String] bytes that make up the type
|
201
|
+
# @param endian [Symbol] endian of data within buf
|
202
|
+
# @return [::Array(Struct, ::String)] decoded struct, and remaining bytes
|
203
|
+
#
|
204
|
+
# @see Type#unpack
|
205
|
+
#
|
206
|
+
# @example
|
207
|
+
# class TLV < CTypes::Struct
|
208
|
+
# layout do
|
209
|
+
# attribute :type, enum(uint8, %i[hello, read, write, bye])
|
210
|
+
# attribute :len, uint16.with_endian(:big)
|
211
|
+
# attribute :value, string
|
212
|
+
# size { |struct| offsetof(:value) + struct[:len] }
|
213
|
+
# end
|
214
|
+
# end
|
215
|
+
# TLV.unpack_one("\0\0\5helloextra")
|
216
|
+
# # => [#<TLV type=:hello len=5 value="hello">, "extra"]
|
217
|
+
#
|
218
|
+
def self.unpack_one(buf, endian: default_endian)
|
219
|
+
rest = buf
|
220
|
+
trimmed = nil # set to the unused portion of buf when we have @size
|
221
|
+
out = _new # output structure instance
|
222
|
+
out.instance_variable_set(:@endian, endian)
|
223
|
+
|
224
|
+
@fields.each do |(name, type)|
|
225
|
+
# if the type is greedy, and we have a dynamic size, and we haven't
|
226
|
+
# already trimmed the input buffer, let's do so now.
|
227
|
+
#
|
228
|
+
# note: we do this while unpacking because the @size proc may require
|
229
|
+
# some of the unpacked fields to determine the size of the struct such
|
230
|
+
# as in TLV structs
|
231
|
+
if type.greedy? && @size && !trimmed
|
232
|
+
|
233
|
+
# caluclate the total size of the struct from the decoded fields
|
234
|
+
size = instance_exec(out, &@size)
|
235
|
+
raise missing_bytes_error(input: buf, need: size) if
|
236
|
+
size > buf.size
|
237
|
+
|
238
|
+
# adjust the size for how much we've already unpacked
|
239
|
+
size -= offsetof(name.is_a?(Array) ? name[0] : name)
|
240
|
+
|
241
|
+
# split the remaining buffer; we stick the part we aren't going to
|
242
|
+
# use in trimmed, and update rest to point at our buffer
|
243
|
+
trimmed = rest.byteslice(size..)
|
244
|
+
rest = rest.byteslice(0, size)
|
245
|
+
end
|
246
|
+
|
247
|
+
value, rest = type.unpack_one(rest, endian: type.endian || endian)
|
248
|
+
case name
|
249
|
+
when Symbol
|
250
|
+
out[name] = value
|
251
|
+
when ::Array
|
252
|
+
name.each { |n| out[n] = value[n] }
|
253
|
+
when Pad
|
254
|
+
# no op
|
255
|
+
else
|
256
|
+
raise Error, "unsupported field name type: %p" % [name]
|
257
|
+
end
|
258
|
+
end
|
259
|
+
|
260
|
+
[out, trimmed || rest]
|
261
|
+
end
|
262
|
+
|
263
|
+
# get the offset of a field within the structure in bytes
|
264
|
+
#
|
265
|
+
# @param attr [Symbol] name of the attribute
|
266
|
+
# @return [Integer] byte offset
|
267
|
+
def self.offsetof(attr)
|
268
|
+
@offsets ||= @fields.inject([0, {}]) do |(offset, o), (key, type)|
|
269
|
+
o[key] = offset
|
270
|
+
[type.size ? offset + type.size : nil, o]
|
271
|
+
end.last
|
272
|
+
|
273
|
+
@offsets[attr]
|
274
|
+
end
|
275
|
+
|
276
|
+
# check if this type is greedy
|
277
|
+
#
|
278
|
+
# @api private
|
279
|
+
def self.greedy?
|
280
|
+
@greedy
|
281
|
+
end
|
282
|
+
|
283
|
+
# get the minimum size of the structure
|
284
|
+
#
|
285
|
+
# For fixed-size structures, this will return the size of the structure.
|
286
|
+
# For dynamic length structures, this will return the minimum size of the
|
287
|
+
# structure
|
288
|
+
#
|
289
|
+
# @return [Integer] structure size in bytes
|
290
|
+
def self.size
|
291
|
+
return @size if @size.is_a?(Integer)
|
292
|
+
|
293
|
+
@min_size ||= @fields&.inject(0) { |s, (_, t)| s + t.size } || 0
|
294
|
+
end
|
295
|
+
|
296
|
+
# check if the struct has a given attribute
|
297
|
+
# @param k [Symbol] attribute name
|
298
|
+
def self.has_field?(k)
|
299
|
+
@field_accessors.has_key?(k)
|
300
|
+
end
|
301
|
+
|
302
|
+
# return the list of fields in this structure
|
303
|
+
# @api.private
|
304
|
+
def self.fields
|
305
|
+
@field_accessors.keys
|
306
|
+
end
|
307
|
+
|
308
|
+
# return the list of fields with their associated types
|
309
|
+
# @api.private
|
310
|
+
def self.field_layout
|
311
|
+
@fields
|
312
|
+
end
|
313
|
+
|
314
|
+
# return the struct name if supplied
|
315
|
+
# @api.private
|
316
|
+
def self.type_name
|
317
|
+
@name
|
318
|
+
end
|
319
|
+
|
320
|
+
def self.pretty_print(q) # :nodoc:
|
321
|
+
q.ctype("struct", @endian) do
|
322
|
+
q.line("name %p" % [@name]) if @name
|
323
|
+
q.seplist(@fields, -> { q.breakable(";") }) do |name, type|
|
324
|
+
case name
|
325
|
+
when Symbol
|
326
|
+
q.text("attribute %p, " % name)
|
327
|
+
q.pp(type)
|
328
|
+
when ::Array
|
329
|
+
q.text("attribute ")
|
330
|
+
q.pp(type)
|
331
|
+
when Pad
|
332
|
+
q.pp(type)
|
333
|
+
else
|
334
|
+
raise Error, "unsupported field name type: %p" % [name]
|
335
|
+
end
|
336
|
+
end
|
337
|
+
end
|
338
|
+
end
|
339
|
+
|
340
|
+
# @api.private
|
341
|
+
def self.export_type(q)
|
342
|
+
q << "CTypes::Struct.builder()"
|
343
|
+
q.break
|
344
|
+
q.nest(2) do
|
345
|
+
q << ".name(%p)\n" % [@name] if @name
|
346
|
+
q << ".endian(%p)\n" % [@endian] if @endian
|
347
|
+
@fields.each do |name, type|
|
348
|
+
case name
|
349
|
+
when Symbol
|
350
|
+
q << ".attribute(%p, " % [name]
|
351
|
+
q << type
|
352
|
+
q << ")"
|
353
|
+
q.break
|
354
|
+
when ::Array
|
355
|
+
q << ".attribute("
|
356
|
+
q << type
|
357
|
+
q << ")"
|
358
|
+
q.break
|
359
|
+
when Pad
|
360
|
+
q << type
|
361
|
+
q.break
|
362
|
+
else
|
363
|
+
raise Error, "unsupported field name type: %p" % [name]
|
364
|
+
end
|
365
|
+
end
|
366
|
+
q << ".build()"
|
367
|
+
end
|
368
|
+
end
|
369
|
+
|
370
|
+
class << self
|
371
|
+
alias_method :inspect, :pretty_inspect # :nodoc:
|
372
|
+
|
373
|
+
# @method _new
|
374
|
+
# allocate an uninitialized instance of the struct
|
375
|
+
# @return [Struct] uninitialized struct instance
|
376
|
+
# @api private
|
377
|
+
alias_method :_new, :new
|
378
|
+
private :_new
|
379
|
+
end
|
380
|
+
|
381
|
+
# allocate an instance of the Struct and initialize default values
|
382
|
+
# @param fields [Hash] values to set
|
383
|
+
# @return [Struct]
|
384
|
+
def self.new(fields = nil)
|
385
|
+
buf = fields.nil? ? ("\0" * size) : pack(fields)
|
386
|
+
unpack(buf)
|
387
|
+
end
|
388
|
+
|
389
|
+
# check if another Struct subclass has the same attributes as this Struct
|
390
|
+
# @note this method does not handle dynamic sized Structs correctly, but
|
391
|
+
# the current implementation is sufficient for testing
|
392
|
+
def self.==(other)
|
393
|
+
return true if super
|
394
|
+
return false unless other.is_a?(Class) && other < Struct
|
395
|
+
other.field_layout == @fields &&
|
396
|
+
other.default_endian == default_endian &&
|
397
|
+
other.size == size
|
398
|
+
end
|
399
|
+
|
400
|
+
# set an attribute value
|
401
|
+
# @param k [Symbol] attribute name
|
402
|
+
# @param v value
|
403
|
+
#
|
404
|
+
# @example
|
405
|
+
# include CTypes::Helpers
|
406
|
+
# t = struct(id: uint32, value: uint32)
|
407
|
+
# i = t.new
|
408
|
+
# i[:id] = 12
|
409
|
+
# i.id # => 12
|
410
|
+
# i.id = 55
|
411
|
+
# i.id # => 55
|
412
|
+
def []=(k, v)
|
413
|
+
has_attribute!(k)
|
414
|
+
instance_variable_set(:"@#{k}", v)
|
415
|
+
end
|
416
|
+
|
417
|
+
# get an attribute value
|
418
|
+
# @param k [Symbol] attribute name
|
419
|
+
# @return value
|
420
|
+
#
|
421
|
+
# @example
|
422
|
+
# include CTypes::Helpers
|
423
|
+
# t = struct(id: uint32, value: uint32)
|
424
|
+
# i = t.new
|
425
|
+
# i[:value] = 123
|
426
|
+
# i[:value] # => 123
|
427
|
+
def [](k)
|
428
|
+
has_attribute!(k)
|
429
|
+
instance_variable_get(:"@#{k}")
|
430
|
+
end
|
431
|
+
|
432
|
+
# check if the {Struct} has a specific attribute name
|
433
|
+
def has_key?(name)
|
434
|
+
self.class.has_field?(name)
|
435
|
+
end
|
436
|
+
|
437
|
+
# raise an exception unless {Struct} includes a specific attribute name
|
438
|
+
def has_attribute!(name)
|
439
|
+
raise UnknownAttributeError, "unknown attribute: %p" % name unless
|
440
|
+
self.class.has_field?(name)
|
441
|
+
end
|
442
|
+
private :has_attribute!
|
443
|
+
|
444
|
+
# return a Hash representation of the data type
|
445
|
+
# @param shallow [Boolean] set to true to disable deep traversal
|
446
|
+
# @return [Hash]
|
447
|
+
#
|
448
|
+
# @example deep
|
449
|
+
# include CTypes::Helpers
|
450
|
+
# t = struct do
|
451
|
+
# attribute :inner, struct(value: uint8)
|
452
|
+
# end
|
453
|
+
# i = t.new
|
454
|
+
# i.inner.value = 5
|
455
|
+
# i.to_h # => {inner: {value: 5}}
|
456
|
+
#
|
457
|
+
# @example shallow
|
458
|
+
# include CTypes::Helpers
|
459
|
+
# t = struct do
|
460
|
+
# attribute :inner, struct(value: uint8)
|
461
|
+
# end
|
462
|
+
# i = t.new
|
463
|
+
# i.inner.value = 5
|
464
|
+
# i.to_h(shallow: true) # => {inner: #<Class:0x646456 value=5>}
|
465
|
+
def to_h(shallow: false)
|
466
|
+
out = {}
|
467
|
+
self.class.fields.each do |field|
|
468
|
+
value = send(field)
|
469
|
+
unless shallow || value.is_a?(::Array) || !value.respond_to?(:to_h)
|
470
|
+
value = value.to_h
|
471
|
+
end
|
472
|
+
out[field] = value
|
473
|
+
end
|
474
|
+
out
|
475
|
+
end
|
476
|
+
alias_method :to_hash, :to_h
|
477
|
+
|
478
|
+
# return the binary representation of this Struct instance
|
479
|
+
# @return [String] binary representation of struct
|
480
|
+
#
|
481
|
+
# @example
|
482
|
+
# include CTypes::Helpers
|
483
|
+
# t = struct(id: uint32, value: string)
|
484
|
+
# i = t.new
|
485
|
+
# i.id = 1
|
486
|
+
# i.value = "hello"
|
487
|
+
# i.to_binstr # => "\1\0\0\0hello"
|
488
|
+
def to_binstr(endian: @endian)
|
489
|
+
self.class.pack(to_h, endian:)
|
490
|
+
end
|
491
|
+
|
492
|
+
# determine if this instance of the struct is equal to another instance
|
493
|
+
#
|
494
|
+
# @note this implementation also supports Hash equality through {to_h}
|
495
|
+
def ==(other)
|
496
|
+
case other
|
497
|
+
when self.class
|
498
|
+
self.class.field_layout.all? do |field, _|
|
499
|
+
instance_variable_get(:"@#{field}") == other[field]
|
500
|
+
end
|
501
|
+
when Hash
|
502
|
+
other == to_h
|
503
|
+
else
|
504
|
+
super
|
505
|
+
end
|
506
|
+
end
|
507
|
+
|
508
|
+
def pretty_print(q) # :nodoc:
|
509
|
+
open = if (name = self.class.type_name || self.class.name)
|
510
|
+
"struct #{name} {"
|
511
|
+
else
|
512
|
+
"struct {"
|
513
|
+
end
|
514
|
+
q.group(4, open, "}") do
|
515
|
+
q.seplist(self.class.field_layout, -> { q.breakable("") }) do |name, _|
|
516
|
+
names = name.is_a?(::Array) ? name : [name]
|
517
|
+
names.each do |name|
|
518
|
+
q.text(".#{name} = ")
|
519
|
+
q.pp(instance_variable_get(:"@#{name}"))
|
520
|
+
q.text(", ")
|
521
|
+
end
|
522
|
+
end
|
523
|
+
end
|
524
|
+
end
|
525
|
+
alias_method :inspect, :pretty_inspect # :nodoc:
|
526
|
+
end
|
527
|
+
end
|
528
|
+
|
529
|
+
require_relative "struct/builder"
|
@@ -0,0 +1,65 @@
|
|
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
|
+
# Wrap another CTypes::Type to provide terminated implementations of that
|
9
|
+
# type. Used by {CTypes::Array} and {CTypes::String} to truncate the buffer
|
10
|
+
# passed to the real type to terminate greedy types.
|
11
|
+
#
|
12
|
+
# During #unpack, this class will locate the terminator in the input buffer,
|
13
|
+
# then pass a truncated input to the underlying greedy type for unpacking.
|
14
|
+
#
|
15
|
+
# During #pack, this class will call the underlying greedy type #pack
|
16
|
+
# method, then append the terminator.
|
17
|
+
#
|
18
|
+
# @api private
|
19
|
+
class Terminated
|
20
|
+
include Type
|
21
|
+
|
22
|
+
def initialize(type:, locate:, terminate:)
|
23
|
+
@type = type
|
24
|
+
@locate = locate
|
25
|
+
@term = terminate
|
26
|
+
end
|
27
|
+
|
28
|
+
def dry_type
|
29
|
+
@type.dry_type
|
30
|
+
end
|
31
|
+
|
32
|
+
def greedy?
|
33
|
+
false
|
34
|
+
end
|
35
|
+
|
36
|
+
def size
|
37
|
+
@term_size ||= terminate(@type.default_value.dup,
|
38
|
+
endian: default_endian).size
|
39
|
+
end
|
40
|
+
|
41
|
+
def pack(value, endian: default_endian, validate: true)
|
42
|
+
buf = @type.pack(value, endian:, validate:)
|
43
|
+
terminate(buf, endian:)
|
44
|
+
end
|
45
|
+
|
46
|
+
def unpack_one(buf, endian: default_endian)
|
47
|
+
value_size, term_size = @locate.call(buf, endian:)
|
48
|
+
if value_size.nil?
|
49
|
+
raise TerminatorNotFoundError,
|
50
|
+
"terminator not found in: %p" % buf
|
51
|
+
end
|
52
|
+
value = @type.unpack(buf[0, value_size], endian:)
|
53
|
+
[value, buf.byteslice((value_size + term_size)..)]
|
54
|
+
end
|
55
|
+
|
56
|
+
def terminate(buf, endian:)
|
57
|
+
buf << case @term
|
58
|
+
when Proc
|
59
|
+
@term.call(buf, endian)
|
60
|
+
else
|
61
|
+
@term.to_s
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|