iostruct 0.6.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: a36ab3a3a2de5e2cb4f94175231aa664be8cee2c5361d83cc2f62def71f2b067
4
- data.tar.gz: 2604c514757df2287d4652c0929ebfc09b1d54cd281599b6e7f52fd2d7f270d8
3
+ metadata.gz: f3b93bdd7f541a9cec7f68169c9927e989203b38cea668441523c499e3b62562
4
+ data.tar.gz: ee73bec71e144d684769c6238718e82cab1f2b526543721bfc2fcfe5e979bcc3
5
5
  SHA512:
6
- metadata.gz: e99bb3cdd28c0f1283332470cf1e88aca1c425d79545a28d448bde831630e272dca6a5eda4d88ea869482e21c9d7e2adf80d61ee62661fa4d2a09420bad82f36
7
- data.tar.gz: 4bf4896671e0a04675210468855b5efa5ec0df22378ac4bad1bef3a193080be8f908fc571bdcc3325c1dc4b7d5d50c10dd98ac213376a5f1d4ed396fdd85c247
6
+ metadata.gz: b910103dc94c253a01cf0f82a94d2359bed9489ceb829621fc6acdd04fdc140188dbb80c3cf58d9de06a289ec27479333b43b2e14f0c0028b31d6f94050c37eb
7
+ data.tar.gz: dbecaa47ac2e8077b8b2e23818add7ccf2142e0349cf482aa4e8108f03fa19a512c1acb30eceb94d18fc736ce5cf9516dd480ab49b1b1f59089d6714ddd1622f
data/.rubocop.yml CHANGED
@@ -46,6 +46,9 @@ Style/CommentedKeyword:
46
46
  Style/Documentation:
47
47
  Enabled: false
48
48
 
49
+ Style/EmptyElse:
50
+ Enabled: false
51
+
49
52
  Style/FormatString:
50
53
  Enabled: false
51
54
 
data/CHANGELOG.md CHANGED
@@ -1,3 +1,31 @@
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
+
1
29
  # 0.6.0
2
30
 
3
31
  - added alternative hash-based struct definition with C type names:
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- iostruct (0.6.0)
4
+ iostruct (0.7.0)
5
5
 
6
6
  GEM
7
7
  remote: https://rubygems.org/
@@ -18,7 +18,6 @@ GEM
18
18
  prism (1.8.0)
19
19
  racc (1.8.1)
20
20
  rainbow (3.1.1)
21
- rake (13.3.1)
22
21
  regexp_parser (2.11.3)
23
22
  rspec (3.13.2)
24
23
  rspec-core (~> 3.13.0)
@@ -63,9 +62,7 @@ PLATFORMS
63
62
  ruby
64
63
 
65
64
  DEPENDENCIES
66
- bundler
67
65
  iostruct!
68
- rake
69
66
  rspec
70
67
  rubocop
71
68
  rubocop-rake
@@ -2,6 +2,7 @@
2
2
 
3
3
  module IOStruct
4
4
  module HashFmt
5
+ # rubocop:disable Style/WordArray
5
6
  KNOWN_FIELD_TYPES_REVERSED = {
6
7
  'C' => ['uint8_t', 'unsigned char', '_BYTE'],
7
8
  'S' => ['uint16_t', 'unsigned short'],
@@ -15,9 +16,23 @@ module IOStruct
15
16
  'l' => ['long', 'signed long'],
16
17
  'q' => ['int64_t', 'long long', 'signed long long'],
17
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
18
28
  'd' => ['double'],
19
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
20
34
  }.freeze
35
+ # rubocop:enable Style/WordArray
21
36
 
22
37
  KNOWN_FIELD_TYPES = KNOWN_FIELD_TYPES_REVERSED.map { |t, a| a.map { |v| [v, t] } }.flatten.each_slice(2).to_h
23
38
 
@@ -73,7 +88,12 @@ module IOStruct
73
88
  end
74
89
 
75
90
  if f_count != 1
76
- f_fmt = "#{type_code}#{f_count}"
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
77
97
  f_size *= f_count
78
98
  type_code = "a#{f_size}"
79
99
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module IOStruct
4
- VERSION = "0.6.0"
4
+ VERSION = "0.7.0"
5
5
  end
data/lib/iostruct.rb CHANGED
@@ -35,10 +35,13 @@ module IOStruct
35
35
  extend ClassMethods
