bindata 0.9.3 → 0.10.0

Sign up to get free protection for your applications and to get access to all the features.

Potentially problematic release.


This version of bindata might be problematic. Click here for more details.

Files changed (47) hide show
  1. data/ChangeLog +18 -0
  2. data/NEWS +59 -0
  3. data/README +22 -23
  4. data/TODO +18 -12
  5. data/examples/gzip.rb +4 -4
  6. data/lib/bindata.rb +4 -3
  7. data/lib/bindata/array.rb +202 -132
  8. data/lib/bindata/base.rb +147 -166
  9. data/lib/bindata/{single.rb → base_primitive.rb} +82 -56
  10. data/lib/bindata/bits.rb +31 -770
  11. data/lib/bindata/choice.rb +157 -82
  12. data/lib/bindata/float.rb +25 -27
  13. data/lib/bindata/int.rb +144 -177
  14. data/lib/bindata/io.rb +59 -49
  15. data/lib/bindata/lazy.rb +80 -50
  16. data/lib/bindata/params.rb +134 -26
  17. data/lib/bindata/{single_value.rb → primitive.rb} +71 -64
  18. data/lib/bindata/{multi_value.rb → record.rb} +52 -70
  19. data/lib/bindata/registry.rb +49 -17
  20. data/lib/bindata/rest.rb +6 -10
  21. data/lib/bindata/sanitize.rb +55 -70
  22. data/lib/bindata/string.rb +60 -42
  23. data/lib/bindata/stringz.rb +34 -35
  24. data/lib/bindata/struct.rb +197 -152
  25. data/lib/bindata/trace.rb +35 -0
  26. data/spec/array_spec.rb +128 -112
  27. data/spec/{single_spec.rb → base_primitive_spec.rb} +102 -61
  28. data/spec/base_spec.rb +190 -185
  29. data/spec/bits_spec.rb +126 -98
  30. data/spec/choice_spec.rb +89 -98
  31. data/spec/example.rb +19 -0
  32. data/spec/float_spec.rb +28 -44
  33. data/spec/int_spec.rb +217 -127
  34. data/spec/io_spec.rb +41 -24
  35. data/spec/lazy_spec.rb +95 -49
  36. data/spec/primitive_spec.rb +191 -0
  37. data/spec/{multi_value_spec.rb → record_spec.rb} +124 -89
  38. data/spec/registry_spec.rb +53 -12
  39. data/spec/rest_spec.rb +2 -3
  40. data/spec/sanitize_spec.rb +47 -73
  41. data/spec/spec_common.rb +13 -1
  42. data/spec/string_spec.rb +34 -23
  43. data/spec/stringz_spec.rb +10 -18
  44. data/spec/struct_spec.rb +91 -63
  45. data/spec/system_spec.rb +291 -0
  46. metadata +12 -8
  47. data/spec/single_value_spec.rb +0 -131
data/lib/bindata/io.rb CHANGED
@@ -53,7 +53,6 @@ module BinData
53
53
  if positioning_supported?
54
54
  @raw_io.pos - @initial_pos
55
55
  else
56
- # stream does not support positioning
57
56
  0
58
57
  end
59
58
  end
@@ -69,7 +68,7 @@ module BinData
69
68
  #
70
69
  # If the data read is too short an IOError is raised.
71
70
  def readbytes(n)
72
- raise "Internal state error nbits = #{@rnbits}" if @rnbits > 8
71
+ raise "Internal state error nbits = #{@rnbits}" if @rnbits >= 8
73
72
  @rnbits = 0
74
73
  @rval = 0
75
74
 
@@ -79,9 +78,18 @@ module BinData
79
78
  str
80
79
  end
81
80
 
81
+ # Reads all remaining bytes from the stream.
82
+ def read_all_bytes
83
+ raise "Internal state error nbits = #{@rnbits}" if @rnbits >= 8
84
+ @rnbits = 0
85
+ @rval = 0
86
+
87
+ @raw_io.read
88
+ end
89
+
82
90
  # Reads exactly +nbits+ bits from the stream. +endian+ specifies whether
