bin_struct 0.1.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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: d5412a49a789a410de1120e6cbcbf55a7bd07783846a4e2f4cf9bc14eac76bfd
4
+ data.tar.gz: 9ae9eabeb7c3fe672e8ba229bc116c9a72bf26c7b1fd9eace52bb0f8036f0fea
5
+ SHA512:
6
+ metadata.gz: 8fa11b03a84dca208c63d23358187b575982f5331d5c1da7fdd7c2343505ed932bc884a440b23589943d4eb5d9f78f942d77e8b271bef919277536a0ab9ab3b7
7
+ data.tar.gz: ecd4e847f6dc8392b759b8ef7a23df05191c697bb471a1ef8c685e12009be544b2dac4f8d77a944190a607b26216d90bd034742c95f9b2034b2701c4ec726602
data/CHANGELOG.md ADDED
@@ -0,0 +1,5 @@
1
+ ## [Unreleased]
2
+
3
+ ## [0.1.0] - 2024-07-13
4
+
5
+ - Initial release
data/README.md ADDED
@@ -0,0 +1,18 @@
1
+ # BinStruct
2
+
3
+ BinStruct provides a simple ways to create and dissect binary data. It is an extraction from [PacketGen](https://github.com/lemontree55/packetgen) fields.
4
+
5
+ ## Installation
6
+
7
+ Installation using RubyGems is easy:
8
+
9
+ $ gem install bin_struct
10
+
11
+ Or add it to a Gemfile:
12
+ ```ruby
13
+ gem 'bin_struct'
14
+ ```
15
+
16
+ ## License
17
+
18
+ The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
@@ -0,0 +1,270 @@
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) 2024 LemonTree55 <lenontree@proton.me>
6
+ # This program is published under MIT license.
7
+
8
+ module BinStruct
9
+ # This class is an abstract class to define type-length-value data.
10
+ #
11
+ # This class supersedes {TLV} class, which is not well defined on some corner
12
+ # cases.
13
+ #
14
+ # ===Usage
15
+ # To simply define a new TLV class, do:
16
+ # MyTLV = PacketGen::Types::AbstractTLV.create
17
+ # MyTLV.define_type_enum 'one' => 1, 'two' => 2
18
+ # This will define a new +MyTLV+ class, subclass of {Fields}. This class will
19
+ # define 3 fields:
20
+ # * +#type+, as a {Int8Enum} by default,
21
+ # * +#length+, as a {Int8} by default,
22
+ # * and +#value+, as a {String} by default.
23
+ # +.define_type_enum+ is, here, necessary to define enum hash to be used
24
+ # for +#type+ accessor, as this one is defined as an {Enum}.
25
+ #
26
+ # This class may then be used as older {TLV} class:
27
+ # tlv = MyTLV.new(type: 1, value: 'abcd') # automagically set #length from value
28
+ # tlv.type #=> 1
29
+ # tlv.human_type #=> 'one'
30
+ # tlv.length #=> 4
31
+ # tlv.value #=> "abcd"
32
+ #
33
+ # ===Advanced usage
34
+ # Each field's type may be changed at generating TLV class:
35
+ # MyTLV = PacketGen::Types::AbstractTLV.create(type_class: PacketGen::Types::Int16,
36
+ # length_class: PacketGen::Types::Int16,
37
+ # value_class: PacketGen::Header::IP::Addr)
38
+ # tlv = MyTLV.new(type: 1, value: '1.2.3.4')
39
+ # tlv.type #=> 1
40
+ # tlv.length #=> 4
41
+ # tlv.value #=> '1.2.3.4'
42
+ # tlv.to_s #=> "\x00\x01\x00\x04\x01\x02\x03\x04"
43
+ #
44
+ # Some aliases may also be defined. For example, to create a TLV type
45
+ # whose +type+ field should be named +code+:
46
+ # MyTLV = PacketGen::Types::AbstractTLV.create(type_class: PacketGen::Types::Int16,
47
+ # length_class: PacketGen::Types::Int16,
48
+ # aliases: { code: :type })
49
+ # tlv = MyTLV.new(code: 1, value: 'abcd')
50
+ # tlv.code #=> 1
51
+ # tlv.type #=> 1
52
+ # tlv.length #=> 4
53
+ # tlv.value #=> 'abcd'
54
+ #
55
+ # @author Sylvain Daubert
56
+ # @since 3.1.0
57
+ # @since 3.1.1 add +:aliases+ keyword to {#initialize}
58
+ class AbstractTLV < Fields
59
+ include Fieldable
60
+
61
+ # @private
62
+ FIELD_TYPES = { 'T' => :type, 'L' => :length, 'V' => :value }.freeze
63
+
64
+ class << self
65
+ # @return [Hash]
66
+ attr_accessor :aliases
67
+ # @private
68
+ attr_accessor :field_in_length
69
+
70
+ # rubocop:disable Metrics/ParameterLists
71
+
72
+ # Generate a TLV class
73
+ # @param [Class] type_class Class to use for +type+
74
+ # @param [Class] length_class Class to use for +length+
75
+ # @param [Class] value_class Class to use for +value+
76
+ # @param [String] field_order give field order. Each character in [T,L,V] MUST be present once,
77
+ # in the desired order.
78
+ # @param [String] field_in_length give fields to compute length on.
79
+ # @return [Class]
80
+ # @since 3.1.4 Add +header_in_length+ parameter
81
+ # @since 3.3.1 Add +field_order+ and +field_in_length' parameters. Deprecate +header_in_length+ parameter.
82
+ def create(type_class: Int8Enum, length_class: Int8, value_class: String,
83
+ aliases: {}, field_order: 'TLV', field_in_length: 'V')
84
+ unless equal?(AbstractTLV)
85
+ raise Error,
86
+ '.create cannot be called on a subclass of PacketGen::Types::AbstractTLV'
87
+ end
88
+
89
+ klass = Class.new(self)
90
+ klass.aliases = aliases
91
+ klass.field_in_length = field_in_length
92
+
93
+ check_field_in_length(field_in_length)
94
+ check_field_order(field_order)
95
+ generate_fields(klass, field_order, type_class, length_class, value_class)
96
+
97
+ aliases.each do |al, orig|
98
+ klass.instance_eval do
99
+ alias_method al, orig if klass.method_defined?(orig)
100
+ alias_method :"#{al}=", :"#{orig}=" if klass.method_defined?(:"#{orig}=")
101
+ end
102
+ end
103
+
104
+ klass
105
+ end
106
+ # rubocop:enable Metrics/ParameterLists
107
+
108
+ # @!attribute type
109
+ # @abstract Type attribute for real TLV class
110
+ # @return [Integer]
111
+ # @!attribute length
112
+ # @abstract Length attribute for real TLV class
113
+ # @return [Integer]
114
+ # @!attribute value
115
+ # @abstract Value attribute for real TLV class
116
+ # @return [Object]
117
+
118
+ # @abstract Should only be called on real TLV classes, created by {.create}.
119
+ # Set enum hash for {#type} field.
120
+ # @param [Hash{String, Symbol => Integer}] hsh enum hash
121
+ # @return [void]
122
+ def define_type_enum(hsh)
123
+ field_defs[:type][:options][:enum].clear
124
+ field_defs[:type][:options][:enum].merge!(hsh)
125
+ end
126
+
127
+ # @abstract Should only be called on real TLV classes, created by {.create}.
128
+ # Set default value for {#type} field.
129
+ # @param [Integer,String,Symbol,nil] default default value from +hsh+ for type
130
+ # @return [void]
131
+ # @since 3.4.0
132
+ def define_type_default(default)
133
+ field_defs[:type][:default] = default
134
+ end
135
+
136
+ private
137
+
138
+ def check_field_in_length(field_in_length)
139
+ return if /^[TLV]{1,3}$/.match?(field_in_length)
140
+
141
+ raise 'field_in_length must only contain T, L and/or V characters'
142
+ end
143
+
144
+ def check_field_order(field_order)
145
+ if field_order.match(/^[TLV]{3,3}$/) &&
146
+ (field_order[0] != field_order[1]) &&
147
+ (field_order[0] != field_order[2]) &&
148
+ (field_order[1] != field_order[2])
149
+ return
150
+ end
151
+
152
+ raise 'field_order must contain all three letters TLV, each once'
153
+ end
154
+
155
+ def generate_fields(klass, field_order, type_class, length_class, value_class)
156
+ field_order.each_char do |field_type|
157
+ case field_type
158
+ when 'T'
159
+ if type_class < Enum
160
+ klass.define_field(:type, type_class, enum: {})
161
+ else
162
+ klass.define_field(:type, type_class)
163
+ end
164
+ when 'L'
165
+ klass.define_field(:length, length_class)
166
+ when 'V'
167
+ klass.define_field(:value, value_class)
168
+ end
169
+ end
170
+ end
171
+ end
172
+
173
+ # @!attribute type
174
+ # @abstract Type attribute
175
+ # @return [Integer]
176
+ # @!attribute length
177
+ # @abstract Length
178
+ # @return [Integer]
179
+ # @!attribute value
180
+ # @abstract Value attribute
181
+ # @return [Object]enum
182
+
183
+ # @abstract Should only be called on real TLV classes, created by {.create}.
184
+ # @param [Hash] options
185
+ # @option options [Integer] :type
186
+ # @option options [Integer] :length
187
+ # @option options [Object] :value
188
+ def initialize(options = {})
189
+ @field_in_length = self.class.field_in_length
190
+ self.class.aliases.each do |al, orig|
191
+ options[orig] = options[al] if options.key?(al)
192
+ end
193
+
194
+ super
195
+ # used #value= defined below, which set length if needed
196
+ self.value = options[:value] if options[:value]
197
+ calc_length unless options.key?(:length)
198
+ end
199
+
200
+ # @abstract Should only be called on real TLV class instances.
201
+ # Populate object from a binary string
202
+ # @param [String,nil] str
203
+ # @return [Fields] self
204
+ def read(str)
205
+ return self if str.nil?
206
+
207
+ idx = 0
208
+ fields.each do |field_name|
209
+ field = self[field_name]
210
+ length = field_name == :value ? real_length : field.sz
211
+ field.read(str[idx, length])
212
+ idx += field.sz
213
+ end
214
+
215
+ self
216
+ end
217
+
218
+ # @abstract Should only be called on real TLV class instances.
219
+ # Set +value+. May set +length+ if value is a {Types::String}.
220
+ # @param [Object] val
221
+ # @return [Object]
222
+ # @since 3.4.0 Base on field's +#from_human+ method
223
+ def value=(val)
224
+ if val.is_a?(self[:value].class)
225
+ self[:value] = val
226
+ elsif self[:value].respond_to?(:from_human)
227
+ self[:value].from_human(val)
228
+ else
229
+ self[:value].read(val)
230
+ end
231
+ calc_length
232
+ end
233
+
234
+ # @abstract Should only be called on real TLV class instances.
235
+ # Get human-readable type
236
+ # @return [String]
237
+ def human_type
238
+ self[:type].to_human.to_s
239
+ end
240
+
241
+ # @abstract Should only be called on real TLV class instances.
242
+ # @return [String]
243
+ def to_human
244
+ my_value = self[:value].is_a?(String) ? self[:value].inspect : self[:value].to_human
245
+ 'type:%s,length:%u,value:%s' % [human_type, length, my_value]
246
+ end
247
+
248
+ # Calculate length
249
+ # @return [void]
250
+ # @since 3.4.0
251
+ def calc_length
252
+ fil = @field_in_length
253
+
254
+ length = 0
255
+ fil.each_char do |field_type|
256
+ length += self[FIELD_TYPES[field_type]].sz
257
+ end
258
+ self.length = length
259
+ end
260
+
261
+ private
262
+
263
+ def real_length
264
+ length = self.length
265
+ length -= self[:type].sz if @field_in_length.include?('T')
266
+ length -= self[:length].sz if @field_in_length.include?('L')
267
+ length
268
+ end
269
+ end
270
+ end
@@ -0,0 +1,288 @@
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
+ require 'forwardable'
10
+
11
+ module BinStruct
12
+ # @abstract Base class to define set of {Fields} subclasses.
13
+ # == #record_from_hash
14
+ # Subclasses should define private method +#record_from_hash+. This method
15
+ # is called by {#push} to add an object to the set.
16
+ #
17
+ # A default method is defined by {Array}: it calls constructor of class defined
18
+ # by {.set_of}.
19
+ #
20
+ # == #real_type
21
+ # Subclasses should define private method +#real_type+ if {.set_of} type
22
+ # may be subclassed. This method should return real class to use. It
23
+ # takes an only argument, which is of type given by {.set_of}.
24
+ #
25
+ # Default behaviour of this method is to return argument's class.
26
+ #
27
+ # @author Sylvain Daubert
28
+ class Array
29
+ extend Forwardable
30
+ include Enumerable
31
+ include Fieldable
32
+ include LengthFrom
33
+
34
+ # @!method [](index)
35
+ # Return the element at +index+.
36
+ # @param [integer] index
37
+ # @return [Object]
38
+ # @!method clear
39
+ # Clear array.
40
+ # @return [void]
41
+ # @!method each
42
+ # Calls the given block once for each element in self, passing that
43
+ # element as a parameter. Returns the array itself.
44
+ # @return [Array]
45
+ # @method empty?
46
+ # Return +true+ if contains no element.
47
+ # @return [Booelan]
48
+ # @!method first
49
+ # Return first element
50
+ # @return [Object]
51
+ # @!method last
52
+ # Return last element.
53
+ # @return [Object]
54
+ # @!method size
55
+ # Get number of element in array
56
+ # @return [Integer]
57
+ def_delegators :@array, :[], :clear, :each, :empty?, :first, :last, :size
58
+ alias length size
59
+
60
+ # Separator used in {#to_human}.
61
+ # May be ovverriden by subclasses
62
+ HUMAN_SEPARATOR = ','
63
+
64
+ # rubocop:disable Naming/AccessorMethodName
65
+ class << self
66
+ # Get class set with {.set_of}.
67
+ # @return [Class]
68
+ # @since 3.0.0
69
+ def set_of_klass
70
+ @klass
71
+ end
72
+
73
+ # Define type of objects in set. Used by {#read} and {#push}.
74
+ # @param [Class] klass
75
+ # @return [void]
76
+ def set_of(klass)
77
+ @klass = klass
78
+ end
79
+ end
80
+ # rubocop:enable Naming/AccessorMethodName
81
+
82
+ # @param [Hash] options
83
+ # @option options [Int] counter Int object used as a counter for this set
84
+ def initialize(options = {})
85
+ @counter = options[:counter]
86
+ @array = []
87
+ initialize_length_from(options)
88
+ end
89
+
90
+ # Initialize array for copy:
91
+ # * duplicate internal array.
92
+ def initialize_copy(_other)
93
+ @array = @array.dup
94
+ end
95
+
96
+ def ==(other)
97
+ @array == case other
98
+ when Array
99
+ other.to_a
100
+ else
101
+ other
102
+ end
103
+ end
104
+
105
+ # Clear array. Reset associated counter, if any.
106
+ # @return [void]
107
+ def clear!
108
+ @array.clear
109
+ @counter&.from_human(0)
110
+ end
111
+
112
+ # Delete an object from this array. Update associated counter if any
113
+ # @param [Object] obj
114
+ # @return [Object] deleted object
115
+ def delete(obj)
116
+ deleted = @array.delete(obj)
117
+ @counter.from_human(@counter.to_i - 1) if @counter && deleted
118
+ deleted
119
+ end
120
+
121
+ # Delete element at +index+.
122
+ # @param [Integer] index
123
+ # @return [Object,nil] deleted object
124
+ def delete_at(index)
125
+ deleted = @array.delete_at(index)
126
+ @counter.from_human(@counter.to_i - 1) if @counter && deleted
127
+ deleted
128
+ end
129
+
130
+ # @abstract depend on private method +#record_from_hash+ which should be
131
+ # declared by subclasses.
132
+ # Add an object to this array
133
+ # @param [Object] obj type depends on subclass
134
+ # @return [Array] self
135
+ def push(obj)
136
+ obj = case obj
137
+ when Hash
138
+ record_from_hash obj
139
+ else
140
+ obj
141
+ end
142
+ @array << obj
143
+ self
144
+ end
145
+
146
+ # @abstract depend on private method +#record_from_hash+ which should be
147
+ # declared by subclasses.
148
+ # Add an object to this array, and increment associated counter, if any
149
+ # @param [Object] obj type depends on subclass
150
+ # @return [Array] self
151
+ def <<(obj)
152
+ push obj
153
+ @counter&.from_human(@counter.to_i + 1)
154
+ self
155
+ end
156
+
157
+ # Populate object from a string or from an array of hashes
158
+ # @param [String, Array<Hash>] data
159
+ # @return [self]
160
+ def read(data)
161
+ clear
162
+ case data
163
+ when ::Array
164
+ read_from_array(data)
165
+ else
166
+ read_from_string(data)
167
+ end
168
+ self
169
+ end
170
+
171
+ # Get size in bytes
172
+ # @return [Integer]
173
+ def sz
174
+ to_s.size
175
+ end
176
+
177
+ # Return an Array
178
+ # @return [::Array]
179
+ def to_a
180
+ @array
181
+ end
182
+
183
+ # Get binary string
184
+ # @return [String]
185
+ def to_s
186
+ @array.map(&:to_s).join
187
+ end
188
+
189
+ # Get a human readable string
190
+ # @return [String]
191
+ def to_human
192
+ @array.map(&:to_human).join(self.class::HUMAN_SEPARATOR)
193
+ end
194
+
195
+ private
196
+
197
+ # rubocop:disable Metrics/CyclomaticComplexity
198
+
199
+ def read_from_string(str)
200
+ return self if str.nil? || @counter&.to_i&.zero?
201
+
202
+ str = read_with_length_from(str)
203
+ until str.empty? || (@counter && size == @counter.to_i)
204
+ obj = create_object_from_str(str)
205
+ @array << obj
206
+ str.slice!(0, obj.sz)
207
+ end
208
+ end
209
+ # rubocop:enable Metrics/CyclomaticComplexity
210
+
211
+ def read_from_array(ary)
212
+ return self if ary.empty?
213
+
214
+ ary.each do |hsh|
215
+ self << hsh
216
+ end
217
+ end
218
+
219
+ def record_from_hash(hsh)
220
+ obj_klass = self.class.set_of_klass
221
+ unless obj_klass
222
+ raise NotImplementedError,
223
+ 'class should define #record_from_hash or declare type of elements in set with .set_of'
224
+ end
225
+
226
+ obj = obj_klass.new(hsh) if obj_klass
227
+ klass = real_type(obj)
228
+ klass == obj_klass ? obj : klass.new(hsh)
229
+ end
230
+
231
+ def real_type(_obj)
232
+ self.class.set_of_klass
233
+ end
234
+
235
+ def create_object_from_str(str)
236
+ klass = self.class.set_of_klass
237
+ obj = klass.new.read(str)
238
+ real_klass = real_type(obj)
239
+
240
+ if real_klass == klass
241
+ obj
242
+ else
243
+ real_klass.new.read(str)
244
+ end
245
+ end
246
+ end
247
+
248
+ # @private
249
+ module ArrayOfIntMixin
250
+ def read_from_array(ary)
251
+ return self if ary.empty?
252
+
253
+ ary.each do |i|
254
+ self << self.class.set_of_klass.new(i)
255
+ end
256
+ end
257
+ end
258
+
259
+ # Specialized array to handle serie of {Int8}.
260
+ class ArrayOfInt8 < Array
261
+ include ArrayOfIntMixin
262
+ set_of Int8
263
+ end
264
+
265
+ # Specialized array to handle serie of {Int16}.
266
+ class ArrayOfInt16 < Array
267
+ include ArrayOfIntMixin
268
+ set_of Int16
269
+ end
270
+
271
+ # Specialized array to handle serie of {Int16le}.
272
+ class ArrayOfInt16le < Array
273
+ include ArrayOfIntMixin
274
+ set_of Int16le
275
+ end
276
+
277
+ # Specialized array to handle serie of {Int32}.
278
+ class ArrayOfInt32 < BinStruct::Array
279
+ include ArrayOfIntMixin
280
+ set_of Int32
281
+ end
282
+
283
+ # Specialized array to handle serie of {Int32le}.
284
+ class ArrayOfInt32le < BinStruct::Array
285
+ include ArrayOfIntMixin
286
+ set_of Int32le
287
+ end
288
+ end
@@ -0,0 +1,106 @@
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
+ require 'forwardable'
10
+
11
+ module BinStruct
12
+ # This class handles null-terminated strings (aka C strings).
13
+ # @author Sylvain Daubert
14
+ # @since 3.1.6 no more a subclass or regular String
15
+ class CString
16
+ extend Forwardable
17
+ include Fieldable
18
+
19
+ def_delegators :@string, :[], :length, :size, :inspect, :==,
20
+ :unpack, :force_encoding, :encoding, :index, :empty?,
21
+ :encode, :slice, :slice!
22
+
23
+ # @return [::String]
24
+ attr_reader :string
25
+ # @return [Integer]
26
+ attr_reader :static_length
27
+
28
+ # @param [Hash] options
29
+ # @option options [Integer] :static_length set a static length for this string
30
+ def initialize(options = {})
31
+ register_internal_string(+'')
32
+ @static_length = options[:static_length]
33
+ end
34
+
35
+ # @param [::String] str
36
+ # @return [String] self
37
+ def read(str)
38
+ s = str.to_s
39
+ s = s[0, static_length] if static_length?
40
+ register_internal_string s
41
+ remove_null_character
42
+ self
43
+ end
44
+
45
+ # get null-terminated string
46
+ # @return [String]
47
+ def to_s
48
+ if static_length?
49
+ s = string[0, static_length - 1]
50
+ s << ("\x00" * (static_length - s.length))
51
+ else
52
+ s = "#{string}\x00"
53
+ end
54
+ BinStruct.force_binary(s)
55
+ end
56
+
57
+ # Append the given string to CString
58
+ # @param [#to_s] str
59
+ # @return [self]
60
+ def <<(str)
61
+ @string << str.to_s
62
+ remove_null_character
63
+ self
64
+ end
65
+
66
+ # @return [Integer]
67
+ def sz
68
+ if static_length?
69
+ static_length
70
+ else
71
+ to_s.size
72
+ end
73
+ end
74
+
75
+ # Say if a static length is defined
76
+ # @return [Boolean]
77
+ # @since 3.1.6
78
+ def static_length?
79
+ !static_length.nil?
80
+ end
81
+
82
+ # Populate CString from a human readable string
83
+ # @param [String] str
84
+ # @return [self]
85
+ def from_human(str)
86
+ read str
87
+ end
88
+
89
+ # @return [String]
90
+ def to_human
91
+ string
92
+ end
93
+
94
+ private
95
+
96
+ def register_internal_string(str)
97
+ @string = str
98
+ BinStruct.force_binary(@string)
99
+ end
100
+
101
+ def remove_null_character
102
+ idx = string.index(0.chr)
103
+ register_internal_string(string[0, idx]) unless idx.nil?
104
+ end
105
+ end
106
+ end