diff --git a/library/core/api/core.api b/library/core/api/core.api index 395c630a..362494f1 100644 --- a/library/core/api/core.api +++ b/library/core/api/core.api @@ -250,3 +250,8 @@ public final class io/matthewnelson/encoding/core/util/LineBreakOutFeed : io/mat public final fun reset ()V } +public final class io/matthewnelson/encoding/core/util/TextUtil { + public static final fun wipe (Ljava/lang/StringBuilder;)Ljava/lang/StringBuilder; + public static final fun wipe (Ljava/lang/StringBuilder;I)Ljava/lang/StringBuilder; +} + diff --git a/library/core/api/core.klib.api b/library/core/api/core.klib.api index 81e95cf3..3bfdf41a 100644 --- a/library/core/api/core.klib.api +++ b/library/core/api/core.klib.api @@ -270,4 +270,6 @@ sealed class <#A: io.matthewnelson.encoding.core/EncoderDecoder.Config> io.matth } final fun (io.matthewnelson.encoding.core/EncoderDecoder.Feed<*>?).io.matthewnelson.encoding.core/useFinally(kotlin/Throwable?) // io.matthewnelson.encoding.core/useFinally|useFinally@io.matthewnelson.encoding.core.EncoderDecoder.Feed<*>?(kotlin.Throwable?){}[0] +final fun (kotlin.text/StringBuilder).io.matthewnelson.encoding.core.util/wipe(kotlin/Int): kotlin.text/StringBuilder // io.matthewnelson.encoding.core.util/wipe|wipe@kotlin.text.StringBuilder(kotlin.Int){}[0] +final inline fun (kotlin.text/StringBuilder).io.matthewnelson.encoding.core.util/wipe(): kotlin.text/StringBuilder // io.matthewnelson.encoding.core.util/wipe|wipe@kotlin.text.StringBuilder(){}[0] final inline fun <#A: io.matthewnelson.encoding.core/EncoderDecoder.Config, #B: io.matthewnelson.encoding.core/EncoderDecoder.Feed<#A>?, #C: kotlin/Any?> (#B).io.matthewnelson.encoding.core/use(kotlin/Function1<#B, #C>): #C // io.matthewnelson.encoding.core/use|use@0:1(kotlin.Function1<0:1,0:2>){0§;1§?>;2§}[0] diff --git a/library/core/build.gradle.kts b/library/core/build.gradle.kts index c2995752..509f0b48 100644 --- a/library/core/build.gradle.kts +++ b/library/core/build.gradle.kts @@ -19,5 +19,19 @@ plugins { } kmpConfiguration { - configureShared(java9ModuleName = "io.matthewnelson.encoding.core", publish = true) {} + configureShared(java9ModuleName = "io.matthewnelson.encoding.core", publish = true) { + kotlin { + with(sourceSets) { + val sets = arrayOf( + "jvm", + "native", + "wasmJs", + "wasmWasi", + ).mapNotNull { name -> findByName(name + "Test") } + if (sets.isEmpty()) return@kotlin + val test = maybeCreate("nonJsTest").apply { dependsOn(getByName("commonTest")) } + sets.forEach { t -> t.dependsOn(test) } + } + } + } } diff --git a/library/core/src/commonMain/kotlin/io/matthewnelson/encoding/core/Encoder.kt b/library/core/src/commonMain/kotlin/io/matthewnelson/encoding/core/Encoder.kt index 7e5e903d..9a7964b6 100644 --- a/library/core/src/commonMain/kotlin/io/matthewnelson/encoding/core/Encoder.kt +++ b/library/core/src/commonMain/kotlin/io/matthewnelson/encoding/core/Encoder.kt @@ -21,6 +21,7 @@ import io.matthewnelson.encoding.core.internal.closedException import io.matthewnelson.encoding.core.internal.encode import io.matthewnelson.encoding.core.internal.encodeOutMaxSizeOrFail import io.matthewnelson.encoding.core.util.LineBreakOutFeed +import io.matthewnelson.encoding.core.util.wipe import kotlin.jvm.JvmField import kotlin.jvm.JvmName import kotlin.jvm.JvmStatic @@ -221,14 +222,8 @@ public sealed class Encoder(config: C): Decoder(con return encoder.encodeOutMaxSizeOrFail(size) { maxSize -> val sb = StringBuilder(maxSize) encoder.encode(this, sb::append) - val length = sb.length val result = sb.toString() - if (encoder.config.backFillBuffers) { - // Some implementations of StringBuilder do not overwrite buffered - // data when clear() is used. Must set to 0 length and do manually. - sb.setLength(0) - repeat(length) { sb.append(' ') } - } + if (encoder.config.backFillBuffers) sb.wipe() result } } diff --git a/library/core/src/commonMain/kotlin/io/matthewnelson/encoding/core/internal/-Helpers.kt b/library/core/src/commonMain/kotlin/io/matthewnelson/encoding/core/internal/-Helpers.kt index c2a5e54f..65119637 100644 --- a/library/core/src/commonMain/kotlin/io/matthewnelson/encoding/core/internal/-Helpers.kt +++ b/library/core/src/commonMain/kotlin/io/matthewnelson/encoding/core/internal/-Helpers.kt @@ -13,14 +13,31 @@ * See the License for the specific language governing permissions and * limitations under the License. **/ -@file:Suppress("KotlinRedundantDiagnosticSuppress") +@file:Suppress("NOTHING_TO_INLINE") package io.matthewnelson.encoding.core.internal -@Suppress("NOTHING_TO_INLINE") internal inline fun Char.isSpaceOrNewLine(): Boolean { return when(this) { '\n', '\r', ' ', '\t' -> true else -> false } } + +// Here for testing purposes. Implementation uses finalLen = 0 +internal inline fun StringBuilder.commonWipe(len: Int, finalLen: Int): StringBuilder { + setLength(0) + // Kotlin/Js returns StringBuilder.length for capacity() as there is + // no backing array. On all other platforms this will be the backing + // array size. So will always return here on Kotlin/Js b/c we just set + // length to 0. + @Suppress("DEPRECATION") + val cap = capacity() + if (cap == 0) return this + // All other platforms will set the new length from 0 to newLen, and + // in doing so will fill their backing arrays via Array.fill + val newLen = if (len !in 1..cap) cap else len + setLength(newLen) + setLength(finalLen) + return this +} diff --git a/library/core/src/commonMain/kotlin/io/matthewnelson/encoding/core/util/TextUtil.kt b/library/core/src/commonMain/kotlin/io/matthewnelson/encoding/core/util/TextUtil.kt new file mode 100644 index 00000000..6ac7b749 --- /dev/null +++ b/library/core/src/commonMain/kotlin/io/matthewnelson/encoding/core/util/TextUtil.kt @@ -0,0 +1,51 @@ +/* + * Copyright (c) 2025 Matthew Nelson + * + * 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 + * + * https://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. + **/ +@file:JvmName("TextUtil") +@file:Suppress("NOTHING_TO_INLINE") + +package io.matthewnelson.encoding.core.util + +import io.matthewnelson.encoding.core.internal.commonWipe +import kotlin.jvm.JvmName + +/** + * Wipes the [StringBuilder] backing array from index `0` to [StringBuilder.length] + * (exclusive) with the null character `\u0000`, and then sets it's length back to `0`. + * If [StringBuilder.length] is `0`, then [StringBuilder.capacity] will be used. + * + * On Kotlin/Js there is no backing array, so after setting length to `0` will return + * early. + * + * @return [StringBuilder] + * */ +public inline fun StringBuilder.wipe(): StringBuilder = wipe(length) + +/** + * Wipes the [StringBuilder] backing array from index `0` to [len] (exclusive) + * with the null character `\u0000`, and then sets it's length back to `0`. + * If [len] is less than `1` or greater than [StringBuilder.capacity], then + * [StringBuilder.capacity] is used in place of [len]. + * + * On Kotlin/Js there is no backing array, so after setting length to `0` will return + * early. + * + * @param [len] The length (exclusive), starting from index `0`, to wipe. If less + * than `1` or greater than [StringBuilder.capacity], then [StringBuilder.capacity] + * is used instead. + * + * @return [StringBuilder] + * */ +public fun StringBuilder.wipe(len: Int): StringBuilder = commonWipe(len, finalLen = 0) diff --git a/library/core/src/commonTest/kotlin/io/matthewnelson/encoding/core/util/TextUtilUnitTest.kt b/library/core/src/commonTest/kotlin/io/matthewnelson/encoding/core/util/TextUtilUnitTest.kt new file mode 100644 index 00000000..f8658f48 --- /dev/null +++ b/library/core/src/commonTest/kotlin/io/matthewnelson/encoding/core/util/TextUtilUnitTest.kt @@ -0,0 +1,43 @@ +/* + * Copyright (c) 2025 Matthew Nelson + * + * 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 + * + * https://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.matthewnelson.encoding.core.util + +import io.matthewnelson.encoding.core.internal.commonWipe +import kotlin.test.Test +import kotlin.test.assertEquals + +@Suppress("DEPRECATION") +class TextUtilUnitTest { + + @Test + fun givenStringBuilder_whenCapacity0_thenWipeDoesNothing() { + val sb = StringBuilder(0) + assertEquals(0, sb.capacity()) + // If we didn't return early (capacity == 0), + // finalLen of 1 would extend capacity. + sb.commonWipe(sb.length, finalLen = 1) + assertEquals(0, sb.capacity()) + assertEquals(0, sb.length) + } + + @Test + fun givenStringBuilder_whenWipe_thenSetsFinalLength0() { + val sb = StringBuilder(1).append('a') + assertEquals(1, sb.length) + sb.wipe() + assertEquals(0, sb.length) + } +} diff --git a/library/core/src/nonJsTest/kotlin/io/matthewnelson/encoding/core/util/TextUtilNonJsUnitTest.kt b/library/core/src/nonJsTest/kotlin/io/matthewnelson/encoding/core/util/TextUtilNonJsUnitTest.kt new file mode 100644 index 00000000..017cb37f --- /dev/null +++ b/library/core/src/nonJsTest/kotlin/io/matthewnelson/encoding/core/util/TextUtilNonJsUnitTest.kt @@ -0,0 +1,64 @@ +/* + * Copyright (c) 2025 Matthew Nelson + * + * 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 + * + * https://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.matthewnelson.encoding.core.util + +import io.matthewnelson.encoding.core.internal.commonWipe +import kotlin.test.Test +import kotlin.test.assertEquals + +class TextUtilNonJsUnitTest { + + @Test + fun givenStringBuilder_whenWipeLenLessThan1_thenUsesCapacity() { + val expected = 4 + val sb = StringBuilder(expected) + repeat(expected) { sb.append('a') } + assertEquals(expected, sb.capacity()) + assertEquals(expected, sb.length) + + // 0 + sb.commonWipe(len = 0, finalLen = 1) + assertEquals(expected, sb.capacity()) + // Confirm via finalLen that it did not return early + assertEquals(1, sb.length) + + repeat(expected - 1) { sb.append('a') } + assertEquals(expected, sb.capacity()) + assertEquals(expected, sb.length) + + // Try negative + sb.commonWipe(len = -1, finalLen = 1) + assertEquals(expected, sb.capacity()) + // Confirm via finalLen that it did not return early + assertEquals(1, sb.length) + } + + @Test + fun givenStringBuilder_whenWipeLenGreaterThanCapacity_thenUsesCapacity() { + val expected = 4 + val sb = StringBuilder(expected) + repeat(expected) { sb.append('a') } + assertEquals(expected, sb.capacity()) + assertEquals(expected, sb.length) + + // 0 + sb.commonWipe(len = expected + 1, finalLen = 1) + // backing array did not increase + assertEquals(expected, sb.capacity()) + // Confirm via finalLen that it did not return early + assertEquals(1, sb.length) + } +}