jbangert-bindata 1.5.0

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 (71) hide show
  1. data/.gitignore +1 -0
  2. data/BSDL +22 -0
  3. data/COPYING +52 -0
  4. data/ChangeLog.rdoc +204 -0
  5. data/Gemfile +2 -0
  6. data/INSTALL +11 -0
  7. data/NEWS.rdoc +164 -0
  8. data/README.md +54 -0
  9. data/Rakefile +13 -0
  10. data/bindata.gemspec +31 -0
  11. data/doc/manual.haml +407 -0
  12. data/doc/manual.md +1649 -0
  13. data/examples/NBT.txt +149 -0
  14. data/examples/gzip.rb +161 -0
  15. data/examples/ip_address.rb +22 -0
  16. data/examples/list.rb +124 -0
  17. data/examples/nbt.rb +178 -0
  18. data/lib/bindata.rb +33 -0
  19. data/lib/bindata/alignment.rb +83 -0
  20. data/lib/bindata/array.rb +335 -0
  21. data/lib/bindata/base.rb +388 -0
  22. data/lib/bindata/base_primitive.rb +214 -0
  23. data/lib/bindata/bits.rb +87 -0
  24. data/lib/bindata/choice.rb +216 -0
  25. data/lib/bindata/count_bytes_remaining.rb +35 -0
  26. data/lib/bindata/deprecated.rb +50 -0
  27. data/lib/bindata/dsl.rb +312 -0
  28. data/lib/bindata/float.rb +80 -0
  29. data/lib/bindata/int.rb +184 -0
  30. data/lib/bindata/io.rb +274 -0
  31. data/lib/bindata/lazy.rb +105 -0
  32. data/lib/bindata/offset.rb +91 -0
  33. data/lib/bindata/params.rb +135 -0
  34. data/lib/bindata/primitive.rb +135 -0
  35. data/lib/bindata/record.rb +110 -0
  36. data/lib/bindata/registry.rb +92 -0
  37. data/lib/bindata/rest.rb +35 -0
  38. data/lib/bindata/sanitize.rb +290 -0
  39. data/lib/bindata/skip.rb +48 -0
  40. data/lib/bindata/string.rb +145 -0
  41. data/lib/bindata/stringz.rb +96 -0
  42. data/lib/bindata/struct.rb +388 -0
  43. data/lib/bindata/trace.rb +94 -0
  44. data/lib/bindata/version.rb +3 -0
  45. data/setup.rb +1585 -0
  46. data/spec/alignment_spec.rb +61 -0
  47. data/spec/array_spec.rb +331 -0
  48. data/spec/base_primitive_spec.rb +238 -0
  49. data/spec/base_spec.rb +376 -0
  50. data/spec/bits_spec.rb +163 -0
  51. data/spec/choice_spec.rb +263 -0
  52. data/spec/count_bytes_remaining_spec.rb +38 -0
  53. data/spec/deprecated_spec.rb +31 -0
  54. data/spec/example.rb +21 -0
  55. data/spec/float_spec.rb +37 -0
  56. data/spec/int_spec.rb +216 -0
  57. data/spec/io_spec.rb +352 -0
  58. data/spec/lazy_spec.rb +217 -0
  59. data/spec/primitive_spec.rb +202 -0
  60. data/spec/record_spec.rb +530 -0
  61. data/spec/registry_spec.rb +108 -0
  62. data/spec/rest_spec.rb +26 -0
  63. data/spec/skip_spec.rb +27 -0
  64. data/spec/spec_common.rb +58 -0
  65. data/spec/string_spec.rb +300 -0
  66. data/spec/stringz_spec.rb +118 -0
  67. data/spec/struct_spec.rb +350 -0
  68. data/spec/system_spec.rb +380 -0
  69. data/tasks/manual.rake +36 -0
  70. data/tasks/rspec.rake +17 -0
  71. metadata +208 -0
