binaryparse 0.1.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/binaryparse.gemspec +18 -0
- data/doc/classes/BinaryBlocker/BitFieldEncoder.html +328 -0
- data/doc/classes/BinaryBlocker/Blocker.html +168 -0
- data/doc/classes/BinaryBlocker/CountedArrayEncoder.html +347 -0
- data/doc/classes/BinaryBlocker/Encoder.html +526 -0
- data/doc/classes/BinaryBlocker/FixedArrayEncoder.html +278 -0
- data/doc/classes/BinaryBlocker/FixedStringEncoder.html +177 -0
- data/doc/classes/BinaryBlocker/FixedUTF16StringEncoder.html +229 -0
- data/doc/classes/BinaryBlocker/GroupEncoder.html +650 -0
- data/doc/classes/BinaryBlocker/ListOfEncoder.html +343 -0
- data/doc/classes/BinaryBlocker/OneOfEncoder.html +306 -0
- data/doc/classes/BinaryBlocker/PackedDateEncoder.html +220 -0
- data/doc/classes/BinaryBlocker/PackedDateTimeEncoder.html +220 -0
- data/doc/classes/BinaryBlocker/PackedNumberEncoder.html +231 -0
- data/doc/classes/BinaryBlocker/SimpleEncoder.html +284 -0
- data/doc/classes/BinaryBlocker.html +329 -0
- data/doc/classes/BufferedIO.html +324 -0
- data/doc/classes/TestBlocker/BBDate.html +120 -0
- data/doc/classes/TestBlocker/BBList.html +120 -0
- data/doc/classes/TestBlocker/BBPacked.html +120 -0
- data/doc/classes/TestBlocker/BBString.html +120 -0
- data/doc/classes/TestBlocker/BBSub1.html +120 -0
- data/doc/classes/TestBlocker/BBSub2.html +120 -0
- data/doc/classes/TestBlocker/BBTest1.html +120 -0
- data/doc/classes/TestBlocker/BBTest2.html +120 -0
- data/doc/classes/TestBlocker/BBTest3.html +120 -0
- data/doc/classes/TestBlocker/BBTest4.html +120 -0
- data/doc/classes/TestBlocker/BBTest5.html +120 -0
- data/doc/classes/TestBlocker/BBTest6.html +120 -0
- data/doc/classes/TestBlocker/BBTest7.html +120 -0
- data/doc/classes/TestBlocker/BBTime.html +120 -0
- data/doc/classes/TestBlocker/BBUTF16.html +120 -0
- data/doc/classes/TestBlocker/ItemA.html +120 -0
- data/doc/classes/TestBlocker/ItemB.html +120 -0
- data/doc/classes/TestBlocker.html +802 -0
- data/doc/created.rid +1 -0
- data/doc/dot/f_0.dot +208 -0
- data/doc/dot/f_0.png +0 -0
- data/doc/dot/f_1.dot +23 -0
- data/doc/dot/f_1.png +0 -0
- data/doc/dot/f_2.dot +32 -0
- data/doc/dot/f_2.png +0 -0
- data/doc/dot/m_0_0.dot +208 -0
- data/doc/dot/m_0_0.png +0 -0
- data/doc/files/lib/blocker_rb.html +144 -0
- data/doc/files/lib/buffered_io_rb.html +115 -0
- data/doc/files/test/test_blocker_rb.html +116 -0
- data/doc/fr_class_index.html +60 -0
- data/doc/fr_file_index.html +29 -0
- data/doc/fr_method_index.html +135 -0
- data/doc/index.html +24 -0
- data/doc/rdoc-style.css +208 -0
- data/lib/blocker.rb +736 -0
- data/lib/buffered_io.rb +58 -0
- data/test/test_blocker.rb +412 -0
- metadata +111 -0
data/lib/blocker.rb
ADDED
@@ -0,0 +1,736 @@
|
|
1
|
+
# The purpose of BinaryBlocker is to parse semi-complicated binary files. It
|
2
|
+
# can handle all the various data types supported by Array#pack and
|
3
|
+
# String#unpack, but the real benefit is with more complicated layouts, that
|
4
|
+
# might include files with headers and a variable number of body records
|
5
|
+
# followed by a footer. It also supports, packed numeric
|
6
|
+
# (BinaryBlocker::PackedNumberEncoder) and date fields
|
7
|
+
# (BinaryBlocker::PackedDateEncoder)
|
8
|
+
|
9
|
+
require 'uconv'
|
10
|
+
require 'date'
|
11
|
+
require 'time'
|
12
|
+
|
13
|
+
module BinaryBlocker
|
14
|
+
class << self
|
15
|
+
def klasses
|
16
|
+
@klasses ||= {}
|
17
|
+
end
|
18
|
+
|
19
|
+
# To simplify naming of classes and laying out fields in your structures
|
20
|
+
# they can be registered:
|
21
|
+
#
|
22
|
+
# BinaryBlocker.register_klass(:string, FixedStringEncoder)
|
23
|
+
def register_klass(sym, klass)
|
24
|
+
@klasses ||= {}
|
25
|
+
@klasses[sym] = klass
|
26
|
+
end
|
27
|
+
|
28
|
+
# Handy helper method that returns the size of a given pack/unpack format
|
29
|
+
# string
|
30
|
+
def sizeof_format(format)
|
31
|
+
length = 0
|
32
|
+
format.scan(/(\S_?)\s*(\d*)/).each do |directive,count|
|
33
|
+
count = count.to_i
|
34
|
+
count = 1 if count == 0
|
35
|
+
|
36
|
+
length += case directive
|
37
|
+
when 'A', 'a', 'C', 'c', 'Z', 'x' : count
|
38
|
+
when 'B', 'b' : (count / 8.0).ceil
|
39
|
+
when 'D', 'd', 'E', 'G' : count * 8
|
40
|
+
when 'e', 'F', 'f', 'g' : count * 4
|
41
|
+
when 'H', 'h' : (count / 2.0).ceil
|
42
|
+
when 'I', 'i', 'L', 'l', 'N', 'V' : count * 4
|
43
|
+
when 'n', 'S', 's', 'v' : count * 2
|
44
|
+
when 'Q', 'q' : count * 8
|
45
|
+
when 'X' : count * -1
|
46
|
+
else raise ArgumentError.new("#{directive} is not supported in sizeof_format")
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
length
|
51
|
+
end
|
52
|
+
|
53
|
+
def pack_symbols
|
54
|
+
{
|
55
|
+
:int8 => 'c',
|
56
|
+
:uint8 => 'C',
|
57
|
+
:int16 => 's',
|
58
|
+
:uint16 => 'S',
|
59
|
+
:int32 => 'i',
|
60
|
+
:uint32 => 'I',
|
61
|
+
:int64 => 'q',
|
62
|
+
:uint64 => 'Q'
|
63
|
+
}
|
64
|
+
end
|
65
|
+
|
66
|
+
# As we have to process some fields to determine what type they are
|
67
|
+
# (that is we sometimes start and half to backup), this routine takes
|
68
|
+
# any io (that can be repositioned) and yields to a block -- if the
|
69
|
+
# block returns not _true_ it will reset the original position of the
|
70
|
+
# io stream. If you need to use BinaryBlocker with a non-repositioning
|
71
|
+
# stream (like a TCP/IP stream), see the handy BufferedIO class.
|
72
|
+
def with_guarded_io_pos(io)
|
73
|
+
pos = io.pos
|
74
|
+
status = yield io
|
75
|
+
ensure
|
76
|
+
io.pos = pos unless status
|
77
|
+
end
|
78
|
+
|
79
|
+
end
|
80
|
+
|
81
|
+
# This is the base class for all the various encoders. It supports a variety
|
82
|
+
# of options (as Symbols in a Hash):
|
83
|
+
#
|
84
|
+
# pre_block:: passed the value before being blocked
|
85
|
+
# post_block:: passed the blocked value before returned
|
86
|
+
# pre_deblock:: passed the io before attempting to deblock it (hard to imagine
|
87
|
+
# why you would need this but I was compelled by orthaganality)
|
88
|
+
# post_deblock:: passed the deblocked value before being stored internally
|
89
|
+
# get_filter:: more info
|
90
|
+
# set_filter:: all done
|
91
|
+
#
|
92
|
+
# It also supports either a string or io parameter which will be used to
|
93
|
+
# initialize the class
|
94
|
+
class Encoder
|
95
|
+
|
96
|
+
# Parameters: (io | buf, options_hash)
|
97
|
+
#
|
98
|
+
# Options (lambda):
|
99
|
+
#
|
100
|
+
# pre_block:: passed the value before being blocked
|
101
|
+
# post_block:: passed the blocked value before returned
|
102
|
+
# pre_deblock:: passed the io before attempting to deblock it (hard to imagine
|
103
|
+
# why you would need this but I was compelled by orthaganality)
|
104
|
+
# post_deblock:: passed the deblocked value before being stored internally
|
105
|
+
# get_filter:: more info
|
106
|
+
# set_filter:: all done
|
107
|
+
#
|
108
|
+
def initialize(*opts)
|
109
|
+
initialize_options(*opts)
|
110
|
+
initialize_data(*opts)
|
111
|
+
end
|
112
|
+
|
113
|
+
#
|
114
|
+
def block(io=nil)
|
115
|
+
val = if @pre_block
|
116
|
+
@pre_block.call(self.value)
|
117
|
+
else
|
118
|
+
self.value
|
119
|
+
end
|
120
|
+
result = internal_block(val)
|
121
|
+
if @post_block
|
122
|
+
result = @post_block.call(result)
|
123
|
+
end
|
124
|
+
io.write(result) if io
|
125
|
+
result
|
126
|
+
end
|
127
|
+
|
128
|
+
# This routine takes an io and will parse the stream
|
129
|
+
# on success it returns the object, on failure it
|
130
|
+
# returns a nil
|
131
|
+
def deblock(io)
|
132
|
+
with_guarded_io_pos(io) do
|
133
|
+
if @pre_deblock
|
134
|
+
# does this serve any real purpose? other
|
135
|
+
# than making me feel good and orthoginal
|
136
|
+
io = @pre_deblock.call(io)
|
137
|
+
end
|
138
|
+
self.value = internal_deblock(io)
|
139
|
+
if @post_deblock
|
140
|
+
self.value = @post_deblock.call(self.value)
|
141
|
+
end
|
142
|
+
self.value
|
143
|
+
end
|
144
|
+
end
|
145
|
+
|
146
|
+
protected
|
147
|
+
|
148
|
+
# Override valid? to allow check constraints on a particular
|
149
|
+
# record type, on failure
|
150
|
+
def valid?
|
151
|
+
true
|
152
|
+
end
|
153
|
+
|
154
|
+
def value
|
155
|
+
v = @value
|
156
|
+
if @get_filter
|
157
|
+
@get_filter.call(v)
|
158
|
+
else
|
159
|
+
v
|
160
|
+
end
|
161
|
+
end
|
162
|
+
|
163
|
+
def value=(val)
|
164
|
+
val = @set_filter.call(val) if @set_filter
|
165
|
+
@value = val
|
166
|
+
end
|
167
|
+
|
168
|
+
def initialize_options(*opts)
|
169
|
+
@opts ||= {}
|
170
|
+
opts = opts.find { |o| o.respond_to? :to_hash }
|
171
|
+
if opts
|
172
|
+
@opts = @opts.merge(opts)
|
173
|
+
@pre_block = @opts[:pre_block]
|
174
|
+
@pre_deblock = @opts[:pre_deblock]
|
175
|
+
|
176
|
+
@get_filter = @opts[:get_filter]
|
177
|
+
@set_filter = @opts[:set_filter]
|
178
|
+
|
179
|
+
@post_block = @opts[:post_block]
|
180
|
+
@post_deblock = @opts[:post_deblock]
|
181
|
+
end
|
182
|
+
end
|
183
|
+
|
184
|
+
def initialize_data(*opts)
|
185
|
+
data = opts.find { |o| !o.respond_to? :to_hash }
|
186
|
+
if data.respond_to? :to_str
|
187
|
+
raise "Unable to parse block" unless deblock(StringIO.new(data))
|
188
|
+
elsif data.respond_to? :read
|
189
|
+
raise "Unable to parse block" unless deblock(data)
|
190
|
+
end
|
191
|
+
end
|
192
|
+
|
193
|
+
def with_guarded_io_pos(io)
|
194
|
+
result = nil
|
195
|
+
BinaryBlocker.with_guarded_io_pos(io) do
|
196
|
+
result = yield io
|
197
|
+
valid?
|
198
|
+
end && result
|
199
|
+
end
|
200
|
+
|
201
|
+
def internal_block(value)
|
202
|
+
value
|
203
|
+
end
|
204
|
+
|
205
|
+
def internal_deblock(io)
|
206
|
+
''
|
207
|
+
end
|
208
|
+
end
|
209
|
+
|
210
|
+
# All Encoders that store multiple items subclass from
|
211
|
+
# here.
|
212
|
+
class GroupEncoder < Encoder
|
213
|
+
class << self
|
214
|
+
def inherited(obj)
|
215
|
+
obj.instance_eval do
|
216
|
+
@klasses = BinaryBlocker.klasses.clone
|
217
|
+
@attributes = []
|
218
|
+
@lookup = {}
|
219
|
+
end
|
220
|
+
super
|
221
|
+
end
|
222
|
+
|
223
|
+
# One and only one (this is the easiest :-)
|
224
|
+
def has_one(sym, klass, *opts)
|
225
|
+
klass = @klasses[klass] if @klasses[klass]
|
226
|
+
@lookup[sym] = @attributes.size
|
227
|
+
@attributes << lambda { klass.new(*opts) }
|
228
|
+
end
|
229
|
+
|
230
|
+
def include_klasses(klasses, *opts)
|
231
|
+
klasses = klasses.map do |k|
|
232
|
+
case
|
233
|
+
when @klasses[k] : lambda { @klasses[k].new(*opts) }
|
234
|
+
when k.respond_to?(:call) : k
|
235
|
+
when k.respond_to?(:new) : lambda { k.new(*opts) }
|
236
|
+
else raise "Unable to process class: #{k}"
|
237
|
+
end
|
238
|
+
end
|
239
|
+
end
|
240
|
+
|
241
|
+
def has_one_of(sym, klasses, *opts)
|
242
|
+
klasses = include_klasses(klasses, *opts)
|
243
|
+
@lookup[sym] = @attributes.size
|
244
|
+
@attributes << lambda { OneOfEncoder.new(klasses, *opts) }
|
245
|
+
end
|
246
|
+
|
247
|
+
def has_counted_array(sym, count_type, klasses, *opts)
|
248
|
+
klasses = include_klasses(klasses, *opts)
|
249
|
+
@lookup[sym] = @attributes.size
|
250
|
+
@attributes << lambda { CountedArrayEncoder.new(count_type, klasses, *opts) }
|
251
|
+
end
|
252
|
+
|
253
|
+
def has_fixed_array(sym, count, klasses, *opts)
|
254
|
+
klasses = include_klasses(klasses, *opts)
|
255
|
+
@lookup[sym] = @attributes.size
|
256
|
+
@attributes << lambda { FixedArrayEncoder.new(count, klasses, *opts) }
|
257
|
+
end
|
258
|
+
|
259
|
+
def has_bit_field(sym, type, bit_info, *opts)
|
260
|
+
@lookup[sym] = @attributes.size
|
261
|
+
@attributes << lambda { BitFieldEncoder.new(type, bit_info, *opts) }
|
262
|
+
end
|
263
|
+
|
264
|
+
def has_list_of(sym, klasses, *opts)
|
265
|
+
klasses = include_klasses(klasses, *opts)
|
266
|
+
@lookup[sym] = @attributes.size
|
267
|
+
@attributes << lambda { ListOfEncoder.new(klasses, *opts) }
|
268
|
+
end
|
269
|
+
|
270
|
+
def register_klass(sym, klass)
|
271
|
+
@klasses ||= {}
|
272
|
+
@klasses[sym] = klass
|
273
|
+
end
|
274
|
+
|
275
|
+
def clear_registered_klasses
|
276
|
+
@klasses = {}
|
277
|
+
end
|
278
|
+
|
279
|
+
def attributes
|
280
|
+
@attributes
|
281
|
+
end
|
282
|
+
|
283
|
+
def lookup
|
284
|
+
@lookup
|
285
|
+
end
|
286
|
+
end
|
287
|
+
|
288
|
+
def initialize(*opts)
|
289
|
+
@lookup = self.class.lookup.clone
|
290
|
+
@value = self.class.attributes.map { |a| a.call }
|
291
|
+
super
|
292
|
+
end
|
293
|
+
|
294
|
+
def block
|
295
|
+
@value.inject("") { |a,b| a + b.block }
|
296
|
+
end
|
297
|
+
|
298
|
+
def deblock(io)
|
299
|
+
BinaryBlocker.with_guarded_io_pos(io) do
|
300
|
+
@value.all? { |o| o.deblock(io) }
|
301
|
+
end
|
302
|
+
end
|
303
|
+
|
304
|
+
def method_missing(sym, *args)
|
305
|
+
super unless @lookup
|
306
|
+
if pos = @lookup[sym]
|
307
|
+
return @value[pos].value
|
308
|
+
else
|
309
|
+
sym = sym.to_s
|
310
|
+
if sym[-1] == ?=
|
311
|
+
if pos = @lookup[sym[0..-2].to_sym]
|
312
|
+
return @value[pos].value = args.first
|
313
|
+
end
|
314
|
+
end
|
315
|
+
end
|
316
|
+
super
|
317
|
+
end
|
318
|
+
|
319
|
+
def value
|
320
|
+
self
|
321
|
+
end
|
322
|
+
|
323
|
+
def value=(val)
|
324
|
+
raise "Now what?"
|
325
|
+
end
|
326
|
+
|
327
|
+
def valid?
|
328
|
+
@value.all? { |a| a.valid? }
|
329
|
+
end
|
330
|
+
end
|
331
|
+
|
332
|
+
class SimpleEncoder < Encoder
|
333
|
+
|
334
|
+
def self.register(sym, fmt, *opts)
|
335
|
+
klass = Class.new(SimpleEncoder)
|
336
|
+
klass.send(:define_method,:initialize) do |*opts|
|
337
|
+
initialize_options(*opts)
|
338
|
+
|
339
|
+
@format = fmt
|
340
|
+
@length = BinaryBlocker.sizeof_format(@format)
|
341
|
+
|
342
|
+
@key = @opts[:key]
|
343
|
+
@valid = @opts[:valid]
|
344
|
+
|
345
|
+
initialize_data(*opts)
|
346
|
+
end
|
347
|
+
BinaryBlocker.register_klass(sym, klass)
|
348
|
+
end
|
349
|
+
|
350
|
+
def internal_block(val)
|
351
|
+
[val].pack(@format)
|
352
|
+
end
|
353
|
+
|
354
|
+
def internal_deblock(io)
|
355
|
+
buffer = io.read(@length)
|
356
|
+
result = buffer.unpack(@format)
|
357
|
+
result.first
|
358
|
+
end
|
359
|
+
|
360
|
+
def valid?
|
361
|
+
if @valid
|
362
|
+
@valid.call(self.value)
|
363
|
+
else
|
364
|
+
self.value != nil && (@key == nil || @key === self.value)
|
365
|
+
end
|
366
|
+
end
|
367
|
+
|
368
|
+
def inspect
|
369
|
+
"SE: #{self.class} - #{@format} - #{@length} - #{self.value.inspect}"
|
370
|
+
end
|
371
|
+
end
|
372
|
+
|
373
|
+
SimpleEncoder.register( :int8, 'c')
|
374
|
+
SimpleEncoder.register(:uint8, 'C')
|
375
|
+
SimpleEncoder.register( :int16, 's')
|
376
|
+
SimpleEncoder.register(:uint16, 'S')
|
377
|
+
SimpleEncoder.register( :int32, 'i')
|
378
|
+
SimpleEncoder.register(:uint32, 'I')
|
379
|
+
SimpleEncoder.register( :int64, 'q')
|
380
|
+
SimpleEncoder.register(:uint64, 'Q')
|
381
|
+
|
382
|
+
class FixedStringEncoder < SimpleEncoder
|
383
|
+
def initialize(*opts)
|
384
|
+
initialize_options(*opts)
|
385
|
+
|
386
|
+
@length = @opts[:length].to_i
|
387
|
+
raise ArgumentError.new("Missing or invalid string length") unless @length > 0
|
388
|
+
@format = "Z#{@length}"
|
389
|
+
|
390
|
+
@key = @opts[:key]
|
391
|
+
@valid = @opts[:valid]
|
392
|
+
|
393
|
+
initialize_data(*opts)
|
394
|
+
end
|
395
|
+
end
|
396
|
+
BinaryBlocker.register_klass(:string, FixedStringEncoder)
|
397
|
+
|
398
|
+
class FixedUTF16StringEncoder < SimpleEncoder
|
399
|
+
def initialize(*opts)
|
400
|
+
initialize_options(*opts)
|
401
|
+
|
402
|
+
@length = @opts[:length].to_i
|
403
|
+
@length *= 2
|
404
|
+
raise ArgumentError.new("Missing or invalid string length") unless @length > 0
|
405
|
+
@format = "Z#{@length}"
|
406
|
+
|
407
|
+
@key = @opts[:key]
|
408
|
+
@valid = @opts[:valid]
|
409
|
+
|
410
|
+
initialize_data(*opts)
|
411
|
+
end
|
412
|
+
|
413
|
+
def internal_block(val)
|
414
|
+
[Uconv.u8tou16(val || "")].pack(@format)
|
415
|
+
end
|
416
|
+
|
417
|
+
def internal_deblock(io)
|
418
|
+
buffer = io.read(@length)
|
419
|
+
Uconv.u16tou8(buffer).sub(/\000+$/,'')
|
420
|
+
end
|
421
|
+
end
|
422
|
+
BinaryBlocker.register_klass(:utf16_string, FixedUTF16StringEncoder)
|
423
|
+
BinaryBlocker.register_klass(:utf16, FixedUTF16StringEncoder)
|
424
|
+
|
425
|
+
class PackedNumberEncoder < SimpleEncoder
|
426
|
+
def initialize(*opts)
|
427
|
+
initialize_options(*opts)
|
428
|
+
|
429
|
+
@length = @opts[:length].to_i
|
430
|
+
raise ArgumentError.new("Missing or invalid string length") unless @length > 0
|
431
|
+
@length += 1 if @length[0] == 1
|
432
|
+
@bytes = @length / 2
|
433
|
+
@format = "H#{@length}"
|
434
|
+
|
435
|
+
@key = @opts[:key]
|
436
|
+
@valid = @opts[:valid]
|
437
|
+
|
438
|
+
initialize_data(*opts)
|
439
|
+
end
|
440
|
+
|
441
|
+
def internal_block(val)
|
442
|
+
["%0#{@length}d" % val].pack(@format)
|
443
|
+
end
|
444
|
+
|
445
|
+
def internal_deblock(io)
|
446
|
+
buffer = io.read(@bytes)
|
447
|
+
result = buffer.unpack(@format)
|
448
|
+
result.first.to_i
|
449
|
+
end
|
450
|
+
end
|
451
|
+
BinaryBlocker.register_klass(:packed, PackedNumberEncoder)
|
452
|
+
|
453
|
+
class PackedDateEncoder < PackedNumberEncoder
|
454
|
+
def initialize_options(*opts)
|
455
|
+
super
|
456
|
+
@opts[:length] = 8
|
457
|
+
end
|
458
|
+
|
459
|
+
def internal_block(val)
|
460
|
+
super val.year * 10000 + val.month * 100 + val.mday
|
461
|
+
end
|
462
|
+
|
463
|
+
def internal_deblock(io)
|
464
|
+
buffer = io.read(@bytes)
|
465
|
+
result = buffer.unpack(@format)
|
466
|
+
year, month, day = result.first.unpack("A4A2A2").map { |v| v.to_i }
|
467
|
+
Date.civil(year, month, day)
|
468
|
+
end
|
469
|
+
|
470
|
+
end
|
471
|
+
BinaryBlocker.register_klass(:date, PackedDateEncoder)
|
472
|
+
|
473
|
+
class PackedDateTimeEncoder < PackedNumberEncoder
|
474
|
+
def initialize_options(*opts)
|
475
|
+
super
|
476
|
+
@opts[:length] = 14
|
477
|
+
end
|
478
|
+
|
479
|
+
def internal_block(val)
|
480
|
+
super sprintf("%04d%02d%02d%02d%02d%02d", val.year, val.month, val.mday, val.hour, val.min, val.sec).to_i
|
481
|
+
end
|
482
|
+
|
483
|
+
def internal_deblock(io)
|
484
|
+
buffer = io.read(@bytes)
|
485
|
+
result = buffer.unpack(@format)
|
486
|
+
year, month, day, hour, min, sec = result.first.unpack("A4A2A2A2A2A2").map { |v| v.to_i }
|
487
|
+
Time.local(year, month, day, hour, min, sec)
|
488
|
+
end
|
489
|
+
|
490
|
+
end
|
491
|
+
BinaryBlocker.register_klass(:time, PackedDateTimeEncoder)
|
492
|
+
BinaryBlocker.register_klass(:datetime, PackedDateTimeEncoder)
|
493
|
+
|
494
|
+
class OneOfEncoder < Encoder
|
495
|
+
|
496
|
+
def inspect
|
497
|
+
"OneOf #{@classes.join(',')} -> #{@obj.class} -> #{@obj.inspect}"
|
498
|
+
end
|
499
|
+
|
500
|
+
def initialize(classes, *opts)
|
501
|
+
@classes = classes.map { |o| o.call(*opts) }
|
502
|
+
@obj = nil
|
503
|
+
super(*opts)
|
504
|
+
end
|
505
|
+
|
506
|
+
def internal_block(val)
|
507
|
+
val.block if val
|
508
|
+
end
|
509
|
+
|
510
|
+
def internal_deblock(io)
|
511
|
+
@obj = nil
|
512
|
+
with_guarded_io_pos(io) do
|
513
|
+
@classes.each do |obj|
|
514
|
+
if obj.deblock(io)
|
515
|
+
@obj = obj
|
516
|
+
break
|
517
|
+
end
|
518
|
+
false
|
519
|
+
end
|
520
|
+
end
|
521
|
+
@obj
|
522
|
+
end
|
523
|
+
|
524
|
+
def method_missing(sym, *args)
|
525
|
+
if @obj
|
526
|
+
@obj.send sym, *args
|
527
|
+
else
|
528
|
+
super
|
529
|
+
end
|
530
|
+
end
|
531
|
+
|
532
|
+
def valid?
|
533
|
+
@obj && @obj.valid?
|
534
|
+
end
|
535
|
+
end
|
536
|
+
|
537
|
+
class FixedArrayEncoder < GroupEncoder
|
538
|
+
def initialize(count, classes, *opts)
|
539
|
+
initialize_options(*opts)
|
540
|
+
@count = count
|
541
|
+
@classes = classes
|
542
|
+
@value = Array.new(count) { OneOfEncoder.new(classes, *opts) }
|
543
|
+
initialize_data(*opts)
|
544
|
+
end
|
545
|
+
|
546
|
+
def internal_block(val)
|
547
|
+
val.inject("") { |r, o| r + o.block }
|
548
|
+
end
|
549
|
+
|
550
|
+
def deblock(io)
|
551
|
+
result = []
|
552
|
+
with_guarded_io_pos(io) do
|
553
|
+
@count.times do
|
554
|
+
result << OneOfEncoder.new(@classes, io, @opts)
|
555
|
+
end
|
556
|
+
end
|
557
|
+
@value = result
|
558
|
+
end
|
559
|
+
|
560
|
+
def [](offset)
|
561
|
+
raise RangeError.new("Access (#{offset}) out of range (#{@count})") unless (0...@count) === offset
|
562
|
+
@value[offset]
|
563
|
+
end
|
564
|
+
|
565
|
+
def []=(offset,val)
|
566
|
+
raise RangeError.new("Access (#{offset}) out of range (#{@count})") unless (0...@count) === offset
|
567
|
+
@value[offset] = val
|
568
|
+
end
|
569
|
+
end
|
570
|
+
|
571
|
+
class ListOfEncoder < GroupEncoder
|
572
|
+
def initialize(classes, *opts)
|
573
|
+
initialize_options(*opts)
|
574
|
+
@count = 0
|
575
|
+
@classes = classes
|
576
|
+
@value = []
|
577
|
+
initialize_data(*opts)
|
578
|
+
end
|
579
|
+
|
580
|
+
def internal_block(val)
|
581
|
+
val.inject("") { |r, o| r + o.block }
|
582
|
+
end
|
583
|
+
|
584
|
+
def deblock(io)
|
585
|
+
result = []
|
586
|
+
with_guarded_io_pos(io) do
|
587
|
+
oe = OneOfEncoder.new(@classes, @opts)
|
588
|
+
while oe.deblock(io)
|
589
|
+
result << oe
|
590
|
+
oe = OneOfEncoder.new(@classes, @opts)
|
591
|
+
end
|
592
|
+
end
|
593
|
+
@value = result
|
594
|
+
end
|
595
|
+
|
596
|
+
def [](offset)
|
597
|
+
raise RangeError.new("Access (#{offset}) out of range (#{@value.size})") unless (0...@value.size) === offset
|
598
|
+
@value[offset]
|
599
|
+
end
|
600
|
+
|
601
|
+
def []=(offset,val)
|
602
|
+
raise RangeError.new("Access (#{offset}) out of range (#{@value.size})") unless (0...@value.size) === offset
|
603
|
+
@value[offset] = val
|
604
|
+
end
|
605
|
+
|
606
|
+
def <<(val)
|
607
|
+
@value << val
|
608
|
+
end
|
609
|
+
|
610
|
+
def size
|
611
|
+
@value.size
|
612
|
+
end
|
613
|
+
alias length size
|
614
|
+
end
|
615
|
+
|
616
|
+
class CountedArrayEncoder < GroupEncoder
|
617
|
+
def initialize(count_type, classes, *opts)
|
618
|
+
# this is dynamic now, but we init to zero for the range checking
|
619
|
+
@count_enc = BinaryBlocker.pack_symbols[count_type] || count_type
|
620
|
+
@count = 0
|
621
|
+
initialize_options(*opts)
|
622
|
+
@classes = classes
|
623
|
+
@value = []
|
624
|
+
initialize_data(*opts)
|
625
|
+
end
|
626
|
+
|
627
|
+
def internal_block(val)
|
628
|
+
buf = [val.size].pack(@count_enc)
|
629
|
+
val.inject(buf) { |r, o| r + o.block }
|
630
|
+
end
|
631
|
+
|
632
|
+
def deblock(io)
|
633
|
+
length = BinaryBlocker.sizeof_format(@count_enc)
|
634
|
+
result = []
|
635
|
+
with_guarded_io_pos(io) do
|
636
|
+
@count = io.read(length).unpack(@count_enc)
|
637
|
+
@count.times do
|
638
|
+
result << OneOfEncoder.new(@classes, io, @opts)
|
639
|
+
end
|
640
|
+
end
|
641
|
+
@value = result
|
642
|
+
end
|
643
|
+
|
644
|
+
def [](offset)
|
645
|
+
raise RangeError.new("Access (#{offset}) out of range (#{@count})") unless (0...@count) === offset
|
646
|
+
@value[offset]
|
647
|
+
end
|
648
|
+
|
649
|
+
def []=(offset,val)
|
650
|
+
raise RangeError.new("Access (#{offset}) out of range (#{@count})") unless (0...@count) === offset
|
651
|
+
@value[offset] = val
|
652
|
+
end
|
653
|
+
|
654
|
+
def <<(val)
|
655
|
+
@count += 1
|
656
|
+
@value << val
|
657
|
+
end
|
658
|
+
|
659
|
+
def size
|
660
|
+
@count
|
661
|
+
end
|
662
|
+
alias length size
|
663
|
+
end
|
664
|
+
|
665
|
+
class BitFieldEncoder < Encoder
|
666
|
+
def initialize(type, bit_info, *opts)
|
667
|
+
@type = BinaryBlocker.pack_symbols[type] || type
|
668
|
+
@length = BinaryBlocker.sizeof_format(@type)
|
669
|
+
@bit_info = {}
|
670
|
+
pos = 0
|
671
|
+
bit_info.each do |bi|
|
672
|
+
case bi
|
673
|
+
when Symbol
|
674
|
+
@bit_info[bi.to_sym] = [pos,1]
|
675
|
+
pos += 1
|
676
|
+
|
677
|
+
when Fixnum
|
678
|
+
pos += bi
|
679
|
+
|
680
|
+
when Array
|
681
|
+
@bit_info[bi.first.to_sym] = [pos, bi.last.to_i]
|
682
|
+
pos += bi.last.to_i
|
683
|
+
end
|
684
|
+
end
|
685
|
+
@value = 0
|
686
|
+
initialize_options(*opts)
|
687
|
+
initialize_data(*opts)
|
688
|
+
end
|
689
|
+
|
690
|
+
def internal_block(val)
|
691
|
+
[val.raw_value].pack(@type)
|
692
|
+
end
|
693
|
+
|
694
|
+
def internal_deblock(io)
|
695
|
+
buffer = io.read(@length)
|
696
|
+
result = buffer.unpack(@type)
|
697
|
+
result.first
|
698
|
+
end
|
699
|
+
|
700
|
+
def value
|
701
|
+
self
|
702
|
+
end
|
703
|
+
|
704
|
+
def raw_value
|
705
|
+
@value
|
706
|
+
end
|
707
|
+
|
708
|
+
def method_missing(sym, *args)
|
709
|
+
if (bi = @bit_info[sym])
|
710
|
+
v = @value >> bi.first
|
711
|
+
mask = (1 << bi.last) - 1
|
712
|
+
v = v & mask
|
713
|
+
else
|
714
|
+
sym = sym.to_s
|
715
|
+
if sym[-1] == ?=
|
716
|
+
if bi = @bit_info[sym[0..-2].to_sym]
|
717
|
+
@value &= ~(((1 << bi.last) - 1) << bi.first)
|
718
|
+
@value |= args.first.to_i << bi.first
|
719
|
+
return @value
|
720
|
+
end
|
721
|
+
end
|
722
|
+
raise NoMethodError.new("undefined method `#{sym}''")
|
723
|
+
# I was using super, but it was throwing a different error
|
724
|
+
# which seemed wrong, not sure why
|
725
|
+
end
|
726
|
+
end
|
727
|
+
|
728
|
+
end
|
729
|
+
|
730
|
+
class Blocker < GroupEncoder
|
731
|
+
def inspect
|
732
|
+
"Blocker: #{@value.inspect} (index: #{@lookup})"
|
733
|
+
end
|
734
|
+
end
|
735
|
+
|
736
|
+
end
|