bindata 2.4.10 → 2.5.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (82) hide show
  1. checksums.yaml +4 -4
  2. data/ChangeLog.rdoc +39 -0
  3. data/LICENSE +25 -0
  4. data/NEWS.rdoc +5 -0
  5. data/README.md +6 -9
  6. data/bindata.gemspec +9 -4
  7. data/examples/NBT.txt +1 -1
  8. data/examples/list.rb +1 -1
  9. data/lib/bindata/alignment.rb +15 -7
  10. data/lib/bindata/array.rb +54 -54
  11. data/lib/bindata/base.rb +14 -25
  12. data/lib/bindata/base_primitive.rb +24 -20
  13. data/lib/bindata/bits.rb +15 -15
  14. data/lib/bindata/buffer.rb +89 -11
  15. data/lib/bindata/choice.rb +9 -6
  16. data/lib/bindata/count_bytes_remaining.rb +1 -1
  17. data/lib/bindata/delayed_io.rb +18 -10
  18. data/lib/bindata/dsl.rb +37 -35
  19. data/lib/bindata/float.rb +3 -3
  20. data/lib/bindata/framework.rb +8 -10
  21. data/lib/bindata/int.rb +14 -16
  22. data/lib/bindata/io.rb +276 -253
  23. data/lib/bindata/name.rb +1 -1
  24. data/lib/bindata/params.rb +9 -7
  25. data/lib/bindata/primitive.rb +3 -3
  26. data/lib/bindata/registry.rb +18 -18
  27. data/lib/bindata/rest.rb +1 -1
  28. data/lib/bindata/sanitize.rb +9 -16
  29. data/lib/bindata/section.rb +97 -0
  30. data/lib/bindata/skip.rb +140 -51
  31. data/lib/bindata/string.rb +9 -9
  32. data/lib/bindata/stringz.rb +12 -10
  33. data/lib/bindata/struct.rb +92 -68
  34. data/lib/bindata/trace.rb +35 -42
  35. data/lib/bindata/transform/brotli.rb +35 -0
  36. data/lib/bindata/transform/lz4.rb +35 -0
  37. data/lib/bindata/transform/lzma.rb +35 -0
  38. data/lib/bindata/transform/xor.rb +19 -0
  39. data/lib/bindata/transform/xz.rb +35 -0
  40. data/lib/bindata/transform/zlib.rb +33 -0
  41. data/lib/bindata/transform/zstd.rb +35 -0
  42. data/lib/bindata/uint8_array.rb +2 -2
  43. data/lib/bindata/version.rb +1 -1
  44. data/lib/bindata/virtual.rb +4 -7
  45. data/lib/bindata/warnings.rb +1 -1
  46. data/lib/bindata.rb +1 -0
  47. data/test/alignment_test.rb +8 -8
  48. data/test/array_test.rb +98 -96
  49. data/test/base_primitive_test.rb +47 -47
  50. data/test/base_test.rb +24 -24
  51. data/test/bits_test.rb +15 -15
  52. data/test/buffer_test.rb +31 -22
  53. data/test/choice_test.rb +32 -32
  54. data/test/count_bytes_remaining_test.rb +8 -8
  55. data/test/delayed_io_test.rb +91 -30
  56. data/test/float_test.rb +8 -8
  57. data/test/int_test.rb +14 -14
  58. data/test/io_test.rb +110 -302
  59. data/test/lazy_test.rb +38 -38
  60. data/test/params_test.rb +19 -19
  61. data/test/primitive_test.rb +26 -26
  62. data/test/record_test.rb +99 -99
  63. data/test/registry_test.rb +43 -43
  64. data/test/rest_test.rb +5 -5
  65. data/test/section_test.rb +111 -0
  66. data/test/skip_test.rb +71 -26
  67. data/test/string_test.rb +60 -60
  68. data/test/stringz_test.rb +34 -26
  69. data/test/struct_test.rb +167 -92
  70. data/test/system_test.rb +159 -41
  71. data/test/test_helper.rb +24 -13
  72. data/test/uint8_array_test.rb +6 -6
  73. data/test/virtual_test.rb +7 -7
  74. data/test/warnings_test.rb +14 -2
  75. metadata +19 -22
  76. data/.gitignore +0 -2
  77. data/.travis.yml +0 -15
  78. data/BSDL +0 -22
  79. data/COPYING +0 -52
  80. data/INSTALL +0 -12
  81. data/lib/bindata/offset.rb +0 -94
  82. data/test/offset_test.rb +0 -100
