kaitai-struct-visualizer 0.3
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 +7 -0
- data/LICENSE +674 -0
- data/README.md +57 -0
- data/bin/ksv +20 -0
- data/kaitai-struct-visualizer.gemspec +37 -0
- data/lib/kaitai/struct/visualizer.rb +2 -0
- data/lib/kaitai/struct/visualizer/hex_viewer.rb +294 -0
- data/lib/kaitai/struct/visualizer/node.rb +242 -0
- data/lib/kaitai/struct/visualizer/tree.rb +253 -0
- data/lib/kaitai/struct/visualizer/version.rb +7 -0
- data/lib/kaitai/struct/visualizer/visualizer.rb +47 -0
- data/lib/kaitai/struct/visualizer/visualizer_main.rb +24 -0
- data/lib/kaitai/struct/visualizer/visualizer_ruby.rb +18 -0
- data/lib/kaitai/tui.rb +169 -0
- metadata +105 -0
@@ -0,0 +1,253 @@
|
|
1
|
+
require 'benchmark'
|
2
|
+
|
3
|
+
require 'kaitai/struct/visualizer/version'
|
4
|
+
require 'kaitai/struct/visualizer/node'
|
5
|
+
require 'kaitai/struct/visualizer/hex_viewer'
|
6
|
+
|
7
|
+
module Kaitai::Struct::Visualizer
|
8
|
+
class Tree
|
9
|
+
def initialize(ui, st)
|
10
|
+
@ui = ui
|
11
|
+
@st = st
|
12
|
+
@root = Node.new(self, st, 0)
|
13
|
+
@root.id = '[root]'
|
14
|
+
@max_scr_ln = @ui.rows - 3
|
15
|
+
|
16
|
+
@hv_shift_x = @ui.cols - HexViewer.line_width - 1
|
17
|
+
|
18
|
+
@cur_io = nil
|
19
|
+
@hv = HexViewer.new(ui, nil, @hv_shift_x, self)
|
20
|
+
@hv_hidden = false
|
21
|
+
|
22
|
+
@cur_line = 0
|
23
|
+
@cur_shift = 0
|
24
|
+
@do_exit = false
|
25
|
+
end
|
26
|
+
|
27
|
+
def run
|
28
|
+
c = nil
|
29
|
+
loop {
|
30
|
+
t = redraw
|
31
|
+
|
32
|
+
if @cur_node.nil? and not @cur_line.nil?
|
33
|
+
# gone beyond the end of the tree
|
34
|
+
@cur_line = @root.height - 1
|
35
|
+
clamp_cursor
|
36
|
+
redraw
|
37
|
+
end
|
38
|
+
|
39
|
+
raise '@cur_line is undetermined' if @cur_line.nil?
|
40
|
+
raise '@cur_node is undetermined' if @cur_node.nil?
|
41
|
+
|
42
|
+
thv = Benchmark.realtime {
|
43
|
+
unless @hv_hidden
|
44
|
+
hv_update_io
|
45
|
+
|
46
|
+
unless @cur_node.pos1.nil?
|
47
|
+
if (@hv.addr < @cur_node.pos1) or (@hv.addr >= @cur_node.pos2)
|
48
|
+
@hv.addr = @cur_node.pos1
|
49
|
+
@hv.ensure_visible
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
@hv.redraw
|
54
|
+
regs = highlight_regions(4)
|
55
|
+
@hv.highlight(regs)
|
56
|
+
end
|
57
|
+
}
|
58
|
+
|
59
|
+
@ui.goto(0, @max_scr_ln + 1)
|
60
|
+
printf "all: %d, tree: %d, tree_draw: %d, hexview: %d, ln: %d, ", (t + thv) * 1e6, t * 1e6, @draw_time * 1e6, thv * 1e6, @ln
|
61
|
+
puts "highlight = #{@cur_node.pos1}..#{@cur_node.pos2}"
|
62
|
+
#puts "keypress: #{c.inspect}"
|
63
|
+
|
64
|
+
begin
|
65
|
+
process_keypress
|
66
|
+
rescue EOFError => e
|
67
|
+
@ui.message_box_exception(e)
|
68
|
+
rescue Kaitai::Struct::Stream::UnexpectedDataError => e
|
69
|
+
@ui.message_box_exception(e)
|
70
|
+
end
|
71
|
+
|
72
|
+
return if @do_exit
|
73
|
+
|
74
|
+
clamp_cursor
|
75
|
+
}
|
76
|
+
end
|
77
|
+
|
78
|
+
def process_keypress
|
79
|
+
c = @ui.read_char_mapped
|
80
|
+
case c
|
81
|
+
when :up_arrow
|
82
|
+
@cur_line -= 1
|
83
|
+
@cur_node = nil
|
84
|
+
when :down_arrow
|
85
|
+
@cur_line += 1
|
86
|
+
@cur_node = nil
|
87
|
+
when :left_arrow
|
88
|
+
if @cur_node.open?
|
89
|
+
@cur_node.close
|
90
|
+
else
|
91
|
+
par = @cur_node.parent
|
92
|
+
if par
|
93
|
+
@cur_line = nil
|
94
|
+
@cur_node = par
|
95
|
+
end
|
96
|
+
end
|
97
|
+
when :right_arrow
|
98
|
+
if @cur_node.openable?
|
99
|
+
if @cur_node.open?
|
100
|
+
@cur_line += 1
|
101
|
+
@cur_node = nil
|
102
|
+
else
|
103
|
+
@cur_node.open
|
104
|
+
end
|
105
|
+
end
|
106
|
+
when :home
|
107
|
+
@cur_line = @cur_shift = 0
|
108
|
+
@cur_node = nil
|
109
|
+
when :end
|
110
|
+
@cur_line = @root.height - 1
|
111
|
+
@cur_node = nil
|
112
|
+
when :pg_up
|
113
|
+
@cur_line -= 20
|
114
|
+
@cur_node = nil
|
115
|
+
when :pg_dn
|
116
|
+
@cur_line += 20
|
117
|
+
@cur_node = nil
|
118
|
+
when :enter
|
119
|
+
if @cur_node.hex?
|
120
|
+
@ui.clear
|
121
|
+
hv = HexViewer.new(@ui, @cur_node.value)
|
122
|
+
hv.redraw
|
123
|
+
hv.run
|
124
|
+
@ui.clear
|
125
|
+
redraw
|
126
|
+
else
|
127
|
+
@cur_node.toggle
|
128
|
+
end
|
129
|
+
when :tab
|
130
|
+
@hv.run
|
131
|
+
when 'H'
|
132
|
+
@hv_hidden = !@hv_hidden
|
133
|
+
@ui.clear
|
134
|
+
redraw
|
135
|
+
when 'q'
|
136
|
+
@do_exit = true
|
137
|
+
end
|
138
|
+
end
|
139
|
+
|
140
|
+
def clamp_cursor
|
141
|
+
if @cur_line
|
142
|
+
@cur_line = 0 if @cur_line < 0
|
143
|
+
|
144
|
+
if @cur_line - @cur_shift < 0
|
145
|
+
@cur_shift = @cur_line
|
146
|
+
end
|
147
|
+
if @cur_line - @cur_shift > @max_scr_ln
|
148
|
+
@cur_shift = @cur_line - @max_scr_ln
|
149
|
+
end
|
150
|
+
end
|
151
|
+
end
|
152
|
+
|
153
|
+
def redraw
|
154
|
+
@draw_time = 0
|
155
|
+
Benchmark.realtime {
|
156
|
+
@ui.clear
|
157
|
+
@ln = 0
|
158
|
+
draw_rec(@root)
|
159
|
+
}
|
160
|
+
end
|
161
|
+
|
162
|
+
def draw_rec(n)
|
163
|
+
scr_ln = @ln - @cur_shift
|
164
|
+
return if @cur_node and scr_ln > @max_scr_ln
|
165
|
+
|
166
|
+
if @ln == @cur_line
|
167
|
+
# Seeking cur_node by cur_line
|
168
|
+
@cur_node = n
|
169
|
+
@ui.bg_color = :gray
|
170
|
+
@ui.fg_color = :black
|
171
|
+
elsif @cur_node == n
|
172
|
+
# Seeking cur_line by cur_node
|
173
|
+
@cur_line = @ln
|
174
|
+
@ui.bg_color = :gray
|
175
|
+
@ui.fg_color = :black
|
176
|
+
end
|
177
|
+
|
178
|
+
@draw_time += Benchmark.realtime {
|
179
|
+
# n.draw(@ui) if scr_ln >= 0
|
180
|
+
n.draw(@ui) if scr_ln >= 0 and scr_ln <= @max_scr_ln
|
181
|
+
}
|
182
|
+
|
183
|
+
@ui.reset_colors if @ln == @cur_line
|
184
|
+
@ln += 1
|
185
|
+
if n.open?
|
186
|
+
n.children.each { |ch|
|
187
|
+
draw_rec(ch)
|
188
|
+
break if scr_ln > @max_scr_ln
|
189
|
+
}
|
190
|
+
end
|
191
|
+
end
|
192
|
+
|
193
|
+
def do_exit
|
194
|
+
@do_exit = true
|
195
|
+
end
|
196
|
+
|
197
|
+
def hv_update_io
|
198
|
+
io = @cur_node.io
|
199
|
+
if io != @cur_io
|
200
|
+
@cur_io = io
|
201
|
+
io.seek(0)
|
202
|
+
buf = io.read_bytes_full
|
203
|
+
@hv.buf = buf
|
204
|
+
|
205
|
+
# @hv.redraw
|
206
|
+
end
|
207
|
+
end
|
208
|
+
|
209
|
+
def highlight_regions(max_levels)
|
210
|
+
node = @cur_node
|
211
|
+
r = []
|
212
|
+
max_levels.times { |i|
|
213
|
+
return r if node.nil?
|
214
|
+
r << [node.pos1, node.pos2]
|
215
|
+
node = node.parent
|
216
|
+
}
|
217
|
+
r
|
218
|
+
end
|
219
|
+
|
220
|
+
def tree_width
|
221
|
+
if @hv_hidden
|
222
|
+
@ui.cols
|
223
|
+
else
|
224
|
+
@hv_shift_x
|
225
|
+
end
|
226
|
+
end
|
227
|
+
|
228
|
+
def self.explore_object(obj, level)
|
229
|
+
root = Node.new(obj, level)
|
230
|
+
if obj.is_a?(Fixnum) or obj.is_a?(String)
|
231
|
+
# do nothing else
|
232
|
+
elsif obj.is_a?(Array)
|
233
|
+
root = Node.new(obj, level)
|
234
|
+
obj.each_with_index { |el, i|
|
235
|
+
n = explore_object(el, level + 1)
|
236
|
+
n.id = i
|
237
|
+
root.add(n)
|
238
|
+
}
|
239
|
+
else
|
240
|
+
root = Node.new(obj, level)
|
241
|
+
obj.instance_variables.each { |k|
|
242
|
+
k = k.to_s
|
243
|
+
next if k =~ /^@_/
|
244
|
+
el = obj.instance_eval(k)
|
245
|
+
n = explore_object(el, level + 1)
|
246
|
+
n.id = k
|
247
|
+
root.add(n)
|
248
|
+
}
|
249
|
+
end
|
250
|
+
root
|
251
|
+
end
|
252
|
+
end
|
253
|
+
end
|
@@ -0,0 +1,47 @@
|
|
1
|
+
require 'tmpdir'
|
2
|
+
|
3
|
+
require 'kaitai/struct/visualizer/version'
|
4
|
+
require 'kaitai/tui'
|
5
|
+
require 'kaitai/struct/visualizer/tree'
|
6
|
+
|
7
|
+
# TODO: should be inside compiled files
|
8
|
+
require 'zlib'
|
9
|
+
require 'stringio'
|
10
|
+
|
11
|
+
module Kaitai::Struct::Visualizer
|
12
|
+
class Visualizer
|
13
|
+
def initialize(bin_fn, formats_fn)
|
14
|
+
@bin_fn = bin_fn
|
15
|
+
@formats_fn = formats_fn
|
16
|
+
@primary_format = @formats_fn.shift
|
17
|
+
|
18
|
+
main_class_name = compile_format(@primary_format)
|
19
|
+
|
20
|
+
@formats_fn.each { |fn|
|
21
|
+
compile_format(fn)
|
22
|
+
}
|
23
|
+
|
24
|
+
main_class = Kernel::const_get(main_class_name)
|
25
|
+
@data = main_class.from_file(@bin_fn)
|
26
|
+
|
27
|
+
load_exc = nil
|
28
|
+
begin
|
29
|
+
@data._read
|
30
|
+
rescue EOFError => e
|
31
|
+
load_exc = e
|
32
|
+
rescue Kaitai::Struct::Stream::UnexpectedDataError => e
|
33
|
+
load_exc = e
|
34
|
+
end
|
35
|
+
|
36
|
+
@ui = Kaitai::TUI.new
|
37
|
+
@tree = Tree.new(@ui, @data)
|
38
|
+
|
39
|
+
@tree.redraw
|
40
|
+
@ui.message_box_exception(load_exc) if load_exc
|
41
|
+
end
|
42
|
+
|
43
|
+
def run
|
44
|
+
@tree.run
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
require 'kaitai/struct/visualizer/version'
|
2
|
+
require 'kaitai/struct/visualizer/visualizer'
|
3
|
+
|
4
|
+
module Kaitai::Struct::Visualizer
|
5
|
+
|
6
|
+
class ExternalCompilerVisualizer < Visualizer
|
7
|
+
def compile_format(fn)
|
8
|
+
main_class_name = nil
|
9
|
+
Dir.mktmpdir { |code_dir|
|
10
|
+
system("ksc -- --debug -t ruby '#{fn}' -d '#{code_dir}'")
|
11
|
+
exit $?.exitstatus if $?.exitstatus != 0
|
12
|
+
|
13
|
+
compiled_path = Dir.glob("#{code_dir}/*.rb")[0]
|
14
|
+
|
15
|
+
require compiled_path
|
16
|
+
|
17
|
+
main_class_name = File.readlines(compiled_path).grep(/^class /)[0].strip.gsub(/^class /, '').gsub(/ <.*$/, '')
|
18
|
+
}
|
19
|
+
|
20
|
+
return main_class_name
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
require 'ks_ruby_compiler'
|
2
|
+
|
3
|
+
class RubyCompilerVisualizer extends Visualizer
|
4
|
+
def compile
|
5
|
+
Dir.mktmpdir { |code_dir|
|
6
|
+
compiled_path = "#{code_dir}/compiled.rb"
|
7
|
+
@compiler = CompileToRuby.new(@format_fn, compiled_path)
|
8
|
+
@compiler.compile
|
9
|
+
|
10
|
+
require compiled_path
|
11
|
+
|
12
|
+
main_class_name = @compiler.type2class(@compiler.desc['meta']['id'])
|
13
|
+
#puts "Main class: #{main_class_name}"
|
14
|
+
main_class = Kernel::const_get(main_class_name)
|
15
|
+
@data = main_class.from_file(@bin_fn)
|
16
|
+
}
|
17
|
+
end
|
18
|
+
end
|
data/lib/kaitai/tui.rb
ADDED
@@ -0,0 +1,169 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
require 'io/console'
|
3
|
+
|
4
|
+
module Kaitai
|
5
|
+
|
6
|
+
class TUI
|
7
|
+
attr_reader :cols
|
8
|
+
attr_reader :rows
|
9
|
+
|
10
|
+
def initialize
|
11
|
+
@cols = `tput cols`.to_i
|
12
|
+
@rows = `tput lines`.to_i
|
13
|
+
|
14
|
+
@seq_clear = `tput clear`
|
15
|
+
@seq_sgr0 = `tput sgr0`
|
16
|
+
@seq_fgcolor = []
|
17
|
+
@seq_bgcolor = []
|
18
|
+
end
|
19
|
+
|
20
|
+
def clear
|
21
|
+
print @seq_clear
|
22
|
+
end
|
23
|
+
|
24
|
+
##
|
25
|
+
# Put the cursor up to screen position (x, y). First line is 0,
|
26
|
+
# first column is 0.
|
27
|
+
def goto(x, y)
|
28
|
+
#print `tput cup #{y} #{x}`
|
29
|
+
printf "\e[%d;%dH", y + 1, x + 1
|
30
|
+
end
|
31
|
+
|
32
|
+
COLORS = {
|
33
|
+
:black => 0,
|
34
|
+
:gray => 7,
|
35
|
+
:gray0 => 232,
|
36
|
+
:gray1 => 233,
|
37
|
+
:gray2 => 234,
|
38
|
+
:gray3 => 235,
|
39
|
+
:gray4 => 236,
|
40
|
+
:gray5 => 237,
|
41
|
+
:gray6 => 238,
|
42
|
+
:gray7 => 239,
|
43
|
+
:gray8 => 240,
|
44
|
+
:gray9 => 241,
|
45
|
+
:gray10 => 242,
|
46
|
+
:gray11 => 243,
|
47
|
+
:gray12 => 244,
|
48
|
+
:gray13 => 245,
|
49
|
+
:gray14 => 246,
|
50
|
+
:gray15 => 247,
|
51
|
+
:gray16 => 248,
|
52
|
+
:gray17 => 249,
|
53
|
+
:gray18 => 250,
|
54
|
+
:gray19 => 251,
|
55
|
+
:gray20 => 252,
|
56
|
+
:gray21 => 253,
|
57
|
+
:gray22 => 254,
|
58
|
+
:gray23 => 255,
|
59
|
+
}
|
60
|
+
|
61
|
+
def fg_color=(col)
|
62
|
+
#print @seq_fgcolor[col] ||= `tput setaf #{col}`
|
63
|
+
code = COLORS[col]
|
64
|
+
raise "Invalid color: #{col}" unless code
|
65
|
+
print "\e[38;5;#{code}m"
|
66
|
+
end
|
67
|
+
|
68
|
+
def bg_color=(col)
|
69
|
+
#print @seq_bgcolor[col] ||= `tput setab #{col}`
|
70
|
+
code = COLORS[col]
|
71
|
+
raise "Invalid color: #{col}" unless code
|
72
|
+
print "\e[48;5;#{code}m"
|
73
|
+
end
|
74
|
+
|
75
|
+
def reset_colors
|
76
|
+
print @seq_sgr0
|
77
|
+
end
|
78
|
+
|
79
|
+
# Reads keypresses from the user including 2 and 3 escape character sequences.
|
80
|
+
def read_char
|
81
|
+
$stdin.echo = false
|
82
|
+
$stdin.raw!
|
83
|
+
|
84
|
+
input = $stdin.getc.chr
|
85
|
+
if input == "\e" then
|
86
|
+
input << $stdin.read_nonblock(3) rescue nil
|
87
|
+
input << $stdin.read_nonblock(2) rescue nil
|
88
|
+
end
|
89
|
+
ensure
|
90
|
+
$stdin.echo = true
|
91
|
+
$stdin.cooked!
|
92
|
+
|
93
|
+
return input
|
94
|
+
end
|
95
|
+
|
96
|
+
def read_char_mapped
|
97
|
+
c = read_char
|
98
|
+
c2 = KEY_MAP[c]
|
99
|
+
c2 ? c2 : c
|
100
|
+
end
|
101
|
+
|
102
|
+
KEY_MAP = {
|
103
|
+
"\t" => :tab,
|
104
|
+
"\r" => :enter,
|
105
|
+
"\e[A" => :up_arrow,
|
106
|
+
"\e[B" => :down_arrow,
|
107
|
+
"\e[C" => :right_arrow,
|
108
|
+
"\e[D" => :left_arrow,
|
109
|
+
"\e[5~" => :pg_up,
|
110
|
+
"\e[6~" => :pg_dn,
|
111
|
+
"\e[H" => :home,
|
112
|
+
"\e[F" => :end,
|
113
|
+
}
|
114
|
+
|
115
|
+
def message_box_exception(e)
|
116
|
+
message_box("Error while parsing", e.message)
|
117
|
+
end
|
118
|
+
|
119
|
+
SINGLE_CHARSET = '┌┐└┘─│'
|
120
|
+
HEAVY_CHARSET = '┏┓┗┛━┃'
|
121
|
+
DOUBLE_CHARSET = '╔╗╚╝═║'
|
122
|
+
|
123
|
+
CHAR_TL = 0
|
124
|
+
CHAR_TR = 1
|
125
|
+
CHAR_BL = 2
|
126
|
+
CHAR_BR = 3
|
127
|
+
CHAR_H = 4
|
128
|
+
CHAR_V = 5
|
129
|
+
|
130
|
+
def message_box(header, msg)
|
131
|
+
top_y = @rows / 2 - 5
|
132
|
+
draw_rectangle(10, top_y, @cols - 20, 10)
|
133
|
+
goto(@cols / 2 - (header.length / 2) - 1, top_y)
|
134
|
+
print ' ', header, ' '
|
135
|
+
goto(11, top_y + 1)
|
136
|
+
puts msg
|
137
|
+
draw_button(@cols / 2 - 10, top_y + 8, 10, 'OK')
|
138
|
+
loop {
|
139
|
+
c = read_char_mapped
|
140
|
+
return if c == :enter
|
141
|
+
}
|
142
|
+
end
|
143
|
+
|
144
|
+
def draw_rectangle(x, y, w, h, charset = DOUBLE_CHARSET)
|
145
|
+
goto(x, y)
|
146
|
+
print charset[CHAR_TL]
|
147
|
+
print charset[CHAR_H] * (w - 2)
|
148
|
+
print charset[CHAR_TR]
|
149
|
+
|
150
|
+
((y + 1)..(y + h - 1)).each { |i|
|
151
|
+
goto(x, i)
|
152
|
+
print charset[CHAR_V]
|
153
|
+
print ' ' * (w - 2)
|
154
|
+
print charset[CHAR_V]
|
155
|
+
}
|
156
|
+
|
157
|
+
goto(x, y + h)
|
158
|
+
print charset[CHAR_BL]
|
159
|
+
print charset[CHAR_H] * (w - 2)
|
160
|
+
print charset[CHAR_BR]
|
161
|
+
end
|
162
|
+
|
163
|
+
def draw_button(x, y, w, caption)
|
164
|
+
goto(x, y)
|
165
|
+
puts "[ #{caption} ]"
|
166
|
+
end
|
167
|
+
end
|
168
|
+
|
169
|
+
end
|