iostruct 0.4.0 → 0.6.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: 0376aab4653f3f1fb656d82f679346bd1d04c6d8f80e17540a4a7fbb3bc470d7
4
- data.tar.gz: 211894d68015bc52658e8e818296a42c115bdeda8cfa2cb6525cadb1e9554e9b
3
+ metadata.gz: a36ab3a3a2de5e2cb4f94175231aa664be8cee2c5361d83cc2f62def71f2b067
4
+ data.tar.gz: 2604c514757df2287d4652c0929ebfc09b1d54cd281599b6e7f52fd2d7f270d8
5
5
  SHA512:
6
- metadata.gz: 79edc6088c3258506085cf22aee37e75386bab8309c478786ec3022f3895f34a4b05490640bca22db56b78242b500d7030d3fc6b141a24aa0f9cfc1681baef7d
7
- data.tar.gz: 36ddc86813c0c0fc92874025fbe37f755a8f1cef22533949b101428b50ca2a0369aae9beddfe99a4b00d71c0db5985ec4f89b5beddefcc262ea6c91dfd2787b7
6
+ metadata.gz: e99bb3cdd28c0f1283332470cf1e88aca1c425d79545a28d448bde831630e272dca6a5eda4d88ea869482e21c9d7e2adf80d61ee62661fa4d2a09420bad82f36
7
+ data.tar.gz: 4bf4896671e0a04675210468855b5efa5ec0df22378ac4bad1bef3a193080be8f908fc571bdcc3325c1dc4b7d5d50c10dd98ac213376a5f1d4ed396fdd85c247
data/.rubocop.yml ADDED
@@ -0,0 +1,68 @@
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/FormatString:
50
+ Enabled: false
51
+
52
+ Style/NumericLiterals:
53
+ Enabled: false
54
+
55
+ Style/NumericPredicate:
56
+ Enabled: false
57
+
58
+ Style/StringConcatenation:
59
+ Enabled: false
60
+
61
+ Style/StringLiterals:
62
+ Enabled: false
63
+
64
+ Style/TrailingCommaInArguments:
65
+ Enabled: false
66
+
67
+ Style/TrailingCommaInHashLiteral:
68
+ Enabled: false
data/.ruby-version ADDED
@@ -0,0 +1 @@
1
+ 3.4.5
data/CHANGELOG.md CHANGED
@@ -1,3 +1,47 @@
1
+ # 0.6.0
2
+
3
+ - added alternative hash-based struct definition with C type names:
4
+
5
+ ```ruby
6
+ Point = IOStruct.new(
7
+ struct_name: 'Point',
8
+ fields: {
9
+ x: 'int',
10
+ y: :int,
11
+ z: { type: :int, offset: 0x10 }, # explicit offset
12
+ }
13
+ )
14
+
15
+ # supports nested structs
16
+ Rect = IOStruct.new(fields: { topLeft: Point, bottomRight: Point })
17
+
18
+ # supports arrays
19
+ IOStruct.new(fields: { values: { type: 'int', count: 10 } })
20
+ ```
21
+
22
+ - added `pack` support for nested structs and arrays:
23
+
24
+ ```ruby
25
+ r = Rect.read(data)
26
+ r.pack # now works!
27
+ ```
28
+
29
+ - added `to_table` method with decimal formatting (`:inspect => :dec`)
30
+ - added `get_type_size` helper method
31
+ - deprecated `inspect_name_override` in favor of `struct_name`
32
+ - fixed `to_table` handling of unknown field types
33
+ - fixed `_BYTE` type alias (was incorrectly mapped to both signed and unsigned)
34
+ - fixed operator precedence bug in `format_integer` methods
35
+
36
+ # 0.5.0
37
+
38
+ - added `inspect_name_override` constructor param, useful for dynamic declarations:
39
+
40
+ ```ruby
41
+ IOStruct.new("NN").new.inspect # "<#<Class:0x000000011c45fa20> f0=nil f4=nil>"
42
+ IOStruct.new("NN", inspect_name_override: "Point").new.inspect # "<Point f0=nil f4=nil>"
43
+ ```
44
+
1
45
  # 0.4.0
2
46
 
