diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 4a149880..1c4ccd8d 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -6,6 +6,8 @@ gradle-kmp-configuration = "0.5.2" gradle-kotlin = "2.2.21" gradle-publish-maven = "0.35.0" +kotlinx-coroutines = "1.10.2" + [libraries] benchmark-runtime = { module = "org.jetbrains.kotlinx:kotlinx-benchmark-runtime", version.ref = "gradle-benchmark" } @@ -14,6 +16,9 @@ gradle-kmp-configuration = { module = "io.matthewnelson:gradle-kmp-configurat gradle-kotlin = { module = "org.jetbrains.kotlin:kotlin-gradle-plugin", version.ref = "gradle-kotlin" } gradle-publish-maven = { module = "com.vanniktech:gradle-maven-publish-plugin", version.ref = "gradle-publish-maven" } +# tests +kotlinx-coroutines-test = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-test", version.ref = "kotlinx-coroutines" } + [plugins] benchmark = { id = "org.jetbrains.kotlinx.benchmark", version.ref = "gradle-benchmark" } binary-compat = { id = "org.jetbrains.kotlinx.binary-compatibility-validator", version.ref = "gradle-binary-compat" } 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 01d77198..e77a3ecd 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 @@ -263,9 +263,9 @@ public class Base32CrockfordConfigBuilder { * }.newEncoderFeed { encodedChar -> * sb.append(encodedChar) * }.use { feed -> - * bytes1.forEach { b -> feed.consume(b) } + * bytes1.forEach(feed::consume) * feed.flush() - * bytes2.forEach { b -> feed.consume(b) } + * bytes2.forEach(feed::consume) * } * println(sb.toString()) * // 91JP-RV3F-*41BP-YWKC-CGGG-* @@ -280,9 +280,9 @@ public class Base32CrockfordConfigBuilder { * }.newEncoderFeed { encodedChar -> * sb.append(encodedChar) * }.use { feed -> - * bytes1.forEach { b -> feed.consume(b) } + * bytes1.forEach(feed::consume) * feed.flush() - * bytes2.forEach { b -> feed.consume(b) } + * bytes2.forEach(feed::consume) * } * println(sb.toString()) * // 91JP-RV3F-41BP-YWKC-CGGG-* diff --git a/library/core/README.md b/library/core/README.md index 4a8adb58..1166131f 100644 --- a/library/core/README.md +++ b/library/core/README.md @@ -25,10 +25,10 @@ fun main() { Base64.Default.newEncoderFeed(outN).use { feed -> // Encode UTF-8 bytes to base64 - "Hello World 1!".decodeToByteArray(UTF8).forEach { b -> feed.consume(b) } + "Hello World 1!".decodeToByteArray(UTF8).forEach(feed::consume) feed.flush() // Finalize first encoding to reuse the Feed outN.output('.') // Add a separator or something. - "Hello World 2!".decodeToByteArray(UTF8).forEach { b -> feed.consume(b) } + "Hello World 2!".decodeToByteArray(UTF8).forEach(feed::consume) } // << `Feed.use` extension function will call Feed.doFinal automatically val encoded = sb.toString() @@ -44,10 +44,10 @@ fun main() { // 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) } + encoded.substringBefore('.').forEach(feedB64::consume) feedB64.flush() // Finalize first decoding to reuse the Feed feedUTF8.flush() // Prepare UTF8 feed for second decoding - encoded.substringAfter('.').forEach { c -> feedB64.consume(c) } + encoded.substringAfter('.').forEach(feedB64::consume) } // << `Feed.use` extension function will call Feed.doFinal automatically } // << `Feed.use` extension function will call Feed.doFinal automatically diff --git a/library/core/api/core.api b/library/core/api/core.api index 0ff1f8b7..503811a8 100644 --- a/library/core/api/core.api +++ b/library/core/api/core.api @@ -3,26 +3,42 @@ 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;Lio/matthewnelson/encoding/core/Decoder;ZIIILkotlin/jvm/functions/Function3;)J + public static final fun decodeBuffered (Ljava/lang/CharSequence;Lio/matthewnelson/encoding/core/Decoder;ZIILkotlin/jvm/functions/Function3;)J + public static final fun decodeBuffered (Ljava/lang/CharSequence;Lio/matthewnelson/encoding/core/Decoder;ZII[BLkotlin/jvm/functions/Function3;)J public static final fun decodeBuffered (Ljava/lang/CharSequence;Lio/matthewnelson/encoding/core/Decoder;ZILkotlin/jvm/functions/Function3;)J public static final fun decodeBuffered (Ljava/lang/CharSequence;Lio/matthewnelson/encoding/core/Decoder;ZLkotlin/jvm/functions/Function3;)J public static final fun decodeBuffered (Ljava/lang/CharSequence;Lio/matthewnelson/encoding/core/Decoder;Z[BLkotlin/jvm/functions/Function3;)J + public static final fun decodeBuffered ([CLio/matthewnelson/encoding/core/Decoder;ZIIILkotlin/jvm/functions/Function3;)J + public static final fun decodeBuffered ([CLio/matthewnelson/encoding/core/Decoder;ZIILkotlin/jvm/functions/Function3;)J + public static final fun decodeBuffered ([CLio/matthewnelson/encoding/core/Decoder;ZII[BLkotlin/jvm/functions/Function3;)J public static final fun decodeBuffered ([CLio/matthewnelson/encoding/core/Decoder;ZILkotlin/jvm/functions/Function3;)J public static final fun decodeBuffered ([CLio/matthewnelson/encoding/core/Decoder;ZLkotlin/jvm/functions/Function3;)J public static final fun decodeBuffered ([CLio/matthewnelson/encoding/core/Decoder;Z[BLkotlin/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;Lio/matthewnelson/encoding/core/Decoder;ZIIILkotlin/jvm/functions/Function4;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public static final fun decodeBufferedAsync (Ljava/lang/CharSequence;Lio/matthewnelson/encoding/core/Decoder;ZIILkotlin/jvm/functions/Function4;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public static final fun decodeBufferedAsync (Ljava/lang/CharSequence;Lio/matthewnelson/encoding/core/Decoder;ZII[BLkotlin/jvm/functions/Function4;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; public static final fun decodeBufferedAsync (Ljava/lang/CharSequence;Lio/matthewnelson/encoding/core/Decoder;ZILkotlin/jvm/functions/Function4;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; public static final fun decodeBufferedAsync (Ljava/lang/CharSequence;Lio/matthewnelson/encoding/core/Decoder;ZLkotlin/jvm/functions/Function4;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; public static final fun decodeBufferedAsync (Ljava/lang/CharSequence;Lio/matthewnelson/encoding/core/Decoder;Z[BLkotlin/jvm/functions/Function4;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public static final fun decodeBufferedAsync ([CLio/matthewnelson/encoding/core/Decoder;ZIIILkotlin/jvm/functions/Function4;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public static final fun decodeBufferedAsync ([CLio/matthewnelson/encoding/core/Decoder;ZIILkotlin/jvm/functions/Function4;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public static final fun decodeBufferedAsync ([CLio/matthewnelson/encoding/core/Decoder;ZII[BLkotlin/jvm/functions/Function4;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; public static final fun decodeBufferedAsync ([CLio/matthewnelson/encoding/core/Decoder;ZILkotlin/jvm/functions/Function4;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; public static final fun decodeBufferedAsync ([CLio/matthewnelson/encoding/core/Decoder;ZLkotlin/jvm/functions/Function4;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; public static final fun decodeBufferedAsync ([CLio/matthewnelson/encoding/core/Decoder;Z[BLkotlin/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 (Ljava/lang/CharSequence;Lio/matthewnelson/encoding/core/Decoder;II)[B public static final fun decodeToByteArray ([BLio/matthewnelson/encoding/core/Decoder;)[B public static final fun decodeToByteArray ([CLio/matthewnelson/encoding/core/Decoder;)[B + public static final fun decodeToByteArray ([CLio/matthewnelson/encoding/core/Decoder;II)[B public static final fun decodeToByteArrayOrNull (Ljava/lang/CharSequence;Lio/matthewnelson/encoding/core/Decoder;)[B + public static final fun decodeToByteArrayOrNull (Ljava/lang/CharSequence;Lio/matthewnelson/encoding/core/Decoder;II)[B public static final fun decodeToByteArrayOrNull ([BLio/matthewnelson/encoding/core/Decoder;)[B public static final fun decodeToByteArrayOrNull ([CLio/matthewnelson/encoding/core/Decoder;)[B + public static final fun decodeToByteArrayOrNull ([CLio/matthewnelson/encoding/core/Decoder;II)[B public final fun getConfig ()Lio/matthewnelson/encoding/core/EncoderDecoder$Config; public final fun newDecoderFeed (Lio/matthewnelson/encoding/core/Decoder$OutFeed;)Lio/matthewnelson/encoding/core/Decoder$Feed; protected abstract fun newDecoderFeedProtected (Lio/matthewnelson/encoding/core/Decoder$OutFeed;)Lio/matthewnelson/encoding/core/Decoder$Feed; @@ -31,26 +47,42 @@ 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;Lio/matthewnelson/encoding/core/Decoder;ZIIILkotlin/jvm/functions/Function3;)J + public final fun decodeBuffered (Ljava/lang/CharSequence;Lio/matthewnelson/encoding/core/Decoder;ZIILkotlin/jvm/functions/Function3;)J + public final fun decodeBuffered (Ljava/lang/CharSequence;Lio/matthewnelson/encoding/core/Decoder;ZII[BLkotlin/jvm/functions/Function3;)J public final fun decodeBuffered (Ljava/lang/CharSequence;Lio/matthewnelson/encoding/core/Decoder;ZILkotlin/jvm/functions/Function3;)J public final fun decodeBuffered (Ljava/lang/CharSequence;Lio/matthewnelson/encoding/core/Decoder;ZLkotlin/jvm/functions/Function3;)J public final fun decodeBuffered (Ljava/lang/CharSequence;Lio/matthewnelson/encoding/core/Decoder;Z[BLkotlin/jvm/functions/Function3;)J + public final fun decodeBuffered ([CLio/matthewnelson/encoding/core/Decoder;ZIIILkotlin/jvm/functions/Function3;)J + public final fun decodeBuffered ([CLio/matthewnelson/encoding/core/Decoder;ZIILkotlin/jvm/functions/Function3;)J + public final fun decodeBuffered ([CLio/matthewnelson/encoding/core/Decoder;ZII[BLkotlin/jvm/functions/Function3;)J public final fun decodeBuffered ([CLio/matthewnelson/encoding/core/Decoder;ZILkotlin/jvm/functions/Function3;)J public final fun decodeBuffered ([CLio/matthewnelson/encoding/core/Decoder;ZLkotlin/jvm/functions/Function3;)J public final fun decodeBuffered ([CLio/matthewnelson/encoding/core/Decoder;Z[BLkotlin/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;Lio/matthewnelson/encoding/core/Decoder;ZIIILkotlin/jvm/functions/Function4;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public final fun decodeBufferedAsync (Ljava/lang/CharSequence;Lio/matthewnelson/encoding/core/Decoder;ZIILkotlin/jvm/functions/Function4;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public final fun decodeBufferedAsync (Ljava/lang/CharSequence;Lio/matthewnelson/encoding/core/Decoder;ZII[BLkotlin/jvm/functions/Function4;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; public final fun decodeBufferedAsync (Ljava/lang/CharSequence;Lio/matthewnelson/encoding/core/Decoder;ZILkotlin/jvm/functions/Function4;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; public final fun decodeBufferedAsync (Ljava/lang/CharSequence;Lio/matthewnelson/encoding/core/Decoder;ZLkotlin/jvm/functions/Function4;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; public final fun decodeBufferedAsync (Ljava/lang/CharSequence;Lio/matthewnelson/encoding/core/Decoder;Z[BLkotlin/jvm/functions/Function4;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public final fun decodeBufferedAsync ([CLio/matthewnelson/encoding/core/Decoder;ZIIILkotlin/jvm/functions/Function4;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public final fun decodeBufferedAsync ([CLio/matthewnelson/encoding/core/Decoder;ZIILkotlin/jvm/functions/Function4;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public final fun decodeBufferedAsync ([CLio/matthewnelson/encoding/core/Decoder;ZII[BLkotlin/jvm/functions/Function4;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; public final fun decodeBufferedAsync ([CLio/matthewnelson/encoding/core/Decoder;ZILkotlin/jvm/functions/Function4;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; public final fun decodeBufferedAsync ([CLio/matthewnelson/encoding/core/Decoder;ZLkotlin/jvm/functions/Function4;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; public final fun decodeBufferedAsync ([CLio/matthewnelson/encoding/core/Decoder;Z[BLkotlin/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 (Ljava/lang/CharSequence;Lio/matthewnelson/encoding/core/Decoder;II)[B public final fun decodeToByteArray ([BLio/matthewnelson/encoding/core/Decoder;)[B public final fun decodeToByteArray ([CLio/matthewnelson/encoding/core/Decoder;)[B + public final fun decodeToByteArray ([CLio/matthewnelson/encoding/core/Decoder;II)[B public final fun decodeToByteArrayOrNull (Ljava/lang/CharSequence;Lio/matthewnelson/encoding/core/Decoder;)[B + public final fun decodeToByteArrayOrNull (Ljava/lang/CharSequence;Lio/matthewnelson/encoding/core/Decoder;II)[B public final fun decodeToByteArrayOrNull ([BLio/matthewnelson/encoding/core/Decoder;)[B public final fun decodeToByteArrayOrNull ([CLio/matthewnelson/encoding/core/Decoder;)[B + public final fun decodeToByteArrayOrNull ([CLio/matthewnelson/encoding/core/Decoder;II)[B } public abstract class io/matthewnelson/encoding/core/Decoder$Feed : io/matthewnelson/encoding/core/EncoderDecoder$Feed { @@ -255,8 +287,10 @@ public final class io/matthewnelson/encoding/core/util/DecoderAction$Parser { public final class io/matthewnelson/encoding/core/util/DecoderInput { public fun (Ljava/lang/CharSequence;)V + public fun (Ljava/lang/CharSequence;II)V public fun ([B)V public fun ([C)V + public fun ([CII)V public final fun get (I)C } diff --git a/library/core/api/core.klib.api b/library/core/api/core.klib.api index 77781379..ba462a8f 100644 --- a/library/core/api/core.klib.api +++ b/library/core/api/core.klib.api @@ -148,7 +148,9 @@ final class io.matthewnelson.encoding.core.util/CTCase { // io.matthewnelson.enc final class io.matthewnelson.encoding.core.util/DecoderInput { // io.matthewnelson.encoding.core.util/DecoderInput|null[0] constructor (kotlin/ByteArray) // io.matthewnelson.encoding.core.util/DecoderInput.|(kotlin.ByteArray){}[0] constructor (kotlin/CharArray) // io.matthewnelson.encoding.core.util/DecoderInput.|(kotlin.CharArray){}[0] + constructor (kotlin/CharArray, kotlin/Int, kotlin/Int) // io.matthewnelson.encoding.core.util/DecoderInput.|(kotlin.CharArray;kotlin.Int;kotlin.Int){}[0] constructor (kotlin/CharSequence) // io.matthewnelson.encoding.core.util/DecoderInput.|(kotlin.CharSequence){}[0] + constructor (kotlin/CharSequence, kotlin/Int, kotlin/Int) // io.matthewnelson.encoding.core.util/DecoderInput.|(kotlin.CharSequence;kotlin.Int;kotlin.Int){}[0] final fun get(kotlin/Int): kotlin/Char // io.matthewnelson.encoding.core.util/DecoderInput.get|get(kotlin.Int){}[0] } @@ -223,22 +225,38 @@ sealed class <#A: io.matthewnelson.encoding.core/EncoderDecoder.Config> io.matth 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(io.matthewnelson.encoding.core/Decoder<*>, kotlin/Boolean, kotlin/ByteArray, kotlin/Function3): kotlin/Long // io.matthewnelson.encoding.core/Decoder.Companion.decodeBuffered|decodeBuffered@kotlin.CharArray(io.matthewnelson.encoding.core.Decoder<*>;kotlin.Boolean;kotlin.ByteArray;kotlin.Function3){}[0] final fun (kotlin/CharArray).decodeBuffered(io.matthewnelson.encoding.core/Decoder<*>, kotlin/Boolean, kotlin/Int, kotlin/Function3): kotlin/Long // io.matthewnelson.encoding.core/Decoder.Companion.decodeBuffered|decodeBuffered@kotlin.CharArray(io.matthewnelson.encoding.core.Decoder<*>;kotlin.Boolean;kotlin.Int;kotlin.Function3){}[0] + final fun (kotlin/CharArray).decodeBuffered(io.matthewnelson.encoding.core/Decoder<*>, kotlin/Boolean, kotlin/Int, kotlin/Int, kotlin/ByteArray, kotlin/Function3): kotlin/Long // io.matthewnelson.encoding.core/Decoder.Companion.decodeBuffered|decodeBuffered@kotlin.CharArray(io.matthewnelson.encoding.core.Decoder<*>;kotlin.Boolean;kotlin.Int;kotlin.Int;kotlin.ByteArray;kotlin.Function3){}[0] + final fun (kotlin/CharArray).decodeBuffered(io.matthewnelson.encoding.core/Decoder<*>, kotlin/Boolean, kotlin/Int, kotlin/Int, kotlin/Int, kotlin/Function3): kotlin/Long // io.matthewnelson.encoding.core/Decoder.Companion.decodeBuffered|decodeBuffered@kotlin.CharArray(io.matthewnelson.encoding.core.Decoder<*>;kotlin.Boolean;kotlin.Int;kotlin.Int;kotlin.Int;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).decodeToByteArray(io.matthewnelson.encoding.core/Decoder<*>, kotlin/Int, kotlin/Int): kotlin/ByteArray // io.matthewnelson.encoding.core/Decoder.Companion.decodeToByteArray|decodeToByteArray@kotlin.CharArray(io.matthewnelson.encoding.core.Decoder<*>;kotlin.Int;kotlin.Int){}[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/CharArray).decodeToByteArrayOrNull(io.matthewnelson.encoding.core/Decoder<*>, kotlin/Int, kotlin/Int): kotlin/ByteArray? // io.matthewnelson.encoding.core/Decoder.Companion.decodeToByteArrayOrNull|decodeToByteArrayOrNull@kotlin.CharArray(io.matthewnelson.encoding.core.Decoder<*>;kotlin.Int;kotlin.Int){}[0] final fun (kotlin/CharSequence).decodeBuffered(io.matthewnelson.encoding.core/Decoder<*>, kotlin/Boolean, kotlin/ByteArray, kotlin/Function3): kotlin/Long // io.matthewnelson.encoding.core/Decoder.Companion.decodeBuffered|decodeBuffered@kotlin.CharSequence(io.matthewnelson.encoding.core.Decoder<*>;kotlin.Boolean;kotlin.ByteArray;kotlin.Function3){}[0] final fun (kotlin/CharSequence).decodeBuffered(io.matthewnelson.encoding.core/Decoder<*>, kotlin/Boolean, kotlin/Int, kotlin/Function3): kotlin/Long // io.matthewnelson.encoding.core/Decoder.Companion.decodeBuffered|decodeBuffered@kotlin.CharSequence(io.matthewnelson.encoding.core.Decoder<*>;kotlin.Boolean;kotlin.Int;kotlin.Function3){}[0] + final fun (kotlin/CharSequence).decodeBuffered(io.matthewnelson.encoding.core/Decoder<*>, kotlin/Boolean, kotlin/Int, kotlin/Int, kotlin/ByteArray, kotlin/Function3): kotlin/Long // io.matthewnelson.encoding.core/Decoder.Companion.decodeBuffered|decodeBuffered@kotlin.CharSequence(io.matthewnelson.encoding.core.Decoder<*>;kotlin.Boolean;kotlin.Int;kotlin.Int;kotlin.ByteArray;kotlin.Function3){}[0] + final fun (kotlin/CharSequence).decodeBuffered(io.matthewnelson.encoding.core/Decoder<*>, kotlin/Boolean, kotlin/Int, kotlin/Int, kotlin/Int, kotlin/Function3): kotlin/Long // io.matthewnelson.encoding.core/Decoder.Companion.decodeBuffered|decodeBuffered@kotlin.CharSequence(io.matthewnelson.encoding.core.Decoder<*>;kotlin.Boolean;kotlin.Int;kotlin.Int;kotlin.Int;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).decodeToByteArray(io.matthewnelson.encoding.core/Decoder<*>, kotlin/Int, kotlin/Int): kotlin/ByteArray // io.matthewnelson.encoding.core/Decoder.Companion.decodeToByteArray|decodeToByteArray@kotlin.CharSequence(io.matthewnelson.encoding.core.Decoder<*>;kotlin.Int;kotlin.Int){}[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 fun (kotlin/CharSequence).decodeToByteArrayOrNull(io.matthewnelson.encoding.core/Decoder<*>, kotlin/Int, kotlin/Int): kotlin/ByteArray? // io.matthewnelson.encoding.core/Decoder.Companion.decodeToByteArrayOrNull|decodeToByteArrayOrNull@kotlin.CharSequence(io.matthewnelson.encoding.core.Decoder<*>;kotlin.Int;kotlin.Int){}[0] + final inline fun (kotlin/CharArray).decodeBuffered(io.matthewnelson.encoding.core/Decoder<*>, kotlin/Boolean, kotlin/Int, kotlin/Int, noinline kotlin/Function3): kotlin/Long // io.matthewnelson.encoding.core/Decoder.Companion.decodeBuffered|decodeBuffered@kotlin.CharArray(io.matthewnelson.encoding.core.Decoder<*>;kotlin.Boolean;kotlin.Int;kotlin.Int;kotlin.Function3){}[0] final inline fun (kotlin/CharArray).decodeBuffered(io.matthewnelson.encoding.core/Decoder<*>, kotlin/Boolean, noinline kotlin/Function3): kotlin/Long // io.matthewnelson.encoding.core/Decoder.Companion.decodeBuffered|decodeBuffered@kotlin.CharArray(io.matthewnelson.encoding.core.Decoder<*>;kotlin.Boolean;kotlin.Function3){}[0] + final inline fun (kotlin/CharSequence).decodeBuffered(io.matthewnelson.encoding.core/Decoder<*>, kotlin/Boolean, kotlin/Int, kotlin/Int, noinline kotlin/Function3): kotlin/Long // io.matthewnelson.encoding.core/Decoder.Companion.decodeBuffered|decodeBuffered@kotlin.CharSequence(io.matthewnelson.encoding.core.Decoder<*>;kotlin.Boolean;kotlin.Int;kotlin.Int;kotlin.Function3){}[0] final inline fun (kotlin/CharSequence).decodeBuffered(io.matthewnelson.encoding.core/Decoder<*>, kotlin/Boolean, noinline kotlin/Function3): kotlin/Long // io.matthewnelson.encoding.core/Decoder.Companion.decodeBuffered|decodeBuffered@kotlin.CharSequence(io.matthewnelson.encoding.core.Decoder<*>;kotlin.Boolean;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(io.matthewnelson.encoding.core/Decoder<*>, kotlin/Boolean, kotlin/ByteArray, kotlin.coroutines/SuspendFunction3): kotlin/Long // io.matthewnelson.encoding.core/Decoder.Companion.decodeBufferedAsync|decodeBufferedAsync@kotlin.CharArray(io.matthewnelson.encoding.core.Decoder<*>;kotlin.Boolean;kotlin.ByteArray;kotlin.coroutines.SuspendFunction3){}[0] final suspend fun (kotlin/CharArray).decodeBufferedAsync(io.matthewnelson.encoding.core/Decoder<*>, kotlin/Boolean, kotlin/Int, kotlin.coroutines/SuspendFunction3): kotlin/Long // io.matthewnelson.encoding.core/Decoder.Companion.decodeBufferedAsync|decodeBufferedAsync@kotlin.CharArray(io.matthewnelson.encoding.core.Decoder<*>;kotlin.Boolean;kotlin.Int;kotlin.coroutines.SuspendFunction3){}[0] + final suspend fun (kotlin/CharArray).decodeBufferedAsync(io.matthewnelson.encoding.core/Decoder<*>, kotlin/Boolean, kotlin/Int, kotlin/Int, kotlin/ByteArray, kotlin.coroutines/SuspendFunction3): kotlin/Long // io.matthewnelson.encoding.core/Decoder.Companion.decodeBufferedAsync|decodeBufferedAsync@kotlin.CharArray(io.matthewnelson.encoding.core.Decoder<*>;kotlin.Boolean;kotlin.Int;kotlin.Int;kotlin.ByteArray;kotlin.coroutines.SuspendFunction3){}[0] + final suspend fun (kotlin/CharArray).decodeBufferedAsync(io.matthewnelson.encoding.core/Decoder<*>, kotlin/Boolean, kotlin/Int, kotlin/Int, kotlin/Int, kotlin.coroutines/SuspendFunction3): kotlin/Long // io.matthewnelson.encoding.core/Decoder.Companion.decodeBufferedAsync|decodeBufferedAsync@kotlin.CharArray(io.matthewnelson.encoding.core.Decoder<*>;kotlin.Boolean;kotlin.Int;kotlin.Int;kotlin.Int;kotlin.coroutines.SuspendFunction3){}[0] final suspend fun (kotlin/CharSequence).decodeBufferedAsync(io.matthewnelson.encoding.core/Decoder<*>, kotlin/Boolean, kotlin/ByteArray, kotlin.coroutines/SuspendFunction3): kotlin/Long // io.matthewnelson.encoding.core/Decoder.Companion.decodeBufferedAsync|decodeBufferedAsync@kotlin.CharSequence(io.matthewnelson.encoding.core.Decoder<*>;kotlin.Boolean;kotlin.ByteArray;kotlin.coroutines.SuspendFunction3){}[0] final suspend fun (kotlin/CharSequence).decodeBufferedAsync(io.matthewnelson.encoding.core/Decoder<*>, kotlin/Boolean, kotlin/Int, kotlin.coroutines/SuspendFunction3): kotlin/Long // io.matthewnelson.encoding.core/Decoder.Companion.decodeBufferedAsync|decodeBufferedAsync@kotlin.CharSequence(io.matthewnelson.encoding.core.Decoder<*>;kotlin.Boolean;kotlin.Int;kotlin.coroutines.SuspendFunction3){}[0] + final suspend fun (kotlin/CharSequence).decodeBufferedAsync(io.matthewnelson.encoding.core/Decoder<*>, kotlin/Boolean, kotlin/Int, kotlin/Int, kotlin/ByteArray, kotlin.coroutines/SuspendFunction3): kotlin/Long // io.matthewnelson.encoding.core/Decoder.Companion.decodeBufferedAsync|decodeBufferedAsync@kotlin.CharSequence(io.matthewnelson.encoding.core.Decoder<*>;kotlin.Boolean;kotlin.Int;kotlin.Int;kotlin.ByteArray;kotlin.coroutines.SuspendFunction3){}[0] + final suspend fun (kotlin/CharSequence).decodeBufferedAsync(io.matthewnelson.encoding.core/Decoder<*>, kotlin/Boolean, kotlin/Int, kotlin/Int, kotlin/Int, kotlin.coroutines/SuspendFunction3): kotlin/Long // io.matthewnelson.encoding.core/Decoder.Companion.decodeBufferedAsync|decodeBufferedAsync@kotlin.CharSequence(io.matthewnelson.encoding.core.Decoder<*>;kotlin.Boolean;kotlin.Int;kotlin.Int;kotlin.Int;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<*>, kotlin/Boolean, kotlin/Int, kotlin/Int, noinline kotlin.coroutines/SuspendFunction3): kotlin/Long // io.matthewnelson.encoding.core/Decoder.Companion.decodeBufferedAsync|decodeBufferedAsync@kotlin.CharArray(io.matthewnelson.encoding.core.Decoder<*>;kotlin.Boolean;kotlin.Int;kotlin.Int;kotlin.coroutines.SuspendFunction3){}[0] final suspend inline fun (kotlin/CharArray).decodeBufferedAsync(io.matthewnelson.encoding.core/Decoder<*>, kotlin/Boolean, noinline kotlin.coroutines/SuspendFunction3): kotlin/Long // io.matthewnelson.encoding.core/Decoder.Companion.decodeBufferedAsync|decodeBufferedAsync@kotlin.CharArray(io.matthewnelson.encoding.core.Decoder<*>;kotlin.Boolean;kotlin.coroutines.SuspendFunction3){}[0] + final suspend inline fun (kotlin/CharSequence).decodeBufferedAsync(io.matthewnelson.encoding.core/Decoder<*>, kotlin/Boolean, kotlin/Int, kotlin/Int, noinline kotlin.coroutines/SuspendFunction3): kotlin/Long // io.matthewnelson.encoding.core/Decoder.Companion.decodeBufferedAsync|decodeBufferedAsync@kotlin.CharSequence(io.matthewnelson.encoding.core.Decoder<*>;kotlin.Boolean;kotlin.Int;kotlin.Int;kotlin.coroutines.SuspendFunction3){}[0] final suspend inline fun (kotlin/CharSequence).decodeBufferedAsync(io.matthewnelson.encoding.core/Decoder<*>, kotlin/Boolean, noinline kotlin.coroutines/SuspendFunction3): kotlin/Long // io.matthewnelson.encoding.core/Decoder.Companion.decodeBufferedAsync|decodeBufferedAsync@kotlin.CharSequence(io.matthewnelson.encoding.core.Decoder<*>;kotlin.Boolean;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/build.gradle.kts b/library/core/build.gradle.kts index 509f0b48..f0179295 100644 --- a/library/core/build.gradle.kts +++ b/library/core/build.gradle.kts @@ -20,6 +20,14 @@ plugins { kmpConfiguration { configureShared(java9ModuleName = "io.matthewnelson.encoding.core", publish = true) { + common { + sourceSetTest { + dependencies { + implementation(libs.kotlinx.coroutines.test) + } + } + } + kotlin { with(sourceSets) { val sets = arrayOf( 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 237da689..fa1ac411 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 @@ -52,7 +52,7 @@ public sealed class Decoder(public val config: C) { * myDecoder.newDecoderFeed { decodedByte -> * sb.append(decodedByte) * }.use { feed -> - * "MYencoDEdTEXt".forEach { c -> feed.consume(c) } + * "MYencoDEdTEXt".forEach(feed::consume) * } * println(sb.toString()) * @@ -235,12 +235,37 @@ public sealed class Decoder(public val config: C) { return decoder.decode(DecoderInput(this), ::get) } + /** + * Decode [len] number of characters from the sequence, starting at index [offset]. + * + * @param [decoder] The [Decoder] to use. + * @param [offset] The index in the sequence to start at. + * @param [len] the number of characters, starting at index [offset]. + * + * @return The array of decoded data. + * + * @see [CharSequence.decodeToByteArrayOrNull] + * @see [CharSequence.decodeBuffered] + * @see [CharSequence.decodeBufferedAsync] + * + * @throws [EncodingException] If decoding failed, such as the [decoder] rejecting + * an invalid character or sequence. + * @throws [EncodingSizeException] If [EncoderDecoder.Config.decodeOutMaxSizeOrFail] + * throws an exception (i.e. output would exceed [Int.MAX_VALUE]). + * @throws [IndexOutOfBoundsException] If [offset] or [len] are inappropriate. + * */ + @JvmStatic + @Throws(EncodingException::class) + public fun CharSequence.decodeToByteArray(decoder: Decoder<*>, offset: Int, len: Int): ByteArray { + return decoder.decode(DecoderInput(this, offset, len), ::get) + } + /** * Decode a [CharSequence]. * * @param [decoder] The [Decoder] to use. * - * @return The array of decoded data, or `null` if there was a decoding error. + * @return The array of decoded data, or `null` if there was an [EncodingException]. * * @see [CharSequence.decodeToByteArray] * @see [CharSequence.decodeBuffered] @@ -255,6 +280,30 @@ public sealed class Decoder(public val config: C) { } } + /** + * Decode [len] number of characters from the sequence, starting at index [offset]. + * + * @param [decoder] The [Decoder] to use. + * @param [offset] The index in the sequence to start at. + * @param [len] the number of characters, starting at index [offset]. + * + * @return The array of decoded data, or `null` if there was an [EncodingException]. + * + * @see [CharSequence.decodeToByteArray] + * @see [CharSequence.decodeBuffered] + * @see [CharSequence.decodeBufferedAsync] + * + * @throws [IndexOutOfBoundsException] If [offset] or [len] are inappropriate. + * */ + @JvmStatic + public fun CharSequence.decodeToByteArrayOrNull(decoder: Decoder<*>, offset: Int, len: Int): ByteArray? { + return try { + decodeToByteArray(decoder, offset, len) + } catch (_: EncodingException) { + null + } + } + /** * Decode a [CharArray]. * @@ -276,12 +325,37 @@ public sealed class Decoder(public val config: C) { return decoder.decode(DecoderInput(this), ::get) } + /** + * Decode [len] number of characters from the array, starting at index [offset]. + * + * @param [decoder] The [Decoder] to use. + * @param [offset] The index in the array to start at. + * @param [len] the number of characters, starting at index [offset]. + * + * @return The array of decoded data. + * + * @see [CharArray.decodeToByteArrayOrNull] + * @see [CharArray.decodeBuffered] + * @see [CharArray.decodeBufferedAsync] + * + * @throws [EncodingException] If decoding failed, such as the [decoder] rejecting + * an invalid character or sequence. + * @throws [EncodingSizeException] If [EncoderDecoder.Config.decodeOutMaxSizeOrFail] + * throws an exception (i.e. output would exceed [Int.MAX_VALUE]). + * @throws [IndexOutOfBoundsException] If [offset] or [len] are inappropriate. + * */ + @JvmStatic + @Throws(EncodingException::class) + public fun CharArray.decodeToByteArray(decoder: Decoder<*>, offset: Int, len: Int): ByteArray { + return decoder.decode(DecoderInput(this, offset, len), ::get) + } + /** * Decode a [CharArray]. * * @param [decoder] The [Decoder] to use. * - * @return The array of decoded data, or `null` if there was a decoding error. + * @return The array of decoded data, or `null` if there was an [EncodingException]. * * @see [CharArray.decodeToByteArray] * @see [CharArray.decodeBuffered] @@ -296,6 +370,30 @@ public sealed class Decoder(public val config: C) { } } + /** + * Decode [len] number of characters from the array, starting at index [offset]. + * + * @param [decoder] The [Decoder] to use. + * @param [offset] The index in the array to start at. + * @param [len] the number of characters, starting at index [offset]. + * + * @return The array of decoded data, or `null` if there was an [EncodingException]. + * + * @see [CharArray.decodeToByteArray] + * @see [CharArray.decodeBuffered] + * @see [CharArray.decodeBufferedAsync] + * + * @throws [IndexOutOfBoundsException] If [offset] or [len] are inappropriate. + * */ + @JvmStatic + public fun CharArray.decodeToByteArrayOrNull(decoder: Decoder<*>, offset: Int, len: Int): ByteArray? { + return try { + decodeToByteArray(decoder, offset, len) + } catch (_: EncodingException) { + null + } + } + /** * Decode a [CharSequence] using a buffer of maximum size [DEFAULT_BUFFER_SIZE]. * @@ -364,16 +462,17 @@ public sealed class Decoder(public val config: C) { ): Long = decodeBuffered(decoder, throwOnOverflow, DEFAULT_BUFFER_SIZE, action) /** - * Decode a [CharSequence] using a buffer of maximum size [maxBufSize]. + * Decode [len] number of characters from the sequence, starting at index [offset], using + * a buffer of maximum size [DEFAULT_BUFFER_SIZE]. * * The decoding operation will allocate a single buffer, 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 a buffer of that size will be allocated + * equal to the [DEFAULT_BUFFER_SIZE], then a buffer 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]) while [throwOnOverflow] - * is `false`, or its return value is greater than [maxBufSize], then this + * is `false`, or its return value 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. * @@ -383,8 +482,8 @@ public sealed class Decoder(public val config: C) { * 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(UTF8, false, 1024, stream::write) + * val n = "Some long string" + * .decodeBuffered(UTF8, false, 2, 4, stream::write) * println("Wrote $n UTF-8 bytes to file.txt") * } * @@ -392,7 +491,7 @@ public sealed class Decoder(public val config: C) { * * val d = SHA256() * "SGVsbG8gV29ybGQh" - * .decodeBuffered(Base64.Default, false, 1024, d::update) + * .decodeBuffered(Base64.Default, false, d::update) * // ... * * **NOTE:** The [Decoder] implementation must be compatible with version `2.6.0+` @@ -407,8 +506,8 @@ public sealed class Decoder(public val config: C) { * @param [throwOnOverflow] If `true` and [EncoderDecoder.Config.decodeOutMaxSizeOrFail] * throws an [EncodingSizeException], it will be re-thrown. If `false`, the exception * will be ignored and stream decoding to the buffer will continue. - * @param [maxBufSize] The maximum size buffer this function will allocate. Must - * be greater than [EncoderDecoder.Config.maxDecodeEmit]. + * @param [offset] The index in the sequence to start at. + * @param [len] The number of characters, starting at index [offset]. * @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". @@ -423,63 +522,48 @@ public sealed class Decoder(public val config: C) { * an invalid character or sequence. * @throws [EncodingSizeException] If [EncoderDecoder.Config.decodeOutMaxSizeOrFail] * threw its exception and [throwOnOverflow] is `true`. - * @throws [IllegalArgumentException] If [maxBufSize] is less than or equal to - * [EncoderDecoder.Config.maxDecodeEmit]. + * @throws [IndexOutOfBoundsException] If [offset] or [len] are inappropriate. * */ @JvmStatic @Throws(EncodingException::class) - public fun CharSequence.decodeBuffered( + public inline fun CharSequence.decodeBuffered( decoder: Decoder<*>, throwOnOverflow: Boolean, - maxBufSize: Int, - action: (buf: ByteArray, offset: Int, len: Int) -> Unit, - ): Long = decoder.decodeBuffered( - buf = null, - maxBufSize = maxBufSize, - throwOnOverflow = throwOnOverflow, - _get = ::get, - _input = { DecoderInput(this) }, - _action = action, - ) + offset: Int, + len: Int, + noinline action: (buf: ByteArray, offset: Int, len: Int) -> Unit, + ): Long = decodeBuffered(decoder, throwOnOverflow, offset, len, DEFAULT_BUFFER_SIZE, action) /** - * Decode a [CharSequence] using the provided pre-allocated, reusable, [buf] array. + * Decode a [CharSequence] using a buffer of maximum size [maxBufSize]. * - * The decoding operation will stream decoded bytes to the provided [buf], 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 + * The decoding operation will allocate a single buffer, 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 a buffer 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]) while [throwOnOverflow] - * is `false`, or its return value is greater than [buf] size, then this + * is `false`, or its return value 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. * - * **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(UTF8, false, buf, stream::write) - * n += "Some other string" - * .decodeBuffered(UTF8, false, buf, stream::write) + * val n = "Some string" + * .decodeBuffered(UTF8, false, 1024, 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(Base64.Default, false, buf, d::update) * "SGVsbG8gV29ybGQh" - * .decodeBuffered(Base64.Default, false, buf, d::update) + * .decodeBuffered(Base64.Default, false, 1024, d::update) * // ... * * **NOTE:** The [Decoder] implementation must be compatible with version `2.6.0+` @@ -494,8 +578,8 @@ public sealed class Decoder(public val config: C) { * @param [throwOnOverflow] If `true` and [EncoderDecoder.Config.decodeOutMaxSizeOrFail] * throws an [EncodingSizeException], it will be re-thrown. If `false`, the exception * will be ignored and stream decoding to the buffer will continue. - * @param [buf] The pre-allocated array to use as the buffer. Its size must be - * greater than [EncoderDecoder.Config.maxDecodeEmit]. + * @param [maxBufSize] The maximum size buffer this function will allocate. Must + * be greater than [EncoderDecoder.Config.maxDecodeEmit]. * @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". @@ -510,7 +594,7 @@ public sealed class Decoder(public val config: C) { * an invalid character or sequence. * @throws [EncodingSizeException] If [EncoderDecoder.Config.decodeOutMaxSizeOrFail] * threw its exception and [throwOnOverflow] is `true`. - * @throws [IllegalArgumentException] If [buf] size is less than or equal to + * @throws [IllegalArgumentException] If [maxBufSize] is less than or equal to * [EncoderDecoder.Config.maxDecodeEmit]. * */ @JvmStatic @@ -518,11 +602,11 @@ public sealed class Decoder(public val config: C) { public fun CharSequence.decodeBuffered( decoder: Decoder<*>, throwOnOverflow: Boolean, - buf: ByteArray, + maxBufSize: Int, action: (buf: ByteArray, offset: Int, len: Int) -> Unit, ): Long = decoder.decodeBuffered( - buf = buf, - maxBufSize = buf.size, + buf = null, + maxBufSize = maxBufSize, throwOnOverflow = throwOnOverflow, _get = ::get, _input = { DecoderInput(this) }, @@ -530,38 +614,38 @@ public sealed class Decoder(public val config: C) { ) /** - * Decode a [CharSequence] using a buffer of maximum of [DEFAULT_BUFFER_SIZE]. + * Decode [len] number of characters from the sequence, starting at index [offset], using + * a buffer of maximum size [maxBufSize]. * * The decoding operation will allocate a single buffer, 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 a buffer of that size will be allocated + * equal to the [maxBufSize], then a buffer 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]) while [throwOnOverflow] - * is `false`, or its return value is greater than [DEFAULT_BUFFER_SIZE], then this + * is `false`, or its return value 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`) + * e.g. (Using `io.matthewnelson.kmp-file:file` & 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, - * false, - * stream::writeAsync, - * ) - * println("Wrote $n UTF-8 bytes to file.txt") - * } + * "/path/to/file.txt".toFile().openWrite(excl = null).use { stream -> + * val n = "Some string" + * .decodeBuffered(UTF8, false, 2, 6, 1024, 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, false, 1024, 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 @@ -574,64 +658,86 @@ public sealed class Decoder(public val config: C) { * @param [throwOnOverflow] If `true` and [EncoderDecoder.Config.decodeOutMaxSizeOrFail] * throws an [EncodingSizeException], it will be re-thrown. If `false`, the exception * will be ignored and stream decoding to the buffer will continue. - * @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". + * @param [offset] The index in the sequence to start at. + * @param [len] The number of characters, starting at index [offset]. + * @param [maxBufSize] The maximum size buffer this function will allocate. Must + * be greater than [EncoderDecoder.Config.maxDecodeEmit]. + * @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.decodeBuffered] + * @see [CharSequence.decodeBufferedAsync] * - * @throws [CancellationException] * @throws [EncodingException] If decoding failed, such as the [decoder] rejecting * an invalid character or sequence. * @throws [EncodingSizeException] If [EncoderDecoder.Config.decodeOutMaxSizeOrFail] * threw its exception and [throwOnOverflow] is `true`. + * @throws [IllegalArgumentException] If [maxBufSize] is less than or equal to + * [EncoderDecoder.Config.maxDecodeEmit]. + * @throws [IndexOutOfBoundsException] If [offset] or [len] are inappropriate. * */ @JvmStatic - @Throws(CancellationException::class, EncodingException::class) - public suspend inline fun CharSequence.decodeBufferedAsync( + @Throws(EncodingException::class) + public fun CharSequence.decodeBuffered( decoder: Decoder<*>, throwOnOverflow: Boolean, - noinline action: suspend (buf: ByteArray, offset: Int, len: Int) -> Unit, - ): Long = decodeBufferedAsync(decoder, throwOnOverflow, DEFAULT_BUFFER_SIZE, action) + offset: Int, + len: Int, + maxBufSize: Int, + action: (buf: ByteArray, offset: Int, len: Int) -> Unit, + ): Long = decoder.decodeBuffered( + buf = null, + maxBufSize = maxBufSize, + throwOnOverflow = throwOnOverflow, + _get = ::get, + _input = { DecoderInput(this, offset, len) }, + _action = action, + ) /** - * Decode a [CharSequence] using a buffer of maximum size [maxBufSize]. + * Decode a [CharSequence] using the provided pre-allocated, reusable, [buf] array. * - * The decoding operation will allocate a single buffer, 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 a buffer of that size will be allocated - * and [action] is only invoked once (single-shot decoding). In the event that + * The decoding operation will stream decoded bytes to the provided [buf], 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]) while [throwOnOverflow] - * is `false`, or its return value is greater than [maxBufSize], then this + * is `false`, or its return value is greater than [buf] 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`) + * **NOTE:** If [EncoderDecoder.Config.backFillBuffers] is `true`, provided [buf] + * array will be back-filled with `0` bytes upon decoding completion. * - * AsyncFs.Default.with { - * "/path/to/file.txt".toFile() - * .openWriteAsync(excl = null) - * .useAsync { stream -> - * val text = "Some long string" - * val n = text.decodeBufferedAsync( - * UTF8, - * false, - * 1024, - * stream::writeAsync, - * ) - * println("Wrote $n UTF-8 bytes to file.txt") - * } + * 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(UTF8, false, buf, stream::write) + * n += "Some other string" + * .decodeBuffered(UTF8, false, buf, 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(Base64.Default, false, buf, d::update) + * "SGVsbG8gV29ybGQh" + * .decodeBuffered(Base64.Default, false, buf, 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 @@ -644,24 +750,525 @@ public sealed class Decoder(public val config: C) { * @param [throwOnOverflow] If `true` and [EncoderDecoder.Config.decodeOutMaxSizeOrFail] * throws an [EncodingSizeException], it will be re-thrown. If `false`, the exception * will be ignored and stream decoding to the buffer will continue. - * @param [maxBufSize] The maximum size buffer this function will allocate. Must - * be greater than [EncoderDecoder.Config.maxDecodeEmit]. - * @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". + * @param [buf] The pre-allocated array to use as the buffer. Its size must be + * greater than [EncoderDecoder.Config.maxDecodeEmit]. + * @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.decodeBuffered] + * @see [CharSequence.decodeBufferedAsync] * - * @throws [CancellationException] * @throws [EncodingException] If decoding failed, such as the [decoder] rejecting * an invalid character or sequence. * @throws [EncodingSizeException] If [EncoderDecoder.Config.decodeOutMaxSizeOrFail] * threw its exception and [throwOnOverflow] is `true`. - * @throws [IllegalArgumentException] If [maxBufSize] is less than or equal to + * @throws [IllegalArgumentException] If [buf] size is less than or equal to + * [EncoderDecoder.Config.maxDecodeEmit]. + * */ + @JvmStatic + @Throws(EncodingException::class) + public fun CharSequence.decodeBuffered( + decoder: Decoder<*>, + throwOnOverflow: Boolean, + buf: ByteArray, + action: (buf: ByteArray, offset: Int, len: Int) -> Unit, + ): Long = decoder.decodeBuffered( + buf = buf, + maxBufSize = buf.size, + throwOnOverflow = throwOnOverflow, + _get = ::get, + _input = { DecoderInput(this) }, + _action = action, + ) + + /** + * Decode [len] number of characters from the sequence, starting at index [offset], using + * the provided pre-allocated, reusable, [buf] array. + * + * The decoding operation will stream decoded bytes to the provided [buf], 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]) while [throwOnOverflow] + * is `false`, or its return value is greater than [buf] 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. + * + * **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(UTF8, false, 1, 2, buf, stream::write) + * n += "Some other string" + * .decodeBuffered(UTF8, false, 2, 5, buf, 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(Base64.Default, false, buf, d::update) + * "SGVsbG8gV29ybGQh" + * .decodeBuffered(Base64.Default, false, buf, 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 [throwOnOverflow] If `true` and [EncoderDecoder.Config.decodeOutMaxSizeOrFail] + * throws an [EncodingSizeException], it will be re-thrown. If `false`, the exception + * will be ignored and stream decoding to the buffer will continue. + * @param [offset] The index in the sequence to start at. + * @param [len] The number of characters, starting at index [offset]. + * @param [buf] The pre-allocated array to use as the buffer. Its size must be + * greater than [EncoderDecoder.Config.maxDecodeEmit]. + * @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, such as the [decoder] rejecting + * an invalid character or sequence. + * @throws [EncodingSizeException] If [EncoderDecoder.Config.decodeOutMaxSizeOrFail] + * threw its exception and [throwOnOverflow] is `true`. + * @throws [IllegalArgumentException] If [buf] size is less than or equal to + * [EncoderDecoder.Config.maxDecodeEmit]. + * @throws [IndexOutOfBoundsException] If [offset] or [len] are inappropriate. + * */ + @JvmStatic + @Throws(EncodingException::class) + public fun CharSequence.decodeBuffered( + decoder: Decoder<*>, + throwOnOverflow: Boolean, + offset: Int, + len: Int, + buf: ByteArray, + action: (buf: ByteArray, offset: Int, len: Int) -> Unit, + ): Long = decoder.decodeBuffered( + buf = buf, + maxBufSize = buf.size, + throwOnOverflow = throwOnOverflow, + _get = ::get, + _input = { DecoderInput(this, offset, len) }, + _action = action, + ) + + /** + * Decode a [CharSequence] using a buffer of maximum size [DEFAULT_BUFFER_SIZE]. + * + * The decoding operation will allocate a single buffer, 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 a buffer 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]) while [throwOnOverflow] + * is `false`, or its return value 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 { + * "/path/to/file.txt".toFile() + * .openWriteAsync(excl = null) + * .useAsync { stream -> + * val text = "Some long string" + * val n = text.decodeBufferedAsync( + * UTF8, + * false, + * 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 [throwOnOverflow] If `true` and [EncoderDecoder.Config.decodeOutMaxSizeOrFail] + * throws an [EncodingSizeException], it will be re-thrown. If `false`, the exception + * will be ignored and stream decoding to the buffer will continue. + * @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, such as the [decoder] rejecting + * an invalid character or sequence. + * @throws [EncodingSizeException] If [EncoderDecoder.Config.decodeOutMaxSizeOrFail] + * threw its exception and [throwOnOverflow] is `true`. + * */ + @JvmStatic + @Throws(CancellationException::class, EncodingException::class) + public suspend inline fun CharSequence.decodeBufferedAsync( + decoder: Decoder<*>, + throwOnOverflow: Boolean, + noinline action: suspend (buf: ByteArray, offset: Int, len: Int) -> Unit, + ): Long = decodeBufferedAsync(decoder, throwOnOverflow, DEFAULT_BUFFER_SIZE, action) + + /** + * Decode [len] number of characters from the sequence, starting at index [offset], using + * a buffer of maximum size [DEFAULT_BUFFER_SIZE]. + * + * The decoding operation will allocate a single buffer, 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 a buffer 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]) while [throwOnOverflow] + * is `false`, or its return value 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 { + * "/path/to/file.txt".toFile() + * .openWriteAsync(excl = null) + * .useAsync { stream -> + * val text = "Some long string" + * val n = text.decodeBufferedAsync( + * UTF8, + * false, + * offset = 0, + * len = 4, + * 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 [throwOnOverflow] If `true` and [EncoderDecoder.Config.decodeOutMaxSizeOrFail] + * throws an [EncodingSizeException], it will be re-thrown. If `false`, the exception + * will be ignored and stream decoding to the buffer will continue. + * @param [offset] The index in the sequence to start at. + * @param [len] The number of characters, starting at index [offset]. + * @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, such as the [decoder] rejecting + * an invalid character or sequence. + * @throws [EncodingSizeException] If [EncoderDecoder.Config.decodeOutMaxSizeOrFail] + * threw its exception and [throwOnOverflow] is `true`. + * @throws [IndexOutOfBoundsException] If [offset] or [len] are inappropriate. + * */ + @JvmStatic + @Throws(CancellationException::class, EncodingException::class) + public suspend inline fun CharSequence.decodeBufferedAsync( + decoder: Decoder<*>, + throwOnOverflow: Boolean, + offset: Int, + len: Int, + noinline action: suspend (buf: ByteArray, offset: Int, len: Int) -> Unit, + ): Long = decodeBufferedAsync(decoder, throwOnOverflow, offset, len, DEFAULT_BUFFER_SIZE, action) + + /** + * Decode a [CharSequence] using a buffer of maximum size [maxBufSize]. + * + * The decoding operation will allocate a single buffer, 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 a buffer 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]) while [throwOnOverflow] + * is `false`, or its return value 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 { + * "/path/to/file.txt".toFile() + * .openWriteAsync(excl = null) + * .useAsync { stream -> + * val text = "Some long string" + * val n = text.decodeBufferedAsync( + * UTF8, + * false, + * 1024, + * 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 [throwOnOverflow] If `true` and [EncoderDecoder.Config.decodeOutMaxSizeOrFail] + * throws an [EncodingSizeException], it will be re-thrown. If `false`, the exception + * will be ignored and stream decoding to the buffer will continue. + * @param [maxBufSize] The maximum size buffer this function will allocate. Must + * be greater than [EncoderDecoder.Config.maxDecodeEmit]. + * @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, such as the [decoder] rejecting + * an invalid character or sequence. + * @throws [EncodingSizeException] If [EncoderDecoder.Config.decodeOutMaxSizeOrFail] + * threw its exception and [throwOnOverflow] is `true`. + * @throws [IllegalArgumentException] If [maxBufSize] is less than or equal to + * [EncoderDecoder.Config.maxDecodeEmit]. + * */ + @JvmStatic + @Throws(CancellationException::class, EncodingException::class) + public suspend fun CharSequence.decodeBufferedAsync( + decoder: Decoder<*>, + throwOnOverflow: Boolean, + maxBufSize: Int, + action: suspend (buf: ByteArray, offset: Int, len: Int) -> Unit, + ): Long = decoder.decodeBuffered( + buf = null, + maxBufSize = maxBufSize, + throwOnOverflow = throwOnOverflow, + _get = ::get, + _input = { DecoderInput(this) }, + _action = { _buf, _offset, _len -> action(_buf, _offset, _len) }, + ) + + /** + * Decode [len] number of characters from the sequence, starting at index [offset], using + * a buffer of maximum size [maxBufSize]. + * + * The decoding operation will allocate a single buffer, 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 a buffer 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]) while [throwOnOverflow] + * is `false`, or its return value 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 { + * "/path/to/file.txt".toFile() + * .openWriteAsync(excl = null) + * .useAsync { stream -> + * val text = "Some long string" + * val n = text.decodeBufferedAsync( + * UTF8, + * false, + * offset = 5, + * len = text.length - 5, + * 1024, + * 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 [throwOnOverflow] If `true` and [EncoderDecoder.Config.decodeOutMaxSizeOrFail] + * throws an [EncodingSizeException], it will be re-thrown. If `false`, the exception + * will be ignored and stream decoding to the buffer will continue. + * @param [offset] The index in the sequence to start at. + * @param [len] The number of characters, starting at index [offset]. + * @param [maxBufSize] The maximum size buffer this function will allocate. Must + * be greater than [EncoderDecoder.Config.maxDecodeEmit]. + * @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, such as the [decoder] rejecting + * an invalid character or sequence. + * @throws [EncodingSizeException] If [EncoderDecoder.Config.decodeOutMaxSizeOrFail] + * threw its exception and [throwOnOverflow] is `true`. + * @throws [IllegalArgumentException] If [maxBufSize] is less than or equal to + * [EncoderDecoder.Config.maxDecodeEmit]. + * @throws [IndexOutOfBoundsException] If [offset] or [len] are inappropriate. + * */ + @JvmStatic + @Throws(CancellationException::class, EncodingException::class) + public suspend fun CharSequence.decodeBufferedAsync( + decoder: Decoder<*>, + throwOnOverflow: Boolean, + offset: Int, + len: Int, + maxBufSize: Int, + action: suspend (buf: ByteArray, offset: Int, len: Int) -> Unit, + ): Long = decoder.decodeBuffered( + buf = null, + maxBufSize = maxBufSize, + throwOnOverflow = throwOnOverflow, + _get = ::get, + _input = { DecoderInput(this, offset, len) }, + _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 [buf], 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]) while [throwOnOverflow] + * is `false`, or its return value is greater than [buf] 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. + * + * **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( + * UTF8, + * false, + * buf, + * stream::writeAsync, + * ) + * n += text.decodeBufferedAsync( + * UTF8, + * false, + * buf, + * 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 [throwOnOverflow] If `true` and [EncoderDecoder.Config.decodeOutMaxSizeOrFail] + * throws an [EncodingSizeException], it will be re-thrown. If `false`, the exception + * will be ignored and stream decoding to the buffer will continue. + * @param [buf] The pre-allocated array to use as the buffer. Its size must be + * greater than [EncoderDecoder.Config.maxDecodeEmit]. + * @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, such as the [decoder] rejecting + * an invalid character or sequence. + * @throws [EncodingSizeException] If [EncoderDecoder.Config.decodeOutMaxSizeOrFail] + * threw its exception and [throwOnOverflow] is `true`. + * @throws [IllegalArgumentException] If [buf] size is less than or equal to * [EncoderDecoder.Config.maxDecodeEmit]. * */ @JvmStatic @@ -669,19 +1276,20 @@ public sealed class Decoder(public val config: C) { public suspend fun CharSequence.decodeBufferedAsync( decoder: Decoder<*>, throwOnOverflow: Boolean, - maxBufSize: Int, + buf: ByteArray, action: suspend (buf: ByteArray, offset: Int, len: Int) -> Unit, ): Long = decoder.decodeBuffered( - buf = null, - maxBufSize = maxBufSize, + buf = buf, + maxBufSize = buf.size, throwOnOverflow = throwOnOverflow, _get = ::get, _input = { DecoderInput(this) }, - _action = { buf, offset, len -> action(buf, offset, len) }, + _action = { _buf, _offset, _len -> action(_buf, _offset, _len) }, ) /** - * Decode a [CharSequence] using the provided pre-allocated, reusable, [buf] array. + * Decode [len] number of characters from the sequence, starting at index [offset], using + * the provided pre-allocated, reusable, [buf] array. * * The decoding operation will stream decoded bytes to the provided [buf], flushing * to [action] when needed. If the pre-calculated size returned by @@ -710,12 +1318,16 @@ public sealed class Decoder(public val config: C) { * var n = text.decodeBufferedAsync( * UTF8, * false, + * offset = 2, + * len = text.length - 2, * buf, * stream::writeAsync, * ) * n += text.decodeBufferedAsync( * UTF8, * false, + * offset = 1, + * len = 4, * buf, * stream::writeAsync, * ) @@ -735,6 +1347,8 @@ public sealed class Decoder(public val config: C) { * @param [throwOnOverflow] If `true` and [EncoderDecoder.Config.decodeOutMaxSizeOrFail] * throws an [EncodingSizeException], it will be re-thrown. If `false`, the exception * will be ignored and stream decoding to the buffer will continue. + * @param [offset] The index in the sequence to start at. + * @param [len] The number of characters, starting at index [offset]. * @param [buf] The pre-allocated array to use as the buffer. Its size must be * greater than [EncoderDecoder.Config.maxDecodeEmit]. * @param [action] The suspend function to flush the buffer to; a destination to @@ -754,12 +1368,15 @@ public sealed class Decoder(public val config: C) { * threw its exception and [throwOnOverflow] is `true`. * @throws [IllegalArgumentException] If [buf] size is less than or equal to * [EncoderDecoder.Config.maxDecodeEmit]. + * @throws [IndexOutOfBoundsException] If [offset] or [len] are inappropriate. * */ @JvmStatic @Throws(CancellationException::class, EncodingException::class) public suspend fun CharSequence.decodeBufferedAsync( decoder: Decoder<*>, throwOnOverflow: Boolean, + offset: Int, + len: Int, buf: ByteArray, action: suspend (buf: ByteArray, offset: Int, len: Int) -> Unit, ): Long = decoder.decodeBuffered( @@ -767,8 +1384,8 @@ public sealed class Decoder(public val config: C) { maxBufSize = buf.size, throwOnOverflow = throwOnOverflow, _get = ::get, - _input = { DecoderInput(this) }, - _action = { _buf, offset, len -> action(_buf, offset, len) }, + _input = { DecoderInput(this, offset, len) }, + _action = { _buf, _offset, _len -> action(_buf, _offset, _len) }, ) /** @@ -840,6 +1457,81 @@ public sealed class Decoder(public val config: C) { noinline action: (buf: ByteArray, offset: Int, len: Int) -> Unit, ): Long = decodeBuffered(decoder, throwOnOverflow, DEFAULT_BUFFER_SIZE, action) + /** + * Decode [len] number of characters from the array, starting at index [offset], using + * a buffer of maximum size [DEFAULT_BUFFER_SIZE]. + * + * The decoding operation will allocate a single buffer, 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 a buffer 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]) while [throwOnOverflow] + * is `false`, or its return value 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 -> + * val n = "Some long string" + * .toCharArray() + * .decodeBuffered(UTF8, false, 2, 5, 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, false, 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 [throwOnOverflow] If `true` and [EncoderDecoder.Config.decodeOutMaxSizeOrFail] + * throws an [EncodingSizeException], it will be re-thrown. If `false`, the exception + * will be ignored and stream decoding to the buffer will continue. + * @param [offset] The index in the array to start at. + * @param [len] The number of characters, starting at index [offset]. + * @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, such as the [decoder] rejecting + * an invalid character or sequence. + * @throws [EncodingSizeException] If [EncoderDecoder.Config.decodeOutMaxSizeOrFail] + * threw its exception and [throwOnOverflow] is `true`. + * @throws [IndexOutOfBoundsException] If [offset] or [len] are inappropriate. + * */ + @JvmStatic + @Throws(EncodingException::class) + public inline fun CharArray.decodeBuffered( + decoder: Decoder<*>, + throwOnOverflow: Boolean, + offset: Int, + len: Int, + noinline action: (buf: ByteArray, offset: Int, len: Int) -> Unit, + ): Long = decodeBuffered(decoder, throwOnOverflow, offset, len, DEFAULT_BUFFER_SIZE, action) + /** * Decode a [CharArray] using a buffer of maximum size [maxBufSize]. * @@ -871,7 +1563,185 @@ public sealed class Decoder(public val config: C) { * val d = SHA256() * "SGVsbG8gV29ybGQh" * .toCharArray() - * .decodeBuffered(Base64.Default, false, 1024, d::update) + * .decodeBuffered(Base64.Default, false, 1024, 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 [throwOnOverflow] If `true` and [EncoderDecoder.Config.decodeOutMaxSizeOrFail] + * throws an [EncodingSizeException], it will be re-thrown. If `false`, the exception + * will be ignored and stream decoding to the buffer will continue. + * @param [maxBufSize] The maximum size buffer this function will allocate. Must + * be greater than [EncoderDecoder.Config.maxDecodeEmit]. + * @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, such as the [decoder] rejecting + * an invalid character or sequence. + * @throws [EncodingSizeException] If [EncoderDecoder.Config.decodeOutMaxSizeOrFail] + * threw its exception and [throwOnOverflow] is `true`. + * @throws [IllegalArgumentException] If [maxBufSize] is less than or equal to + * [EncoderDecoder.Config.maxDecodeEmit]. + * */ + @JvmStatic + @Throws(EncodingException::class) + public fun CharArray.decodeBuffered( + decoder: Decoder<*>, + throwOnOverflow: Boolean, + maxBufSize: Int, + action: (buf: ByteArray, offset: Int, len: Int) -> Unit, + ): Long = decoder.decodeBuffered( + buf = null, + maxBufSize = maxBufSize, + throwOnOverflow = throwOnOverflow, + _get = ::get, + _input = { DecoderInput(this) }, + _action = action, + ) + + /** + * Decode [len] number of characters from the array, starting at index [offset], using + * a buffer of maximum size [maxBufSize]. + * + * The decoding operation will allocate a single buffer, 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 a buffer 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]) while [throwOnOverflow] + * is `false`, or its return value 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 -> + * val n = "Some string" + * .toCharArray() + * .decodeBuffered(UTF8, false, 1, 4, 1024, 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, false, 1024, 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 [throwOnOverflow] If `true` and [EncoderDecoder.Config.decodeOutMaxSizeOrFail] + * throws an [EncodingSizeException], it will be re-thrown. If `false`, the exception + * will be ignored and stream decoding to the buffer will continue. + * @param [offset] The index in the array to start at. + * @param [len] The number of characters, starting at index [offset]. + * @param [maxBufSize] The maximum size buffer this function will allocate. Must + * be greater than [EncoderDecoder.Config.maxDecodeEmit]. + * @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, such as the [decoder] rejecting + * an invalid character or sequence. + * @throws [EncodingSizeException] If [EncoderDecoder.Config.decodeOutMaxSizeOrFail] + * threw its exception and [throwOnOverflow] is `true`. + * @throws [IllegalArgumentException] If [maxBufSize] is less than or equal to + * [EncoderDecoder.Config.maxDecodeEmit]. + * @throws [IndexOutOfBoundsException] If [offset] or [len] are inappropriate. + * */ + @JvmStatic + @Throws(EncodingException::class) + public fun CharArray.decodeBuffered( + decoder: Decoder<*>, + throwOnOverflow: Boolean, + offset: Int, + len: Int, + maxBufSize: Int, + action: (buf: ByteArray, offset: Int, len: Int) -> Unit, + ): Long = decoder.decodeBuffered( + buf = null, + maxBufSize = maxBufSize, + throwOnOverflow = throwOnOverflow, + _get = ::get, + _input = { DecoderInput(this, offset, len) }, + _action = action, + ) + + /** + * Decode a [CharArray] using the provided pre-allocated, reusable, [buf] array. + * + * The decoding operation will stream decoded bytes to the provided [buf], 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]) while [throwOnOverflow] + * is `false`, or its return value is greater than [buf] 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. + * + * **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(UTF8, false, buf, stream::write) + * n += "Some other string" + * .toCharArray() + * .decodeBuffered(UTF8, false, buf, 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(Base64.Default, false, buf, d::update) + * "SGVsbG8gV29ybGQh" + * .toCharArray() + * .decodeBuffered(Base64.Default, false, buf, d::update) * // ... * * **NOTE:** The [Decoder] implementation must be compatible with version `2.6.0+` @@ -886,8 +1756,8 @@ public sealed class Decoder(public val config: C) { * @param [throwOnOverflow] If `true` and [EncoderDecoder.Config.decodeOutMaxSizeOrFail] * throws an [EncodingSizeException], it will be re-thrown. If `false`, the exception * will be ignored and stream decoding to the buffer will continue. - * @param [maxBufSize] The maximum size buffer this function will allocate. Must - * be greater than [EncoderDecoder.Config.maxDecodeEmit]. + * @param [buf] The pre-allocated array to use as the buffer. Its size must be + * greater than [EncoderDecoder.Config.maxDecodeEmit]. * @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". @@ -902,7 +1772,7 @@ public sealed class Decoder(public val config: C) { * an invalid character or sequence. * @throws [EncodingSizeException] If [EncoderDecoder.Config.decodeOutMaxSizeOrFail] * threw its exception and [throwOnOverflow] is `true`. - * @throws [IllegalArgumentException] If [maxBufSize] is less than or equal to + * @throws [IllegalArgumentException] If [buf] size is less than or equal to * [EncoderDecoder.Config.maxDecodeEmit]. * */ @JvmStatic @@ -910,11 +1780,11 @@ public sealed class Decoder(public val config: C) { public fun CharArray.decodeBuffered( decoder: Decoder<*>, throwOnOverflow: Boolean, - maxBufSize: Int, + buf: ByteArray, action: (buf: ByteArray, offset: Int, len: Int) -> Unit, ): Long = decoder.decodeBuffered( - buf = null, - maxBufSize = maxBufSize, + buf = buf, + maxBufSize = buf.size, throwOnOverflow = throwOnOverflow, _get = ::get, _input = { DecoderInput(this) }, @@ -922,7 +1792,8 @@ public sealed class Decoder(public val config: C) { ) /** - * Decode a [CharArray] using the provided pre-allocated, reusable, [buf] array. + * Decode [len] number of characters from the array, starting at index [offset], using + * the provided pre-allocated, reusable, [buf] array. * * The decoding operation will stream decoded bytes to the provided [buf], flushing * to [action] when needed. If the pre-calculated size returned by @@ -946,10 +1817,10 @@ public sealed class Decoder(public val config: C) { * val buf = ByteArray(DEFAULT_BUFFER_SIZE) * var n = "Some string" * .toCharArray() - * .decodeBuffered(UTF8, false, buf, stream::write) + * .decodeBuffered(UTF8, false, 0, 4, buf, stream::write) * n += "Some other string" * .toCharArray() - * .decodeBuffered(UTF8, false, buf, stream::write) + * .decodeBuffered(UTF8, false, 1, 3, buf, stream::write) * println("Wrote $n UTF-8 bytes to file.txt") * } * @@ -977,6 +1848,8 @@ public sealed class Decoder(public val config: C) { * @param [throwOnOverflow] If `true` and [EncoderDecoder.Config.decodeOutMaxSizeOrFail] * throws an [EncodingSizeException], it will be re-thrown. If `false`, the exception * will be ignored and stream decoding to the buffer will continue. + * @param [offset] The index in the array to start at. + * @param [len] The number of characters, starting at index [offset]. * @param [buf] The pre-allocated array to use as the buffer. Its size must be * greater than [EncoderDecoder.Config.maxDecodeEmit]. * @param [action] The function to flush the buffer to; a destination to "write" @@ -995,12 +1868,15 @@ public sealed class Decoder(public val config: C) { * threw its exception and [throwOnOverflow] is `true`. * @throws [IllegalArgumentException] If [buf] size is less than or equal to * [EncoderDecoder.Config.maxDecodeEmit]. + * @throws [IndexOutOfBoundsException] If [offset] or [len] are inappropriate. * */ @JvmStatic @Throws(EncodingException::class) public fun CharArray.decodeBuffered( decoder: Decoder<*>, throwOnOverflow: Boolean, + offset: Int, + len: Int, buf: ByteArray, action: (buf: ByteArray, offset: Int, len: Int) -> Unit, ): Long = decoder.decodeBuffered( @@ -1008,7 +1884,7 @@ public sealed class Decoder(public val config: C) { maxBufSize = buf.size, throwOnOverflow = throwOnOverflow, _get = ::get, - _input = { DecoderInput(this) }, + _input = { DecoderInput(this, offset, len) }, _action = action, ) @@ -1082,6 +1958,84 @@ public sealed class Decoder(public val config: C) { noinline action: suspend (buf: ByteArray, offset: Int, len: Int) -> Unit, ): Long = decodeBufferedAsync(decoder, throwOnOverflow, DEFAULT_BUFFER_SIZE, action) + /** + * Decode [len] number of characters from the array, starting at index [offset], using + * a buffer of maximum size [DEFAULT_BUFFER_SIZE]. + * + * The decoding operation will allocate a single buffer, 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 a buffer 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]) while [throwOnOverflow] + * is `false`, or its return value 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 { + * "/path/to/file.txt".toFile() + * .openWriteAsync(excl = null) + * .useAsync { stream -> + * val chars = "Some long string" + * .toCharArray() + * val n = chars.decodeBufferedAsync( + * UTF8, + * false, + * offset = 0, + * len = 4, + * 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 [throwOnOverflow] If `true` and [EncoderDecoder.Config.decodeOutMaxSizeOrFail] + * throws an [EncodingSizeException], it will be re-thrown. If `false`, the exception + * will be ignored and stream decoding to the buffer will continue. + * @param [offset] The index in the array to start at. + * @param [len] The number of characters, starting at index [offset]. + * @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, such as the [decoder] rejecting + * an invalid character or sequence. + * @throws [EncodingSizeException] If [EncoderDecoder.Config.decodeOutMaxSizeOrFail] + * threw its exception and [throwOnOverflow] is `true`. + * @throws [IndexOutOfBoundsException] If [offset] or [len] are inappropriate. + * */ + @JvmStatic + @Throws(CancellationException::class, EncodingException::class) + public suspend inline fun CharArray.decodeBufferedAsync( + decoder: Decoder<*>, + throwOnOverflow: Boolean, + offset: Int, + len: Int, + noinline action: suspend (buf: ByteArray, offset: Int, len: Int) -> Unit, + ): Long = decodeBufferedAsync(decoder, throwOnOverflow, offset, len, DEFAULT_BUFFER_SIZE, action) + /** * Decode a [CharArray] using a buffer of maximum size [maxBufSize]. * @@ -1162,7 +2116,98 @@ public sealed class Decoder(public val config: C) { throwOnOverflow = throwOnOverflow, _get = ::get, _input = { DecoderInput(this) }, - _action = { buf, offset, len -> action(buf, offset, len) }, + _action = { _buf, _offset, _len -> action(_buf, _offset, _len) }, + ) + + /** + * Decode [len] number of characters from the array, starting at index [offset], using + * a buffer of maximum size [maxBufSize]. + * + * The decoding operation will allocate a single buffer, 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 a buffer 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]) while [throwOnOverflow] + * is `false`, or its return value 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 { + * "/path/to/file.txt".toFile() + * .openWriteAsync(excl = null) + * .useAsync { stream -> + * val chars = "Some long string" + * .toCharArray() + * val n = chars.decodeBufferedAsync( + * UTF8, + * false, + * offset = 5, + * len = chars.size - 5, + * 1024, + * 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 [throwOnOverflow] If `true` and [EncoderDecoder.Config.decodeOutMaxSizeOrFail] + * throws an [EncodingSizeException], it will be re-thrown. If `false`, the exception + * will be ignored and stream decoding to the buffer will continue. + * @param [offset] The index in the array to start at. + * @param [len] The number of characters, starting at index [offset]. + * @param [maxBufSize] The maximum size buffer this function will allocate. Must + * be greater than [EncoderDecoder.Config.maxDecodeEmit]. + * @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, such as the [decoder] rejecting + * an invalid character or sequence. + * @throws [EncodingSizeException] If [EncoderDecoder.Config.decodeOutMaxSizeOrFail] + * threw its exception and [throwOnOverflow] is `true`. + * @throws [IllegalArgumentException] If [maxBufSize] is less than or equal to + * [EncoderDecoder.Config.maxDecodeEmit]. + * @throws [IndexOutOfBoundsException] If [offset] or [len] are inappropriate. + * */ + @JvmStatic + @Throws(CancellationException::class, EncodingException::class) + public suspend fun CharArray.decodeBufferedAsync( + decoder: Decoder<*>, + throwOnOverflow: Boolean, + offset: Int, + len: Int, + maxBufSize: Int, + action: suspend (buf: ByteArray, offset: Int, len: Int) -> Unit, + ): Long = decoder.decodeBuffered( + buf = null, + maxBufSize = maxBufSize, + throwOnOverflow = throwOnOverflow, + _get = ::get, + _input = { DecoderInput(this, offset, len) }, + _action = { _buf, _offset, _len -> action(_buf, _offset, _len) }, ) /** @@ -1254,7 +2299,109 @@ public sealed class Decoder(public val config: C) { throwOnOverflow = throwOnOverflow, _get = ::get, _input = { DecoderInput(this) }, - _action = { _buf, offset, len -> action(_buf, offset, len) }, + _action = { _buf, _offset, _len -> action(_buf, _offset, _len) }, + ) + + /** + * Decode [len] number of characters from the array, starting at index [offset], using + * the provided pre-allocated, reusable, [buf] array. + * + * The decoding operation will stream decoded bytes to the provided [buf], 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]) while [throwOnOverflow] + * is `false`, or its return value is greater than [buf] 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. + * + * **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( + * UTF8, + * false, + * offset = 2, + * len = chars.size - 2, + * buf, + * stream::writeAsync, + * ) + * n += chars.decodeBufferedAsync( + * UTF8, + * false, + * offset = 1, + * len = chars.size - 1, + * buf, + * 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 [throwOnOverflow] If `true` and [EncoderDecoder.Config.decodeOutMaxSizeOrFail] + * throws an [EncodingSizeException], it will be re-thrown. If `false`, the exception + * will be ignored and stream decoding to the buffer will continue. + * @param [offset] The index in the array to start at. + * @param [len] The number of characters, starting at index [offset]. + * @param [buf] The pre-allocated array to use as the buffer. Its size must be + * greater than [EncoderDecoder.Config.maxDecodeEmit]. + * @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, such as the [decoder] rejecting + * an invalid character or sequence. + * @throws [EncodingSizeException] If [EncoderDecoder.Config.decodeOutMaxSizeOrFail] + * threw its exception and [throwOnOverflow] is `true`. + * @throws [IllegalArgumentException] If [buf] size is less than or equal to + * [EncoderDecoder.Config.maxDecodeEmit]. + * @throws [IndexOutOfBoundsException] If [offset] or [len] are inappropriate. + * */ + @JvmStatic + @Throws(CancellationException::class, EncodingException::class) + public suspend fun CharArray.decodeBufferedAsync( + decoder: Decoder<*>, + throwOnOverflow: Boolean, + offset: Int, + len: Int, + buf: ByteArray, + action: suspend (buf: ByteArray, offset: Int, len: Int) -> Unit, + ): Long = decoder.decodeBuffered( + buf = buf, + maxBufSize = buf.size, + throwOnOverflow = throwOnOverflow, + _get = ::get, + _input = { DecoderInput(this, offset, len) }, + _action = { _buf, _offset, _len -> action(_buf, _offset, _len) }, ) /** 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 220a0704..321bb2f6 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 @@ -59,7 +59,7 @@ public sealed class Encoder(config: C): Decoder(con * }.use { feed -> * "Hello World!" * .encodeToByteArray() - * .forEach { b -> feed.consume(b) } + * .forEach(feed::consume) * } * println(sb.toString()) * diff --git a/library/core/src/commonMain/kotlin/io/matthewnelson/encoding/core/internal/-Decoder.kt b/library/core/src/commonMain/kotlin/io/matthewnelson/encoding/core/internal/-Decoder.kt index a95e2c12..8ce7a423 100644 --- a/library/core/src/commonMain/kotlin/io/matthewnelson/encoding/core/internal/-Decoder.kt +++ b/library/core/src/commonMain/kotlin/io/matthewnelson/encoding/core/internal/-Decoder.kt @@ -35,7 +35,7 @@ internal inline fun Decoder.decode( val maxDecodeSize = config.decodeOutMaxSizeOrFail(input) val a = ByteArray(maxDecodeSize) @Suppress("DEPRECATION") - val len = decodeTo(maxDecodeSizeArray = a, input.size, _get) + val len = decodeToUnsafe(maxDecodeSizeArray = a, input.offset, input.size, _get) if (len == maxDecodeSize) return a val copy = a.copyOf(len) if (config.backFillBuffers) { @@ -90,7 +90,7 @@ internal inline fun Decoder.decodeBuffered( // Maximum decoded size will be less than or equal to maxBufSize. One-shot it. val decoded = buf ?: ByteArray(maxDecodeSize) @Suppress("DEPRECATION") - val len = decodeTo(maxDecodeSizeArray = decoded, input.size, _get) + val len = decodeToUnsafe(maxDecodeSizeArray = decoded, input.offset, input.size, _get) try { _action(decoded, 0, len) } finally { @@ -101,15 +101,15 @@ internal inline fun Decoder.decodeBuffered( // Chunk 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 { + var iBuf = 0 newDecoderFeed(out = { b -> _buf[iBuf++] = b }).use { feed -> - while (i < inputSize) { - feed.consume(input = _get(i++)) + val limit = _buf.size - config.maxDecodeEmit + val offset = input.offset + + for (i in 0 until input.size) { + feed.consume(_get(offset + i)) if (iBuf <= limit) continue _action(_buf, 0, iBuf) size += iBuf @@ -125,23 +125,24 @@ internal inline fun Decoder.decodeBuffered( return size } +// Does not check offset/len, thus the *Unsafe suffix @Throws(EncodingException::class) @OptIn(ExperimentalContracts::class) @Deprecated("UNSAFE; do not reference directly. Used by decode/decodeBuffered only.") -internal inline fun Decoder.decodeTo( +internal inline fun Decoder.decodeToUnsafe( maxDecodeSizeArray: ByteArray, - inputSize: Int, + offset: Int, + len: Int, _get: (i: Int) -> Char, ): Int { contract { callsInPlace(_get, InvocationKind.UNKNOWN) } - if (inputSize == 0) return 0 + if (len == 0) return 0 var i = 0 try { newDecoderFeed(out = { b -> maxDecodeSizeArray[i++] = b }).use { feed -> - var j = 0 - while (j < inputSize) { - feed.consume(_get(j++)) + for (j in 0 until len) { + feed.consume(_get(offset + j)) } } } catch (t: Throwable) { @@ -149,7 +150,6 @@ internal inline fun Decoder.decodeTo( maxDecodeSizeArray.fill(0, 0, min(maxDecodeSizeArray.size, i)) } if (t is IndexOutOfBoundsException && i >= maxDecodeSizeArray.size) { - // Something is wrong with the encoder's pre-calculation throw EncodingSizeException("Decoder's pre-calculation of Size[${maxDecodeSizeArray.size}] was incorrect", t) } throw t diff --git a/library/core/src/commonMain/kotlin/io/matthewnelson/encoding/core/internal/-Encoder.kt b/library/core/src/commonMain/kotlin/io/matthewnelson/encoding/core/internal/-Encoder.kt index 39c0758a..142116b3 100644 --- a/library/core/src/commonMain/kotlin/io/matthewnelson/encoding/core/internal/-Encoder.kt +++ b/library/core/src/commonMain/kotlin/io/matthewnelson/encoding/core/internal/-Encoder.kt @@ -36,8 +36,12 @@ internal inline fun Encoder.encodeUnsafe( ) { contract { callsInPlace(_outFeed, InvocationKind.AT_MOST_ONCE) } if (len == 0) return - @Suppress("DEPRECATION") - encodeToUnsafe(data, offset, len, out = _outFeed()) + val out = _outFeed() + newEncoderFeed(out).use { feed -> + repeat(len) { i -> + feed.consume(data[offset + i]) + } + } } // Does not check offset/len, thus the *Unsafe suffix @@ -67,7 +71,6 @@ internal inline fun Encoder.encodeBufferedUnsafe( val parameter = if (buf != null) "buf.size" else "maxBufSize" "$parameter[$maxBufSize] <= ${this}.config.maxEncodeEmitWithLineBreak[${config.maxEncodeEmitWithLineBreak}]" } - if (len == 0) return 0L try { config.encodeOutMaxSize(len) @@ -80,8 +83,7 @@ internal inline fun Encoder.encodeBufferedUnsafe( // Maximum encoded size will be less than or equal to maxBufSize. One-shot it. var i = 0 val encoded = buf ?: CharArray(maxEncodeSize) - @Suppress("DEPRECATION") - encodeToUnsafe(data, offset, len, out = { c -> encoded[i++] = c }) + encodeUnsafe(data, offset, len, _outFeed = { Encoder.OutFeed { c -> encoded[i++] = c } }) try { _action(encoded, 0, i) } finally { @@ -92,11 +94,11 @@ internal inline fun Encoder.encodeBufferedUnsafe( // Chunk val _buf = buf ?: CharArray(maxBufSize) - val limit = _buf.size - config.maxEncodeEmitWithLineBreak - var iBuf = 0 var size = 0L try { + var iBuf = 0 newEncoderFeed(out = { c -> _buf[iBuf++] = c }).use { feed -> + val limit = _buf.size - config.maxEncodeEmitWithLineBreak for (i in 0 until len) { feed.consume(data[offset + i]) if (iBuf <= limit) continue @@ -113,18 +115,3 @@ internal inline fun Encoder.encodeBufferedUnsafe( } return size } - -// Does not check offset/len, thus the *Unsafe suffix -@Deprecated("UNSAFE; do not reference directly. Used by encodeUnsafe/encodeBufferedUnsafe only.") -internal inline fun Encoder.encodeToUnsafe( - data: ByteArray, - offset: Int, - len: Int, - out: Encoder.OutFeed, -) { - newEncoderFeed(out).use { feed -> - repeat(len) { i -> - feed.consume(data[offset + i]) - } - } -} 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 8377fb09..1cfbcc49 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 @@ -20,6 +20,7 @@ package io.matthewnelson.encoding.core.util import io.matthewnelson.encoding.core.Decoder import io.matthewnelson.encoding.core.EncoderDecoder import io.matthewnelson.encoding.core.EncodingException +import io.matthewnelson.encoding.core.internal.checkBounds import kotlin.jvm.JvmSynthetic /** @@ -33,18 +34,34 @@ import kotlin.jvm.JvmSynthetic * */ public class DecoderInput { + @get:JvmSynthetic + internal val offset: Int @get:JvmSynthetic internal val size: Int private val _get: (Int) -> Char - private constructor(size: Int, get: (Int) -> Char) { this.size = size; this._get = get } - public constructor(input: CharSequence): this(input.length, get = input::get) - public constructor(input: CharArray): this(input.size, get = input::get) + private constructor(offset: Int, size: Int, get: (Int) -> Char) { + this.offset = offset + this.size = size + this._get = get + } + + public constructor(input: CharSequence): this(offset = 0, size = input.length, input::get) + @Throws(IndexOutOfBoundsException::class) + public constructor(input: CharSequence, offset: Int, len: Int): this(offset = offset, size = len, input::get) { + input.checkBounds(offset, len) + } + + public constructor(input: CharArray): this(offset = 0, size = input.size, input::get) + @Throws(IndexOutOfBoundsException::class) + public constructor(input: CharArray, offset: Int, len: Int): this(offset = offset, size = len, input::get) { + input.checkBounds(offset, len) + } @Throws(EncodingException::class) public operator fun get(index: Int): Char { try { - return _get(index) + return _get(index + offset) } catch (e: IndexOutOfBoundsException) { throw EncodingException("Index out of bounds", e) } @@ -58,5 +75,5 @@ public class DecoderInput { message = "Should not utilize. Underlying Byte to Char conversion can produce incorrect results", level = DeprecationLevel.ERROR, ) - public constructor(input: ByteArray): this(input.size, get = { i -> input[i].toInt().toChar() }) + public constructor(input: ByteArray): this(0, input.size, get = { i -> input[i].toInt().toChar() }) } diff --git a/library/core/src/commonTest/kotlin/io/matthewnelson/encoding/core/PartialDecodingUnitTest.kt b/library/core/src/commonTest/kotlin/io/matthewnelson/encoding/core/PartialDecodingUnitTest.kt new file mode 100644 index 00000000..4f833a54 --- /dev/null +++ b/library/core/src/commonTest/kotlin/io/matthewnelson/encoding/core/PartialDecodingUnitTest.kt @@ -0,0 +1,242 @@ +/* + * Copyright (c) 2025 Matthew Nelson + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + **/ +package io.matthewnelson.encoding.core + +import io.matthewnelson.encoding.core.Decoder.Companion.decodeBuffered +import io.matthewnelson.encoding.core.Decoder.Companion.decodeBufferedAsync +import io.matthewnelson.encoding.core.Decoder.Companion.decodeToByteArray +import io.matthewnelson.encoding.core.Decoder.Companion.decodeToByteArrayOrNull +import io.matthewnelson.encoding.core.helpers.TestConfig +import io.matthewnelson.encoding.core.helpers.TestEncoderDecoder +import kotlinx.coroutines.test.runTest +import kotlin.test.Test +import kotlin.test.assertContentEquals +import kotlin.test.assertEquals +import kotlin.test.assertFailsWith +import kotlin.test.assertNotEquals + +class PartialDecodingUnitTest { + + @Test + fun givenDecode_whenPartialDecoding_thenChecksBounds() { + val decoder = TestEncoderDecoder(TestConfig()) + assertFailsWith { + "a".decodeToByteArray(decoder, -1, 1) + } + assertFailsWith { + charArrayOf('a').decodeToByteArray(decoder, -1, 1) + } + assertFailsWith { + "a".decodeToByteArrayOrNull(decoder, -1, 1) + } + assertFailsWith { + charArrayOf('a').decodeToByteArrayOrNull(decoder, -1, 1) + } + } + + @Test + fun givenDecodeBuffered_whenPartialDecoding_thenChecksBounds() = runTest { + val decoder = TestEncoderDecoder(TestConfig()) + assertFailsWith { + "a".decodeBuffered( + decoder, + true, + offset = -1, + len = 1, + action = { _, _, _ -> error("Should not make it here") } + ) + } + assertFailsWith { + charArrayOf('a').decodeBuffered( + decoder, + true, + offset = -1, + len = 1, + action = { _, _, _ -> error("Should not make it here") } + ) + } + assertFailsWith { + "a".decodeBuffered( + decoder, + true, + offset = -1, + len = 1, + maxBufSize = 25, + action = { _, _, _ -> error("Should not make it here") } + ) + } + assertFailsWith { + charArrayOf('a').decodeBuffered( + decoder, + true, + offset = -1, + len = 1, + maxBufSize = 25, + action = { _, _, _ -> error("Should not make it here") } + ) + } + assertFailsWith { + "a".decodeBuffered( + decoder, + true, + offset = -1, + len = 1, + buf = ByteArray(25), + action = { _, _, _ -> error("Should not make it here") } + ) + } + assertFailsWith { + charArrayOf('a').decodeBuffered( + decoder, + true, + offset = -1, + len = 1, + buf = ByteArray(25), + action = { _, _, _ -> error("Should not make it here") } + ) + } + assertFailsWith { + "a".decodeBufferedAsync( + decoder, + true, + offset = -1, + len = 1, + action = { _, _, _ -> error("Should not make it here") } + ) + } + assertFailsWith { + charArrayOf('a').decodeBufferedAsync( + decoder, + true, + offset = -1, + len = 1, + action = { _, _, _ -> error("Should not make it here") } + ) + } + assertFailsWith { + "a".decodeBufferedAsync( + decoder, + true, + offset = -1, + len = 1, + maxBufSize = 25, + action = { _, _, _ -> error("Should not make it here") } + ) + } + assertFailsWith { + charArrayOf('a').decodeBufferedAsync( + decoder, + true, + offset = -1, + len = 1, + maxBufSize = 25, + action = { _, _, _ -> error("Should not make it here") } + ) + } + assertFailsWith { + "a".decodeBufferedAsync( + decoder, + true, + offset = -1, + len = 1, + buf = ByteArray(25), + action = { _, _, _ -> error("Should not make it here") } + ) + } + assertFailsWith { + charArrayOf('a').decodeBufferedAsync( + decoder, + true, + offset = -1, + len = 1, + buf = ByteArray(25), + action = { _, _, _ -> error("Should not make it here") } + ) + } + } + + @Test + fun givenDecode_whenPartialDecoding_thenConsumesCorrectIndices() { + val expected = 4 + var invocationDecodeOut = 0 + var invocationDecoderConsume = 0 + val decoder = TestEncoderDecoder( + config = TestConfig( + decodeOutInputReturn = { size -> + invocationDecodeOut++ + assertEquals(expected, size) + size + }, + ), + decoderConsume = { c -> + invocationDecoderConsume++ + assertEquals(expected.toChar(), c) + output(expected.toByte()) + }, + decoderDoFinal = {}, + ) + + val data = charArrayOf('\u0000', expected.toChar(), expected.toChar(), expected.toChar(), expected.toChar(), '\u0000') + + // Exercises -Decoder.kt decode & decodeToUnsafe + assertContentEquals( + byteArrayOf(expected.toByte(), expected.toByte(), expected.toByte(), expected.toByte()), + data.decodeToByteArray(decoder, 1, expected) + ) + assertEquals(1, invocationDecodeOut) + assertEquals(expected, invocationDecoderConsume) + + // Exercises -Decoder.kt decodeBuffered & decodeToUnsafe + var invocationAction = 0 + // single shot decode + var result = data.decodeBuffered( + decoder, + throwOnOverflow = true, + offset = 1, + len = expected, + action = { buf, offset, len -> + invocationAction++ + assertEquals(expected, len) + assertEquals(0, offset) + repeat(len) { i -> assertEquals(expected.toByte(), buf[i]) } + } + ) + assertEquals(expected.toLong(), result) + assertEquals(1, invocationAction) + assertEquals(2, invocationDecodeOut) + assertEquals(expected * 2, invocationDecoderConsume) + invocationAction = 0 // reset + + // stream to buffer & flush + result = data.decodeBuffered( + decoder, + throwOnOverflow = true, + offset = 1, + len = expected, + maxBufSize = 3, + action = { buf, offset, len -> + invocationAction++ + assertEquals(0, offset) + assertNotEquals(0, len) + repeat(len) { i -> assertEquals(expected.toByte(), buf[i]) } + } + ) + assertEquals(expected.toLong(), result) + assertEquals(2, invocationAction) + assertEquals(3, invocationDecodeOut) + assertEquals(expected * 3, invocationDecoderConsume) + } +} diff --git a/library/core/src/commonTest/kotlin/io/matthewnelson/encoding/core/PartialEncodingUnitTest.kt b/library/core/src/commonTest/kotlin/io/matthewnelson/encoding/core/PartialEncodingUnitTest.kt index 5f9d1180..bbc47754 100644 --- a/library/core/src/commonTest/kotlin/io/matthewnelson/encoding/core/PartialEncodingUnitTest.kt +++ b/library/core/src/commonTest/kotlin/io/matthewnelson/encoding/core/PartialEncodingUnitTest.kt @@ -16,10 +16,12 @@ package io.matthewnelson.encoding.core import io.matthewnelson.encoding.core.Encoder.Companion.encodeBuffered +import io.matthewnelson.encoding.core.Encoder.Companion.encodeBufferedAsync import io.matthewnelson.encoding.core.Encoder.Companion.encodeToCharArray import io.matthewnelson.encoding.core.Encoder.Companion.encodeToString import io.matthewnelson.encoding.core.helpers.TestConfig import io.matthewnelson.encoding.core.helpers.TestEncoderDecoder +import kotlinx.coroutines.test.runTest import kotlin.test.Test import kotlin.test.assertContentEquals import kotlin.test.assertEquals @@ -40,8 +42,17 @@ class PartialEncodingUnitTest { } @Test - fun givenEncodeBuffered_whenPartialEncoding_thenChecksBounds() { + fun givenEncodeBuffered_whenPartialEncoding_thenChecksBounds() = runTest { val encoder = TestEncoderDecoder(TestConfig()) + assertFailsWith { + ByteArray(1).encodeBuffered( + encoder, + true, + offset = -1, + len = 1, + action = { _, _, _ -> error("Should not make it here") } + ) + } assertFailsWith { ByteArray(1).encodeBuffered( encoder, @@ -62,6 +73,35 @@ class PartialEncodingUnitTest { action = { _, _, _ -> error("Should not make it here") } ) } + assertFailsWith { + ByteArray(1).encodeBufferedAsync( + encoder, + true, + offset = -1, + len = 1, + action = { _, _, _ -> error("Should not make it here") } + ) + } + assertFailsWith { + ByteArray(1).encodeBufferedAsync( + encoder, + true, + offset = -1, + len = 1, + maxBufSize = 25, + action = { _, _, _ -> error("Should not make it here") } + ) + } + assertFailsWith { + ByteArray(1).encodeBufferedAsync( + encoder, + true, + offset = -1, + len = 1, + buf = CharArray(25), + action = { _, _, _ -> error("Should not make it here") } + ) + } } @Test @@ -86,14 +126,18 @@ class PartialEncodingUnitTest { ) val data = byteArrayOf(0, expected.toByte(), expected.toByte(), expected.toByte(), expected.toByte(), 0) + + // Exercises -Encoder.kt encodeUnsafe for the ToString implementation assertEquals("bbbb", data.encodeToString(encoder, 1, expected)) assertEquals(1, invocationEncodeOut) assertEquals(expected, invocationEncoderConsume) + // Exercises -Encoder.kt encodeUnsafe for the ToCharArray implementation assertContentEquals(charArrayOf('b', 'b', 'b', 'b'), data.encodeToCharArray(encoder, 1, expected)) assertEquals(2, invocationEncodeOut) assertEquals(expected * 2, invocationEncoderConsume) + // Exercises -Encoder.kt encodeBufferedUnsafe var invocationAction = 0 // single shot encode var result = data.encodeBuffered( diff --git a/library/core/src/commonTest/kotlin/io/matthewnelson/encoding/core/util/DecoderInputUnitTest.kt b/library/core/src/commonTest/kotlin/io/matthewnelson/encoding/core/util/DecoderInputUnitTest.kt index 58bbe1be..c4c9d50c 100644 --- a/library/core/src/commonTest/kotlin/io/matthewnelson/encoding/core/util/DecoderInputUnitTest.kt +++ b/library/core/src/commonTest/kotlin/io/matthewnelson/encoding/core/util/DecoderInputUnitTest.kt @@ -20,6 +20,7 @@ import io.matthewnelson.encoding.core.EncodingSizeException import io.matthewnelson.encoding.core.helpers.TestConfig import kotlin.test.Test import kotlin.test.assertEquals +import kotlin.test.assertFailsWith import kotlin.test.fail class DecoderInputUnitTest { @@ -136,4 +137,20 @@ class DecoderInputUnitTest { @Suppress("DEPRECATION_ERROR") config.decodeOutMaxSizeOrFail(DecoderInput(validInput)) } + + @Test + fun givenDecoderInput_whenOffsetLen_thenChecksBounds() { + // internal checkBounds function is tested. Just need to verify + // it is being utilized in the offset/len constructor. + assertFailsWith { DecoderInput("123", offset = -1, len = 2) } + assertFailsWith { DecoderInput(charArrayOf(' '), offset = -1, len = 2) } + } + + @Test + fun givenDecoderInput_whenOffset_thenGetReturnsExpected() { + val input = DecoderInput("123", offset = 1, len = 2) + assertEquals(1, input.offset) + assertEquals(2, input.size) + assertEquals('2', input[0]) + } }