36
36
  include InstanceMethods
37
37
  include NestedInstanceMethods if finfos.any?(&:fmt)
38
- if inspect == :hex
38
+ case inspect
39
+ when :hex
39
40
  include HexInspect
40
- else
41
+ when :dec
41
42
  include DecInspect
43
+ else
44
+ # ruby default inspect
42
45
  end
43
46
  define_singleton_method(:to_s) { struct_name } if struct_name
44
47
  define_singleton_method(:name) { struct_name } if struct_name
@@ -55,6 +58,10 @@ module IOStruct
55
58
  names
56
59
  end
57
60
 
61
+ def self.get_name(klass)
62
+ (klass.respond_to?(:name) && klass.name) || 'struct'
63
+ end
64
+
58
65
  module ClassMethods
59
66
  # src can be IO or String, or anything that responds to :read or :unpack
60
67
  def read(src, size = nil)
@@ -72,10 +79,6 @@ module IOStruct
72
79
  new(*data.unpack(const_get('FORMAT'))).tap { |x| x.__offset = pos }
73
80
  end
74
81
 
75
- def name
76
- 'struct'
77
- end
78
-
79
82
  def size
80
83
  self::SIZE
81
84
  end
@@ -115,10 +118,20 @@ module IOStruct
115
118
  super
116
119
  self.class::FIELDS.each do |k, v|
117
120
  next unless v.fmt
118
-
119
- if (value = self[k])
120
- self[k] = v.fmt.is_a?(String) ? value.unpack(v.fmt) : v.fmt.read(value)
121
- end
121
+ next unless (value = self[k])
122
+
123
+ self[k] =
124
+ if v.fmt.is_a?(String)
125
+ # Primitive array (e.g., "i3" for 3 ints)
126
+ value.unpack(v.fmt)
127
+ elsif v.count && v.count > 1
128
+ # Nested struct array: split data and read each chunk
129
+ item_size = v.fmt.size
130
+ v.count.times.map { |i| v.fmt.read(value[i * item_size, item_size]) }
131
+ else
132
+ # Single nested struct
133
+ v.fmt.read(value)
134
+ end
122
135
  end
123
136
  end
124
137
 
@@ -128,7 +141,16 @@ module IOStruct
128
141
  next value unless v&.fmt && value
129
142
 
130
143
  # Reverse the unpacking done in initialize
131
- v.fmt.is_a?(String) ? value.pack(v.fmt) : value.pack
144
+ if v.fmt.is_a?(String)
145
+ # Primitive array
146
+ value.pack(v.fmt)
147
+ elsif v.count && v.count > 1
148
+ # Nested struct array: pack each and concatenate
149
+ value.map(&:pack).join
150
+ else
151
+ # Single nested struct
152
+ value.pack
153
+ end
132
154
  end
133
155
  values.pack self.class.const_get('FORMAT')
134
156
  end
@@ -140,7 +162,7 @@ module IOStruct
140
162
  # rubocop:disable Lint/DuplicateBranch
141
163
  def to_table
142
164
  values = to_a
143
- "<#{self.class.name} " + self.class::FIELDS.map.with_index do |el, idx|
165
+ "<#{IOStruct.get_name(self.class)} " + self.class::FIELDS.map.with_index do |el, idx|
144
166
  v = values[idx]
145
167
  fname, f = el
146
168
 
@@ -150,7 +172,7 @@ module IOStruct
150
172
  v.inspect
151
173
  when f.type == Integer
152
174
  v ||= 0 # avoid "`sprintf': can't convert nil into Integer" error
153
- format_integer(v, f.size, fname)
175
+ format_int(v, f.size, fname)
154
176
  when f.type == Float
155
177
  v ||= 0 # avoid "`sprintf': can't convert nil into Float" error
156
178
  "%8.3f" % v
@@ -160,6 +182,17 @@ module IOStruct
160
182
  end.join(' ') + ">"
161
183
  end
162
184
  # rubocop:enable Lint/DuplicateBranch
185
+
186
+ # don't make inspect an alias to to_s or vice versa, because:
187
+ # - ruby's default struct inspect() and to_s() returns same result, but they are different methods
188
+ # - inspect/to_s may already be used by some code (zsteg), where aliasing would break things
189
+ def inspect
190
+ _inspect
191
+ end
192
+
193
+ def to_s
194
+ _inspect
195
+ end
163
196
  end
