From 4a7d459c4a5cda7c267fb2bd831b265571be1ac6 Mon Sep 17 00:00:00 2001 From: Jeremy Bopp Date: Wed, 22 Jul 2026 09:05:55 -0500 Subject: [PATCH 01/16] Add spec for IO#binmode disabling newline conversion when reading Verify that calling IO#binmode on an IO stream disables any active newline conversion (such as universal newline mode) when reading data. --- core/io/binmode_spec.rb | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/core/io/binmode_spec.rb b/core/io/binmode_spec.rb index 8117229c9..cc31fa381 100644 --- a/core/io/binmode_spec.rb +++ b/core/io/binmode_spec.rb @@ -31,6 +31,19 @@ @io.binmode @io.internal_encoding.should == nil end + + it "disables newline conversion for reading" do + data = "line1\r\nline2\r\n" + + @io = new_io(@name, "wb") + @io.write(data) + @io.close + + @io = new_io(@name, "rt") + @io.set_encoding("utf-8:ISO-8859-1", newline: :universal) + @io.binmode + @io.read.should == data + end end describe "IO#binmode?" do From 976d046ce1ce64cbfd59ccba13d878fdcc5819ca Mon Sep 17 00:00:00 2001 From: Jeremy Bopp Date: Wed, 22 Jul 2026 09:06:05 -0500 Subject: [PATCH 02/16] Add specs for IO#getbyte after ungetc - Add spec for reading a byte with IO#getbyte after calling ungetc on a stream without character conversion. - Add spec verifying that calling IO#getbyte after ungetc on a stream with character conversion enabled raises IOError ("byte oriented read for character buffered IO"). --- core/io/getbyte_spec.rb | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/core/io/getbyte_spec.rb b/core/io/getbyte_spec.rb index 668d81519..246633e0e 100644 --- a/core/io/getbyte_spec.rb +++ b/core/io/getbyte_spec.rb @@ -25,6 +25,22 @@ it "raises an IOError on closed stream" do -> { IOSpecs.closed_io.getbyte }.should.raise(IOError) end + + it "reads after ungetc without character conversion" do + @io.set_encoding("utf-8") + c = @io.getc + @io.ungetc(c) + @io.getbyte.should == 86 + end + + it "raises an exception after ungetc with character conversion" do + @io.set_encoding("utf-8:utf-16be") + c = @io.getc + @io.ungetc(c) + -> do + @io.getbyte + end.should.raise(IOError, "byte oriented read for character buffered IO") + end end describe "IO#getbyte" do From 6f9fba96fa138a1e27447431bbdc0e224bf0ee98 Mon Sep 17 00:00:00 2001 From: Jeremy Bopp Date: Wed, 22 Jul 2026 09:06:10 -0500 Subject: [PATCH 03/16] Add specs for IO#internal_encoding with BINARY external encoding - Add spec verifying IO#internal_encoding returns nil when external encoding is BINARY. - Add Ruby version-specific specs for setting internal encoding when external encoding is BINARY: Ruby < 3.3 returns the assigned value, while Ruby >= 3.3 ignores the internal encoding and returns nil because binary mode does not support internal character conversion. --- core/io/internal_encoding_spec.rb | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/core/io/internal_encoding_spec.rb b/core/io/internal_encoding_spec.rb index 9963a93f3..c0bd86785 100644 --- a/core/io/internal_encoding_spec.rb +++ b/core/io/internal_encoding_spec.rb @@ -93,6 +93,27 @@ @io = new_io @name, "#{@object}:binary" @io.internal_encoding.should == nil end + + it "returns nil when the external encoding is BINARY and internal encoding is set" do + @io = new_io @name, "#{@object}:binary:ibm437" + @io.internal_encoding.should == nil + end + + ruby_version_is ""..."3.3" do + it "returns the value set by #set_encoding when the external encoding is BINARY" do + @io = new_io @name, @object + @io.set_encoding(Encoding::BINARY, Encoding::IBM437) + @io.internal_encoding.should.equal?(Encoding::IBM437) + end + end + + ruby_version_is "3.3" do + it "ignores the value set by #set_encoding when the external encoding is BINARY and returns nil" do + @io = new_io @name, @object + @io.set_encoding(Encoding::BINARY, Encoding::IBM437) + @io.internal_encoding.should == nil + end + end end end From a8dd789083f7e133232679160de10d97e010c9ac Mon Sep 17 00:00:00 2001 From: Jeremy Bopp Date: Wed, 22 Jul 2026 09:06:13 -0500 Subject: [PATCH 04/16] Add spec for IO#pwrite raising Errno::EINVAL on invalid offset Verify that calling IO#pwrite with a negative offset raises Errno::EINVAL. --- core/io/pwrite_spec.rb | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/core/io/pwrite_spec.rb b/core/io/pwrite_spec.rb index c318d551b..748521094 100644 --- a/core/io/pwrite_spec.rb +++ b/core/io/pwrite_spec.rb @@ -59,6 +59,12 @@ }.should.raise(NoMethodError, /undefined method [`']to_s'/) end + it "raises a Errno::EINVAL if the offset is invalid" do + -> { + @file.pwrite("foo", -1) + }.should.raise(Errno::EINVAL) + end + it "raises a TypeError if the offset cannot be converted to an Integer" do -> { @file.pwrite("foo", Object.new) From d91bdf7671403b86e13ade81906d2ec2f0cd58a0 Mon Sep 17 00:00:00 2001 From: Jeremy Bopp Date: Wed, 22 Jul 2026 09:06:16 -0500 Subject: [PATCH 05/16] Update and add specs for IO#read_nonblock after ungetc and with 0 length - Add spec for CRuby Bug #18421 (fixed in 3.0.4) ensuring IO#read_nonblock(0, buffer) clears the output buffer and returns it empty when length is 0. - Update IOError expectation message check when read_nonblock is called after ungetc with character conversion enabled ("byte oriented read for character buffered IO"). --- core/io/read_nonblock_spec.rb | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/core/io/read_nonblock_spec.rb b/core/io/read_nonblock_spec.rb index 511cf0326..c63289dc5 100644 --- a/core/io/read_nonblock_spec.rb +++ b/core/io/read_nonblock_spec.rb @@ -66,14 +66,16 @@ @read.read_nonblock(3).should == "bar" end - it "raises an exception after ungetc with data in the buffer and character conversion enabled" do + it "raises an exception after ungetc with character conversion enabled" do @write.write("foobar") @read.set_encoding( 'utf-8', universal_newline: true ) c = @read.getc @read.ungetc(c) - -> { @read.read_nonblock(3).should == "foo" }.should.raise(IOError) + -> do + @read.read_nonblock(3) + end.should.raise(IOError, "byte oriented read for character buffered IO") end it "returns less data if that is all that is available" do @@ -137,6 +139,14 @@ -> { @read.read_nonblock(5) }.should.raise(EOFError) end + ruby_bug "#18421", ""..."3.0.4" do + it "clears and returns the given buffer if the length argument is 0" do + buffer = String.new("existing content") + @read.read_nonblock(0, buffer).should == buffer + buffer.should == "" + end + end + it "preserves the encoding of the given buffer" do buffer = ''.encode(Encoding::ISO_8859_1) @write.write("abc") From 40e859faedc22749cc632e82783fa6a365778296 Mon Sep 17 00:00:00 2001 From: Jeremy Bopp Date: Wed, 22 Jul 2026 09:06:19 -0500 Subject: [PATCH 06/16] Add specs for IO#read after ungetc - Add specs verifying that IO#read successfully reads bytes after calling ungetc on streams without character conversion. - Add specs across various mode combinations verifying that IO#read raises IOError ("byte oriented read for character buffered IO") after ungetc when character conversion is enabled. --- core/io/read_spec.rb | 38 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 38 insertions(+) diff --git a/core/io/read_spec.rb b/core/io/read_spec.rb index 0c165814c..6d146c1ff 100644 --- a/core/io/read_spec.rb +++ b/core/io/read_spec.rb @@ -672,6 +672,12 @@ @io.read.encoding.should.equal?(Encoding::EUC_JP) end + it "reads after ungetc" do + c = @io.getc + @io.ungetc(c) + @io.read(2).should == [164, 162].pack('C*').force_encoding(Encoding::BINARY) + end + it_behaves_like :io_read_size_internal_encoding, nil end @@ -680,6 +686,14 @@ @io = IOSpecs.io_fixture "read_euc_jp.txt", "r:euc-jp:utf-8" end + it "raises an exception after ungetc" do + c = @io.getc + @io.ungetc(c) + -> do + @io.read(2) + end.should.raise(IOError, "byte oriented read for character buffered IO") + end + it_behaves_like :io_read_internal_encoding, nil it_behaves_like :io_read_size_internal_encoding, nil end @@ -689,6 +703,14 @@ @io = IOSpecs.io_fixture "read_euc_jp.txt", mode: "r:euc-jp:utf-8" end + it "raises an exception after ungetc" do + c = @io.getc + @io.ungetc(c) + -> do + @io.read(2) + end.should.raise(IOError, "byte oriented read for character buffered IO") + end + it_behaves_like :io_read_internal_encoding, nil it_behaves_like :io_read_size_internal_encoding, nil end @@ -701,6 +723,14 @@ @io = IOSpecs.io_fixture "read_euc_jp.txt", options end + it "raises an exception after ungetc" do + c = @io.getc + @io.ungetc(c) + -> do + @io.read(2) + end.should.raise(IOError, "byte oriented read for character buffered IO") + end + it_behaves_like :io_read_internal_encoding, nil it_behaves_like :io_read_size_internal_encoding, nil end @@ -711,6 +741,14 @@ @io = IOSpecs.io_fixture "read_euc_jp.txt", options end + it "raises an exception after ungetc" do + c = @io.getc + @io.ungetc(c) + -> do + @io.read(2) + end.should.raise(IOError, "byte oriented read for character buffered IO") + end + it_behaves_like :io_read_internal_encoding, nil it_behaves_like :io_read_size_internal_encoding, nil end From 3a69b22942cd1b5e093c443624b325a2a0be355e Mon Sep 17 00:00:00 2001 From: Jeremy Bopp Date: Wed, 22 Jul 2026 09:06:22 -0500 Subject: [PATCH 07/16] Add specs for IO#readbyte after ungetc - Add spec for IO#readbyte after ungetc on a stream without character conversion. - Add spec verifying that IO#readbyte raises IOError ("byte oriented read for character buffered IO") after ungetc when character conversion is enabled. --- core/io/readbyte_spec.rb | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/core/io/readbyte_spec.rb b/core/io/readbyte_spec.rb index 07da1da91..5a425b994 100644 --- a/core/io/readbyte_spec.rb +++ b/core/io/readbyte_spec.rb @@ -21,4 +21,20 @@ @io.readbyte end.should.raise EOFError end + + it "reads after ungetc without character conversion" do + @io.set_encoding("utf-8") + c = @io.getc + @io.ungetc(c) + @io.readbyte.should == ?r.getbyte(0) + end + + it "raises an exception after ungetc with character conversion" do + @io.set_encoding("utf-8:utf-16be") + c = @io.getc + @io.ungetc(c) + -> do + @io.readbyte + end.should.raise(IOError, "byte oriented read for character buffered IO") + end end From f0a3b5ffb98cff2c57982ba243d36c691330d6fe Mon Sep 17 00:00:00 2001 From: Jeremy Bopp Date: Wed, 22 Jul 2026 09:06:26 -0500 Subject: [PATCH 08/16] Add spec for IO#readpartial raising IOError after ungetc with character conversion Verify that calling IO#readpartial after ungetc on an IO stream with character conversion enabled raises IOError ("byte oriented read for character buffered IO"). --- core/io/readpartial_spec.rb | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/core/io/readpartial_spec.rb b/core/io/readpartial_spec.rb index d3f5545c8..b96e10be4 100644 --- a/core/io/readpartial_spec.rb +++ b/core/io/readpartial_spec.rb @@ -58,6 +58,16 @@ @rd.readpartial(2).should == "b" end + it "raises an exception after ungetc with character conversion enabled" do + @wr.write("foobar") + @rd.set_encoding('utf-8', 'ascii') + c = @rd.getc + @rd.ungetc(c) + -> do + @rd.readpartial(3) + end.should.raise(IOError, "byte oriented read for character buffered IO") + end + it "discards the existing buffer content upon successful read" do buffer = +"existing content" @wr.write("hello world") From 46f9838371437e85a8a62530467236ec846c9daa Mon Sep 17 00:00:00 2001 From: Jeremy Bopp Date: Wed, 22 Jul 2026 09:06:32 -0500 Subject: [PATCH 09/16] Add specs for IO#reopen argument checking and open modes - Add spec verifying IO#reopen raises ArgumentError when passed excess arguments. - Add specs for reopening an IO with a String path, verifying that the newly opened stream preserves the capabilities and restrictions of the original open mode ('r', 'w', 'r+', 'a'). --- core/io/reopen_spec.rb | 51 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 51 insertions(+) diff --git a/core/io/reopen_spec.rb b/core/io/reopen_spec.rb index 3b972d897..8d2e3b30c 100644 --- a/core/io/reopen_spec.rb +++ b/core/io/reopen_spec.rb @@ -57,6 +57,10 @@ @io.close -> { @io.reopen(STDOUT) }.should.raise(IOError) end + + it "raises ArgumentError when too many arguments are given" do + -> { @io.reopen(@other_name, "r", "excess argument") }.should.raise(ArgumentError) + end end describe "IO#reopen with a String" do @@ -170,6 +174,53 @@ @io.reopen(@other_name) File.should.exist?(@other_name) end + + it "opens the file in read mode if the IO is read-only" do + touch(@name) { |f| f.write "original data" } + touch(@other_name) { |f| f.write "new data" } + @io = new_io @name, "r" + + @io.reopen(@other_name) + -> { @io.write("overwrite content") }.should.raise(IOError) + @io.read.should == "new data" + end + + it "opens the file in write mode if the IO is write-only" do + touch(@name) { |f| f.write "original data" } + touch(@other_name) { |f| f.write "new data" } + @io = new_io @name, "w" + + @io.reopen(@other_name) + -> { @io.read }.should.raise(IOError) + @io.write("overwrite content").should == 17 + @io.close + File.read(@other_name).should == "overwrite content" + end + + it "opens the file in read-write mode if the IO is read-write" do + touch(@name) { |f| f.write "original data" } + touch(@other_name) { |f| f.write "new data" } + @io = new_io @name, "r+" + + @io.reopen(@other_name) + @io.read.should == "new data" + @io.rewind + @io.write("overwrite content").should == 17 + @io.close + File.read(@other_name).should == "overwrite content" + end + + it "opens the file in append mode if the IO appends" do + touch(@name) { |f| f.write "original data" } + touch(@other_name) { |f| f.write "new data" } + @io = new_io @name, "a" + + @io.reopen(@other_name) + -> { @io.read }.should.raise(IOError) + @io.write("overwrite content").should == 17 + @io.close + File.read(@other_name).should == "new dataoverwrite content" + end end describe "IO#reopen with a String" do From 538111fd1176e220f53cf4ddd270f040d1609041 Mon Sep 17 00:00:00 2001 From: Jeremy Bopp Date: Wed, 22 Jul 2026 09:06:38 -0500 Subject: [PATCH 10/16] Add spec for IO#seek clearing character buffer after ungetc When ungetc pushes characters into the IO stream's character buffer, calling IO#seek should discard that buffer before seeking to the target offset. This adds a spec for CRuby Bug #20919 fixed in Ruby 3.4. --- core/io/seek_spec.rb | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/core/io/seek_spec.rb b/core/io/seek_spec.rb index d6e553bae..9d42e3a5c 100644 --- a/core/io/seek_spec.rb +++ b/core/io/seek_spec.rb @@ -77,6 +77,19 @@ value[-1].should == @io.read[0] end + ruby_bug "#20919", "" ... "3.4" do + it "clears the character buffer" do + @io.ungetc("a") + @io.seek(1, IO::SEEK_SET) + @io.getc.should == "o" + + @io.set_encoding(Encoding::UTF_8, Encoding::UTF_16LE) + @io.ungetc("a".encode(Encoding::UTF_16LE)) + @io.seek(1, IO::SEEK_SET) + @io.getc.should == "o".encode(Encoding::UTF_16LE) + end + end + platform_is :darwin do it "supports seek offsets greater than 2^32" do begin From 950b6ccdcc2c34161577d84abd225bb380b459f0 Mon Sep 17 00:00:00 2001 From: Jeremy Bopp Date: Wed, 22 Jul 2026 09:06:42 -0500 Subject: [PATCH 11/16] Refactor IO#set_encoding specs and add missing edge cases - Refactor `IO#set_encoding when passed nil, nil` mode blocks ('r', 'rb', 'r+', 'w', 'w+', 'a', 'a+') to reuse shared behaviors `:io_set_encoding_common`, `:io_set_encoding_readonly`, and `:io_set_encoding_write`. - Add spec for passing "-" as second argument to reset internal_encoding to nil. - Add validation specs for non-ASCII compatible arguments, nil first argument with non-nil second argument, and invalid newline decorator option. - Add specs for ASCII-incompatible encodings (handling with binmode, character conversion, write-only streams, and error raising on readable streams). - Add Ruby 3.2+ spec for newline decorator in binary mode. - Add Ruby 3.3+ specs for setting internal encoding when second argument is nil. - Add spec for encoding names containing null bytes. --- core/io/set_encoding_spec.rb | 278 +++++++++++++++++++++++++++++------ 1 file changed, 237 insertions(+), 41 deletions(-) diff --git a/core/io/set_encoding_spec.rb b/core/io/set_encoding_spec.rb index 237251de5..3e89eb306 100644 --- a/core/io/set_encoding_spec.rb +++ b/core/io/set_encoding_spec.rb @@ -1,5 +1,94 @@ require_relative '../../spec_helper' +describe :io_set_encoding_common, shared: true do + describe "when Encoding.default_external is not binary and Encoding.default_internal is not nil" do + it "sets the encodings to the current Encoding defaults" do + @io = new_io @name, "#{@object}" + + Encoding.default_external = Encoding::ISO_8859_1 + Encoding.default_internal = Encoding::ISO_8859_2 + + @io.set_encoding nil, nil + + @io.external_encoding.should.equal?(Encoding::ISO_8859_1) + @io.internal_encoding.should.equal?(Encoding::ISO_8859_2) + end + + it "prevents the encodings from changing when Encoding defaults are changed" do + @io = new_io @name, "#{@object}" + + Encoding.default_external = Encoding::ISO_8859_1 + Encoding.default_internal = Encoding::ISO_8859_2 + + @io.set_encoding nil, nil + + Encoding.default_external = Encoding::IBM437 + Encoding.default_internal = Encoding::IBM866 + + @io.external_encoding.should.equal?(Encoding::ISO_8859_1) + @io.internal_encoding.should.equal?(Encoding::ISO_8859_2) + end + end +end + +describe :io_set_encoding_readonly, shared: true do + describe "when Encoding.default_external is binary" do + it "prevents the #internal_encoding from changing when Encoding.default_internal is changed" do + @io = new_io @name, "#{@object}" + + Encoding.default_external = Encoding::ASCII_8BIT + Encoding.default_internal = Encoding::ISO_8859_2 + + @io.set_encoding nil, nil + + Encoding.default_internal = Encoding::IBM437 + + @io.internal_encoding.should == nil + end + + it "allows the #external_encoding to change when Encoding.default_external is changed" do + @io = new_io @name, "#{@object}" + + Encoding.default_external = Encoding::ASCII_8BIT + Encoding.default_internal = Encoding::ISO_8859_2 + + @io.set_encoding nil, nil + + Encoding.default_external = Encoding::IBM437 + + @io.external_encoding.should.equal?(Encoding::IBM437) + end + end + + describe "when Encoding.default_internal is nil" do + it "prevents the #internal_encoding from changing when Encoding.default_internal is changed" do + @io = new_io @name, "#{@object}" + + Encoding.default_external = Encoding::ISO_8859_1 + Encoding.default_internal = nil + + @io.set_encoding nil, nil + + Encoding.default_internal = Encoding::IBM437 + + @io.internal_encoding.should == nil + end + + it "allows the #external_encoding to change when Encoding.default_external is changed" do + @io = new_io @name, "#{@object}" + + Encoding.default_external = Encoding::ISO_8859_1 + Encoding.default_internal = nil + + @io.set_encoding nil, nil + + Encoding.default_external = Encoding::IBM437 + + @io.external_encoding.should.equal?(Encoding::IBM437) + end + end +end + describe :io_set_encoding_write, shared: true do it "sets the encodings to nil when they were set previously" do @io = new_io @name, "#{@object}:ibm437:ibm866" @@ -26,23 +115,28 @@ @io = new_io @name, "#{@object}:utf-8:us-ascii" @io.set_encoding nil, nil - Encoding.default_external = Encoding::IBM437 - Encoding.default_internal = Encoding::IBM866 + Encoding.default_external = Encoding::UTF_8 + Encoding.default_internal = nil + + @io.set_encoding nil, nil @io.external_encoding.should == nil @io.internal_encoding.should == nil end - it "sets the encodings to the current Encoding defaults" do - @io = new_io @name, @object + it "prevents the encodings from changing when Encoding defaults are changed" do + @io = new_io @name, "#{@object}:utf-8:us-ascii" - Encoding.default_external = Encoding::IBM437 - Encoding.default_internal = Encoding::IBM866 + Encoding.default_external = Encoding::UTF_8 + Encoding.default_internal = nil @io.set_encoding nil, nil - @io.external_encoding.should == Encoding::IBM437 - @io.internal_encoding.should == Encoding::IBM866 + Encoding.default_external = Encoding::IBM437 + Encoding.default_internal = Encoding::IBM866 + + @io.external_encoding.should == nil + @io.internal_encoding.should == nil end end @@ -68,63 +162,44 @@ end describe "with 'r' mode" do - it "sets the encodings to the current Encoding defaults" do - @io = new_io @name, "r" + it_behaves_like :io_set_encoding_common, nil, "r" - Encoding.default_external = Encoding::IBM437 - Encoding.default_internal = Encoding::IBM866 - - @io.set_encoding nil, nil - @io.external_encoding.should.equal?(Encoding::IBM437) - @io.internal_encoding.should.equal?(Encoding::IBM866) - end - - it "prevents the #internal_encoding from changing when Encoding.default_internal is changed" do - @io = new_io @name, "r" - @io.set_encoding nil, nil - - Encoding.default_internal = Encoding::IBM437 - - @io.internal_encoding.should == nil - end - - it "allows the #external_encoding to change when Encoding.default_external is changed" do - @io = new_io @name, "r" - @io.set_encoding nil, nil - - Encoding.default_external = Encoding::IBM437 - - @io.external_encoding.should.equal?(Encoding::IBM437) - end + it_behaves_like :io_set_encoding_readonly, nil, "r" end describe "with 'rb' mode" do - it "returns Encoding.default_external" do - @io = new_io @name, "rb" - @io.external_encoding.should.equal?(Encoding::BINARY) + it_behaves_like :io_set_encoding_common, nil, "rb" - @io.set_encoding nil, nil - @io.external_encoding.should.equal?(Encoding.default_external) - end + it_behaves_like :io_set_encoding_readonly, nil, "rb" end describe "with 'r+' mode" do + it_behaves_like :io_set_encoding_common, nil, "r+" + it_behaves_like :io_set_encoding_write, nil, "r+" end describe "with 'w' mode" do + it_behaves_like :io_set_encoding_common, nil, "w" + it_behaves_like :io_set_encoding_write, nil, "w" end describe "with 'w+' mode" do + it_behaves_like :io_set_encoding_common, nil, "w+" + it_behaves_like :io_set_encoding_write, nil, "w+" end describe "with 'a' mode" do + it_behaves_like :io_set_encoding_common, nil, "a" + it_behaves_like :io_set_encoding_write, nil, "a" end describe "with 'a+' mode" do + it_behaves_like :io_set_encoding_common, nil, "a+" + it_behaves_like :io_set_encoding_write, nil, "a+" end @@ -203,6 +278,17 @@ @io.internal_encoding.should == Encoding::UTF_16BE end + it "sets the internal encoding to nil when passed '-' as the second argument" do + default_internal = Encoding.default_internal + Encoding.default_internal = Encoding::UTF_16BE + + @io.set_encoding("utf-8", "-") + @io.external_encoding.should == Encoding::UTF_8 + @io.internal_encoding.should == nil + ensure + Encoding.default_internal = default_internal + end + it "calls #to_str to convert an abject to a String" do obj = mock("io_set_encoding") obj.should_receive(:to_str).and_return("utf-8:utf-16be") @@ -235,4 +321,114 @@ it "raises ArgumentError when too many arguments are given" do -> { @io.set_encoding(1, 2, 3) }.should.raise(ArgumentError) end + + it "raises ArgumentError when argument is not ASCII compatible" do + -> { @io.set_encoding("utf-8".encode(Encoding::UTF_16BE)) }.should.raise(ArgumentError) + end + + it "raises TypeError when the first argument is nil and the second is not nil" do + -> { @io.set_encoding(nil, Encoding::UTF_8) }.should.raise(TypeError) + end + + it "raises ArgumentError when newline decorator has invalid value" do + -> { + @io.set_encoding("utf-8", newline: :invalid) + }.should.raise(ArgumentError, "unexpected value for newline option: invalid") + -> { + @io.set_encoding("utf-8", newline: "invalid") + }.should.raise(ArgumentError, "unexpected value for newline option") + end + + it "raises ArgumentError when ASCII incompatible encoding is used with readable stream without binmode or character conversion" do + default_external = Encoding.default_external + + io = new_io @name, "r" + + -> { + io.set_encoding(Encoding::UTF_16BE) + }.should.raise(ArgumentError, "ASCII incompatible encoding needs binmode") + + Encoding.default_external = Encoding::UTF_16BE + + -> { + io.set_encoding(nil) + }.should.raise(ArgumentError, "ASCII incompatible encoding needs binmode") + ensure + io.close + Encoding.default_external = default_external + end + + it "sets the external encoding when ASCII incompatible encoding is used with binmode" do + io = new_io @name, "r" + io.binmode + + io.set_encoding(Encoding::UTF_16BE) + io.external_encoding.should == Encoding::UTF_16BE + + default_external = Encoding.default_external + Encoding.default_external = Encoding::UTF_16BE + + io.set_encoding(nil) + io.external_encoding.should == Encoding::UTF_16BE + ensure + io.close + Encoding.default_external = default_external + end + + it "sets the external and internal encodings when ASCII incompatible encoding is used with character conversion" do + io = new_io @name, "r" + + io.set_encoding(Encoding::UTF_16BE, Encoding::UTF_16LE) + io.external_encoding.should == Encoding::UTF_16BE + io.internal_encoding.should == Encoding::UTF_16LE + ensure + io.close + end + + it "sets the external encoding when ASCII incompatible encoding is used with a write-only stream" do + default_external = Encoding.default_external + + @io.set_encoding(Encoding::UTF_16BE) + @io.external_encoding.should == Encoding::UTF_16BE + + Encoding.default_external = Encoding::UTF_16BE + + @io.set_encoding(nil) + @io.external_encoding.should == nil + ensure + Encoding.default_external = default_external + end + + ruby_version_is "3.2" do + it "raises ArgumentError when newline decorator provided in binary mode" do + @io.binmode + -> { + @io.set_encoding("utf-8", newline: :lf) + }.should.raise(ArgumentError, "newline decorator with binary mode") + end + end + + ruby_version_is "" ... "3.3" do + it "sets the internal encoding to nil when the second argument is nil" do + @io.set_encoding(Encoding::UTF_8, nil) + @io.internal_encoding.should == nil + end + end + + ruby_version_is "3.3" do + it "sets the internal encoding to Encoding.default_internal when the second argument is nil" do + default_internal = Encoding.default_internal + Encoding.default_internal = Encoding::UTF_16BE + + @io.set_encoding(Encoding::UTF_8, nil) + @io.internal_encoding.should == Encoding::UTF_16BE + ensure + Encoding.default_internal = default_internal + end + end + + it "raises Argument error when given 2 arguments and an encoding name containing a null byte" do + -> { @io.set_encoding(Encoding::UTF_8, "null\0byte") }.should.raise(ArgumentError) + -> { @io.set_encoding("null\0byte", Encoding::UTF_8) }.should.raise(ArgumentError) + end end From 09eb3354e48e9e909c0c61f313864e4037e02792 Mon Sep 17 00:00:00 2001 From: Jeremy Bopp Date: Wed, 22 Jul 2026 09:06:50 -0500 Subject: [PATCH 12/16] Add and update specs for IO#sysread with buffered IO - Update IOError message expectation when sysread is called after buffered reads. - Add spec verifying that calling sysread on an IO with a non-empty character buffer raises IOError ("byte oriented read for character buffered IO"). --- core/io/sysread_spec.rb | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/core/io/sysread_spec.rb b/core/io/sysread_spec.rb index 078e97934..69e6f7a1a 100644 --- a/core/io/sysread_spec.rb +++ b/core/io/sysread_spec.rb @@ -52,7 +52,17 @@ it "raises an error when called after buffered reads" do @file.readline - -> { @file.sysread(5) }.should.raise(IOError) + -> do + @file.sysread(5) + end.should.raise(IOError, "sysread for buffered IO") + end + + it "raises an error when called with a non-empty character buffer" do + @file.set_encoding(Encoding::UTF_8, Encoding::UTF_16BE) + @file.ungetc("a".encode(Encoding::UTF_16BE)) + -> do + @file.sysread(5) + end.should.raise(IOError, "byte oriented read for character buffered IO") end it "reads normally even when called immediately after a buffered IO#read" do From 18069f946330f9dce7f83c04ca25fe533a48ec37 Mon Sep 17 00:00:00 2001 From: Jeremy Bopp Date: Wed, 22 Jul 2026 09:06:53 -0500 Subject: [PATCH 13/16] Add and update specs for IO#sysseek with buffered IO - Update IOError message expectation when sysseek is called after buffered reads. - Add spec verifying that calling sysseek on an IO with a non-empty character buffer raises IOError ("sysseek for buffered IO"). --- core/io/sysseek_spec.rb | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/core/io/sysseek_spec.rb b/core/io/sysseek_spec.rb index e513d189a..ba24a1822 100644 --- a/core/io/sysseek_spec.rb +++ b/core/io/sysseek_spec.rb @@ -28,7 +28,17 @@ it "raises an error when called after buffered reads" do @io.readline - -> { @io.sysseek(-5, IO::SEEK_CUR) }.should.raise(IOError) + -> do + @io.sysseek(-5, IO::SEEK_CUR) + end.should.raise(IOError, "sysseek for buffered IO") + end + + it "raises an error when called with a non-empty character buffer" do + @io.set_encoding(Encoding::UTF_8, Encoding::UTF_16BE) + @io.ungetc("a".encode(Encoding::UTF_16BE)) + -> do + @io.sysseek(-5, IO::SEEK_CUR) + end.should.raise(IOError, "sysseek for buffered IO") end it "seeks normally even when called immediately after a buffered IO#read" do From e1b197212cafeb46cf503661e3dfadb9af05ef4b Mon Sep 17 00:00:00 2001 From: Jeremy Bopp Date: Wed, 22 Jul 2026 09:06:55 -0500 Subject: [PATCH 14/16] Add and refactor specs for IO#syswrite on pipes - Refactor pipe test fixture setup to use before/after blocks for clean tear-down. - Add specs for Ruby < 3.2 verifying that non-blocking syswrite on a full pipe raises Errno::EAGAIN (or Errno::EWOULDBLOCK on Windows) instead of returning partial bytes. --- core/io/syswrite_spec.rb | 45 ++++++++++++++++++++++++++++++---------- 1 file changed, 34 insertions(+), 11 deletions(-) diff --git a/core/io/syswrite_spec.rb b/core/io/syswrite_spec.rb index 8bf61a27c..1cf28dae6 100644 --- a/core/io/syswrite_spec.rb +++ b/core/io/syswrite_spec.rb @@ -60,19 +60,42 @@ end describe "IO#syswrite on a pipe" do - it "returns the written bytes if the fd is in nonblock mode and write would block" do + before do require 'io/nonblock' - r, w = IO.pipe - begin - w.nonblock = true - larger_than_pipe_capacity = 2 * 1024 * 1024 - written = w.syswrite("a"*larger_than_pipe_capacity) - written.should > 0 - written.should < larger_than_pipe_capacity - ensure - w.close - r.close + @read, @write = IO.pipe + end + + after do + @read.close + @write.close + end + + ruby_version_is ""..."3.2" do + platform_is_not :windows do + it "raises Errno::EAGAIN when the write would block" do + @write.nonblock = true + -> { + loop { @write.syswrite('a' * 10_000) } + }.should.raise(Errno::EAGAIN) + end end + + platform_is :windows do + it "raises Errno::EWOULDBLOCK when the write would block" do + @write.nonblock = true + -> { + loop { @write.syswrite('a' * 10_000) } + }.should.raise(Errno::EWOULDBLOCK) + end + end + end + + it "returns the written bytes if the fd is in nonblock mode and write would block" do + @write.nonblock = true + larger_than_pipe_capacity = 2 * 1024 * 1024 + written = @write.syswrite("a"*larger_than_pipe_capacity) + written.should > 0 + written.should < larger_than_pipe_capacity end end From c9449cbcb27a076594f7fceb7002c7c1659230ab Mon Sep 17 00:00:00 2001 From: Jeremy Bopp Date: Wed, 22 Jul 2026 09:06:57 -0500 Subject: [PATCH 15/16] Add specs for IO#write with ASCII-8BIT encoding and newline conversion - Add spec for IO#write writing binary data when external encoding is ASCII-8BIT. - Add specs verifying that IO#write performs newline conversion on binary data when no encoding or ASCII-8BIT encoding is specified alongside newline conversion options. --- core/io/write_spec.rb | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/core/io/write_spec.rb b/core/io/write_spec.rb index 469f9a89e..463e73e0a 100644 --- a/core/io/write_spec.rb +++ b/core/io/write_spec.rb @@ -140,6 +140,36 @@ File.binread(@filename).bytes.should == [0x61, 0x00, 0x00, 0x00, 0xC4, 0x85] end end + + it "writes binary data if encoding is ASCII-8BIT" do + File.open(@filename, "w:ascii-8bit") do |file| + file.write('Hëllö'.encode('ISO-8859-1')) + end + ë = ([235].pack('U')).encode('ISO-8859-1') + ö = ([246].pack('U')).encode('ISO-8859-1') + res = "H#{ë}ll#{ö}" + File.binread(@filename).should == res.force_encoding(Encoding::BINARY) + end + + it "writes binary data with newline conversion if no encoding is given" do + File.open(@filename, "w", newline: :crlf) do |file| + file.write("Hëllö\n".encode('ISO-8859-1')) + end + ë = ([235].pack('U')).encode('ISO-8859-1') + ö = ([246].pack('U')).encode('ISO-8859-1') + res = "H#{ë}ll#{ö}\r\n" + File.binread(@filename).should == res.force_encoding(Encoding::BINARY) + end + + it "writes binary data with newline conversion if encoding is ASCII-8BIT" do + File.open(@filename, "w:ascii-8bit", newline: :crlf) do |file| + file.write("Hëllö\n".encode('ISO-8859-1')) + end + ë = ([235].pack('U')).encode('ISO-8859-1') + ö = ([246].pack('U')).encode('ISO-8859-1') + res = "H#{ë}ll#{ö}\r\n" + File.binread(@filename).should == res.force_encoding(Encoding::BINARY) + end end describe "IO.write" do From 5694592a4424a31b417a701129a31a437d1e2613 Mon Sep 17 00:00:00 2001 From: Jeremy Bopp Date: Thu, 23 Jul 2026 06:32:58 -0500 Subject: [PATCH 16/16] Omit IO#pwrite test for EINVAL result on Windows The implementation on Windows does not error in this case --- core/io/pwrite_spec.rb | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/core/io/pwrite_spec.rb b/core/io/pwrite_spec.rb index 748521094..1fbbbd6ed 100644 --- a/core/io/pwrite_spec.rb +++ b/core/io/pwrite_spec.rb @@ -59,10 +59,12 @@ }.should.raise(NoMethodError, /undefined method [`']to_s'/) end - it "raises a Errno::EINVAL if the offset is invalid" do - -> { - @file.pwrite("foo", -1) - }.should.raise(Errno::EINVAL) + platform_is_not :windows do + it "raises a Errno::EINVAL if the offset is invalid" do + -> { + @file.pwrite("foo", -1) + }.should.raise(Errno::EINVAL) + end end it "raises a TypeError if the offset cannot be converted to an Integer" do