bindata 2.4.15 → 2.5.1

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.
Files changed (60) hide show
  1. checksums.yaml +4 -4
  2. data/ChangeLog.rdoc +16 -0
  3. data/README.md +7 -10
  4. data/bindata.gemspec +5 -4
  5. data/examples/list.rb +1 -1
  6. data/lib/bindata/alignment.rb +15 -7
  7. data/lib/bindata/array.rb +54 -54
  8. data/lib/bindata/base.rb +14 -25
  9. data/lib/bindata/base_primitive.rb +24 -20
  10. data/lib/bindata/bits.rb +5 -5
  11. data/lib/bindata/buffer.rb +89 -11
  12. data/lib/bindata/choice.rb +9 -6
  13. data/lib/bindata/count_bytes_remaining.rb +1 -1
  14. data/lib/bindata/delayed_io.rb +10 -10
  15. data/lib/bindata/dsl.rb +34 -32
  16. data/lib/bindata/float.rb +3 -3
  17. data/lib/bindata/framework.rb +8 -10
  18. data/lib/bindata/int.rb +9 -9
  19. data/lib/bindata/io.rb +276 -253
  20. data/lib/bindata/name.rb +1 -1
  21. data/lib/bindata/params.rb +9 -7
  22. data/lib/bindata/primitive.rb +3 -3
  23. data/lib/bindata/registry.rb +46 -51
  24. data/lib/bindata/rest.rb +1 -1
  25. data/lib/bindata/sanitize.rb +9 -16
  26. data/lib/bindata/section.rb +97 -0
  27. data/lib/bindata/skip.rb +140 -51
  28. data/lib/bindata/string.rb +9 -9
  29. data/lib/bindata/stringz.rb +12 -10
  30. data/lib/bindata/struct.rb +83 -66
  31. data/lib/bindata/trace.rb +35 -42
  32. data/lib/bindata/transform/brotli.rb +35 -0
  33. data/lib/bindata/transform/lz4.rb +35 -0
  34. data/lib/bindata/transform/lzma.rb +35 -0
  35. data/lib/bindata/transform/xor.rb +19 -0
  36. data/lib/bindata/transform/xz.rb +35 -0
  37. data/lib/bindata/transform/zlib.rb +33 -0
  38. data/lib/bindata/transform/zstd.rb +35 -0
  39. data/lib/bindata/uint8_array.rb +2 -2
  40. data/lib/bindata/version.rb +1 -1
  41. data/lib/bindata/virtual.rb +4 -7
  42. data/lib/bindata/warnings.rb +1 -1
  43. data/lib/bindata.rb +3 -2
  44. data/test/array_test.rb +10 -8
  45. data/test/buffer_test.rb +9 -0
  46. data/test/choice_test.rb +1 -1
  47. data/test/delayed_io_test.rb +16 -0
  48. data/test/io_test.rb +54 -246
  49. data/test/registry_test.rb +1 -1
  50. data/test/section_test.rb +111 -0
  51. data/test/skip_test.rb +55 -10
  52. data/test/string_test.rb +4 -4
  53. data/test/stringz_test.rb +8 -0
  54. data/test/struct_test.rb +87 -12
  55. data/test/system_test.rb +119 -1
  56. data/test/test_helper.rb +30 -15
  57. data/test/warnings_test.rb +12 -0
  58. metadata +20 -18
  59. data/lib/bindata/offset.rb +0 -94
  60. data/test/offset_test.rb +0 -100
data/lib/bindata/io.rb CHANGED
@@ -5,217 +5,10 @@ module BinData
5
5
  # interface for BinData objects to use when accessing the IO.
6
6
  module IO
7
7
 
