Skip to content

Commit a6eb22e

Browse files
committed
Add support for buffer parameter.
1 parent d614747 commit a6eb22e

File tree

3 files changed

+339
-20
lines changed

3 files changed

+339
-20
lines changed

lib/io/stream/readable.rb

Lines changed: 59 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -58,9 +58,18 @@ def block_size=(value)
5858

5959
# Read data from the stream.
6060
# @parameter size [Integer | Nil] The number of bytes to read. If nil, read until end of stream.
61-
# @returns [String] The data read from the stream.
62-
def read(size = nil)
63-
return String.new(encoding: Encoding::BINARY) if size == 0
61+
# @parameter buffer [String | Nil] An optional buffer to fill with data instead of allocating a new string.
62+
# @returns [String] The data read from the stream, or the provided buffer filled with data.
63+
def read(size = nil, buffer = nil)
64+
if size == 0
65+
if buffer
66+
buffer.clear
67+
buffer.force_encoding(Encoding::BINARY)
68+
return buffer
69+
else
70+
return String.new(encoding: Encoding::BINARY)
71+
end
72+
end
6473

6574
if size
6675
until @done or @read_buffer.bytesize >= size
@@ -76,26 +85,37 @@ def read(size = nil)
7685
end
7786
end
7887

79-
return consume_read_buffer(size)
88+
return consume_read_buffer(size, buffer)
8089
end
8190

8291
# Read at most `size` bytes from the stream. Will avoid reading from the underlying stream if possible.
83-
def read_partial(size = nil)
84-
return String.new(encoding: Encoding::BINARY) if size == 0
92+
# @parameter size [Integer | Nil] The number of bytes to read. If nil, read all available data.
93+
# @parameter buffer [String | Nil] An optional buffer to fill with data instead of allocating a new string.
94+
# @returns [String] The data read from the stream, or the provided buffer filled with data.
95+
def read_partial(size = nil, buffer = nil)
96+
if size == 0
97+
if buffer
98+
buffer.clear
99+
buffer.force_encoding(Encoding::BINARY)
100+
return buffer
101+
else
102+
return String.new(encoding: Encoding::BINARY)
103+
end
104+
end
85105

86106
if !@done and @read_buffer.empty?
87107
fill_read_buffer
88108
end
89109

90-
return consume_read_buffer(size)
110+
return consume_read_buffer(size, buffer)
91111
end
92112

93113
# Read exactly the specified number of bytes.
94114
# @parameter size [Integer] The number of bytes to read.
95115
# @parameter exception [Class] The exception to raise if not enough data is available.
96116
# @returns [String] The data read from the stream.
97-
def read_exactly(size, exception: EOFError)
98-
if buffer = read(size)
117+
def read_exactly(size, buffer = nil, exception: EOFError)
118+
if buffer = read(size, buffer)
99119
if buffer.bytesize != size
100120
raise exception, "Could not read enough data!"
101121
end
@@ -107,8 +127,11 @@ def read_exactly(size, exception: EOFError)
107127
end
108128

109129
# This is a compatibility shim for existing code that uses `readpartial`.
110-
def readpartial(size = nil)
111-
read_partial(size) or raise EOFError, "Encountered done while reading data!"
130+
# @parameter size [Integer | Nil] The number of bytes to read.
131+
# @parameter buffer [String | Nil] An optional buffer to fill with data instead of allocating a new string.
132+
# @returns [String] The data read from the stream.
133+
def readpartial(size = nil, buffer = nil)
134+
read_partial(size, buffer) or raise EOFError, "Encountered done while reading data!"
112135
end
113136

114137
# Find the index of a pattern in the read buffer, reading more data if needed.
@@ -330,25 +353,43 @@ def fill_read_buffer(size = @minimum_read_size)
330353

331354
# Consumes at most `size` bytes from the buffer.
332355
# @parameter size [Integer | Nil] The amount of data to consume. If nil, consume entire buffer.
333-
def consume_read_buffer(size = nil)
356+
# @parameter buffer [String | Nil] An optional buffer to fill with data instead of allocating a new string.
357+
# @returns [String | Nil] The consumed data, or nil if no data available.
358+
def consume_read_buffer(size = nil, buffer = nil)
334359
# If we are at done, and the read buffer is empty, we can't consume anything.
335-
return nil if @done && @read_buffer.empty?
360+
if @done && @read_buffer.empty?
361+
# Clear the buffer even when returning nil
362+
if buffer
363+
buffer.clear
364+
buffer.force_encoding(Encoding::BINARY)
365+
end
366+
return nil
367+
end
336368

337369
result = nil
338370

339371
if size.nil? or size >= @read_buffer.bytesize
340372
# Consume the entire read buffer:
341-
result = @read_buffer
373+
if buffer
374+
buffer.clear
375+
buffer << @read_buffer
376+
result = buffer
377+
else
378+
result = @read_buffer
379+
end
342380
@read_buffer = StringBuffer.new
343381
else
344-
# This approach uses more memory.
345-
# result = @read_buffer.slice!(0, size)
346-
347382
# We know that we are not going to reuse the original buffer.
348383
# But byteslice will generate a hidden copy. So let's freeze it first:
349384
@read_buffer.freeze
350385

351-
result = @read_buffer.byteslice(0, size)
386+
if buffer
387+
# Use replace instead of clear + << for better performance
388+
buffer.replace(@read_buffer.byteslice(0, size))
389+
result = buffer
390+
else
391+
result = @read_buffer.byteslice(0, size)
392+
end
352393
@read_buffer = @read_buffer.byteslice(size, @read_buffer.bytesize)
353394
end
354395

releases.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
# Releases
22

3+
## Unreleased
4+
5+
- Add support for `buffer` parameter in `read`, `read_exactly`, and `read_partial` methods to allow reading into a provided buffer.
6+
37
## v0.8.0
48

59
- On Ruby v3.3+, use `IO#write` directly instead of `IO#write_nonblock`, for better performance.

0 commit comments

Comments
 (0)