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 +4 -4
- data/.rubocop.yml +71 -0
- data/.ruby-version +1 -0
- data/CHANGELOG.md +63 -0
- data/Gemfile +9 -0
- data/Gemfile.lock +48 -11
- data/README.md +247 -6
- data/Rakefile +3 -1
- data/iostruct.gemspec +8 -9
- data/lib/iostruct/hash_fmt.rb +112 -0
- data/lib/iostruct/pack_fmt.rb +68 -0
- data/lib/iostruct/version.rb +1 -1
- data/lib/iostruct.rb +159 -103
- data/spec/.rubocop.yml +30 -0
- data/spec/hash_fmt_spec.rb +287 -0
- data/spec/iostruct_spec.rb +295 -22
- 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,100 +1,70 @@
|
|
|
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, inspect_name_override: nil, **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
|
|
56
|
-
x.define_singleton_method(:name) { inspect_name_override } if inspect_name_override
|
|
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"
|
|
57
25
|
end
|
|
58
|
-
end # self.new
|
|
59
26
|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
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
|
+
case inspect
|
|
39
|
+
when :hex
|
|
40
|
+
include HexInspect
|
|
41
|
+
when :dec
|
|
42
|
+
include DecInspect
|
|
74
43
|
else
|
|
75
|
-
|
|
76
|
-
fields << FieldInfo.new(klass, size, offset)
|
|
77
|
-
offset += size
|
|
78
|
-
end
|
|
44
|
+
# ruby default inspect
|
|
79
45
|
end
|
|
46
|
+
define_singleton_method(:to_s) { struct_name } if struct_name
|
|
47
|
+
define_singleton_method(:name) { struct_name } if struct_name
|
|
80
48
|
end
|
|
81
|
-
|
|
82
|
-
end
|
|
49
|
+
end # self.new
|
|
83
50
|
|
|
84
|
-
def self.auto_names
|
|
51
|
+
def self.auto_names(fields, _size)
|
|
85
52
|
names = []
|
|
86
53
|
offset = 0
|
|
87
54
|
fields.each do |f|
|
|
88
55
|
names << sprintf("f%x", offset).to_sym
|
|
89
56
|
offset += f.size
|
|
90
57
|
end
|
|
91
|
-
#raise "size mismatch: #{size} != #{offset}" if size != offset
|
|
92
58
|
names
|
|
93
59
|
end
|
|
94
60
|
|
|
61
|
+
def self.get_name(klass)
|
|
62
|
+
(klass.respond_to?(:name) && klass.name) || 'struct'
|
|
63
|
+
end
|
|
64
|
+
|
|
95
65
|
module ClassMethods
|
|
96
66
|
# src can be IO or String, or anything that responds to :read or :unpack
|
|
97
|
-
def read
|
|
67
|
+
def read(src, size = nil)
|
|
98
68
|
pos = nil
|
|
99
69
|
size ||= const_get 'SIZE'
|
|
100
70
|
data =
|
|
@@ -106,19 +76,12 @@ module IOStruct
|
|
|
106
76
|
else
|
|
107
77
|
raise "[?] don't know how to read from #{src.inspect}"
|
|
108
78
|
end
|
|
109
|
-
|
|
110
|
-
# $stderr.puts "[!] #{self.to_s} want #{size} bytes, got #{data.size}"
|
|
111
|
-
# end
|
|
112
|
-
new(*data.unpack(const_get('FORMAT'))).tap{ |x| x.__offset = pos }
|
|
79
|
+
new(*data.unpack(const_get('FORMAT'))).tap { |x| x.__offset = pos }
|
|
113
80
|
end
|
|
114
81
|
|
|
115
82
|
def size
|
|
116
83
|
self::SIZE
|
|
117
84
|
end
|
|
118
|
-
|
|
119
|
-
def name
|
|
120
|
-
self.to_s
|
|
121
|
-
end
|
|
122
85
|
end # ClassMethods
|
|
123
86
|
|
|
124
87
|
module InstanceMethods
|
|
@@ -129,7 +92,7 @@ module IOStruct
|
|
|
129
92
|
end
|
|
130
93
|
|
|
131
94
|
def empty?
|
|
132
|
-
to_a.all?{ |t| t == 0 || t.nil? || t.to_s.tr("\x00","").empty? }
|
|
95
|
+
to_a.all? { |t| t == 0 || t.nil? || t.to_s.tr("\x00", "").empty? }
|
|
133
96
|
end
|
|
134
97
|
|
|
135
98
|
# allow initializing individual struct members by name, like:
|
|
@@ -140,7 +103,7 @@ module IOStruct
|
|
|
140
103
|
def initialize *args
|
|
141
104
|
if args.size == 1 && args.first.is_a?(Hash)
|
|
142
105
|
super()
|
|
143
|
-
args.first.each do |k,v|
|
|
106
|
+
args.first.each do |k, v|
|
|
144
107
|
send "#{k}=", v
|
|
145
108
|
end
|
|
146
109
|
else
|
|
@@ -149,35 +112,128 @@ module IOStruct
|
|
|
149
112
|
end
|
|
150
113
|
end # InstanceMethods
|
|
151
114
|
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
115
|
+
# initialize nested structures / arrays
|
|
116
|
+
module NestedInstanceMethods
|
|
117
|
+
def initialize *args
|
|
118
|
+
super
|
|
119
|
+
self.class::FIELDS.each do |k, v|
|
|
120
|
+
next unless v.fmt
|
|
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
|
|
135
|
+
end
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
def pack
|
|
139
|
+
values = self.class::FIELDS.map do |k, v|
|
|
140
|
+
value = self[k]
|
|
141
|
+
next value unless v&.fmt && value
|
|
142
|
+
|
|
143
|
+
# Reverse the unpacking done in initialize
|
|
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
|
|
157
150
|
else
|
|
158
|
-
|
|
151
|
+
# Single nested struct
|
|
152
|
+
value.pack
|
|
159
153
|
end
|
|
160
|
-
end
|
|
154
|
+
end
|
|
155
|
+
values.pack self.class.const_get('FORMAT')
|
|
161
156
|
end
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
module InspectBase
|
|
160
|
+
INT_MASKS = { 1 => 0xff, 2 => 0xffff, 4 => 0xffffffff, 8 => 0xffffffffffffffff }.freeze
|
|
162
161
|
|
|
162
|
+
# rubocop:disable Lint/DuplicateBranch
|
|
163
163
|
def to_table
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
164
|
+
values = to_a
|
|
165
|
+
"<#{IOStruct.get_name(self.class)} " + self.class::FIELDS.map.with_index do |el, idx|
|
|
166
|
+
v = values[idx]
|
|
167
|
+
fname, f = el
|
|
168
|
+
|
|
169
|
+
"#{fname}=" +
|
|
170
|
+
case
|
|
171
|
+
when f.nil? # unknown field type
|
|
172
|
+
v.inspect
|
|
167
173
|
when f.type == Integer
|
|
168
|
-
"
|
|
174
|
+
v ||= 0 # avoid "`sprintf': can't convert nil into Integer" error
|
|
175
|
+
format_int(v, f.size, fname)
|
|
169
176
|
when f.type == Float
|
|
170
|
-
"
|
|
177
|
+
v ||= 0 # avoid "`sprintf': can't convert nil into Float" error
|
|
178
|
+
"%8.3f" % v
|
|
171
179
|
else
|
|
172
|
-
|
|
180
|
+
v.inspect
|
|
173
181
|
end
|
|
174
|
-
"#{name}=#{fmt}"
|
|
175
182
|
end.join(' ') + ">"
|
|
176
|
-
sprintf @fmtstr_tbl, *to_a.map{ |v| v.is_a?(String) ? v.inspect : (v||0) } # "||0" to avoid "`sprintf': can't convert nil into Integer" error
|
|
177
183
|
end
|
|
184
|
+
# rubocop:enable Lint/DuplicateBranch
|
|
178
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
|
|
179
189
|
def inspect
|
|
180
|
-
|
|
190
|
+
_inspect
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
def to_s
|
|
194
|
+
_inspect
|
|
195
|
+
end
|
|
196
|
+
end
|
|
197
|
+
|
|
198
|
+
module DecInspect
|
|
199
|
+
include InspectBase
|
|
200
|
+
|
|
201
|
+
DEC_FMTS = { 1 => "%4d", 2 => "%6d", 4 => "%11d", 8 => "%20d" }.freeze
|
|
202
|
+
|
|
203
|
+
def format_int(value, size, fname)
|
|
204
|
+
fmt = DEC_FMTS[size] || raise("Unsupported Integer size #{size} for field #{fname}")
|
|
205
|
+
fmt % value
|
|
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
|
|
213
|
+
end
|
|
214
|
+
|
|
215
|
+
module HexInspect
|
|
216
|
+
include InspectBase
|
|
217
|
+
|
|
218
|
+
HEX_FMTS = { 1 => "%2x", 2 => "%4x", 4 => "%8x", 8 => "%16x" }.freeze
|
|
219
|
+
|
|
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|
|
|
231
|
+
if v.is_a?(Integer) && v > 9
|
|
232
|
+
"#{k}=0x%x" % v
|
|
233
|
+
else
|
|
234
|
+
"#{k}=#{v.inspect}"
|
|
235
|
+
end
|
|
236
|
+
end.join(' ') + ">"
|
|
181
237
|
end
|
|
182
238
|
end
|
|
183
239
|
end # IOStruct
|
data/spec/.rubocop.yml
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
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/NestedGroups:
|
|
27
|
+
Enabled: false
|
|
28
|
+
|
|
29
|
+
RSpec/SpecFilePathFormat:
|
|
30
|
+
Enabled: false
|