From ac272b91b1979f08a1359c570129bbb0975c52f6 Mon Sep 17 00:00:00 2001 From: Charles Oliver Nutter Date: Mon, 1 Mar 2021 16:10:20 -0600 Subject: [PATCH] Call write directly rather than through stream The OutputStream returned by RubyIO.getOutputStream encodes all strings as ASCII-8BIT which makes it incompatible with any IO that has an incompatible external encoding. That is a separate bug, but it affects uses of IOOutputStream that should be ok. IOOutputStream aggregates an encoding to use, so it should be able to pass properly-encoded strings through to the IO. This change avoids using the stream from getOutputStream and instead calls RubyIO.write directly. This fixes the ASCII-8BIT failure noted in ruby/psych#481. --- .../java/org/jruby/util/IOOutputStream.java | 53 ++++++++++++------- 1 file changed, 35 insertions(+), 18 deletions(-) diff --git a/core/src/main/java/org/jruby/util/IOOutputStream.java b/core/src/main/java/org/jruby/util/IOOutputStream.java index b9805280c6f..dd1507eb4a9 100644 --- a/core/src/main/java/org/jruby/util/IOOutputStream.java +++ b/core/src/main/java/org/jruby/util/IOOutputStream.java @@ -36,6 +36,7 @@ import org.jruby.RubyIO; import org.jruby.RubyString; import org.jruby.runtime.CallSite; +import org.jruby.runtime.ThreadContext; import org.jruby.runtime.builtin.IRubyObject; import org.jruby.runtime.MethodIndex; @@ -52,7 +53,8 @@ */ public class IOOutputStream extends OutputStream { private final IRubyObject io; - private final OutputStream out; + private final RubyIO realIO; + private final Ruby runtime; private final CallSite writeAdapter; private final CallSite closeAdapter = MethodIndex.getFunctionalCallSite("close"); private final Encoding encoding; @@ -64,10 +66,11 @@ public class IOOutputStream extends OutputStream { */ public IOOutputStream(final IRubyObject io, Encoding encoding, boolean checkAppend, boolean verifyCanWrite) { this.io = io; - this.out = ( io instanceof RubyIO && !((RubyIO) io).isClosed() && + this.runtime = io.getRuntime(); + this.realIO = ( io instanceof RubyIO && !((RubyIO) io).isClosed() && ((RubyIO) io).isBuiltin("write") ) ? - ((RubyIO) io).getOutStream() : null; - if (out == null || verifyCanWrite) { + ((RubyIO) io) : null; + if (realIO == null || verifyCanWrite) { final String site; if (io.respondsTo("write")) { site = "write"; @@ -106,12 +109,16 @@ public IOOutputStream(final IRubyObject io, Encoding encoding) { @Override public void write(final int bite) throws IOException { - if (out != null) { - out.write(bite); + ThreadContext context = runtime.getCurrentContext(); + + RubyString str = RubyString.newStringLight(runtime, new ByteList(new byte[]{(byte) bite}, encoding, false)); + + RubyIO realIO = this.realIO; + if (realIO != null) { + realIO.write(context, str); } else { - final Ruby runtime = io.getRuntime(); - writeAdapter.call(runtime.getCurrentContext(), io, io, - RubyString.newStringLight(runtime, new ByteList(new byte[] { (byte) bite }, encoding, false))); + IRubyObject io = this.io; + writeAdapter.call(context, io, io, str); } } @@ -122,21 +129,31 @@ public void write(final byte[] b) throws IOException { @Override public void write(final byte[] b,final int off, final int len) throws IOException { - if (out != null) { - out.write(b, off, len); + ThreadContext context = runtime.getCurrentContext(); + + RubyString str = RubyString.newStringLight(runtime, new ByteList(b, off, len, encoding, false)); + + RubyIO realIO = this.realIO; + if (realIO != null) { + realIO.write(context, str); } else { - final Ruby runtime = io.getRuntime(); - writeAdapter.call(runtime.getCurrentContext(), io, io, - RubyString.newStringLight(runtime, new ByteList(b, off, len, encoding, false))); + IRubyObject io = this.io; + writeAdapter.call(context, io, io, str); } } @Override public void close() throws IOException { - if (out != null) { - out.close(); - } else if (io.respondsTo("close")) { - closeAdapter.call(io.getRuntime().getCurrentContext(), io, io); + ThreadContext context = runtime.getCurrentContext(); + + RubyIO realIO = this.realIO; + if (realIO != null) { + realIO.close(context); + } else { + IRubyObject io = this.io; + if (io.respondsTo("close")) { + closeAdapter.call(context, io, io); + } } } }