8
- # Common operations for both Read and Write.
9
- module Common
10
- def initialize(io)
11
- if self.class === io
12
- raise ArgumentError, "io must not be a #{self.class}"
13
- end
14
-
15
- # wrap strings in a StringIO
16
- if io.respond_to?(:to_str)
17
- io = BinData::IO.create_string_io(io.to_str)
18
- end
19
-
20
- @raw_io = io
21
- @buffer_end_points = nil
22
-
23
- extend seekable? ? SeekableStream : UnSeekableStream
24
- stream_init
25
- end
26
-
27
- #-------------
28
- private
29
-
30
- def seekable?
31
- @raw_io.pos
32
- rescue NoMethodError, Errno::ESPIPE, Errno::EPIPE, Errno::EINVAL
33
- nil
34
- end
35
-
36
- def seek(n)
37
- seek_raw(buffer_limited_n(n))
38
- end
39
-
40
- def buffer_limited_n(n)
41
- if @buffer_end_points
42
- if n.nil? || n > 0
43
- max = @buffer_end_points[1] - offset
44
- n = max if n.nil? || n > max
45
- else
46
- min = @buffer_end_points[0] - offset
47
- n = min if n < min
48
- end
49
- end
50
-
51
- n
52
- end
53
-
54
- def with_buffer_common(n)
55
- prev = @buffer_end_points
56
- if prev
57
- avail = prev[1] - offset
58
- n = avail if n > avail
59
- end
60
- @buffer_end_points = [offset, offset + n]
61
- begin
62
- yield(*@buffer_end_points)
63
- ensure
64
- @buffer_end_points = prev
65
- end
66
- end
67
-
68
- # Use #seek and #pos on seekable streams
69
- module SeekableStream
70
- # The number of bytes remaining in the input stream.
71
- def num_bytes_remaining
72
- start_mark = @raw_io.pos
73
- @raw_io.seek(0, ::IO::SEEK_END)
74
- end_mark = @raw_io.pos
75
-
76
- if @buffer_end_points
77
- if @buffer_end_points[1] < end_mark
78
- end_mark = @buffer_end_points[1]
79
- end
80
- end
81
-
82
- bytes_remaining = end_mark - start_mark
83
- @raw_io.seek(start_mark, ::IO::SEEK_SET)
84
-
85
- bytes_remaining
86
- end
87
-
88
- # All io calls in +block+ are rolled back after this
89
- # method completes.
90
- def with_readahead
91
- mark = @raw_io.pos
92
- begin
93
- yield
94
- ensure
95
- @raw_io.seek(mark, ::IO::SEEK_SET)
96
- end
97
- end
98
-
99
- #-----------
100
- private
101
-
102
- def stream_init
103
- @initial_pos = @raw_io.pos
104
- end
105
-
106
- def offset_raw
107
- @raw_io.pos - @initial_pos
108
- end
109
-
110
- def seek_raw(n)
111
- @raw_io.seek(n, ::IO::SEEK_CUR)
112
- end
113
-
114
- def read_raw(n)
115
- @raw_io.read(n)
116
- end
117
-
118
- def write_raw(data)
119
- @raw_io.write(data)
120
- end
121
- end
122
-
123
- # Manually keep track of offset for unseekable streams.
124
- module UnSeekableStream
125
- def offset_raw
126
- @offset
127
- end
128
-
129
- # The number of bytes remaining in the input stream.
130
- def num_bytes_remaining
131
- raise IOError, "stream is unseekable"
132
- end
133
-
134
- # All io calls in +block+ are rolled back after this
135
- # method completes.
136
- def with_readahead
137
- mark = @offset
138
- @read_data = ""
139
- @in_readahead = true
140
-
141
- class << self
142
- alias_method :read_raw_without_readahead, :read_raw
143
- alias_method :read_raw, :read_raw_with_readahead
144
- end
145
-
146
- begin
147
- yield
148
- ensure
149
- @offset = mark
150
- @in_readahead = false
151
- end
152
- end
153
-
154
- #-----------
155
- private
156
-
157
- def stream_init
158
- @offset = 0
159
- end
160
-
161
- def read_raw(n)
162
- data = @raw_io.read(n)
163
- @offset += data.size if data
164
- data
165
- end
166
-
167
- def read_raw_with_readahead(n)
168
- data = ""
169
-
170
- unless @read_data.empty? || @in_readahead
171
- bytes_to_consume = [n, @read_data.length].min
172
- data += @read_data.slice!(0, bytes_to_consume)
173
- n -= bytes_to_consume
174
-
175
- if @read_data.empty?
176
- class << self
177
- alias_method :read_raw, :read_raw_without_readahead
178
- end
179
- end
180
- end
181
-
182
- raw_data = @raw_io.read(n)
183
- data += raw_data if raw_data
184
-
185
- if @in_readahead
186
- @read_data += data
187
- end
188
-
189
- @offset += data.size
190
-
191
- data
192
- end
193
-
194
- def write_raw(data)
195
- @offset += data.size
196
- @raw_io.write(data)
197
- end
198
-
199
- def seek_raw(n)
200
- raise IOError, "stream is unseekable" if n < 0
201
-
202
- # NOTE: how do we seek on a writable stream?
203
-
204
- # skip over data in 8k blocks
205
- while n > 0
206
- bytes_to_read = [n, 8192].min
207
- read_raw(bytes_to_read)
208
- n -= bytes_to_read
209
- end
210
- end
211
- end
212
- end
213
-
214
8
  # Creates a StringIO around +str+.
