bin_struct 0.2.1 → 0.4.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 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
  - - ">="