kaitai-struct-visualizer 0.5 → 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 -28
- data/bin/ksdump +96 -0
- data/bin/ksv +53 -7
- data/lib/kaitai/console_ansi.rb +103 -98
- data/lib/kaitai/console_windows.rb +175 -230
- data/lib/kaitai/struct/visualizer/hex_viewer.rb +250 -241
- data/lib/kaitai/struct/visualizer/ks_error_matcher.rb +44 -0
- data/lib/kaitai/struct/visualizer/ksy_compiler.rb +243 -0
- data/lib/kaitai/struct/visualizer/node.rb +241 -225
- data/lib/kaitai/struct/visualizer/obj_to_h.rb +40 -0
- data/lib/kaitai/struct/visualizer/parser.rb +40 -0
- data/lib/kaitai/struct/visualizer/tree.rb +179 -198
- data/lib/kaitai/struct/visualizer/version.rb +3 -1
- data/lib/kaitai/struct/visualizer/visualizer.rb +10 -31
- data/lib/kaitai/struct/visualizer.rb +4 -1
- data/lib/kaitai/tui.rb +85 -94
- metadata +58 -27
- data/kaitai-struct-visualizer.gemspec +0 -37
- data/lib/kaitai/struct/visualizer/visualizer_main.rb +0 -42
- data/lib/kaitai/struct/visualizer/visualizer_ruby.rb +0 -18
@@ -1,268 +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
|
-
attr_reader :level
|
11
|
-
attr_reader :pos1
|
12
|
-
attr_reader :pos2
|
13
|
-
attr_reader :children
|
14
|
-
attr_accessor :parent
|
15
|
-
attr_accessor :type
|
16
|
-
|
17
|
-
def initialize(tree, value, level, value_method = nil, pos1 = nil, pos2 = nil)
|
18
|
-
@tree = tree
|
19
|
-
@value = value
|
20
|
-
@level = level
|
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
|
27
11
|
|
28
|
-
|
29
|
-
|
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
|
30
17
|
|
31
|
-
|
32
|
-
|
18
|
+
unless pos1.nil? || pos2.nil?
|
19
|
+
@pos1 = pos1
|
20
|
+
@pos2 = pos2
|
21
|
+
end
|
33
22
|
|
34
|
-
|
35
|
-
|
36
|
-
child.parent = self
|
37
|
-
end
|
23
|
+
@open = false
|
24
|
+
@explored = false
|
38
25
|
|
39
|
-
|
40
|
-
|
41
|
-
def openable?
|
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
|
26
|
+
@children = []
|
27
|
+
end
|
52
28
|
|
53
|
-
|
54
|
-
|
55
|
-
|
29
|
+
def add(child)
|
30
|
+
@children << child
|
31
|
+
child.parent = self
|
32
|
+
end
|
56
33
|
|
57
|
-
|
58
|
-
|
59
|
-
close
|
60
|
-
else
|
61
|
-
open
|
34
|
+
def open?
|
35
|
+
@open
|
62
36
|
end
|
63
|
-
end
|
64
37
|
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
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
|
70
48
|
|
71
|
-
|
72
|
-
|
73
|
-
|
49
|
+
def hex?
|
50
|
+
@value.is_a?(String)
|
51
|
+
end
|
74
52
|
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
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
|
53
|
+
def toggle
|
54
|
+
if @open
|
55
|
+
close
|
56
|
+
else
|
57
|
+
open
|
123
58
|
end
|
124
59
|
end
|
125
60
|
|
126
|
-
|
127
|
-
|
61
|
+
def open
|
62
|
+
return unless openable?
|
128
63
|
|
129
|
-
|
130
|
-
|
131
|
-
|
132
|
-
s.each_byte { |x|
|
133
|
-
r << sprintf('%02x ', x)
|
134
|
-
i += 1
|
135
|
-
break if i >= n
|
136
|
-
}
|
137
|
-
r
|
138
|
-
end
|
64
|
+
explore
|
65
|
+
@open = true if @explored
|
66
|
+
end
|
139
67
|
|
140
|
-
|
141
|
-
|
142
|
-
def detect_str_mode
|
143
|
-
if @value.encoding == Encoding::ASCII_8BIT
|
144
|
-
:hex
|
145
|
-
else
|
146
|
-
:str_esc
|
68
|
+
def close
|
69
|
+
@open = false
|
147
70
|
end
|
148
|
-
end
|
149
71
|
|
150
|
-
|
151
|
-
|
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
|
152
110
|
|
153
|
-
|
154
|
-
|
155
|
-
|
156
|
-
|
157
|
-
|
158
|
-
|
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
|
130
|
+
|
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
|
141
|
+
end
|
142
|
+
|
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
|
153
|
+
|
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
|
-
else
|
205
|
-
# Gather seq attributes
|
206
|
-
@value.class::SEQ_FIELDS.each { |k|
|
207
|
-
el = @value.instance_eval("@#{k}")
|
208
|
-
aid = @value._debug[k]
|
209
|
-
if aid
|
210
|
-
aid_s = aid[:start]
|
211
|
-
aid_e = aid[:end]
|
212
|
-
else
|
213
|
-
#raise "Unable to get debugging aid for '#{k}'"
|
214
|
-
aid_s = nil
|
215
|
-
aid_e = nil
|
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]
|
216
204
|
end
|
217
|
-
|
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)
|
225
|
+
end
|
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
|
+
|
218
241
|
n = Node.new(@tree, el, level + 1, nil, aid_s, aid_e)
|
219
242
|
n.id = k
|
220
243
|
n.type = :seq
|
221
244
|
add(n)
|
222
245
|
end
|
223
|
-
|
224
|
-
|
225
|
-
|
226
|
-
|
227
|
-
|
228
|
-
|
229
|
-
|
230
|
-
|
231
|
-
|
232
|
-
|
233
|
-
|
234
|
-
|
235
|
-
|
236
|
-
|
237
|
-
|
238
|
-
|
239
|
-
|
240
|
-
|
241
|
-
}
|
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
|
242
264
|
end
|
243
|
-
end
|
244
265
|
|
245
|
-
|
246
|
-
|
247
|
-
|
248
|
-
|
249
|
-
|
250
|
-
|
251
|
-
|
252
|
-
|
253
|
-
|
254
|
-
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
|
255
275
|
end
|
256
|
-
end
|
257
276
|
|
258
|
-
|
259
|
-
|
260
|
-
|
261
|
-
|
262
|
-
|
263
|
-
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
|
264
282
|
end
|
265
|
-
n
|
266
283
|
end
|
267
284
|
end
|
268
|
-
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
|
@@ -0,0 +1,40 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'kaitai/struct/visualizer/version'
|
4
|
+
require 'kaitai/tui'
|
5
|
+
require 'kaitai/struct/visualizer/tree'
|
6
|
+
require 'kaitai/struct/visualizer/ks_error_matcher'
|
7
|
+
|
8
|
+
# TODO: should be inside compiled files
|
9
|
+
require 'zlib'
|
10
|
+
require 'stringio'
|
11
|
+
|
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
|
23
|
+
|
24
|
+
def load
|
25
|
+
main_class_name = @compiler.compile_formats_if(@formats_fn)
|
26
|
+
|
27
|
+
main_class = Kernel.const_get(main_class_name)
|
28
|
+
@data = main_class.from_file(@bin_fn)
|
29
|
+
|
30
|
+
load_exc = nil
|
31
|
+
begin
|
32
|
+
@data._read
|
33
|
+
rescue Kaitai::Struct::Visualizer::KSErrorMatcher => e
|
34
|
+
load_exc = e
|
35
|
+
end
|
36
|
+
|
37
|
+
load_exc
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|