bin_struct 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
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