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/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
@@ -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
@@ -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
@@ -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