bindata 1.6.0 → 1.8.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/.gitignore +1 -0
- data/.travis.yml +5 -0
- data/ChangeLog.rdoc +9 -0
- data/README.md +8 -1
- data/doc/manual.md +27 -2
- data/examples/tcp_ip.rb +179 -0
- data/lib/bindata.rb +4 -2
- data/lib/bindata/base.rb +2 -2
- data/lib/bindata/buffer.rb +119 -0
- data/lib/bindata/dsl.rb +41 -0
- data/lib/bindata/io.rb +321 -190
- data/lib/bindata/primitive.rb +6 -0
- data/lib/bindata/record.rb +1 -37
- data/lib/bindata/registry.rb +27 -23
- data/lib/bindata/version.rb +1 -1
- data/lib/bindata/virtual.rb +25 -19
- data/test/buffer_test.rb +144 -0
- data/test/common.rb +1 -1
- data/test/io_test.rb +117 -51
- data/test/lazy_test.rb +12 -12
- data/test/primitive_test.rb +20 -0
- data/test/registry_test.rb +23 -0
- data/test/virtual_test.rb +38 -0
- metadata +9 -4
data/lib/bindata/io.rb
CHANGED
@@ -3,11 +3,7 @@ require 'stringio'
|
|
3
3
|
module BinData
|
4
4
|
# A wrapper around an IO object. The wrapper provides a consistent
|
5
5
|
# interface for BinData objects to use when accessing the IO.
|
6
|
-
|
7
|
-
|
8
|
-
# The underlying IO is unseekable
|
9
|
-
class Unseekable < StandardError; end
|
10
|
-
|
6
|
+
module IO
|
11
7
|
# Creates a StringIO around +str+.
|
12
8
|
def self.create_string_io(str = "")
|
13
9
|
if str.respond_to?(:force_encoding)
|
@@ -16,10 +12,10 @@ module BinData
|
|
16
12
|
StringIO.new(str)
|
17
13
|
end
|
18
14
|
|
19
|
-
# Create a new IO wrapper around +io+. +io+ must
|
20
|
-
#
|
21
|
-
# stream position
|
22
|
-
#
|
15
|
+
# Create a new IO Read wrapper around +io+. +io+ must provide #read,
|
16
|
+
# #pos if reading the current stream position and #seek if setting the
|
17
|
+
# current stream position. If +io+ is a string it will be automatically
|
18
|
+
# wrapped in an StringIO object.
|
23
19
|
#
|
24
20
|
# The IO can handle bitstreams in either big or little endian format.
|
25
21
|
#
|
@@ -33,242 +29,377 @@ module BinData
|
|
33
29
|
# In little endian format:
|
34
30
|
# readbits(6), readbits(5) #=> [543210, a9876]
|
35
31
|
#
|
36
|
-
|
37
|
-
|
32
|
+
class Read
|
33
|
+
def initialize(io)
|
34
|
+
raise ArgumentError, "io must not be a BinData::IO::Read" if BinData::IO::Read === io
|
38
35
|
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
36
|
+
# wrap strings in a StringIO
|
37
|
+
if io.respond_to?(:to_str)
|
38
|
+
io = BinData::IO.create_string_io(io.to_str)
|
39
|
+
end
|
43
40
|
|
44
|
-
|
41
|
+
@raw_io = io
|
45
42
|
|
46
|
-
|
47
|
-
|
43
|
+
# bits when reading
|
44
|
+
@rnbits = 0
|
45
|
+
@rval = 0
|
46
|
+
@rendian = nil
|
48
47
|
|
49
|
-
|
50
|
-
@rnbits = 0
|
51
|
-
@rval = 0
|
52
|
-
@rendian = nil
|
48
|
+
@buffer_end_pos = nil
|
53
49
|
|
54
|
-
|
55
|
-
|
56
|
-
@wval = 0
|
57
|
-
@wendian = nil
|
58
|
-
end
|
50
|
+
extend seekable? ? SeekableStream : UnSeekableStream
|
51
|
+
end
|
59
52
|
|
60
|
-
|
61
|
-
|
53
|
+
# Sets a buffer of +n+ bytes on the io stream. Any reading or seeking
|
54
|
+
# calls inside the +block+ will be contained within this buffer.
|
55
|
+
def with_buffer(n, &block)
|
56
|
+
prev = @buffer_end_pos
|
57
|
+
if prev
|
58
|
+
avail = prev - offset
|
59
|
+
n = avail if n > avail
|
60
|
+
end
|
61
|
+
@buffer_end_pos = offset + n
|
62
|
+
begin
|
63
|
+
block.call
|
64
|
+
read
|
65
|
+
ensure
|
66
|
+
@buffer_end_pos = prev
|
67
|
+
end
|
68
|
+
end
|
62
69
|
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
0
|
69
|
-
end
|
70
|
+
# Seek +n+ bytes from the current position in the io stream.
|
71
|
+
def seekbytes(n)
|
72
|
+
reset_read_bits
|
73
|
+
seek(n)
|
74
|
+
end
|
70
75
|
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
76
|
+
# Reads exactly +n+ bytes from +io+.
|
77
|
+
#
|
78
|
+
# If the data read is nil an EOFError is raised.
|
79
|
+
#
|
80
|
+
# If the data read is too short an IOError is raised.
|
81
|
+
def readbytes(n)
|
82
|
+
reset_read_bits
|
77
83
|
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
84
|
+
str = read(n)
|
85
|
+
raise EOFError, "End of file reached" if str.nil?
|
86
|
+
raise IOError, "data truncated" if str.size < n
|
87
|
+
str
|
88
|
+
end
|
82
89
|
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
skipbytes(n)
|
89
|
-
end
|
90
|
+
# Reads all remaining bytes from the stream.
|
91
|
+
def read_all_bytes
|
92
|
+
reset_read_bits
|
93
|
+
read
|
94
|
+
end
|
90
95
|
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
str = @raw_io.read(n)
|
100
|
-
raise EOFError, "End of file reached" if str.nil?
|
101
|
-
raise IOError, "data truncated" if str.size < n
|
102
|
-
str
|
103
|
-
end
|
96
|
+
# Reads exactly +nbits+ bits from the stream. +endian+ specifies whether
|
97
|
+
# the bits are stored in +:big+ or +:little+ endian format.
|
98
|
+
def readbits(nbits, endian)
|
99
|
+
if @rendian != endian
|
100
|
+
# don't mix bits of differing endian
|
101
|
+
reset_read_bits
|
102
|
+
@rendian = endian
|
103
|
+
end
|
104
104
|
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
105
|
+
if endian == :big
|
106
|
+
read_big_endian_bits(nbits)
|
107
|
+
else
|
108
|
+
read_little_endian_bits(nbits)
|
109
|
+
end
|
110
|
+
end
|
110
111
|
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
reset_read_bits
|
117
|
-
@rendian = endian
|
112
|
+
# Discards any read bits so the stream becomes aligned at the
|
113
|
+
# next byte boundary.
|
114
|
+
def reset_read_bits
|
115
|
+
@rnbits = 0
|
116
|
+
@rval = 0
|
118
117
|
end
|
119
118
|
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
119
|
+
#---------------
|
120
|
+
private
|
121
|
+
|
122
|
+
def seekable?
|
123
|
+
@raw_io.pos
|
124
|
+
rescue NoMethodError, Errno::ESPIPE, Errno::EPIPE
|
125
|
+
nil
|
124
126
|
end
|
125
|
-
end
|
126
127
|
|
127
|
-
|
128
|
-
|
129
|
-
|
130
|
-
@rnbits = 0
|
131
|
-
@rval = 0
|
132
|
-
end
|
128
|
+
def seek(n)
|
129
|
+
seek_raw(buffer_limited_n(n))
|
130
|
+
end
|
133
131
|
|
134
|
-
|
135
|
-
|
136
|
-
|
137
|
-
@raw_io.write(str)
|
138
|
-
end
|
132
|
+
def read(n = nil)
|
133
|
+
read_raw(buffer_limited_n(n))
|
134
|
+
end
|
139
135
|
|
140
|
-
|
141
|
-
|
142
|
-
|
143
|
-
|
144
|
-
|
145
|
-
|
146
|
-
|
136
|
+
def buffer_limited_n(n)
|
137
|
+
if @buffer_end_pos
|
138
|
+
max = @buffer_end_pos - offset
|
139
|
+
n = max if n.nil? or n > max
|
140
|
+
end
|
141
|
+
|
142
|
+
n
|
147
143
|
end
|
148
144
|
|
149
|
-
|
145
|
+
def read_big_endian_bits(nbits)
|
146
|
+
while @rnbits < nbits
|
147
|
+
accumulate_big_endian_bits
|
148
|
+
end
|
149
|
+
|
150
|
+
val = (@rval >> (@rnbits - nbits)) & mask(nbits)
|
151
|
+
@rnbits -= nbits
|
152
|
+
@rval &= mask(@rnbits)
|
150
153
|
|
151
|
-
|
152
|
-
write_big_endian_bits(clamped_val, nbits)
|
153
|
-
else
|
154
|
-
write_little_endian_bits(clamped_val, nbits)
|
154
|
+
val
|
155
155
|
end
|
156
|
-
end
|
157
156
|
|
158
|
-
|
159
|
-
|
160
|
-
|
157
|
+
def accumulate_big_endian_bits
|
158
|
+
byte = read(1)
|
159
|
+
raise EOFError, "End of file reached" if byte.nil?
|
160
|
+
byte = byte.unpack('C').at(0) & 0xff
|
161
161
|
|
162
|
-
|
163
|
-
|
162
|
+
@rval = (@rval << 8) | byte
|
163
|
+
@rnbits += 8
|
164
164
|
end
|
165
|
-
end
|
166
|
-
alias_method :flush, :flushbits
|
167
165
|
|
168
|
-
|
169
|
-
|
166
|
+
def read_little_endian_bits(nbits)
|
167
|
+
while @rnbits < nbits
|
168
|
+
accumulate_little_endian_bits
|
169
|
+
end
|
170
170
|
|
171
|
-
|
172
|
-
|
173
|
-
|
174
|
-
raise Unseekable
|
175
|
-
end
|
171
|
+
val = @rval & mask(nbits)
|
172
|
+
@rnbits -= nbits
|
173
|
+
@rval >>= nbits
|
176
174
|
|
177
|
-
|
178
|
-
# skip over data in 8k blocks
|
179
|
-
while n > 0
|
180
|
-
bytes_to_read = [n, 8192].min
|
181
|
-
@raw_io.read(bytes_to_read)
|
182
|
-
n -= bytes_to_read
|
175
|
+
val
|
183
176
|
end
|
184
|
-
end
|
185
177
|
|
186
|
-
|
187
|
-
|
188
|
-
|
178
|
+
def accumulate_little_endian_bits
|
179
|
+
byte = read(1)
|
180
|
+
raise EOFError, "End of file reached" if byte.nil?
|
181
|
+
byte = byte.unpack('C').at(0) & 0xff
|
182
|
+
|
183
|
+
@rval = @rval | (byte << @rnbits)
|
184
|
+
@rnbits += 8
|
189
185
|
end
|
190
186
|
|
191
|
-
|
192
|
-
|
193
|
-
|
187
|
+
def mask(nbits)
|
188
|
+
(1 << nbits) - 1
|
189
|
+
end
|
194
190
|
|
195
|
-
|
196
|
-
|
191
|
+
# Use #seek and #pos on seekable streams
|
192
|
+
module SeekableStream
|
193
|
+
# Returns the current offset of the io stream. The exact value of
|
194
|
+
# the offset when reading bitfields is not defined.
|
195
|
+
def offset
|
196
|
+
raw_io.pos - @initial_pos
|
197
|
+
end
|
197
198
|
|
198
|
-
|
199
|
-
|
200
|
-
|
201
|
-
|
199
|
+
# The number of bytes remaining in the input stream.
|
200
|
+
def num_bytes_remaining
|
201
|
+
mark = raw_io.pos
|
202
|
+
raw_io.seek(0, ::IO::SEEK_END)
|
203
|
+
bytes_remaining = raw_io.pos - mark
|
204
|
+
raw_io.seek(mark, ::IO::SEEK_SET)
|
202
205
|
|
203
|
-
|
204
|
-
|
205
|
-
|
206
|
+
bytes_remaining
|
207
|
+
end
|
208
|
+
|
209
|
+
#-----------
|
210
|
+
private
|
211
|
+
|
212
|
+
def read_raw(n)
|
213
|
+
raw_io.read(n)
|
214
|
+
end
|
215
|
+
|
216
|
+
def seek_raw(n)
|
217
|
+
raw_io.seek(n, ::IO::SEEK_CUR)
|
218
|
+
end
|
206
219
|
|
207
|
-
|
208
|
-
|
209
|
-
|
220
|
+
def raw_io
|
221
|
+
@initial_pos ||= @raw_io.pos
|
222
|
+
@raw_io
|
223
|
+
end
|
210
224
|
end
|
211
225
|
|
212
|
-
|
213
|
-
|
214
|
-
|
226
|
+
# Manually keep track of offset for unseekable streams.
|
227
|
+
module UnSeekableStream
|
228
|
+
# Returns the current offset of the io stream. The exact value of
|
229
|
+
# the offset when reading bitfields is not defined.
|
230
|
+
def offset
|
231
|
+
@read_count ||= 0
|
232
|
+
end
|
215
233
|
|
216
|
-
|
217
|
-
|
234
|
+
# The number of bytes remaining in the input stream.
|
235
|
+
def num_bytes_remaining
|
236
|
+
raise IOError, "stream is unseekable"
|
237
|
+
end
|
218
238
|
|
219
|
-
|
220
|
-
|
221
|
-
raise EOFError, "End of file reached" if byte.nil?
|
222
|
-
byte = byte.unpack('C').at(0) & 0xff
|
239
|
+
#-----------
|
240
|
+
private
|
223
241
|
|
224
|
-
|
225
|
-
|
226
|
-
end
|
242
|
+
def read_raw(n)
|
243
|
+
@read_count ||= 0
|
227
244
|
|
228
|
-
|
229
|
-
|
230
|
-
|
231
|
-
|
232
|
-
msb_bits = (val >> (nbits - bits_req)) & mask(bits_req)
|
233
|
-
nbits -= bits_req
|
234
|
-
val &= mask(nbits)
|
245
|
+
data = @raw_io.read(n)
|
246
|
+
@read_count += data.size if data
|
247
|
+
data
|
248
|
+
end
|
235
249
|
|
236
|
-
|
237
|
-
|
250
|
+
def seek_raw(n)
|
251
|
+
raise IOError, "stream is unseekable" if n < 0
|
238
252
|
|
239
|
-
|
240
|
-
|
241
|
-
|
242
|
-
|
243
|
-
|
244
|
-
|
253
|
+
# skip over data in 8k blocks
|
254
|
+
while n > 0
|
255
|
+
bytes_to_read = [n, 8192].min
|
256
|
+
read_raw(bytes_to_read)
|
257
|
+
n -= bytes_to_read
|
258
|
+
end
|
245
259
|
end
|
246
260
|
end
|
247
261
|
end
|
248
262
|
|
249
|
-
|
250
|
-
|
251
|
-
|
252
|
-
|
253
|
-
|
254
|
-
|
255
|
-
|
263
|
+
# Create a new IO Write wrapper around +io+. +io+ must provide #write.
|
264
|
+
# If +io+ is a string it will be automatically wrapped in an StringIO
|
265
|
+
# object.
|
266
|
+
#
|
267
|
+
# The IO can handle bitstreams in either big or little endian format.
|
268
|
+
#
|
269
|
+
# See IO::Read for more information.
|
270
|
+
class Write
|
271
|
+
def initialize(io)
|
272
|
+
if BinData::IO::Write === io
|
273
|
+
raise ArgumentError, "io must not be a BinData::IO::Write"
|
274
|
+
end
|
275
|
+
|
276
|
+
# wrap strings in a StringIO
|
277
|
+
if io.respond_to?(:to_str)
|
278
|
+
io = BinData::IO.create_string_io(io.to_str)
|
279
|
+
end
|
280
|
+
|
281
|
+
@raw_io = io
|
282
|
+
|
283
|
+
@wnbits = 0
|
284
|
+
@wval = 0
|
285
|
+
@wendian = nil
|
286
|
+
|
287
|
+
@bytes_remaining = nil
|
288
|
+
end
|
289
|
+
|
290
|
+
# Sets a buffer of +n+ bytes on the io stream. Any writes inside the
|
291
|
+
# +block+ will be contained within this buffer. If less than +n+ bytes
|
292
|
+
# are written inside the block, the remainder will be padded with '\0'
|
293
|
+
# bytes.
|
294
|
+
def with_buffer(n, &block)
|
295
|
+
prev = @bytes_remaining
|
296
|
+
if prev
|
297
|
+
n = prev if n > prev
|
298
|
+
prev -= n
|
299
|
+
end
|
300
|
+
|
301
|
+
@bytes_remaining = n
|
302
|
+
begin
|
303
|
+
block.call
|
304
|
+
write_raw("\0" * @bytes_remaining)
|
305
|
+
ensure
|
306
|
+
@bytes_remaining = prev
|
307
|
+
end
|
308
|
+
end
|
309
|
+
|
310
|
+
# Writes the given string of bytes to the io stream.
|
311
|
+
def writebytes(str)
|
312
|
+
flushbits
|
313
|
+
write_raw(str)
|
314
|
+
end
|
315
|
+
|
316
|
+
# Writes +nbits+ bits from +val+ to the stream. +endian+ specifies whether
|
317
|
+
# the bits are to be stored in +:big+ or +:little+ endian format.
|
318
|
+
def writebits(val, nbits, endian)
|
319
|
+
if @wendian != endian
|
320
|
+
# don't mix bits of differing endian
|
321
|
+
flushbits
|
322
|
+
@wendian = endian
|
323
|
+
end
|
256
324
|
|
257
|
-
|
258
|
-
@raw_io.write(@wval.chr)
|
325
|
+
clamped_val = val & mask(nbits)
|
259
326
|
|
260
|
-
|
261
|
-
|
327
|
+
if endian == :big
|
328
|
+
write_big_endian_bits(clamped_val, nbits)
|
262
329
|
else
|
263
|
-
|
264
|
-
@wnbits += nbits
|
265
|
-
nbits = 0
|
330
|
+
write_little_endian_bits(clamped_val, nbits)
|
266
331
|
end
|
267
332
|
end
|
268
|
-
end
|
269
333
|
|
270
|
-
|
271
|
-
|
334
|
+
# To be called after all +writebits+ have been applied.
|
335
|
+
def flushbits
|
336
|
+
raise "Internal state error nbits = #{@wnbits}" if @wnbits >= 8
|
337
|
+
|
338
|
+
if @wnbits > 0
|
339
|
+
writebits(0, 8 - @wnbits, @wendian)
|
340
|
+
end
|
341
|
+
end
|
342
|
+
alias_method :flush, :flushbits
|
343
|
+
|
344
|
+
#---------------
|
345
|
+
private
|
346
|
+
|
347
|
+
def write_big_endian_bits(val, nbits)
|
348
|
+
while nbits > 0
|
349
|
+
bits_req = 8 - @wnbits
|
350
|
+
if nbits >= bits_req
|
351
|
+
msb_bits = (val >> (nbits - bits_req)) & mask(bits_req)
|
352
|
+
nbits -= bits_req
|
353
|
+
val &= mask(nbits)
|
354
|
+
|
355
|
+
@wval = (@wval << bits_req) | msb_bits
|
356
|
+
write_raw(@wval.chr)
|
357
|
+
|
358
|
+
@wval = 0
|
359
|
+
@wnbits = 0
|
360
|
+
else
|
361
|
+
@wval = (@wval << nbits) | val
|
362
|
+
@wnbits += nbits
|
363
|
+
nbits = 0
|
364
|
+
end
|
365
|
+
end
|
366
|
+
end
|
367
|
+
|
368
|
+
def write_little_endian_bits(val, nbits)
|
369
|
+
while nbits > 0
|
370
|
+
bits_req = 8 - @wnbits
|
371
|
+
if nbits >= bits_req
|
372
|
+
lsb_bits = val & mask(bits_req)
|
373
|
+
nbits -= bits_req
|
374
|
+
val >>= bits_req
|
375
|
+
|
376
|
+
@wval = @wval | (lsb_bits << @wnbits)
|
377
|
+
write_raw(@wval.chr)
|
378
|
+
|
379
|
+
@wval = 0
|
380
|
+
@wnbits = 0
|
381
|
+
else
|
382
|
+
@wval = @wval | (val << @wnbits)
|
383
|
+
@wnbits += nbits
|
384
|
+
nbits = 0
|
385
|
+
end
|
386
|
+
end
|
387
|
+
end
|
388
|
+
|
389
|
+
def write_raw(data)
|
390
|
+
if @bytes_remaining
|
391
|
+
if data.size > @bytes_remaining
|
392
|
+
data = data[0, @bytes_remaining]
|
393
|
+
end
|
394
|
+
@bytes_remaining -= data.size
|
395
|
+
end
|
396
|
+
|
397
|
+
@raw_io.write(data)
|
398
|
+
end
|
399
|
+
|
400
|
+
def mask(nbits)
|
401
|
+
(1 << nbits) - 1
|
402
|
+
end
|
272
403
|
end
|
273
404
|
end
|
274
405
|
end
|