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 +4 -4
- data/.rubocop.yml +68 -0
- data/.ruby-version +1 -0
- data/CHANGELOG.md +44 -0
- data/Gemfile +9 -0
- data/Gemfile.lock +49 -9
- data/README.md +247 -6
- data/Rakefile +3 -1
- data/iostruct.gemspec +8 -9
- data/lib/iostruct/hash_fmt.rb +92 -0
- data/lib/iostruct/pack_fmt.rb +68 -0
- data/lib/iostruct/version.rb +1 -1
- data/lib/iostruct.rb +122 -98
- data/spec/.rubocop.yml +27 -0
- data/spec/hash_fmt_spec.rb +215 -0
- data/spec/iostruct_spec.rb +271 -16
- metadata +12 -51
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module IOStruct
|
|
4
|
+
module PackFmt
|
|
5
|
+
# https://apidock.com/ruby/String/unpack
|
|
6
|
+
FMTSPEC = {
|
|
7
|
+
'C' => [1, Integer ], # 8-bit unsigned (uint8_t, unsigned char)
|
|
8
|
+
'S' => [2, Integer ], # 16-bit unsigned, native endian (uint16_t)
|
|
9
|
+
'I' => [4, Integer ], # 32-bit unsigned, native endian (uint32_t, unsigned int)
|
|
10
|
+
'L' => [4, Integer ], # 32-bit unsigned, native endian (unsigned long)
|
|
11
|
+
'Q' => [8, Integer ], # 64-bit unsigned, native endian (uint64_t)
|
|
12
|
+
|
|
13
|
+
'c' => [1, Integer ], # 8-bit signed (int8_t, signed char)
|
|
14
|
+
's' => [2, Integer ], # 16-bit signed, native endian (int16_t)
|
|
15
|
+
'i' => [4, Integer ], # 32-bit signed, native endian (int32_t, int)
|
|
16
|
+
'l' => [4, Integer ], # 32-bit signed, native endian (long)
|
|
17
|
+
'q' => [8, Integer ], # 64-bit signed, native endian (int64_t)
|
|
18
|
+
|
|
19
|
+
'n' => [2, Integer ], # 16-bit unsigned, network (big-endian) byte order
|
|
20
|
+
'N' => [4, Integer ], # 32-bit unsigned, network (big-endian) byte order
|
|
21
|
+
'v' => [2, Integer ], # 16-bit unsigned, VAX (little-endian) byte order
|
|
22
|
+
'V' => [4, Integer ], # 32-bit unsigned, VAX (little-endian) byte order
|
|
23
|
+
|
|
24
|
+
'A' => [1, String ], # arbitrary binary string (remove trailing nulls and ASCII spaces)
|
|
25
|
+
'a' => [1, String ], # arbitrary binary string
|
|
26
|
+
'Z' => [1, String ], # arbitrary binary string (remove trailing nulls)
|
|
27
|
+
'H' => [1, String ], # hex string (high nibble first)
|
|
28
|
+
'h' => [1, String ], # hex string (low nibble first)
|
|
29
|
+
|
|
30
|
+
'D' => [8, Float ], # double-precision, native format
|
|
31
|
+
'd' => [8, Float ],
|
|
32
|
+
'F' => [4, Float ], # single-precision, native format
|
|
33
|
+
'f' => [4, Float ],
|
|
34
|
+
'E' => [8, Float ], # double-precision, little-endian byte order
|
|
35
|
+
'e' => [4, Float ], # single-precision, little-endian byte order
|
|
36
|
+
'G' => [8, Float ], # double-precision, network (big-endian) byte order
|
|
37
|
+
'g' => [4, Float ], # single-precision, network (big-endian) byte order
|
|
38
|
+
|
|
39
|
+
'x' => [1, nil ], # skip forward one byte
|
|
40
|
+
}.freeze
|
|
41
|
+
|
|
42
|
+
private
|
|
43
|
+
|
|
44
|
+
def parse_pack_format(fmt, _names)
|
|
45
|
+
offset = 0
|
|
46
|
+
fields = []
|
|
47
|
+
fmt.scan(/([a-z])(\d*)/i).map do |type, len|
|
|
48
|
+
size, klass = FMTSPEC[type] || raise("Unknown field type #{type.inspect}")
|
|
49
|
+
len = len.empty? ? 1 : len.to_i
|
|
50
|
+
case type
|
|
51
|
+
when 'A', 'a', 'x', 'Z'
|
|
52
|
+
fields << FieldInfo.new(klass, size * len, offset) if klass
|
|
53
|
+
offset += len
|
|
54
|
+
when 'H', 'h'
|
|
55
|
+
# XXX ruby's String#unpack length for hex strings is in characters, not bytes, i.e. "x".unpack("H2") => ["78"]
|
|
56
|
+
fields << FieldInfo.new(klass, size * len / 2, offset) if klass
|
|
57
|
+
offset += len / 2
|
|
58
|
+
else
|
|
59
|
+
len.times do |_i|
|
|
60
|
+
fields << FieldInfo.new(klass, size, offset)
|
|
61
|
+
offset += size
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
[fields, offset]
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
end
|
data/lib/iostruct/version.rb
CHANGED
data/lib/iostruct.rb
CHANGED
|
@@ -1,99 +1,63 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
require_relative 'iostruct/pack_fmt'
|
|
4
|
+
require_relative 'iostruct/hash_fmt'
|
|
4
5
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
'A' => [1, String ], # arbitrary binary string (remove trailing nulls and ASCII spaces)
|
|
25
|
-
'a' => [1, String ], # arbitrary binary string
|
|
26
|
-
'Z' => [1, String ], # arbitrary binary string (remove trailing nulls)
|
|
27
|
-
'H' => [1, String ], # hex string (high nibble first)
|
|
28
|
-
'h' => [1, String ], # hex string (low nibble first)
|
|
29
|
-
|
|
30
|
-
'D' => [8, Float ], # double-precision, native format
|
|
31
|
-
'd' => [8, Float ],
|
|
32
|
-
'F' => [4, Float ], # single-precision, native format
|
|
33
|
-
'f' => [4, Float ],
|
|
34
|
-
'E' => [8, Float ], # double-precision, little-endian byte order
|
|
35
|
-
'e' => [4, Float ], # single-precision, little-endian byte order
|
|
36
|
-
'G' => [8, Float ], # double-precision, network (big-endian) byte order
|
|
37
|
-
'g' => [4, Float ], # single-precision, network (big-endian) byte order
|
|
38
|
-
|
|
39
|
-
'x' => [1, nil ], # skip forward one byte
|
|
40
|
-
}.freeze
|
|
41
|
-
|
|
42
|
-
FieldInfo = Struct.new :type, :size, :offset
|
|
43
|
-
|
|
44
|
-
def self.new fmt, *names, inspect: :hex, **renames
|
|
45
|
-
fields, size = parse_format(fmt, names)
|
|
46
|
-
names = auto_names(fields, size) if names.empty?
|
|
47
|
-
names.map!{ |n| renames[n] || n } if renames.any?
|
|
48
|
-
|
|
49
|
-
Struct.new( *names ).tap do |x|
|
|
50
|
-
x.const_set 'FIELDS', names.zip(fields).to_h
|
|
51
|
-
x.const_set 'FORMAT', fmt
|
|
52
|
-
x.const_set 'SIZE', size
|
|
53
|
-
x.extend ClassMethods
|
|
54
|
-
x.include InstanceMethods
|
|
55
|
-
x.include HexInspect if inspect == :hex
|
|
6
|
+
module IOStruct
|
|
7
|
+
extend PackFmt
|
|
8
|
+
extend HashFmt
|
|
9
|
+
|
|
10
|
+
# rubocop:disable Lint/StructNewOverride
|
|
11
|
+
FieldInfo = Struct.new :type, :size, :offset, :count, :fmt
|
|
12
|
+
# rubocop:enable Lint/StructNewOverride
|
|
13
|
+
|
|
14
|
+
def self.new fmt = nil, *names, inspect: :hex, inspect_name_override: nil, struct_name: nil, **kwargs
|
|
15
|
+
struct_name ||= inspect_name_override # XXX inspect_name_override is deprecated
|
|
16
|
+
if fmt
|
|
17
|
+
renames = kwargs
|
|
18
|
+
finfos, size = parse_pack_format(fmt, names)
|
|
19
|
+
names = auto_names(finfos, size) if names.empty?
|
|
20
|
+
names.map! { |n| renames[n] || n } if renames.any?
|
|
21
|
+
elsif kwargs[:fields]
|
|
22
|
+
fmt, names, finfos, size = parse_hash_format(name: struct_name, **kwargs)
|
|
23
|
+
else
|
|
24
|
+
raise "IOStruct: no fmt and no :fields specified"
|
|
56
25
|
end
|
|
57
|
-
end # self.new
|
|
58
26
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
offset += len/2
|
|
27
|
+
# if first argument to Struct.new() is a string - it creates a named struct in the Struct:: namespace
|
|
28
|
+
# convert all just for the case
|
|
29
|
+
names = names.map(&:to_sym)
|
|
30
|
+
|
|
31
|
+
Struct.new( *names ) do
|
|
32
|
+
const_set 'FIELDS', names.zip(finfos).to_h
|
|
33
|
+
const_set 'FORMAT', fmt
|
|
34
|
+
const_set 'SIZE', size
|
|
35
|
+
extend ClassMethods
|
|
36
|
+
include InstanceMethods
|
|
37
|
+
include NestedInstanceMethods if finfos.any?(&:fmt)
|
|
38
|
+
if inspect == :hex
|
|
39
|
+
include HexInspect
|
|
73
40
|
else
|
|
74
|
-
|
|
75
|
-
fields << FieldInfo.new(klass, size, offset)
|
|
76
|
-
offset += size
|
|
77
|
-
end
|
|
41
|
+
include DecInspect
|
|
78
42
|
end
|
|
43
|
+
define_singleton_method(:to_s) { struct_name } if struct_name
|
|
44
|
+
define_singleton_method(:name) { struct_name } if struct_name
|
|
79
45
|
end
|
|
80
|
-
|
|
81
|
-
end
|
|
46
|
+
end # self.new
|
|
82
47
|
|
|
83
|
-
def self.auto_names
|
|
48
|
+
def self.auto_names(fields, _size)
|
|
84
49
|
names = []
|
|
85
50
|
offset = 0
|
|
86
51
|
fields.each do |f|
|
|
87
52
|
names << sprintf("f%x", offset).to_sym
|
|
88
53
|
offset += f.size
|
|
89
54
|
end
|
|
90
|
-
#raise "size mismatch: #{size} != #{offset}" if size != offset
|
|
91
55
|
names
|
|
92
56
|
end
|
|
93
57
|
|
|
94
58
|
module ClassMethods
|
|
95
59
|
# src can be IO or String, or anything that responds to :read or :unpack
|
|
96
|
-
def read
|
|
60
|
+
def read(src, size = nil)
|
|
97
61
|
pos = nil
|
|
98
62
|
size ||= const_get 'SIZE'
|
|
99
63
|
data =
|
|
@@ -105,10 +69,11 @@ module IOStruct
|
|
|
105
69
|
else
|
|
106
70
|
raise "[?] don't know how to read from #{src.inspect}"
|
|
107
71
|
end
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
72
|
+
new(*data.unpack(const_get('FORMAT'))).tap { |x| x.__offset = pos }
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def name
|
|
76
|
+
'struct'
|
|
112
77
|
end
|
|
113
78
|
|
|
114
79
|
def size
|
|
@@ -124,7 +89,7 @@ module IOStruct
|
|
|
124
89
|
end
|
|
125
90
|
|
|
126
91
|
def empty?
|
|
127
|
-
to_a.all?{ |t| t == 0 || t.nil? || t.to_s.tr("\x00","").empty? }
|
|
92
|
+
to_a.all? { |t| t == 0 || t.nil? || t.to_s.tr("\x00", "").empty? }
|
|
128
93
|
end
|
|
129
94
|
|
|
130
95
|
# allow initializing individual struct members by name, like:
|
|
@@ -135,7 +100,7 @@ module IOStruct
|
|
|
135
100
|
def initialize *args
|
|
136
101
|
if args.size == 1 && args.first.is_a?(Hash)
|
|
137
102
|
super()
|
|
138
|
-
args.first.each do |k,v|
|
|
103
|
+
args.first.each do |k, v|
|
|
139
104
|
send "#{k}=", v
|
|
140
105
|
end
|
|
141
106
|
else
|
|
@@ -144,35 +109,94 @@ module IOStruct
|
|
|
144
109
|
end
|
|
145
110
|
end # InstanceMethods
|
|
146
111
|
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
112
|
+
# initialize nested structures / arrays
|
|
113
|
+
module NestedInstanceMethods
|
|
114
|
+
def initialize *args
|
|
115
|
+
super
|
|
116
|
+
self.class::FIELDS.each do |k, v|
|
|
117
|
+
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)
|
|
154
121
|
end
|
|
155
|
-
end
|
|
122
|
+
end
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
def pack
|
|
126
|
+
values = self.class::FIELDS.map do |k, v|
|
|
127
|
+
value = self[k]
|
|
128
|
+
next value unless v&.fmt && value
|
|
129
|
+
|
|
130
|
+
# Reverse the unpacking done in initialize
|
|
131
|
+
v.fmt.is_a?(String) ? value.pack(v.fmt) : value.pack
|
|
132
|
+
end
|
|
133
|
+
values.pack self.class.const_get('FORMAT')
|
|
156
134
|
end
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
module InspectBase
|
|
138
|
+
INT_MASKS = { 1 => 0xff, 2 => 0xffff, 4 => 0xffffffff, 8 => 0xffffffffffffffff }.freeze
|
|
157
139
|
|
|
140
|
+
# rubocop:disable Lint/DuplicateBranch
|
|
158
141
|
def to_table
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
142
|
+
values = to_a
|
|
143
|
+
"<#{self.class.name} " + self.class::FIELDS.map.with_index do |el, idx|
|
|
144
|
+
v = values[idx]
|
|
145
|
+
fname, f = el
|
|
146
|
+
|
|
147
|
+
"#{fname}=" +
|
|
148
|
+
case
|
|
149
|
+
when f.nil? # unknown field type
|
|
150
|
+
v.inspect
|
|
162
151
|
when f.type == Integer
|
|
163
|
-
"
|
|
152
|
+
v ||= 0 # avoid "`sprintf': can't convert nil into Integer" error
|
|
153
|
+
format_integer(v, f.size, fname)
|
|
164
154
|
when f.type == Float
|
|
165
|
-
"
|
|
155
|
+
v ||= 0 # avoid "`sprintf': can't convert nil into Float" error
|
|
156
|
+
"%8.3f" % v
|
|
166
157
|
else
|
|
167
|
-
|
|
158
|
+
v.inspect
|
|
168
159
|
end
|
|
169
|
-
"#{name}=#{fmt}"
|
|
170
160
|
end.join(' ') + ">"
|
|
171
|
-
|
|
161
|
+
end
|
|
162
|
+
# rubocop:enable Lint/DuplicateBranch
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
module DecInspect
|
|
166
|
+
include InspectBase
|
|
167
|
+
|
|
168
|
+
DEC_FMTS = { 1 => "%4d", 2 => "%6d", 4 => "%11d", 8 => "%20d" }.freeze
|
|
169
|
+
|
|
170
|
+
def format_integer(value, size, fname)
|
|
171
|
+
fmt = DEC_FMTS[size] || raise("Unsupported Integer size #{size} for field #{fname}")
|
|
172
|
+
fmt % value
|
|
173
|
+
end
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
module HexInspect
|
|
177
|
+
include InspectBase
|
|
178
|
+
|
|
179
|
+
HEX_FMTS = { 1 => "%2x", 2 => "%4x", 4 => "%8x", 8 => "%16x" }.freeze
|
|
180
|
+
|
|
181
|
+
def to_s
|
|
182
|
+
"<#{self.class.name} " + to_h.map do |k, v|
|
|
183
|
+
if v.is_a?(Integer) && v > 9
|
|
184
|
+
"#{k}=0x%x" % v
|
|
185
|
+
else
|
|
186
|
+
"#{k}=#{v.inspect}"
|
|
187
|
+
end
|
|
188
|
+
end.join(' ') + ">"
|
|
172
189
|
end
|
|
173
190
|
|
|
174
191
|
def inspect
|
|
175
192
|
to_s
|
|
176
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
|
|
177
201
|
end
|
|
178
202
|
end # IOStruct
|
data/spec/.rubocop.yml
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
inherit_from: ../.rubocop.yml
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
Layout/SpaceInsideParens:
|
|
5
|
+
Enabled: false
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
Style/FrozenStringLiteralComment:
|
|
9
|
+
Enabled: false
|
|
10
|
+
|
|
11
|
+
Style/SymbolArray:
|
|
12
|
+
Enabled: false
|
|
13
|
+
|
|
14
|
+
RSpec/ContextWording:
|
|
15
|
+
Enabled: false
|
|
16
|
+
|
|
17
|
+
RSpec/MultipleExpectations:
|
|
18
|
+
Enabled: false
|
|
19
|
+
|
|
20
|
+
RSpec/ExampleLength:
|
|
21
|
+
Enabled: false
|
|
22
|
+
|
|
23
|
+
RSpec/ExampleWording:
|
|
24
|
+
Enabled: false
|
|
25
|
+
|
|
26
|
+
RSpec/SpecFilePathFormat:
|
|
27
|
+
Enabled: false
|
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
require 'spec_helper'
|
|
2
|
+
require 'stringio'
|
|
3
|
+
|
|
4
|
+
describe IOStruct do
|
|
5
|
+
describe ".get_type_size" do
|
|
6
|
+
it "returns size for known types" do
|
|
7
|
+
expect(described_class.get_type_size('int')).to eq 4
|
|
8
|
+
expect(described_class.get_type_size('char')).to eq 1
|
|
9
|
+
expect(described_class.get_type_size('short')).to eq 2
|
|
10
|
+
expect(described_class.get_type_size('long long')).to eq 8
|
|
11
|
+
expect(described_class.get_type_size('double')).to eq 8
|
|
12
|
+
expect(described_class.get_type_size('float')).to eq 4
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
it "works with symbol input" do
|
|
16
|
+
expect(described_class.get_type_size(:int)).to eq 4
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
it "returns nil for unknown types" do
|
|
20
|
+
expect(described_class.get_type_size('unknown')).to be_nil
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
describe "hash format ctor" do
|
|
25
|
+
it "works" do
|
|
26
|
+
klass = described_class.new(
|
|
27
|
+
fields: {
|
|
28
|
+
x: "int",
|
|
29
|
+
y: :int,
|
|
30
|
+
z: { type: :int },
|
|
31
|
+
},
|
|
32
|
+
struct_name: 'Point'
|
|
33
|
+
)
|
|
34
|
+
expect(klass.new.inspect).to match(/<Point x=nil y=nil z=nil>/)
|
|
35
|
+
expect(klass.size).to eq(12)
|
|
36
|
+
expect(klass::SIZE).to eq(12)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
it "respects :name" do
|
|
40
|
+
klass = described_class.new(
|
|
41
|
+
fields: {
|
|
42
|
+
x: "int",
|
|
43
|
+
y: :int,
|
|
44
|
+
z: { type: :int },
|
|
45
|
+
},
|
|
46
|
+
struct_name: 'Point'
|
|
47
|
+
)
|
|
48
|
+
expect(klass.new.inspect).to match(/<Point x=nil y=nil z=nil>/)
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
it "respects :offset" do
|
|
52
|
+
klass = described_class.new(
|
|
53
|
+
struct_name: 'Point',
|
|
54
|
+
fields: {
|
|
55
|
+
x: "int",
|
|
56
|
+
y: :int,
|
|
57
|
+
z: { type: :int, offset: 0x10 },
|
|
58
|
+
}
|
|
59
|
+
)
|
|
60
|
+
expect(klass.new.inspect).to match(/<Point x=nil y=nil z=nil>/)
|
|
61
|
+
expect(klass.size).to eq(0x14)
|
|
62
|
+
|
|
63
|
+
obj = klass.read((0..0x20).to_a.pack('i*'))
|
|
64
|
+
expect(obj.x).to eq(0)
|
|
65
|
+
expect(obj.y).to eq(1)
|
|
66
|
+
expect(obj.z).to eq(4)
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
context 'when two fields have same offset' do
|
|
70
|
+
it 'fails' do
|
|
71
|
+
expect do
|
|
72
|
+
described_class.new(
|
|
73
|
+
struct_name: 'Point',
|
|
74
|
+
fields: {
|
|
75
|
+
x: { type: :int, offset: 0 },
|
|
76
|
+
y: { type: :char, offset: 0 },
|
|
77
|
+
}
|
|
78
|
+
)
|
|
79
|
+
end.to raise_error(RuntimeError)
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
it "can override size" do
|
|
84
|
+
klass = described_class.new(
|
|
85
|
+
fields: {
|
|
86
|
+
x: "int",
|
|
87
|
+
y: :int,
|
|
88
|
+
z: { type: 'int' },
|
|
89
|
+
},
|
|
90
|
+
size: 0x100
|
|
91
|
+
)
|
|
92
|
+
expect(klass.size).to eq(0x100)
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
it "supports arrays" do
|
|
96
|
+
klass = described_class.new(
|
|
97
|
+
fields: {
|
|
98
|
+
x: "int",
|
|
99
|
+
a: { type: 'int', count: 3 },
|
|
100
|
+
y: :int,
|
|
101
|
+
},
|
|
102
|
+
)
|
|
103
|
+
expect(klass.size).to eq(4 * 5)
|
|
104
|
+
|
|
105
|
+
v = klass.read([1, 2, 3, 4, 5, 6, 7].pack('i*'))
|
|
106
|
+
|
|
107
|
+
expect(v.x).to eq(1)
|
|
108
|
+
expect(v.a).to eq([2, 3, 4])
|
|
109
|
+
expect(v.y).to eq(5)
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
it "supports nesting" do
|
|
113
|
+
point = described_class.new( fields: { x: "int", y: :int } )
|
|
114
|
+
rect = described_class.new(
|
|
115
|
+
struct_name: 'Rect',
|
|
116
|
+
fields: {
|
|
117
|
+
topLeft: point,
|
|
118
|
+
bottomRight: point,
|
|
119
|
+
}
|
|
120
|
+
)
|
|
121
|
+
expect(rect.size).to eq(16)
|
|
122
|
+
|
|
123
|
+
r = rect.read([10, 20, 100, 200].pack('i*'))
|
|
124
|
+
expect(r.topLeft).to be_instance_of(point)
|
|
125
|
+
expect(r.bottomRight).to be_instance_of(point)
|
|
126
|
+
expect(r.topLeft.x).to eq(10)
|
|
127
|
+
expect(r.topLeft.y).to eq(20)
|
|
128
|
+
expect(r.bottomRight.x).to eq(100)
|
|
129
|
+
expect(r.bottomRight.y).to eq(200)
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
it "packs nested structs" do
|
|
133
|
+
point = described_class.new(fields: { x: "int", y: :int })
|
|
134
|
+
rect = described_class.new(fields: { topLeft: point, bottomRight: point })
|
|
135
|
+
|
|
136
|
+
r = rect.read([10, 20, 100, 200].pack('i*'))
|
|
137
|
+
packed = r.pack
|
|
138
|
+
reparsed = rect.read(packed)
|
|
139
|
+
|
|
140
|
+
expect(reparsed.topLeft.x).to eq 10
|
|
141
|
+
expect(reparsed.topLeft.y).to eq 20
|
|
142
|
+
expect(reparsed.bottomRight.x).to eq 100
|
|
143
|
+
expect(reparsed.bottomRight.y).to eq 200
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
it "packs arrays" do
|
|
147
|
+
klass = described_class.new(
|
|
148
|
+
fields: {
|
|
149
|
+
x: "int",
|
|
150
|
+
a: { type: 'int', count: 3 },
|
|
151
|
+
y: :int,
|
|
152
|
+
}
|
|
153
|
+
)
|
|
154
|
+
|
|
155
|
+
v = klass.read([1, 2, 3, 4, 5].pack('i*'))
|
|
156
|
+
packed = v.pack
|
|
157
|
+
reparsed = klass.read(packed)
|
|
158
|
+
|
|
159
|
+
expect(reparsed.x).to eq 1
|
|
160
|
+
expect(reparsed.a).to eq [2, 3, 4]
|
|
161
|
+
expect(reparsed.y).to eq 5
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
context "error handling" do
|
|
165
|
+
it "raises on unknown field type" do
|
|
166
|
+
expect do
|
|
167
|
+
described_class.new(fields: { x: "unknown_type" })
|
|
168
|
+
end.to raise_error(/unknown field type/)
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
it "raises on invalid type format" do
|
|
172
|
+
expect do
|
|
173
|
+
described_class.new(fields: { x: 12345 })
|
|
174
|
+
end.to raise_error(/unexpected field desc type/)
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
it "raises when forced size is smaller than actual" do
|
|
178
|
+
expect do
|
|
179
|
+
described_class.new(
|
|
180
|
+
fields: { x: "int", y: "int", z: "int" },
|
|
181
|
+
size: 4
|
|
182
|
+
)
|
|
183
|
+
end.to raise_error(/actual struct size .* is greater than forced size/)
|
|
184
|
+
end
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
context "C type aliases" do
|
|
188
|
+
it "supports uint types" do
|
|
189
|
+
klass = described_class.new(fields: {
|
|
190
|
+
a: 'uint8_t',
|
|
191
|
+
b: 'uint16_t',
|
|
192
|
+
c: 'uint32_t',
|
|
193
|
+
d: 'uint64_t'
|
|
194
|
+
})
|
|
195
|
+
expect(klass.size).to eq(1 + 2 + 4 + 8)
|
|
196
|
+
end
|
|
197
|
+
|
|
198
|
+
it "supports int types" do
|
|
199
|
+
klass = described_class.new(fields: {
|
|
200
|
+
a: 'int8_t',
|
|
201
|
+
b: 'int16_t',
|
|
202
|
+
c: 'int32_t',
|
|
203
|
+
d: 'int64_t'
|
|
204
|
+
})
|
|
205
|
+
expect(klass.size).to eq(1 + 2 + 4 + 8)
|
|
206
|
+
end
|
|
207
|
+
|
|
208
|
+
it "supports _BYTE type" do
|
|
209
|
+
klass = described_class.new(fields: { a: '_BYTE' })
|
|
210
|
+
expect(klass.size).to eq 1
|
|
211
|
+
expect(klass.read("\xff").a).to eq 255 # unsigned
|
|
212
|
+
end
|
|
213
|
+
end
|
|
214
|
+
end
|
|
215
|
+
end
|