bindata 0.5.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/COPYING +52 -0
- data/ChangeLog +9 -0
- data/GPL +339 -0
- data/INSTALL +7 -0
- data/README +215 -0
- data/TODO +14 -0
- data/examples/gzip.rb +174 -0
- data/lib/bindata.rb +13 -0
- data/lib/bindata/array.rb +160 -0
- data/lib/bindata/base.rb +260 -0
- data/lib/bindata/choice.rb +120 -0
- data/lib/bindata/int.rb +171 -0
- data/lib/bindata/lazy.rb +71 -0
- data/lib/bindata/registry.rb +37 -0
- data/lib/bindata/single.rb +170 -0
- data/lib/bindata/string.rb +98 -0
- data/lib/bindata/stringz.rb +83 -0
- data/lib/bindata/struct.rb +292 -0
- data/spec/array_spec.rb +121 -0
- data/spec/base_spec.rb +194 -0
- data/spec/choice_spec.rb +105 -0
- data/spec/int_spec.rb +141 -0
- data/spec/lazy_spec.rb +120 -0
- data/spec/registry_spec.rb +47 -0
- data/spec/single_spec.rb +210 -0
- data/spec/spec_common.rb +10 -0
- data/spec/string_spec.rb +205 -0
- data/spec/stringz_spec.rb +159 -0
- data/spec/struct_spec.rb +190 -0
- metadata +78 -0
@@ -0,0 +1,98 @@
|
|
1
|
+
require "bindata/single"
|
2
|
+
|
3
|
+
module BinData
|
4
|
+
# A String is a sequence of bytes. This is the same as strings in Ruby.
|
5
|
+
# The issue of character encoding is ignored by this class.
|
6
|
+
#
|
7
|
+
# == Parameters
|
8
|
+
#
|
9
|
+
# String objects accept all the params that BinData::Single
|
10
|
+
# does, as well as the following:
|
11
|
+
#
|
12
|
+
# <tt>:initial_length</tt>:: The initial length to use before a value is
|
13
|
+
# either read or set.
|
14
|
+
# <tt>:length</tt>:: The fixed length of the string. If a shorter
|
15
|
+
# string is set, it will be padded to this length.
|
16
|
+
# <tt>:pad_char</tt>:: The character to use when padding a string to a
|
17
|
+
# set length. Valid values are Integers and
|
18
|
+
# Strings of length 1. "\0" is the default.
|
19
|
+
# <tt>:trim_value</tt>:: Boolean, default false. If set, #value will
|
20
|
+
# return the value with all pad_chars trimmed
|
21
|
+
# from the end of the string. The value will
|
22
|
+
# not be trimmed when writing.
|
23
|
+
class String < Single
|
24
|
+
# These are the parameters used by this class.
|
25
|
+
mandatory_parameters :pad_char
|
26
|
+
optional_parameters :initial_length, :length, :trim_value
|
27
|
+
|
28
|
+
def initialize(params = {}, env = nil)
|
29
|
+
super(cleaned_params(params), env)
|
30
|
+
|
31
|
+
# the only valid param combinations of length and value are:
|
32
|
+
# :initial_length and :value
|
33
|
+
# :length and :initial_value
|
34
|
+
ensure_mutual_exclusion(:initial_value, :value)
|
35
|
+
ensure_mutual_exclusion(:initial_length, :length)
|
36
|
+
ensure_mutual_exclusion(:initial_length, :initial_value)
|
37
|
+
ensure_mutual_exclusion(:length, :value)
|
38
|
+
end
|
39
|
+
|
40
|
+
# Overrides value to return the value padded to the desired length or
|
41
|
+
# trimmed as required.
|
42
|
+
def value
|
43
|
+
v = val_to_str(_value)
|
44
|
+
v.sub!(/#{eval_param(:pad_char)}*$/, "") if param(:trim_value) == true
|
45
|
+
v
|
46
|
+
end
|
47
|
+
|
48
|
+
#---------------
|
49
|
+
private
|
50
|
+
|
51
|
+
# Returns +val+ ensuring that it is padded to the desired length.
|
52
|
+
def val_to_str(val)
|
53
|
+
# trim val if necessary
|
54
|
+
len = val_num_bytes(val)
|
55
|
+
str = val.slice(0, len)
|
56
|
+
|
57
|
+
# then pad to length if str is short
|
58
|
+
str << (eval_param(:pad_char) * (len - str.length))
|
59
|
+
end
|
60
|
+
|
61
|
+
# Read a number of bytes from +io+ and return the value they represent.
|
62
|
+
def read_val(io)
|
63
|
+
readbytes(io, val_num_bytes(""))
|
64
|
+
end
|
65
|
+
|
66
|
+
# Returns an empty string as default.
|
67
|
+
def sensible_default
|
68
|
+
""
|
69
|
+
end
|
70
|
+
|
71
|
+
# Return the number of bytes that +val+ will occupy when written.
|
72
|
+
def val_num_bytes(val)
|
73
|
+
if clear? and (evaluated = eval_param(:initial_length))
|
74
|
+
evaluated
|
75
|
+
elsif (evaluated = eval_param(:length))
|
76
|
+
evaluated
|
77
|
+
else
|
78
|
+
val.length
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
# Returns a hash of cleaned +params+. Cleaning means that param
|
83
|
+
# values are converted to a desired format.
|
84
|
+
def cleaned_params(params)
|
85
|
+
new_params = params.dup
|
86
|
+
|
87
|
+
# set :pad_char to be a single length character string
|
88
|
+
ch = new_params[:pad_char] || 0
|
89
|
+
ch = ch.respond_to?(:chr) ? ch.chr : ch.to_s
|
90
|
+
if ch.length > 1
|
91
|
+
raise ArgumentError, ":pad_char must not contain more than 1 char"
|
92
|
+
end
|
93
|
+
new_params[:pad_char] = ch
|
94
|
+
|
95
|
+
new_params
|
96
|
+
end
|
97
|
+
end
|
98
|
+
end
|
@@ -0,0 +1,83 @@
|
|
1
|
+
require "bindata/single"
|
2
|
+
|
3
|
+
module BinData
|
4
|
+
# A BinData::Stringz object is a container for a zero ("\0") terminated
|
5
|
+
# string.
|
6
|
+
#
|
7
|
+
# For convenience, the zero terminator is not necessary when setting the
|
8
|
+
# value. Likewise, the returned value will not be zero terminated.
|
9
|
+
#
|
10
|
+
# == Parameters
|
11
|
+
#
|
12
|
+
# Stringz objects accept all the params that BinData::Single
|
13
|
+
# does, as well as the following:
|
14
|
+
#
|
15
|
+
# <tt>:max_length</tt>:: The maximum length of the string including the zero
|
16
|
+
# byte.
|
17
|
+
class Stringz < Single
|
18
|
+
# These are the parameters used by this class.
|
19
|
+
optional_parameters :max_length
|
20
|
+
|
21
|
+
# Overrides value to return the value of this data excluding the trailing
|
22
|
+
# zero byte.
|
23
|
+
def value
|
24
|
+
v = super
|
25
|
+
val_to_str(v).chomp("\0")
|
26
|
+
end
|
27
|
+
|
28
|
+
#---------------
|
29
|
+
private
|
30
|
+
|
31
|
+
# Returns +val+ ensuring it is zero terminated and no longer
|
32
|
+
# than <tt>:max_length</tt> bytes.
|
33
|
+
def val_to_str(val)
|
34
|
+
zero_terminate(val, eval_param(:max_length))
|
35
|
+
end
|
36
|
+
|
37
|
+
# Read a number of bytes from +io+ and return the value they represent.
|
38
|
+
def read_val(io)
|
39
|
+
max_length = eval_param(:max_length)
|
40
|
+
str = ""
|
41
|
+
i = 0
|
42
|
+
ch = nil
|
43
|
+
|
44
|
+
# read until zero byte or we have read in the max number of bytes
|
45
|
+
while ch != "\0" and i != max_length
|
46
|
+
ch = readbytes(io, 1)
|
47
|
+
str << ch
|
48
|
+
i += 1
|
49
|
+
end
|
50
|
+
|
51
|
+
zero_terminate(str, max_length)
|
52
|
+
end
|
53
|
+
|
54
|
+
# Returns an empty string as default.
|
55
|
+
def sensible_default
|
56
|
+
""
|
57
|
+
end
|
58
|
+
|
59
|
+
# Returns +str+ after it has been zero terminated. The returned string
|
60
|
+
# will not be longer than +max_length+.
|
61
|
+
def zero_terminate(str, max_length = nil)
|
62
|
+
# str must not be empty
|
63
|
+
str = "\0" if str == ""
|
64
|
+
|
65
|
+
# remove anything after the first \0
|
66
|
+
str = str.sub(/([^\0]*\0).*/, '\1')
|
67
|
+
|
68
|
+
# trim string to be no longer than max_length including zero byte
|
69
|
+
if max_length
|
70
|
+
max_length = 1 if max_length < 1
|
71
|
+
str = str[0, max_length]
|
72
|
+
if str.length == max_length and str[-1, 1] != "\0"
|
73
|
+
str[-1, 1] = "\0"
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
# ensure last byte in the string is a zero byte
|
78
|
+
str << "\0" if str[-1, 1] != "\0"
|
79
|
+
|
80
|
+
str
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
@@ -0,0 +1,292 @@
|
|
1
|
+
require 'bindata/base'
|
2
|
+
|
3
|
+
module BinData
|
4
|
+
# A Struct is an ordered collection of named data objects.
|
5
|
+
#
|
6
|
+
# require 'bindata'
|
7
|
+
#
|
8
|
+
# class Tuple < BinData::Struct
|
9
|
+
# int8 :x
|
10
|
+
# int8 :y
|
11
|
+
# int8 :z
|
12
|
+
# end
|
13
|
+
#
|
14
|
+
# class SomeStruct < BinData::Struct
|
15
|
+
# hide 'a'
|
16
|
+
#
|
17
|
+
# int32le :a
|
18
|
+
# int16le :b
|
19
|
+
# tuple nil
|
20
|
+
# end
|
21
|
+
#
|
22
|
+
# obj = SomeStruct.new
|
23
|
+
# obj.field_names =># ["b", "x", "y", "z"]
|
24
|
+
#
|
25
|
+
# == Parameters
|
26
|
+
#
|
27
|
+
# Parameters may be provided at initialisation to control the behaviour of
|
28
|
+
# an object. These params are:
|
29
|
+
#
|
30
|
+
# <tt>:fields</tt>:: An array specifying the fields for this struct. Each
|
31
|
+
# element of the array is of the form
|
32
|
+
# [type, name, params]. Type is a symbol representing
|
33
|
+
# a registered type. Name is the name of this field.
|
34
|
+
# Name may be nil as in the example above. Params is an
|
35
|
+
# optional hash of parameters to pass to this field when
|
36
|
+
# instantiating it.
|
37
|
+
# <tt>:hide</tt>:: A list of the names of fields that are to be hidden
|
38
|
+
# from the outside world. Hidden fields don't appear
|
39
|
+
# in #snapshot or #field_names but are still accessible
|
40
|
+
# by name.
|
41
|
+
class Struct < Base
|
42
|
+
# A hash that can be accessed via attributes.
|
43
|
+
class Snapshot < Hash #:nodoc:
|
44
|
+
def method_missing(symbol, *args)
|
45
|
+
self[symbol.id2name] || super
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
# Register this class
|
50
|
+
register(self.name, self)
|
51
|
+
|
52
|
+
class << self
|
53
|
+
# Register the names of all subclasses of this class.
|
54
|
+
def inherited(subclass) #:nodoc:
|
55
|
+
register(subclass.name, subclass)
|
56
|
+
end
|
57
|
+
|
58
|
+
# Returns the names of any hidden fields in this struct. Any given args
|
59
|
+
# are appended to the hidden list.
|
60
|
+
def hide(*args)
|
61
|
+
# note that fields are stored in an instance variable not a class var
|
62
|
+
@hide ||= []
|
63
|
+
args.each do |name|
|
64
|
+
next if name.nil?
|
65
|
+
@hide << name.to_s
|
66
|
+
end
|
67
|
+
@hide
|
68
|
+
end
|
69
|
+
|
70
|
+
# Used to define fields for this structure.
|
71
|
+
def method_missing(symbol, *args)
|
72
|
+
name, params = args
|
73
|
+
|
74
|
+
type = symbol
|
75
|
+
name = name.to_s unless name.nil?
|
76
|
+
params ||= {}
|
77
|
+
|
78
|
+
if lookup(type).nil?
|
79
|
+
raise TypeError, "unknown type '#{type}' for #{self}", caller
|
80
|
+
end
|
81
|
+
|
82
|
+
# note that fields are stored in an instance variable not a class var
|
83
|
+
|
84
|
+
# check for duplicate names
|
85
|
+
@fields ||= []
|
86
|
+
if @fields.detect { |t, n, p| n == name and n != nil }
|
87
|
+
raise SyntaxError, "duplicate field '#{name}' in #{self}", caller
|
88
|
+
end
|
89
|
+
|
90
|
+
# remember this field. These fields will be recalled upon creating
|
91
|
+
# an instance of this class
|
92
|
+
@fields.push([type, name, params])
|
93
|
+
end
|
94
|
+
|
95
|
+
# Returns all stored fields. Should only be called by #cleaned_params.
|
96
|
+
def fields
|
97
|
+
@fields || []
|
98
|
+
end
|
99
|
+
end
|
100
|
+
|
101
|
+
# These are the parameters used by this class.
|
102
|
+
mandatory_parameter :fields
|
103
|
+
optional_parameter :hide
|
104
|
+
|
105
|
+
# Creates a new Struct.
|
106
|
+
def initialize(params = {}, env = nil)
|
107
|
+
super(cleaned_params(params), env)
|
108
|
+
|
109
|
+
# create instances of the fields
|
110
|
+
@fields = param(:fields).collect do |type, name, params|
|
111
|
+
klass = self.class.lookup(type)
|
112
|
+
raise TypeError, "unknown type '#{type}' for #{self}" if klass.nil?
|
113
|
+
[name, klass.new(params, create_env)]
|
114
|
+
end
|
115
|
+
end
|
116
|
+
|
117
|
+
# Clears the field represented by +name+. If no +name+
|
118
|
+
# is given, clears all fields in the struct.
|
119
|
+
def clear(name = nil)
|
120
|
+
if name.nil?
|
121
|
+
bindata_objects.each { |f| f.clear }
|
122
|
+
else
|
123
|
+
find_obj_for_name(name.to_s).clear
|
124
|
+
end
|
125
|
+
end
|
126
|
+
|
127
|
+
# Returns if the field represented by +name+ is clear?. If no +name+
|
128
|
+
# is given, returns whether all fields are clear.
|
129
|
+
def clear?(name = nil)
|
130
|
+
if name.nil?
|
131
|
+
bindata_objects.each { |f| return false if not f.clear? }
|
132
|
+
true
|
133
|
+
else
|
134
|
+
find_obj_for_name(name.to_s).clear?
|
135
|
+
end
|
136
|
+
end
|
137
|
+
|
138
|
+
# Reads the values for all fields in this object from +io+.
|
139
|
+
def _do_read(io)
|
140
|
+
bindata_objects.each { |f| f.do_read(io) }
|
141
|
+
end
|
142
|
+
|
143
|
+
# To be called after calling #read.
|
144
|
+
def done_read
|
145
|
+
bindata_objects.each { |f| f.done_read }
|
146
|
+
end
|
147
|
+
|
148
|
+
# Writes the values for all fields in this object to +io+.
|
149
|
+
def _write(io)
|
150
|
+
bindata_objects.each { |f| f.write(io) }
|
151
|
+
end
|
152
|
+
|
153
|
+
# Returns the number of bytes it will take to write the field represented
|
154
|
+
# by +name+. If +name+ is nil then returns the number of bytes required
|
155
|
+
# to write all fields.
|
156
|
+
def _num_bytes(name)
|
157
|
+
if name.nil?
|
158
|
+
bindata_objects.inject(0) { |sum, f| sum + f.num_bytes }
|
159
|
+
else
|
160
|
+
find_obj_for_name(name.to_s).num_bytes
|
161
|
+
end
|
162
|
+
end
|
163
|
+
|
164
|
+
# Returns a snapshot of this struct as a hash.
|
165
|
+
def snapshot
|
166
|
+
# allow structs to fake single value
|
167
|
+
return value if single_value?
|
168
|
+
|
169
|
+
hash = Snapshot.new
|
170
|
+
field_names.each do |name|
|
171
|
+
hash[name] = find_obj_for_name(name).snapshot
|
172
|
+
end
|
173
|
+
hash
|
174
|
+
end
|
175
|
+
|
176
|
+
# Returns a list of the names of all fields accessible through this
|
177
|
+
# object. +include_hidden+ specifies whether to include hidden names
|
178
|
+
# in the listing.
|
179
|
+
def field_names(include_hidden = false)
|
180
|
+
# single values don't have any fields
|
181
|
+
return [] if single_value?
|
182
|
+
|
183
|
+
names = []
|
184
|
+
@fields.each do |name, obj|
|
185
|
+
if name != ""
|
186
|
+
names << name unless (param(:hide).include?(name) and !include_hidden)
|
187
|
+
else
|
188
|
+
names.concat(obj.field_names)
|
189
|
+
end
|
190
|
+
end
|
191
|
+
names
|
192
|
+
end
|
193
|
+
|
194
|
+
# Returns the data object that stores values for +name+.
|
195
|
+
def find_obj_for_name(name)
|
196
|
+
@fields.each do |n, o|
|
197
|
+
if n == name
|
198
|
+
return o
|
199
|
+
elsif n == "" and o.field_names.include?(name)
|
200
|
+
return o.find_obj_for_name(name)
|
201
|
+
end
|
202
|
+
end
|
203
|
+
nil
|
204
|
+
end
|
205
|
+
|
206
|
+
def offset_of(field)
|
207
|
+
field_name = field.to_s
|
208
|
+
offset = 0
|
209
|
+
@fields.each do |name, obj|
|
210
|
+
if name != ""
|
211
|
+
break if name == field_name
|
212
|
+
offset += obj.num_bytes
|
213
|
+
elsif obj.field_names.include?(field_name)
|
214
|
+
offset += obj.offset_of(field)
|
215
|
+
break
|
216
|
+
end
|
217
|
+
end
|
218
|
+
offset
|
219
|
+
end
|
220
|
+
|
221
|
+
# Override to include field names.
|
222
|
+
alias_method :orig_respond_to?, :respond_to?
|
223
|
+
def respond_to?(symbol, include_private = false)
|
224
|
+
orig_respond_to?(symbol, include_private) ||
|
225
|
+
field_names(true).include?(symbol.id2name.chomp("="))
|
226
|
+
end
|
227
|
+
|
228
|
+
# Returns whether this data object contains a single value. Single
|
229
|
+
# value data objects respond to <tt>#value</tt> and <tt>#value=</tt>.
|
230
|
+
def single_value?
|
231
|
+
# need to use original respond_to? to prevent infinite recursion
|
232
|
+
orig_respond_to?(:value)
|
233
|
+
end
|
234
|
+
|
235
|
+
def method_missing(symbol, *args)
|
236
|
+
name = symbol.id2name
|
237
|
+
|
238
|
+
is_writer = (name[-1, 1] == "=")
|
239
|
+
name.chomp!("=")
|
240
|
+
|
241
|
+
# find the object that is responsible for name
|
242
|
+
if (obj = find_obj_for_name(name))
|
243
|
+
# pass on the request
|
244
|
+
if obj.single_value? and is_writer
|
245
|
+
obj.value = *args
|
246
|
+
elsif obj.single_value?
|
247
|
+
obj.value
|
248
|
+
else
|
249
|
+
obj
|
250
|
+
end
|
251
|
+
else
|
252
|
+
super
|
253
|
+
end
|
254
|
+
end
|
255
|
+
|
256
|
+
#---------------
|
257
|
+
private
|
258
|
+
|
259
|
+
# Returns a list of all the bindata objects for this struct.
|
260
|
+
def bindata_objects
|
261
|
+
@fields.collect { |f| f[1] }
|
262
|
+
end
|
263
|
+
|
264
|
+
# Returns a hash of cleaned +params+. Cleaning means that param
|
265
|
+
# values are converted to a desired format.
|
266
|
+
def cleaned_params(params)
|
267
|
+
new_params = params.dup
|
268
|
+
|
269
|
+
# use fields defined in this class if no fields are passed as params
|
270
|
+
fields = new_params[:fields] || self.class.fields
|
271
|
+
|
272
|
+
# ensure the names of fields are strings and that params is a hash
|
273
|
+
new_params[:fields] = fields.collect do |t, n, p|
|
274
|
+
[t, n.to_s, (p || {}).dup]
|
275
|
+
end
|
276
|
+
|
277
|
+
# collect all non blank field names
|
278
|
+
field_names = new_params[:fields].collect { |f| f[1] }
|
279
|
+
field_names = field_names.delete_if { |n| n == "" }
|
280
|
+
|
281
|
+
# collect all hidden names that correspond to a field name
|
282
|
+
hide = []
|
283
|
+
(new_params[:hide] || self.class.hide).each do |h|
|
284
|
+
h = h.to_s
|
285
|
+
hide << h if field_names.include?(h)
|
286
|
+
end
|
287
|
+
new_params[:hide] = hide
|
288
|
+
|
289
|
+
new_params
|
290
|
+
end
|
291
|
+
end
|
292
|
+
end
|