@@ -0,0 +1,388 @@
1
+ require 'bindata/io'
2
+ require 'bindata/lazy'
3
+ require 'bindata/offset'
4
+ require 'bindata/params'
5
+ require 'bindata/registry'
6
+ require 'bindata/sanitize'
7
+
8
+ module BinData
9
+ # Error raised when unexpected results occur when reading data from IO.
10
+ class ValidityError < StandardError ; end
11
+
12
+ # ArgExtractors take the arguments passed to BinData::Base.new and
13
+ # separates them into [value, parameters, parent].
14
+ class BaseArgExtractor
15
+ @@empty_hash = Hash.new.freeze
16
+
17
+ def self.extract(the_class, the_args)
18
+ args = the_args.dup
19
+ value = parameters = parent = nil
20
+
21
+ if args.length > 1 and args.last.is_a? BinData::Base
22
+ parent = args.pop
23
+ end
24
+
25
+ if args.length > 0 and args.last.is_a? Hash
26
+ parameters = args.pop
27
+ end
28
+
29
+ if args.length > 0
30
+ value = args.pop
31
+ end
32
+
33
+ parameters ||= @@empty_hash
34
+
35
+ return [value, parameters, parent]
36
+ end
37
+ end
38
+
39
+ # This is the abstract base class for all data objects.
40
+ class Base
41
+ include AcceptedParametersMixin
42
+ include CheckOrAdjustOffsetMixin
43
+
44
+ class << self
45
+
46
+ # Instantiates this class and reads from +io+, returning the newly
47
+ # created data object.
48
+ def read(io)
49
+ obj = self.new
50
+ obj.read(io)
51
+ obj
52
+ end
53
+
54
+ # The arg extractor for this class.
55
+ def arg_extractor
56
+ BaseArgExtractor
57
+ end
58
+
59
+ # The name of this class as used by Records, Arrays etc.
60
+ def bindata_name
61
+ RegisteredClasses.underscore_name(self.name)
62
+ end
63
+
64
+ # Call this method if this class is abstract and not to be used.
65
+ def unregister_self
66
+ RegisteredClasses.unregister(name)
67
+ end
68
+
69
+ # Registers all subclasses of this class for use
70
+ def register_subclasses #:nodoc:
71
+ class << self
72
+ define_method(:inherited) do |subclass|
73
+ RegisteredClasses.register(subclass.name, subclass)
74
+ register_subclasses
75
+ end
76
+ end
77
+ end
78
+
79
+ private :unregister_self, :register_subclasses
80
+ end
81
+
82
+ # Register all subclasses of this class.
83
+ register_subclasses
84
+
85
+ # The registered name may be provided explicitly.
86
+ optional_parameter :name
87
+
88
+ # Creates a new data object.
89
+ #
90
+ # Args are optional, but if present, must be in the following order.
91
+ #
92
+ # +value+ is a value that is +assign+ed immediately after initialization.
93
+ #
94
+ # +parameters+ is a hash containing symbol keys. Some parameters may
95
+ # reference callable objects (methods or procs).
96
+ #
97
+ # +parent+ is the parent data object (e.g. struct, array, choice) this
98
+ # object resides under.
99
+ #
100
+ # == Parameters
101
+ #
102
+ # Parameters may be provided at initialisation to control the behaviour of
103
+ # an object. These params are:
104
+ #
105
+ # <tt>:name</tt>:: The name that this object can be referred to may be
106
+ # set explicitly. This is only useful when dynamically
107
+ # generating types.
108
+ # <code><pre>
109
+ # BinData::Struct.new(:name => :my_struct, :fields => ...)
110
+ # array = BinData::Array.new(:type => :my_struct)
111
+ # </pre></code>
112
+ #
113
+ def initialize(*args)
114
+ value, parameters, parent = extract_args(args)
115
+
116
+ @params = SanitizedParameters.sanitize(parameters, self.class)
117
+ @parent = parent
118
+
119
+ register_prototype
120
+ add_methods_for_check_or_adjust_offset
121
+
122
+ initialize_shared_instance
123
+ initialize_instance
124
+ assign(value) if value
125
+ end
126
+
127
+ attr_accessor :parent
128
+ protected :parent=
129
+
130
+ # Creates a new data object based on this instance.
131
+ #
132
+ # All parameters will be be duplicated. Use this method
133
+ # when creating multiple objects with the same parameters.
134
+ def new(value = nil, parent = nil)
135
+ obj = clone
136
+ obj.parent = parent if parent
137
+ obj.initialize_instance
138
+ obj.assign(value) if value
139
+
140
+ obj
141
+ end
142
+
143
+ # Returns the result of evaluating the parameter identified by +key+.
144
+ #
145
+ # +overrides+ is an optional +parameters+ like hash that allow the
146
+ # parameters given at object construction to be overridden.
147
+ #
148
+ # Returns nil if +key+ does not refer to any parameter.
149
+ def eval_parameter(key, overrides = nil)
150
+ value = get_parameter(key)
151
+ if value.is_a?(Symbol) or value.respond_to?(:arity)
152
+ lazy_evaluator.lazy_eval(value, overrides)
153
+ else
154
+ value
155
+ end
156
+ end
157
+
158
+ # Returns a lazy evaluator for this object.
159
+ def lazy_evaluator #:nodoc:
160
+ @lazy ||= LazyEvaluator.new(self)
161
+ end
162
+
163
+ # Returns the parameter referenced by +key+.
164
+ # Use this method if you are sure the parameter is not to be evaluated.
165
+ # You most likely want #eval_parameter.
166
+ def get_parameter(key)
167
+ @params[key]
168
+ end
169
+
170
+ # Returns whether +key+ exists in the +parameters+ hash.
171
+ def has_parameter?(key)
172
+ @params.has_parameter?(key)
173
+ end
174
+
175
+ # Reads data into this data object.
176
+ def read(io)
177
+ io = BinData::IO.new(io) unless BinData::IO === io
178
+
179
+ @in_read = true
180
+ clear
181
+ do_read(io)
182
+ @in_read = false
183
+
184
+ self
185
+ end
186
+
187
+ #:nodoc:
188
+ attr_reader :in_read
189
+ protected :in_read
190
+
191
+ # Returns if this object is currently being read. This is used
192
+ # internally by BasePrimitive.
193
+ def reading? #:nodoc:
194
+ furthest_ancestor.in_read
195
+ end
196
+ protected :reading?
197
+
198
+ # Writes the value for this data object to +io+.
199
+ def write(io)
200
+ io = BinData::IO.new(io) unless BinData::IO === io
201
+
202
+ do_write(io)
203
+ io.flush
204
+ self
205
+ end
206
+
207
+ # Returns the number of bytes it will take to write this data object.
208
+ def num_bytes
209
+ do_num_bytes.ceil
210
+ end
211
+
212
+ # Returns the string representation of this data object.
213
+ def to_binary_s
214
+ io = BinData::IO.create_string_io
215
+ write(io)
216
+ io.rewind
217
+ io.read
218
+ end
219
+
220
+ # Return a human readable representation of this data object.
221
+ def inspect
222
+ snapshot.inspect
223
+ end
224
+
225
+ # Return a string representing this data object.
226
+ def to_s
227
+ snapshot.to_s
228
+ end
229
+
230
+ # Work with Ruby's pretty-printer library.
231
+ def pretty_print(pp) #:nodoc:
232
+ pp.pp(snapshot)
233
+ end
234
+
235
+ # Override and delegate =~ as it is defined in Object.
236
+ def =~(other)
237
+ snapshot =~ other
238
+ end
239
+
240
+ # Returns a user friendly name of this object for debugging purposes.
241
+ def debug_name
242
+ if @parent
243
+ @parent.debug_name_of(self)
244
+ else
245
+ "obj"
246
+ end
247
+ end
248
+
249
+ # Returns the offset of this object wrt to its most distant ancestor.
250
+ def offset
251
+ if @parent
252
+ @parent.offset + @parent.offset_of(self)
253
+ else
254
+ 0
255
+ end
256
+ end
257
+
258
+ # Returns the offset of this object wrt to its parent.
259
+ def rel_offset
260
+ if @parent
261
+ @parent.offset_of(self)
262
+ else
263
+ 0
264
+ end
265
+ end
266
+
267
+ def ==(other) #:nodoc:
268
+ # double dispatch
269
+ other == snapshot
270
+ end
271
+
272
+ # A version of +respond_to?+ used by the lazy evaluator. It doesn't
273
+ # reinvoke the evaluator so as to avoid infinite evaluation loops.
274
+ def safe_respond_to?(symbol, include_private = false) #:nodoc:
275
+ respond_to?(symbol, include_private)
276
+ end
277
+ alias_method :orig_respond_to?, :respond_to? #:nodoc:
278
+
279
+ #---------------
280
+ private
281
+
282
+ def extract_args(the_args)
283
+ self.class.arg_extractor.extract(self.class, the_args)
284
+ end
285
+
286
+ def register_prototype
287
+ if has_parameter?(:name)
288
+ RegisteredClasses.register(get_parameter(:name), self)
289
+ end
290
+ end
291
+
292
+ def furthest_ancestor
293
+ if parent.nil?
294
+ self
295
+ else
296
+ an = parent
297
+ an = an.parent while an.parent
298
+ an
299
+ end
300
+ end
301
+
302
+ def binary_string(str)
303
+ if str.respond_to?(:force_encoding)
304
+ str.dup.force_encoding(Encoding::BINARY)
305
+ else
306
+ str.dup
307
+ end
308
+ end
309
+
310
+ ###########################################################################
311
+ # To be implemented by subclasses
312
+
313
+ # Performs sanity checks on the given parameters. This method converts
314
+ # the parameters to the form expected by this data object.
315
+ def self.sanitize_parameters!(parameters) #:nodoc:
316
+ end
317
+
318
+ # Initializes the state of the object. All instance variables that
319
+ # are used by the object must be initialized here.
320
+ def initialize_instance
321
+ end
322
+
323
+ # Initialises state that is shared by objects with the same parameters.
324
+ #
325
+ # This should only be used when optimising for performance. Instance
326
+ # variables set here, and changes to the singleton class will be shared
327
+ # between all objects that are initialized with the same parameters.
328
+ # This method is called only once for a particular set of parameters.
329
+ def initialize_shared_instance
330
+ end
331
+
332
+ # Resets the internal state to that of a newly created object.
333
+ def clear
334
+ raise NotImplementedError
335
+ end
336
+
337
+ # Returns true if the object has not been changed since creation.
338
+ def clear?
339
+ raise NotImplementedError
340
+ end
341
+
342
+ # Assigns the value of +val+ to this data object. Note that +val+ must
343
+ # always be deep copied to ensure no aliasing problems can occur.
344
+ def assign(val)
345
+ raise NotImplementedError
346
+ end
347
+
348
+ # Returns a snapshot of this data object.
349
+ def snapshot
350
+ raise NotImplementedError
351
+ end
352
+
353
+ # Returns the debug name of +child+. This only needs to be implemented
354
+ # by objects that contain child objects.
355
+ def debug_name_of(child) #:nodoc:
356
+ debug_name
357
+ end
358
+
359
+ # Returns the offset of +child+. This only needs to be implemented
360
+ # by objects that contain child objects.
361
+ def offset_of(child) #:nodoc:
362
+ 0
363
+ end
364
+
365
+ # Reads the data for this data object from +io+.
366
+ def do_read(io) #:nodoc:
367
+ raise NotImplementedError
368
+ end
369
+
370
+ # Writes the value for this data to +io+.
371
+ def do_write(io) #:nodoc:
372
+ raise NotImplementedError
373
+ end
374
+
375
+ # Returns the number of bytes it will take to write this data.
376
+ def do_num_bytes #:nodoc:
377
+ raise NotImplementedError
378
+ end
379
+
380
+ # Set visibility requirements of methods to implement
381
+ public :clear, :clear?, :assign, :snapshot, :debug_name_of, :offset_of
382
+ protected :initialize_instance, :initialize_shared_instance
383
+ protected :do_read, :do_write, :do_num_bytes
384
+
385
+ # End To be implemented by subclasses
386
+ ###########################################################################
387
+ end
388
+ end
@@ -0,0 +1,214 @@
1
+ require 'bindata/base'
2
+
3
+ module BinData
4
+ # A BinData::BasePrimitive object is a container for a value that has a
5
+ # particular binary representation. A value corresponds to a primitive type
6
+ # such as as integer, float or string. Only one value can be contained by
7
+ # this object. This value can be read from or written to an IO stream.
8
+ #
9
+ # require 'bindata'
10
+ #
11
+ # obj = BinData::Uint8.new(:initial_value => 42)
12
+ # obj #=> 42
13
+ # obj.assign(5)
14
+ # obj #=> 5
15
+ # obj.clear
16
+ # obj #=> 42
17
+ #
18
+ # obj = BinData::Uint8.new(:value => 42)
19
+ # obj #=> 42
20
+ # obj.assign(5)
21
+ # obj #=> 42
22
+ #
23
+ # obj = BinData::Uint8.new(:check_value => 3)
24
+ # obj.read("\005") #=> BinData::ValidityError: value is '5' but expected '3'
25
+ #
26
+ # obj = BinData::Uint8.new(:check_value => lambda { value < 5 })
27
+ # obj.read("\007") #=> BinData::ValidityError: value not as expected
28
+ #
29
+ # == Parameters
30
+ #
31
+ # Parameters may be provided at initialisation to control the behaviour of
32
+ # an object. These params include those for BinData::Base as well as:
33
+ #
34
+ # [<tt>:initial_value</tt>] This is the initial value to use before one is
35
+ # either #read or explicitly set with #value=.
36
+ # [<tt>:value</tt>] The object will always have this value.
37
+ # Calls to #value= are ignored when
38
+ # using this param. While reading, #value
39
+ # will return the value of the data read from the
40
+ # IO, not the result of the <tt>:value</tt> param.
41
+ # [<tt>:check_value</tt>] Raise an error unless the value read in meets
42
+ # this criteria. The variable +value+ is made
43
+ # available to any lambda assigned to this
44
+ # parameter. A boolean return indicates success
45
+ # or failure. Any other return is compared to
46
+ # the value just read in.
47
+ class BasePrimitive < BinData::Base
48
+ unregister_self
49
+
50
+ optional_parameters :initial_value, :value, :check_value
51
+ mutually_exclusive_parameters :initial_value, :value
52
+
53
+ def initialize_shared_instance
54
+ if has_parameter?(:check_value)
55
+ class << self
56
+ alias_method :do_read_without_check_value, :do_read
57
+ alias_method :do_read, :do_read_with_check_value
58
+ end
59
+ end
60
+ if has_parameter?(:value)
61
+ class << self
62
+ alias_method :_value, :_value_with_value
63
+ end
64
+ end
65
+ if has_parameter?(:initial_value)
66
+ class << self
67
+ alias_method :_value, :_value_with_initial_value
68
+ end
69
+ end
70
+ end
71
+
72
+ def initialize_instance
73
+ @value = nil
74
+ end
75
+
76
+ def clear #:nodoc:
77
+ @value = nil
78
+ end
79
+
80
+ def clear? #:nodoc:
81
+ @value.nil?
82
+ end
83
+
84
+ def assign(val)
85
+ raise ArgumentError, "can't set a nil value for #{debug_name}" if val.nil?
86
+
87
+ unless has_parameter?(:value)
88
+ raw_val = val.respond_to?(:snapshot) ? val.snapshot : val
89
+ @value = begin
90
+ raw_val.dup
91
+ rescue TypeError
92
+ # can't dup Fixnums
93
+ raw_val
94
+ end
95
+ end
96
+ end
97
+
98
+ def snapshot
99
+ _value
100
+ end
101
+
102
+ def value
103
+ snapshot
104
+ end
105
+ alias_method :value=, :assign
106
+
107
+ def respond_to?(symbol, include_private = false) #:nodoc:
108
+ child = snapshot
109
+ child.respond_to?(symbol, include_private) || super
110
+ end
111
+
112
+ def method_missing(symbol, *args, &block) #:nodoc:
113
+ child = snapshot
114
+ if child.respond_to?(symbol)
115
+ child.__send__(symbol, *args, &block)
116
+ else
117
+ super
118
+ end
119
+ end
120
+
121
+ def <=>(other)
122
+ snapshot <=> other
123
+ end
124
+
125
+ def eql?(other)
126
+ # double dispatch
127
+ other.eql?(snapshot)
128
+ end
129
+
130
+ def hash
131
+ snapshot.hash
132
+ end
133
+
134
+ def do_read(io) #:nodoc:
135
+ @value = read_and_return_value(io)
136
+ hook_after_do_read
137
+ end
138
+
139
+ def do_read_with_check_value(io) #:nodoc:
140
+ do_read_without_check_value(io)
141
+ check_value(snapshot)
142
+ end
143
+
144
+ def do_write(io) #:nodoc:
145
+ io.writebytes(value_to_binary_string(_value))
146
+ end
147
+
148
+ def do_num_bytes #:nodoc:
149
+ value_to_binary_string(_value).length
150
+ end
151
+
152
+ #---------------
153
+ private
154
+
155
+ def hook_after_do_read; end
156
+
157
+ def check_value(current_value)
158
+ expected = eval_parameter(:check_value, :value => current_value)
159
+ if not expected
160
+ raise ValidityError,
161
+ "value '#{current_value}' not as expected for #{debug_name}"
162
+ elsif current_value != expected and expected != true
163
+ raise ValidityError,
164
+ "value is '#{current_value}' but " +
165
+ "expected '#{expected}' for #{debug_name}"
166
+ end
167
+ end
168
+
169
+ # The unmodified value of this data object. Note that #snapshot calls this
170
+ # method. This indirection is so that #snapshot can be overridden in
171
+ # subclasses to modify the presentation value.
172
+ #
173
+ # Table of possible preconditions and expected outcome
174
+ # 1. :value and !reading? -> :value
175
+ # 2. :value and reading? -> @value
176
+ # 3. :initial_value and clear? -> :initial_value
177
+ # 4. :initial_value and !clear? -> @value
178
+ # 5. clear? -> sensible_default
179
+ # 6. !clear? -> @value
180
+
181
+ def _value
182
+ @value != nil ? @value : sensible_default()
183
+ end
184
+
185
+ def _value_with_value #:nodoc:
186
+ reading? ? @value : eval_parameter(:value)
187
+ end
188
+
189
+ def _value_with_initial_value #:nodoc:
190
+ @value != nil ? @value : eval_parameter(:initial_value)
191
+ end
192
+
193
+ ###########################################################################
194
+ # To be implemented by subclasses
195
+
196
+ # Return the string representation that +val+ will take when written.
197
+ def value_to_binary_string(val)
198
+ raise NotImplementedError
199
+ end
200
+
201
+ # Read a number of bytes from +io+ and return the value they represent.
202
+ def read_and_return_value(io)
203
+ raise NotImplementedError
204
+ end
205
+
206
+ # Return a sensible default for this data.
207
+ def sensible_default
208
+ raise NotImplementedError
209
+ end
210
+
211
+ # To be implemented by subclasses
212
+ ###########################################################################
213
+ end
214
+ end