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.
@@ -0,0 +1,243 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'kaitai/struct/visualizer/version'
4
+ require 'kaitai/tui'
5
+
6
+ require 'open3'
7
+ require 'json'
8
+ require 'tmpdir'
9
+
10
+ require 'psych'
11
+
12
+ module Kaitai::Struct::Visualizer
13
+ class KSYCompiler
14
+ # Initializes a new instance of the KSYCompiler class that is used to
15
+ # compile Kaitai Struct formats into Ruby classes by invoking the
16
+ # command line kaitai-struct-compiler.
17
+ #
18
+ # @param [Hash] opts Options
19
+ # @option opts [String] :outdir Output directory for compiled code; if
20
+ # not specified, a temporary directory will be used that will be
21
+ # deleted after the compilation is done
22
+ # @option opts [String] :import_path Additional import paths
23
+ # @option opts [String] :opaque_types "true" or "false" to enable or
24
+ # disable opaque types
25
+ #
26
+ # @param [String] prog_name Program name to be used as a prefix in
27
+ # error messages
28
+ # @param [IO] out IO stream to write error/warning messages to
29
+ def initialize(opts, prog_name = 'ksv', out = $stderr)
30
+ @opts = opts
31
+ @prog_name = prog_name
32
+ @out = out
33
+
34
+ @outdir = opts[:outdir]
35
+ end
36
+
37
+ def compile_formats_if(fns)
38
+ return compile_formats(fns) if (fns.length > 1) || fns[0].end_with?('.ksy')
39
+
40
+ fname = File.basename(fns[0], '.rb')
41
+ dname = File.dirname(fns[0])
42
+ gpath = File.expand_path('*.rb', dname)
43
+
44
+ Dir.glob(gpath) do |fn|
45
+ require File.expand_path(fn, dname)
46
+ end
47
+
48
+ # The name of the main class is that of the given file by convention.
49
+ fname.split('_').map(&:capitalize).join
50
+ end
51
+
52
+ # Compiles Kaitai Struct formats into Ruby classes by invoking the
53
+ # command line kaitai-struct-compiler, and loads the generated Ruby
54
+ # files into current Ruby interpreter by running `require` on them.
55
+ #
56
+ # If the :outdir option was specified, the compiled code will be
57
+ # stored in that directory. Otherwise, a temporary directory will be
58
+ # used that will be deleted after the compilation and loading is done.
59
+ #
60
+ # @param [Array<String>] fns List of Kaitai Struct format files to
61
+ # compile
62
+ # @return [String] Main class name, or nil if were errors
63
+ def compile_formats(fns)
64
+ if @outdir.nil?
65
+ main_class_name = nil
66
+ Dir.mktmpdir { |code_dir| main_class_name = compile_and_load(fns, code_dir) }
67
+ else
68
+ main_class_name = compile_and_load(fns, @outdir)
69
+ end
70
+
71
+ if main_class_name.nil?
72
+ @out.puts 'Fatal errors encountered, cannot continue'
73
+ exit 1
74
+ end
75
+
76
+ main_class_name
77
+ end
78
+
79
+ # Compiles Kaitai Struct formats into Ruby classes by invoking the
80
+ # command line kaitai-struct-compiler, and loads the generated Ruby
81
+ # files into current Ruby interpreter by running `require` on them.
82
+ #
83
+ # @param [Array<String>] fns List of Kaitai Struct format files to
84
+ # compile
85
+ # @param [String] code_dir Directory to store the compiled code in
86
+ # @return [String] Main class name, or nil if were errors
87
+ def compile_and_load(fns, code_dir)
88
+ log = compile_formats_to_output(fns, code_dir)
89
+ load_ruby_files(fns, code_dir, log)
90
+ end
91
+
92
+ # Compiles Kaitai Struct formats into Ruby classes by invoking the
93
+ # command line kaitai-struct-compiler.
94
+ #
95
+ # @param [Array<String>] fns List of Kaitai Struct format files to
96
+ # compile
97
+ # @param [String] code_dir Directory to store the compiled code in
98
+ # @return [Hash] Structured output of kaitai-struct-compiler
99
+ def compile_formats_to_output(fns, code_dir)
100
+ args = ['--ksc-json-output', '--debug', '-t', 'ruby', '-d', code_dir, *fns]
101
+
102
+ # Extra arguments
103
+ extra = []
104
+ extra += ['--import-path', @opts[:import_path]] if @opts[:import_path]
105
+ extra += ['--opaque-types', @opts[:opaque_types]] if @opts[:opaque_types]
106
+
107
+ args = extra + args
108
+
109
+ # UNIX-based systems run ksc via a shell wrapper that requires
110
+ # extra '--' in invocation to disambiguate our '-d' from java runner
111
+ # '-d' (which allows to pass defines to JVM). Windows-based systems
112
+ # do not need and do not support this extra '--', so we don't add it
113
+ # on Windows.
114
+ args.unshift('--') unless Kaitai::TUI.windows?
115
+
116
+ begin
117
+ log_str, err_str, status = Open3.capture3('kaitai-struct-compiler', *args)
118
+ rescue Errno::ENOENT
119
+ @out.puts "#{@prog_name}: unable to find and execute kaitai-struct-compiler in your PATH"
120
+ exit 1
121
+ end
122
+ unless status.success?
123
+ if err_str =~ /Error: Unknown option --ksc-json-output/
124
+ @out.puts "#{@prog_name}: your kaitai-struct-compiler is too old:"
125
+ system('kaitai-struct-compiler', '--version')
126
+ @out.puts "\nPlease use at least v0.7."
127
+ else
128
+ @out.puts "ksc crashed (exit status = #{status}):\n"
129
+ @out.puts "== STDOUT\n"
130
+ @out.puts log_str
131
+ @out.puts
132
+ @out.puts "== STDERR\n"
133
+ @out.puts err_str
134
+ @out.puts
135
+ end
136
+ exit status.exitstatus
137
+ end
138
+
139
+ JSON.parse(log_str)
140
+ end
141
+
142
+ # Loads Ruby files generated by kaitai-struct-compiler into current Ruby interpreter
143
+ # by running `require` on them.
144
+ #
145
+ # @param [Array<String>] fns List of Kaitai Struct format files that were compiled
146
+ # @param [String] code_dir Directory where the compiled Ruby files are stored
147
+ # @param [Hash] log Structured output of kaitai-struct-compiler
148
+ # @return [String] Main class name, or nil if were errors
149
+ def load_ruby_files(fns, code_dir, log)
150
+ errs = false
151
+ main_class_name = nil
152
+
153
+ fns.each_with_index do |fn, idx|
154
+ log_fn = log[fn]
155
+ if log_fn['errors']
156
+ report_err(log_fn['errors'])
157
+ errs = true
158
+ else
159
+ log_classes = log_fn['output']['ruby']
160
+ log_classes.each_pair do |_k, v|
161
+ if v['errors']
162
+ report_err(v['errors'])
163
+ errs = true
164
+ else
165
+ compiled_name = v['files'][0]['fileName']
166
+ compiled_path = File.join(code_dir, compiled_name)
167
+
168
+ require compiled_path
169
+ end
170
+ end
171
+
172
+ # Is it main ClassSpecs?
173
+ if idx.zero?
174
+ main = log_classes[log_fn['firstSpecName']]
175
+ main_class_name = main['topLevelName']
176
+ end
177
+ end
178
+ end
179
+
180
+ errs ? nil : main_class_name
181
+ end
182
+
183
+ def report_err(errs)
184
+ @out.puts((errs.length > 1 ? 'Errors' : 'Error') + ":\n\n")
185
+ errs.each do |err|
186
+ @out << err['file']
187
+
188
+ row = err['line']
189
+ col = err['col']
190
+
191
+ if row.nil? && err['path']
192
+ begin
193
+ node = resolve_yaml_path(err['file'], err['path'])
194
+
195
+ # Psych line numbers are 0-based, but we want 1-based
196
+ row = node.start_line + 1
197
+
198
+ # Psych column numbers are 0-based, but we want 1-based
199
+ col = node.start_column + 1
200
+ rescue StandardError
201
+ row = '!'
202
+ col = '!'
203
+ end
204
+ end
205
+
206
+ if row
207
+ @out << ':' << row
208
+ @out << ':' << col if col
209
+ end
210
+
211
+ @out << ':/' << err['path'].join('/') if err['path']
212
+ @out << ': ' << err['message'] << "\n"
213
+ end
214
+ end
215
+
216
+ # Parses YAML file using Ruby's mid-level Psych API and resolve YAML
217
+ # path reported by ksc to row & column.
218
+ def resolve_yaml_path(file, path)
219
+ doc = Psych.parse(File.read(file))
220
+ yaml = doc.children[0]
221
+ path.each do |path_part|
222
+ yaml = psych_find(yaml, path_part)
223
+ end
224
+ yaml
225
+ end
226
+
227
+ def psych_find(yaml, path_part)
228
+ if yaml.is_a?(Psych::Nodes::Mapping)
229
+ # mapping are key-values, which are represented as [k1, v1, k2, v2, ...]
230
+ yaml.children.each_slice(2) do |map_key, map_value|
231
+ return map_value if map_key.value == path_part
232
+ end
233
+ nil
234
+ elsif yaml.is_a?(Psych::Nodes::Sequence)
235
+ # sequences are just integer-indexed arrays - [a0, a1, a2, ...]
236
+ idx = Integer(path_part)
237
+ yaml.children[idx]
238
+ else
239
+ raise "Unknown Psych component encountered: #{yaml.class}"
240
+ end
241
+ end
242
+ end
243
+ end