From d8c59a292b86a77d71fad708d758d1e340447b55 Mon Sep 17 00:00:00 2001 From: David Keller Date: Fri, 26 Dec 2025 19:55:16 +0100 Subject: [PATCH 1/4] feat: add #read_greedy method --- src/io.cr | 22 ++++++++++++++++++++++ src/io/memory.cr | 5 +++++ 2 files changed, 27 insertions(+) diff --git a/src/io.cr b/src/io.cr index 4b94d2f29baa..4193a823ea76 100644 --- a/src/io.cr +++ b/src/io.cr @@ -548,6 +548,28 @@ abstract class IO count end + # Similar to `#read`, but with the additional guarantee that either + # the buffer will be entirely filled or the EOF will be reached while trying. + # + # Calling this method may result in multiple read calls if necessary. + # + # ``` + # io = IO::Memory.new "123451234" + # slice = Bytes.new(5) + # io.read_greedy(slice) # => 5 + # slice # => Bytes[49, 50, 51, 52, 53] + # io.read_greedy(slice) # => 4 + # ``` + def read_greedy(slice : Bytes) : Int32 + count = slice.size + while slice.size > 0 + read_bytes = read(slice) + return slice_total_size &- slice.size if read_bytes == 0 + slice += read_bytes + end + count + end + # Reads the rest of this `IO` data as a `String`. # # ``` diff --git a/src/io/memory.cr b/src/io/memory.cr index 493db2de3cd7..9678c241fc47 100644 --- a/src/io/memory.cr +++ b/src/io/memory.cr @@ -175,6 +175,11 @@ class IO::Memory < IO end end + def read_greedy(slice : Bytes) : Int32 + # IO::Memory is always greedy + read(slice) + end + def peek : Bytes check_open From 4e9f608d1d460c5775c76a52435855103d95eefd Mon Sep 17 00:00:00 2001 From: David Keller Date: Mon, 5 Jan 2026 19:39:08 +0100 Subject: [PATCH 2/4] add specs, fix pr issues --- spec/std/io/io_spec.cr | 11 +++++++++++ src/io.cr | 2 +- src/io/memory.cr | 5 ----- 3 files changed, 12 insertions(+), 6 deletions(-) diff --git a/spec/std/io/io_spec.cr b/spec/std/io/io_spec.cr index abff1cef0a7e..dfe944e98000 100644 --- a/spec/std/io/io_spec.cr +++ b/spec/std/io/io_spec.cr @@ -486,6 +486,17 @@ describe IO do IO.same_content?(io1, io2).should be_false end end + + it "combines multiple reads using #read_greedy" do + bytes = Bytes.new 7 + + io = SimpleIOMemory.new("Hello World", max_read: 2) + io.read_greedy(bytes).should eq(7) + bytes.should eq("Hello W".to_slice) + io.read_greedy(bytes).should eq(4) + bytes[0, 4].should eq("orld".to_slice) + io.read_greedy(bytes).should eq(0) + end end describe "write operations" do diff --git a/src/io.cr b/src/io.cr index 4193a823ea76..b1a076eb5e5d 100644 --- a/src/io.cr +++ b/src/io.cr @@ -564,7 +564,7 @@ abstract class IO count = slice.size while slice.size > 0 read_bytes = read(slice) - return slice_total_size &- slice.size if read_bytes == 0 + return count &- slice.size if read_bytes == 0 slice += read_bytes end count diff --git a/src/io/memory.cr b/src/io/memory.cr index 9678c241fc47..493db2de3cd7 100644 --- a/src/io/memory.cr +++ b/src/io/memory.cr @@ -175,11 +175,6 @@ class IO::Memory < IO end end - def read_greedy(slice : Bytes) : Int32 - # IO::Memory is always greedy - read(slice) - end - def peek : Bytes check_open From 79649dd303a65713cb4e5a8304d9e696935a832e Mon Sep 17 00:00:00 2001 From: David Keller Date: Wed, 21 Jan 2026 20:09:32 +0100 Subject: [PATCH 3/4] Apply suggestions from code review MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Johannes Müller --- src/io.cr | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/io.cr b/src/io.cr index b1a076eb5e5d..ec8d3f6925f4 100644 --- a/src/io.cr +++ b/src/io.cr @@ -551,7 +551,7 @@ abstract class IO # Similar to `#read`, but with the additional guarantee that either # the buffer will be entirely filled or the EOF will be reached while trying. # - # Calling this method may result in multiple read calls if necessary. + # Calling this method may result in multiple calls to `#read` if necessary. # # ``` # io = IO::Memory.new "123451234" @@ -560,6 +560,8 @@ abstract class IO # slice # => Bytes[49, 50, 51, 52, 53] # io.read_greedy(slice) # => 4 # ``` + # + # `#read_fully` and `#read_fully?` also try to fill the entire buffer but error on unexpected EOF. def read_greedy(slice : Bytes) : Int32 count = slice.size while slice.size > 0 From 973bbae86cf8c1978afbe87193cefb05ab8ca2fe Mon Sep 17 00:00:00 2001 From: David Keller Date: Wed, 21 Jan 2026 20:18:21 +0100 Subject: [PATCH 4/4] docs: add read_greedy cross-references to read_fully(?) --- src/io.cr | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/src/io.cr b/src/io.cr index ec8d3f6925f4..ea13b4204b34 100644 --- a/src/io.cr +++ b/src/io.cr @@ -523,6 +523,9 @@ abstract class IO # slice # => Bytes[49, 50, 51, 52, 53] # io.read_fully(slice) # raises IO::EOFError # ``` + # + # `#read_greedy` also tries to fill the entire buffer if possible, + # but still allows the partially filled slice to be used if an early EOF was reached. def read_fully(slice : Bytes) : Int32 read_fully?(slice) || raise(EOFError.new) end @@ -538,13 +541,12 @@ abstract class IO # slice # => Bytes[49, 50, 51, 52, 53] # io.read_fully?(slice) # => nil # ``` + # + # `#read_greedy` also tries to fill the entire buffer if possible, + # but still allows the partially filled slice to be used if an early EOF was reached. def read_fully?(slice : Bytes) : Int32? - count = slice.size - while slice.size > 0 - read_bytes = read slice - return nil if read_bytes == 0 - slice += read_bytes - end + count = read_greedy(slice) + return nil if count != slice.size count end @@ -560,7 +562,7 @@ abstract class IO # slice # => Bytes[49, 50, 51, 52, 53] # io.read_greedy(slice) # => 4 # ``` - # + # # `#read_fully` and `#read_fully?` also try to fill the entire buffer but error on unexpected EOF. def read_greedy(slice : Bytes) : Int32 count = slice.size