bin_struct 0.2.1 → 0.4.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: df710325cf6aa4414fc997d77033b8bea34d4dc80e571ceb184462b2aa047eb3
4
- data.tar.gz: aa372402e27bc5b254b7e4e82cb7d9b2178823c84adc3c1bda17bba7806ff05e
3
+ metadata.gz: aeb290e624ce4004c20cfaf40fc60ae43938c80eab8fd1df1e8aa908a75e26cf
4
+ data.tar.gz: 8cb5dd0abdce7421b8269a4d35c14886f57cc6440a50c924b0937e5b30fe20ce
5
5
  SHA512:
6
- metadata.gz: 6ef8207d1fc62509bda66a0caa779ec2b6b95e2a7805fb10199423bcf1300a7f05dd5c5b595f33f8b6605b93277e485a5a18f80a83b7c6954339eea0541f6073
7
- data.tar.gz: b47f00bd2497c5330eeb6a01721b3cbd4a305d624e6df7dce2fd13b8f996c61e287ab92df393ea0fcb67fece0396497354220e9a56cb50bf617fa0d9b3ccdaff
6
+ metadata.gz: ecf6acc2f5c406556598212d22ad6257d22ed646c1128a145cff1730dc537bb7acfd40903f211aad28b11fa30c11324a98b98ecebb70ad7b84ea991bcf181f25
7
+ data.tar.gz: 1dcfbb20a54f7c08d2794e0e8391b70c633d65f6ccbc41c372e46c77da6fb036fbfa9d1548462030b86603e3e41b132019da70b3c2d603f7f21116bdc842ec14
data/CHANGELOG.md CHANGED
@@ -3,6 +3,28 @@
3
3
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
4
4
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
5
5
 
6
+ ## 0.4.0 - 2025-02-13
7
+
8
+ ### Added
9
+
10
+ - Add `Struct#attribute?` to check existence of an attribute.
11
+ - Add `AbstractTLV.derive` to derive a new subclass from a concrete TLV class.
12
+
13
+ ### Fixed
14
+
15
+ - Update and fix Yard documentation.
16
+
17
+ ## 0.3.0 - 2024-12-02
18
+
19
+ ### Added
20
+
21
+ - `BitAddr` class is added. This class is used as a `Structable` type to handle bitfield attributes.
22
+ - Add `Struct.define_bit_attr`, `.define_bit_attr_before` and `.define_bit_attr_before` to define bitfield attributes.
23
+
24
+ ### Changed
25
+
26
+ - `Struct.define_bit_attr_on` is removed in favor of `Struct.define_bit_attr`. Bitfield attributes are now first class attributes, and no more an onverlay on `Int`.
27
+
6
28
  ## 0.2.1 - 2024-11-25
7
29
 
8
30
  ### Added
data/README.md CHANGED
@@ -19,6 +19,89 @@ Or add it to a Gemfile:
19
19
  gem 'bin_struct'
