metasploit-framework/lib/bindata/io.rb

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