bin_struct 0.1.0 → 0.2.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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +29 -2
- data/README.md +8 -2
- data/lib/bin_struct/abstract_tlv.rb +75 -73
- data/lib/bin_struct/array.rb +35 -26
- data/lib/bin_struct/cstring.rb +64 -12
- data/lib/bin_struct/enum.rb +35 -25
- data/lib/bin_struct/int.rb +59 -77
- data/lib/bin_struct/int_string.rb +20 -13
- data/lib/bin_struct/length_from.rb +6 -6
- data/lib/bin_struct/oui.rb +13 -10
- data/lib/bin_struct/string.rb +26 -13
- data/lib/bin_struct/struct.rb +628 -0
- data/lib/bin_struct/{fieldable.rb → structable.rb} +15 -15
- data/lib/bin_struct/version.rb +2 -1
- data/lib/bin_struct.rb +2 -2
- metadata +4 -4
- data/lib/bin_struct/fields.rb +0 -612
@@ -0,0 +1,628 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# This file is part of BinStruct
|
4
|
+
# see https://github.com/lemontree55/bin_struct for more informations
|
5
|
+
# Copyright (C) 2016 Sylvain Daubert <sylvain.daubert@laposte.net>
|
6
|
+
# Copyright (C) 2024 LemonTree55 <lenontree@proton.me>
|
7
|
+
# This program is published under MIT license.
|
8
|
+
|
9
|
+
# rubocop:disable Metrics/ClassLength
|
10
|
+
|
11
|
+
module BinStruct
|
12
|
+
# @abstract Set of attributes
|
13
|
+
# This class is a base class to define headers or anything else with a binary
|
14
|
+
# format containing multiple attributes.
|
15
|
+
#
|
16
|
+
# == Basics
|
17
|
+
# A {Struct} subclass is generaly composed of multiple binary attributes. These attributes
|
18
|
+
# have each a given type. All {Structable} types are supported.
|
19
|
+
#
|
20
|
+
# To define a new subclass, it has to inherit from {Struct}. And some class
|
21
|
+
# methods have to be used to declare attributes:
|
22
|
+
# class MyBinaryStructure < BinStruct::Struct
|
23
|
+
# # define a first Int8 attribute, with default value: 1
|
24
|
+
# define_attr :attr1, BinStruct::Int8, default: 1
|
25
|
+
# #define a second attribute, of kind Int32
|
26
|
+
# define_attr :attr2, BinStruct::Int32
|
27
|
+
# end
|
28
|
+
#
|
29
|
+
# These defintions create 4 methods: +#attr1+, +#attr1=+, +#attr2+ and +#attr2=+.
|
30
|
+
# All these methods take and/or return Integers.
|
31
|
+
#
|
32
|
+
# Attributes may also be accessed through {#[]} ans {#[]=}. These methods give access
|
33
|
+
# to type object:
|
34
|
+
# mybs = MyBinaryStructure.new
|
35
|
+
# mybs.attr1 # => Integer
|
36
|
+
# mybs[:attr1] # => BinStruct::Int8
|
37
|
+
#
|
38
|
+
# {#initialize} accepts an option hash to populate attributes. Keys are attribute
|
39
|
+
# name symbols, and values are those expected by writer accessor.
|
40
|
+
#
|
41
|
+
# {#read} is able to populate object from a binary string.
|
42
|
+
#
|
43
|
+
# {#to_s} returns binary string from object.
|
44
|
+
#
|
45
|
+
# == Add attributes
|
46
|
+
# {.define_attr} adds an attribute to Struct subclass. A lot of attribute types may be
|
47
|
+
# defined: integer types, string types (to handle a stream of bytes). More
|
48
|
+
# complex attribute types may be defined using others Struct subclasses:
|
49
|
+
# # define a 16-bit little-endian integer attribute, named type
|
50
|
+
# define_attr :type, BinStruct::Int16le
|
51
|
+
# # define a string attribute
|
52
|
+
# define_attr :body, BinStruct::String
|
53
|
+
# # define a attribute using a complex type (Struct subclass)
|
54
|
+
# define_attr :oui, BinStruct::OUI
|
55
|
+
#
|
56
|
+
# This example creates six methods on our Struct subclass: +#type+, +#type=+,
|
57
|
+
# +#body+, +#body=+, +#mac_addr+ and +#mac_addr=+.
|
58
|
+
#
|
59
|
+
# {.define_attr} has many options (third optional Hash argument):
|
60
|
+
# * +:default+ gives default attribute value. It may be a simple value (an Integer
|
61
|
+
# for an Int attribute, for example) or a lambda,
|
62
|
+
# * +:builder+ to give a builder/constructor lambda to create attribute. The lambda
|
63
|
+
# takes 2 arguments: {Struct} subclass object owning attribute, and type class as passes
|
64
|
+
# as second argument to {.define_attr},
|
65
|
+
# * +:optional+ to define this attribute as optional. This option takes a lambda
|
66
|
+
# parameter used to say if this attribute is present or not. The lambda takes an argument
|
67
|
+
# ({Struct} subclass object owning attribute),
|
68
|
+
# For example:
|
69
|
+
# # 32-bit integer attribute defaulting to 1
|
70
|
+
# define_attr :type, BinStruct::Int32, default: 1
|
71
|
+
# # 16-bit integer attribute, created with a random value. Each instance of this
|
72
|
+
# # object will have a different value.
|
73
|
+
# define_attr :id, BinStruct::Int16, default: ->(obj) { rand(65535) }
|
74
|
+
# # a size attribute
|
75
|
+
# define_attr :body_size, BinStruct::Int16
|
76
|
+
# # String attribute which length is taken from body_size attribute
|
77
|
+
# define_attr :body, BinStruct::String, builder: ->(obj, type) { type.new(length_from: obj[:body_size]) }
|
78
|
+
# # 16-bit enumeration type. As :default not specified, default to first value of enum
|
79
|
+
# define_attr :type_class, BinStruct::Int16Enum, enum: { 'class1' => 1, 'class2' => 2}
|
80
|
+
# # optional attribute. Only present if another attribute has a certain value
|
81
|
+
# define_attr :opt1, BinStruct::Int16, optional: ->(h) { h.type == 42 }
|
82
|
+
#
|
83
|
+
# == Generating bit attributes
|
84
|
+
# {.define_bit_attr_on} creates bit attributes on a previously declared integer
|
85
|
+
# attribute. For example, +frag+ attribute in IP header:
|
86
|
+
# define_attr :frag, BinStruct::Int16, default: 0
|
87
|
+
# define_bit_attr_on :frag, :flag_rsv, :flag_df, :flag_mf, :fragment_offset, 13
|
88
|
+
#
|
89
|
+
# This example generates methods:
|
90
|
+
# * +#frag+ and +#frag=+ to access +frag+ attribute as a 16-bit integer,
|
91
|
+
# * +#flag_rsv?+, +#flag_rsv=+, +#flag_df?+, +#flag_df=+, +#flag_mf?+ and +#flag_mf=+
|
92
|
+
# to access Boolean RSV, MF and DF flags from +frag+ attribute,
|
93
|
+
# * +#fragment_offset+ and +#fragment_offset=+ to access 13-bit integer fragment
|
94
|
+
# offset subattribute from +frag+ attribute.
|
95
|
+
#
|
96
|
+
# == Creating a new Struct class from another one
|
97
|
+
# Some methods may help in this case:
|
98
|
+
# * {.define_attr_before} to define a new attribute before an existing one,
|
99
|
+
# * {.define_attr_after} to define a new attribute after an existing onr,
|
100
|
+
# * {.remove_attribute} to remove an existing attribute,
|
101
|
+
# * {.uptade_fied} to change options of an attribute (but not its type),
|
102
|
+
# * {.remove_bit_attrs_on} to remove bit attribute definition.
|
103
|
+
#
|
104
|
+
# @author Sylvain Daubert (2016-2024)
|
105
|
+
# @author LemonTree55
|
106
|
+
class Struct
|
107
|
+
# @private
|
108
|
+
StructDef = ::Struct.new(:type, :default, :builder, :optional, :options)
|
109
|
+
# @private attribute names, ordered as they were declared
|
110
|
+
@ordered_attrs = []
|
111
|
+
# @private attribute definitions
|
112
|
+
@attr_defs = {}
|
113
|
+
# @private bit attribute definitions
|
114
|
+
@bit_attrs = {}
|
115
|
+
|
116
|
+
# Format to inspect attribute
|
117
|
+
FMT_ATTR = "%14s %16s: %s\n"
|
118
|
+
|
119
|
+
class << self
|
120
|
+
# Get attribute definitions for this class.
|
121
|
+
# @return [Hash]
|
122
|
+
attr_reader :attr_defs
|
123
|
+
# Get bit attribute defintions for this class
|
124
|
+
# @return [Hash]
|
125
|
+
attr_reader :bit_attrs
|
126
|
+
|
127
|
+
# On inheritage, create +@attr_defs+ class variable
|
128
|
+
# @param [Class] klass
|
129
|
+
# @return [void]
|
130
|
+
def inherited(klass)
|
131
|
+
super
|
132
|
+
|
133
|
+
attr_defs = {}
|
134
|
+
@attr_defs.each do |k, v|
|
135
|
+
attr_defs[k] = v.clone
|
136
|
+
end
|
137
|
+
ordered = @ordered_attrs.clone
|
138
|
+
bf = bit_attrs.clone
|
139
|
+
|
140
|
+
klass.class_eval do
|
141
|
+
@ordered_attrs = ordered
|
142
|
+
@attr_defs = attr_defs
|
143
|
+
@bit_attrs = bf
|
144
|
+
end
|
145
|
+
end
|
146
|
+
|
147
|
+
# Get attribute names
|
148
|
+
# @return [Array<Symbol>]
|
149
|
+
def attributes
|
150
|
+
@ordered_attrs
|
151
|
+
end
|
152
|
+
|
153
|
+
# Define an attribute in class
|
154
|
+
# class BinaryStruct < BinStruct::Struct
|
155
|
+
# # 8-bit value
|
156
|
+
# define_attr :value1, BinStruct::Int8
|
157
|
+
# # 16-bit value
|
158
|
+
# define_attr :value2, BinStruct::Int16
|
159
|
+
# # specific class, may use a specific constructor
|
160
|
+
# define_attr :value3, MyClass, builder: ->(obj, type) { type.new(obj) }
|
161
|
+
# end
|
162
|
+
#
|
163
|
+
# bs = BinaryStruct.new
|
164
|
+
# bs[value1] # => BinStruct::Int8
|
165
|
+
# bs.value1 # => Integer
|
166
|
+
# @param [Symbol] name attribute name
|
167
|
+
# @param [Structable] type class or instance
|
168
|
+
# @param [Hash] options Unrecognized options are passed to object builder if
|
169
|
+
# +:builder+ option is not set.
|
170
|
+
# @option options [Object] :default default value. May be a proc. This lambda
|
171
|
+
# take one argument: the caller object.
|
172
|
+
# @option options [Lambda] :builder lambda to construct this attribute.
|
173
|
+
# Parameters to this lambda is the caller object and the attribute type class.
|
174
|
+
# @option options [Lambda] :optional define this attribute as optional. Given lambda
|
175
|
+
# is used to known if this attribute is present or not. Parameter to this lambda is
|
176
|
+
# the being defined Struct object.
|
177
|
+
# @return [void]
|
178
|
+
def define_attr(name, type, options = {})
|
179
|
+
attributes << name
|
180
|
+
attr_defs[name] = StructDef.new(type,
|
181
|
+
options.delete(:default),
|
182
|
+
options.delete(:builder),
|
183
|
+
options.delete(:optional),
|
184
|
+
options)
|
185
|
+
|
186
|
+
add_methods(name, type)
|
187
|
+
end
|
188
|
+
|
189
|
+
# Define a attribute, before another one
|
190
|
+
# @param [Symbol,nil] other attribute name to create a new one before. If +nil+,
|
191
|
+
# new attribute is appended.
|
192
|
+
# @param [Symbol] name attribute name to create
|
193
|
+
# @param [Structable] type class or instance
|
194
|
+
# @param [Hash] options See {.define_attr}.
|
195
|
+
# @return [void]
|
196
|
+
# @see .define_attr
|
197
|
+
def define_attr_before(other, name, type, options = {})
|
198
|
+
define_attr name, type, options
|
199
|
+
return if other.nil?
|
200
|
+
|
201
|
+
attributes.delete name
|
202
|
+
idx = attributes.index(other)
|
203
|
+
raise ArgumentError, "unknown #{other} attribute" if idx.nil?
|
204
|
+
|
205
|
+
attributes[idx, 0] = name
|
206
|
+
end
|
207
|
+
|
208
|
+
# Define an attribute, after another one
|
209
|
+
# @param [Symbol,nil] other attribute name to create a new one after. If +nil+,
|
210
|
+
# new attribute is appended.
|
211
|
+
# @param [Symbol] name attribute name to create
|
212
|
+
# @param [Structable] type class or instance
|
213
|
+
# @param [Hash] options See {.define_attr}.
|
214
|
+
# @return [void]
|
215
|
+
# @see .define_attr
|
216
|
+
def define_attr_after(other, name, type, options = {})
|
217
|
+
define_attr name, type, options
|
218
|
+
return if other.nil?
|
219
|
+
|
220
|
+
attributes.delete name
|
221
|
+
idx = attributes.index(other)
|
222
|
+
raise ArgumentError, "unknown #{other} attribute" if idx.nil?
|
223
|
+
|
224
|
+
attributes[idx + 1, 0] = name
|
225
|
+
end
|
226
|
+
|
227
|
+
# Remove a previously defined attribute
|
228
|
+
# @param [Symbol] name
|
229
|
+
# @return [void]
|
230
|
+
def remove_attr(name)
|
231
|
+
attributes.delete(name)
|
232
|
+
@attr_defs.delete(name)
|
233
|
+
undef_method name if method_defined?(name)
|
234
|
+
undef_method :"#{name}=" if method_defined?(:"#{name}=")
|
235
|
+
end
|
236
|
+
|
237
|
+
# Update a previously defined attribute
|
238
|
+
# @param [Symbol] name attribute name to create
|
239
|
+
# @param [Hash] options See {.define_attr}.
|
240
|
+
# @return [void]
|
241
|
+
# @see .define_attr
|
242
|
+
# @raise [ArgumentError] unknown attribute
|
243
|
+
def update_attr(name, options)
|
244
|
+
check_existence_of(name)
|
245
|
+
|
246
|
+
%i[default builder optional].each do |property|
|
247
|
+
attr_defs_property_from(name, property, options)
|
248
|
+
end
|
249
|
+
|
250
|
+
attr_defs[name].options.merge!(options)
|
251
|
+
end
|
252
|
+
|
253
|
+
# Define a bit attribute on given attribute
|
254
|
+
# class MyHeader < BinStruct::Struct
|
255
|
+
# define_attr :flags, BinStruct::Int16
|
256
|
+
# # define a bit attribute on :flag attribute
|
257
|
+
# # flag1, flag2 and flag3 are 1-bit attributes
|
258
|
+
# # type and stype are 3-bit attributes. reserved is a 6-bit attribute
|
259
|
+
# define_bit_attributes_on :flags, :flag1, :flag2, :flag3, :type, 3, :stype, 3, :reserved, 7
|
260
|
+
# end
|
261
|
+
# A bit attribute of size 1 bit defines 2 methods:
|
262
|
+
# * +#attr+ which returns a Boolean,
|
263
|
+
# * +#attr=+ which takes and returns a Boolean.
|
264
|
+
# A bit attribute of more bits defines 2 methods:
|
265
|
+
# * +#attr+ which returns an Integer,
|
266
|
+
# * +#attr=+ which takes and returns an Integer.
|
267
|
+
# @param [Symbol] attr attribute name (attribute should be a {BinStruct::Int}
|
268
|
+
# subclass)
|
269
|
+
# @param [Array] args list of bit attribute names. Name may be followed
|
270
|
+
# by bit size. If no size is given, 1 bit is assumed.
|
271
|
+
# @raise [ArgumentError] unknown +attr+
|
272
|
+
# @return [void]
|
273
|
+
def define_bit_attrs_on(attr, *args)
|
274
|
+
check_existence_of(attr)
|
275
|
+
|
276
|
+
type = attr_defs[attr].type
|
277
|
+
raise TypeError, "#{attr} is not a BinStruct::Int" unless type < Int
|
278
|
+
|
279
|
+
total_size = type.new.nbits
|
280
|
+
idx = total_size - 1
|
281
|
+
|
282
|
+
until args.empty?
|
283
|
+
arg = args.shift
|
284
|
+
next unless arg.is_a? Symbol
|
285
|
+
|
286
|
+
size = size_from(args)
|
287
|
+
|
288
|
+
unless attr == :_
|
289
|
+
add_bit_methods(attr, arg, size, total_size, idx)
|
290
|
+
register_bit_attr_size(attr, arg, size)
|
291
|
+
end
|
292
|
+
|
293
|
+
idx -= size
|
294
|
+
end
|
295
|
+
end
|
296
|
+
|
297
|
+
# Remove all bit attributes defined on +attr+
|
298
|
+
# @param [Symbol] attr attribute defining bit attributes
|
299
|
+
# @return [void]
|
300
|
+
def remove_bit_attrs_on(attr)
|
301
|
+
bits = bit_attrs.delete(attr)
|
302
|
+
return if bits.nil?
|
303
|
+
|
304
|
+
bits.each do |bit, size|
|
305
|
+
undef_method :"#{bit}="
|
306
|
+
undef_method(size == 1 ? "#{bit}?" : bit)
|
307
|
+
end
|
308
|
+
end
|
309
|
+
|
310
|
+
private
|
311
|
+
|
312
|
+
def add_methods(name, type)
|
313
|
+
define = []
|
314
|
+
if type < Enum
|
315
|
+
define << "def #{name}; self[:#{name}].to_i; end"
|
316
|
+
define << "def #{name}=(val) self[:#{name}].value = val; end"
|
317
|
+
else
|
318
|
+
define << "def #{name}\n " \
|
319
|
+
"to_and_from_human?(:#{name}) ? self[:#{name}].to_human : self[:#{name}]\n" \
|
320
|
+
'end'
|
321
|
+
define << "def #{name}=(val)\n " \
|
322
|
+
"to_and_from_human?(:#{name}) ? self[:#{name}].from_human(val) : self[:#{name}].read(val)\n" \
|
323
|
+
'end'
|
324
|
+
end
|
325
|
+
|
326
|
+
define.delete_at(1) if instance_methods.include?(:"#{name}=")
|
327
|
+
define.delete_at(0) if instance_methods.include?(name)
|
328
|
+
class_eval define.join("\n")
|
329
|
+
end
|
330
|
+
|
331
|
+
def add_bit_methods(attr, name, size, total_size, idx)
|
332
|
+
shift = idx - (size - 1)
|
333
|
+
|
334
|
+
if size == 1
|
335
|
+
add_single_bit_methods(attr, name, size, total_size, shift)
|
336
|
+
else
|
337
|
+
add_multibit_methods(attr, name, size, total_size, shift)
|
338
|
+
end
|
339
|
+
end
|
340
|
+
|
341
|
+
def compute_mask(size, shift)
|
342
|
+
((2**size) - 1) << shift
|
343
|
+
end
|
344
|
+
|
345
|
+
def compute_clear_mask(total_size, mask)
|
346
|
+
((2**total_size) - 1) & (~mask & ((2**total_size) - 1))
|
347
|
+
end
|
348
|
+
|
349
|
+
def add_single_bit_methods(attr, name, size, total_size, shift)
|
350
|
+
mask = compute_mask(size, shift)
|
351
|
+
clear_mask = compute_clear_mask(total_size, mask)
|
352
|
+
|
353
|
+
class_eval <<-METHODS, __FILE__, __LINE__ + 1
|
354
|
+
def #{name}? # def bit?
|
355
|
+
val = (self[:#{attr}].to_i & #{mask}) >> #{shift} # val = (self[:attr}].to_i & 1}) >> 1
|
356
|
+
val != 0 # val != 0
|
357
|
+
end # end
|
358
|
+
def #{name}=(v) # def bit=(v)
|
359
|
+
val = v ? 1 : 0 # val = v ? 1 : 0
|
360
|
+
self[:#{attr}].value = self[:#{attr}].to_i & #{clear_mask} # self[:attr].value = self[:attr].to_i & 0xfffd
|
361
|
+
self[:#{attr}].value |= val << #{shift} # self[:attr].value |= val << 1
|
362
|
+
end # end
|
363
|
+
METHODS
|
364
|
+
end
|
365
|
+
|
366
|
+
def add_multibit_methods(attr, name, size, total_size, shift)
|
367
|
+
mask = compute_mask(size, shift)
|
368
|
+
clear_mask = compute_clear_mask(total_size, mask)
|
369
|
+
|
370
|
+
class_eval <<-METHODS, __FILE__, __LINE__ + 1
|
371
|
+
def #{name} # def multibit
|
372
|
+
(self[:#{attr}].to_i & #{mask}) >> #{shift} # (self[:attr].to_i & 6) >> 1
|
373
|
+
end # end
|
374
|
+
def #{name}=(v) # def multibit=(v)
|
375
|
+
self[:#{attr}].value = self[:#{attr}].to_i & #{clear_mask} # self[:attr].value = self[:attr].to_i & 0xfff9
|
376
|
+
self[:#{attr}].value |= (v & #{(2**size) - 1}) << #{shift} # self[:attr].value |= (v & 3) << 1
|
377
|
+
end # end
|
378
|
+
METHODS
|
379
|
+
end
|
380
|
+
|
381
|
+
def register_bit_attr_size(attr, name, size)
|
382
|
+
bit_attrs[attr] = {} if bit_attrs[attr].nil?
|
383
|
+
bit_attrs[attr][name] = size
|
384
|
+
end
|
385
|
+
|
386
|
+
def attr_defs_property_from(attr, property, options)
|
387
|
+
attr_defs[attr].send(:"#{property}=", options.delete(property)) if options.key?(property)
|
388
|
+
end
|
389
|
+
|
390
|
+
def size_from(args)
|
391
|
+
if args.first.is_a? Integer
|
392
|
+
args.shift
|
393
|
+
else
|
394
|
+
1
|
395
|
+
end
|
396
|
+
end
|
397
|
+
|
398
|
+
def check_existence_of(attr)
|
399
|
+
raise ArgumentError, "unknown #{attr} attribute for #{self}" unless attr_defs.key?(attr)
|
400
|
+
end
|
401
|
+
end
|
402
|
+
|
403
|
+
# Create a new Struct object
|
404
|
+
# @param [Hash] options Keys are symbols. They should have name of object
|
405
|
+
# attributes, as defined by {.define_attr} and by {.define_bit_attrs_on}.
|
406
|
+
def initialize(options = {})
|
407
|
+
@attributes = {}
|
408
|
+
@optional_attributes = {}
|
409
|
+
|
410
|
+
self.class.attributes.each do |attr|
|
411
|
+
build_attribute(attr)
|
412
|
+
initialize_value(attr, options[attr])
|
413
|
+
initialize_optional(attr)
|
414
|
+
end
|
415
|
+
|
416
|
+
self.class.bit_attrs.each_value do |hsh|
|
417
|
+
hsh.each_key do |bit|
|
418
|
+
send(:"#{bit}=", options[bit]) if options[bit]
|
419
|
+
end
|
420
|
+
end
|
421
|
+
end
|
422
|
+
|
423
|
+
# Get attribute object
|
424
|
+
# @param [Symbol] attr attribute name
|
425
|
+
# @return [Structable]
|
426
|
+
def [](attr)
|
427
|
+
@attributes[attr]
|
428
|
+
end
|
429
|
+
|
430
|
+
# Set attribute object
|
431
|
+
# @param [Symbol] attr attribute name
|
432
|
+
# @param [Object] obj
|
433
|
+
# @return [Object]
|
434
|
+
def []=(attr, obj)
|
435
|
+
@attributes[attr] = obj
|
436
|
+
end
|
437
|
+
|
438
|
+
# Get all attribute names
|
439
|
+
# @return [Array<Symbol>]
|
440
|
+
def attributes
|
441
|
+
self.class.attributes
|
442
|
+
end
|
443
|
+
|
444
|
+
# Get all optional attribute names
|
445
|
+
# @return[Array<Symbol>,nil]
|
446
|
+
def optional_attributes
|
447
|
+
@optional_attributes.keys
|
448
|
+
end
|
449
|
+
|
450
|
+
# Say if this attribue is optional
|
451
|
+
# @param [Symbol] attr attribute name
|
452
|
+
# @return [Boolean]
|
453
|
+
def optional?(attr)
|
454
|
+
@optional_attributes.key?(attr)
|
455
|
+
end
|
456
|
+
|
457
|
+
# Say if an optional attribute is present
|
458
|
+
# @return [Boolean]
|
459
|
+
def present?(attr)
|
460
|
+
return true unless optional?(attr)
|
461
|
+
|
462
|
+
@optional_attributes[attr].call(self)
|
463
|
+
end
|
464
|
+
|
465
|
+
# Populate object from a binary string
|
466
|
+
# @param [String] str
|
467
|
+
# @return [Struct] self
|
468
|
+
def read(str)
|
469
|
+
return self if str.nil?
|
470
|
+
|
471
|
+
force_binary(str)
|
472
|
+
start = 0
|
473
|
+
attributes.each do |attr|
|
474
|
+
next unless present?(attr)
|
475
|
+
|
476
|
+
obj = self[attr].read(str[start..])
|
477
|
+
start += self[attr].sz
|
478
|
+
self[attr] = obj unless obj == self[attr]
|
479
|
+
end
|
480
|
+
|
481
|
+
self
|
482
|
+
end
|
483
|
+
|
484
|
+
# Common inspect method for structs.
|
485
|
+
#
|
486
|
+
# A block may be given to differently format some attributes. This
|
487
|
+
# may be used by subclasses to handle specific attributes.
|
488
|
+
# @yieldparam attr [Symbol] attribute to inspect
|
489
|
+
# @yieldreturn [String,nil] the string to print for +attr+, or +nil+
|
490
|
+
# to let +inspect+ generate it
|
491
|
+
# @return [String]
|
492
|
+
def inspect
|
493
|
+
str = inspect_titleize
|
494
|
+
attributes.each do |attr|
|
495
|
+
next if attr == :body
|
496
|
+
next unless present?(attr)
|
497
|
+
|
498
|
+
result = yield(attr) if block_given?
|
499
|
+
str << (result || inspect_attribute(attr, self[attr], 1))
|
500
|
+
end
|
501
|
+
str
|
502
|
+
end
|
503
|
+
|
504
|
+
# Return object as a binary string
|
505
|
+
# @return [String]
|
506
|
+
def to_s
|
507
|
+
attributes.select { |attr| present?(attr) }
|
508
|
+
.map! { |attr| force_binary @attributes[attr].to_s }.join
|
509
|
+
end
|
510
|
+
|
511
|
+
# Size of object as binary string
|
512
|
+
# @return [nteger]
|
513
|
+
def sz
|
514
|
+
to_s.size
|
515
|
+
end
|
516
|
+
|
517
|
+
# Return object as a hash
|
518
|
+
# @return [Hash] keys: attributes, values: attribute values
|
519
|
+
def to_h
|
520
|
+
attributes.to_h { |attr| [attr, @attributes[attr].to_human] }
|
521
|
+
end
|
522
|
+
|
523
|
+
# Get offset of given attribute in {Struct}.
|
524
|
+
# @param [Symbol] attr attribute name
|
525
|
+
# @return [Integer]
|
526
|
+
# @raise [ArgumentError] unknown attribute
|
527
|
+
def offset_of(attr)
|
528
|
+
raise ArgumentError, "#{attr} is an unknown attribute of #{self.class}" unless @attributes.include?(attr)
|
529
|
+
|
530
|
+
offset = 0
|
531
|
+
attributes.each do |a|
|
532
|
+
break offset if a == attr
|
533
|
+
next unless present?(a)
|
534
|
+
|
535
|
+
offset += self[a].sz
|
536
|
+
end
|
537
|
+
end
|
538
|
+
|
539
|
+
# Get bit attributes definition for given attribute.
|
540
|
+
# @param [Symbol] attr attribute defining bit attributes
|
541
|
+
# @return [Hash,nil] keys: bit attributes, values: their size in bits
|
542
|
+
def bits_on(attr)
|
543
|
+
self.class.bit_attrs[attr]
|
544
|
+
end
|
545
|
+
|
546
|
+
private
|
547
|
+
|
548
|
+
# Deeply duplicate +@attributes+
|
549
|
+
# @return [void]
|
550
|
+
def initialize_copy(_other)
|
551
|
+
attributes = {}
|
552
|
+
@attributes.each { |k, v| attributes[k] = v.dup }
|
553
|
+
@attributes = attributes
|
554
|
+
end
|
555
|
+
|
556
|
+
# Force str to binary encoding
|
557
|
+
# @param [String] str
|
558
|
+
# @return [String]
|
559
|
+
def force_binary(str)
|
560
|
+
BinStruct.force_binary(str)
|
561
|
+
end
|
562
|
+
|
563
|
+
# @param [Symbol] attr attribute name
|
564
|
+
# @return [Boolean] +true= if #from_human and #to_human are both defined for given attribute
|
565
|
+
def to_and_from_human?(attr)
|
566
|
+
self[attr].respond_to?(:to_human) && self[attr].respond_to?(:from_human)
|
567
|
+
end
|
568
|
+
|
569
|
+
def attr_defs
|
570
|
+
self.class.attr_defs
|
571
|
+
end
|
572
|
+
|
573
|
+
# rubocop:disable Metrics/AbcSize
|
574
|
+
def build_attribute(attr)
|
575
|
+
type = attr_defs[attr].type
|
576
|
+
|
577
|
+
@attributes[attr] = if attr_defs[attr].builder
|
578
|
+
attr_defs[attr].builder.call(self, type)
|
579
|
+
elsif !attr_defs[attr].options.empty?
|
580
|
+
type.new(attr_defs[attr].options)
|
581
|
+
else
|
582
|
+
type.new
|
583
|
+
end
|
584
|
+
end
|
585
|
+
# rubocop:enable Metrics/AbcSize
|
586
|
+
|
587
|
+
def initialize_value(attr, val)
|
588
|
+
type = attr_defs[attr].type
|
589
|
+
default = attr_defs[attr].default
|
590
|
+
default = default.to_proc.call(self) if default.is_a?(Proc)
|
591
|
+
|
592
|
+
value = val || default
|
593
|
+
if value.class <= type
|
594
|
+
@attributes[attr] = value
|
595
|
+
elsif @attributes[attr].respond_to?(:from_human)
|
596
|
+
@attributes[attr].from_human(value)
|
597
|
+
else
|
598
|
+
@attributes[attr].read(value)
|
599
|
+
end
|
600
|
+
end
|
601
|
+
|
602
|
+
def initialize_optional(attr)
|
603
|
+
optional = attr_defs[attr].optional
|
604
|
+
@optional_attributes[attr] = optional if optional
|
605
|
+
end
|
606
|
+
|
607
|
+
def inspect_titleize
|
608
|
+
title = self.class.to_s
|
609
|
+
+"-- #{title} #{'-' * (66 - title.length)}\n"
|
610
|
+
end
|
611
|
+
|
612
|
+
def inspect_attribute(attr, value, level = 1)
|
613
|
+
type = value.class.to_s.sub(/.*::/, '')
|
614
|
+
inspect_format(type, attr, value.format_inspect, level)
|
615
|
+
end
|
616
|
+
|
617
|
+
def inspect_format(type, attr, value, level = 1)
|
618
|
+
str = inspect_shift_level(level)
|
619
|
+
str << (FMT_ATTR % [type, attr, value])
|
620
|
+
end
|
621
|
+
|
622
|
+
def inspect_shift_level(level = 1)
|
623
|
+
' ' * (level + 1)
|
624
|
+
end
|
625
|
+
end
|
626
|
+
end
|
627
|
+
|
628
|
+
# rubocop:enable Metrics/ClassLength
|
@@ -7,17 +7,17 @@
|
|
7
7
|
# This program is published under MIT license.
|
8
8
|
|
9
9
|
module BinStruct
|
10
|
-
# Mixin to define minimal API for a class to be embbeded as
|
11
|
-
# {
|
10
|
+
# Mixin to define minimal API for a class to be embbeded as an attribute in
|
11
|
+
# {Struct} object.
|
12
12
|
#
|
13
13
|
# == Optional methods
|
14
|
-
#
|
15
|
-
# * +from_human+ to load data from a human-readable
|
16
|
-
# @author Sylvain Daubert
|
17
|
-
# @
|
18
|
-
module
|
14
|
+
# This method may, optionally, be defined by structable types:
|
15
|
+
# * +from_human+ to load data from a human-readable ruby object (String, Integer,...).
|
16
|
+
# @author Sylvain Daubert (2016-2024)
|
17
|
+
# @author LemonTree55
|
18
|
+
module Structable
|
19
19
|
# Get type name
|
20
|
-
# @return [String]
|
20
|
+
# @return [::String]
|
21
21
|
def type_name
|
22
22
|
self.class.to_s.split('::').last
|
23
23
|
end
|
@@ -26,15 +26,15 @@ module BinStruct
|
|
26
26
|
# These methods are defined for documentation.
|
27
27
|
|
28
28
|
# Populate object from a binary string
|
29
|
-
# @param [String] str
|
30
|
-
# @return [
|
29
|
+
# @param [::String] str
|
30
|
+
# @return [self]
|
31
31
|
# @abstract subclass should overload it.
|
32
32
|
def read(str)
|
33
33
|
super
|
34
34
|
end
|
35
35
|
|
36
36
|
# Return object as a binary string
|
37
|
-
# @return [String]
|
37
|
+
# @return [::String]
|
38
38
|
# @abstract subclass should overload it.
|
39
39
|
def to_s
|
40
40
|
super
|
@@ -46,8 +46,8 @@ module BinStruct
|
|
46
46
|
to_s.size
|
47
47
|
end
|
48
48
|
|
49
|
-
# Return a human-readbale
|
50
|
-
# @return [String]
|
49
|
+
# Return a human-readbale object
|
50
|
+
# @return [::String,Integer]
|
51
51
|
# @abstract subclass should overload it.
|
52
52
|
def to_human
|
53
53
|
super
|
@@ -55,8 +55,8 @@ module BinStruct
|
|
55
55
|
|
56
56
|
# rubocop:enable Lint/UselessMethodDefinition
|
57
57
|
|
58
|
-
# Format object when inspecting a {
|
59
|
-
# @return [String]
|
58
|
+
# Format object when inspecting a {Struct} object
|
59
|
+
# @return [::String]
|
60
60
|
def format_inspect
|
61
61
|
to_human
|
62
62
|
end
|