@@ -1,6 +1,6 @@
1
1
  module BinData
2
-
3
- class UnRegisteredTypeError < StandardError ; end
2
+ # Raised when #lookup fails.
3
+ class UnRegisteredTypeError < StandardError; end
4
4
 
5
5
  # This registry contains a register of name -> class mappings.
6
6
  #
@@ -18,7 +18,6 @@ module BinData
18
18
  #
19
19
  # Names are stored in under_score_style, not camelCase.
20
20
  class Registry
21
-
22
21
  def initialize
23
22
  @registry = {}
24
23
  end
@@ -49,13 +48,13 @@ module BinData
49
48
 
50
49
  # Convert CamelCase +name+ to underscore style.
51
50
  def underscore_name(name)
52
- name.
53
- to_s.
54
- sub(/.*::/, "").
55
- gsub(/([A-Z]+)([A-Z][a-z])/, '\1_\2').
56
- gsub(/([a-z\d])([A-Z])/, '\1_\2').
57
- tr("-", "_").
58
- downcase
51
+ name
52
+ .to_s
53
+ .sub(/.*::/, "")
54
+ .gsub(/([A-Z]+)([A-Z][a-z])/, '\1_\2')
55
+ .gsub(/([a-z\d])([A-Z])/, '\1_\2')
56
+ .tr('-', '_')
57
+ .downcase
59
58
  end
60
59
 
61
60
  #---------------
@@ -65,7 +64,7 @@ module BinData
65
64
  name = underscore_name(name)
66
65
 
67
66
  if !registered?(name)
68
- search_prefix = [""].concat(Array(hints[:search_prefix]))
67
+ search_prefix = [""] + Array(hints[:search_prefix])
69
68
  search_prefix.each do |prefix|
70
69
  nwp = name_with_prefix(name, prefix)
71
70
  if registered?(nwp)
@@ -85,7 +84,7 @@ module BinData
85
84
  end
86
85
 
87
86
  def name_with_prefix(name, prefix)
88
- prefix = prefix.to_s.chomp("_")
87
+ prefix = prefix.to_s.chomp('_')
89
88
  if prefix == ""
90
89
  name
91
90
  else
@@ -96,11 +95,11 @@ module BinData
96
95
  def name_with_endian(name, endian)
97
96
  return name if endian.nil?
98
97
 
99
- suffix = (endian == :little) ? "le" : "be"
100
- if /^u?int\d+$/ =~ name
98
+ suffix = (endian == :little) ? 'le' : 'be'
99
+ if /^u?int\d+$/.match?(name)
101
100
  name + suffix
102
101
  else
103
- name + "_" + suffix
102
+ name + '_' + suffix
104
103
  end
105
104
  end
106
105
 
@@ -111,9 +110,10 @@ module BinData
111
110
  end
112
111
 
113
112
  def register_dynamic_class(name)
114
- if /^u?int\d+(le|be)$/ =~ name || /^s?bit\d+(le)?$/ =~ name
113
+ if /^u?int\d+(le|be)$/.match?(name) || /^s?bit\d+(le)?$/.match?(name)
115
114
  class_name = name.gsub(/(?:^|_)(.)/) { $1.upcase }
116
115
  begin
116
+ # call const_get for side effects
117
117
  BinData.const_get(class_name)
118
118
  rescue NameError
119
119
  end
