bindata 0.8.1 → 0.9.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.
- data/ChangeLog +11 -2
- data/README +55 -19
- data/TODO +4 -0
- data/examples/gzip.rb +3 -3
- data/lib/bindata.rb +8 -1
- data/lib/bindata/array.rb +58 -26
- data/lib/bindata/base.rb +133 -83
- data/lib/bindata/choice.rb +142 -61
- data/lib/bindata/float.rb +4 -0
- data/lib/bindata/int.rb +14 -0
- data/lib/bindata/lazy.rb +19 -2
- data/lib/bindata/multi_value.rb +144 -0
- data/lib/bindata/registry.rb +13 -8
- data/lib/bindata/rest.rb +41 -0
- data/lib/bindata/sanitize.rb +36 -0
- data/lib/bindata/single.rb +31 -6
- data/lib/bindata/single_value.rb +203 -0
- data/lib/bindata/string.rb +57 -32
- data/lib/bindata/stringz.rb +15 -0
- data/lib/bindata/struct.rb +189 -204
- data/spec/array_spec.rb +66 -42
- data/spec/base_spec.rb +202 -80
- data/spec/choice_spec.rb +172 -51
- data/spec/int_spec.rb +5 -5
- data/spec/lazy_spec.rb +26 -23
- data/spec/multi_value_spec.rb +250 -0
- data/spec/registry_spec.rb +8 -8
- data/spec/rest_spec.rb +30 -0
- data/spec/sanitize_spec.rb +108 -0
- data/spec/single_spec.rb +63 -49
- data/spec/single_value_spec.rb +131 -0
- data/spec/string_spec.rb +47 -47
- data/spec/stringz_spec.rb +29 -29
- data/spec/struct_spec.rb +112 -198
- metadata +58 -57
data/ChangeLog
CHANGED
@@ -1,13 +1,22 @@
|
|
1
1
|
= BinData Changelog
|
2
2
|
|
3
|
-
|
3
|
+
== Version 0.9.0 (2008-06-02)
|
4
|
+
|
5
|
+
* Added :adjust_offset option to automatically seek to a given offset.
|
6
|
+
* Modified #read to accept strings as well as IO streams.
|
7
|
+
* Choice now accepts sparse arrays and hashes as :choice.
|
8
|
+
* Added BinData::Rest to help with debugging.
|
9
|
+
* Major internal restructuring - memory usage is much better.
|
10
|
+
* Improved documentation
|
11
|
+
|
12
|
+
== Version 0.8.1 (2008-01-14)
|
4
13
|
|
5
14
|
* Reduced memory consumption.
|
6
15
|
* Increased execution speed.
|
7
16
|
* Deprecated BinData::Base.parameters
|
8
17
|
* Fixed spec syntax (thanks to David Goodlad)
|
9
18
|
|
10
|
-
|
19
|
+
== Version 0.8.0 (2007-10-14)
|
11
20
|
|
12
21
|
* Add reserved field names to Struct.
|
13
22
|
* Prevent warnings about method redefinition.
|
data/README
CHANGED
@@ -15,7 +15,7 @@ Do you ever find yourself writing code like this?
|
|
15
15
|
It's ugly, violates DRY and feels like you're writing Perl, not Ruby.
|
16
16
|
There is a better way.
|
17
17
|
|
18
|
-
class Rectangle < BinData::
|
18
|
+
class Rectangle < BinData::MultiValue
|
19
19
|
uint16le :len
|
20
20
|
string :name, :read_length => :len
|
21
21
|
uint32le :width
|
@@ -36,7 +36,7 @@ download[http://rubyforge.org/frs/?group_id=3252] page.
|
|
36
36
|
|
37
37
|
BinData declarations are easy to read. Here's an example.
|
38
38
|
|
39
|
-
class MyFancyFormat < BinData::
|
39
|
+
class MyFancyFormat < BinData::MultiValue
|
40
40
|
stringz :comment
|
41
41
|
uint8 :count, :check_value => lambda { (value % 2) == 0 }
|
42
42
|
array :some_ints, :type => :int32be, :initial_length => :count
|
@@ -62,7 +62,7 @@ the writing code, have a go at the reading code.
|
|
62
62
|
The general format of a BinData declaration is a class containing one or more
|
63
63
|
fields.
|
64
64
|
|
65
|
-
class MyName < BinData::
|
65
|
+
class MyName < BinData::MultiValue
|
66
66
|
type field_name, :param1 => "foo", :param2 => bar, ...
|
67
67
|
...
|
68
68
|
end
|
@@ -100,7 +100,7 @@ the string contains the string's length.
|
|
100
100
|
|
101
101
|
Here's how we'd implement the same example with BinData.
|
102
102
|
|
103
|
-
class PascalString < BinData::
|
103
|
+
class PascalString < BinData::MultiValue
|
104
104
|
uint8 :len, :value => lambda { data.length }
|
105
105
|
string :data, :read_length => :len
|
106
106
|
end
|
@@ -120,7 +120,7 @@ Here's how we'd implement the same example with BinData.
|
|
120
120
|
This syntax needs explaining. Let's simplify by examining reading and
|
121
121
|
writing separately.
|
122
122
|
|
123
|
-
class PascalStringReader < BinData::
|
123
|
+
class PascalStringReader < BinData::MultiValue
|
124
124
|
uint8 :len
|
125
125
|
string :data, :read_length => :len
|
126
126
|
end
|
@@ -132,7 +132,7 @@ This states that when reading the string, the initial length of the string
|
|
132
132
|
Note that <tt>:read_length => :len</tt> is syntactic sugar for
|
133
133
|
<tt>:read_length => lambda { len }</tt>, but more on that later.
|
134
134
|
|
135
|
-
class PascalStringWriter < BinData::
|
135
|
+
class PascalStringWriter < BinData::MultiValue
|
136
136
|
uint8 :len, :value => lambda { data.length }
|
137
137
|
string :data
|
138
138
|
end
|
@@ -152,6 +152,13 @@ length afterwards.
|
|
152
152
|
These are the predefined types. Custom types can be created by composing
|
153
153
|
these types.
|
154
154
|
|
155
|
+
BinData::String:: A sequence of bytes.
|
156
|
+
BinData::Stringz:: A zero terminated sequence of bytes.
|
157
|
+
|
158
|
+
BinData::Array:: A list of objects of the same type.
|
159
|
+
BinData::Choice:: A choice between several objects.
|
160
|
+
BinData::Struct:: An ordered collection of named objects.
|
161
|
+
|
155
162
|
BinData::Int8:: Signed 8 bit integer.
|
156
163
|
BinData::Int16le:: Signed 16 bit integer (little endian).
|
157
164
|
BinData::Int16be:: Signed 16 bit integer (big endian).
|
@@ -173,16 +180,11 @@ BinData::FloatBe:: Single precision floating point number (big endian).
|
|
173
180
|
BinData::DoubleLe:: Double precision floating point number (little endian).
|
174
181
|
BinData::DoubleBe:: Double precision floating point number (big endian).
|
175
182
|
|
176
|
-
BinData::
|
177
|
-
BinData::Stringz:: A zero terminated sequence of bytes.
|
178
|
-
|
179
|
-
BinData::Array:: A list of objects of the same type.
|
180
|
-
BinData::Choice:: A choice between several objects.
|
181
|
-
BinData::Struct:: An ordered collection of named objects.
|
183
|
+
BinData::Rest:: Consumes the rest of the input stream.
|
182
184
|
|
183
185
|
== Parameters
|
184
186
|
|
185
|
-
class PascalStringWriter < BinData::
|
187
|
+
class PascalStringWriter < BinData::MultiValue
|
186
188
|
uint8 :len, :value => lambda { data.length }
|
187
189
|
string :data
|
188
190
|
end
|
@@ -217,7 +219,7 @@ produced is independent of architecture. Explicitly specifying the
|
|
217
219
|
endianess of each numeric type can become tedious, so the following
|
218
220
|
shortcut is provided.
|
219
221
|
|
220
|
-
class A < BinData::
|
222
|
+
class A < BinData::MultiValue
|
221
223
|
endian :little
|
222
224
|
|
223
225
|
uint16 :a
|
@@ -229,7 +231,7 @@ shortcut is provided.
|
|
229
231
|
|
230
232
|
is equivalent to:
|
231
233
|
|
232
|
-
class A < BinData::
|
234
|
+
class A < BinData::MultiValue
|
233
235
|
uint16le :a
|
234
236
|
uint32le :b
|
235
237
|
double_le :c
|
@@ -243,13 +245,47 @@ cascade to nested types, as illustrated with the array in the above example.
|
|
243
245
|
|
244
246
|
== Creating custom types
|
245
247
|
|
246
|
-
Custom types should be created by subclassing BinData::
|
247
|
-
Ocassionally it may be useful to subclass
|
248
|
-
other classes may have unexpected results
|
248
|
+
Custom types should be created by subclassing BinData::MultiValue or
|
249
|
+
BinData::SingleValue. Ocassionally it may be useful to subclass
|
250
|
+
BinData::Single. Subclassing other classes may have unexpected results
|
251
|
+
and is unsupported.
|
252
|
+
|
253
|
+
Let us revisit the Pascal String example.
|
254
|
+
|
255
|
+
class PascalString < BinData::MultiValue
|
256
|
+
uint8 :len, :value => lambda { data.length }
|
257
|
+
string :data, :read_length => :len
|
258
|
+
end
|
259
|
+
|
260
|
+
We'd like to make PascalString a custom type that behaves like a
|
261
|
+
BinData::Single object so we can use :initial_value etc. Here's an
|
262
|
+
example usage of what we'd like:
|
263
|
+
|
264
|
+
class Favourites < BinData::MultiValue
|
265
|
+
pascal_string :language, :initial_value => "ruby"
|
266
|
+
pascal_string :os, :initial_value => "unix"
|
267
|
+
end
|
268
|
+
|
269
|
+
f = Favourites.new
|
270
|
+
f.os = "freebsd"
|
271
|
+
f.to_s #=> "\004ruby\007freebsd"
|
272
|
+
|
273
|
+
We create this type of custom string by inheriting from BinData::SingleValue
|
274
|
+
and implementing the #get and #set methods.
|
275
|
+
|
276
|
+
class PascalString < BinData::SingleValue
|
277
|
+
uint8 :len, :value => lambda { data.length }
|
278
|
+
string :data, :read_length => :len
|
279
|
+
|
280
|
+
def get; self.data; end
|
281
|
+
def set(v) self.data = v; end
|
282
|
+
end
|
249
283
|
|
284
|
+
If the type we are creating represents a single value then inherit from
|
285
|
+
BinData::SingleValue, otherwise inherit from BinData::MultiValue.
|
250
286
|
|
251
287
|
== License
|
252
288
|
|
253
289
|
BinData is released under the same license as Ruby.
|
254
290
|
|
255
|
-
Copyright (c) 2007 Dion Mendel
|
291
|
+
Copyright (c) 2007, 2008 Dion Mendel
|
data/TODO
CHANGED
data/examples/gzip.rb
CHANGED
@@ -9,12 +9,12 @@ class Gzip
|
|
9
9
|
# Known compression methods
|
10
10
|
DEFLATE = 8
|
11
11
|
|
12
|
-
class Extra < BinData::
|
12
|
+
class Extra < BinData::MultiValue
|
13
13
|
uint16le :len, :length => lambda { data.length }
|
14
14
|
string :data, :read_length => :len
|
15
15
|
end
|
16
16
|
|
17
|
-
class Header < BinData::
|
17
|
+
class Header < BinData::MultiValue
|
18
18
|
uint16le :ident, :value => 0x8b1f, :check_value => 0x8b1f
|
19
19
|
uint8 :compression_method, :initial_value => DEFLATE
|
20
20
|
uint8 :flags, :value => :calculate_flags_val,
|
@@ -60,7 +60,7 @@ class Gzip
|
|
60
60
|
end
|
61
61
|
end
|
62
62
|
|
63
|
-
class Footer < BinData::
|
63
|
+
class Footer < BinData::MultiValue
|
64
64
|
uint32le :crc32
|
65
65
|
uint32le :uncompressed_size
|
66
66
|
end
|
data/lib/bindata.rb
CHANGED
@@ -5,10 +5,17 @@ require 'bindata/array'
|
|
5
5
|
require 'bindata/choice'
|
6
6
|
require 'bindata/float'
|
7
7
|
require 'bindata/int'
|
8
|
+
require 'bindata/multi_value'
|
9
|
+
require 'bindata/rest'
|
10
|
+
require 'bindata/single_value'
|
8
11
|
require 'bindata/string'
|
9
12
|
require 'bindata/stringz'
|
10
13
|
require 'bindata/struct'
|
11
14
|
|
15
|
+
# = BinData
|
16
|
+
#
|
17
|
+
# A declarative way to read and write structured binary data.
|
18
|
+
#
|
12
19
|
module BinData
|
13
|
-
VERSION = "0.
|
20
|
+
VERSION = "0.9.0"
|
14
21
|
end
|
data/lib/bindata/array.rb
CHANGED
@@ -1,15 +1,31 @@
|
|
1
1
|
require 'bindata/base'
|
2
|
+
require 'bindata/sanitize'
|
2
3
|
|
3
4
|
module BinData
|
4
5
|
# An Array is a list of data objects of the same type.
|
5
6
|
#
|
6
7
|
# require 'bindata'
|
7
|
-
# require 'stringio'
|
8
8
|
#
|
9
|
-
#
|
10
|
-
#
|
11
|
-
#
|
12
|
-
#
|
9
|
+
# data = "\x03\x04\x05\x06\x07\x08\x09"
|
10
|
+
#
|
11
|
+
# obj = BinData::Array.new(:type => :int8, :initial_length => 6)
|
12
|
+
# obj.read(data)
|
13
|
+
# obj.snapshot #=> [3, 4, 5, 6, 7, 8]
|
14
|
+
#
|
15
|
+
# obj = BinData::Array.new(:type => :int8,
|
16
|
+
# :read_until => lambda { index == 1 })
|
17
|
+
# obj.read(data)
|
18
|
+
# obj.snapshot #=> [3, 4]
|
19
|
+
#
|
20
|
+
# obj = BinData::Array.new(:type => :int8,
|
21
|
+
# :read_until => lambda { element >= 6 })
|
22
|
+
# obj.read(data)
|
23
|
+
# obj.snapshot #=> [3, 4, 5, 6]
|
24
|
+
#
|
25
|
+
# obj = BinData::Array.new(:type => :int8,
|
26
|
+
# :read_until => lambda { array[index] + array[index - 1] == 13 })
|
27
|
+
# obj.read(data)
|
28
|
+
# obj.snapshot #=> [3, 4, 5, 6, 7]
|
13
29
|
#
|
14
30
|
# == Parameters
|
15
31
|
#
|
@@ -30,7 +46,7 @@ module BinData
|
|
30
46
|
#
|
31
47
|
# Each data object in an array has the variable +index+ made available
|
32
48
|
# to any lambda evaluated as a parameter of that data object.
|
33
|
-
class Array < Base
|
49
|
+
class Array < BinData::Base
|
34
50
|
include Enumerable
|
35
51
|
|
36
52
|
# Register this class
|
@@ -39,22 +55,44 @@ module BinData
|
|
39
55
|
# These are the parameters used by this class.
|
40
56
|
mandatory_parameter :type
|
41
57
|
optional_parameters :initial_length, :read_until
|
58
|
+
mutually_exclusive_parameters :initial_length, :read_until
|
59
|
+
|
60
|
+
class << self
|
61
|
+
# Returns a sanitized +params+ that is of the form expected
|
62
|
+
# by #initialize.
|
63
|
+
def sanitize_parameters(params, endian = nil)
|
64
|
+
params = params.dup
|
65
|
+
|
66
|
+
unless params.has_key?(:initial_length) or params.has_key?(:read_until)
|
67
|
+
# ensure one of :initial_length and :read_until exists
|
68
|
+
params[:initial_length] = 0
|
69
|
+
end
|
70
|
+
|
71
|
+
if params.has_key?(:type)
|
72
|
+
type, el_params = params[:type]
|
73
|
+
klass = lookup(type, endian)
|
74
|
+
raise TypeError, "unknown type '#{type}' for #{self}" if klass.nil?
|
75
|
+
params[:type] = [klass, SanitizedParameters.new(klass, el_params, endian)]
|
76
|
+
end
|
42
77
|
|
43
|
-
|
44
|
-
|
78
|
+
super(params, endian)
|
79
|
+
end
|
80
|
+
|
81
|
+
# An array has no fields.
|
82
|
+
def all_possible_field_names(sanitized_params)
|
83
|
+
[]
|
84
|
+
end
|
85
|
+
end
|
45
86
|
|
46
87
|
# Creates a new Array
|
47
88
|
def initialize(params = {}, env = nil)
|
48
|
-
super(
|
49
|
-
ensure_mutual_exclusion(:initial_length, :read_until)
|
89
|
+
super(params, env)
|
50
90
|
|
51
|
-
|
52
|
-
klass = klass_lookup(type)
|
53
|
-
raise TypeError, "unknown type '#{type}' for #{self}" if klass.nil?
|
91
|
+
klass, el_params = param(:type)
|
54
92
|
|
55
93
|
@element_list = nil
|
56
94
|
@element_klass = klass
|
57
|
-
@element_params = el_params
|
95
|
+
@element_params = el_params
|
58
96
|
end
|
59
97
|
|
60
98
|
# Clears the element at position +index+. If +index+ is not given, then
|
@@ -126,6 +164,12 @@ module BinData
|
|
126
164
|
elements.collect { |e| e.snapshot }
|
127
165
|
end
|
128
166
|
|
167
|
+
# Returns whether this data object contains a single value. Single
|
168
|
+
# value data objects respond to <tt>#value</tt> and <tt>#value=</tt>.
|
169
|
+
def single_value?
|
170
|
+
return false
|
171
|
+
end
|
172
|
+
|
129
173
|
# An array has no fields.
|
130
174
|
def field_names
|
131
175
|
[]
|
@@ -239,17 +283,5 @@ module BinData
|
|
239
283
|
@element_list << element
|
240
284
|
element
|
241
285
|
end
|
242
|
-
|
243
|
-
# Returns a hash of cleaned +params+. Cleaning means that param
|
244
|
-
# values are converted to a desired format.
|
245
|
-
def cleaned_params(params)
|
246
|
-
unless params.has_key?(:initial_length) or params.has_key?(:read_until)
|
247
|
-
# ensure one of :initial_length and :read_until exists
|
248
|
-
new_params = params.dup
|
249
|
-
new_params[:initial_length] = 0
|
250
|
-
params = new_params
|
251
|
-
end
|
252
|
-
params
|
253
|
-
end
|
254
286
|
end
|
255
287
|
end
|
data/lib/bindata/base.rb
CHANGED
@@ -1,5 +1,7 @@
|
|
1
1
|
require 'bindata/lazy'
|
2
|
+
require 'bindata/sanitize'
|
2
3
|
require 'bindata/registry'
|
4
|
+
require 'stringio'
|
3
5
|
|
4
6
|
module BinData
|
5
7
|
# Error raised when unexpected results occur when reading data from IO.
|
@@ -21,6 +23,10 @@ module BinData
|
|
21
23
|
# is made available to any lambda assigned to
|
22
24
|
# this parameter. This parameter is only checked
|
23
25
|
# before reading.
|
26
|
+
# [<tt>:adjust_offset</tt>] Ensures that the current IO offset is at this
|
27
|
+
# position before reading. This is like
|
28
|
+
# <tt>:check_offset</tt>, except that it will
|
29
|
+
# adjust the IO offset instead of raising an error.
|
24
30
|
class Base
|
25
31
|
class << self
|
26
32
|
# Returns the mandatory parameters used by this class. Any given args
|
@@ -63,11 +69,75 @@ module BinData
|
|
63
69
|
end
|
64
70
|
alias_method :optional_parameter, :optional_parameters
|
65
71
|
|
66
|
-
# Returns
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
72
|
+
# Returns the default parameters used by this class. Any given args
|
73
|
+
# are appended to the parameters list. The parameters for a class will
|
74
|
+
# include the parameters of its ancestors.
|
75
|
+
def default_parameters(params = {})
|
76
|
+
unless defined? @default_parameters
|
77
|
+
@default_parameters = {}
|
78
|
+
ancestors[1..-1].each do |parent|
|
79
|
+
if parent.respond_to?(:default_parameters)
|
80
|
+
@default_parameters = @default_parameters.merge(parent.default_parameters)
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
84
|
+
if not params.empty?
|
85
|
+
@default_parameters = @default_parameters.merge(params)
|
86
|
+
end
|
87
|
+
@default_parameters
|
88
|
+
end
|
89
|
+
alias_method :default_parameter, :default_parameters
|
90
|
+
|
91
|
+
# Returns the pairs of mutually exclusive parameters used by this class.
|
92
|
+
# Any given args are appended to the parameters list. The parameters for
|
93
|
+
# a class will include the parameters of its ancestors.
|
94
|
+
def mutually_exclusive_parameters(*args)
|
95
|
+
unless defined? @mutually_exclusive_parameters
|
96
|
+
@mutually_exclusive_parameters = []
|
97
|
+
ancestors[1..-1].each do |parent|
|
98
|
+
if parent.respond_to?(:mutually_exclusive_parameters)
|
99
|
+
@mutually_exclusive_parameters.concat(parent.mutually_exclusive_parameters)
|
100
|
+
end
|
101
|
+
end
|
102
|
+
end
|
103
|
+
if not args.empty?
|
104
|
+
@mutually_exclusive_parameters << [args[0].to_sym, args[1].to_sym]
|
105
|
+
end
|
106
|
+
@mutually_exclusive_parameters
|
107
|
+
end
|
108
|
+
|
109
|
+
# Returns a list of parameters that are accepted by this object
|
110
|
+
def accepted_parameters
|
111
|
+
(mandatory_parameters + optional_parameters + default_parameters.keys).uniq
|
112
|
+
end
|
113
|
+
|
114
|
+
# Returns a sanitized +params+ that is of the form expected
|
115
|
+
# by #initialize.
|
116
|
+
def sanitize_parameters(params, *args)
|
117
|
+
params = params.dup
|
118
|
+
|
119
|
+
# add default parameters
|
120
|
+
default_parameters.each do |k,v|
|
121
|
+
params[k] = v unless params.has_key?(k)
|
122
|
+
end
|
123
|
+
|
124
|
+
# ensure mandatory parameters exist
|
125
|
+
mandatory_parameters.each do |prm|
|
126
|
+
if not params.has_key?(prm)
|
127
|
+
raise ArgumentError, "parameter ':#{prm}' must be specified " +
|
128
|
+
"in #{self}"
|
129
|
+
end
|
130
|
+
end
|
131
|
+
|
132
|
+
# ensure mutual exclusion
|
133
|
+
mutually_exclusive_parameters.each do |param1, param2|
|
134
|
+
if params.has_key?(param1) and params.has_key?(param2)
|
135
|
+
raise ArgumentError, "params #{param1} and #{param2} " +
|
136
|
+
"are mutually exclusive"
|
137
|
+
end
|
138
|
+
end
|
139
|
+
|
140
|
+
params
|
71
141
|
end
|
72
142
|
|
73
143
|
# Instantiates this class and reads from +io+. For single value objects
|
@@ -86,19 +156,17 @@ module BinData
|
|
86
156
|
private :register
|
87
157
|
|
88
158
|
# Returns the class matching a previously registered +name+.
|
89
|
-
def lookup(name)
|
159
|
+
def lookup(name, endian = nil)
|
160
|
+
name = name.to_s
|
90
161
|
klass = Registry.instance.lookup(name)
|
91
|
-
if klass.nil?
|
162
|
+
if klass.nil? and endian != nil
|
92
163
|
# lookup failed so attempt endian lookup
|
93
|
-
if
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
new_name = name + ((self.endian == :little) ? "_le" : "_be")
|
100
|
-
klass = Registry.instance.lookup(new_name)
|
101
|
-
end
|
164
|
+
if /^u?int\d{1,3}$/ =~ name
|
165
|
+
new_name = name + ((endian == :little) ? "le" : "be")
|
166
|
+
klass = Registry.instance.lookup(new_name)
|
167
|
+
elsif ["float", "double"].include?(name)
|
168
|
+
new_name = name + ((endian == :little) ? "_le" : "_be")
|
169
|
+
klass = Registry.instance.lookup(new_name)
|
102
170
|
end
|
103
171
|
end
|
104
172
|
klass
|
@@ -106,7 +174,9 @@ module BinData
|
|
106
174
|
end
|
107
175
|
|
108
176
|
# Define the parameters we use in this class.
|
109
|
-
optional_parameters :check_offset, :
|
177
|
+
optional_parameters :check_offset, :adjust_offset
|
178
|
+
default_parameters :readwrite => true
|
179
|
+
mutually_exclusive_parameters :check_offset, :adjust_offset
|
110
180
|
|
111
181
|
# Creates a new data object.
|
112
182
|
#
|
@@ -114,68 +184,26 @@ module BinData
|
|
114
184
|
# reference callable objects (methods or procs). +env+ is the
|
115
185
|
# environment that these callable objects are evaluated in.
|
116
186
|
def initialize(params = {}, env = nil)
|
117
|
-
|
118
|
-
|
119
|
-
optional = self.class.optional_parameters
|
120
|
-
|
121
|
-
# default :readwrite param to true if unspecified
|
122
|
-
if not params.has_key?(:readwrite)
|
123
|
-
params = params.dup
|
124
|
-
params[:readwrite] = true
|
187
|
+
unless SanitizedParameters === params
|
188
|
+
params = SanitizedParameters.new(self.class, params)
|
125
189
|
end
|
126
190
|
|
127
|
-
|
128
|
-
mandatory.each do |prm|
|
129
|
-
if not params.has_key?(prm)
|
130
|
-
raise ArgumentError, "parameter ':#{prm}' must be specified " +
|
131
|
-
"in #{self}"
|
132
|
-
end
|
133
|
-
end
|
134
|
-
|
135
|
-
# partition parameters into known and extra parameters
|
136
|
-
@params = {}
|
137
|
-
extra = {}
|
138
|
-
params.each do |k,v|
|
139
|
-
k = k.to_sym
|
140
|
-
raise ArgumentError, "parameter :#{k} is nil in #{self}" if v.nil?
|
141
|
-
if mandatory.include?(k) or optional.include?(k)
|
142
|
-
@params[k] = v.freeze
|
143
|
-
else
|
144
|
-
extra[k] = v.freeze
|
145
|
-
end
|
146
|
-
end
|
191
|
+
@params = params.accepted_parameters
|
147
192
|
|
148
193
|
# set up the environment
|
149
194
|
@env = env || LazyEvalEnv.new
|
150
|
-
@env.params =
|
195
|
+
@env.params = params.extra_parameters
|
151
196
|
@env.data_object = self
|
152
197
|
end
|
153
198
|
|
154
|
-
#
|
155
|
-
def klass_lookup(name)
|
156
|
-
@cache ||= {}
|
157
|
-
klass = @cache[name]
|
158
|
-
if klass.nil?
|
159
|
-
klass = self.class.lookup(name)
|
160
|
-
if klass.nil? and @env.parent_data_object != nil
|
161
|
-
# lookup failed so retry in the context of the parent data object
|
162
|
-
klass = @env.parent_data_object.klass_lookup(name)
|
163
|
-
end
|
164
|
-
@cache[name] = klass
|
165
|
-
end
|
166
|
-
klass
|
167
|
-
end
|
168
|
-
|
169
|
-
# Returns a list of parameters that are accepted by this object
|
170
|
-
def accepted_parameters
|
171
|
-
(self.class.mandatory_parameters + self.class.optional_parameters).uniq
|
172
|
-
end
|
173
|
-
|
174
|
-
# Reads data into this bin object by calling #do_read then #done_read.
|
199
|
+
# Reads data into this data object by calling #do_read then #done_read.
|
175
200
|
def read(io)
|
201
|
+
# wrap strings in a StringIO
|
202
|
+
io = StringIO.new(io) if io.respond_to?(:to_str)
|
203
|
+
|
176
204
|
# remove previous method to prevent warnings
|
177
205
|
class << io
|
178
|
-
|
206
|
+
remove_method(:bindata_mark) if method_defined?(:bindata_mark)
|
179
207
|
end
|
180
208
|
|
181
209
|
# remember the current position in the IO object
|
@@ -198,17 +226,19 @@ module BinData
|
|
198
226
|
_write(io) if eval_param(:readwrite) != false
|
199
227
|
end
|
200
228
|
|
229
|
+
# Returns the string representation of this data object.
|
230
|
+
def to_s
|
231
|
+
io = StringIO.new
|
232
|
+
write(io)
|
233
|
+
io.rewind
|
234
|
+
io.read
|
235
|
+
end
|
236
|
+
|
201
237
|
# Returns the number of bytes it will take to write this data.
|
202
238
|
def num_bytes(what = nil)
|
203
239
|
(eval_param(:readwrite) != false) ? _num_bytes(what) : 0
|
204
240
|
end
|
205
241
|
|
206
|
-
# Returns whether this data object contains a single value. Single
|
207
|
-
# value data objects respond to <tt>#value</tt> and <tt>#value=</tt>.
|
208
|
-
def single_value?
|
209
|
-
respond_to? :value
|
210
|
-
end
|
211
|
-
|
212
242
|
# Return a human readable representation of this object.
|
213
243
|
def inspect
|
214
244
|
snapshot.inspect
|
@@ -243,14 +273,6 @@ module BinData
|
|
243
273
|
@params.has_key?(key.to_sym)
|
244
274
|
end
|
245
275
|
|
246
|
-
# Raise an error if +param1+ and +param2+ are both given as params.
|
247
|
-
def ensure_mutual_exclusion(param1, param2)
|
248
|
-
if has_param?(param1) and has_param?(param2)
|
249
|
-
raise ArgumentError, "params #{param1} and #{param2} " +
|
250
|
-
"are mutually exclusive"
|
251
|
-
end
|
252
|
-
end
|
253
|
-
|
254
276
|
# Checks that the current offset of +io+ is as expected. This should
|
255
277
|
# be called from #do_read before performing the reading.
|
256
278
|
def check_offset(io)
|
@@ -264,11 +286,32 @@ module BinData
|
|
264
286
|
raise ValidityError, "offset is '#{actual_offset}' but " +
|
265
287
|
"expected '#{expected}'"
|
266
288
|
end
|
289
|
+
elsif has_param?(:adjust_offset)
|
290
|
+
actual_offset = io.pos - io.bindata_mark
|
291
|
+
expected = eval_param(:adjust_offset)
|
292
|
+
if actual_offset != expected
|
293
|
+
begin
|
294
|
+
seek = expected - actual_offset
|
295
|
+
io.seek(seek, IO::SEEK_CUR)
|
296
|
+
warn "adjusting stream position by #{seek} bytes" if $VERBOSE
|
297
|
+
rescue
|
298
|
+
# could not seek so raise an error
|
299
|
+
raise ValidityError, "offset is '#{actual_offset}' but " +
|
300
|
+
"couldn't seek to expected '#{expected}'"
|
301
|
+
end
|
302
|
+
end
|
267
303
|
end
|
268
304
|
end
|
269
305
|
|
306
|
+
###########################################################################
|
270
307
|
# To be implemented by subclasses
|
271
308
|
|
309
|
+
# Returns a list of the names of all possible field names for an object
|
310
|
+
# created with +sanitized_params+.
|
311
|
+
def self.all_possible_field_names(sanitized_params)
|
312
|
+
raise NotImplementedError
|
313
|
+
end
|
314
|
+
|
272
315
|
# Resets the internal state to that of a newly created object.
|
273
316
|
def clear
|
274
317
|
raise NotImplementedError
|
@@ -299,6 +342,12 @@ module BinData
|
|
299
342
|
raise NotImplementedError
|
300
343
|
end
|
301
344
|
|
345
|
+
# Returns whether this data object contains a single value. Single
|
346
|
+
# value data objects respond to <tt>#value</tt> and <tt>#value=</tt>.
|
347
|
+
def single_value?
|
348
|
+
raise NotImplementedError
|
349
|
+
end
|
350
|
+
|
302
351
|
# Returns a list of the names of all fields accessible through this
|
303
352
|
# object.
|
304
353
|
def field_names
|
@@ -306,9 +355,10 @@ module BinData
|
|
306
355
|
end
|
307
356
|
|
308
357
|
# Set visibility requirements of methods to implement
|
309
|
-
public :clear, :done_read, :snapshot, :field_names
|
358
|
+
public :clear, :done_read, :snapshot, :single_value?, :field_names
|
310
359
|
private :_do_read, :_write, :_num_bytes
|
311
360
|
|
312
361
|
# End To be implemented by subclasses
|
362
|
+
###########################################################################
|
313
363
|
end
|
314
364
|
end
|