module IO::Stream::Readable
def block_size
Legacy accessor for backwards compatibility
def block_size @minimum_read_size end
def block_size=(value)
Legacy setter for backwards compatibility
def block_size=(value) @minimum_read_size = value end
def close_read
def close_read end
def consume_read_buffer(size = nil, buffer = nil)
@parameter buffer [String | Nil] An optional buffer to fill with data instead of allocating a new string.
@parameter size [Integer | Nil] The amount of data to consume. If nil, consume entire buffer.
Consumes at most `size` bytes from the buffer.
def consume_read_buffer(size = nil, buffer = nil) # If we are at finished, and the read buffer is empty, we can't consume anything. if @finished && @read_buffer.empty? # Clear the buffer even when returning nil if buffer buffer.clear buffer.force_encoding(Encoding::BINARY) end return nil end result = nil if size.nil? or size >= @read_buffer.bytesize # Consume the entire read buffer: if buffer buffer.clear buffer << @read_buffer result = buffer else result = @read_buffer end @read_buffer = StringBuffer.new else # We know that we are not going to reuse the original buffer. # But byteslice will generate a hidden copy. So let's freeze it first: @read_buffer.freeze if buffer # Use replace instead of clear + << for better performance buffer.replace(@read_buffer.byteslice(0, size)) result = buffer else result = @read_buffer.byteslice(0, size) end @read_buffer = @read_buffer.byteslice(size, @read_buffer.bytesize) end return result end
def discard_until(pattern, offset = 0, limit: nil)
@parameter limit [Integer] The maximum number of bytes to read, including the pattern.
@parameter offset [Integer] The offset to start searching from.
@parameter pattern [String] The pattern to match.
Efficiently discard data from the stream until encountering pattern.
def discard_until(pattern, offset = 0, limit: nil) if index = index_of(pattern, offset, limit, true) @read_buffer.freeze if limit and index >= limit @read_buffer = @read_buffer.byteslice(limit, @read_buffer.bytesize) return nil end matched = @read_buffer.byteslice(0, index+pattern.bytesize) @read_buffer = @read_buffer.byteslice(index+pattern.bytesize, @read_buffer.bytesize) return matched end end
def fill_read_buffer(size = @minimum_read_size)
def fill_read_buffer(size = @minimum_read_size) # Limit the read size to avoid exceeding SSIZE_MAX and to manage memory usage. # Very large reads can also hurt interactive performance by blocking for too long. if size > @maximum_read_size size = @maximum_read_size end # This effectively ties the input and output stream together. flush if @read_buffer.empty? if sysread(size, @read_buffer) # Console.info(self, name: "read") {@read_buffer.inspect} return true end else if chunk = sysread(size, @input_buffer) @read_buffer << chunk # Console.info(self, name: "read") {@read_buffer.inspect} return true end end # else for both cases above: @finished = true return false end
def finish!
def finish! @read_buffer.clear @finished = true raise EOFError end
def finished?
See {readable?} for a non-blocking alternative.
Determins if the stream has consumed all available data. May block if the stream is not readable.
def finished? if !@read_buffer.empty? return false elsif @finished return true else return !self.fill_read_buffer end end
def gets(separator = $/, limit = nil, chomp: false)
@parameter chomp [Boolean] Whether to remove the separator from the returned line.
@parameter limit [Integer | Nil] The maximum number of bytes to read.
@parameter separator [String] The line separator to search for.
Read a line from the stream, similar to IO#gets.
def gets(separator = $/, limit = nil, chomp: false) # Compatibility with IO#gets: if separator.is_a?(Integer) limit = separator separator = $/ end # We don't want to split in the middle of the separator, so we subtract the size of the separator from the start of the search: split_offset = separator.bytesize - 1 offset = 0 until index = @read_buffer.index(separator, offset) offset = @read_buffer.bytesize - split_offset offset = 0 if offset < 0 # If a limit was given, and the offset is beyond the limit, we should return up to the limit: if limit and offset >= limit # As we didn't find the separator, there is nothing to chomp either. return consume_read_buffer(limit) end # If we can't read any more data, we should return what we have: return consume_read_buffer unless fill_read_buffer end # If the index of the separator was beyond the limit: if limit and index >= limit # Return up to the limit: return consume_read_buffer(limit) end # Freeze the read buffer, as this enables us to use byteslice without generating a hidden copy: @read_buffer.freeze line = @read_buffer.byteslice(0, index+(chomp ? 0 : separator.bytesize)) @read_buffer = @read_buffer.byteslice(index+separator.bytesize, @read_buffer.bytesize) return line end
def index_of(pattern, offset, limit, discard = false)
@parameter limit [Integer | Nil] The maximum number of bytes to read while searching.
@parameter offset [Integer] The offset to start searching from.
@parameter pattern [String] The pattern to search for.
Find the index of a pattern in the read buffer, reading more data if needed.
def index_of(pattern, offset, limit, discard = false) n't want to split on the pattern, so we subtract the size of the pattern. ffset = pattern.bytesize - 1 ndex = @read_buffer.index(pattern, offset) = @read_buffer.bytesize - split_offset = 0 if offset < 0 it and offset >= limit n nil fill_read_buffer n nil card we are discarding, we should consume the read buffer up to the offset: me_read_buffer(offset) t = 0 index
def initialize(minimum_read_size: MINIMUM_READ_SIZE, maximum_read_size: MAXIMUM_READ_SIZE, block_size: nil, **, &block)
@parameter maximum_read_size [Integer] The maximum size for read operations.
@parameter minimum_read_size [Integer] The minimum size for read operations.
Initialize readable stream functionality.
def initialize(minimum_read_size: MINIMUM_READ_SIZE, maximum_read_size: MAXIMUM_READ_SIZE, block_size: nil, **, &block) @finished = false @read_buffer = StringBuffer.new # Used as destination buffer for underlying reads. @input_buffer = StringBuffer.new # Support legacy block_size parameter for backwards compatibility @minimum_read_size = block_size || minimum_read_size @maximum_read_size = maximum_read_size super(**, &block) if defined?(super) end
def peek(size = nil)
@parameter size [Integer | Nil] The number of bytes to peek at. If nil, peek at all available data.
Peek at data in the buffer without consuming it.
def peek(size = nil) if size until @finished or @read_buffer.bytesize >= size # Compute the amount of data we need to read from the underlying stream: read_size = size - @read_buffer.bytesize # Don't read less than @minimum_read_size to avoid lots of small reads: fill_read_buffer(read_size > @minimum_read_size ? read_size : @minimum_read_size) end return @read_buffer[..([size, @read_buffer.size].min - 1)] end until (block_given? && yield(@read_buffer)) or @finished fill_read_buffer end return @read_buffer end
def read(size = nil, buffer = nil)
@parameter buffer [String | Nil] An optional buffer to fill with data instead of allocating a new string.
@parameter size [Integer | Nil] The number of bytes to read. If nil, read until end of stream.
Read data from the stream.
def read(size = nil, buffer = nil) if size == 0 if buffer buffer.clear buffer.force_encoding(Encoding::BINARY) return buffer else return String.new(encoding: Encoding::BINARY) end end if size until @finished or @read_buffer.bytesize >= size # Compute the amount of data we need to read from the underlying stream: read_size = size - @read_buffer.bytesize # Don't read less than @minimum_read_size to avoid lots of small reads: fill_read_buffer(read_size > @minimum_read_size ? read_size : @minimum_read_size) end else until @finished fill_read_buffer end if buffer buffer.replace(@read_buffer) @read_buffer.clear else buffer = @read_buffer @read_buffer = StringBuffer.new end # Read without size always returns a non-nil value, even if it is an empty string. return buffer end return consume_read_buffer(size, buffer) end
def read_exactly(size, buffer = nil, exception: EOFError)
@parameter exception [Class] The exception to raise if not enough data is available.
@parameter size [Integer] The number of bytes to read.
Read exactly the specified number of bytes.
def read_exactly(size, buffer = nil, exception: EOFError) if buffer = read(size, buffer) if buffer.bytesize != size raise exception, "Could not read enough data!" end return buffer end raise exception, "Stream finished before reading enough data!" end
def read_partial(size = nil, buffer = nil)
@parameter buffer [String | Nil] An optional buffer to fill with data instead of allocating a new string.
@parameter size [Integer | Nil] The number of bytes to read. If nil, read all available data.
Read at most `size` bytes from the stream. Will avoid reading from the underlying stream if possible.
def read_partial(size = nil, buffer = nil) if size == 0 if buffer buffer.clear buffer.force_encoding(Encoding::BINARY) return buffer else return String.new(encoding: Encoding::BINARY) end end if !@finished and @read_buffer.empty? fill_read_buffer end return consume_read_buffer(size, buffer) end
def read_until(pattern, offset = 0, limit: nil, chomp: true)
@parameter chomp [Boolean] Whether to remove the pattern from the returned data.
@parameter limit [Integer] The maximum number of bytes to read, including the pattern (even if chomped).
@parameter offset [Integer] The offset to start searching from.
@parameter pattern [String] The pattern to match.
Efficiently read data from the stream until encountering pattern.
def read_until(pattern, offset = 0, limit: nil, chomp: true) if index = index_of(pattern, offset, limit) return nil if limit and index >= limit @read_buffer.freeze matched = @read_buffer.byteslice(0, index+(chomp ? 0 : pattern.bytesize)) @read_buffer = @read_buffer.byteslice(index+pattern.bytesize, @read_buffer.bytesize) return matched end end
def readable?
Whether there is a chance that a read operation will succeed or not.
def readable? # If we are at the end of the file, we can't read any more data: if @finished return false end # If the read buffer is not empty, we can read more data: if !@read_buffer.empty? return true end # If the underlying stream is readable, we can read more data: return !closed? end
def readpartial(size = nil, buffer = nil)
@parameter buffer [String | Nil] An optional buffer to fill with data instead of allocating a new string.
@parameter size [Integer | Nil] The number of bytes to read.
This is a compatibility shim for existing code that uses `readpartial`.
def readpartial(size = nil, buffer = nil) read_partial(size, buffer) or raise EOFError, "Stream finished before reading enough data!" end