From 5f108b608ae4f3f83c2c9fd9af1ac3039677b6ac Mon Sep 17 00:00:00 2001 From: Cedrick Cooke Date: Sat, 7 Feb 2026 20:55:18 -0800 Subject: [PATCH] Add CBOR kotlinx-io support --- build.gradle.kts | 1 + .../kotlin/publishing-conventions.gradle.kts | 1 + .../api/kotlinx-serialization-cbor-io.api | 5 + .../kotlinx-serialization-cbor-io.klib.api | 12 +++ formats/cbor-io/build.gradle.kts | 41 ++++++++ .../serialization/cbor/io/IoStreams.kt | 55 +++++++++++ .../cbor/io/internal/IoCborStreams.kt | 38 ++++++++ .../kotlinx/serialization/cbor/io/IoTests.kt | 48 ++++++++++ .../cbor/api/kotlinx-serialization-cbor.api | 58 ++++++++++++ .../api/kotlinx-serialization-cbor.klib.api | 63 +++++++++++++ formats/cbor/build.gradle.kts | 7 ++ .../src/kotlinx/serialization/cbor/Cbor.kt | 32 ++++--- .../cbor/internal/CborFriendModuleApi.kt | 8 ++ .../serialization/cbor/internal/Decoder.kt | 8 +- .../serialization/cbor/internal/Encoder.kt | 93 +++++++++---------- .../serialization/cbor/internal/Streams.kt | 39 +++++--- settings.gradle.kts | 3 + 17 files changed, 432 insertions(+), 80 deletions(-) create mode 100644 formats/cbor-io/api/kotlinx-serialization-cbor-io.api create mode 100644 formats/cbor-io/api/kotlinx-serialization-cbor-io.klib.api create mode 100644 formats/cbor-io/build.gradle.kts create mode 100644 formats/cbor-io/commonMain/src/kotlinx/serialization/cbor/io/IoStreams.kt create mode 100644 formats/cbor-io/commonMain/src/kotlinx/serialization/cbor/io/internal/IoCborStreams.kt create mode 100644 formats/cbor-io/src/commonTest/kotlin/kotlinx/serialization/cbor/io/IoTests.kt create mode 100644 formats/cbor/commonMain/src/kotlinx/serialization/cbor/internal/CborFriendModuleApi.kt diff --git a/build.gradle.kts b/build.gradle.kts index 538b1804c3..2342994d2b 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -166,6 +166,7 @@ val documentedSubprojects "kotlinx-serialization-json-okio", "kotlinx-serialization-json-io", "kotlinx-serialization-cbor", + "kotlinx-serialization-cbor-io", "kotlinx-serialization-properties", "kotlinx-serialization-hocon", "kotlinx-serialization-protobuf" diff --git a/buildSrc/src/main/kotlin/publishing-conventions.gradle.kts b/buildSrc/src/main/kotlin/publishing-conventions.gradle.kts index 4436677f3a..ab386f7eb2 100644 --- a/buildSrc/src/main/kotlin/publishing-conventions.gradle.kts +++ b/buildSrc/src/main/kotlin/publishing-conventions.gradle.kts @@ -24,6 +24,7 @@ val isMultiplatform = name in listOf( "kotlinx-serialization-json-tests", "kotlinx-serialization-protobuf", "kotlinx-serialization-cbor", + "kotlinx-serialization-cbor-io", "kotlinx-serialization-properties" ) diff --git a/formats/cbor-io/api/kotlinx-serialization-cbor-io.api b/formats/cbor-io/api/kotlinx-serialization-cbor-io.api new file mode 100644 index 0000000000..ee961dc52d --- /dev/null +++ b/formats/cbor-io/api/kotlinx-serialization-cbor-io.api @@ -0,0 +1,5 @@ +public final class kotlinx/serialization/cbor/io/IoStreamsKt { + public static final fun decodeFromSource (Lkotlinx/serialization/cbor/Cbor;Lkotlinx/serialization/DeserializationStrategy;Lkotlinx/io/Source;)Ljava/lang/Object; + public static final fun encodeToSink (Lkotlinx/serialization/cbor/Cbor;Lkotlinx/serialization/SerializationStrategy;Ljava/lang/Object;Lkotlinx/io/Sink;)V +} + diff --git a/formats/cbor-io/api/kotlinx-serialization-cbor-io.klib.api b/formats/cbor-io/api/kotlinx-serialization-cbor-io.klib.api new file mode 100644 index 0000000000..292f20b6ad --- /dev/null +++ b/formats/cbor-io/api/kotlinx-serialization-cbor-io.klib.api @@ -0,0 +1,12 @@ +// Klib ABI Dump +// Targets: [androidNativeArm32, androidNativeArm64, androidNativeX64, androidNativeX86, iosArm64, iosSimulatorArm64, iosX64, js, linuxArm32Hfp, linuxArm64, linuxX64, macosArm64, macosX64, mingwX64, tvosArm64, tvosSimulatorArm64, tvosX64, wasmJs, wasmWasi, watchosArm32, watchosArm64, watchosDeviceArm64, watchosSimulatorArm64, watchosX64] +// Rendering settings: +// - Signature version: 2 +// - Show manifest properties: true +// - Show declarations: true + +// Library unique name: +final fun <#A: kotlin/Any?> (kotlinx.serialization.cbor/Cbor).kotlinx.serialization.cbor.io/decodeFromSource(kotlinx.serialization/DeserializationStrategy<#A>, kotlinx.io/Source): #A // kotlinx.serialization.cbor.io/decodeFromSource|decodeFromSource@kotlinx.serialization.cbor.Cbor(kotlinx.serialization.DeserializationStrategy<0:0>;kotlinx.io.Source){0§}[0] +final fun <#A: kotlin/Any?> (kotlinx.serialization.cbor/Cbor).kotlinx.serialization.cbor.io/encodeToSink(kotlinx.serialization/SerializationStrategy<#A>, #A, kotlinx.io/Sink) // kotlinx.serialization.cbor.io/encodeToSink|encodeToSink@kotlinx.serialization.cbor.Cbor(kotlinx.serialization.SerializationStrategy<0:0>;0:0;kotlinx.io.Sink){0§}[0] +final inline fun <#A: reified kotlin/Any?> (kotlinx.serialization.cbor/Cbor).kotlinx.serialization.cbor.io/decodeFromSource(kotlinx.io/Source): #A // kotlinx.serialization.cbor.io/decodeFromSource|decodeFromSource@kotlinx.serialization.cbor.Cbor(kotlinx.io.Source){0§}[0] +final inline fun <#A: reified kotlin/Any?> (kotlinx.serialization.cbor/Cbor).kotlinx.serialization.cbor.io/encodeToSink(#A, kotlinx.io/Sink) // kotlinx.serialization.cbor.io/encodeToSink|encodeToSink@kotlinx.serialization.cbor.Cbor(0:0;kotlinx.io.Sink){0§}[0] diff --git a/formats/cbor-io/build.gradle.kts b/formats/cbor-io/build.gradle.kts new file mode 100644 index 0000000000..1a8b68a5cc --- /dev/null +++ b/formats/cbor-io/build.gradle.kts @@ -0,0 +1,41 @@ +/* + * Copyright 2017-2022 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ +import Java9Modularity.configureJava9ModuleInfo +import Java9Modularity.configureMetadataJarAutomaticModuleName + +plugins { + kotlin("multiplatform") + kotlin("plugin.serialization") + + id("native-targets-conventions") + id("source-sets-conventions") +} + +kotlin { + sourceSets { + configureEach { + languageSettings { + optIn("kotlinx.serialization.internal.CoreFriendModuleApi") + optIn("kotlinx.serialization.cbor.internal.CborFriendModuleApi") + } + } + val commonMain by getting { + dependencies { + api(project(":kotlinx-serialization-core")) + api(project(":kotlinx-serialization-cbor")) + implementation(libs.kotlinx.io) + } + } + } +} + +project.configureJava9ModuleInfo() +project.configureMetadataJarAutomaticModuleName() + +dokka.dokkaSourceSets.configureEach { + externalDocumentationLinks.register("kotlinx-io") { + url("https://kotlinlang.org/api/kotlinx-io/") + packageListUrl = file("dokka/kotlinx-io.package-list").toURI() + } +} diff --git a/formats/cbor-io/commonMain/src/kotlinx/serialization/cbor/io/IoStreams.kt b/formats/cbor-io/commonMain/src/kotlinx/serialization/cbor/io/IoStreams.kt new file mode 100644 index 0000000000..b1944f8111 --- /dev/null +++ b/formats/cbor-io/commonMain/src/kotlinx/serialization/cbor/io/IoStreams.kt @@ -0,0 +1,55 @@ +package kotlinx.serialization.cbor.io + +import kotlinx.io.* +import kotlinx.serialization.* +import kotlinx.serialization.cbor.* +import kotlinx.serialization.cbor.io.internal.* + +/** + * Serializes the [value] with [serializer] into a [sink] using CBOR format. + * + * @throws [SerializationException] if the given value cannot be serialized to CBOR. + * @throws [kotlinx.io.IOException] If an I/O error occurs and sink can't be written to. + */ +@ExperimentalSerializationApi +public fun Cbor.encodeToSink(serializer: SerializationStrategy, value: T, sink: Sink) { + encodeToOutput(serializer, value, IoStreamOutput(sink)) +} + +/** + * Serializes given [value] to a [sink] using CBOR format and serializer retrieved from the reified type parameter. + * + * @throws [SerializationException] if the given value cannot be serialized to CBOR. + * @throws [kotlinx.io.IOException] If an I/O error occurs and sink can't be written to. + */ +@ExperimentalSerializationApi +public inline fun Cbor.encodeToSink( + value: T, + sink: Sink +): Unit = encodeToSink(serializersModule.serializer(), value, sink) + +/** + * Deserializes CBOR from [source] to a value of type [T] using [deserializer]. + * + * Note that this functions expects that exactly one object would be present in the source + * and throws an exception if there are any dangling bytes after an object. + * + * @throws [SerializationException] if the given CBOR input cannot be deserialized to the value of type [T]. + * @throws [kotlinx.io.IOException] If an I/O error occurs and source can't be read from. + */ +@ExperimentalSerializationApi +public fun Cbor.decodeFromSource(deserializer: DeserializationStrategy, source: Source): T = + decodeFromInput(deserializer, IoStreamInput(source)) + +/** + * Deserializes CBOR from [source] to a value of type [T] using deserializer retrieved from the reified type parameter. + * + * Note that this functions expects that exactly one object would be present in the stream + * and throws an exception if there are any dangling bytes after an object. + * + * @throws [SerializationException] if the given CBOR input cannot be deserialized to the value of type [T]. + * @throws [kotlinx.io.IOException] If an I/O error occurs and source can't be read from. + */ +@ExperimentalSerializationApi +public inline fun Cbor.decodeFromSource(source: Source): T = + decodeFromSource(serializersModule.serializer(), source) diff --git a/formats/cbor-io/commonMain/src/kotlinx/serialization/cbor/io/internal/IoCborStreams.kt b/formats/cbor-io/commonMain/src/kotlinx/serialization/cbor/io/internal/IoCborStreams.kt new file mode 100644 index 0000000000..40d4151a2a --- /dev/null +++ b/formats/cbor-io/commonMain/src/kotlinx/serialization/cbor/io/internal/IoCborStreams.kt @@ -0,0 +1,38 @@ +/* + * Copyright 2017-2026 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package kotlinx.serialization.cbor.io.internal + +import kotlinx.io.* +import kotlinx.serialization.cbor.internal.* + +internal class IoStreamOutput(private val sink: Sink) : Output { + + override fun write(buffer: ByteArray, offset: Int, count: Int) { + sink.write(buffer, startIndex = offset, endIndex = offset + count) + } + + override fun write(byteValue: Byte) { + sink.writeByte(byteValue) + } +} + +internal class IoStreamInput(private val source: Source): Input { + override val availableBytes: Int + get() = source.peek().readByteArray().size + + override fun read(): Int = + try { + source.readByte().toInt() + } catch (_: EOFException) { + return -1 + } + + override fun read(b: ByteArray, offset: Int, length: Int): Int = + source.readAtMostTo(b, startIndex = offset, endIndex = offset + length) + + override fun skip(length: Int) { + source.skip(length.toLong()) + } +} diff --git a/formats/cbor-io/src/commonTest/kotlin/kotlinx/serialization/cbor/io/IoTests.kt b/formats/cbor-io/src/commonTest/kotlin/kotlinx/serialization/cbor/io/IoTests.kt new file mode 100644 index 0000000000..412a5e993d --- /dev/null +++ b/formats/cbor-io/src/commonTest/kotlin/kotlinx/serialization/cbor/io/IoTests.kt @@ -0,0 +1,48 @@ +/* + * Copyright 2017-2024 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package kotlinx.serialization.cbor.io + +import kotlinx.io.* +import kotlinx.serialization.* +import kotlinx.serialization.cbor.* +import kotlin.test.* + +class IoTests { + + @Serializable + data class Simple(val i: Int) + + @Test + fun testEncodingDefinite() { + val value = Simple(42) + val buffer = Buffer() + Cbor { useDefiniteLengthEncoding = true }.encodeToSink(value, buffer) + assertEquals(expected = "a16169182a", actual = buffer.readByteArray().toHexString()) + } + + @Test + fun testEncodingIndefinite() { + val buffer = Buffer() + Cbor { useDefiniteLengthEncoding = false }.encodeToSink(Simple(42), buffer) + assertEquals(expected = "bf6169182aff", actual = buffer.readByteArray().toHexString()) + } + + @Test + fun testDecoding() { + val buffer = Buffer() + buffer.write("a16169182a".hexToByteArray()) + val decoded = Cbor.decodeFromSource(buffer) + assertEquals(expected = Simple(42), actual = decoded) + + assertTrue(buffer.exhausted()) + } + + @Test + fun testDecodingFailsWithUnprocessedBytes() { + val buffer = Buffer() + buffer.write("bf6169182aff00".hexToByteArray()) + assertFailsWith { Cbor.decodeFromSource(buffer) } + } +} diff --git a/formats/cbor/api/kotlinx-serialization-cbor.api b/formats/cbor/api/kotlinx-serialization-cbor.api index 6b580add00..0f032204e4 100644 --- a/formats/cbor/api/kotlinx-serialization-cbor.api +++ b/formats/cbor/api/kotlinx-serialization-cbor.api @@ -9,7 +9,9 @@ public abstract class kotlinx/serialization/cbor/Cbor : kotlinx/serialization/Bi public static final field Default Lkotlinx/serialization/cbor/Cbor$Default; public synthetic fun (Lkotlinx/serialization/cbor/CborConfiguration;Lkotlinx/serialization/modules/SerializersModule;Lkotlin/jvm/internal/DefaultConstructorMarker;)V public fun decodeFromByteArray (Lkotlinx/serialization/DeserializationStrategy;[B)Ljava/lang/Object; + public final fun decodeFromInput (Lkotlinx/serialization/DeserializationStrategy;Lkotlinx/serialization/cbor/internal/Input;)Ljava/lang/Object; public fun encodeToByteArray (Lkotlinx/serialization/SerializationStrategy;Ljava/lang/Object;)[B + public final fun encodeToOutput (Lkotlinx/serialization/SerializationStrategy;Ljava/lang/Object;Lkotlinx/serialization/cbor/internal/Output;)V public final fun getConfiguration ()Lkotlinx/serialization/cbor/CborConfiguration; public fun getSerializersModule ()Lkotlinx/serialization/modules/SerializersModule; } @@ -149,3 +151,59 @@ public final synthetic class kotlinx/serialization/cbor/ValueTags$Impl : kotlinx public final synthetic fun tags ()[J } +public abstract class kotlinx/serialization/cbor/internal/CborWriter : kotlinx/serialization/encoding/AbstractEncoder, kotlinx/serialization/cbor/CborEncoder { + public synthetic fun (Lkotlinx/serialization/cbor/Cbor;Lkotlinx/serialization/cbor/internal/Output;Lkotlin/jvm/internal/DefaultConstructorMarker;)V + public fun encodeBoolean (Z)V + public fun encodeByte (B)V + public fun encodeChar (C)V + public fun encodeDouble (D)V + public fun encodeElement (Lkotlinx/serialization/descriptors/SerialDescriptor;I)Z + public fun encodeEnum (Lkotlinx/serialization/descriptors/SerialDescriptor;I)V + public fun encodeFloat (F)V + public fun encodeInt (I)V + public fun encodeLong (J)V + public fun encodeNull ()V + public fun encodeSerializableValue (Lkotlinx/serialization/SerializationStrategy;Ljava/lang/Object;)V + public fun encodeShort (S)V + public fun encodeString (Ljava/lang/String;)V + public fun getCbor ()Lkotlinx/serialization/cbor/Cbor; + protected abstract fun getDestination ()Lkotlinx/serialization/cbor/internal/Output; + protected final fun getEncodeByteArrayAsByteString ()Z + protected final fun getOutput ()Lkotlinx/serialization/cbor/internal/Output; + public fun getSerializersModule ()Lkotlinx/serialization/modules/SerializersModule; + protected abstract fun incrementChildren ()V + protected final fun isClass ()Z + protected final fun setClass (Z)V + protected final fun setEncodeByteArrayAsByteString (Z)V + public fun shouldEncodeElementDefault (Lkotlinx/serialization/descriptors/SerialDescriptor;I)Z +} + +public final class kotlinx/serialization/cbor/internal/DefiniteLengthCborWriter : kotlinx/serialization/cbor/internal/CborWriter { + public fun (Lkotlinx/serialization/cbor/Cbor;Lkotlinx/serialization/cbor/internal/Output;)V + public fun beginStructure (Lkotlinx/serialization/descriptors/SerialDescriptor;)Lkotlinx/serialization/encoding/CompositeEncoder; + public fun endStructure (Lkotlinx/serialization/descriptors/SerialDescriptor;)V +} + +public final class kotlinx/serialization/cbor/internal/IndefiniteLengthCborWriter : kotlinx/serialization/cbor/internal/CborWriter { + public fun (Lkotlinx/serialization/cbor/Cbor;Lkotlinx/serialization/cbor/internal/Output;)V + public fun beginStructure (Lkotlinx/serialization/descriptors/SerialDescriptor;)Lkotlinx/serialization/encoding/CompositeEncoder; + public fun endStructure (Lkotlinx/serialization/descriptors/SerialDescriptor;)V +} + +public abstract interface class kotlinx/serialization/cbor/internal/Input { + public abstract fun getAvailableBytes ()I + public abstract fun read ()I + public abstract fun read ([BII)I + public abstract fun skip (I)V +} + +public abstract interface class kotlinx/serialization/cbor/internal/Output { + public abstract fun write (B)V + public abstract fun write ([BII)V + public static synthetic fun write$default (Lkotlinx/serialization/cbor/internal/Output;[BIIILjava/lang/Object;)V +} + +public final class kotlinx/serialization/cbor/internal/Output$DefaultImpls { + public static synthetic fun write$default (Lkotlinx/serialization/cbor/internal/Output;[BIIILjava/lang/Object;)V +} + diff --git a/formats/cbor/api/kotlinx-serialization-cbor.klib.api b/formats/cbor/api/kotlinx-serialization-cbor.klib.api index 346416c8fa..0cb27432a1 100644 --- a/formats/cbor/api/kotlinx-serialization-cbor.klib.api +++ b/formats/cbor/api/kotlinx-serialization-cbor.klib.api @@ -42,6 +42,20 @@ open annotation class kotlinx.serialization.cbor/ValueTags : kotlin/Annotation { final fun (): kotlin/ULongArray // kotlinx.serialization.cbor/ValueTags.tags.|(){}[0] } +abstract interface kotlinx.serialization.cbor.internal/Input { // kotlinx.serialization.cbor.internal/Input|null[0] + abstract val availableBytes // kotlinx.serialization.cbor.internal/Input.availableBytes|{}availableBytes[0] + abstract fun (): kotlin/Int // kotlinx.serialization.cbor.internal/Input.availableBytes.|(){}[0] + + abstract fun read(): kotlin/Int // kotlinx.serialization.cbor.internal/Input.read|read(){}[0] + abstract fun read(kotlin/ByteArray, kotlin/Int, kotlin/Int): kotlin/Int // kotlinx.serialization.cbor.internal/Input.read|read(kotlin.ByteArray;kotlin.Int;kotlin.Int){}[0] + abstract fun skip(kotlin/Int) // kotlinx.serialization.cbor.internal/Input.skip|skip(kotlin.Int){}[0] +} + +abstract interface kotlinx.serialization.cbor.internal/Output { // kotlinx.serialization.cbor.internal/Output|null[0] + abstract fun write(kotlin/Byte) // kotlinx.serialization.cbor.internal/Output.write|write(kotlin.Byte){}[0] + abstract fun write(kotlin/ByteArray, kotlin/Int = ..., kotlin/Int = ...) // kotlinx.serialization.cbor.internal/Output.write|write(kotlin.ByteArray;kotlin.Int;kotlin.Int){}[0] +} + abstract interface kotlinx.serialization.cbor/CborDecoder : kotlinx.serialization.encoding/Decoder { // kotlinx.serialization.cbor/CborDecoder|null[0] abstract val cbor // kotlinx.serialization.cbor/CborDecoder.cbor|{}cbor[0] abstract fun (): kotlinx.serialization.cbor/Cbor // kotlinx.serialization.cbor/CborDecoder.cbor.|(){}[0] @@ -52,6 +66,20 @@ abstract interface kotlinx.serialization.cbor/CborEncoder : kotlinx.serializatio abstract fun (): kotlinx.serialization.cbor/Cbor // kotlinx.serialization.cbor/CborEncoder.cbor.|(){}[0] } +final class kotlinx.serialization.cbor.internal/DefiniteLengthCborWriter : kotlinx.serialization.cbor.internal/CborWriter { // kotlinx.serialization.cbor.internal/DefiniteLengthCborWriter|null[0] + constructor (kotlinx.serialization.cbor/Cbor, kotlinx.serialization.cbor.internal/Output) // kotlinx.serialization.cbor.internal/DefiniteLengthCborWriter.|(kotlinx.serialization.cbor.Cbor;kotlinx.serialization.cbor.internal.Output){}[0] + + final fun beginStructure(kotlinx.serialization.descriptors/SerialDescriptor): kotlinx.serialization.encoding/CompositeEncoder // kotlinx.serialization.cbor.internal/DefiniteLengthCborWriter.beginStructure|beginStructure(kotlinx.serialization.descriptors.SerialDescriptor){}[0] + final fun endStructure(kotlinx.serialization.descriptors/SerialDescriptor) // kotlinx.serialization.cbor.internal/DefiniteLengthCborWriter.endStructure|endStructure(kotlinx.serialization.descriptors.SerialDescriptor){}[0] +} + +final class kotlinx.serialization.cbor.internal/IndefiniteLengthCborWriter : kotlinx.serialization.cbor.internal/CborWriter { // kotlinx.serialization.cbor.internal/IndefiniteLengthCborWriter|null[0] + constructor (kotlinx.serialization.cbor/Cbor, kotlinx.serialization.cbor.internal/Output) // kotlinx.serialization.cbor.internal/IndefiniteLengthCborWriter.|(kotlinx.serialization.cbor.Cbor;kotlinx.serialization.cbor.internal.Output){}[0] + + final fun beginStructure(kotlinx.serialization.descriptors/SerialDescriptor): kotlinx.serialization.encoding/CompositeEncoder // kotlinx.serialization.cbor.internal/IndefiniteLengthCborWriter.beginStructure|beginStructure(kotlinx.serialization.descriptors.SerialDescriptor){}[0] + final fun endStructure(kotlinx.serialization.descriptors/SerialDescriptor) // kotlinx.serialization.cbor.internal/IndefiniteLengthCborWriter.endStructure|endStructure(kotlinx.serialization.descriptors.SerialDescriptor){}[0] +} + final class kotlinx.serialization.cbor/CborBuilder { // kotlinx.serialization.cbor/CborBuilder|null[0] final var alwaysUseByteString // kotlinx.serialization.cbor/CborBuilder.alwaysUseByteString|{}alwaysUseByteString[0] final fun (): kotlin/Boolean // kotlinx.serialization.cbor/CborBuilder.alwaysUseByteString.|(){}[0] @@ -118,12 +146,47 @@ final class kotlinx.serialization.cbor/CborConfiguration { // kotlinx.serializat final fun toString(): kotlin/String // kotlinx.serialization.cbor/CborConfiguration.toString|toString(){}[0] } +sealed class kotlinx.serialization.cbor.internal/CborWriter : kotlinx.serialization.cbor/CborEncoder, kotlinx.serialization.encoding/AbstractEncoder { // kotlinx.serialization.cbor.internal/CborWriter|null[0] + final val output // kotlinx.serialization.cbor.internal/CborWriter.output|{}output[0] + final fun (): kotlinx.serialization.cbor.internal/Output // kotlinx.serialization.cbor.internal/CborWriter.output.|(){}[0] + open val cbor // kotlinx.serialization.cbor.internal/CborWriter.cbor|{}cbor[0] + open fun (): kotlinx.serialization.cbor/Cbor // kotlinx.serialization.cbor.internal/CborWriter.cbor.|(){}[0] + open val serializersModule // kotlinx.serialization.cbor.internal/CborWriter.serializersModule|{}serializersModule[0] + open fun (): kotlinx.serialization.modules/SerializersModule // kotlinx.serialization.cbor.internal/CborWriter.serializersModule.|(){}[0] + + final var encodeByteArrayAsByteString // kotlinx.serialization.cbor.internal/CborWriter.encodeByteArrayAsByteString|{}encodeByteArrayAsByteString[0] + final fun (): kotlin/Boolean // kotlinx.serialization.cbor.internal/CborWriter.encodeByteArrayAsByteString.|(){}[0] + final fun (kotlin/Boolean) // kotlinx.serialization.cbor.internal/CborWriter.encodeByteArrayAsByteString.|(kotlin.Boolean){}[0] + final var isClass // kotlinx.serialization.cbor.internal/CborWriter.isClass|{}isClass[0] + final fun (): kotlin/Boolean // kotlinx.serialization.cbor.internal/CborWriter.isClass.|(){}[0] + final fun (kotlin/Boolean) // kotlinx.serialization.cbor.internal/CborWriter.isClass.|(kotlin.Boolean){}[0] + + abstract fun getDestination(): kotlinx.serialization.cbor.internal/Output // kotlinx.serialization.cbor.internal/CborWriter.getDestination|getDestination(){}[0] + abstract fun incrementChildren() // kotlinx.serialization.cbor.internal/CborWriter.incrementChildren|incrementChildren(){}[0] + open fun <#A1: kotlin/Any?> encodeSerializableValue(kotlinx.serialization/SerializationStrategy<#A1>, #A1) // kotlinx.serialization.cbor.internal/CborWriter.encodeSerializableValue|encodeSerializableValue(kotlinx.serialization.SerializationStrategy<0:0>;0:0){0§}[0] + open fun encodeBoolean(kotlin/Boolean) // kotlinx.serialization.cbor.internal/CborWriter.encodeBoolean|encodeBoolean(kotlin.Boolean){}[0] + open fun encodeByte(kotlin/Byte) // kotlinx.serialization.cbor.internal/CborWriter.encodeByte|encodeByte(kotlin.Byte){}[0] + open fun encodeChar(kotlin/Char) // kotlinx.serialization.cbor.internal/CborWriter.encodeChar|encodeChar(kotlin.Char){}[0] + open fun encodeDouble(kotlin/Double) // kotlinx.serialization.cbor.internal/CborWriter.encodeDouble|encodeDouble(kotlin.Double){}[0] + open fun encodeElement(kotlinx.serialization.descriptors/SerialDescriptor, kotlin/Int): kotlin/Boolean // kotlinx.serialization.cbor.internal/CborWriter.encodeElement|encodeElement(kotlinx.serialization.descriptors.SerialDescriptor;kotlin.Int){}[0] + open fun encodeEnum(kotlinx.serialization.descriptors/SerialDescriptor, kotlin/Int) // kotlinx.serialization.cbor.internal/CborWriter.encodeEnum|encodeEnum(kotlinx.serialization.descriptors.SerialDescriptor;kotlin.Int){}[0] + open fun encodeFloat(kotlin/Float) // kotlinx.serialization.cbor.internal/CborWriter.encodeFloat|encodeFloat(kotlin.Float){}[0] + open fun encodeInt(kotlin/Int) // kotlinx.serialization.cbor.internal/CborWriter.encodeInt|encodeInt(kotlin.Int){}[0] + open fun encodeLong(kotlin/Long) // kotlinx.serialization.cbor.internal/CborWriter.encodeLong|encodeLong(kotlin.Long){}[0] + open fun encodeNull() // kotlinx.serialization.cbor.internal/CborWriter.encodeNull|encodeNull(){}[0] + open fun encodeShort(kotlin/Short) // kotlinx.serialization.cbor.internal/CborWriter.encodeShort|encodeShort(kotlin.Short){}[0] + open fun encodeString(kotlin/String) // kotlinx.serialization.cbor.internal/CborWriter.encodeString|encodeString(kotlin.String){}[0] + open fun shouldEncodeElementDefault(kotlinx.serialization.descriptors/SerialDescriptor, kotlin/Int): kotlin/Boolean // kotlinx.serialization.cbor.internal/CborWriter.shouldEncodeElementDefault|shouldEncodeElementDefault(kotlinx.serialization.descriptors.SerialDescriptor;kotlin.Int){}[0] +} + sealed class kotlinx.serialization.cbor/Cbor : kotlinx.serialization/BinaryFormat { // kotlinx.serialization.cbor/Cbor|null[0] final val configuration // kotlinx.serialization.cbor/Cbor.configuration|{}configuration[0] final fun (): kotlinx.serialization.cbor/CborConfiguration // kotlinx.serialization.cbor/Cbor.configuration.|(){}[0] open val serializersModule // kotlinx.serialization.cbor/Cbor.serializersModule|{}serializersModule[0] open fun (): kotlinx.serialization.modules/SerializersModule // kotlinx.serialization.cbor/Cbor.serializersModule.|(){}[0] + final fun <#A1: kotlin/Any?> decodeFromInput(kotlinx.serialization/DeserializationStrategy<#A1>, kotlinx.serialization.cbor.internal/Input): #A1 // kotlinx.serialization.cbor/Cbor.decodeFromInput|decodeFromInput(kotlinx.serialization.DeserializationStrategy<0:0>;kotlinx.serialization.cbor.internal.Input){0§}[0] + final fun <#A1: kotlin/Any?> encodeToOutput(kotlinx.serialization/SerializationStrategy<#A1>, #A1, kotlinx.serialization.cbor.internal/Output) // kotlinx.serialization.cbor/Cbor.encodeToOutput|encodeToOutput(kotlinx.serialization.SerializationStrategy<0:0>;0:0;kotlinx.serialization.cbor.internal.Output){0§}[0] open fun <#A1: kotlin/Any?> decodeFromByteArray(kotlinx.serialization/DeserializationStrategy<#A1>, kotlin/ByteArray): #A1 // kotlinx.serialization.cbor/Cbor.decodeFromByteArray|decodeFromByteArray(kotlinx.serialization.DeserializationStrategy<0:0>;kotlin.ByteArray){0§}[0] open fun <#A1: kotlin/Any?> encodeToByteArray(kotlinx.serialization/SerializationStrategy<#A1>, #A1): kotlin/ByteArray // kotlinx.serialization.cbor/Cbor.encodeToByteArray|encodeToByteArray(kotlinx.serialization.SerializationStrategy<0:0>;0:0){0§}[0] diff --git a/formats/cbor/build.gradle.kts b/formats/cbor/build.gradle.kts index 879040fef2..96cd1d7352 100644 --- a/formats/cbor/build.gradle.kts +++ b/formats/cbor/build.gradle.kts @@ -16,6 +16,13 @@ plugins { kotlin { sourceSets { + configureEach { + languageSettings { + optIn("kotlinx.serialization.internal.CoreFriendModuleApi") + optIn("kotlinx.serialization.cbor.internal.CborFriendModuleApi") + } + } + commonMain { dependencies { api(project(":kotlinx-serialization-core")) diff --git a/formats/cbor/commonMain/src/kotlinx/serialization/cbor/Cbor.kt b/formats/cbor/commonMain/src/kotlinx/serialization/cbor/Cbor.kt index d922044847..6e94043951 100644 --- a/formats/cbor/commonMain/src/kotlinx/serialization/cbor/Cbor.kt +++ b/formats/cbor/commonMain/src/kotlinx/serialization/cbor/Cbor.kt @@ -70,26 +70,30 @@ public sealed class Cbor( override fun encodeToByteArray(serializer: SerializationStrategy, value: T): ByteArray { val output = ByteArrayOutput() - val dumper = if (configuration.useDefiniteLengthEncoding) DefiniteLengthCborWriter( - this, - output - ) else IndefiniteLengthCborWriter( - this, - output - ) - dumper.encodeSerializableValue(serializer, value) - + encodeToOutput(serializer, value, output) return output.toByteArray() + } + override fun decodeFromByteArray(deserializer: DeserializationStrategy, bytes: ByteArray): T = + decodeFromInput(deserializer, ByteArrayInput(bytes)) + + @CborFriendModuleApi + public fun encodeToOutput(serializer: SerializationStrategy, value: T, output: Output) { + val dumper = if (configuration.useDefiniteLengthEncoding) { + DefiniteLengthCborWriter(this, output) + } else { + IndefiniteLengthCborWriter(this, output) + } + dumper.encodeSerializableValue(serializer, value) } - override fun decodeFromByteArray(deserializer: DeserializationStrategy, bytes: ByteArray): T { - val stream = ByteArrayInput(bytes) - val reader = CborReader(this, CborParser(stream, configuration.verifyObjectTags)) + @CborFriendModuleApi + public fun decodeFromInput(deserializer: DeserializationStrategy, input: Input): T { + val reader = CborReader(this, CborParser(input, configuration.verifyObjectTags)) val result = reader.decodeSerializableValue(deserializer) - if (stream.availableBytes > 0) { + if (input.availableBytes > 0) { throw CborDecodingException( - "Input contains ${stream.availableBytes} unprocessed bytes left after decoding a value." + "Input contains ${input.availableBytes} unprocessed bytes left after decoding a value." ) } return result diff --git a/formats/cbor/commonMain/src/kotlinx/serialization/cbor/internal/CborFriendModuleApi.kt b/formats/cbor/commonMain/src/kotlinx/serialization/cbor/internal/CborFriendModuleApi.kt new file mode 100644 index 0000000000..805a52a11d --- /dev/null +++ b/formats/cbor/commonMain/src/kotlinx/serialization/cbor/internal/CborFriendModuleApi.kt @@ -0,0 +1,8 @@ +/* + * Copyright 2017-2026 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package kotlinx.serialization.cbor.internal + +@RequiresOptIn(level = RequiresOptIn.Level.ERROR) +internal annotation class CborFriendModuleApi diff --git a/formats/cbor/commonMain/src/kotlinx/serialization/cbor/internal/Decoder.kt b/formats/cbor/commonMain/src/kotlinx/serialization/cbor/internal/Decoder.kt index 8371f50f8b..1ac6cc09e6 100644 --- a/formats/cbor/commonMain/src/kotlinx/serialization/cbor/internal/Decoder.kt +++ b/formats/cbor/commonMain/src/kotlinx/serialization/cbor/internal/Decoder.kt @@ -159,7 +159,7 @@ internal open class CborReader(override val cbor: Cbor, protected val parser: Cb } } -internal class CborParser(private val input: ByteArrayInput, private val verifyObjectTags: Boolean) { +internal class CborParser(private val input: Input, private val verifyObjectTags: Boolean) { private var curByteOrEof: Int = -1 private fun peekCurByteOrFail(): Int { @@ -398,7 +398,7 @@ internal class CborParser(private val input: ByteArrayInput, private val verifyO return if (negative) -(unsignedValue + 1) else unsignedValue } - private fun ByteArrayInput.readExact(bytes: Int): Long { + private fun Input.readExact(bytes: Int): Long { val arr = readExactNBytes(bytes) var result = 0L for (i in 0 until bytes) { @@ -407,13 +407,13 @@ internal class CborParser(private val input: ByteArrayInput, private val verifyO return result } - private fun ByteArrayInput.ensureEnoughBytes(bytesCount: Int) { + private fun Input.ensureEnoughBytes(bytesCount: Int) { if (bytesCount > availableBytes) { throw CborDecodingException("Unexpected EOF, available $availableBytes bytes, requested: $bytesCount") } } - private fun ByteArrayInput.readExactNBytes(bytesCount: Int): ByteArray { + private fun Input.readExactNBytes(bytesCount: Int): ByteArray { ensureEnoughBytes(bytesCount) val array = ByteArray(bytesCount) val _ = read(array, 0, bytesCount) diff --git a/formats/cbor/commonMain/src/kotlinx/serialization/cbor/internal/Encoder.kt b/formats/cbor/commonMain/src/kotlinx/serialization/cbor/internal/Encoder.kt index e84fbd8cde..df9767b2a2 100644 --- a/formats/cbor/commonMain/src/kotlinx/serialization/cbor/internal/Encoder.kt +++ b/formats/cbor/commonMain/src/kotlinx/serialization/cbor/internal/Encoder.kt @@ -13,28 +13,18 @@ import kotlinx.serialization.encoding.* import kotlinx.serialization.modules.* import kotlin.experimental.* - -//value classes are only inlined on the JVM, so we use a typealias and extensions instead -private typealias Stack = MutableList - -private fun Stack(initial: CborWriter.Data): Stack = mutableListOf(initial) -private fun Stack.push(value: CborWriter.Data) = add(value) -private fun Stack.pop() = removeLast() -private fun Stack.peek() = last() - // Writes class as map [fieldName, fieldValue] // Split implementation to optimize base case -internal sealed class CborWriter( +@CborFriendModuleApi +public sealed class CborWriter( override val cbor: Cbor, - protected val output: ByteArrayOutput, + protected val output: Output, ) : AbstractEncoder(), CborEncoder { - protected var isClass = false + protected var isClass: Boolean = false - protected var encodeByteArrayAsByteString = false + protected var encodeByteArrayAsByteString: Boolean = false - class Data(val bytes: ByteArrayOutput, var elementCount: Int) - - protected abstract fun getDestination(): ByteArrayOutput + protected abstract fun getDestination(): Output override val serializersModule: SerializersModule get() = cbor.serializersModule @@ -147,7 +137,8 @@ internal sealed class CborWriter( // optimized indefinite length encoder -internal class IndefiniteLengthCborWriter(cbor: Cbor, output: ByteArrayOutput) : CborWriter( +@CborFriendModuleApi +public class IndefiniteLengthCborWriter(cbor: Cbor, output: Output) : CborWriter( cbor, output ) { @@ -171,7 +162,7 @@ internal class IndefiniteLengthCborWriter(cbor: Cbor, output: ByteArrayOutput) : output.end() } - override fun getDestination(): ByteArrayOutput = output + override fun getDestination(): Output = output override fun incrementChildren() {/*NOOP*/ @@ -180,25 +171,28 @@ internal class IndefiniteLengthCborWriter(cbor: Cbor, output: ByteArrayOutput) : } //optimized definite length encoder -internal class DefiniteLengthCborWriter(cbor: Cbor, output: ByteArrayOutput) : CborWriter(cbor, output) { +@CborFriendModuleApi +public class DefiniteLengthCborWriter(cbor: Cbor, output: Output) : CborWriter(cbor, output) { + + private class Data(val bytes: ByteArrayOutput, var elementCount: Int) - private val structureStack = Stack(Data(output, -1)) - override fun getDestination(): ByteArrayOutput = - structureStack.peek().bytes + private val structureStack = mutableListOf() + override fun getDestination(): Output = + structureStack.lastOrNull()?.bytes ?: output override fun incrementChildren() { - structureStack.peek().elementCount++ + structureStack.last().elementCount++ } override fun beginStructure(descriptor: SerialDescriptor): CompositeEncoder { val current = Data(ByteArrayOutput(), 0) - val _ = structureStack.push(current) + val _ = structureStack.add(current) return this } override fun endStructure(descriptor: SerialDescriptor) { - val completedCurrent = structureStack.pop() + val completedCurrent = structureStack.removeLast() val accumulator = getDestination() @@ -217,73 +211,73 @@ internal class DefiniteLengthCborWriter(cbor: Cbor, output: ByteArrayOutput) : C else -> accumulator.startMap((numChildren).toULong()) } } - accumulator.copyFrom(completedCurrent.bytes) + completedCurrent.bytes.copyInto(accumulator) } } -private fun ByteArrayOutput.startArray() = write(BEGIN_ARRAY) +private fun Output.startArray() = write(BEGIN_ARRAY.toByte()) -private fun ByteArrayOutput.startArray(size: ULong) { +private fun Output.startArray(size: ULong) { composePositiveInline(size, HEADER_ARRAY) } -private fun ByteArrayOutput.startMap() = write(BEGIN_MAP) +private fun Output.startMap() = write(BEGIN_MAP.toByte()) -private fun ByteArrayOutput.startMap(size: ULong) { +private fun Output.startMap(size: ULong) { composePositiveInline(size, HEADER_MAP) } -private fun ByteArrayOutput.encodeTag(tag: ULong) { +private fun Output.encodeTag(tag: ULong) { composePositiveInline(tag, HEADER_TAG) } -internal fun ByteArrayOutput.end() = write(BREAK) +internal fun Output.end() = write(BREAK.toByte()) -internal fun ByteArrayOutput.encodeNull() = write(NULL) +internal fun Output.encodeNull() = write(NULL.toByte()) -internal fun ByteArrayOutput.encodeEmptyMap() = write(EMPTY_MAP) +internal fun Output.encodeEmptyMap() = write(EMPTY_MAP.toByte()) -internal fun ByteArrayOutput.writeByte(byteValue: Int) = write(byteValue) +internal fun Output.writeByte(byteValue: Int) = write(byteValue.toByte()) -internal fun ByteArrayOutput.encodeBoolean(value: Boolean) = write(if (value) TRUE else FALSE) +internal fun Output.encodeBoolean(value: Boolean) = write(if (value) TRUE.toByte() else FALSE.toByte()) -internal fun ByteArrayOutput.encodeNumber(value: Long) = write(composeNumber(value)) +internal fun Output.encodeNumber(value: Long) = write(composeNumber(value)) -internal fun ByteArrayOutput.encodeByteString(data: ByteArray) { +internal fun Output.encodeByteString(data: ByteArray) { this.encodeByteArray(data, HEADER_BYTE_STRING) } -internal fun ByteArrayOutput.encodeString(value: String) { +internal fun Output.encodeString(value: String) { this.encodeByteArray(value.encodeToByteArray(), HEADER_STRING) } -internal fun ByteArrayOutput.encodeByteArray(data: ByteArray, type: Int) { +internal fun Output.encodeByteArray(data: ByteArray, type: Int) { composePositiveInline(data.size.toULong(), type) write(data) } -internal fun ByteArrayOutput.encodeFloat(value: Float) { - write(NEXT_FLOAT) +internal fun Output.encodeFloat(value: Float) { + write(NEXT_FLOAT.toByte()) val bits = value.toRawBits() for (i in 0..3) { - write((bits shr (24 - 8 * i)) and 0xFF) + write(((bits shr (24 - 8 * i)) and 0xFF).toByte()) } } -internal fun ByteArrayOutput.encodeDouble(value: Double) { - write(NEXT_DOUBLE) +internal fun Output.encodeDouble(value: Double) { + write(NEXT_DOUBLE.toByte()) val bits = value.toRawBits() for (i in 0..7) { - write(((bits shr (56 - 8 * i)) and 0xFF).toInt()) + write(((bits shr (56 - 8 * i)) and 0xFF).toByte()) } } -//don't know why, but if the negative branch is also optimized and everything operates directly on the ByteArrayOutput it gets slower +//don't know why, but if the negative branch is also optimized and everything operates directly on the Output it gets slower private fun composeNumber(value: Long): ByteArray = if (value >= 0) composePositive(value.toULong()) else composeNegative(value) -private fun ByteArrayOutput.composePositiveInline(value: ULong, mod: Int) = when (value) { +private fun Output.composePositiveInline(value: ULong, mod: Int) = when (value) { in 0u..23u -> writeByte(value.toInt() or mod) in 24u..UByte.MAX_VALUE.toUInt() -> { writeByte(24 or mod) @@ -305,7 +299,7 @@ private fun composePositive(value: ULong): ByteArray = when (value) { } -private fun ByteArrayOutput.encodeToInline(value: ULong, bytes: Int, tag: Int) { +private fun Output.encodeToInline(value: ULong, bytes: Int, tag: Int) { val limit = bytes * 8 - 8 writeByte(tag) for (i in 0 until bytes) { @@ -329,4 +323,3 @@ private fun composeNegative(value: Long): ByteArray { data[0] = data[0] or HEADER_NEGATIVE return data } - diff --git a/formats/cbor/commonMain/src/kotlinx/serialization/cbor/internal/Streams.kt b/formats/cbor/commonMain/src/kotlinx/serialization/cbor/internal/Streams.kt index fdbfca674a..cfcd8baa75 100644 --- a/formats/cbor/commonMain/src/kotlinx/serialization/cbor/internal/Streams.kt +++ b/formats/cbor/commonMain/src/kotlinx/serialization/cbor/internal/Streams.kt @@ -4,15 +4,30 @@ package kotlinx.serialization.cbor.internal -internal class ByteArrayInput(private var array: ByteArray) { +@CborFriendModuleApi +public interface Input { + public val availableBytes: Int + /** Returns a -1 if no bytes are available. Otherwise returns a value between 0 and 255 (inclusive). */ + public fun read(): Int + public fun read(b: ByteArray, offset: Int, length: Int): Int + public fun skip(length: Int) +} + +@CborFriendModuleApi +public interface Output { + public fun write(buffer: ByteArray, offset: Int = 0, count: Int = buffer.size) + public fun write(byteValue: Byte) +} + +internal class ByteArrayInput(private var array: ByteArray) : Input { private var position: Int = 0 - public val availableBytes: Int get() = array.size - position + override val availableBytes: Int get() = array.size - position - fun read(): Int { + override fun read(): Int { return if (position < array.size) array[position++].toInt() and 0xFF else -1 } - fun read(b: ByteArray, offset: Int, length: Int): Int { + override fun read(b: ByteArray, offset: Int, length: Int): Int { // avoid int overflow if (offset < 0 || offset > b.size || length < 0 || length > b.size - offset @@ -33,12 +48,12 @@ internal class ByteArrayInput(private var array: ByteArray) { return copied } - fun skip(length: Int) { + override fun skip(length: Int) { position += length } } -internal class ByteArrayOutput { +internal class ByteArrayOutput : Output { private var array: ByteArray = ByteArray(32) private var position: Int = 0 @@ -51,17 +66,17 @@ internal class ByteArrayOutput { array = newArray } - public fun toByteArray(): ByteArray { + fun toByteArray(): ByteArray { val newArray = ByteArray(position) array.copyInto(newArray, startIndex = 0, endIndex = this.position) return newArray } - fun copyFrom(src: ByteArrayOutput) { - write(src.array, count = src.position) + fun copyInto(other: Output) { + other.write(array, 0, position) } - fun write(buffer: ByteArray, offset: Int = 0, count: Int = buffer.size) { + override fun write(buffer: ByteArray, offset: Int, count: Int) { // avoid int overflow if (offset < 0 || offset > buffer.size || count < 0 || count > buffer.size - offset @@ -82,8 +97,8 @@ internal class ByteArrayOutput { this.position += count } - fun write(byteValue: Int) { + override fun write(byteValue: Byte) { ensureCapacity(1) - array[position++] = byteValue.toByte() + array[position++] = byteValue } } diff --git a/settings.gradle.kts b/settings.gradle.kts index 64d86bb48d..2a6c704481 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -65,6 +65,9 @@ project(":kotlinx-serialization-protobuf:proto-test-model").projectDir = file(". include(":kotlinx-serialization-cbor") project(":kotlinx-serialization-cbor").projectDir = file("./formats/cbor") +include(":kotlinx-serialization-cbor-io") +project(":kotlinx-serialization-cbor-io").projectDir = file("./formats/cbor-io") + include(":kotlinx-serialization-hocon") project(":kotlinx-serialization-hocon").projectDir = file("./formats/hocon")