@@ -122,8 +122,8 @@ module BinData
122
122
 
123
123
  def warn_if_name_is_already_registered(name, class_to_register)
124
124
  prev_class = @registry[name]
125
- if $VERBOSE && prev_class && prev_class != class_to_register
126
- warn "warning: replacing registered class #{prev_class} " \
125
+ if prev_class && prev_class != class_to_register
126
+ Kernel.warn "warning: replacing registered class #{prev_class} " \
127
127
  "with #{class_to_register}"
128
128
  end
129
129
  end
data/lib/bindata/rest.rb CHANGED
@@ -1,4 +1,4 @@
1
- require "bindata/base_primitive"
1
+ require 'bindata/base_primitive'
2
2
 
3
3
  module BinData
4
4
  # Rest will consume the input stream from the current position to the end of
@@ -49,14 +49,10 @@ module BinData
49
49
  @prototype = SanitizedPrototype.new(field_type, field_params, hints)
50
50
  end
51
51
 
52
- attr_reader :prototype
52
+ attr_reader :prototype, :name
53
53
 
54
54
  def name_as_sym
55
- @name.nil? ? nil : @name.to_sym
56
- end
57
-
58
- def name
59
- @name
55
+ @name&.to_sym
60
56
  end
61
57
 
62
58
  def has_parameter?(param)
@@ -74,11 +70,7 @@ module BinData
74
70
 
75
71
  def initialize(hints, base_fields = nil)
76
72
  @hints = hints
77
- if base_fields
78
- @fields = base_fields.raw_fields
79
- else
80
- @fields = []
81
- end
73
+ @fields = base_fields ? base_fields.raw_fields : []
82
74
  end
83
75
 
84
76
  def add_field(type, name, params)
@@ -179,7 +171,6 @@ module BinData
179
171
  # is to recursively sanitize the parameters of an entire BinData object chain
180
172
  # at a single time.
181
173
  class SanitizedParameters < Hash
182
-
183
174
  # Memoized constants
184
175
  BIG_ENDIAN = SanitizedBigEndian.new
185
176
  LITTLE_ENDIAN = SanitizedLittleEndian.new
@@ -210,7 +201,7 @@ module BinData
210
201
  sanitize!
211
202
  end
212
203
 
213
- alias_method :has_parameter?, :key?
204
+ alias has_parameter? key?
214
205
 
215
206
  def has_at_least_one_of?(*keys)
216
207
  keys.each do |key|
@@ -257,7 +248,9 @@ module BinData
257
248
  end
258
249
 
259
250
  def sanitize_object_prototype(key)
260
- sanitize(key) { |obj_type, obj_params| create_sanitized_object_prototype(obj_type, obj_params) }
251
+ sanitize(key) do |obj_type, obj_params|
252
+ create_sanitized_object_prototype(obj_type, obj_params)
253
+ end
261
254
  end
262
255
 
263
256
  def sanitize_fields(key, &block)
@@ -306,7 +299,7 @@ module BinData
306
299
  end
307
300
 
308
301
  def needs_sanitizing?(key)
309
- has_key?(key) && ! self[key].is_a?(SanitizedParameter)
302
+ has_parameter?(key) && !self[key].is_a?(SanitizedParameter)
310
303
  end
311
304
 
312
305
  def ensure_no_nil_values
@@ -320,7 +313,7 @@ module BinData
320
313
 
321
314
  def merge_default_parameters!
322
315
  @the_class.default_parameters.each do |key, value|
323
- self[key] = value unless has_key?(key)
316
+ self[key] = value unless has_parameter?(key)
324
317
  end
325
318
  end
326
319
 
