kaitai-struct 0.4 → 0.9

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
- SHA1:
3
- metadata.gz: 023b59d1105bf10850106f5bf1ace310c9d532b7
4
- data.tar.gz: 1ad3a5d47c5ce5be18b79bfbcd9910b1f0d9ed7b
2
+ SHA256:
3
+ metadata.gz: 3eb5375d9ddf8772844c25d3319de5be17eaafc71d5da835191e5e61ee84fec7
4
+ data.tar.gz: 82d10de0ba0c3bb004286df6a38febe40fb9b3223a7e24a6386c6ca854386b7e
5
5
  SHA512:
6
- metadata.gz: 5d5b1a4726c8c5c010baaca1cb771daf32762b84fd77cf408b91ebadd6e6a865d3fba69b32deb9618a3463e6c68789c2e9ec7cc401524939c729ab519b832457
7
- data.tar.gz: 0a3dc2c987d2a327bae5b0ecff06bde62c6039df1180416aad5f4d966fab8d618b0462f29d3f64e7858caee032b7c64f572b6a4579d74eaf461f89830c7f3d54
6
+ metadata.gz: db13f61f4b06680cb5b1e2b156fd466aa1c80a3a1e2c0ddad9a181ecdd4e66c9099c70de4cb05e25d6279ea007263db7e591d2ae0672fdba5431b93aa2b1e955
7
+ data.tar.gz: 3688b04940d1b4e8fa06cc3434b5ddc441387f6a62af5811cfc66cbfbf444565393000022df3b8ea997e6034968ff3a8067d258cc5e000969880986ea37a2197
data/.gitignore CHANGED
@@ -1 +1 @@
1
- /test/compiled
1
+ *.gem
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2015-2019 Kaitai Project
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
data/README.md CHANGED
@@ -1,65 +1,41 @@
1
- # Kaitai Struct: runtime library for Ruby
2
-
3
- This library implements Kaitai Struct API for Ruby.
4
-
5
- Kaitai Struct is a declarative language used for describe various binary
6
- data structures, laid out in files or in memory: i.e. binary file
7
- formats, network stream packet formats, etc.
8
-
9
- Further reading:
10
-
11
- * [About Kaitai Struct](https://github.com/kaitai-io/kaitai_struct/)
12
- * [About API implemented in this library](https://github.com/kaitai-io/kaitai_struct/wiki/Kaitai-Struct-stream-API)
13
- * [Ruby-specific notes](https://github.com/kaitai-io/kaitai_struct/wiki/Ruby)
14
-
15
- ## Installing
16
-
17
- ### Using `Gemfile`
18
-
19
- If your project uses Bundler, just include the line
20
-
21
- ```
22
- gem 'kaitai-struct'
23
- ```
24
-
25
- in your project's `Gemfile`.
26
-
27
- ### Using `gem install`
28
-
29
- If you have a RubyGems package manager installed, you can use command
30
-
31
- ```
32
- gem install kaitai-struct
33
- ```
34
-
35
- to install this runtime library.
36
-
37
- ### Manually
38
-
39
- This library is intentionally kept as very simple, one-file `.rb`
40
- file. One can just copy it to your project from this
41
- repository. Usually you won't `require` it directly, it will be loaded
42
- by Ruby source code generate by Kaitai Struct compiler.
43
-
44
- ## Licensing
45
-
46
- Copyright 2015-2016 Kaitai Project: MIT license
47
-
48
- Permission is hereby granted, free of charge, to any person obtaining
49
- a copy of this software and associated documentation files (the
50
- "Software"), to deal in the Software without restriction, including
51
- without limitation the rights to use, copy, modify, merge, publish,
52
- distribute, sublicense, and/or sell copies of the Software, and to
53
- permit persons to whom the Software is furnished to do so, subject to
54
- the following conditions:
55
-
56
- The above copyright notice and this permission notice shall be
57
- included in all copies or substantial portions of the Software.
58
-
59
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
60
- EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
61
- MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
62
- NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
63
- LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
64
- OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
65
- WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
1
+ # Kaitai Struct: runtime library for Ruby
2
+
3
+ This library implements Kaitai Struct API for Ruby.
4
+
5
+ Kaitai Struct is a declarative language used for describe various binary
6
+ data structures, laid out in files or in memory: i.e. binary file
7
+ formats, network stream packet formats, etc.
8
+
9
+ Further reading:
10
+
11
+ * [About Kaitai Struct](http://kaitai.io/)
12
+ * [About API implemented in this library](http://doc.kaitai.io/stream_api.html)
13
+
14
+ ## Installing
15
+
16
+ ### Using `Gemfile`
17
+
18
+ If your project uses Bundler, just include the line
19
+
20
+ ```
21
+ gem 'kaitai-struct'
22
+ ```
23
+
24
+ in your project's `Gemfile`.
25
+
26
+ ### Using `gem install`
27
+
28
+ If you have a RubyGems package manager installed, you can use command
29
+
30
+ ```
31
+ gem install kaitai-struct
32
+ ```
33
+
34
+ to install this runtime library.
35
+
36
+ ### Manually
37
+
38
+ This library is intentionally kept as very simple, one-file `.rb`
39
+ file. One can just copy it to your project from this
40
+ repository. Usually you won't `require` it directly, it will be loaded
41
+ by Ruby source code generate by Kaitai Struct compiler.
@@ -1,31 +1,31 @@
1
- # -*- encoding: utf-8 -*-
2
-
3
- require File.expand_path("../lib/kaitai/struct/struct", __FILE__)
4
- require 'date'
5
-
6
- Gem::Specification.new { |s|
7
- s.name = 'kaitai-struct'
8
- s.version = Kaitai::Struct::VERSION
9
- s.date = Date.today.to_s
10
-
11
- s.authors = ['Mikhail Yakshin']
12
- s.email = 'greycat@kaitai.io'
13
-
14
- s.homepage = 'http://kaitai.io'
15
- s.summary = 'Kaitai Struct: runtime library for Ruby'
16
- s.license = 'MIT'
17
- s.description = <<-EOF
18
- Kaitai Struct is a declarative language used for describe various binary data structures, laid out in files or in memory: i.e. binary file formats, network stream packet formats, etc.
19
-
20
- The main idea is that a particular format is described in Kaitai Struct language (.ksy file) and then can be compiled with ksc into source files in one of the supported programming languages. These modules will include a generated code for a parser that can read described data structure from a file / stream and give access to it in a nice, easy-to-comprehend API.
21
-
22
- This package provides small runtime library used by code generated by Kaitai Struct compiler.
23
- EOF
24
-
25
- s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
26
- s.require_paths = ['lib']
27
-
28
- s.files = `git ls-files`.split("\n")
29
- s.executables = s.files.grep(%r{^bin/}) { |f| File.basename(f) }
30
- s.test_files = s.files.grep(%r{^(test|spec|features)/})
31
- }
1
+ # -*- encoding: utf-8 -*-
2
+
3
+ require File.expand_path("../lib/kaitai/struct/struct", __FILE__)
4
+ require 'date'
5
+
6
+ Gem::Specification.new { |s|
7
+ s.name = 'kaitai-struct'
8
+ s.version = Kaitai::Struct::VERSION
9
+ s.date = Date.today.to_s
10
+
11
+ s.authors = ['Mikhail Yakshin']
12
+ s.email = 'greycat@kaitai.io'
13
+
14
+ s.homepage = 'http://kaitai.io'
15
+ s.summary = 'Kaitai Struct: runtime library for Ruby'
16
+ s.license = 'MIT'
17
+ s.description = <<-EOF
18
+ Kaitai Struct is a declarative language used for describe various binary data structures, laid out in files or in memory: i.e. binary file formats, network stream packet formats, etc.
19
+
20
+ The main idea is that a particular format is described in Kaitai Struct language (.ksy file) and then can be compiled with ksc into source files in one of the supported programming languages. These modules will include a generated code for a parser that can read described data structure from a file / stream and give access to it in a nice, easy-to-comprehend API.
21
+
22
+ This package provides small runtime library used by code generated by Kaitai Struct compiler.
23
+ EOF
24
+
25
+ s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
26
+ s.require_paths = ['lib']
27
+
28
+ s.files = `git ls-files`.split("\n")
29
+ s.executables = s.files.grep(%r{^bin/}) { |f| File.basename(f) }
30
+ s.test_files = s.files.grep(%r{^(test|spec|features)/})
31
+ }
@@ -1,429 +1,621 @@
1
- require 'stringio'
2
-
3
- module Kaitai
4
- module Struct
5
-
6
- VERSION = '0.4'
7
-
8
- ##
9
- # Common base class for all structured generated by Kaitai Struct.
10
- # Stores stream object that this object was parsed from in {#_io},
11
- # stores reference to parent structure in {#_parent} and root
12
- # structure in {#_root} and provides a few helper methods.
13
- class Struct
14
-
15
- def initialize(_io, _parent = nil, _root = self)
16
- @_io = _io
17
- @_parent = _parent
18
- @_root = _root
19
- end
20
-
21
- ##
22
- # Factory method to instantiate a Kaitai Struct-powered structure,
23
- # parsing it from a local file with a given filename.
24
- # @param filename [String] local file to parse
25
- def self.from_file(filename)
26
- self.new(Stream.open(filename))
27
- end
28
-
29
- ##
30
- # Implementation of {Object#inspect} to aid debugging (at the very
31
- # least, to aid exception raising) for KS-based classes. This one
32
- # uses a bit terser syntax than Ruby's default one, purposely skips
33
- # any internal fields (i.e. starting with `_`, such as `_io`,
34
- # `_parent` and `_root`) to reduce confusion, and does no
35
- # recursivity tracking (as proper general-purpose `inspect`
36
- # implementation should do) because there are no endless recursion
37
- # in KS-based classes by design (except for already mentioned
38
- # internal navigation variables).
39
- def inspect
40
- vars = []
41
- instance_variables.each { |nsym|
42
- nstr = nsym.to_s
43
-
44
- # skip all internal variables
45
- next if nstr[0..1] == '@_'
46
-
47
- # strip mandatory `@` at the beginning of the name for brevity
48
- nstr = nstr[1..-1]
49
-
50
- nvalue = instance_variable_get(nsym).inspect
51
-
52
- vars << "#{nstr}=#{nvalue}"
53
- }
54
-
55
- "#{self.class}(#{vars.join(' ')})"
56
- end
57
-
58
- attr_reader :_io, :_parent, :_root
59
- end
60
-
61
- ##
62
- # Kaitai::Stream is an implementation of
63
- # {https://github.com/kaitai-io/kaitai_struct/wiki/Kaitai-Struct-stream-API
64
- # Kaitai Struct stream API} for Ruby. It's implemented as a wrapper
65
- # for generic IO objects.
66
- #
67
- # It provides a wide variety of simple methods to read (parse) binary
68
- # representations of primitive types, such as integer and floating
69
- # point numbers, byte arrays and strings, and also provides stream
70
- # positioning / navigation methods with unified cross-language and
71
- # cross-toolkit semantics.
72
- #
73
- # Typically, end users won't access Kaitai Stream class manually, but
74
- # would describe a binary structure format using .ksy language and
75
- # then would use Kaitai Struct compiler to generate source code in
76
- # desired target language. That code, in turn, would use this class
77
- # and API to do the actual parsing job.
78
- class Stream
79
- ##
80
- # Exception class for an error that occurs when some fixed content
81
- # was expected to appear, but actual data read was different.
82
- class UnexpectedDataError < Exception
83
- def initialize(actual, expected)
84
- super("Unexpected fixed contents: got #{Stream.format_hex(actual)}, was waiting for #{Stream.format_hex(expected)}")
85
- @actual = actual
86
- @expected = expected
87
- end
88
- end
89
-
90
- ##
91
- # Constructs new Kaitai Stream object.
92
- # @param arg [String, IO] if String, it will be used as byte array to read data from;
93
- # if IO, if will be used literally as source of data
94
- def initialize(arg)
95
- if arg.is_a?(String)
96
- @_io = StringIO.new(arg)
97
- elsif arg.is_a?(IO)
98
- @_io = arg
99
- else
100
- raise TypeError.new('can be initialized with IO or String only')
101
- end
102
- end
103
-
104
- ##
105
- # Convenience method to create a Kaitai Stream object, opening a
106
- # local file with a given filename.
107
- # @param filename [String] local file to open
108
- def self.open(filename)
109
- self.new(File.open(filename, 'rb:ASCII-8BIT'))
110
- end
111
-
112
- # Test endianness of the platform
113
- @@big_endian = [0x0102].pack('s') == [0x0102].pack('n')
114
-
115
- # ========================================================================
116
- # Stream positioning
117
- # ========================================================================
118
-
119
- ##
120
- # Check if stream pointer is at the end of stream.
121
- # @return [true, false] true if we are located at the end of the stream
122
- def eof?; @_io.eof?; end
123
-
124
- ##
125
- # Set stream pointer to designated position.
126
- # @param x [Fixnum] new position (offset in bytes from the beginning of the stream)
127
- def seek(x); @_io.seek(x); end
128
-
129
- ##
130
- # Get current position of a stream pointer.
131
- # @return [Fixnum] pointer position, number of bytes from the beginning of the stream
132
- def pos; @_io.pos; end
133
-
134
- ##
135
- # Get total size of the stream in bytes.
136
- # @return [Fixnum] size of the stream in bytes
137
- def size; @_io.size; end
138
-
139
- # ========================================================================
140
- # Integer numbers
141
- # ========================================================================
142
-
143
- # ------------------------------------------------------------------------
144
- # Signed
145
- # ------------------------------------------------------------------------
146
-
147
- def read_s1
148
- read_bytes(1).unpack('c')[0]
149
- end
150
-
151
- # ........................................................................
152
- # Big-endian
153
- # ........................................................................
154
-
155
- def read_s2be
156
- to_signed(read_u2be, SIGN_MASK_16)
157
- end
158
-
159
- def read_s4be
160
- to_signed(read_u4be, SIGN_MASK_32)
161
- end
162
-
163
- if @@big_endian
164
- def read_s8be
165
- read_bytes(8).unpack('q')[0]
166
- end
167
- else
168
- def read_s8be
169
- to_signed(read_u8be, SIGN_MASK_64)
170
- end
171
- end
172
-
173
- # ........................................................................
174
- # Little-endian
175
- # ........................................................................
176
-
177
- def read_s2le
178
- to_signed(read_u2le, SIGN_MASK_16)
179
- end
180
-
181
- def read_s4le
182
- to_signed(read_u4le, SIGN_MASK_32)
183
- end
184
-
185
- unless @@big_endian
186
- def read_s8le
187
- read_bytes(8).unpack('q')[0]
188
- end
189
- else
190
- def read_s8le
191
- to_signed(read_u8le, SIGN_MASK_64)
192
- end
193
- end
194
-
195
- # ------------------------------------------------------------------------
196
- # Unsigned
197
- # ------------------------------------------------------------------------
198
-
199
- def read_u1
200
- read_bytes(1).unpack('C')[0]
201
- end
202
-
203
- # ........................................................................
204
- # Big-endian
205
- # ........................................................................
206
-
207
- def read_u2be
208
- read_bytes(2).unpack('n')[0]
209
- end
210
-
211
- def read_u4be
212
- read_bytes(4).unpack('N')[0]
213
- end
214
-
215
- if @@big_endian
216
- def read_u8be
217
- read_bytes(8).unpack('Q')[0]
218
- end
219
- else
220
- def read_u8be
221
- a, b = read_bytes(8).unpack('NN')
222
- (a << 32) + b
223
- end
224
- end
225
-
226
- # ........................................................................
227
- # Little-endian
228
- # ........................................................................
229
-
230
- def read_u2le
231
- read_bytes(2).unpack('v')[0]
232
- end
233
-
234
- def read_u4le
235
- read_bytes(4).unpack('V')[0]
236
- end
237
-
238
- unless @@big_endian
239
- def read_u8le
240
- read_bytes(8).unpack('Q')[0]
241
- end
242
- else
243
- def read_u8le
244
- a, b = read_bytes(8).unpack('VV')
245
- (b << 32) + a
246
- end
247
- end
248
-
249
- # ========================================================================
250
- # Floating point numbers
251
- # ========================================================================
252
-
253
- # ------------------------------------------------------------------------
254
- # Big-endian
255
- # ------------------------------------------------------------------------
256
-
257
- def read_f4be
258
- read_bytes(4).unpack('g')[0]
259
- end
260
-
261
- def read_f8be
262
- read_bytes(8).unpack('G')[0]
263
- end
264
-
265
- # ------------------------------------------------------------------------
266
- # Little-endian
267
- # ------------------------------------------------------------------------
268
-
269
- def read_f4le
270
- read_bytes(4).unpack('e')[0]
271
- end
272
-
273
- def read_f8le
274
- read_bytes(8).unpack('E')[0]
275
- end
276
-
277
- # ========================================================================
278
- # Byte arrays
279
- # ========================================================================
280
-
281
- ##
282
- # Reads designated number of bytes from the stream.
283
- # @param n [Fixnum] number of bytes to read
284
- # @return [String] read bytes as byte array
285
- # @raise [EOFError] if there were less bytes than requested
286
- # available in the stream
287
- def read_bytes(n)
288
- r = @_io.read(n)
289
- if r
290
- rl = r.bytesize
291
- else
292
- rl = 0
293
- end
294
- raise EOFError.new("attempted to read #{n} bytes, got only #{rl}") if rl < n
295
- r
296
- end
297
-
298
- ##
299
- # Reads all the remaining bytes in a stream as byte array.
300
- # @return [String] all remaining bytes in a stream as byte array
301
- def read_bytes_full
302
- @_io.read
303
- end
304
-
305
- ##
306
- # Reads next len bytes from the stream and ensures that they match
307
- # expected fixed byte array. If they differ, throws a
308
- # {UnexpectedDataError} runtime exception.
309
- # @param len [Fixnum] number of bytes to read
310
- # @param expected [String] contents to be expected
311
- # @return [String] read bytes as byte array, which are guaranteed to
312
- # equal to expected
313
- # @raise [UnexpectedDataError]
314
- def ensure_fixed_contents(len, expected)
315
- buf = @_io.read(len)
316
- actual = buf.bytes
317
- if actual != expected
318
- raise UnexpectedDataError.new(actual, expected)
319
- end
320
- buf
321
- end
322
-
323
- # ========================================================================
324
- # Strings
325
- # ========================================================================
326
-
327
- def read_str_eos(encoding)
328
- read_bytes_full.force_encoding(encoding)
329
- end
330
-
331
- def read_str_byte_limit(byte_size, encoding)
332
- read_bytes(byte_size).force_encoding(encoding)
333
- end
334
-
335
- def read_strz(encoding, term, include_term, consume_term, eos_error)
336
- r = ''
337
- loop {
338
- if @_io.eof?
339
- if eos_error
340
- raise EOFError.new("end of stream reached, but no terminator #{term} found")
341
- else
342
- return r.force_encoding(encoding)
343
- end
344
- end
345
- c = @_io.getc
346
- if c.ord == term
347
- r << c if include_term
348
- @_io.seek(@_io.pos - 1) unless consume_term
349
- return r.force_encoding(encoding)
350
- end
351
- r << c
352
- }
353
- end
354
-
355
- # ========================================================================
356
- # Byte array processing
357
- # ========================================================================
358
-
359
- ##
360
- # Performs a XOR processing with given data, XORing every byte of
361
- # input with a single given value.
362
- # @param data [String] data to process
363
- # @param key [Fixnum] value to XOR with
364
- # @return [String] processed data
365
- def process_xor_one(data, key)
366
- data.bytes.map { |x| x ^ key }.pack('C*')
367
- end
368
-
369
- ##
370
- # Performs a XOR processing with given data, XORing every byte of
371
- # input with a key array, repeating key array many times, if
372
- # necessary (i.e. if data array is longer than key array).
373
- # @param data [String] data to process
374
- # @param key [String] array of bytes to XOR with
375
- # @return [String] processed data
376
- def process_xor_many(data, key)
377
- kb = key.bytes
378
- kl = kb.size
379
- ki = 0
380
- data.bytes.map { |x|
381
- r = x ^ kb[ki]
382
- ki += 1
383
- ki = 0 if ki >= kl
384
- r
385
- }.pack('C*')
386
- end
387
-
388
- ##
389
- # Performs a circular left rotation shift for a given buffer by a
390
- # given amount of bits, using groups of groupSize bytes each
391
- # time. Right circular rotation should be performed using this
392
- # procedure with corrected amount.
393
- # @param data [String] source data to process
394
- # @param amount [Fixnum] number of bits to shift by
395
- # @param group_size [Fixnum] number of bytes per group to shift
396
- # @return [String] copy of source array with requested shift applied
397
- def process_rotate_left(data, amount, group_size)
398
- raise NotImplementedError.new("unable to rotate group #{group_size} bytes yet") unless group_size == 1
399
-
400
- mask = group_size * 8 - 1
401
- anti_amount = -amount & mask
402
-
403
- # NB: actually, left bit shift (<<) in Ruby would have required
404
- # truncation to type_bits size (i.e. something like "& 0xff" for
405
- # group_size == 8), but we can skip this one, because later these
406
- # number would be packed with Array#pack, which will do truncation
407
- # anyway
408
-
409
- data.bytes.map { |x| (x << amount) | (x >> anti_amount) }.pack('C*')
410
- end
411
-
412
- # ========================================================================
413
-
414
- private
415
- SIGN_MASK_16 = (1 << (16 - 1))
416
- SIGN_MASK_32 = (1 << (32 - 1))
417
- SIGN_MASK_64 = (1 << (64 - 1))
418
-
419
- def to_signed(x, mask)
420
- (x & ~mask) - (x & mask)
421
- end
422
-
423
- def self.format_hex(arr)
424
- arr.map { |x| sprintf('%02X', x) }.join(' ')
425
- end
426
- end
427
-
428
- end
429
- end
1
+ require 'stringio'
2
+
3
+ module Kaitai
4
+ module Struct
5
+
6
+ VERSION = '0.9'
7
+
8
+ ##
9
+ # Common base class for all structured generated by Kaitai Struct.
10
+ # Stores stream object that this object was parsed from in {#_io},
11
+ # stores reference to parent structure in {#_parent} and root
12
+ # structure in {#_root} and provides a few helper methods.
13
+ class Struct
14
+
15
+ def initialize(_io, _parent = nil, _root = self)
16
+ @_io = _io
17
+ @_parent = _parent
18
+ @_root = _root
19
+ end
20
+
21
+ ##
22
+ # Factory method to instantiate a Kaitai Struct-powered structure,
23
+ # parsing it from a local file with a given filename.
24
+ # @param filename [String] local file to parse
25
+ def self.from_file(filename)
26
+ self.new(Stream.open(filename))
27
+ end
28
+
29
+ ##
30
+ # Implementation of {Object#inspect} to aid debugging (at the very
31
+ # least, to aid exception raising) for KS-based classes. This one
32
+ # uses a bit terser syntax than Ruby's default one, purposely skips
33
+ # any internal fields (i.e. starting with `_`, such as `_io`,
34
+ # `_parent` and `_root`) to reduce confusion, and does no
35
+ # recursivity tracking (as proper general-purpose `inspect`
36
+ # implementation should do) because there are no endless recursion
37
+ # in KS-based classes by design (except for already mentioned
38
+ # internal navigation variables).
39
+ def inspect
40
+ vars = []
41
+ instance_variables.each { |nsym|
42
+ nstr = nsym.to_s
43
+
44
+ # skip all internal variables
45
+ next if nstr[0..1] == '@_'
46
+
47
+ # strip mandatory `@` at the beginning of the name for brevity
48
+ nstr = nstr[1..-1]
49
+
50
+ nvalue = instance_variable_get(nsym).inspect
51
+
52
+ vars << "#{nstr}=#{nvalue}"
53
+ }
54
+
55
+ "#{self.class}(#{vars.join(' ')})"
56
+ end
57
+
58
+ attr_reader :_io, :_parent, :_root
59
+ end
60
+
61
+ ##
62
+ # Kaitai::Struct::Stream is an implementation of
63
+ # {https://github.com/kaitai-io/kaitai_struct/wiki/Kaitai-Struct-stream-API
64
+ # Kaitai Struct stream API} for Ruby. It's implemented as a wrapper
65
+ # for generic IO objects.
66
+ #
67
+ # It provides a wide variety of simple methods to read (parse) binary
68
+ # representations of primitive types, such as integer and floating
69
+ # point numbers, byte arrays and strings, and also provides stream
70
+ # positioning / navigation methods with unified cross-language and
71
+ # cross-toolkit semantics.
72
+ #
73
+ # Typically, end users won't access Kaitai Stream class manually, but
74
+ # would describe a binary structure format using .ksy language and
75
+ # then would use Kaitai Struct compiler to generate source code in
76
+ # desired target language. That code, in turn, would use this class
77
+ # and API to do the actual parsing job.
78
+ class Stream
79
+ ##
80
+ # Unused since Kaitai Struct Compiler v0.9+ - compatibility with
81
+ # older versions.
82
+ #
83
+ # Exception class for an error that occurs when some fixed content
84
+ # was expected to appear, but actual data read was different.
85
+ class UnexpectedDataError < Exception
86
+ def initialize(actual, expected)
87
+ super("Unexpected fixed contents: got #{Stream.format_hex(actual)}, was waiting for #{Stream.format_hex(expected)}")
88
+ @actual = actual
89
+ @expected = expected
90
+ end
91
+ end
92
+
93
+
94
+ ##
95
+ # Constructs new Kaitai Stream object.
96
+ # @param arg [String, IO] if String, it will be used as byte array to read data from;
97
+ # if IO, if will be used literally as source of data
98
+ def initialize(arg)
99
+ if arg.is_a?(String)
100
+ @_io = StringIO.new(arg)
101
+ elsif arg.is_a?(IO)
102
+ @_io = arg
103
+ else
104
+ raise TypeError.new('can be initialized with IO or String only')
105
+ end
106
+ align_to_byte
107
+ end
108
+
109
+ ##
110
+ # Convenience method to create a Kaitai Stream object, opening a
111
+ # local file with a given filename.
112
+ # @param filename [String] local file to open
113
+ def self.open(filename)
114
+ self.new(File.open(filename, 'rb:ASCII-8BIT'))
115
+ end
116
+
117
+ ##
118
+ # Closes underlying IO object.
119
+ def close
120
+ @_io.close
121
+ end
122
+
123
+ # Test endianness of the platform
124
+ @@big_endian = [0x0102].pack('s') == [0x0102].pack('n')
125
+
126
+ # @!group Stream positioning
127
+
128
+ ##
129
+ # Check if stream pointer is at the end of stream.
130
+ # @return [true, false] true if we are located at the end of the stream
131
+ def eof?; @_io.eof? and @bits_left == 0; end
132
+
133
+ ##
134
+ # Set stream pointer to designated position.
135
+ # @param x [Fixnum] new position (offset in bytes from the beginning of the stream)
136
+ def seek(x); @_io.seek(x); end
137
+
138
+ ##
139
+ # Get current position of a stream pointer.
140
+ # @return [Fixnum] pointer position, number of bytes from the beginning of the stream
141
+ def pos; @_io.pos; end
142
+
143
+ ##
144
+ # Get total size of the stream in bytes.
145
+ # @return [Fixnum] size of the stream in bytes
146
+ def size; @_io.size; end
147
+
148
+ # @!endgroup
149
+
150
+ # @!group Integer numbers
151
+
152
+ # ------------------------------------------------------------------------
153
+ # Signed
154
+ # ------------------------------------------------------------------------
155
+
156
+ def read_s1
157
+ read_bytes(1).unpack('c')[0]
158
+ end
159
+
160
+ # ........................................................................
161
+ # Big-endian
162
+ # ........................................................................
163
+
164
+ def read_s2be
165
+ to_signed(read_u2be, SIGN_MASK_16)
166
+ end
167
+
168
+ def read_s4be
169
+ to_signed(read_u4be, SIGN_MASK_32)
170
+ end
171
+
172
+ if @@big_endian
173
+ def read_s8be
174
+ read_bytes(8).unpack('q')[0]
175
+ end
176
+ else
177
+ def read_s8be
178
+ to_signed(read_u8be, SIGN_MASK_64)
179
+ end
180
+ end
181
+
182
+ # ........................................................................
183
+ # Little-endian
184
+ # ........................................................................
185
+
186
+ def read_s2le
187
+ to_signed(read_u2le, SIGN_MASK_16)
188
+ end
189
+
190
+ def read_s4le
191
+ to_signed(read_u4le, SIGN_MASK_32)
192
+ end
193
+
194
+ unless @@big_endian
195
+ def read_s8le
196
+ read_bytes(8).unpack('q')[0]
197
+ end
198
+ else
199
+ def read_s8le
200
+ to_signed(read_u8le, SIGN_MASK_64)
201
+ end
202
+ end
203
+
204
+ # ------------------------------------------------------------------------
205
+ # Unsigned
206
+ # ------------------------------------------------------------------------
207
+
208
+ def read_u1
209
+ read_bytes(1).unpack('C')[0]
210
+ end
211
+
212
+ # ........................................................................
213
+ # Big-endian
214
+ # ........................................................................
215
+
216
+ def read_u2be
217
+ read_bytes(2).unpack('n')[0]
218
+ end
219
+
220
+ def read_u4be
221
+ read_bytes(4).unpack('N')[0]
222
+ end
223
+
224
+ if @@big_endian
225
+ def read_u8be
226
+ read_bytes(8).unpack('Q')[0]
227
+ end
228
+ else
229
+ def read_u8be
230
+ a, b = read_bytes(8).unpack('NN')
231
+ (a << 32) + b
232
+ end
233
+ end
234
+
235
+ # ........................................................................
236
+ # Little-endian
237
+ # ........................................................................
238
+
239
+ def read_u2le
240
+ read_bytes(2).unpack('v')[0]
241
+ end
242
+
243
+ def read_u4le
244
+ read_bytes(4).unpack('V')[0]
245
+ end
246
+
247
+ unless @@big_endian
248
+ def read_u8le
249
+ read_bytes(8).unpack('Q')[0]
250
+ end
251
+ else
252
+ def read_u8le
253
+ a, b = read_bytes(8).unpack('VV')
254
+ (b << 32) + a
255
+ end
256
+ end
257
+
258
+ # @!endgroup
259
+
260
+ # @!group Floating point numbers
261
+
262
+ # ------------------------------------------------------------------------
263
+ # Big-endian
264
+ # ------------------------------------------------------------------------
265
+
266
+ def read_f4be
267
+ read_bytes(4).unpack('g')[0]
268
+ end
269
+
270
+ def read_f8be
271
+ read_bytes(8).unpack('G')[0]
272
+ end
273
+
274
+ # ------------------------------------------------------------------------
275
+ # Little-endian
276
+ # ------------------------------------------------------------------------
277
+
278
+ def read_f4le
279
+ read_bytes(4).unpack('e')[0]
280
+ end
281
+
282
+ def read_f8le
283
+ read_bytes(8).unpack('E')[0]
284
+ end
285
+
286
+ # @!endgroup
287
+
288
+ # @!group Unaligned bit values
289
+
290
+ def align_to_byte
291
+ @bits_left = 0
292
+ @bits = 0
293
+ end
294
+
295
+ def read_bits_int_be(n)
296
+ bits_needed = n - @bits_left
297
+ if bits_needed > 0
298
+ # 1 bit => 1 byte
299
+ # 8 bits => 1 byte
300
+ # 9 bits => 2 bytes
301
+ bytes_needed = ((bits_needed - 1) / 8) + 1
302
+ buf = read_bytes(bytes_needed)
303
+ buf.each_byte { |byte|
304
+ @bits <<= 8
305
+ @bits |= byte
306
+ @bits_left += 8
307
+ }
308
+ end
309
+
310
+ # raw mask with required number of 1s, starting from lowest bit
311
+ mask = (1 << n) - 1
312
+ # shift @bits to align the highest bits with the mask & derive reading result
313
+ shift_bits = @bits_left - n
314
+ res = (@bits >> shift_bits) & mask
315
+ # clear top bits that we've just read => AND with 1s
316
+ @bits_left -= n
317
+ mask = (1 << @bits_left) - 1
318
+ @bits &= mask
319
+
320
+ res
321
+ end
322
+
323
+ # Unused since Kaitai Struct Compiler v0.9+ - compatibility with
324
+ # older versions.
325
+ def read_bits_int(n)
326
+ read_bits_int_be(n)
327
+ end
328
+
329
+ def read_bits_int_le(n)
330
+ bits_needed = n - @bits_left
331
+ if bits_needed > 0
332
+ # 1 bit => 1 byte
333
+ # 8 bits => 1 byte
334
+ # 9 bits => 2 bytes
335
+ bytes_needed = ((bits_needed - 1) / 8) + 1
336
+ buf = read_bytes(bytes_needed)
337
+ buf.each_byte { |byte|
338
+ @bits |= (byte << @bits_left)
339
+ @bits_left += 8
340
+ }
341
+ end
342
+
343
+ # raw mask with required number of 1s, starting from lowest bit
344
+ mask = (1 << n) - 1
345
+ # derive reading result
346
+ res = @bits & mask
347
+ # remove bottom bits that we've just read by shifting
348
+ @bits >>= n
349
+ @bits_left -= n
350
+
351
+ res
352
+ end
353
+
354
+ # @!endgroup
355
+
356
+ # @!group Byte arrays
357
+
358
+ ##
359
+ # Reads designated number of bytes from the stream.
360
+ # @param n [Fixnum] number of bytes to read
361
+ # @return [String] read bytes as byte array
362
+ # @raise [EOFError] if there were less bytes than requested
363
+ # available in the stream
364
+ def read_bytes(n)
365
+ r = @_io.read(n)
366
+ if r
367
+ rl = r.bytesize
368
+ else
369
+ rl = 0
370
+ end
371
+ raise EOFError.new("attempted to read #{n} bytes, got only #{rl}") if rl < n
372
+ r
373
+ end
374
+
375
+ ##
376
+ # Reads all the remaining bytes in a stream as byte array.
377
+ # @return [String] all remaining bytes in a stream as byte array
378
+ def read_bytes_full
379
+ @_io.read
380
+ end
381
+
382
+ def read_bytes_term(term, include_term, consume_term, eos_error)
383
+ r = ''
384
+ loop {
385
+ if @_io.eof?
386
+ if eos_error
387
+ raise EOFError.new("end of stream reached, but no terminator #{term} found")
388
+ else
389
+ return r
390
+ end
391
+ end
392
+ c = @_io.getc
393
+ if c.ord == term
394
+ r << c if include_term
395
+ @_io.seek(@_io.pos - 1) unless consume_term
396
+ return r
397
+ end
398
+ r << c
399
+ }
400
+ end
401
+
402
+ ##
403
+ # Unused since Kaitai Struct Compiler v0.9+ - compatibility with
404
+ # older versions.
405
+ #
406
+ # Reads next len bytes from the stream and ensures that they match
407
+ # expected fixed byte array. If they differ, throws a
408
+ # {UnexpectedDataError} runtime exception.
409
+ # @param expected [String] contents to be expected
410
+ # @return [String] read bytes as byte array, which are guaranteed to
411
+ # equal to expected
412
+ # @raise [UnexpectedDataError]
413
+ def ensure_fixed_contents(expected)
414
+ len = expected.bytesize
415
+ actual = @_io.read(len)
416
+ raise UnexpectedDataError.new(actual, expected) if actual != expected
417
+ actual
418
+ end
419
+
420
+ def self.bytes_strip_right(bytes, pad_byte)
421
+ new_len = bytes.length
422
+ while new_len > 0 and bytes.getbyte(new_len - 1) == pad_byte
423
+ new_len -= 1
424
+ end
425
+
426
+ bytes[0, new_len]
427
+ end
428
+
429
+ def self.bytes_terminate(bytes, term, include_term)
430
+ new_len = 0
431
+ max_len = bytes.length
432
+ while bytes.getbyte(new_len) != term and new_len < max_len
433
+ new_len += 1
434
+ end
435
+ new_len += 1 if include_term and new_len < max_len
436
+ bytes[0, new_len]
437
+ end
438
+
439
+ # @!endgroup
440
+
441
+ # @!group Byte array processing
442
+
443
+ ##
444
+ # Performs a XOR processing with given data, XORing every byte of
445
+ # input with a single given value. Uses pure Ruby implementation suggested
446
+ # by [Thomas Leitner](https://github.com/gettalong), borrowed from
447
+ # https://github.com/fny/xorcist/blob/master/bin/benchmark
448
+ # @param data [String] data to process
449
+ # @param key [Fixnum] value to XOR with
450
+ # @return [String] processed data
451
+ def self.process_xor_one(data, key)
452
+ out = data.dup
453
+ i = 0
454
+ max = data.length
455
+ while i < max
456
+ out.setbyte(i, data.getbyte(i) ^ key)
457
+ i += 1
458
+ end
459
+ out
460
+ end
461
+
462
+ ##
463
+ # Performs a XOR processing with given data, XORing every byte of
464
+ # input with a key array, repeating key array many times, if
465
+ # necessary (i.e. if data array is longer than key array).
466
+ # Uses pure Ruby implementation suggested by
467
+ # [Thomas Leitner](https://github.com/gettalong), borrowed from
468
+ # https://github.com/fny/xorcist/blob/master/bin/benchmark
469
+ # @param data [String] data to process
470
+ # @param key [String] array of bytes to XOR with
471
+ # @return [String] processed data
472
+ def self.process_xor_many(data, key)
473
+ out = data.dup
474
+ kl = key.length
475
+ ki = 0
476
+ i = 0
477
+ max = data.length
478
+ while i < max
479
+ out.setbyte(i, data.getbyte(i) ^ key.getbyte(ki))
480
+ ki += 1
481
+ ki = 0 if ki >= kl
482
+ i += 1
483
+ end
484
+ out
485
+ end
486
+
487
+ ##
488
+ # Performs a circular left rotation shift for a given buffer by a
489
+ # given amount of bits, using groups of groupSize bytes each
490
+ # time. Right circular rotation should be performed using this
491
+ # procedure with corrected amount.
492
+ # @param data [String] source data to process
493
+ # @param amount [Fixnum] number of bits to shift by
494
+ # @param group_size [Fixnum] number of bytes per group to shift
495
+ # @return [String] copy of source array with requested shift applied
496
+ def self.process_rotate_left(data, amount, group_size)
497
+ raise NotImplementedError.new("unable to rotate group #{group_size} bytes yet") unless group_size == 1
498
+
499
+ mask = group_size * 8 - 1
500
+ anti_amount = -amount & mask
501
+
502
+ # NB: actually, left bit shift (<<) in Ruby would have required
503
+ # truncation to type_bits size (i.e. something like "& 0xff" for
504
+ # group_size == 8), but we can skip this one, because later these
505
+ # number would be packed with Array#pack, which will do truncation
506
+ # anyway
507
+
508
+ data.bytes.map { |x| (x << amount) | (x >> anti_amount) }.pack('C*')
509
+ end
510
+
511
+ # @!endgroup
512
+
513
+ ##
514
+ # Resolves value using enum: if the value is not found in the map,
515
+ # we'll just use literal value per se.
516
+ def self.resolve_enum(enum_map, value)
517
+ enum_map[value] || value
518
+ end
519
+
520
+ # ========================================================================
521
+
522
+ private
523
+ SIGN_MASK_16 = (1 << (16 - 1))
524
+ SIGN_MASK_32 = (1 << (32 - 1))
525
+ SIGN_MASK_64 = (1 << (64 - 1))
526
+
527
+ def to_signed(x, mask)
528
+ (x & ~mask) - (x & mask)
529
+ end
530
+
531
+ def self.format_hex(arr)
532
+ arr.unpack('H*')[0].gsub(/(..)/, '\1 ').chop
533
+ end
534
+ end
535
+
536
+ ##
537
+ # Common ancestor for all error originating from Kaitai Struct usage.
538
+ # Stores KSY source path, pointing to an element supposedly guilty of
539
+ # an error.
540
+ class KaitaiStructError < Exception
541
+ def initialize(msg, src_path)
542
+ super("#{src_path}: #{msg}")
543
+ @src_path = src_path
544
+ end
545
+ end
546
+
547
+ ##
548
+ # Error that occurs when default endianness should be decided with
549
+ # a switch, but nothing matches (although using endianness expression
550
+ # implies that there should be some positive result).
551
+ class UndecidedEndiannessError < KaitaiStructError
552
+ def initialize(src_path)
553
+ super("unable to decide on endianness for a type", src_path)
554
+ end
555
+ end
556
+
557
+ ##
558
+ # Common ancestor for all validation failures. Stores pointer to
559
+ # KaitaiStream IO object which was involved in an error.
560
+ class ValidationFailedError < KaitaiStructError
561
+ def initialize(msg, io, src_path)
562
+ super("at pos #{io.pos}: validation failed: #{msg}", src_path)
563
+ @io = io
564
+ end
565
+ end
566
+
567
+ ##
568
+ # Signals validation failure: we required "actual" value to be equal to
569
+ # "expected", but it turned out that it's not.
570
+ class ValidationNotEqualError < ValidationFailedError
571
+ def initialize(expected, actual, io, src_path)
572
+ super("not equal, expected #{expected.inspect}, but got #{actual.inspect}", io, src_path)
573
+ @expected = expected
574
+ @actual = actual
575
+ end
576
+ end
577
+
578
+ ##
579
+ # Signals validation failure: we required "actual" value to be greater
580
+ # than or equal to "min", but it turned out that it's not.
581
+ class ValidationLessThanError < ValidationFailedError
582
+ def initialize(min, actual, io, src_path)
583
+ super("not in range, min #{min.inspect}, but got #{actual.inspect}", io, src_path)
584
+ @min = min
585
+ @actual = actual
586
+ end
587
+ end
588
+
589
+ ##
590
+ # Signals validation failure: we required "actual" value to be less
591
+ # than or equal to "max", but it turned out that it's not.
592
+ class ValidationGreaterThanError < ValidationFailedError
593
+ def initialize(max, actual, io, src_path)
594
+ super("not in range, max #{max.inspect}, but got #{actual.inspect}", io, src_path)
595
+ @max = max
596
+ @actual = actual
597
+ end
598
+ end
599
+
600
+ ##
601
+ # Signals validation failure: we required "actual" value to be any of
602
+ # the given list, but it turned out that it's not.
603
+ class ValidationNotAnyOfError < ValidationFailedError
604
+ def initialize(actual, io, src_path)
605
+ super("not any of the list, got #{actual.inspect}", io, src_path)
606
+ @actual = actual
607
+ end
608
+ end
609
+
610
+ ##
611
+ # Signals validation failure: we required "actual" value to match
612
+ # the expression, but it turned out that it doesn't.
613
+ class ValidationExprError < ValidationFailedError
614
+ def initialize(actual, io, src_path)
615
+ super("not matching the expression, got #{actual.inspect}", io, src_path)
616
+ @actual = actual
617
+ end
618
+ end
619
+
620
+ end
621
+ end