iostruct 0.5.0 → 0.7.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: 1f676ed52c2ea4738cc80dc0c6634ee50d9b4ef844107caad6f15937f70bca6f
4
- data.tar.gz: b66779b78bc9abb15007089fb4cb4320324b6efa07e92d4df39b82fb16717bbb
3
+ metadata.gz: f3b93bdd7f541a9cec7f68169c9927e989203b38cea668441523c499e3b62562
4
+ data.tar.gz: ee73bec71e144d684769c6238718e82cab1f2b526543721bfc2fcfe5e979bcc3
5
5
  SHA512:
6
- metadata.gz: ee0f456796c677b2d9bd9638b993d88ff828e84453ad8b89dc2b4f7d365cb8c6ab3b34156de415c47294ee88541f2b87157f04526c04930171ed211d51371709
7
- data.tar.gz: 914f2243b76864fa1dbc476528512b1827a690fdff29b4981157860c2311e8f0caa31b4cfe5f102dff656fd2ccbd7f0ffa04b22617270726646cbd4a6eb4585a
6
+ metadata.gz: b910103dc94c253a01cf0f82a94d2359bed9489ceb829621fc6acdd04fdc140188dbb80c3cf58d9de06a289ec27479333b43b2e14f0c0028b31d6f94050c37eb
7
+ data.tar.gz: dbecaa47ac2e8077b8b2e23818add7ccf2142e0349cf482aa4e8108f03fa19a512c1acb30eceb94d18fc736ce5cf9516dd480ab49b1b1f59089d6714ddd1622f
data/.rubocop.yml ADDED
@@ -0,0 +1,71 @@
1
+ plugins:
2
+ - rubocop-rake
3
+ - rubocop-rspec
4
+
5
+
6
+ AllCops:
7
+ NewCops: enable
8
+
9
+
10
+ Gemspec/RequiredRubyVersion:
11
+ Enabled: false
12
+
13
+
14
+ Layout/LineLength:
15
+ Max: 140
16
+
17
+ Layout/SpaceInsideArrayLiteralBrackets:
18
+ Enabled: false
19
+
20
+ Layout/SpaceInsideParens:
21
+ Enabled: false
22
+
23
+
24
+ Metrics/AbcSize:
25
+ Enabled: false
26
+
27
+ Metrics/CyclomaticComplexity:
28
+ Enabled: false
29
+
30
+ Metrics/BlockLength:
31
+ Max: 50
32
+
33
+ Metrics/MethodLength:
34
+ Max: 55
35
+
36
+ Metrics/ParameterLists:
37
+ Enabled: false
38
+
39
+ Metrics/PerceivedComplexity:
40
+ Max: 18
41
+
42
+
43
+ Style/CommentedKeyword:
44
+ Enabled: false
45
+
46
+ Style/Documentation:
47
+ Enabled: false
48
+
49
+ Style/EmptyElse:
50
+ Enabled: false
51
+
52
+ Style/FormatString:
53
+ Enabled: false
54
+
55
+ Style/NumericLiterals:
56
+ Enabled: false
57
+
58
+ Style/NumericPredicate:
59
+ Enabled: false
60
+
61
+ Style/StringConcatenation:
62
+ Enabled: false
63
+
64
+ Style/StringLiterals:
65
+ Enabled: false
66
+
67
+ Style/TrailingCommaInArguments:
68
+ Enabled: false
69
+
70
+ Style/TrailingCommaInHashLiteral:
71
+ Enabled: false
data/.ruby-version ADDED
@@ -0,0 +1 @@
1
+ 3.4.5
data/CHANGELOG.md CHANGED
@@ -1,3 +1,66 @@
1
+ # 0.7.0
2
+
3
+ - added big-endian and little-endian type support in hash format:
4
+
5
+ ```ruby
6
+ IOStruct.new(fields: {
7
+ be_val: 'uint16_be', # or 'be16', 'uint16_t_be'
8
+ le_val: 'uint32_le', # or 'le32', 'uint32_t_le'
9
+ })
10
+ ```
11
+
12
+ - added nested struct arrays:
13
+
14
+ ```ruby
15
+ Point = IOStruct.new(fields: { x: 'int', y: 'int' })
16
+ Polygon = IOStruct.new(fields: {
17
+ num_points: 'int',
18
+ points: { type: Point, count: 3 }, # array of 3 nested structs
19
+ })
20
+ p = Polygon.read(data)
21
+ p.points[0].x # access nested struct in array
22
+ p.pack # packing works too!
23
+ ```
24
+
25
+ - added endian-specific float types: `float_le`, `float_be`, `double_le`, `double_be`
26
+ - improved class name handling in inspect for subclasses
27
+ - `DecInspect` now defines `to_s` for consistent behavior with `HexInspect`
28
+
29
+ # 0.6.0
30
+
31
+ - added alternative hash-based struct definition with C type names:
32
+
33
+ ```ruby
34
+ Point = IOStruct.new(
35
+ struct_name: 'Point',
36
+ fields: {
37
+ x: 'int',
38
+ y: :int,
39
+ z: { type: :int, offset: 0x10 }, # explicit offset
40
+ }
41
+ )
42
+
43
+ # supports nested structs
44
+ Rect = IOStruct.new(fields: { topLeft: Point, bottomRight: Point })
45
+
46
+ # supports arrays
47
+ IOStruct.new(fields: { values: { type: 'int', count: 10 } })
48
+ ```
49
+
50
+ - added `pack` support for nested structs and arrays:
51
+
52
+ ```ruby
53
+ r = Rect.read(data)
54
+ r.pack # now works!
55
+ ```
56
+
57
+ - added `to_table` method with decimal formatting (`:inspect => :dec`)
58
+ - added `get_type_size` helper method
59
+ - deprecated `inspect_name_override` in favor of `struct_name`
60
+ - fixed `to_table` handling of unknown field types
61
+ - fixed `_BYTE` type alias (was incorrectly mapped to both signed and unsigned)
62
+ - fixed operator precedence bug in `format_integer` methods
63
+
1
64
  # 0.5.0
