kaitai-struct 0.9 → 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,621 +1,893 @@
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
1
+ require 'stringio'
2
+
3
+ module Kaitai
4
+ module Struct
5
+
6
+ VERSION = '0.11'
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 = nil)
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
+ # {Kaitai Stream API}[https://doc.kaitai.io/stream_api.html] for Ruby.
64
+ # It's implemented as a wrapper for generic IO objects.
65
+ #
66
+ # It provides a wide variety of simple methods to read (parse) binary
67
+ # representations of primitive types, such as integer and floating
68
+ # point numbers, byte arrays and strings, and also provides stream
69
+ # positioning / navigation methods with unified cross-language and
70
+ # cross-toolkit semantics.
71
+ #
72
+ # Typically, end users won't access Kaitai Stream class manually, but
73
+ # would describe a binary structure format using .ksy language and
74
+ # then would use Kaitai Struct compiler to generate source code in
75
+ # desired target language. That code, in turn, would use this class
76
+ # and API to do the actual parsing job.
77
+ class Stream
78
+ ##
79
+ # @deprecated Unused since Kaitai Struct compiler 0.9. It
80
+ # is only available for backward compatibility and will be
81
+ # removed in the future. KSC 0.9 and later versions use
82
+ # {ValidationNotEqualError} instead.
83
+ #
84
+ # Exception class for an error that occurs when some fixed content
85
+ # was expected to appear, but actual data read was different.
86
+ class UnexpectedDataError < Exception
87
+ def initialize(actual, expected)
88
+ super("Unexpected fixed contents: got #{Internal.format_hex(actual)}, " \
89
+ "was waiting for #{Internal.format_hex(expected)}")
90
+ @actual = actual
91
+ @expected = expected
92
+ end
93
+ end
94
+
95
+ # Module#deprecate_constant was added in Ruby 2.3, see
96
+ # https://rubyreferences.github.io/rubychanges/evolution.html#modules-and-classes
97
+ deprecate_constant :UnexpectedDataError if respond_to?(:deprecate_constant)
98
+
99
+ ##
100
+ # Constructs new Kaitai Stream object.
101
+ # @param arg [String, IO, StringIO, SubIO] if String, it will be used as byte
102
+ # array to read data from; if IO (or StringIO, or SubIO), if will be used literally
103
+ # as the source of data
104
+ def initialize(arg)
105
+ if arg.is_a?(String)
106
+ @_io = StringIO.new(arg)
107
+ elsif arg.is_a?(IO) or arg.is_a?(StringIO) or arg.is_a?(SubIO)
108
+ @_io = arg
109
+ else
110
+ raise TypeError.new('can be initialized with IO, StringIO, SubIO or String only')
111
+ end
112
+ align_to_byte
113
+ end
114
+
115
+ ##
116
+ # Convenience method to create a Kaitai Stream object, opening a
117
+ # local file with a given filename.
118
+ # @param filename [String] local file to open
119
+ def self.open(filename)
120
+ self.new(File.open(filename, 'rb:ASCII-8BIT'))
121
+ end
122
+
123
+ ##
124
+ # Closes underlying IO object.
125
+ def close
126
+ # NOTE: `unless @_io.closed?` is only needed in Ruby 2.2 and below. Ruby 2.3
127
+ # and later versions no longer raise `IOError: closed stream` when
128
+ # `StringIO#close` is called a second time, see
129
+ # https://github.com/ruby/ruby/commit/2e02f2dfd2dab936e7cd9a68d46bd910c5d184e5
130
+ @_io.close unless @_io.closed?
131
+ end
132
+
133
+ # @!group Stream positioning
134
+
135
+ ##
136
+ # Check if stream pointer is at the end of stream.
137
+ # @return [true, false] true if we are located at the end of the stream
138
+ def eof?; @_io.eof? and @bits_left == 0; end
139
+
140
+ ##
141
+ # Set stream pointer to designated position.
142
+ # @param x [Fixnum] new position (offset in bytes from the beginning of the stream)
143
+ def seek(x); @_io.seek(x); end
144
+
145
+ ##
146
+ # Get current position of a stream pointer.
147
+ # @return [Fixnum] pointer position, number of bytes from the beginning of the stream
148
+ def pos; @_io.pos; end
149
+
150
+ ##
151
+ # Get total size of the stream in bytes.
152
+ # @return [Fixnum] size of the stream in bytes
153
+ def size; @_io.size; end
154
+
155
+ # @!endgroup
156
+
157
+ # @!group Integer numbers
158
+
159
+ # ------------------------------------------------------------------------
160
+ # Signed
161
+ # ------------------------------------------------------------------------
162
+
163
+ def read_s1
164
+ read_bytes(1).unpack('c')[0]
165
+ end
166
+
167
+ # ........................................................................
168
+ # Big-endian
169
+ # ........................................................................
170
+
171
+ def read_s2be
172
+ read_bytes(2).unpack('s>')[0]
173
+ end
174
+
175
+ def read_s4be
176
+ read_bytes(4).unpack('l>')[0]
177
+ end
178
+
179
+ def read_s8be
180
+ read_bytes(8).unpack('q>')[0]
181
+ end
182
+
183
+ # ........................................................................
184
+ # Little-endian
185
+ # ........................................................................
186
+
187
+ def read_s2le
188
+ read_bytes(2).unpack('s<')[0]
189
+ end
190
+
191
+ def read_s4le
192
+ read_bytes(4).unpack('l<')[0]
193
+ end
194
+
195
+ def read_s8le
196
+ read_bytes(8).unpack('q<')[0]
197
+ end
198
+
199
+ # ------------------------------------------------------------------------
200
+ # Unsigned
201
+ # ------------------------------------------------------------------------
202
+
203
+ def read_u1
204
+ read_bytes(1).unpack('C')[0]
205
+ end
206
+
207
+ # ........................................................................
208
+ # Big-endian
209
+ # ........................................................................
210
+
211
+ def read_u2be
212
+ read_bytes(2).unpack('S>')[0]
213
+ end
214
+
215
+ def read_u4be
216
+ read_bytes(4).unpack('L>')[0]
217
+ end
218
+
219
+ def read_u8be
220
+ read_bytes(8).unpack('Q>')[0]
221
+ end
222
+
223
+ # ........................................................................
224
+ # Little-endian
225
+ # ........................................................................
226
+
227
+ def read_u2le
228
+ read_bytes(2).unpack('S<')[0]
229
+ end
230
+
231
+ def read_u4le
232
+ read_bytes(4).unpack('L<')[0]
233
+ end
234
+
235
+ def read_u8le
236
+ read_bytes(8).unpack('Q<')[0]
237
+ end
238
+
239
+ # @!endgroup
240
+
241
+ # @!group Floating point numbers
242
+
243
+ # ------------------------------------------------------------------------
244
+ # Big-endian
245
+ # ------------------------------------------------------------------------
246
+
247
+ def read_f4be
248
+ read_bytes(4).unpack('g')[0]
249
+ end
250
+
251
+ def read_f8be
252
+ read_bytes(8).unpack('G')[0]
253
+ end
254
+
255
+ # ------------------------------------------------------------------------
256
+ # Little-endian
257
+ # ------------------------------------------------------------------------
258
+
259
+ def read_f4le
260
+ read_bytes(4).unpack('e')[0]
261
+ end
262
+
263
+ def read_f8le
264
+ read_bytes(8).unpack('E')[0]
265
+ end
266
+
267
+ # @!endgroup
268
+
269
+ # @!group Unaligned bit values
270
+
271
+ def align_to_byte
272
+ @bits_left = 0
273
+ @bits = 0
274
+ end
275
+
276
+ def read_bits_int_be(n)
277
+ res = 0
278
+
279
+ bits_needed = n - @bits_left
280
+ @bits_left = -bits_needed % 8
281
+
282
+ if bits_needed > 0
283
+ # 1 bit => 1 byte
284
+ # 8 bits => 1 byte
285
+ # 9 bits => 2 bytes
286
+ bytes_needed = ((bits_needed - 1) / 8) + 1 # `ceil(bits_needed / 8)`
287
+ buf = read_bytes(bytes_needed)
288
+ buf.each_byte { |byte|
289
+ res = res << 8 | byte
290
+ }
291
+
292
+ new_bits = res
293
+ res = res >> @bits_left | @bits << bits_needed
294
+ @bits = new_bits # will be masked at the end of the function
295
+ else
296
+ res = @bits >> -bits_needed # shift unneeded bits out
297
+ end
298
+
299
+ mask = (1 << @bits_left) - 1 # `@bits_left` is in range 0..7
300
+ @bits &= mask
301
+
302
+ res
303
+ end
304
+
305
+ ##
306
+ # @deprecated Unused since Kaitai Struct compiler 0.9. It
307
+ # is only available for backward compatibility and will be
308
+ # removed in the future. KSC 0.9 and later versions use
309
+ # {#read_bits_int_be} instead.
310
+ def read_bits_int(n)
311
+ Internal.warn_deprecated(
312
+ 'method Stream#read_bits_int is deprecated since 0.9, ' \
313
+ 'use Stream#read_bits_int_be instead'
314
+ )
315
+ read_bits_int_be(n)
316
+ end
317
+
318
+ def read_bits_int_le(n)
319
+ res = 0
320
+ bits_needed = n - @bits_left
321
+
322
+ if bits_needed > 0
323
+ # 1 bit => 1 byte
324
+ # 8 bits => 1 byte
325
+ # 9 bits => 2 bytes
326
+ bytes_needed = ((bits_needed - 1) / 8) + 1 # `ceil(bits_needed / 8)`
327
+ buf = read_bytes(bytes_needed)
328
+ i = 0
329
+ buf.each_byte { |byte|
330
+ res |= byte << (i * 8)
331
+ i += 1
332
+ }
333
+
334
+ new_bits = res >> bits_needed
335
+ res = res << @bits_left | @bits
336
+ @bits = new_bits
337
+ else
338
+ res = @bits
339
+ @bits >>= n
340
+ end
341
+
342
+ @bits_left = -bits_needed % 8
343
+
344
+ mask = (1 << n) - 1 # no problem with this in Ruby (arbitrary precision integers)
345
+ res &= mask
346
+ return res
347
+ end
348
+
349
+ # @!endgroup
350
+
351
+ # @!group Byte arrays
352
+
353
+ ##
354
+ # Reads designated number of bytes from the stream.
355
+ # @param n [Fixnum] number of bytes to read
356
+ # @return [String] read bytes as byte array
357
+ # @raise [EOFError] if there were less bytes than requested
358
+ # available in the stream
359
+ def read_bytes(n)
360
+ if n.nil?
361
+ # This `read(0)` call is only used to raise `IOError: not opened for reading`
362
+ # if the stream is closed. This ensures identical behavior to the `substream`
363
+ # method.
364
+ @_io.read(0)
365
+ raise TypeError.new('no implicit conversion from nil to integer')
366
+ end
367
+
368
+ r = @_io.read(n)
369
+ rl = r ? r.bytesize : 0
370
+ n = n.to_int
371
+ if rl < n
372
+ begin
373
+ @_io.seek(@_io.pos - rl)
374
+ rescue Errno::ESPIPE
375
+ # We have a non-seekable stream, so we can't go back to the
376
+ # previous position - that's fine.
377
+ end
378
+ raise EOFError.new("attempted to read #{n} bytes, got only #{rl}")
379
+ end
380
+ r
381
+ end
382
+
383
+ ##
384
+ # Reads all the remaining bytes in a stream as byte array.
385
+ # @return [String] all remaining bytes in a stream as byte array
386
+ def read_bytes_full
387
+ @_io.read
388
+ end
389
+
390
+ def read_bytes_term(term, include_term, consume_term, eos_error)
391
+ term_byte = term.chr
392
+ r = String.new
393
+ loop {
394
+ c = @_io.getc
395
+ if c.nil?
396
+ if eos_error
397
+ raise EOFError.new("end of stream reached, but no terminator #{term} found")
398
+ end
399
+
400
+ return r
401
+ end
402
+ if c == term_byte
403
+ r << c if include_term
404
+ @_io.seek(@_io.pos - 1) unless consume_term
405
+ return r
406
+ end
407
+ r << c
408
+ }
409
+ end
410
+
411
+ def read_bytes_term_multi(term, include_term, consume_term, eos_error)
412
+ unit_size = term.bytesize
413
+ r = String.new
414
+ loop {
415
+ c = @_io.read(unit_size) || ''
416
+ if c.bytesize < unit_size
417
+ if eos_error
418
+ raise EOFError.new("end of stream reached, but no terminator #{term} found")
419
+ end
420
+
421
+ r << c
422
+ return r
423
+ end
424
+ if c == term
425
+ r << c if include_term
426
+ @_io.seek(@_io.pos - unit_size) unless consume_term
427
+ return r
428
+ end
429
+ r << c
430
+ }
431
+ end
432
+
433
+ ##
434
+ # @deprecated Unused since Kaitai Struct compiler 0.9. It
435
+ # is only available for backward compatibility and will be
436
+ # removed in the future. KSC 0.9 and later versions raise
437
+ # {ValidationNotEqualError} instead.
438
+ #
439
+ # Reads next len bytes from the stream and ensures that they match
440
+ # expected fixed byte array. If they differ, throws a
441
+ # {UnexpectedDataError} runtime exception.
442
+ # @param expected [String] contents to be expected
443
+ # @return [String] read bytes as byte array, which are guaranteed to
444
+ # equal to expected
445
+ # @raise [UnexpectedDataError]
446
+ def ensure_fixed_contents(expected)
447
+ Internal.warn_deprecated(
448
+ 'method Stream#ensure_fixed_contents is deprecated since 0.9, ' \
449
+ 'explicitly raise ValidationNotEqualError from an `if` statement instead'
450
+ )
451
+ len = expected.bytesize
452
+ actual = @_io.read(len)
453
+ raise UnexpectedDataError.new(actual, expected) if actual != expected
454
+ actual
455
+ end
456
+
457
+ def self.bytes_strip_right(bytes, pad_byte)
458
+ new_len = bytes.length
459
+ while new_len > 0 and bytes.getbyte(new_len - 1) == pad_byte
460
+ new_len -= 1
461
+ end
462
+
463
+ bytes[0, new_len]
464
+ end
465
+
466
+ def self.bytes_terminate(bytes, term, include_term)
467
+ term_index = bytes.index(term.chr)
468
+ if term_index.nil?
469
+ bytes.dup
470
+ else
471
+ bytes[0, term_index + (include_term ? 1 : 0)]
472
+ end
473
+ end
474
+
475
+ def self.bytes_terminate_multi(bytes, term, include_term)
476
+ unit_size = term.bytesize
477
+ search_index = bytes.index(term)
478
+ loop {
479
+ if search_index.nil?
480
+ return bytes.dup
481
+ end
482
+ mod = search_index % unit_size
483
+ if mod == 0
484
+ return bytes[0, search_index + (include_term ? unit_size : 0)]
485
+ end
486
+ search_index = bytes.index(term, search_index + (unit_size - mod))
487
+ }
488
+ end
489
+
490
+ # @!endgroup
491
+
492
+ # @!group Byte array processing
493
+
494
+ ##
495
+ # Performs a XOR processing with given data, XORing every byte of
496
+ # input with a single given value. Uses pure Ruby implementation suggested
497
+ # by [Thomas Leitner](https://github.com/gettalong), borrowed from
498
+ # https://github.com/fny/xorcist/blob/master/bin/benchmark
499
+ # @param data [String] data to process
500
+ # @param key [Fixnum] value to XOR with
501
+ # @return [String] processed data
502
+ def self.process_xor_one(data, key)
503
+ out = data.dup
504
+ i = 0
505
+ max = data.length
506
+ while i < max
507
+ out.setbyte(i, data.getbyte(i) ^ key)
508
+ i += 1
509
+ end
510
+ out
511
+ end
512
+
513
+ ##
514
+ # Performs a XOR processing with given data, XORing every byte of
515
+ # input with a key array, repeating key array many times, if
516
+ # necessary (i.e. if data array is longer than key array).
517
+ # Uses pure Ruby implementation suggested by
518
+ # [Thomas Leitner](https://github.com/gettalong), borrowed from
519
+ # https://github.com/fny/xorcist/blob/master/bin/benchmark
520
+ # @param data [String] data to process
521
+ # @param key [String] array of bytes to XOR with
522
+ # @return [String] processed data
523
+ def self.process_xor_many(data, key)
524
+ out = data.dup
525
+ kl = key.length
526
+ ki = 0
527
+ i = 0
528
+ max = data.length
529
+ while i < max
530
+ out.setbyte(i, data.getbyte(i) ^ key.getbyte(ki))
531
+ ki += 1
532
+ ki = 0 if ki >= kl
533
+ i += 1
534
+ end
535
+ out
536
+ end
537
+
538
+ ##
539
+ # Performs a circular left rotation shift for a given buffer by a
540
+ # given amount of bits, using groups of groupSize bytes each
541
+ # time. Right circular rotation should be performed using this
542
+ # procedure with corrected amount.
543
+ # @param data [String] source data to process
544
+ # @param amount [Fixnum] number of bits to shift by
545
+ # @param group_size [Fixnum] number of bytes per group to shift
546
+ # @return [String] copy of source array with requested shift applied
547
+ def self.process_rotate_left(data, amount, group_size)
548
+ raise NotImplementedError.new("unable to rotate group #{group_size} bytes yet") unless group_size == 1
549
+
550
+ mask = group_size * 8 - 1
551
+ anti_amount = -amount & mask
552
+
553
+ # NB: actually, left bit shift (<<) in Ruby would have required
554
+ # truncation to type_bits size (i.e. something like "& 0xff" for
555
+ # group_size == 8), but we can skip this one, because later these
556
+ # number would be packed with Array#pack, which will do truncation
557
+ # anyway
558
+
559
+ data.bytes.map { |x| (x << amount) | (x >> anti_amount) }.pack('C*')
560
+ end
561
+
562
+ # @!endgroup
563
+
564
+ ##
565
+ # Reserves next n bytes from current stream as a
566
+ # Kaitai::Struct::Stream substream. Substream has its own pointer
567
+ # and addressing in the range of [0, n) bytes. This stream's pointer
568
+ # is advanced to the position right after this substream.
569
+ # @param n [Fixnum] number of bytes to reserve for a substream
570
+ # @return [Stream] substream covering n bytes from the current
571
+ # position
572
+ def substream(n)
573
+ raise IOError.new('not opened for reading') if @_io.closed?
574
+
575
+ n = Internal.num2long(n)
576
+ raise ArgumentError.new("negative length #{n} given") if n < 0
577
+
578
+ rl = [0, @_io.size - @_io.pos].max
579
+ raise EOFError.new("attempted to read #{n} bytes, got only #{rl}") if rl < n
580
+
581
+ sub = Stream.new(SubIO.new(@_io, @_io.pos, n))
582
+ @_io.seek(@_io.pos + n)
583
+ sub
584
+ end
585
+
586
+ ##
587
+ # Resolves value using enum: if the value is not found in the map,
588
+ # we'll just use literal value per se.
589
+ def self.resolve_enum(enum_map, value)
590
+ enum_map[value] || value
591
+ end
592
+ end
593
+
594
+ ##
595
+ # Substream IO implementation: a IO object which wraps existing IO object
596
+ # and provides similar byte/bytes reading functionality, but only for a
597
+ # limited set of bytes starting from specified offset and spanning up to
598
+ # specified length.
599
+ class SubIO
600
+ ##
601
+ # Parent IO object that this substream is projecting data from.
602
+ attr_reader :parent_io
603
+
604
+ ##
605
+ # Offset of start of substream in coordinates of parent stream. In
606
+ # coordinates of substream itself start will be always 0.
607
+ attr_reader :parent_start
608
+
609
+ ##
610
+ # Size of substream in bytes.
611
+ attr_reader :size
612
+
613
+ ##
614
+ # Current position in a substream. Independent from a position in a
615
+ # parent IO.
616
+ attr_reader :pos
617
+
618
+ def initialize(parent_io, parent_start, size)
619
+ @parent_io = parent_io
620
+ @parent_start = parent_start
621
+ @size = size
622
+ @pos = 0
623
+ @closed = false
624
+ end
625
+
626
+ def eof?
627
+ raise IOError.new('not opened for reading') if @closed
628
+
629
+ @pos >= @size
630
+ end
631
+
632
+ def seek(offset, whence = IO::SEEK_SET)
633
+ raise ArgumentError.new('only IO::SEEK_SET is supported by SubIO#seek') unless whence == IO::SEEK_SET
634
+
635
+ offset = Internal.num2long(offset)
636
+ raise IOError.new('closed stream') if @closed
637
+ raise Errno::EINVAL if offset < 0
638
+ @pos = offset.to_int
639
+ return 0
640
+ end
641
+
642
+ def getc
643
+ raise IOError.new('not opened for reading') if @closed
644
+
645
+ return nil if @pos >= @size
646
+
647
+ # remember position in parent IO
648
+ old_pos = @parent_io.pos
649
+ @parent_io.seek(@parent_start + @pos)
650
+ begin
651
+ res = @parent_io.getc
652
+ @pos += 1
653
+ ensure
654
+ # restore position in parent IO
655
+ @parent_io.seek(old_pos)
656
+ end
657
+
658
+ res
659
+ end
660
+
661
+ def read(len = nil)
662
+ raise IOError.new('not opened for reading') if @closed
663
+
664
+ # read until the end of substream
665
+ if len.nil?
666
+ len = @size - @pos
667
+ return String.new if len <= 0
668
+ elsif len.respond_to?(:to_int)
669
+ len = len.to_int
670
+ # special case for requesting exactly 0 bytes
671
+ return String.new if len == 0
672
+
673
+ if len > 0
674
+ # cap intent to read if going beyond substream boundary
675
+ left = @size - @pos
676
+
677
+ # if actually requested reading and we're beyond the boundary, return nil
678
+ return nil if left <= 0
679
+
680
+ # otherwise, still return something, but less than requested
681
+ len = left if len > left
682
+ end
683
+ end
684
+
685
+ # remember position in parent IO
686
+ old_pos = @parent_io.pos
687
+
688
+ @parent_io.seek(@parent_start + @pos)
689
+ begin
690
+ res = @parent_io.read(len)
691
+ read_len = res.bytesize
692
+ @pos += read_len
693
+ ensure
694
+ # restore position in parent IO
695
+ @parent_io.seek(old_pos)
696
+ end
697
+
698
+ res
699
+ end
700
+
701
+ def close
702
+ @closed = true
703
+ nil
704
+ end
705
+
706
+ def closed?
707
+ @closed
708
+ end
709
+ end
710
+
711
+ ##
712
+ # Common ancestor for all error originating from Kaitai Struct usage.
713
+ # Stores KSY source path, pointing to an element supposedly guilty of
714
+ # an error.
715
+ class KaitaiStructError < StandardError
716
+ def initialize(msg, src_path)
717
+ super("#{src_path}: #{msg}")
718
+ @src_path = src_path
719
+ end
720
+ end
721
+
722
+ ##
723
+ # Error that occurs when default endianness should be decided with
724
+ # a switch, but nothing matches (although using endianness expression
725
+ # implies that there should be some positive result).
726
+ class UndecidedEndiannessError < KaitaiStructError
727
+ def initialize(src_path)
728
+ super("unable to decide on endianness for a type", src_path)
729
+ end
730
+ end
731
+
732
+ ##
733
+ # Common ancestor for all validation failures. Stores pointer to
734
+ # KaitaiStream IO object which was involved in an error.
735
+ class ValidationFailedError < KaitaiStructError
736
+ def initialize(msg, io, src_path)
737
+ super("at pos #{io.pos}: validation failed: #{msg}", src_path)
738
+ @io = io
739
+ end
740
+ end
741
+
742
+ ##
743
+ # Signals validation failure: we required "actual" value to be equal to
744
+ # "expected", but it turned out that it's not.
745
+ class ValidationNotEqualError < ValidationFailedError
746
+ def initialize(expected, actual, io, src_path)
747
+ expected_repr, actual_repr = Internal.inspect_values(expected, actual)
748
+ super("not equal, expected #{expected_repr}, but got #{actual_repr}", io, src_path)
749
+
750
+ @expected = expected
751
+ @actual = actual
752
+ end
753
+ end
754
+
755
+ ##
756
+ # Signals validation failure: we required "actual" value to be greater
757
+ # than or equal to "min", but it turned out that it's not.
758
+ class ValidationLessThanError < ValidationFailedError
759
+ def initialize(min, actual, io, src_path)
760
+ min_repr, actual_repr = Internal.inspect_values(min, actual)
761
+ super("not in range, min #{min_repr}, but got #{actual_repr}", io, src_path)
762
+ @min = min
763
+ @actual = actual
764
+ end
765
+ end
766
+
767
+ ##
768
+ # Signals validation failure: we required "actual" value to be less
769
+ # than or equal to "max", but it turned out that it's not.
770
+ class ValidationGreaterThanError < ValidationFailedError
771
+ def initialize(max, actual, io, src_path)
772
+ max_repr, actual_repr = Internal.inspect_values(max, actual)
773
+ super("not in range, max #{max_repr}, but got #{actual_repr}", io, src_path)
774
+ @max = max
775
+ @actual = actual
776
+ end
777
+ end
778
+
779
+ ##
780
+ # Signals validation failure: we required "actual" value to be any of
781
+ # the given list, but it turned out that it's not.
782
+ class ValidationNotAnyOfError < ValidationFailedError
783
+ def initialize(actual, io, src_path)
784
+ actual_repr = Internal.inspect_values(actual)
785
+ super("not any of the list, got #{actual_repr}", io, src_path)
786
+ @actual = actual
787
+ end
788
+ end
789
+
790
+ ##
791
+ # Signals validation failure: we required "actual" value to be in
792
+ # the enum, but it turned out that it's not.
793
+ class ValidationNotInEnumError < ValidationFailedError
794
+ def initialize(actual, io, src_path)
795
+ actual_repr = Internal.inspect_values(actual)
796
+ super("not in the enum, got #{actual_repr}", io, src_path)
797
+ @actual = actual
798
+ end
799
+ end
800
+
801
+ ##
802
+ # Signals validation failure: we required "actual" value to match
803
+ # the expression, but it turned out that it doesn't.
804
+ class ValidationExprError < ValidationFailedError
805
+ def initialize(actual, io, src_path)
806
+ actual_repr = Internal.inspect_values(actual)
807
+ super("not matching the expression, got #{actual_repr}", io, src_path)
808
+ @actual = actual
809
+ end
810
+ end
811
+
812
+ ##
813
+ # \Internal implementation helpers.
814
+ module Internal
815
+ ##
816
+ # This method reproduces the behavior of the +rb_num2long+ function:
817
+ # https://github.com/ruby/ruby/blob/d2930f8e7a5db8a7337fa43370940381b420cc3e/numeric.c#L3195-L3221
818
+ def self.num2long(val)
819
+ val_as_int = val.to_int
820
+ rescue NoMethodError
821
+ raise TypeError.new('no implicit conversion from nil to integer') if val.nil?
822
+
823
+ val_as_human =
824
+ case val
825
+ when true, false
826
+ val.to_s
827
+ else
828
+ val.class
829
+ end
830
+ raise TypeError.new("no implicit conversion of #{val_as_human} into Integer")
831
+ else
832
+ val_as_int
833
+ end
834
+
835
+ def self.format_hex(bytes)
836
+ bytes.unpack('H*')[0].gsub(/(..)/, '\1 ').chop
837
+ end
838
+
839
+ ###
840
+ # Guess if the given args are most likely byte arrays.
841
+ # <p>
842
+ # There's no way to know for sure, but {@code Encoding::ASCII_8BIT} is a special encoding that is
843
+ # usually used for a byte array(/string), not a character string. For those reasons, that encoding
844
+ # is NOT planned to be allowed for human readable texts by KS in general as well.
845
+ # </p>
846
+ # @param args [...] Something to check.
847
+ # @see <a href="https://ruby-doc.org/core-3.0.0/Encoding.html">Encoding</a>
848
+ # @see <a href="https://github.com/kaitai-io/kaitai_struct/issues/116">List of supported encodings</a>
849
+ #
850
+ def self.is_byte_array?(*args)
851
+ args.all? { |arg| arg.is_a?(String) and (arg.encoding == Encoding::ASCII_8BIT) }
852
+ end
853
+
854
+ private_class_method :is_byte_array?
855
+
856
+ def self.inspect_values(*args)
857
+ reprs = args.map { |arg|
858
+ if is_byte_array?(arg)
859
+ "[#{format_hex(arg)}]"
860
+ else
861
+ arg.inspect
862
+ end
863
+ }
864
+ reprs.length == 1 ? reprs[0] : reprs
865
+ end
866
+
867
+ # The `uplevel` keyword argument of Kernel#warn was added in Ruby 2.5,
868
+ # see https://rubyreferences.github.io/rubychanges/2.5.html#warn-uplevel-keyword-argument
869
+ #
870
+ # The `category` keyword argument of Kernel#warn was added in Ruby 3.0,
871
+ # see https://rubyreferences.github.io/rubychanges/3.0.html#warningwarn-category-keyword-argument
872
+ #
873
+ # NOTE: `.dup` is needed in Ruby 1.9, otherwise `RuntimeError: can't modify frozen String` occurs
874
+ WARN_SUPPORTS_UPLEVEL = Gem::Version.new(RUBY_VERSION.dup) >= Gem::Version.new('2.5.0')
875
+ WARN_SUPPORTS_CATEGORY = Gem::Version.new(RUBY_VERSION.dup) >= Gem::Version.new('3.0.0')
876
+ private_constant :WARN_SUPPORTS_UPLEVEL
877
+ private_constant :WARN_SUPPORTS_CATEGORY
878
+
879
+ def self.warn_deprecated(msg)
880
+ if WARN_SUPPORTS_CATEGORY
881
+ warn(msg, uplevel: 2, category: :deprecated)
882
+ elsif WARN_SUPPORTS_UPLEVEL
883
+ warn(msg, uplevel: 2)
884
+ else
885
+ warn(msg)
886
+ end
887
+ end
888
+ end
889
+
890
+ private_constant :Internal
891
+
892
+ end
893
+ end