diff --git a/libraries/stdlib/jvm/src/kotlin/io/Console.kt b/libraries/stdlib/jvm/src/kotlin/io/Console.kt index 925dbd12541..b21ab0538fa 100644 --- a/libraries/stdlib/jvm/src/kotlin/io/Console.kt +++ b/libraries/stdlib/jvm/src/kotlin/io/Console.kt @@ -156,7 +156,7 @@ internal fun readLine(inputStream: InputStream, decoder: CharsetDecoder): String require(decoder.maxCharsPerByte() <= 1) { "Encodings with multiple chars per byte are not supported" } val byteBuffer = ByteBuffer.allocate(BUFFER_SIZE) - val charBuffer = CharBuffer.allocate(LINE_SEPARATOR_MAX_LENGTH) + val charBuffer = CharBuffer.allocate(LINE_SEPARATOR_MAX_LENGTH * 2) // twice for surrogate pairs val stringBuilder = StringBuilder() var read = inputStream.read() @@ -164,11 +164,11 @@ internal fun readLine(inputStream: InputStream, decoder: CharsetDecoder): String do { byteBuffer.put(read.toByte()) if (decoder.tryDecode(byteBuffer, charBuffer, false)) { - if (charBuffer.containsLineSeparator()) { + if (charBuffer.endsWithLineSeparator()) { break } - if (!charBuffer.hasRemaining()) { - stringBuilder.append(charBuffer.dequeue()) + if (charBuffer.remaining() < 2) { + charBuffer.offloadPrefixTo(stringBuilder) } } read = inputStream.read() @@ -180,15 +180,16 @@ internal fun readLine(inputStream: InputStream, decoder: CharsetDecoder): String } with(charBuffer) { - val length = position() - val first = get(0) - val second = get(1) - when (length) { - 2 -> { - if (!(first == '\r' && second == '\n')) stringBuilder.append(first) - if (second != '\n') stringBuilder.append(second) + var length = position() + if (length > 0 && get(length - 1) == '\n') { + length-- + if (length > 0 && get(length - 1) == '\r') { + length-- } - 1 -> if (first != '\n') stringBuilder.append(first) + } + flip() + repeat(length) { + stringBuilder.append(get()) } } @@ -206,8 +207,9 @@ private fun CharsetDecoder.tryDecode(byteBuffer: ByteBuffer, charBuffer: CharBuf } } -private fun CharBuffer.containsLineSeparator(): Boolean { - return get(1) == '\n' || get(0) == '\n' +private fun CharBuffer.endsWithLineSeparator(): Boolean { + val p = position() + return p > 0 && get(p - 1) == '\n' } private fun Buffer.flipBack() { @@ -215,7 +217,11 @@ private fun Buffer.flipBack() { limit(capacity()) } -private fun CharBuffer.dequeue(): Char { +/** Extracts everything except the last char into [builder]. */ +private fun CharBuffer.offloadPrefixTo(builder: StringBuilder) { flip() - return get().also { compact() } + repeat(limit() - 1) { + builder.append(get()) + } + compact() } diff --git a/libraries/stdlib/jvm/test/io/Console.kt b/libraries/stdlib/jvm/test/io/Console.kt index afd15cf714f..1576cd8716c 100644 --- a/libraries/stdlib/jvm/test/io/Console.kt +++ b/libraries/stdlib/jvm/test/io/Console.kt @@ -64,6 +64,16 @@ class ConsoleTest { testReadLine("first${linuxLineSeparator}second", listOf("first", "second"), charset = Charsets.UTF_32) } + @Test + fun readSurrogatePairs() { + val c = "\uD83D\uDC4D" // thumb-up emoji + testReadLine("$c$linuxLineSeparator", listOf(c)) + testReadLine("e $c$linuxLineSeparator", listOf("e $c")) + testReadLine("$c$windowsLineSeparator", listOf(c)) + testReadLine("e $c$c", listOf("e $c$c")) + testReadLine("e $c$linuxLineSeparator$c", listOf("e $c", c)) + } + private fun testReadLine(text: String, expected: List, charset: Charset = Charsets.UTF_8) { val actual = readLines(text, charset) assertEquals(expected, actual)