20
20
  ```
21
21
 
22
+ ## Usage
23
+
24
+ ### Create a struct
25
+
26
+ To create a BinStruct, create a new class inheriting from `BinStruct::Struct`. Then, defines struct attributes using `.define_attr`. `.define_bit_attr` may also be used to define bit field attributes.
27
+
28
+ ```ruby
29
+ require 'bin_struct'
30
+
31
+ class IPHeader < BinStruct::Struct
32
+ # Define a bir field, defaulting to 0x45, and splitted in 2 sub-fields: version and ihl,
33
+ # 4-bit size each
34
+ define_bit_attr :u8, default: 0x45, version: 4, ihl: 4
35
+ # Define a 8-bit unsigned integer named tos
36
+ # 1st argument: a symbol to define attribute name
37
+ # 2nd argument: a class to define attribute type. May be a type provided by BinStruct,
38
+ # or a user-defined class inheriting from one of these classes
39
+ # others arguments: options. Here, :default defines a default value for the attribute.
40
+ define_attr :tos, BinStruct::Int8, default: 0
41
+ # Define a 16-bit unsigned integer named length. Default to 20.
42
+ define_attr :length, BinStruct::Int16, default: 20
43
+ # Define a 16-bir unsigned integer named id. It is initialized with a random number
44
+ define_attr :id, BinStruct::Int16, default: ->(_) { rand(65_535) }
45
+ # Define a bit field composed of 4 subfields of 1, 1, 1 and 13 bit, respectively
46
+ define_bit_attr :frag, flag_rsv: 1, flag_df: 1, flag_mf: 1, fragment_offset: 13
47
+ # Define TTL field, a 8-bit unsigned integer, default to 64
48
+ define_attr :ttl, BinStruct::Int8, default: 64
49
+ # Define protocol field (8-bit unsigned integer)
50
+ define_attr :protocol, BinStruct::Int8
51
+ # Define checksum field (16-bit unsigned integer), default to 0
52
+ define_attr :checksum, BinStruct::Int16, default: 0
53
+ # Source and destination addresses, defined as array of 4 8-bit unsigned integers
54
+ define_attr :src, BinStruct::ArrayOfInt8, length_from: -> { 4 }
55
+ define_attr :dst, BinStruct::ArrayOfInt8, length_from: -> { 4 }
56
+ end
57
+ ```
58
+
59
+ ### Parse a binary string
60
+
61
+ ```ruby
62
+ # Initialize struct from a binary string
63
+ ip = IPHeader.new.read("\x45\x00\x00\x14\x43\x21\x00\x00\x40\x01\x00\x00\x7f\x00\x00\x01\x7f\x00\x00\x01".b)
64
+
65
+ # Access some fields
66
+ p ip.version #=> 4
67
+ p ip.ihl #=> 5
68
+ p ip.id.to_s(16) #=> "4321"
69
+ p ip.protocol #=> 1
70
+ p ip.src.map { |byte| byte.to_i }.join('.') #=> "127.0.0.1"
71
+ ```
72
+
73
+ ```text
74
+ > p IPHeader.new.read("\x45\x00\x00\x14\x43\x21\x00\x00\x40\x01\x00\x00\x7f\x00\x00\x01\x7f\x00\x00\x01")
75
+ -- IPHeader -----------------------------------------------------------
76
+ BitAttr8 u8: 69 (0x45)
77
+ version:4 ihl:5
78
+ Int8 tos: 0 (0x00)
79
+ Int16 length: 20 (0x0014)
80
+ Int16 id: 17185 (0x4321)
81
+ BitAttr16 frag: 0 (0x0000)
82
+ flag_rsv:0 flag_df:0 flag_mf:0 fragment_offset:0
83
+ Int8 ttl: 64 (0x40)
84
+ Int8 protocol: 1 (0x01)
85
+ Int16 checksum: 0 (0x0000)
86
+ ArrayOfInt8 src: 127,0,0,1
87
+ ArrayOfInt8 dst: 127,0,0,1
88
+
89
+ ```
90
+
91
+ ### Generate a binary string
92
+
93
+ ```ruby
94
+ # Create a new struct with some fields initialized
95
+ ip = IPHeader.new(tos: 42, id: 0x1234)
96
+
97
+ # Initialize fields after creation
98
+ ip.src = [192, 168, 1, 1]
99
+ ip.dst = [192, 168, 1, 2]
100
+
101
+ # Generate binary string
102
+ ip.to_s
103
+ ```
104
+
22
105
  ## License
23
106
 
24
107
  The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
@@ -13,7 +13,7 @@ module BinStruct
13
13
  #
14
14
  # ===Usage
15
15
  # To simply define a new TLV class, do:
16
- # MyTLV = PacketGen::Types::AbstractTLV.create
16
+ # MyTLV = BinStruct::AbstractTLV.create
17
17
  # MyTLV.define_type_enum 'one' => 1, 'two' => 2
18
18
  # This will define a new +MyTLV+ class, subclass of {AbstractTLV}. This class will
19
19
  # define 3 attributes:
@@ -32,9 +32,9 @@ module BinStruct
32
32
  #
33
33
  # ===Advanced usage
34
34
  # Each attribute'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)
35
+ # MyTLV = BinStruct::AbstractTLV.create(type_class: BinStruct::Int16,
36
+ # length_class: BinStruct::Int16,
37
+ # value_class: PacketGen::Header::IP::Addr)
38
38
  # tlv = MyTLV.new(type: 1, value: '1.2.3.4')
39
39
  # tlv.type #=> 1
40
40
  # tlv.length #=> 4
@@ -43,9 +43,9 @@ module BinStruct
43
43
  #
44
44
  # Some aliases may also be defined. For example, to create a TLV type
45
45
  # whose +type+ attribute 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 })
46
+ # MyTLV = BinStruct::AbstractTLV.create(type_class: BinStruct::Int16,
47
+ # length_class: BinStruct::Int16,
48
+ # aliases: { code: :type })
49
49
  # tlv = MyTLV.new(code: 1, value: 'abcd')
50
50
  # tlv.code #=> 1
51
51
  # tlv.type #=> 1
@@ -77,11 +77,12 @@ module BinStruct
77
77
  # in the desired order.
78
78
  # @param [::String] attr_in_length give attributes to compute length on.
79
79
  # @return [Class]
80
+ # @raise [Error] Called on {AbstractTLV} subclass
80
81
  def create(type_class: Int8Enum, length_class: Int8, value_class: String,
81
82
  aliases: {}, attr_order: 'TLV', attr_in_length: 'V')
82
83
  unless equal?(AbstractTLV)
83
84
  raise Error,
84
- '.create cannot be called on a subclass of PacketGen::Types::AbstractTLV'
85
+ '.create cannot be called on a subclass of BinStruct::AbstractTLV'
85
86
  end
86
87
 
87
88
  klass = Class.new(self)
@@ -91,7 +92,7 @@ module BinStruct
91
92
  check_attr_in_length(attr_in_length)
92
93
  check_attr_order(attr_order)
93
94
  generate_attributes(klass, attr_order, type_class, length_class, value_class)
94
-
95
+ generate_aliases_for(klass, aliases)
95
96
  aliases.each do |al, orig|
96
97
  klass.instance_eval do
97
98
  alias_method al, orig if klass.method_defined?(orig)
@@ -103,6 +104,50 @@ module BinStruct
103
104
  end
104
105
  # rubocop:enable Metrics/ParameterLists
105
106
 
107
+ # On inheritage, copy aliases and attr_in_length
108
+ # @param [Class] klass inheriting class
109
+ # @return [void]
110
+ # @since 0.4.0
111
+ # @author LemonTree55
112
+ def inherited(klass)
113
+ super
114
+
115
+ aliases = @aliases.clone
116
+ attr_in_length = @attr_in_length.clone
117
+
118
+ klass.class_eval do
119
+ @aliases = aliases
120
+ @attr_in_length = attr_in_length
121
+ end
122
+ end
123
+
124
+ # Derive a new TLV class from an existing one
125
+ # @param [Class,nil] type_class New class to use for +type+. Unchanged if +nil+.
126
+ # @param [Class,nil] length_class New class to use for +length+. Unchanged if +nil+.
127
+ # @param [Class,nil] value_class New class to use for +value+. Unchanged if +nil+.
128
+ # @return [Class]
129
+ # @raise [Error] Called on {AbstractTLV} class
130
+ # @since 0.4.0
131
+ # @author LemonTree55
132
+ # @example
133
+ # # TLV with type and length on 16 bits, value is a BinStruct::String
134
+ # FirstTLV = BinStruct::AbstractTLV.create(type_class: BinStruct::Int16, length_class: BinStruct::Int16)
135
+ # # TLV with same type and length classes than FirstTLV, but value is an array of Int8
136
+ # SecondTLV = FirstTLV.derive(value_class: BinStruct::ArrayOfInt8)
137
+ def derive(type_class: nil, length_class: nil, value_class: nil, aliases: {})
138
+ raise Error, ".derive cannot be called on #{name}" if equal?(AbstractTLV)
139
+
140
+ klass = Class.new(self)
141
+ klass.aliases.merge!(aliases)
142
+ generate_aliases_for(klass, aliases)
143
+
144
+ klass.attr_defs[:type].type = type_class unless type_class.nil?
145
+ klass.attr_defs[:length].type = length_class unless length_class.nil?
146
+ klass.attr_defs[:value].type = value_class unless value_class.nil?
147
+
148
+ klass
149
+ end
150
+
106
151
  # @!attribute type
107
152
  # @abstract
108
153
  # Type attribute for real TLV class
@@ -168,6 +213,15 @@ module BinStruct
168
213
  end
169
214
  end
170
215
  end
216
+
217
+ def generate_aliases_for(klass, aliases)
218
+ aliases.each do |al, orig|
219
+ klass.instance_eval do
220
+ alias_method al, orig if klass.method_defined?(orig)
221
+ alias_method :"#{al}=", :"#{orig}=" if klass.method_defined?(:"#{orig}=")
222
+ end
223
+ end
224
+ end
171
225
  end
172
226
 
173
227
  # @!attribute type
@@ -9,7 +9,7 @@
9
9
  require 'forwardable'
10
10
 
11
11
  module BinStruct
12
- # @abstract Base class to define set of {Struct} subclasses.
12
+ # @abstract Base class to define set of {Structable} subclasses.
13
13
  #
14
14
  # This class mimics regular Ruby Array, but it is {Structable} and responds to {LengthFrom}.
15
15
  #
@@ -44,10 +44,11 @@ module BinStruct
44
44
  # @!method clear
45
45
  # Clear array.
46
46
  # @return [void]
47
+ # @see #clear!
47
48
  # @!method each
48
49
  # Calls the given block once for each element in self, passing that
49
- # element as a parameter. Returns the array itself.
50
- # @return [::Array]
50
+ # element as a parameter. Returns the array itself, or an enumerator if no block is given.
51
+ # @return [::Array, Enumerator]
51
52
  # @method empty?
52
53
  # Return +true+ if contains no element.
53
54
  # @return [Boolean]
@@ -112,6 +113,7 @@ module BinStruct
112
113
 
113
114
  # Clear array. Reset associated counter, if any.
114
115
  # @return [void]
116
+ # @see #clear
115
117
  def clear!
116
118
  @array.clear
117
119
  @counter&.from_human(0)
@@ -137,7 +139,8 @@ module BinStruct
137
139
 
138
140
  # @abstract depend on private method +#record_from_hash+ which should be
139
141
  # declared by subclasses.
140
- # Add an object to this array. Do not update associated counter.
142
+ # Add an object to this array. Do not update associated counter. If associated must be incremented, use
143
+ # {#<<}
141
144
  # @param [Object] obj type depends on subclass
142
145
  # @return [self]
143
146
  # @see #<<
@@ -154,9 +157,11 @@ module BinStruct
154
157
 
155
158
  # @abstract depend on private method +#record_from_hash+ which should be
156
159
  # declared by subclasses.
157
- # Add an object to this array, and increment associated counter, if any
160
+ # Add an object to this array, and increment associated counter, if any. If associated counter must not be
161
+ # incremented, use {#push}.
158
162
  # @param [Object] obj type depends on subclass
159
163
  # @return [self]
164
+ # @see #push
160
165
  def <<(obj)
161
166
  push(obj)
162
167
  @counter&.from_human(@counter.to_i + 1)
@@ -0,0 +1,188 @@
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
+ require 'digest'
8
+
9
+ module BinStruct
10
+ # Define a bitfield attribute to embed in a {Struct}.
11
+ #
12
+ # class MyStruct < BinStruct::Struct
13
+ # # Create a 32-bit bitfield attribute, with fields a (16 bits), b and c (4 bits each) and d (8 bits).
14
+ # # a is the leftmost field in bitfield, and d the rightmost one.
15
+ # define_attr :int32, BinStruct::BitAttr.create(width: 32, a: 16, b: 4, c: 4, d:8)
16
+ # end
17
+ # @since 0.3.0
18
+ # @abstract Subclasses must de derived using {.create}.
19
+ # @author LemonTree55
20
+ class BitAttr
21
+ include Structable
22
+
23
+ # @return [Integer] width in bits of bit attribute
24
+ attr_reader :width
25
+ # @return [::Array[Symbol]]
26
+ attr_reader :bit_methods
27
+
28
+ # @private
29
+ Parameters = Struct.new(:width, :fields, :int)
30
+
31
+ class << self
32
+ @cache = {}
33
+
34
+ # @private
35
+ # @return [Parameters]
36
+ attr_reader :parameters
37
+
38
+ # Create a new {BitAttr} subclass with specified parameters
39
+ # @param [Integer] width size of bitfields in bits. Must be a size of an {Int} (8, 16, 24, 32 or 64 bits).
40
+ # @param [:big,:little,:native] endian endianess of bit attribute as an integer
41
+ # @param [Hash{Symbol=>Integer}] fields hash associating field names with their size. Total size MUST be equal
42
+ # to +width+.
43
+ # @return [Class]
44
+ # @raise [ArgumentError] raise if:
45
+ # * width is not a size of one of {Int} subclasses,
46
+ # * sum of bitfield sizes is not equal to +width+
47
+ def create(width:, endian: :big, **fields)
48
+ raise ArgumentError, 'with must be 8, 16, 24, 32 or 64' unless [8, 16, 24, 32, 64].include?(width)
49
+
50
+ hsh = compute_hash(width, endian, fields)
51
+ cached = cache[hsh]
52
+ return cached if cached
53
+
54
+ total_size = fields.reduce(0) { |acc, ary| acc + ary.last }
55
+ raise ArgumentError, "sum of bitfield sizes is not equal to #{width}" unless total_size == width
56
+
57
+ cache[hsh] = Class.new(self) do
58
+ int_klass = BinStruct.const_get("Int#{width}")
59
+ @parameters = Parameters.new(width, fields, int_klass.new(endian: endian)).freeze
60
+ end
61
+ end
62
+
63
+ private
64
+
65
+ # @return [Hash{::String=>Class}]
66
+ def cache
67
+ return @cache if defined? @cache
68
+
69
+ @cache = {}
70
+ end
71
+
72
+ # @param [::Array] params
73
+ # @return [::String]
74
+ def compute_hash(*params)
75
+ Digest::MD5.digest(Marshal.dump(params))
76
+ end
77
+ end
78
+
79
+ # Initialize bit attribute
80
+ # @param [Hash{Symbol=>Integer}] opts initialization values for fields, where keys are field names and values are
81
+ # initialization values
82
+ # @return [self]
83
+ # @raise [NotImplementedError] raised when called on {BitAttr} class
84
+ def initialize(opts = {})
85
+ parameters = self.class.parameters
86
+ raise NotImplementedError, "#initialize may only be called on subclass of #{self.class}" if parameters.nil?
87
+
88
+ @width = parameters.width
89
+ @fields = parameters.fields
90
+ @int = parameters.int.dup
91
+ @data = {}
92
+ @bit_methods = []
93
+
94
+ parameters.fields.each do |name, size|
95
+ @data[name] = opts[name] || 0
96
+ define_methods(name, size)
97
+ end
98
+ @bit_methods.freeze
99
+ end
100
+
101
+ # Get type name
102
+ # @return [::String]
103
+ def type_name
104
+ return @type_name if defined? @type_name
105
+
106
+ endian_suffix = case @int.endian
107
+ when :big then ''
108
+ when :little then 'le'
109
+ when :native then 'n'
110
+ end
111
+ @type_name = "BitAttr#{@width}#{endian_suffix}"
112
+ end
113
+
114
+ # Populate bit attribute from +str+
115
+ # @param [#to_s,nil] str
116
+ # @return [self]
117
+ def read(str)
118
+ return self if str.nil?
119
+
120
+ @int.read(str)
121
+ compute_data(@int.to_i)
122
+ end
123
+
124
+ # Give integer associated to this attribute
125
+ # @return [Integer]
126
+ def to_i
127
+ v = 0
128
+ @fields.each do |name, size|
129
+ v <<= size
130
+ v |= @data[name]
131
+ end
132
+
133
+ v
134
+ end
135
+ alias to_human to_i
136
+
137
+ # Return binary string
138
+ # @return [::String]
139
+ def to_s
140
+ @int.value = to_i
141
+ @int.to_s
142
+ end
143
+
144
+ # Set fields from associated integer
145
+ # @param [#to_i] value
146
+ # @return [self]
147
+ def from_human(value)
148
+ compute_data(value.to_i)
149
+ end
150
+
151
+ def format_inspect
152
+ str = @int.format_inspect << "\n"
153
+ str << @data.map { |name, value| "#{name}:#{value}" }.join(' ')
154
+ end
155
+
156
+ private
157
+
158
+ # @param [Integer] value
159
+ # @return [self]
160
+ def compute_data(value)
161
+ @fields.reverse_each do |name, size|
162
+ @data[name] = value & ((2**size) - 1)
163
+ value >>= size
164
+ end
165
+
166
+ self
167
+ end
168
+
169
+ # @param [Symbol] name
170
+ # @return [void]
171
+ def define_methods(name, size)
172
+ instance_eval "def #{name}; @data[#{name.inspect}]; end\n", __FILE__, __LINE__ # def name; data[:name]; end
173
+ bit_methods << name
174
+ bit_methods << :"#{name}="
175
+
176
+ # rubocop:disable Style/DocumentDynamicEvalDefinition
177
+ if size == 1
178
+ instance_eval "def #{name}?; @data[#{name.inspect}] != 0; end\n", __FILE__, __LINE__
179
+ instance_eval "def #{name}=(val); v = case val when TrueClass; 1 when FalseClass; 0 else val end; " \
180
+ "@data[#{name.inspect}] = v; end", __FILE__, __LINE__ - 1
181
+ bit_methods << :"#{name}?"
182
+ else
183
+ instance_eval "def #{name}=(val); @data[#{name.inspect}] = val; end", __FILE__, __LINE__
184
+ end
185
+ # rubocop:enable Style/DocumentDynamicEvalDefinition
186
+ end
187
+ end
188
+ end
@@ -76,14 +76,14 @@ module BinStruct
76
76
 
77
77
  # @param [Hash] options
78
78
  # @option options [Integer] :static_length set a static length for this string
79
- # @option options [::String] :value string value (default to +''+)
79
+ # @option options [::String] :value string value (default to +""+)
80
80
  def initialize(options = {})
81
81
  register_internal_string(options[:value] || +'')
82
82
  @static_length = options[:static_length]
83
83
  end
84
84
 
85
85
  # Populate self from binary string
86
- # @param [::String] str
86
+ # @param [#to_s] str
87
87
  # @return [self]
88
88
  def read(str)
89
89
  s = str.to_s
@@ -13,19 +13,20 @@ module BinStruct
13
13
  # and named values.
14
14
  #
15
15
  # == Simple example
16
- # enum = Int8Enum.new('low' => 0, 'medium' => 1, 'high' => 2})
16
+ # enum = Int8Enum.new('low' => 0, 'medium' => 1, 'high' => 2})
17
17
  # In this example, +enum+ is a 8-bit attribute which may take one
18
18
  # among three values: +low+, +medium+ or +high+:
19
- # enum.value = 'high'
20
- # enum.value # => 2
21
- # enum.value = 1
22
- # enum.value # => 1
23
- # enum.to_human # => "medium"
24
- # Setting an unknown value will raise an exception:
25
- # enum.value = 4 # => raise!
26
- # enum.value = 'unknown' # => raise!
27
- # But {#read} will not raise when reading an outbound value. This
19
+ # enum.value = 'high'
20
+ # enum.value # => 2
21
+ # enum.value = 1
22
+ # enum.value # => 1
23
+ # enum.to_human # => "medium"
24
+ # Setting an unknown name will raise an exception:
25
+ # enum.value = 'unknown' # => raise!
26
+ # But {#read} and {#value=} will not raise when reading/setting an out-of-bound integer. This
28
27
  # to enable decoding (or forging) of bad packets.
28
+ # enum.read("\x05".b).value # => 5
29
+ # enum.value = 4 # => 4
29
30
  # @author Sylvain Daubert (2016-2024)
30
31
  # @author LemonTree55
31
32
  class Enum < Int
@@ -55,7 +55,7 @@ module BinStruct
55
55
 
56
56
  # @abstract
57
57
  # @return [::String]
58
- # @raise [Error] This is an abstrat method and must be redefined
58
+ # @raise [Error] This is an abstract method and must be redefined
59
59
  def to_s
60
60
  raise Error, 'BinStruct::Int#to_s is abstract' unless defined? @packstr
61
61
 
@@ -9,6 +9,13 @@
9
9
  module BinStruct
10
10
  # Provides a class for creating strings preceeded by their length as an {Int}.
11
11
  # By default, a null string will have one byte length (length byte set to 0).
12
+ # == Examples
13
+ # # IntString with 8-bit length
14
+ # is8 = BinStruct::IntString.new(value: "abcd")
15
+ # is8.to_s # => "\x04abcd"
16
+ # # IntString with 16-bit length
17
+ # is16 = BinStruct::IntString.new(length_type: BinStruct::Int16le, value: "abcd")
18
+ # is16.to_s # => "\x04\x00abcd"
12
19
  # @author Sylvain Daubert (2016-2024)
13
20
  # @author LemonTree55
14
21
  class IntString
@@ -61,7 +68,7 @@ module BinStruct
61
68
  # @return [::String]
62
69
  def string=(str)
63
70
  @length.value = str.to_s.size
64
- @string = str.to_s
71
+ @string.read(str)
65
72
  end
66
73
 
67
74
  # Get binary string
@@ -82,7 +89,7 @@ module BinStruct
82
89
  # Get human readable string
83
90
  # @return [::String]
84
91
  def to_human
85
- @string
92
+ @string.to_s
86
93
  end
87
94
 
88
95
  # Set length from internal string length
@@ -32,7 +32,7 @@ module BinStruct
32
32
  # @option options [Int,Proc] :length_from object or proc from which
33
33
  # takes length when reading
34
34
  # @option options [Integer] :static_length set a static length for this string
35
- # @option options [::String] :value string value (default to +''+)
35
+ # @option options [::String] :value string value (default to +""+)
36
36
  def initialize(options = {})
37
37
  register_internal_string(options[:value] || +'')
38
38
  initialize_length_from(options)
@@ -47,7 +47,7 @@ module BinStruct
47
47
  end
48
48
 
49
49
  # Populate String from a binary String. Limit length using {LengthFrom} or {#static_length}, if one is set.
50
- # @param [::String] str
50
+ # @param [::String,nil] str
51
51
  # @return [self]
52
52
  def read(str)
53
53
  s = read_with_length_from(str)
@@ -87,7 +87,7 @@ module BinStruct
87
87
  self
88
88
  end
89
89
 
90
- # Generate binary string
90
+ # Generate "binary" string
91
91
  # @return [::String]
92
92
  def to_s
93
93
  @string
@@ -81,25 +81,23 @@ module BinStruct
81
81
  # define_attr :opt1, BinStruct::Int16, optional: ->(h) { h.type == 42 }
82
82
  #
83
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
84
+ # {.define_bit_attr} creates a bit attribute. For example, +frag+ attribute in IP header:
85
+ # define_bit_attr :frag, flag_rsv: 1, flag_df: 1, flag_mf: 1, fragment_offset: 13
88
86
  #
89
87
  # This example generates methods:
90
88
  # * +#frag+ and +#frag=+ to access +frag+ attribute as a 16-bit integer,
91
89
  # * +#flag_rsv?+, +#flag_rsv=+, +#flag_df?+, +#flag_df=+, +#flag_mf?+ and +#flag_mf=+
92
90
  # to access Boolean RSV, MF and DF flags from +frag+ attribute,
91
+ # * +#flag_rsv+, +#flag_df+ and +#flag_mf# to read RSV, MF and DF flags as Integer,
93
92
  # * +#fragment_offset+ and +#fragment_offset=+ to access 13-bit integer fragment
94
93
  # offset subattribute from +frag+ attribute.
95
94
  #
96
95
  # == Creating a new Struct class from another one
97
96
  # 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.
97
+ # * {.define_attr_before} and {.define_bit_attr_before} to define a new attribute before an existing one,
98
+ # * {.define_attr_after} and {.define_bit_attr_after} to define a new attribute after an existing onr,
99
+ # * {.remove_attr} to remove an existing attribute,
100
+ # * {.uptade_attr} to change options of an attribute (but not its type),
103
101
  #
104
102
  # @author Sylvain Daubert (2016-2024)
105
103
  # @author LemonTree55
@@ -121,7 +119,7 @@ module BinStruct
121
119
  # @return [Hash]
122
120
  attr_reader :attr_defs
123
121
  # Get bit attribute defintions for this class
124
- # @return [Hash]
122
+ # @return [Hash{Symbol=>Array[Symbol]}]
125
123
  attr_reader :bit_attrs
126
124
 
127
125
  # On inheritage, create +@attr_defs+ class variable
@@ -198,11 +196,7 @@ module BinStruct
198
196
  define_attr name, type, options
199
197
  return if other.nil?
200
198
 
201
- attributes.delete name
202
- idx = attributes.index(other)
203
- raise ArgumentError, "unknown #{other} attribute" if idx.nil?
204
-
205
- attributes[idx, 0] = name
199
+ move_attr(name, before: other)
206
200
  end
207
201
 
208
202
  # Define an attribute, after another one
@@ -217,11 +211,7 @@ module BinStruct
217
211
  define_attr name, type, options
218
212
  return if other.nil?
219
213
 
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
214
+ move_attr(name, after: other)
225
215
  end
226
216
 
227
217
  # Remove a previously defined attribute
@@ -229,9 +219,12 @@ module BinStruct
229
219
  # @return [void]
230
220
  def remove_attr(name)
231
221
  attributes.delete(name)
232
- @attr_defs.delete(name)
222
+ attr_def = attr_defs.delete(name)
233
223
  undef_method name if method_defined?(name)
234
224
  undef_method :"#{name}=" if method_defined?(:"#{name}=")
225
+ return unless bit_attrs[name]
226
+
227
+ attr_def.type.new.bit_methods.each { |meth| undef_method(meth) }
235
228
  end
236
229
 
237
230
  # Update a previously defined attribute
@@ -250,64 +243,95 @@ module BinStruct
250
243
  attr_defs[name].options.merge!(options)
251
244
  end
252
245
 
253
- # Define a bit attribute on given attribute
246
+ # Define a bit attribute
254
247
  # class MyHeader < BinStruct::Struct
255
- # define_attr :flags, BinStruct::Int16
256
- # # define a bit attribute on :flag attribute
248
+ # # define a 16-bit attribute named :flag
257
249
  # # 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
250
+ # # type and stype are 3-bit attributes. reserved is a 7-bit attribute
251
+ # define_bit_attr :flags, flag1: 1, flag2: 1, flag3: 1, type: 3, stype: 3, reserved: 7
260
252
  # 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:
253
+ # A bit attribute of size 1 bit defines 3 methods:
254
+ # * +#attr+ which returns an Integer,
255
+ # * +#attr?+ which returns a Boolean,
256
+ # * +#attr=+ which accepts an Integer or a Boolean.
257
+ # A bit attribute of more bits defines only 2 methods:
265
258
  # * +#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+
259
+ # * +#attr=+ which takes an Integer.
260
+ # @param [Symbol] attr attribute name
261
+ # @param [:big,:little,:native] endian endianess of Integer
262
+ # @param [Integer] default default value for whole attribute
263
+ # @param [Hash{Symbol=>Integer}] fields Hash defining fields. Keys are field names, values are field sizes.
272
264
  # @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
265
+ # @since 0.3.0
266
+ def define_bit_attr(attr, endian: :big, default: 0, **fields)
267
+ width = fields.reduce(0) { |acc, ary| acc + ary.last }
268
+ bit_attr_klass = BitAttr.create(width: width, endian: endian, **fields)
269
+ define_attr(attr, bit_attr_klass, default: default)
270
+ fields.each_key { |field| register_bit_attr_field(attr, field) }
271
+ bit_attr_klass.new.bit_methods.each do |meth|
272
+ if meth.to_s.end_with?('=')
273
+ define_method(meth) { |value| self[attr].send(meth, value) }
274
+ else
275
+ define_method(meth) { self[attr].send(meth) }
276
+ end
277
+ end
278
+ end
281
279
 
282
- until args.empty?
283
- arg = args.shift
284
- next unless arg.is_a? Symbol
280
+ # Define a bit attribute, before another attribute
281
+ # @param [Symbol,nil] other attribute name to create a new one before.
282
+ # If +nil+, new attribute is appended.
283
+ # @param [Symbol] name attribute name to create
284
+ # @param [:big,:little,:native] endian endianess of Integer
285
+ # @param [Hash{Symbol=>Integer}] fields Hash defining fields. Keys are field names, values are field sizes.
286
+ # @return [void]
287
+ # @since 0.3.0
288
+ # @see .define_bit_attr
289
+ def define_bit_attr_before(other, name, endian: :big, **fields)
290
+ define_bit_attr(name, endian: endian, **fields)
291
+ return if other.nil?
285
292
 
286
- size = size_from(args)
293
+ move_attr(name, before: other)
294
+ end
287
295
 
288
- unless attr == :_
289
- add_bit_methods(attr, arg, size, total_size, idx)
290
- register_bit_attr_size(attr, arg, size)
291
- end
296
+ # Define a bit attribute after another attribute
297
+ # @param [Symbol,nil] other attribute name to create a new one after.
298
+ # If +nil+, new attribute is appended.
299
+ # @param [Symbol] name attribute name to create
300
+ # @param [:big,:little,:native] endian endianess of Integer
301
+ # @param [Hash{Symbol=>Integer}] fields Hash defining fields. Keys are field names, values are field sizes.
302
+ # @return [void]
303
+ # @since 0.3.0
304
+ # @see .define_bit_attr
305
+ def define_bit_attr_after(other, name, endian: :big, **fields)
306
+ define_bit_attr(name, endian: endian, **fields)
307
+ return if other.nil?
292
308
 
293
- idx -= size
294
- end
309
+ move_attr(name, after: other)
295
310
  end
296
311
 
297
- # Remove all bit attributes defined on +attr+
298
- # @param [Symbol] attr attribute defining bit attributes
312
+ private
313
+
314
+ # @param [Symbol] name
315
+ # @param [Symbol,nil] before
316
+ # @param [Symbol,nil] after
299
317
  # @return [void]
300
- def remove_bit_attrs_on(attr)
301
- bits = bit_attrs.delete(attr)
302
- return if bits.nil?
318
+ # @raise [ArgumentError] Both +before+ and +after+ are nil, or both are set.
319
+ def move_attr(name, before: nil, after: nil)
320
+ move_check_destination(before, after)
303
321
 
304
- bits.each do |bit, size|
305
- undef_method :"#{bit}="
306
- undef_method(size == 1 ? "#{bit}?" : bit)
307
- end
322
+ other = before || after
323
+ attributes.delete(name)
324
+ idx = attributes.index(other)
325
+ raise ArgumentError, "unknown #{other} attribute" if idx.nil?
326
+
327
+ idx += 1 unless after.nil?
328
+ attributes[idx, 0] = name
308
329
  end
309
330
 
310
- private
331
+ def move_check_destination(before, after)
332
+ raise ArgumentError 'one of before: and after: arguments MUST be set' if before.nil? && after.nil?
333
+ raise ArgumentError 'only one of before and after argument MUST be set' if !before.nil? && !after.nil?
334
+ end
311
335
 
312
336
  def add_methods(name, type)
313
337
  define = []
@@ -328,73 +352,15 @@ module BinStruct
328
352
  class_eval define.join("\n")
329
353
  end
330
354
 
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
355
+ def register_bit_attr_field(attr, field)
356
+ bit_attrs[attr] ||= []
357
+ bit_attrs[attr] << field
384
358
  end
385
359
 
386
360
  def attr_defs_property_from(attr, property, options)
387
361
  attr_defs[attr].send(:"#{property}=", options.delete(property)) if options.key?(property)
388
362
  end
389
363
 
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
364
  def check_existence_of(attr)
399
365
  raise ArgumentError, "unknown #{attr} attribute for #{self}" unless attr_defs.key?(attr)
400
366
  end
@@ -402,7 +368,7 @@ module BinStruct
402
368
 
403
369
  # Create a new Struct object
404
370
  # @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}.
371
+ # attributes, as defined by {.define_attr} and by {.define_bit_attr}.
406
372
  def initialize(options = {})
407
373
  @attributes = {}
408
374
  @optional_attributes = {}
@@ -413,8 +379,8 @@ module BinStruct
413
379
  initialize_optional(attr)
414
380
  end
415
381
 
416
- self.class.bit_attrs.each_value do |hsh|
417
- hsh.each_key do |bit|
382
+ self.class.bit_attrs.each_value do |bit_fields|
383
+ bit_fields.each do |bit|
418
384
  send(:"#{bit}=", options[bit]) if options[bit]
419
385
  end
420
386
  end
@@ -435,6 +401,15 @@ module BinStruct
435
401
  @attributes[attr] = obj
436
402
  end
437
403
 
404
+ # Say if struct has given attribute
405
+ # @param [Symbol] attr attribute name
406
+ # @return [Boolean]
407
+ # @since 0.4.0
408
+ # @author LemonTree55
409
+ def attribute?(attr)
410
+ @attributes.key?(attr)
411
+ end
412
+
438
413
  # Get all attribute names
439
414
  # @return [Array<Symbol>]
440
415
  def attributes
@@ -599,26 +574,39 @@ module BinStruct
599
574
  end
600
575
  end
601
576
 
577
+ # @param [Symbol] attr
578
+ # @return [void]
602
579
  def initialize_optional(attr)
603
580
  optional = attr_defs[attr].optional
604
581
  @optional_attributes[attr] = optional if optional
605
582
  end
606
583
 
584
+ # @return [String]
607
585
  def inspect_titleize
608
586
  title = self.class.to_s
609
- +"-- #{title} #{'-' * (66 - title.length)}\n"
587
+ "-- #{title} #{'-' * (66 - title.length)}\n"
610
588
  end
611
589
 
590
+ # @param [:Symbol] attr
591
+ # @param [Structable] value
592
+ # @param [Integer] level
593
+ # @return [::String]
612
594
  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
595
  str = inspect_shift_level(level)
619
- str << (FMT_ATTR % [type, attr, value])
596
+ value_lines = value.format_inspect.split("\n")
597
+ str << (FMT_ATTR % [value.type_name, attr, value_lines.shift])
598
+ return str if value_lines.empty?
599
+
600
+ shift = (FMT_ATTR % ['', '', 'START']).index('START')
601
+ value_lines.each do |l|
602
+ str << inspect_shift_level(level)
603
+ str << (' ' * shift) << l << "\n"
604
+ end
605
+ str
620
606
  end
621
607
 
608
+ # @param [Integer] level
609
+ # @return [String]
622
610
  def inspect_shift_level(level = 1)
623
611
  ' ' * (level + 1)
624
612
  end
@@ -58,7 +58,7 @@ module BinStruct
58
58
  # Format object when inspecting a {Struct} object
59
59
  # @return [::String]
60
60
  def format_inspect
61
- to_human
61
+ to_human.to_s
62
62
  end
63
63
  end
64
64
  end
@@ -8,5 +8,5 @@
8
8
 
9
9
  module BinStruct
10
10
  # BinStruct version
11
- VERSION = '0.2.1'
11
+ VERSION = '0.4.0'
12
12
  end
data/lib/bin_struct.rb CHANGED
@@ -25,6 +25,7 @@ end
25
25
  require_relative 'bin_struct/structable'
26
26
  require_relative 'bin_struct/int'
27
27
  require_relative 'bin_struct/enum'
28
+ require_relative 'bin_struct/bit_attr'
28
29
  require_relative 'bin_struct/struct'
29
30
  require_relative 'bin_struct/length_from'
30
31
  require_relative 'bin_struct/abstract_tlv'
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: bin_struct
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.1
4
+ version: 0.4.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - LemonTree55
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2024-11-25 00:00:00.000000000 Z
11
+ date: 2025-02-13 00:00:00.000000000 Z
12
12
  dependencies: []
13
13
  description: 'BinStruct is a binary dissector and generator. It eases manipulating
14
14
  complex binary data.
@@ -26,6 +26,7 @@ files:
26
26
  - lib/bin_struct.rb
27
27
  - lib/bin_struct/abstract_tlv.rb
28
28
  - lib/bin_struct/array.rb
29
+ - lib/bin_struct/bit_attr.rb
29
30
  - lib/bin_struct/cstring.rb
30
31
  - lib/bin_struct/enum.rb
31
32
  - lib/bin_struct/int.rb
@@ -57,7 +58,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
57
58
  requirements:
58
59
  - - ">="
59
60
  - !ruby/object:Gem::Version
60
- version: 2.7.0
61
+ version: 3.0.0
61
62
  required_rubygems_version: !ruby/object:Gem::Requirement
62
63
  requirements:
63
64
  - - ">="