-
Notifications
You must be signed in to change notification settings - Fork 672
Add CBOR kotlinx-io support #3149
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: dev
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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 | ||
| } | ||
|
|
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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: <org.jetbrains.kotlinx:kotlinx-serialization-cbor-io> | ||
| 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§<kotlin.Any?>}[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§<kotlin.Any?>}[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§<kotlin.Any?>}[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§<kotlin.Any?>}[0] |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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() | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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 <T> Cbor.encodeToSink(serializer: SerializationStrategy<T>, 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 <reified T> 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 <T> Cbor.decodeFromSource(deserializer: DeserializationStrategy<T>, 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 <reified T> Cbor.decodeFromSource(source: Source): T = | ||
| decodeFromSource(serializersModule.serializer(), source) |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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 { | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Using try-catch on the hot path is also a performance problem. |
||
| 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) | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Semantic is different: We have |
||
|
|
||
| override fun skip(length: Int) { | ||
| source.skip(length.toLong()) | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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 { | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Preferably, the whole test suite should be adapted to work with IO streams; see how it is done in
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Also, a pair of basic benchmarks (see CborBaseline.kt) would be nice. |
||
|
|
||
| @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<Simple>(buffer) | ||
| assertEquals(expected = Simple(42), actual = decoded) | ||
|
|
||
| assertTrue(buffer.exhausted()) | ||
| } | ||
|
|
||
| @Test | ||
| fun testDecodingFailsWithUnprocessedBytes() { | ||
| val buffer = Buffer() | ||
| buffer.write("bf6169182aff00".hexToByteArray()) | ||
| assertFailsWith<SerializationException> { Cbor.decodeFromSource<Simple>(buffer) } | ||
| } | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Using
peekand reading whole input to the memory is a huge potential performance problem.exhausted()should be used instead. Yes, it returns Boolean, so calls ofavailableBytesshould also be adapted.