kaitai-struct-visualizer 0.3
Sign up to get free protection for your applications and to get access to all the features.
- 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
|