2
65
 
3
66
  - added `inspect_name_override` constructor param, useful for dynamic declarations:
data/Gemfile CHANGED
@@ -1,4 +1,13 @@
1
+ # frozen_string_literal: true
2
+
1
3
  source 'https://rubygems.org'
2
4
 
3
5
  # Specify your gem's dependencies in iostruct.gemspec
4
6
  gemspec
7
+
8
+ group :development, :test do
9
+ gem 'rspec', require: false
10
+ gem 'rubocop', require: false
11
+ gem 'rubocop-rake', require: false
12
+ gem 'rubocop-rspec', require: false
13
+ end
data/Gemfile.lock CHANGED
@@ -1,35 +1,72 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- iostruct (0.3.0)
4
+ iostruct (0.7.0)
5
5
 
6
6
  GEM
7
7
  remote: https://rubygems.org/
8
8
  specs:
9
- diff-lcs (1.5.1)
10
- rake (13.2.1)
11
- rspec (3.13.0)
9
+ ast (2.4.3)
10
+ diff-lcs (1.6.2)
11
+ json (2.18.0)
12
+ language_server-protocol (3.17.0.5)
13
+ lint_roller (1.1.0)
14
+ parallel (1.27.0)
15
+ parser (3.3.10.1)
16
+ ast (~> 2.4.1)
17
+ racc
18
+ prism (1.8.0)
19
+ racc (1.8.1)
20
+ rainbow (3.1.1)
21
+ regexp_parser (2.11.3)
22
+ rspec (3.13.2)
12
23
  rspec-core (~> 3.13.0)
13
24
  rspec-expectations (~> 3.13.0)
14
25
  rspec-mocks (~> 3.13.0)
15
- rspec-core (3.13.0)
26
+ rspec-core (3.13.6)
16
27
  rspec-support (~> 3.13.0)
17
- rspec-expectations (3.13.0)
28
+ rspec-expectations (3.13.5)
18
29
  diff-lcs (>= 1.2.0, < 2.0)
19
30
  rspec-support (~> 3.13.0)
20
- rspec-mocks (3.13.1)
31
+ rspec-mocks (3.13.7)
21
32
  diff-lcs (>= 1.2.0, < 2.0)
22
33
  rspec-support (~> 3.13.0)