215
9
  def self.create_string_io(str = "")
216
- s = StringIO.new(str.dup.force_encoding(Encoding::BINARY))
217
- s.binmode
218
- s
10
+ bin_str = str.dup.force_encoding(Encoding::BINARY)
11
+ StringIO.new(bin_str).tap(&:binmode)
219
12
  end
220
13
 
221
14
  # Create a new IO Read wrapper around +io+. +io+ must provide #read,
@@ -236,10 +29,17 @@ module BinData
236
29
  # readbits(6), readbits(5) #=> [543210, a9876]
237
30
  #
238
31
  class Read
239
- include Common
240
-
241
32
  def initialize(io)
242
- super(io)
33
+ if self.class === io
34
+ raise ArgumentError, "io must not be a #{self.class}"
35
+ end
36
+
37
+ # wrap strings in a StringIO
38
+ if io.respond_to?(:to_str)
39
+ io = BinData::IO.create_string_io(io.to_str)
40
+ end
41
+
42
+ @io = RawIO.new(io)
243
43
 
244
44
  # bits when reading
245
45
  @rnbits = 0
@@ -247,25 +47,38 @@ module BinData
247
47
  @rendian = nil
248
48
  end
249
49
 
250
- # Sets a buffer of +n+ bytes on the io stream. Any reading or seeking
251
- # calls inside the +block+ will be contained within this buffer.
252
- def with_buffer(n)
253
- with_buffer_common(n) do
254
- yield
255
- read
256
- end
50
+ # Allow transforming data in the input stream.
51
+ # See +BinData::Buffer+ as an example.
52
+ #
53
+ # +io+ must be an instance of +Transform+.
54
+ #
55
+ # yields +self+ and +io+ to the given block
56
+ def transform(io)
57
+ reset_read_bits
58
+
59
+ saved = @io
60
+ @io = io.prepend_to_chain(@io)
61
+ yield(self, io)
62
+ io.after_read_transform
63
+ ensure
64
+ @io = saved
257
65
  end
258
66
 
259
- # Returns the current offset of the io stream. Offset will be rounded
260
- # up when reading bitfields.
261
- def offset
262
- offset_raw
67
+ # The number of bytes remaining in the io steam.
68
+ def num_bytes_remaining
69
+ @io.num_bytes_remaining
263
70
  end
264
71
 
265
72
  # Seek +n+ bytes from the current position in the io stream.
266
- def seekbytes(n)
73
+ def skipbytes(n)
267
74
  reset_read_bits
268
- seek(n)
75
+ @io.skip(n)
76
+ end
77
+
78
+ # Seek to an absolute offset within the io stream.
79
+ def seek_to_abs_offset(n)
80
+ reset_read_bits
81
+ @io.seek_abs(n)
269
82
  end
270
83
 
271
84
  # Reads exactly +n+ bytes from +io+.
@@ -311,7 +124,7 @@ module BinData
311
124
  private
312
125
 
313
126
  def read(n = nil)
314
- str = read_raw(buffer_limited_n(n))
127
+ str = @io.read(n)
315
128
  if n
316
129
  raise EOFError, "End of file reached" if str.nil?
