IO::StreamSourceIOStreamReadable

module Readable

A module providing readable stream functionality.

You must implement the sysread method to read data from the underlying IO.

Definitions

def initialize(minimum_read_size: MINIMUM_READ_SIZE, maximum_read_size: MAXIMUM_READ_SIZE, block_size: nil, **, &block)

Initialize readable stream functionality.

Signature

parameter minimum_read_size Integer

The minimum size for read operations.

parameter maximum_read_size Integer

The maximum size for read operations.

parameter block_size Integer

Legacy parameter, use minimum_read_size instead.

Implementation

def initialize(minimum_read_size: MINIMUM_READ_SIZE, maximum_read_size: MAXIMUM_READ_SIZE, block_size: nil, **, &block)
	@done = 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 block_size

Legacy accessor for backwards compatibility

Signature

returns Integer

The minimum read size.

Implementation

def block_size
	@minimum_read_size
end

def block_size=(value)

Legacy setter for backwards compatibility

Signature

parameter value Integer

The minimum read size.

Implementation

def block_size=(value)
	@minimum_read_size = value
end

def read(size = nil, buffer = nil)

Read data from the stream.

Signature

parameter size Integer | Nil

The number of bytes to read. If nil, read until end of stream.

parameter buffer String | Nil

An optional buffer to fill with data instead of allocating a new string.

returns String

The data read from the stream, or the provided buffer filled with data.

Implementation

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 @done 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 @done
			fill_read_buffer
		end
	end
	
	return consume_read_buffer(size, buffer)
end

def read_partial(size = nil, buffer = nil)

Read at most size bytes from the stream. Will avoid reading from the underlying stream if possible.

Signature

parameter size Integer | Nil

The number of bytes to read. If nil, read all available data.

parameter buffer String | Nil

An optional buffer to fill with data instead of allocating a new string.

returns String

The data read from the stream, or the provided buffer filled with data.

Implementation

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 !@done and @read_buffer.empty?
		fill_read_buffer
	end
	
	return consume_read_buffer(size, buffer)
end

def read_exactly(size, buffer = nil, exception: EOFError)

Read exactly the specified number of bytes.

Signature

parameter size Integer

The number of bytes to read.

parameter exception Class

The exception to raise if not enough data is available.

returns String

The data read from the stream.

Implementation

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, "Encountered done while reading data!"
end

def readpartial(size = nil, buffer = nil)

This is a compatibility shim for existing code that uses readpartial.

Signature

parameter size Integer | Nil

The number of bytes to read.

parameter buffer String | Nil

An optional buffer to fill with data instead of allocating a new string.

returns String

The data read from the stream.

Implementation

def readpartial(size = nil, buffer = nil)
	read_partial(size, buffer) or raise EOFError, "Encountered done while reading data!"
end

def read_until(pattern, offset = 0, limit: nil, chomp: true)

Efficiently read data from the stream until encountering pattern.

Signature

parameter pattern String

The pattern to match.

parameter offset Integer

The offset to start searching from.

parameter limit Integer

The maximum number of bytes to read, including the pattern (even if chomped).

parameter chomp Boolean

Whether to remove the pattern from the returned data.

returns String | Nil

The contents of the stream up until the pattern, or nil if the pattern was not found.

Implementation

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 discard_until(pattern, offset = 0, limit: nil)

Efficiently discard data from the stream until encountering pattern.

Signature

parameter pattern String

The pattern to match.

parameter offset Integer

The offset to start searching from.

parameter limit Integer

The maximum number of bytes to read, including the pattern.

returns String | Nil

The contents of the stream up until the pattern, or nil if the pattern was not found.

Implementation

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 peek(size = nil)

Peek at data in the buffer without consuming it.

Signature

parameter size Integer | Nil

The number of bytes to peek at. If nil, peek at all available data.

returns String

The data in the buffer without consuming it.

Implementation

def peek(size = nil)
	if size
		until @done 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 @done
		fill_read_buffer
	end
	return @read_buffer
end

def gets(separator = $/, limit = nil, chomp: false)

Read a line from the stream, similar to IO#gets.

Signature

parameter separator String

The line separator to search for.

parameter limit Integer | Nil

The maximum number of bytes to read.

parameter chomp Boolean

Whether to remove the separator from the returned line.

returns String | Nil

The line read from the stream, or nil if at end of stream.

Implementation

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 done?

Determins if the stream has consumed all available data. May block if the stream is not readable. See IO::Stream::Readable#readable? for a non-blocking alternative.

Signature

returns Boolean

If the stream is at file which means there is no more data to be read.

Implementation

def done?
	if !@read_buffer.empty?
		return false
	elsif @done
		return true
	else
		return !self.fill_read_buffer
	end
end

def done!

Mark the stream as done and raise EOFError.

Implementation

def done!
	@read_buffer.clear
	@done = true
	
	raise EOFError
end

def readable?

Whether there is a chance that a read operation will succeed or not.

Signature

returns Boolean

If the stream is readable, i.e. a read operation has a chance of success.

Implementation

def readable?
	# If we are at the end of the file, we can't read any more data:
	if @done
		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 close_read

Close the read end of the stream.

Implementation

def close_read
end

def fill_read_buffer(size = @minimum_read_size)

Fills the buffer from the underlying stream.

Implementation

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:
	@done = true
	return false
end

def consume_read_buffer(size = nil, buffer = nil)

Consumes at most size bytes from the buffer.

Signature

parameter size Integer | Nil

The amount of data to consume. If nil, consume entire buffer.

parameter buffer String | Nil

An optional buffer to fill with data instead of allocating a new string.

returns String | Nil

The consumed data, or nil if no data available.

Implementation

def consume_read_buffer(size = nil, buffer = nil)
	# If we are at done, and the read buffer is empty, we can't consume anything.
	if @done && @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