83
91
  # the bits are stored in +:big+ or +:little+ endian format.
84
- def readbits(nbits, endian = :big)
92
+ def readbits(nbits, endian)
85
93
  if @rendian != endian
86
94
  # don't mix bits of differing endian
87
95
  @rnbits = 0
@@ -90,9 +98,9 @@ module BinData
90
98
  end
91
99
 
92
100
  if endian == :big
93
- read_be_bits(nbits)
101
+ read_big_endian_bits(nbits)
94
102
  else
95
- read_le_bits(nbits)
103
+ read_little_endian_bits(nbits)
96
104
  end
97
105
  end
98
106
 
@@ -104,32 +112,29 @@ module BinData
104
112
 
105
113
  # Writes +nbits+ bits from +val+ to the stream. +endian+ specifies whether
106
114
  # the bits are to be stored in +:big+ or +:little+ endian format.
107
- def writebits(val, nbits, endian = :big)
108
- # clamp val to range
109
- val = val & ((1 << nbits) - 1)
110
-
115
+ def writebits(val, nbits, endian)
111
116
  if @wendian != endian
112
117
  # don't mix bits of differing endian
113
- flushbits if @wnbits > 0
118
+ flushbits
114
119
 
115
120
  @wendian = endian
116
121
  end
117
122
 
123
+ clamped_val = val & mask(nbits)
124
+
118
125
  if endian == :big
119
- write_be_bits(val, nbits)
126
+ write_big_endian_bits(clamped_val, nbits)
120
127
  else
121
- write_le_bits(val, nbits)
128
+ write_little_endian_bits(clamped_val, nbits)
122
129
  end
123
130
  end
124
131
 
125
132
  # To be called after all +writebits+ have been applied.
126
133
  def flushbits
127
- if @wnbits > 8
128
- raise "Internal state error nbits = #{@wnbits}" if @wnbits > 8
129
- elsif @wnbits > 0
134
+ raise "Internal state error nbits = #{@wnbits}" if @wnbits >= 8
135
+
136
+ if @wnbits > 0
130
137
  writebits(0, 8 - @wnbits, @wendian)
131
- else
132
- # do nothing
133
138
  end
134
139
  end
135
140
  alias_method :flush, :flushbits
@@ -137,7 +142,6 @@ module BinData
137
142
  #---------------
138
143
  private
139
144
 
140
- # Checks if positioning is supported by the underlying io stream.
141
145
  def positioning_supported?
142
146
  unless defined? @positioning_supported
143
147
  @positioning_supported = begin
@@ -150,52 +154,55 @@ module BinData
150
154
  @positioning_supported
151
155
  end
152
156
 
153
- # Reads exactly +nbits+ big endian bits from the stream.
154
- def read_be_bits(nbits)
155
- # ensure enough bits have accumulated
156
- while nbits > @rnbits
157
- byte = @raw_io.read(1)
158
- raise EOFError, "End of file reached" if byte.nil?
159
- byte = byte.unpack('C').at(0) & 0xff
160
-
161
- @rval = (@rval << 8) | byte
162
- @rnbits += 8
157
+ def read_big_endian_bits(nbits)
158
+ while @rnbits < nbits
159
+ accumulate_big_endian_bits
163
160
  end
164
161
 
165
- val = (@rval >> (@rnbits - nbits)) & ((1 << nbits) - 1)
162
+ val = (@rval >> (@rnbits - nbits)) & mask(nbits)
166
163
  @rnbits -= nbits
167
- @rval &= ((1 << @rnbits) - 1)
164
+ @rval &= mask(@rnbits)
168
165
 
169
166
  val
170
167
  end
171
168
 