317
130
  raise IOError, "data truncated" if str.size < n
@@ -332,7 +145,7 @@ module BinData
332
145
  end
333
146
 
334
147
  def accumulate_big_endian_bits
335
- byte = read(1).unpack('C').at(0) & 0xff
148
+ byte = read(1).unpack1('C') & 0xff
336
149
  @rval = (@rval << 8) | byte
337
150
  @rnbits += 8
338
151
  end
@@ -350,7 +163,7 @@ module BinData
350
163
  end
351
164
 
352
165
  def accumulate_little_endian_bits
353
- byte = read(1).unpack('C').at(0) & 0xff
166
+ byte = read(1).unpack1('C') & 0xff
354
167
  @rval = @rval | (byte << @rnbits)
355
168
  @rnbits += 8
356
169
  end
@@ -368,36 +181,46 @@ module BinData
368
181
  #
369
182
  # See IO::Read for more information.
370
183
  class Write
371
- include Common
372
184
  def initialize(io)
373
- super(io)
185
+ if self.class === io
186
+ raise ArgumentError, "io must not be a #{self.class}"
187
+ end
188
+
189
+ # wrap strings in a StringIO
190
+ if io.respond_to?(:to_str)
191
+ io = BinData::IO.create_string_io(io.to_str)
192
+ end
193
+
194
+ @io = RawIO.new(io)
374
195
 
375
196
  @wnbits = 0
376
197
  @wval = 0
377
198
  @wendian = nil
378
199
  end
379
200
 
380
- # Sets a buffer of +n+ bytes on the io stream. Any writes inside the
381
- # +block+ will be contained within this buffer. If less than +n+ bytes
382
- # are written inside the block, the remainder will be padded with '\0'
383
- # bytes.
384
- def with_buffer(n)
385
- with_buffer_common(n) do |_buf_start, buf_end|
386
- yield
387
- write("\0" * (buf_end - offset))
388
- end
389
- end
201
+ # Allow transforming data in the output stream.
202
+ # See +BinData::Buffer+ as an example.
203
+ #
204
+ # +io+ must be an instance of +Transform+.
205
+ #
206
+ # yields +self+ and +io+ to the given block
207
+ def transform(io)
208
+ flushbits
390
209
 
391
- # Returns the current offset of the io stream. Offset will be rounded
392
- # up when writing bitfields.
393
- def offset
394
- offset_raw + (@wnbits > 0 ? 1 : 0)
210
+ saved = @io
211
+ @io = io.prepend_to_chain(@io)
212
+ yield(self, io)
213
+ io.after_write_transform
214
+ ensure
215
+ @io = saved
395
216
  end
396
217
 
397
- # Seek +n+ bytes from the current position in the io stream.
398
- def seekbytes(n)
218
+ # Seek to an absolute offset within the io stream.
219
+ def seek_to_abs_offset(n)
220
+ raise IOError, "stream is unseekable" unless @io.seekable?
221
+
399
222
  flushbits
400
- seek(n)
223
+ @io.seek_abs(n)
401
224
  end
402
225
 
403
226
  # Writes the given string of bytes to the io stream.
@@ -438,12 +261,7 @@ module BinData
438
261
  private
439
262
 
440
263
  def write(data)
441
- n = buffer_limited_n(data.size)
442
- if n < data.size
443
- data = data[0, n]
444
- end
445
-
446
- write_raw(data)
264
+ @io.write(data)
447
265
  end
448
266
 
449
267
  def write_big_endian_bits(val, nbits)
@@ -492,5 +310,210 @@ module BinData
492
310
  (1 << nbits) - 1
493
311
  end
494
312
  end
