From f6c188639482b96dd0c2b4e2d4191c0b27241aeb Mon Sep 17 00:00:00 2001 From: meztihn Date: Sun, 24 Dec 2017 21:13:17 +0500 Subject: [PATCH] KT-11208 Remove stream buffering --- libraries/stdlib/src/kotlin/io/Console.kt | 121 +++++++++++++++------- libraries/stdlib/test/io/Console.kt | 81 +++++++++++++++ 2 files changed, 163 insertions(+), 39 deletions(-) create mode 100644 libraries/stdlib/test/io/Console.kt diff --git a/libraries/stdlib/src/kotlin/io/Console.kt b/libraries/stdlib/src/kotlin/io/Console.kt index ee9dcd0feee..5f0ffddcd73 100644 --- a/libraries/stdlib/src/kotlin/io/Console.kt +++ b/libraries/stdlib/src/kotlin/io/Console.kt @@ -1,10 +1,14 @@ @file:JvmVersion @file:JvmName("ConsoleKt") + package kotlin.io import java.io.InputStream -import java.io.InputStreamReader -import java.io.BufferedReader +import java.nio.Buffer +import java.nio.ByteBuffer +import java.nio.CharBuffer +import java.nio.charset.Charset +import java.nio.charset.CharsetDecoder /** Prints the given message to the standard output stream. */ @kotlin.internal.InlineOnly @@ -132,50 +136,89 @@ public inline fun println() { System.out.println() } -// Since System.in can change its value on the course of program running, -// we should always delegate to current value and cannot just pass it to InputStreamReader constructor. -// We could use "by" implementation, but we can only use "by" with interfaces and InputStream is abstract class. -private val stdin: BufferedReader by lazy { BufferedReader(InputStreamReader(object : InputStream() { - public override fun read(): Int { - return System.`in`.read() - } +private const val BUFFER_SIZE: Int = 32 - public override fun reset() { - System.`in`.reset() - } +// Since System.in can change its value on the course of program running, we should always delegate to current value. +private val stdin: InputStream + get() = System.`in` - public override fun read(b: ByteArray): Int { - return System.`in`.read(b) - } +private var cachedDecoder: CharsetDecoder? = null - public override fun close() { - System.`in`.close() +private fun decoderFor(charset: Charset): CharsetDecoder { + return cachedDecoder?.takeIf { cached -> + cached.charset() == charset + } ?: charset.newDecoder().also { newDecoder -> + cachedDecoder = newDecoder } - - public override fun mark(readlimit: Int) { - System.`in`.mark(readlimit) - } - - public override fun skip(n: Long): Long { - return System.`in`.skip(n) - } - - public override fun available(): Int { - return System.`in`.available() - } - - public override fun markSupported(): Boolean { - return System.`in`.markSupported() - } - - public override fun read(b: ByteArray, off: Int, len: Int): Int { - return System.`in`.read(b, off, len) - } -}))} +} /** * Reads a line of input from the standard input stream. * * @return the line read or `null` if the input stream is redirected to a file and the end of file has been reached. */ -public fun readLine(): String? = stdin.readLine() +fun readLine(lineSeparator: String = System.lineSeparator(), charset: Charset = Charset.defaultCharset()): String? = + readLine(stdin, lineSeparator, decoderFor(charset)) + +internal fun readLine(inputStream: InputStream, lineSeparator: String, 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(lineSeparator.length) + val stringBuilder = StringBuilder() + + var read = inputStream.read() + if (read == -1) return null + while (read != -1) { + byteBuffer.put(read.toByte()) + if (decoder.tryDecode(byteBuffer, charBuffer, false)) { + if (charBuffer.contentEquals(lineSeparator)) { + break + } + if (!charBuffer.hasRemaining()) { + stringBuilder.append(charBuffer.dequeue()) + } + } + read = inputStream.read() + } + + with(decoder) { + tryDecode(byteBuffer, charBuffer, true) // throws exception if undecoded bytes are left + reset() + } + + with(charBuffer) { + if (!contentEquals(lineSeparator)) { + flip() + while (hasRemaining()) stringBuilder.append(get()) + } + } + + return stringBuilder.toString() +} + +private fun CharsetDecoder.tryDecode(byteBuffer: ByteBuffer, charBuffer: CharBuffer, isEndOfStream: Boolean): Boolean { + val positionBefore = charBuffer.position() + byteBuffer.flip() + with(decode(byteBuffer, charBuffer, isEndOfStream)) { + if (isError) throwException() + } + return (charBuffer.position() > positionBefore).also { isDecoded -> + byteBuffer.apply { if (isDecoded) clear() else flipBack() } + } +} + +private fun CharBuffer.contentEquals(string: String): Boolean { + flip() + return string.contentEquals(this).also { flipBack() } +} + +private fun Buffer.flipBack(): Buffer = apply { + position(limit()) + limit(capacity()) +} + +private fun CharBuffer.dequeue(): Char { + flip() + return get().also { compact() } +} \ No newline at end of file diff --git a/libraries/stdlib/test/io/Console.kt b/libraries/stdlib/test/io/Console.kt new file mode 100644 index 00000000000..f4371fd6ee7 --- /dev/null +++ b/libraries/stdlib/test/io/Console.kt @@ -0,0 +1,81 @@ +/* + * Copyright 2010-2017 JetBrains s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io + +import org.junit.Test +import java.nio.charset.Charset +import kotlin.test.* + +class ConsoleTest { + private val defaultLineSeparator: String = "\\n" + + @Test + fun shouldReadEmptyLine() { + testReadLine("") + } + + @Test + fun shouldReadOneLetter() { + testReadLine("a", "a") + } + + @Test + fun shouldReadOneLine() { + testReadLine("first", "first") + } + + @Test + fun shouldReadTwoLines() { + testReadLine("first${defaultLineSeparator}second", "first", "second") + } + + @Test + fun shouldReadWindowsLineSeparator() { + val lineSeparator = "\\r\\n" + testReadLine("first${lineSeparator}second", "first", "second", lineSeparator = lineSeparator) + } + + @Test + fun shouldReadOldMacLineSeparator() { + val lineSeparator = "\\r" + testReadLine("first${lineSeparator}second", "first", "second", lineSeparator = lineSeparator) + } + + @Test + fun shouldReadMultibyteEncodings() { + testReadLine("first${defaultLineSeparator}second", "first", "second", charset = Charsets.UTF_32) + } + + private fun testReadLine( + text: String, + vararg expected: String, + lineSeparator: String = defaultLineSeparator, + charset: Charset = Charsets.UTF_8 + ) { + val actual = readLines(text, lineSeparator, charset) + assertEquals(expected.asList(), actual) + } + + private fun readLines(text: String, lineSeparator: String, charset: Charset): List { + text.byteInputStream(charset).use { stream -> + val decoder = charset.newDecoder() + return generateSequence { readLine(stream, lineSeparator, decoder) }.toList().also { + assertTrue("All bytes should be read") { stream.read() == -1 } + } + } + } +} \ No newline at end of file