23
- rspec-support (3.13.1)
34
+ rspec-support (3.13.6)
35
+ rubocop (1.84.0)
36
+ json (~> 2.3)
37
+ language_server-protocol (~> 3.17.0.2)
38
+ lint_roller (~> 1.1.0)
39
+ parallel (~> 1.10)
40
+ parser (>= 3.3.0.2)
41
+ rainbow (>= 2.2.2, < 4.0)
42
+ regexp_parser (>= 2.9.3, < 3.0)
43
+ rubocop-ast (>= 1.49.0, < 2.0)
44
+ ruby-progressbar (~> 1.7)
45
+ unicode-display_width (>= 2.4.0, < 4.0)
46
+ rubocop-ast (1.49.0)
47
+ parser (>= 3.3.7.2)
48
+ prism (~> 1.7)
49
+ rubocop-rake (0.7.1)
50
+ lint_roller (~> 1.1)
51
+ rubocop (>= 1.72.1)
52
+ rubocop-rspec (3.9.0)
53
+ lint_roller (~> 1.1)
54
+ rubocop (~> 1.81)
55
+ ruby-progressbar (1.13.0)
56
+ unicode-display_width (3.2.0)
57
+ unicode-emoji (~> 4.1)
58
+ unicode-emoji (4.2.0)
24
59
 
25
60
  PLATFORMS
61
+ arm64-darwin-24
26
62
  ruby
27
63
 
28
64
  DEPENDENCIES
29
- bundler
30
65
  iostruct!
31
- rake
32
66
  rspec
67
+ rubocop
68
+ rubocop-rake
69
+ rubocop-rspec
33
70
 
34
71
  BUNDLED WITH
35
- 2.5.6
72
+ 2.6.9
data/README.md CHANGED
@@ -1,24 +1,261 @@
1
- # Iostruct
1
+ # IOStruct
2
2
 
3
- TODO: Write a gem description
3
+ A Ruby Struct that can read/write itself from/to IO-like objects. Perfect for parsing binary file formats, network protocols, and other structured binary data.
4
4
 
5
5
  ## Installation
6
6
 
7
7
  Add this line to your application's Gemfile:
8
8
 
9
- gem 'iostruct'
9
+ ```ruby
10
+ gem 'iostruct'
11
+ ```
10
12
 
11
13
  And then execute:
12
14
 
13
- $ bundle
15
+ ```bash
16
+ $ bundle install
17
+ ```
14
18
 
15
19
  Or install it yourself as:
16
20
 
17
- $ gem install iostruct
21
+ ```bash
22
+ $ gem install iostruct
23
+ ```
18
24
 
19
25
  ## Usage
20
26
 