313
+
314
+ # API used to access the raw data stream.
315
+ class RawIO
316
+ def initialize(io)
317
+ @io = io
318
+ @pos = 0
319
+
320
+ if is_seekable?(io)
321
+ @initial_pos = io.pos
322
+ else
323
+ singleton_class.prepend(UnSeekableIO)
324
+ end
325
+ end
326
+
327
+ def is_seekable?(io)
328
+ io.pos
329
+ rescue NoMethodError, Errno::ESPIPE, Errno::EPIPE, Errno::EINVAL
330
+ nil
331
+ end
332
+
333
+ def seekable?
334
+ true
335
+ end
336
+
337
+ def num_bytes_remaining
338
+ start_mark = @io.pos
339
+ @io.seek(0, ::IO::SEEK_END)
340
+ end_mark = @io.pos
341
+ @io.seek(start_mark, ::IO::SEEK_SET)
342
+
343
+ end_mark - start_mark
344
+ end
345
+
346
+ def offset
347
+ @pos
348
+ end
349
+
350
+ def skip(n)
351
+ raise IOError, "can not skip backwards" if n.negative?
352
+ @io.seek(n, ::IO::SEEK_CUR)
353
+ @pos += n
354
+ end
355
+
356
+ def seek_abs(n)
357
+ @io.seek(n + @initial_pos, ::IO::SEEK_SET)
358
+ @pos = n
359
+ end
360
+
361
+ def read(n)
362
+ @io.read(n).tap { |data| @pos += (data&.size || 0) }
363
+ end
364
+
365
+ def write(data)
366
+ @io.write(data)
367
+ end
368
+ end
369
+
370
+ # An IO stream may be transformed before processing.
371
+ # e.g. encoding, compression, buffered.
372
+ #
373
+ # Multiple transforms can be chained together.
374
+ #
375
+ # To create a new transform layer, subclass +Transform+.
376
+ # Override the public methods +#read+ and +#write+ at a minimum.
377
+ # Additionally the hook, +#before_transform+, +#after_read_transform+
378
+ # and +#after_write_transform+ are available as well.
379
+ #
380
+ # IMPORTANT! If your transform changes the size of the underlying
381
+ # data stream (e.g. compression), then call
382
+ # +::transform_changes_stream_length!+ in your subclass.
383
+ class Transform
384
+ class << self
385
+ # Indicates that this transform changes the length of the
386
+ # underlying data. e.g. performs compression or error correction
387
+ def transform_changes_stream_length!
388
+ prepend(UnSeekableIO)
389
+ end
390
+ end
391
+
392
+ def initialize
393
+ @chain_io = nil
394
+ end
395
+
396
+ # Initialises this transform.
397
+ #
398
+ # Called before any IO operations.
399
+ def before_transform; end
400
+
401
+ # Flushes the input stream.
402
+ #
403
+ # Called after the final read operation.
404
+ def after_read_transform; end
405
+
406
+ # Flushes the output stream.
407
+ #
408
+ # Called after the final write operation.
409
+ def after_write_transform; end
410
+
411
+ # Prepends this transform to the given +chain+.
412
+ #
413
+ # Returns self (the new head of chain).
414
+ def prepend_to_chain(chain)
415
+ @chain_io = chain
416
+ before_transform
417
+ self
418
+ end
419
+
420
+ # Is the IO seekable?
421
+ def seekable?
422
+ @chain_io.seekable?
423
+ end
424
+
425
+ # How many bytes are available for reading?
426
+ def num_bytes_remaining
427
+ chain_num_bytes_remaining
428
+ end
429
+
430
+ # The current offset within the stream.
431
+ def offset
432
+ chain_offset
433
+ end
434
+
435
+ # Skips forward +n+ bytes in the input stream.
436
+ def skip(n)
437
+ chain_skip(n)
438
+ end
439
+
440
+ # Seeks to the given absolute position.
441
+ def seek_abs(n)
442
+ chain_seek_abs(n)
443
+ end
444
+
445
+ # Reads +n+ bytes from the stream.
446
+ def read(n)
447
+ chain_read(n)
448
+ end
449
+
450
+ # Writes +data+ to the stream.
451
+ def write(data)
452
+ chain_write(data)
453
+ end
454
+
455
+ #-------------
456
+ private
457
+
458
+ def create_empty_binary_string
459
+ String.new.force_encoding(Encoding::BINARY)
460
+ end
461
+
462
+ def chain_seekable?
463
+ @chain_io.seekable?
464
+ end
465
+
466
+ def chain_num_bytes_remaining
467
+ @chain_io.num_bytes_remaining
468
+ end
469
+
470
+ def chain_offset
471
+ @chain_io.offset
472
+ end
473
+
474
+ def chain_skip(n)
475
+ @chain_io.skip(n)
476
+ end
477
+
478
+ def chain_seek_abs(n)
479
+ @chain_io.seek_abs(n)
480
+ end
481
+
482
+ def chain_read(n)
483
+ @chain_io.read(n)
484
+ end
485
+
486
+ def chain_write(data)
487
+ @chain_io.write(data)
488
+ end
489
+ end
490
+
491
+ # A module to be prepended to +RawIO+ or +Transform+ when the data
492
+ # stream is not seekable. This is either due to underlying stream
493
+ # being unseekable or the transform changes the number of bytes.
494
+ module UnSeekableIO
495
+ def seekable?
496
+ false
497
+ end
498
+
499
+ def num_bytes_remaining
500
+ raise IOError, "stream is unseekable"
501
+ end
502
+
503
+ def skip(n)
504
+ raise IOError, "can not skip backwards" if n.negative?
505
+
506
+ # skip over data in 8k blocks
507
+ while n > 0
508
+ bytes_to_read = [n, 8192].min
509
+ read(bytes_to_read)
510
+ n -= bytes_to_read
511
+ end
512
+ end
513
+
514
+ def seek_abs(n)
515
+ skip(n - offset)
516
+ end
517
+ end
495
518
  end
