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.
- data/.gitignore +1 -0
- data/BSDL +22 -0
- data/COPYING +52 -0
- data/ChangeLog.rdoc +204 -0
- data/Gemfile +2 -0
- data/INSTALL +11 -0
- data/NEWS.rdoc +164 -0
- data/README.md +54 -0
- data/Rakefile +13 -0
- data/bindata.gemspec +31 -0
- data/doc/manual.haml +407 -0
- data/doc/manual.md +1649 -0
- data/examples/NBT.txt +149 -0
- data/examples/gzip.rb +161 -0
- data/examples/ip_address.rb +22 -0
- data/examples/list.rb +124 -0
- data/examples/nbt.rb +178 -0
- data/lib/bindata.rb +33 -0
- data/lib/bindata/alignment.rb +83 -0
- data/lib/bindata/array.rb +335 -0
- data/lib/bindata/base.rb +388 -0
- data/lib/bindata/base_primitive.rb +214 -0
- data/lib/bindata/bits.rb +87 -0
- data/lib/bindata/choice.rb +216 -0
- data/lib/bindata/count_bytes_remaining.rb +35 -0
- data/lib/bindata/deprecated.rb +50 -0
- data/lib/bindata/dsl.rb +312 -0
- data/lib/bindata/float.rb +80 -0
- data/lib/bindata/int.rb +184 -0
- data/lib/bindata/io.rb +274 -0
- data/lib/bindata/lazy.rb +105 -0
- data/lib/bindata/offset.rb +91 -0
- data/lib/bindata/params.rb +135 -0
- data/lib/bindata/primitive.rb +135 -0
- data/lib/bindata/record.rb +110 -0
- data/lib/bindata/registry.rb +92 -0
- data/lib/bindata/rest.rb +35 -0
- data/lib/bindata/sanitize.rb +290 -0
- data/lib/bindata/skip.rb +48 -0
- data/lib/bindata/string.rb +145 -0
- data/lib/bindata/stringz.rb +96 -0
- data/lib/bindata/struct.rb +388 -0
- data/lib/bindata/trace.rb +94 -0
- data/lib/bindata/version.rb +3 -0
- data/setup.rb +1585 -0
- data/spec/alignment_spec.rb +61 -0
- data/spec/array_spec.rb +331 -0
- data/spec/base_primitive_spec.rb +238 -0
- data/spec/base_spec.rb +376 -0
- data/spec/bits_spec.rb +163 -0
- data/spec/choice_spec.rb +263 -0
- data/spec/count_bytes_remaining_spec.rb +38 -0
- data/spec/deprecated_spec.rb +31 -0
- data/spec/example.rb +21 -0
- data/spec/float_spec.rb +37 -0
- data/spec/int_spec.rb +216 -0
- data/spec/io_spec.rb +352 -0
- data/spec/lazy_spec.rb +217 -0
- data/spec/primitive_spec.rb +202 -0
- data/spec/record_spec.rb +530 -0
- data/spec/registry_spec.rb +108 -0
- data/spec/rest_spec.rb +26 -0
- data/spec/skip_spec.rb +27 -0
- data/spec/spec_common.rb +58 -0
- data/spec/string_spec.rb +300 -0
- data/spec/stringz_spec.rb +118 -0
- data/spec/struct_spec.rb +350 -0
- data/spec/system_spec.rb +380 -0
- data/tasks/manual.rake +36 -0
- data/tasks/rspec.rake +17 -0
- metadata +208 -0
data/lib/bindata/skip.rb
ADDED
@@ -0,0 +1,48 @@
|
|
1
|
+
require "bindata/base_primitive"
|
2
|
+
|
3
|
+
module BinData
|
4
|
+
# Skip will skip over bytes from the input stream. If the stream is not
|
5
|
+
# seekable, then the bytes are consumed and discarded.
|
6
|
+
#
|
7
|
+
# When writing, skip will write <tt>:length</tt> number of zero bytes.
|
8
|
+
#
|
9
|
+
# require 'bindata'
|
10
|
+
#
|
11
|
+
# class A < BinData::Record
|
12
|
+
# skip :length => 5
|
13
|
+
# string :a, :read_length => 5
|
14
|
+
# end
|
15
|
+
#
|
16
|
+
# obj = A.read("abcdefghij")
|
17
|
+
# obj.a #=> "fghij"
|
18
|
+
#
|
19
|
+
# == Parameters
|
20
|
+
#
|
21
|
+
# Skip objects accept all the params that BinData::BasePrimitive
|
22
|
+
# does, as well as the following:
|
23
|
+
#
|
24
|
+
# <tt>:length</tt>:: The number of bytes to skip.
|
25
|
+
#
|
26
|
+
class Skip < BinData::BasePrimitive
|
27
|
+
|
28
|
+
mandatory_parameter :length
|
29
|
+
|
30
|
+
#---------------
|
31
|
+
private
|
32
|
+
|
33
|
+
def value_to_binary_string(val)
|
34
|
+
len = eval_parameter(:length)
|
35
|
+
"\000" * len
|
36
|
+
end
|
37
|
+
|
38
|
+
def read_and_return_value(io)
|
39
|
+
len = eval_parameter(:length)
|
40
|
+
io.seekbytes(len)
|
41
|
+
""
|
42
|
+
end
|
43
|
+
|
44
|
+
def sensible_default
|
45
|
+
""
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
@@ -0,0 +1,145 @@
|
|
1
|
+
require "bindata/base_primitive"
|
2
|
+
|
3
|
+
module BinData
|
4
|
+
# A String is a sequence of bytes. This is the same as strings in Ruby 1.8.
|
5
|
+
# The issue of character encoding is ignored by this class.
|
6
|
+
#
|
7
|
+
# require 'bindata'
|
8
|
+
#
|
9
|
+
# data = "abcdefghij"
|
10
|
+
#
|
11
|
+
# obj = BinData::String.new(:read_length => 5)
|
12
|
+
# obj.read(data)
|
13
|
+
# obj #=> "abcde"
|
14
|
+
#
|
15
|
+
# obj = BinData::String.new(:length => 6)
|
16
|
+
# obj.read(data)
|
17
|
+
# obj #=> "abcdef"
|
18
|
+
# obj.assign("abcdefghij")
|
19
|
+
# obj #=> "abcdef"
|
20
|
+
# obj.assign("abcd")
|
21
|
+
# obj #=> "abcd\000\000"
|
22
|
+
#
|
23
|
+
# obj = BinData::String.new(:length => 6, :trim_padding => true)
|
24
|
+
# obj.assign("abcd")
|
25
|
+
# obj #=> "abcd"
|
26
|
+
# obj.to_binary_s #=> "abcd\000\000"
|
27
|
+
#
|
28
|
+
# obj = BinData::String.new(:length => 6, :pad_byte => 'A')
|
29
|
+
# obj.assign("abcd")
|
30
|
+
# obj #=> "abcdAA"
|
31
|
+
# obj.to_binary_s #=> "abcdAA"
|
32
|
+
#
|
33
|
+
# == Parameters
|
34
|
+
#
|
35
|
+
# String objects accept all the params that BinData::BasePrimitive
|
36
|
+
# does, as well as the following:
|
37
|
+
#
|
38
|
+
# <tt>:read_length</tt>:: The length in bytes to use when reading a value.
|
39
|
+
# <tt>:length</tt>:: The fixed length of the string. If a shorter
|
40
|
+
# string is set, it will be padded to this length.
|
41
|
+
# <tt>:pad_byte</tt>:: The byte to use when padding a string to a
|
42
|
+
# set length. Valid values are Integers and
|
43
|
+
# Strings of length 1. "\0" is the default.
|
44
|
+
# <tt>:pad_front</tt>:: Signifies that the padding occurs at the front
|
45
|
+
# of the string rather than the end. Default
|
46
|
+
# is false.
|
47
|
+
# <tt>:trim_padding</tt>:: Boolean, default false. If set, #value will
|
48
|
+
# return the value with all pad_bytes trimmed
|
49
|
+
# from the end of the string. The value will
|
50
|
+
# not be trimmed when writing.
|
51
|
+
class String < BinData::BasePrimitive
|
52
|
+
|
53
|
+
optional_parameters :read_length, :length, :trim_padding, :pad_front, :pad_left
|
54
|
+
default_parameters :pad_byte => "\0"
|
55
|
+
mutually_exclusive_parameters :read_length, :length
|
56
|
+
mutually_exclusive_parameters :length, :value
|
57
|
+
|
58
|
+
class << self
|
59
|
+
|
60
|
+
def sanitize_parameters!(params) #:nodoc:
|
61
|
+
params.warn_replacement_parameter(:initial_length, :read_length)
|
62
|
+
|
63
|
+
params.warn_renamed_parameter(:pad_char, :pad_byte) # Remove this line in the future
|
64
|
+
|
65
|
+
if params.has_parameter?(:pad_left)
|
66
|
+
params[:pad_front] = params.delete(:pad_left)
|
67
|
+
end
|
68
|
+
|
69
|
+
if params.has_parameter?(:pad_byte)
|
70
|
+
byte = params[:pad_byte]
|
71
|
+
params[:pad_byte] = sanitized_pad_byte(byte)
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
#-------------
|
76
|
+
private
|
77
|
+
|
78
|
+
def sanitized_pad_byte(byte)
|
79
|
+
result = byte.is_a?(Integer) ? byte.chr : byte.to_s
|
80
|
+
len = result.respond_to?(:bytesize) ? result.bytesize : result.length
|
81
|
+
if len > 1
|
82
|
+
raise ArgumentError, ":pad_byte must not contain more than 1 byte"
|
83
|
+
end
|
84
|
+
result
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
def assign(val)
|
89
|
+
super(binary_string(val))
|
90
|
+
end
|
91
|
+
|
92
|
+
def snapshot
|
93
|
+
# override to trim padding
|
94
|
+
result = super
|
95
|
+
result = clamp_to_length(result)
|
96
|
+
|
97
|
+
if get_parameter(:trim_padding)
|
98
|
+
result = trim_padding(result)
|
99
|
+
end
|
100
|
+
result
|
101
|
+
end
|
102
|
+
|
103
|
+
#---------------
|
104
|
+
private
|
105
|
+
|
106
|
+
def clamp_to_length(str)
|
107
|
+
str = binary_string(str)
|
108
|
+
|
109
|
+
len = eval_parameter(:length) || str.length
|
110
|
+
if str.length == len
|
111
|
+
str
|
112
|
+
elsif str.length > len
|
113
|
+
str.slice(0, len)
|
114
|
+
else
|
115
|
+
padding = (eval_parameter(:pad_byte) * (len - str.length))
|
116
|
+
if get_parameter(:pad_front)
|
117
|
+
padding + str
|
118
|
+
else
|
119
|
+
str + padding
|
120
|
+
end
|
121
|
+
end
|
122
|
+
end
|
123
|
+
|
124
|
+
def trim_padding(str)
|
125
|
+
if get_parameter(:pad_front)
|
126
|
+
str.sub(/\A#{eval_parameter(:pad_byte)}*/, "")
|
127
|
+
else
|
128
|
+
str.sub(/#{eval_parameter(:pad_byte)}*\z/, "")
|
129
|
+
end
|
130
|
+
end
|
131
|
+
|
132
|
+
def value_to_binary_string(val)
|
133
|
+
clamp_to_length(val)
|
134
|
+
end
|
135
|
+
|
136
|
+
def read_and_return_value(io)
|
137
|
+
len = eval_parameter(:read_length) || eval_parameter(:length) || 0
|
138
|
+
io.readbytes(len)
|
139
|
+
end
|
140
|
+
|
141
|
+
def sensible_default
|
142
|
+
""
|
143
|
+
end
|
144
|
+
end
|
145
|
+
end
|
@@ -0,0 +1,96 @@
|
|
1
|
+
require "bindata/base_primitive"
|
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
|
+
# require 'bindata'
|
11
|
+
#
|
12
|
+
# data = "abcd\x00efgh"
|
13
|
+
#
|
14
|
+
# obj = BinData::Stringz.new
|
15
|
+
# obj.read(data)
|
16
|
+
# obj.snapshot #=> "abcd"
|
17
|
+
# obj.num_bytes #=> 5
|
18
|
+
# obj.to_binary_s #=> "abcd\000"
|
19
|
+
#
|
20
|
+
# == Parameters
|
21
|
+
#
|
22
|
+
# Stringz objects accept all the params that BinData::BasePrimitive
|
23
|
+
# does, as well as the following:
|
24
|
+
#
|
25
|
+
# <tt>:max_length</tt>:: The maximum length of the string including the zero
|
26
|
+
# byte.
|
27
|
+
class Stringz < BinData::BasePrimitive
|
28
|
+
|
29
|
+
optional_parameters :max_length
|
30
|
+
|
31
|
+
def assign(val)
|
32
|
+
super(binary_string(val))
|
33
|
+
end
|
34
|
+
|
35
|
+
def snapshot
|
36
|
+
# override to always remove trailing zero bytes
|
37
|
+
result = super
|
38
|
+
trim_and_zero_terminate(result).chomp("\0")
|
39
|
+
end
|
40
|
+
|
41
|
+
#---------------
|
42
|
+
private
|
43
|
+
|
44
|
+
def value_to_binary_string(val)
|
45
|
+
trim_and_zero_terminate(val)
|
46
|
+
end
|
47
|
+
|
48
|
+
def read_and_return_value(io)
|
49
|
+
max_length = eval_parameter(:max_length)
|
50
|
+
str = ""
|
51
|
+
i = 0
|
52
|
+
ch = nil
|
53
|
+
|
54
|
+
# read until zero byte or we have read in the max number of bytes
|
55
|
+
while ch != "\0" and i != max_length
|
56
|
+
ch = io.readbytes(1)
|
57
|
+
str << ch
|
58
|
+
i += 1
|
59
|
+
end
|
60
|
+
|
61
|
+
trim_and_zero_terminate(str)
|
62
|
+
end
|
63
|
+
|
64
|
+
def sensible_default
|
65
|
+
""
|
66
|
+
end
|
67
|
+
|
68
|
+
def trim_and_zero_terminate(str)
|
69
|
+
result = binary_string(str)
|
70
|
+
truncate_after_first_zero_byte!(result)
|
71
|
+
trim_to!(result, eval_parameter(:max_length))
|
72
|
+
append_zero_byte_if_needed!(result)
|
73
|
+
result
|
74
|
+
end
|
75
|
+
|
76
|
+
def truncate_after_first_zero_byte!(str)
|
77
|
+
str.sub!(/([^\0]*\0).*/, '\1')
|
78
|
+
end
|
79
|
+
|
80
|
+
def trim_to!(str, max_length = nil)
|
81
|
+
if max_length
|
82
|
+
max_length = 1 if max_length < 1
|
83
|
+
str.slice!(max_length)
|
84
|
+
if str.length == max_length and str[-1, 1] != "\0"
|
85
|
+
str[-1, 1] = "\0"
|
86
|
+
end
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
def append_zero_byte_if_needed!(str)
|
91
|
+
if str.length == 0 or str[-1, 1] != "\0"
|
92
|
+
str << "\0"
|
93
|
+
end
|
94
|
+
end
|
95
|
+
end
|
96
|
+
end
|
@@ -0,0 +1,388 @@
|
|
1
|
+
require 'bindata/base'
|
2
|
+
|
3
|
+
module BinData
|
4
|
+
|
5
|
+
class Base
|
6
|
+
optional_parameter :onlyif # Used by Struct
|
7
|
+
end
|
8
|
+
|
9
|
+
# A Struct is an ordered collection of named data objects.
|
10
|
+
#
|
11
|
+
# require 'bindata'
|
12
|
+
#
|
13
|
+
# class Tuple < BinData::Record
|
14
|
+
# int8 :x
|
15
|
+
# int8 :y
|
16
|
+
# int8 :z
|
17
|
+
# end
|
18
|
+
#
|
19
|
+
# obj = BinData::Struct.new(:hide => :a,
|
20
|
+
# :fields => [ [:int32le, :a],
|
21
|
+
# [:int16le, :b],
|
22
|
+
# [:tuple, :s] ])
|
23
|
+
# obj.field_names =># ["b", "s"]
|
24
|
+
#
|
25
|
+
#
|
26
|
+
# == Parameters
|
27
|
+
#
|
28
|
+
# Parameters may be provided at initialisation to control the behaviour of
|
29
|
+
# an object. These params are:
|
30
|
+
#
|
31
|
+
# <tt>:fields</tt>:: An array specifying the fields for this struct.
|
32
|
+
# Each element of the array is of the form [type, name,
|
33
|
+
# params]. Type is a symbol representing a registered
|
34
|
+
# type. Name is the name of this field. Params is an
|
35
|
+
# optional hash of parameters to pass to this field
|
36
|
+
# when instantiating it. If name is "" or nil, then
|
37
|
+
# that field is anonymous and behaves as a hidden field.
|
38
|
+
# <tt>:hide</tt>:: A list of the names of fields that are to be hidden
|
39
|
+
# from the outside world. Hidden fields don't appear
|
40
|
+
# in #snapshot or #field_names but are still accessible
|
41
|
+
# by name.
|
42
|
+
# <tt>:endian</tt>:: Either :little or :big. This specifies the default
|
43
|
+
# endian of any numerics in this struct, or in any
|
44
|
+
# nested data objects.
|
45
|
+
#
|
46
|
+
# == Field Parameters
|
47
|
+
#
|
48
|
+
# Fields may have have extra parameters as listed below:
|
49
|
+
#
|
50
|
+
# [<tt>:onlyif</tt>] Used to indicate a data object is optional.
|
51
|
+
# if +false+, this object will not be included in any
|
52
|
+
# calls to #read, #write, #num_bytes or #snapshot.
|
53
|
+
class Struct < BinData::Base
|
54
|
+
|
55
|
+
mandatory_parameter :fields
|
56
|
+
optional_parameters :endian, :hide
|
57
|
+
|
58
|
+
# These reserved words may not be used as field names
|
59
|
+
RESERVED = Hash[*
|
60
|
+
(Hash.instance_methods +
|
61
|
+
%w{alias and begin break case class def defined do else elsif
|
62
|
+
end ensure false for if in module next nil not or redo
|
63
|
+
rescue retry return self super then true undef unless until
|
64
|
+
when while yield} +
|
65
|
+
%w{array element index value} ).collect { |name| name.to_sym }.
|
66
|
+
uniq.collect { |key| [key, true] }.flatten
|
67
|
+
]
|
68
|
+
|
69
|
+
class << self
|
70
|
+
|
71
|
+
def sanitize_parameters!(params) #:nodoc:
|
72
|
+
sanitize_endian(params)
|
73
|
+
sanitize_fields(params)
|
74
|
+
sanitize_hide(params)
|
75
|
+
end
|
76
|
+
|
77
|
+
#-------------
|
78
|
+
private
|
79
|
+
|
80
|
+
def sanitize_endian(params)
|
81
|
+
if params.needs_sanitizing?(:endian)
|
82
|
+
endian = params.create_sanitized_endian(params[:endian])
|
83
|
+
params[:endian] = endian
|
84
|
+
params.endian = endian # sync params[:endian] and params.endian
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
def sanitize_fields(params)
|
89
|
+
if params.needs_sanitizing?(:fields)
|
90
|
+
fields = params[:fields]
|
91
|
+
|
92
|
+
params[:fields] = params.create_sanitized_fields
|
93
|
+
fields.each do |ftype, fname, fparams|
|
94
|
+
params[:fields].add_field(ftype, fname, fparams)
|
95
|
+
end
|
96
|
+
|
97
|
+
field_names = sanitized_field_names(params[:fields])
|
98
|
+
ensure_field_names_are_valid(field_names)
|
99
|
+
end
|
100
|
+
end
|
101
|
+
|
102
|
+
def sanitize_hide(params)
|
103
|
+
if params.needs_sanitizing?(:hide) and params.has_parameter?(:fields)
|
104
|
+
field_names = sanitized_field_names(params[:fields])
|
105
|
+
hfield_names = hidden_field_names(params[:hide])
|
106
|
+
|
107
|
+
params[:hide] = (hfield_names & field_names)
|
108
|
+
end
|
109
|
+
end
|
110
|
+
|
111
|
+
def sanitized_field_names(sanitized_fields)
|
112
|
+
sanitized_fields.field_names.compact
|
113
|
+
end
|
114
|
+
|
115
|
+
def hidden_field_names(hidden)
|
116
|
+
(hidden || []).collect do |h|
|
117
|
+
unless Symbol === h
|
118
|
+
warn "Hidden field '#{h}' should be provided as a symbol. Using strings is deprecated"
|
119
|
+
end
|
120
|
+
h.to_sym
|
121
|
+
end
|
122
|
+
end
|
123
|
+
|
124
|
+
def ensure_field_names_are_valid(field_names)
|
125
|
+
reserved_names = RESERVED
|
126
|
+
|
127
|
+
field_names.each do |name|
|
128
|
+
if self.class.method_defined?(name)
|
129
|
+
raise NameError.new("Rename field '#{name}' in #{self}, " +
|
130
|
+
"as it shadows an existing method.", name)
|
131
|
+
end
|
132
|
+
if reserved_names.include?(name)
|
133
|
+
raise NameError.new("Rename field '#{name}' in #{self}, " +
|
134
|
+
"as it is a reserved name.", name)
|
135
|
+
end
|
136
|
+
if field_names.count(name) != 1
|
137
|
+
raise NameError.new("field '#{name}' in #{self}, " +
|
138
|
+
"is defined multiple times.", name)
|
139
|
+
end
|
140
|
+
end
|
141
|
+
end
|
142
|
+
end
|
143
|
+
|
144
|
+
def initialize_shared_instance
|
145
|
+
@field_names = get_parameter(:fields).field_names.freeze
|
146
|
+
end
|
147
|
+
|
148
|
+
def initialize_instance
|
149
|
+
@field_objs = []
|
150
|
+
end
|
151
|
+
|
152
|
+
def clear #:nodoc:
|
153
|
+
@field_objs.each { |f| f.clear unless f.nil? }
|
154
|
+
end
|
155
|
+
|
156
|
+
def clear? #:nodoc:
|
157
|
+
@field_objs.all? { |f| f.nil? or f.clear? }
|
158
|
+
end
|
159
|
+
|
160
|
+
def assign(val)
|
161
|
+
clear
|
162
|
+
assign_fields(val)
|
163
|
+
end
|
164
|
+
|
165
|
+
def snapshot
|
166
|
+
snapshot = Snapshot.new
|
167
|
+
field_names.each do |name|
|
168
|
+
obj = find_obj_for_name(name)
|
169
|
+
snapshot[name] = obj.snapshot if include_obj(obj)
|
170
|
+
end
|
171
|
+
snapshot
|
172
|
+
end
|
173
|
+
|
174
|
+
# Returns a list of the names of all fields accessible through this
|
175
|
+
# object. +include_hidden+ specifies whether to include hidden names
|
176
|
+
# in the listing.
|
177
|
+
def field_names(include_hidden = false)
|
178
|
+
if include_hidden
|
179
|
+
@field_names.compact
|
180
|
+
else
|
181
|
+
hidden = get_parameter(:hide) || []
|
182
|
+
@field_names.compact - hidden
|
183
|
+
end.collect { |x| x.to_s }
|
184
|
+
end
|
185
|
+
|
186
|
+
def respond_to?(symbol, include_private = false) #:nodoc:
|
187
|
+
@field_names.include?(base_field_name(symbol)) || super
|
188
|
+
end
|
189
|
+
|
190
|
+
def method_missing(symbol, *args, &block) #:nodoc:
|
191
|
+
obj = find_obj_for_name(symbol)
|
192
|
+
if obj
|
193
|
+
invoke_field(obj, symbol, args)
|
194
|
+
else
|
195
|
+
super
|
196
|
+
end
|
197
|
+
end
|
198
|
+
|
199
|
+
def debug_name_of(child) #:nodoc:
|
200
|
+
field_name = @field_names[find_index_of(child)]
|
201
|
+
"#{debug_name}.#{field_name}"
|
202
|
+
end
|
203
|
+
|
204
|
+
def offset_of(child) #:nodoc:
|
205
|
+
instantiate_all_objs
|
206
|
+
sum = sum_num_bytes_below_index(find_index_of(child))
|
207
|
+
child.do_num_bytes.is_a?(Integer) ? sum.ceil : sum.floor
|
208
|
+
end
|
209
|
+
|
210
|
+
def do_read(io) #:nodoc:
|
211
|
+
instantiate_all_objs
|
212
|
+
@field_objs.each { |f| f.do_read(io) if include_obj(f) }
|
213
|
+
end
|
214
|
+
|
215
|
+
def do_write(io) #:nodoc
|
216
|
+
instantiate_all_objs
|
217
|
+
@field_objs.each { |f| f.do_write(io) if include_obj(f) }
|
218
|
+
end
|
219
|
+
|
220
|
+
def do_num_bytes #:nodoc:
|
221
|
+
instantiate_all_objs
|
222
|
+
sum_num_bytes_for_all_fields
|
223
|
+
end
|
224
|
+
|
225
|
+
def [](key)
|
226
|
+
find_obj_for_name(key)
|
227
|
+
end
|
228
|
+
|
229
|
+
def []=(key, value)
|
230
|
+
obj = find_obj_for_name(key)
|
231
|
+
if obj
|
232
|
+
obj.assign(value)
|
233
|
+
end
|
234
|
+
end
|
235
|
+
|
236
|
+
def has_key?(key)
|
237
|
+
@field_names.index(base_field_name(key))
|
238
|
+
end
|
239
|
+
|
240
|
+
def each_pair
|
241
|
+
@field_names.compact.each do |name|
|
242
|
+
yield [name, find_obj_for_name(name)]
|
243
|
+
end
|
244
|
+
end
|
245
|
+
|
246
|
+
#---------------
|
247
|
+
private
|
248
|
+
|
249
|
+
def base_field_name(name)
|
250
|
+
name.to_s.chomp("=").to_sym
|
251
|
+
end
|
252
|
+
|
253
|
+
def invoke_field(obj, symbol, args)
|
254
|
+
name = symbol.to_s
|
255
|
+
is_writer = (name[-1, 1] == "=")
|
256
|
+
|
257
|
+
if is_writer
|
258
|
+
obj.assign(*args)
|
259
|
+
else
|
260
|
+
obj
|
261
|
+
end
|
262
|
+
end
|
263
|
+
|
264
|
+
def find_index_of(obj)
|
265
|
+
@field_objs.index { |el| el.equal?(obj) }
|
266
|
+
end
|
267
|
+
|
268
|
+
def find_obj_for_name(name)
|
269
|
+
index = @field_names.index(base_field_name(name))
|
270
|
+
if index
|
271
|
+
instantiate_obj_at(index)
|
272
|
+
@field_objs[index]
|
273
|
+
else
|
274
|
+
nil
|
275
|
+
end
|
276
|
+
end
|
277
|
+
|
278
|
+
def instantiate_all_objs
|
279
|
+
@field_names.each_index { |i| instantiate_obj_at(i) }
|
280
|
+
end
|
281
|
+
|
282
|
+
def instantiate_obj_at(index)
|
283
|
+
if @field_objs[index].nil?
|
284
|
+
field = get_parameter(:fields)[index]
|
285
|
+
@field_objs[index] = field.instantiate(nil, self)
|
286
|
+
end
|
287
|
+
end
|
288
|
+
|
289
|
+
def assign_fields(val)
|
290
|
+
src = as_stringified_hash(val)
|
291
|
+
|
292
|
+
@field_names.compact.each do |name|
|
293
|
+
obj = find_obj_for_name(name)
|
294
|
+
if obj and src.has_key?(name)
|
295
|
+
obj.assign(src[name])
|
296
|
+
end
|
297
|
+
end
|
298
|
+
end
|
299
|
+
|
300
|
+
def as_stringified_hash(val)
|
301
|
+
if BinData::Struct === val
|
302
|
+
val
|
303
|
+
elsif val.nil?
|
304
|
+
{}
|
305
|
+
else
|
306
|
+
hash = Snapshot.new
|
307
|
+
val.each_pair { |k,v| hash[k] = v }
|
308
|
+
hash
|
309
|
+
end
|
310
|
+
end
|
311
|
+
|
312
|
+
def sum_num_bytes_for_all_fields
|
313
|
+
sum_num_bytes_below_index(@field_objs.length)
|
314
|
+
end
|
315
|
+
|
316
|
+
def sum_num_bytes_below_index(index)
|
317
|
+
sum = 0
|
318
|
+
(0...index).each do |i|
|
319
|
+
obj = @field_objs[i]
|
320
|
+
if include_obj(obj)
|
321
|
+
nbytes = obj.do_num_bytes
|
322
|
+
sum = (nbytes.is_a?(Integer) ? sum.ceil : sum) + nbytes
|
323
|
+
end
|
324
|
+
end
|
325
|
+
|
326
|
+
sum
|
327
|
+
end
|
328
|
+
|
329
|
+
def include_obj(obj)
|
330
|
+
not obj.has_parameter?(:onlyif) or obj.eval_parameter(:onlyif)
|
331
|
+
end
|
332
|
+
|
333
|
+
if RUBY_VERSION <= "1.9"
|
334
|
+
module OrderedHash #:nodoc:
|
335
|
+
def keys
|
336
|
+
@order ||= []
|
337
|
+
k = super
|
338
|
+
@order & k
|
339
|
+
end
|
340
|
+
|
341
|
+
def []=(key, value)
|
342
|
+
@order ||= []
|
343
|
+
@order << key
|
344
|
+
super(key, value)
|
345
|
+
end
|
346
|
+
|
347
|
+
def each
|
348
|
+
keys.each do |k|
|
349
|
+
yield [k, self[k]]
|
350
|
+
end
|
351
|
+
end
|
352
|
+
|
353
|
+
def each_pair
|
354
|
+
each do |el|
|
355
|
+
yield *el
|
356
|
+
end
|
357
|
+
end
|
358
|
+
end
|
359
|
+
end
|
360
|
+
|
361
|
+
# A hash that can be accessed via attributes.
|
362
|
+
class Snapshot < ::Hash #:nodoc:
|
363
|
+
include OrderedHash if RUBY_VERSION <= "1.9"
|
364
|
+
|
365
|
+
def has_key?(key)
|
366
|
+
super(key.to_s)
|
367
|
+
end
|
368
|
+
|
369
|
+
def [](key)
|
370
|
+
super(key.to_s)
|
371
|
+
end
|
372
|
+
|
373
|
+
def []=(key, value)
|
374
|
+
if value != nil
|
375
|
+
super(key.to_s, value)
|
376
|
+
end
|
377
|
+
end
|
378
|
+
|
379
|
+
def respond_to?(symbol, include_private = false)
|
380
|
+
has_key?(symbol) || super
|
381
|
+
end
|
382
|
+
|
383
|
+
def method_missing(symbol, *args)
|
384
|
+
self[symbol] || super
|
385
|
+
end
|
386
|
+
end
|
387
|
+
end
|
388
|
+
end
|