21
- TODO: Write usage instructions here
27
+ ### Basic Usage with Pack Format
28
+
29
+ Define structs using Ruby's [pack/unpack format strings](https://ruby-doc.org/core/String.html#method-i-unpack):
30
+
31
+ ```ruby
32
+ require 'iostruct'
33
+
34
+ # Define a struct with two 32-bit unsigned integers
35
+ Point = IOStruct.new('LL', :x, :y)
36
+
37
+ # Read from binary data
38
+ data = [100, 200].pack('LL')
39
+ point = Point.read(data)
40
+ point.x # => 100
41
+ point.y # => 200
42
+
43
+ # Write back to binary
44
+ point.pack # => "\x64\x00\x00\x00\xC8\x00\x00\x00"
45
+ ```
46
+
47
+ ### Hash-Based Definition with C Types
48
+
49
+ For more readable code, define structs using C-style type names:
50
+
51
+ ```ruby
52
+ Point = IOStruct.new(
53
+ struct_name: 'Point',
54
+ fields: {
55
+ x: 'int',
56
+ y: 'int',
57
+ z: 'int',
58
+ }
59
+ )
60
+
61
+ point = Point.read(binary_data)
62
+ point.inspect # => "<Point x=0x64 y=0xc8 z=0x0>"
63
+ ```
64
+
65
+ #### Supported C Types
66
+
67
+ | Type | Aliases | Size |
68
+ |------|---------|------|
69
+ | `uint8_t` | `unsigned char`, `_BYTE` | 1 |
70
+ | `uint16_t` | `unsigned short` | 2 |
71
+ | `uint32_t` | `unsigned int`, `unsigned` | 4 |
72
+ | `uint64_t` | `unsigned long long` | 8 |
73
+ | `int8_t` | `char`, `signed char` | 1 |
74
+ | `int16_t` | `short`, `signed short` | 2 |
75
+ | `int32_t` | `int`, `signed int`, `signed` | 4 |
76
+ | `int64_t` | `long long`, `signed long long` | 8 |
77
+ | `float` | | 4 |
78
+ | `double` | | 8 |
79
+
80
+ ### Reading from IO or String
81
+
82
+ ```ruby
83
+ Header = IOStruct.new('L S S', :magic, :version, :flags)
84
+
85
+ # Read from a File
86
+ File.open('binary_file', 'rb') do |f|
87
+ header = Header.read(f)
88
+ puts header.magic
89
+ end
90
+
91
+ # Read from a String
92
+ header = Header.read("\x7fELF\x01\x00\x00\x00")
93
+
94
+ # Track file position with __offset
95
+ io = StringIO.new(data)
96
+ record = MyStruct.read(io)
97
+ record.__offset # => position where the record was read from
98
+ ```
99
+
100
+ ### Explicit Field Offsets
101
+
102
+ Specify exact byte offsets for fields (useful for structs with padding or gaps):
103
+
104
+ ```ruby
105
+ MyStruct = IOStruct.new(
106
+ fields: {
107
+ magic: 'uint32_t',
108
+ flags: { type: 'uint16_t', offset: 0x10 }, # starts at byte 16
109
+ data: { type: 'uint32_t', offset: 0x20 }, # starts at byte 32
110
+ }
111
+ )
112
+ ```
113
+
114
+ ### Arrays
115
+
116
+ Define fixed-size arrays within structs:
117
+
118
+ ```ruby
119
+ Matrix = IOStruct.new(
120
+ fields: {
121
+ rows: 'int',
122
+ cols: 'int',
123
+ data: { type: 'float', count: 16 }, # 16-element float array
124
+ }
125
+ )
126
+
127
+ m = Matrix.read(binary_data)
128
+ m.data # => [1.0, 2.0, 3.0, ...]
129
+ m.pack # serializes back to binary
130
+ ```
131
+
132
+ ### Nested Structs
133
+
134
+ Compose complex structures from simpler ones:
135
+
136
+ ```ruby
137
+ Point = IOStruct.new(fields: { x: 'int', y: 'int' })
138
+
139
+ Rect = IOStruct.new(
140
+ struct_name: 'Rect',
141
+ fields: {
142
+ top_left: Point,
143
+ bottom_right: Point,
144
+ }
145
+ )
146
+
147
+ rect = Rect.read([0, 0, 100, 100].pack('i*'))
148
+ rect.top_left.x # => 0
149
+ rect.bottom_right.x # => 100
150
+ rect.pack # serializes entire structure including nested structs
151
+ ```
152
+
153
+ ### Inspect Modes
154
+
155
+ Choose between hexadecimal (default) or decimal display:
156
+
157
+ ```ruby
158
+ # Hex display (default)
159
+ HexStruct = IOStruct.new('L L', :a, :b, inspect: :hex)
160
+ HexStruct.new(a: 255, b: 256).inspect
161
+ # => "<struct a=0xff b=0x100>"
162
+
163
+ # Decimal display
164
+ DecStruct = IOStruct.new('L L', :a, :b, inspect: :dec)
165
+ DecStruct.new(a: 255, b: 256).inspect
166
+ # => "#<struct DecStruct a=255, b=256>"
167
+
168
+ # Table format for aligned output
169
+ struct.to_table
170
+ # => "<struct a= ff b= 100>"
171
+ ```
172
+
173
+ ### Auto-Generated Field Names
174
+
175
+ If you don't specify field names, they're generated based on byte offset:
176
+
177
+ ```ruby
178
+ s = IOStruct.new('C S L')
179
+ s.members # => [:f0, :f1, :f3] (offsets 0, 1, 3)
180
+ ```
181
+
182
+ ### Field Renaming
183
+
184
+ Rename auto-generated or explicit field names:
185
+
186
+ ```ruby
187
+ # Rename auto-generated names
188
+ IOStruct.new('C S L', f0: :byte_val, f3: :long_val)
189
+
190
+ # Rename explicit names
191
+ IOStruct.new('C S L', :a, :b, :c, a: :first, c: :last)
192
+ ```
193
+
194
+ ## API Reference
195
+
196
+ ### Class Methods
197
+
198
+ | Method | Description |
199
+ |--------|-------------|
200
+ | `IOStruct.new(fmt, *names, **options)` | Create a new struct class with pack format |
201
+ | `IOStruct.new(fields:, **options)` | Create a new struct class with hash definition |
202
+ | `IOStruct.get_type_size(typename)` | Get byte size for a C type name |
203
+ | `MyStruct.read(io_or_string)` | Read and parse binary data |
204
+ | `MyStruct.size` | Return struct size in bytes |
205
+ | `MyStruct::SIZE` | Struct size constant |
206
+ | `MyStruct::FORMAT` | Pack format string |
207
+ | `MyStruct::FIELDS` | Hash of field names to FieldInfo |
208
+
209
+ ### Instance Methods
210
+
211
+ | Method | Description |
212
+ |--------|-------------|
213
+ | `#pack` | Serialize to binary string |
214
+ | `#empty?` | True if all fields are zero/nil/empty |
215
+ | `#to_table` | Formatted string with aligned values |
216
+ | `#__offset` | File position where struct was read (nil if from string) |
217
+
218
+ ### Constructor Options
219
+
220
+ | Option | Description |
221
+ |--------|-------------|
222
+ | `struct_name:` | Custom name for inspect output |
223
+ | `inspect:` | `:hex` (default) or `:dec` for display format |
224
+ | `size:` | Override calculated struct size |
225
+ | `fields:` | Hash defining fields (for hash-based definition) |
226
+
227
+ ## Examples
228
+
229
+ ### Parsing a BMP File Header
230
+
231
+ ```ruby
232
+ BMPHeader = IOStruct.new(
233
+ struct_name: 'BMPHeader',
234
+ fields: {
235
+ magic: { type: 'uint16_t' },
236
+ file_size: { type: 'uint32_t' },
237
+ reserved: { type: 'uint32_t' },
238
+ data_offset: { type: 'uint32_t' },
239
+ }
240
+ )
241
+
242
+ File.open('image.bmp', 'rb') do |f|
243
+ header = BMPHeader.read(f)
244
+ puts "File size: #{header.file_size} bytes"
245
+ puts "Pixel data starts at: #{header.data_offset}"
246
+ end
247
+ ```
248
+
249
+ ### Network Protocol Packet
250
+
251
+ ```ruby
252
+ Packet = IOStruct.new('n n N', :src_port, :dst_port, :sequence,
253
+ struct_name: 'TCPHeader'
254
+ )
255
+
256
+ # Big-endian format for network byte order
257
+ packet = Packet.read(socket.read(8))
258
+ ```
22
259
 
