Skip to content

Commit f386554

Browse files
ludwig-jbintellij-monorepo-bot
authored andcommitted
LSP-306 [lsp] fix line reader
When implementing readUTF8Line, it wasn't taken into account that the `readBuffer` getter is not guaranteed to return the exact same buffer state between accesses, even if `awaitContent` wasn't called. The specific implementation of ByteChannel from ktor tried to fill the buffer on access whenever it happens to be exhausted and some data was available in the "flush" buffer. This could lead to more data being read in the line reader loop than expected, resulting in an invalid payload and broken framing in the LSP protocol framing. Now we read sequentially byte after byte. The new implementation is an almost verbatim copy of the readUTF8LineTo method from the ktor utils. GitOrigin-RevId: fe0cd982e94be15c6281462aa9c360d7472512dd
1 parent f768344 commit f386554

File tree

1 file changed

+40
-14
lines changed
  • fleet/lsp.protocol/srcCommonMain/com/jetbrains/lsp/implementation

1 file changed

+40
-14
lines changed

fleet/lsp.protocol/srcCommonMain/com/jetbrains/lsp/implementation/io.kt

Lines changed: 40 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -31,26 +31,52 @@ fun ByteReader.cancel() {
3131
cancel(IOException("Channel was cancelled"))
3232
}
3333

34+
private const val CR = 0x0D.toByte()
35+
private const val LF = 0x0A.toByte()
36+
37+
// The implementation is derived from ByteReadChannel#readUTF8LineTo.
38+
@OptIn(InternalIoApi::class)
3439
suspend fun ByteReader.readUTF8Line(): String? {
35-
val builder = StringBuilder()
36-
do {
37-
val linefeed = readBuffer.indexOf(0x0A)
38-
if (linefeed != -1L) {
39-
builder.append(readBuffer.readString(linefeed))
40-
if (builder.isNotEmpty() && builder[builder.length - 1] == '\r') {
41-
builder.deleteAt(builder.length - 1)
40+
val out = StringBuilder()
41+
val completed = run {
42+
Buffer().use { lineBuffer ->
43+
while (!isClosedForRead) {
44+
while (!readBuffer.exhausted()) {
45+
when (val b = readBuffer.readByte()) {
46+
CR -> {
47+
// Check if LF follows CR after awaiting.
48+
if (readBuffer.exhausted()) awaitContent()
49+
if (readBuffer.buffer[0] == LF) {
50+
readBuffer.buffer.skip(1)
51+
}
52+
else {
53+
throw IOException("Unexpected line ending <CR>")
54+
}
55+
out.append(lineBuffer.readString())
56+
return@run true
57+
}
58+
59+
LF -> {
60+
out.append(lineBuffer.readString())
61+
return@run true
62+
}
63+
64+
else -> lineBuffer.writeByte(b)
65+
}
66+
}
67+
68+
awaitContent()
4269
}
43-
check(readBuffer.readByte() == 0x0A.toByte()) { "expected to see the previously found line terminator" }
4470

45-
return builder.toString()
71+
(lineBuffer.size > 0).also { remaining ->
72+
if (remaining) {
73+
out.append(lineBuffer.readString())
74+
}
75+
}
4676
}
47-
48-
builder.append(readBuffer.readString())
4977
}
50-
while (awaitContent())
5178

52-
// Line terminator was never found before the byte stream was closed.
53-
return null
79+
return if (completed) out.toString() else null
5480
}
5581

5682
suspend fun ByteReader.readByteArray(count: Int): ByteArray {

0 commit comments

Comments
 (0)