172
- # Reads exactly +nbits+ little endian bits from the stream.
173
- def read_le_bits(nbits)
174
- # ensure enough bits have accumulated
175
- while nbits > @rnbits
176
- byte = @raw_io.read(1)
177
- raise EOFError, "End of file reached" if byte.nil?
178
- byte = byte.unpack('C').at(0) & 0xff
169
+ def accumulate_big_endian_bits
170
+ byte = @raw_io.read(1)
171
+ raise EOFError, "End of file reached" if byte.nil?
172
+ byte = byte.unpack('C').at(0) & 0xff
173
+
174
+ @rval = (@rval << 8) | byte
175
+ @rnbits += 8
176
+ end
179
177
 
180
- @rval = @rval | (byte << @rnbits)
181
- @rnbits += 8
178
+ def read_little_endian_bits(nbits)
179
+ while @rnbits < nbits
180
+ accumulate_little_endian_bits
182
181
  end
183
182
 
184
- val = @rval & ((1 << nbits) - 1)
183
+ val = @rval & mask(nbits)
185
184
  @rnbits -= nbits
186
185
  @rval >>= nbits
187
186
 
188
187
  val
189
188
  end
190
189
 
191
- # Writes +nbits+ bits from +val+ to the stream in big endian format.
192
- def write_be_bits(val, nbits)
190
+ def accumulate_little_endian_bits
191
+ byte = @raw_io.read(1)
192
+ raise EOFError, "End of file reached" if byte.nil?
193
+ byte = byte.unpack('C').at(0) & 0xff
194
+
195
+ @rval = @rval | (byte << @rnbits)
196
+ @rnbits += 8
197
+ end
198
+
199
+ def write_big_endian_bits(val, nbits)
193
200
  while nbits > 0
194
201
  bits_req = 8 - @wnbits
195
202
  if nbits >= bits_req
196
- msb_bits = (val >> (nbits - bits_req)) & ((1 << bits_req) - 1)
203
+ msb_bits = (val >> (nbits - bits_req)) & mask(bits_req)
197
204
  nbits -= bits_req
198
- val &= (1 << nbits) - 1
205
+ val &= mask(nbits)
199
206
 
200
207
  @wval = (@wval << bits_req) | msb_bits
201
208
  @raw_io.write(@wval.chr)
@@ -210,26 +217,29 @@ module BinData
210
217
  end
211
218
  end
212
219
 
213
- # Writes +nbits+ bits from +val+ to the stream in little endian format.
214
- def write_le_bits(val, nbits)
220
+ def write_little_endian_bits(val, nbits)
215
221
  while nbits > 0
216
222
  bits_req = 8 - @wnbits
217
223
  if nbits >= bits_req
218
- lsb_bits = val & ((1 << bits_req) - 1)
224
+ lsb_bits = val & mask(bits_req)
219
225
  nbits -= bits_req
220
226
  val >>= bits_req
221
227
 
222
- @wval |= (lsb_bits << @wnbits)
228
+ @wval = @wval | (lsb_bits << @wnbits)
223
229
  @raw_io.write(@wval.chr)
224
230
 
225
231
  @wval = 0
226
232
  @wnbits = 0
227
233
  else
228
- @wval |= (val << @wnbits)
234
+ @wval = @wval | (val << @wnbits)
229
235
  @wnbits += nbits
230
236
  nbits = 0
231
237
  end
232
238
  end
233
239
  end
240
+
241
+ def mask(nbits)
242
+ (1 << nbits) - 1
243
+ end
234
244
  end
235
245
  end
data/lib/bindata/lazy.rb CHANGED
@@ -5,31 +5,56 @@ module BinData
5
5
  #
6
6
  # BinData::String.new(:value => lambda { %w{a test message}.join(" ") })
7
7
  #
8
- # When evaluating lambdas, unknown methods are resolved in the context of
9
- # the parent of the bound data object, first as keys in #parameters, and
10
- # secondly as methods in this parent. This resolution propagates up the
11
- # chain of parent data objects.
8
+ # As a shortcut, :foo is the equivalent of lambda { foo }.
9
+ #
10
+ # When evaluating lambdas, unknown methods are resolved in the context of the
11
+ # parent of the bound data object. Resolution is attempted firstly as keys
12
+ # in #parameters, and secondly as methods in this parent. This
13
+ # resolution propagates up the chain of parent data objects.
14
+ #
15
+ # An evaluation will recurse until it returns a result that is not
16
+ # a lambda or a symbol.
12
17
  #
