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
data/TODO
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
* Add a way to do something like: read a bunch of integers and stop reading
|
2
|
+
after reading an integer with a value of 0.
|
3
|
+
|
4
|
+
* Add scoping so the value of a param doesn't need to call parent
|
5
|
+
|
6
|
+
* Optimise int.rb for speed.
|
7
|
+
|
8
|
+
* Maybe add an endian method to struct so you can say int16 instead of int16le.
|
9
|
+
- Should this by lazily evaluated, or evaluated only once when
|
10
|
+
instantiating fields?
|
11
|
+
|
12
|
+
* Think how offset_of should work.
|
13
|
+
|
14
|
+
* Raise error when a struct defines a name of an existing method
|
data/examples/gzip.rb
ADDED
@@ -0,0 +1,174 @@
|
|
1
|
+
require 'bindata'
|
2
|
+
require 'forwardable'
|
3
|
+
|
4
|
+
# An example of a reader / writer for the GZIP file format as per rfc1952.
|
5
|
+
# Note that compression is not implemented to keep the example small.
|
6
|
+
class Gzip
|
7
|
+
extend Forwardable
|
8
|
+
|
9
|
+
# Known compression methods
|
10
|
+
DEFLATE = 8
|
11
|
+
|
12
|
+
class Extra < BinData::Struct
|
13
|
+
uint16le :len, :length => lambda { data.length }
|
14
|
+
string :data, :initial_length => :len
|
15
|
+
end
|
16
|
+
|
17
|
+
class Header < BinData::Struct
|
18
|
+
uint16le :id, :value => 0x8b1f, :check_value => 0x8b1f
|
19
|
+
uint8 :compression_method, :initial_value => DEFLATE
|
20
|
+
uint8 :flags, :value => :calculate_flags_val,
|
21
|
+
# Upper 3 bits must be zero
|
22
|
+
:check_value => lambda { (value & 0xe0) == 0 }
|
23
|
+
uint32le :mtime
|
24
|
+
uint8 :extra_flags
|
25
|
+
uint8 :os, :initial_value => 255 # unknown OS
|
26
|
+
|
27
|
+
# These fields are optional depending on the bits in flags
|
28
|
+
extra :extra, :readwrite => :extra?
|
29
|
+
stringz :file_name, :readwrite => :file_name?
|
30
|
+
stringz :comment, :readwrite => :comment?
|
31
|
+
uint16le :crc16, :readwrite => :crc16?
|
32
|
+
|
33
|
+
|
34
|
+
## Convenience methods for accessing and manipulating flags
|
35
|
+
|
36
|
+
attr_writer :text
|
37
|
+
|
38
|
+
# Access bits of flags
|
39
|
+
def text?; flag_val(0) end
|
40
|
+
def crc16?; flag_val(1) end
|
41
|
+
def extra?; flag_val(2) end
|
42
|
+
def file_name?; flag_val(3) end
|
43
|
+
def comment?; flag_val(4) end
|
44
|
+
|
45
|
+
def flag_val(bit) (flags & (1 << bit)) != 0 end
|
46
|
+
|
47
|
+
# Calculate the value of flags based on current state.
|
48
|
+
def calculate_flags_val
|
49
|
+
((@text ? 1 : 0) << 0) |
|
50
|
+
|
51
|
+
# Never include header crc. This is because the current versions of the
|
52
|
+
# command-line version of gzip (up through version 1.3.x) do not
|
53
|
+
# support header crc's, and will report that it is a "multi-part gzip
|
54
|
+
# file" and give up.
|
55
|
+
((!clear?(:crc16) ? 0 : 0) << 1) |
|
56
|
+
|
57
|
+
((!clear?(:extra) ? 1 : 0) << 2) |
|
58
|
+
((!clear?(:file_name) ? 1 : 0) << 3) |
|
59
|
+
((!clear?(:comment) ? 1 : 0) << 4)
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
class Footer < BinData::Struct
|
64
|
+
uint32le :crc32
|
65
|
+
uint32le :uncompressed_size
|
66
|
+
end
|
67
|
+
|
68
|
+
def initialize
|
69
|
+
@header = Header.new
|
70
|
+
@footer = Footer.new
|
71
|
+
end
|
72
|
+
|
73
|
+
attr_accessor :compressed
|
74
|
+
def_delegators :@header, :file_name=, :file_name, :file_name?
|
75
|
+
def_delegators :@header, :comment=, :comment, :comment?
|
76
|
+
def_delegators :@header, :compression_method
|
77
|
+
def_delegators :@footer, :crc32, :uncompressed_size
|
78
|
+
|
79
|
+
def mtime
|
80
|
+
Time.at(@header.mtime)
|
81
|
+
end
|
82
|
+
|
83
|
+
def mtime=(tm)
|
84
|
+
@header.mtime = tm.to_i
|
85
|
+
end
|
86
|
+
|
87
|
+
def total_size
|
88
|
+
@header.num_bytes + @compressed.size + @footer.num_bytes
|
89
|
+
end
|
90
|
+
|
91
|
+
def compressed_data
|
92
|
+
@compressed
|
93
|
+
end
|
94
|
+
|
95
|
+
def set_compressed_data(compressed, crc32, uncompressed_size)
|
96
|
+
@compressed = compressed
|
97
|
+
@footer.crc32 = crc32
|
98
|
+
@footer.uncompressed_size = uncompressed_size
|
99
|
+
end
|
100
|
+
|
101
|
+
def read(file_name)
|
102
|
+
File.open(file_name, "r") do |io|
|
103
|
+
@header.read(io)
|
104
|
+
|
105
|
+
# Determine the size of the compressed data. This is needed because
|
106
|
+
# we don't actually uncompress the data. Ideally the uncompression
|
107
|
+
# method would read the correct number of bytes from the IO and the
|
108
|
+
# IO would be positioned ready to read the footer.
|
109
|
+
|
110
|
+
pos = io.pos
|
111
|
+
io.seek(-@footer.num_bytes, IO::SEEK_END)
|
112
|
+
compressed_size = io.pos - pos
|
113
|
+
io.seek(pos)
|
114
|
+
|
115
|
+
@compressed = io.read(compressed_size)
|
116
|
+
@footer.read(io)
|
117
|
+
end
|
118
|
+
end
|
119
|
+
|
120
|
+
def write(file_name)
|
121
|
+
File.open(file_name, "w") do |io|
|
122
|
+
@header.write(io)
|
123
|
+
io.write(@compressed)
|
124
|
+
@footer.write(io)
|
125
|
+
end
|
126
|
+
end
|
127
|
+
end
|
128
|
+
|
129
|
+
if __FILE__ == $0
|
130
|
+
# Write a gzip file.
|
131
|
+
print "Creating a gzip file ... "
|
132
|
+
g = Gzip.new
|
133
|
+
# Uncompressed data is "the cat sat on the mat"
|
134
|
+
g.set_compressed_data("+\311HUHN,Q(\006\342\374<\205\022 77\261\004\000",
|
135
|
+
3464689835, 22)
|
136
|
+
g.file_name = "poetry"
|
137
|
+
g.mtime = Time.now
|
138
|
+
g.comment = "A stunning piece of prose"
|
139
|
+
g.write("poetry.gz")
|
140
|
+
puts "done."
|
141
|
+
puts
|
142
|
+
|
143
|
+
# Read the created gzip file.
|
144
|
+
print "Reading newly created gzip file ... "
|
145
|
+
g = Gzip.new
|
146
|
+
g.read("poetry.gz")
|
147
|
+
puts "done."
|
148
|
+
puts
|
149
|
+
|
150
|
+
puts "Printing gzip file details in the format of gzip -l -v"
|
151
|
+
|
152
|
+
# compression ratio
|
153
|
+
ratio = 100.0 * (g.uncompressed_size - g.compressed.size) /
|
154
|
+
g.uncompressed_size
|
155
|
+
|
156
|
+
comp_meth = (g.compression_method == Gzip::DEFLATE) ? "defla" : ""
|
157
|
+
|
158
|
+
# Output using the same format as gzip -l -v
|
159
|
+
puts "method crc date time compressed " +
|
160
|
+
"uncompressed ratio uncompressed_name"
|
161
|
+
puts "%5s %08x %6s %5s %19s %19s %5.1f%% %s" % [comp_meth,
|
162
|
+
g.crc32,
|
163
|
+
g.mtime.strftime('%b %d'),
|
164
|
+
g.mtime.strftime('%H:%M'),
|
165
|
+
g.total_size,
|
166
|
+
g.uncompressed_size,
|
167
|
+
ratio,
|
168
|
+
g.file_name]
|
169
|
+
puts "Comment: #{g.comment}" if g.comment?
|
170
|
+
puts
|
171
|
+
|
172
|
+
puts "Executing gzip -l -v"
|
173
|
+
puts `gzip -l -v poetry.gz`
|
174
|
+
end
|
data/lib/bindata.rb
ADDED
@@ -0,0 +1,13 @@
|
|
1
|
+
# BinData -- Binary data manipulator.
|
2
|
+
# Copyright (c) 2007 Dion Mendel.
|
3
|
+
|
4
|
+
require 'bindata/array'
|
5
|
+
require 'bindata/choice'
|
6
|
+
require 'bindata/int'
|
7
|
+
require 'bindata/string'
|
8
|
+
require 'bindata/stringz'
|
9
|
+
require 'bindata/struct'
|
10
|
+
|
11
|
+
module BinData
|
12
|
+
VERSION = "0.5.0"
|
13
|
+
end
|
@@ -0,0 +1,160 @@
|
|
1
|
+
require 'bindata/base'
|
2
|
+
|
3
|
+
module BinData
|
4
|
+
# An Array is a list of data objects of the same type.
|
5
|
+
#
|
6
|
+
# require 'bindata'
|
7
|
+
# require 'stringio'
|
8
|
+
#
|
9
|
+
# a = BinData::Array.new(:type => :int8, :initial_length => 5)
|
10
|
+
# io = StringIO.new("\x03\x04\x05\x06\x07")
|
11
|
+
# a.read(io)
|
12
|
+
# a.snapshot #=> [3, 4, 5, 6, 7]
|
13
|
+
#
|
14
|
+
# == Parameters
|
15
|
+
#
|
16
|
+
# Parameters may be provided at initialisation to control the behaviour of
|
17
|
+
# an object. These params are:
|
18
|
+
#
|
19
|
+
# <tt>:type</tt>:: The symbol representing the data type of the
|
20
|
+
# array elements. If the type is to have params
|
21
|
+
# passed to it, then it should be provided as
|
22
|
+
# <tt>[type_symbol, hash_params]</tt>.
|
23
|
+
# <tt>:initial_length</tt>:: The initial length of the array.
|
24
|
+
class Array < Base
|
25
|
+
include Enumerable
|
26
|
+
|
27
|
+
# Register this class
|
28
|
+
register(self.name, self)
|
29
|
+
|
30
|
+
# These are the parameters used by this class.
|
31
|
+
mandatory_parameters :type, :initial_length
|
32
|
+
|
33
|
+
# Creates a new Array
|
34
|
+
def initialize(params = {}, env = nil)
|
35
|
+
super(params, env)
|
36
|
+
|
37
|
+
type, el_params = param(:type)
|
38
|
+
klass = self.class.lookup(type)
|
39
|
+
raise TypeError, "unknown type '#{type}' for #{self}" if klass.nil?
|
40
|
+
|
41
|
+
@element_list = nil
|
42
|
+
@element_klass = klass
|
43
|
+
@element_params = el_params || {}
|
44
|
+
|
45
|
+
# TODO: how to increase the size of the array?
|
46
|
+
end
|
47
|
+
|
48
|
+
# Clears the element at position +index+. If +index+ is not given, then
|
49
|
+
# the internal state of the array is reset to that of a newly created
|
50
|
+
# object.
|
51
|
+
def clear(index = nil)
|
52
|
+
if @element_list.nil?
|
53
|
+
# do nothing as the array is already clear
|
54
|
+
elsif index.nil?
|
55
|
+
@element_list = nil
|
56
|
+
else
|
57
|
+
elements[index].clear
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
# Returns if the element at position +index+ is clear?. If +index+
|
62
|
+
# is not given, then returns whether all fields are clear.
|
63
|
+
def clear?(index = nil)
|
64
|
+
if @element_list.nil?
|
65
|
+
true
|
66
|
+
elsif index.nil?
|
67
|
+
elements.each { |f| return false if not f.clear? }
|
68
|
+
true
|
69
|
+
else
|
70
|
+
elements[index].clear?
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
# Reads the values for all fields in this object from +io+.
|
75
|
+
def _do_read(io)
|
76
|
+
elements.each { |f| f.do_read(io) }
|
77
|
+
end
|
78
|
+
|
79
|
+
# To be called after calling #do_read.
|
80
|
+
def done_read
|
81
|
+
elements.each { |f| f.done_read }
|
82
|
+
end
|
83
|
+
|
84
|
+
# Writes the values for all fields in this object to +io+.
|
85
|
+
def _write(io)
|
86
|
+
elements.each { |f| f.write(io) }
|
87
|
+
end
|
88
|
+
|
89
|
+
# Returns the number of bytes it will take to write the element at
|
90
|
+
# +index+. If +index+, then returns the number of bytes required
|
91
|
+
# to write all fields.
|
92
|
+
def _num_bytes(index)
|
93
|
+
if index.nil?
|
94
|
+
elements.inject(0) { |sum, f| sum + f.num_bytes }
|
95
|
+
else
|
96
|
+
elements[index].num_bytes
|
97
|
+
end
|
98
|
+
end
|
99
|
+
|
100
|
+
# Returns a snapshot of the data in this array.
|
101
|
+
def snapshot
|
102
|
+
elements.collect { |e| e.snapshot }
|
103
|
+
end
|
104
|
+
|
105
|
+
# An array has no fields.
|
106
|
+
def field_names
|
107
|
+
[]
|
108
|
+
end
|
109
|
+
|
110
|
+
# Returns the element at +index+. If the element is a single_value
|
111
|
+
# then the value of the element is returned instead.
|
112
|
+
def [](index)
|
113
|
+
obj = elements[index]
|
114
|
+
obj.single_value? ? obj.value : obj
|
115
|
+
end
|
116
|
+
|
117
|
+
# Sets the element at +index+. If the element is a single_value
|
118
|
+
# then the value of the element is set instead.
|
119
|
+
def []=(index, value)
|
120
|
+
obj = elements[index]
|
121
|
+
unless obj.single_value?
|
122
|
+
raise NoMethodError, "undefined method `[]=' for #{self}", caller
|
123
|
+
end
|
124
|
+
obj.value = value
|
125
|
+
end
|
126
|
+
|
127
|
+
# Iterate over each element in the array. If the elements are
|
128
|
+
# single_values then the values of the elements are iterated instead.
|
129
|
+
def each
|
130
|
+
elements.each do |el|
|
131
|
+
yield(el.single_value? ? el.value : el)
|
132
|
+
end
|
133
|
+
end
|
134
|
+
|
135
|
+
# The number of elements in this array.
|
136
|
+
def length
|
137
|
+
elements.length
|
138
|
+
end
|
139
|
+
alias_method :size, :length
|
140
|
+
|
141
|
+
#---------------
|
142
|
+
private
|
143
|
+
|
144
|
+
# Returns the list of all elements in the array. The elements
|
145
|
+
# will be instantiated on the first call to this method.
|
146
|
+
def elements
|
147
|
+
if @element_list.nil?
|
148
|
+
@element_list = []
|
149
|
+
|
150
|
+
# create the desired number of instances
|
151
|
+
eval_param(:initial_length).times do |i|
|
152
|
+
env = create_env
|
153
|
+
env.index = i
|
154
|
+
@element_list << @element_klass.new(@element_params, env)
|
155
|
+
end
|
156
|
+
end
|
157
|
+
@element_list
|
158
|
+
end
|
159
|
+
end
|
160
|
+
end
|
data/lib/bindata/base.rb
ADDED
@@ -0,0 +1,260 @@
|
|
1
|
+
require 'bindata/lazy'
|
2
|
+
require 'bindata/registry'
|
3
|
+
|
4
|
+
module BinData
|
5
|
+
# Error raised when unexpected results occur when reading data from IO.
|
6
|
+
class ValidityError < StandardError ; end
|
7
|
+
|
8
|
+
# This is the abstract base class for all data objects.
|
9
|
+
#
|
10
|
+
# == Parameters
|
11
|
+
#
|
12
|
+
# Parameters may be provided at initialisation to control the behaviour of
|
13
|
+
# an object. These params are:
|
14
|
+
#
|
15
|
+
# [<tt>:readwrite</tt>] If false, calls to #read or #write will
|
16
|
+
# not perform any I/O. Default is true.
|
17
|
+
# [<tt>:check_offset</tt>] Raise an error if the current IO offest doesn't
|
18
|
+
# meet this criteria. A boolean return indicates
|
19
|
+
# success or failure. Any other return is compared
|
20
|
+
# to the current offset. This parameter is
|
21
|
+
# only checked before reading.
|
22
|
+
class Base
|
23
|
+
class << self
|
24
|
+
# Returns the mandatory parameters used by this class. Any given args
|
25
|
+
# are appended to the parameters list. The parameters for a class will
|
26
|
+
# include the parameters of its ancestors.
|
27
|
+
def mandatory_parameters(*args)
|
28
|
+
unless defined? @mandatory_parameters
|
29
|
+
@mandatory_parameters = []
|
30
|
+
ancestors[1..-1].each do |parent|
|
31
|
+
if parent.respond_to?(:mandatory_parameters)
|
32
|
+
@mandatory_parameters.concat(parent.mandatory_parameters)
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
unless (args.empty?)
|
37
|
+
args.each { |arg| @mandatory_parameters << arg.to_sym }
|
38
|
+
@mandatory_parameters.uniq!
|
39
|
+
end
|
40
|
+
@mandatory_parameters
|
41
|
+
end
|
42
|
+
alias_method :mandatory_parameter, :mandatory_parameters
|
43
|
+
|
44
|
+
# Returns the optional parameters used by this class. Any given args
|
45
|
+
# are appended to the parameters list. The parameters for a class will
|
46
|
+
# include the parameters of its ancestors.
|
47
|
+
def optional_parameters(*args)
|
48
|
+
unless defined? @optional_parameters
|
49
|
+
@optional_parameters = []
|
50
|
+
ancestors[1..-1].each do |parent|
|
51
|
+
if parent.respond_to?(:optional_parameters)
|
52
|
+
@optional_parameters.concat(parent.optional_parameters)
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
56
|
+
unless (args.empty?)
|
57
|
+
args.each { |arg| @optional_parameters << arg.to_sym }
|
58
|
+
@optional_parameters.uniq!
|
59
|
+
end
|
60
|
+
@optional_parameters
|
61
|
+
end
|
62
|
+
alias_method :optional_parameter, :optional_parameters
|
63
|
+
|
64
|
+
# Returns both the mandatory and optional parameters used by this class.
|
65
|
+
def parameters
|
66
|
+
(mandatory_parameters + optional_parameters).uniq
|
67
|
+
end
|
68
|
+
|
69
|
+
# Instantiates this class and reads from +io+. For single value objects
|
70
|
+
# just the value is returned, otherwise the newly created data object is
|
71
|
+
# returned.
|
72
|
+
def read(io)
|
73
|
+
data = self.new
|
74
|
+
data.read(io)
|
75
|
+
data.single_value? ? data.value : data
|
76
|
+
end
|
77
|
+
|
78
|
+
# Registers the mapping of +name+ to +klass+.
|
79
|
+
def register(name, klass)
|
80
|
+
Registry.instance.register(name, klass)
|
81
|
+
end
|
82
|
+
private :register
|
83
|
+
|
84
|
+
# Returns the class matching a previously registered +name+.
|
85
|
+
def lookup(name)
|
86
|
+
Registry.instance.lookup(name)
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
# Define the parameters we use in this class.
|
91
|
+
optional_parameters :check_offset, :readwrite
|
92
|
+
|
93
|
+
# Creates a new data object.
|
94
|
+
#
|
95
|
+
# +params+ is a hash containing symbol keys. Some params may
|
96
|
+
# reference callable objects (methods or procs). +env+ is the
|
97
|
+
# environment that these callable objects are evaluated in.
|
98
|
+
def initialize(params = {}, env = nil)
|
99
|
+
# default :readwrite param to true if unspecified
|
100
|
+
unless params.has_key?(:readwrite)
|
101
|
+
params = params.dup
|
102
|
+
params[:readwrite] = true
|
103
|
+
end
|
104
|
+
|
105
|
+
# ensure mandatory parameters exist
|
106
|
+
self.class.mandatory_parameters.each do |prm|
|
107
|
+
unless params.has_key?(prm)
|
108
|
+
raise ArgumentError, "parameter ':#{prm}' must be specified " +
|
109
|
+
"in #{self}"
|
110
|
+
end
|
111
|
+
end
|
112
|
+
|
113
|
+
known_params = self.class.parameters
|
114
|
+
|
115
|
+
# partition parameters into known and extra parameters
|
116
|
+
@params = {}
|
117
|
+
extra = {}
|
118
|
+
params.each do |k,v|
|
119
|
+
k = k.to_sym
|
120
|
+
raise ArgumentError, "parameter :#{k} is nil in #{self}" if v.nil?
|
121
|
+
if known_params.include?(k)
|
122
|
+
@params[k] = v.freeze
|
123
|
+
else
|
124
|
+
extra[k] = v.freeze
|
125
|
+
end
|
126
|
+
end
|
127
|
+
|
128
|
+
# set up the environment
|
129
|
+
@env = env || LazyEvalEnv.new
|
130
|
+
@env.params = extra
|
131
|
+
@env.data_object = self
|
132
|
+
end
|
133
|
+
|
134
|
+
# Reads data into this bin object by calling #do_read then #done_read.
|
135
|
+
def read(io)
|
136
|
+
# remember the current position in the IO object
|
137
|
+
io.instance_eval "def mark; #{io.pos}; end"
|
138
|
+
|
139
|
+
do_read(io)
|
140
|
+
done_read
|
141
|
+
end
|
142
|
+
|
143
|
+
# Reads the value for this data from +io+.
|
144
|
+
def do_read(io)
|
145
|
+
clear
|
146
|
+
check_offset(io)
|
147
|
+
_do_read(io) if eval_param(:readwrite) != false
|
148
|
+
end
|
149
|
+
|
150
|
+
# Writes the value for this data to +io+.
|
151
|
+
def write(io)
|
152
|
+
_write(io) if eval_param(:readwrite) != false
|
153
|
+
end
|
154
|
+
|
155
|
+
# Returns the number of bytes it will take to write this data.
|
156
|
+
def num_bytes(what = nil)
|
157
|
+
(eval_param(:readwrite) != false) ? _num_bytes(what) : 0
|
158
|
+
end
|
159
|
+
|
160
|
+
# Returns whether this data object contains a single value. Single
|
161
|
+
# value data objects respond to <tt>#value</tt> and <tt>#value=</tt>.
|
162
|
+
def single_value?
|
163
|
+
respond_to? :value
|
164
|
+
end
|
165
|
+
|
166
|
+
#---------------
|
167
|
+
private
|
168
|
+
|
169
|
+
# Creates a new LazyEvalEnv for use by a child data object.
|
170
|
+
def create_env
|
171
|
+
LazyEvalEnv.new(@env)
|
172
|
+
end
|
173
|
+
|
174
|
+
# Returns the value of the evaluated parameter. +key+ references a
|
175
|
+
# parameter from the +params+ hash used when creating the data object.
|
176
|
+
# Returns nil if +key+ does not refer to any parameter.
|
177
|
+
def eval_param(key)
|
178
|
+
@env.lazy_eval(@params[key])
|
179
|
+
end
|
180
|
+
|
181
|
+
# Returns the parameter from the +params+ hash referenced by +key+.
|
182
|
+
# Use this method if you are sure the parameter is not to be evaluated.
|
183
|
+
# You most likely want #eval_param.
|
184
|
+
def param(key)
|
185
|
+
@params[key]
|
186
|
+
end
|
187
|
+
|
188
|
+
# Returns whether +key+ exists in the +params+ hash used when creating
|
189
|
+
# this data object.
|
190
|
+
def has_param?(key)
|
191
|
+
@params.has_key?(key.to_sym)
|
192
|
+
end
|
193
|
+
|
194
|
+
# Raise an error if +param1+ and +param2+ are both given as params.
|
195
|
+
def ensure_mutual_exclusion(param1, param2)
|
196
|
+
if has_param?(param1) and has_param?(param2)
|
197
|
+
raise ArgumentError, "params #{param1} and #{param2} " +
|
198
|
+
"are mutually exclusive"
|
199
|
+
end
|
200
|
+
end
|
201
|
+
|
202
|
+
# Checks that the current offset of +io+ is as expected. This should
|
203
|
+
# be called from #do_read before performing the reading.
|
204
|
+
def check_offset(io)
|
205
|
+
if has_param?(:check_offset)
|
206
|
+
@env.offset = io.pos - io.mark
|
207
|
+
expected = eval_param(:check_offset)
|
208
|
+
|
209
|
+
if not expected
|
210
|
+
raise ValidityError, "offset not as expected"
|
211
|
+
elsif @env.offset != expected and expected != true
|
212
|
+
raise ValidityError, "offset is '#{@env.offset}' but " +
|
213
|
+
"expected '#{expected}'"
|
214
|
+
end
|
215
|
+
end
|
216
|
+
end
|
217
|
+
|
218
|
+
=begin
|
219
|
+
# To be implemented by subclasses
|
220
|
+
|
221
|
+
# Resets the internal state to that of a newly created object.
|
222
|
+
def clear
|
223
|
+
raise NotImplementedError
|
224
|
+
end
|
225
|
+
|
226
|
+
# Reads the data for this data object from +io+.
|
227
|
+
def _do_read(io)
|
228
|
+
raise NotImplementedError
|
229
|
+
end
|
230
|
+
|
231
|
+
# To be called after calling #do_read.
|
232
|
+
def done_read
|
233
|
+
raise NotImplementedError
|
234
|
+
end
|
235
|
+
|
236
|
+
# Writes the value for this data to +io+.
|
237
|
+
def _write(io)
|
238
|
+
raise NotImplementedError
|
239
|
+
end
|
240
|
+
|
241
|
+
# Returns the number of bytes it will take to write this data.
|
242
|
+
def _num_bytes
|
243
|
+
raise NotImplementedError
|
244
|
+
end
|
245
|
+
|
246
|
+
# Returns a snapshot of this data object.
|
247
|
+
def snapshot
|
248
|
+
raise NotImplementedError
|
249
|
+
end
|
250
|
+
|
251
|
+
# Returns a list of the names of all fields accessible through this
|
252
|
+
# object.
|
253
|
+
def field_names
|
254
|
+
raise NotImplementedError
|
255
|
+
end
|
256
|
+
|
257
|
+
# To be implemented by subclasses
|
258
|
+
=end
|
259
|
+
end
|
260
|
+
end
|