@@ -0,0 +1,97 @@
1
+ require 'bindata/base'
2
+ require 'bindata/dsl'
3
+
4
+ module BinData
5
+ # A Section is a layer on top of a stream that transforms the underlying
6
+ # data. This allows BinData to process a stream that has multiple
7
+ # encodings. e.g. Some data data is compressed or encrypted.
8
+ #
9
+ # require 'bindata'
10
+ #
11
+ # class XorTransform < BinData::IO::Transform
12
+ # def initialize(xor)
13
+ # super()
14
+ # @xor = xor
15
+ # end
16
+ #
17
+ # def read(n)
18
+ # chain_read(n).bytes.map { |byte| (byte ^ @xor).chr }.join
19
+ # end
20
+ #
21
+ # def write(data)
22
+ # chain_write(data.bytes.map { |byte| (byte ^ @xor).chr }.join)
23
+ # end
24
+ # end
25
+ #
26
+ # obj = BinData::Section.new(transform: -> { XorTransform.new(0xff) },
27
+ # type: [:string, read_length: 5])
28
+ #
29
+ # obj.read("\x97\x9A\x93\x93\x90") #=> "hello"
30
+ #
31
+ #
32
+ # == Parameters
33
+ #
34
+ # Parameters may be provided at initialisation to control the behaviour of
35
+ # an object. These params are:
36
+ #
37
+ # <tt>:transform</tt>:: A callable that returns a new BinData::IO::Transform.
38
+ # <tt>:type</tt>:: The single type inside the buffer. Use a struct if
39
+ # multiple fields are required.
40
+ class Section < BinData::Base
41
+ extend DSLMixin
42
+
43
+ dsl_parser :section
44
+ arg_processor :section
45
+
46
+ mandatory_parameters :transform, :type
47
+
48
+ def initialize_instance
49
+ @type = get_parameter(:type).instantiate(nil, self)
50
+ end
51
+
52
+ def clear?
53
+ @type.clear?
54
+ end
55
+
56
+ def assign(val)
57
+ @type.assign(val)
58
+ end
59
+
60
+ def snapshot
61
+ @type.snapshot
62
+ end
63
+
64
+ def respond_to_missing?(symbol, include_all = false) # :nodoc:
65
+ @type.respond_to?(symbol, include_all) || super
66
+ end
67
+
68
+ def method_missing(symbol, *args, &block) # :nodoc:
69
+ @type.__send__(symbol, *args, &block)
70
+ end
71
+
72
+ def do_read(io) # :nodoc:
73
+ io.transform(eval_parameter(:transform)) do |transformed_io, _raw_io|
74
+ @type.do_read(transformed_io)
75
+ end
76
+ end
77
+
78
+ def do_write(io) # :nodoc:
79
+ io.transform(eval_parameter(:transform)) do |transformed_io, _raw_io|
80
+ @type.do_write(transformed_io)
81
+ end
82
+ end
83
+
84
+ def do_num_bytes # :nodoc:
85
+ to_binary_s.size
86
+ end
87
+ end
88
+
89
+ class SectionArgProcessor < BaseArgProcessor
90
+ include MultiFieldArgSeparator
91
+
92
+ def sanitize_parameters!(obj_class, params)
93
+ params.merge!(obj_class.dsl_params)
94
+ params.sanitize_object_prototype(:type)
95
+ end
96
+ end
97
+ end
data/lib/bindata/skip.rb CHANGED
@@ -1,4 +1,5 @@
1
- require "bindata/base_primitive"
1
+ require 'bindata/base_primitive'
2
+ require 'bindata/dsl'
2
3
 
3
4
  module BinData
4
5
  # Skip will skip over bytes from the input stream. If the stream is not
@@ -18,12 +19,14 @@ module BinData
18
19
  #
19
20
  #
20
21
  # class B < BinData::Record
21
- # skip until_valid: [:string, {read_length: 2, assert: "ef"} ]
22
- # string :b, read_length: 5
22
+ # skip do
23
+ # string read_length: 2, assert: 'ef'
24
+ # end
25
+ # string :s, read_length: 5
23
26
  # end
24
27
  #
25
28
  # obj = B.read("abcdefghij")
