readLine: extend buffer to decode surrogate pairs from utf-8 correctly
CharBuffer had capacity to hold 2 chars, which was not enough to append a surrogate pair when the buffer was not empty. The buffer was extended and the decoding algorithm rewritten to deal with a buffer of any length. #KT-28572 Fixed
This commit is contained in:
@@ -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" }
|
require(decoder.maxCharsPerByte() <= 1) { "Encodings with multiple chars per byte are not supported" }
|
||||||
|
|
||||||
val byteBuffer = ByteBuffer.allocate(BUFFER_SIZE)
|
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()
|
val stringBuilder = StringBuilder()
|
||||||
|
|
||||||
var read = inputStream.read()
|
var read = inputStream.read()
|
||||||
@@ -164,11 +164,11 @@ internal fun readLine(inputStream: InputStream, decoder: CharsetDecoder): String
|
|||||||
do {
|
do {
|
||||||
byteBuffer.put(read.toByte())
|
byteBuffer.put(read.toByte())
|
||||||
if (decoder.tryDecode(byteBuffer, charBuffer, false)) {
|
if (decoder.tryDecode(byteBuffer, charBuffer, false)) {
|
||||||
if (charBuffer.containsLineSeparator()) {
|
if (charBuffer.endsWithLineSeparator()) {
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
if (!charBuffer.hasRemaining()) {
|
if (charBuffer.remaining() < 2) {
|
||||||
stringBuilder.append(charBuffer.dequeue())
|
charBuffer.offloadPrefixTo(stringBuilder)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
read = inputStream.read()
|
read = inputStream.read()
|
||||||
@@ -180,15 +180,16 @@ internal fun readLine(inputStream: InputStream, decoder: CharsetDecoder): String
|
|||||||
}
|
}
|
||||||
|
|
||||||
with(charBuffer) {
|
with(charBuffer) {
|
||||||
val length = position()
|
var length = position()
|
||||||
val first = get(0)
|
if (length > 0 && get(length - 1) == '\n') {
|
||||||
val second = get(1)
|
length--
|
||||||
when (length) {
|
if (length > 0 && get(length - 1) == '\r') {
|
||||||
2 -> {
|
length--
|
||||||
if (!(first == '\r' && second == '\n')) stringBuilder.append(first)
|
|
||||||
if (second != '\n') stringBuilder.append(second)
|
|
||||||
}
|
}
|
||||||
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 {
|
private fun CharBuffer.endsWithLineSeparator(): Boolean {
|
||||||
return get(1) == '\n' || get(0) == '\n'
|
val p = position()
|
||||||
|
return p > 0 && get(p - 1) == '\n'
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun Buffer.flipBack() {
|
private fun Buffer.flipBack() {
|
||||||
@@ -215,7 +217,11 @@ private fun Buffer.flipBack() {
|
|||||||
limit(capacity())
|
limit(capacity())
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun CharBuffer.dequeue(): Char {
|
/** Extracts everything except the last char into [builder]. */
|
||||||
|
private fun CharBuffer.offloadPrefixTo(builder: StringBuilder) {
|
||||||
flip()
|
flip()
|
||||||
return get().also { compact() }
|
repeat(limit() - 1) {
|
||||||
|
builder.append(get())
|
||||||
|
}
|
||||||
|
compact()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -64,6 +64,16 @@ class ConsoleTest {
|
|||||||
testReadLine("first${linuxLineSeparator}second", listOf("first", "second"), charset = Charsets.UTF_32)
|
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<String>, charset: Charset = Charsets.UTF_8) {
|
private fun testReadLine(text: String, expected: List<String>, charset: Charset = Charsets.UTF_8) {
|
||||||
val actual = readLines(text, charset)
|
val actual = readLines(text, charset)
|
||||||
assertEquals(expected, actual)
|
assertEquals(expected, actual)
|
||||||
|
|||||||
Reference in New Issue
Block a user