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 +4 -4
- data/.rubocop.yml +3 -0
- data/CHANGELOG.md +28 -0
- data/Gemfile.lock +1 -4
- data/lib/iostruct/hash_fmt.rb +21 -1
- data/lib/iostruct/version.rb +1 -1
- data/lib/iostruct.rb +64 -27
- data/spec/.rubocop.yml +3 -0
- data/spec/hash_fmt_spec.rb +72 -0
- data/spec/iostruct_spec.rb +35 -1
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: f3b93bdd7f541a9cec7f68169c9927e989203b38cea668441523c499e3b62562
|
|
4
|
+
data.tar.gz: ee73bec71e144d684769c6238718e82cab1f2b526543721bfc2fcfe5e979bcc3
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: b910103dc94c253a01cf0f82a94d2359bed9489ceb829621fc6acdd04fdc140188dbb80c3cf58d9de06a289ec27479333b43b2e14f0c0028b31d6f94050c37eb
|
|
7
|
+
data.tar.gz: dbecaa47ac2e8077b8b2e23818add7ccf2142e0349cf482aa4e8108f03fa19a512c1acb30eceb94d18fc736ce5cf9516dd480ab49b1b1f59089d6714ddd1622f
|
data/.rubocop.yml
CHANGED
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.
|
|
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
|
data/lib/iostruct/hash_fmt.rb
CHANGED
|
@@ -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
|
|
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
|
data/lib/iostruct/version.rb
CHANGED
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
|
-
|
|
38
|
+
case inspect
|
|
39
|
+
when :hex
|
|
39
40
|
include HexInspect
|
|
40
|
-
|
|
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
|
-
|
|
120
|
-
|
|
121
|
-
|
|
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)
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
182
|
-
|
|
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
data/spec/hash_fmt_spec.rb
CHANGED
|
@@ -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
|
data/spec/iostruct_spec.rb
CHANGED
|
@@ -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 "
|
|
221
|
+
describe "read" do
|
|
188
222
|
let(:a) { [12345, 56789] }
|
|
189
223
|
let(:data) { a.pack('L2') }
|
|
190
224
|
|