192 lines
5.0 KiB
Ruby
192 lines
5.0 KiB
Ruby
module BinData
|
|
# A wrapper around an IO object. The wrapper provides a consistent
|
|
# interface for BinData objects to use when accessing the IO.
|
|
class IO
|
|
|
|
# Create a new IO wrapper around +io+. +io+ must support #read if used
|
|
# for reading, #write if used for writing, #pos if reading the current
|
|
# stream position and #seek if setting the current stream position. If
|
|
# +io+ is a string it will be automatically wrapped in an StringIO object.
|
|
#
|
|
# The IO can handle bitstreams in either big or little endian format.
|
|
#
|
|
# M byte1 L M byte2 L
|
|
# S 76543210 S S fedcba98 S
|
|
# B B B B
|
|
#
|
|
# In big endian format:
|
|
# readbits(6), readbits(5) #=> [765432, 10fed]
|
|
#
|
|
# In little endian format:
|
|
# readbits(6), readbits(5) #=> [543210, a9876]
|
|
#
|
|
def initialize(io)
|
|
raise ArgumentError, "io must not be a BinData::IO" if BinData::IO === io
|
|
|
|
# wrap strings in a StringIO
|
|
if io.respond_to?(:to_str)
|
|
io = StringIO.new(io)
|
|
end
|
|
|
|
@raw_io = io
|
|
|
|
# initial stream position if stream supports positioning
|
|
@initial_pos = io.respond_to?(:pos) ? io.pos : 0
|
|
|
|
# bits when reading
|
|
@rnbits = 0
|
|
@rval = 0
|
|
@rendian = nil
|
|
|
|
# bits when writing
|
|
@wnbits = 0
|
|
@wval = 0
|
|
@wendian = nil
|
|
end
|
|
|
|
# Access to the underlying raw io.
|
|
attr_reader :raw_io
|
|
|
|
# Returns the current offset of the io stream. The exact value of
|
|
# the offset when reading bitfields is not defined.
|
|
def offset
|
|
if @raw_io.respond_to?(:pos)
|
|
@raw_io.pos - @initial_pos
|
|
else
|
|
# stream does not support positioning
|
|
0
|
|
end
|
|
end
|
|
|
|
# Seek +n+ bytes from the current position in the io stream.
|
|
def seekbytes(n)
|
|
@raw_io.seek(n, ::IO::SEEK_CUR)
|
|
end
|
|
|
|
# Reads exactly +n+ bytes from +io+.
|
|
#
|
|
# If the data read is nil an EOFError is raised.
|
|
#
|
|
# If the data read is too short an IOError is raised.
|
|
def readbytes(n)
|
|
raise "Internal state error nbits = #{@rnbits}" if @rnbits > 8
|
|
@rnbits = 0
|
|
@rval = 0
|
|
|
|
str = @raw_io.read(n)
|
|
raise EOFError, "End of file reached" if str.nil?
|
|
raise IOError, "data truncated" if str.size < n
|
|
str
|
|
end
|
|
|
|
# Reads exactly +nbits+ bits from +io+. +endian+ specifies whether
|
|
# the bits are stored in :big or :little endian format.
|
|
def readbits(nbits, endian = :big)
|
|
if @rendian != endian
|
|
# don't mix bits of differing endian
|
|
@rnbits = 0
|
|
@rval = 0
|
|
@rendian = endian
|
|
end
|
|
|
|
while nbits > @rnbits
|
|
byte = @raw_io.read(1)
|
|
raise EOFError, "End of file reached" if byte.nil?
|
|
byte = byte.unpack('C').at(0) & 0xff
|
|
|
|
if endian == :big
|
|
@rval = (@rval << 8) | byte
|
|
else
|
|
@rval = @rval | (byte << @rnbits)
|
|
end
|
|
|
|
@rnbits += 8
|
|
end
|
|
|
|
if endian == :big
|
|
val = (@rval >> (@rnbits - nbits)) & ((1 << nbits) - 1)
|
|
@rnbits -= nbits
|
|
@rval &= ((1 << @rnbits) - 1)
|
|
else
|
|
val = @rval & ((1 << nbits) - 1)
|
|
@rnbits -= nbits
|
|
@rval >>= nbits
|
|
end
|
|
|
|
val
|
|
end
|
|
|
|
# Writes the given string of bytes to the io stream.
|
|
def writebytes(str)
|
|
flushbits
|
|
@raw_io.write(str)
|
|
end
|
|
|
|
# Reads +nbits+ bits from +val+ to the stream. +endian+ specifies whether
|
|
# the bits are to be stored in :big or :little endian format.
|
|
def writebits(val, nbits, endian = :big)
|
|
# clamp val to range
|
|
val = val & ((1 << nbits) - 1)
|
|
|
|
if @wendian != endian
|
|
# don't mix bits of differing endian
|
|
flushbits if @wnbits > 0
|
|
|
|
@wendian = endian
|
|
end
|
|
|
|
if endian == :big
|
|
while nbits > 0
|
|
bits_req = 8 - @wnbits
|
|
if nbits >= bits_req
|
|
msb_bits = (val >> (nbits - bits_req)) & ((1 << bits_req) - 1)
|
|
nbits -= bits_req
|
|
val &= (1 << nbits) - 1
|
|
|
|
@wval = (@wval << bits_req) | msb_bits
|
|
@raw_io.write(@wval.chr)
|
|
|
|
@wval = 0
|
|
@wnbits = 0
|
|
else
|
|
@wval = (@wval << nbits) | val
|
|
@wnbits += nbits
|
|
nbits = 0
|
|
end
|
|
end
|
|
else
|
|
while nbits > 0
|
|
bits_req = 8 - @wnbits
|
|
if nbits >= bits_req
|
|
lsb_bits = val & ((1 << bits_req) - 1)
|
|
nbits -= bits_req
|
|
val >>= bits_req
|
|
|
|
@wval |= (lsb_bits << @wnbits)
|
|
@raw_io.write(@wval.chr)
|
|
|
|
@wval = 0
|
|
@wnbits = 0
|
|
else
|
|
@wval |= (val << @wnbits)
|
|
@wnbits += nbits
|
|
nbits = 0
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
# To be called after all +writebits+ have been applied.
|
|
def flushbits
|
|
if @wnbits > 8
|
|
raise "Internal state error nbits = #{@wnbits}" if @wnbits > 8
|
|
elsif @wnbits > 0
|
|
writebits(0, 8 - @wnbits, @wendian)
|
|
else
|
|
# do nothing
|
|
end
|
|
end
|
|
alias_method :flush, :flushbits
|
|
|
|
end
|
|
end |