kaitai-struct-visualizer 0.7 → 0.11
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 +5 -5
- data/LICENSE +4 -4
- data/README.md +88 -27
- data/bin/ksdump +39 -69
- data/bin/ksv +25 -12
- data/lib/kaitai/console_ansi.rb +101 -109
- data/lib/kaitai/console_windows.rb +174 -233
- data/lib/kaitai/struct/visualizer/hex_viewer.rb +250 -243
- data/lib/kaitai/struct/visualizer/ks_error_matcher.rb +44 -0
- data/lib/kaitai/struct/visualizer/ksy_compiler.rb +178 -51
- data/lib/kaitai/struct/visualizer/node.rb +242 -230
- data/lib/kaitai/struct/visualizer/obj_to_h.rb +40 -0
- data/lib/kaitai/struct/visualizer/parser.rb +24 -26
- data/lib/kaitai/struct/visualizer/tree.rb +174 -203
- data/lib/kaitai/struct/visualizer/version.rb +3 -1
- data/lib/kaitai/struct/visualizer/visualizer.rb +11 -9
- data/lib/kaitai/struct/visualizer.rb +2 -0
- data/lib/kaitai/tui.rb +85 -98
- metadata +54 -25
- data/kaitai-struct-visualizer.gemspec +0 -37
@@ -1,272 +1,284 @@
|
|
1
|
-
#
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
2
3
|
require 'set'
|
3
4
|
|
4
5
|
require 'kaitai/struct/visualizer/version'
|
5
6
|
|
6
7
|
module Kaitai::Struct::Visualizer
|
7
|
-
class Node
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
@value_method = value_method
|
22
|
-
|
23
|
-
unless pos1.nil? or pos2.nil?
|
24
|
-
@pos1 = pos1
|
25
|
-
@pos2 = pos2
|
26
|
-
end
|
8
|
+
class Node
|
9
|
+
attr_accessor :id, :parent, :type
|
10
|
+
attr_reader :value, :level, :pos1, :pos2, :children
|
11
|
+
|
12
|
+
def initialize(tree, value, level, value_method = nil, pos1 = nil, pos2 = nil)
|
13
|
+
@tree = tree
|
14
|
+
@value = value
|
15
|
+
@level = level
|
16
|
+
@value_method = value_method
|
17
|
+
|
18
|
+
unless pos1.nil? || pos2.nil?
|
19
|
+
@pos1 = pos1
|
20
|
+
@pos2 = pos2
|
21
|
+
end
|
27
22
|
|
28
|
-
|
29
|
-
|
23
|
+
@open = false
|
24
|
+
@explored = false
|
30
25
|
|
31
|
-
|
32
|
-
|
26
|
+
@children = []
|
27
|
+
end
|
33
28
|
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
29
|
+
def add(child)
|
30
|
+
@children << child
|
31
|
+
child.parent = self
|
32
|
+
end
|
38
33
|
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
not (
|
43
|
-
@value.is_a?(Fixnum) or
|
44
|
-
@value.is_a?(Bignum) or
|
45
|
-
@value.is_a?(Float) or
|
46
|
-
@value.is_a?(String) or
|
47
|
-
@value.is_a?(Symbol) or
|
48
|
-
@value === true or
|
49
|
-
@value === false
|
50
|
-
)
|
51
|
-
end
|
34
|
+
def open?
|
35
|
+
@open
|
36
|
+
end
|
52
37
|
|
53
|
-
|
54
|
-
|
55
|
-
|
38
|
+
def openable?
|
39
|
+
!(
|
40
|
+
@value.is_a?(Float) or
|
41
|
+
@value.is_a?(Integer) or
|
42
|
+
@value.is_a?(String) or
|
43
|
+
@value.is_a?(Symbol) or
|
44
|
+
@value == true or
|
45
|
+
@value == false
|
46
|
+
)
|
47
|
+
end
|
56
48
|
|
57
|
-
|
58
|
-
|
59
|
-
close
|
60
|
-
else
|
61
|
-
open
|
49
|
+
def hex?
|
50
|
+
@value.is_a?(String)
|
62
51
|
end
|
63
|
-
end
|
64
52
|
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
53
|
+
def toggle
|
54
|
+
if @open
|
55
|
+
close
|
56
|
+
else
|
57
|
+
open
|
58
|
+
end
|
59
|
+
end
|
70
60
|
|
71
|
-
|
72
|
-
|
73
|
-
end
|
61
|
+
def open
|
62
|
+
return unless openable?
|
74
63
|
|
75
|
-
|
76
|
-
|
77
|
-
print(if @value.nil?
|
78
|
-
'[?]'
|
79
|
-
elsif open?
|
80
|
-
'[-]'
|
81
|
-
elsif openable?
|
82
|
-
'[+]'
|
83
|
-
else
|
84
|
-
'[.]'
|
85
|
-
end)
|
86
|
-
print " #{@id}"
|
87
|
-
|
88
|
-
pos = 2 * level + 4 + @id.length
|
89
|
-
|
90
|
-
if open? or not openable?
|
91
|
-
if @value.is_a?(Fixnum) or @value.is_a?(Bignum) or @value.is_a?(Float)
|
92
|
-
print " = #{@value}"
|
93
|
-
elsif @value.is_a?(Symbol)
|
94
|
-
print " = #{@value}"
|
95
|
-
elsif @value.is_a?(String)
|
96
|
-
print ' = '
|
97
|
-
pos += 3
|
98
|
-
@str_mode = detect_str_mode unless @str_mode
|
99
|
-
max_len = @tree.tree_width - pos
|
100
|
-
case @str_mode
|
101
|
-
when :str
|
102
|
-
v = @value.encode('UTF-8')
|
103
|
-
s = v[0, max_len]
|
104
|
-
when :str_esc
|
105
|
-
v = @value.encode('UTF-8')
|
106
|
-
s = v.inspect[0, max_len]
|
107
|
-
when :hex
|
108
|
-
s = first_n_bytes_dump(@value, max_len / 3 + 1)
|
109
|
-
else
|
110
|
-
raise "Invalid str_mode: #{@str_mode.inspect}"
|
111
|
-
end
|
112
|
-
if s.length > max_len
|
113
|
-
s = s[0, max_len - 1]
|
114
|
-
s += '…'
|
115
|
-
end
|
116
|
-
print s
|
117
|
-
elsif @value === true or @value === false
|
118
|
-
print " = #{@value}"
|
119
|
-
elsif @value.nil?
|
120
|
-
print " = null"
|
121
|
-
elsif @value.is_a?(Array)
|
122
|
-
printf ' (%d = 0x%x entries)', @value.size, @value.size
|
123
|
-
end
|
64
|
+
explore
|
65
|
+
@open = true if @explored
|
124
66
|
end
|
125
67
|
|
126
|
-
|
127
|
-
|
68
|
+
def close
|
69
|
+
@open = false
|
70
|
+
end
|
128
71
|
|
129
|
-
|
130
|
-
|
131
|
-
|
132
|
-
|
133
|
-
|
134
|
-
|
135
|
-
|
136
|
-
|
137
|
-
|
138
|
-
|
72
|
+
def draw(_ui)
|
73
|
+
print ' ' * level
|
74
|
+
print(if @value.nil?
|
75
|
+
'[?]'
|
76
|
+
elsif open?
|
77
|
+
'[-]'
|
78
|
+
elsif openable?
|
79
|
+
'[+]'
|
80
|
+
else
|
81
|
+
'[.]'
|
82
|
+
end)
|
83
|
+
print " #{@id}"
|
84
|
+
|
85
|
+
pos = 2 * level + 4 + @id.length
|
86
|
+
|
87
|
+
if open? || !openable?
|
88
|
+
if @value.is_a?(Float) || @value.is_a?(Integer)
|
89
|
+
print " = #{@value}"
|
90
|
+
elsif @value.is_a?(Symbol)
|
91
|
+
print " = #{@value}"
|
92
|
+
elsif @value.is_a?(String)
|
93
|
+
print ' = '
|
94
|
+
pos += 3
|
95
|
+
@str_mode ||= detect_str_mode
|
96
|
+
max_len = @tree.tree_width - pos
|
97
|
+
|
98
|
+
case @str_mode
|
99
|
+
when :str
|
100
|
+
v = @value.encode('UTF-8')
|
101
|
+
s = v[0, max_len]
|
102
|
+
when :str_esc
|
103
|
+
v = @value.encode('UTF-8')
|
104
|
+
s = v.inspect[0, max_len]
|
105
|
+
when :hex
|
106
|
+
s = first_n_bytes_dump(@value, max_len / 3 + 1)
|
107
|
+
else
|
108
|
+
raise "Invalid str_mode: #{@str_mode.inspect}"
|
109
|
+
end
|
110
|
+
|
111
|
+
s = clamp_string(s, max_len)
|
112
|
+
print s
|
113
|
+
elsif (@value == true) || (@value == false)
|
114
|
+
print " = #{@value}"
|
115
|
+
elsif @value.nil?
|
116
|
+
print ' = null'
|
117
|
+
elsif @value.is_a?(Array)
|
118
|
+
printf ' (%d = 0x%x entries)', @value.size, @value.size
|
119
|
+
elsif @value.public_methods(false).include?(:to_s)
|
120
|
+
s = @value.to_s
|
121
|
+
pos += 2
|
122
|
+
max_len = @tree.tree_width - pos
|
123
|
+
if s.is_a?(String)
|
124
|
+
print ": #{clamp_string(s, max_len)}"
|
125
|
+
else
|
126
|
+
print ": #{clamp_string(s.class.to_s, max_len)}"
|
127
|
+
end
|
128
|
+
end
|
129
|
+
end
|
139
130
|
|
140
|
-
|
141
|
-
|
142
|
-
|
143
|
-
|
144
|
-
|
145
|
-
|
146
|
-
|
131
|
+
puts
|
132
|
+
end
|
133
|
+
|
134
|
+
def clamp_string(s, max_len)
|
135
|
+
s ||= ''
|
136
|
+
if s.length > max_len
|
137
|
+
s = s[0, max_len - 1] || ''
|
138
|
+
s += '…'
|
139
|
+
end
|
140
|
+
s
|
147
141
|
end
|
148
|
-
end
|
149
142
|
|
150
|
-
|
151
|
-
|
143
|
+
def first_n_bytes_dump(s, n)
|
144
|
+
i = 0
|
145
|
+
r = +''
|
146
|
+
s.each_byte do |x|
|
147
|
+
r << format('%02x ', x)
|
148
|
+
i += 1
|
149
|
+
break if i >= n
|
150
|
+
end
|
151
|
+
r
|
152
|
+
end
|
152
153
|
|
153
|
-
|
154
|
-
|
155
|
-
|
156
|
-
|
157
|
-
|
158
|
-
|
154
|
+
# Empirically detects a mode that would be best to show a designated string
|
155
|
+
def detect_str_mode
|
156
|
+
if @value.encoding == Encoding::ASCII_8BIT
|
157
|
+
:hex
|
158
|
+
else
|
159
|
+
:str_esc
|
159
160
|
end
|
160
|
-
@io = obj.value._io
|
161
161
|
end
|
162
|
-
end
|
163
162
|
|
164
|
-
|
165
|
-
|
163
|
+
def io
|
164
|
+
return @io if @io
|
166
165
|
|
167
|
-
|
168
|
-
|
166
|
+
if @parent.nil?
|
167
|
+
@io = @value._io
|
168
|
+
else
|
169
|
+
obj = @parent
|
170
|
+
obj = obj.parent until obj.value.respond_to?(:_io)
|
171
|
+
@io = obj.value._io
|
172
|
+
end
|
169
173
|
end
|
170
174
|
|
171
|
-
|
172
|
-
|
173
|
-
|
174
|
-
|
175
|
-
|
176
|
-
|
177
|
-
|
178
|
-
|
179
|
-
|
180
|
-
|
181
|
-
|
182
|
-
|
183
|
-
#raise "Unable to get debugging aid for: #{@parent.value._debug.inspect} using ID '#{clean_id}'" unless debug_el
|
184
|
-
if debug_el
|
185
|
-
@pos1 = debug_el[:start]
|
186
|
-
@pos2 = debug_el[:end]
|
175
|
+
def explore
|
176
|
+
return if @explored
|
177
|
+
|
178
|
+
if @value.nil?
|
179
|
+
@value = @parent.value.send(@value_method)
|
180
|
+
clean_id = @id[0] == '@' ? @id[1..-1] : @id
|
181
|
+
debug_el = @parent.value._debug[clean_id]
|
182
|
+
# raise "Unable to get debugging aid for: #{@parent.value._debug.inspect} using ID '#{clean_id}'" unless debug_el
|
183
|
+
if debug_el
|
184
|
+
@pos1 = debug_el[:start]
|
185
|
+
@pos2 = debug_el[:end]
|
186
|
+
end
|
187
187
|
end
|
188
|
-
|
189
|
-
|
190
|
-
|
191
|
-
|
192
|
-
|
193
|
-
|
194
|
-
|
195
|
-
|
196
|
-
|
197
|
-
|
198
|
-
|
199
|
-
|
200
|
-
|
201
|
-
|
202
|
-
|
203
|
-
|
204
|
-
|
205
|
-
|
206
|
-
|
207
|
-
|
208
|
-
|
209
|
-
|
210
|
-
|
211
|
-
|
212
|
-
aid
|
213
|
-
|
214
|
-
|
215
|
-
|
216
|
-
|
217
|
-
|
218
|
-
|
219
|
-
|
188
|
+
|
189
|
+
@explored = true
|
190
|
+
|
191
|
+
if @value.is_a?(Float) ||
|
192
|
+
@value.is_a?(Integer) ||
|
193
|
+
@value.is_a?(String) ||
|
194
|
+
(@value == true) ||
|
195
|
+
(@value == false) ||
|
196
|
+
@value.nil? ||
|
197
|
+
@value.is_a?(Symbol)
|
198
|
+
clean_id = @id[0] == '@' ? @id[1..-1] : @id
|
199
|
+
debug_el = @parent.value._debug[clean_id]
|
200
|
+
# raise "Unable to get debugging aid for: #{@parent.value._debug.inspect} using ID '#{clean_id}'" unless debug_el
|
201
|
+
if debug_el
|
202
|
+
@pos1 = debug_el[:start]
|
203
|
+
@pos2 = debug_el[:end]
|
204
|
+
end
|
205
|
+
elsif @value.is_a?(Array)
|
206
|
+
# Bail out early for empty array: it doesn't have proper
|
207
|
+
# debugging aids structure anyway
|
208
|
+
return if @value.empty?
|
209
|
+
|
210
|
+
clean_id = @id[0] == '@' ? @id[1..-1] : @id
|
211
|
+
debug_el = @parent.value._debug[clean_id]
|
212
|
+
# raise "Unable to get debugging aid for array: #{@parent.value._debug.inspect} using ID '#{clean_id}'" unless debug_el
|
213
|
+
|
214
|
+
aid = (debug_el && debug_el[:arr]) || {}
|
215
|
+
# raise "Unable to get debugging aid for array: #{debug_el.inspect}" unless aid
|
216
|
+
|
217
|
+
max_val_digits = @value.size.to_s.size
|
218
|
+
fmt = "%#{max_val_digits}d"
|
219
|
+
|
220
|
+
@value.each_with_index do |el, i|
|
221
|
+
aid_el = aid[i] || {}
|
222
|
+
n = Node.new(@tree, el, level + 1, nil, aid_el[:start], aid_el[:end])
|
223
|
+
n.id = format(fmt, i)
|
224
|
+
add(n)
|
220
225
|
end
|
221
|
-
|
226
|
+
else
|
227
|
+
# Gather seq attributes
|
228
|
+
@value.class::SEQ_FIELDS.each do |k|
|
229
|
+
el = @value.instance_eval("@#{k}", __FILE__, __LINE__)
|
230
|
+
aid = @value._debug[k]
|
231
|
+
if aid
|
232
|
+
aid_s = aid[:start]
|
233
|
+
aid_e = aid[:end]
|
234
|
+
else
|
235
|
+
# raise "Unable to get debugging aid for '#{k}'"
|
236
|
+
aid_s = nil
|
237
|
+
aid_e = nil
|
238
|
+
end
|
239
|
+
next if el.nil?
|
240
|
+
|
222
241
|
n = Node.new(@tree, el, level + 1, nil, aid_s, aid_e)
|
223
242
|
n.id = k
|
224
243
|
n.type = :seq
|
225
244
|
add(n)
|
226
245
|
end
|
227
|
-
|
228
|
-
|
229
|
-
|
230
|
-
|
231
|
-
|
232
|
-
|
233
|
-
|
234
|
-
|
235
|
-
|
236
|
-
|
237
|
-
|
238
|
-
|
239
|
-
|
240
|
-
|
241
|
-
|
242
|
-
|
243
|
-
|
244
|
-
|
245
|
-
}
|
246
|
+
|
247
|
+
attrs = Set.new(@value.class::SEQ_FIELDS)
|
248
|
+
|
249
|
+
# Gather instances
|
250
|
+
prop_meths = @value.public_methods(false)
|
251
|
+
prop_meths.each do |meth|
|
252
|
+
k = meth.to_s
|
253
|
+
# NB: we don't need to consider `_unnamed*` attributes here
|
254
|
+
# (https://github.com/kaitai-io/kaitai_struct/issues/1064) because
|
255
|
+
# only `seq` fields can be unnamed, not `instances`
|
256
|
+
next if k.start_with?('_') || attrs.include?(k) || meth == :to_s
|
257
|
+
|
258
|
+
n = Node.new(@tree, nil, level + 1, meth)
|
259
|
+
n.id = k
|
260
|
+
n.type = :instance
|
261
|
+
add(n)
|
262
|
+
end
|
263
|
+
end
|
246
264
|
end
|
247
|
-
end
|
248
265
|
|
249
|
-
|
250
|
-
|
251
|
-
|
252
|
-
|
253
|
-
|
254
|
-
|
255
|
-
|
256
|
-
|
257
|
-
|
258
|
-
1
|
266
|
+
# Determine total height of an element, including all children if it's open and visible
|
267
|
+
def height
|
268
|
+
if @open
|
269
|
+
r = 1
|
270
|
+
@children.each { |n| r += n.height }
|
271
|
+
r
|
272
|
+
else
|
273
|
+
1
|
274
|
+
end
|
259
275
|
end
|
260
|
-
end
|
261
276
|
|
262
|
-
|
263
|
-
|
264
|
-
|
265
|
-
|
266
|
-
|
267
|
-
n = n.children.last
|
277
|
+
# Find out last (deepest) descendant of current node
|
278
|
+
def last_descendant
|
279
|
+
n = self
|
280
|
+
n = n.children.last while n.open?
|
281
|
+
n
|
268
282
|
end
|
269
|
-
n
|
270
283
|
end
|
271
284
|
end
|
272
|
-
end
|
@@ -0,0 +1,40 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Kaitai::Struct::Visualizer
|
4
|
+
# Recursively convert object received from Kaitai Struct to a hash.
|
5
|
+
# Used by ksdump to prepare data for JSON/XML/YAML output.
|
6
|
+
def self.obj_to_h(obj)
|
7
|
+
if (obj == true) || (obj == false) || obj.is_a?(Numeric) || obj.nil?
|
8
|
+
obj
|
9
|
+
elsif obj.is_a?(Symbol)
|
10
|
+
obj.to_s
|
11
|
+
elsif obj.is_a?(String)
|
12
|
+
if obj.encoding == Encoding::ASCII_8BIT
|
13
|
+
r = +''
|
14
|
+
obj.each_byte { |x| r << format('%02X ', x) }
|
15
|
+
r.chop!
|
16
|
+
r
|
17
|
+
else
|
18
|
+
obj.encode('UTF-8')
|
19
|
+
end
|
20
|
+
elsif obj.is_a?(Array)
|
21
|
+
obj.map { |x| obj_to_h(x) }
|
22
|
+
else
|
23
|
+
return "OPAQUE (#{obj.class})" unless obj.is_a?(Kaitai::Struct::Struct)
|
24
|
+
|
25
|
+
root = {}
|
26
|
+
|
27
|
+
prop_meths = obj.public_methods(false)
|
28
|
+
prop_meths.sort.each do |meth|
|
29
|
+
k = meth.to_s
|
30
|
+
next if (k.start_with?('_') && !k.start_with?('_unnamed')) || meth == :to_s
|
31
|
+
|
32
|
+
el = obj.send(meth)
|
33
|
+
v = obj_to_h(el)
|
34
|
+
root[k] = v unless v.nil?
|
35
|
+
end
|
36
|
+
|
37
|
+
root
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
@@ -1,42 +1,40 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require 'kaitai/struct/visualizer/version'
|
2
4
|
require 'kaitai/tui'
|
3
5
|
require 'kaitai/struct/visualizer/tree'
|
6
|
+
require 'kaitai/struct/visualizer/ks_error_matcher'
|
4
7
|
|
5
8
|
# TODO: should be inside compiled files
|
6
9
|
require 'zlib'
|
7
10
|
require 'stringio'
|
8
11
|
|
9
12
|
module Kaitai::Struct::Visualizer
|
13
|
+
# Base class for everything that deals with compiling .ksy and parsing stuff as object tree.
|
14
|
+
class Parser
|
15
|
+
attr_reader :data
|
16
|
+
|
17
|
+
def initialize(compiler, bin_fn, formats_fn, opts)
|
18
|
+
@compiler = compiler
|
19
|
+
@bin_fn = bin_fn
|
20
|
+
@formats_fn = formats_fn
|
21
|
+
@opts = opts
|
22
|
+
end
|
10
23
|
|
11
|
-
|
12
|
-
|
13
|
-
# stuff as object tree.
|
14
|
-
class Parser
|
15
|
-
attr_reader :data
|
16
|
-
|
17
|
-
def initialize(compiler, bin_fn, formats_fn, opts)
|
18
|
-
@compiler = compiler
|
19
|
-
@bin_fn = bin_fn
|
20
|
-
@formats_fn = formats_fn
|
21
|
-
@opts = opts
|
22
|
-
end
|
24
|
+
def load
|
25
|
+
main_class_name = @compiler.compile_formats_if(@formats_fn)
|
23
26
|
|
24
|
-
|
25
|
-
|
27
|
+
main_class = Kernel.const_get(main_class_name)
|
28
|
+
@data = main_class.from_file(@bin_fn)
|
26
29
|
|
27
|
-
|
28
|
-
|
30
|
+
load_exc = nil
|
31
|
+
begin
|
32
|
+
@data._read
|
33
|
+
rescue Kaitai::Struct::Visualizer::KSErrorMatcher => e
|
34
|
+
load_exc = e
|
35
|
+
end
|
29
36
|
|
30
|
-
|
31
|
-
begin
|
32
|
-
@data._read
|
33
|
-
rescue EOFError => e
|
34
|
-
load_exc = e
|
35
|
-
rescue Kaitai::Struct::Stream::UnexpectedDataError => e
|
36
|
-
load_exc = e
|
37
|
+
load_exc
|
37
38
|
end
|
38
|
-
|
39
|
-
return load_exc
|
40
39
|
end
|
41
40
|
end
|
42
|
-
end
|