lib/bindata/io.rb
require 'stringio'
module BinData
# A wrapper around an IO object. The wrapper provides a consistent
# interface for BinData objects to use when accessing the IO.
module IO
# Creates a StringIO around +str+.
def self.create_string_io(str = "")
bin_str = str.dup.force_encoding(Encoding::BINARY)
StringIO.new(bin_str).tap(&:binmode)
end
# Create a new IO Read wrapper around +io+. +io+ must provide #read,
# #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]
#
class Read
def initialize(io)
if self.class === io
raise ArgumentError, "io must not be a #{self.class}"
end
# wrap strings in a StringIO
if io.respond_to?(:to_str)
io = BinData::IO.create_string_io(io.to_str)
end
@io = RawIO.new(io)
# bits when reading
@rnbits = 0
@rval = 0
@rendian = nil
end
# Allow transforming data in the input stream.
# See +BinData::Buffer+ as an example.
#
# +io+ must be an instance of +Transform+.
#
# yields +self+ and +io+ to the given block
def transform(io)
reset_read_bits
saved = @io
@io = io.prepend_to_chain(@io)
yield(self, io)
io.after_read_transform
ensure
@io = saved
end
# The number of bytes remaining in the io steam.
def num_bytes_remaining
@io.num_bytes_remaining
end
# Seek +n+ bytes from the current position in the io stream.
def skipbytes(n)
reset_read_bits
@io.skip(n)
end
# Seek to an absolute offset within the io stream.
def seek_to_abs_offset(n)
reset_read_bits
@io.seek_abs(n)
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)
reset_read_bits
read(n)
end
# Reads all remaining bytes from the stream.
def read_all_bytes
reset_read_bits
read
end
# Reads exactly +nbits+ bits from the stream. +endian+ specifies whether
# the bits are stored in +:big+ or +:little+ endian format.
def readbits(nbits, endian)
if @rendian != endian
# don't mix bits of differing endian
reset_read_bits
@rendian = endian
end
if endian == :big
read_big_endian_bits(nbits)
else
read_little_endian_bits(nbits)
end
end
# Discards any read bits so the stream becomes aligned at the
# next byte boundary.
def reset_read_bits
@rnbits = 0
@rval = 0
end
#---------------
private
def read(n = nil)
str = @io.read(n)
if n
raise EOFError, "End of file reached" if str.nil?
raise IOError, "data truncated" if str.size < n
end
str
end
def read_big_endian_bits(nbits)
while @rnbits < nbits
accumulate_big_endian_bits
end
val = (@rval >> (@rnbits - nbits)) & mask(nbits)
@rnbits -= nbits
@rval &= mask(@rnbits)
val
end
def accumulate_big_endian_bits
byte = read(1).unpack1('C') & 0xff
@rval = (@rval << 8) | byte
@rnbits += 8
end
def read_little_endian_bits(nbits)
while @rnbits < nbits
accumulate_little_endian_bits
end
val = @rval & mask(nbits)
@rnbits -= nbits
@rval >>= nbits
val
end
def accumulate_little_endian_bits
byte = read(1).unpack1('C') & 0xff
@rval = @rval | (byte << @rnbits)
@rnbits += 8
end
def mask(nbits)
(1 << nbits) - 1
end
end
# Create a new IO Write wrapper around +io+. +io+ must provide #write.
# 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.
#
# See IO::Read for more information.
class Write
def initialize(io)
if self.class === io
raise ArgumentError, "io must not be a #{self.class}"
end
# wrap strings in a StringIO
if io.respond_to?(:to_str)
io = BinData::IO.create_string_io(io.to_str)
end
@io = RawIO.new(io)
@wnbits = 0
@wval = 0
@wendian = nil
end
# Allow transforming data in the output stream.
# See +BinData::Buffer+ as an example.
#
# +io+ must be an instance of +Transform+.
#
# yields +self+ and +io+ to the given block
def transform(io)
flushbits
saved = @io
@io = io.prepend_to_chain(@io)
yield(self, io)
io.after_write_transform
ensure
@io = saved
end
# Seek to an absolute offset within the io stream.
def seek_to_abs_offset(n)
raise IOError, "stream is unseekable" unless @io.seekable?
flushbits
@io.seek_abs(n)
end
# Writes the given string of bytes to the io stream.
def writebytes(str)
flushbits
write(str)
end
# Writes +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)
if @wendian != endian
# don't mix bits of differing endian
flushbits
@wendian = endian
end
clamped_val = val & mask(nbits)
if endian == :big
write_big_endian_bits(clamped_val, nbits)
else
write_little_endian_bits(clamped_val, nbits)
end
end
# To be called after all +writebits+ have been applied.
def flushbits
raise "Internal state error nbits = #{@wnbits}" if @wnbits >= 8
if @wnbits > 0
writebits(0, 8 - @wnbits, @wendian)
end
end
alias flush flushbits
#---------------
private
def write(data)
@io.write(data)
end
def write_big_endian_bits(val, nbits)
while nbits > 0
bits_req = 8 - @wnbits
if nbits >= bits_req
msb_bits = (val >> (nbits - bits_req)) & mask(bits_req)
nbits -= bits_req
val &= mask(nbits)
@wval = (@wval << bits_req) | msb_bits
write(@wval.chr)
@wval = 0
@wnbits = 0
else
@wval = (@wval << nbits) | val
@wnbits += nbits
nbits = 0
end
end
end
def write_little_endian_bits(val, nbits)
while nbits > 0
bits_req = 8 - @wnbits
if nbits >= bits_req
lsb_bits = val & mask(bits_req)
nbits -= bits_req
val >>= bits_req
@wval = @wval | (lsb_bits << @wnbits)
write(@wval.chr)
@wval = 0
@wnbits = 0
else
@wval = @wval | (val << @wnbits)
@wnbits += nbits
nbits = 0
end
end
end
def mask(nbits)
(1 << nbits) - 1
end
end
# API used to access the raw data stream.
class RawIO
def initialize(io)
@io = io
@pos = 0
if is_seekable?(io)
@initial_pos = io.pos
else
singleton_class.prepend(UnSeekableIO)
end
end
def is_seekable?(io)
io.pos
rescue NoMethodError, Errno::ESPIPE, Errno::EPIPE, Errno::EINVAL
nil
end
def seekable?
true
end
def num_bytes_remaining
start_mark = @io.pos
@io.seek(0, ::IO::SEEK_END)
end_mark = @io.pos
@io.seek(start_mark, ::IO::SEEK_SET)
end_mark - start_mark
end
def offset
@pos
end
def skip(n)
raise IOError, "can not skip backwards" if n.negative?
@io.seek(n, ::IO::SEEK_CUR)
@pos += n
end
def seek_abs(n)
@io.seek(n + @initial_pos, ::IO::SEEK_SET)
@pos = n
end
def read(n)
@io.read(n).tap { |data| @pos += (data&.size || 0) }
end
def write(data)
@io.write(data)
end
end
# An IO stream may be transformed before processing.
# e.g. encoding, compression, buffered.
#
# Multiple transforms can be chained together.
#
# To create a new transform layer, subclass +Transform+.
# Override the public methods +#read+ and +#write+ at a minimum.
# Additionally the hook, +#before_transform+, +#after_read_transform+
# and +#after_write_transform+ are available as well.
#
# IMPORTANT! If your transform changes the size of the underlying
# data stream (e.g. compression), then call
# +::transform_changes_stream_length!+ in your subclass.
class Transform
class << self
# Indicates that this transform changes the length of the
# underlying data. e.g. performs compression or error correction
def transform_changes_stream_length!
prepend(UnSeekableIO)
end
end
def initialize
@chain_io = nil
end
# Initialises this transform.
#
# Called before any IO operations.
def before_transform; end
# Flushes the input stream.
#
# Called after the final read operation.
def after_read_transform; end
# Flushes the output stream.
#
# Called after the final write operation.
def after_write_transform; end
# Prepends this transform to the given +chain+.
#
# Returns self (the new head of chain).
def prepend_to_chain(chain)
@chain_io = chain
before_transform
self
end
# Is the IO seekable?
def seekable?
@chain_io.seekable?
end
# How many bytes are available for reading?
def num_bytes_remaining
chain_num_bytes_remaining
end
# The current offset within the stream.
def offset
chain_offset
end
# Skips forward +n+ bytes in the input stream.
def skip(n)
chain_skip(n)
end
# Seeks to the given absolute position.
def seek_abs(n)
chain_seek_abs(n)
end
# Reads +n+ bytes from the stream.
def read(n)
chain_read(n)
end
# Writes +data+ to the stream.
def write(data)
chain_write(data)
end
#-------------
private
def create_empty_binary_string
"".force_encoding(Encoding::BINARY)
end
def chain_seekable?
@chain_io.seekable?
end
def chain_num_bytes_remaining
@chain_io.num_bytes_remaining
end
def chain_offset
@chain_io.offset
end
def chain_skip(n)
@chain_io.skip(n)
end
def chain_seek_abs(n)
@chain_io.seek_abs(n)
end
def chain_read(n)
@chain_io.read(n)
end
def chain_write(data)
@chain_io.write(data)
end
end
# A module to be prepended to +RawIO+ or +Transform+ when the data
# stream is not seekable. This is either due to underlying stream
# being unseekable or the transform changes the number of bytes.
module UnSeekableIO
def seekable?
false
end
def num_bytes_remaining
raise IOError, "stream is unseekable"
end
def skip(n)
raise IOError, "can not skip backwards" if n.negative?
# skip over data in 8k blocks
while n > 0
bytes_to_read = [n, 8192].min
read(bytes_to_read)
n -= bytes_to_read
end
end
def seek_abs(n)
skip(n - offset)
end
end
end
end