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.
@@ -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,7 @@
1
+ module Kaitai
2
+ module Struct
3
+ module Visualizer
4
+ VERSION = '0.3'
5
+ end
6
+ end
7
+ 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