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
@@ -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
@@ -37,55 +36,56 @@ module BinData
37
36
  end
38
37
 
39
38
  def lookup(name, hints = {})
40
- the_class = @registry[normalize_name(name, hints)]
41
- if the_class
42
- the_class
43
- elsif @registry[normalize_name(name, hints.merge(endian: :big))]
44
- raise(UnRegisteredTypeError, "#{name}, do you need to specify endian?")
45
- else
46
- raise(UnRegisteredTypeError, name)
39
+ search_names(name, hints).each do |search|
40
+ register_dynamic_class(search)
41
+ if @registry.has_key?(search)
42
+ return @registry[search]
43
+ end
47
44
  end
45
+
46
+ # give the user a hint if the endian keyword is missing
47
+ search_names(name, hints.merge(endian: :big)).each do |search|
48
+ register_dynamic_class(search)
49
+ if @registry.has_key?(search)
50
+ raise(UnRegisteredTypeError, "#{name}, do you need to specify endian?")
51
+ end
52
+ end
53
+
54
+ raise(UnRegisteredTypeError, name)
48
55
  end
49
56
 
50
57
  # Convert CamelCase +name+ to underscore style.
51
58
  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
59
+ name
60
+ .to_s
61
+ .sub(/.*::/, "")
62
+ .gsub(/([A-Z]+)([A-Z][a-z])/, '\1_\2')
63
+ .gsub(/([a-z\d])([A-Z])/, '\1_\2')
64
+ .tr('-', '_')
65
+ .downcase
59
66
  end
60
67
 
61
68
  #---------------
62
69
  private
63
70
 
64
- def normalize_name(name, hints)
65
- name = underscore_name(name)
66
-
67
- if !registered?(name)
68
- search_prefix = [""].concat(Array(hints[:search_prefix]))
69
- search_prefix.each do |prefix|
70
- nwp = name_with_prefix(name, prefix)
71
- if registered?(nwp)
72
- name = nwp
73
- break
74
- end
75
-
76
- nwe = name_with_endian(nwp, hints[:endian])
77
- if registered?(nwe)
78
- name = nwe
79
- break
80
- end
81
- end
71
+ def search_names(name, hints)
72
+ base = underscore_name(name)
73
+ searches = []
74
+
75
+ search_prefix = [""] + Array(hints[:search_prefix])
76
+ search_prefix.each do |prefix|
77
+ nwp = name_with_prefix(base, prefix)
78
+ nwe = name_with_endian(nwp, hints[:endian])
79
+
80
+ searches << nwp
81
+ searches << nwe if nwe
82
82
  end
83
83
 
84
- name
84
+ searches
85
85
  end
86
86
 
87
87
  def name_with_prefix(name, prefix)
88
- prefix = prefix.to_s.chomp("_")
88
+ prefix = prefix.to_s.chomp('_')
89
89
  if prefix == ""
90
90
  name
91
91
  else
@@ -94,26 +94,21 @@ module BinData
94
94
  end
95
95
 
96
96
  def name_with_endian(name, endian)
97
- return name if endian.nil?
97
+ return nil if endian.nil?
98
98
 
99
- suffix = (endian == :little) ? "le" : "be"
100
- if /^u?int\d+$/ =~ name
99
+ suffix = (endian == :little) ? 'le' : 'be'
100
+ if /^u?int\d+$/.match?(name)
101
101
  name + suffix
102
102
  else
103
- name + "_" + suffix
103
+ name + '_' + suffix
104
104
  end
105
105
  end
106
106
 
107
- def registered?(name)
108
- register_dynamic_class(name) unless @registry.key?(name)
109
-
110
- @registry.key?(name)
111
- end
112
-
113
107
  def register_dynamic_class(name)
114
- if /^u?int\d+(le|be)$/ =~ name || /^s?bit\d+(le)?$/ =~ name
108
+ if /^u?int\d+(le|be)$/.match?(name) || /^s?bit\d+(le)?$/.match?(name)
115
109
  class_name = name.gsub(/(?:^|_)(.)/) { $1.upcase }
116
110
  begin
111
+ # call const_get for side effect of creating class
117
112
  BinData.const_get(class_name)
118
113
  rescue NameError
119
114
  end
@@ -122,9 +117,9 @@ module BinData
122
117
 
123
118
  def warn_if_name_is_already_registered(name, class_to_register)
124
119
  prev_class = @registry[name]
125
- if $VERBOSE && prev_class && prev_class != class_to_register
126
- warn "warning: replacing registered class #{prev_class} " \
127
- "with #{class_to_register}"
120
+ if prev_class && prev_class != class_to_register
121
+ Kernel.warn "warning: replacing registered class #{prev_class} " \
122
+ "with #{class_to_register}"
128
123
  end
129
124
  end
130
125
  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