File: bytebuffer.rb

package info (click to toggle)
ruby-nio4r 2.7.4-1
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid
  • size: 608 kB
  • sloc: ansic: 7,611; java: 700; ruby: 364; makefile: 7
file content (235 lines) | stat: -rw-r--r-- 7,020 bytes parent folder | download | duplicates (2)
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