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.
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'kaitai/struct/visualizer/version'
2
4
  require 'kaitai/tui'
3
5
 
@@ -5,19 +7,97 @@ require 'open3'
5
7
  require 'json'
6
8
  require 'tmpdir'
7
9
 
10
+ require 'psych'
11
+
8
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
9
36
 
10
- class KSYCompiler
11
- def initialize(opts, out = $stderr)
12
- @opts = opts
13
- @out = out
14
- end
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
15
70
 
16
- def compile_formats(fns)
17
- errs = false
18
- main_class_name = nil
19
- Dir.mktmpdir { |code_dir|
20
- args = ['--ksc-json-output', '--debug', '-t', 'ruby', *fns, '-d', code_dir]
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]
21
101
 
22
102
  # Extra arguments
23
103
  extra = []
@@ -31,22 +111,17 @@ class KSYCompiler
31
111
  # '-d' (which allows to pass defines to JVM). Windows-based systems
32
112
  # do not need and do not support this extra '--', so we don't add it
33
113
  # on Windows.
34
- args.unshift('--') unless Kaitai::TUI::is_windows?
35
-
36
- status = nil
37
- log_str = nil
38
- err_str = nil
39
- Open3.popen3('kaitai-struct-compiler', *args) { |stdin, stdout, stderr, wait_thr|
40
- status = wait_thr.value
41
- log_str = stdout.read
42
- err_str = stderr.read
43
- }
44
-
45
- if not status.success?
46
- if status.exitstatus == 127
47
- @out.puts "ksv: unable to find and execute kaitai-struct-compiler in your PATH"
48
- elsif err_str =~ /Error: Unknown option --ksc-json-output/
49
- @out.puts "ksv: your kaitai-struct-compiler is too old:"
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:"
50
125
  system('kaitai-struct-compiler', '--version')
51
126
  @out.puts "\nPlease use at least v0.7."
52
127
  else
@@ -61,56 +136,108 @@ class KSYCompiler
61
136
  exit status.exitstatus
62
137
  end
63
138
 
64
- log = JSON.load(log_str)
65
-
66
- # FIXME: add log results check
67
- @out.puts "Compilation OK"
68
-
69
- fns.each_with_index { |fn, idx|
70
- @out.puts "... processing #{fn} #{idx}"
139
+ JSON.parse(log_str)
140
+ end
71
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|
72
154
  log_fn = log[fn]
73
155
  if log_fn['errors']
74
156
  report_err(log_fn['errors'])
75
157
  errs = true
76
158
  else
77
159
  log_classes = log_fn['output']['ruby']
78
- log_classes.each_pair { |k, v|
160
+ log_classes.each_pair do |_k, v|
79
161
  if v['errors']
80
162
  report_err(v['errors'])
81
163
  errs = true
82
164
  else
83
165
  compiled_name = v['files'][0]['fileName']
84
- compiled_path = "#{code_dir}/#{compiled_name}"
166
+ compiled_path = File.join(code_dir, compiled_name)
85
167
 
86
- @out.puts "...... loading #{compiled_name}"
87
168
  require compiled_path
88
169
  end
89
- }
170
+ end
90
171
 
91
172
  # Is it main ClassSpecs?
92
- if idx == 0
173
+ if idx.zero?
93
174
  main = log_classes[log_fn['firstSpecName']]
94
175
  main_class_name = main['topLevelName']
95
176
  end
96
177
  end
97
- }
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']
98
190
 
99
- }
191
+ if row.nil? && err['path']
192
+ begin
193
+ node = resolve_yaml_path(err['file'], err['path'])
100
194
 
101
- if errs
102
- @out.puts "Fatal errors encountered, cannot continue"
103
- exit 1
104
- else
105
- @out.puts "Classes loaded OK, main class = #{main_class_name}"
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
106
214
  end
107
215
 
108
- return main_class_name
109
- end
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
110
226
 
111
- def report_err(err)
112
- @out.puts "Error: #{err.inspect}"
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
113
242
  end
114
243
  end
115
-
116
- end