lib/bindata/io.rb in bindata-2.2.0 vs lib/bindata/io.rb in bindata-2.3.0

- old
+ new

@@ -2,10 +2,151 @@ 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 + + # Common operations for both Read and Write. + module Common + 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 + + @raw_io = io + @buffer_end_pos = nil + + extend seekable? ? SeekableStream : UnSeekableStream + stream_init + end + + #------------- + private + + def seekable? + @raw_io.pos + rescue NoMethodError, Errno::ESPIPE, Errno::EPIPE + nil + end + + def seek(n) + seek_raw(buffer_limited_n(n)) + end + + def buffer_limited_n(n) + if @buffer_end_pos + if n.nil? or n > 0 + max = @buffer_end_pos[1] - offset + n = max if n.nil? or n > max + else + min = @buffer_end_pos[0] - offset + n = min if n < min + end + end + + n + end + + def with_buffer_common(n, &block) + prev = @buffer_end_pos + if prev + avail = prev[1] - offset + n = avail if n > avail + end + @buffer_end_pos = [offset, offset + n] + begin + block.call(*@buffer_end_pos) + ensure + @buffer_end_pos = prev + end + end + + # Use #seek and #pos on seekable streams + module SeekableStream + # 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) + + bytes_remaining + end + + #----------- + private + + def stream_init + @initial_pos = @raw_io.pos + end + + def offset_raw + @raw_io.pos - @initial_pos + end + + def seek_raw(n) + @raw_io.seek(n, ::IO::SEEK_CUR) + end + + def read_raw(n) + @raw_io.read(n) + end + + def write_raw(data) + @raw_io.write(data) + end + end + + # Manually keep track of offset for unseekable streams. + module UnSeekableStream + def offset_raw + @offset + end + + # The number of bytes remaining in the input stream. + def num_bytes_remaining + raise IOError, "stream is unseekable" + end + + #----------- + private + + def stream_init + @offset = 0 + end + + def read_raw(n) + data = @raw_io.read(n) + @offset += data.size if data + data + end + + def write_raw(data) + @offset += data.size + @raw_io.write(data) + end + + def seek_raw(n) + raise IOError, "stream is unseekable" if n < 0 + + # NOTE: how do we seek on a writable stream? + + # 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 + # Creates a StringIO around +str+. def self.create_string_io(str = "") StringIO.new(str.dup.force_encoding(Encoding::BINARY)) end @@ -25,47 +166,36 @@ # # In little endian format: # readbits(6), readbits(5) #=> [543210, a9876] # class Read + include Common + def initialize(io) - raise ArgumentError, "io must not be a BinData::IO::Read" if BinData::IO::Read === io + super(io) - # wrap strings in a StringIO - if io.respond_to?(:to_str) - io = BinData::IO.create_string_io(io.to_str) - end - - @raw_io = io - # bits when reading @rnbits = 0 @rval = 0 @rendian = nil - - @buffer_end_pos = nil - - extend seekable? ? SeekableStream : UnSeekableStream end # 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 + with_buffer_common(n) do block.call read - ensure - @buffer_end_pos = prev end end + # Returns the current offset of the io stream. Offset will be rounded + # up when reading bitfields. + def offset + offset_raw + end + # Seek +n+ bytes from the current position in the io stream. def seekbytes(n) reset_read_bits seek(n) end @@ -114,33 +244,14 @@ end #--------------- private - def seekable? - @raw_io.pos - rescue NoMethodError, Errno::ESPIPE, Errno::EPIPE - nil - end - - def seek(n) - seek_raw(buffer_limited_n(n)) - end - def read(n = nil) read_raw(buffer_limited_n(n)) end - def buffer_limited_n(n) - if @buffer_end_pos - max = @buffer_end_pos - offset - n = max if n.nil? or n > max - end - - n - end - def read_big_endian_bits(nbits) while @rnbits < nbits accumulate_big_endian_bits end @@ -182,142 +293,56 @@ end def mask(nbits) (1 << nbits) - 1 end - - # Use #seek and #pos on seekable streams - module SeekableStream - # Returns the current offset of the io stream. Offset will be rounded - # up when reading bitfields. - def offset - raw_io.pos - @initial_pos - end - - # 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) - - bytes_remaining - end - - #----------- - 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 - - # Manually keep track of offset for unseekable streams. - module UnSeekableStream - # Returns the current offset of the io stream. Offset will be rounded - # up when reading bitfields. - def offset - @read_count ||= 0 - end - - # The number of bytes remaining in the input stream. - def num_bytes_remaining - raise IOError, "stream is unseekable" - end - - #----------- - private - - def read_raw(n) - @read_count ||= 0 - - data = @raw_io.read(n) - @read_count += data.size if data - data - end - - def seek_raw(n) - raise IOError, "stream is unseekable" if n < 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 # 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 + include Common def initialize(io) - if BinData::IO::Write === io - raise ArgumentError, "io must not be a BinData::IO::Write" - end + super(io) - # wrap strings in a StringIO - if io.respond_to?(:to_str) - io = BinData::IO.create_string_io(io.to_str) - end - - @raw_io = io - @wnbits = 0 @wval = 0 @wendian = nil - - @write_count = 0 - - @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 + with_buffer_common(n) do |buf_start, buf_end| block.call - write_raw("\0" * @bytes_remaining) - ensure - @bytes_remaining = prev + write("\0" * (buf_end - offset)) end end # Returns the current offset of the io stream. Offset will be rounded # up when writing bitfields. def offset - @write_count + (@wnbits > 0 ? 1 : 0) + offset_raw + (@wnbits > 0 ? 1 : 0) end + # Seek +n+ bytes from the current position in the io stream. + def seekbytes(n) + flushbits + seek(n) + end + # Writes the given string of bytes to the io stream. def writebytes(str) flushbits - write_raw(str) + 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) @@ -347,20 +372,29 @@ alias_method :flush, :flushbits #--------------- private + def write(data) + n = buffer_limited_n(data.size) + if n < data.size + data = data[0, n] + end + + write_raw(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_raw(@wval.chr) + write(@wval.chr) @wval = 0 @wnbits = 0 else @wval = (@wval << nbits) | val @@ -377,31 +411,19 @@ lsb_bits = val & mask(bits_req) nbits -= bits_req val >>= bits_req @wval = @wval | (lsb_bits << @wnbits) - write_raw(@wval.chr) + write(@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 - - @write_count += data.size - @raw_io.write(data) end def mask(nbits) (1 << nbits) - 1 end