26
- # obj.b #=> "efghi"
29
+ # obj.s #=> "efghi"
27
30
  #
28
31
  #
29
32
  # == Parameters
@@ -33,15 +36,18 @@ module BinData
33
36
  #
34
37
  # <tt>:length</tt>:: The number of bytes to skip.
35
38
  # <tt>:to_abs_offset</tt>:: Skips to the given absolute offset.
36
- # <tt>:until_valid</tt>:: Skips untils a given byte pattern is matched.
39
+ # <tt>:until_valid</tt>:: Skips until a given byte pattern is matched.
37
40
  # This parameter contains a type that will raise
38
41
  # a BinData::ValidityError unless an acceptable byte
39
42
  # sequence is found. The type is represented by a
40
- # Symbol, or if the type is to have params #
41
- # passed to it, then it should be provided as #
43
+ # Symbol, or if the type is to have params
44
+ # passed to it, then it should be provided as
42
45
  # <tt>[type_symbol, hash_params]</tt>.
43
46
  #
44
47
  class Skip < BinData::BasePrimitive
48
+ extend DSLMixin
49
+
50
+ dsl_parser :skip
45
51
  arg_processor :skip
46
52
 
47
53
  optional_parameters :length, :to_abs_offset, :until_valid
@@ -57,10 +63,11 @@ module BinData
57
63
  #---------------
58
64
  private
59
65
 
60
- def value_to_binary_string(val)
66
+ def value_to_binary_string(_)
61
67
  len = skip_length
62
- if len < 0
63
- raise ValidityError, "#{debug_name} attempted to seek backwards by #{len.abs} bytes"
68
+ if len.negative?
69
+ raise ArgumentError,
70
+ "#{debug_name} attempted to seek backwards by #{len.abs} bytes"
64
71
  end
65
72
 
66
73
  "\000" * skip_length
@@ -68,66 +75,148 @@ module BinData
68
75
 
69
76
  def read_and_return_value(io)
70
77
  len = skip_length
71
- if len < 0
72
- raise ValidityError, "#{debug_name} attempted to seek backwards by #{len.abs} bytes"
78
+ if len.negative?
79
+ raise ArgumentError,
80
+ "#{debug_name} attempted to seek backwards by #{len.abs} bytes"
73
81
  end
74
82
 
75
- io.seekbytes(len)
83
+ io.skipbytes(len)
76
84
  ""
77
85
  end
78
86
 
79
87
  def sensible_default
80
88
  ""
81
89
  end
82
- end
83
90
 
84
- class SkipArgProcessor < BaseArgProcessor
85
- def sanitize_parameters!(obj_class, params)
86
- unless params.has_at_least_one_of?(:length, :to_abs_offset, :until_valid)
87
- raise ArgumentError,
88
- "#{obj_class} requires either :length, :to_abs_offset or :until_valid"
91
+ # Logic for the :length parameter
92
+ module SkipLengthPlugin
93
+ def skip_length
94
+ eval_parameter(:length)
89
95
  end
90
- params.must_be_integer(:to_abs_offset, :length)
91
- params.sanitize_object_prototype(:until_valid)
92
96
  end
93
- end
94
97
 
95
- # Logic for the :length parameter
96
- module SkipLengthPlugin
97
- def skip_length
98
- eval_parameter(:length)
98
+ # Logic for the :to_abs_offset parameter
99
+ module SkipToAbsOffsetPlugin
100
+ def skip_length
101
+ eval_parameter(:to_abs_offset) - abs_offset
102
+ end
99
103
  end
100
- end
101
104
 
102
- # Logic for the :to_abs_offset parameter
103
- module SkipToAbsOffsetPlugin
104
- def skip_length
105
- eval_parameter(:to_abs_offset) - abs_offset
106
- end
107
- end
105
+ # Logic for the :until_valid parameter
106
+ module SkipUntilValidPlugin
107
+ def skip_length
108
+ @skip_length ||= 0
109
+ end
108
110
 
