lib/bindata/io.rb in bindata-1.6.0 vs lib/bindata/io.rb in bindata-1.8.0

- old
+ new

@@ -1,27 +1,23 @@ 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. - class IO - - # The underlying IO is unseekable - class Unseekable < StandardError; end - + module IO # Creates a StringIO around +str+. def self.create_string_io(str = "") if str.respond_to?(:force_encoding) str = str.dup.force_encoding(Encoding::BINARY) end StringIO.new(str) end - # 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. + # 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 @@ -31,244 +27,379 @@ # 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 + class Read + def initialize(io) + raise ArgumentError, "io must not be a BinData::IO::Read" if BinData::IO::Read === io - # wrap strings in a StringIO - if io.respond_to?(:to_str) - io = BinData::IO.create_string_io(io.to_str) - end + # wrap strings in a StringIO + if io.respond_to?(:to_str) + io = BinData::IO.create_string_io(io.to_str) + end - @raw_io = io + @raw_io = io - # initial stream position if stream supports positioning - @initial_pos = current_position rescue 0 + # bits when reading + @rnbits = 0 + @rval = 0 + @rendian = nil - # bits when reading - @rnbits = 0 - @rval = 0 - @rendian = nil + @buffer_end_pos = nil - # bits when writing - @wnbits = 0 - @wval = 0 - @wendian = nil - end + extend seekable? ? SeekableStream : UnSeekableStream + end - # Access to the underlying raw io. - attr_reader :raw_io + # Sets a buffer of +n+ bytes on the io stream. Any reading or seeking + # calls inside the +block+ will be contained within this buffer. + def with_buffer(n, &block) + prev = @buffer_end_pos + if prev + avail = prev - offset + n = avail if n > avail + end + @buffer_end_pos = offset + n + begin + block.call + read + ensure + @buffer_end_pos = prev + end + end - # Returns the current offset of the io stream. The exact value of - # the offset when reading bitfields is not defined. - def offset - current_position - @initial_pos - rescue Unseekable - 0 - end + # Seek +n+ bytes from the current position in the io stream. + def seekbytes(n) + reset_read_bits + seek(n) + end - # The number of bytes remaining in the input stream. - def num_bytes_remaining - pos = current_position - @raw_io.seek(0, ::IO::SEEK_END) - bytes_remaining = current_position - pos - @raw_io.seek(pos, ::IO::SEEK_SET) + # 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 - bytes_remaining - rescue Unseekable - 0 - end + str = read(n) + raise EOFError, "End of file reached" if str.nil? + raise IOError, "data truncated" if str.size < n + str + end - # Seek +n+ bytes from the current position in the io stream. - def seekbytes(n) - reset_read_bits - @raw_io.seek(n, ::IO::SEEK_CUR) - rescue NoMethodError, Errno::ESPIPE, Errno::EPIPE - skipbytes(n) - end + # Reads all remaining bytes from the stream. + def read_all_bytes + reset_read_bits + read + 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 + # 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 - str = @raw_io.read(n) - raise EOFError, "End of file reached" if str.nil? - raise IOError, "data truncated" if str.size < n - str - end + if endian == :big + read_big_endian_bits(nbits) + else + read_little_endian_bits(nbits) + end + end - # Reads all remaining bytes from the stream. - def read_all_bytes - reset_read_bits - @raw_io.read - end + # Discards any read bits so the stream becomes aligned at the + # next byte boundary. + def reset_read_bits + @rnbits = 0 + @rval = 0 + 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 + #--------------- + private + + def seekable? + @raw_io.pos + rescue NoMethodError, Errno::ESPIPE, Errno::EPIPE + nil end - if endian == :big - read_big_endian_bits(nbits) - else - read_little_endian_bits(nbits) + def seek(n) + seek_raw(buffer_limited_n(n)) 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 + def read(n = nil) + read_raw(buffer_limited_n(n)) + end - # Writes the given string of bytes to the io stream. - def writebytes(str) - flushbits - @raw_io.write(str) - end + def buffer_limited_n(n) + if @buffer_end_pos + max = @buffer_end_pos - offset + n = max if n.nil? or n > max + 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 + n end - clamped_val = val & mask(nbits) + def read_big_endian_bits(nbits) + while @rnbits < nbits + accumulate_big_endian_bits + end - if endian == :big - write_big_endian_bits(clamped_val, nbits) - else - write_little_endian_bits(clamped_val, nbits) + val = (@rval >> (@rnbits - nbits)) & mask(nbits) + @rnbits -= nbits + @rval &= mask(@rnbits) + + val end - end - # To be called after all +writebits+ have been applied. - def flushbits - raise "Internal state error nbits = #{@wnbits}" if @wnbits >= 8 + def accumulate_big_endian_bits + byte = read(1) + raise EOFError, "End of file reached" if byte.nil? + byte = byte.unpack('C').at(0) & 0xff - if @wnbits > 0 - writebits(0, 8 - @wnbits, @wendian) + @rval = (@rval << 8) | byte + @rnbits += 8 end - end - alias_method :flush, :flushbits - #--------------- - private + def read_little_endian_bits(nbits) + while @rnbits < nbits + accumulate_little_endian_bits + end - def current_position - @raw_io.pos - rescue NoMethodError, Errno::ESPIPE - raise Unseekable - end + val = @rval & mask(nbits) + @rnbits -= nbits + @rval >>= nbits - def skipbytes(n) - # skip over data in 8k blocks - while n > 0 - bytes_to_read = [n, 8192].min - @raw_io.read(bytes_to_read) - n -= bytes_to_read + val end - end - def read_big_endian_bits(nbits) - while @rnbits < nbits - accumulate_big_endian_bits + def accumulate_little_endian_bits + byte = read(1) + raise EOFError, "End of file reached" if byte.nil? + byte = byte.unpack('C').at(0) & 0xff + + @rval = @rval | (byte << @rnbits) + @rnbits += 8 end - val = (@rval >> (@rnbits - nbits)) & mask(nbits) - @rnbits -= nbits - @rval &= mask(@rnbits) + def mask(nbits) + (1 << nbits) - 1 + end - val - end + # Use #seek and #pos on seekable streams + module SeekableStream + # Returns the current offset of the io stream. The exact value of + # the offset when reading bitfields is not defined. + def offset + raw_io.pos - @initial_pos + end - def accumulate_big_endian_bits - byte = @raw_io.read(1) - raise EOFError, "End of file reached" if byte.nil? - byte = byte.unpack('C').at(0) & 0xff + # The number of bytes remaining in the input stream. + def num_bytes_remaining + mark = raw_io.pos + raw_io.seek(0, ::IO::SEEK_END) + bytes_remaining = raw_io.pos - mark + raw_io.seek(mark, ::IO::SEEK_SET) - @rval = (@rval << 8) | byte - @rnbits += 8 - end + bytes_remaining + end - def read_little_endian_bits(nbits) - while @rnbits < nbits - accumulate_little_endian_bits + #----------- + private + + def read_raw(n) + raw_io.read(n) + end + + def seek_raw(n) + raw_io.seek(n, ::IO::SEEK_CUR) + end + + def raw_io + @initial_pos ||= @raw_io.pos + @raw_io + end end - val = @rval & mask(nbits) - @rnbits -= nbits - @rval >>= nbits + # Manually keep track of offset for unseekable streams. + module UnSeekableStream + # Returns the current offset of the io stream. The exact value of + # the offset when reading bitfields is not defined. + def offset + @read_count ||= 0 + end - val - end + # The number of bytes remaining in the input stream. + def num_bytes_remaining + raise IOError, "stream is unseekable" + end - def accumulate_little_endian_bits - byte = @raw_io.read(1) - raise EOFError, "End of file reached" if byte.nil? - byte = byte.unpack('C').at(0) & 0xff + #----------- + private - @rval = @rval | (byte << @rnbits) - @rnbits += 8 - end + def read_raw(n) + @read_count ||= 0 - 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) + data = @raw_io.read(n) + @read_count += data.size if data + data + end - @wval = (@wval << bits_req) | msb_bits - @raw_io.write(@wval.chr) + def seek_raw(n) + raise IOError, "stream is unseekable" if n < 0 - @wval = 0 - @wnbits = 0 - else - @wval = (@wval << nbits) | val - @wnbits += nbits - nbits = 0 + # skip over data in 8k blocks + while n > 0 + bytes_to_read = [n, 8192].min + read_raw(bytes_to_read) + n -= bytes_to_read + end 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 + # 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 BinData::IO::Write === io + raise ArgumentError, "io must not be a BinData::IO::Write" + end - @wval = @wval | (lsb_bits << @wnbits) - @raw_io.write(@wval.chr) + # wrap strings in a StringIO + if io.respond_to?(:to_str) + io = BinData::IO.create_string_io(io.to_str) + end - @wval = 0 - @wnbits = 0 + @raw_io = io + + @wnbits = 0 + @wval = 0 + @wendian = nil + + @bytes_remaining = nil + end + + # Sets a buffer of +n+ bytes on the io stream. Any writes inside the + # +block+ will be contained within this buffer. If less than +n+ bytes + # are written inside the block, the remainder will be padded with '\0' + # bytes. + def with_buffer(n, &block) + prev = @bytes_remaining + if prev + n = prev if n > prev + prev -= n + end + + @bytes_remaining = n + begin + block.call + write_raw("\0" * @bytes_remaining) + ensure + @bytes_remaining = prev + end + end + + # Writes the given string of bytes to the io stream. + def writebytes(str) + flushbits + write_raw(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 - @wval = @wval | (val << @wnbits) - @wnbits += nbits - nbits = 0 + write_little_endian_bits(clamped_val, nbits) end end - end - def mask(nbits) - (1 << nbits) - 1 + # 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_method :flush, :flushbits + + #--------------- + private + + 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_raw(@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_raw(@wval.chr) + + @wval = 0 + @wnbits = 0 + else + @wval = @wval | (val << @wnbits) + @wnbits += nbits + nbits = 0 + end + end + end + + def write_raw(data) + if @bytes_remaining + if data.size > @bytes_remaining + data = data[0, @bytes_remaining] + end + @bytes_remaining -= data.size + end + + @raw_io.write(data) + end + + def mask(nbits) + (1 << nbits) - 1 + end end end end