Bug #22239
openMultiple buffer clear errors in IO
Description
While testing my changes on the Windows platform to use an encoding converter for newline conversion for "r", I discovered a failure in test_reopen. This corresponds to case (3) below.
I asked an LLM to investigate similar cases and related code that raised concerns, and was able to identify several issues, so I am reporting them here. I'll submit a PR later.
I used Hy3 (High) via opencode GO to perform this survey and create the test code. (For transparency—this is not an advertisement.)
case(1)
In io.c, free_io_buffer only clears ptr and left off, len and capa as they were.
Since clear_readconv releases the character buffer through it, READ_CHAR_PENDING() kept reporting pending characters after the buffer had been released.
As a result, every operation that clears the code converter (IO#rewind, IO#seek, IO#pos=, IO#flush, IO#binmode, IO#tell, IO#set_encoding, ...) left the IO in a state where byte oriented reads raise IOError.
Also IO#eof? answers false at the end of file and IO#sysseek refuses to work.
File.binwrite("t", "foo")
open("t", "rt") do |f|
f.ungetc(f.getc)
f.rewind
f.getbyte # => 'IO#getbyte': byte oriented read for character buffered IO (IOError)
end
case(2)
IO#reopen(io) only flushes the write buffer when the IO is writable, and never unreads the read buffer.
The buffer happens to be dropped by the io_seek after dup2, but only when the other stream is readable and seekable, so a read-write IO keeps stale bytes otherwise.
File.binwrite("t", "0123456789")
f = File.open("t", "r+b")
f.getbyte
IO.pipe do |r, w|
w.write("ABC"); w.close
f.reopen(r)
f.read # => "123456789ABC" instead of "ABC"
end
case(3)
IO#reopen(name) discards the byte buffer but keeps the code converter and the character buffer, so characters left over from the previous stream leak into the reopened one.
File.binwrite("t", "foo")
open(File::NULL, "rt") do |f|
f.ungetc("a")
f.reopen("t")
f.read # => "afoo" instead of "foo"
end
case(4)
IO#reopen(io) relies on flush_before_seek to drop the buffers, but io_unread returns at once when rbuf is empty and therefore never reaches clear_codeconv.
With characters pending in cbuf and nothing in rbuf they leak into the reopened stream.
File.write("t", "foo\n")
open(File::NULL, "rt") do |f|
f.ungetc("a")
open("t", "rt") {|f2| f.reopen(f2)}
f.gets # => "afoo\n" instead of "foo\n"
end
case(5)
Unlike IO#rewind, IO#seek and IO#pos= do not clear the character buffer.
As io_unread returns at once when rbuf is empty, an ungotten character survives the repositioning while it does not in binary mode.
Updated by YO4 (Yoshinao Muramatsu) 25 days ago
GH PR created. https://github.com/ruby/ruby/pull/18276