109
- # Logic for the :until_valid parameter
110
- module SkipUntilValidPlugin
111
- def skip_length
112
- # no skipping when writing
113
- 0
114
- end
111
+ def read_and_return_value(io)
112
+ prototype = get_parameter(:until_valid)
113
+ validator = prototype.instantiate(nil, self)
114
+ fs = fast_search_for_obj(validator)
115
115
 
116
- def read_and_return_value(io)
117
- prototype = get_parameter(:until_valid)
118
- validator = prototype.instantiate(nil, self)
119
-
120
- valid = false
121
- until valid
122
- begin
123
- io.with_readahead do
124
- validator.read(io)
125
- valid = true
116
+ io.transform(ReadaheadIO.new) do |transformed_io, raw_io|
117
+ pos = 0
118
+ loop do
119
+ seek_to_pos(pos, raw_io)
120
+ validator.clear
121
+ validator.do_read(transformed_io)
122
+ break
123
+ rescue ValidityError
124
+ pos += 1
125
+
126
+ if fs
127
+ seek_to_pos(pos, raw_io)
128
+ pos += next_search_index(raw_io, fs)
129
+ end
126
130
  end
127
- rescue ValidityError
128
- io.readbytes(1)
131
+
132
+ seek_to_pos(pos, raw_io)
133
+ @skip_length = pos
129
134
  end
130
135
  end
136
+
137
+ def seek_to_pos(pos, io)
138
+ io.rollback
139
+ io.skip(pos)
140
+ end
141
+
142
+ # A fast search has a pattern string at a specific offset.
143
+ FastSearch = ::Struct.new('FastSearch', :pattern, :offset)
144
+
145
+ def fast_search_for(obj)
146
+ if obj.respond_to?(:asserted_binary_s)
147
+ FastSearch.new(obj.asserted_binary_s, obj.rel_offset)
148
+ else
149
+ nil
150
+ end
151
+ end
152
+
153
+ # If a search object has an +asserted_value+ field then we
154
+ # perform a faster search for a valid object.
155
+ def fast_search_for_obj(obj)
156
+ if BinData::Struct === obj
157
+ obj.each_pair(true) do |_, field|
158
+ fs = fast_search_for(field)
159
+ return fs if fs
160
+ end
161
+ elsif BinData::BasePrimitive === obj
162
+ return fast_search_for(obj)
163
+ end
164
+
165
+ nil
166
+ end
167
+
168
+ SEARCH_SIZE = 100_000
169
+
170
+ def next_search_index(io, fs)
171
+ buffer = binary_string("")
172
+
173
+ # start searching at fast_search offset
174
+ pos = fs.offset
175
+ io.skip(fs.offset)
176
+
177
+ loop do
178
+ data = io.read(SEARCH_SIZE)
179
+ raise EOFError, "no match" if data.nil?
180
+
181
+ buffer << data
182
+ index = buffer.index(fs.pattern)
183
+ if index
184
+ return pos + index - fs.offset
185
+ end
186
+
187
+ # advance buffer
188
+ searched = buffer.slice!(0..-fs.pattern.size)
189
+ pos += searched.size
190
+ end
191
+ end
192
+
193
+ class ReadaheadIO < BinData::IO::Transform
194
+ def before_transform
195
+ if !seekable?
196
+ raise IOError, "readahead is not supported on unseekable streams"
197
+ end
198
+
199
+ @mark = offset
200
+ end
201
+
202
+ def rollback
203
+ seek_abs(@mark)
204
+ end
205
+ end
206
+ end
207
+ end
208
+
209
+ class SkipArgProcessor < BaseArgProcessor
210
+ def sanitize_parameters!(obj_class, params)
211
+ params.merge!(obj_class.dsl_params)
212
+
213
+ unless params.has_at_least_one_of?(:length, :to_abs_offset, :until_valid)
214
+ raise ArgumentError,
215
+ "#{obj_class} requires :length, :to_abs_offset or :until_valid"
216
+ end
217
+
218
+ params.must_be_integer(:to_abs_offset, :length)
219
+ params.sanitize_object_prototype(:until_valid)
131
220
  end
