Avoid allocating large buffers in File.writeText #KT-51058

Also fixes File.appendText and Path.write/appendText.


Merge-request: KT-MR-12804
Merged-by: Abduqodiri Qurbonzoda <abduqodiri.qurbonzoda@jetbrains.com>
This commit is contained in:
Abduqodiri Qurbonzoda
2023-11-08 11:18:56 +00:00
committed by Space Team
parent 54ab3d39db
commit 999b0f7099
3 changed files with 191 additions and 5 deletions
@@ -10,6 +10,7 @@
package kotlin.io.path
import java.io.*
import java.nio.CharBuffer
import java.nio.charset.Charset
import java.nio.file.Files
import java.nio.file.OpenOption
@@ -168,7 +169,22 @@ public fun Path.readText(charset: Charset = Charsets.UTF_8): String =
@WasExperimental(ExperimentalPathApi::class)
@Throws(IOException::class)
public fun Path.writeText(text: CharSequence, charset: Charset = Charsets.UTF_8, vararg options: OpenOption) {
Files.newOutputStream(this, *options).writer(charset).use { it.append(text) }
Files.newOutputStream(this, *options).use { out ->
if (text is String) {
out.writeTextImpl(text, charset)
return@use
}
val encoder = charset.newReplaceEncoder()
val charBuffer = if (text is CharBuffer) text.asReadOnlyBuffer() else CharBuffer.wrap(text)
val byteBuffer = byteBufferForEncoding(chunkSize = minOf(text.length, DEFAULT_BUFFER_SIZE), encoder)
while (charBuffer.hasRemaining()) {
encoder.encode(charBuffer, byteBuffer, /*endOfInput = */true).also { check(!it.isError) }
out.write(byteBuffer.array(), 0, byteBuffer.position())
byteBuffer.clear()
}
}
}
/**
@@ -181,7 +197,7 @@ public fun Path.writeText(text: CharSequence, charset: Charset = Charsets.UTF_8,
@WasExperimental(ExperimentalPathApi::class)
@Throws(IOException::class)
public fun Path.appendText(text: CharSequence, charset: Charset = Charsets.UTF_8) {
Files.newOutputStream(this, StandardOpenOption.APPEND).writer(charset).use { it.append(text) }
writeText(text, charset, StandardOpenOption.APPEND)
}
/**
@@ -5,12 +5,130 @@
package kotlin.jdk7.test
import java.io.ByteArrayOutputStream
import java.nio.CharBuffer
import java.nio.charset.Charset
import java.nio.file.Path
import java.nio.file.StandardOpenOption
import kotlin.io.path.*
import kotlin.random.Random
import kotlin.test.*
class PathReadWriteTest : AbstractPathTest() {
private fun String.encodeToByteArray(charset: Charset): ByteArray {
val out = ByteArrayOutputStream()
out.writer(charset).use { it.append(this) }
return out.toByteArray()
}
private val hexFormat = HexFormat {
bytes.bytesPerLine = 32
bytes.bytesPerGroup = 8
}
private fun Path.testContentEquals(expectedContent: ByteArray, charset: Charset) {
val expected = expectedContent.toHexString(hexFormat)
val actualContent = readBytes()
val actual = actualContent.toHexString(hexFormat)
assertEquals(expected, actual, "$charset. Expected size is ${expectedContent.size}, actual size is ${actualContent.size}")
}
private fun Path.testWriteText(text: String, charset: Charset) {
// Path.writeText
val encodedText = text.encodeToByteArray(charset)
writeText(text, charset)
testContentEquals(encodedText, charset)
writeText(StringBuilder(text), charset)
testContentEquals(encodedText, charset)
val position = 1.coerceAtMost(text.length)
val limit = (text.length - 1).coerceAtLeast(position)
val charBuffer = CharBuffer.wrap(text, position, limit)
val encodedCharBuffer = text.substring(position, limit).encodeToByteArray(charset)
writeText(charBuffer, charset)
testContentEquals(encodedCharBuffer, charset)
assertEquals(position, charBuffer.position())
assertEquals(limit, charBuffer.limit())
assertEquals(text.length, charBuffer.capacity())
// Path.appendText
val prefix = "_"
val encodedPrefix = prefix.encodeToByteArray(charset)
writeText(prefix, charset)
appendText(text, charset)
testContentEquals(encodedPrefix + encodedText, charset)
writeText(prefix, charset)
appendText(StringBuilder(text), charset)
testContentEquals(encodedPrefix + encodedText, charset)
writeText(prefix, charset)
appendText(charBuffer, charset)
testContentEquals(encodedPrefix + encodedCharBuffer, charset)
assertEquals(position, charBuffer.position())
assertEquals(limit, charBuffer.limit())
assertEquals(text.length, charBuffer.capacity())
// File.writeText
toFile().writeText(text, charset)
testContentEquals(encodedText, charset)
// File.appendText
toFile().writeText(prefix, charset)
toFile().appendText(text, charset)
testContentEquals(encodedPrefix + encodedText, charset)
}
@Test fun writeText() {
val charsets = listOf(
Charsets.UTF_8,
Charsets.UTF_16,
Charsets.UTF_32,
Charsets.ISO_8859_1,
Charsets.US_ASCII,
)
val highSurrogate = Char.MIN_HIGH_SURROGATE
val lowSurrogate = Char.MIN_LOW_SURROGATE
val smallString = "Hello"
val chunkSize = DEFAULT_BUFFER_SIZE
val string = "k".repeat(chunkSize - 1)
val path = createTempFile().cleanup()
for (charset in charsets) {
path.testWriteText("$highSurrogate", charset)
path.testWriteText("$lowSurrogate", charset)
path.testWriteText("$smallString$highSurrogate", charset)
path.testWriteText("$smallString$lowSurrogate", charset)
path.testWriteText("$string$highSurrogate", charset)
path.testWriteText("$string$lowSurrogate", charset)
path.testWriteText("$string$highSurrogate$lowSurrogate$string", charset)
path.testWriteText("$string$lowSurrogate$highSurrogate$string", charset)
path.testWriteText(
"$string$highSurrogate$lowSurrogate${string.substring(2)}$highSurrogate$lowSurrogate",
charset
)
path.testWriteText("$string$lowSurrogate$highSurrogate$lowSurrogate$string", charset)
}
}
@Test
fun appendText() {
val file = createTempFile().cleanup()