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,263 +1,234 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require 'benchmark'
|
2
4
|
|
3
5
|
require 'kaitai/struct/visualizer/version'
|
4
6
|
require 'kaitai/struct/visualizer/node'
|
5
7
|
require 'kaitai/struct/visualizer/hex_viewer'
|
8
|
+
require 'kaitai/struct/visualizer/ks_error_matcher'
|
6
9
|
|
7
10
|
module Kaitai::Struct::Visualizer
|
8
|
-
class Tree
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
11
|
+
class Tree
|
12
|
+
def initialize(ui, st)
|
13
|
+
@ui = ui
|
14
|
+
@st = st
|
15
|
+
@root = Node.new(self, st, 0)
|
16
|
+
@root.id = '[root]'
|
14
17
|
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
+
@cur_io = nil
|
19
|
+
@hv = HexViewer.new(ui, nil, self)
|
20
|
+
@hv_hidden = false
|
18
21
|
|
19
|
-
|
22
|
+
recalc_sizes
|
20
23
|
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
+
@cur_line = 0
|
25
|
+
@cur_shift = 0
|
26
|
+
@do_exit = false
|
24
27
|
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
28
|
+
@ui.on_resize = proc { |redraw_needed|
|
29
|
+
recalc_sizes
|
30
|
+
redraw if redraw_needed
|
31
|
+
@hv.redraw if redraw_needed
|
32
|
+
}
|
33
|
+
end
|
31
34
|
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
35
|
+
def recalc_sizes
|
36
|
+
@max_scr_ln = @ui.rows - 3
|
37
|
+
@hv.shift_x = @ui.cols - HexViewer.line_width - 1
|
38
|
+
end
|
36
39
|
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
t = redraw
|
40
|
+
def run
|
41
|
+
loop do
|
42
|
+
t = redraw
|
41
43
|
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
44
|
+
if @cur_node.nil? && !@cur_line.nil?
|
45
|
+
# gone beyond the end of the tree
|
46
|
+
@cur_line = @root.height - 1
|
47
|
+
clamp_cursor
|
48
|
+
redraw
|
49
|
+
end
|
48
50
|
|
49
|
-
|
50
|
-
|
51
|
+
raise '@cur_line is undetermined' if @cur_line.nil?
|
52
|
+
raise '@cur_node is undetermined' if @cur_node.nil?
|
51
53
|
|
52
|
-
|
53
|
-
|
54
|
-
|
54
|
+
thv = Benchmark.realtime do
|
55
|
+
unless @hv_hidden
|
56
|
+
hv_update_io
|
55
57
|
|
56
|
-
|
57
|
-
if (@hv.addr < @cur_node.pos1) or (@hv.addr >= @cur_node.pos2)
|
58
|
+
if !@cur_node.pos1.nil? && ((@hv.addr < @cur_node.pos1) || (@hv.addr >= @cur_node.pos2))
|
58
59
|
@hv.addr = @cur_node.pos1
|
59
60
|
@hv.ensure_visible
|
60
61
|
end
|
61
|
-
end
|
62
62
|
|
63
|
-
|
64
|
-
|
65
|
-
|
63
|
+
@hv.redraw
|
64
|
+
regs = highlight_regions(4)
|
65
|
+
@hv.highlight(regs)
|
66
|
+
end
|
66
67
|
end
|
67
|
-
}
|
68
68
|
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
begin
|
75
|
-
process_keypress
|
76
|
-
rescue EOFError => e
|
77
|
-
@ui.message_box_exception(e)
|
78
|
-
rescue Kaitai::Struct::Stream::UnexpectedDataError => e
|
79
|
-
@ui.message_box_exception(e)
|
80
|
-
end
|
69
|
+
@ui.goto(0, @max_scr_ln + 1)
|
70
|
+
printf 'all: %d, tree: %d, tree_draw: %d, hexview: %d, ln: %d, ', (t + thv) * 1e6, t * 1e6, @draw_time * 1e6, thv * 1e6, @ln
|
71
|
+
puts "highlight = #{@cur_node.pos1}..#{@cur_node.pos2}"
|
72
|
+
# puts "keypress: #{c.inspect}"
|
81
73
|
|
82
|
-
|
74
|
+
begin
|
75
|
+
process_keypress
|
76
|
+
rescue Kaitai::Struct::Visualizer::KSErrorMatcher => e
|
77
|
+
@ui.message_box_exception(e)
|
78
|
+
end
|
83
79
|
|
84
|
-
|
85
|
-
}
|
86
|
-
end
|
80
|
+
return if @do_exit
|
87
81
|
|
88
|
-
|
89
|
-
c = @ui.read_char_mapped
|
90
|
-
case c
|
91
|
-
when :up_arrow
|
92
|
-
@cur_line -= 1
|
93
|
-
@cur_node = nil
|
94
|
-
when :down_arrow
|
95
|
-
@cur_line += 1
|
96
|
-
@cur_node = nil
|
97
|
-
when :left_arrow
|
98
|
-
if @cur_node.open?
|
99
|
-
@cur_node.close
|
100
|
-
else
|
101
|
-
par = @cur_node.parent
|
102
|
-
if par
|
103
|
-
@cur_line = nil
|
104
|
-
@cur_node = par
|
105
|
-
end
|
82
|
+
clamp_cursor
|
106
83
|
end
|
107
|
-
|
108
|
-
|
84
|
+
end
|
85
|
+
|
86
|
+
def process_keypress
|
87
|
+
c = @ui.read_char_mapped
|
88
|
+
case c
|
89
|
+
when :up_arrow
|
90
|
+
@cur_line -= 1
|
91
|
+
@cur_node = nil
|
92
|
+
when :down_arrow
|
93
|
+
@cur_line += 1
|
94
|
+
@cur_node = nil
|
95
|
+
when :left_arrow
|
109
96
|
if @cur_node.open?
|
110
|
-
@
|
111
|
-
@cur_node = nil
|
97
|
+
@cur_node.close
|
112
98
|
else
|
113
|
-
@cur_node.
|
99
|
+
par = @cur_node.parent
|
100
|
+
if par
|
101
|
+
@cur_line = nil
|
102
|
+
@cur_node = par
|
103
|
+
end
|
114
104
|
end
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
|
128
|
-
|
129
|
-
|
130
|
-
|
131
|
-
|
132
|
-
|
133
|
-
|
105
|
+
when :right_arrow
|
106
|
+
if @cur_node.openable?
|
107
|
+
if @cur_node.open?
|
108
|
+
@cur_line += 1
|
109
|
+
@cur_node = nil
|
110
|
+
else
|
111
|
+
@cur_node.open
|
112
|
+
end
|
113
|
+
end
|
114
|
+
when :home
|
115
|
+
@cur_line = @cur_shift = 0
|
116
|
+
@cur_node = nil
|
117
|
+
when :end
|
118
|
+
@cur_line = @root.height - 1
|
119
|
+
@cur_node = nil
|
120
|
+
when :pg_up
|
121
|
+
@cur_line -= 20
|
122
|
+
@cur_node = nil
|
123
|
+
when :pg_dn
|
124
|
+
@cur_line += 20
|
125
|
+
@cur_node = nil
|
126
|
+
when :enter
|
127
|
+
if @cur_node.hex?
|
128
|
+
@ui.clear
|
129
|
+
hv = HexViewer.new(@ui, @cur_node.value)
|
130
|
+
hv.redraw
|
131
|
+
hv.run
|
132
|
+
@ui.clear
|
133
|
+
redraw
|
134
|
+
else
|
135
|
+
@cur_node.toggle
|
136
|
+
end
|
137
|
+
when :tab
|
138
|
+
@hv.run
|
139
|
+
when 'H'
|
140
|
+
@hv_hidden = !@hv_hidden
|
134
141
|
@ui.clear
|
135
142
|
redraw
|
136
|
-
|
137
|
-
@
|
143
|
+
when 'q'
|
144
|
+
@do_exit = true
|
138
145
|
end
|
139
|
-
when :tab
|
140
|
-
@hv.run
|
141
|
-
when 'H'
|
142
|
-
@hv_hidden = !@hv_hidden
|
143
|
-
@ui.clear
|
144
|
-
redraw
|
145
|
-
when 'q'
|
146
|
-
@do_exit = true
|
147
146
|
end
|
148
|
-
end
|
149
147
|
|
150
|
-
|
151
|
-
|
152
|
-
@cur_line = 0 if @cur_line < 0
|
148
|
+
def clamp_cursor
|
149
|
+
return unless @cur_line
|
153
150
|
|
154
|
-
|
155
|
-
|
156
|
-
|
157
|
-
if @cur_line - @cur_shift > @max_scr_ln
|
158
|
-
|
151
|
+
@cur_line = 0 if @cur_line.negative?
|
152
|
+
|
153
|
+
@cur_shift = @cur_line if (@cur_line - @cur_shift).negative?
|
154
|
+
@cur_shift = @cur_line - @max_scr_ln if (@cur_line - @cur_shift) > @max_scr_ln
|
155
|
+
end
|
156
|
+
|
157
|
+
def redraw
|
158
|
+
@draw_time = 0
|
159
|
+
Benchmark.realtime do
|
160
|
+
@ui.clear
|
161
|
+
@ln = 0
|
162
|
+
draw_rec(@root)
|
159
163
|
end
|
160
164
|
end
|
161
|
-
end
|
162
165
|
|
163
|
-
|
164
|
-
|
165
|
-
|
166
|
-
|
167
|
-
@ln
|
168
|
-
|
169
|
-
|
170
|
-
|
166
|
+
def draw_rec(n)
|
167
|
+
scr_ln = @ln - @cur_shift
|
168
|
+
return if @cur_node && (scr_ln > @max_scr_ln)
|
169
|
+
|
170
|
+
if @ln == @cur_line
|
171
|
+
# Seeking cur_node by cur_line
|
172
|
+
@cur_node = n
|
173
|
+
@ui.bg_color = :white
|
174
|
+
@ui.fg_color = :black
|
175
|
+
elsif @cur_node == n
|
176
|
+
# Seeking cur_line by cur_node
|
177
|
+
@cur_line = @ln
|
178
|
+
@ui.bg_color = :white
|
179
|
+
@ui.fg_color = :black
|
180
|
+
end
|
171
181
|
|
172
|
-
|
173
|
-
|
174
|
-
|
175
|
-
|
176
|
-
if @ln == @cur_line
|
177
|
-
# Seeking cur_node by cur_line
|
178
|
-
@cur_node = n
|
179
|
-
@ui.bg_color = :gray
|
180
|
-
@ui.fg_color = :black
|
181
|
-
elsif @cur_node == n
|
182
|
-
# Seeking cur_line by cur_node
|
183
|
-
@cur_line = @ln
|
184
|
-
@ui.bg_color = :gray
|
185
|
-
@ui.fg_color = :black
|
186
|
-
end
|
182
|
+
@draw_time += Benchmark.realtime do
|
183
|
+
# n.draw(@ui) if scr_ln >= 0
|
184
|
+
n.draw(@ui) if (scr_ln >= 0) && (scr_ln <= @max_scr_ln)
|
185
|
+
end
|
187
186
|
|
188
|
-
|
189
|
-
|
190
|
-
n.draw(@ui) if scr_ln >= 0 and scr_ln <= @max_scr_ln
|
191
|
-
}
|
187
|
+
@ui.reset_colors if @ln == @cur_line
|
188
|
+
@ln += 1
|
192
189
|
|
193
|
-
|
194
|
-
|
195
|
-
|
196
|
-
n.children.each { |ch|
|
190
|
+
return unless n.open?
|
191
|
+
|
192
|
+
n.children.each do |ch|
|
197
193
|
draw_rec(ch)
|
198
194
|
break if scr_ln > @max_scr_ln
|
199
|
-
|
195
|
+
end
|
200
196
|
end
|
201
|
-
end
|
202
197
|
|
203
|
-
|
204
|
-
|
205
|
-
|
198
|
+
def do_exit
|
199
|
+
@do_exit = true
|
200
|
+
end
|
201
|
+
|
202
|
+
def hv_update_io
|
203
|
+
io = @cur_node.io
|
204
|
+
return unless io != @cur_io
|
206
205
|
|
207
|
-
def hv_update_io
|
208
|
-
io = @cur_node.io
|
209
|
-
if io != @cur_io
|
210
206
|
@cur_io = io
|
211
207
|
io.seek(0)
|
212
208
|
buf = io.read_bytes_full
|
213
209
|
@hv.buf = buf
|
214
210
|
|
215
|
-
#
|
211
|
+
# @hv.redraw
|
216
212
|
end
|
217
|
-
end
|
218
213
|
|
219
|
-
|
220
|
-
|
221
|
-
|
222
|
-
|
223
|
-
|
224
|
-
r << [node.pos1, node.pos2]
|
225
|
-
node = node.parent
|
226
|
-
}
|
227
|
-
r
|
228
|
-
end
|
214
|
+
def highlight_regions(max_levels)
|
215
|
+
node = @cur_node
|
216
|
+
r = []
|
217
|
+
max_levels.times do |_i|
|
218
|
+
return r if node.nil?
|
229
219
|
|
230
|
-
|
231
|
-
|
232
|
-
|
233
|
-
|
234
|
-
@hv.shift_x
|
220
|
+
r << [node.pos1, node.pos2]
|
221
|
+
node = node.parent
|
222
|
+
end
|
223
|
+
r
|
235
224
|
end
|
236
|
-
end
|
237
225
|
|
238
|
-
|
239
|
-
|
240
|
-
|
241
|
-
|
242
|
-
|
243
|
-
|
244
|
-
obj.each_with_index { |el, i|
|
245
|
-
n = explore_object(el, level + 1)
|
246
|
-
n.id = i
|
247
|
-
root.add(n)
|
248
|
-
}
|
249
|
-
else
|
250
|
-
root = Node.new(obj, level)
|
251
|
-
obj.instance_variables.each { |k|
|
252
|
-
k = k.to_s
|
253
|
-
next if k =~ /^@_/
|
254
|
-
el = obj.instance_eval(k)
|
255
|
-
n = explore_object(el, level + 1)
|
256
|
-
n.id = k
|
257
|
-
root.add(n)
|
258
|
-
}
|
226
|
+
def tree_width
|
227
|
+
if @hv_hidden
|
228
|
+
@ui.cols
|
229
|
+
else
|
230
|
+
@hv.shift_x
|
231
|
+
end
|
259
232
|
end
|
260
|
-
root
|
261
233
|
end
|
262
234
|
end
|
263
|
-
end
|
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require 'kaitai/struct/visualizer/version'
|
2
4
|
require 'kaitai/tui'
|
3
5
|
require 'kaitai/struct/visualizer/parser'
|
@@ -8,17 +10,17 @@ require 'zlib'
|
|
8
10
|
require 'stringio'
|
9
11
|
|
10
12
|
module Kaitai::Struct::Visualizer
|
11
|
-
class Visualizer < Parser
|
12
|
-
|
13
|
-
|
13
|
+
class Visualizer < Parser
|
14
|
+
def run
|
15
|
+
load_exc = load
|
14
16
|
|
15
|
-
|
16
|
-
|
17
|
+
@ui = Kaitai::TUI.new
|
18
|
+
@tree = Tree.new(@ui, @data)
|
17
19
|
|
18
|
-
|
19
|
-
|
20
|
+
@tree.redraw
|
21
|
+
@ui.message_box_exception(load_exc) if load_exc
|
20
22
|
|
21
|
-
|
23
|
+
@tree.run
|
24
|
+
end
|
22
25
|
end
|
23
26
|
end
|
24
|
-
end
|
data/lib/kaitai/tui.rb
CHANGED
@@ -1,113 +1,100 @@
|
|
1
|
-
#
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
2
3
|
require 'forwardable'
|
3
4
|
|
4
5
|
module Kaitai
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
:white,
|
22
|
-
]
|
23
|
-
else
|
24
|
-
require 'kaitai/console_ansi'
|
25
|
-
@console = ConsoleANSI.new
|
26
|
-
@highlight_colors = [
|
27
|
-
:gray14,
|
28
|
-
:gray11,
|
29
|
-
:gray8,
|
30
|
-
:gray5,
|
31
|
-
:gray2,
|
32
|
-
]
|
6
|
+
class TUI
|
7
|
+
extend Forwardable
|
8
|
+
def_delegators :@console, :rows, :cols, :goto, :clear, :fg_color=, :bg_color=, :reset_colors, :read_char_mapped
|
9
|
+
|
10
|
+
attr_reader :highlight_colors
|
11
|
+
|
12
|
+
def initialize
|
13
|
+
if TUI.windows?
|
14
|
+
require 'kaitai/console_windows'
|
15
|
+
@console = ConsoleWindows.new
|
16
|
+
@highlight_colors = %i[bright_white bright_cyan cyan gray]
|
17
|
+
else
|
18
|
+
require 'kaitai/console_ansi'
|
19
|
+
@console = ConsoleANSI.new
|
20
|
+
@highlight_colors = %i[bright_white bright_cyan cyan gray]
|
21
|
+
end
|
33
22
|
end
|
34
|
-
end
|
35
23
|
|
36
|
-
|
37
|
-
|
38
|
-
|
24
|
+
def on_resize=(handler)
|
25
|
+
@console.on_resize = handler
|
26
|
+
end
|
39
27
|
|
40
|
-
|
41
|
-
|
42
|
-
|
28
|
+
def message_box_exception(e)
|
29
|
+
message_box('Error while parsing', e.message)
|
30
|
+
end
|
43
31
|
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
32
|
+
SINGLE_CHARSET = '┌┐└┘─│'
|
33
|
+
HEAVY_CHARSET = '┏┓┗┛━┃'
|
34
|
+
DOUBLE_CHARSET = '╔╗╚╝═║'
|
35
|
+
|
36
|
+
CHAR_TL = 0
|
37
|
+
CHAR_TR = 1
|
38
|
+
CHAR_BL = 2
|
39
|
+
CHAR_BR = 3
|
40
|
+
CHAR_H = 4
|
41
|
+
CHAR_V = 5
|
42
|
+
|
43
|
+
def message_box(header, msg)
|
44
|
+
top_y = @console.rows / 2 - 5
|
45
|
+
draw_rectangle(10, top_y, @console.cols - 20, 10)
|
46
|
+
@console.goto(@console.cols / 2 - (header.length / 2) - 1, top_y)
|
47
|
+
print ' ', header, ' '
|
48
|
+
@console.goto(11, top_y + 1)
|
49
|
+
puts msg
|
50
|
+
draw_button(@console.cols / 2 - 10, top_y + 8, 10, 'OK')
|
51
|
+
loop do
|
52
|
+
c = @console.read_char_mapped
|
53
|
+
return if c == :enter
|
54
|
+
end
|
55
|
+
end
|
68
56
|
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
57
|
+
def input_str(header, _msg)
|
58
|
+
top_y = @console.rows / 2 - 5
|
59
|
+
draw_rectangle(10, top_y, @console.cols - 20, 10)
|
60
|
+
goto(@console.cols / 2 - (header.length / 2) - 1, top_y)
|
61
|
+
print ' ', header, ' '
|
74
62
|
|
75
|
-
|
76
|
-
|
77
|
-
|
63
|
+
goto(11, top_y + 1)
|
64
|
+
Readline.readline('', false)
|
65
|
+
end
|
78
66
|
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
67
|
+
def draw_rectangle(x, y, w, h, charset = DOUBLE_CHARSET)
|
68
|
+
goto(x, y)
|
69
|
+
print charset[CHAR_TL]
|
70
|
+
print charset[CHAR_H] * (w - 2)
|
71
|
+
print charset[CHAR_TR]
|
72
|
+
|
73
|
+
((y + 1)..(y + h - 1)).each do |i|
|
74
|
+
goto(x, i)
|
75
|
+
print charset[CHAR_V]
|
76
|
+
print ' ' * (w - 2)
|
77
|
+
print charset[CHAR_V]
|
78
|
+
end
|
79
|
+
|
80
|
+
goto(x, y + h)
|
81
|
+
print charset[CHAR_BL]
|
82
|
+
print charset[CHAR_H] * (w - 2)
|
83
|
+
print charset[CHAR_BR]
|
84
|
+
end
|
97
85
|
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
86
|
+
def draw_button(x, y, _w, caption)
|
87
|
+
goto(x, y)
|
88
|
+
puts "[ #{caption} ]"
|
89
|
+
end
|
102
90
|
|
103
|
-
|
104
|
-
|
105
|
-
|
91
|
+
# Regexp borrowed from
|
92
|
+
# http://stackoverflow.com/questions/170956/how-can-i-find-which-operating-system-my-ruby-program-is-running-on
|
93
|
+
@@is_windows = (RUBY_PLATFORM =~ /cygwin|mswin|mingw|bccwin|wince|emx/) ? true : false
|
106
94
|
|
107
|
-
|
108
|
-
|
109
|
-
|
95
|
+
# Detects whether the current platform is Windows-based.
|
96
|
+
def self.windows?
|
97
|
+
@@is_windows
|
98
|
+
end
|
110
99
|
end
|
111
100
|
end
|
112
|
-
|
113
|
-
end
|