496
519
  end
data/lib/bindata/name.rb CHANGED
@@ -13,7 +13,7 @@ module BinData
13
13
  # </pre></code>
14
14
  module RegisterNamePlugin
15
15
 
16
- def self.included(base) #:nodoc:
16
+ def self.included(base) # :nodoc:
17
17
  # The registered name may be provided explicitly.
18
18
  base.optional_parameter :name
19
19
  end
@@ -27,7 +27,7 @@ module BinData
27
27
  alias optional_parameter optional_parameters
28
28
  alias default_parameter default_parameters
29
29
 
30
- def accepted_parameters #:nodoc:
30
+ def accepted_parameters # :nodoc:
31
31
  @accepted_parameters ||= begin
32
32
  ancestor_params = superclass.respond_to?(:accepted_parameters) ?
33
33
  superclass.accepted_parameters : nil
@@ -114,13 +114,15 @@ module BinData
114
114
  end
115
115
  end
116
116
 
117
- def self.invalid_parameter_names
118
- @invalid_names ||= begin
119
- all_names = LazyEvaluator.instance_methods(true)
120
- allowed_names = [:name, :type]
121
- invalid_names = (all_names - allowed_names).uniq
117
+ class << self
118
+ def invalid_parameter_names
119
+ @invalid_parameter_names ||= begin
120
+ all_names = LazyEvaluator.instance_methods(true)
121
+ allowed_names = [:name, :type]
122
+ invalid_names = (all_names - allowed_names).uniq
122
123
 
123
- Hash[*invalid_names.collect { |key| [key.to_sym, true] }.flatten]
124
+ Hash[*invalid_names.collect { |key| [key.to_sym, true] }.flatten]
125
+ end
124
126
  end
125
127
  end
126
128
  end
@@ -73,11 +73,11 @@ module BinData
73
73
  @struct = BinData::Struct.new(get_parameter(:struct_params), self)
74
74
  end
75
75
 
76
- def respond_to?(symbol, include_private = false) #:nodoc:
76
+ def respond_to?(symbol, include_private = false) # :nodoc:
77
77
  @struct.respond_to?(symbol, include_private) || super
78
78
  end
79
79
 
80
- def method_missing(symbol, *args, &block) #:nodoc:
80
+ def method_missing(symbol, *args, &block) # :nodoc:
81
81
  if @struct.respond_to?(symbol)
82
82
  @struct.__send__(symbol, *args, &block)
83
83
  else
@@ -91,7 +91,7 @@ module BinData
91
91
  @value = get
92
92
  end
93
93
 
94
- def debug_name_of(child) #:nodoc:
94
+ def debug_name_of(child) # :nodoc:
95
95
  debug_name + "-internal-"
96
96
  end
97
97