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.
@@ -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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module IOStruct
4
- VERSION = "0.5.0"
4
+ VERSION = "0.7.0"
5
5
  end
data/lib/iostruct.rb CHANGED
@@ -1,100 +1,70 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- module IOStruct
3
+ require_relative 'iostruct/pack_fmt'
4
+ require_relative 'iostruct/hash_fmt'
4
5
 
5
- # https://apidock.com/ruby/String/unpack
6
- FMTSPEC = {
7
- 'C' => [1, Integer ], # 8-bit unsigned (unsigned char)
8
- 'S' => [2, Integer ], # 16-bit unsigned, native endian (uint16_t)
9
- 'I' => [4, Integer ], # 32-bit unsigned, native endian (uint32_t)
10
- 'L' => [4, Integer ], # 32-bit unsigned, native endian (uint32_t)
11
- 'Q' => [8, Integer ], # 64-bit unsigned, native endian (uint64_t)
12
-
13
- 'c' => [1, Integer ], # 8-bit signed (signed char)
14
- 's' => [2, Integer ], # 16-bit signed, native endian (int16_t)
15
- 'i' => [4, Integer ], # 32-bit signed, native endian (int32_t)
16
- 'l' => [4, Integer ], # 32-bit signed, native endian (int32_t)
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
- 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
- def self.parse_format(fmt, names)
61
- offset = 0
62
- fields = []
63
- fmt.scan(/([a-z])(\d*)/i).map do |type,len|
64
- size, klass = FMTSPEC[type] || raise("Unknown field type #{type.inspect}")
65
- len = len.empty? ? 1 : len.to_i
66
- case type
67
- when 'A', 'a', 'x', 'Z'
68
- fields << FieldInfo.new(klass, size*len, offset) if klass
69
- offset += len
70
- when 'H', 'h'
71
- # XXX ruby's String#unpack length for hex strings is in characters, not bytes, i.e. "x".unpack("H2") => ["78"]
72
- fields << FieldInfo.new(klass, size*len/2, offset) if klass
73
- 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
+ case inspect
39
+ when :hex
40
+ include HexInspect
41
+ when :dec
42
+ include DecInspect
74
43
  else
75
- len.times do |i|
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
- [fields, offset]
82
- end
49
+ end # self.new
83
50
 
84
- def self.auto_names fields, size
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 src, size = nil
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
- # if data.size < size
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
- module HexInspect
153
- def to_s
154
- "<#{self.class.name} " + to_h.map do |k, v|
155
- if v.is_a?(Integer) && v > 9
156
- "#{k}=0x%x" % v
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
- "#{k}=#{v.inspect}"
151
+ # Single nested struct
152
+ value.pack
159
153
  end
160
- end.join(' ') + ">"
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
- @fmtstr_tbl = "<#{self.class.name} " + self.class.const_get('FIELDS').map do |name, f|
165
- fmt =
166
- case
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
- "%#{f.size*2}x"
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
- "%8.3f"
177
+ v ||= 0 # avoid "`sprintf': can't convert nil into Float" error
178
+ "%8.3f" % v
171
179
  else
172
- "%s"
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
- to_s
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