132
221
  end
133
222
  end
@@ -1,4 +1,4 @@
1
- require "bindata/base_primitive"
1
+ require 'bindata/base_primitive'
2
2
 
3
3
  module BinData
4
4
  # A String is a sequence of bytes. This is the same as strings in Ruby 1.8.
@@ -121,6 +121,14 @@ module BinData
121
121
  def sensible_default
122
122
  ""
123
123
  end
124
+
125
+ # Warns when reading if :value && no :read_length
126
+ module WarnNoReadLengthPlugin
127
+ def read_and_return_value(io)
128
+ Kernel.warn "#{debug_name} does not have a :read_length parameter - returning empty string"
129
+ ""
130
+ end
131
+ end
124
132
  end
125
133
 
126
134
  class StringArgProcessor < BaseArgProcessor
@@ -142,12 +150,4 @@ module BinData
142
150
  pad_byte
143
151
  end
144
152
  end
145
-
146
- # Warns when reading if :value && no :read_length
147
- module WarnNoReadLengthPlugin
148
- def read_and_return_value(io)
149
- warn "#{debug_name} does not have a :read_length parameter - returning empty string"
150
- ""
151
- end
152
- end
153
153
  end
@@ -1,4 +1,4 @@
1
- require "bindata/base_primitive"
1
+ require 'bindata/base_primitive'
2
2
 
3
3
  module BinData
4
4
  # A BinData::Stringz object is a container for a zero ("\0") terminated
@@ -25,7 +25,6 @@ module BinData
25
25
  # <tt>:max_length</tt>:: The maximum length of the string including the zero
26
26
  # byte.
27
27
  class Stringz < BinData::BasePrimitive
28
-
29
28
  optional_parameters :max_length
30
29
 
31
30
  def assign(val)
@@ -47,14 +46,14 @@ module BinData
47
46
 
48
47
  def read_and_return_value(io)
49
48
  max_length = eval_parameter(:max_length)
50
- str = ""
49
+ str = binary_string("")
51
50
  i = 0
52
51
  ch = nil
53
52
 
54
53
  # read until zero byte or we have read in the max number of bytes
55
54
  while ch != "\0" && i != max_length
56
55
  ch = io.readbytes(1)
57
- str += ch
56
+ str << ch
58
57
  i += 1
59
58
  end
60
59
 
@@ -66,9 +65,15 @@ module BinData
66
65
  end
67
66
 
68
67
  def trim_and_zero_terminate(str)
68
+ max_length = eval_parameter(:max_length)
69
+ if max_length && max_length < 1
70
+ msg = "max_length must be >= 1 in #{debug_name} (got #{max_length})"
71
+ raise ArgumentError, msg
72
+ end
73
+
69
74
  result = binary_string(str)
70
75
  truncate_after_first_zero_byte!(result)
71
- trim_to!(result, eval_parameter(:max_length))
76
+ trim_to!(result, max_length)
72
77
  append_zero_byte_if_needed!(result)
73
78
  result
74
79
  end
@@ -79,16 +84,13 @@ module BinData
79
84
 
80
85
  def trim_to!(str, max_length = nil)
81
86
  if max_length
82
- max_length = 1 if max_length < 1
83
87
  str.slice!(max_length..-1)
84
- if str.length == max_length && str[-1, 1] != "\0"
85
- str[-1, 1] = "\0"
86
- end
88
+ str[-1, 1] = "\0" if str.length == max_length
87
89
  end
88
90
  end
89
91
 
90
92
  def append_zero_byte_if_needed!(str)
91
- if str.length == 0 || str[-1, 1] != "\0"
93
+ if str.empty? || str[-1, 1] != "\0"
92
94
  str << "\0"
93
95
  end
94
96
  end