kaitai-struct 0.8 → 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: 30c10c742a6b3c8908f17006910643fa1a97607c
4
- data.tar.gz: 62d30b905f0c22b2eb076c2d4eb43714172a0b08
2
+ SHA256:
3
+ metadata.gz: 3eb5375d9ddf8772844c25d3319de5be17eaafc71d5da835191e5e61ee84fec7
4
+ data.tar.gz: 82d10de0ba0c3bb004286df6a38febe40fb9b3223a7e24a6386c6ca854386b7e
5
5
  SHA512:
6
- metadata.gz: 52979e774b3fa89691646a5cc5d3b75b3d2676a6516de12ff5fef1dfa22bb6f856d205b54b2c0c4a5e302349a36aafbd1bce75fcce5714e81757863b737f4555
7
- data.tar.gz: 85a035c2959cfdf73da65268d0d4c72a21b42d0aff890f56bc7dc7969855676d7c6f04a96e60aff27bf6bb3585ea112f86aa71e5b70834be98d5360f95686cef
6
+ metadata.gz: db13f61f4b06680cb5b1e2b156fd466aa1c80a3a1e2c0ddad9a181ecdd4e66c9099c70de4cb05e25d6279ea007263db7e591d2ae0672fdba5431b93aa2b1e955
7
+ data.tar.gz: 3688b04940d1b4e8fa06cc3434b5ddc441387f6a62af5811cfc66cbfbf444565393000022df3b8ea997e6034968ff3a8067d258cc5e000969880986ea37a2197
data/.gitignore CHANGED
@@ -1 +1 @@
1
- *.gem
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,64 +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](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.
42
-
43
- ## Licensing
44
-
45
- Copyright 2015-2017 Kaitai Project: MIT license
46
-
47
- Permission is hereby granted, free of charge, to any person obtaining
48
- a copy of this software and associated documentation files (the
49
- "Software"), to deal in the Software without restriction, including
50
- without limitation the rights to use, copy, modify, merge, publish,
51
- distribute, sublicense, and/or sell copies of the Software, and to
52
- permit persons to whom the Software is furnished to do so, subject to
53
- the following conditions:
54
-
55
- The above copyright notice and this permission notice shall be
56
- included in all copies or substantial portions of the Software.
57
-
58
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
59
- EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
60
- MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
61
- NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
62
- LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
63
- OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
64
- 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,502 +1,621 @@
1
- require 'stringio'
2
-
3
- module Kaitai
4
- module Struct
5
-
6
- VERSION = '0.8'
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
- # 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
- # Error that occurs when default endianness should be decided with
92
- # a switch, but nothing matches (although using endianness expression
93
- # implies that there should be some positive result).
94
- class UndecidedEndiannessError < Exception
95
- end
96
-
97
- ##
98
- # Constructs new Kaitai Stream object.
99
- # @param arg [String, IO] if String, it will be used as byte array to read data from;
100
- # if IO, if will be used literally as source of data
101
- def initialize(arg)
102
- if arg.is_a?(String)
103
- @_io = StringIO.new(arg)
104
- elsif arg.is_a?(IO)
105
- @_io = arg
106
- else
107
- raise TypeError.new('can be initialized with IO or String only')
108
- end
109
- align_to_byte
110
- end
111
-
112
- ##
113
- # Convenience method to create a Kaitai Stream object, opening a
114
- # local file with a given filename.
115
- # @param filename [String] local file to open
116
- def self.open(filename)
117
- self.new(File.open(filename, 'rb:ASCII-8BIT'))
118
- end
119
-
120
- # Test endianness of the platform
121
- @@big_endian = [0x0102].pack('s') == [0x0102].pack('n')
122
-
123
- # @!group Stream positioning
124
-
125
- ##
126
- # Check if stream pointer is at the end of stream.
127
- # @return [true, false] true if we are located at the end of the stream
128
- def eof?; @_io.eof?; end
129
-
130
- ##
131
- # Set stream pointer to designated position.
132
- # @param x [Fixnum] new position (offset in bytes from the beginning of the stream)
133
- def seek(x); @_io.seek(x); end
134
-
135
- ##
136
- # Get current position of a stream pointer.
137
- # @return [Fixnum] pointer position, number of bytes from the beginning of the stream
138
- def pos; @_io.pos; end
139
-
140
- ##
141
- # Get total size of the stream in bytes.
142
- # @return [Fixnum] size of the stream in bytes
143
- def size; @_io.size; end
144
-
145
- # @!endgroup
146
-
147
- # @!group Integer numbers
148
-
149
- # ------------------------------------------------------------------------
150
- # Signed
151
- # ------------------------------------------------------------------------
152
-
153
- def read_s1
154
- read_bytes(1).unpack('c')[0]
155
- end
156
-
157
- # ........................................................................
158
- # Big-endian
159
- # ........................................................................
160
-
161
- def read_s2be
162
- to_signed(read_u2be, SIGN_MASK_16)
163
- end
164
-
165
- def read_s4be
166
- to_signed(read_u4be, SIGN_MASK_32)
167
- end
168
-
169
- if @@big_endian
170
- def read_s8be
171
- read_bytes(8).unpack('q')[0]
172
- end
173
- else
174
- def read_s8be
175
- to_signed(read_u8be, SIGN_MASK_64)
176
- end
177
- end
178
-
179
- # ........................................................................
180
- # Little-endian
181
- # ........................................................................
182
-
183
- def read_s2le
184
- to_signed(read_u2le, SIGN_MASK_16)
185
- end
186
-
187
- def read_s4le
188
- to_signed(read_u4le, SIGN_MASK_32)
189
- end
190
-
191
- unless @@big_endian
192
- def read_s8le
193
- read_bytes(8).unpack('q')[0]
194
- end
195
- else
196
- def read_s8le
197
- to_signed(read_u8le, SIGN_MASK_64)
198
- end
199
- end
200
-
201
- # ------------------------------------------------------------------------
202
- # Unsigned
203
- # ------------------------------------------------------------------------
204
-
205
- def read_u1
206
- read_bytes(1).unpack('C')[0]
207
- end
208
-
209
- # ........................................................................
210
- # Big-endian
211
- # ........................................................................
212
-
213
- def read_u2be
214
- read_bytes(2).unpack('n')[0]
215
- end
216
-
217
- def read_u4be
218
- read_bytes(4).unpack('N')[0]
219
- end
220
-
221
- if @@big_endian
222
- def read_u8be
223
- read_bytes(8).unpack('Q')[0]
224
- end
225
- else
226
- def read_u8be
227
- a, b = read_bytes(8).unpack('NN')
228
- (a << 32) + b
229
- end
230
- end
231
-
232
- # ........................................................................
233
- # Little-endian
234
- # ........................................................................
235
-
236
- def read_u2le
237
- read_bytes(2).unpack('v')[0]
238
- end
239
-
240
- def read_u4le
241
- read_bytes(4).unpack('V')[0]
242
- end
243
-
244
- unless @@big_endian
245
- def read_u8le
246
- read_bytes(8).unpack('Q')[0]
247
- end
248
- else
249
- def read_u8le
250
- a, b = read_bytes(8).unpack('VV')
251
- (b << 32) + a
252
- end
253
- end
254
-
255
- # @!endgroup
256
-
257
- # @!group Floating point numbers
258
-
259
- # ------------------------------------------------------------------------
260
- # Big-endian
261
- # ------------------------------------------------------------------------
262
-
263
- def read_f4be
264
- read_bytes(4).unpack('g')[0]
265
- end
266
-
267
- def read_f8be
268
- read_bytes(8).unpack('G')[0]
269
- end
270
-
271
- # ------------------------------------------------------------------------
272
- # Little-endian
273
- # ------------------------------------------------------------------------
274
-
275
- def read_f4le
276
- read_bytes(4).unpack('e')[0]
277
- end
278
-
279
- def read_f8le
280
- read_bytes(8).unpack('E')[0]
281
- end
282
-
283
- # @!endgroup
284
-
285
- # @!group Unaligned bit values
286
-
287
- def align_to_byte
288
- @bits_left = 0
289
- @bits = 0
290
- end
291
-
292
- def read_bits_int(n)
293
- bits_needed = n - @bits_left
294
- if bits_needed > 0
295
- # 1 bit => 1 byte
296
- # 8 bits => 1 byte
297
- # 9 bits => 2 bytes
298
- bytes_needed = ((bits_needed - 1) / 8) + 1
299
- buf = read_bytes(bytes_needed)
300
- buf.each_byte { |byte|
301
- @bits <<= 8
302
- @bits |= byte
303
- @bits_left += 8
304
- }
305
- end
306
-
307
- # raw mask with required number of 1s, starting from lowest bit
308
- mask = (1 << n) - 1
309
- # shift mask to align with highest bits available in @bits
310
- shift_bits = @bits_left - n
311
- mask <<= shift_bits
312
- # derive reading result
313
- res = (@bits & mask) >> shift_bits
314
- # clear top bits that we've just read => AND with 1s
315
- @bits_left -= n
316
- mask = (1 << @bits_left) - 1
317
- @bits &= mask
318
-
319
- res
320
- end
321
-
322
- # @!endgroup
323
-
324
- # @!group Byte arrays
325
-
326
- ##
327
- # Reads designated number of bytes from the stream.
328
- # @param n [Fixnum] number of bytes to read
329
- # @return [String] read bytes as byte array
330
- # @raise [EOFError] if there were less bytes than requested
331
- # available in the stream
332
- def read_bytes(n)
333
- r = @_io.read(n)
334
- if r
335
- rl = r.bytesize
336
- else
337
- rl = 0
338
- end
339
- raise EOFError.new("attempted to read #{n} bytes, got only #{rl}") if rl < n
340
- r
341
- end
342
-
343
- ##
344
- # Reads all the remaining bytes in a stream as byte array.
345
- # @return [String] all remaining bytes in a stream as byte array
346
- def read_bytes_full
347
- @_io.read
348
- end
349
-
350
- def read_bytes_term(term, include_term, consume_term, eos_error)
351
- r = ''
352
- loop {
353
- if @_io.eof?
354
- if eos_error
355
- raise EOFError.new("end of stream reached, but no terminator #{term} found")
356
- else
357
- return r
358
- end
359
- end
360
- c = @_io.getc
361
- if c.ord == term
362
- r << c if include_term
363
- @_io.seek(@_io.pos - 1) unless consume_term
364
- return r
365
- end
366
- r << c
367
- }
368
- end
369
-
370
- ##
371
- # Reads next len bytes from the stream and ensures that they match
372
- # expected fixed byte array. If they differ, throws a
373
- # {UnexpectedDataError} runtime exception.
374
- # @param expected [String] contents to be expected
375
- # @return [String] read bytes as byte array, which are guaranteed to
376
- # equal to expected
377
- # @raise [UnexpectedDataError]
378
- def ensure_fixed_contents(expected)
379
- len = expected.bytesize
380
- actual = @_io.read(len)
381
- raise UnexpectedDataError.new(actual, expected) if actual != expected
382
- actual
383
- end
384
-
385
- def self.bytes_strip_right(bytes, pad_byte)
386
- new_len = bytes.length
387
- while new_len > 0 and bytes.getbyte(new_len - 1) == pad_byte
388
- new_len -= 1
389
- end
390
-
391
- bytes[0, new_len]
392
- end
393
-
394
- def self.bytes_terminate(bytes, term, include_term)
395
- new_len = 0
396
- max_len = bytes.length
397
- while bytes.getbyte(new_len) != term and new_len < max_len
398
- new_len += 1
399
- end
400
- new_len += 1 if include_term and new_len < max_len
401
- bytes[0, new_len]
402
- end
403
-
404
- # @!endgroup
405
-
406
- # @!group Byte array processing
407
-
408
- ##
409
- # Performs a XOR processing with given data, XORing every byte of
410
- # input with a single given value. Uses pure Ruby implementation suggested
411
- # by [Thomas Leitner](https://github.com/gettalong), borrowed from
412
- # https://github.com/fny/xorcist/blob/master/bin/benchmark
413
- # @param data [String] data to process
414
- # @param key [Fixnum] value to XOR with
415
- # @return [String] processed data
416
- def self.process_xor_one(data, key)
417
- out = data.dup
418
- i = 0
419
- max = data.length
420
- while i < max
421
- out.setbyte(i, data.getbyte(i) ^ key)
422
- i += 1
423
- end
424
- out
425
- end
426
-
427
- ##
428
- # Performs a XOR processing with given data, XORing every byte of
429
- # input with a key array, repeating key array many times, if
430
- # necessary (i.e. if data array is longer than key array).
431
- # Uses pure Ruby implementation suggested by
432
- # [Thomas Leitner](https://github.com/gettalong), borrowed from
433
- # https://github.com/fny/xorcist/blob/master/bin/benchmark
434
- # @param data [String] data to process
435
- # @param key [String] array of bytes to XOR with
436
- # @return [String] processed data
437
- def self.process_xor_many(data, key)
438
- out = data.dup
439
- kl = key.length
440
- ki = 0
441
- i = 0
442
- max = data.length
443
- while i < max
444
- out.setbyte(i, data.getbyte(i) ^ key.getbyte(ki))
445
- ki += 1
446
- ki = 0 if ki >= kl
447
- i += 1
448
- end
449
- out
450
- end
451
-
452
- ##
453
- # Performs a circular left rotation shift for a given buffer by a
454
- # given amount of bits, using groups of groupSize bytes each
455
- # time. Right circular rotation should be performed using this
456
- # procedure with corrected amount.
457
- # @param data [String] source data to process
458
- # @param amount [Fixnum] number of bits to shift by
459
- # @param group_size [Fixnum] number of bytes per group to shift
460
- # @return [String] copy of source array with requested shift applied
461
- def self.process_rotate_left(data, amount, group_size)
462
- raise NotImplementedError.new("unable to rotate group #{group_size} bytes yet") unless group_size == 1
463
-
464
- mask = group_size * 8 - 1
465
- anti_amount = -amount & mask
466
-
467
- # NB: actually, left bit shift (<<) in Ruby would have required
468
- # truncation to type_bits size (i.e. something like "& 0xff" for
469
- # group_size == 8), but we can skip this one, because later these
470
- # number would be packed with Array#pack, which will do truncation
471
- # anyway
472
-
473
- data.bytes.map { |x| (x << amount) | (x >> anti_amount) }.pack('C*')
474
- end
475
-
476
- # @!endgroup
477
-
478
- ##
479
- # Resolves value using enum: if the value is not found in the map,
480
- # we'll just use literal value per se.
481
- def self.resolve_enum(enum_map, value)
482
- enum_map[value] || value
483
- end
484
-
485
- # ========================================================================
486
-
487
- private
488
- SIGN_MASK_16 = (1 << (16 - 1))
489
- SIGN_MASK_32 = (1 << (32 - 1))
490
- SIGN_MASK_64 = (1 << (64 - 1))
491
-
492
- def to_signed(x, mask)
493
- (x & ~mask) - (x & mask)
494
- end
495
-
496
- def self.format_hex(arr)
497
- arr.unpack('H*')[0].gsub(/(..)/, '\1 ').chop
498
- end
499
- end
500
-
501
- end
502
- 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