13
18
  # This resolution process makes the lambda easier to read as we just write
14
19
  # <tt>field</tt> instead of <tt>obj.field</tt>.
15
20
  class LazyEvaluator
21
+
16
22
  class << self
17
23
  # Lazily evaluates +val+ in the context of +obj+, with possibility of
18
24
  # +overrides+.
19
- def eval(val, obj, overrides = nil)
20
- env = self.new(obj)
21
- env.lazy_eval(val, overrides)
25
+ def eval(obj, val, overrides = {})
26
+ if can_eval?(val)
27
+ env = self.new(obj, overrides)
28
+ env.lazy_eval(val)
29
+ else
30
+ val
31
+ end
22
32
  end
23
- end
24
33
 
25
- # An empty hash shared by all instances
26
- @@empty_hash = Hash.new.freeze
34
+ #-------------
35
+ private
36
+
37
+ def can_eval?(val)
38
+ val.is_a?(Symbol) or val.respond_to?(:arity)
39
+ end
40
+ end
27
41
 
28
42
  # Creates a new evaluator. All lazy evaluation is performed in the
29
43
  # context of +obj+.
30
- def initialize(obj)
44
+ # +overrides+ is an optional +obj.parameters+ like hash.
45
+ def initialize(obj, overrides = {})
31
46
  @obj = obj
32
- @overrides = @@empty_hash
47
+ @overrides = overrides
48
+ end
49
+
50
+ def lazy_eval(val)
51
+ if val.is_a? Symbol
52
+ __send__(val)
53
+ elsif val.respond_to? :arity
54
+ instance_eval(&val)
55
+ else
56
+ val
57
+ end
33
58
  end
34
59
 
35
60
  # Returns a LazyEvaluator for the parent of this data object.
@@ -41,35 +66,30 @@ module BinData
41
66
  end
42
67
  end
43
68
 
44
- # Evaluates +val+ in the context of this data object. Evaluation
45
- # recurses until it yields a value that is not a symbol or lambda.
46
- # +overrides+ is an optional +obj.parameters+ like hash.
47
- def lazy_eval(val, overrides = nil)
48
- result = val
49
- @overrides = overrides if overrides
50
- if val.is_a? Symbol
51
- # treat :foo as lambda { foo }
52
- result = __send__(val)
53
- elsif val.respond_to? :arity
54
- result = instance_eval(&val)
69
+ # Returns the index of this data object inside it's nearest container
70
+ # array.
71
+ def index
72
+ return @overrides[:index] if @overrides.has_key?(:index)
73
+
74
+ bindata_array_class = BinData.const_defined?("Array") ?
75
+ BinData.const_get("Array") : nil
76
+ child = @obj
77
+ parent = @obj.parent
78
+ while parent
79
+ if parent.class == bindata_array_class
80
+ return parent.find_index_of(child)
81
+ end
82
+ child = parent
83
+ parent = parent.parent
55
84
  end
56
- @overrides = @@empty_hash
57
- result
85
+ raise NoMethodError, "no index found"
58
86
  end
59
87
 
60
88
  def method_missing(symbol, *args)
61
- if @overrides.include?(symbol)
62
- @overrides[symbol]
63
- elsif symbol == :index
64
- array_index
65
- elsif @obj.parent
66
- val = symbol
67
- if @obj.parent.parameters and @obj.parent.parameters.has_key?(symbol)
68
- val = @obj.parent.parameters[symbol]
69
- elsif @obj.parent and @obj.parent.respond_to?(symbol)
70
- val = @obj.parent.__send__(symbol, *args)
71
- end
72
- LazyEvaluator.eval(val, @obj.parent)
89
+ return @overrides[symbol] if @overrides.has_key?(symbol)
90
+
91
+ if @obj.parent
92
+ eval_symbol_in_parent_context(symbol, args)
73
93
  else
74
94
  super
75
95
  end
