1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235
|
# frozen_string_literal: true
# Released under the MIT License.
# Copyright, 2016, by Upekshe Jayasekera.
# Copyright, 2016-2017, by Tony Arcieri.
# Copyright, 2020, by Thomas Dziedzic.
# Copyright, 2023, by Samuel Williams.
module NIO
# Efficient byte buffers for performant I/O operations
class ByteBuffer
include Enumerable
attr_reader :position, :limit, :capacity
# Insufficient capacity in buffer
OverflowError = Class.new(IOError)
# Not enough data remaining in buffer
UnderflowError = Class.new(IOError)
# Mark has not been set
MarkUnsetError = Class.new(IOError)
# Create a new ByteBuffer, either with a specified capacity or populating
# it from a given string
#
# @param capacity [Integer] size of buffer in bytes
#
# @return [NIO::ByteBuffer]
def initialize(capacity)
raise TypeError, "no implicit conversion of #{capacity.class} to Integer" unless capacity.is_a?(Integer)
@capacity = capacity
clear
end
# Clear the buffer, resetting it to the default state
def clear
@buffer = ("\0" * @capacity).force_encoding(Encoding::BINARY)
@position = 0
@limit = @capacity
@mark = nil
self
end
# Set the position to the given value. New position must be less than limit.
# Preserves mark if it's less than the new position, otherwise clears it.
#
# @param new_position [Integer] position in the buffer
#
# @raise [ArgumentError] new position was invalid
def position=(new_position)
raise ArgumentError, "negative position given" if new_position < 0
raise ArgumentError, "specified position exceeds capacity" if new_position > @capacity
@mark = nil if @mark && @mark > new_position
@position = new_position
end
# Set the limit to the given value. New limit must be less than capacity.
# Preserves limit and mark if they're less than the new limit, otherwise
# sets position to the new limit and clears the mark.
#
# @param new_limit [Integer] position in the buffer
#
# @raise [ArgumentError] new limit was invalid
def limit=(new_limit)
raise ArgumentError, "negative limit given" if new_limit < 0
raise ArgumentError, "specified limit exceeds capacity" if new_limit > @capacity
@position = new_limit if @position > new_limit
@mark = nil if @mark && @mark > new_limit
@limit = new_limit
end
# Number of bytes remaining in the buffer before the limit
#
# @return [Integer] number of bytes remaining
def remaining
@limit - @position
end
# Does the ByteBuffer have any space remaining?
#
# @return [true, false]
def full?
remaining.zero?
end
# Obtain the requested number of bytes from the buffer, advancing the position.
# If no length is given, all remaining bytes are consumed.
#
# @raise [NIO::ByteBuffer::UnderflowError] not enough data remaining in buffer
#
# @return [String] bytes read from buffer
def get(length = remaining)
raise ArgumentError, "negative length given" if length < 0
raise UnderflowError, "not enough data in buffer" if length > @limit - @position
result = @buffer[@position...length]
@position += length
result
end
# Obtain the byte at a given index in the buffer as an Integer
#
# @raise [ArgumentError] index is invalid (either negative or larger than limit)
#
# @return [Integer] byte at the given index
def [](index)
raise ArgumentError, "negative index given" if index < 0
raise ArgumentError, "specified index exceeds limit" if index >= @limit
@buffer.bytes[index]
end
# Add a String to the buffer
#
# @param str [#to_str] data to add to the buffer
#
# @raise [TypeError] given a non-string type
# @raise [NIO::ByteBuffer::OverflowError] buffer is full
#
# @return [self]
def put(str)
raise TypeError, "expected String, got #{str.class}" unless str.respond_to?(:to_str)
str = str.to_str
raise OverflowError, "buffer is full" if str.length > @limit - @position
@buffer[@position...str.length] = str
@position += str.length
self
end
alias << put
# Perform a non-blocking read from the given IO object into the buffer
# Reads as much data as is immediately available and returns
#
# @param [IO] Ruby IO object to read from
#
# @return [Integer] number of bytes read (0 if none were available)
def read_from(io)
nbytes = @limit - @position
raise OverflowError, "buffer is full" if nbytes.zero?
bytes_read = IO.try_convert(io).read_nonblock(nbytes, exception: false)
return 0 if bytes_read == :wait_readable
self << bytes_read
bytes_read.length
end
# Perform a non-blocking write of the buffer's contents to the given I/O object
# Writes as much data as is immediately possible and returns
#
# @param [IO] Ruby IO object to write to
#
# @return [Integer] number of bytes written (0 if the write would block)
def write_to(io)
nbytes = @limit - @position
raise UnderflowError, "no data remaining in buffer" if nbytes.zero?
bytes_written = IO.try_convert(io).write_nonblock(@buffer[@position...@limit], exception: false)
return 0 if bytes_written == :wait_writable
@position += bytes_written
bytes_written
end
# Set the buffer's current position as the limit and set the position to 0
def flip
@limit = @position
@position = 0
@mark = nil
self
end
# Set the buffer's current position to 0, leaving the limit unchanged
def rewind
@position = 0
@mark = nil
self
end
# Mark a position to return to using the `#reset` method
def mark
@mark = @position
self
end
# Reset position to the previously marked location
#
# @raise [NIO::ByteBuffer::MarkUnsetError] mark has not been set (call `#mark` first)
def reset
raise MarkUnsetError, "mark has not been set" unless @mark
@position = @mark
self
end
# Move data between the position and limit to the beginning of the buffer
# Sets the position to the end of the moved data, and the limit to the capacity
def compact
@buffer[0...(@limit - @position)] = @buffer[@position...@limit]
@position = @limit - @position
@limit = capacity
self
end
# Iterate over the bytes in the buffer (as Integers)
#
# @return [self]
def each(&block)
@buffer[0...@limit].each_byte(&block)
end
# Inspect the state of the buffer
#
# @return [String] string describing the state of the buffer
def inspect
format(
"#<%s:0x%x @position=%d @limit=%d @capacity=%d>",
self.class,
object_id << 1,
@position,
@limit,
@capacity
)
end
end
end
|