IO::StreamSourceIOStreamGeneric

class Generic

Definitions

def read(size = nil)

Reads size bytes from the stream. If size is not specified, read until end of file.

Implementation

def read(size = nil)
	return String.new(encoding: Encoding::BINARY) if size == 0
	
	if size
		until @eof 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 @block_size to avoid lots of small reads:
			fill_read_buffer(read_size > @block_size ? read_size : @block_size)
		end
	else
		until @eof
			fill_read_buffer
		end
	end
	
	return consume_read_buffer(size)
end

def read_partial(size = nil)

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

Implementation

def read_partial(size = nil)
	return String.new(encoding: Encoding::BINARY) if size == 0

	if !@eof and @read_buffer.empty?
		fill_read_buffer
	end
	
	return consume_read_buffer(size)
end

def readpartial(size = nil)

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

Implementation

def readpartial(size = nil)
	read_partial(size) or raise EOFError, "Encountered eof 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).

returns String | Nil

The contents of the stream up until the pattern, which is consumed but not returned.

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 flush

Flushes buffered data to the stream.

Implementation

def flush
	return if @write_buffer.empty?
	
	@writing.synchronize do
		self.drain(@write_buffer)
	end
end

def write(string, flush: false)

Writes string to the buffer. When the buffer is full or #sync is true the buffer is flushed to the underlying io.

Signature

parameter string String

the string to write to the buffer.

returns Integer

the number of bytes appended to the buffer.

Implementation

def write(string, flush: false)
	@writing.synchronize do
		@write_buffer << string
		
		flush |= (@write_buffer.bytesize >= @block_size)
		
		if flush
			self.drain(@write_buffer)
		end
	end
	
	return string.bytesize
end

def <<(string)

Writes string to the stream and returns self.

Implementation

def <<(string)
	write(string)
	
	return self
end

def close

Best effort to flush any unwritten data, and then close the underling IO.

Implementation

def close
	return if closed?
	
	begin
		flush
	rescue
		# We really can't do anything here unless we want #close to raise exceptions.
	ensure
		self.sysclose
	end
end

def eof?

Determins if the stream has consumed all available data. May block if the stream is not readable. See IO::Stream::Generic#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 eof?
	if !@read_buffer.empty?
		return false
	elsif @eof
		return true
	else
		return !self.fill_read_buffer
	end
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 @eof
		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 sysread(size, buffer)

Reads data from the underlying stream as efficiently as possible.

Implementation

def sysread(size, buffer)
	raise NotImplementedError
end

def fill_read_buffer(size = @block_size)

Fills the buffer from the underlying stream.

Implementation

def fill_read_buffer(size = @block_size)
	# We impose a limit because the underlying `read` system call can fail if we request too much data in one go.
	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:
	@eof = true
	return false
end

def consume_read_buffer(size = 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.

Implementation

def consume_read_buffer(size = nil)
	# If we are at eof, and the read buffer is empty, we can't consume anything.
	return nil if @eof && @read_buffer.empty?
	
	result = nil
	
	if size.nil? or size >= @read_buffer.bytesize
		# Consume the entire read buffer:
		result = @read_buffer
		@read_buffer = StringBuffer.new
	else
		# This approach uses more memory.
		# result = @read_buffer.slice!(0, size)
		
		# 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
		
		result = @read_buffer.byteslice(0, size)
		@read_buffer = @read_buffer.byteslice(size, @read_buffer.bytesize)
	end
	
	return result
end