From 3752ee550a9233c2ec3a18cd334a7a6e97cd07aa Mon Sep 17 00:00:00 2001 From: Matthew Nelson Date: Sun, 7 Dec 2025 18:53:51 -0500 Subject: [PATCH 1/9] Add version info to deprecation documentation --- .../component/encoding/base16/Base16.kt | 10 +++--- .../matthewnelson/encoding/base16/Base16.kt | 4 +-- .../matthewnelson/encoding/base16/Builders.kt | 10 +++--- .../component/encoding/base32/Base32.kt | 32 +++++++++---------- .../matthewnelson/encoding/base32/Base32.kt | 12 +++---- .../matthewnelson/encoding/base32/Builders.kt | 30 ++++++++--------- .../matthewnelson/component/base64/Base64.kt | 18 +++++------ .../matthewnelson/encoding/base64/Base64.kt | 4 +-- .../matthewnelson/encoding/base64/Builders.kt | 10 +++--- .../io/matthewnelson/encoding/core/Decoder.kt | 6 ++-- .../io/matthewnelson/encoding/core/Encoder.kt | 4 +-- .../encoding/core/EncoderDecoder.kt | 6 ++-- .../core/internal/InternalEncodingApi.kt | 2 +- .../encoding/core/util/CTCase.kt | 2 +- .../encoding/core/util/DecoderAction.kt | 4 +-- .../encoding/core/util/DecoderInput.kt | 2 +- 16 files changed, 78 insertions(+), 78 deletions(-) diff --git a/library/base16/src/commonMain/kotlin/io/matthewnelson/component/encoding/base16/Base16.kt b/library/base16/src/commonMain/kotlin/io/matthewnelson/component/encoding/base16/Base16.kt index f7debc8f..76366abd 100644 --- a/library/base16/src/commonMain/kotlin/io/matthewnelson/component/encoding/base16/Base16.kt +++ b/library/base16/src/commonMain/kotlin/io/matthewnelson/component/encoding/base16/Base16.kt @@ -24,7 +24,7 @@ import io.matthewnelson.encoding.core.Encoder.Companion.encodeToCharArray import io.matthewnelson.encoding.core.Encoder.Companion.encodeToString /** - * DEPRECATED + * DEPRECATED since `1.2.0` * @suppress * */ @Deprecated( @@ -43,7 +43,7 @@ public inline fun String.decodeBase16ToArray(): ByteArray? { } /** - * DEPRECATED + * DEPRECATED since `1.2.0` * @suppress * */ @Deprecated( @@ -62,7 +62,7 @@ public fun CharArray.decodeBase16ToArray(): ByteArray? { } /** - * DEPRECATED + * DEPRECATED since `1.2.0` * @suppress * */ @Deprecated( @@ -81,7 +81,7 @@ public inline fun ByteArray.encodeBase16(): String { } /** - * DEPRECATED + * DEPRECATED since `1.2.0` * @suppress * */ @Deprecated( @@ -100,7 +100,7 @@ public inline fun ByteArray.encodeBase16ToCharArray(): CharArray { } /** - * DEPRECATED + * DEPRECATED since `1.2.0` * @suppress * */ @Deprecated( diff --git a/library/base16/src/commonMain/kotlin/io/matthewnelson/encoding/base16/Base16.kt b/library/base16/src/commonMain/kotlin/io/matthewnelson/encoding/base16/Base16.kt index 34f94b68..0e05e4cf 100644 --- a/library/base16/src/commonMain/kotlin/io/matthewnelson/encoding/base16/Base16.kt +++ b/library/base16/src/commonMain/kotlin/io/matthewnelson/encoding/base16/Base16.kt @@ -228,7 +228,7 @@ public class Base16: EncoderDecoder { } /** - * DEPRECATED + * DEPRECATED since `2.6.0` * @suppress * */ @JvmField @@ -385,7 +385,7 @@ public class Base16: EncoderDecoder { } /** - * DEPRECATED + * DEPRECATED since `2.6.0` * @suppress * */ @Deprecated( diff --git a/library/base16/src/commonMain/kotlin/io/matthewnelson/encoding/base16/Builders.kt b/library/base16/src/commonMain/kotlin/io/matthewnelson/encoding/base16/Builders.kt index 66077538..9192d324 100644 --- a/library/base16/src/commonMain/kotlin/io/matthewnelson/encoding/base16/Builders.kt +++ b/library/base16/src/commonMain/kotlin/io/matthewnelson/encoding/base16/Builders.kt @@ -23,7 +23,7 @@ import kotlin.jvm.JvmOverloads import kotlin.jvm.JvmSynthetic /** - * DEPRECATED + * DEPRECATED since `2.6.0` * @suppress * @see [Base16.Builder] * @see [Base16.Companion.Builder] @@ -38,7 +38,7 @@ public fun Base16( ): Base16 = Base16ConfigBuilder(config).apply(block).buildCompat() /** - * DEPRECATED + * DEPRECATED since `2.6.0` * @suppress * @see [Base16.Builder] * @see [Base16.Companion.Builder] @@ -52,7 +52,7 @@ public fun Base16( ): Base16 = Base16(config = null, block) /** - * DEPRECATED + * DEPRECATED since `2.6.0` * @suppress * @see [Base16.Builder] * @see [Base16.Companion.Builder] @@ -67,7 +67,7 @@ public fun Base16( ): Base16 = Base16.Builder { if (strict) strictSpec() } /** - * DEPRECATED + * DEPRECATED since `2.6.0` * @suppress * @see [Base16.Builder] * @see [Base16.Companion.Builder] @@ -129,7 +129,7 @@ public class Base16ConfigBuilder { .build() /** - * DEPRECATED + * DEPRECATED since `2.3.1` * @suppress * */ @JvmField diff --git a/library/base32/src/commonMain/kotlin/io/matthewnelson/component/encoding/base32/Base32.kt b/library/base32/src/commonMain/kotlin/io/matthewnelson/component/encoding/base32/Base32.kt index bd53f8ea..c0b9e143 100644 --- a/library/base32/src/commonMain/kotlin/io/matthewnelson/component/encoding/base32/Base32.kt +++ b/library/base32/src/commonMain/kotlin/io/matthewnelson/component/encoding/base32/Base32.kt @@ -24,7 +24,7 @@ import io.matthewnelson.encoding.core.Encoder.Companion.encodeToString import kotlin.jvm.JvmOverloads /** - * DEPRECATED + * DEPRECATED since `1.2.0` * @suppress * */ @Deprecated( @@ -71,7 +71,7 @@ public sealed class Base32 { } /** - * DEPRECATED + * DEPRECATED since `1.2.0` * @suppress * */ @Deprecated( @@ -91,7 +91,7 @@ public inline fun String.decodeBase32ToArray(base32: Base32.Default = Base32.Def } /** - * DEPRECATED + * DEPRECATED since `1.2.0` * @suppress * */ @Deprecated( @@ -110,7 +110,7 @@ public inline fun String.decodeBase32ToArray(base32: Base32.Hex): ByteArray? { } /** - * DEPRECATED + * DEPRECATED since `1.2.0` * @suppress * */ @Deprecated( @@ -129,7 +129,7 @@ public inline fun String.decodeBase32ToArray(base32: Base32.Crockford): ByteArra } /** - * DEPRECATED + * DEPRECATED since `1.2.0` * @suppress * */ @Deprecated( @@ -149,7 +149,7 @@ public fun CharArray.decodeBase32ToArray(base32: Base32.Default = Base32.Default } /** - * DEPRECATED + * DEPRECATED since `1.2.0` * @suppress * */ @Deprecated( @@ -168,7 +168,7 @@ public fun CharArray.decodeBase32ToArray(base32: Base32.Hex): ByteArray? { } /** - * DEPRECATED + * DEPRECATED since `1.2.0` * @suppress * */ @Deprecated( @@ -187,7 +187,7 @@ public fun CharArray.decodeBase32ToArray(base32: Base32.Crockford): ByteArray? { } /** - * DEPRECATED + * DEPRECATED since `1.2.0` * @suppress * */ @Deprecated( @@ -207,7 +207,7 @@ public inline fun ByteArray.encodeBase32(base32: Base32.Default = Base32.Default } /** - * DEPRECATED + * DEPRECATED since `1.2.0` * @suppress * */ @Deprecated( @@ -226,7 +226,7 @@ public inline fun ByteArray.encodeBase32(base32: Base32.Hex): String { } /** - * DEPRECATED + * DEPRECATED since `1.2.0` * @suppress * */ @Deprecated( @@ -245,7 +245,7 @@ public inline fun ByteArray.encodeBase32(base32: Base32.Crockford): String { } /** - * DEPRECATED + * DEPRECATED since `1.2.0` * @suppress * */ @Deprecated( @@ -265,7 +265,7 @@ public inline fun ByteArray.encodeBase32ToCharArray(base32: Base32.Default = Bas } /** - * DEPRECATED + * DEPRECATED since `1.2.0` * @suppress * */ @Deprecated( @@ -284,7 +284,7 @@ public inline fun ByteArray.encodeBase32ToCharArray(base32: Base32.Hex): CharArr } /** - * DEPRECATED + * DEPRECATED since `1.2.0` * @suppress * */ @Deprecated( @@ -303,7 +303,7 @@ public inline fun ByteArray.encodeBase32ToCharArray(base32: Base32.Crockford): C } /** - * DEPRECATED + * DEPRECATED since `1.2.0` * @suppress * */ @Deprecated( @@ -323,7 +323,7 @@ public fun ByteArray.encodeBase32ToByteArray(base32: Base32.Default = Base32.Def } /** - * DEPRECATED + * DEPRECATED since `1.2.0` * @suppress * */ @Deprecated( @@ -342,7 +342,7 @@ public fun ByteArray.encodeBase32ToByteArray(base32: Base32.Hex): ByteArray { } /** - * DEPRECATED + * DEPRECATED since `1.2.0` * @suppress * */ @Deprecated( diff --git a/library/base32/src/commonMain/kotlin/io/matthewnelson/encoding/base32/Base32.kt b/library/base32/src/commonMain/kotlin/io/matthewnelson/encoding/base32/Base32.kt index 4ed2ada8..ea0342bf 100644 --- a/library/base32/src/commonMain/kotlin/io/matthewnelson/encoding/base32/Base32.kt +++ b/library/base32/src/commonMain/kotlin/io/matthewnelson/encoding/base32/Base32.kt @@ -328,7 +328,7 @@ public sealed class Base32(config: C): EncoderDecoder< } /** - * DEPRECATED + * DEPRECATED since `2.6.0` * @suppress * */ @JvmField @@ -435,7 +435,7 @@ public sealed class Base32(config: C): EncoderDecoder< } /** - * DEPRECATED + * DEPRECATED since `2.6.0` * @suppress * */ @Deprecated( @@ -661,7 +661,7 @@ public sealed class Base32(config: C): EncoderDecoder< } /** - * DEPRECATED + * DEPRECATED since `2.6.0` * @suppress * */ @JvmField @@ -760,7 +760,7 @@ public sealed class Base32(config: C): EncoderDecoder< } /** - * DEPRECATED + * DEPRECATED since `2.6.0` * @suppress * */ @Deprecated( @@ -986,7 +986,7 @@ public sealed class Base32(config: C): EncoderDecoder< } /** - * DEPRECATED + * DEPRECATED since `2.6.0` * @suppress * */ @JvmField @@ -1085,7 +1085,7 @@ public sealed class Base32(config: C): EncoderDecoder< } /** - * DEPRECATED + * DEPRECATED since `2.6.0` * @suppress * */ @Deprecated( diff --git a/library/base32/src/commonMain/kotlin/io/matthewnelson/encoding/base32/Builders.kt b/library/base32/src/commonMain/kotlin/io/matthewnelson/encoding/base32/Builders.kt index 3a1b7ae6..cf9cdae7 100644 --- a/library/base32/src/commonMain/kotlin/io/matthewnelson/encoding/base32/Builders.kt +++ b/library/base32/src/commonMain/kotlin/io/matthewnelson/encoding/base32/Builders.kt @@ -26,7 +26,7 @@ import kotlin.jvm.JvmOverloads import kotlin.jvm.JvmSynthetic /** - * DEPRECATED + * DEPRECATED since `2.6.0` * @suppress * @see [Base32.Crockford.Builder] * @see [Base32.Crockford.Companion.Builder] @@ -41,7 +41,7 @@ public fun Base32Crockford( ): Base32.Crockford = Base32CrockfordConfigBuilder(config).apply(block).buildCompat() /** - * DEPRECATED + * DEPRECATED since `2.6.0` * @suppress * @see [Base32.Crockford.Builder] * @see [Base32.Crockford.Companion.Builder] @@ -55,7 +55,7 @@ public fun Base32Crockford( ): Base32.Crockford = Base32Crockford(config = null, block) /** - * DEPRECATED + * DEPRECATED since `2.6.0` * @suppress * @see [Base32.Crockford.Builder] * @see [Base32.Crockford.Companion.Builder] @@ -70,7 +70,7 @@ public fun Base32Crockford( ): Base32.Crockford = Base32Crockford { if (strict) strict() } /** - * DEPRECATED + * DEPRECATED since `2.6.0` * @suppress * @see [Base32.Default.Builder] * @see [Base32.Default.Companion.Builder] @@ -85,7 +85,7 @@ public fun Base32Default( ): Base32.Default = Base32DefaultConfigBuilder(config).apply(block).buildCompat() /** - * DEPRECATED + * DEPRECATED since `2.6.0` * @suppress * @see [Base32.Default.Builder] * @see [Base32.Default.Companion.Builder] @@ -99,7 +99,7 @@ public fun Base32Default( ): Base32.Default = Base32Default(config = null, block) /** - * DEPRECATED + * DEPRECATED since `2.6.0` * @suppress * @see [Base32.Default.Builder] * @see [Base32.Default.Companion.Builder] @@ -114,7 +114,7 @@ public fun Base32Default( ): Base32.Default = Base32.Default.Builder { if (strict) strictSpec() } /** - * DEPRECATED + * DEPRECATED since `2.6.0` * @suppress * @see [Base32.Hex.Builder] * @see [Base32.Hex.Companion.Builder] @@ -129,7 +129,7 @@ public fun Base32Hex( ): Base32.Hex = Base32HexConfigBuilder(config).apply(block).buildCompat() /** - * DEPRECATED + * DEPRECATED since `2.6.0` * @suppress * @see [Base32.Hex.Builder] * @see [Base32.Hex.Companion.Builder] @@ -143,7 +143,7 @@ public fun Base32Hex( ): Base32.Hex = Base32Hex(config = null, block) /** - * DEPRECATED + * DEPRECATED since `2.6.0` * @suppress * @see [Base32.Hex.Builder] * @see [Base32.Hex.Companion.Builder] @@ -158,7 +158,7 @@ public fun Base32Hex( ): Base32.Hex = Base32.Hex.Builder { if (strict) strictSpec() } /** - * DEPRECATED + * DEPRECATED since `2.6.0` * @suppress * @see [Base32.Crockford.Builder] * @see [Base32.Crockford.Companion.Builder] @@ -315,7 +315,7 @@ public class Base32CrockfordConfigBuilder { .build() /** - * DEPRECATED + * DEPRECATED since `2.3.1` * @suppress * */ @JvmField @@ -327,7 +327,7 @@ public class Base32CrockfordConfigBuilder { } /** - * DEPRECATED + * DEPRECATED since `2.6.0` * @suppress * @see [Base32.Default.Builder] * @see [Base32.Default.Companion.Builder] @@ -398,7 +398,7 @@ public class Base32DefaultConfigBuilder { .build() /** - * DEPRECATED + * DEPRECATED since `2.3.1` * @suppress * */ @JvmField @@ -410,7 +410,7 @@ public class Base32DefaultConfigBuilder { } /** - * DEPRECATED + * DEPRECATED since `2.6.0` * @suppress * @see [Base32.Hex.Builder] * @see [Base32.Hex.Companion.Builder] @@ -481,7 +481,7 @@ public class Base32HexConfigBuilder { .build() /** - * DEPRECATED + * DEPRECATED since `2.3.1` * @suppress * */ @JvmField diff --git a/library/base64/src/commonMain/kotlin/io/matthewnelson/component/base64/Base64.kt b/library/base64/src/commonMain/kotlin/io/matthewnelson/component/base64/Base64.kt index d4848a56..d08273a5 100644 --- a/library/base64/src/commonMain/kotlin/io/matthewnelson/component/base64/Base64.kt +++ b/library/base64/src/commonMain/kotlin/io/matthewnelson/component/base64/Base64.kt @@ -33,7 +33,7 @@ import io.matthewnelson.encoding.core.Encoder.Companion.encodeToString import kotlin.jvm.JvmOverloads /** - * DEPRECATED + * DEPRECATED since `1.2.0` * @suppress * */ @Deprecated( @@ -61,7 +61,7 @@ public sealed class Base64 { } /** - * DEPRECATED + * DEPRECATED since `1.2.0` * @suppress * */ @Deprecated( @@ -80,7 +80,7 @@ public inline fun String.decodeBase64ToArray(): ByteArray? { } /** - * DEPRECATED + * DEPRECATED since `1.2.0` * @suppress * */ @Deprecated( @@ -99,7 +99,7 @@ public fun CharArray.decodeBase64ToArray(): ByteArray? { } /** - * DEPRECATED + * DEPRECATED since `1.2.0` * @suppress * */ @Deprecated( @@ -119,7 +119,7 @@ public inline fun ByteArray.encodeBase64(base64: Base64.Default = Base64.Default } /** - * DEPRECATED + * DEPRECATED since `1.2.0` * @suppress * */ @Deprecated( @@ -141,7 +141,7 @@ public inline fun ByteArray.encodeBase64(base64: Base64.UrlSafe): String { } /** - * DEPRECATED + * DEPRECATED since `1.2.0` * @suppress * */ @Deprecated( @@ -161,7 +161,7 @@ public inline fun ByteArray.encodeBase64ToCharArray(base64: Base64.Default = Bas } /** - * DEPRECATED + * DEPRECATED since `1.2.0` * @suppress * */ @Deprecated( @@ -183,7 +183,7 @@ public inline fun ByteArray.encodeBase64ToCharArray(base64: Base64.UrlSafe): Cha } /** - * DEPRECATED + * DEPRECATED since `1.2.0` * @suppress * */ @Deprecated( @@ -203,7 +203,7 @@ public fun ByteArray.encodeBase64ToByteArray(base64: Base64.Default = Base64.Def } /** - * DEPRECATED + * DEPRECATED since `1.2.0` * @suppress * */ @Deprecated( diff --git a/library/base64/src/commonMain/kotlin/io/matthewnelson/encoding/base64/Base64.kt b/library/base64/src/commonMain/kotlin/io/matthewnelson/encoding/base64/Base64.kt index 1ef2e500..6047148e 100644 --- a/library/base64/src/commonMain/kotlin/io/matthewnelson/encoding/base64/Base64.kt +++ b/library/base64/src/commonMain/kotlin/io/matthewnelson/encoding/base64/Base64.kt @@ -303,7 +303,7 @@ public class Base64: EncoderDecoder { } /** - * DEPRECATED + * DEPRECATED since `2.6.0` * @suppress * */ @JvmField @@ -561,7 +561,7 @@ public class Base64: EncoderDecoder { } /** - * DEPRECATED + * DEPRECATED since `2.6.0` * @suppress * */ @Deprecated( diff --git a/library/base64/src/commonMain/kotlin/io/matthewnelson/encoding/base64/Builders.kt b/library/base64/src/commonMain/kotlin/io/matthewnelson/encoding/base64/Builders.kt index d3802109..2b41806a 100644 --- a/library/base64/src/commonMain/kotlin/io/matthewnelson/encoding/base64/Builders.kt +++ b/library/base64/src/commonMain/kotlin/io/matthewnelson/encoding/base64/Builders.kt @@ -22,7 +22,7 @@ import kotlin.jvm.JvmOverloads import kotlin.jvm.JvmSynthetic /** - * DEPRECATED + * DEPRECATED since `2.6.0` * @suppress * @see [Base64.Builder] * @see [Base64.Companion.Builder] @@ -37,7 +37,7 @@ public fun Base64( ): Base64 = Base64ConfigBuilder(config).apply(block).buildCompat() /** - * DEPRECATED + * DEPRECATED since `2.6.0` * @suppress * @see [Base64.Builder] * @see [Base64.Companion.Builder] @@ -51,7 +51,7 @@ public fun Base64( ): Base64 = Base64(null, block) /** - * DEPRECATED + * DEPRECATED since `2.6.0` * @suppress * @see [Base64.Builder] * @see [Base64.Companion.Builder] @@ -66,7 +66,7 @@ public fun Base64( ): Base64 = Base64.Builder { if (strict) strictSpec() } /** - * DEPRECATED + * DEPRECATED since `2.6.0` * @suppress * @see [Base64.Builder] * @see [Base64.Companion.Builder] @@ -135,7 +135,7 @@ public class Base64ConfigBuilder { .build() /** - * DEPRECATED + * DEPRECATED since `2.3.1` * @suppress * */ @JvmField diff --git a/library/core/src/commonMain/kotlin/io/matthewnelson/encoding/core/Decoder.kt b/library/core/src/commonMain/kotlin/io/matthewnelson/encoding/core/Decoder.kt index 6ba9aac3..5cf56481 100644 --- a/library/core/src/commonMain/kotlin/io/matthewnelson/encoding/core/Decoder.kt +++ b/library/core/src/commonMain/kotlin/io/matthewnelson/encoding/core/Decoder.kt @@ -171,7 +171,7 @@ public sealed class Decoder(public val config: C) { public final override fun toString(): String = "${this@Decoder}.Decoder.Feed@${hashCode()}" /** - * DEPRECATED + * DEPRECATED since `2.6.0` * @suppress * */ @Deprecated( @@ -251,7 +251,7 @@ public sealed class Decoder(public val config: C) { } /** - * DEPRECATED + * DEPRECATED since `2.3.0` * @suppress * */ @JvmStatic @@ -268,7 +268,7 @@ public sealed class Decoder(public val config: C) { } /** - * DEPRECATED + * DEPRECATED since `2.3.0` * @suppress * */ @JvmStatic 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 795aaef9..adf7261a 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 @@ -161,7 +161,7 @@ public sealed class Encoder(config: C): Decoder(con public final override fun toString(): String = "${this@Encoder}.Encoder.Feed@${hashCode()}" /** - * DEPRECATED + * DEPRECATED since `2.6.0` * @suppress * */ @Deprecated( @@ -241,7 +241,7 @@ public sealed class Encoder(config: C): Decoder(con } /** - * DEPRECATED + * DEPRECATED since `2.3.0` * @suppress * */ @JvmStatic diff --git a/library/core/src/commonMain/kotlin/io/matthewnelson/encoding/core/EncoderDecoder.kt b/library/core/src/commonMain/kotlin/io/matthewnelson/encoding/core/EncoderDecoder.kt index 48cab1de..783a5b99 100644 --- a/library/core/src/commonMain/kotlin/io/matthewnelson/encoding/core/EncoderDecoder.kt +++ b/library/core/src/commonMain/kotlin/io/matthewnelson/encoding/core/EncoderDecoder.kt @@ -463,7 +463,7 @@ public abstract class EncoderDecoder(config: C): Encod } /** - * DEPRECATED + * DEPRECATED since `2.6.0` * @see [encodeOutMaxSize] * @suppress * */ @@ -475,7 +475,7 @@ public abstract class EncoderDecoder(config: C): Encod public fun encodeOutSize(unEncodedSize: Long): Long = encodeOutMaxSize(unEncodedSize, lineBreakInterval) /** - * DEPRECATED + * DEPRECATED since `2.6.0` * @see [encodeOutMaxSize] * @suppress * */ @@ -487,7 +487,7 @@ public abstract class EncoderDecoder(config: C): Encod public fun encodeOutSize(unEncodedSize: Long, lineBreakInterval: Byte): Long = encodeOutMaxSize(unEncodedSize, lineBreakInterval) /** - * DEPRECATED + * DEPRECATED since `2.6.0` * @suppress * */ @Deprecated( diff --git a/library/core/src/commonMain/kotlin/io/matthewnelson/encoding/core/internal/InternalEncodingApi.kt b/library/core/src/commonMain/kotlin/io/matthewnelson/encoding/core/internal/InternalEncodingApi.kt index 7fbb26e7..d2f9de39 100644 --- a/library/core/src/commonMain/kotlin/io/matthewnelson/encoding/core/internal/InternalEncodingApi.kt +++ b/library/core/src/commonMain/kotlin/io/matthewnelson/encoding/core/internal/InternalEncodingApi.kt @@ -18,7 +18,7 @@ package io.matthewnelson.encoding.core.internal /** - * DEPRECATED + * DEPRECATED since `2.2.0` * @suppress * */ @RequiresOptIn diff --git a/library/core/src/commonMain/kotlin/io/matthewnelson/encoding/core/util/CTCase.kt b/library/core/src/commonMain/kotlin/io/matthewnelson/encoding/core/util/CTCase.kt index ebb0ed64..a474b18d 100644 --- a/library/core/src/commonMain/kotlin/io/matthewnelson/encoding/core/util/CTCase.kt +++ b/library/core/src/commonMain/kotlin/io/matthewnelson/encoding/core/util/CTCase.kt @@ -18,7 +18,7 @@ package io.matthewnelson.encoding.core.util import kotlin.jvm.JvmField /** - * DEPRECATED + * DEPRECATED since `2.3.1` * @suppress * */ @Deprecated( diff --git a/library/core/src/commonMain/kotlin/io/matthewnelson/encoding/core/util/DecoderAction.kt b/library/core/src/commonMain/kotlin/io/matthewnelson/encoding/core/util/DecoderAction.kt index ad937f75..c4e8652d 100644 --- a/library/core/src/commonMain/kotlin/io/matthewnelson/encoding/core/util/DecoderAction.kt +++ b/library/core/src/commonMain/kotlin/io/matthewnelson/encoding/core/util/DecoderAction.kt @@ -18,7 +18,7 @@ package io.matthewnelson.encoding.core.util import kotlin.jvm.JvmField /** - * DEPRECATED + * DEPRECATED since `2.3.1` * @suppress * */ @Deprecated( @@ -30,7 +30,7 @@ public fun interface DecoderAction { public fun convert(input: Char): Int /** - * DEPRECATED + * DEPRECATED since `2.3.1` * @suppress * */ @Deprecated( diff --git a/library/core/src/commonMain/kotlin/io/matthewnelson/encoding/core/util/DecoderInput.kt b/library/core/src/commonMain/kotlin/io/matthewnelson/encoding/core/util/DecoderInput.kt index f6751012..8377fb09 100644 --- a/library/core/src/commonMain/kotlin/io/matthewnelson/encoding/core/util/DecoderInput.kt +++ b/library/core/src/commonMain/kotlin/io/matthewnelson/encoding/core/util/DecoderInput.kt @@ -51,7 +51,7 @@ public class DecoderInput { } /** - * DEPRECATED + * DEPRECATED since `2.3.0` * @suppress * */ @Deprecated( From 4dad1f99e1157d42d8822c7746287862f5ffddd4 Mon Sep 17 00:00:00 2001 From: Matthew Nelson Date: Sun, 7 Dec 2025 18:56:22 -0500 Subject: [PATCH 2/9] Move parameter linBreakInterval to above secondary constructor (for better readability) --- .../encoding/core/EncoderDecoder.kt | 28 +++++++++---------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/library/core/src/commonMain/kotlin/io/matthewnelson/encoding/core/EncoderDecoder.kt b/library/core/src/commonMain/kotlin/io/matthewnelson/encoding/core/EncoderDecoder.kt index 783a5b99..0c52f29f 100644 --- a/library/core/src/commonMain/kotlin/io/matthewnelson/encoding/core/EncoderDecoder.kt +++ b/library/core/src/commonMain/kotlin/io/matthewnelson/encoding/core/EncoderDecoder.kt @@ -100,6 +100,20 @@ public abstract class EncoderDecoder(config: C): Encod @Suppress("UNUSED", "UNUSED_PARAMETER") unused: Any?, ) { + /** + * If greater than `0`, [Encoder.newEncoderFeed] will use the [LineBreakOutFeed] such that + * for every [lineBreakInterval] number of encoded characters output, the next encoded + * character will be preceded with the new line character `\n`. + * + * **NOTE:** This setting will always be `0` if [isLenient] is `false`. + * */ + @JvmField + public val lineBreakInterval: Byte = if (isLenient != false && lineBreakInterval > 0) { + lineBreakInterval + } else { + 0 + } + /** * Instantiates a new [Config] instance. * @@ -122,20 +136,6 @@ public abstract class EncoderDecoder(config: C): Encod require(maxDecodeEmit > 0) { "maxDecodeEmit must be greater than 0" } } - /** - * If greater than `0`, [Encoder.newEncoderFeed] will use the [LineBreakOutFeed] such that - * for every [lineBreakInterval] number of encoded characters output, the next encoded - * character will be preceded with the new line character `\n`. - * - * **NOTE:** This setting will always be `0` if [isLenient] is `false`. - * */ - @JvmField - public val lineBreakInterval: Byte = if (isLenient != false && lineBreakInterval > 0) { - lineBreakInterval - } else { - 0 - } - /** * Pre-calculates and returns the maximum size of the output, after encoding would occur, * based off the [Config] options set for the implementation. Most implementations (such From 373031aa0b1b9cb496b5f843437b4966cb699930 Mon Sep 17 00:00:00 2001 From: Matthew Nelson Date: Mon, 8 Dec 2025 05:39:30 -0500 Subject: [PATCH 3/9] Add TODO items --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index 521bd169..f4ef8ad2 100644 --- a/README.md +++ b/README.md @@ -39,6 +39,7 @@ Configurable, streamable, efficient and extensible encoding/decoding for Kotlin ### Get Started + ```kotlin // build.gradle.kts @@ -54,6 +55,7 @@ dependencies { ``` + Alternatively, you can use the BOM. ```kotlin From 9f51eb600b92cad05d207d71a1857bf03f425471 Mon Sep 17 00:00:00 2001 From: Matthew Nelson Date: Mon, 8 Dec 2025 06:01:34 -0500 Subject: [PATCH 4/9] WIP: Stub in decodeBuffered API --- library/core/api/core.api | 21 +++ library/core/api/core.klib.api | 13 ++ .../io/matthewnelson/encoding/core/Decoder.kt | 131 ++++++++++++++++-- .../io/matthewnelson/encoding/core/Encoder.kt | 4 +- .../encoding/core/EncoderDecoder.kt | 73 +++++----- .../encoding/core/internal/-EncoderDecoder.kt | 109 +++++++++++++-- 6 files changed, 293 insertions(+), 58 deletions(-) diff --git a/library/core/api/core.api b/library/core/api/core.api index bef9e1cd..47e7b5e5 100644 --- a/library/core/api/core.api +++ b/library/core/api/core.api @@ -1,6 +1,14 @@ public abstract class io/matthewnelson/encoding/core/Decoder { public static final field Companion Lio/matthewnelson/encoding/core/Decoder$Companion; public synthetic fun (Lio/matthewnelson/encoding/core/EncoderDecoder$Config;Lkotlin/jvm/internal/DefaultConstructorMarker;)V + public static final fun decodeBuffered (Ljava/lang/CharSequence;ILio/matthewnelson/encoding/core/Decoder;Lkotlin/jvm/functions/Function3;)J + public static final fun decodeBuffered (Ljava/lang/CharSequence;ILio/matthewnelson/encoding/core/Decoder;Lkotlin/jvm/functions/Function4;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public static final fun decodeBuffered (Ljava/lang/CharSequence;Lio/matthewnelson/encoding/core/Decoder;Lkotlin/jvm/functions/Function3;)J + public static final fun decodeBuffered (Ljava/lang/CharSequence;Lio/matthewnelson/encoding/core/Decoder;Lkotlin/jvm/functions/Function4;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public static final fun decodeBuffered ([CILio/matthewnelson/encoding/core/Decoder;Lkotlin/jvm/functions/Function3;)J + public static final fun decodeBuffered ([CILio/matthewnelson/encoding/core/Decoder;Lkotlin/jvm/functions/Function4;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public static final fun decodeBuffered ([CLio/matthewnelson/encoding/core/Decoder;Lkotlin/jvm/functions/Function3;)J + public static final fun decodeBuffered ([CLio/matthewnelson/encoding/core/Decoder;Lkotlin/jvm/functions/Function4;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; public static final fun decodeToByteArray (Ljava/lang/CharSequence;Lio/matthewnelson/encoding/core/Decoder;)[B public static final fun decodeToByteArray ([BLio/matthewnelson/encoding/core/Decoder;)[B public static final fun decodeToByteArray ([CLio/matthewnelson/encoding/core/Decoder;)[B @@ -13,6 +21,14 @@ public abstract class io/matthewnelson/encoding/core/Decoder { } public final class io/matthewnelson/encoding/core/Decoder$Companion { + public final fun decodeBuffered (Ljava/lang/CharSequence;ILio/matthewnelson/encoding/core/Decoder;Lkotlin/jvm/functions/Function3;)J + public final fun decodeBuffered (Ljava/lang/CharSequence;ILio/matthewnelson/encoding/core/Decoder;Lkotlin/jvm/functions/Function4;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public final fun decodeBuffered (Ljava/lang/CharSequence;Lio/matthewnelson/encoding/core/Decoder;Lkotlin/jvm/functions/Function3;)J + public final fun decodeBuffered (Ljava/lang/CharSequence;Lio/matthewnelson/encoding/core/Decoder;Lkotlin/jvm/functions/Function4;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public final fun decodeBuffered ([CILio/matthewnelson/encoding/core/Decoder;Lkotlin/jvm/functions/Function3;)J + public final fun decodeBuffered ([CILio/matthewnelson/encoding/core/Decoder;Lkotlin/jvm/functions/Function4;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public final fun decodeBuffered ([CLio/matthewnelson/encoding/core/Decoder;Lkotlin/jvm/functions/Function3;)J + public final fun decodeBuffered ([CLio/matthewnelson/encoding/core/Decoder;Lkotlin/jvm/functions/Function4;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; public final fun decodeToByteArray (Ljava/lang/CharSequence;Lio/matthewnelson/encoding/core/Decoder;)[B public final fun decodeToByteArray ([BLio/matthewnelson/encoding/core/Decoder;)[B public final fun decodeToByteArray ([CLio/matthewnelson/encoding/core/Decoder;)[B @@ -81,6 +97,8 @@ public final class io/matthewnelson/encoding/core/Encoder$OutFeed$Companion { } public abstract class io/matthewnelson/encoding/core/EncoderDecoder : io/matthewnelson/encoding/core/Encoder { + public static final field Companion Lio/matthewnelson/encoding/core/EncoderDecoder$Companion; + public static final field DEFAULT_BUFFER_SIZE I public fun (Lio/matthewnelson/encoding/core/EncoderDecoder$Config;)V public final fun equals (Ljava/lang/Object;)Z public final fun hashCode ()I @@ -88,6 +106,9 @@ public abstract class io/matthewnelson/encoding/core/EncoderDecoder : io/matthew public final fun toString ()Ljava/lang/String; } +public final class io/matthewnelson/encoding/core/EncoderDecoder$Companion { +} + public abstract class io/matthewnelson/encoding/core/EncoderDecoder$Config { public static final field Companion Lio/matthewnelson/encoding/core/EncoderDecoder$Config$Companion; public final field backFillBuffers Z diff --git a/library/core/api/core.klib.api b/library/core/api/core.klib.api index 5a572735..6262071f 100644 --- a/library/core/api/core.klib.api +++ b/library/core/api/core.klib.api @@ -92,6 +92,11 @@ abstract class <#A: io.matthewnelson.encoding.core/EncoderDecoder.Config> io.mat abstract fun isClosed(): kotlin/Boolean // io.matthewnelson.encoding.core/EncoderDecoder.Feed.isClosed|isClosed(){}[0] final fun doFinal() // io.matthewnelson.encoding.core/EncoderDecoder.Feed.doFinal|doFinal(){}[0] } + + final object Companion { // io.matthewnelson.encoding.core/EncoderDecoder.Companion|null[0] + final const val DEFAULT_BUFFER_SIZE // io.matthewnelson.encoding.core/EncoderDecoder.Companion.DEFAULT_BUFFER_SIZE|{}DEFAULT_BUFFER_SIZE[0] + final fun (): kotlin/Int // io.matthewnelson.encoding.core/EncoderDecoder.Companion.DEFAULT_BUFFER_SIZE.|(){}[0] + } } abstract class io.matthewnelson.encoding.core.util/FeedBuffer { // io.matthewnelson.encoding.core.util/FeedBuffer|null[0] @@ -199,10 +204,18 @@ sealed class <#A: io.matthewnelson.encoding.core/EncoderDecoder.Config> io.matth final object Companion { // io.matthewnelson.encoding.core/Decoder.Companion|null[0] final fun (kotlin/ByteArray).decodeToByteArray(io.matthewnelson.encoding.core/Decoder<*>): kotlin/ByteArray // io.matthewnelson.encoding.core/Decoder.Companion.decodeToByteArray|decodeToByteArray@kotlin.ByteArray(io.matthewnelson.encoding.core.Decoder<*>){}[0] final fun (kotlin/ByteArray).decodeToByteArrayOrNull(io.matthewnelson.encoding.core/Decoder<*>): kotlin/ByteArray? // io.matthewnelson.encoding.core/Decoder.Companion.decodeToByteArrayOrNull|decodeToByteArrayOrNull@kotlin.ByteArray(io.matthewnelson.encoding.core.Decoder<*>){}[0] + final fun (kotlin/CharArray).decodeBuffered(kotlin/Int, io.matthewnelson.encoding.core/Decoder<*>, kotlin/Function3): kotlin/Long // io.matthewnelson.encoding.core/Decoder.Companion.decodeBuffered|decodeBuffered@kotlin.CharArray(kotlin.Int;io.matthewnelson.encoding.core.Decoder<*>;kotlin.Function3){}[0] final fun (kotlin/CharArray).decodeToByteArray(io.matthewnelson.encoding.core/Decoder<*>): kotlin/ByteArray // io.matthewnelson.encoding.core/Decoder.Companion.decodeToByteArray|decodeToByteArray@kotlin.CharArray(io.matthewnelson.encoding.core.Decoder<*>){}[0] final fun (kotlin/CharArray).decodeToByteArrayOrNull(io.matthewnelson.encoding.core/Decoder<*>): kotlin/ByteArray? // io.matthewnelson.encoding.core/Decoder.Companion.decodeToByteArrayOrNull|decodeToByteArrayOrNull@kotlin.CharArray(io.matthewnelson.encoding.core.Decoder<*>){}[0] + final fun (kotlin/CharSequence).decodeBuffered(kotlin/Int, io.matthewnelson.encoding.core/Decoder<*>, kotlin/Function3): kotlin/Long // io.matthewnelson.encoding.core/Decoder.Companion.decodeBuffered|decodeBuffered@kotlin.CharSequence(kotlin.Int;io.matthewnelson.encoding.core.Decoder<*>;kotlin.Function3){}[0] final fun (kotlin/CharSequence).decodeToByteArray(io.matthewnelson.encoding.core/Decoder<*>): kotlin/ByteArray // io.matthewnelson.encoding.core/Decoder.Companion.decodeToByteArray|decodeToByteArray@kotlin.CharSequence(io.matthewnelson.encoding.core.Decoder<*>){}[0] final fun (kotlin/CharSequence).decodeToByteArrayOrNull(io.matthewnelson.encoding.core/Decoder<*>): kotlin/ByteArray? // io.matthewnelson.encoding.core/Decoder.Companion.decodeToByteArrayOrNull|decodeToByteArrayOrNull@kotlin.CharSequence(io.matthewnelson.encoding.core.Decoder<*>){}[0] + final inline fun (kotlin/CharArray).decodeBuffered(io.matthewnelson.encoding.core/Decoder<*>, noinline kotlin/Function3): kotlin/Long // io.matthewnelson.encoding.core/Decoder.Companion.decodeBuffered|decodeBuffered@kotlin.CharArray(io.matthewnelson.encoding.core.Decoder<*>;kotlin.Function3){}[0] + final inline fun (kotlin/CharSequence).decodeBuffered(io.matthewnelson.encoding.core/Decoder<*>, noinline kotlin/Function3): kotlin/Long // io.matthewnelson.encoding.core/Decoder.Companion.decodeBuffered|decodeBuffered@kotlin.CharSequence(io.matthewnelson.encoding.core.Decoder<*>;kotlin.Function3){}[0] + final suspend fun (kotlin/CharArray).decodeBuffered(kotlin/Int, io.matthewnelson.encoding.core/Decoder<*>, kotlin.coroutines/SuspendFunction3): kotlin/Long // io.matthewnelson.encoding.core/Decoder.Companion.decodeBuffered|decodeBuffered@kotlin.CharArray(kotlin.Int;io.matthewnelson.encoding.core.Decoder<*>;kotlin.coroutines.SuspendFunction3){}[0] + final suspend fun (kotlin/CharSequence).decodeBuffered(kotlin/Int, io.matthewnelson.encoding.core/Decoder<*>, kotlin.coroutines/SuspendFunction3): kotlin/Long // io.matthewnelson.encoding.core/Decoder.Companion.decodeBuffered|decodeBuffered@kotlin.CharSequence(kotlin.Int;io.matthewnelson.encoding.core.Decoder<*>;kotlin.coroutines.SuspendFunction3){}[0] + final suspend inline fun (kotlin/CharArray).decodeBuffered(io.matthewnelson.encoding.core/Decoder<*>, noinline kotlin.coroutines/SuspendFunction3): kotlin/Long // io.matthewnelson.encoding.core/Decoder.Companion.decodeBuffered|decodeBuffered@kotlin.CharArray(io.matthewnelson.encoding.core.Decoder<*>;kotlin.coroutines.SuspendFunction3){}[0] + final suspend inline fun (kotlin/CharSequence).decodeBuffered(io.matthewnelson.encoding.core/Decoder<*>, noinline kotlin.coroutines/SuspendFunction3): kotlin/Long // io.matthewnelson.encoding.core/Decoder.Companion.decodeBuffered|decodeBuffered@kotlin.CharSequence(io.matthewnelson.encoding.core.Decoder<*>;kotlin.coroutines.SuspendFunction3){}[0] } } diff --git a/library/core/src/commonMain/kotlin/io/matthewnelson/encoding/core/Decoder.kt b/library/core/src/commonMain/kotlin/io/matthewnelson/encoding/core/Decoder.kt index 5cf56481..a7dcfa02 100644 --- a/library/core/src/commonMain/kotlin/io/matthewnelson/encoding/core/Decoder.kt +++ b/library/core/src/commonMain/kotlin/io/matthewnelson/encoding/core/Decoder.kt @@ -13,14 +13,17 @@ * See the License for the specific language governing permissions and * limitations under the License. **/ -@file:Suppress("LocalVariableName", "PropertyName", "RemoveRedundantQualifierName") +@file:Suppress("LocalVariableName", "NOTHING_TO_INLINE", "PropertyName", "RemoveRedundantQualifierName") package io.matthewnelson.encoding.core +import io.matthewnelson.encoding.core.EncoderDecoder.Companion.DEFAULT_BUFFER_SIZE import io.matthewnelson.encoding.core.internal.closedException import io.matthewnelson.encoding.core.internal.decode +import io.matthewnelson.encoding.core.internal.decodeBuffered import io.matthewnelson.encoding.core.internal.isSpaceOrNewLine import io.matthewnelson.encoding.core.util.DecoderInput +import kotlin.coroutines.cancellation.CancellationException import kotlin.jvm.JvmField import kotlin.jvm.JvmName import kotlin.jvm.JvmStatic @@ -207,16 +210,19 @@ public sealed class Decoder(public val config: C) { * returns the decoded bytes. * * @see [decodeToByteArrayOrNull] + * @see [decodeBuffered] + * * @throws [EncodingException] if decoding failed. * */ @JvmStatic @Throws(EncodingException::class) public fun CharSequence.decodeToByteArray(decoder: Decoder<*>): ByteArray { - return decoder.decode(DecoderInput(this)) { feed -> - forEach { c -> feed.consume(c) } - } + return decoder.decode(DecoderInput(this), ::get) } + /** + * TODO + * */ @JvmStatic public fun CharSequence.decodeToByteArrayOrNull(decoder: Decoder<*>): ByteArray? { return try { @@ -231,16 +237,19 @@ public sealed class Decoder(public val config: C) { * returns the decoded bytes. * * @see [decodeToByteArrayOrNull] + * @see [decodeBuffered] + * * @throws [EncodingException] if decoding failed. * */ @JvmStatic @Throws(EncodingException::class) public fun CharArray.decodeToByteArray(decoder: Decoder<*>): ByteArray { - return decoder.decode(DecoderInput(this)) { feed -> - forEach { c -> feed.consume(c) } - } + return decoder.decode(DecoderInput(this), ::get) } + /** + * TODO + * */ @JvmStatic public fun CharArray.decodeToByteArrayOrNull(decoder: Decoder<*>): ByteArray? { return try { @@ -250,6 +259,110 @@ public sealed class Decoder(public val config: C) { } } + /** + * TODO + * */ + @JvmStatic + @Throws(EncodingException::class) + public inline fun CharSequence.decodeBuffered( + decoder: Decoder<*>, + noinline action: (buf: ByteArray, offset: Int, len: Int) -> Unit, + ): Long = decodeBuffered(DEFAULT_BUFFER_SIZE, decoder, action) + + /** + * TODO + * */ + @JvmStatic + @Throws(EncodingException::class) + public fun CharSequence.decodeBuffered( + maxBufSize: Int, + decoder: Decoder<*>, + action: (buf: ByteArray, offset: Int, len: Int) -> Unit, + ): Long = decoder.decodeBuffered( + maxBufSize, + _get = ::get, + _input = { DecoderInput(this) }, + _action = action, + ) + + /** + * TODO + * */ + @JvmStatic + @Throws(CancellationException::class, EncodingException::class) + public suspend inline fun CharSequence.decodeBuffered( + decoder: Decoder<*>, + noinline action: suspend (buf: ByteArray, offset: Int, len: Int) -> Unit, + ): Long = decodeBuffered(DEFAULT_BUFFER_SIZE, decoder, action) + + /** + * TODO + * */ + @JvmStatic + @Throws(CancellationException::class, EncodingException::class) + public suspend fun CharSequence.decodeBuffered( + maxBufSize: Int, + decoder: Decoder<*>, + action: suspend (buf: ByteArray, offset: Int, len: Int) -> Unit, + ): Long = decoder.decodeBuffered( + maxBufSize, + _get = ::get, + _input = { DecoderInput(this) }, + _action = { buf, offset, len -> action(buf, offset, len) }, + ) + + /** + * TODO + * */ + @JvmStatic + @Throws(EncodingException::class) + public inline fun CharArray.decodeBuffered( + decoder: Decoder<*>, + noinline action: (buf: ByteArray, offset: Int, len: Int) -> Unit, + ): Long = decodeBuffered(DEFAULT_BUFFER_SIZE, decoder, action) + + /** + * TODO + * */ + @JvmStatic + @Throws(EncodingException::class) + public fun CharArray.decodeBuffered( + maxBufSize: Int, + decoder: Decoder<*>, + action: (buf: ByteArray, offset: Int, len: Int) -> Unit, + ): Long = decoder.decodeBuffered( + maxBufSize, + _get = ::get, + _input = { DecoderInput(this) }, + _action = action, + ) + + /** + * TODO + * */ + @JvmStatic + @Throws(CancellationException::class, EncodingException::class) + public suspend inline fun CharArray.decodeBuffered( + decoder: Decoder<*>, + noinline action: suspend (buf: ByteArray, offset: Int, len: Int) -> Unit, + ): Long = decodeBuffered(DEFAULT_BUFFER_SIZE, decoder, action) + + /** + * TODO + * */ + @JvmStatic + @Throws(CancellationException::class, EncodingException::class) + public suspend fun CharArray.decodeBuffered( + maxBufSize: Int, + decoder: Decoder<*>, + action: suspend (buf: ByteArray, offset: Int, len: Int) -> Unit, + ): Long = decoder.decodeBuffered( + maxBufSize, + _get = ::get, + _input = { DecoderInput(this) }, + _action = { buf, offset, len -> action(buf, offset, len) }, + ) + /** * DEPRECATED since `2.3.0` * @suppress @@ -262,9 +375,7 @@ public sealed class Decoder(public val config: C) { ) public fun ByteArray.decodeToByteArray(decoder: Decoder<*>): ByteArray { @Suppress("DEPRECATION_ERROR") - return decoder.decode(DecoderInput(this)) { feed -> - forEach { b -> feed.consume(b.toInt().toChar()) } - } + return decoder.decode(DecoderInput(this), _get = { i -> this[i].toInt().toChar() }) } /** 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 adf7261a..ee6af805 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 @@ -135,8 +135,6 @@ public sealed class Encoder(config: C): Decoder(con if (_isClosed) throw closedException() try { - // should not throw exception, but just - // in case, we close the Feed. doFinalProtected() (_out as? LineBreakOutFeed)?.reset() } catch (t: Throwable) { @@ -258,7 +256,7 @@ public sealed class Encoder(config: C): Decoder(con if (i == maxSize) return@block a val copy = a.copyOf(i) if (encoder.config.backFillBuffers) { - copy.fill(0, 0, i) + a.fill(0, 0, i) } copy } diff --git a/library/core/src/commonMain/kotlin/io/matthewnelson/encoding/core/EncoderDecoder.kt b/library/core/src/commonMain/kotlin/io/matthewnelson/encoding/core/EncoderDecoder.kt index 0c52f29f..3246676d 100644 --- a/library/core/src/commonMain/kotlin/io/matthewnelson/encoding/core/EncoderDecoder.kt +++ b/library/core/src/commonMain/kotlin/io/matthewnelson/encoding/core/EncoderDecoder.kt @@ -36,6 +36,14 @@ import kotlin.jvm.JvmStatic * */ public abstract class EncoderDecoder(config: C): Encoder(config) { + public companion object { + + /** + * TODO + * */ + public const val DEFAULT_BUFFER_SIZE: Int = 8 * 1024 + } + /** * Base configuration for an [EncoderDecoder]. More options may be specified by the implementation. * */ @@ -50,7 +58,15 @@ public abstract class EncoderDecoder(config: C): Encod @JvmField public val isLenient: Boolean?, - lineBreakInterval: Byte, + /** + * If greater than `0`, [Encoder.newEncoderFeed] will use the [LineBreakOutFeed] such that + * for every [lineBreakInterval] number of encoded characters output, the next encoded + * character will be preceded with the new line character `\n`. + * + * **NOTE:** This setting will always be `0` if [isLenient] is `false`. + * */ + @JvmField + public val lineBreakInterval: Byte, /** * The character that is used when padding encoded output. This is used by [Decoder.Feed] @@ -69,9 +85,9 @@ public abstract class EncoderDecoder(config: C): Encod * emit on a single invocation of [Decoder.Feed.consume]. For example, `Base16` decoding * will emit `1` byte for every `2` characters of input, so its value is `1`. `Base32` * decoding will emit `5` bytes for every `8` characters of input, so its value is `5`. - * `UTF8` decoding (character to byte transformations) can emit `4` to `6` bytes, depending - * on buffered input and the size of the replacement byte sequence being used, so would - * require a calculation such as `(3 + replacementStrategy.size).coerceAtLeast(4)`. + * `UTF8` "decoding" (i.e. text to UTF-8 byte transformations) can emit `4` to `6` bytes, + * depending on buffered input and the size of the replacement byte sequence being used, + * so would require a calculation such as `(3 + replacementStrategy.size).coerceAtLeast(4)`. * * Value will be greater than `0`, or `-1` which indicates that the [EncoderDecoder.Config] * implementation has not updated to the new constructor introduced in version `2.6.0`. @@ -80,15 +96,15 @@ public abstract class EncoderDecoder(config: C): Encod public val maxDecodeEmit: Int, /** - * When the functions [Encoder.encodeToString], [Encoder.encodeToCharArray], and - * [Decoder.decodeToByteArray] are utilized, an initial buffer is allocated based on the - * pre-calculated return values of [encodeOutMaxSize] or [decodeOutMaxSize] (respectively). - * After encoding/decoding operations have completed, the initial buffer may be trimmed - * to size in the event of an over-allocation. If that happens, the initial buffer is - * then dropped and the correct sized copy is returned. Prior versions always back-filled - * the initial buffer with `0` or a space character when this occurred, but that can be - * computationally expensive for large data sets and potentially unnecessary if data is - * known to not be sensitive in nature. + * When the functions [Encoder.encodeToString], [Encoder.encodeToCharArray], + * [Decoder.decodeToByteArray], and [Decoder.decodeBuffered] are utilized, an initial + * buffer gets allocated based on the pre-calculated return values of [encodeOutMaxSize] + * or [decodeOutMaxSize] (respectively). After encoding/decoding operations have completed, + * the initial buffer may be trimmed to size in the event of an over-allocation. If that + * happens, the initial buffer is then dropped and the correct sized copy is returned. Prior + * versions always back-filled the initial buffer with `0` or a space character when this + * occurred, but that can be computationally expensive for large data sets and potentially + * unnecessary if data is known to not be sensitive in nature. * * If `true`, the initial buffer (if it was trimmed to size) is back-filled. If `false`, * back-filling is skipped. @@ -100,24 +116,10 @@ public abstract class EncoderDecoder(config: C): Encod @Suppress("UNUSED", "UNUSED_PARAMETER") unused: Any?, ) { - /** - * If greater than `0`, [Encoder.newEncoderFeed] will use the [LineBreakOutFeed] such that - * for every [lineBreakInterval] number of encoded characters output, the next encoded - * character will be preceded with the new line character `\n`. - * - * **NOTE:** This setting will always be `0` if [isLenient] is `false`. - * */ - @JvmField - public val lineBreakInterval: Byte = if (isLenient != false && lineBreakInterval > 0) { - lineBreakInterval - } else { - 0 - } - /** * Instantiates a new [Config] instance. * - * @throws [IllegalArgumentException] If [maxDecodeEmit] is less than or equal to 0. + * @throws [IllegalArgumentException] If [maxDecodeEmit] is less than or equal to `0`. * */ protected constructor( isLenient: Boolean?, @@ -127,7 +129,7 @@ public abstract class EncoderDecoder(config: C): Encod backFillBuffers: Boolean, ): this( isLenient = isLenient, - lineBreakInterval = lineBreakInterval, + lineBreakInterval = lineBreakIntervalOrZero(isLenient, lineBreakInterval), paddingChar = paddingChar, maxDecodeEmit = maxDecodeEmit, backFillBuffers = backFillBuffers, @@ -231,7 +233,7 @@ public abstract class EncoderDecoder(config: C): Encod * @see [decodeOutMaxSizeOrFail] * @param [encodedSize] The size of the encoded data being decoded. * @throws [EncodingSizeException] If [encodedSize] is negative, or - * the calculated size exceeded [Long.MAX_VALUE]. + * the calculated output size exceeded [Long.MAX_VALUE]. * */ @Throws(EncodingSizeException::class) public fun decodeOutMaxSize(encodedSize: Long): Long { @@ -270,8 +272,8 @@ public abstract class EncoderDecoder(config: C): Encod * * @param [input] The data which is to be decoded. * @see [DecoderInput] - * @throws [EncodingSizeException] If the calculates size exceeded - * [Int.MAX_VALUE]. + * @throws [EncodingSizeException] If the calculated output size + * exceeds [Int.MAX_VALUE]. * @throws [EncodingException] If the implementation has integrity * checks to fail quickly and verification of the [input] * failed (e.g. Base32 Crockford's checkSymbol). @@ -502,7 +504,7 @@ public abstract class EncoderDecoder(config: C): Encod paddingChar: Char?, ): this( isLenient = isLenient, - lineBreakInterval = lineBreakInterval, + lineBreakInterval = lineBreakIntervalOrZero(isLenient, lineBreakInterval), paddingChar = paddingChar, maxDecodeEmit = -1, backFillBuffers = true, @@ -664,3 +666,8 @@ public abstract class EncoderDecoder(config: C): Encod return "EncoderDecoder[${name()}]@${hashCode()}" } } + +@Suppress("NOTHING_TO_INLINE") +private inline fun lineBreakIntervalOrZero(isLenient: Boolean?, interval: Byte): Byte { + return if (isLenient != false && interval > 0) interval else 0 +} diff --git a/library/core/src/commonMain/kotlin/io/matthewnelson/encoding/core/internal/-EncoderDecoder.kt b/library/core/src/commonMain/kotlin/io/matthewnelson/encoding/core/internal/-EncoderDecoder.kt index 3614096f..60cde27c 100644 --- a/library/core/src/commonMain/kotlin/io/matthewnelson/encoding/core/internal/-EncoderDecoder.kt +++ b/library/core/src/commonMain/kotlin/io/matthewnelson/encoding/core/internal/-EncoderDecoder.kt @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. **/ -@file:Suppress("NOTHING_TO_INLINE") +@file:Suppress("LocalVariableName", "NOTHING_TO_INLINE") package io.matthewnelson.encoding.core.internal @@ -31,28 +31,43 @@ private const val MAX_ENCODE_OUT_SIZE: Long = Int.MAX_VALUE.toLong() @OptIn(ExperimentalContracts::class) internal inline fun Decoder.decode( input: DecoderInput, - action: (feed: Decoder<*>.Feed) -> Unit + _get: (i: Int) -> Char, ): ByteArray { - contract { callsInPlace(action, InvocationKind.UNKNOWN) } + contract { callsInPlace(_get, InvocationKind.UNKNOWN) } + val maxDecodeSize = config.decodeOutMaxSizeOrFail(input) + return decode(maxDecodeSize, input.size, _get) +} - val size = config.decodeOutMaxSizeOrFail(input) - val a = ByteArray(size) +@Throws(EncodingException::class) +@OptIn(ExperimentalContracts::class) +internal inline fun Decoder.decode( + maxDecodeSize: Int, + inputSize: Int, + _get: (i: Int) -> Char, +): ByteArray { + contract { callsInPlace(_get, InvocationKind.UNKNOWN) } + val a = ByteArray(maxDecodeSize) var i = 0 try { - newDecoderFeed(out = { b -> a[i++] = b }).use { feed -> action.invoke(feed) } + newDecoderFeed(out = { b -> a[i++] = b }).use { feed -> + var j = 0 + while (j < inputSize) { + feed.consume(_get(j++)) + } + } } catch (t: Throwable) { if (config.backFillBuffers) { a.fill(0, toIndex = min(a.size, i)) } - if (t is IndexOutOfBoundsException && i >= size) { + if (t is IndexOutOfBoundsException && i >= maxDecodeSize) { // Something is wrong with the encoder's pre-calculation - throw EncodingSizeException("Encoder's pre-calculation of Size[$size] was incorrect", t) + throw EncodingSizeException("Encoder's pre-calculation of Size[$maxDecodeSize] was incorrect", t) } throw t } - if (i == size) return a + if (i == maxDecodeSize) return a val copy = a.copyOf(i) if (config.backFillBuffers) { @@ -61,6 +76,76 @@ internal inline fun Decoder.decode( return copy } +@Throws(EncodingException::class) +@OptIn(ExperimentalContracts::class) +internal inline fun Decoder.decodeBuffered( + maxBufSize: Int, + _get: (i: Int) -> Char, + _input: () -> DecoderInput, + _action: (buf: ByteArray, offset: Int, len: Int) -> Unit, +): Long { + contract { + callsInPlace(_get, InvocationKind.UNKNOWN) + callsInPlace(_input, InvocationKind.AT_MOST_ONCE) + callsInPlace(_action, InvocationKind.UNKNOWN) + } + + if (config.maxDecodeEmit == -1) { + // EncoderDecoder.Config implementation has not updated to + // new constructor which requires it to be greater than 0. + throw EncodingException("Decoder.Config.maxDecodeEmit == -1") + } + require(maxBufSize > config.maxDecodeEmit) { + "maxBufSize[$maxBufSize] <= Decoder.Config.maxDecodeEmit[${config.maxDecodeEmit}]" + } + + val input = _input() + try { + config.decodeOutMaxSizeOrFail(input) + } catch (_: EncodingSizeException) { + // Only ignore EncodingSizeException such that any checks + // the implementation has (such as Base32 Crockford) can + // fall through and end early. + + -1 // output size exceeded Int.MAX_VALUE + }.let { maxDecodeSize -> + if (maxDecodeSize !in 0..maxBufSize) return@let // Chunk + + // Maximum decoded size will be smaller than or equal to maxBufSize. One-shot it. + val decoded = decode(maxDecodeSize, input.size, _get) + try { + _action(decoded, 0, decoded.size) + } finally { + if (config.backFillBuffers) decoded.fill(0) + } + return decoded.size.toLong() + } + + // Chunk + val buf = ByteArray(maxBufSize) + val limit = buf.size - config.maxDecodeEmit + val inputSize = input.size + var iBuf = 0 + var i = 0 + var size = 0L + try { + newDecoderFeed(out = { b -> buf[iBuf++] = b }).use { feed -> + while (i < inputSize) { + feed.consume(input = _get(i++)) + if (iBuf <= limit) continue + _action(buf, 0, iBuf) + size += iBuf + iBuf = 0 + } + } + if (iBuf > 0) _action(buf, 0, iBuf) + size += iBuf + } finally { + if (config.backFillBuffers) buf.fill(0) + } + return size +} + /** * Fails if the returned [Long] for [Config.encodeOutMaxSize] exceeds [Int.MAX_VALUE]. * */ @@ -68,16 +153,16 @@ internal inline fun Decoder.decode( @Throws(EncodingSizeException::class) internal inline fun Encoder<*>.encodeOutMaxSizeOrFail( size: Int, - block: (maxSize: Int) -> T + _block: (maxSize: Int) -> T, ): T { - contract { callsInPlace(block, InvocationKind.AT_MOST_ONCE) } + contract { callsInPlace(_block, InvocationKind.AT_MOST_ONCE) } val maxSize = config.encodeOutMaxSize(size.toLong()) if (maxSize > MAX_ENCODE_OUT_SIZE) { throw Config.outSizeExceedsMaxEncodingSizeException(maxSize, MAX_ENCODE_OUT_SIZE) } - return block.invoke(maxSize.toInt()) + return _block(maxSize.toInt()) } internal inline fun Encoder.encode( From a468568991009a017748553fc67fc10616915eb2 Mon Sep 17 00:00:00 2001 From: Matthew Nelson Date: Mon, 8 Dec 2025 08:11:52 -0500 Subject: [PATCH 5/9] Fix UTF8.Config.maxDecodeEmit calculation --- .../matthewnelson/encoding/core/EncoderDecoder.kt | 13 +++++++------ .../kotlin/io/matthewnelson/encoding/utf8/UTF8.kt | 2 +- 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/library/core/src/commonMain/kotlin/io/matthewnelson/encoding/core/EncoderDecoder.kt b/library/core/src/commonMain/kotlin/io/matthewnelson/encoding/core/EncoderDecoder.kt index 3246676d..da502904 100644 --- a/library/core/src/commonMain/kotlin/io/matthewnelson/encoding/core/EncoderDecoder.kt +++ b/library/core/src/commonMain/kotlin/io/matthewnelson/encoding/core/EncoderDecoder.kt @@ -82,12 +82,13 @@ public abstract class EncoderDecoder(config: C): Encod /** * The maximum number of bytes that the implementation's [Decoder.Feed] can potentially - * emit on a single invocation of [Decoder.Feed.consume]. For example, `Base16` decoding - * will emit `1` byte for every `2` characters of input, so its value is `1`. `Base32` - * decoding will emit `5` bytes for every `8` characters of input, so its value is `5`. - * `UTF8` "decoding" (i.e. text to UTF-8 byte transformations) can emit `4` to `6` bytes, - * depending on buffered input and the size of the replacement byte sequence being used, - * so would require a calculation such as `(3 + replacementStrategy.size).coerceAtLeast(4)`. + * emit on a single invocation of [Decoder.Feed.consume] or [Decoder.Feed.doFinal]. For + * example, `Base16` decoding will emit `1` byte for every `2` characters of input, so + * its value is `1`. `Base32` decoding will emit `5` bytes for every `8` characters of + * input, so its value is `5`. `UTF8` "decoding" (i.e. text to UTF-8 byte transformations) + * can emit `4` to `6` bytes, depending on buffered input and the size of the replacement + * byte sequence being used, so would require a calculation such as + * `(replacementStrategy.size * 2).coerceAtLeast(4)`. * * Value will be greater than `0`, or `-1` which indicates that the [EncoderDecoder.Config] * implementation has not updated to the new constructor introduced in version `2.6.0`. diff --git a/library/utf8/src/commonMain/kotlin/io/matthewnelson/encoding/utf8/UTF8.kt b/library/utf8/src/commonMain/kotlin/io/matthewnelson/encoding/utf8/UTF8.kt index f0426bd5..0d231e1a 100644 --- a/library/utf8/src/commonMain/kotlin/io/matthewnelson/encoding/utf8/UTF8.kt +++ b/library/utf8/src/commonMain/kotlin/io/matthewnelson/encoding/utf8/UTF8.kt @@ -237,7 +237,7 @@ public open class UTF8: EncoderDecoder { isLenient = null, lineBreakInterval = -1, paddingChar = null, - maxDecodeEmit = (3 + replacementStrategy.size).coerceAtLeast(4), + maxDecodeEmit = (replacementStrategy.size * 2).coerceAtLeast(4), backFillBuffers, ) { From 1c2d21c8432a13916d75ca2688db71465d11485d Mon Sep 17 00:00:00 2001 From: Matthew Nelson Date: Mon, 8 Dec 2025 08:58:38 -0500 Subject: [PATCH 6/9] Rename decodeBuffered suspend functions to decodeBufferedAsync --- library/core/api/core.api | 16 ++++++++-------- library/core/api/core.klib.api | 8 ++++---- .../io/matthewnelson/encoding/core/Decoder.kt | 12 ++++++------ 3 files changed, 18 insertions(+), 18 deletions(-) diff --git a/library/core/api/core.api b/library/core/api/core.api index 47e7b5e5..e9e90778 100644 --- a/library/core/api/core.api +++ b/library/core/api/core.api @@ -2,13 +2,13 @@ public abstract class io/matthewnelson/encoding/core/Decoder { public static final field Companion Lio/matthewnelson/encoding/core/Decoder$Companion; public synthetic fun (Lio/matthewnelson/encoding/core/EncoderDecoder$Config;Lkotlin/jvm/internal/DefaultConstructorMarker;)V public static final fun decodeBuffered (Ljava/lang/CharSequence;ILio/matthewnelson/encoding/core/Decoder;Lkotlin/jvm/functions/Function3;)J - public static final fun decodeBuffered (Ljava/lang/CharSequence;ILio/matthewnelson/encoding/core/Decoder;Lkotlin/jvm/functions/Function4;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; public static final fun decodeBuffered (Ljava/lang/CharSequence;Lio/matthewnelson/encoding/core/Decoder;Lkotlin/jvm/functions/Function3;)J - public static final fun decodeBuffered (Ljava/lang/CharSequence;Lio/matthewnelson/encoding/core/Decoder;Lkotlin/jvm/functions/Function4;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; public static final fun decodeBuffered ([CILio/matthewnelson/encoding/core/Decoder;Lkotlin/jvm/functions/Function3;)J - public static final fun decodeBuffered ([CILio/matthewnelson/encoding/core/Decoder;Lkotlin/jvm/functions/Function4;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; public static final fun decodeBuffered ([CLio/matthewnelson/encoding/core/Decoder;Lkotlin/jvm/functions/Function3;)J - public static final fun decodeBuffered ([CLio/matthewnelson/encoding/core/Decoder;Lkotlin/jvm/functions/Function4;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public static final fun decodeBufferedAsync (Ljava/lang/CharSequence;ILio/matthewnelson/encoding/core/Decoder;Lkotlin/jvm/functions/Function4;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public static final fun decodeBufferedAsync (Ljava/lang/CharSequence;Lio/matthewnelson/encoding/core/Decoder;Lkotlin/jvm/functions/Function4;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public static final fun decodeBufferedAsync ([CILio/matthewnelson/encoding/core/Decoder;Lkotlin/jvm/functions/Function4;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public static final fun decodeBufferedAsync ([CLio/matthewnelson/encoding/core/Decoder;Lkotlin/jvm/functions/Function4;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; public static final fun decodeToByteArray (Ljava/lang/CharSequence;Lio/matthewnelson/encoding/core/Decoder;)[B public static final fun decodeToByteArray ([BLio/matthewnelson/encoding/core/Decoder;)[B public static final fun decodeToByteArray ([CLio/matthewnelson/encoding/core/Decoder;)[B @@ -22,13 +22,13 @@ public abstract class io/matthewnelson/encoding/core/Decoder { public final class io/matthewnelson/encoding/core/Decoder$Companion { public final fun decodeBuffered (Ljava/lang/CharSequence;ILio/matthewnelson/encoding/core/Decoder;Lkotlin/jvm/functions/Function3;)J - public final fun decodeBuffered (Ljava/lang/CharSequence;ILio/matthewnelson/encoding/core/Decoder;Lkotlin/jvm/functions/Function4;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; public final fun decodeBuffered (Ljava/lang/CharSequence;Lio/matthewnelson/encoding/core/Decoder;Lkotlin/jvm/functions/Function3;)J - public final fun decodeBuffered (Ljava/lang/CharSequence;Lio/matthewnelson/encoding/core/Decoder;Lkotlin/jvm/functions/Function4;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; public final fun decodeBuffered ([CILio/matthewnelson/encoding/core/Decoder;Lkotlin/jvm/functions/Function3;)J - public final fun decodeBuffered ([CILio/matthewnelson/encoding/core/Decoder;Lkotlin/jvm/functions/Function4;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; public final fun decodeBuffered ([CLio/matthewnelson/encoding/core/Decoder;Lkotlin/jvm/functions/Function3;)J - public final fun decodeBuffered ([CLio/matthewnelson/encoding/core/Decoder;Lkotlin/jvm/functions/Function4;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public final fun decodeBufferedAsync (Ljava/lang/CharSequence;ILio/matthewnelson/encoding/core/Decoder;Lkotlin/jvm/functions/Function4;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public final fun decodeBufferedAsync (Ljava/lang/CharSequence;Lio/matthewnelson/encoding/core/Decoder;Lkotlin/jvm/functions/Function4;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public final fun decodeBufferedAsync ([CILio/matthewnelson/encoding/core/Decoder;Lkotlin/jvm/functions/Function4;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public final fun decodeBufferedAsync ([CLio/matthewnelson/encoding/core/Decoder;Lkotlin/jvm/functions/Function4;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; public final fun decodeToByteArray (Ljava/lang/CharSequence;Lio/matthewnelson/encoding/core/Decoder;)[B public final fun decodeToByteArray ([BLio/matthewnelson/encoding/core/Decoder;)[B public final fun decodeToByteArray ([CLio/matthewnelson/encoding/core/Decoder;)[B diff --git a/library/core/api/core.klib.api b/library/core/api/core.klib.api index 6262071f..f3799181 100644 --- a/library/core/api/core.klib.api +++ b/library/core/api/core.klib.api @@ -212,10 +212,10 @@ sealed class <#A: io.matthewnelson.encoding.core/EncoderDecoder.Config> io.matth final fun (kotlin/CharSequence).decodeToByteArrayOrNull(io.matthewnelson.encoding.core/Decoder<*>): kotlin/ByteArray? // io.matthewnelson.encoding.core/Decoder.Companion.decodeToByteArrayOrNull|decodeToByteArrayOrNull@kotlin.CharSequence(io.matthewnelson.encoding.core.Decoder<*>){}[0] final inline fun (kotlin/CharArray).decodeBuffered(io.matthewnelson.encoding.core/Decoder<*>, noinline kotlin/Function3): kotlin/Long // io.matthewnelson.encoding.core/Decoder.Companion.decodeBuffered|decodeBuffered@kotlin.CharArray(io.matthewnelson.encoding.core.Decoder<*>;kotlin.Function3){}[0] final inline fun (kotlin/CharSequence).decodeBuffered(io.matthewnelson.encoding.core/Decoder<*>, noinline kotlin/Function3): kotlin/Long // io.matthewnelson.encoding.core/Decoder.Companion.decodeBuffered|decodeBuffered@kotlin.CharSequence(io.matthewnelson.encoding.core.Decoder<*>;kotlin.Function3){}[0] - final suspend fun (kotlin/CharArray).decodeBuffered(kotlin/Int, io.matthewnelson.encoding.core/Decoder<*>, kotlin.coroutines/SuspendFunction3): kotlin/Long // io.matthewnelson.encoding.core/Decoder.Companion.decodeBuffered|decodeBuffered@kotlin.CharArray(kotlin.Int;io.matthewnelson.encoding.core.Decoder<*>;kotlin.coroutines.SuspendFunction3){}[0] - final suspend fun (kotlin/CharSequence).decodeBuffered(kotlin/Int, io.matthewnelson.encoding.core/Decoder<*>, kotlin.coroutines/SuspendFunction3): kotlin/Long // io.matthewnelson.encoding.core/Decoder.Companion.decodeBuffered|decodeBuffered@kotlin.CharSequence(kotlin.Int;io.matthewnelson.encoding.core.Decoder<*>;kotlin.coroutines.SuspendFunction3){}[0] - final suspend inline fun (kotlin/CharArray).decodeBuffered(io.matthewnelson.encoding.core/Decoder<*>, noinline kotlin.coroutines/SuspendFunction3): kotlin/Long // io.matthewnelson.encoding.core/Decoder.Companion.decodeBuffered|decodeBuffered@kotlin.CharArray(io.matthewnelson.encoding.core.Decoder<*>;kotlin.coroutines.SuspendFunction3){}[0] - final suspend inline fun (kotlin/CharSequence).decodeBuffered(io.matthewnelson.encoding.core/Decoder<*>, noinline kotlin.coroutines/SuspendFunction3): kotlin/Long // io.matthewnelson.encoding.core/Decoder.Companion.decodeBuffered|decodeBuffered@kotlin.CharSequence(io.matthewnelson.encoding.core.Decoder<*>;kotlin.coroutines.SuspendFunction3){}[0] + final suspend fun (kotlin/CharArray).decodeBufferedAsync(kotlin/Int, io.matthewnelson.encoding.core/Decoder<*>, kotlin.coroutines/SuspendFunction3): kotlin/Long // io.matthewnelson.encoding.core/Decoder.Companion.decodeBufferedAsync|decodeBufferedAsync@kotlin.CharArray(kotlin.Int;io.matthewnelson.encoding.core.Decoder<*>;kotlin.coroutines.SuspendFunction3){}[0] + final suspend fun (kotlin/CharSequence).decodeBufferedAsync(kotlin/Int, io.matthewnelson.encoding.core/Decoder<*>, kotlin.coroutines/SuspendFunction3): kotlin/Long // io.matthewnelson.encoding.core/Decoder.Companion.decodeBufferedAsync|decodeBufferedAsync@kotlin.CharSequence(kotlin.Int;io.matthewnelson.encoding.core.Decoder<*>;kotlin.coroutines.SuspendFunction3){}[0] + final suspend inline fun (kotlin/CharArray).decodeBufferedAsync(io.matthewnelson.encoding.core/Decoder<*>, noinline kotlin.coroutines/SuspendFunction3): kotlin/Long // io.matthewnelson.encoding.core/Decoder.Companion.decodeBufferedAsync|decodeBufferedAsync@kotlin.CharArray(io.matthewnelson.encoding.core.Decoder<*>;kotlin.coroutines.SuspendFunction3){}[0] + final suspend inline fun (kotlin/CharSequence).decodeBufferedAsync(io.matthewnelson.encoding.core/Decoder<*>, noinline kotlin.coroutines/SuspendFunction3): kotlin/Long // io.matthewnelson.encoding.core/Decoder.Companion.decodeBufferedAsync|decodeBufferedAsync@kotlin.CharSequence(io.matthewnelson.encoding.core.Decoder<*>;kotlin.coroutines.SuspendFunction3){}[0] } } diff --git a/library/core/src/commonMain/kotlin/io/matthewnelson/encoding/core/Decoder.kt b/library/core/src/commonMain/kotlin/io/matthewnelson/encoding/core/Decoder.kt index a7dcfa02..a235a101 100644 --- a/library/core/src/commonMain/kotlin/io/matthewnelson/encoding/core/Decoder.kt +++ b/library/core/src/commonMain/kotlin/io/matthewnelson/encoding/core/Decoder.kt @@ -290,17 +290,17 @@ public sealed class Decoder(public val config: C) { * */ @JvmStatic @Throws(CancellationException::class, EncodingException::class) - public suspend inline fun CharSequence.decodeBuffered( + public suspend inline fun CharSequence.decodeBufferedAsync( decoder: Decoder<*>, noinline action: suspend (buf: ByteArray, offset: Int, len: Int) -> Unit, - ): Long = decodeBuffered(DEFAULT_BUFFER_SIZE, decoder, action) + ): Long = decodeBufferedAsync(DEFAULT_BUFFER_SIZE, decoder, action) /** * TODO * */ @JvmStatic @Throws(CancellationException::class, EncodingException::class) - public suspend fun CharSequence.decodeBuffered( + public suspend fun CharSequence.decodeBufferedAsync( maxBufSize: Int, decoder: Decoder<*>, action: suspend (buf: ByteArray, offset: Int, len: Int) -> Unit, @@ -342,17 +342,17 @@ public sealed class Decoder(public val config: C) { * */ @JvmStatic @Throws(CancellationException::class, EncodingException::class) - public suspend inline fun CharArray.decodeBuffered( + public suspend inline fun CharArray.decodeBufferedAsync( decoder: Decoder<*>, noinline action: suspend (buf: ByteArray, offset: Int, len: Int) -> Unit, - ): Long = decodeBuffered(DEFAULT_BUFFER_SIZE, decoder, action) + ): Long = decodeBufferedAsync(DEFAULT_BUFFER_SIZE, decoder, action) /** * TODO * */ @JvmStatic @Throws(CancellationException::class, EncodingException::class) - public suspend fun CharArray.decodeBuffered( + public suspend fun CharArray.decodeBufferedAsync( maxBufSize: Int, decoder: Decoder<*>, action: suspend (buf: ByteArray, offset: Int, len: Int) -> Unit, From 0023af28535fe579663ae68ec0a2aeb109db58c6 Mon Sep 17 00:00:00 2001 From: Matthew Nelson Date: Mon, 8 Dec 2025 15:07:54 -0500 Subject: [PATCH 7/9] Add documentation --- library/core/README.md | 43 +- .../io/matthewnelson/encoding/core/Decoder.kt | 452 +++++++++++++++++- .../encoding/core/EncoderDecoder.kt | 4 +- .../encoding/core/internal/-EncoderDecoder.kt | 9 +- 4 files changed, 475 insertions(+), 33 deletions(-) diff --git a/library/core/README.md b/library/core/README.md index 429594fd..44ee06bd 100644 --- a/library/core/README.md +++ b/library/core/README.md @@ -5,6 +5,7 @@ higher level implementations. ```kotlin // Using modules :base64 and :utf8 for example purposes +// Using io.matthewnelson.kmp-file:{file/async} for example purposes fun main() { //// Encoder.Feed example //// @@ -16,16 +17,17 @@ fun main() { // come out of the Encoder.Feed val out = Encoder.OutFeed { char -> sb.append(char) } - // Wrap it in helper LineBreakOutFeed which will output \n every `interval` + // Wrap it in helper LineBreakOutFeed which will output `\n` every `interval` // characters of output. val outN = LineBreakOutFeed(interval = 64, out) Base64.Default.newEncoderFeed(outN).use { feed -> + // Encode UTF-8 bytes to base64 "Hello World 1!".decodeToByteArray(UTF8).forEach { b -> feed.consume(b) } feed.flush() // Finalize first encoding to reuse the Feed - outN.output('.') + outN.output('.') // Add a separator or something. "Hello World 2!".decodeToByteArray(UTF8).forEach { b -> feed.consume(b) } - } // << `Feed.use` extension function will call Feed.doFinal automatically for us here + } // << `Feed.use` extension function will call Feed.doFinal automatically val encoded = sb.toString() println(encoded) // SGVsbG8gV29ybGQgMSE=.SGVsbG8gV29ybGQgMiE= @@ -33,15 +35,42 @@ fun main() { //// Decoder.Feed example //// val decoded = StringBuilder() - UTF8.newEncoderFeed { char -> decoded.append(char) }.use { feedUTF8 -> - Base64.Default.newDecoderFeed { decodedByte -> feedUTF8.consume(decodedByte) }.use { feedB64 -> + UTF8.newEncoderFeed(decoded::append).use { feedUTF8 -> + + // As the base64 decoder outputs decoded bytes, pipe them through + // the UTF8 "encoder" feed (i.e. UTF-8 byte to text transform), + // which will then pipe each "encoded" character of output to + // the StringBuilder. + Base64.Default.newDecoderFeed(feedUTF8::consume).use { feedB64 -> encoded.substringBefore('.').forEach { c -> feedB64.consume(c) } feedB64.flush() // Finalize first decoding to reuse the Feed feedUTF8.flush() // Prepare UTF8 feed for second decoding encoded.substringAfter('.').forEach { c -> feedB64.consume(c) } - } // << `Feed.use` extension function will call Feed.doFinal automatically for us here - } // << `Feed.use` extension function will call Feed.doFinal automatically for us here + } // << `Feed.use` extension function will call Feed.doFinal automatically + } // << `Feed.use` extension function will call Feed.doFinal automatically println(decoded.toString()) // Hello World 1!Hello World 2! + + //// Decoder decodeBuffered/decodeBufferedAsync examples /// + + // Write UTF-8 encoded bytes to a FileStream (kmp-file:file) + val file = "/path/to/file.txt".toFile() + file.openWrite(excl = null).use { stream -> + decoded.decodeBuffered(UTF8, action = stream::write) + } + + // Now do it asynchronously (kmp-file:async) + GlobalScope.launch { + AsyncFs.Default.with { + file.openAppendAsync(excl = OpenExcl.MustExist) + .useAsync { stream -> + decoded.decodeBufferedAsync( + maxBufSize = 1024, + decoder = UTF8.ThrowOnInvalid, + action = stream::writeAsync, + ) + } + } + } } ``` diff --git a/library/core/src/commonMain/kotlin/io/matthewnelson/encoding/core/Decoder.kt b/library/core/src/commonMain/kotlin/io/matthewnelson/encoding/core/Decoder.kt index a235a101..b630ca8c 100644 --- a/library/core/src/commonMain/kotlin/io/matthewnelson/encoding/core/Decoder.kt +++ b/library/core/src/commonMain/kotlin/io/matthewnelson/encoding/core/Decoder.kt @@ -206,13 +206,17 @@ public sealed class Decoder(public val config: C) { public companion object { /** - * Decodes a [String] for the provided [decoder] and - * returns the decoded bytes. + * Decode a [CharSequence]. * - * @see [decodeToByteArrayOrNull] - * @see [decodeBuffered] + * @param [decoder] The [Decoder] to use. * - * @throws [EncodingException] if decoding failed. + * @return The array of decoded data. + * + * @see [CharSequence.decodeToByteArrayOrNull] + * @see [CharSequence.decodeBuffered] + * @see [CharSequence.decodeBufferedAsync] + * + * @throws [EncodingException] If decoding failed. * */ @JvmStatic @Throws(EncodingException::class) @@ -221,7 +225,15 @@ public sealed class Decoder(public val config: C) { } /** - * TODO + * Decode a [CharSequence]. + * + * @param [decoder] The [Decoder] to use. + * + * @return The array of decoded data, or `null` if there was a decoding error. + * + * @see [CharSequence.decodeToByteArray] + * @see [CharSequence.decodeBuffered] + * @see [CharSequence.decodeBufferedAsync] * */ @JvmStatic public fun CharSequence.decodeToByteArrayOrNull(decoder: Decoder<*>): ByteArray? { @@ -233,13 +245,17 @@ public sealed class Decoder(public val config: C) { } /** - * Decodes a [CharArray] for the provided [decoder] and - * returns the decoded bytes. + * Decode a [CharArray]. * - * @see [decodeToByteArrayOrNull] - * @see [decodeBuffered] + * @param [decoder] The [Decoder] to use. * - * @throws [EncodingException] if decoding failed. + * @return The array of decoded data. + * + * @see [CharArray.decodeToByteArrayOrNull] + * @see [CharArray.decodeBuffered] + * @see [CharArray.decodeBufferedAsync] + * + * @throws [EncodingException] If decoding failed. * */ @JvmStatic @Throws(EncodingException::class) @@ -248,7 +264,15 @@ public sealed class Decoder(public val config: C) { } /** - * TODO + * Decode a [CharArray]. + * + * @param [decoder] The [Decoder] to use. + * + * @return The array of decoded data, or `null` if there was a decoding error. + * + * @see [CharArray.decodeToByteArray] + * @see [CharArray.decodeBuffered] + * @see [CharArray.decodeBufferedAsync] * */ @JvmStatic public fun CharArray.decodeToByteArrayOrNull(decoder: Decoder<*>): ByteArray? { @@ -260,7 +284,52 @@ public sealed class Decoder(public val config: C) { } /** - * TODO + * Decode a [CharSequence] using a maximum array size of [DEFAULT_BUFFER_SIZE]. + * The decoding operation will allocate a single array, streaming decoded bytes + * to it and flushing to [action] when needed. If the pre-calculated size + * returned by [EncoderDecoder.Config.decodeOutMaxSizeOrFail] is less than or + * equal to the [DEFAULT_BUFFER_SIZE], then [decodeToByteArray] will be used + * and [action] is only invoked once (single-shot decoding). In the event that + * [EncoderDecoder.Config.decodeOutMaxSizeOrFail] throws its [EncodingSizeException] + * due to an overflow (i.e. decoding would exceed [Int.MAX_VALUE]), or is greater + * than [DEFAULT_BUFFER_SIZE], then this function will always stream decode to a + * buffer while flushing to [action] until the decoding operation has completed. + * + * e.g. (Using `io.matthewnelson.kmp-file:file` & module `:utf8`) + * + * "/path/to/file.txt".toFile().openWrite(excl = null).use { stream -> + * val n = "Some long string" + * .decodeBuffered(UTF8, stream::write) + * println("Wrote $n UTF-8 bytes to file.txt") + * } + * + * e.g. (Using `org.kotlincrypto.hash:sha2` & module `:base64`) + * + * val d = SHA256() + * "SGVsbG8gV29ybGQh" + * .decodeBuffered(Base64.Default, d::update) + * // ... + * + * **NOTE:** The [Decoder] implementation must be compatible with version `2.6.0+` + * APIs and define a [EncoderDecoder.Config.maxDecodeEmit]. If the value is `-1` (i.e. + * it has not updated to the new API yet), then this function will fail with an + * [EncodingException]. All implementations provided by this library have been updated + * to meet the API requirement; only [EncoderDecoder] implementations external to this + * library that have not updated yet may fail when using them with [decodeBuffered] + * and [decodeBufferedAsync] APIs. + * + * @param [decoder] The [Decoder] to use. + * @param [action] The function to flush the buffer to; a destination to "write" + * decoded data to whereby `len` is the number of bytes within `buf`, starting + * at index `offset`, to "write". + * + * @return The number of decoded bytes. + * + * @see [CharSequence.decodeToByteArray] + * @see [CharSequence.decodeToByteArrayOrNull] + * @see [CharSequence.decodeBufferedAsync] + * + * @throws [EncodingException] If decoding failed. * */ @JvmStatic @Throws(EncodingException::class) @@ -270,7 +339,56 @@ public sealed class Decoder(public val config: C) { ): Long = decodeBuffered(DEFAULT_BUFFER_SIZE, decoder, action) /** - * TODO + * Decode a [CharSequence] using a maximum array size of [maxBufSize]. + * The decoding operation will allocate a single array, streaming decoded bytes + * to it and flushing to [action] when needed. If the pre-calculated size + * returned by [EncoderDecoder.Config.decodeOutMaxSizeOrFail] is less than or + * equal to the [maxBufSize], then [decodeToByteArray] will be used + * and [action] is only invoked once (single-shot decoding). In the event that + * [EncoderDecoder.Config.decodeOutMaxSizeOrFail] throws its [EncodingSizeException] + * due to an overflow (i.e. decoding would exceed [Int.MAX_VALUE]), or is greater + * than [maxBufSize], then this function will always stream decode to a + * buffer while flushing to [action] until the decoding operation has completed. + * + * e.g. (Using `io.matthewnelson.kmp-file:file` & module `:utf8`) + * + * "/path/to/file.txt".toFile().openWrite(excl = null).use { stream -> + * val n = "Some string" + * .decodeBuffered(1024, UTF8, stream::write) + * println("Wrote $n UTF-8 bytes to file.txt") + * } + * + * e.g. (Using `org.kotlincrypto.hash:sha2` & module `:base64`) + * + * val d = SHA256() + * "SGVsbG8gV29ybGQh" + * .decodeBuffered(1024, Base64.Default, d::update) + * // ... + * + * **NOTE:** The [Decoder] implementation must be compatible with version `2.6.0+` + * APIs and define a [EncoderDecoder.Config.maxDecodeEmit]. If the value is `-1` (i.e. + * it has not updated to the new API yet), then this function will fail with an + * [EncodingException]. All implementations provided by this library have been updated + * to meet the API requirement; only [EncoderDecoder] implementations external to this + * library that have not updated yet may fail when using them with [decodeBuffered] + * and [decodeBufferedAsync] APIs. + * + * @param [maxBufSize] The maximum size array this function will allocate. Must + * be greater than [EncoderDecoder.Config.maxDecodeEmit]. + * @param [decoder] The [Decoder] to use. + * @param [action] The function to flush the buffer to; a destination to "write" + * decoded data to whereby `len` is the number of bytes within `buf`, starting + * at index `offset`, to "write". + * + * @return The number of decoded bytes. + * + * @see [CharSequence.decodeToByteArray] + * @see [CharSequence.decodeToByteArrayOrNull] + * @see [CharSequence.decodeBufferedAsync] + * + * @throws [EncodingException] If decoding failed. + * @throws [IllegalArgumentException] If [maxBufSize] is less than or equal to + * [EncoderDecoder.Config.maxDecodeEmit]. * */ @JvmStatic @Throws(EncodingException::class) @@ -286,7 +404,53 @@ public sealed class Decoder(public val config: C) { ) /** - * TODO + * Decode a [CharSequence] using a maximum array size of [DEFAULT_BUFFER_SIZE]. + * The decoding operation will allocate a single array, streaming decoded bytes + * to it and flushing to [action] when needed. If the pre-calculated size + * returned by [EncoderDecoder.Config.decodeOutMaxSizeOrFail] is less than or + * equal to the [DEFAULT_BUFFER_SIZE], then [decodeToByteArray] will be used + * and [action] is only invoked once (single-shot decoding). In the event that + * [EncoderDecoder.Config.decodeOutMaxSizeOrFail] throws its [EncodingSizeException] + * due to an overflow (i.e. decoding would exceed [Int.MAX_VALUE]), or is greater + * than [DEFAULT_BUFFER_SIZE], then this function will always stream decode to a + * buffer while flushing to [action] until the decoding operation has completed. + * + * e.g. (Using `io.matthewnelson.kmp-file:async` & module `:utf8`) + * + * AsyncFs.Default.with { + * "/path/to/file.txt".toFile() + * .openWriteAsync(excl = null) + * .useAsync { stream -> + * val text = "Some long string" + * val n = text.decodeBufferedAsync( + * UTF8, + * stream::writeAsync, + * ) + * println("Wrote $n UTF-8 bytes to file.txt") + * } + * } + * + * **NOTE:** The [Decoder] implementation must be compatible with version `2.6.0+` + * APIs and define a [EncoderDecoder.Config.maxDecodeEmit]. If the value is `-1` (i.e. + * it has not updated to the new API yet), then this function will fail with an + * [EncodingException]. All implementations provided by this library have been updated + * to meet the API requirement; only [EncoderDecoder] implementations external to this + * library that have not updated yet may fail when using them with [decodeBuffered] + * and [decodeBufferedAsync] APIs. + * + * @param [decoder] The [Decoder] to use. + * @param [action] The suspend function to flush the buffer to; a destination to + * "write" decoded data to whereby `len` is the number of bytes within `buf`, + * starting at index `offset`, to "write". + * + * @return The number of decoded bytes. + * + * @see [CharSequence.decodeToByteArray] + * @see [CharSequence.decodeToByteArrayOrNull] + * @see [CharSequence.decodeBuffered] + * + * @throws [CancellationException] + * @throws [EncodingException] If decoding failed. * */ @JvmStatic @Throws(CancellationException::class, EncodingException::class) @@ -296,7 +460,58 @@ public sealed class Decoder(public val config: C) { ): Long = decodeBufferedAsync(DEFAULT_BUFFER_SIZE, decoder, action) /** - * TODO + * Decode a [CharSequence] using a maximum array size of [maxBufSize]. + * The decoding operation will allocate a single array, streaming decoded bytes + * to it and flushing to [action] when needed. If the pre-calculated size + * returned by [EncoderDecoder.Config.decodeOutMaxSizeOrFail] is less than or + * equal to the [maxBufSize], then [decodeToByteArray] will be used + * and [action] is only invoked once (single-shot decoding). In the event that + * [EncoderDecoder.Config.decodeOutMaxSizeOrFail] throws its [EncodingSizeException] + * due to an overflow (i.e. decoding would exceed [Int.MAX_VALUE]), or is greater + * than [maxBufSize], then this function will always stream decode to a + * buffer while flushing to [action] until the decoding operation has completed. + * + * e.g. (Using `io.matthewnelson.kmp-file:async` & module `:utf8`) + * + * AsyncFs.Default.with { + * "/path/to/file.txt".toFile() + * .openWriteAsync(excl = null) + * .useAsync { stream -> + * val text = "Some long string" + * val n = text.decodeBufferedAsync( + * 1024, + * UTF8, + * stream::writeAsync, + * ) + * println("Wrote $n UTF-8 bytes to file.txt") + * } + * } + * + * **NOTE:** The [Decoder] implementation must be compatible with version `2.6.0+` + * APIs and define a [EncoderDecoder.Config.maxDecodeEmit]. If the value is `-1` (i.e. + * it has not updated to the new API yet), then this function will fail with an + * [EncodingException]. All implementations provided by this library have been updated + * to meet the API requirement; only [EncoderDecoder] implementations external to this + * library that have not updated yet may fail when using them with [decodeBuffered] + * and [decodeBufferedAsync] APIs. + * + * @param [maxBufSize] The maximum size array this function will allocate. Must + * be greater than [EncoderDecoder.Config.maxDecodeEmit]. + * @param [decoder] The [Decoder] to use. + * @param [action] The suspend function to flush the buffer to; a destination to + * "write" decoded data to whereby `len` is the number of bytes within `buf`, + * starting at index `offset`, to "write". + * + * @return The number of decoded bytes. + * + * @see [CharSequence.decodeToByteArray] + * @see [CharSequence.decodeToByteArrayOrNull] + * @see [CharSequence.decodeBuffered] + * + * @throws [CancellationException] + * @throws [EncodingException] If decoding failed. + * @throws [IllegalArgumentException] If [maxBufSize] is less than or equal to + * [EncoderDecoder.Config.maxDecodeEmit]. * */ @JvmStatic @Throws(CancellationException::class, EncodingException::class) @@ -312,7 +527,54 @@ public sealed class Decoder(public val config: C) { ) /** - * TODO + * Decode a [CharArray] using a maximum array size of [DEFAULT_BUFFER_SIZE]. + * The decoding operation will allocate a single array, streaming decoded bytes + * to it and flushing to [action] when needed. If the pre-calculated size + * returned by [EncoderDecoder.Config.decodeOutMaxSizeOrFail] is less than or + * equal to the [DEFAULT_BUFFER_SIZE], then [decodeToByteArray] will be used + * and [action] is only invoked once (single-shot decoding). In the event that + * [EncoderDecoder.Config.decodeOutMaxSizeOrFail] throws its [EncodingSizeException] + * due to an overflow (i.e. decoding would exceed [Int.MAX_VALUE]), or is greater + * than [DEFAULT_BUFFER_SIZE], then this function will always stream decode to a + * buffer while flushing to [action] until the decoding operation has completed. + * + * e.g. (Using `io.matthewnelson.kmp-file:file` & module `:utf8`) + * + * "/path/to/file.txt".toFile().openWrite(excl = null).use { stream -> + * val n = "Some long string" + * .toCharArray() + * .decodeBuffered(UTF8, stream::write) + * println("Wrote $n UTF-8 bytes to file.txt") + * } + * + * e.g. (Using `org.kotlincrypto.hash:sha2` & module `:base64`) + * + * val d = SHA256() + * "SGVsbG8gV29ybGQh" + * .toCharArray() + * .decodeBuffered(Base64.Default, d::update) + * // ... + * + * **NOTE:** The [Decoder] implementation must be compatible with version `2.6.0+` + * APIs and define a [EncoderDecoder.Config.maxDecodeEmit]. If the value is `-1` (i.e. + * it has not updated to the new API yet), then this function will fail with an + * [EncodingException]. All implementations provided by this library have been updated + * to meet the API requirement; only [EncoderDecoder] implementations external to this + * library that have not updated yet may fail when using them with [decodeBuffered] + * and [decodeBufferedAsync] APIs. + * + * @param [decoder] The [Decoder] to use. + * @param [action] The function to flush the buffer to; a destination to "write" + * decoded data to whereby `len` is the number of bytes within `buf`, starting + * at index `offset`, to "write". + * + * @return The number of decoded bytes. + * + * @see [CharArray.decodeToByteArray] + * @see [CharArray.decodeToByteArrayOrNull] + * @see [CharArray.decodeBufferedAsync] + * + * @throws [EncodingException] If decoding failed. * */ @JvmStatic @Throws(EncodingException::class) @@ -322,7 +584,58 @@ public sealed class Decoder(public val config: C) { ): Long = decodeBuffered(DEFAULT_BUFFER_SIZE, decoder, action) /** - * TODO + * Decode a [CharArray] using a maximum array size of [maxBufSize]. + * The decoding operation will allocate a single array, streaming decoded bytes + * to it and flushing to [action] when needed. If the pre-calculated size + * returned by [EncoderDecoder.Config.decodeOutMaxSizeOrFail] is less than or + * equal to the [maxBufSize], then [decodeToByteArray] will be used + * and [action] is only invoked once (single-shot decoding). In the event that + * [EncoderDecoder.Config.decodeOutMaxSizeOrFail] throws its [EncodingSizeException] + * due to an overflow (i.e. decoding would exceed [Int.MAX_VALUE]), or is greater + * than [maxBufSize], then this function will always stream decode to a + * buffer while flushing to [action] until the decoding operation has completed. + * + * e.g. (Using `io.matthewnelson.kmp-file:file` & module `:utf8`) + * + * "/path/to/file.txt".toFile().openWrite(excl = null).use { stream -> + * val n = "Some string" + * .toCharArray() + * .decodeBuffered(1024, UTF8, stream::write) + * println("Wrote $n UTF-8 bytes to file.txt") + * } + * + * e.g. (Using `org.kotlincrypto.hash:sha2` & module `:base64`) + * + * val d = SHA256() + * "SGVsbG8gV29ybGQh" + * .toCharArray() + * .decodeBuffered(1024, Base64.Default, d::update) + * // ... + * + * **NOTE:** The [Decoder] implementation must be compatible with version `2.6.0+` + * APIs and define a [EncoderDecoder.Config.maxDecodeEmit]. If the value is `-1` (i.e. + * it has not updated to the new API yet), then this function will fail with an + * [EncodingException]. All implementations provided by this library have been updated + * to meet the API requirement; only [EncoderDecoder] implementations external to this + * library that have not updated yet may fail when using them with [decodeBuffered] + * and [decodeBufferedAsync] APIs. + * + * @param [maxBufSize] The maximum size array this function will allocate. Must + * be greater than [EncoderDecoder.Config.maxDecodeEmit]. + * @param [decoder] The [Decoder] to use. + * @param [action] The function to flush the buffer to; a destination to "write" + * decoded data to whereby `len` is the number of bytes within `buf`, starting + * at index `offset`, to "write". + * + * @return The number of decoded bytes. + * + * @see [CharArray.decodeToByteArray] + * @see [CharArray.decodeToByteArrayOrNull] + * @see [CharArray.decodeBufferedAsync] + * + * @throws [EncodingException] If decoding failed. + * @throws [IllegalArgumentException] If [maxBufSize] is less than or equal to + * [EncoderDecoder.Config.maxDecodeEmit]. * */ @JvmStatic @Throws(EncodingException::class) @@ -338,7 +651,54 @@ public sealed class Decoder(public val config: C) { ) /** - * TODO + * Decode a [CharArray] using a maximum array size of [DEFAULT_BUFFER_SIZE]. + * The decoding operation will allocate a single array, streaming decoded bytes + * to it and flushing to [action] when needed. If the pre-calculated size + * returned by [EncoderDecoder.Config.decodeOutMaxSizeOrFail] is less than or + * equal to the [DEFAULT_BUFFER_SIZE], then [decodeToByteArray] will be used + * and [action] is only invoked once (single-shot decoding). In the event that + * [EncoderDecoder.Config.decodeOutMaxSizeOrFail] throws its [EncodingSizeException] + * due to an overflow (i.e. decoding would exceed [Int.MAX_VALUE]), or is greater + * than [DEFAULT_BUFFER_SIZE], then this function will always stream decode to a + * buffer while flushing to [action] until the decoding operation has completed. + * + * e.g. (Using `io.matthewnelson.kmp-file:async` & module `:utf8`) + * + * AsyncFs.Default.with { + * "/path/to/file.txt".toFile() + * .openWriteAsync(excl = null) + * .useAsync { stream -> + * val chars = "Some long string" + * .toCharArray() + * val n = chars.decodeBufferedAsync( + * UTF8, + * stream::writeAsync, + * ) + * println("Wrote $n UTF-8 bytes to file.txt") + * } + * } + * + * **NOTE:** The [Decoder] implementation must be compatible with version `2.6.0+` + * APIs and define a [EncoderDecoder.Config.maxDecodeEmit]. If the value is `-1` (i.e. + * it has not updated to the new API yet), then this function will fail with an + * [EncodingException]. All implementations provided by this library have been updated + * to meet the API requirement; only [EncoderDecoder] implementations external to this + * library that have not updated yet may fail when using them with [decodeBuffered] + * and [decodeBufferedAsync] APIs. + * + * @param [decoder] The [Decoder] to use. + * @param [action] The suspend function to flush the buffer to; a destination to + * "write" decoded data to whereby `len` is the number of bytes within `buf`, + * starting at index `offset`, to "write". + * + * @return The number of decoded bytes. + * + * @see [CharArray.decodeToByteArray] + * @see [CharArray.decodeToByteArrayOrNull] + * @see [CharArray.decodeBuffered] + * + * @throws [CancellationException] + * @throws [EncodingException] If decoding failed. * */ @JvmStatic @Throws(CancellationException::class, EncodingException::class) @@ -348,7 +708,59 @@ public sealed class Decoder(public val config: C) { ): Long = decodeBufferedAsync(DEFAULT_BUFFER_SIZE, decoder, action) /** - * TODO + * Decode a [CharArray] using a maximum array size of [maxBufSize]. + * The decoding operation will allocate a single array, streaming decoded bytes + * to it and flushing to [action] when needed. If the pre-calculated size + * returned by [EncoderDecoder.Config.decodeOutMaxSizeOrFail] is less than or + * equal to the [maxBufSize], then [decodeToByteArray] will be used + * and [action] is only invoked once (single-shot decoding). In the event that + * [EncoderDecoder.Config.decodeOutMaxSizeOrFail] throws its [EncodingSizeException] + * due to an overflow (i.e. decoding would exceed [Int.MAX_VALUE]), or is greater + * than [maxBufSize], then this function will always stream decode to a + * buffer while flushing to [action] until the decoding operation has completed. + * + * e.g. (Using `io.matthewnelson.kmp-file:async` & module `:utf8`) + * + * AsyncFs.Default.with { + * "/path/to/file.txt".toFile() + * .openWriteAsync(excl = null) + * .useAsync { stream -> + * val chars = "Some long string" + * .toCharArray() + * val n = chars.decodeBufferedAsync( + * 1024, + * UTF8, + * stream::writeAsync, + * ) + * println("Wrote $n UTF-8 bytes to file.txt") + * } + * } + * + * **NOTE:** The [Decoder] implementation must be compatible with version `2.6.0+` + * APIs and define a [EncoderDecoder.Config.maxDecodeEmit]. If the value is `-1` (i.e. + * it has not updated to the new API yet), then this function will fail with an + * [EncodingException]. All implementations provided by this library have been updated + * to meet the API requirement; only [EncoderDecoder] implementations external to this + * library that have not updated yet may fail when using them with [decodeBuffered] + * and [decodeBufferedAsync] APIs. + * + * @param [maxBufSize] The maximum size array this function will allocate. Must + * be greater than [EncoderDecoder.Config.maxDecodeEmit]. + * @param [decoder] The [Decoder] to use. + * @param [action] The suspend function to flush the buffer to; a destination to + * "write" decoded data to whereby `len` is the number of bytes within `buf`, + * starting at index `offset`, to "write". + * + * @return The number of decoded bytes. + * + * @see [CharArray.decodeToByteArray] + * @see [CharArray.decodeToByteArrayOrNull] + * @see [CharArray.decodeBuffered] + * + * @throws [CancellationException] + * @throws [EncodingException] If decoding failed. + * @throws [IllegalArgumentException] If [maxBufSize] is less than or equal to + * [EncoderDecoder.Config.maxDecodeEmit]. * */ @JvmStatic @Throws(CancellationException::class, EncodingException::class) diff --git a/library/core/src/commonMain/kotlin/io/matthewnelson/encoding/core/EncoderDecoder.kt b/library/core/src/commonMain/kotlin/io/matthewnelson/encoding/core/EncoderDecoder.kt index da502904..a6c21afd 100644 --- a/library/core/src/commonMain/kotlin/io/matthewnelson/encoding/core/EncoderDecoder.kt +++ b/library/core/src/commonMain/kotlin/io/matthewnelson/encoding/core/EncoderDecoder.kt @@ -39,9 +39,9 @@ public abstract class EncoderDecoder(config: C): Encod public companion object { /** - * TODO + * A "default" buffer size of `8 * 1024` * */ - public const val DEFAULT_BUFFER_SIZE: Int = 8 * 1024 + public const val DEFAULT_BUFFER_SIZE: Int = 8 * 1024 // Note: If changing, update documentation. } /** diff --git a/library/core/src/commonMain/kotlin/io/matthewnelson/encoding/core/internal/-EncoderDecoder.kt b/library/core/src/commonMain/kotlin/io/matthewnelson/encoding/core/internal/-EncoderDecoder.kt index 60cde27c..1781096a 100644 --- a/library/core/src/commonMain/kotlin/io/matthewnelson/encoding/core/internal/-EncoderDecoder.kt +++ b/library/core/src/commonMain/kotlin/io/matthewnelson/encoding/core/internal/-EncoderDecoder.kt @@ -76,8 +76,8 @@ internal inline fun Decoder.decode( return copy } -@Throws(EncodingException::class) @OptIn(ExperimentalContracts::class) +@Throws(EncodingException::class, IllegalArgumentException::class) internal inline fun Decoder.decodeBuffered( maxBufSize: Int, _get: (i: Int) -> Char, @@ -93,10 +93,10 @@ internal inline fun Decoder.decodeBuffered( if (config.maxDecodeEmit == -1) { // EncoderDecoder.Config implementation has not updated to // new constructor which requires it to be greater than 0. - throw EncodingException("Decoder.Config.maxDecodeEmit == -1") + throw EncodingException("Decoder misconfiguration. ${this}.config.maxDecodeEmit == -1") } require(maxBufSize > config.maxDecodeEmit) { - "maxBufSize[$maxBufSize] <= Decoder.Config.maxDecodeEmit[${config.maxDecodeEmit}]" + "maxBufSize[$maxBufSize] <= ${this}.config.maxDecodeEmit[${config.maxDecodeEmit}]" } val input = _input() @@ -138,7 +138,8 @@ internal inline fun Decoder.decodeBuffered( iBuf = 0 } } - if (iBuf > 0) _action(buf, 0, iBuf) + if (iBuf == 0) return size + _action(buf, 0, iBuf) size += iBuf } finally { if (config.backFillBuffers) buf.fill(0) From b32bd2a4f77575c2c35184c91d2b954d0f8a033c Mon Sep 17 00:00:00 2001 From: Matthew Nelson Date: Mon, 8 Dec 2025 15:29:32 -0500 Subject: [PATCH 8/9] Mitigate unnecessary decode array copying by moving logic to higher level function --- .../encoding/core/internal/-EncoderDecoder.kt | 41 ++++++++++--------- 1 file changed, 21 insertions(+), 20 deletions(-) diff --git a/library/core/src/commonMain/kotlin/io/matthewnelson/encoding/core/internal/-EncoderDecoder.kt b/library/core/src/commonMain/kotlin/io/matthewnelson/encoding/core/internal/-EncoderDecoder.kt index 1781096a..027f175b 100644 --- a/library/core/src/commonMain/kotlin/io/matthewnelson/encoding/core/internal/-EncoderDecoder.kt +++ b/library/core/src/commonMain/kotlin/io/matthewnelson/encoding/core/internal/-EncoderDecoder.kt @@ -35,22 +35,29 @@ internal inline fun Decoder.decode( ): ByteArray { contract { callsInPlace(_get, InvocationKind.UNKNOWN) } val maxDecodeSize = config.decodeOutMaxSizeOrFail(input) - return decode(maxDecodeSize, input.size, _get) + val a = ByteArray(maxDecodeSize) + val len = decode(maxDecodeSizeArray = a, input.size, _get) + if (len == maxDecodeSize) return a + val copy = a.copyOf(len) + if (config.backFillBuffers) { + a.fill(0, 0, len) + } + return copy } @Throws(EncodingException::class) @OptIn(ExperimentalContracts::class) internal inline fun Decoder.decode( - maxDecodeSize: Int, + maxDecodeSizeArray: ByteArray, inputSize: Int, _get: (i: Int) -> Char, -): ByteArray { +): Int { contract { callsInPlace(_get, InvocationKind.UNKNOWN) } - val a = ByteArray(maxDecodeSize) + if (inputSize == 0) return 0 var i = 0 try { - newDecoderFeed(out = { b -> a[i++] = b }).use { feed -> + newDecoderFeed(out = { b -> maxDecodeSizeArray[i++] = b }).use { feed -> var j = 0 while (j < inputSize) { feed.consume(_get(j++)) @@ -58,22 +65,15 @@ internal inline fun Decoder.decode( } } catch (t: Throwable) { if (config.backFillBuffers) { - a.fill(0, toIndex = min(a.size, i)) + maxDecodeSizeArray.fill(0, 0, min(maxDecodeSizeArray.size, i)) } - if (t is IndexOutOfBoundsException && i >= maxDecodeSize) { + if (t is IndexOutOfBoundsException && i >= maxDecodeSizeArray.size) { // Something is wrong with the encoder's pre-calculation - throw EncodingSizeException("Encoder's pre-calculation of Size[$maxDecodeSize] was incorrect", t) + throw EncodingSizeException("Encoder's pre-calculation of Size[${maxDecodeSizeArray.size}] was incorrect", t) } throw t } - - if (i == maxDecodeSize) return a - - val copy = a.copyOf(i) - if (config.backFillBuffers) { - a.fill(0, 0, i) - } - return copy + return i } @OptIn(ExperimentalContracts::class) @@ -112,13 +112,14 @@ internal inline fun Decoder.decodeBuffered( if (maxDecodeSize !in 0..maxBufSize) return@let // Chunk // Maximum decoded size will be smaller than or equal to maxBufSize. One-shot it. - val decoded = decode(maxDecodeSize, input.size, _get) + val decoded = ByteArray(maxDecodeSize) + val len = decode(maxDecodeSizeArray = decoded, input.size, _get) try { - _action(decoded, 0, decoded.size) + _action(decoded, 0, len) } finally { - if (config.backFillBuffers) decoded.fill(0) + if (config.backFillBuffers) decoded.fill(0, 0, len) } - return decoded.size.toLong() + return len.toLong() } // Chunk From c690fd1125e2352bbdc06cd3e02014798e4505ab Mon Sep 17 00:00:00 2001 From: Matthew Nelson Date: Tue, 9 Dec 2025 08:51:51 -0500 Subject: [PATCH 9/9] Add ability to pass in an already allocated buffer to decodeBuffered/decodeBufferedAsync --- library/core/api/core.api | 8 + library/core/api/core.klib.api | 4 + .../io/matthewnelson/encoding/core/Decoder.kt | 383 +++++++++++++++++- .../io/matthewnelson/encoding/core/Encoder.kt | 4 +- .../encoding/core/EncoderDecoder.kt | 56 +-- .../encoding/core/internal/-EncoderDecoder.kt | 24 +- 6 files changed, 429 insertions(+), 50 deletions(-) diff --git a/library/core/api/core.api b/library/core/api/core.api index e9e90778..59d6f517 100644 --- a/library/core/api/core.api +++ b/library/core/api/core.api @@ -3,12 +3,16 @@ public abstract class io/matthewnelson/encoding/core/Decoder { public synthetic fun (Lio/matthewnelson/encoding/core/EncoderDecoder$Config;Lkotlin/jvm/internal/DefaultConstructorMarker;)V public static final fun decodeBuffered (Ljava/lang/CharSequence;ILio/matthewnelson/encoding/core/Decoder;Lkotlin/jvm/functions/Function3;)J public static final fun decodeBuffered (Ljava/lang/CharSequence;Lio/matthewnelson/encoding/core/Decoder;Lkotlin/jvm/functions/Function3;)J + public static final fun decodeBuffered (Ljava/lang/CharSequence;[BLio/matthewnelson/encoding/core/Decoder;Lkotlin/jvm/functions/Function3;)J public static final fun decodeBuffered ([CILio/matthewnelson/encoding/core/Decoder;Lkotlin/jvm/functions/Function3;)J public static final fun decodeBuffered ([CLio/matthewnelson/encoding/core/Decoder;Lkotlin/jvm/functions/Function3;)J + public static final fun decodeBuffered ([C[BLio/matthewnelson/encoding/core/Decoder;Lkotlin/jvm/functions/Function3;)J public static final fun decodeBufferedAsync (Ljava/lang/CharSequence;ILio/matthewnelson/encoding/core/Decoder;Lkotlin/jvm/functions/Function4;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; public static final fun decodeBufferedAsync (Ljava/lang/CharSequence;Lio/matthewnelson/encoding/core/Decoder;Lkotlin/jvm/functions/Function4;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public static final fun decodeBufferedAsync (Ljava/lang/CharSequence;[BLio/matthewnelson/encoding/core/Decoder;Lkotlin/jvm/functions/Function4;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; public static final fun decodeBufferedAsync ([CILio/matthewnelson/encoding/core/Decoder;Lkotlin/jvm/functions/Function4;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; public static final fun decodeBufferedAsync ([CLio/matthewnelson/encoding/core/Decoder;Lkotlin/jvm/functions/Function4;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public static final fun decodeBufferedAsync ([C[BLio/matthewnelson/encoding/core/Decoder;Lkotlin/jvm/functions/Function4;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; public static final fun decodeToByteArray (Ljava/lang/CharSequence;Lio/matthewnelson/encoding/core/Decoder;)[B public static final fun decodeToByteArray ([BLio/matthewnelson/encoding/core/Decoder;)[B public static final fun decodeToByteArray ([CLio/matthewnelson/encoding/core/Decoder;)[B @@ -23,12 +27,16 @@ public abstract class io/matthewnelson/encoding/core/Decoder { public final class io/matthewnelson/encoding/core/Decoder$Companion { public final fun decodeBuffered (Ljava/lang/CharSequence;ILio/matthewnelson/encoding/core/Decoder;Lkotlin/jvm/functions/Function3;)J public final fun decodeBuffered (Ljava/lang/CharSequence;Lio/matthewnelson/encoding/core/Decoder;Lkotlin/jvm/functions/Function3;)J + public final fun decodeBuffered (Ljava/lang/CharSequence;[BLio/matthewnelson/encoding/core/Decoder;Lkotlin/jvm/functions/Function3;)J public final fun decodeBuffered ([CILio/matthewnelson/encoding/core/Decoder;Lkotlin/jvm/functions/Function3;)J public final fun decodeBuffered ([CLio/matthewnelson/encoding/core/Decoder;Lkotlin/jvm/functions/Function3;)J + public final fun decodeBuffered ([C[BLio/matthewnelson/encoding/core/Decoder;Lkotlin/jvm/functions/Function3;)J public final fun decodeBufferedAsync (Ljava/lang/CharSequence;ILio/matthewnelson/encoding/core/Decoder;Lkotlin/jvm/functions/Function4;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; public final fun decodeBufferedAsync (Ljava/lang/CharSequence;Lio/matthewnelson/encoding/core/Decoder;Lkotlin/jvm/functions/Function4;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public final fun decodeBufferedAsync (Ljava/lang/CharSequence;[BLio/matthewnelson/encoding/core/Decoder;Lkotlin/jvm/functions/Function4;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; public final fun decodeBufferedAsync ([CILio/matthewnelson/encoding/core/Decoder;Lkotlin/jvm/functions/Function4;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; public final fun decodeBufferedAsync ([CLio/matthewnelson/encoding/core/Decoder;Lkotlin/jvm/functions/Function4;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public final fun decodeBufferedAsync ([C[BLio/matthewnelson/encoding/core/Decoder;Lkotlin/jvm/functions/Function4;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; public final fun decodeToByteArray (Ljava/lang/CharSequence;Lio/matthewnelson/encoding/core/Decoder;)[B public final fun decodeToByteArray ([BLio/matthewnelson/encoding/core/Decoder;)[B public final fun decodeToByteArray ([CLio/matthewnelson/encoding/core/Decoder;)[B diff --git a/library/core/api/core.klib.api b/library/core/api/core.klib.api index f3799181..a211ddc4 100644 --- a/library/core/api/core.klib.api +++ b/library/core/api/core.klib.api @@ -204,15 +204,19 @@ sealed class <#A: io.matthewnelson.encoding.core/EncoderDecoder.Config> io.matth final object Companion { // io.matthewnelson.encoding.core/Decoder.Companion|null[0] final fun (kotlin/ByteArray).decodeToByteArray(io.matthewnelson.encoding.core/Decoder<*>): kotlin/ByteArray // io.matthewnelson.encoding.core/Decoder.Companion.decodeToByteArray|decodeToByteArray@kotlin.ByteArray(io.matthewnelson.encoding.core.Decoder<*>){}[0] final fun (kotlin/ByteArray).decodeToByteArrayOrNull(io.matthewnelson.encoding.core/Decoder<*>): kotlin/ByteArray? // io.matthewnelson.encoding.core/Decoder.Companion.decodeToByteArrayOrNull|decodeToByteArrayOrNull@kotlin.ByteArray(io.matthewnelson.encoding.core.Decoder<*>){}[0] + final fun (kotlin/CharArray).decodeBuffered(kotlin/ByteArray, io.matthewnelson.encoding.core/Decoder<*>, kotlin/Function3): kotlin/Long // io.matthewnelson.encoding.core/Decoder.Companion.decodeBuffered|decodeBuffered@kotlin.CharArray(kotlin.ByteArray;io.matthewnelson.encoding.core.Decoder<*>;kotlin.Function3){}[0] final fun (kotlin/CharArray).decodeBuffered(kotlin/Int, io.matthewnelson.encoding.core/Decoder<*>, kotlin/Function3): kotlin/Long // io.matthewnelson.encoding.core/Decoder.Companion.decodeBuffered|decodeBuffered@kotlin.CharArray(kotlin.Int;io.matthewnelson.encoding.core.Decoder<*>;kotlin.Function3){}[0] final fun (kotlin/CharArray).decodeToByteArray(io.matthewnelson.encoding.core/Decoder<*>): kotlin/ByteArray // io.matthewnelson.encoding.core/Decoder.Companion.decodeToByteArray|decodeToByteArray@kotlin.CharArray(io.matthewnelson.encoding.core.Decoder<*>){}[0] final fun (kotlin/CharArray).decodeToByteArrayOrNull(io.matthewnelson.encoding.core/Decoder<*>): kotlin/ByteArray? // io.matthewnelson.encoding.core/Decoder.Companion.decodeToByteArrayOrNull|decodeToByteArrayOrNull@kotlin.CharArray(io.matthewnelson.encoding.core.Decoder<*>){}[0] + final fun (kotlin/CharSequence).decodeBuffered(kotlin/ByteArray, io.matthewnelson.encoding.core/Decoder<*>, kotlin/Function3): kotlin/Long // io.matthewnelson.encoding.core/Decoder.Companion.decodeBuffered|decodeBuffered@kotlin.CharSequence(kotlin.ByteArray;io.matthewnelson.encoding.core.Decoder<*>;kotlin.Function3){}[0] final fun (kotlin/CharSequence).decodeBuffered(kotlin/Int, io.matthewnelson.encoding.core/Decoder<*>, kotlin/Function3): kotlin/Long // io.matthewnelson.encoding.core/Decoder.Companion.decodeBuffered|decodeBuffered@kotlin.CharSequence(kotlin.Int;io.matthewnelson.encoding.core.Decoder<*>;kotlin.Function3){}[0] final fun (kotlin/CharSequence).decodeToByteArray(io.matthewnelson.encoding.core/Decoder<*>): kotlin/ByteArray // io.matthewnelson.encoding.core/Decoder.Companion.decodeToByteArray|decodeToByteArray@kotlin.CharSequence(io.matthewnelson.encoding.core.Decoder<*>){}[0] final fun (kotlin/CharSequence).decodeToByteArrayOrNull(io.matthewnelson.encoding.core/Decoder<*>): kotlin/ByteArray? // io.matthewnelson.encoding.core/Decoder.Companion.decodeToByteArrayOrNull|decodeToByteArrayOrNull@kotlin.CharSequence(io.matthewnelson.encoding.core.Decoder<*>){}[0] final inline fun (kotlin/CharArray).decodeBuffered(io.matthewnelson.encoding.core/Decoder<*>, noinline kotlin/Function3): kotlin/Long // io.matthewnelson.encoding.core/Decoder.Companion.decodeBuffered|decodeBuffered@kotlin.CharArray(io.matthewnelson.encoding.core.Decoder<*>;kotlin.Function3){}[0] final inline fun (kotlin/CharSequence).decodeBuffered(io.matthewnelson.encoding.core/Decoder<*>, noinline kotlin/Function3): kotlin/Long // io.matthewnelson.encoding.core/Decoder.Companion.decodeBuffered|decodeBuffered@kotlin.CharSequence(io.matthewnelson.encoding.core.Decoder<*>;kotlin.Function3){}[0] + final suspend fun (kotlin/CharArray).decodeBufferedAsync(kotlin/ByteArray, io.matthewnelson.encoding.core/Decoder<*>, kotlin.coroutines/SuspendFunction3): kotlin/Long // io.matthewnelson.encoding.core/Decoder.Companion.decodeBufferedAsync|decodeBufferedAsync@kotlin.CharArray(kotlin.ByteArray;io.matthewnelson.encoding.core.Decoder<*>;kotlin.coroutines.SuspendFunction3){}[0] final suspend fun (kotlin/CharArray).decodeBufferedAsync(kotlin/Int, io.matthewnelson.encoding.core/Decoder<*>, kotlin.coroutines/SuspendFunction3): kotlin/Long // io.matthewnelson.encoding.core/Decoder.Companion.decodeBufferedAsync|decodeBufferedAsync@kotlin.CharArray(kotlin.Int;io.matthewnelson.encoding.core.Decoder<*>;kotlin.coroutines.SuspendFunction3){}[0] + final suspend fun (kotlin/CharSequence).decodeBufferedAsync(kotlin/ByteArray, io.matthewnelson.encoding.core/Decoder<*>, kotlin.coroutines/SuspendFunction3): kotlin/Long // io.matthewnelson.encoding.core/Decoder.Companion.decodeBufferedAsync|decodeBufferedAsync@kotlin.CharSequence(kotlin.ByteArray;io.matthewnelson.encoding.core.Decoder<*>;kotlin.coroutines.SuspendFunction3){}[0] final suspend fun (kotlin/CharSequence).decodeBufferedAsync(kotlin/Int, io.matthewnelson.encoding.core/Decoder<*>, kotlin.coroutines/SuspendFunction3): kotlin/Long // io.matthewnelson.encoding.core/Decoder.Companion.decodeBufferedAsync|decodeBufferedAsync@kotlin.CharSequence(kotlin.Int;io.matthewnelson.encoding.core.Decoder<*>;kotlin.coroutines.SuspendFunction3){}[0] final suspend inline fun (kotlin/CharArray).decodeBufferedAsync(io.matthewnelson.encoding.core/Decoder<*>, noinline kotlin.coroutines/SuspendFunction3): kotlin/Long // io.matthewnelson.encoding.core/Decoder.Companion.decodeBufferedAsync|decodeBufferedAsync@kotlin.CharArray(io.matthewnelson.encoding.core.Decoder<*>;kotlin.coroutines.SuspendFunction3){}[0] final suspend inline fun (kotlin/CharSequence).decodeBufferedAsync(io.matthewnelson.encoding.core/Decoder<*>, noinline kotlin.coroutines/SuspendFunction3): kotlin/Long // io.matthewnelson.encoding.core/Decoder.Companion.decodeBufferedAsync|decodeBufferedAsync@kotlin.CharSequence(io.matthewnelson.encoding.core.Decoder<*>;kotlin.coroutines.SuspendFunction3){}[0] diff --git a/library/core/src/commonMain/kotlin/io/matthewnelson/encoding/core/Decoder.kt b/library/core/src/commonMain/kotlin/io/matthewnelson/encoding/core/Decoder.kt index b630ca8c..26409641 100644 --- a/library/core/src/commonMain/kotlin/io/matthewnelson/encoding/core/Decoder.kt +++ b/library/core/src/commonMain/kotlin/io/matthewnelson/encoding/core/Decoder.kt @@ -33,21 +33,28 @@ import kotlin.jvm.JvmSynthetic * Decode things. * * @see [EncoderDecoder] + * @see [decodeToByteArray] + * @see [decodeToByteArrayOrNull] + * @see [decodeBuffered] + * @see [decodeBufferedAsync] * @see [Decoder.Feed] * */ public sealed class Decoder(public val config: C) { /** - * Creates a new [Decoder.Feed], outputting decoded data to - * the supplied [Decoder.OutFeed]. + * Creates a new [Decoder.Feed], outputting decoded data to the supplied [Decoder.OutFeed]. * * e.g. * + * val sb = StringBuilder() + * + * // Alternatively use newDecoderFeed(sb::append) * myDecoder.newDecoderFeed { decodedByte -> - * println(decodedByte) + * sb.append(decodedByte) * }.use { feed -> * "MYencoDEdTEXt".forEach { c -> feed.consume(c) } * } + * println(sb.toString()) * * @see [Decoder.Feed] * */ @@ -189,6 +196,7 @@ public sealed class Decoder(public val config: C) { * are produced by [Decoder.Feed]. * * @see [newDecoderFeed] + * @see [NoOp] * */ public fun interface OutFeed { public fun output(decoded: Byte) @@ -288,13 +296,16 @@ public sealed class Decoder(public val config: C) { * The decoding operation will allocate a single array, streaming decoded bytes * to it and flushing to [action] when needed. If the pre-calculated size * returned by [EncoderDecoder.Config.decodeOutMaxSizeOrFail] is less than or - * equal to the [DEFAULT_BUFFER_SIZE], then [decodeToByteArray] will be used + * equal to the [DEFAULT_BUFFER_SIZE], then an array of that size will be allocated * and [action] is only invoked once (single-shot decoding). In the event that * [EncoderDecoder.Config.decodeOutMaxSizeOrFail] throws its [EncodingSizeException] * due to an overflow (i.e. decoding would exceed [Int.MAX_VALUE]), or is greater * than [DEFAULT_BUFFER_SIZE], then this function will always stream decode to a * buffer while flushing to [action] until the decoding operation has completed. * + * **NOTE:** Documented exceptions thrown by this function do not include those + * for which [action] may throw. + * * e.g. (Using `io.matthewnelson.kmp-file:file` & module `:utf8`) * * "/path/to/file.txt".toFile().openWrite(excl = null).use { stream -> @@ -343,13 +354,16 @@ public sealed class Decoder(public val config: C) { * The decoding operation will allocate a single array, streaming decoded bytes * to it and flushing to [action] when needed. If the pre-calculated size * returned by [EncoderDecoder.Config.decodeOutMaxSizeOrFail] is less than or - * equal to the [maxBufSize], then [decodeToByteArray] will be used + * equal to the [maxBufSize], then an array of that size will be allocated * and [action] is only invoked once (single-shot decoding). In the event that * [EncoderDecoder.Config.decodeOutMaxSizeOrFail] throws its [EncodingSizeException] * due to an overflow (i.e. decoding would exceed [Int.MAX_VALUE]), or is greater * than [maxBufSize], then this function will always stream decode to a * buffer while flushing to [action] until the decoding operation has completed. * + * **NOTE:** Documented exceptions thrown by this function do not include those + * for which [action] may throw. + * * e.g. (Using `io.matthewnelson.kmp-file:file` & module `:utf8`) * * "/path/to/file.txt".toFile().openWrite(excl = null).use { stream -> @@ -397,7 +411,85 @@ public sealed class Decoder(public val config: C) { decoder: Decoder<*>, action: (buf: ByteArray, offset: Int, len: Int) -> Unit, ): Long = decoder.decodeBuffered( - maxBufSize, + buf = null, + maxBufSize = maxBufSize, + _get = ::get, + _input = { DecoderInput(this) }, + _action = action, + ) + + /** + * Decode a [CharSequence] using the provided pre-allocated, reusable, [buf] array. + * The decoding operation will stream decoded bytes to the provided array, flushing + * to [action] when needed. If the pre-calculated size returned by + * [EncoderDecoder.Config.decodeOutMaxSizeOrFail] is less than or equal to the [buf] + * size, then [action] is only invoked once (single-shot decoding). In the event that + * [EncoderDecoder.Config.decodeOutMaxSizeOrFail] throws its [EncodingSizeException] + * due to an overflow (i.e. decoding would exceed [Int.MAX_VALUE]), or is greater + * than [buf] size, then this function will always stream decode to [buf] while + * flushing to [action] until the decoding operation has completed. + * + * **NOTE:** Documented exceptions thrown by this function do not include those + * for which [action] may throw. + * + * **NOTE:** If [EncoderDecoder.Config.backFillBuffers] is `true`, provided [buf] + * array will be back-filled with `0` bytes upon decoding completion. + * + * e.g. (Using `io.matthewnelson.kmp-file:file` & module `:utf8`) + * + * "/path/to/file.txt".toFile().openWrite(excl = null).use { stream -> + * val buf = ByteArray(DEFAULT_BUFFER_SIZE) + * var n = "Some string" + * .decodeBuffered(buf, UTF8, stream::write) + * n += "Some other string" + * .decodeBuffered(buf, UTF8, stream::write) + * println("Wrote $n UTF-8 bytes to file.txt") + * } + * + * e.g. (Using `org.kotlincrypto.hash:sha2` & module `:base64`) + * + * val d = SHA256() + * val buf = ByteArray(DEFAULT_BUFFER_SIZE) + * "SGVsbG8gV29ybGQh" + * .decodeBuffered(buf, Base64.Default, d::update) + * "SGVsbG8gV29ybGQh" + * .decodeBuffered(buf, Base64.Default, d::update) + * // ... + * + * **NOTE:** The [Decoder] implementation must be compatible with version `2.6.0+` + * APIs and define a [EncoderDecoder.Config.maxDecodeEmit]. If the value is `-1` (i.e. + * it has not updated to the new API yet), then this function will fail with an + * [EncodingException]. All implementations provided by this library have been updated + * to meet the API requirement; only [EncoderDecoder] implementations external to this + * library that have not updated yet may fail when using them with [decodeBuffered] + * and [decodeBufferedAsync] APIs. + * + * @param [buf] The pre-allocated array to use as the buffer. Its size must be + * greater than [EncoderDecoder.Config.maxDecodeEmit]. + * @param [decoder] The [Decoder] to use. + * @param [action] The function to flush the buffer to; a destination to "write" + * decoded data to whereby `len` is the number of bytes within `buf`, starting + * at index `offset`, to "write". + * + * @return The number of decoded bytes. + * + * @see [CharSequence.decodeToByteArray] + * @see [CharSequence.decodeToByteArrayOrNull] + * @see [CharSequence.decodeBufferedAsync] + * + * @throws [EncodingException] If decoding failed. + * @throws [IllegalArgumentException] If [buf] size is less than or equal to + * [EncoderDecoder.Config.maxDecodeEmit]. + * */ + @JvmStatic + @Throws(EncodingException::class) + public fun CharSequence.decodeBuffered( + buf: ByteArray, + decoder: Decoder<*>, + action: (buf: ByteArray, offset: Int, len: Int) -> Unit, + ): Long = decoder.decodeBuffered( + buf = buf, + maxBufSize = buf.size, _get = ::get, _input = { DecoderInput(this) }, _action = action, @@ -408,13 +500,16 @@ public sealed class Decoder(public val config: C) { * The decoding operation will allocate a single array, streaming decoded bytes * to it and flushing to [action] when needed. If the pre-calculated size * returned by [EncoderDecoder.Config.decodeOutMaxSizeOrFail] is less than or - * equal to the [DEFAULT_BUFFER_SIZE], then [decodeToByteArray] will be used + * equal to the [DEFAULT_BUFFER_SIZE], then an array of that size will be allocated * and [action] is only invoked once (single-shot decoding). In the event that * [EncoderDecoder.Config.decodeOutMaxSizeOrFail] throws its [EncodingSizeException] * due to an overflow (i.e. decoding would exceed [Int.MAX_VALUE]), or is greater * than [DEFAULT_BUFFER_SIZE], then this function will always stream decode to a * buffer while flushing to [action] until the decoding operation has completed. * + * **NOTE:** Documented exceptions thrown by this function do not include those + * for which [action] may throw. + * * e.g. (Using `io.matthewnelson.kmp-file:async` & module `:utf8`) * * AsyncFs.Default.with { @@ -464,13 +559,16 @@ public sealed class Decoder(public val config: C) { * The decoding operation will allocate a single array, streaming decoded bytes * to it and flushing to [action] when needed. If the pre-calculated size * returned by [EncoderDecoder.Config.decodeOutMaxSizeOrFail] is less than or - * equal to the [maxBufSize], then [decodeToByteArray] will be used + * equal to the [maxBufSize], then an array of that size will be allocated * and [action] is only invoked once (single-shot decoding). In the event that * [EncoderDecoder.Config.decodeOutMaxSizeOrFail] throws its [EncodingSizeException] * due to an overflow (i.e. decoding would exceed [Int.MAX_VALUE]), or is greater * than [maxBufSize], then this function will always stream decode to a * buffer while flushing to [action] until the decoding operation has completed. * + * **NOTE:** Documented exceptions thrown by this function do not include those + * for which [action] may throw. + * * e.g. (Using `io.matthewnelson.kmp-file:async` & module `:utf8`) * * AsyncFs.Default.with { @@ -520,24 +618,107 @@ public sealed class Decoder(public val config: C) { decoder: Decoder<*>, action: suspend (buf: ByteArray, offset: Int, len: Int) -> Unit, ): Long = decoder.decodeBuffered( - maxBufSize, + buf = null, + maxBufSize = maxBufSize, _get = ::get, _input = { DecoderInput(this) }, _action = { buf, offset, len -> action(buf, offset, len) }, ) + /** + * Decode a [CharSequence] using the provided pre-allocated, reusable, [buf] array. + * The decoding operation will stream decoded bytes to the provided array, flushing + * to [action] when needed. If the pre-calculated size returned by + * [EncoderDecoder.Config.decodeOutMaxSizeOrFail] is less than or equal to the [buf] + * size, then [action] is only invoked once (single-shot decoding). In the event that + * [EncoderDecoder.Config.decodeOutMaxSizeOrFail] throws its [EncodingSizeException] + * due to an overflow (i.e. decoding would exceed [Int.MAX_VALUE]), or is greater + * than [buf] size, then this function will always stream decode to [buf] while + * flushing to [action] until the decoding operation has completed. + * + * **NOTE:** Documented exceptions thrown by this function do not include those + * for which [action] may throw. + * + * **NOTE:** If [EncoderDecoder.Config.backFillBuffers] is `true`, provided [buf] + * array will be back-filled with `0` bytes upon decoding completion. + * + * e.g. (Using `io.matthewnelson.kmp-file:async` & module `:utf8`) + * + * AsyncFs.Default.with { + * "/path/to/file.txt".toFile() + * .openWriteAsync(excl = null) + * .useAsync { stream -> + * val text = "Some long string" + * val buf = ByteArray(DEFAULT_BUFFER_SIZE) + * var n = text.decodeBufferedAsync( + * buf, + * UTF8, + * stream::writeAsync, + * ) + * n += text.decodeBufferedAsync( + * buf, + * UTF8, + * stream::writeAsync, + * ) + * println("Wrote $n UTF-8 bytes to file.txt") + * } + * } + * + * **NOTE:** The [Decoder] implementation must be compatible with version `2.6.0+` + * APIs and define a [EncoderDecoder.Config.maxDecodeEmit]. If the value is `-1` (i.e. + * it has not updated to the new API yet), then this function will fail with an + * [EncodingException]. All implementations provided by this library have been updated + * to meet the API requirement; only [EncoderDecoder] implementations external to this + * library that have not updated yet may fail when using them with [decodeBuffered] + * and [decodeBufferedAsync] APIs. + * + * @param [buf] The pre-allocated array to use as the buffer. Its size must be + * greater than [EncoderDecoder.Config.maxDecodeEmit]. + * @param [decoder] The [Decoder] to use. + * @param [action] The suspend function to flush the buffer to; a destination to + * "write" decoded data to whereby `len` is the number of bytes within `buf`, + * starting at index `offset`, to "write". + * + * @return The number of decoded bytes. + * + * @see [CharSequence.decodeToByteArray] + * @see [CharSequence.decodeToByteArrayOrNull] + * @see [CharSequence.decodeBuffered] + * + * @throws [CancellationException] + * @throws [EncodingException] If decoding failed. + * @throws [IllegalArgumentException] If [buf] size is less than or equal to + * [EncoderDecoder.Config.maxDecodeEmit]. + * */ + @JvmStatic + @Throws(CancellationException::class, EncodingException::class) + public suspend fun CharSequence.decodeBufferedAsync( + buf: ByteArray, + decoder: Decoder<*>, + action: suspend (buf: ByteArray, offset: Int, len: Int) -> Unit, + ): Long = decoder.decodeBuffered( + buf = buf, + maxBufSize = buf.size, + _get = ::get, + _input = { DecoderInput(this) }, + _action = { _buf, offset, len -> action(_buf, offset, len) }, + ) + /** * Decode a [CharArray] using a maximum array size of [DEFAULT_BUFFER_SIZE]. * The decoding operation will allocate a single array, streaming decoded bytes * to it and flushing to [action] when needed. If the pre-calculated size * returned by [EncoderDecoder.Config.decodeOutMaxSizeOrFail] is less than or - * equal to the [DEFAULT_BUFFER_SIZE], then [decodeToByteArray] will be used + * equal to the [DEFAULT_BUFFER_SIZE], then an array of that size will be allocated * and [action] is only invoked once (single-shot decoding). In the event that * [EncoderDecoder.Config.decodeOutMaxSizeOrFail] throws its [EncodingSizeException] * due to an overflow (i.e. decoding would exceed [Int.MAX_VALUE]), or is greater * than [DEFAULT_BUFFER_SIZE], then this function will always stream decode to a * buffer while flushing to [action] until the decoding operation has completed. * + * **NOTE:** Documented exceptions thrown by this function do not include those + * for which [action] may throw. + * * e.g. (Using `io.matthewnelson.kmp-file:file` & module `:utf8`) * * "/path/to/file.txt".toFile().openWrite(excl = null).use { stream -> @@ -588,13 +769,16 @@ public sealed class Decoder(public val config: C) { * The decoding operation will allocate a single array, streaming decoded bytes * to it and flushing to [action] when needed. If the pre-calculated size * returned by [EncoderDecoder.Config.decodeOutMaxSizeOrFail] is less than or - * equal to the [maxBufSize], then [decodeToByteArray] will be used + * equal to the [maxBufSize], then an array of that size will be allocated * and [action] is only invoked once (single-shot decoding). In the event that * [EncoderDecoder.Config.decodeOutMaxSizeOrFail] throws its [EncodingSizeException] * due to an overflow (i.e. decoding would exceed [Int.MAX_VALUE]), or is greater * than [maxBufSize], then this function will always stream decode to a * buffer while flushing to [action] until the decoding operation has completed. * + * **NOTE:** Documented exceptions thrown by this function do not include those + * for which [action] may throw. + * * e.g. (Using `io.matthewnelson.kmp-file:file` & module `:utf8`) * * "/path/to/file.txt".toFile().openWrite(excl = null).use { stream -> @@ -644,7 +828,89 @@ public sealed class Decoder(public val config: C) { decoder: Decoder<*>, action: (buf: ByteArray, offset: Int, len: Int) -> Unit, ): Long = decoder.decodeBuffered( - maxBufSize, + buf = null, + maxBufSize = maxBufSize, + _get = ::get, + _input = { DecoderInput(this) }, + _action = action, + ) + + /** + * Decode a [CharArray] using the provided pre-allocated, reusable, [buf] array. + * The decoding operation will stream decoded bytes to the provided array, flushing + * to [action] when needed. If the pre-calculated size returned by + * [EncoderDecoder.Config.decodeOutMaxSizeOrFail] is less than or equal to the [buf] + * size, then [action] is only invoked once (single-shot decoding). In the event that + * [EncoderDecoder.Config.decodeOutMaxSizeOrFail] throws its [EncodingSizeException] + * due to an overflow (i.e. decoding would exceed [Int.MAX_VALUE]), or is greater + * than [buf] size, then this function will always stream decode to [buf] while + * flushing to [action] until the decoding operation has completed. + * + * **NOTE:** Documented exceptions thrown by this function do not include those + * for which [action] may throw. + * + * **NOTE:** If [EncoderDecoder.Config.backFillBuffers] is `true`, provided [buf] + * array will be back-filled with `0` bytes upon decoding completion. + * + * e.g. (Using `io.matthewnelson.kmp-file:file` & module `:utf8`) + * + * "/path/to/file.txt".toFile().openWrite(excl = null).use { stream -> + * val buf = ByteArray(DEFAULT_BUFFER_SIZE) + * var n = "Some string" + * .toCharArray() + * .decodeBuffered(buf, UTF8, stream::write) + * n += "Some other string" + * .toCharArray() + * .decodeBuffered(buf, UTF8, stream::write) + * println("Wrote $n UTF-8 bytes to file.txt") + * } + * + * e.g. (Using `org.kotlincrypto.hash:sha2` & module `:base64`) + * + * val d = SHA256() + * val buf = ByteArray(DEFAULT_BUFFER_SIZE) + * "SGVsbG8gV29ybGQh" + * .toCharArray() + * .decodeBuffered(buf, Base64.Default, d::update) + * "SGVsbG8gV29ybGQh" + * .toCharArray() + * .decodeBuffered(buf, Base64.Default, d::update) + * // ... + * + * **NOTE:** The [Decoder] implementation must be compatible with version `2.6.0+` + * APIs and define a [EncoderDecoder.Config.maxDecodeEmit]. If the value is `-1` (i.e. + * it has not updated to the new API yet), then this function will fail with an + * [EncodingException]. All implementations provided by this library have been updated + * to meet the API requirement; only [EncoderDecoder] implementations external to this + * library that have not updated yet may fail when using them with [decodeBuffered] + * and [decodeBufferedAsync] APIs. + * + * @param [buf] The pre-allocated array to use as the buffer. Its size must be + * greater than [EncoderDecoder.Config.maxDecodeEmit]. + * @param [decoder] The [Decoder] to use. + * @param [action] The function to flush the buffer to; a destination to "write" + * decoded data to whereby `len` is the number of bytes within `buf`, starting + * at index `offset`, to "write". + * + * @return The number of decoded bytes. + * + * @see [CharArray.decodeToByteArray] + * @see [CharArray.decodeToByteArrayOrNull] + * @see [CharArray.decodeBufferedAsync] + * + * @throws [EncodingException] If decoding failed. + * @throws [IllegalArgumentException] If [buf] size is less than or equal to + * [EncoderDecoder.Config.maxDecodeEmit]. + * */ + @JvmStatic + @Throws(EncodingException::class) + public fun CharArray.decodeBuffered( + buf: ByteArray, + decoder: Decoder<*>, + action: (buf: ByteArray, offset: Int, len: Int) -> Unit, + ): Long = decoder.decodeBuffered( + buf = buf, + maxBufSize = buf.size, _get = ::get, _input = { DecoderInput(this) }, _action = action, @@ -655,13 +921,16 @@ public sealed class Decoder(public val config: C) { * The decoding operation will allocate a single array, streaming decoded bytes * to it and flushing to [action] when needed. If the pre-calculated size * returned by [EncoderDecoder.Config.decodeOutMaxSizeOrFail] is less than or - * equal to the [DEFAULT_BUFFER_SIZE], then [decodeToByteArray] will be used + * equal to the [DEFAULT_BUFFER_SIZE], then an array of that size will be allocated * and [action] is only invoked once (single-shot decoding). In the event that * [EncoderDecoder.Config.decodeOutMaxSizeOrFail] throws its [EncodingSizeException] * due to an overflow (i.e. decoding would exceed [Int.MAX_VALUE]), or is greater * than [DEFAULT_BUFFER_SIZE], then this function will always stream decode to a * buffer while flushing to [action] until the decoding operation has completed. * + * **NOTE:** Documented exceptions thrown by this function do not include those + * for which [action] may throw. + * * e.g. (Using `io.matthewnelson.kmp-file:async` & module `:utf8`) * * AsyncFs.Default.with { @@ -712,13 +981,16 @@ public sealed class Decoder(public val config: C) { * The decoding operation will allocate a single array, streaming decoded bytes * to it and flushing to [action] when needed. If the pre-calculated size * returned by [EncoderDecoder.Config.decodeOutMaxSizeOrFail] is less than or - * equal to the [maxBufSize], then [decodeToByteArray] will be used + * equal to the [maxBufSize], then an array of that size will be allocated * and [action] is only invoked once (single-shot decoding). In the event that * [EncoderDecoder.Config.decodeOutMaxSizeOrFail] throws its [EncodingSizeException] * due to an overflow (i.e. decoding would exceed [Int.MAX_VALUE]), or is greater * than [maxBufSize], then this function will always stream decode to a * buffer while flushing to [action] until the decoding operation has completed. * + * **NOTE:** Documented exceptions thrown by this function do not include those + * for which [action] may throw. + * * e.g. (Using `io.matthewnelson.kmp-file:async` & module `:utf8`) * * AsyncFs.Default.with { @@ -769,12 +1041,93 @@ public sealed class Decoder(public val config: C) { decoder: Decoder<*>, action: suspend (buf: ByteArray, offset: Int, len: Int) -> Unit, ): Long = decoder.decodeBuffered( - maxBufSize, + buf = null, + maxBufSize = maxBufSize, _get = ::get, _input = { DecoderInput(this) }, _action = { buf, offset, len -> action(buf, offset, len) }, ) + /** + * Decode a [CharArray] using the provided pre-allocated, reusable, [buf] array. + * The decoding operation will stream decoded bytes to the provided array, flushing + * to [action] when needed. If the pre-calculated size returned by + * [EncoderDecoder.Config.decodeOutMaxSizeOrFail] is less than or equal to the [buf] + * size, then [action] is only invoked once (single-shot decoding). In the event that + * [EncoderDecoder.Config.decodeOutMaxSizeOrFail] throws its [EncodingSizeException] + * due to an overflow (i.e. decoding would exceed [Int.MAX_VALUE]), or is greater + * than [buf] size, then this function will always stream decode to [buf] while + * flushing to [action] until the decoding operation has completed. + * + * **NOTE:** Documented exceptions thrown by this function do not include those + * for which [action] may throw. + * + * **NOTE:** If [EncoderDecoder.Config.backFillBuffers] is `true`, provided [buf] + * array will be back-filled with `0` bytes upon decoding completion. + * + * e.g. (Using `io.matthewnelson.kmp-file:async` & module `:utf8`) + * + * AsyncFs.Default.with { + * "/path/to/file.txt".toFile() + * .openWriteAsync(excl = null) + * .useAsync { stream -> + * val chars = "Some long string" + * .toCharArray() + * val buf = ByteArray(DEFAULT_BUFFER_SIZE) + * var n = chars.decodeBufferedAsync( + * buf, + * UTF8, + * stream::writeAsync, + * ) + * n += chars.decodeBufferedAsync( + * buf, + * UTF8, + * stream::writeAsync, + * ) + * println("Wrote $n UTF-8 bytes to file.txt") + * } + * } + * + * **NOTE:** The [Decoder] implementation must be compatible with version `2.6.0+` + * APIs and define a [EncoderDecoder.Config.maxDecodeEmit]. If the value is `-1` (i.e. + * it has not updated to the new API yet), then this function will fail with an + * [EncodingException]. All implementations provided by this library have been updated + * to meet the API requirement; only [EncoderDecoder] implementations external to this + * library that have not updated yet may fail when using them with [decodeBuffered] + * and [decodeBufferedAsync] APIs. + * + * @param [buf] The pre-allocated array to use as the buffer. Its size must be + * greater than [EncoderDecoder.Config.maxDecodeEmit]. + * @param [decoder] The [Decoder] to use. + * @param [action] The suspend function to flush the buffer to; a destination to + * "write" decoded data to whereby `len` is the number of bytes within `buf`, + * starting at index `offset`, to "write". + * + * @return The number of decoded bytes. + * + * @see [CharArray.decodeToByteArray] + * @see [CharArray.decodeToByteArrayOrNull] + * @see [CharArray.decodeBuffered] + * + * @throws [CancellationException] + * @throws [EncodingException] If decoding failed. + * @throws [IllegalArgumentException] If [buf] size is less than or equal to + * [EncoderDecoder.Config.maxDecodeEmit]. + * */ + @JvmStatic + @Throws(CancellationException::class, EncodingException::class) + public suspend fun CharArray.decodeBufferedAsync( + buf: ByteArray, + decoder: Decoder<*>, + action: suspend (buf: ByteArray, offset: Int, len: Int) -> Unit, + ): Long = decoder.decodeBuffered( + buf = buf, + maxBufSize = buf.size, + _get = ::get, + _input = { DecoderInput(this) }, + _action = { _buf, offset, len -> action(_buf, offset, len) }, + ) + /** * DEPRECATED since `2.3.0` * @suppress 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 ee6af805..9c4c8e72 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 @@ -32,7 +32,6 @@ import kotlin.jvm.JvmSynthetic * @see [EncoderDecoder] * @see [encodeToString] * @see [encodeToCharArray] - * @see [encodeToByteArray] * @see [Encoder.Feed] * */ public sealed class Encoder(config: C): Decoder(config) { @@ -46,6 +45,8 @@ public sealed class Encoder(config: C): Decoder(con * e.g. * * val sb = StringBuilder() + * + * // Alternatively use newEncoderFeed(sb::append) * myEncoder.newEncoderFeed { encodedChar -> * sb.append(encodedChar) * }.use { feed -> @@ -174,6 +175,7 @@ public sealed class Encoder(config: C): Decoder(con * are produced by [Encoder.Feed]. * * @see [newEncoderFeed] + * @see [NoOp] * */ public fun interface OutFeed { public fun output(encoded: Char) diff --git a/library/core/src/commonMain/kotlin/io/matthewnelson/encoding/core/EncoderDecoder.kt b/library/core/src/commonMain/kotlin/io/matthewnelson/encoding/core/EncoderDecoder.kt index a6c21afd..36569451 100644 --- a/library/core/src/commonMain/kotlin/io/matthewnelson/encoding/core/EncoderDecoder.kt +++ b/library/core/src/commonMain/kotlin/io/matthewnelson/encoding/core/EncoderDecoder.kt @@ -59,11 +59,13 @@ public abstract class EncoderDecoder(config: C): Encod public val isLenient: Boolean?, /** - * If greater than `0`, [Encoder.newEncoderFeed] will use the [LineBreakOutFeed] such that - * for every [lineBreakInterval] number of encoded characters output, the next encoded - * character will be preceded with the new line character `\n`. + * If greater than `0`, [Encoder.newEncoderFeed] may use a [LineBreakOutFeed] such that + * for every [lineBreakInterval] number of encoded characters output by the [Encoder.Feed], + * the next encoded character output will be preceded with a new line character `\n`. * * **NOTE:** This setting will always be `0` if [isLenient] is `false`. + * + * @see [Encoder.newEncoderFeed] * */ @JvmField public val lineBreakInterval: Byte, @@ -82,33 +84,37 @@ public abstract class EncoderDecoder(config: C): Encod /** * The maximum number of bytes that the implementation's [Decoder.Feed] can potentially - * emit on a single invocation of [Decoder.Feed.consume] or [Decoder.Feed.doFinal]. For - * example, `Base16` decoding will emit `1` byte for every `2` characters of input, so - * its value is `1`. `Base32` decoding will emit `5` bytes for every `8` characters of - * input, so its value is `5`. `UTF8` "decoding" (i.e. text to UTF-8 byte transformations) - * can emit `4` to `6` bytes, depending on buffered input and the size of the replacement - * byte sequence being used, so would require a calculation such as - * `(replacementStrategy.size * 2).coerceAtLeast(4)`. + * emit on a single invocation of [Decoder.Feed.consume], [Decoder.Feed.flush], or + * [Decoder.Feed.doFinal]. + * + * For example, `Base16` decoding will emit `1` byte for every `2` characters of input, + * so its maximum emission is `1`. `Base32` decoding will emit `5` bytes for every `8` + * characters of input, so its maximum emission is `5`. `UTF8` "decoding" (i.e. text to + * UTF-8 byte transformations) can emit `4` bytes, but also depending on the size of the + * replacement byte sequence being used, can emit more; its maximum emission size is + * required to be calculated, such as `(replacementStrategy.size * 2).coerceAtLeast(4)`. * * Value will be greater than `0`, or `-1` which indicates that the [EncoderDecoder.Config] - * implementation has not updated to the new constructor introduced in version `2.6.0`. + * implementation has not updated to the new constructor introduced in version `2.6.0` and + * as such is unable to be used with `:core` module APIs dependent on this value (such as + * [Decoder.decodeBuffered] or [Decoder.decodeBufferedAsync]). * */ @JvmField public val maxDecodeEmit: Int, /** * When the functions [Encoder.encodeToString], [Encoder.encodeToCharArray], - * [Decoder.decodeToByteArray], and [Decoder.decodeBuffered] are utilized, an initial - * buffer gets allocated based on the pre-calculated return values of [encodeOutMaxSize] - * or [decodeOutMaxSize] (respectively). After encoding/decoding operations have completed, - * the initial buffer may be trimmed to size in the event of an over-allocation. If that - * happens, the initial buffer is then dropped and the correct sized copy is returned. Prior - * versions always back-filled the initial buffer with `0` or a space character when this - * occurred, but that can be computationally expensive for large data sets and potentially - * unnecessary if data is known to not be sensitive in nature. - * - * If `true`, the initial buffer (if it was trimmed to size) is back-filled. If `false`, - * back-filling is skipped. + * [Decoder.decodeToByteArray], [Decoder.decodeBuffered], and [Decoder.decodeBufferedAsync] + * are utilized, they may allocate an appropriate medium (a buffer) to store encoded/decoded + * data (e.g. a [StringBuilder], [CharArray], or [ByteArray]). Depending on the underlying + * encoding/decoding operation, such as an array over-allocation due to [encodeOutMaxSize] + * or [decodeOutMaxSize], those initially allocated buffers may not be returned as the + * function's result. Prior versions of this library always back-filled them with `0` or a + * space character, but that can be computationally expensive for large datasets and + * potentially unnecessary if data is known to not be sensitive in nature. + * + * If `true`, any non-result buffer allocations are back-filled before being de-referenced + * by function return. If `false`, back-filling is skipped. * */ @JvmField public val backFillBuffers: Boolean, @@ -120,7 +126,7 @@ public abstract class EncoderDecoder(config: C): Encod /** * Instantiates a new [Config] instance. * - * @throws [IllegalArgumentException] If [maxDecodeEmit] is less than or equal to `0`. + * @throws [IllegalArgumentException] If [maxDecodeEmit] is less than `1`. * */ protected constructor( isLenient: Boolean?, @@ -494,7 +500,7 @@ public abstract class EncoderDecoder(config: C): Encod * @suppress * */ @Deprecated( - message = "Parameter maxDecodeEmit and backFillBuffers were added. Use the new constructor.", + message = "Parameters maxDecodeEmit and backFillBuffers were added. Use the new constructor.", replaceWith = ReplaceWith( expression = "EncoderDecoder.Config(isLenient, lineBreakInterval, paddingChar, maxDecodeEmit = 0 /* TODO */, backFillBuffers = true)"), level = DeprecationLevel.WARNING, @@ -507,7 +513,7 @@ public abstract class EncoderDecoder(config: C): Encod isLenient = isLenient, lineBreakInterval = lineBreakIntervalOrZero(isLenient, lineBreakInterval), paddingChar = paddingChar, - maxDecodeEmit = -1, + maxDecodeEmit = -1, // NOTE: NEVER change. backFillBuffers = true, unused = null, ) diff --git a/library/core/src/commonMain/kotlin/io/matthewnelson/encoding/core/internal/-EncoderDecoder.kt b/library/core/src/commonMain/kotlin/io/matthewnelson/encoding/core/internal/-EncoderDecoder.kt index 027f175b..aa5942ce 100644 --- a/library/core/src/commonMain/kotlin/io/matthewnelson/encoding/core/internal/-EncoderDecoder.kt +++ b/library/core/src/commonMain/kotlin/io/matthewnelson/encoding/core/internal/-EncoderDecoder.kt @@ -79,6 +79,7 @@ internal inline fun Decoder.decode( @OptIn(ExperimentalContracts::class) @Throws(EncodingException::class, IllegalArgumentException::class) internal inline fun Decoder.decodeBuffered( + buf: ByteArray?, maxBufSize: Int, _get: (i: Int) -> Char, _input: () -> DecoderInput, @@ -90,13 +91,18 @@ internal inline fun Decoder.decodeBuffered( callsInPlace(_action, InvocationKind.UNKNOWN) } + if (buf != null) { + // Ensure function caller passed in buf.size for maxBufSize + check(buf.size == maxBufSize) { "buf.size[${buf.size}] != maxBufSize[$maxBufSize]" } + } if (config.maxDecodeEmit == -1) { // EncoderDecoder.Config implementation has not updated to // new constructor which requires it to be greater than 0. throw EncodingException("Decoder misconfiguration. ${this}.config.maxDecodeEmit == -1") } require(maxBufSize > config.maxDecodeEmit) { - "maxBufSize[$maxBufSize] <= ${this}.config.maxDecodeEmit[${config.maxDecodeEmit}]" + val parameter = if (buf != null) "buf.size" else "maxBufSize" + "$parameter[$maxBufSize] <= ${this}.config.maxDecodeEmit[${config.maxDecodeEmit}]" } val input = _input() @@ -111,8 +117,8 @@ internal inline fun Decoder.decodeBuffered( }.let { maxDecodeSize -> if (maxDecodeSize !in 0..maxBufSize) return@let // Chunk - // Maximum decoded size will be smaller than or equal to maxBufSize. One-shot it. - val decoded = ByteArray(maxDecodeSize) + // Maximum decoded size will be less than or equal to maxBufSize. One-shot it. + val decoded = buf ?: ByteArray(maxDecodeSize) val len = decode(maxDecodeSizeArray = decoded, input.size, _get) try { _action(decoded, 0, len) @@ -123,27 +129,27 @@ internal inline fun Decoder.decodeBuffered( } // Chunk - val buf = ByteArray(maxBufSize) - val limit = buf.size - config.maxDecodeEmit + val _buf = buf ?: ByteArray(maxBufSize) + val limit = _buf.size - config.maxDecodeEmit val inputSize = input.size var iBuf = 0 var i = 0 var size = 0L try { - newDecoderFeed(out = { b -> buf[iBuf++] = b }).use { feed -> + newDecoderFeed(out = { b -> _buf[iBuf++] = b }).use { feed -> while (i < inputSize) { feed.consume(input = _get(i++)) if (iBuf <= limit) continue - _action(buf, 0, iBuf) + _action(_buf, 0, iBuf) size += iBuf iBuf = 0 } } if (iBuf == 0) return size - _action(buf, 0, iBuf) + _action(_buf, 0, iBuf) size += iBuf } finally { - if (config.backFillBuffers) buf.fill(0) + if (config.backFillBuffers) _buf.fill(0) } return size }