164
197
 
165
198
  module DecInspect
@@ -167,10 +200,16 @@ module IOStruct
167
200
 
168
201
  DEC_FMTS = { 1 => "%4d", 2 => "%6d", 4 => "%11d", 8 => "%20d" }.freeze
169
202
 
170
- def format_integer(value, size, fname)
203
+ def format_int(value, size, fname)
171
204
  fmt = DEC_FMTS[size] || raise("Unsupported Integer size #{size} for field #{fname}")
172
205
  fmt % value
173
206
  end
207
+
208
+ private
209
+
210
+ def _inspect
211
+ "<#{IOStruct.get_name(self.class)} " + to_h.map { |k, v| "#{k}=#{v.inspect}" }.join(' ') + ">"
212
+ end
174
213
  end
175
214
 
176
215
  module HexInspect
@@ -178,8 +217,17 @@ module IOStruct
178
217
 
179
218
  HEX_FMTS = { 1 => "%2x", 2 => "%4x", 4 => "%8x", 8 => "%16x" }.freeze
180
219
 
181
- def to_s
182
- "<#{self.class.name} " + to_h.map do |k, v|
220
+ # display as unsigned, because signed %x looks ugly: "..f" for -1
221
+ def format_int(value, size, fname)
222
+ fmt = HEX_FMTS[size] || raise("Unsupported Integer size #{size} for field #{fname}")
223
+ mask = INT_MASKS[size] || raise("Unsupported Integer size #{size} for field #{fname}")
224
+ fmt % (value & mask)
225
+ end
226
+
227
+ private
228
+
229
+ def _inspect
230
+ "<#{IOStruct.get_name(self.class)} " + to_h.map do |k, v|
183
231
  if v.is_a?(Integer) && v > 9
184
232
  "#{k}=0x%x" % v
185
233
  else
@@ -187,16 +235,5 @@ module IOStruct
187
235
  end
188
236
  end.join(' ') + ">"
189
237
  end
190
-
191
- def inspect
192
- to_s
193
- end
194
-
195
- # display as unsigned, because signed %x looks ugly: "..f" for -1
196
- def format_integer(value, size, fname)
197
- fmt = HEX_FMTS[size] || raise("Unsupported Integer size #{size} for field #{fname}")
198
- mask = INT_MASKS[size] || raise("Unsupported Integer size #{size} for field #{fname}")
199
- fmt % (value & mask)
200
- end
201
238
  end
202
239
  end # IOStruct
data/spec/.rubocop.yml CHANGED
@@ -23,5 +23,8 @@ RSpec/ExampleLength:
23
23
  RSpec/ExampleWording:
24
24
  Enabled: false
25
25
 
26
+ RSpec/NestedGroups:
27
+ Enabled: false
28
+
26
29
  RSpec/SpecFilePathFormat:
27
30
  Enabled: false
@@ -143,6 +143,34 @@ describe IOStruct do
143
143
  expect(reparsed.bottomRight.y).to eq 200
144
144
  end
145
145
 
146
+ it "supports nested struct arrays" do
147
+ point = described_class.new(fields: { x: "int", y: :int })
148
+ polygon = described_class.new(
149
+ fields: {
150
+ num_points: 'int',
151
+ points: { type: point, count: 3 },
152
+ }
153
+ )
154
+ expect(polygon.size).to eq(4 + (8 * 3))
155
+
156
+ data = [3, 10, 20, 30, 40, 50, 60].pack('i*')
157
+ p = polygon.read(data)
158
+
159
+ expect(p.num_points).to eq 3
160
+ expect(p.points.size).to eq 3
161
+ expect(p.points[0]).to be_instance_of(point)
162
+ expect(p.points[0].x).to eq 10
163
+ expect(p.points[0].y).to eq 20
164
+ expect(p.points[1].x).to eq 30
165
+ expect(p.points[1].y).to eq 40
166
+ expect(p.points[2].x).to eq 50
167
+ expect(p.points[2].y).to eq 60
168
+
169
+ # Test round-trip
170
+ reparsed = polygon.read(p.pack)
171
+ expect(reparsed.points[2].y).to eq 60
172
+ end
173
+
146
174
  it "packs arrays" do
