From 0fa03a94d0e9481a4e09b849c0c1831db5610d09 Mon Sep 17 00:00:00 2001 From: Guillaume Nodet Date: Tue, 19 Sep 2017 10:13:37 +0200 Subject: [PATCH] Fix possible encoding problems with wide chars if written separately --- .../org/jline/utils/WriterOutputStream.java | 71 +++++++++++++++++-- .../jline/utils/WriterOutputStreamTest.java | 31 ++++++++ 2 files changed, 95 insertions(+), 7 deletions(-) create mode 100644 terminal/src/test/java/org/jline/utils/WriterOutputStreamTest.java diff --git a/terminal/src/main/java/org/jline/utils/WriterOutputStream.java b/terminal/src/main/java/org/jline/utils/WriterOutputStream.java index deaa3d225..9031d533f 100644 --- a/terminal/src/main/java/org/jline/utils/WriterOutputStream.java +++ b/terminal/src/main/java/org/jline/utils/WriterOutputStream.java @@ -11,7 +11,12 @@ import java.io.IOException; import java.io.OutputStream; import java.io.Writer; +import java.nio.ByteBuffer; +import java.nio.CharBuffer; import java.nio.charset.Charset; +import java.nio.charset.CharsetDecoder; +import java.nio.charset.CoderResult; +import java.nio.charset.CodingErrorAction; /** * Redirects an {@link OutputStream} to a {@link Writer} by decoding the data @@ -25,39 +30,91 @@ public class WriterOutputStream extends OutputStream { private final Writer out; - private final Charset charset; + private final CharsetDecoder decoder; + private final ByteBuffer decoderIn = ByteBuffer.allocate(256); + private final CharBuffer decoderOut = CharBuffer.allocate(128); public WriterOutputStream(Writer out, Charset charset) { + this(out, charset.newDecoder() + .onMalformedInput(CodingErrorAction.REPLACE) + .onUnmappableCharacter(CodingErrorAction.REPLACE)); + } + + public WriterOutputStream(Writer out, CharsetDecoder decoder) { this.out = out; - this.charset = charset; + this.decoder = decoder; } @Override public void write(int b) throws IOException { - out.write(b); - flush(); + write(new byte[] { (byte)b }, 0, 1); } @Override public void write(byte[] b) throws IOException { - out.write(new String(b, this.charset)); - flush(); + write(b, 0, b.length); } @Override public void write(byte[] b, int off, int len) throws IOException { - out.write(new String(b, off, len, this.charset)); + while (len > 0) { + final int c = Math.min(len, decoderIn.remaining()); + decoderIn.put(b, off, c); + processInput(false); + len -= c; + off += c; + } flush(); } @Override public void flush() throws IOException { + flushOutput(); out.flush(); } @Override public void close() throws IOException { + processInput(true); + flush(); out.close(); } + /** + * Decode the contents of the input ByteBuffer into a CharBuffer. + * + * @param endOfInput indicates end of input + * @throws IOException if an I/O error occurs + */ + private void processInput(final boolean endOfInput) throws IOException { + // Prepare decoderIn for reading + decoderIn.flip(); + CoderResult coderResult; + while (true) { + coderResult = decoder.decode(decoderIn, decoderOut, endOfInput); + if (coderResult.isOverflow()) { + flushOutput(); + } else if (coderResult.isUnderflow()) { + break; + } else { + // The decoder is configured to replace malformed input and unmappable characters, + // so we should not get here. + throw new IOException("Unexpected coder result"); + } + } + // Discard the bytes that have been read + decoderIn.compact(); + } + + /** + * Flush the output. + * + * @throws IOException if an I/O error occurs + */ + private void flushOutput() throws IOException { + if (decoderOut.position() > 0) { + out.write(decoderOut.array(), 0, decoderOut.position()); + decoderOut.rewind(); + } + } } diff --git a/terminal/src/test/java/org/jline/utils/WriterOutputStreamTest.java b/terminal/src/test/java/org/jline/utils/WriterOutputStreamTest.java new file mode 100644 index 000000000..3c69c89e6 --- /dev/null +++ b/terminal/src/test/java/org/jline/utils/WriterOutputStreamTest.java @@ -0,0 +1,31 @@ +/* + * Copyright (c) 2002-2017, the original author or authors. + * + * This software is distributable under the BSD license. See the terms of the + * BSD license in the documentation provided with this software. + * + * http://www.opensource.org/licenses/bsd-license.php + */ +package org.jline.utils; + +import org.junit.Test; + +import java.io.StringWriter; +import java.nio.charset.Charset; + +import static org.junit.Assert.assertEquals; + +public class WriterOutputStreamTest { + + @Test + public void testWideChar() throws Exception { + StringWriter sw = new StringWriter(); + WriterOutputStream wos = new WriterOutputStream(sw, Charset.forName("UTF-8")); + byte[] bytes = "㐀".getBytes("UTF-8"); + for (byte b : bytes) { + wos.write(b); + } + wos.flush(); + assertEquals("㐀", sw.toString()); + } +}