23
260
  ## Contributing
24
261
 
@@ -27,3 +264,7 @@ TODO: Write usage instructions here
27
264
  3. Commit your changes (`git commit -am 'Add some feature'`)
28
265
  4. Push to the branch (`git push origin my-new-feature`)
29
266
  5. Create new Pull Request
267
+
268
+ ## License
269
+
270
+ MIT License - see [LICENSE.txt](LICENSE.txt) for details.
data/Rakefile CHANGED
@@ -1,7 +1,9 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require "bundler/gem_tasks"
2
4
  require 'rspec/core/rake_task'
3
5
 
4
6
  desc "run specs"
5
7
  RSpec::Core::RakeTask.new
6
8
 
7
- task :default => :spec
9
+ task default: :spec
data/iostruct.gemspec CHANGED
@@ -1,5 +1,7 @@
1
- # coding: utf-8
2
- lib = File.expand_path('../lib', __FILE__)
1
+ # frozen_string_literal: true
2
+
3
+ require 'English'
4
+ lib = File.expand_path('lib', __dir__)
3
5
  $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
6
  require 'iostruct/version'
5
7
 
@@ -9,16 +11,13 @@ Gem::Specification.new do |s|
9
11
 
10
12
  s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
11
13
  s.authors = ["Andrey \"Zed\" Zaikin"]
12
- s.date = "2013-01-08"
13
14
  s.email = "zed.0xff@gmail.com"
14
- s.files = `git ls-files`.split($/)
15
+ s.files = `git ls-files`.split($INPUT_RECORD_SEPARATOR)
15
16
  s.homepage = "http://github.com/zed-0xff/iostruct"
16
17
  s.licenses = ["MIT"]
17
- s.require_paths = ["lib"]
18
18
  s.summary = "A Struct that can read/write itself from/to IO-like objects"
19
19
 
20
- s.add_development_dependency "bundler"
21
- s.add_development_dependency "rake"
22
- s.add_development_dependency "rspec"
23
- end
20
+ s.require_paths = ["lib"]
24
21
 