147
175
  klass = described_class.new(
148
176
  fields: {
@@ -211,5 +239,49 @@ describe IOStruct do
211
239
  expect(klass.read("\xff").a).to eq 255 # unsigned
212
240
  end
213
241
  end
242
+
243
+ context "endian types" do
244
+ it "supports big-endian uint16" do
245
+ klass = described_class.new(fields: { a: 'uint16_be' })
246
+ expect(klass.size).to eq 2
247
+ expect(klass.read("\x01\x02").a).to eq 0x0102 # big-endian
248
+ end
249
+
250
+ it "supports big-endian uint32" do
251
+ klass = described_class.new(fields: { a: 'uint32_be' })
252
+ expect(klass.size).to eq 4
253
+ expect(klass.read("\x01\x02\x03\x04").a).to eq 0x01020304
254
+ end
255
+
256
+ it "supports little-endian uint16" do
257
+ klass = described_class.new(fields: { a: 'uint16_le' })
258
+ expect(klass.size).to eq 2
259
+ expect(klass.read("\x01\x02").a).to eq 0x0201 # little-endian
260
+ end
261
+
262
+ it "supports little-endian uint32" do
263
+ klass = described_class.new(fields: { a: 'uint32_le' })
264
+ expect(klass.size).to eq 4
265
+ expect(klass.read("\x01\x02\x03\x04").a).to eq 0x04030201
266
+ end
267
+
268
+ it "supports alternate names (be16, le32, etc.)" do
269
+ klass = described_class.new(fields: {
270
+ a: 'be16',
271
+ b: 'be32',
272
+ c: 'le16',
273
+ d: 'le32'
274
+ })
275
+ expect(klass.size).to eq(2 + 4 + 2 + 4)
276
+ end
277
+
278
+ it "round-trips big-endian values" do
279
+ klass = described_class.new(fields: { a: 'uint16_be', b: 'uint32_be' })
280
+ obj = klass.read([0x1234, 0xDEADBEEF].pack('nN'))
281
+ expect(obj.a).to eq 0x1234
282
+ expect(obj.b).to eq 0xDEADBEEF
283
+ expect(obj.pack).to eq [0x1234, 0xDEADBEEF].pack('nN')
284
+ end
285
+ end
214
286
  end
215
287
  end
@@ -43,6 +43,40 @@ describe IOStruct do
43
43
  end
44
44
  end
45
45
 
46
+ describe "#inspect" do
47
+ [nil, "MyClass"].each do |struct_name|
48
+ context "when struct_name is #{struct_name.inspect}" do
49
+ [:hex, :dec].each do |inspect_mode|
50
+ context "when inspect is :#{inspect_mode}" do
51
+ context "for IOStruct" do
52
+ it "shows default struct name" do
53
+ struct = described_class.new('L S C', :a, :b, :c, inspect: inspect_mode, struct_name: struct_name)
54
+ cname = struct_name || "struct"
55
+ expect(struct.new.inspect).to match(/<#{cname} a=/)
56
+ end
57
+ end
58
+
59
+ context "for IOStruct anonymous subclass" do
60
+ it "shows default struct name" do
61
+ struct = Class.new( described_class.new('L S C', :a, :b, :c, inspect: inspect_mode, struct_name: struct_name) )
62
+ cname = struct_name || "struct"
63
+ expect(struct.new.inspect).to match(/<#{cname} a=/)
64
+ end
65
+ end
66
+
67
+ context "for named IOStruct subclass" do
68
+ it "shows custom struct name" do
69
+ stub_const("C1", described_class.new('L S C', :a, :b, :c, inspect: inspect_mode, struct_name: struct_name) )
70
+ cname = struct_name || "C1"
71
+ expect(C1.new.inspect).to match(/<#{cname} a=/)
72
+ end
73
+ end
74
+ end
75
+ end
76
+ end
77
+ end
78
+ end
79
+
46
80
  describe "hash-style initialization" do
47
81
  let(:struct) { described_class.new('L S C', :a, :b, :c) }
48
82
 
@@ -184,7 +218,7 @@ describe IOStruct do
184
218
  end
185
219
  end
186
220
 
187
- describe "#read" do
221
+ describe "read" do
188
222
  let(:a) { [12345, 56789] }
189
223
  let(:data) { a.pack('L2') }
190
224
 
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: iostruct
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.6.0
4
+ version: 0.7.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Andrey "Zed" Zaikin