lib/bindata/io.rb in bindata-2.4.15 vs lib/bindata/io.rb in bindata-2.5.0

- old
+ new

@@ -3,221 +3,14 @@ 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_points = nil - - extend seekable? ? SeekableStream : UnSeekableStream - stream_init - end - - #------------- - private - - def seekable? - @raw_io.pos - rescue NoMethodError, Errno::ESPIPE, Errno::EPIPE, Errno::EINVAL - nil - end - - def seek(n) - seek_raw(buffer_limited_n(n)) - end - - def buffer_limited_n(n) - if @buffer_end_points - if n.nil? || n > 0 - max = @buffer_end_points[1] - offset - n = max if n.nil? || n > max - else - min = @buffer_end_points[0] - offset - n = min if n < min - end - end - - n - end - - def with_buffer_common(n) - prev = @buffer_end_points - if prev - avail = prev[1] - offset - n = avail if n > avail - end - @buffer_end_points = [offset, offset + n] - begin - yield(*@buffer_end_points) - ensure - @buffer_end_points = 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 - start_mark = @raw_io.pos - @raw_io.seek(0, ::IO::SEEK_END) - end_mark = @raw_io.pos - - if @buffer_end_points - if @buffer_end_points[1] < end_mark - end_mark = @buffer_end_points[1] - end - end - - bytes_remaining = end_mark - start_mark - @raw_io.seek(start_mark, ::IO::SEEK_SET) - - bytes_remaining - end - - # All io calls in +block+ are rolled back after this - # method completes. - def with_readahead - mark = @raw_io.pos - begin - yield - ensure - @raw_io.seek(mark, ::IO::SEEK_SET) - end - 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 - - # All io calls in +block+ are rolled back after this - # method completes. - def with_readahead - mark = @offset - @read_data = "" - @in_readahead = true - - class << self - alias_method :read_raw_without_readahead, :read_raw - alias_method :read_raw, :read_raw_with_readahead - end - - begin - yield - ensure - @offset = mark - @in_readahead = false - end - 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 read_raw_with_readahead(n) - data = "" - - unless @read_data.empty? || @in_readahead - bytes_to_consume = [n, @read_data.length].min - data += @read_data.slice!(0, bytes_to_consume) - n -= bytes_to_consume - - if @read_data.empty? - class << self - alias_method :read_raw, :read_raw_without_readahead - end - end - end - - raw_data = @raw_io.read(n) - data += raw_data if raw_data - - if @in_readahead - @read_data += data - end - - @offset += data.size - - 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 = "") - s = StringIO.new(str.dup.force_encoding(Encoding::BINARY)) - s.binmode - s + 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 @@ -234,42 +27,62 @@ # # In little endian format: # readbits(6), readbits(5) #=> [543210, a9876] # class Read - include Common - def initialize(io) - super(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 - # 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) - with_buffer_common(n) do - yield - read - 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 - # Returns the current offset of the io stream. Offset will be rounded - # up when reading bitfields. - def offset - offset_raw + # 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 seekbytes(n) + def skipbytes(n) reset_read_bits - seek(n) + @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. @@ -309,11 +122,11 @@ #--------------- private def read(n = nil) - str = read_raw(buffer_limited_n(n)) + 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 @@ -330,11 +143,11 @@ val end def accumulate_big_endian_bits - byte = read(1).unpack('C').at(0) & 0xff + byte = read(1).unpack1('C') & 0xff @rval = (@rval << 8) | byte @rnbits += 8 end def read_little_endian_bits(nbits) @@ -348,11 +161,11 @@ val end def accumulate_little_endian_bits - byte = read(1).unpack('C').at(0) & 0xff + byte = read(1).unpack1('C') & 0xff @rval = @rval | (byte << @rnbits) @rnbits += 8 end def mask(nbits) @@ -366,40 +179,50 @@ # # 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) - super(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 - # 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) - with_buffer_common(n) do |_buf_start, buf_end| - yield - write("\0" * (buf_end - offset)) - end - 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 - # Returns the current offset of the io stream. Offset will be rounded - # up when writing bitfields. - def offset - offset_raw + (@wnbits > 0 ? 1 : 0) + saved = @io + @io = io.prepend_to_chain(@io) + yield(self, io) + io.after_write_transform + ensure + @io = saved end - # Seek +n+ bytes from the current position in the io stream. - def seekbytes(n) + # Seek to an absolute offset within the io stream. + def seek_to_abs_offset(n) + raise IOError, "stream is unseekable" unless @io.seekable? + flushbits - seek(n) + @io.seek_abs(n) end # Writes the given string of bytes to the io stream. def writebytes(str) flushbits @@ -436,16 +259,11 @@ #--------------- private def write(data) - n = buffer_limited_n(data.size) - if n < data.size - data = data[0, n] - end - - write_raw(data) + @io.write(data) end def write_big_endian_bits(val, nbits) while nbits > 0 bits_req = 8 - @wnbits @@ -488,9 +306,214 @@ 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