3
47
  - added `size` class method that returns SIZE constant
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,28 +1,65 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- iostruct (0.3.0)
4
+ iostruct (0.6.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
+ rake (13.3.1)
22
+ regexp_parser (2.11.3)
23
+ rspec (3.13.2)
12
24
  rspec-core (~> 3.13.0)
13
25
  rspec-expectations (~> 3.13.0)
14
26
  rspec-mocks (~> 3.13.0)
15
- rspec-core (3.13.0)
27
+ rspec-core (3.13.6)
16
28
  rspec-support (~> 3.13.0)
17
- rspec-expectations (3.13.0)
29
+ rspec-expectations (3.13.5)
18
30
  diff-lcs (>= 1.2.0, < 2.0)
19
31
  rspec-support (~> 3.13.0)
20
- rspec-mocks (3.13.1)
32
+ rspec-mocks (3.13.7)
21
33
  diff-lcs (>= 1.2.0, < 2.0)
22
34
  rspec-support (~> 3.13.0)
23
- rspec-support (3.13.1)
35
+ rspec-support (3.13.6)
36
+ rubocop (1.84.0)
37
+ json (~> 2.3)
38
+ language_server-protocol (~> 3.17.0.2)
39
+ lint_roller (~> 1.1.0)
40
+ parallel (~> 1.10)
41
+ parser (>= 3.3.0.2)
42
+ rainbow (>= 2.2.2, < 4.0)
43
+ regexp_parser (>= 2.9.3, < 3.0)
44
+ rubocop-ast (>= 1.49.0, < 2.0)
45
+ ruby-progressbar (~> 1.7)
46
+ unicode-display_width (>= 2.4.0, < 4.0)
47
+ rubocop-ast (1.49.0)
48
+ parser (>= 3.3.7.2)
49
+ prism (~> 1.7)
50
+ rubocop-rake (0.7.1)
51
+ lint_roller (~> 1.1)
52
+ rubocop (>= 1.72.1)
53
+ rubocop-rspec (3.9.0)
54
+ lint_roller (~> 1.1)
55
+ rubocop (~> 1.81)
56
+ ruby-progressbar (1.13.0)
57
+ unicode-display_width (3.2.0)
58
+ unicode-emoji (~> 4.1)
59
+ unicode-emoji (4.2.0)
24
60
 
25
61
  PLATFORMS
62
+ arm64-darwin-24
26
63
  ruby
27
64
 
28
65
  DEPENDENCIES
@@ -30,6 +67,9 @@ DEPENDENCIES
30
67
  iostruct!
31
68
  rake
32
69
  rspec
70
+ rubocop
71
+ rubocop-rake
72
+ rubocop-rspec
33
73
 
34
74
  BUNDLED WITH
35
- 2.5.6
75
+ 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,92 @@
1
+ # frozen_string_literal: true
2
+
3
+ module IOStruct
4
+ module HashFmt
5
+ KNOWN_FIELD_TYPES_REVERSED = {
6
+ 'C' => ['uint8_t', 'unsigned char', '_BYTE'],
7
+ 'S' => ['uint16_t', 'unsigned short'],
8
+ 'I' => ['uint32_t', 'unsigned', 'unsigned int'],
9
+ 'L' => ['unsigned long'],
10
+ 'Q' => ['uint64_t', 'unsigned long long'],
11
+
12
+ 'c' => ['int8_t', 'char', 'signed char'],
13
+ 's' => ['int16_t', 'short', 'signed short'],
14
+ 'i' => ['int32_t', 'int', 'signed', 'signed int'],
15
+ 'l' => ['long', 'signed long'],
16
+ 'q' => ['int64_t', 'long long', 'signed long long'],
17
+
18
+ 'd' => ['double'],
19
+ 'f' => ['float'],
20
+ }.freeze
21
+
22
+ KNOWN_FIELD_TYPES = KNOWN_FIELD_TYPES_REVERSED.map { |t, a| a.map { |v| [v, t] } }.flatten.each_slice(2).to_h
23
+
24
+ # for external use
25
+ def get_type_size(typename)
26
+ type_code = KNOWN_FIELD_TYPES[typename.to_s]
27
+ f_size, = PackFmt::FMTSPEC[type_code]
28
+ f_size
29
+ end
30
+
31
+ private
32
+
33
+ def parse_hash_format(fields:, size: nil, name: nil)
34
+ struct_name = name
35
+ offset = 0
36
+ names = []
37
+ fmt_arr = []
38
+ finfos = fields.map do |f_name, type|
39
+ f_offset = offset
40
+ f_count = 1
41
+ klass = f_fmt = type_code = nil
42
+
43
+ if type.is_a?(Hash)
44
+ f_offset = type.fetch(:offset, offset)
45
+ if f_offset > offset
46
+ fmt_arr << "x#{f_offset - offset}"
47
+ elsif f_offset < offset
48
+ raise "#{struct_name}: field #{f_name.inspect} overlaps previous field"
49
+ end
50
+ f_count = type.fetch(:count, f_count)
51
+ type = type[:type]
52
+ end
53
+
54
+ case type
55
+ when String
56
+ # noop
57
+ when Symbol
58
+ type = type.to_s
59
+ when Class
60
+ klass = type
61
+ f_size = klass.size
62
+ f_fmt = klass
63
+ type_code = "a#{f_size}"
64
+ else
65
+ raise "#{f_name}: unexpected field desc type #{type.class}"
66
+ end
67
+
68
+ unless type_code
69
+ type_code = KNOWN_FIELD_TYPES[type]
70
+ raise "#{f_name}: unknown field type #{type.inspect}" unless type_code
71
+
72
+ f_size, klass = PackFmt::FMTSPEC[type_code] || raise("Unknown field type code #{type_code.inspect}")
73
+ end
74
+
75
+ if f_count != 1
76
+ f_fmt = "#{type_code}#{f_count}"
77
+ f_size *= f_count
78
+ type_code = "a#{f_size}"
79
+ end
80
+
81
+ offset = f_offset + f_size
82
+ fmt_arr << type_code
83
+ names << f_name
84
+
85
+ FieldInfo.new(klass, f_size, f_offset, f_count, f_fmt)
86
+ end
87
+ raise "#{struct_name}: actual struct size #{offset} is greater than forced size #{size}: #{fields.inspect}" if size && offset > size
88
+
89
+ [fmt_arr.join, names, finfos, size || offset]
90
+ end
91
+ end
92
+ end