@@ -78,21 +98,31 @@ module BinData
78
98
  #---------------
79
99
  private
80
100
 
81
- # Returns the index in the closest ancestor array of this data object.
82
- def array_index
83
- bindata_array_klass = BinData.const_defined?("Array") ?
84
- BinData.const_get("Array") : nil
85
- child = @obj
86
- parent = @obj.parent
87
- while parent
88
- if parent.class == bindata_array_klass
89
- return parent.index(child)
90
- end
91
- child = parent
92
- parent = parent.parent
101
+ def eval_symbol_in_parent_context(symbol, args)
102
+ result = resolve_symbol_in_parent_context(symbol, args)
103
+ recursively_eval(result, args)
104
+ end
105
+
106
+ def resolve_symbol_in_parent_context(symbol, args)
107
+ obj_parent = @obj.parent
108
+
109
+ if obj_parent.has_parameter?(symbol)
110
+ result = obj_parent.get_parameter(symbol)
111
+ elsif obj_parent.respond_to?(symbol)
112
+ result = obj_parent.__send__(symbol, *args)
113
+ else
114
+ result = symbol
93
115
  end
94
- raise NoMethodError, "no index found"
95
116
  end
96
117
 
118
+ def recursively_eval(val, args)
119
+ if val.is_a?(Symbol)
120
+ parent.__send__(val, *args)
121
+ elsif val.respond_to?(:arity)
122
+ parent.instance_eval(&val)
123
+ else
124
+ val
125
+ end
126
+ end
97
127
  end
98
128
  end
@@ -1,36 +1,144 @@
1
1
  module BinData
2
- module Parameters
3
-
4
- # Definition method for creating a method that maintains a collection
5
- # of parameters. This is used for creating collections of mandatory,
6
- # optional, default etc parameters. The parameters for a class will
7
- # include the parameters of its ancestors.
8
- def define_x_parameters(name, empty, &block) #:nodoc:
9
- full_name_singular = "#{name.to_s}_parameter"
10
- full_name_plural = "#{name.to_s}_parameters"
11
-
12
- sym = full_name_plural.to_sym
13
- iv = "@#{full_name_plural}".to_sym
14
-
15
- body = Proc.new do |*args|
16
- # initialize collection to duplicate ancestor's collection
17
- unless instance_variable_defined?(iv)
18
- ancestor = ancestors[1..-1].find { |a| a.respond_to?(sym) }
19
- val = ancestor.nil? ? empty : ancestor.send(sym).dup
20
- instance_variable_set(iv, val)
2
+ # BinData objects accept parameters when initializing. AcceptedParameters
3
+ # allow a BinData class to declaratively identify accepted parameters as
4
+ # mandatory, optional, default or mutually exclusive.
5
+ class AcceptedParameters
6
+ class << self
7
+ def define_all_accessors(obj_class, param_name)
8
+ all_accessors = [:mandatory, :optional, :default, :mutually_exclusive]
9
+ all_accessors.each do |accessor|
10
+ define_accessor(obj_class, param_name, accessor)
21
11
  end