22
+ s.metadata['rubygems_mfa_required'] = 'true'
23
+ end
@@ -0,0 +1,112 @@
1
+ # frozen_string_literal: true
2
+
3
+ module IOStruct
4
+ module HashFmt
5
+ # rubocop:disable Style/WordArray
6
+ KNOWN_FIELD_TYPES_REVERSED = {
7
+ 'C' => ['uint8_t', 'unsigned char', '_BYTE'],
8
+ 'S' => ['uint16_t', 'unsigned short'],
9
+ 'I' => ['uint32_t', 'unsigned', 'unsigned int'],
10
+ 'L' => ['unsigned long'],
11
+ 'Q' => ['uint64_t', 'unsigned long long'],
12
+
13
+ 'c' => ['int8_t', 'char', 'signed char'],
14
+ 's' => ['int16_t', 'short', 'signed short'],
15
+ 'i' => ['int32_t', 'int', 'signed', 'signed int'],
16
+ 'l' => ['long', 'signed long'],
17
+ 'q' => ['int64_t', 'long long', 'signed long long'],
18
+
19
+ # Big-endian (network byte order)
20
+ 'n' => ['uint16_be', 'uint16_t_be', 'be16'],
21
+ 'N' => ['uint32_be', 'uint32_t_be', 'be32'],
22
+
23
+ # Little-endian (VAX byte order)
24
+ 'v' => ['uint16_le', 'uint16_t_le', 'le16'],
25
+ 'V' => ['uint32_le', 'uint32_t_le', 'le32'],
26
+
27
+ # Floats
28
+ 'd' => ['double'],
29
+ 'f' => ['float'],
30
+ 'E' => ['double_le'], # double-precision, little-endian
31
+ 'e' => ['float_le'], # single-precision, little-endian
32
+ 'G' => ['double_be'], # double-precision, big-endian
33
+ 'g' => ['float_be'], # single-precision, big-endian
34
+ }.freeze
35
+ # rubocop:enable Style/WordArray
36
+
37
+ KNOWN_FIELD_TYPES = KNOWN_FIELD_TYPES_REVERSED.map { |t, a| a.map { |v| [v, t] } }.flatten.each_slice(2).to_h
38
+
39
+ # for external use
40
+ def get_type_size(typename)
41
+ type_code = KNOWN_FIELD_TYPES[typename.to_s]
42
+ f_size, = PackFmt::FMTSPEC[type_code]
43
+ f_size
44
+ end
45
+
46
+ private
47
+
48
+ def parse_hash_format(fields:, size: nil, name: nil)
49
+ struct_name = name
50
+ offset = 0
51
+ names = []
52
+ fmt_arr = []
53
+ finfos = fields.map do |f_name, type|
54
+ f_offset = offset
55
+ f_count = 1
56
+ klass = f_fmt = type_code = nil
57
+
58
+ if type.is_a?(Hash)
59
+ f_offset = type.fetch(:offset, offset)
60
+ if f_offset > offset
61
+ fmt_arr << "x#{f_offset - offset}"
62
+ elsif f_offset < offset
63
+ raise "#{struct_name}: field #{f_name.inspect} overlaps previous field"
64
+ end
65
+ f_count = type.fetch(:count, f_count)
66
+ type = type[:type]
67
+ end
68
+
69
+ case type
70
+ when String
71
+ # noop
72
+ when Symbol
73
+ type = type.to_s
74
+ when Class
75
+ klass = type
76
+ f_size = klass.size
77
+ f_fmt = klass
78
+ type_code = "a#{f_size}"
79
+ else
80
+ raise "#{f_name}: unexpected field desc type #{type.class}"
81
+ end
82
+
83
+ unless type_code
84
+ type_code = KNOWN_FIELD_TYPES[type]
85
+ raise "#{f_name}: unknown field type #{type.inspect}" unless type_code
86
+
87
+ f_size, klass = PackFmt::FMTSPEC[type_code] || raise("Unknown field type code #{type_code.inspect}")
88
+ end
89
+
90
+ if f_count != 1
91
+ if f_fmt.is_a?(Class)
92
+ # Nested struct array: keep f_fmt as the Class, just update size and type_code
93
+ else
94
+ # Primitive array: set f_fmt to pack format string (e.g., "i3")
95
+ f_fmt = "#{type_code}#{f_count}"
96
+ end
97
+ f_size *= f_count
98
+ type_code = "a#{f_size}"
99
+ end
100
+
101
+ offset = f_offset + f_size
102
+ fmt_arr << type_code
103
+ names << f_name
104
+
105
+ FieldInfo.new(klass, f_size, f_offset, f_count, f_fmt)
106
+ end
107
+ raise "#{struct_name}: actual struct size #{offset} is greater than forced size #{size}: #{fields.inspect}" if size && offset > size
108
+
109
+ [fmt_arr.join, names, finfos, size || offset]
110
+ end
111
+ end
112
+ end