12
+ end
13
+
14
+ def define_accessors(obj_class, param_name, *accessors)
15
+ accessors.each do |accessor|
16
+ define_accessor(obj_class, param_name, accessor)
17
+ end
18
+ end
19
+
20
+ def get(obj_class, param_name)
21
+ obj_class.__send__(internal_storage_method_name(param_name))
22
+ end
23
+
24
+ #-------------
25
+ private
26
+
27
+ def define_accessor(obj_class, param_name, accessor)
28
+ singular_name = accessor_method_name(accessor)
29
+ plural_name = singular_name + "s"
30
+ internal_storage_method = internal_storage_method_name(param_name)
31
+
32
+ ensure_parameter_storage_exists(obj_class, internal_storage_method)
33
+
34
+ obj_class.class_eval <<-END
35
+ def #{singular_name}(*args)
36
+ #{internal_storage_method}.#{accessor}(*args)
37
+ end
38
+ alias_method :#{plural_name}, :#{singular_name}
39
+ END
40
+ end
41
+
42
+ def accessor_method_name(accessor)
43
+ "#{accessor}_parameter"
44
+ end
45
+
46
+ def internal_storage_method_name(param_name)
47
+ "_bindata_accepted_parameters_#{param_name}"
48
+ end
49
+
50
+ def ensure_parameter_storage_exists(obj_class, method_name)
51
+ return if obj_class.instance_methods.include?(method_name)
52
+
53
+ iv = "@#{method_name}"
54
+ obj_class.class_eval <<-END
55
+ def #{method_name}
56
+ unless defined? #{iv}
57
+ ancestor = ancestors[1..-1].find { |a| a.instance_variable_defined?(:#{iv}) }
58
+ ancestor_params = ancestor.nil? ? nil : ancestor.instance_variable_get(:#{iv})
59
+ #{iv} = AcceptedParameters.new(ancestor_params)
60
+ end
61
+ #{iv}
62
+ end
63
+ END
64
+ end
65
+ end
66
+
67
+ def initialize(ancestor_params = nil)
68
+ @mandatory = ancestor_params ? ancestor_params.mandatory : []
69
+ @optional = ancestor_params ? ancestor_params.optional : []
70
+ @default = ancestor_params ? ancestor_params.default : Hash.new
71
+ @mutually_exclusive = ancestor_params ? ancestor_params.mutually_exclusive : []
72
+ end
73
+
74
+ def mandatory(*args)
75
+ if not args.empty?
76
+ @mandatory.concat(args.collect { |a| a.to_sym })
77
+ @mandatory.uniq!
78
+ end
79
+ @mandatory.dup
80
+ end
81
+
82
+ def optional(*args)
83
+ if not args.empty?
84
+ @optional.concat(args.collect { |a| a.to_sym })
85
+ @optional.uniq!
86
+ end
87
+ @optional.dup
88
+ end
22
89
 
23
- # add new parameters to the collection
24
- if not args.empty?
25
- block.call(instance_variable_get(iv), args)
90
+ def default(args = {})
91
+ if not args.empty?
92
+ args.each_pair do |k,v|
93
+ @default[k.to_sym] = v
26
94
  end
95
+ end
96
+ @default.dup
97
+ end
98
+
99
+ def mutually_exclusive(*args)
100
+ arg1, arg2 = args
101
+ if arg1 != nil && arg2 != nil
102
+ @mutually_exclusive.push([arg1.to_sym, arg2.to_sym])
103
+ @mutually_exclusive.uniq!
104
+ end
105
+ @mutually_exclusive.dup
106
+ end
107
+
108
+ def all
109
+ (@mandatory + @optional + @default.keys).uniq
110
+ end
111
+
112
+ def sanitize_parameters!(sanitizer, params)
113
+ merge_default_parameters!(params)
114
+ ensure_mandatory_parameters_exist(params)
115
+ ensure_mutual_exclusion_of_parameters(params)
116
+ end
27
117
 
28
- # return collection
29
- instance_variable_get(iv)
118
+ #---------------
119
+ private
120
+
121
+ def merge_default_parameters!(params)
122
+ @default.each do |k,v|
123
+ params[k] = v unless params.has_key?(k)
30
124
  end
125
+ end
31
126
 
32
- define_method(sym, body)
33
- alias_method(full_name_singular.to_sym, sym)
127
+ def ensure_mandatory_parameters_exist(params)
128
+ @mandatory.each do |prm|
129
+ unless params.has_key?(prm)
130
+ raise ArgumentError, "parameter ':#{prm}' must be specified"
131
+ end
132
+ end
133
+ end
134
+
135
+ def ensure_mutual_exclusion_of_parameters(params)
136
+ @mutually_exclusive.each do |param1, param2|
137
+ if params.has_key?(param1) and params.has_key?(param2)
138
+ raise ArgumentError, "params ':#{param1}' and ':#{param2}' " +
139
+ "are mutually exclusive"
140
+ end
141
+ end
34
142
  end
35
143
  end
36
144
  end